[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug report\ndescription: Report a concrete problem in Touch Mapper\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nassignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## Read before submitting\n\n        Use this form to report concrete, reproducible bugs in Touch Mapper. This is not a support or discussion channel.\n\n        If you are unsure whether something is a bug, or want to explore an idea, use Discussions instead.\n\n  - type: checkboxes\n    attributes:\n      label: Scope confirmation\n      options:\n        - label: >\n            I am reporting an actual malfunction or incorrect behavior in Touch Mapper\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Questions / discussions\n    url: https://github.com/skarkkai/touch-mapper/discussions\n    about: Use Discussions for questions or exploratory ideas\n    "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature request\ndescription: Suggest a feature for Touch Mapper\ntitle: \"[Feature]: \"\nlabels: [\"feature-request\"]\nassignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## Read before submitting\n\n        Touch Mapper is intentionally limited in scope. It exists solely to produce tactile maps for visually impaired users.\n\n        The primary design constraint is tactile clarity.\n\n        This requires flat maps, very simple geometry, and minimal detail. Features that primarily benefit\n        visualization, simulation, or gaming use cases are out of scope and will not be implemented.\n\n\n  - type: checkboxes\n    attributes:\n      label: Scope confirmation\n      options:\n        - label: >\n            I understand that Touch Mapper is focused on tactile maps for visually impaired users only.\n          required: true\n"
  },
  {
    "path": ".gitignore",
    "content": "*~\n.*.swp\n.~*#\n__pycache__\n/blender*\n/runtime\n/ol\n.tmp/\n\n"
  },
  {
    "path": ".idea/inspectionProfiles/Project_Default.xml",
    "content": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project Default\" />\n    <inspection_tool class=\"JSCheckFunctionSignatures\" enabled=\"true\" level=\"WARNING\" enabled_by_default=\"true\">\n      <option name=\"myCheckGuessedTypes\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"JSValidateTypes\" enabled=\"true\" level=\"WARNING\" enabled_by_default=\"true\" />\n  </profile>\n</component>"
  },
  {
    "path": ".idea/jsLibraryMappings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"JavaScriptLibraryMappings\">\n    <file url=\"PROJECT\" libraries=\"{jquery-2.1.4.min}\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/libraries/OSM2World.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"OSM2World\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/OSM2World.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/annotations_java8.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"annotations-java8\">\n    <CLASSES>\n      <root url=\"jar://$APPLICATION_HOME_DIR$/redist/annotations-java8.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/com_google_code_findbugs_jsr305_3_0_2.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"com.google.code.findbugs:jsr305:3.0.2\" type=\"repository\">\n    <properties maven-id=\"com.google.code.findbugs:jsr305:3.0.2\" />\n    <CLASSES>\n      <root url=\"jar://$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/commons_math3_3_0.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"commons-math3-3.0\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/commons/commons-math3-3.0.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/commons_math3_3_01.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"commons-math3-3.01\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/commons/commons-math3-3.0.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/configuration.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"configuration\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/configuration/commons-configuration-1.6.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/configuration/commons-lang-2.6.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/configuration/commons-collections-3.2.1.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/configuration/commons-logging-1.1.1.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/configuration1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"configuration1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/configuration/commons-collections-3.2.1.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/configuration/commons-logging-1.1.1.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/configuration/commons-lang-2.6.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/configuration/commons-configuration-1.6.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/guava_20_0.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"guava-20.0\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/guava/guava-20.0.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/guava_20_01.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"guava-20.01\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/guava/guava-20.0.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/jackson.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"jackson\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jackson/jackson-annotations-2.8.0.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jackson/jackson-core-2.8.6.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jackson/jackson-databind-2.3.1.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/jackson1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"jackson1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jackson/jackson-annotations-2.8.0.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jackson/jackson-databind-2.3.1.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jackson/jackson-core-2.8.6.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/javailp_1_2a.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"javailp-1.2a\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/javailp/javailp-1.2a.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/javailp_1_2a1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"javailp-1.2a1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/javailp/javailp-1.2a.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/javaproj_1_0_6.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"javaproj-1.0.6\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/javaproj/javaproj-1.0.6.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/javaproj_1_0_61.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"javaproj-1.0.61\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/javaproj/javaproj-1.0.6.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/jewelcli_0_6.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"jewelcli-0.6\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jewelcli/jewelcli-0.6.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/jewelcli_0_61.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"jewelcli-0.61\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jewelcli/jewelcli-0.6.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/jogl.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"jogl\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/gluegen-rt-natives-linux-i586.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/jogl-all.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/jogl-all-natives-macosx-universal.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/jogl-all-natives-linux-amd64.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/jogl-all-natives-windows-amd64.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/gluegen-rt-natives-windows-i586.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/gluegen-rt.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/jogl-all-natives-windows-i586.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/gluegen-rt-natives-macosx-universal.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/gluegen-rt-natives-windows-amd64.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/gluegen-rt-natives-linux-amd64.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jogl/jogl-all-natives-linux-i586.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/jogl1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"jogl1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/jogl-all-natives-windows-amd64.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/gluegen-rt-natives-windows-amd64.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/jogl-all-natives-linux-i586.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/gluegen-rt-natives-macosx-universal.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/gluegen-rt-natives-windows-i586.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/jogl-all-natives-macosx-universal.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/jogl-all-natives-linux-amd64.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/gluegen-rt-natives-linux-amd64.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/gluegen-rt.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/jogl-all.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/jogl-all-natives-windows-i586.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jogl/gluegen-rt-natives-linux-i586.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/jquery_2_1_4_min.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"jquery-2.1.4.min\" type=\"javaScript\">\n    <properties>\n      <option name=\"frameworkName\" value=\"jQuery\" />\n      <option name=\"frameworkVersion\" value=\"\" />\n    </properties>\n    <CLASSES>\n      <root url=\"file://$PROJECT_DIR$/web/src/scripts/vendor-common/jquery-2.1.4.min.js\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/jts.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"jts\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jts/jts-1.11-src.zip!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/jts/jts-1.11.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/jts1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"jts1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jts/jts-1.11.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/jts/jts-1.11-src.zip!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/lpsolve55j.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"lpsolve55j\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/lp_solve/lpsolve55j.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/lpsolve55j1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"lpsolve55j1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/lp_solve/lpsolve55j.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/osmosis.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"osmosis\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/osmosis/xercesImpl-2.9.1.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/osmosis/osmpbf-1.1.1-754a33af.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/osmosis/osmosis-core-0.41.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/osmosis/protobuf-java-2.4.1.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/osmosis/osmosis-xml-0.41.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/osmosis/commons-compress-1.2.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/osmosis/osmosis-pbf-0.41.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/osmosis1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"osmosis1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/osmosis/osmosis-xml-0.41.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/osmosis/osmpbf-1.1.1-754a33af.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/osmosis/commons-compress-1.2.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/osmosis/osmosis-core-0.41.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/osmosis/osmosis-pbf-0.41.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/osmosis/xercesImpl-2.9.1.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/osmosis/protobuf-java-2.4.1.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/pngj_2_0_0.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"pngj-2.0.0\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/pngj/pngj-2.0.0.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/pngj_2_0_01.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"pngj-2.0.01\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/pngj/pngj-2.0.0.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/poly2tri_core_0_1_1_SNAPSHOT.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"poly2tri-core-0.1.1-SNAPSHOT\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/poly2tri/poly2tri-core-0.1.1-SNAPSHOT.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/poly2tri_core_0_1_1_SNAPSHOT1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"poly2tri-core-0.1.1-SNAPSHOT1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/poly2tri/poly2tri-core-0.1.1-SNAPSHOT.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/slf4j.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"slf4j\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/slf4j/slf4j-api-1.7.5.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/slf4j/slf4j-simple-1.7.5.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/slf4j1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"slf4j1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/slf4j/slf4j-api-1.7.5.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/slf4j/slf4j-simple-1.7.5.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/trove.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"trove\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/trove/trove-3.0.1-src.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/build/lib/trove/trove-111118193120.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/trove1.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"trove1\">\n    <CLASSES>\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/trove/trove-3.0.1-src.jar!/\" />\n      <root url=\"jar://$PROJECT_DIR$/OSM2World/lib/trove/trove-111118193120.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/misc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"JavaScriptSettings\">\n    <option name=\"languageLevel\" value=\"ES5\" />\n  </component>\n  <component name=\"ProjectRootManager\" version=\"2\" project-jdk-name=\"1.8\" project-jdk-type=\"JavaSDK\" />\n</project>"
  },
  {
    "path": ".idea/modules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n      <module fileurl=\"file://$PROJECT_DIR$/OSM2World/OSM2World.iml\" filepath=\"$PROJECT_DIR$/OSM2World/OSM2World.iml\" />\n      <module fileurl=\"file://$PROJECT_DIR$/.idea/touch-mapper.iml\" filepath=\"$PROJECT_DIR$/.idea/touch-mapper.iml\" />\n      <module fileurl=\"file://$PROJECT_DIR$/web/web.iml\" filepath=\"$PROJECT_DIR$/web/web.iml\" />\n    </modules>\n  </component>\n</project>"
  },
  {
    "path": ".idea/touch-mapper.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <excludeFolder url=\"file://$MODULE_DIR$/blender\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/blender-2.78b-linux-glibc219-x86_64\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/converter/OSM2World\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/converter/blender\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/converter/py-lib\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/runtime\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/t\" />\n    </content>\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": ".idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/workspace.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"AnalysisUIOptions\">\n    <option name=\"SCOPE_TYPE\" value=\"3\" />\n  </component>\n  <component name=\"ChangeListManager\">\n    <list default=\"true\" id=\"f5e21612-e46a-47ff-99b7-74b0fd763205\" name=\"Default\" comment=\"\">\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/OSM2World.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/annotations_java8.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/commons_math3_3_0.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/commons_math3_3_01.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/configuration.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/configuration1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/guava_20_0.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/guava_20_01.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/jackson.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/jackson1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/javailp_1_2a.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/javailp_1_2a1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/javaproj_1_0_6.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/javaproj_1_0_61.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/jewelcli_0_6.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/jewelcli_0_61.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/jogl.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/jogl1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/jts.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/jts1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/lpsolve55j.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/lpsolve55j1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/osmosis.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/osmosis1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/pngj_2_0_0.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/pngj_2_0_01.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/poly2tri_core_0_1_1_SNAPSHOT.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/poly2tri_core_0_1_1_SNAPSHOT1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/slf4j.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/slf4j1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/trove.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/libraries/trove1.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/.idea/vcs.xml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/OSM2World/OSM2World.iml\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package-info.java\" />\n      <change type=\"NEW\" beforePath=\"\" afterPath=\"$PROJECT_DIR$/web/web.iml\" />\n      <change type=\"MODIFICATION\" beforePath=\"$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java\" afterPath=\"$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java\" />\n      <change type=\"MODIFICATION\" beforePath=\"$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/osmosis/OSMFileDataSource.java\" afterPath=\"$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/osmosis/OSMFileDataSource.java\" />\n      <change type=\"MODIFICATION\" beforePath=\"$PROJECT_DIR$/OSM2World/src/org/osm2world/console/OSM2World.java\" afterPath=\"$PROJECT_DIR$/OSM2World/src/org/osm2world/console/OSM2World.java\" />\n      <change type=\"MODIFICATION\" beforePath=\"$PROJECT_DIR$/install/cloudformation.json\" afterPath=\"$PROJECT_DIR$/install/cloudformation.json\" />\n      <change type=\"MODIFICATION\" beforePath=\"$PROJECT_DIR$/web/src/scripts/index.js\" afterPath=\"$PROJECT_DIR$/web/src/scripts/index.js\" />\n      <change type=\"MODIFICATION\" beforePath=\"$PROJECT_DIR$/web/src/scripts/map.js\" afterPath=\"$PROJECT_DIR$/web/src/scripts/map.js\" />\n    </list>\n    <option name=\"EXCLUDED_CONVERTED_TO_IGNORED\" value=\"true\" />\n    <option name=\"TRACKING_ENABLED\" value=\"true\" />\n    <option name=\"SHOW_DIALOG\" value=\"false\" />\n    <option name=\"HIGHLIGHT_CONFLICTS\" value=\"true\" />\n    <option name=\"HIGHLIGHT_NON_ACTIVE_CHANGELIST\" value=\"false\" />\n    <option name=\"LAST_RESOLUTION\" value=\"IGNORE\" />\n  </component>\n  <component name=\"ExecutionTargetManager\" SELECTED_TARGET=\"default_target\" />\n  <component name=\"FileEditorManager\">\n    <leaf SIDE_TABS_SIZE_LIMIT_KEY=\"300\">\n      <file leaf-file-name=\"OSM2World.java\" pinned=\"false\" current-in-tab=\"true\">\n        <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/OSM2World.java\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"324\">\n              <caret line=\"129\" column=\"12\" lean-forward=\"true\" selection-start-line=\"129\" selection-start-column=\"12\" selection-end-line=\"129\" selection-end-column=\"12\" />\n              <folding />\n            </state>\n          </provider>\n        </entry>\n      </file>\n      <file leaf-file-name=\"CLIArgumentsGroup.java\" pinned=\"false\" current-in-tab=\"false\">\n        <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/CLIArgumentsGroup.java\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"1098\">\n              <caret line=\"71\" column=\"0\" lean-forward=\"false\" selection-start-line=\"71\" selection-start-column=\"0\" selection-end-line=\"71\" selection-end-column=\"0\" />\n              <folding />\n            </state>\n          </provider>\n        </entry>\n      </file>\n      <file leaf-file-name=\"ConversionFacade.java\" pinned=\"false\" current-in-tab=\"false\">\n        <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/core/ConversionFacade.java\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"576\">\n              <caret line=\"99\" column=\"0\" lean-forward=\"false\" selection-start-line=\"99\" selection-start-column=\"0\" selection-end-line=\"99\" selection-end-column=\"0\" />\n              <folding />\n            </state>\n          </provider>\n        </entry>\n      </file>\n      <file leaf-file-name=\"DataSource.java\" pinned=\"false\" current-in-tab=\"false\">\n        <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/DataSource.java\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"378\">\n              <caret line=\"21\" column=\"18\" lean-forward=\"false\" selection-start-line=\"21\" selection-start-column=\"18\" selection-end-line=\"21\" selection-end-column=\"18\" />\n              <folding />\n            </state>\n          </provider>\n        </entry>\n      </file>\n      <file leaf-file-name=\"OSMFileDataSource.java\" pinned=\"false\" current-in-tab=\"false\">\n        <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/osmosis/OSMFileDataSource.java\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"1512\">\n              <caret line=\"112\" column=\"24\" lean-forward=\"false\" selection-start-line=\"112\" selection-start-column=\"24\" selection-end-line=\"112\" selection-end-column=\"24\" />\n              <folding />\n            </state>\n          </provider>\n        </entry>\n      </file>\n      <file leaf-file-name=\"lambda-update.sh\" pinned=\"false\" current-in-tab=\"false\">\n        <entry file=\"file://$PROJECT_DIR$/install/lambda-update.sh\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"162\">\n              <caret line=\"9\" column=\"0\" lean-forward=\"false\" selection-start-line=\"9\" selection-start-column=\"0\" selection-end-line=\"9\" selection-end-column=\"0\" />\n              <folding />\n            </state>\n          </provider>\n        </entry>\n      </file>\n      <file leaf-file-name=\"MapBasedTagGroup.java\" pinned=\"false\" current-in-tab=\"false\">\n        <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"2304\">\n              <caret line=\"133\" column=\"17\" lean-forward=\"false\" selection-start-line=\"133\" selection-start-column=\"17\" selection-end-line=\"133\" selection-end-column=\"17\" />\n              <folding />\n            </state>\n          </provider>\n        </entry>\n      </file>\n      <file leaf-file-name=\"package.html\" pinned=\"false\" current-in-tab=\"false\">\n        <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package.html\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"486\">\n              <caret line=\"27\" column=\"14\" lean-forward=\"false\" selection-start-line=\"27\" selection-start-column=\"14\" selection-end-line=\"27\" selection-end-column=\"14\" />\n              <folding />\n            </state>\n          </provider>\n        </entry>\n      </file>\n      <file leaf-file-name=\"package-info.java\" pinned=\"false\" current-in-tab=\"false\">\n        <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package-info.java\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"0\">\n              <caret line=\"0\" column=\"0\" lean-forward=\"false\" selection-start-line=\"0\" selection-start-column=\"0\" selection-end-line=\"0\" selection-end-column=\"0\" />\n              <folding />\n            </state>\n          </provider>\n        </entry>\n      </file>\n    </leaf>\n  </component>\n  <component name=\"FindInProjectRecents\">\n    <findStrings>\n      <find>setdata</find>\n      <find>initsim</find>\n    </findStrings>\n  </component>\n  <component name=\"Git.Settings\">\n    <option name=\"RECENT_GIT_ROOT_PATH\" value=\"$PROJECT_DIR$\" />\n  </component>\n  <component name=\"GradleLocalSettings\">\n    <option name=\"externalProjectsViewState\">\n      <projects_view />\n    </option>\n  </component>\n  <component name=\"IdeDocumentHistory\">\n    <option name=\"CHANGED_PATHS\">\n      <list>\n        <option value=\"$PROJECT_DIR$/web/src/scripts/map.js\" />\n        <option value=\"$PROJECT_DIR$/web/src/scripts/index.js\" />\n        <option value=\"$PROJECT_DIR$/web/src/scripts/backbone-helpers.js\" />\n        <option value=\"$PROJECT_DIR$/OSM2World/lib/jewelcli/jewelcli-0.6.jar!/uk/co/flamingpenguin/jewel/cli/CliFactory.class\" />\n        <option value=\"$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package.html\" />\n        <option value=\"$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package.java\" />\n        <option value=\"$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package-info.java\" />\n        <option value=\"$PROJECT_DIR$/OSM2World/src/org/osm2world/console/OSM2World.java\" />\n        <option value=\"$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java\" />\n        <option value=\"/HTML Fragment (OSMFileDataSource.java:3446).html\" />\n        <option value=\"$APPLICATION_CONFIG_DIR$/scratches/scratch.html\" />\n        <option value=\"$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/osmosis/OSMFileDataSource.java\" />\n        <option value=\"$PROJECT_DIR$/install/lambda-update.sh\" />\n      </list>\n    </option>\n  </component>\n  <component name=\"JsBuildToolGruntFileManager\" detection-done=\"true\" sorting=\"DEFINITION_ORDER\" />\n  <component name=\"JsBuildToolPackageJson\" detection-done=\"true\" sorting=\"DEFINITION_ORDER\" />\n  <component name=\"JsFlowSettings\">\n    <service-enabled>true</service-enabled>\n    <exe-path />\n    <annotation-enable>false</annotation-enable>\n    <other-services-enabled>true</other-services-enabled>\n  </component>\n  <component name=\"JsGulpfileManager\">\n    <detection-done>true</detection-done>\n    <sorting>DEFINITION_ORDER</sorting>\n  </component>\n  <component name=\"ProjectFrameBounds\">\n    <option name=\"x\" value=\"64\" />\n    <option name=\"y\" value=\"-11\" />\n    <option name=\"width\" value=\"1857\" />\n    <option name=\"height\" value=\"1212\" />\n  </component>\n  <component name=\"ProjectInspectionProfilesVisibleTreeState\">\n    <entry key=\"Project Default\">\n      <profile-state>\n        <expanded-state>\n          <State>\n            <id />\n          </State>\n          <State>\n            <id>Android</id>\n          </State>\n          <State>\n            <id>Android &gt; Lint &gt; Correctness</id>\n          </State>\n          <State>\n            <id>Android &gt; Lint &gt; Performance</id>\n          </State>\n          <State>\n            <id>Android Lint for Kotlin</id>\n          </State>\n          <State>\n            <id>CoffeeScript</id>\n          </State>\n          <State>\n            <id>CorrectnessLintAndroid</id>\n          </State>\n          <State>\n            <id>GSPGrailsGroovy</id>\n          </State>\n          <State>\n            <id>General</id>\n          </State>\n          <State>\n            <id>GeneralCoffeeScript</id>\n          </State>\n          <State>\n            <id>GeneralJavaScript</id>\n          </State>\n          <State>\n            <id>GrailsGroovy</id>\n          </State>\n          <State>\n            <id>Groovy</id>\n          </State>\n          <State>\n            <id>HTML</id>\n          </State>\n          <State>\n            <id>JBoss Seam issues</id>\n          </State>\n          <State>\n            <id>Java</id>\n          </State>\n          <State>\n            <id>JavaScript</id>\n          </State>\n          <State>\n            <id>Kotlin</id>\n          </State>\n          <State>\n            <id>Language Injection</id>\n          </State>\n          <State>\n            <id>LintAndroid</id>\n          </State>\n          <State>\n            <id>Maven</id>\n          </State>\n          <State>\n            <id>Potentially confusing code constructsGroovy</id>\n          </State>\n          <State>\n            <id>Probable bugsJava</id>\n          </State>\n          <State>\n            <id>TypeScript</id>\n          </State>\n          <State>\n            <id>XPath</id>\n          </State>\n        </expanded-state>\n        <selected-state>\n          <State>\n            <id>JSMismatchedCollectionQueryUpdate</id>\n          </State>\n        </selected-state>\n      </profile-state>\n    </entry>\n  </component>\n  <component name=\"ProjectView\">\n    <navigator currentView=\"ProjectPane\" proportions=\"\" version=\"1\">\n      <flattenPackages />\n      <showMembers />\n      <showModules />\n      <showLibraryContents />\n      <hideEmptyPackages />\n      <abbreviatePackageNames />\n      <autoscrollToSource />\n      <autoscrollFromSource />\n      <sortByType />\n      <manualOrder />\n      <foldersAlwaysOnTop value=\"true\" />\n    </navigator>\n    <panes>\n      <pane id=\"Scope\" />\n      <pane id=\"PackagesPane\" />\n      <pane id=\"ProjectPane\">\n        <subPane>\n          <PATH>\n            <PATH_ELEMENT>\n              <option name=\"myItemId\" value=\"touch-mapper\" />\n              <option name=\"myItemType\" value=\"com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode\" />\n            </PATH_ELEMENT>\n            <PATH_ELEMENT>\n              <option name=\"myItemId\" value=\"touch-mapper\" />\n              <option name=\"myItemType\" value=\"com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode\" />\n            </PATH_ELEMENT>\n          </PATH>\n          <PATH>\n            <PATH_ELEMENT>\n              <option name=\"myItemId\" value=\"touch-mapper\" />\n              <option name=\"myItemType\" value=\"com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode\" />\n            </PATH_ELEMENT>\n            <PATH_ELEMENT>\n              <option name=\"myItemId\" value=\"touch-mapper\" />\n              <option name=\"myItemType\" value=\"com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode\" />\n            </PATH_ELEMENT>\n            <PATH_ELEMENT>\n              <option name=\"myItemId\" value=\"install\" />\n              <option name=\"myItemType\" value=\"com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode\" />\n            </PATH_ELEMENT>\n          </PATH>\n        </subPane>\n      </pane>\n      <pane id=\"Scratches\" />\n    </panes>\n  </component>\n  <component name=\"PropertiesComponent\">\n    <property name=\"last_opened_file_path\" value=\"$APPLICATION_HOME_DIR$/redist/annotations-java8.jar!/\" />\n    <property name=\"project.structure.last.edited\" value=\"Libraries\" />\n    <property name=\"project.structure.proportion\" value=\"0.15\" />\n    <property name=\"project.structure.side.proportion\" value=\"0.2\" />\n    <property name=\"settings.editor.selected.configurable\" value=\"preferences.pluginManager\" />\n    <property name=\"WebServerToolWindowFactoryState\" value=\"false\" />\n    <property name=\"aspect.path.notification.shown\" value=\"true\" />\n    <property name=\"run.code.analysis.last.selected.profile\" value=\"pProject Default\" />\n    <property name=\"Downloaded.Files.Path.Enabled\" value=\"false\" />\n    <property name=\"Repository.Attach.JavaDocs\" value=\"false\" />\n    <property name=\"Repository.Attach.Sources\" value=\"false\" />\n    <property name=\"JavaScriptPreferStrict\" value=\"true\" />\n    <property name=\"JavaScriptWeakerCompletionTypeGuess\" value=\"true\" />\n    <property name=\"SearchEverywhereHistoryKey\" value=\"bin/&#9;FILE&#9;file:///home/skarkkai/touch-mapper/OSM2World/bin&#10;variable&#9;ACTION&#9;IntroduceVariable&#10;infer n&#9;ACTION&#9;InferNullity\" />\n    <property name=\"com.intellij.ide.scratch.LRUPopupBuilder$2/Languages\" value=\"HTML\" />\n  </component>\n  <component name=\"RunDashboard\">\n    <option name=\"ruleStates\">\n      <list>\n        <RuleState>\n          <option name=\"name\" value=\"ConfigurationTypeDashboardGroupingRule\" />\n        </RuleState>\n        <RuleState>\n          <option name=\"name\" value=\"StatusDashboardGroupingRule\" />\n        </RuleState>\n      </list>\n    </option>\n  </component>\n  <component name=\"RunManager\">\n    <configuration default=\"true\" type=\"AndroidRunConfigurationType\" factoryName=\"Android App\">\n      <module name=\"\" />\n      <option name=\"DEPLOY\" value=\"true\" />\n      <option name=\"ARTIFACT_NAME\" value=\"\" />\n      <option name=\"PM_INSTALL_OPTIONS\" value=\"\" />\n      <option name=\"ACTIVITY_EXTRA_FLAGS\" value=\"\" />\n      <option name=\"MODE\" value=\"default_activity\" />\n      <option name=\"TARGET_SELECTION_MODE\" value=\"SHOW_DIALOG\" />\n      <option name=\"PREFERRED_AVD\" value=\"\" />\n      <option name=\"CLEAR_LOGCAT\" value=\"false\" />\n      <option name=\"SHOW_LOGCAT_AUTOMATICALLY\" value=\"false\" />\n      <option name=\"SKIP_NOOP_APK_INSTALLATIONS\" value=\"true\" />\n      <option name=\"FORCE_STOP_RUNNING_APP\" value=\"true\" />\n      <option name=\"DEBUGGER_TYPE\" value=\"Java\" />\n      <option name=\"USE_LAST_SELECTED_DEVICE\" value=\"false\" />\n      <option name=\"PREFERRED_AVD\" value=\"\" />\n      <Java />\n      <Profilers>\n        <option name=\"ENABLE_ADVANCED_PROFILING\" value=\"true\" />\n        <option name=\"GAPID_ENABLED\" value=\"false\" />\n        <option name=\"GAPID_DISABLE_PCS\" value=\"false\" />\n        <option name=\"SUPPORT_LIB_ENABLED\" value=\"true\" />\n        <option name=\"INSTRUMENTATION_ENABLED\" value=\"true\" />\n      </Profilers>\n      <option name=\"DEEP_LINK\" value=\"\" />\n      <option name=\"ACTIVITY_CLASS\" value=\"\" />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"AndroidTestRunConfigurationType\" factoryName=\"Android Tests\">\n      <module name=\"\" />\n      <option name=\"TESTING_TYPE\" value=\"0\" />\n      <option name=\"INSTRUMENTATION_RUNNER_CLASS\" value=\"\" />\n      <option name=\"METHOD_NAME\" value=\"\" />\n      <option name=\"CLASS_NAME\" value=\"\" />\n      <option name=\"PACKAGE_NAME\" value=\"\" />\n      <option name=\"EXTRA_OPTIONS\" value=\"\" />\n      <option name=\"TARGET_SELECTION_MODE\" value=\"SHOW_DIALOG\" />\n      <option name=\"PREFERRED_AVD\" value=\"\" />\n      <option name=\"CLEAR_LOGCAT\" value=\"false\" />\n      <option name=\"SHOW_LOGCAT_AUTOMATICALLY\" value=\"false\" />\n      <option name=\"SKIP_NOOP_APK_INSTALLATIONS\" value=\"true\" />\n      <option name=\"FORCE_STOP_RUNNING_APP\" value=\"true\" />\n      <option name=\"DEBUGGER_TYPE\" value=\"Java\" />\n      <option name=\"USE_LAST_SELECTED_DEVICE\" value=\"false\" />\n      <option name=\"PREFERRED_AVD\" value=\"\" />\n      <Java />\n      <Profilers>\n        <option name=\"ENABLE_ADVANCED_PROFILING\" value=\"true\" />\n        <option name=\"GAPID_ENABLED\" value=\"false\" />\n        <option name=\"GAPID_DISABLE_PCS\" value=\"false\" />\n        <option name=\"SUPPORT_LIB_ENABLED\" value=\"true\" />\n        <option name=\"INSTRUMENTATION_ENABLED\" value=\"true\" />\n      </Profilers>\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"Applet\" factoryName=\"Applet\">\n      <option name=\"HTML_USED\" value=\"false\" />\n      <option name=\"WIDTH\" value=\"400\" />\n      <option name=\"HEIGHT\" value=\"300\" />\n      <option name=\"POLICY_FILE\" value=\"$APPLICATION_HOME_DIR$/bin/appletviewer.policy\" />\n      <module />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"Application\" factoryName=\"Application\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <option name=\"MAIN_CLASS_NAME\" />\n      <option name=\"VM_PARAMETERS\" />\n      <option name=\"PROGRAM_PARAMETERS\" />\n      <option name=\"WORKING_DIRECTORY\" value=\"$PROJECT_DIR$\" />\n      <option name=\"ALTERNATIVE_JRE_PATH_ENABLED\" value=\"false\" />\n      <option name=\"ALTERNATIVE_JRE_PATH\" />\n      <option name=\"ENABLE_SWING_INSPECTOR\" value=\"false\" />\n      <option name=\"ENV_VARIABLES\" />\n      <option name=\"PASS_PARENT_ENVS\" value=\"true\" />\n      <module name=\"\" />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"ArquillianJUnit\" factoryName=\"\" nameIsGenerated=\"true\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <module name=\"\" />\n      <option name=\"arquillianRunConfiguration\">\n        <value>\n          <option name=\"containerStateName\" value=\"\" />\n        </value>\n      </option>\n      <option name=\"ALTERNATIVE_JRE_PATH_ENABLED\" value=\"false\" />\n      <option name=\"ALTERNATIVE_JRE_PATH\" />\n      <option name=\"PACKAGE_NAME\" />\n      <option name=\"MAIN_CLASS_NAME\" />\n      <option name=\"METHOD_NAME\" />\n      <option name=\"TEST_OBJECT\" value=\"class\" />\n      <option name=\"VM_PARAMETERS\" />\n      <option name=\"PARAMETERS\" />\n      <option name=\"WORKING_DIRECTORY\" />\n      <option name=\"ENV_VARIABLES\" />\n      <option name=\"PASS_PARENT_ENVS\" value=\"true\" />\n      <option name=\"TEST_SEARCH_SCOPE\">\n        <value defaultName=\"singleModule\" />\n      </option>\n      <envs />\n      <patterns />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"ArquillianTestNG\" factoryName=\"\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <module name=\"\" />\n      <option name=\"arquillianRunConfiguration\">\n        <value>\n          <option name=\"containerStateName\" value=\"\" />\n        </value>\n      </option>\n      <option name=\"ALTERNATIVE_JRE_PATH_ENABLED\" value=\"false\" />\n      <option name=\"ALTERNATIVE_JRE_PATH\" />\n      <option name=\"SUITE_NAME\" />\n      <option name=\"PACKAGE_NAME\" />\n      <option name=\"MAIN_CLASS_NAME\" />\n      <option name=\"METHOD_NAME\" />\n      <option name=\"GROUP_NAME\" />\n      <option name=\"TEST_OBJECT\" value=\"CLASS\" />\n      <option name=\"VM_PARAMETERS\" />\n      <option name=\"PARAMETERS\" />\n      <option name=\"WORKING_DIRECTORY\" />\n      <option name=\"OUTPUT_DIRECTORY\" />\n      <option name=\"ANNOTATION_TYPE\" />\n      <option name=\"ENV_VARIABLES\" />\n      <option name=\"PASS_PARENT_ENVS\" value=\"true\" />\n      <option name=\"TEST_SEARCH_SCOPE\">\n        <value defaultName=\"singleModule\" />\n      </option>\n      <option name=\"USE_DEFAULT_REPORTERS\" value=\"false\" />\n      <option name=\"PROPERTIES_FILE\" />\n      <envs />\n      <properties />\n      <listeners />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"Cold Fusion runner description\" factoryName=\"Cold Fusion\" custom_browser=\"\" web_path=\"\">\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"CucumberJavaRunConfigurationType\" factoryName=\"Cucumber java\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <option name=\"myFilePath\" />\n      <option name=\"GLUE\" />\n      <option name=\"myNameFilter\" />\n      <option name=\"myGeneratedName\" />\n      <option name=\"MAIN_CLASS_NAME\" />\n      <option name=\"VM_PARAMETERS\" />\n      <option name=\"PROGRAM_PARAMETERS\" />\n      <option name=\"WORKING_DIRECTORY\" />\n      <option name=\"ALTERNATIVE_JRE_PATH_ENABLED\" value=\"false\" />\n      <option name=\"ALTERNATIVE_JRE_PATH\" />\n      <option name=\"ENABLE_SWING_INSPECTOR\" value=\"false\" />\n      <option name=\"ENV_VARIABLES\" />\n      <option name=\"PASS_PARENT_ENVS\" value=\"true\" />\n      <module name=\"\" />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"FlashRunConfigurationType\" factoryName=\"Flash App\">\n      <option name=\"BCName\" value=\"\" />\n      <option name=\"IOSSimulatorSdkPath\" value=\"\" />\n      <option name=\"adlOptions\" value=\"\" />\n      <option name=\"airProgramParameters\" value=\"\" />\n      <option name=\"appDescriptorForEmulator\" value=\"Android\" />\n      <option name=\"debugTransport\" value=\"USB\" />\n      <option name=\"debuggerSdkRaw\" value=\"BC SDK\" />\n      <option name=\"emulator\" value=\"NexusOne\" />\n      <option name=\"emulatorAdlOptions\" value=\"\" />\n      <option name=\"fastPackaging\" value=\"true\" />\n      <option name=\"fullScreenHeight\" value=\"0\" />\n      <option name=\"fullScreenWidth\" value=\"0\" />\n      <option name=\"launchUrl\" value=\"false\" />\n      <option name=\"launcherParameters\">\n        <LauncherParameters>\n          <option name=\"browser\" value=\"a7bb68e0-33c0-4d6f-a81a-aac1fdb870c8\" />\n          <option name=\"launcherType\" value=\"OSDefault\" />\n          <option name=\"newPlayerInstance\" value=\"false\" />\n          <option name=\"playerPath\" value=\"/usr/bin/flashplayerdebugger\" />\n        </LauncherParameters>\n      </option>\n      <option name=\"mobileRunTarget\" value=\"Emulator\" />\n      <option name=\"moduleName\" value=\"\" />\n      <option name=\"overriddenMainClass\" value=\"\" />\n      <option name=\"overriddenOutputFileName\" value=\"\" />\n      <option name=\"overrideMainClass\" value=\"false\" />\n      <option name=\"runTrusted\" value=\"true\" />\n      <option name=\"screenDpi\" value=\"0\" />\n      <option name=\"screenHeight\" value=\"0\" />\n      <option name=\"screenWidth\" value=\"0\" />\n      <option name=\"url\" value=\"http://\" />\n      <option name=\"usbDebugPort\" value=\"7936\" />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"FlexUnitRunConfigurationType\" factoryName=\"FlexUnit\" appDescriptorForEmulator=\"Android\" class_name=\"\" emulatorAdlOptions=\"\" method_name=\"\" package_name=\"\" scope=\"Class\">\n      <option name=\"BCName\" value=\"\" />\n      <option name=\"launcherParameters\">\n        <LauncherParameters>\n          <option name=\"browser\" value=\"a7bb68e0-33c0-4d6f-a81a-aac1fdb870c8\" />\n          <option name=\"launcherType\" value=\"OSDefault\" />\n          <option name=\"newPlayerInstance\" value=\"false\" />\n          <option name=\"playerPath\" value=\"/usr/bin/flashplayerdebugger\" />\n        </LauncherParameters>\n      </option>\n      <option name=\"moduleName\" value=\"\" />\n      <option name=\"trusted\" value=\"true\" />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"GradleRunConfiguration\" factoryName=\"Gradle\">\n      <ExternalSystemSettings>\n        <option name=\"executionName\" />\n        <option name=\"externalProjectPath\" />\n        <option name=\"externalSystemIdString\" value=\"GRADLE\" />\n        <option name=\"scriptParameters\" />\n        <option name=\"taskDescriptions\">\n          <list />\n        </option>\n        <option name=\"taskNames\">\n          <list />\n        </option>\n        <option name=\"vmOptions\" />\n      </ExternalSystemSettings>\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"GrailsRunConfigurationType\" factoryName=\"Grails\">\n      <setting name=\"vmparams\" value=\"\" />\n      <setting name=\"cmdLine\" value=\"run-app\" />\n      <setting name=\"passParentEnv\" value=\"true\" />\n      <setting name=\"launchBrowser\" value=\"true\" />\n      <setting name=\"launchBrowserUrl\" value=\"\" />\n      <setting name=\"depsClasspath\" value=\"false\" />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"JUnit\" factoryName=\"JUnit\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <module name=\"\" />\n      <option name=\"ALTERNATIVE_JRE_PATH_ENABLED\" value=\"false\" />\n      <option name=\"ALTERNATIVE_JRE_PATH\" />\n      <option name=\"PACKAGE_NAME\" />\n      <option name=\"MAIN_CLASS_NAME\" />\n      <option name=\"METHOD_NAME\" />\n      <option name=\"TEST_OBJECT\" value=\"class\" />\n      <option name=\"VM_PARAMETERS\" value=\"-ea\" />\n      <option name=\"PARAMETERS\" />\n      <option name=\"WORKING_DIRECTORY\" value=\"$MODULE_DIR$\" />\n      <option name=\"ENV_VARIABLES\" />\n      <option name=\"PASS_PARENT_ENVS\" value=\"true\" />\n      <option name=\"TEST_SEARCH_SCOPE\">\n        <value defaultName=\"singleModule\" />\n      </option>\n      <envs />\n      <patterns />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"JarApplication\" factoryName=\"JAR Application\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"Java Scratch\" factoryName=\"Java Scratch\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <option name=\"SCRATCH_FILE_ID\" value=\"0\" />\n      <option name=\"MAIN_CLASS_NAME\" />\n      <option name=\"VM_PARAMETERS\" />\n      <option name=\"PROGRAM_PARAMETERS\" />\n      <option name=\"WORKING_DIRECTORY\" />\n      <option name=\"ALTERNATIVE_JRE_PATH_ENABLED\" value=\"false\" />\n      <option name=\"ALTERNATIVE_JRE_PATH\" />\n      <option name=\"ENABLE_SWING_INSPECTOR\" value=\"false\" />\n      <option name=\"ENV_VARIABLES\" />\n      <option name=\"PASS_PARENT_ENVS\" value=\"true\" />\n      <module name=\"\" />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"JavaScriptTestRunnerJest\" factoryName=\"Jest\">\n      <node-interpreter value=\"project\" />\n      <working-dir value=\"\" />\n      <envs />\n      <scope-kind value=\"ALL\" />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"JavaScriptTestRunnerProtractor\" factoryName=\"Protractor\">\n      <config-file value=\"\" />\n      <node-interpreter value=\"project\" />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"JavascriptDebugType\" factoryName=\"JavaScript Debug\">\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"JetRunConfigurationType\" factoryName=\"Kotlin\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <option name=\"MAIN_CLASS_NAME\" />\n      <option name=\"VM_PARAMETERS\" />\n      <option name=\"PROGRAM_PARAMETERS\" />\n      <option name=\"WORKING_DIRECTORY\" />\n      <option name=\"ALTERNATIVE_JRE_PATH_ENABLED\" value=\"false\" />\n      <option name=\"ALTERNATIVE_JRE_PATH\" />\n      <option name=\"PASS_PARENT_ENVS\" value=\"true\" />\n      <module name=\"touch-mapper\" />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"KotlinStandaloneScriptRunConfigurationType\" factoryName=\"Kotlin script\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <option name=\"filePath\" />\n      <option name=\"vmParameters\" />\n      <option name=\"alternativeJrePath\" />\n      <option name=\"programParameters\" />\n      <option name=\"passParentEnvs\" value=\"true\" />\n      <option name=\"workingDirectory\" />\n      <option name=\"isAlternativeJrePathEnabled\" value=\"false\" />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"NodeJSConfigurationType\" factoryName=\"Node.js\" path-to-node=\"project\" working-dir=\"\">\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"Remote\" factoryName=\"Remote\">\n      <option name=\"USE_SOCKET_TRANSPORT\" value=\"true\" />\n      <option name=\"SERVER_MODE\" value=\"false\" />\n      <option name=\"SHMEM_ADDRESS\" value=\"javadebug\" />\n      <option name=\"HOST\" value=\"localhost\" />\n      <option name=\"PORT\" value=\"5005\" />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"SpringBootApplicationConfigurationType\" factoryName=\"Spring Boot\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <module name=\"\" />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"TestNG\" factoryName=\"TestNG\">\n      <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n      <module name=\"\" />\n      <option name=\"ALTERNATIVE_JRE_PATH_ENABLED\" value=\"false\" />\n      <option name=\"ALTERNATIVE_JRE_PATH\" />\n      <option name=\"SUITE_NAME\" />\n      <option name=\"PACKAGE_NAME\" />\n      <option name=\"MAIN_CLASS_NAME\" />\n      <option name=\"METHOD_NAME\" />\n      <option name=\"GROUP_NAME\" />\n      <option name=\"TEST_OBJECT\" value=\"CLASS\" />\n      <option name=\"VM_PARAMETERS\" value=\"-ea\" />\n      <option name=\"PARAMETERS\" />\n      <option name=\"WORKING_DIRECTORY\" value=\"$MODULE_DIR$\" />\n      <option name=\"OUTPUT_DIRECTORY\" />\n      <option name=\"ANNOTATION_TYPE\" />\n      <option name=\"ENV_VARIABLES\" />\n      <option name=\"PASS_PARENT_ENVS\" value=\"true\" />\n      <option name=\"TEST_SEARCH_SCOPE\">\n        <value defaultName=\"singleModule\" />\n      </option>\n      <option name=\"USE_DEFAULT_REPORTERS\" value=\"false\" />\n      <option name=\"PROPERTIES_FILE\" />\n      <envs />\n      <properties />\n      <listeners />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"js.build_tools.gulp\" factoryName=\"Gulp.js\">\n      <node-interpreter>project</node-interpreter>\n      <node-options />\n      <gulpfile />\n      <tasks />\n      <arguments />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"js.build_tools.npm\" factoryName=\"npm\">\n      <command value=\"run\" />\n      <scripts />\n      <node-interpreter value=\"project\" />\n      <envs />\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"mocha-javascript-test-runner\" factoryName=\"Mocha\">\n      <node-interpreter>project</node-interpreter>\n      <node-options />\n      <working-directory />\n      <pass-parent-env>true</pass-parent-env>\n      <envs />\n      <ui />\n      <extra-mocha-options />\n      <test-kind>DIRECTORY</test-kind>\n      <test-directory />\n      <recursive>false</recursive>\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"osgi.bnd.run\" factoryName=\"Run Launcher\">\n      <method />\n    </configuration>\n    <configuration default=\"true\" type=\"osgi.bnd.run\" factoryName=\"Test Launcher (JUnit)\">\n      <method />\n    </configuration>\n    <configuration name=\"&lt;template&gt;\" type=\"#org.jetbrains.idea.devkit.run.PluginConfigurationType\" default=\"true\" selected=\"false\">\n      <option name=\"VM_PARAMETERS\" value=\"-Xmx512m -Xms256m -XX:MaxPermSize=250m -ea\" />\n    </configuration>\n  </component>\n  <component name=\"ShelveChangesManager\" show_recycled=\"false\">\n    <option name=\"remove_strategy\" value=\"false\" />\n  </component>\n  <component name=\"SvnConfiguration\">\n    <configuration />\n  </component>\n  <component name=\"TaskManager\">\n    <task active=\"true\" id=\"Default\" summary=\"Default task\">\n      <changelist id=\"f5e21612-e46a-47ff-99b7-74b0fd763205\" name=\"Default\" comment=\"\" />\n      <created>1495997840019</created>\n      <option name=\"number\" value=\"Default\" />\n      <option name=\"presentableId\" value=\"Default\" />\n      <updated>1495997840019</updated>\n      <workItem from=\"1495998265275\" duration=\"8300000\" />\n      <workItem from=\"1496084350965\" duration=\"121000\" />\n      <workItem from=\"1496084494570\" duration=\"11910000\" />\n      <workItem from=\"1496263578270\" duration=\"5016000\" />\n      <workItem from=\"1498900520174\" duration=\"96000\" />\n      <workItem from=\"1498900629532\" duration=\"20000\" />\n      <workItem from=\"1498900669145\" duration=\"211000\" />\n    </task>\n    <servers />\n  </component>\n  <component name=\"TimeTrackingManager\">\n    <option name=\"totallyTimeSpent\" value=\"25674000\" />\n  </component>\n  <component name=\"ToolWindowManager\">\n    <frame x=\"64\" y=\"-11\" width=\"1857\" height=\"1212\" extended-state=\"6\" />\n    <editor active=\"true\" />\n    <layout>\n      <window_info id=\"TODO\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"6\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Nl-Palette\" active=\"false\" anchor=\"left\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"2\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Palette&#9;\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"3\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Image Layers\" active=\"false\" anchor=\"left\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"2\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Capture Analysis\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"3\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Event Log\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"7\" side_tool=\"true\" content_ui=\"tabs\" />\n      <window_info id=\"Maven Projects\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"3\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Run\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"2\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Version Control\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.21985158\" sideWeight=\"0.5\" order=\"7\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Properties\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"3\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Terminal\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"7\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Capture Tool\" active=\"false\" anchor=\"left\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"2\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Designer\" active=\"false\" anchor=\"left\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"2\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Project\" active=\"false\" anchor=\"left\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"true\" show_stripe_button=\"true\" weight=\"0.20950802\" sideWeight=\"0.5\" order=\"0\" side_tool=\"false\" content_ui=\"combo\" />\n      <window_info id=\"Database\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"3\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Structure\" active=\"false\" anchor=\"left\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.25\" sideWeight=\"0.5\" order=\"1\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Ant Build\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.25\" sideWeight=\"0.5\" order=\"1\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Theme Preview\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"3\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Debug\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.4\" sideWeight=\"0.5\" order=\"3\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Favorites\" active=\"false\" anchor=\"left\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"2\" side_tool=\"true\" content_ui=\"tabs\" />\n      <window_info id=\"Cvs\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.25\" sideWeight=\"0.5\" order=\"4\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Message\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"0\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Commander\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.4\" sideWeight=\"0.5\" order=\"0\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Analyze Dataflow to\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.32931355\" sideWeight=\"0.5\" order=\"8\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Hierarchy\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"true\" show_stripe_button=\"true\" weight=\"0.24958494\" sideWeight=\"0.5\" order=\"2\" side_tool=\"false\" content_ui=\"combo\" />\n      <window_info id=\"Inspection Results\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.32931355\" sideWeight=\"0.5\" order=\"7\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Dynamic Properties\" active=\"false\" anchor=\"right\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.33\" sideWeight=\"0.5\" order=\"4\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Inspection\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.4\" sideWeight=\"0.5\" order=\"5\" side_tool=\"false\" content_ui=\"tabs\" />\n      <window_info id=\"Find\" active=\"false\" anchor=\"bottom\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"false\" show_stripe_button=\"true\" weight=\"0.28014842\" sideWeight=\"0.5\" order=\"1\" side_tool=\"false\" content_ui=\"tabs\" />\n    </layout>\n  </component>\n  <component name=\"TypeScriptGeneratedFilesManager\">\n    <option name=\"processedProjectFiles\" value=\"true\" />\n  </component>\n  <component name=\"Vcs.Log.Tabs.Properties\">\n    <option name=\"TAB_STATES\">\n      <map>\n        <entry key=\"MAIN\">\n          <value>\n            <State>\n              <option name=\"RECENTLY_FILTERED_USER_GROUPS\">\n                <collection />\n              </option>\n              <option name=\"RECENTLY_FILTERED_BRANCH_GROUPS\">\n                <collection />\n              </option>\n              <option name=\"COLUMN_ORDER\">\n                <list>\n                  <option value=\"0\" />\n                  <option value=\"1\" />\n                  <option value=\"2\" />\n                  <option value=\"3\" />\n                </list>\n              </option>\n            </State>\n          </value>\n        </entry>\n      </map>\n    </option>\n  </component>\n  <component name=\"VcsContentAnnotationSettings\">\n    <option name=\"myLimit\" value=\"2678400000\" />\n  </component>\n  <component name=\"XDebuggerManager\">\n    <breakpoint-manager>\n      <breakpoints-dialog>\n        <breakpoints-dialog />\n      </breakpoints-dialog>\n      <option name=\"time\" value=\"2\" />\n    </breakpoint-manager>\n    <watches-manager />\n  </component>\n  <component name=\"antWorkspaceConfiguration\">\n    <option name=\"IS_AUTOSCROLL_TO_SOURCE\" value=\"false\" />\n    <option name=\"FILTER_TARGETS\" value=\"false\" />\n  </component>\n  <component name=\"editorHistoryManager\">\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"2304\">\n          <caret line=\"133\" column=\"17\" lean-forward=\"false\" selection-start-line=\"133\" selection-start-column=\"17\" selection-end-line=\"133\" selection-end-column=\"17\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package.html\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"486\">\n          <caret line=\"27\" column=\"14\" lean-forward=\"false\" selection-start-line=\"27\" selection-start-column=\"14\" selection-end-line=\"27\" selection-end-column=\"14\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package-info.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"0\">\n          <caret line=\"0\" column=\"0\" lean-forward=\"false\" selection-start-line=\"0\" selection-start-column=\"0\" selection-end-line=\"0\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/install/lambda-update.sh\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"360\">\n          <caret line=\"20\" column=\"0\" lean-forward=\"true\" selection-start-line=\"20\" selection-start-column=\"0\" selection-end-line=\"20\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/OSM2World.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"756\">\n          <caret line=\"64\" column=\"17\" lean-forward=\"false\" selection-start-line=\"64\" selection-start-column=\"17\" selection-end-line=\"64\" selection-end-column=\"17\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/CLIArgumentsGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"936\">\n          <caret line=\"62\" column=\"38\" lean-forward=\"false\" selection-start-line=\"62\" selection-start-column=\"38\" selection-end-line=\"62\" selection-end-column=\"38\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/core/ConversionFacade.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"576\">\n          <caret line=\"99\" column=\"0\" lean-forward=\"false\" selection-start-line=\"99\" selection-start-column=\"0\" selection-end-line=\"99\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/DataSource.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"378\">\n          <caret line=\"21\" column=\"18\" lean-forward=\"false\" selection-start-line=\"21\" selection-start-column=\"18\" selection-end-line=\"21\" selection-end-column=\"18\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/osmosis/OSMFileDataSource.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"1512\">\n          <caret line=\"112\" column=\"24\" lean-forward=\"true\" selection-start-line=\"112\" selection-start-column=\"24\" selection-end-line=\"112\" selection-end-column=\"24\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"2304\">\n          <caret line=\"133\" column=\"17\" lean-forward=\"false\" selection-start-line=\"133\" selection-start-column=\"17\" selection-end-line=\"133\" selection-end-column=\"17\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package.html\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"486\">\n          <caret line=\"27\" column=\"14\" lean-forward=\"false\" selection-start-line=\"27\" selection-start-column=\"14\" selection-end-line=\"27\" selection-end-column=\"14\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package-info.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"0\">\n          <caret line=\"0\" column=\"0\" lean-forward=\"false\" selection-start-line=\"0\" selection-start-column=\"0\" selection-end-line=\"0\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/install/lambda-update.sh\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"216\">\n          <caret line=\"12\" column=\"10\" lean-forward=\"true\" selection-start-line=\"12\" selection-start-column=\"10\" selection-end-line=\"12\" selection-end-column=\"10\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/OSM2World.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"756\">\n          <caret line=\"64\" column=\"17\" lean-forward=\"false\" selection-start-line=\"64\" selection-start-column=\"17\" selection-end-line=\"64\" selection-end-column=\"17\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/CLIArgumentsGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"936\">\n          <caret line=\"62\" column=\"38\" lean-forward=\"false\" selection-start-line=\"62\" selection-start-column=\"38\" selection-end-line=\"62\" selection-end-column=\"38\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/core/ConversionFacade.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"558\">\n          <caret line=\"98\" column=\"11\" lean-forward=\"false\" selection-start-line=\"98\" selection-start-column=\"11\" selection-end-line=\"98\" selection-end-column=\"11\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/DataSource.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"162\">\n          <caret line=\"9\" column=\"22\" lean-forward=\"false\" selection-start-line=\"9\" selection-start-column=\"22\" selection-end-line=\"9\" selection-end-column=\"22\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"2304\">\n          <caret line=\"133\" column=\"17\" lean-forward=\"false\" selection-start-line=\"133\" selection-start-column=\"17\" selection-end-line=\"133\" selection-end-column=\"17\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package.html\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"486\">\n          <caret line=\"27\" column=\"14\" lean-forward=\"false\" selection-start-line=\"27\" selection-start-column=\"14\" selection-end-line=\"27\" selection-end-column=\"14\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package-info.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"0\">\n          <caret line=\"0\" column=\"0\" lean-forward=\"false\" selection-start-line=\"0\" selection-start-column=\"0\" selection-end-line=\"0\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/osmosis/OSMFileDataSource.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"1332\">\n          <caret line=\"102\" column=\"0\" lean-forward=\"false\" selection-start-line=\"102\" selection-start-column=\"0\" selection-end-line=\"102\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/OSM2World.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"756\">\n          <caret line=\"64\" column=\"17\" lean-forward=\"false\" selection-start-line=\"64\" selection-start-column=\"17\" selection-end-line=\"64\" selection-end-column=\"17\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/CLIArgumentsGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"936\">\n          <caret line=\"62\" column=\"38\" lean-forward=\"false\" selection-start-line=\"62\" selection-start-column=\"38\" selection-end-line=\"62\" selection-end-column=\"38\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/core/ConversionFacade.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"558\">\n          <caret line=\"98\" column=\"11\" lean-forward=\"false\" selection-start-line=\"98\" selection-start-column=\"11\" selection-end-line=\"98\" selection-end-column=\"11\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/DataSource.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"162\">\n          <caret line=\"9\" column=\"22\" lean-forward=\"false\" selection-start-line=\"9\" selection-start-column=\"22\" selection-end-line=\"9\" selection-end-column=\"22\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"2304\">\n          <caret line=\"133\" column=\"17\" lean-forward=\"false\" selection-start-line=\"133\" selection-start-column=\"17\" selection-end-line=\"133\" selection-end-column=\"17\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package.html\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"486\">\n          <caret line=\"27\" column=\"14\" lean-forward=\"false\" selection-start-line=\"27\" selection-start-column=\"14\" selection-end-line=\"27\" selection-end-column=\"14\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package-info.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"0\">\n          <caret line=\"0\" column=\"0\" lean-forward=\"false\" selection-start-line=\"0\" selection-start-column=\"0\" selection-end-line=\"0\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/osmosis/OSMFileDataSource.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"1332\">\n          <caret line=\"102\" column=\"29\" lean-forward=\"false\" selection-start-line=\"102\" selection-start-column=\"29\" selection-end-line=\"102\" selection-end-column=\"29\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"jar://$APPLICATION_HOME_DIR$/plugins/JavaScriptLanguage/lib/JavaScriptLanguage.jar!/com/intellij/lang/javascript/index/predefined/HTML5.js\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"51\">\n          <caret line=\"1331\" column=\"0\" lean-forward=\"true\" selection-start-line=\"1331\" selection-start-column=\"0\" selection-end-line=\"1331\" selection-end-column=\"0\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/web/pre-src/area.pre\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"54\">\n          <caret line=\"3\" column=\"9\" lean-forward=\"false\" selection-start-line=\"3\" selection-start-column=\"9\" selection-end-line=\"3\" selection-end-column=\"9\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/web/src/scripts/util.js\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"501\">\n          <caret line=\"127\" column=\"0\" lean-forward=\"true\" selection-start-line=\"127\" selection-start-column=\"0\" selection-end-line=\"127\" selection-end-column=\"0\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/web/src/scripts/multipart-mode.js\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"301\">\n          <caret line=\"35\" column=\"27\" lean-forward=\"false\" selection-start-line=\"35\" selection-start-column=\"27\" selection-end-line=\"35\" selection-end-column=\"27\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/web/src/scripts/vendor-common/jquery-2.1.4.min.js\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"231\">\n          <caret line=\"2\" column=\"23552\" lean-forward=\"false\" selection-start-line=\"2\" selection-start-column=\"23552\" selection-end-line=\"2\" selection-end-column=\"23552\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"jar://$PROJECT_DIR$/OSM2World/lib/jewelcli/jewelcli-0.6.jar!/uk/co/flamingpenguin/jewel/cli/CliFactory.class\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"144\">\n          <caret line=\"8\" column=\"25\" lean-forward=\"true\" selection-start-line=\"8\" selection-start-column=\"25\" selection-end-line=\"8\" selection-end-column=\"25\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/web/src/scripts/map.js\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"216\">\n          <caret line=\"12\" column=\"37\" lean-forward=\"false\" selection-start-line=\"12\" selection-start-column=\"37\" selection-end-line=\"12\" selection-end-column=\"37\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/web/src/scripts/backbone-helpers.js\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"54\">\n          <caret line=\"6\" column=\"0\" lean-forward=\"true\" selection-start-line=\"6\" selection-start-column=\"0\" selection-end-line=\"6\" selection-end-column=\"0\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/web/src/scripts/index.js\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"270\">\n          <caret line=\"15\" column=\"39\" lean-forward=\"false\" selection-start-line=\"15\" selection-start-column=\"39\" selection-end-line=\"15\" selection-end-column=\"39\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/CLIArgumentsUtil.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"285\">\n          <caret line=\"81\" column=\"36\" lean-forward=\"false\" selection-start-line=\"81\" selection-start-column=\"36\" selection-end-line=\"81\" selection-end-column=\"36\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/web/src/scripts/vendor-other/ol-3.15.1-custom.min.js\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"231\">\n          <caret line=\"156\" column=\"239\" lean-forward=\"false\" selection-start-line=\"156\" selection-start-column=\"239\" selection-end-line=\"156\" selection-end-column=\"239\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/EmptyTagGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"108\">\n          <caret line=\"7\" column=\"46\" lean-forward=\"true\" selection-start-line=\"7\" selection-start-column=\"38\" selection-end-line=\"7\" selection-end-column=\"53\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$APPLICATION_CONFIG_DIR$/scratches/scratch.html\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"18\">\n          <caret line=\"1\" column=\"8\" lean-forward=\"true\" selection-start-line=\"1\" selection-start-column=\"8\" selection-end-line=\"1\" selection-end-column=\"8\" />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package-info.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"0\">\n          <caret line=\"0\" column=\"0\" lean-forward=\"false\" selection-start-line=\"0\" selection-start-column=\"0\" selection-end-line=\"0\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/package.html\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"486\">\n          <caret line=\"27\" column=\"14\" lean-forward=\"false\" selection-start-line=\"27\" selection-start-column=\"14\" selection-end-line=\"27\" selection-end-column=\"14\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"2304\">\n          <caret line=\"133\" column=\"17\" lean-forward=\"false\" selection-start-line=\"133\" selection-start-column=\"17\" selection-end-line=\"133\" selection-end-column=\"17\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/DataSource.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"378\">\n          <caret line=\"21\" column=\"18\" lean-forward=\"false\" selection-start-line=\"21\" selection-start-column=\"18\" selection-end-line=\"21\" selection-end-column=\"18\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/osmosis/OSMFileDataSource.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"1512\">\n          <caret line=\"112\" column=\"24\" lean-forward=\"false\" selection-start-line=\"112\" selection-start-column=\"24\" selection-end-line=\"112\" selection-end-column=\"24\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/install/lambda-update.sh\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"162\">\n          <caret line=\"9\" column=\"0\" lean-forward=\"false\" selection-start-line=\"9\" selection-start-column=\"0\" selection-end-line=\"9\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/core/ConversionFacade.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"576\">\n          <caret line=\"99\" column=\"0\" lean-forward=\"false\" selection-start-line=\"99\" selection-start-column=\"0\" selection-end-line=\"99\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/CLIArgumentsGroup.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"1098\">\n          <caret line=\"71\" column=\"0\" lean-forward=\"false\" selection-start-line=\"71\" selection-start-column=\"0\" selection-end-line=\"71\" selection-end-column=\"0\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n    <entry file=\"file://$PROJECT_DIR$/OSM2World/src/org/osm2world/console/OSM2World.java\">\n      <provider selected=\"true\" editor-type-id=\"text-editor\">\n        <state relative-caret-position=\"324\">\n          <caret line=\"129\" column=\"12\" lean-forward=\"true\" selection-start-line=\"129\" selection-start-column=\"12\" selection-end-line=\"129\" selection-end-column=\"12\" />\n          <folding />\n        </state>\n      </provider>\n    </entry>\n  </component>\n  <component name=\"masterDetails\">\n    <states>\n      <state key=\"ArtifactsStructureConfigurable.UI\">\n        <settings>\n          <artifact-editor />\n          <splitter-proportions>\n            <option name=\"proportions\">\n              <list>\n                <option value=\"0.2\" />\n              </list>\n            </option>\n          </splitter-proportions>\n        </settings>\n      </state>\n      <state key=\"FacetStructureConfigurable.UI\">\n        <settings>\n          <last-edited>No facets are configured</last-edited>\n          <splitter-proportions>\n            <option name=\"proportions\">\n              <list>\n                <option value=\"0.2\" />\n              </list>\n            </option>\n          </splitter-proportions>\n        </settings>\n      </state>\n      <state key=\"GlobalLibrariesConfigurable.UI\">\n        <settings>\n          <last-edited>jquery-2.1.4.min</last-edited>\n          <splitter-proportions>\n            <option name=\"proportions\">\n              <list>\n                <option value=\"0.2\" />\n              </list>\n            </option>\n          </splitter-proportions>\n        </settings>\n      </state>\n      <state key=\"JdkListConfigurable.UI\">\n        <settings>\n          <last-edited>1.8</last-edited>\n          <splitter-proportions>\n            <option name=\"proportions\">\n              <list>\n                <option value=\"0.2\" />\n              </list>\n            </option>\n          </splitter-proportions>\n        </settings>\n      </state>\n      <state key=\"ModuleStructureConfigurable.UI\">\n        <settings>\n          <last-edited>web</last-edited>\n          <splitter-proportions>\n            <option name=\"proportions\">\n              <list>\n                <option value=\"0.2\" />\n                <option value=\"0.6\" />\n              </list>\n            </option>\n          </splitter-proportions>\n        </settings>\n      </state>\n      <state key=\"ProjectJDKs.UI\">\n        <settings>\n          <last-edited>1.8</last-edited>\n          <splitter-proportions>\n            <option name=\"proportions\">\n              <list>\n                <option value=\"0.2\" />\n              </list>\n            </option>\n          </splitter-proportions>\n        </settings>\n      </state>\n      <state key=\"ProjectLibrariesConfigurable.UI\">\n        <settings>\n          <last-edited>com.google.code.findbugs:jsr305:3.0.2</last-edited>\n          <splitter-proportions>\n            <option name=\"proportions\">\n              <list>\n                <option value=\"0.2\" />\n              </list>\n            </option>\n          </splitter-proportions>\n        </settings>\n      </state>\n    </states>\n  </component>\n</project>"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"makefile.configureOnOpen\": false,\n    \"java.compile.nullAnalysis.mode\": \"automatic\",\n    \"eslint.useFlatConfig\": true,\n    \"eslint.workingDirectories\": [\n        { \"directory\": \"web\", \"changeProcessCWD\": true }\n    ]\n}"
  },
  {
    "path": "AGENTS.md",
    "content": "# Touch Mapper Agent Guide\n\nThis file is the operational contract for coding agents and contributors.\n\nTouch Mapper generates tactile maps for blind and visually impaired users. Every change must preserve tactile clarity, semantic encoding, and pipeline integrity.\n\nIf unsure, stop and consult files in `doc/`.\n\n## Scope and authority\n\n`AGENTS.md` is authoritative for non-negotiable policy and guardrails.\n`doc/*.md` is authoritative for procedures, commands, and implementation details.\nIf guidance conflicts, follow `doc/*.md` for operational behavior.\n\n---\n\n# Critical invariants (never violate)\n\nThese are architectural truths of Touch Mapper.\n\n## Accessibility and tactile semantics\n\nTouch Mapper is a symbolic tactile encoding system, not a realistic renderer.\n\nPriority order:\n\n1. Tactile clarity\n2. Semantic contrast\n3. Printability\n4. Performance\n5. Geometric correctness\n\nNever reverse this order.\n\nVertical elevation encodes meaning and must remain tactually distinguishable.\nRoad width is part of tactile encoding and must not be arbitrarily changed.\nGround elevation must remain ignored.\nNon-manifold geometry and overlapping solids are acceptable if prints are tactually correct.\n\nAuthoritative tier constants and mappings: `doc/way-area-extrusion-tiers.md`.\n\n---\n\n## Pipeline stage integrity (strict)\n\nThe converter pipeline is:\n\nOSM\n→ OSM2World\n→ clip-2d\n→ Blender tactile extrusion\n→ metadata enrichment\n→ render-ready model\n→ map-content.json\n→ S3 upload\n→ UI rendering\n\nRules:\n\n- Always merge bugs upstream (as early in the pipeline as possible), instead of being defensive downstream.\n- Do not merge pipeline stages.\n- Do not bypass stages.\n- Do not change stage responsibilities without updating `doc/converter-pipeline-stages.md`.\n\nMetadata lifecycle must remain consistent:\n\n`map-meta-raw.json` → `map-meta.augmented.json` → `map-meta.json` → `map-content.json`\n\nAuthoritative stage definitions: `doc/converter-pipeline-stages.md`.\n\n---\n\n## map-content.json is a UI contract\n\n`map-content.json` defines what users perceive in textual map description.\n\nDefault sections include:\n\n- roads\n- paths\n- railways\n- waterways\n- buildings\n- otherLinear\n\nAgents modifying `map-content.json` must:\n\n- Update schema docs.\n- Update verification tests.\n- Validate UI description output.\n\nBackward compatibility is not required. Maps are mostly ephemeral.\n\nAuthoritative schema and verification workflows:\n\n- `doc/map-description-model-schema.md`\n- `doc/map-content-verification.md`\n- `doc/map-description-introspection.md`\n\n---\n\n## Map content authority\n\nMap content is controlled only by UI modes:\n\n1. Normal\n2. No buildings\n3. Only big roads (includes water areas and railways)\n\n---\n\n# Development guardrails\n\nAlways:\n\n- Scope changes tightly.\n- Preserve tactile meaning.\n- Update docs if behavior changes.\n\nNever:\n\n- Compromise tactile clarity.\n- Work around map content problems downstream because earlier pipeline stages have created poor output.\n- Introduce hidden pipeline coupling.\n- Assume geometric correctness is more important than tactile clarity.\n- Use Python newer than 3.5 where Blender/runtime constraints apply.\n- Present multi-core performance as production performance.\n\nPerformance baseline constraints:\n\n- Production assumptions are single core, 1 GB RAM, EC2 T-class instance.\n- Benchmark with `taskset -c 0` when making performance claims.\n\nRuntime architecture constraints:\n\nBrowser → SQS message → EC2 converter → S3 output → browser fetch\n\nExecution time: 1-300 seconds.\nMemory budget: 1 GB RAM maximum.\nOSM data reading over Overpass API is dominant cost.\n\nTemporary file policy:\n\n- Use `.tmp/` and `bin/tmpctl`.\n- Do not use `/tmp/` for project temp files.\n\nWeb i18n guardrail:\n\n- There is no full runtime locale dictionary.\n- JS must receive text through templates or `data-*` attributes.\n- Locale files live in `web/locales/<lang>/tm.json`.\n\nAuthoritative coding conventions: `doc/development-conventions.md`.\n\n---\n\n## Docs index\n\n- `doc/development-conventions.md`: coding conventions, web/i18n details, Python guidelines.\n- `doc/development-setup.md`: authoritative local setup and developer workflows.\n- `doc/map-content-verification.md`: authoritative verification commands and locale/visual checks.\n- `doc/map-description-introspection.md`: canonical map-description inspection CLI flow.\n- `doc/converter-pipeline-stages.md`: authoritative OSM2World outputs, pipeline stage names, and metadata lifecycle.\n- `doc/map-description-model-schema.md`: map-description JSON schema.\n- `doc/deployed-map-inspection.md`: inspect deployed map artifacts via map ID.\n- `doc/ui-visual-baseline.md`: UI visual/style baseline.\n- `doc/way-area-extrusion-tiers.md`: authoritative tactile extrusion tiers.\n- `doc/creating-new-server.doc`: legacy server provisioning notes.\n"
  },
  {
    "path": "COPYING.txt",
    "content": "Touch Mapper is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License v3.0.\n\nFor-media content such as doc/image-for-media-licenced-cc-by.jpg can be reused under the Creative Commons Attribution 2.0 Generic (CC BY 2.0) license. \n\nOSM2World and Blender use separate licenses.\n\n"
  },
  {
    "path": "Makefile",
    "content": "all:\n\t@echo \"No default target exists\"\n\nFORCE: ;\n\nosm2world:\n\tcd OSM2World && ant clean jar\n\ntest: FORCE\n\ttest/run-osm2world-regression.sh\n\ndev-aws-install:\n\tinstall/lambda-update.sh dev\n\tinstall/cloudformation-update.sh dev\n\ntest-aws-install:\n\tinstall/lambda-update.sh test\n\tinstall/cloudformation-update.sh test\n\nprod-aws-install:\n\tinstall/lambda-update.sh prod\n\t@echo 'run: install/cloudformation-update.sh prod'\n\ndev-web-s3-install:\n\tinstall/web-s3.sh dev\n\ntest-web-s3-install:\n\tinstall/web-s3.sh test\n\nprod-web-s3-install:\n\t@echo 'run: install/web-s3.sh prod'\n\npackage:\n\tinstall/package.sh\n\ntest-install-ec2: package\n\t# First run: eval \"$(ssh-agent -s)\"; ssh-add .../ssh-key\n\t# \"tm-ec2\" needs to be defined as a Host in ~/.ssh/config\n\trsync -a --delete --delay-updates -e ssh install/dist/ tm-ec2:touch-mapper/test/dist/\n\ntest-restart: package\n\tssh tm-ec2 touch-mapper/test/dist/ec2-restart-pollers.sh\n\nprod-install-ec2: package\n\tssh tm-ec2 rsync -a --delete touch-mapper/test/dist touch-mapper/prod/\n"
  },
  {
    "path": "OSM2World/.classpath",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<classpath>\n\t<classpathentry kind=\"src\" path=\"src\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/commons/commons-math3-3.0.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/configuration/commons-collections-3.2.1.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/configuration/commons-configuration-1.6.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/configuration/commons-lang-2.6.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/configuration/commons-logging-1.1.1.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/javailp/javailp-1.2a.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/javaproj/javaproj-1.0.6.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jewelcli/jewelcli-0.6.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-linux-amd64.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-linux-i586.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-macosx-universal.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-windows-amd64.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-windows-i586.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-linux-amd64.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-linux-i586.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-macosx-universal.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-windows-amd64.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-windows-i586.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/jts/jts-1.11.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/lp_solve/lpsolve55j.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/commons-compress-1.2.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/osmosis-core-0.41.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/osmosis-pbf-0.41.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/osmosis-xml-0.41.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/osmpbf-1.1.1-754a33af.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/protobuf-java-2.4.1.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/xercesImpl-2.9.1.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/pngj/pngj-2.0.0.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/poly2tri/poly2tri-core-0.1.1-SNAPSHOT.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/slf4j/slf4j-api-1.7.5.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/slf4j/slf4j-simple-1.7.5.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/trove/trove-111118193120.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/lib/trove/trove-3.0.1-src.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"build/OSM2World.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/commons/commons-math3-3.0.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/configuration/commons-collections-3.2.1.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/configuration/commons-configuration-1.6.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/configuration/commons-lang-2.6.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/configuration/commons-logging-1.1.1.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/javailp/javailp-1.2a.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/javaproj/javaproj-1.0.6.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jewelcli/jewelcli-0.6.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-linux-amd64.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-linux-i586.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-macosx-universal.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-windows-amd64.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-windows-i586.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-linux-amd64.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-linux-i586.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-macosx-universal.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-windows-amd64.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-windows-i586.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jts/jts-1.11.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/lp_solve/lpsolve55j.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/osmosis/commons-compress-1.2.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/osmosis/osmosis-core-0.41.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/osmosis/osmosis-pbf-0.41.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/osmosis/osmosis-xml-0.41.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/osmosis/osmpbf-1.1.1-754a33af.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/osmosis/protobuf-java-2.4.1.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/osmosis/xercesImpl-2.9.1.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/pngj/pngj-2.0.0.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/poly2tri/poly2tri-core-0.1.1-SNAPSHOT.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/slf4j/slf4j-api-1.7.5.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/slf4j/slf4j-simple-1.7.5.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/trove/trove-111118193120.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/trove/trove-3.0.1-src.jar\"/>\n\t<classpathentry kind=\"con\" path=\"org.eclipse.jdt.launching.JRE_CONTAINER\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jackson/jackson-databind-2.3.1.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/jackson/jackson-core-2.8.6.jar\"/>\n\t<classpathentry kind=\"lib\" path=\"lib/guava/guava-20.0.jar\"/>\n\t<classpathentry kind=\"output\" path=\"eclipse-bin\"/>\n</classpath>\n"
  },
  {
    "path": "OSM2World/.gitignore",
    "content": "/bin\n/build\n\n/eclipse-bin/\n"
  },
  {
    "path": "OSM2World/.project",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<projectDescription>\n\t<name>Touch Mapper OSM2World</name>\n\t<comment></comment>\n\t<projects>\n\t</projects>\n\t<buildSpec>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.jdt.core.javabuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t</buildSpec>\n\t<natures>\n\t\t<nature>org.eclipse.jdt.core.javanature</nature>\n\t</natures>\n\t<filteredResources>\n\t\t<filter>\n\t\t\t<id>1477832440066</id>\n\t\t\t<name></name>\n\t\t\t<type>22</type>\n\t\t\t<matcher>\n\t\t\t\t<id>org.eclipse.ui.ide.multiFilter</id>\n\t\t\t\t<arguments>1.0-name-matches-false-false-*.class</arguments>\n\t\t\t</matcher>\n\t\t</filter>\n\t\t<filter>\n\t\t\t<id>1769337938854</id>\n\t\t\t<name></name>\n\t\t\t<type>30</type>\n\t\t\t<matcher>\n\t\t\t\t<id>org.eclipse.core.resources.regexFilterMatcher</id>\n\t\t\t\t<arguments>node_modules|\\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>\n\t\t\t</matcher>\n\t\t</filter>\n\t</filteredResources>\n</projectDescription>\n"
  },
  {
    "path": "OSM2World/.vscode/settings.json",
    "content": "{\n    \"java.compile.nullAnalysis.mode\": \"automatic\"\n}"
  },
  {
    "path": "OSM2World/COPYING.txt",
    "content": "Copyright © 2010–2011  OSM2World contributors\r\nThe libraries in the \"lib\" directory are not part of OSM2World, separate licenses apply for these files.\r\n  \r\nOSM2World is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\r\n\r\nOSM2World is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of ERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.\r\n\r\nYou should have received a copy of the GNU Lesser General Public License along with OSM2World. If not, see <http://www.gnu.org/licenses/>.\r\n"
  },
  {
    "path": "OSM2World/OSM2World.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/test\" isTestSource=\"true\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/build-adds\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/eclipse-bin\" />\n    </content>\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n    <orderEntry type=\"library\" name=\"configuration\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"commons-math3-3.0\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"commons-math3-3.01\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"pngj-2.0.0\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"pngj-2.0.01\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"javailp-1.2a1\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"jewelcli-0.6\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"guava-20.0\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"jackson\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"poly2tri-core-0.1.1-SNAPSHOT1\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"configuration1\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"jogl1\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"jogl\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"osmosis1\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"javailp-1.2a\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"jackson1\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"jts\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"jts1\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"guava-20.01\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"OSM2World\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"osmosis\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"jewelcli-0.61\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"poly2tri-core-0.1.1-SNAPSHOT\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"annotations-java8\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"com.google.code.findbugs:jsr305:3.0.2\" level=\"project\" />\n  </component>\n</module>"
  },
  {
    "path": "OSM2World/build-adds/example_config.properties",
    "content": "! This is an example for an OSM2World (http://osm2world.org/) config file.\n! It is not used by default. If you want to use a config file, try the\n! \"--config <filename>\" option on the command line when starting OSM2World.\n! For more options and documentation, see the OpenStreetMap wiki:\n! http://wiki.osm.org/OSM2World/Configuration_file\n\n# a directory containing raw SRTM elevation data,\n# which can be obtained from http://dds.cr.usgs.gov/srtm/version2_1/SRTM3/\n# If this is not available, terrain calculation is disabled.\n# srtmDir = srtm\n\n# background color or image for PNG output\nbackgroundColor = #000000\n# backgroundImage = textures/background.png\n\n# tree density in forests\ntreesPerSquareMeter = 0.001\n\n# tree heights\ndefaultTreeHeight = 10.0\ndefaultTreeHeightForest = 20.0\n\n# sets the color of \"empty\" terrain. See Materials.java for other materials.\nmaterial_TERRAIN_DEFAULT_color = #FFFFFF\n\n# sets a texture for building walls.\n# material_BUILDING_DEFAULT_texture_file = textures/plaster.png\n# material_BUILDING_DEFAULT_texture_width = 2.5\n# material_BUILDING_DEFAULT_texture_height = 2.5\n# material_BUILDING_DEFAULT_texture_colorable = true\n\n# enable (true) or disable (false) parsing of building color and material tags\nuseBuildingColors = true\n\n# enable (true) or disable (false) a second texture layer for buildings, showing windows\ndrawBuildingWindows = true\n\n# enable (true) or disable (false) replacing geometry with textured billboards\nuseBillboards = true\n\n# enable (true) or disable (false) rendering of world objects below the ground\nrenderUnderground = true\n\n# true prevents the PNG export from buffering primitives. This reduces RAM usage, but can increase rendering time.\nforceUnbufferedPNGRendering = false\n\n# maximum size for each dimension of the OpenGL canvas used for PNG output.\n# If this is lower than the width or height of the requested png, performance suffers.\n# Increase it if your graphics hardware is capable of handling larger sizes.\ncanvasLimit = 1024\n"
  },
  {
    "path": "OSM2World/build-adds/osm2world-windows.bat",
    "content": "java -Xmx2G -jar OSM2World.jar %*\n"
  },
  {
    "path": "OSM2World/build-adds/osm2world.sh",
    "content": "#!/bin/bash\n\n# retrieve VM parameters\n\nvmparams=\"-Xmx2G\"\n\nif [[ $1 == --vm-params=* ]]\n  then\n    vmparams=${1:12}\n    shift\nfi\n\n# choose path for the native JOGL libs depending on system and java version\n\nMACHINE_TYPE=`uname -m`\nKERNEL_NAME=`uname -s`\n\nif [ ${KERNEL_NAME} == 'Darwin' ]; then\nlpsolvepath=\"lib/lp_solve/mac\"\nelse\nif [ `java -version 2>&1|grep -c 64-Bit` -gt 0 ]; then\nlpsolvepath=\"lib/lp_solve/ux64\"\n  else\nlpsolvepath=\"lib/lp_solve/ux32\"\n  fi\nfi\n\n# run OSM2World\n\nexport LD_LIBRARY_PATH=$lpsolvepath\n\njava -Djava.library.path=$lpsolvepath $vmparams -jar OSM2World.jar --config texture_config.properties $@\n"
  },
  {
    "path": "OSM2World/build.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n\n<project default=\"main\">\n\n    <property name=\"releaseVersion\" value=\"noversion\" />\n\n\t<!-- classpath used for javadoc and javac, don't forget to manually update the jar manifest classpath! -->\n    <property name=\"classpath\" value=\"lib/jsr305-3.0.2.jar:lib/configuration/commons-collections-3.2.1.jar:lib/configuration/commons-configuration-1.6.jar:lib/configuration/commons-lang-2.6.jar:lib/configuration/commons-logging-1.1.1.jar:lib/jogl/gluegen-rt.jar:lib/jogl/gluegen-rt-natives-linux-amd64.jar:lib/jogl/gluegen-rt-natives-linux-i586.jar:lib/jogl/gluegen-rt-natives-macosx-universal.jar:lib/jogl/gluegen-rt-natives-windows-amd64.jar:lib/jogl/gluegen-rt-natives-windows-i586.jar:lib/jogl/jogl-all.jar:lib/jogl/jogl-all-natives-linux-amd64.jar:lib/jogl/jogl-all-natives-linux-i586.jar:lib/jogl/jogl-all-natives-macosx-universal.jar:lib/jogl/jogl-all-natives-windows-amd64.jar:lib/jogl/jogl-all-natives-windows-i586.jar:lib/jewelcli/jewelcli-0.6.jar:lib/jts/jts-1.11.jar:lib/osmosis/commons-compress-1.2.jar:lib/osmosis/osmosis-core-0.41.jar:lib/osmosis/osmosis-pbf-0.41.jar:lib/osmosis/osmosis-xml-0.41.jar:lib/osmosis/osmpbf-1.1.1-754a33af.jar:lib/osmosis/protobuf-java-2.4.1.jar:lib/osmosis/xercesImpl-2.9.1.jar:lib/javaproj/javaproj-1.0.6.jar:lib/guava/guava-20.0.jar:lib/trove/trove-111118193120.jar:lib/poly2tri/poly2tri-core-0.1.1-SNAPSHOT.jar:lib/slf4j/slf4j-api-1.7.5.jar:lib/slf4j/slf4j-simple-1.7.5.jar:lib/commons/commons-math3-3.0.jar:lib/javailp/javailp-1.2a.jar:lib/lp_solve/lpsolve55j.jar:lib/pngj/pngj-2.0.0.jar:lib/jackson/jackson-core-2.8.6.jar:lib/jackson/jackson-databind-2.3.1.jar:lib/jackson/jackson-annotations-2.8.0.jar:resources/\" />\n    <property name=\"manifest-cp\" value=\". lib/jsr305-3.0.2.jar lib/configuration/commons-collections-3.2.1.jar lib/configuration/commons-configuration-1.6.jar lib/configuration/commons-lang-2.6.jar lib/configuration/commons-logging-1.1.1.jar lib/jogl/gluegen-rt.jar lib/jogl/gluegen-rt-natives-linux-amd64.jar lib/jogl/gluegen-rt-natives-linux-i586.jar lib/jogl/gluegen-rt-natives-macosx-universal.jar lib/jogl/gluegen-rt-natives-windows-amd64.jar lib/jogl/gluegen-rt-natives-windows-i586.jar lib/jogl/jogl-all.jar lib/jogl/jogl-all-natives-linux-amd64.jar lib/jogl/jogl-all-natives-linux-i586.jar lib/jogl/jogl-all-natives-macosx-universal.jar lib/jogl/jogl-all-natives-windows-amd64.jar lib/jogl/jogl-all-natives-windows-i586.jar lib/jewelcli/jewelcli-0.6.jar lib/jts/jts-1.11.jar lib/osmosis/commons-compress-1.2.jar lib/osmosis/osmosis-core-0.41.jar lib/osmosis/osmosis-pbf-0.41.jar lib/osmosis/osmosis-xml-0.41.jar lib/osmosis/osmpbf-1.1.1-754a33af.jar lib/osmosis/protobuf-java-2.4.1.jar lib/osmosis/xercesImpl-2.9.1.jar lib/javaproj/javaproj-1.0.6.jar lib/guava/guava-20.0.jar lib/trove/trove-111118193120.jar lib/poly2tri/poly2tri-core-0.1.1-SNAPSHOT.jar lib/slf4j/slf4j-api-1.7.5.jar lib/slf4j/slf4j-simple-1.7.5.jar lib/commons/commons-math3-3.0.jar lib/javailp/javailp-1.2a.jar lib/lp_solve/lpsolve55j.jar lib/pngj/pngj-2.0.0.jar lib/jackson/jackson-core-2.8.6.jar lib/jackson/jackson-databind-2.3.1.jar lib/jackson/jackson-annotations-2.8.0.jar resources/\" />\n\n    <target name=\"help\">\n      <echo message=\"  TARGETS:\"/>\n      <echo message=\"     help    =&gt; print this help\"/>\n      <echo message=\"     clean   =&gt; removes files in build/ and bin/\"/>\n      <echo message=\"     jar     =&gt; builds an optimized version of OSM2World\"/>\n      <echo message=\"     jardbg  =&gt; builds a debug build of OSM2World\"/>\n      <echo message=\"     release =&gt; builds a release version of OSM2World\"/>\n      <echo message=\"     javadoc =&gt; generates the API docs (javadoc)\"/>\n    </target>\n\n    <target name=\"main\" depends=\"help\">\n      <echo message=\"  Now running default target jar\"/>\n      <antcall target=\"jar\"/>\n    </target>\n\n    <target name=\"clean\">\n\t<delete includeemptydirs=\"true\"><fileset dir=\"bin\" includes=\"**/*\" erroronmissingdir=\"false\"/></delete>\n\t<delete includeemptydirs=\"true\"><fileset dir=\"build\" includes=\"*\" excludes=\"*.config,*.inc,*.sh,*.bat,texture*\" erroronmissingdir=\"false\"/></delete>\n\t<delete dir=\"doc/javadoc\"/>\n    </target>\n\n    <target name=\"compile\">\n\t<mkdir dir=\"bin\"/>\n        <javac target=\"1.7\" source=\"1.7\" optimize=\"true\" encoding=\"UTF-8\" srcdir=\"src\" destdir=\"bin\" classpath=\"${classpath}\" includeantruntime=\"false\"/>\n    </target>\n\n    <target name=\"compiledbg\">\n\t<mkdir dir=\"bin\"/>\n        <javac target=\"1.7\" source=\"1.7\" debug=\"true\" encoding=\"UTF-8\" srcdir=\"src\" destdir=\"bin\" classpath=\"${classpath}\" includeantruntime=\"false\"/>\n    </target>\n\t\n    <target name=\"jar\" depends=\"compile\">\n\t<mkdir dir=\"build\"/>\n        <jar destfile=\"build/OSM2World.jar\">\n            <manifest>\n                <attribute name=\"Main-Class\" value=\"org.osm2world.console.OSM2World\"/>\n            \t<attribute name=\"Class-Path\" value=\"${manifest-cp}\"/> \n            </manifest>\n            <fileset dir=\"bin\"/>\n        </jar>\n        <delete dir=\"build/lib\"/>\n        <mkdir dir=\"build/lib\"/>\n        <copy todir=\"build/lib\"><fileset dir=\"lib\"/></copy>\n        <delete dir=\"build/resources\"/>\n        <mkdir dir=\"build/resources\"/>\n        <copy todir=\"build/resources\"><fileset dir=\"resources\"/></copy>\n    </target>\n\n    <target name=\"jardbg\" depends=\"compiledbg\">\n\t<mkdir dir=\"build\"/>\n        <jar destfile=\"build/OSM2World.jar\">\n            <manifest>\n                <attribute name=\"Main-Class\" value=\"org.osm2world.console.OSM2World\"/>\n            \t<attribute name=\"Class-Path\" value=\"${manifest-cp}\"/>\n            </manifest>\n            <fileset dir=\"bin\"/>\n        </jar>\n        <delete dir=\"build/lib\"/>\n        <mkdir dir=\"build/lib\"/>\n        <copy todir=\"build/lib\"><fileset dir=\"lib\"/></copy>\n        <delete dir=\"build/resources\"/>\n        <mkdir dir=\"build/resources\"/>\n        <copy todir=\"build/resources\"><fileset dir=\"resources\"/></copy>\n    </target>\n\t\n\t<target name=\"release.bin\" depends=\"jar\">\n\t\t<mkdir dir=\"build/temp\"/>\n\t\t<copy todir=\"build/temp\" file=\"build/OSM2World.jar\"/>\n\t\t<copy todir=\"build/temp\"><fileset dir=\"build-adds\"/></copy>\n\t\t<copy todir=\"build/temp/resources\"><fileset dir=\"resources\"/></copy>\n\t\t<copy todir=\"build/temp/lib\"><fileset dir=\"lib\" excludes=\"jts/jts-1.11-src.zip\"/></copy>\n\t\t<copy todir=\"build/temp\"><fileset dir=\".\" includes=\"*.txt\"/></copy>\n\t\t<zip destfile=\"build/OSM2World-${releaseVersion}-bin.zip\">\n\t\t\t<zipfileset dir=\"build/temp\" includes=\"*.sh\" filemode=\"755\"/>\n\t\t\t<zipfileset dir=\"build/temp\" excludes=\"*.sh\"/>\n\t\t</zip>\n\t\t<delete dir=\"build/temp\"/>\n\t</target>\n\t\n   \t<target name=\"release.src\" depends=\"javadoc\">\n\t\t<mkdir dir=\"build/temp\"/>\n       \t\t<copy todir=\"build/temp/build-adds\"><fileset dir=\"build-adds\"/></copy>\n       \t\t<copy todir=\"build/temp/resources\"><fileset dir=\"resources\"/></copy>\n       \t\t<copy todir=\"build/temp/doc\"><fileset dir=\"doc\"/></copy>\n       \t\t<copy todir=\"build/temp/lib\"><fileset dir=\"lib\"/></copy>\n       \t\t<copy todir=\"build/temp/src\"><fileset dir=\"src\"/></copy>\n       \t\t<copy todir=\"build/temp/test\"><fileset dir=\"test\"/></copy>\n       \t\t<copy todir=\"build/temp\"><fileset dir=\".\" includes=\"*.txt\"/></copy>\n\t\t<copy todir=\"build/temp\" file=\"build.xml\"/>\n\t\t<zip destfile=\"build/OSM2World-${releaseVersion}-src.zip\" basedir=\"build/temp\"/>\n\t\t<delete dir=\"build/temp\"/>\n\t</target>\n\n    <target name=\"release\" depends=\"release.src,release.bin\"/>\n\n    <target name=\"javadoc\">\n        <javadoc access=\"protected\" additionalparam=\" -windowtitle &quot;OSM2World Javadoc&quot; -group &quot;OSM2World Core&quot; &quot;org.osm2world.core*&quot; -group &quot;OSM2World GUI&quot; &quot;org.osm2world.viewer*&quot; -group &quot;OSM2World Command Line&quot; &quot;org.osm2world.console*&quot; -noqualifier all \" author=\"false\" classpath=\"${classpath}\" destdir=\"doc/javadoc\" doctitle=\"OSM2World Documentation\" nodeprecated=\"false\" nodeprecatedlist=\"true\" noindex=\"false\" nonavbar=\"false\" notree=\"false\" source=\"1.6\" sourcepath=\"src\" splitindex=\"true\" use=\"true\" version=\"false\"/>\n    </target>\n\n</project>\n"
  },
  {
    "path": "OSM2World/gpl.txt",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "OSM2World/lgpl.txt",
    "content": "                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n\n  This version of the GNU Lesser General Public License incorporates\nthe terms and conditions of version 3 of the GNU General Public\nLicense, supplemented by the additional permissions listed below.\n\n  0. Additional Definitions.\n\n  As used herein, \"this License\" refers to version 3 of the GNU Lesser\nGeneral Public License, and the \"GNU GPL\" refers to version 3 of the GNU\nGeneral Public License.\n\n  \"The Library\" refers to a covered work governed by this License,\nother than an Application or a Combined Work as defined below.\n\n  An \"Application\" is any work that makes use of an interface provided\nby the Library, but which is not otherwise based on the Library.\nDefining a subclass of a class defined by the Library is deemed a mode\nof using an interface provided by the Library.\n\n  A \"Combined Work\" is a work produced by combining or linking an\nApplication with the Library.  The particular version of the Library\nwith which the Combined Work was made is also called the \"Linked\nVersion\".\n\n  The \"Minimal Corresponding Source\" for a Combined Work means the\nCorresponding Source for the Combined Work, excluding any source code\nfor portions of the Combined Work that, considered in isolation, are\nbased on the Application, and not on the Linked Version.\n\n  The \"Corresponding Application Code\" for a Combined Work means the\nobject code and/or source code for the Application, including any data\nand utility programs needed for reproducing the Combined Work from the\nApplication, but excluding the System Libraries of the Combined Work.\n\n  1. Exception to Section 3 of the GNU GPL.\n\n  You may convey a covered work under sections 3 and 4 of this License\nwithout being bound by section 3 of the GNU GPL.\n\n  2. Conveying Modified Versions.\n\n  If you modify a copy of the Library, and, in your modifications, a\nfacility refers to a function or data to be supplied by an Application\nthat uses the facility (other than as an argument passed when the\nfacility is invoked), then you may convey a copy of the modified\nversion:\n\n   a) under this License, provided that you make a good faith effort to\n   ensure that, in the event an Application does not supply the\n   function or data, the facility still operates, and performs\n   whatever part of its purpose remains meaningful, or\n\n   b) under the GNU GPL, with none of the additional permissions of\n   this License applicable to that copy.\n\n  3. Object Code Incorporating Material from Library Header Files.\n\n  The object code form of an Application may incorporate material from\na header file that is part of the Library.  You may convey such object\ncode under terms of your choice, provided that, if the incorporated\nmaterial is not limited to numerical parameters, data structure\nlayouts and accessors, or small macros, inline functions and templates\n(ten or fewer lines in length), you do both of the following:\n\n   a) Give prominent notice with each copy of the object code that the\n   Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the object code with a copy of the GNU GPL and this license\n   document.\n\n  4. Combined Works.\n\n  You may convey a Combined Work under terms of your choice that,\ntaken together, effectively do not restrict modification of the\nportions of the Library contained in the Combined Work and reverse\nengineering for debugging such modifications, if you also do each of\nthe following:\n\n   a) Give prominent notice with each copy of the Combined Work that\n   the Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the Combined Work with a copy of the GNU GPL and this license\n   document.\n\n   c) For a Combined Work that displays copyright notices during\n   execution, include the copyright notice for the Library among\n   these notices, as well as a reference directing the user to the\n   copies of the GNU GPL and this license document.\n\n   d) Do one of the following:\n\n       0) Convey the Minimal Corresponding Source under the terms of this\n       License, and the Corresponding Application Code in a form\n       suitable for, and under terms that permit, the user to\n       recombine or relink the Application with a modified version of\n       the Linked Version to produce a modified Combined Work, in the\n       manner specified by section 6 of the GNU GPL for conveying\n       Corresponding Source.\n\n       1) Use a suitable shared library mechanism for linking with the\n       Library.  A suitable mechanism is one that (a) uses at run time\n       a copy of the Library already present on the user's computer\n       system, and (b) will operate properly with a modified version\n       of the Library that is interface-compatible with the Linked\n       Version.\n\n   e) Provide Installation Information, but only if you would otherwise\n   be required to provide such information under section 6 of the\n   GNU GPL, and only to the extent that such information is\n   necessary to install and execute a modified version of the\n   Combined Work produced by recombining or relinking the\n   Application with a modified version of the Linked Version. (If\n   you use option 4d0, the Installation Information must accompany\n   the Minimal Corresponding Source and Corresponding Application\n   Code. If you use option 4d1, you must provide the Installation\n   Information in the manner specified by section 6 of the GNU GPL\n   for conveying Corresponding Source.)\n\n  5. Combined Libraries.\n\n  You may place library facilities that are a work based on the\nLibrary side by side in a single library together with other library\nfacilities that are not Applications and are not covered by this\nLicense, and convey such a combined library under terms of your\nchoice, if you do both of the following:\n\n   a) Accompany the combined library with a copy of the same work based\n   on the Library, uncombined with any other library facilities,\n   conveyed under the terms of this License.\n\n   b) Give prominent notice with the combined library that part of it\n   is a work based on the Library, and explaining where to find the\n   accompanying uncombined form of the same work.\n\n  6. Revised Versions of the GNU Lesser General Public License.\n\n  The Free Software Foundation may publish revised and/or new versions\nof the GNU Lesser General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\n  Each version is given a distinguishing version number. If the\nLibrary as you received it specifies that a certain numbered version\nof the GNU Lesser General Public License \"or any later version\"\napplies to it, you have the option of following the terms and\nconditions either of that published version or of any later version\npublished by the Free Software Foundation. If the Library as you\nreceived it does not specify a version number of the GNU Lesser\nGeneral Public License, you may choose any version of the GNU Lesser\nGeneral Public License ever published by the Free Software Foundation.\n\n  If the Library as you received it specifies that a proxy can decide\nwhether future versions of the GNU Lesser General Public License shall\napply, that proxy's public statement of acceptance of any version is\npermanent authorization for you to choose that version for the\nLibrary.\n"
  },
  {
    "path": "OSM2World/lib/configuration/LICENSE.txt",
    "content": "\r\n                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright [yyyy] [name of copyright owner]\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "OSM2World/lib/guava/COPYING",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "OSM2World/lib/jogl/jogl.LICENSE.txt",
    "content": "The JOGL source code is mostly licensed under the New BSD 2-clause license,\nhowever it contains other licensed material as well.\n\nBelow you find a detailed list of licenses used in this project.\n\n+++\n\nThe content of folder 'make/lib' contains build- and test-time only \nJava binaries (JAR) to ease the build setup.\nEach JAR file has it's corresponding LICENSE file containing the \nsource location and license text. None of these binaries are contained in any way \nby the generated and deployed JOGL binaries.\n\n+++\n\nL.1) The JOGL source tree contains code from the JogAmp Community\n     which is covered by the Simplified BSD 2-clause license:\n\n   Copyright 2010 JogAmp Community. All rights reserved.\n  \n   Redistribution and use in source and binary forms, with or without modification, are\n   permitted provided that the following conditions are met:\n   \n      1. Redistributions of source code must retain the above copyright notice, this list of\n         conditions and the following disclaimer.\n   \n      2. Redistributions in binary form must reproduce the above copyright notice, this list\n         of conditions and the following disclaimer in the documentation and/or other materials\n         provided with the distribution.\n   \n   THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED\n   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\n   FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR\n   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n   ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\n   ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n   \n   The views and conclusions contained in the software and documentation are those of the\n   authors and should not be interpreted as representing official policies, either expressed\n   or implied, of JogAmp Community.\n\n   You can address the JogAmp Community via:\n       Web                http://jogamp.org/\n       Forum/Mailinglist  http://forum.jogamp.org\n       JogAmp Channel     server: conference.jabber.org room: jogamp\n       Repository         http://jogamp.org/git/\n       Email              mediastream _at_ jogamp _dot_ org\n\n\nL.2) The JOGL source tree contains code from Sun Microsystems, Inc. \n     which is covered by the New BSD 3-clause license:\n\n   Copyright (c) 2003-2009 Sun Microsystems, Inc. All Rights Reserved.\n \n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions are\n   met:\n   \n   - Redistribution of source code must retain the above copyright\n     notice, this list of conditions and the following disclaimer.\n   \n   - Redistribution in binary form must reproduce the above copyright\n     notice, this list of conditions and the following disclaimer in the\n     documentation and/or other materials provided with the distribution.\n   \n   Neither the name of Sun Microsystems, Inc. or the names of\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n   \n   This software is provided \"AS IS,\" without a warranty of any kind. ALL\n   EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,\n   INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A\n   PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN\n   MICROSYSTEMS, INC. (\"SUN\") AND ITS LICENSORS SHALL NOT BE LIABLE FOR\n   ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR\n   DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR\n   ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR\n   DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE\n   DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,\n   ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF\n   SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n   \n   You acknowledge that this software is not designed or intended for use\n   in the design, construction, operation or maintenance of any nuclear\n   facility.\n\nL.3) The JOGL source tree contains code ported from the OpenGL sample\n     implementation by Silicon Graphics, Inc. This code is licensed under\n     the SGI Free Software License B, Version 2.0\n\n   License Applicability. Except to the extent portions of this file are\n   made subject to an alternative license as permitted in the SGI Free\n   Software License B, Version 2.0 (the \"License\"), the contents of this\n   file are subject only to the provisions of the License. You may not use\n   this file except in compliance with the License. You may obtain a copy\n   of the License at Silicon Graphics, Inc., attn: Legal Services, 1600\n   Amphitheatre Parkway, Mountain View, CA 94043-1351, or at:\n   \n   http://oss.sgi.com/projects/FreeB\n   http://oss.sgi.com/projects/FreeB/SGIFreeSWLicB.2.0.pdf\n   Or within this repository: doc/licenses/SGIFreeSWLicB.2.0.pdf\n   \n   Note that, as provided in the License, the Software is distributed on an\n   \"AS IS\" basis, with ALL EXPRESS AND IMPLIED WARRANTIES AND CONDITIONS\n   DISCLAIMED, INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES AND\n   CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A\n   PARTICULAR PURPOSE, AND NON-INFRINGEMENT.\n\nL.4) The JOGL source tree contains code from the LWJGL project which is\n     similarly covered by the New BSD 3-clause license:\n\n   Copyright (c) 2002-2004 LWJGL Project\n   All rights reserved.\n   \n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions are \n   met:\n   \n   * Redistributions of source code must retain the above copyright \n     notice, this list of conditions and the following disclaimer.\n  \n   * Redistributions in binary form must reproduce the above copyright\n     notice, this list of conditions and the following disclaimer in the\n     documentation and/or other materials provided with the distribution.\n  \n   * Neither the name of 'LWJGL' nor the names of \n     its contributors may be used to endorse or promote products derived \n     from this software without specific prior written permission.\n   \n   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n   \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\n   TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n   PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR \n   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, \n   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, \n   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR \n   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING \n   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nL.5) The JOGL source tree also contains a Java port of Brian Paul's Tile\n     Rendering library, used with permission of the author under the \n     New BSD 3-clause license instead of the original LGPL:\n\n   Copyright (c) 1997-2005 Brian Paul. All Rights Reserved.\n \n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions are\n   met:\n   \n   - Redistribution of source code must retain the above copyright\n     notice, this list of conditions and the following disclaimer.\n   \n   - Redistribution in binary form must reproduce the above copyright\n     notice, this list of conditions and the following disclaimer in the\n     documentation and/or other materials provided with the distribution.\n   \n   Neither the name of Brian Paul or the names of contributors may be\n   used to endorse or promote products derived from this software\n   without specific prior written permission.\n   \n   This software is provided \"AS IS,\" without a warranty of any\n   kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND\n   WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,\n   FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY\n   EXCLUDED. THE COPYRIGHT HOLDERS AND CONTRIBUTORS SHALL NOT BE\n   LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING,\n   MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO\n   EVENT WILL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY\n   LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,\n   CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND\n   REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR\n   INABILITY TO USE THIS SOFTWARE, EVEN IF THE COPYRIGHT HOLDERS OR\n   CONTRIBUTORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\nA.1) The JOGL source tree also contains header files from Khronos,\n     reflecting OpenKODE, EGL, OpenGL ES1, OpenGL ES2 and OpenGL.\n\n   http://www.khronos.org/legal/license/\n\n   Files:\n     make/stub_includes/opengl/**\n     make/stub_includes/egl/**\n     make/stub_includes/khr/**\n     make/stub_includes/openmax/**\n\n   Copyright (c) 2007-2010 The Khronos Group Inc.\n    \n   Permission is hereby granted, free of charge, to any person obtaining a\n   copy of this software and/or associated documentation files (the\n   \"Materials\"), to deal in the Materials without restriction, including\n   without limitation the rights to use, copy, modify, merge, publish,\n   distribute, sublicense, and/or sell copies of the Materials, and to\n   permit persons to whom the Materials are furnished to do so, subject to\n   the following conditions:\n    \n   The above copyright notice and this permission notice shall be included\n   in all copies or substantial portions of the Materials.\n    \n   THE MATERIALS ARE PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n   MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.\n\n\nA.2) The JOGL source tree contains code from The Apache Software Foundation\n     which is covered by the Apache License Version 2.0\n\n   Apache Harmony - Open Source Java SE\n   =====================================\n\n   <http://harmony.apache.org/>\n\n   Author: The Apache Software Foundation (http://www.apache.org/).\n\n   Copyright 2006, 2010 The Apache Software Foundation.\n\n   Apache License Version 2.0, January 2004\n   http://www.apache.org/licenses/LICENSE-2.0\n   Or within this repository: doc/licenses/Apache.LICENSE-2.0\n\n   Files:\n    src/jogamp/graph/geom/plane/AffineTransform.java\n    src/jogamp/graph/geom/plane/IllegalPathStateException.java\n    src/jogamp/graph/geom/plane/NoninvertibleTransformException.java\n    src/jogamp/graph/geom/plane/PathIterator.java\n    src/jogamp/graph/geom/plane/Path2D.java\n    src/jogamp/graph/math/plane/Crossing.java\n    src/org/apache/harmony/misc/HashCode.java\n\n\nA.3) The JOGL source tree contains code from David Schweinsberg\n     which is covered by the Apache License Version 1.1 and Version 2.0\n\n   Typecast\n   ========\n\n   Typecast is a font development environment for OpenType font technology.\n\n   <http://typecast.dev.java.net/>\n\n   Author: David Schweinsberg\n\n   Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.\n\n   Apache Licenses\n   http://www.apache.org/licenses/\n\n   Apache License Version 1.1\n   http://www.apache.org/licenses/LICENSE-1.1\n   Or within this repository: doc/licenses/Apache.LICENSE-1.1\n   Files:\n    src/jogl/classes/jogamp/graph/font/typecast/ot/*\n    src/jogl/classes/jogamp/graph/font/typecast/ot/table/*\n\n   Apache License Version 2.0\n   http://www.apache.org/licenses/LICENSE-2.0\n   Or within this repository: doc/licenses/Apache.LICENSE-2.0\n    src/jogl/classes/jogamp/graph/font/typecast/ot/*\n    src/jogl/classes/jogamp/graph/font/typecast/ot/mac/*\n    src/jogl/classes/jogamp/graph/font/typecast/ot/table/*\n    src/jogl/classes/jogamp/graph/font/typecast/tt/engine/*\n\nA.4) The JOGL source tree contains fonts from Ubuntu\n     which is covered by the UBUNTU FONT LICENCE Version 1.0\n\n   Ubuntu Font Family\n   ==================\n\n   The Ubuntu Font Family are libre fonts funded by Canonical Ltd on behalf of the Ubuntu project. \n\n   <http://font.ubuntu.com/>\n\n   Copyright 2010 Canonical Ltd.  \n   Licensed under the Ubuntu Font Licence 1.0\n\n   Author: Canonical Ltd., Dalton Maag\n\n   UBUNTU FONT LICENCE \n   Version 1.0\n   http://font.ubuntu.com/ufl/ubuntu-font-licence-1.0.txt\n   Or within this repository: doc/licenses/ubuntu-font-licence-1.0.txt\n\n   Files:\n    src/jogamp/graph/font/fonts/ubuntu/*\n\nA.5) The JOGL source tree also contains header files from NVIDIA,\n     reflecting Cg.\n\n   Files:\n     make/stub_includes/cg/CG/**\n\n   Copyright (c) 2002, NVIDIA Corporation\n   \n   NVIDIA Corporation(\"NVIDIA\") supplies this software to you in consideration \n   of your agreement to the following terms, and your use, installation, \n   modification or redistribution of this NVIDIA software constitutes \n   acceptance of these terms.  If you do not agree with these terms, please do \n   not use, install, modify or redistribute this NVIDIA software.\n   \n   In consideration of your agreement to abide by the following terms, and \n   subject to these terms, NVIDIA grants you a personal, non-exclusive license,\n   under NVIDIA's copyrights in this original NVIDIA software (the \"NVIDIA \n   Software\"), to use, reproduce, modify and redistribute the NVIDIA \n   Software, with or without modifications, in source and/or binary forms; \n   provided that if you redistribute the NVIDIA Software, you must retain the \n   copyright notice of NVIDIA, this notice and the following text and \n   disclaimers in all such redistributions of the NVIDIA Software. Neither the \n   name, trademarks, service marks nor logos of NVIDIA Corporation may be used \n   to endorse or promote products derived from the NVIDIA Software without \n   specific prior written permission from NVIDIA.  Except as expressly stated \n   in this notice, no other rights or licenses express or implied, are granted \n   by NVIDIA herein, including but not limited to any patent rights that may be \n   infringed by your derivative works or by other works in which the NVIDIA \n   Software may be incorporated. No hardware is licensed hereunder. \n   \n   THE NVIDIA SOFTWARE IS BEING PROVIDED ON AN \"AS IS\" BASIS, WITHOUT \n   WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING \n   WITHOUT LIMITATION, WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, \n   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR ITS USE AND OPERATION \n   EITHER ALONE OR IN COMBINATION WITH OTHER PRODUCTS.\n   \n   IN NO EVENT SHALL NVIDIA BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL, \n   EXEMPLARY, CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, LOST \n   PROFITS; PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR \n   PROFITS; OR BUSINESS INTERRUPTION) OR ARISING IN ANY WAY OUT OF THE USE, \n   REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE NVIDIA SOFTWARE, \n   HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING \n   NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF NVIDIA HAS BEEN ADVISED \n   OF THE POSSIBILITY OF SUCH DAMAGE.\n   \nA.6) The JOGL source tree contains code from Hernan J. Gonzalez and Shawn Hartsock\n     which is covered by the Apache License Version 2.0\n\n   PNGJ\n   ====\n\n   PNGJ: Java library for reading and writing PNG images.\n\n   Version 0.85  (1 April 2012)\n\n   <http://code.google.com/p/pngj/>\n\n   Author: Hernan J. Gonzalez and Shawn Hartsock\n\n   Copyright (C) 2004 The Apache Software Foundation. All rights reserved.\n\n   Apache Licenses\n   http://www.apache.org/licenses/\n\n   Apache License Version 2.0\n   http://www.apache.org/licenses/LICENSE-2.0\n   Or within this repository: doc/licenses/Apache.LICENSE-2.0\n    src/jogl/classes/jogamp/opengl/util/pngj/**\n\n\n"
  },
  {
    "path": "OSM2World/lib/jts/LICENSE.txt",
    "content": "\t\t  GNU LESSER GENERAL PUBLIC LICENSE\r\n\t\t       Version 2.1, February 1999\r\n\r\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\r\n     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r\n Everyone is permitted to copy and distribute verbatim copies\r\n of this license document, but changing it is not allowed.\r\n\r\n[This is the first released version of the Lesser GPL.  It also counts\r\n as the successor of the GNU Library Public License, version 2, hence\r\n the version number 2.1.]\r\n\r\n\t\t\t    Preamble\r\n\r\n  The licenses for most software are designed to take away your\r\nfreedom to share and change it.  By contrast, the GNU General Public\r\nLicenses are intended to guarantee your freedom to share and change\r\nfree software--to make sure the software is free for all its users.\r\n\r\n  This license, the Lesser General Public License, applies to some\r\nspecially designated software packages--typically libraries--of the\r\nFree Software Foundation and other authors who decide to use it.  You\r\ncan use it too, but we suggest you first think carefully about whether\r\nthis license or the ordinary General Public License is the better\r\nstrategy to use in any particular case, based on the explanations below.\r\n\r\n  When we speak of free software, we are referring to freedom of use,\r\nnot price.  Our General Public Licenses are designed to make sure that\r\nyou have the freedom to distribute copies of free software (and charge\r\nfor this service if you wish); that you receive source code or can get\r\nit if you want it; that you can change the software and use pieces of\r\nit in new free programs; and that you are informed that you can do\r\nthese things.\r\n\r\n  To protect your rights, we need to make restrictions that forbid\r\ndistributors to deny you these rights or to ask you to surrender these\r\nrights.  These restrictions translate to certain responsibilities for\r\nyou if you distribute copies of the library or if you modify it.\r\n\r\n  For example, if you distribute copies of the library, whether gratis\r\nor for a fee, you must give the recipients all the rights that we gave\r\nyou.  You must make sure that they, too, receive or can get the source\r\ncode.  If you link other code with the library, you must provide\r\ncomplete object files to the recipients, so that they can relink them\r\nwith the library after making changes to the library and recompiling\r\nit.  And you must show them these terms so they know their rights.\r\n\r\n  We protect your rights with a two-step method: (1) we copyright the\r\nlibrary, and (2) we offer you this license, which gives you legal\r\npermission to copy, distribute and/or modify the library.\r\n\r\n  To protect each distributor, we want to make it very clear that\r\nthere is no warranty for the free library.  Also, if the library is\r\nmodified by someone else and passed on, the recipients should know\r\nthat what they have is not the original version, so that the original\r\nauthor's reputation will not be affected by problems that might be\r\nintroduced by others.\r\n\f\r\n  Finally, software patents pose a constant threat to the existence of\r\nany free program.  We wish to make sure that a company cannot\r\neffectively restrict the users of a free program by obtaining a\r\nrestrictive license from a patent holder.  Therefore, we insist that\r\nany patent license obtained for a version of the library must be\r\nconsistent with the full freedom of use specified in this license.\r\n\r\n  Most GNU software, including some libraries, is covered by the\r\nordinary GNU General Public License.  This license, the GNU Lesser\r\nGeneral Public License, applies to certain designated libraries, and\r\nis quite different from the ordinary General Public License.  We use\r\nthis license for certain libraries in order to permit linking those\r\nlibraries into non-free programs.\r\n\r\n  When a program is linked with a library, whether statically or using\r\na shared library, the combination of the two is legally speaking a\r\ncombined work, a derivative of the original library.  The ordinary\r\nGeneral Public License therefore permits such linking only if the\r\nentire combination fits its criteria of freedom.  The Lesser General\r\nPublic License permits more lax criteria for linking other code with\r\nthe library.\r\n\r\n  We call this license the \"Lesser\" General Public License because it\r\ndoes Less to protect the user's freedom than the ordinary General\r\nPublic License.  It also provides other free software developers Less\r\nof an advantage over competing non-free programs.  These disadvantages\r\nare the reason we use the ordinary General Public License for many\r\nlibraries.  However, the Lesser license provides advantages in certain\r\nspecial circumstances.\r\n\r\n  For example, on rare occasions, there may be a special need to\r\nencourage the widest possible use of a certain library, so that it becomes\r\na de-facto standard.  To achieve this, non-free programs must be\r\nallowed to use the library.  A more frequent case is that a free\r\nlibrary does the same job as widely used non-free libraries.  In this\r\ncase, there is little to gain by limiting the free library to free\r\nsoftware only, so we use the Lesser General Public License.\r\n\r\n  In other cases, permission to use a particular library in non-free\r\nprograms enables a greater number of people to use a large body of\r\nfree software.  For example, permission to use the GNU C Library in\r\nnon-free programs enables many more people to use the whole GNU\r\noperating system, as well as its variant, the GNU/Linux operating\r\nsystem.\r\n\r\n  Although the Lesser General Public License is Less protective of the\r\nusers' freedom, it does ensure that the user of a program that is\r\nlinked with the Library has the freedom and the wherewithal to run\r\nthat program using a modified version of the Library.\r\n\r\n  The precise terms and conditions for copying, distribution and\r\nmodification follow.  Pay close attention to the difference between a\r\n\"work based on the library\" and a \"work that uses the library\".  The\r\nformer contains code derived from the library, whereas the latter must\r\nbe combined with the library in order to run.\r\n\f\r\n\t\t  GNU LESSER GENERAL PUBLIC LICENSE\r\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\r\n\r\n  0. This License Agreement applies to any software library or other\r\nprogram which contains a notice placed by the copyright holder or\r\nother authorized party saying it may be distributed under the terms of\r\nthis Lesser General Public License (also called \"this License\").\r\nEach licensee is addressed as \"you\".\r\n\r\n  A \"library\" means a collection of software functions and/or data\r\nprepared so as to be conveniently linked with application programs\r\n(which use some of those functions and data) to form executables.\r\n\r\n  The \"Library\", below, refers to any such software library or work\r\nwhich has been distributed under these terms.  A \"work based on the\r\nLibrary\" means either the Library or any derivative work under\r\ncopyright law: that is to say, a work containing the Library or a\r\nportion of it, either verbatim or with modifications and/or translated\r\nstraightforwardly into another language.  (Hereinafter, translation is\r\nincluded without limitation in the term \"modification\".)\r\n\r\n  \"Source code\" for a work means the preferred form of the work for\r\nmaking modifications to it.  For a library, complete source code means\r\nall the source code for all modules it contains, plus any associated\r\ninterface definition files, plus the scripts used to control compilation\r\nand installation of the library.\r\n\r\n  Activities other than copying, distribution and modification are not\r\ncovered by this License; they are outside its scope.  The act of\r\nrunning a program using the Library is not restricted, and output from\r\nsuch a program is covered only if its contents constitute a work based\r\non the Library (independent of the use of the Library in a tool for\r\nwriting it).  Whether that is true depends on what the Library does\r\nand what the program that uses the Library does.\r\n  \r\n  1. You may copy and distribute verbatim copies of the Library's\r\ncomplete source code as you receive it, in any medium, provided that\r\nyou conspicuously and appropriately publish on each copy an\r\nappropriate copyright notice and disclaimer of warranty; keep intact\r\nall the notices that refer to this License and to the absence of any\r\nwarranty; and distribute a copy of this License along with the\r\nLibrary.\r\n\r\n  You may charge a fee for the physical act of transferring a copy,\r\nand you may at your option offer warranty protection in exchange for a\r\nfee.\r\n\f\r\n  2. You may modify your copy or copies of the Library or any portion\r\nof it, thus forming a work based on the Library, and copy and\r\ndistribute such modifications or work under the terms of Section 1\r\nabove, provided that you also meet all of these conditions:\r\n\r\n    a) The modified work must itself be a software library.\r\n\r\n    b) You must cause the files modified to carry prominent notices\r\n    stating that you changed the files and the date of any change.\r\n\r\n    c) You must cause the whole of the work to be licensed at no\r\n    charge to all third parties under the terms of this License.\r\n\r\n    d) If a facility in the modified Library refers to a function or a\r\n    table of data to be supplied by an application program that uses\r\n    the facility, other than as an argument passed when the facility\r\n    is invoked, then you must make a good faith effort to ensure that,\r\n    in the event an application does not supply such function or\r\n    table, the facility still operates, and performs whatever part of\r\n    its purpose remains meaningful.\r\n\r\n    (For example, a function in a library to compute square roots has\r\n    a purpose that is entirely well-defined independent of the\r\n    application.  Therefore, Subsection 2d requires that any\r\n    application-supplied function or table used by this function must\r\n    be optional: if the application does not supply it, the square\r\n    root function must still compute square roots.)\r\n\r\nThese requirements apply to the modified work as a whole.  If\r\nidentifiable sections of that work are not derived from the Library,\r\nand can be reasonably considered independent and separate works in\r\nthemselves, then this License, and its terms, do not apply to those\r\nsections when you distribute them as separate works.  But when you\r\ndistribute the same sections as part of a whole which is a work based\r\non the Library, the distribution of the whole must be on the terms of\r\nthis License, whose permissions for other licensees extend to the\r\nentire whole, and thus to each and every part regardless of who wrote\r\nit.\r\n\r\nThus, it is not the intent of this section to claim rights or contest\r\nyour rights to work written entirely by you; rather, the intent is to\r\nexercise the right to control the distribution of derivative or\r\ncollective works based on the Library.\r\n\r\nIn addition, mere aggregation of another work not based on the Library\r\nwith the Library (or with a work based on the Library) on a volume of\r\na storage or distribution medium does not bring the other work under\r\nthe scope of this License.\r\n\r\n  3. You may opt to apply the terms of the ordinary GNU General Public\r\nLicense instead of this License to a given copy of the Library.  To do\r\nthis, you must alter all the notices that refer to this License, so\r\nthat they refer to the ordinary GNU General Public License, version 2,\r\ninstead of to this License.  (If a newer version than version 2 of the\r\nordinary GNU General Public License has appeared, then you can specify\r\nthat version instead if you wish.)  Do not make any other change in\r\nthese notices.\r\n\f\r\n  Once this change is made in a given copy, it is irreversible for\r\nthat copy, so the ordinary GNU General Public License applies to all\r\nsubsequent copies and derivative works made from that copy.\r\n\r\n  This option is useful when you wish to copy part of the code of\r\nthe Library into a program that is not a library.\r\n\r\n  4. You may copy and distribute the Library (or a portion or\r\nderivative of it, under Section 2) in object code or executable form\r\nunder the terms of Sections 1 and 2 above provided that you accompany\r\nit with the complete corresponding machine-readable source code, which\r\nmust be distributed under the terms of Sections 1 and 2 above on a\r\nmedium customarily used for software interchange.\r\n\r\n  If distribution of object code is made by offering access to copy\r\nfrom a designated place, then offering equivalent access to copy the\r\nsource code from the same place satisfies the requirement to\r\ndistribute the source code, even though third parties are not\r\ncompelled to copy the source along with the object code.\r\n\r\n  5. A program that contains no derivative of any portion of the\r\nLibrary, but is designed to work with the Library by being compiled or\r\nlinked with it, is called a \"work that uses the Library\".  Such a\r\nwork, in isolation, is not a derivative work of the Library, and\r\ntherefore falls outside the scope of this License.\r\n\r\n  However, linking a \"work that uses the Library\" with the Library\r\ncreates an executable that is a derivative of the Library (because it\r\ncontains portions of the Library), rather than a \"work that uses the\r\nlibrary\".  The executable is therefore covered by this License.\r\nSection 6 states terms for distribution of such executables.\r\n\r\n  When a \"work that uses the Library\" uses material from a header file\r\nthat is part of the Library, the object code for the work may be a\r\nderivative work of the Library even though the source code is not.\r\nWhether this is true is especially significant if the work can be\r\nlinked without the Library, or if the work is itself a library.  The\r\nthreshold for this to be true is not precisely defined by law.\r\n\r\n  If such an object file uses only numerical parameters, data\r\nstructure layouts and accessors, and small macros and small inline\r\nfunctions (ten lines or less in length), then the use of the object\r\nfile is unrestricted, regardless of whether it is legally a derivative\r\nwork.  (Executables containing this object code plus portions of the\r\nLibrary will still fall under Section 6.)\r\n\r\n  Otherwise, if the work is a derivative of the Library, you may\r\ndistribute the object code for the work under the terms of Section 6.\r\nAny executables containing that work also fall under Section 6,\r\nwhether or not they are linked directly with the Library itself.\r\n\f\r\n  6. As an exception to the Sections above, you may also combine or\r\nlink a \"work that uses the Library\" with the Library to produce a\r\nwork containing portions of the Library, and distribute that work\r\nunder terms of your choice, provided that the terms permit\r\nmodification of the work for the customer's own use and reverse\r\nengineering for debugging such modifications.\r\n\r\n  You must give prominent notice with each copy of the work that the\r\nLibrary is used in it and that the Library and its use are covered by\r\nthis License.  You must supply a copy of this License.  If the work\r\nduring execution displays copyright notices, you must include the\r\ncopyright notice for the Library among them, as well as a reference\r\ndirecting the user to the copy of this License.  Also, you must do one\r\nof these things:\r\n\r\n    a) Accompany the work with the complete corresponding\r\n    machine-readable source code for the Library including whatever\r\n    changes were used in the work (which must be distributed under\r\n    Sections 1 and 2 above); and, if the work is an executable linked\r\n    with the Library, with the complete machine-readable \"work that\r\n    uses the Library\", as object code and/or source code, so that the\r\n    user can modify the Library and then relink to produce a modified\r\n    executable containing the modified Library.  (It is understood\r\n    that the user who changes the contents of definitions files in the\r\n    Library will not necessarily be able to recompile the application\r\n    to use the modified definitions.)\r\n\r\n    b) Use a suitable shared library mechanism for linking with the\r\n    Library.  A suitable mechanism is one that (1) uses at run time a\r\n    copy of the library already present on the user's computer system,\r\n    rather than copying library functions into the executable, and (2)\r\n    will operate properly with a modified version of the library, if\r\n    the user installs one, as long as the modified version is\r\n    interface-compatible with the version that the work was made with.\r\n\r\n    c) Accompany the work with a written offer, valid for at\r\n    least three years, to give the same user the materials\r\n    specified in Subsection 6a, above, for a charge no more\r\n    than the cost of performing this distribution.\r\n\r\n    d) If distribution of the work is made by offering access to copy\r\n    from a designated place, offer equivalent access to copy the above\r\n    specified materials from the same place.\r\n\r\n    e) Verify that the user has already received a copy of these\r\n    materials or that you have already sent this user a copy.\r\n\r\n  For an executable, the required form of the \"work that uses the\r\nLibrary\" must include any data and utility programs needed for\r\nreproducing the executable from it.  However, as a special exception,\r\nthe materials to be distributed need not include anything that is\r\nnormally distributed (in either source or binary form) with the major\r\ncomponents (compiler, kernel, and so on) of the operating system on\r\nwhich the executable runs, unless that component itself accompanies\r\nthe executable.\r\n\r\n  It may happen that this requirement contradicts the license\r\nrestrictions of other proprietary libraries that do not normally\r\naccompany the operating system.  Such a contradiction means you cannot\r\nuse both them and the Library together in an executable that you\r\ndistribute.\r\n\f\r\n  7. You may place library facilities that are a work based on the\r\nLibrary side-by-side in a single library together with other library\r\nfacilities not covered by this License, and distribute such a combined\r\nlibrary, provided that the separate distribution of the work based on\r\nthe Library and of the other library facilities is otherwise\r\npermitted, and provided that you do these two things:\r\n\r\n    a) Accompany the combined library with a copy of the same work\r\n    based on the Library, uncombined with any other library\r\n    facilities.  This must be distributed under the terms of the\r\n    Sections above.\r\n\r\n    b) Give prominent notice with the combined library of the fact\r\n    that part of it is a work based on the Library, and explaining\r\n    where to find the accompanying uncombined form of the same work.\r\n\r\n  8. You may not copy, modify, sublicense, link with, or distribute\r\nthe Library except as expressly provided under this License.  Any\r\nattempt otherwise to copy, modify, sublicense, link with, or\r\ndistribute the Library is void, and will automatically terminate your\r\nrights under this License.  However, parties who have received copies,\r\nor rights, from you under this License will not have their licenses\r\nterminated so long as such parties remain in full compliance.\r\n\r\n  9. You are not required to accept this License, since you have not\r\nsigned it.  However, nothing else grants you permission to modify or\r\ndistribute the Library or its derivative works.  These actions are\r\nprohibited by law if you do not accept this License.  Therefore, by\r\nmodifying or distributing the Library (or any work based on the\r\nLibrary), you indicate your acceptance of this License to do so, and\r\nall its terms and conditions for copying, distributing or modifying\r\nthe Library or works based on it.\r\n\r\n  10. Each time you redistribute the Library (or any work based on the\r\nLibrary), the recipient automatically receives a license from the\r\noriginal licensor to copy, distribute, link with or modify the Library\r\nsubject to these terms and conditions.  You may not impose any further\r\nrestrictions on the recipients' exercise of the rights granted herein.\r\nYou are not responsible for enforcing compliance by third parties with\r\nthis License.\r\n\f\r\n  11. If, as a consequence of a court judgment or allegation of patent\r\ninfringement or for any other reason (not limited to patent issues),\r\nconditions are imposed on you (whether by court order, agreement or\r\notherwise) that contradict the conditions of this License, they do not\r\nexcuse you from the conditions of this License.  If you cannot\r\ndistribute so as to satisfy simultaneously your obligations under this\r\nLicense and any other pertinent obligations, then as a consequence you\r\nmay not distribute the Library at all.  For example, if a patent\r\nlicense would not permit royalty-free redistribution of the Library by\r\nall those who receive copies directly or indirectly through you, then\r\nthe only way you could satisfy both it and this License would be to\r\nrefrain entirely from distribution of the Library.\r\n\r\nIf any portion of this section is held invalid or unenforceable under any\r\nparticular circumstance, the balance of the section is intended to apply,\r\nand the section as a whole is intended to apply in other circumstances.\r\n\r\nIt is not the purpose of this section to induce you to infringe any\r\npatents or other property right claims or to contest validity of any\r\nsuch claims; this section has the sole purpose of protecting the\r\nintegrity of the free software distribution system which is\r\nimplemented by public license practices.  Many people have made\r\ngenerous contributions to the wide range of software distributed\r\nthrough that system in reliance on consistent application of that\r\nsystem; it is up to the author/donor to decide if he or she is willing\r\nto distribute software through any other system and a licensee cannot\r\nimpose that choice.\r\n\r\nThis section is intended to make thoroughly clear what is believed to\r\nbe a consequence of the rest of this License.\r\n\r\n  12. If the distribution and/or use of the Library is restricted in\r\ncertain countries either by patents or by copyrighted interfaces, the\r\noriginal copyright holder who places the Library under this License may add\r\nan explicit geographical distribution limitation excluding those countries,\r\nso that distribution is permitted only in or among countries not thus\r\nexcluded.  In such case, this License incorporates the limitation as if\r\nwritten in the body of this License.\r\n\r\n  13. The Free Software Foundation may publish revised and/or new\r\nversions of the Lesser General Public License from time to time.\r\nSuch new versions will be similar in spirit to the present version,\r\nbut may differ in detail to address new problems or concerns.\r\n\r\nEach version is given a distinguishing version number.  If the Library\r\nspecifies a version number of this License which applies to it and\r\n\"any later version\", you have the option of following the terms and\r\nconditions either of that version or of any later version published by\r\nthe Free Software Foundation.  If the Library does not specify a\r\nlicense version number, you may choose any version ever published by\r\nthe Free Software Foundation.\r\n\f\r\n  14. If you wish to incorporate parts of the Library into other free\r\nprograms whose distribution conditions are incompatible with these,\r\nwrite to the author to ask for permission.  For software which is\r\ncopyrighted by the Free Software Foundation, write to the Free\r\nSoftware Foundation; we sometimes make exceptions for this.  Our\r\ndecision will be guided by the two goals of preserving the free status\r\nof all derivatives of our free software and of promoting the sharing\r\nand reuse of software generally.\r\n\r\n\t\t\t    NO WARRANTY\r\n\r\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\r\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\r\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\r\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\r\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\r\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\r\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\r\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\r\n\r\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\r\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\r\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\r\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\r\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\r\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\r\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\r\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\r\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\r\nDAMAGES.\r\n\r\n\t\t     END OF TERMS AND CONDITIONS\r\n\f\r\n           How to Apply These Terms to Your New Libraries\r\n\r\n  If you develop a new library, and you want it to be of the greatest\r\npossible use to the public, we recommend making it free software that\r\neveryone can redistribute and change.  You can do so by permitting\r\nredistribution under these terms (or, alternatively, under the terms of the\r\nordinary General Public License).\r\n\r\n  To apply these terms, attach the following notices to the library.  It is\r\nsafest to attach them to the start of each source file to most effectively\r\nconvey the exclusion of warranty; and each file should have at least the\r\n\"copyright\" line and a pointer to where the full notice is found.\r\n\r\n    <one line to give the library's name and a brief idea of what it does.>\r\n    Copyright (C) <year>  <name of author>\r\n\r\n    This library is free software; you can redistribute it and/or\r\n    modify it under the terms of the GNU Lesser General Public\r\n    License as published by the Free Software Foundation; either\r\n    version 2.1 of the License, or (at your option) any later version.\r\n\r\n    This library is distributed in the hope that it will be useful,\r\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r\n    Lesser General Public License for more details.\r\n\r\n    You should have received a copy of the GNU Lesser General Public\r\n    License along with this library; if not, write to the Free Software\r\n    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r\n\r\nAlso add information on how to contact you by electronic and paper mail.\r\n\r\nYou should also get your employer (if you work as a programmer) or your\r\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\r\nnecessary.  Here is a sample; alter the names:\r\n\r\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\r\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\r\n\r\n  <signature of Ty Coon>, 1 April 1990\r\n  Ty Coon, President of Vice\r\n\r\nThat's all there is to it!\r\n\r\n\r\n"
  },
  {
    "path": "OSM2World/lib/lp_solve/LGPL",
    "content": "\t\t  GNU LESSER GENERAL PUBLIC LICENSE\r\n\t\t       Version 2.1, February 1999\r\n\r\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\r\n     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r\n Everyone is permitted to copy and distribute verbatim copies\r\n of this license document, but changing it is not allowed.\r\n\r\n[This is the first released version of the Lesser GPL.  It also counts\r\n as the successor of the GNU Library Public License, version 2, hence\r\n the version number 2.1.]\r\n\r\n\t\t\t    Preamble\r\n\r\n  The licenses for most software are designed to take away your\r\nfreedom to share and change it.  By contrast, the GNU General Public\r\nLicenses are intended to guarantee your freedom to share and change\r\nfree software--to make sure the software is free for all its users.\r\n\r\n  This license, the Lesser General Public License, applies to some\r\nspecially designated software packages--typically libraries--of the\r\nFree Software Foundation and other authors who decide to use it.  You\r\ncan use it too, but we suggest you first think carefully about whether\r\nthis license or the ordinary General Public License is the better\r\nstrategy to use in any particular case, based on the explanations below.\r\n\r\n  When we speak of free software, we are referring to freedom of use,\r\nnot price.  Our General Public Licenses are designed to make sure that\r\nyou have the freedom to distribute copies of free software (and charge\r\nfor this service if you wish); that you receive source code or can get\r\nit if you want it; that you can change the software and use pieces of\r\nit in new free programs; and that you are informed that you can do\r\nthese things.\r\n\r\n  To protect your rights, we need to make restrictions that forbid\r\ndistributors to deny you these rights or to ask you to surrender these\r\nrights.  These restrictions translate to certain responsibilities for\r\nyou if you distribute copies of the library or if you modify it.\r\n\r\n  For example, if you distribute copies of the library, whether gratis\r\nor for a fee, you must give the recipients all the rights that we gave\r\nyou.  You must make sure that they, too, receive or can get the source\r\ncode.  If you link other code with the library, you must provide\r\ncomplete object files to the recipients, so that they can relink them\r\nwith the library after making changes to the library and recompiling\r\nit.  And you must show them these terms so they know their rights.\r\n\r\n  We protect your rights with a two-step method: (1) we copyright the\r\nlibrary, and (2) we offer you this license, which gives you legal\r\npermission to copy, distribute and/or modify the library.\r\n\r\n  To protect each distributor, we want to make it very clear that\r\nthere is no warranty for the free library.  Also, if the library is\r\nmodified by someone else and passed on, the recipients should know\r\nthat what they have is not the original version, so that the original\r\nauthor's reputation will not be affected by problems that might be\r\nintroduced by others.\r\n\f\r\n  Finally, software patents pose a constant threat to the existence of\r\nany free program.  We wish to make sure that a company cannot\r\neffectively restrict the users of a free program by obtaining a\r\nrestrictive license from a patent holder.  Therefore, we insist that\r\nany patent license obtained for a version of the library must be\r\nconsistent with the full freedom of use specified in this license.\r\n\r\n  Most GNU software, including some libraries, is covered by the\r\nordinary GNU General Public License.  This license, the GNU Lesser\r\nGeneral Public License, applies to certain designated libraries, and\r\nis quite different from the ordinary General Public License.  We use\r\nthis license for certain libraries in order to permit linking those\r\nlibraries into non-free programs.\r\n\r\n  When a program is linked with a library, whether statically or using\r\na shared library, the combination of the two is legally speaking a\r\ncombined work, a derivative of the original library.  The ordinary\r\nGeneral Public License therefore permits such linking only if the\r\nentire combination fits its criteria of freedom.  The Lesser General\r\nPublic License permits more lax criteria for linking other code with\r\nthe library.\r\n\r\n  We call this license the \"Lesser\" General Public License because it\r\ndoes Less to protect the user's freedom than the ordinary General\r\nPublic License.  It also provides other free software developers Less\r\nof an advantage over competing non-free programs.  These disadvantages\r\nare the reason we use the ordinary General Public License for many\r\nlibraries.  However, the Lesser license provides advantages in certain\r\nspecial circumstances.\r\n\r\n  For example, on rare occasions, there may be a special need to\r\nencourage the widest possible use of a certain library, so that it becomes\r\na de-facto standard.  To achieve this, non-free programs must be\r\nallowed to use the library.  A more frequent case is that a free\r\nlibrary does the same job as widely used non-free libraries.  In this\r\ncase, there is little to gain by limiting the free library to free\r\nsoftware only, so we use the Lesser General Public License.\r\n\r\n  In other cases, permission to use a particular library in non-free\r\nprograms enables a greater number of people to use a large body of\r\nfree software.  For example, permission to use the GNU C Library in\r\nnon-free programs enables many more people to use the whole GNU\r\noperating system, as well as its variant, the GNU/Linux operating\r\nsystem.\r\n\r\n  Although the Lesser General Public License is Less protective of the\r\nusers' freedom, it does ensure that the user of a program that is\r\nlinked with the Library has the freedom and the wherewithal to run\r\nthat program using a modified version of the Library.\r\n\r\n  The precise terms and conditions for copying, distribution and\r\nmodification follow.  Pay close attention to the difference between a\r\n\"work based on the library\" and a \"work that uses the library\".  The\r\nformer contains code derived from the library, whereas the latter must\r\nbe combined with the library in order to run.\r\n\f\r\n\t\t  GNU LESSER GENERAL PUBLIC LICENSE\r\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\r\n\r\n  0. This License Agreement applies to any software library or other\r\nprogram which contains a notice placed by the copyright holder or\r\nother authorized party saying it may be distributed under the terms of\r\nthis Lesser General Public License (also called \"this License\").\r\nEach licensee is addressed as \"you\".\r\n\r\n  A \"library\" means a collection of software functions and/or data\r\nprepared so as to be conveniently linked with application programs\r\n(which use some of those functions and data) to form executables.\r\n\r\n  The \"Library\", below, refers to any such software library or work\r\nwhich has been distributed under these terms.  A \"work based on the\r\nLibrary\" means either the Library or any derivative work under\r\ncopyright law: that is to say, a work containing the Library or a\r\nportion of it, either verbatim or with modifications and/or translated\r\nstraightforwardly into another language.  (Hereinafter, translation is\r\nincluded without limitation in the term \"modification\".)\r\n\r\n  \"Source code\" for a work means the preferred form of the work for\r\nmaking modifications to it.  For a library, complete source code means\r\nall the source code for all modules it contains, plus any associated\r\ninterface definition files, plus the scripts used to control compilation\r\nand installation of the library.\r\n\r\n  Activities other than copying, distribution and modification are not\r\ncovered by this License; they are outside its scope.  The act of\r\nrunning a program using the Library is not restricted, and output from\r\nsuch a program is covered only if its contents constitute a work based\r\non the Library (independent of the use of the Library in a tool for\r\nwriting it).  Whether that is true depends on what the Library does\r\nand what the program that uses the Library does.\r\n  \r\n  1. You may copy and distribute verbatim copies of the Library's\r\ncomplete source code as you receive it, in any medium, provided that\r\nyou conspicuously and appropriately publish on each copy an\r\nappropriate copyright notice and disclaimer of warranty; keep intact\r\nall the notices that refer to this License and to the absence of any\r\nwarranty; and distribute a copy of this License along with the\r\nLibrary.\r\n\r\n  You may charge a fee for the physical act of transferring a copy,\r\nand you may at your option offer warranty protection in exchange for a\r\nfee.\r\n\f\r\n  2. You may modify your copy or copies of the Library or any portion\r\nof it, thus forming a work based on the Library, and copy and\r\ndistribute such modifications or work under the terms of Section 1\r\nabove, provided that you also meet all of these conditions:\r\n\r\n    a) The modified work must itself be a software library.\r\n\r\n    b) You must cause the files modified to carry prominent notices\r\n    stating that you changed the files and the date of any change.\r\n\r\n    c) You must cause the whole of the work to be licensed at no\r\n    charge to all third parties under the terms of this License.\r\n\r\n    d) If a facility in the modified Library refers to a function or a\r\n    table of data to be supplied by an application program that uses\r\n    the facility, other than as an argument passed when the facility\r\n    is invoked, then you must make a good faith effort to ensure that,\r\n    in the event an application does not supply such function or\r\n    table, the facility still operates, and performs whatever part of\r\n    its purpose remains meaningful.\r\n\r\n    (For example, a function in a library to compute square roots has\r\n    a purpose that is entirely well-defined independent of the\r\n    application.  Therefore, Subsection 2d requires that any\r\n    application-supplied function or table used by this function must\r\n    be optional: if the application does not supply it, the square\r\n    root function must still compute square roots.)\r\n\r\nThese requirements apply to the modified work as a whole.  If\r\nidentifiable sections of that work are not derived from the Library,\r\nand can be reasonably considered independent and separate works in\r\nthemselves, then this License, and its terms, do not apply to those\r\nsections when you distribute them as separate works.  But when you\r\ndistribute the same sections as part of a whole which is a work based\r\non the Library, the distribution of the whole must be on the terms of\r\nthis License, whose permissions for other licensees extend to the\r\nentire whole, and thus to each and every part regardless of who wrote\r\nit.\r\n\r\nThus, it is not the intent of this section to claim rights or contest\r\nyour rights to work written entirely by you; rather, the intent is to\r\nexercise the right to control the distribution of derivative or\r\ncollective works based on the Library.\r\n\r\nIn addition, mere aggregation of another work not based on the Library\r\nwith the Library (or with a work based on the Library) on a volume of\r\na storage or distribution medium does not bring the other work under\r\nthe scope of this License.\r\n\r\n  3. You may opt to apply the terms of the ordinary GNU General Public\r\nLicense instead of this License to a given copy of the Library.  To do\r\nthis, you must alter all the notices that refer to this License, so\r\nthat they refer to the ordinary GNU General Public License, version 2,\r\ninstead of to this License.  (If a newer version than version 2 of the\r\nordinary GNU General Public License has appeared, then you can specify\r\nthat version instead if you wish.)  Do not make any other change in\r\nthese notices.\r\n\f\r\n  Once this change is made in a given copy, it is irreversible for\r\nthat copy, so the ordinary GNU General Public License applies to all\r\nsubsequent copies and derivative works made from that copy.\r\n\r\n  This option is useful when you wish to copy part of the code of\r\nthe Library into a program that is not a library.\r\n\r\n  4. You may copy and distribute the Library (or a portion or\r\nderivative of it, under Section 2) in object code or executable form\r\nunder the terms of Sections 1 and 2 above provided that you accompany\r\nit with the complete corresponding machine-readable source code, which\r\nmust be distributed under the terms of Sections 1 and 2 above on a\r\nmedium customarily used for software interchange.\r\n\r\n  If distribution of object code is made by offering access to copy\r\nfrom a designated place, then offering equivalent access to copy the\r\nsource code from the same place satisfies the requirement to\r\ndistribute the source code, even though third parties are not\r\ncompelled to copy the source along with the object code.\r\n\r\n  5. A program that contains no derivative of any portion of the\r\nLibrary, but is designed to work with the Library by being compiled or\r\nlinked with it, is called a \"work that uses the Library\".  Such a\r\nwork, in isolation, is not a derivative work of the Library, and\r\ntherefore falls outside the scope of this License.\r\n\r\n  However, linking a \"work that uses the Library\" with the Library\r\ncreates an executable that is a derivative of the Library (because it\r\ncontains portions of the Library), rather than a \"work that uses the\r\nlibrary\".  The executable is therefore covered by this License.\r\nSection 6 states terms for distribution of such executables.\r\n\r\n  When a \"work that uses the Library\" uses material from a header file\r\nthat is part of the Library, the object code for the work may be a\r\nderivative work of the Library even though the source code is not.\r\nWhether this is true is especially significant if the work can be\r\nlinked without the Library, or if the work is itself a library.  The\r\nthreshold for this to be true is not precisely defined by law.\r\n\r\n  If such an object file uses only numerical parameters, data\r\nstructure layouts and accessors, and small macros and small inline\r\nfunctions (ten lines or less in length), then the use of the object\r\nfile is unrestricted, regardless of whether it is legally a derivative\r\nwork.  (Executables containing this object code plus portions of the\r\nLibrary will still fall under Section 6.)\r\n\r\n  Otherwise, if the work is a derivative of the Library, you may\r\ndistribute the object code for the work under the terms of Section 6.\r\nAny executables containing that work also fall under Section 6,\r\nwhether or not they are linked directly with the Library itself.\r\n\f\r\n  6. As an exception to the Sections above, you may also combine or\r\nlink a \"work that uses the Library\" with the Library to produce a\r\nwork containing portions of the Library, and distribute that work\r\nunder terms of your choice, provided that the terms permit\r\nmodification of the work for the customer's own use and reverse\r\nengineering for debugging such modifications.\r\n\r\n  You must give prominent notice with each copy of the work that the\r\nLibrary is used in it and that the Library and its use are covered by\r\nthis License.  You must supply a copy of this License.  If the work\r\nduring execution displays copyright notices, you must include the\r\ncopyright notice for the Library among them, as well as a reference\r\ndirecting the user to the copy of this License.  Also, you must do one\r\nof these things:\r\n\r\n    a) Accompany the work with the complete corresponding\r\n    machine-readable source code for the Library including whatever\r\n    changes were used in the work (which must be distributed under\r\n    Sections 1 and 2 above); and, if the work is an executable linked\r\n    with the Library, with the complete machine-readable \"work that\r\n    uses the Library\", as object code and/or source code, so that the\r\n    user can modify the Library and then relink to produce a modified\r\n    executable containing the modified Library.  (It is understood\r\n    that the user who changes the contents of definitions files in the\r\n    Library will not necessarily be able to recompile the application\r\n    to use the modified definitions.)\r\n\r\n    b) Use a suitable shared library mechanism for linking with the\r\n    Library.  A suitable mechanism is one that (1) uses at run time a\r\n    copy of the library already present on the user's computer system,\r\n    rather than copying library functions into the executable, and (2)\r\n    will operate properly with a modified version of the library, if\r\n    the user installs one, as long as the modified version is\r\n    interface-compatible with the version that the work was made with.\r\n\r\n    c) Accompany the work with a written offer, valid for at\r\n    least three years, to give the same user the materials\r\n    specified in Subsection 6a, above, for a charge no more\r\n    than the cost of performing this distribution.\r\n\r\n    d) If distribution of the work is made by offering access to copy\r\n    from a designated place, offer equivalent access to copy the above\r\n    specified materials from the same place.\r\n\r\n    e) Verify that the user has already received a copy of these\r\n    materials or that you have already sent this user a copy.\r\n\r\n  For an executable, the required form of the \"work that uses the\r\nLibrary\" must include any data and utility programs needed for\r\nreproducing the executable from it.  However, as a special exception,\r\nthe materials to be distributed need not include anything that is\r\nnormally distributed (in either source or binary form) with the major\r\ncomponents (compiler, kernel, and so on) of the operating system on\r\nwhich the executable runs, unless that component itself accompanies\r\nthe executable.\r\n\r\n  It may happen that this requirement contradicts the license\r\nrestrictions of other proprietary libraries that do not normally\r\naccompany the operating system.  Such a contradiction means you cannot\r\nuse both them and the Library together in an executable that you\r\ndistribute.\r\n\f\r\n  7. You may place library facilities that are a work based on the\r\nLibrary side-by-side in a single library together with other library\r\nfacilities not covered by this License, and distribute such a combined\r\nlibrary, provided that the separate distribution of the work based on\r\nthe Library and of the other library facilities is otherwise\r\npermitted, and provided that you do these two things:\r\n\r\n    a) Accompany the combined library with a copy of the same work\r\n    based on the Library, uncombined with any other library\r\n    facilities.  This must be distributed under the terms of the\r\n    Sections above.\r\n\r\n    b) Give prominent notice with the combined library of the fact\r\n    that part of it is a work based on the Library, and explaining\r\n    where to find the accompanying uncombined form of the same work.\r\n\r\n  8. You may not copy, modify, sublicense, link with, or distribute\r\nthe Library except as expressly provided under this License.  Any\r\nattempt otherwise to copy, modify, sublicense, link with, or\r\ndistribute the Library is void, and will automatically terminate your\r\nrights under this License.  However, parties who have received copies,\r\nor rights, from you under this License will not have their licenses\r\nterminated so long as such parties remain in full compliance.\r\n\r\n  9. You are not required to accept this License, since you have not\r\nsigned it.  However, nothing else grants you permission to modify or\r\ndistribute the Library or its derivative works.  These actions are\r\nprohibited by law if you do not accept this License.  Therefore, by\r\nmodifying or distributing the Library (or any work based on the\r\nLibrary), you indicate your acceptance of this License to do so, and\r\nall its terms and conditions for copying, distributing or modifying\r\nthe Library or works based on it.\r\n\r\n  10. Each time you redistribute the Library (or any work based on the\r\nLibrary), the recipient automatically receives a license from the\r\noriginal licensor to copy, distribute, link with or modify the Library\r\nsubject to these terms and conditions.  You may not impose any further\r\nrestrictions on the recipients' exercise of the rights granted herein.\r\nYou are not responsible for enforcing compliance by third parties with\r\nthis License.\r\n\f\r\n  11. If, as a consequence of a court judgment or allegation of patent\r\ninfringement or for any other reason (not limited to patent issues),\r\nconditions are imposed on you (whether by court order, agreement or\r\notherwise) that contradict the conditions of this License, they do not\r\nexcuse you from the conditions of this License.  If you cannot\r\ndistribute so as to satisfy simultaneously your obligations under this\r\nLicense and any other pertinent obligations, then as a consequence you\r\nmay not distribute the Library at all.  For example, if a patent\r\nlicense would not permit royalty-free redistribution of the Library by\r\nall those who receive copies directly or indirectly through you, then\r\nthe only way you could satisfy both it and this License would be to\r\nrefrain entirely from distribution of the Library.\r\n\r\nIf any portion of this section is held invalid or unenforceable under any\r\nparticular circumstance, the balance of the section is intended to apply,\r\nand the section as a whole is intended to apply in other circumstances.\r\n\r\nIt is not the purpose of this section to induce you to infringe any\r\npatents or other property right claims or to contest validity of any\r\nsuch claims; this section has the sole purpose of protecting the\r\nintegrity of the free software distribution system which is\r\nimplemented by public license practices.  Many people have made\r\ngenerous contributions to the wide range of software distributed\r\nthrough that system in reliance on consistent application of that\r\nsystem; it is up to the author/donor to decide if he or she is willing\r\nto distribute software through any other system and a licensee cannot\r\nimpose that choice.\r\n\r\nThis section is intended to make thoroughly clear what is believed to\r\nbe a consequence of the rest of this License.\r\n\r\n  12. If the distribution and/or use of the Library is restricted in\r\ncertain countries either by patents or by copyrighted interfaces, the\r\noriginal copyright holder who places the Library under this License may add\r\nan explicit geographical distribution limitation excluding those countries,\r\nso that distribution is permitted only in or among countries not thus\r\nexcluded.  In such case, this License incorporates the limitation as if\r\nwritten in the body of this License.\r\n\r\n  13. The Free Software Foundation may publish revised and/or new\r\nversions of the Lesser General Public License from time to time.\r\nSuch new versions will be similar in spirit to the present version,\r\nbut may differ in detail to address new problems or concerns.\r\n\r\nEach version is given a distinguishing version number.  If the Library\r\nspecifies a version number of this License which applies to it and\r\n\"any later version\", you have the option of following the terms and\r\nconditions either of that version or of any later version published by\r\nthe Free Software Foundation.  If the Library does not specify a\r\nlicense version number, you may choose any version ever published by\r\nthe Free Software Foundation.\r\n\f\r\n  14. If you wish to incorporate parts of the Library into other free\r\nprograms whose distribution conditions are incompatible with these,\r\nwrite to the author to ask for permission.  For software which is\r\ncopyrighted by the Free Software Foundation, write to the Free\r\nSoftware Foundation; we sometimes make exceptions for this.  Our\r\ndecision will be guided by the two goals of preserving the free status\r\nof all derivatives of our free software and of promoting the sharing\r\nand reuse of software generally.\r\n\r\n\t\t\t    NO WARRANTY\r\n\r\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\r\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\r\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\r\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\r\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\r\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\r\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\r\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\r\n\r\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\r\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\r\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\r\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\r\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\r\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\r\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\r\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\r\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\r\nDAMAGES.\r\n\r\n\t\t     END OF TERMS AND CONDITIONS\r\n\f\r\n           How to Apply These Terms to Your New Libraries\r\n\r\n  If you develop a new library, and you want it to be of the greatest\r\npossible use to the public, we recommend making it free software that\r\neveryone can redistribute and change.  You can do so by permitting\r\nredistribution under these terms (or, alternatively, under the terms of the\r\nordinary General Public License).\r\n\r\n  To apply these terms, attach the following notices to the library.  It is\r\nsafest to attach them to the start of each source file to most effectively\r\nconvey the exclusion of warranty; and each file should have at least the\r\n\"copyright\" line and a pointer to where the full notice is found.\r\n\r\n    <one line to give the library's name and a brief idea of what it does.>\r\n    Copyright (C) <year>  <name of author>\r\n\r\n    This library is free software; you can redistribute it and/or\r\n    modify it under the terms of the GNU Lesser General Public\r\n    License as published by the Free Software Foundation; either\r\n    version 2.1 of the License, or (at your option) any later version.\r\n\r\n    This library is distributed in the hope that it will be useful,\r\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r\n    Lesser General Public License for more details.\r\n\r\n    You should have received a copy of the GNU Lesser General Public\r\n    License along with this library; if not, write to the Free Software\r\n    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r\n\r\nAlso add information on how to contact you by electronic and paper mail.\r\n\r\nYou should also get your employer (if you work as a programmer) or your\r\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\r\nnecessary.  Here is a sample; alter the names:\r\n\r\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\r\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\r\n\r\n  <signature of Ty Coon>, 1 April 1990\r\n  Ty Coon, President of Vice\r\n\r\nThat's all there is to it!\r\n\r\n\r\n"
  },
  {
    "path": "OSM2World/lib/lp_solve/README.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\r\n<html>\r\n<head>\r\n<title>Using lp_solve 5.5 in Java programs</title>\r\n<style type=\"text/css\">\r\n<!--\r\nbody  { font-family: Verdana, Arial, Helvetica, sans-serif; }\r\ntd,th\t{ text-align:left; }\r\nli\t\t{ margin-top: 3px; }\r\npre\t\t{ font-size: smaller; }\r\n-->\r\n</style>\r\n\r\n</head>\r\n\r\n<body>\r\n\r\n<h1>Using lp_solve 5.5 in Java programs</h1>\r\n\r\n<h2>Contents</h2>\r\n\r\n<a href=\"#intro\">1. Introduction</a><br/>\r\n<a href=\"#install\">2. Installation</a><br/>\r\n<a href=\"#usage\">3. Usage</a><br/>\r\n<a href=\"#impl\">4. Implementation notes</a><br/>\r\n<a href=\"#building\">5. Building from source</a><br/>\r\n<a href=\"#jython\">6. Calling lp_solve from Python/Jython</a><br/>\r\n\r\n\r\n<!--\r\n * *******************************************************************\r\n * PART 1\r\n * *******************************************************************\r\n-->\r\n<h2><a name=\"intro\">1. Introduction</a></h2>\r\n\r\n<p>lp_solve is a free (see <a href=\"LGPL\">LGPL</a> for the GNU lesser general public license) linear\r\n(integer) programming solver based on the revised simplex method and the Branch-and-bound method\r\nfor the integers. lp_solve has its own community via the Yahoo group\r\n<a href=\"http://groups.yahoo.com/group/lp_solve\">http://groups.yahoo.com/group/lp_solve</a>.\r\nThere you can find the latest sources, executables for the common platforms, examples, manuals\r\nand a message board where people can share their thoughts on lp_solve.</p>\r\n\r\n<p>lp_solve is written in ANSI C and can be compiled on many different platforms like Linux and Windows.\r\nBasically, lp_solve is a library, a set of routines, that can be called\r\neasily from programming languages like C, C++, C# and VB. Unfortunately, there is no simple and\r\nstraightforward way to use native C libraries like lp_solve in Java programs.\r\nThis library (also called <strong>&quot;Java wrapper&quot;</strong>) is designed to remedy this shortcoming. It\r\nconsists of two main parts:</p>\r\n\r\n<ul>\r\n<li>A Java class library that is used by Java client programs. It gives access to all\r\n  lp_solve routines through the <code>LpSolve</code> class.</li>\r\n\r\n<li> A native library written in C++, also called 'stub' library, that uses the JNI\r\n  (Java Native Interface) API to translate Java method calls into calls to the\r\n  corresponding routines of the lp_solve library. Java client\r\n  programs do not interact directly with the stub library.\r\n  This library must be compiled for each target platform.\r\n  Precompiled binaries are included for Windows and Linux operating systems.\r\n  There is also a build script for compiling the stub library on Mac OS X.</li>\r\n</ul>\r\n\r\n<p>This document should help you getting started using the Java wrapper and lp_solve in your\r\nJava programs. Read it in addition to the documentation that comes with lp_solve.\r\nAlways refer to the lp_solve docs as ultimate reference for\r\nusing the routines of the optimization library.\r\nBug reports, succes stories and requests for changes concerning the Java wrapper are welcome\r\nby email at <code>juergen.ebert@web.de</code> or in the lp_solve discussion group.</p>\r\n\r\n<p>The current wrapper version was written to work with lp_solve 5.5.0.9 and was tested\r\n under Windows XP and Linux.  As long as the API stays the same, other versions of lp_solve\r\n are likely to work as well. The wrapper requires a Java Runtime Environment 1.3 or later.</p>\r\n\r\n<p>The latest version of the Java wrapper can be found in the files section of the lp_solve group.\r\nThe wrapper is released under the same LGPL license conditions as lp_solve. A copy of\r\nthe LGPL text is contained in the distribution archive. </p>\r\n\r\n\r\n<!--\r\n * *******************************************************************\r\n * PART 2\r\n * *******************************************************************\r\n-->\r\n<h2><a name=\"install\">2. Installation</a></h2>\r\n\r\n<ul>\r\n<li>Copy the lp_solve dynamic libraries from the archives <code>lp_solve_5.5_dev.(zip or tar.gz)</code>\r\n  and <code>lp_solve_5.5_exe.(zip or tar.gz)</code> to a standard library directory for your target platform.\r\n  On Windows, a typical place would be <code>\\WINDOWS</code> or <code>\\WINDOWS\\SYSTEM32</code>.\r\n  On Linux, a typical place would be the directory <code>/usr/local/lib</code>.\r\n</li>\r\n\r\n<li>Unzip the Java wrapper distribution file to new directory of your choice.</li>\r\n\r\n<li>On Windows, copy the wrapper stub library <code>lpsolve55j.dll</code>\r\n  to the directory that already contains <code>lpsolve55.dll</code>.\r\n</li>\r\n\r\n<li>On Linux, copy the wrapper stub library  <code>liblpsolve55j.so</code>\r\n  to the directory that already contains <code>liblpsolve55.so</code>. Run <code>ldconfig</code> to include\r\n  the library in the shared libray cache.\r\n</li>\r\n</ul>\r\n\r\n\r\n\r\n<!--\r\n * *******************************************************************\r\n * PART 3\r\n * *******************************************************************\r\n-->\r\n<h2><a name=\"usage\">3. Usage</a></h2>\r\n\r\n<p>To create a Java application that uses lp_solve routines, you must perform the\r\nfollowing steps:</p>\r\n\r\n<ul>\r\n<li>Make sure you have a Java Runtime Environment 1.3 or later installed.</li>\r\n\r\n<li>Install lp_solve and the Java wrapper as described above.</li>\r\n\r\n<li>Copy the archive file <code>lpsolve55j.jar</code> from the Java wrapper distribution to a\r\n  directory that is included in the CLASSPATH of your java program.</li>\r\n\r\n<li>Add an import statement for the package <code>lpsolve.*</code> at the beginning of your\r\n  source file.</li>\r\n\r\n<li>Call <code>LpSolve.makeLp(...)</code> or one of the other static factory methods of the LpSolve\r\n  class to create a <code>LpSolve</code> instance. Each <code>LpSolve</code> instance represents an optimization\r\n  problem.</li>\r\n\r\n<li>Call the methods of the <code>LpSolve</code> instance to define the problem and obtain the solution.\r\n  Use the examples and implementation notes later in this documentation for further\r\n  information.</li>\r\n\r\n<li>When you run your Java file make sure to include <code>lpsolve55j.jar</code> in the CLASSPATH.\r\n  Also, on Windows, if you installed the  native stub library in a directory that is not included\r\n  in the PATH variable, you have to define the Java system variable <code>java.library.path</code>\r\n  which must point to the installation directory. On Linux, the equivalent of the Windows PATH\r\n  variable is called LD_LIBRARY_PATH.</li>\r\n</ul>\r\n\r\n\r\n<h3><a name=\"ex1\">A simple example</a></h3>\r\n\r\n<p>The following program is a very simple example that shows how to program with lp_solve in Java.\r\n</p>\r\n\r\n<pre>\r\nimport lpsolve.*;\r\n\r\npublic class Demo {\r\n\r\n  public static void main(String[] args) {\r\n    try {\r\n      // Create a problem with 4 variables and 0 constraints\r\n      LpSolve solver = LpSolve.makeLp(0, 4);\r\n\r\n      // add constraints\r\n      solver.strAddConstraint(\"3 2 2 1\", LpSolve.LE, 4);\r\n      solver.strAddConstraint(\"0 4 3 1\", LpSolve.GE, 3);\r\n\r\n      // set objective function\r\n      solver.strSetObjFn(\"2 3 -2 3\");\r\n\r\n      // solve the problem\r\n      solver.solve();\r\n\r\n      // print solution\r\n      System.out.println(\"Value of objective function: \" + solver.getObjective());\r\n      double[] var = solver.getPtrVariables();\r\n      for (int i = 0; i < var.length; i++) {\r\n        System.out.println(\"Value of var[\" + i + \"] = \" + var[i]);\r\n      }\r\n\r\n      // delete the problem and free memory\r\n      solver.deleteLp();\r\n    }\r\n    catch (LpSolveException e) {\r\n       e.printStackTrace();\r\n    }\r\n  }\r\n\r\n}\r\n</pre>\r\n\r\n<h3><a name=\"ex2\">Using callbacks</a></h3>\r\n\r\n<p>The following code fragment shows you how to use callbacks in Java.\r\nThe example defines an anonymous inner class that implements the <code>AbortListener</code>\r\ninterface which is then passed to the <code>putAbortfunc</code> method.</p>\r\n\r\n<pre>\r\n    LpSolve solver = LpSolve.makeLp(0, 4);\r\n    AbortListener abortfunc = new AbortListener() {\r\n      public boolean abortfunc(LpSolve problem, Object handle) {\r\n        System.out.println(\"Java abortfunc called, handle = \" + handle);\r\n        return false;\r\n      }\r\n    };\r\n    solver.putAbortfunc(abortfunc, new Integer(123));\r\n</pre>\r\n\r\n\r\n<h3><a name=\"ex3\">Running the demo application</a></h3>\r\n\r\n<p>Follow these steps to run the demo application, which is a port of the C demo\r\nprogram that comes with lp_solve to the Java language. You will need a Java Runtime\r\nEnvironment (JRE) on your machine in order to run the demo. You can download the\r\nlatest JRE from <a href=\"http://java.sun.com\">http://java.sun.com</a></p>\r\n\r\n<ul>\r\n<li>Install lp_solve and the Java wrapper as described above.</li>\r\n<li>On Windows, go to the <code>demo</code> directory and\r\nstart the batch script &quot;run_demo.bat&quot;.</li>\r\n<li>On Linux, go to the <code>demo</code> directory of the wrapper distribution and\r\nrun &quot;sh run_demo&quot;.</li>\r\n</ul>\r\n\r\n<h3><a name=\"ex4\">More example code</a></h3>\r\n\r\n<p>In the <code>demo</code> directory you will find\r\nthe file <code>LpSolveTest.java</code> which contains more than 100\r\nJUnit test cases (see <a href=\"http://www.junit.org\">http://www.junit.org</a> for\r\ndetails about this highly useful software)\r\nto strengthen the faith in the Java wrapper implementation. The test cases\r\nmay also seve as examples of basic lp_solve usage in Java. You will need\r\nthe library <code>junit.jar</code> in your <code>CLASSPATH</code> to run the test cases.\r\n<code>junit.jar</code> is included in the <code>lib</code> directory of the Java wrapper. You\r\ncan run the test cases directly by starting the batch script &quot;run_unittests.bat&quot; on Windows\r\nor &quot;sh run_unittests&quot; on Linux.\r\n</p>\r\n\r\n<!--\r\n * *******************************************************************\r\n * PART 4\r\n * *******************************************************************\r\n-->\r\n<h2><a name=\"impl\">4. Implementation notes</a></h2>\r\n\r\n<ul>\r\n<li>In general, the Java API tries to follow the original C/C++ API of lp_solve as\r\n  closely as possible to make ist easier for programmers who have to write programs for\r\n  lp_solve in multiple different programming languages. However, because of fundamental\r\n  differences between the Java programming language and C/C++ this is not always strictly\r\n  possible. For example, it is not possible in Java to pass simple datatypes by reference.</li>\r\n\r\n<li>Method names have been kept, but changed to Java convention, i.e. underscores have been\r\n  removed, the following character is capitalized. For example <code>str_add_constraint</code>\r\n  becomes <code>strAddConstraint</code> in Java.</li>\r\n\r\n<li>The <code>lprec*</code> argument taken by almost all lp_solve API routines is hidden\r\n  completely inside the <code>LpSolve</code> class. All methods that create new <code>lprec\r\n  </code> structures were made static methods of the <code>LpSolve</code> class.</li>\r\n\r\n<li>Return values denoting fatal internal errors have been changed to Java Exceptions. For\r\n  example <code>set_row_name</code> returns FALSE if an error has occured. In Java, <code>\r\n  setRowName</code> is of type void and throws a <code>LpSolveException</code>.</li>\r\n\r\n<li>Routines that have arguments or return values of type unsigned char, but only allow for TRUE or\r\n  FALSE as legal values have been changed to type <code>boolean</code>. Example: <code>\r\n  set_debug(lprec *lp, unsigned char debug)</code> is <code>setDebug(boolean debug)</code> in Java.\r\n  </li>\r\n\r\n<li>Multiple problems may be solved concurrently by multiple threads, as long as a single\r\n  <code>LpSolve</code> object, which represents a problem, is only used by one thread at a\r\n  time.</li>\r\n\r\n<li>lp_solve does not require client programs to keep argument buffers allocated between calls\r\n  to different library routines, because all input arguments are copied to internal buffers.\r\n  Thus, it is easy to avoid memory leaks in the stub library. All objects returned by the\r\n  Java wrapper routines are allocated by the JVM and are subject to the standard garbage\r\n  collection process.</li>\r\n\r\n<li>The methods <code>get_ptr_sensitivity_rhs</code>, <code>get_ptr_reduced_costs</code>,\r\n  <code>get_ptr_sensitivity_obj</code>, and <code>get_ptr_sensitivity_objex</code> are not implemented,\r\n  because it is not possible in Java to pass pointers by reference to a method. Use the corresponding\r\n  methods without the <code>Ptr</code> part in the method name instead, which require allocation\r\n  of the resulting arrays by the caller.</li>\r\n\r\n<li>See the file <code>reference.html</code> for details on how the lp_solve API functions\r\nare mapped to the Java methods.</li>\r\n</ul>\r\n\r\n\r\n<!--\r\n * *******************************************************************\r\n * PART 5\r\n * *******************************************************************\r\n-->\r\n<h2><a name=\"building\">5. Building from source</a></h2>\r\n\r\n<p>The Java wrapper archive contains precompiled binary libraries for Windows and Linux.\r\nIf you just want to use the wrapper there should be no need to build the libs from the sources.\r\nBut if you absolutely have to, follow the guidelines in this chapter.</p>\r\n\r\n<h3><a name=\"build_win\">On Windows OS</a></h3>\r\n\r\nThe following prerequisites must be met in order to build the wrapper C library from source\r\non Windows operating systems:\r\n\r\n<ul>\r\n<li>Microsoft Visual C++ compiler (I used V 7, others might work)</li>\r\n<li>Visual Studio envirement variables must be set.</li>\r\n<li>Sun Java Development Kit 1.4.x installed and JAVA_HOME environment variable set</li>\r\n<li>lp_solve Windows development archive <code>lp_solve_5.5_dev.zip</code> unpacked</li>\r\n</ul>\r\n\r\nChange to the <code>lib</code> directory and edit the file <code>build.bat</code>.\r\nChange the path to the directory where you unpacked the lp_solve Windows\r\narchive. Run the script to build <code>lpsolve55j.dll</code>.\r\nThe dll will be created in directory win32 or win64 depending on the OS.\r\n\r\n\r\n<h3><a name=\"build_linux\">On Linux</a></h3>\r\n\r\nThe following prerequisites must be met in order to build the wrapper C library from source\r\non Linux operating systems:\r\n\r\n<ul>\r\n<li>gcc and g++ compiler installed (I used gcc Version 3.3.1)</li>\r\n<li>Sun Java Development Kit 1.4.x installed</li>\r\n<li>lp_solve Linux development archive <code>lp_solve_5.5_dev.tar.gz</code> unpacked</li>\r\n</ul>\r\n\r\nChange to the <code>lib</code> directory and edit the file <code>build</code>.\r\nChange the paths to the directory where you unpacked the lp_solve linux archive\r\nand where the JDK is installed.\r\nRun <code>sh build</code> to build <code>liblpsolve55j.so</code>.\r\nThe dll will be created in directory ux32 or ux64 depending on the OS.\r\n\r\n\r\n<h3><a name=\"build_mac\">On Mac OS X</a></h3>\r\n\r\n<p>Change to the <code>lib/mac</code> directory and edit the file <code>build_osx</code>.\r\nChange the directory paths as indicated in the comments.\r\nThanks to Sean P. Kane (spkane@genomatica.com) who provided this build script.</p>\r\n\r\n<!--\r\n * *******************************************************************\r\n * PART 6\r\n * *******************************************************************\r\n-->\r\n<h2><a name=\"jython\">6. Calling lp_solve from Python/Jython</a></h2>\r\n\r\n<p>\r\nJython (<a href=\"http://www.jython.org\">http://www.jython.org</a>) is a 100% Java implementation of\r\nthe popular scripting language Python.\r\nOne of the most remarkable features of Jython is the seamless interaction between Python and Java.\r\nJava programmers can add the Jython libraries to their system to allow end users to write scripts that add\r\nfunctionality to the application. On the other hand, Python/Jython programs can interact with Java packages\r\nor with running Java applications.\r\n</p>\r\n\r\n<p>lp_solve functions can be called via the Java wrapper from Python/Jython programs.\r\nSee the file <code>demo.py</code> in the <code>demo</code> directory of the Java wrapper distribution\r\nfor an example program.\r\nTo run this program, you must install lp_solve, the Java wrapper, and Jython. Don't forget\r\nto include <code>lpsolve55j.jar</code> in the Java CLASSPATH when you run Jython.\r\n</p>\r\n\r\n\r\n</body>\r\n</html>\r\n"
  },
  {
    "path": "OSM2World/lib/osmosis/copying.txt",
    "content": "Osmosis consists of all files in this archive with the exception of the\nthird party libraries in the lib and repo subdirectories.\n\nOsmosis is placed into the public domain and where this is not legally\npossible everybody is granted a perpetual, irrevocable license to use\nthis work for any purpose whatsoever.\n\nDISCLAIMERS\nBy making Osmosis publicly available, it is hoped that users will find the\nsoftware useful.  However:\n* Osmosis comes without any warranty, to the extent permitted by applicable\nlaw.\n* Unless required by applicable law, no liability will be accepted by\nthe authors and distributors of this software for any damages caused\nas a result of its use. \n"
  },
  {
    "path": "OSM2World/lib/poly2tri/LICENSE.txt",
    "content": "Poly2Tri\nCopyright (c) 2009-2010, Poly2Tri Contributors\nhttp://code.google.com/p/poly2tri/\n\nAll rights reserved.\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice,\n  this list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n* Neither the name of Poly2Tri nor the names of its contributors may be\n  used to endorse or promote products derived from this software without specific\n  prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
  },
  {
    "path": "OSM2World/lib/slf4j/LICENSE.txt",
    "content": "Copyright (c) 2004-2008 QOS.ch\r\nAll rights reserved.\r\n\r\nPermission is hereby granted, free  of charge, to any person obtaining\r\na  copy  of this  software  and  associated  documentation files  (the\r\n\"Software\"), to  deal in  the Software without  restriction, including\r\nwithout limitation  the rights to  use, copy, modify,  merge, publish,\r\ndistribute,  sublicense, and/or sell  copies of  the Software,  and to\r\npermit persons to whom the Software  is furnished to do so, subject to\r\nthe following conditions:\r\n\r\nThe  above  copyright  notice  and  this permission  notice  shall  be\r\nincluded in all copies or substantial portions of the Software.\r\n\r\nTHE  SOFTWARE IS  PROVIDED  \"AS  IS\", WITHOUT  WARRANTY  OF ANY  KIND,\r\nEXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF\r\nMERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND\r\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\r\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\r\nOF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION\r\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r\n\r\n\r\n\r\n"
  },
  {
    "path": "OSM2World/lib/trove/AUTHORS.txt",
    "content": "Rob Eden <robeden@users.sourceforge.net>\r\nJohan Parent <jparent@users.sourceforge.net>\r\nJeff Randall <randall@uph.com>\r\nEric D. Friedman <ericdf@users.sourceforge.net>\r\n\r\nPlease do NOT email bug reports or feature requests.\r\n\r\nInstead use the very fine bug tracking system and feature request\r\nservice on SourceForge:  http://sourceforge.net/projects/trove4j/\r\nWe'll read your issue just as quickly, and the project's issues will\r\nremain out in the open where everyone can see them.  We also monitor\r\nthe project forums, so feel free to use those too.\r\n"
  },
  {
    "path": "OSM2World/lib/trove/LICENSE.txt",
    "content": "          GNU LESSER GENERAL PUBLIC LICENSE\r\n               Version 2.1, February 1999\r\n\r\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\r\n     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r\n Everyone is permitted to copy and distribute verbatim copies\r\n of this license document, but changing it is not allowed.\r\n\r\n[This is the first released version of the Lesser GPL.  It also counts\r\n as the successor of the GNU Library Public License, version 2, hence\r\n the version number 2.1.]\r\n\r\n                Preamble\r\n\r\n  The licenses for most software are designed to take away your\r\nfreedom to share and change it.  By contrast, the GNU General Public\r\nLicenses are intended to guarantee your freedom to share and change\r\nfree software--to make sure the software is free for all its users.\r\n\r\n  This license, the Lesser General Public License, applies to some\r\nspecially designated software packages--typically libraries--of the\r\nFree Software Foundation and other authors who decide to use it.  You\r\ncan use it too, but we suggest you first think carefully about whether\r\nthis license or the ordinary General Public License is the better\r\nstrategy to use in any particular case, based on the explanations below.\r\n\r\n  When we speak of free software, we are referring to freedom of use,\r\nnot price.  Our General Public Licenses are designed to make sure that\r\nyou have the freedom to distribute copies of free software (and charge\r\nfor this service if you wish); that you receive source code or can get\r\nit if you want it; that you can change the software and use pieces of\r\nit in new free programs; and that you are informed that you can do\r\nthese things.\r\n\r\n  To protect your rights, we need to make restrictions that forbid\r\ndistributors to deny you these rights or to ask you to surrender these\r\nrights.  These restrictions translate to certain responsibilities for\r\nyou if you distribute copies of the library or if you modify it.\r\n\r\n  For example, if you distribute copies of the library, whether gratis\r\nor for a fee, you must give the recipients all the rights that we gave\r\nyou.  You must make sure that they, too, receive or can get the source\r\ncode.  If you link other code with the library, you must provide\r\ncomplete object files to the recipients, so that they can relink them\r\nwith the library after making changes to the library and recompiling\r\nit.  And you must show them these terms so they know their rights.\r\n\r\n  We protect your rights with a two-step method: (1) we copyright the\r\nlibrary, and (2) we offer you this license, which gives you legal\r\npermission to copy, distribute and/or modify the library.\r\n\r\n  To protect each distributor, we want to make it very clear that\r\nthere is no warranty for the free library.  Also, if the library is\r\nmodified by someone else and passed on, the recipients should know\r\nthat what they have is not the original version, so that the original\r\nauthor's reputation will not be affected by problems that might be\r\nintroduced by others.\r\n\f\r\n  Finally, software patents pose a constant threat to the existence of\r\nany free program.  We wish to make sure that a company cannot\r\neffectively restrict the users of a free program by obtaining a\r\nrestrictive license from a patent holder.  Therefore, we insist that\r\nany patent license obtained for a version of the library must be\r\nconsistent with the full freedom of use specified in this license.\r\n\r\n  Most GNU software, including some libraries, is covered by the\r\nordinary GNU General Public License.  This license, the GNU Lesser\r\nGeneral Public License, applies to certain designated libraries, and\r\nis quite different from the ordinary General Public License.  We use\r\nthis license for certain libraries in order to permit linking those\r\nlibraries into non-free programs.\r\n\r\n  When a program is linked with a library, whether statically or using\r\na shared library, the combination of the two is legally speaking a\r\ncombined work, a derivative of the original library.  The ordinary\r\nGeneral Public License therefore permits such linking only if the\r\nentire combination fits its criteria of freedom.  The Lesser General\r\nPublic License permits more lax criteria for linking other code with\r\nthe library.\r\n\r\n  We call this license the \"Lesser\" General Public License because it\r\ndoes Less to protect the user's freedom than the ordinary General\r\nPublic License.  It also provides other free software developers Less\r\nof an advantage over competing non-free programs.  These disadvantages\r\nare the reason we use the ordinary General Public License for many\r\nlibraries.  However, the Lesser license provides advantages in certain\r\nspecial circumstances.\r\n\r\n  For example, on rare occasions, there may be a special need to\r\nencourage the widest possible use of a certain library, so that it becomes\r\na de-facto standard.  To achieve this, non-free programs must be\r\nallowed to use the library.  A more frequent case is that a free\r\nlibrary does the same job as widely used non-free libraries.  In this\r\ncase, there is little to gain by limiting the free library to free\r\nsoftware only, so we use the Lesser General Public License.\r\n\r\n  In other cases, permission to use a particular library in non-free\r\nprograms enables a greater number of people to use a large body of\r\nfree software.  For example, permission to use the GNU C Library in\r\nnon-free programs enables many more people to use the whole GNU\r\noperating system, as well as its variant, the GNU/Linux operating\r\nsystem.\r\n\r\n  Although the Lesser General Public License is Less protective of the\r\nusers' freedom, it does ensure that the user of a program that is\r\nlinked with the Library has the freedom and the wherewithal to run\r\nthat program using a modified version of the Library.\r\n\r\n  The precise terms and conditions for copying, distribution and\r\nmodification follow.  Pay close attention to the difference between a\r\n\"work based on the library\" and a \"work that uses the library\".  The\r\nformer contains code derived from the library, whereas the latter must\r\nbe combined with the library in order to run.\r\n\f\r\n          GNU LESSER GENERAL PUBLIC LICENSE\r\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\r\n\r\n  0. This License Agreement applies to any software library or other\r\nprogram which contains a notice placed by the copyright holder or\r\nother authorized party saying it may be distributed under the terms of\r\nthis Lesser General Public License (also called \"this License\").\r\nEach licensee is addressed as \"you\".\r\n\r\n  A \"library\" means a collection of software functions and/or data\r\nprepared so as to be conveniently linked with application programs\r\n(which use some of those functions and data) to form executables.\r\n\r\n  The \"Library\", below, refers to any such software library or work\r\nwhich has been distributed under these terms.  A \"work based on the\r\nLibrary\" means either the Library or any derivative work under\r\ncopyright law: that is to say, a work containing the Library or a\r\nportion of it, either verbatim or with modifications and/or translated\r\nstraightforwardly into another language.  (Hereinafter, translation is\r\nincluded without limitation in the term \"modification\".)\r\n\r\n  \"Source code\" for a work means the preferred form of the work for\r\nmaking modifications to it.  For a library, complete source code means\r\nall the source code for all modules it contains, plus any associated\r\ninterface definition files, plus the scripts used to control compilation\r\nand installation of the library.\r\n\r\n  Activities other than copying, distribution and modification are not\r\ncovered by this License; they are outside its scope.  The act of\r\nrunning a program using the Library is not restricted, and output from\r\nsuch a program is covered only if its contents constitute a work based\r\non the Library (independent of the use of the Library in a tool for\r\nwriting it).  Whether that is true depends on what the Library does\r\nand what the program that uses the Library does.\r\n  \r\n  1. You may copy and distribute verbatim copies of the Library's\r\ncomplete source code as you receive it, in any medium, provided that\r\nyou conspicuously and appropriately publish on each copy an\r\nappropriate copyright notice and disclaimer of warranty; keep intact\r\nall the notices that refer to this License and to the absence of any\r\nwarranty; and distribute a copy of this License along with the\r\nLibrary.\r\n\r\n  You may charge a fee for the physical act of transferring a copy,\r\nand you may at your option offer warranty protection in exchange for a\r\nfee.\r\n\f\r\n  2. You may modify your copy or copies of the Library or any portion\r\nof it, thus forming a work based on the Library, and copy and\r\ndistribute such modifications or work under the terms of Section 1\r\nabove, provided that you also meet all of these conditions:\r\n\r\n    a) The modified work must itself be a software library.\r\n\r\n    b) You must cause the files modified to carry prominent notices\r\n    stating that you changed the files and the date of any change.\r\n\r\n    c) You must cause the whole of the work to be licensed at no\r\n    charge to all third parties under the terms of this License.\r\n\r\n    d) If a facility in the modified Library refers to a function or a\r\n    table of data to be supplied by an application program that uses\r\n    the facility, other than as an argument passed when the facility\r\n    is invoked, then you must make a good faith effort to ensure that,\r\n    in the event an application does not supply such function or\r\n    table, the facility still operates, and performs whatever part of\r\n    its purpose remains meaningful.\r\n\r\n    (For example, a function in a library to compute square roots has\r\n    a purpose that is entirely well-defined independent of the\r\n    application.  Therefore, Subsection 2d requires that any\r\n    application-supplied function or table used by this function must\r\n    be optional: if the application does not supply it, the square\r\n    root function must still compute square roots.)\r\n\r\nThese requirements apply to the modified work as a whole.  If\r\nidentifiable sections of that work are not derived from the Library,\r\nand can be reasonably considered independent and separate works in\r\nthemselves, then this License, and its terms, do not apply to those\r\nsections when you distribute them as separate works.  But when you\r\ndistribute the same sections as part of a whole which is a work based\r\non the Library, the distribution of the whole must be on the terms of\r\nthis License, whose permissions for other licensees extend to the\r\nentire whole, and thus to each and every part regardless of who wrote\r\nit.\r\n\r\nThus, it is not the intent of this section to claim rights or contest\r\nyour rights to work written entirely by you; rather, the intent is to\r\nexercise the right to control the distribution of derivative or\r\ncollective works based on the Library.\r\n\r\nIn addition, mere aggregation of another work not based on the Library\r\nwith the Library (or with a work based on the Library) on a volume of\r\na storage or distribution medium does not bring the other work under\r\nthe scope of this License.\r\n\r\n  3. You may opt to apply the terms of the ordinary GNU General Public\r\nLicense instead of this License to a given copy of the Library.  To do\r\nthis, you must alter all the notices that refer to this License, so\r\nthat they refer to the ordinary GNU General Public License, version 2,\r\ninstead of to this License.  (If a newer version than version 2 of the\r\nordinary GNU General Public License has appeared, then you can specify\r\nthat version instead if you wish.)  Do not make any other change in\r\nthese notices.\r\n\f\r\n  Once this change is made in a given copy, it is irreversible for\r\nthat copy, so the ordinary GNU General Public License applies to all\r\nsubsequent copies and derivative works made from that copy.\r\n\r\n  This option is useful when you wish to copy part of the code of\r\nthe Library into a program that is not a library.\r\n\r\n  4. You may copy and distribute the Library (or a portion or\r\nderivative of it, under Section 2) in object code or executable form\r\nunder the terms of Sections 1 and 2 above provided that you accompany\r\nit with the complete corresponding machine-readable source code, which\r\nmust be distributed under the terms of Sections 1 and 2 above on a\r\nmedium customarily used for software interchange.\r\n\r\n  If distribution of object code is made by offering access to copy\r\nfrom a designated place, then offering equivalent access to copy the\r\nsource code from the same place satisfies the requirement to\r\ndistribute the source code, even though third parties are not\r\ncompelled to copy the source along with the object code.\r\n\r\n  5. A program that contains no derivative of any portion of the\r\nLibrary, but is designed to work with the Library by being compiled or\r\nlinked with it, is called a \"work that uses the Library\".  Such a\r\nwork, in isolation, is not a derivative work of the Library, and\r\ntherefore falls outside the scope of this License.\r\n\r\n  However, linking a \"work that uses the Library\" with the Library\r\ncreates an executable that is a derivative of the Library (because it\r\ncontains portions of the Library), rather than a \"work that uses the\r\nlibrary\".  The executable is therefore covered by this License.\r\nSection 6 states terms for distribution of such executables.\r\n\r\n  When a \"work that uses the Library\" uses material from a header file\r\nthat is part of the Library, the object code for the work may be a\r\nderivative work of the Library even though the source code is not.\r\nWhether this is true is especially significant if the work can be\r\nlinked without the Library, or if the work is itself a library.  The\r\nthreshold for this to be true is not precisely defined by law.\r\n\r\n  If such an object file uses only numerical parameters, data\r\nstructure layouts and accessors, and small macros and small inline\r\nfunctions (ten lines or less in length), then the use of the object\r\nfile is unrestricted, regardless of whether it is legally a derivative\r\nwork.  (Executables containing this object code plus portions of the\r\nLibrary will still fall under Section 6.)\r\n\r\n  Otherwise, if the work is a derivative of the Library, you may\r\ndistribute the object code for the work under the terms of Section 6.\r\nAny executables containing that work also fall under Section 6,\r\nwhether or not they are linked directly with the Library itself.\r\n\f\r\n  6. As an exception to the Sections above, you may also combine or\r\nlink a \"work that uses the Library\" with the Library to produce a\r\nwork containing portions of the Library, and distribute that work\r\nunder terms of your choice, provided that the terms permit\r\nmodification of the work for the customer's own use and reverse\r\nengineering for debugging such modifications.\r\n\r\n  You must give prominent notice with each copy of the work that the\r\nLibrary is used in it and that the Library and its use are covered by\r\nthis License.  You must supply a copy of this License.  If the work\r\nduring execution displays copyright notices, you must include the\r\ncopyright notice for the Library among them, as well as a reference\r\ndirecting the user to the copy of this License.  Also, you must do one\r\nof these things:\r\n\r\n    a) Accompany the work with the complete corresponding\r\n    machine-readable source code for the Library including whatever\r\n    changes were used in the work (which must be distributed under\r\n    Sections 1 and 2 above); and, if the work is an executable linked\r\n    with the Library, with the complete machine-readable \"work that\r\n    uses the Library\", as object code and/or source code, so that the\r\n    user can modify the Library and then relink to produce a modified\r\n    executable containing the modified Library.  (It is understood\r\n    that the user who changes the contents of definitions files in the\r\n    Library will not necessarily be able to recompile the application\r\n    to use the modified definitions.)\r\n\r\n    b) Use a suitable shared library mechanism for linking with the\r\n    Library.  A suitable mechanism is one that (1) uses at run time a\r\n    copy of the library already present on the user's computer system,\r\n    rather than copying library functions into the executable, and (2)\r\n    will operate properly with a modified version of the library, if\r\n    the user installs one, as long as the modified version is\r\n    interface-compatible with the version that the work was made with.\r\n\r\n    c) Accompany the work with a written offer, valid for at\r\n    least three years, to give the same user the materials\r\n    specified in Subsection 6a, above, for a charge no more\r\n    than the cost of performing this distribution.\r\n\r\n    d) If distribution of the work is made by offering access to copy\r\n    from a designated place, offer equivalent access to copy the above\r\n    specified materials from the same place.\r\n\r\n    e) Verify that the user has already received a copy of these\r\n    materials or that you have already sent this user a copy.\r\n\r\n  For an executable, the required form of the \"work that uses the\r\nLibrary\" must include any data and utility programs needed for\r\nreproducing the executable from it.  However, as a special exception,\r\nthe materials to be distributed need not include anything that is\r\nnormally distributed (in either source or binary form) with the major\r\ncomponents (compiler, kernel, and so on) of the operating system on\r\nwhich the executable runs, unless that component itself accompanies\r\nthe executable.\r\n\r\n  It may happen that this requirement contradicts the license\r\nrestrictions of other proprietary libraries that do not normally\r\naccompany the operating system.  Such a contradiction means you cannot\r\nuse both them and the Library together in an executable that you\r\ndistribute.\r\n\f\r\n  7. You may place library facilities that are a work based on the\r\nLibrary side-by-side in a single library together with other library\r\nfacilities not covered by this License, and distribute such a combined\r\nlibrary, provided that the separate distribution of the work based on\r\nthe Library and of the other library facilities is otherwise\r\npermitted, and provided that you do these two things:\r\n\r\n    a) Accompany the combined library with a copy of the same work\r\n    based on the Library, uncombined with any other library\r\n    facilities.  This must be distributed under the terms of the\r\n    Sections above.\r\n\r\n    b) Give prominent notice with the combined library of the fact\r\n    that part of it is a work based on the Library, and explaining\r\n    where to find the accompanying uncombined form of the same work.\r\n\r\n  8. You may not copy, modify, sublicense, link with, or distribute\r\nthe Library except as expressly provided under this License.  Any\r\nattempt otherwise to copy, modify, sublicense, link with, or\r\ndistribute the Library is void, and will automatically terminate your\r\nrights under this License.  However, parties who have received copies,\r\nor rights, from you under this License will not have their licenses\r\nterminated so long as such parties remain in full compliance.\r\n\r\n  9. You are not required to accept this License, since you have not\r\nsigned it.  However, nothing else grants you permission to modify or\r\ndistribute the Library or its derivative works.  These actions are\r\nprohibited by law if you do not accept this License.  Therefore, by\r\nmodifying or distributing the Library (or any work based on the\r\nLibrary), you indicate your acceptance of this License to do so, and\r\nall its terms and conditions for copying, distributing or modifying\r\nthe Library or works based on it.\r\n\r\n  10. Each time you redistribute the Library (or any work based on the\r\nLibrary), the recipient automatically receives a license from the\r\noriginal licensor to copy, distribute, link with or modify the Library\r\nsubject to these terms and conditions.  You may not impose any further\r\nrestrictions on the recipients' exercise of the rights granted herein.\r\nYou are not responsible for enforcing compliance by third parties with\r\nthis License.\r\n\f\r\n  11. If, as a consequence of a court judgment or allegation of patent\r\ninfringement or for any other reason (not limited to patent issues),\r\nconditions are imposed on you (whether by court order, agreement or\r\notherwise) that contradict the conditions of this License, they do not\r\nexcuse you from the conditions of this License.  If you cannot\r\ndistribute so as to satisfy simultaneously your obligations under this\r\nLicense and any other pertinent obligations, then as a consequence you\r\nmay not distribute the Library at all.  For example, if a patent\r\nlicense would not permit royalty-free redistribution of the Library by\r\nall those who receive copies directly or indirectly through you, then\r\nthe only way you could satisfy both it and this License would be to\r\nrefrain entirely from distribution of the Library.\r\n\r\nIf any portion of this section is held invalid or unenforceable under any\r\nparticular circumstance, the balance of the section is intended to apply,\r\nand the section as a whole is intended to apply in other circumstances.\r\n\r\nIt is not the purpose of this section to induce you to infringe any\r\npatents or other property right claims or to contest validity of any\r\nsuch claims; this section has the sole purpose of protecting the\r\nintegrity of the free software distribution system which is\r\nimplemented by public license practices.  Many people have made\r\ngenerous contributions to the wide range of software distributed\r\nthrough that system in reliance on consistent application of that\r\nsystem; it is up to the author/donor to decide if he or she is willing\r\nto distribute software through any other system and a licensee cannot\r\nimpose that choice.\r\n\r\nThis section is intended to make thoroughly clear what is believed to\r\nbe a consequence of the rest of this License.\r\n\r\n  12. If the distribution and/or use of the Library is restricted in\r\ncertain countries either by patents or by copyrighted interfaces, the\r\noriginal copyright holder who places the Library under this License may add\r\nan explicit geographical distribution limitation excluding those countries,\r\nso that distribution is permitted only in or among countries not thus\r\nexcluded.  In such case, this License incorporates the limitation as if\r\nwritten in the body of this License.\r\n\r\n  13. The Free Software Foundation may publish revised and/or new\r\nversions of the Lesser General Public License from time to time.\r\nSuch new versions will be similar in spirit to the present version,\r\nbut may differ in detail to address new problems or concerns.\r\n\r\nEach version is given a distinguishing version number.  If the Library\r\nspecifies a version number of this License which applies to it and\r\n\"any later version\", you have the option of following the terms and\r\nconditions either of that version or of any later version published by\r\nthe Free Software Foundation.  If the Library does not specify a\r\nlicense version number, you may choose any version ever published by\r\nthe Free Software Foundation.\r\n\f\r\n  14. If you wish to incorporate parts of the Library into other free\r\nprograms whose distribution conditions are incompatible with these,\r\nwrite to the author to ask for permission.  For software which is\r\ncopyrighted by the Free Software Foundation, write to the Free\r\nSoftware Foundation; we sometimes make exceptions for this.  Our\r\ndecision will be guided by the two goals of preserving the free status\r\nof all derivatives of our free software and of promoting the sharing\r\nand reuse of software generally.\r\n\r\n                NO WARRANTY\r\n\r\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\r\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\r\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\r\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\r\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\r\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\r\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\r\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\r\n\r\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\r\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\r\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\r\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\r\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\r\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\r\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\r\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\r\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\r\nDAMAGES.\r\n\r\n             END OF TERMS AND CONDITIONS\r\n\f\r\n           How to Apply These Terms to Your New Libraries\r\n\r\n  If you develop a new library, and you want it to be of the greatest\r\npossible use to the public, we recommend making it free software that\r\neveryone can redistribute and change.  You can do so by permitting\r\nredistribution under these terms (or, alternatively, under the terms of the\r\nordinary General Public License).\r\n\r\n  To apply these terms, attach the following notices to the library.  It is\r\nsafest to attach them to the start of each source file to most effectively\r\nconvey the exclusion of warranty; and each file should have at least the\r\n\"copyright\" line and a pointer to where the full notice is found.\r\n\r\n    <one line to give the library's name and a brief idea of what it does.>\r\n    Copyright (C) <year>  <name of author>\r\n\r\n    This library is free software; you can redistribute it and/or\r\n    modify it under the terms of the GNU Lesser General Public\r\n    License as published by the Free Software Foundation; either\r\n    version 2.1 of the License, or (at your option) any later version.\r\n\r\n    This library is distributed in the hope that it will be useful,\r\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r\n    Lesser General Public License for more details.\r\n\r\n    You should have received a copy of the GNU Lesser General Public\r\n    License along with this library; if not, write to the Free Software\r\n    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r\n\r\nAlso add information on how to contact you by electronic and paper mail.\r\n\r\nYou should also get your employer (if you work as a programmer) or your\r\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\r\nnecessary.  Here is a sample; alter the names:\r\n\r\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\r\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\r\n\r\n  <signature of Ty Coon>, 1 April 1990\r\n  Ty Coon, President of Vice\r\n\r\nThat's all there is to it!\r\n\r\n\r\n"
  },
  {
    "path": "OSM2World/lib/trove/README-license.txt",
    "content": "The Trove library is licensed under the Lesser GNU Public License,\r\nwhich is included with the distribution in a file called LICENSE.txt.\r\n\r\nOther license arrangements are possible, for a fee: contact \r\nericdf@users.sourceforge.net for terms/pricing.\r\n\r\nThe PrimeFinder and HashFunctions classes in Trove are subject to the\r\nfollowing license restrictions:\r\n\r\nCopyright (c) 1999 CERN - European Organization for Nuclear Research.\r\n\r\nPermission to use, copy, modify, distribute and sell this software and\r\nits documentation for any purpose is hereby granted without fee,\r\nprovided that the above copyright notice appear in all copies and that\r\nboth that copyright notice and this permission notice appear in\r\nsupporting documentation. CERN makes no representations about the\r\nsuitability of this software for any purpose. It is provided \"as is\"\r\nwithout expressed or implied warranty.\r\n\r\n\r\n"
  },
  {
    "path": "OSM2World/lib/trove/README.txt",
    "content": "\r\n   GNU Trove: High performance collections for Java.\r\n\r\nObjectives\r\n\r\n   The GNU Trove library has two objectives:\r\n    1. Provide \"free\" (as in \"free speech\" and \"free beer\"), fast,\r\n       lightweight implementations of the java.util Collections API.\r\n       These implementations are designed to be pluggable replacements\r\n       for their JDK equivalents.\r\n    2. Whenever possible, provide the same collections support for\r\n       primitive types. This gap in the JDK is often addressed by using\r\n       the \"wrapper\" classes (java.lang.Integer, java.lang.Float, etc.)\r\n       with Object-based collections. For most applications, however,\r\n       collections which store primitives directly will require less\r\n       space and yield significant performance gains.\r\n\r\nHashtable techniques\r\n\r\n   The Trove maps/sets use open addressing instead of the chaining\r\n   approach taken by the JDK hashtables. This eliminates the need to\r\n   create Map.Entry wrappper objects for every item in a table and so\r\n   reduces the O (big-oh) in the performance of the hashtable algorithm.\r\n   The size of the tables used in Trove's maps/sets is always a prime\r\n   number, improving the probability of an optimal distribution of\r\n   entries across the table, and so reducing the likelihood of\r\n   performance-degrading collisions. Trove sets are not backed by maps,\r\n   and so using a THashSet does not result in the allocation of an unused\r\n   \"values\" array.\r\n\r\nHashing strategies\r\n\r\n   Trove's maps/sets support the use of custom hashing strategies,\r\n   allowing you to tune collections based on characteristics of the input\r\n   data. This feature also allows you to define hash functions when it is\r\n   not feasible to override Object.hashCode(). For example, the\r\n   java.lang.String class is final, and its implementation of hashCode()\r\n   takes O(n) time to complete. In some applications, however, it may be\r\n   possible for a custom hashing function to save time by skipping\r\n   portions of the string that are invariant.\r\n\r\n   Using java.util.HashMap, it is not possible to use Java language\r\n   arrays as keys. For example, this code:\r\n    char[] foo, bar;\r\n    foo = new char[] {'a','b','c'};\r\n    bar = new char[] {'a','b','c'};\r\n    System.out.println(foo.hashCode() == bar.hashCode() ?\r\n      \"equal\" : \"not equal\");\r\n    System.out.println(foo.equals(bar) ? \"equal\" : \"not equal\");\r\n    \r\n\r\n   produces this output:\r\n    not equal\r\n    not equal\r\n    \r\n\r\n   And so an entry stored in a java.util.HashMap with foo as a key could\r\n   not be retrieved with bar, since there is no way to override\r\n   hashCode() or equals() on language array objects.\r\n\r\n   In a gnu.trove.map.hash.TCustomHashMap, however, you can implement a\r\n   gnu.trove.strategy.HashingStrategy to enable hashing on arrays:\r\n    class CharArrayStrategy implements HashingStrategy {\r\n        public int computeHashCode(Object o) {\r\n            char[] c = (char[])o;\r\n            // use the shift-add-xor class of string hashing functions\r\n            // cf. Ramakrishna and Zobel,\r\n            //     \"Performance in Practice of String Hashing Functions\"\r\n            int h = 31; // seed chosen at random\r\n            for (int i = 0; i < c.length; i++) { // could skip invariants\r\n                // L=5, R=2 works well for ASCII input\r\n                h = h ^ ((h << 5) + (h >> 2) + c[i]);\r\n            }\r\n            return h;\r\n        }\r\n\r\n        public boolean equals(Object o1, Object o2) {\r\n            char[] c1 = (char[])o1;\r\n            char[] c2 = (char[])o2;\r\n            // could drop this check for fixed-length keys\r\n            if (c1.length != c2.length) {\r\n                return false;\r\n            }\r\n            // could skip invariants\r\n            for (int i = 0, len = c1.length; i < len; i++) {\r\n                if (c1[i] != c2[i]) {\r\n                    return false;\r\n                }\r\n            }\r\n            return true;\r\n        }\r\n    }\r\n    \r\n\r\nIterators in primitive collections\r\n\r\n   Trove's primitive mappings include access through Iterators as well\r\n   as procedures and functions. The API documentation on those classes\r\n   contains several examples showing how these can be used effectively\r\n   and explaining why their semantics differ from those of\r\n   java.util.Iterator.\r\n\r\n_________________________________________________________________\r\n\r\n   Last modified: Sep 9, 2011\r\n"
  },
  {
    "path": "OSM2World/osm2world-rel20160312-to-bdb0c396e899ee9.patch",
    "content": "diff --git a/OSM2World/.classpath b/OSM2World/.classpath\nnew file mode 100644\nindex 0000000..6a3f3c9\n--- /dev/null\n+++ b/OSM2World/.classpath\n@@ -0,0 +1,79 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n+<classpath>\n+\t<classpathentry kind=\"src\" path=\"src\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/commons/commons-math3-3.0.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/configuration/commons-collections-3.2.1.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/configuration/commons-configuration-1.6.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/configuration/commons-lang-2.6.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/configuration/commons-logging-1.1.1.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/guava/guava-r08.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/javailp/javailp-1.2a.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/javaproj/javaproj-1.0.6.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jewelcli/jewelcli-0.6.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-linux-amd64.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-linux-i586.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-macosx-universal.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-windows-amd64.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt-natives-windows-i586.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/gluegen-rt.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-linux-amd64.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-linux-i586.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-macosx-universal.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-windows-amd64.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all-natives-windows-i586.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jogl/jogl-all.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/jts/jts-1.11.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/lp_solve/lpsolve55j.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/commons-compress-1.2.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/osmosis-core-0.41.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/osmosis-pbf-0.41.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/osmosis-xml-0.41.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/osmpbf-1.1.1-754a33af.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/protobuf-java-2.4.1.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/osmosis/xercesImpl-2.9.1.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/pngj/pngj-2.0.0.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/poly2tri/poly2tri-core-0.1.1-SNAPSHOT.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/slf4j/slf4j-api-1.7.5.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/slf4j/slf4j-simple-1.7.5.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/trove/trove-111118193120.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/lib/trove/trove-3.0.1-src.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"build/OSM2World.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/commons/commons-math3-3.0.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/configuration/commons-collections-3.2.1.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/configuration/commons-configuration-1.6.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/configuration/commons-lang-2.6.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/configuration/commons-logging-1.1.1.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/guava/guava-r08.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/javailp/javailp-1.2a.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/javaproj/javaproj-1.0.6.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jewelcli/jewelcli-0.6.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-linux-amd64.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-linux-i586.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-macosx-universal.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-windows-amd64.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt-natives-windows-i586.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/gluegen-rt.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-linux-amd64.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-linux-i586.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-macosx-universal.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-windows-amd64.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all-natives-windows-i586.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jogl/jogl-all.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/jts/jts-1.11.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/lp_solve/lpsolve55j.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/osmosis/commons-compress-1.2.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/osmosis/osmosis-core-0.41.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/osmosis/osmosis-pbf-0.41.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/osmosis/osmosis-xml-0.41.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/osmosis/osmpbf-1.1.1-754a33af.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/osmosis/protobuf-java-2.4.1.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/osmosis/xercesImpl-2.9.1.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/pngj/pngj-2.0.0.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/poly2tri/poly2tri-core-0.1.1-SNAPSHOT.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/slf4j/slf4j-api-1.7.5.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/slf4j/slf4j-simple-1.7.5.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/trove/trove-111118193120.jar\"/>\n+\t<classpathentry kind=\"lib\" path=\"lib/trove/trove-3.0.1-src.jar\"/>\n+\t<classpathentry kind=\"con\" path=\"org.eclipse.jdt.launching.JRE_CONTAINER\"/>\n+\t<classpathentry kind=\"output\" path=\"eclipse-bin\"/>\n+</classpath>\ndiff --git a/OSM2World/.gitignore b/OSM2World/.gitignore\nnew file mode 100644\nindex 0000000..6cc54c9\n--- /dev/null\n+++ b/OSM2World/.gitignore\n@@ -0,0 +1,4 @@\n+/bin\n+/build\n+\n+/eclipse-bin/\ndiff --git a/OSM2World/.project b/OSM2World/.project\nnew file mode 100644\nindex 0000000..d66d464\n--- /dev/null\n+++ b/OSM2World/.project\n@@ -0,0 +1,28 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n+<projectDescription>\n+\t<name>Touch Mapper OSM2World</name>\n+\t<comment></comment>\n+\t<projects>\n+\t</projects>\n+\t<buildSpec>\n+\t\t<buildCommand>\n+\t\t\t<name>org.eclipse.jdt.core.javabuilder</name>\n+\t\t\t<arguments>\n+\t\t\t</arguments>\n+\t\t</buildCommand>\n+\t</buildSpec>\n+\t<natures>\n+\t\t<nature>org.eclipse.jdt.core.javanature</nature>\n+\t</natures>\n+\t<filteredResources>\n+\t\t<filter>\n+\t\t\t<id>1477832440066</id>\n+\t\t\t<name></name>\n+\t\t\t<type>22</type>\n+\t\t\t<matcher>\n+\t\t\t\t<id>org.eclipse.ui.ide.multiFilter</id>\n+\t\t\t\t<arguments>1.0-name-matches-false-false-*.class</arguments>\n+\t\t\t</matcher>\n+\t\t</filter>\n+\t</filteredResources>\n+</projectDescription>\ndiff --git a/OSM2World/build.xml b/OSM2World/build.xml\nindex 9459e90..cdcc9aa 100644\n--- a/OSM2World/build.xml\n+++ b/OSM2World/build.xml\n@@ -31,12 +31,12 @@\n \n     <target name=\"compile\">\n \t<mkdir dir=\"bin\"/>\n-        <javac optimize=\"true\" encoding=\"UTF-8\" srcdir=\"src\" destdir=\"bin\" classpath=\"${classpath}\" includeantruntime=\"false\"/>\n+        <javac target=\"1.7\" source=\"1.7\" optimize=\"true\" encoding=\"UTF-8\" srcdir=\"src\" destdir=\"bin\" classpath=\"${classpath}\" includeantruntime=\"false\"/>\n     </target>\n \n     <target name=\"compiledbg\">\n \t<mkdir dir=\"bin\"/>\n-        <javac debug=\"true\" encoding=\"UTF-8\" srcdir=\"src\" destdir=\"bin\" classpath=\"${classpath}\" includeantruntime=\"false\"/>\n+        <javac target=\"1.7\" source=\"1.7\" debug=\"true\" encoding=\"UTF-8\" srcdir=\"src\" destdir=\"bin\" classpath=\"${classpath}\" includeantruntime=\"false\"/>\n     </target>\n \t\n     <target name=\"jar\" depends=\"compile\">\ndiff --git a/OSM2World/src/org/osm2world/core/ConversionFacade.java b/OSM2World/src/org/osm2world/core/ConversionFacade.java\nindex e71693c..39cba67 100644\n--- a/OSM2World/src/org/osm2world/core/ConversionFacade.java\n+++ b/OSM2World/src/org/osm2world/core/ConversionFacade.java\n@@ -28,6 +28,7 @@ import org.osm2world.core.map_elevation.creation.TerrainInterpolator;\n import org.osm2world.core.map_elevation.creation.ZeroInterpolator;\n import org.osm2world.core.map_elevation.data.EleConnector;\n import org.osm2world.core.math.VectorXYZ;\n+import org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\n import org.osm2world.core.osm.creation.JOSMFileHack;\n import org.osm2world.core.osm.creation.OsmosisReader;\n import org.osm2world.core.osm.data.OSMData;\n@@ -133,20 +134,20 @@ public class ConversionFacade {\n \t\t\t\tnew RoadModule(),\n \t\t\t\tnew RailwayModule(),\n \t\t\t\tnew BuildingModule(),\n-\t\t\t\tnew ParkingModule(),\n-\t\t\t\tnew TreeModule(),\n-\t\t\t\tnew StreetFurnitureModule(),\n-\t\t\t\tnew TrafficSignModule(),\n-\t\t\t\tnew WaterModule(),\n-\t\t\t\tnew PoolModule(),\n-\t\t\t\tnew GolfModule(),\n-\t\t\t\tnew CliffModule(),\n-\t\t\t\tnew BarrierModule(),\n-\t\t\t\tnew PowerModule(),\n-\t\t\t\tnew BridgeModule(),\n-\t\t\t\tnew TunnelModule(),\n-\t\t\t\tnew SurfaceAreaModule(),\n-\t\t\t\tnew InvisibleModule()\n+\t\t\t\t//new ParkingModule(),\n+\t\t\t\t//new TreeModule(),\n+\t\t\t\t//new StreetFurnitureModule(),\n+\t\t\t\t//new TrafficSignModule(),\n+\t\t\t\tnew WaterModule()\n+\t\t\t\t//new PoolModule(),\n+\t\t\t\t//new GolfModule(),\n+\t\t\t\t//new CliffModule(),\n+\t\t\t\t//new BarrierModule(),\n+\t\t\t\t//new PowerModule(),\n+\t\t\t\t//new BridgeModule(),\n+\t\t\t\t//new TunnelModule(),\n+\t\t\t\t//new SurfaceAreaModule(),\n+\t\t\t\t//new InvisibleModule()\n \t\t);\n \t\t\n \t}\n@@ -303,7 +304,7 @@ public class ConversionFacade {\n \t\t\t\t}\n \t\t\t}\n \t\t}\n-\t\t\n+\n \t\t/* create map data from OSM data */\n \t\tupdatePhase(Phase.MAP_DATA);\n \t\t\n@@ -313,6 +314,14 @@ public class ConversionFacade {\n \t\tOSMToMapDataConverter converter = new OSMToMapDataConverter(mapProjection, config);\n \t\tMapData mapData = converter.createMapData(osmData);\n \t\t\n+                AxisAlignedBoundingBoxXZ boundary = mapData.getBoundary();\n+                System.out.println(\"Map-boundary:[\"\n+                    + \" minX=\" + boundary.minX\n+                    + \" minZ=\" + boundary.minZ\n+                    + \" maxX=\" + boundary.maxX\n+                    + \" maxZ=\" + boundary.maxZ\n+                    + \" ]\");\n+\t\t\n \t\t/* apply world modules */\n \t\tupdatePhase(Phase.REPRESENTATION);\n \t\t\ndiff --git a/OSM2World/src/org/osm2world/core/target/obj/ObjTarget.java b/OSM2World/src/org/osm2world/core/target/obj/ObjTarget.java\nindex 6be2959..17b2b1f 100644\n--- a/OSM2World/src/org/osm2world/core/target/obj/ObjTarget.java\n+++ b/OSM2World/src/org/osm2world/core/target/obj/ObjTarget.java\n@@ -13,10 +13,12 @@ import java.util.HashMap;\n import java.util.List;\n import java.util.Map;\n \n+import org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\n import org.osm2world.core.map_data.data.MapArea;\n import org.osm2world.core.map_data.data.MapElement;\n import org.osm2world.core.map_data.data.MapNode;\n import org.osm2world.core.map_data.data.MapWaySegment;\n+import org.osm2world.core.map_elevation.data.EleConnector;\n import org.osm2world.core.math.TriangleXYZ;\n import org.osm2world.core.math.TriangleXYZWithNormals;\n import org.osm2world.core.math.VectorXYZ;\n@@ -91,10 +93,21 @@ public class ObjTarget extends FaceTarget<RenderableToObj> {\n \t\t\t/* start an object with the object's class\n \t\t\t * and the underlying OSM element's name/ref tags */\n \t\t\t\n+\t\t\tString roadSuffix = null;\n+\t\t\t\n \t\t\tMapElement element = object.getPrimaryMapElement();\n \t\t\tOSMElement osmElement;\n \t\t\tif (element instanceof MapNode) {\n \t\t\t\tosmElement = ((MapNode) element).getOsmNode();\n+\t\t\t\t\n+\t\t\t\tList<MapWaySegment> connectedWaySegments = ((MapNode) element).getConnectedWaySegments();\n+\t\t\t\tint pedestrians = 0;\n+\t\t\t\tfor (MapWaySegment mapWaySegment : connectedWaySegments) {\n+\t\t\t\t\tpedestrians += isPath(mapWaySegment.getTags()) ? 1 : 0;\n+\t\t\t\t}\n+\t\t\t\tif (pedestrians >= (connectedWaySegments.size()+1) / 2) {\n+\t\t\t\t\troadSuffix = \"::pedestrian\";\n+\t\t\t\t}\n \t\t\t} else if (element instanceof MapWaySegment) {\n \t\t\t\tosmElement = ((MapWaySegment) element).getOsmWay();\n \t\t\t} else if (element instanceof MapArea) {\n@@ -103,18 +116,50 @@ public class ObjTarget extends FaceTarget<RenderableToObj> {\n \t\t\t\tosmElement = null;\n \t\t\t}\n \t\t\t\n+\t\t\tif (roadSuffix == null) {\n+\t\t\t\troadSuffix = isPath(osmElement.tags) ? \"::pedestrian\" : \"\";\n+\t\t\t}\n+\t\t\t\n \t\t\tif (osmElement != null && osmElement.tags.containsKey(\"name\")) {\n-\t\t\t\tobjStream.println(\"o \" + object.getClass().getSimpleName() + \" \" + osmElement.tags.getValue(\"name\"));\n+\t\t\t\tobjStream.println(\"o \" + object.getClass().getSimpleName() + \" \" + osmElement.tags.getValue(\"name\") + roadSuffix);\n \t\t\t} else if (osmElement != null && osmElement.tags.containsKey(\"ref\")) {\n-\t\t\t\tobjStream.println(\"o \" + object.getClass().getSimpleName() + \" \" + osmElement.tags.getValue(\"ref\"));\n+\t\t\t\tobjStream.println(\"o \" + object.getClass().getSimpleName() + \" \" + osmElement.tags.getValue(\"ref\") + roadSuffix);\n \t\t\t} else {\n-\t\t\t\tobjStream.println(\"o \" + object.getClass().getSimpleName() + anonymousWOCounter ++);\n+\t\t\t\tobjStream.println(\"o \" + object.getClass().getSimpleName() + anonymousWOCounter ++ + roadSuffix);\n \t\t\t}\n \t\t\t\n \t\t}\n \t\t\n \t}\n \t\n+\tprivate static boolean isPath(TagGroup tags) {\n+\t\tString highwayValue = tags.getValue(\"highway\");\n+\t\tif (\"path\".equals(highwayValue)\n+\t\t\t|| \"footway\".equals(highwayValue)\n+\t\t\t|| \"cycleway\".equals(highwayValue)\n+\t\t\t|| \"service\".equals(highwayValue)\n+\t\t\t|| \"bridleway\".equals(highwayValue)\n+\t\t\t|| \"living_street\".equals(highwayValue)\n+\t\t\t|| \"pedestrian\".equals(highwayValue)\n+\t\t\t|| \"track\".equals(highwayValue)\n+\t\t\t|| \"steps\".equals(highwayValue)) {\n+\t\t\treturn true;\n+\t\t}\n+\t\tif (tags.containsKey(\"footway\")\n+\t\t\t\t|| tags.contains(\"tourism\", \"attraction\")\n+\t\t\t\t|| tags.contains(\"man_made\", \"pier\")\n+\t\t\t\t|| tags.contains(\"man_made\", \"breakwater\")) {\n+\t\t\treturn true;\n+\t\t}\n+\t\tString footValue = tags.getValue(\"foot\");\n+\t\tif (\"yes\".equals(footValue)\n+\t\t\t|| \"designated\".equals(footValue)) {\n+\t\t\treturn true;\n+\t\t}\n+\t\treturn false;\n+\t}\n+\n+\t\n \t@Override\n \tpublic void drawFace(Material material, List<VectorXYZ> vs,\n \t\t\tList<VectorXYZ> normals, List<List<VectorXZ>> texCoordLists) {\ndiff --git a/OSM2World/src/org/osm2world/core/world/modules/BuildingModule.java b/OSM2World/src/org/osm2world/core/world/modules/BuildingModule.java\nindex df24aa2..b9542b6 100644\n--- a/OSM2World/src/org/osm2world/core/world/modules/BuildingModule.java\n+++ b/OSM2World/src/org/osm2world/core/world/modules/BuildingModule.java\n@@ -76,8 +76,8 @@ public class BuildingModule extends ConfigurableWorldModule {\n \t@Override\n \tpublic void applyTo(MapData mapData) {\n \t\t\n-\t\tboolean useBuildingColors = config.getBoolean(\"useBuildingColors\", true);\n-\t\tboolean drawBuildingWindows = config.getBoolean(\"drawBuildingWindows\", true);\n+\t\tboolean useBuildingColors = config.getBoolean(\"useBuildingColors\", false);\n+\t\tboolean drawBuildingWindows = config.getBoolean(\"drawBuildingWindows\", false);\n \t\t\n \t\tfor (MapArea area : mapData.getMapAreas()) {\n \t\t\t\n@@ -111,23 +111,23 @@ public class BuildingModule extends ConfigurableWorldModule {\n \t\t\t\n \t\t\tthis.area = area;\n \t\t\t\n-\t\t\tfor (MapOverlap<?,?> overlap : area.getOverlaps()) {\n-\t\t\t\tMapElement other = overlap.getOther(area);\n-\t\t\t\tif (other instanceof MapArea\n-\t\t\t\t\t\t&& other.getTags().containsKey(\"building:part\")) {\n-\t\t\t\t\t\n-\t\t\t\t\tMapArea otherArea = (MapArea)other;\n-\t\t\t\t\t\n-\t\t\t\t\t//TODO: check whether the building contains the part (instead of just touching it)\n-\t\t\t\t\tif (area.getPolygon().contains(\n-\t\t\t\t\t\t\totherArea.getPolygon().getOuter())) {\n-\t\t\t\t\t\tparts.add(new BuildingPart(this, otherArea,\n-\t\t\t\t\t\t\totherArea.getPolygon(), useBuildingColors,\n-\t\t\t\t\t\t\tdrawBuildingWindows));\n-\t\t\t\t\t}\n-\t\t\t\t\t\n-\t\t\t\t}\n-\t\t\t}\n+//\t\t\tfor (MapOverlap<?,?> overlap : area.getOverlaps()) {\n+//\t\t\t\tMapElement other = overlap.getOther(area);\n+//\t\t\t\tif (other instanceof MapArea\n+//\t\t\t\t\t\t&& other.getTags().containsKey(\"building:part\")) {\n+//\t\t\t\t\t\n+//\t\t\t\t\tMapArea otherArea = (MapArea)other;\n+//\t\t\t\t\t\n+//\t\t\t\t\t//TODO: check whether the building contains the part (instead of just touching it)\n+//\t\t\t\t\tif (area.getPolygon().contains(\n+//\t\t\t\t\t\t\totherArea.getPolygon().getOuter())) {\n+//\t\t\t\t\t\tparts.add(new BuildingPart(this, otherArea,\n+//\t\t\t\t\t\t\totherArea.getPolygon(), useBuildingColors,\n+//\t\t\t\t\t\t\tdrawBuildingWindows));\n+//\t\t\t\t\t}\n+//\t\t\t\t\t\n+//\t\t\t\t}\n+//\t\t\t}\n \t\t\t\n \t\t\t/* add part(s) for area not covered by building:part polygons */\n \t\t\tboolean isBuildingPart = false;\n@@ -138,23 +138,23 @@ public class BuildingModule extends ConfigurableWorldModule {\n \t\t\t\tparts.add(new BuildingPart(this, area,\n \t\t\t\t\t\tarea.getPolygon(), useBuildingColors, drawBuildingWindows));\n \t\t\t} else {\n-\t\t\t\tList<SimplePolygonXZ> subtractPolygons = new ArrayList<SimplePolygonXZ>();\n-\t\t\t\t\n-\t\t\t\tfor (BuildingPart part : parts) {\n-\t\t\t\t\tsubtractPolygons.add(part.getPolygon().getOuter());\n-\t\t\t\t}\n-\t\t\t\tsubtractPolygons.addAll(area.getPolygon().getHoles());\n-\t\t\t\t\n-\t\t\t\tCollection<PolygonWithHolesXZ> remainingPolys =\n-\t\t\t\t\tCAGUtil.subtractPolygons(\n-\t\t\t\t\t\t\tarea.getPolygon().getOuter(),\n-\t\t\t\t\t\t\tsubtractPolygons);\n-\t\t\t\t\n-\t\t\t\tfor (PolygonWithHolesXZ remainingPoly : remainingPolys) {\n-\t\t\t\t\tparts.add(new BuildingPart(this, area, remainingPoly,\n-\t\t\t\t\t\t\tuseBuildingColors, drawBuildingWindows));\n-\t\t\t\t}\n-\t\t\t\t\n+//\t\t\t\tList<SimplePolygonXZ> subtractPolygons = new ArrayList<SimplePolygonXZ>();\n+//\t\t\t\t\n+//\t\t\t\tfor (BuildingPart part : parts) {\n+//\t\t\t\t\tsubtractPolygons.add(part.getPolygon().getOuter());\n+//\t\t\t\t}\n+//\t\t\t\tsubtractPolygons.addAll(area.getPolygon().getHoles());\n+//\t\t\t\t\n+//\t\t\t\tCollection<PolygonWithHolesXZ> remainingPolys =\n+//\t\t\t\t\tCAGUtil.subtractPolygons(\n+//\t\t\t\t\t\t\tarea.getPolygon().getOuter(),\n+//\t\t\t\t\t\t\tsubtractPolygons);\n+//\t\t\t\t\n+//\t\t\t\tfor (PolygonWithHolesXZ remainingPoly : remainingPolys) {\n+//\t\t\t\t\tparts.add(new BuildingPart(this, area, remainingPoly,\n+//\t\t\t\t\t\t\tuseBuildingColors, drawBuildingWindows));\n+//\t\t\t\t}\n+//\t\t\t\t\n \t\t\t}\n \n \t\t\t/* create connectors along the outline.\n@@ -319,19 +319,19 @@ public class BuildingModule extends ConfigurableWorldModule {\n \t\t\tthis.area = area;\n \t\t\tthis.polygon = polygon;\n \n-\t\t\tsetAttributes(useBuildingColors, drawBuildingWindows);\n-\t\t\t\n-\t\t\tfor (MapNode node : area.getBoundaryNodes()) {\n-\t\t\t\tif ((node.getTags().contains(\"building\", \"entrance\")\n-\t\t\t\t\t\t|| node.getTags().containsKey(\"entrance\"))\n-\t\t\t\t\t\t&& node.getRepresentations().isEmpty()) {\n-\t\t\t\t\t\n-\t\t\t\t\tBuildingEntrance entrance = new BuildingEntrance(this, node);\n-\t\t\t\t\tentrances.add(entrance);\n-\t\t\t\t\tnode.addRepresentation(entrance);\n-\t\t\t\t\t\n-\t\t\t\t}\n-\t\t\t}\n+\t\t\tsetAttributes(false, false);\n+\t\t\t\n+//\t\t\tfor (MapNode node : area.getBoundaryNodes()) {\n+//\t\t\t\tif ((node.getTags().contains(\"building\", \"entrance\")\n+//\t\t\t\t\t\t|| node.getTags().containsKey(\"entrance\"))\n+//\t\t\t\t\t\t&& node.getRepresentations().isEmpty()) {\n+//\t\t\t\t\t\n+//\t\t\t\t\tBuildingEntrance entrance = new BuildingEntrance(this, node);\n+//\t\t\t\t\tentrances.add(entrance);\n+//\t\t\t\t\tnode.addRepresentation(entrance);\n+//\t\t\t\t\t\n+//\t\t\t\t}\n+//\t\t\t}\n \t\t\t\n \t\t}\n \t\t\n@@ -349,10 +349,11 @@ public class BuildingModule extends ConfigurableWorldModule {\n \t\t\n \t\t@Override\n \t\tpublic void renderTo(Target<?> target) {\n+\t\t\trenderFloor(target, 0);\n \t\t\t\n-\t\t\trenderWalls(target, roof);\n+//\t\t\trenderWalls(target, roof);\n \t\t\t\n-\t\t\troof.renderTo(target);\n+//\t\t\troof.renderTo(target);\n \t\t\t\n \t\t}\n \n@@ -380,8 +381,9 @@ public class BuildingModule extends ConfigurableWorldModule {\n \t\t\tdouble floorHeight = calculateFloorHeight(roof);\n \t\t\tboolean renderFloor = (floorHeight > 0);\n \t\t\t\n-\t\t\tif (area.getOverlaps().isEmpty()) {\n-\t\t\t\t\n+\t\t\tif (true || area.getOverlaps().isEmpty()) {\n+\t\t\t\t// Always draw buildings whole. Otherwise there may be\n+\t\t\t\t// eg. ground level tunnels through them, which would screw things up.\n \t\t\t\trenderWalls(target, roof.getPolygon(), false,\n \t\t\t\t\t\tbaseEle, floorHeight, roof);\n \t\t\t\t\n@@ -732,64 +734,68 @@ public class BuildingModule extends ConfigurableWorldModule {\n \t\t\t\n \t\t\t/* determine roof shape */\n \t\t\t\n-\t\t\tboolean explicitRoofTagging = true;\n-\t\t\t\n-\t\t\tif (!(\"no\".equals(area.getTags().getValue(\"roof:lines\"))) && hasComplexRoof(area)) {\n-\t\t\t\troof = new ComplexRoof();\n-\t\t\t} else {\n-\t\t\t\t\n-\t\t\t\tString roofShape = getValue(\"roof:shape\");\n-\t\t\t\tif (roofShape == null) { roofShape = getValue(\"building:roof:shape\"); }\n-\t\t\t\t\n-\t\t\t\tif (roofShape == null) {\n-\t\t\t\t\troofShape = defaultRoofShape;\n-\t\t\t\t\texplicitRoofTagging = false;\n-\t\t\t\t}\n-\t\t\t\t\n-\t\t\t\ttry {\n-\t\t\t\t\t\n-\t\t\t\t\tif (\"pyramidal\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new PyramidalRoof();\n-\t\t\t\t\t} else if (\"onion\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new OnionRoof();\n-\t\t\t\t\t} else if (\"skillion\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new SkillionRoof();\n-\t\t\t\t\t} else if (\"gabled\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new GabledRoof();\n-\t\t\t\t\t} else if (\"hipped\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new HippedRoof();\n-\t\t\t\t\t} else if (\"half-hipped\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new HalfHippedRoof();\n-\t\t\t\t\t} else if (\"gambrel\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new GambrelRoof();\n-\t\t\t\t\t} else if (\"mansard\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new MansardRoof();\n-\t\t\t\t\t} else if (\"dome\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new DomeRoof();\n-\t\t\t\t\t} else if (\"round\".equals(roofShape)) {\n-\t\t\t\t\t\troof = new RoundRoof();\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\troof = new FlatRoof();\n-\t\t\t\t\t}\n-\t\t\t\t\t\n-\t\t\t\t} catch (InvalidGeometryException e) {\n-\t\t\t\t\tSystem.err.println(\"falling back to FlatRoof: \" + e);\n-\t\t\t\t\troof = new FlatRoof();\n-\t\t\t\t\texplicitRoofTagging = false;\n-\t\t\t\t}\n-\t\t\t\t\n-\t\t\t}\n-\t\t\t\n+//\t\t\tboolean explicitRoofTagging = true;\n+//\t\t\t\n+//\t\t\tif (!(\"no\".equals(area.getTags().getValue(\"roof:lines\"))) && hasComplexRoof(area)) {\n+//\t\t\t\troof = new ComplexRoof();\n+//\t\t\t} else {\n+//\t\t\t\t\n+//\t\t\t\tString roofShape = getValue(\"roof:shape\");\n+//\t\t\t\tif (roofShape == null) { roofShape = getValue(\"building:roof:shape\"); }\n+//\t\t\t\t\n+//\t\t\t\tif (roofShape == null) {\n+//\t\t\t\t\troofShape = defaultRoofShape;\n+//\t\t\t\t\texplicitRoofTagging = false;\n+//\t\t\t\t}\n+//\t\t\t\t\n+//\t\t\t\ttry {\n+//\t\t\t\t\t\n+//\t\t\t\t\tif (\"pyramidal\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new PyramidalRoof();\n+//\t\t\t\t\t} else if (\"onion\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new OnionRoof();\n+//\t\t\t\t\t} else if (\"skillion\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new SkillionRoof();\n+//\t\t\t\t\t} else if (\"gabled\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new GabledRoof();\n+//\t\t\t\t\t} else if (\"hipped\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new HippedRoof();\n+//\t\t\t\t\t} else if (\"half-hipped\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new HalfHippedRoof();\n+//\t\t\t\t\t} else if (\"gambrel\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new GambrelRoof();\n+//\t\t\t\t\t} else if (\"mansard\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new MansardRoof();\n+//\t\t\t\t\t} else if (\"dome\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new DomeRoof();\n+//\t\t\t\t\t} else if (\"round\".equals(roofShape)) {\n+//\t\t\t\t\t\troof = new RoundRoof();\n+//\t\t\t\t\t} else {\n+//\t\t\t\t\t\troof = new FlatRoof();\n+//\t\t\t\t\t}\n+//\t\t\t\t\t\n+//\t\t\t\t} catch (InvalidGeometryException e) {\n+//\t\t\t\t\tSystem.err.println(\"falling back to FlatRoof: \" + e);\n+//\t\t\t\t\troof = new FlatRoof();\n+//\t\t\t\t\texplicitRoofTagging = false;\n+//\t\t\t\t}\n+//\t\t\t\t\n+//\t\t\t}\n+\n+\t\t\troof = new FlatRoof();\n+\t\t\tboolean explicitRoofTagging = false;\n+\n \t\t\t/* determine height */\n \t\t\t\n-\t\t\tdouble fallbackHeight = buildingLevels * defaultHeightPerLevel;\n-\t\t\t\n-\t\t\tfallbackHeight += roof.getRoofHeight();\n-\t\t\t\n-\t\t\tfallbackHeight = parseHeight(buildingTags, (float)fallbackHeight);\n-\t\t\t\n-\t\t\tdouble height = parseHeight(tags, (float)fallbackHeight);\n-\t\t    heightWithoutRoof = height - roof.getRoofHeight();\n+//\t\t\tdouble fallbackHeight = buildingLevels * defaultHeightPerLevel;\n+//\t\t\t\n+//\t\t\tfallbackHeight += roof.getRoofHeight();\n+//\t\t\t\n+//\t\t\tfallbackHeight = parseHeight(buildingTags, (float)fallbackHeight);\n+//\t\t\t\n+//\t\t\tdouble height = parseHeight(tags, (float)fallbackHeight);\n+\t\t\tdouble height = 100f;\n+\t\t\theightWithoutRoof = height - roof.getRoofHeight();\n \t\t\t\n \t\t\t/* determine materials */\n \t\t    \ndiff --git a/OSM2World/src/org/osm2world/core/world/modules/RailwayModule.java b/OSM2World/src/org/osm2world/core/world/modules/RailwayModule.java\nindex efaec57..11671b7 100644\n--- a/OSM2World/src/org/osm2world/core/world/modules/RailwayModule.java\n+++ b/OSM2World/src/org/osm2world/core/world/modules/RailwayModule.java\n@@ -40,7 +40,9 @@ public class RailwayModule extends ConfigurableWorldModule {\n \t\t\n \t\tfor (MapWaySegment segment : grid.getMapWaySegments()) {\n \t\t\tif (segment.getTags().containsAny(\"railway\", RAILWAY_VALUES)) {\n-\t\t\t\tsegment.addRepresentation(new Rail(segment));\n+\t\t\t\tif (! TunnelModule.isTunnel(segment.getTags())) {\n+\t\t\t\t\tsegment.addRepresentation(new Rail(segment));\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \t\t\n@@ -66,13 +68,16 @@ public class RailwayModule extends ConfigurableWorldModule {\n \t\n \tprivate static class Rail extends AbstractNetworkWaySegmentWorldObject\n \t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n+\n+\t\tprivate static float TM_SCALE = Float.parseFloat(System.getenv(\"TOUCH_MAPPER_SCALE\"));\n+\t\tprivate static float TM_EXTRUDER = Float.parseFloat(System.getenv(\"TOUCH_MAPPER_EXTRUDER_WIDTH\"));\n \t\t\n-\t\tprivate static final float GROUND_WIDTH = 2.25f;\n-\t\tprivate static final float RAIL_DIST = 1.5f;\n+\t\tprivate static float sizesScaling = TM_SCALE / 3100; // historically things were tuned for scale 3100\n+\n+\t\tprivate static final float GROUND_WIDTH = 2.25f * sizesScaling;\n+\t\tprivate static final float RAIL_DIST = 1.5f * sizesScaling;\n \t\n-\t\tprivate static final float SLEEPER_WIDTH = 2.0f;\n-\t\tprivate static final float SLEEPER_LENGTH = 0.75f;\n-\t\tprivate static final float SLEEPER_HEIGHT = 0.125f;\n+\t\tprivate static final float SLEEPER_HEIGHT = 0.125f * sizesScaling;\n \t\t\n \t\tprivate static final List<VectorXYZ> RAIL_SHAPE = asList(\n \t\t\tnew VectorXYZ(-0.45f, 0, 0), new VectorXYZ(-0.1f, 0.1f, 0),\n@@ -132,37 +137,37 @@ public class RailwayModule extends ConfigurableWorldModule {\n \t\t\t\t\tgetOutline(false), getOutline(true),\n \t\t\t\t\t1 - ((GROUND_WIDTH - RAIL_DIST) / GROUND_WIDTH) / 2);\n \n-\t\t\tfor (List<VectorXYZ> railLine : railLines) {\n-\t\t\t\t\n-\t\t\t\tList<List<VectorXYZ>> stripVectors =\n-\t\t\t\t\tWorldModuleGeometryUtil.createShapeExtrusionAlong(\n-\t\t\t\t\tRAIL_SHAPE, railLine,\n-\t\t\t\t\tCollections.nCopies(railLine.size(), VectorXYZ.Y_UNIT));\n-\t\t\t\t\t\n-\t\t\t\tfor (List<VectorXYZ> stripVector : stripVectors) {\n-\t\t\t\t\ttarget.drawTriangleStrip(Materials.RAIL_DEFAULT, stripVector, null);\n-\t\t\t\t}\n-\t\t\t\n-\t\t\t}\n-\t\t\t\n-\t\t\t\n-\t\t\t/* draw railway ties/sleepers */\n-\t\t\t\n-\t\t\tList<VectorXZ> sleeperPositions = GeometryUtil.equallyDistributePointsAlong(3, false,\n-\t\t\t\t\tgetStartWithOffset(), getEndWithOffset());\n-\t\t\t\t\t\t\n-\t\t\tfor (VectorXZ sleeperPosition : sleeperPositions) {\n-\t\t\t\n-\t\t\t\t//TODO interpolate ele, also using additional points inbetween\n-\t\t\t\t\n-//\t\t\t\tVectorXYZ sleeperPositionXYZ =\n-//\t\t\t\t\t\tsegment.getElevationProfile().getWithEle(sleeperPosition);\n-//\n-//\t\t\t\ttarget.drawBox(Materials.RAIL_SLEEPER_DEFAULT,\n-//\t\t\t\t\t\tsleeperPositionXYZ, segment.getDirection(),\n-//\t\t\t\t\t\tSLEEPER_HEIGHT, SLEEPER_WIDTH, SLEEPER_LENGTH);\n-\t\t\t\t\n-\t\t\t}\n+//\t\t\tfor (List<VectorXYZ> railLine : railLines) {\n+//\t\t\t\t\n+//\t\t\t\tList<List<VectorXYZ>> stripVectors =\n+//\t\t\t\t\tWorldModuleGeometryUtil.createShapeExtrusionAlong(\n+//\t\t\t\t\tRAIL_SHAPE, railLine,\n+//\t\t\t\t\tCollections.nCopies(railLine.size(), VectorXYZ.Y_UNIT));\n+//\t\t\t\t\t\n+//\t\t\t\tfor (List<VectorXYZ> stripVector : stripVectors) {\n+//\t\t\t\t\ttarget.drawTriangleStrip(Materials.RAIL_DEFAULT, stripVector, null);\n+//\t\t\t\t}\n+//\t\t\t\n+//\t\t\t}\n+//\t\t\t\n+//\t\t\t\n+//\t\t\t/* draw railway ties/sleepers */\n+//\t\t\t\n+//\t\t\tList<VectorXZ> sleeperPositions = GeometryUtil.equallyDistributePointsAlong(3, false,\n+//\t\t\t\t\tgetStartWithOffset(), getEndWithOffset());\n+//\t\t\t\t\t\t\n+//\t\t\tfor (VectorXZ sleeperPosition : sleeperPositions) {\n+//\t\t\t\n+//\t\t\t\t//TODO interpolate ele, also using additional points inbetween\n+//\t\t\t\t\n+////\t\t\t\tVectorXYZ sleeperPositionXYZ =\n+////\t\t\t\t\t\tsegment.getElevationProfile().getWithEle(sleeperPosition);\n+////\n+////\t\t\t\ttarget.drawBox(Materials.RAIL_SLEEPER_DEFAULT,\n+////\t\t\t\t\t\tsleeperPositionXYZ, segment.getDirection(),\n+////\t\t\t\t\t\tSLEEPER_HEIGHT, SLEEPER_WIDTH, SLEEPER_LENGTH);\n+//\t\t\t\t\n+//\t\t\t}\n \t\t\t\n \t\t}\n \ndiff --git a/OSM2World/src/org/osm2world/core/world/modules/RoadModule.java b/OSM2World/src/org/osm2world/core/world/modules/RoadModule.java\nindex 66bc74f..b9a11b5 100644\n--- a/OSM2World/src/org/osm2world/core/world/modules/RoadModule.java\n+++ b/OSM2World/src/org/osm2world/core/world/modules/RoadModule.java\n@@ -61,21 +61,24 @@ public class RoadModule extends ConfigurableWorldModule {\n \t\t\n \t\tfor (MapWaySegment line : grid.getMapWaySegments()) {\n \t\t\tif (isRoad(line.getTags())) {\n-\t\t\t\tline.addRepresentation(new Road(line, line.getTags()));\n+\t\t\t\tif (! isTunnelOrSimilar(line.getTags())) {\n+\t\t\t\t\tline.addRepresentation(new Road(line, line.getTags()));\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \n \t\tfor (MapArea area : grid.getMapAreas()) {\n \t\t\t\t\n \t\t\tif (isRoad(area.getTags())) {\n-\t\t\t\t\n-\t\t\t\tList<VectorXZ> coords = new ArrayList<VectorXZ>();\n-\t\t\t\tfor (MapNode node : area.getBoundaryNodes()) {\n-\t\t\t\t\tcoords.add(node.getPos());\n+\t\t\t\tif (! isTunnelOrSimilar(area.getTags())) {\n+\t\t\t\t\tList<VectorXZ> coords = new ArrayList<VectorXZ>();\n+\t\t\t\t\tfor (MapNode node : area.getBoundaryNodes()) {\n+\t\t\t\t\t\tcoords.add(node.getPos());\n+\t\t\t\t\t}\n+\t\t\t\t\tcoords.remove(coords.size()-1);\n+\t\t\t\t\t\n+\t\t\t\t\tarea.addRepresentation(new RoadArea(area));\n \t\t\t\t}\n-\t\t\t\tcoords.remove(coords.size()-1);\n-\t\t\t\t\n-\t\t\t\tarea.addRepresentation(new RoadArea(area));\n \t\t\t}\n \t\t\t\n \t\t}\n@@ -86,7 +89,7 @@ public class RoadModule extends ConfigurableWorldModule {\n \t\t\t\n \t\t\tList<Road> connectedRoads = getConnectedRoads(node, false);\n \t\t\t\n-\t\t\tif (connectedRoads.size() > 2) {\n+\t\t\tif (connectedRoads.size() == 3) {\n \t\t\t\t\n \t\t\t\tnode.addRepresentation(new RoadJunction(node));\n \t\t\t\t\n@@ -112,6 +115,18 @@ public class RoadModule extends ConfigurableWorldModule {\n \t\t\n \t}\n \n+\tpublic static final boolean isTunnelOrSimilar(TagGroup tags) {\n+\t\tif (tags.containsKey(\"tunnel\")\n+\t\t\t\t&& !\"no\".equals(tags.getValue(\"tunnel\"))) {\n+\t\t\treturn true;\n+\t\t};\n+\t\tif (tags.containsKey(\"indoor\")\n+\t\t\t\t&& !\"no\".equals(tags.getValue(\"indoor\"))) {\n+\t\t\treturn true;\n+\t\t};\n+\t\treturn false;\n+\t}\n+\n \tprivate static boolean isRoad(TagGroup tags) {\n \t\tif (tags.containsKey(\"highway\")\n \t\t\t\t&& !tags.contains(\"highway\", \"construction\")\n@@ -119,15 +134,24 @@ public class RoadModule extends ConfigurableWorldModule {\n \t\t\treturn true;\n \t\t} else {\n \t\t\treturn tags.contains(\"railway\", \"platform\")\n-\t\t\t\t|| tags.contains(\"leisure\", \"track\");\n+\t\t\t\t|| tags.contains(\"leisure\", \"track\")\n+\t\t\t\t|| tags.contains(\"tourism\", \"attraction\")\n+\t\t\t\t|| tags.contains(\"man_made\", \"pier\")\n+\t\t\t\t|| tags.contains(\"man_made\", \"breakwater\");\n \t\t}\n \t}\n \t\n \tprivate static boolean isSteps(TagGroup tags) {\n-\t\treturn tags.contains(new Tag(\"highway\",\"steps\"));\n+\t\treturn false;\n+//\t\treturn tags.contains(new Tag(\"highway\",\"steps\"));\n \t}\n \n+\t// See also ObjTarget.isPath()\n \tprivate static boolean isPath(TagGroup tags) {\n+\t\tif (tags.contains(\"man_made\", \"pier\")\n+\t\t\t\t|| tags.contains(\"man_made\", \"breakwater\")) {\n+\t\t\treturn true;\n+\t\t}\n \t\tString highwayValue = tags.getValue(\"highway\");\n \t\treturn \"path\".equals(highwayValue)\n \t\t\t|| \"footway\".equals(highwayValue)\n@@ -339,6 +363,9 @@ public class RoadModule extends ConfigurableWorldModule {\n \t */\n \tprivate static List<LaneConnection> buildLaneConnections(\n \t\t\tMapNode node, boolean isJunction, boolean isCrossing) {\n+\t\tif (true) {\n+\t\t\treturn new ArrayList<LaneConnection>();\n+\t\t}\n \t\t\n \t\tList<Road> roads = getConnectedRoads(node, true);\n \t\t\n@@ -434,6 +461,9 @@ public class RoadModule extends ConfigurableWorldModule {\n \tprivate static List<LaneConnection> buildLaneConnections_allOneway(\n \t\t\tMapNode node, List<Road> inboundOnewayRoadsLTR,\n \t\t\tList<Road> outboundOnewayRoadsLTR) {\n+\t\tif (true) {\n+\t\t\treturn new ArrayList<LaneConnection>();\n+\t\t}\n \n \t\tList<Lane> inboundLanes = new ArrayList<Lane>();\n \t\tList<Lane> outboundLanes = new ArrayList<Lane>();\n@@ -754,7 +784,16 @@ public class RoadModule extends ConfigurableWorldModule {\n \tpublic static class Road\n \t\textends AbstractNetworkWaySegmentWorldObject\n \t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n-\t\t\n+\n+\t\tprivate static float TM_SCALE = Float.parseFloat(System.getenv(\"TOUCH_MAPPER_SCALE\"));\n+\t\tprivate static float TM_EXTRUDER = Float.parseFloat(System.getenv(\"TOUCH_MAPPER_EXTRUDER_WIDTH\"));\n+\n+\t\t// Using exactly extruder width will leave many roads unprinted. The ideal road width may\n+\t\t// be much less than the goal because edges where roads connect are not perpendicular\n+\t\t// to road direction. Further unevenness in road width is caused by fattening in Blender.\n+\t\t// Further, Simplify3D often uses value eg. 1.2x extruder width, depending on printer-dependent configuration.\n+\t\tprivate static final float ROAD_WIDTH_MULTIPLIER = 1.6f;\n+\n \t\tprotected static final float DEFAULT_LANE_WIDTH = 3.5f;\n \t\t\n \t\tprotected static final float DEFAULT_ROAD_CLEARING = 5;\n@@ -799,11 +838,15 @@ public class RoadModule extends ConfigurableWorldModule {\n \t\t\tthis.steps = isSteps(tags);\n \t\t\t\t\t\t\n \t\t\tif (steps) {\n+\t\t\t\t// Never executed\n \t\t\t\tthis.laneLayout = null;\n \t\t\t\tthis.width = parseWidth(tags, 1.0f);\n \t\t\t} else {\n \t\t\t\tthis.laneLayout = buildBasicLaneLayout();\n-\t\t\t\tthis.width = calculateWidth();\n+\t\t\t\tfloat mmToUnits = TM_SCALE / 1000;\n+\t\t\t\tfloat minRoadWidth = TM_EXTRUDER * ROAD_WIDTH_MULTIPLIER * mmToUnits;\n+\t\t\t\tfloat sizesScaling = TM_SCALE / 3100; // historically things were tuned for scale 3100\n+\t\t\t\tthis.width = Math.max(minRoadWidth, calculateWidth() * 0.8f * sizesScaling);\n \t\t\t\tlaneLayout.setCalculatedValues(width);\n \t\t\t}\n \t\t\t\ndiff --git a/OSM2World/src/org/osm2world/core/world/modules/WaterModule.java b/OSM2World/src/org/osm2world/core/world/modules/WaterModule.java\nindex 40e66c8..2be92c1 100644\n--- a/OSM2World/src/org/osm2world/core/world/modules/WaterModule.java\n+++ b/OSM2World/src/org/osm2world/core/world/modules/WaterModule.java\n@@ -45,6 +45,7 @@ public class WaterModule extends ConfigurableWorldModule {\n \n \t//TODO: add canal, ditch, drain\n \n+\tprivate static final float MIN_WATER_WIDTH = 2.2f;\n \tprivate static final Tag WATER_TAG = new Tag(\"natural\", \"water\");\n \tprivate static final Tag RIVERBANK_TAG = new Tag(\"waterway\", \"riverbank\");\n \t\n@@ -52,11 +53,11 @@ public class WaterModule extends ConfigurableWorldModule {\n \t\n \tstatic {\n \t\tWATERWAY_WIDTHS = new HashMap<String, Float>();\n-\t\tWATERWAY_WIDTHS.put(\"river\", 3f);\n-\t\tWATERWAY_WIDTHS.put(\"stream\", 0.5f);\n-\t\tWATERWAY_WIDTHS.put(\"canal\", 2f);\n-\t\tWATERWAY_WIDTHS.put(\"ditch\", 1f);\n-\t\tWATERWAY_WIDTHS.put(\"drain\", 1f);\n+\t\tWATERWAY_WIDTHS.put(\"river\", MIN_WATER_WIDTH * 1.5f);\n+\t\tWATERWAY_WIDTHS.put(\"stream\", MIN_WATER_WIDTH);\n+\t\tWATERWAY_WIDTHS.put(\"canal\", MIN_WATER_WIDTH * 1.2f);\n+\t\tWATERWAY_WIDTHS.put(\"ditch\", MIN_WATER_WIDTH);\n+\t\tWATERWAY_WIDTHS.put(\"drain\", MIN_WATER_WIDTH);\n \t}\n \t\n \t//TODO: apply to is almost always the same! create a superclass handling this!\n@@ -121,8 +122,10 @@ public class WaterModule extends ConfigurableWorldModule {\n \t\t}\n \t\t\n \t\tpublic float getWidth() {\n-\t\t\treturn WorldModuleParseUtil.parseWidth(segment.getTags(),\n-\t\t\t\t\tWATERWAY_WIDTHS.get(segment.getTags().getValue(\"waterway\")));\n+\t\t\treturn 2 * Math.max(\n+\t\t\t\t\t1,\n+\t\t\t\t\tWorldModuleParseUtil.parseWidth(segment.getTags(),\n+\t\t\t\t\tWATERWAY_WIDTHS.get(segment.getTags().getValue(\"waterway\"))));\n \t\t}\n \t\t\n \t\t@Override\n@@ -164,37 +167,37 @@ public class WaterModule extends ConfigurableWorldModule {\n \t\t\t\tList<VectorXYZ> rightWaterBorder = createLineBetween(\n \t\t\t\t\t\tleftOutline, rightOutline, 0.95f);\n \t\t\t\t\n-\t\t\t\tmodifyLineHeight(leftWaterBorder, -0.2f);\n-\t\t\t\tmodifyLineHeight(rightWaterBorder, -0.2f);\n-\t\n-\t\t\t\tList<VectorXYZ> leftGround = createLineBetween(\n-\t\t\t\t\t\tleftOutline, rightOutline, 0.35f);\n-\t\t\t\tList<VectorXYZ> rightGround = createLineBetween(\n-\t\t\t\t\t\tleftOutline, rightOutline, 0.65f);\n-\t\t\t\t\n-\t\t\t\tmodifyLineHeight(leftGround, -1);\n-\t\t\t\tmodifyLineHeight(rightGround, -1);\n-\t\t\t\t\n-\t\t\t\t/* render ground */\n-\t\t\t\t\n-\t\t\t\t@SuppressWarnings(\"unchecked\") // generic vararg is intentional\n-\t\t\t\tList<List<VectorXYZ>> strips = asList(\n-\t\t\t\t\tcreateTriangleStripBetween(\n-\t\t\t\t\t\t\tleftOutline, leftWaterBorder),\n-\t\t\t\t\tcreateTriangleStripBetween(\n-\t\t\t\t\t\t\tleftWaterBorder, leftGround),\n-\t\t\t\t\tcreateTriangleStripBetween(\n-\t\t\t\t\t\t\tleftGround, rightGround),\n-\t\t\t\t\tcreateTriangleStripBetween(\n-\t\t\t\t\t\t\trightGround, rightWaterBorder),\n-\t\t\t\t\tcreateTriangleStripBetween(\n-\t\t\t\t\t\t\trightWaterBorder, rightOutline)\n-\t\t\t\t);\n-\t\t\t\t\n-\t\t\t\tfor (List<VectorXYZ> strip : strips) {\n-\t\t\t\t\ttarget.drawTriangleStrip(TERRAIN_DEFAULT, strip,\n-\t\t\t\t\t\ttexCoordLists(strip, TERRAIN_DEFAULT, GLOBAL_X_Z));\n-\t\t\t\t}\n+//\t\t\t\tmodifyLineHeight(leftWaterBorder, -0.2f);\n+//\t\t\t\tmodifyLineHeight(rightWaterBorder, -0.2f);\n+//\t\n+//\t\t\t\tList<VectorXYZ> leftGround = createLineBetween(\n+//\t\t\t\t\t\tleftOutline, rightOutline, 0.35f);\n+//\t\t\t\tList<VectorXYZ> rightGround = createLineBetween(\n+//\t\t\t\t\t\tleftOutline, rightOutline, 0.65f);\n+//\t\t\t\t\n+//\t\t\t\tmodifyLineHeight(leftGround, -1);\n+//\t\t\t\tmodifyLineHeight(rightGround, -1);\n+//\t\t\t\t\n+//\t\t\t\t/* render ground */\n+//\t\t\t\t\n+//\t\t\t\t@SuppressWarnings(\"unchecked\") // generic vararg is intentional\n+//\t\t\t\tList<List<VectorXYZ>> strips = asList(\n+//\t\t\t\t\tcreateTriangleStripBetween(\n+//\t\t\t\t\t\t\tleftOutline, leftWaterBorder),\n+//\t\t\t\t\tcreateTriangleStripBetween(\n+//\t\t\t\t\t\t\tleftWaterBorder, leftGround),\n+//\t\t\t\t\tcreateTriangleStripBetween(\n+//\t\t\t\t\t\t\tleftGround, rightGround),\n+//\t\t\t\t\tcreateTriangleStripBetween(\n+//\t\t\t\t\t\t\trightGround, rightWaterBorder),\n+//\t\t\t\t\tcreateTriangleStripBetween(\n+//\t\t\t\t\t\t\trightWaterBorder, rightOutline)\n+//\t\t\t\t);\n+//\t\t\t\t\n+//\t\t\t\tfor (List<VectorXYZ> strip : strips) {\n+//\t\t\t\t\ttarget.drawTriangleStrip(TERRAIN_DEFAULT, strip,\n+//\t\t\t\t\t\ttexCoordLists(strip, TERRAIN_DEFAULT, GLOBAL_X_Z));\n+//\t\t\t\t}\n \t\t\t\t\n \t\t\t\t/* render water */\n \t\t\t\t\n"
  },
  {
    "path": "OSM2World/resources/osm2world_definitions.inc",
    "content": "/*\n * This is an example for a POVRay include file that can be used to configure the rendering\n * of .pov files generated with OSM2World (http://osm2world.org/).\n *\n * You can leave out any definition from this file, the rendering will then fall back\n * to (mostly very basic) default materials and shapes.\n * Please be aware that many possible definitions are not listed in this example file.\n * Examine the \"material and object definitions\" section of a .pov file generated\n * by OSM2World to see what definitions are used by that particular scene.\n */\n\n#include \"metals.inc\"\n#include \"woods.inc\"\n#include \"textures.inc\"\n#include \"colors.inc\"\n\n// chooses the season - possible values: spring, summer, autumn, winter\n// (has no effect on the generated scenes yet, could be used in custom model declarations)\n#declare season=\"summer\"\n\n// chooses the time of day - possible values: day, night\n// (has no effect on the generated scenes yet, could be used in custom model declarations)\n#declare time=\"day\"\n\n// activate this #include if you have downloaded the tree models\n// that are available from http://osm2world.org/download (optional)\n// #include \"osm2world_trees.inc\"\n\n// texture definitions\n\n#declare texture_ASPHALT=texture { pigment { color rgb <0.2, 0.2, 0.2> } }\n#declare texture_WATER=texture { T_Chrome_2D normal { waves 0.2 frequency 1000.0 scale 4.0 turbulence 0.1 rotate <0,-45,0> } }\n#declare texture_WOOD=texture { T_Wood5 }\n#declare texture_TERRAIN_DEFAULT=texture { pigment { color rgb <0.2, 0.73, 0.2> } finish { ambient 0.5 diffuse 0.5 } }\n\n// sky sphere definition\n\n// activating this creates a pure blue sky.\n// This is useful when generating tiles. With patterned skies,\n// there will often be visible tile borders in reflecting water.\n//#declare sky_sphere_def = sky_sphere { pigment { rgb <0.0, 0.0, 1.0> } }\n\n\n// activating this creates reflective windows\n// However, you need a material map which is a indexed png with 3 colors:\n// Color 1 is for the transparent part, color 2 is for the window glass, color 3 \n// is for the rest of the window, i.e. the window frame \n//#declare texture_BUILDING_WINDOWS = texture { \n//  material_map {\n//    png \"./textures/transparent.png\"\n//    texture { pigment { color rgb<0,0,0> transmit 1 } }\n//    texture {\n//      pigment { image_map {\n//        png \"./textures/MarekCompositeWall00001_transparent.png\"\n//      } }\n//      finish {\n//        ambient 0.5\n//        diffuse 0.5\n//        phong 1.0\n//        phong_size 150\n//        reflection 0.5\n//      }\n//    }\n//    texture {\n//      pigment { image_map {\n//        png \"./textures/MarekCompositeWall00001_transparent.png\"\n//      } }\n//      finish {\n//        ambient 0.5\n//        diffuse 0.5\n//      }\n//    }\n//  } }\n"
  },
  {
    "path": "OSM2World/resources/shaders/background.fragment",
    "content": "/* Simple fragment shader for drawing a 2d geometry with texture to screen\n */ \n#version 130\n\t\nuniform mat4 ModelViewMatrix;\n \n// corresponds with output from vertex shader, gets interpolated\nin vec2 TexCoord;\n \n// output to buffer\nout vec4 FragColor;\n\nuniform sampler2D Tex;\n\n\nvoid main()\n{\n\tFragColor = texture2D(Tex, TexCoord); \n} \n"
  },
  {
    "path": "OSM2World/resources/shaders/background.vertex",
    "content": "/* Simple vertex shader for drawing a 2d geometry with texture to screen\n */\n#version 130\n \n// per vertex input\nin vec2 VertexPosition;\nin vec2 VertexTexCoord;\n\n// input for at least primitives\nuniform mat4 ModelViewProjectionMatrix;\n\n// output to fragment shader for interpolation\nout vec2 TexCoord;\n \nvoid main()\n{\n\tTexCoord = VertexTexCoord;\n\tgl_Position = ModelViewProjectionMatrix * vec4(VertexPosition.x, VertexPosition.y, 0.0, 1.0);\n} \n"
  },
  {
    "path": "OSM2World/resources/shaders/default.fragment",
    "content": "/* Fragment shader with a per fragment phong lighting model, bumpmaps/normalmaps, shadowmaps and SSAO.\n */ \n#version 130\n// TODO: set from java to match MAX_TEXTURE_LAYERS there\n#define MAX_TEXTURE_LAYERS 4\n#define SSAO_RANDOM_ROTATION\n\t\nuniform bool isShadowed;\nuniform bool useLighting;\nstruct LightInfo {\n\tvec4 Position; // Light position in world coords.\n\tvec3 La; // Ambient light intensity\n\tvec3 Ld; // Diffuse light intensity\n\tvec3 Ls; // Specular light intensity\n};\nuniform LightInfo Light;\n\t\nstruct MaterialInfo {\n\tvec3 Color; // Material color\n\tfloat Ka; // Ambient reflectivity\n\tfloat Kd; // Diffuse reflectivity\n\tfloat Ks; // Specular reflectivity\n\tint Shininess; // Specular shininess factor\n};\nuniform MaterialInfo Material;\n\nuniform mat4 ModelViewMatrix;\nuniform mat4 ProjectionMatrix;\n \n// corresponds with output from vertex shader, gets interpolated\nin vec3 VertexEye;\nin vec3 NormalEye;\nin vec4 TangentEye;\nin vec2 BumpMapCoord;\nin vec4 ShadowCoord;\nin vec2 TexCoord0;\nin vec2 TexCoord1;\nin vec2 TexCoord2;\nin vec2 TexCoord3;\n\nuniform bool useTexture[MAX_TEXTURE_LAYERS];\nuniform sampler2D Tex[MAX_TEXTURE_LAYERS];\nuniform sampler2D BumpMap;\nuniform bool useShadowMap;\nuniform sampler2DShadow ShadowMap;\n\nuniform bool useSSAO;\nuniform sampler2D DepthMap;\nuniform sampler2D NoiseTex;\nuniform vec2 uNoiseScale;\nconst int MAX_KERNEL_SIZE = 128;\nconst float SSAO_SAMPLE_THRESHOLD = 0.15;\nuniform int uKernelSize;\nuniform vec3 uKernelOffsets[MAX_KERNEL_SIZE];\nuniform float uRadius = 1;\n\nuniform bool useAlphaTreshold;\nuniform float alphaTreshold;\n\nuniform bool useBumpMaps;\n \n// output to buffer\nout vec4 FragColor;\n\n/*\n * shading with phong model (ambient, diffuse, specular)\n * lightDir, viewDir and norm have to be in the same coordinate system (e.g. eye or tangent space)\n */\nvec3 phongModel( vec3 lightDir, vec3 viewDir, vec3 norm, out vec3 ambient, out vec3 diffuse, out vec3 spec )\n{\t\n\tvec3 r = reflect( -lightDir, norm );\n\tambient = Light.La;\n\tfloat sDotN = max( dot(lightDir,norm), 0.0 );\n\tdiffuse = Light.Ld * sDotN;\n\tspec = vec3(0.0);\n\tif( sDotN > 0.0 )\n\t\tspec = Light.Ls *\n\t\t\t\tpow( max( dot(r,viewDir), 0.0 ), Material.Shininess );\n\treturn ambient + diffuse + spec;\n}\n\nconst int levels = 3;\nconst float scaleFactor = 1.0 / levels;\n// toon shading (only ambient and diffuse)\nvec3 toonShade( vec3 lightDir, vec3 norm, out vec3 ambient, out vec3 diffuse )\n{\n\tambient = Light.La;\n\tfloat cosine = max( dot(lightDir,norm) , 0.0 );\n\tdiffuse = Light.Ld * floor( cosine * levels ) * scaleFactor;\n\t\n\treturn ambient + diffuse;\n}\n\n//float linearizeDepthPerspective(in float depth, in mat4 projMatrix) {\n//\treturn -projMatrix[3][2] / ((depth*2-1.0) + projMatrix[2][2]);\n//}\n\nfloat linearizeDepth(in float depth, in mat4 projMatrix) {\n\tfloat zNDC = depth*2-1.0;\n\treturn -(zNDC * projMatrix[3][3] - projMatrix[3][2]) / (zNDC * projMatrix[2][3] - projMatrix[2][2]);\n}\n\nfloat ssao( vec3 origin, vec3 normal, out vec3 debug )\n{\n\t/*\n\tvec3 rvec = texture(uTexRandom, vTexcoord * uNoiseScale).xyz * 2.0 - 1.0;\n\tvec3 tangent = normalize(rvec - normal * dot(rvec, normal));\n\tvec3 bitangent = cross(normal, tangent);\n\tmat3 tbn = mat3(tangent, bitangent, normal);\n\t*/\n#ifdef SSAO_RANDOM_ROTATION\n\t//vec3 rvec = texture(NoiseTex, gl_FragCoord.xy * uNoiseScale).xyz; \n\tvec3 rvec = texture(NoiseTex, gl_FragCoord.xy/4.0).xyz; \n\t//vec3 rvec = vec3(gl_FragCoord.xy, 0.0);\n#else \n\tvec3 rvec = vec3(1.0, 0.0, 0.0);\n#endif\n\tvec3 tangent = normalize(rvec - normal * dot(rvec, normal));\n\t//vec3 c1 = cross(normal, vec3(0.0, 0.0, 1.0));\n\t//vec3 c2 = cross(normal, vec3(0.0, 1.0, 0.0)); \n\t//tangent = normalize(length(c1) > length(c2) ? c1 : c2);\n\tvec3 bitangent = cross(normal, tangent);\n\tmat3 tbn = mat3(tangent, bitangent, normal);\n\t\n\tfloat occlusion = 0.0;\n\tint skipped = 0;\n\t// performance improvement: loop could be unrolled with constant kernel size\n\tfor (int i = 0; i < uKernelSize; ++i)\n\t{\n\t\t// get sample position:\n\t\tvec3 sample = tbn * (uKernelOffsets[i]);\n\t\t//vec3 sample = uKernelOffsets[i];\n\t\t/*if ( dot(sample, normal) < SSAO_SAMPLE_THRESHOLD )\n\t\t{\n\t\t\tskipped += 1;\n\t\t\tcontinue;\n\t\t}*/\n\t\tsample = sample * uRadius + origin;\n\t\t\n\t\t// project sample position:\n\t\tvec4 offset = vec4(sample, 1.0);\n\t\toffset = ProjectionMatrix * offset;\n\t\toffset.xy /= offset.w;\n\t\toffset.xy = offset.xy * 0.5 + 0.5;\n\t\t\n\t\t// skip samples outside of depth map texture\n\t\tif ( offset.x < 0 || offset.y < 0 || offset.x > 1 || offset.y > 1 )\n\t\t{\n\t\t\tskipped += 1;\n\t\t\tcontinue;\n\t\t}\n\t\t\n\t\t// get sample depth:\n\t\tfloat sampleDepth = texture(DepthMap, offset.xy).r;\n\t\tsampleDepth = linearizeDepth(sampleDepth, ProjectionMatrix);\n\t\t  \n\t\t// range check & accumulate:\n\t\t//float rangeCheck= abs(origin.z - sampleDepth) < uRadius ? 1.0 : 0.0;\n\t\t//occlusion += (sampleDepth <= sample.z ? 1.0 : 0.0) * rangeCheck;\n\t\tfloat rangeCheck = smoothstep(0.0, 1.0, uRadius / abs(origin.z - sampleDepth));\n\t\tocclusion += rangeCheck * step(sample.z, sampleDepth);\n\t}\n\t\n\t//debug = tbn * vec3(0.0, 0.0, 1.0);\n\t//debug = vec3(skipped/float(255));\n\tdebug = rvec;\n\t\n\t/*vec4 test = ProjectionMatrix * vec4(origin, 1.0);\n\ttest.xy /= test.w;\n\ttest.xy = test.xy * 0.5 + 0.5;\n\tfloat d = texture(DepthMap, test.xy).r;\n\td = linearizeDepth(d, ProjectionMatrix);\n\treturn abs(origin.z - d);\n\treturn origin.z - linearizeDepth(((test.z / test.w)*0.5 + 0.5), ProjectionMatrix);\n\t//return 1.0 - abs(((test.z / test.w)*0.5 + 0.5) - d);\n\t*/\n\t\n\t// normalize and invert\n\tif ( uKernelSize > skipped )\n\t\tocclusion = 1.0 - (occlusion / float(uKernelSize-skipped));\n\telse\n\t\tocclusion = 1.0;\n\t//occlusion = occlusion / float(uKernelSize);\n\treturn occlusion;\n}\n \nvoid main()\n{\n\tvec3 ambient, diff, spec=vec3(0.0);\n\tvec4 texColor = vec4(1.0);\n\n\t// apply textures\n\t// TODO: autogenerate based on MAX_TEXTURE_LAYERS\n\tif ( useTexture[0] ) {\n\t\ttexColor = texture( Tex[0], TexCoord0 );\n\t}\n\ttexColor = texColor * vec4(Material.Color, 1.0); // color first layer only\n\tif ( useTexture[1] ) {\n\t\tvec4 layerColor = texture( Tex[1], TexCoord1 );\n\t\ttexColor = mix(texColor, vec4(layerColor.rgb,1), layerColor.a);\n\t}\n\tif ( useTexture[2] ) {\n\t\tvec4 layerColor = texture( Tex[2], TexCoord2 );\n\t\ttexColor = mix(texColor, vec4(layerColor.rgb,1), layerColor.a);\n\t}\n\tif ( useTexture[3] ) {\n\t\tvec4 layerColor = texture( Tex[3], TexCoord3 );\n\t\ttexColor = mix(texColor, vec4(layerColor.rgb,1), layerColor.a);\n\t}\n\t\n\tif ( useLighting ) {\n\t\tvec3 normalEye = normalize(NormalEye);\n\t\t\n\t\tfloat occlusionFactor = 1.0;\n\t\tvec3 debug = vec3(0.0);\n\t\tif ( useSSAO )\n\t\t\tocclusionFactor = ssao( VertexEye, normalEye, debug );\n\t\t\n\t\tif ( isShadowed ) {\n\t\t\tambient = Light.La;\n\t\t\tdiff = vec3(0.0);\n\t\t} else {\n\t\t\tvec3 viewDirection, lightDirection;\n\t\t\tvec4 lightPositionEye = ModelViewMatrix * Light.Position;\n\t\t\t\n\t\t\t// transform light and view direction to eye space\n\t\t\t\n\t\t\tif( ProjectionMatrix[3][3] == 0.0 )\n\t\t\t\tviewDirection = normalize(-VertexEye); // perspective projection\n\t\t\telse\n\t\t\t\tviewDirection = vec3(0.0, 0.0, 1.0); // orthographic projection: camera is infinite far away, so vertex coordinates don't matter\n\t\t\t\n\t\t\t// directional light?\n\t\t\tif( Light.Position.w == 0.0 )\n\t\t\t\tlightDirection = normalize(vec3(lightPositionEye));\n\t\t\telse\n\t\t\t\tlightDirection = normalize(vec3(lightPositionEye) - VertexEye);\n\t\t\t\n\t\t\tvec3 normal = normalEye;\n\t\t\tif ( useBumpMaps ) {\n\t\t\t\tvec3 tangentEye = normalize(TangentEye.xyz);\n\t\t\t\t// compute bitangent\n\t\t\t\tvec3 bitangentEye = normalize( cross( normalEye, tangentEye ) ) * TangentEye.w;\n\t\t\n\t\t\t\t// transformation from eye to tangent space\n\t\t\t\tmat3 tangentMatrix = mat3(\n\t\t\t\t\ttangentEye.x, bitangentEye.x, normalEye.x,\n\t\t\t\t\ttangentEye.y, bitangentEye.y, normalEye.y,\n\t\t\t\t\ttangentEye.z, bitangentEye.z, normalEye.z );\n\t\t\t\n\t\t\t\t// transform light and view direction to tangent space\n\t\t\t\tviewDirection = tangentMatrix * viewDirection;\n\t\t\t\tlightDirection = tangentMatrix * lightDirection;\n\t\t\t\tnormal = normalize(texture( BumpMap, BumpMapCoord ).xyz * 2.0 - 1.0);\n\t\t\t}\n\t\t\t\n\t\t\tphongModel( lightDirection, viewDirection, normal, ambient, diff, spec );\n\t\t\t//toonShade( lightDirection, normal, ambient, diff );\n\t\t}\n\t\t\n\t\t// shadow map lookup\n\t\tfloat shadowFactor = 1.0;\n\t\tif ( useShadowMap ) {\n\t\t\t//vec3 shadowCoordCart = ShadowCoord.xyz / ShadowCoord.w; // from homogene coordiantes to carthesian\n\t\t\t//float shadowDepthValue = texture(ShadowMap, shadowCoordCart.xy).r; // lookup of depth value in shadow map\n\t\t\t//float fragmentDepthValue = shadowCoordCart.z; // fragment depth value to compare\n\t\t\t//if (shadowDepthValue < fragmentDepthValue)\n\t\t\t//\tshadowFactor = 0.0; // in shadow\n\t\t\t\n\t\t\t// PCF (percentage closer filtering): blur with adjacent shadow map comparison results\n\t\t\tfloat sum = 0 ;\n\t\t\t// Sum contributions from texels around ShadowCoord\n\t\t\tsum += textureProjOffset(ShadowMap, ShadowCoord, ivec2(-1,-1));\n\t\t\tsum += textureProjOffset(ShadowMap, ShadowCoord, ivec2(-1,1));\n\t\t\tsum += textureProjOffset(ShadowMap, ShadowCoord, ivec2(1,1));\n\t\t\tsum += textureProjOffset(ShadowMap, ShadowCoord, ivec2(1,-1));\n\t\t\tshadowFactor = sum * 0.25;\n\t\t}\n\t\tif ( isShadowed )\n\t\t\tshadowFactor = 0.0;\n\t\t\n\t\tdiff = diff * shadowFactor * occlusionFactor;\n\t\tspec = spec * shadowFactor * occlusionFactor;\n\t\tambient = ambient * occlusionFactor;\n\t\t\n\t\tFragColor = vec4(ambient*Material.Ka + diff*Material.Kd, 1.0) * texColor + vec4(spec*Material.Ks, 0.0);\n\t\t//FragColor = texColor;\n\t\t//FragColor = vec4(occlusionFactor, occlusionFactor, occlusionFactor, FragColor.a);\n\t\t//FragColor = vec4(normalEye, FragColor.a);\n\t\t//FragColor = vec4(debug, FragColor.a);\n\t} else {\n\t\tFragColor = texColor;\n\t}\n\t\n\tif ( useAlphaTreshold ) {\n\t\tif ( FragColor.a < alphaTreshold )\n\t\t\tFragColor.a = 0.0;\n\t\telse\n\t\t\tFragColor.a = 1.0;\n\t}\n\t\t\n\tif( FragColor.a < 0.01 )\n\t\tdiscard;\n} \n"
  },
  {
    "path": "OSM2World/resources/shaders/default.vertex",
    "content": "/* Vertex shader with support for bumpmaps/normalmaps, shadowmaps and phong lighting model\n */\n#version 130\n// TODO: set from java to match MAX_TEXTURE_LAYERS there\n#define MAX_TEXTURE_LAYERS 4\n \n// per vertex input\nin vec3 VertexPosition;\nin vec3 VertexNormal;\nin vec4 VertexTangent;\nin vec2 VertexBumpMapCoord;\nin vec2 VertexTexCoord0;\nin vec2 VertexTexCoord1;\nin vec2 VertexTexCoord2;\nin vec2 VertexTexCoord3;\n\n// input for at least primitives\nuniform mat4 ProjectionMatrix;\nuniform mat4 ModelViewMatrix;\nuniform mat4 ModelViewProjectionMatrix;\nuniform mat4 NormalMatrix;\nuniform mat4 ShadowMatrix;\n\n// output to fragment shader for interpolation\nout vec3 VertexEye; // in eye space\nout vec3 NormalEye;\nout vec4 TangentEye; // last coordinate is the handedness to reconstruct the bitangent\nout vec2 BumpMapCoord;\nout vec4 ShadowCoord;\nout vec2 TexCoord0;\nout vec2 TexCoord1;\nout vec2 TexCoord2;\nout vec2 TexCoord3;\n\nvoid main()\n{\n\tTexCoord0 = VertexTexCoord0;\n\tTexCoord1 = VertexTexCoord1;\n\tTexCoord2 = VertexTexCoord2;\n\tTexCoord3 = VertexTexCoord3;\n\tBumpMapCoord = VertexBumpMapCoord;\n\t\n\t// Transform to eye coordinates\n\tNormalEye = normalize(vec3(NormalMatrix * vec4(VertexNormal, 1.0)));\n\tTangentEye = vec4(normalize(vec3(NormalMatrix * vec4(VertexTangent.xyz, 1.0))), VertexTangent.w);\n\tvec4 veye = ModelViewMatrix * vec4(VertexPosition, 1.0);\n\tVertexEye = veye.xyz / veye.w;\n\t\n\t// Transform to shadow coordinates\n\tShadowCoord = ShadowMatrix * vec4(VertexPosition, 1.0);\n\t\n\tgl_Position = ModelViewProjectionMatrix * vec4(VertexPosition, 1.0);\n} \n"
  },
  {
    "path": "OSM2World/resources/shaders/nonarea.fragment",
    "content": "/* Simple fragment shader with static color, no lighting, no textures\n */ \n#version 130\n \n// corresponds with output from vertex shader, gets interpolated\nin vec4 Color;\n \n// output to buffer\nout vec4 FragColor;\n \nvoid main()\n{\n\tFragColor = Color;\n} \n"
  },
  {
    "path": "OSM2World/resources/shaders/nonarea.vertex",
    "content": "/* Simple vertex shader with static color, no lighting, no textures\n */\n#version 130\n \n// per vertex input\nin vec3 VertexPosition;\nin vec4 VertexColor;\n\n// input for at least primitives\nuniform mat4 ModelViewProjectionMatrix;\n\n// output to fragment shader for interpolation\nout vec4 Color;\n \nvoid main()\n{\n\tColor = VertexColor;\n\tgl_Position = ModelViewProjectionMatrix * vec4(VertexPosition, 1.0);\n} \n"
  },
  {
    "path": "OSM2World/resources/shaders/shadowmap.fragment",
    "content": "/* Simple fragment shader with texture layers for depth buffer calculation\n */ \n#version 130\n// TODO: set from java to match MAX_TEXTURE_LAYERS there\n#define MAX_TEXTURE_LAYERS 4\n\n// corresponds with output from vertex shader, gets interpolated\nin vec2 TexCoord0;\nin vec2 TexCoord1;\nin vec2 TexCoord2;\nin vec2 TexCoord3;\n\nuniform bool useTexture[MAX_TEXTURE_LAYERS];\nuniform sampler2D Tex[MAX_TEXTURE_LAYERS];\n\nuniform bool useAlphaTreshold;\nuniform float alphaTreshold;\n \nvoid main()\n{\n\tfloat alpha = 1.0;\n\t\n\t// apply textures\n\t// TODO: autogenerate based on MAX_TEXTURE_LAYERS\n\tif ( useTexture[0] ) {\n\t\talpha = texture( Tex[0], TexCoord0 ).a;\n\t}\n\tif ( useTexture[1] ) {\n\t\tfloat layerAlpha = texture( Tex[1], TexCoord1 ).a;\n\t\talpha = mix(alpha, 1, layerAlpha);\n\t}\n\tif ( useTexture[2] ) {\n\t\tfloat layerAlpha = texture( Tex[2], TexCoord2 ).a;\n\t\talpha = mix(alpha, 1, layerAlpha);\n\t}\n\tif ( useTexture[3] ) {\n\t\tfloat layerAlpha = texture( Tex[3], TexCoord3 ).a;\n\t\talpha = mix(alpha, 1, layerAlpha);\n\t}\n\t\n\tif ( useAlphaTreshold ) {\n\t\tif ( alpha < alphaTreshold )\n\t\t\talpha = 0.0;\n\t\telse\n\t\t\talpha = 1.0;\n\t}\n\t\t\n\tif( alpha < 0.01 )\n\t\tdiscard;\n\t\n\t// nothing to output, only depth buffer needed\n} \n"
  },
  {
    "path": "OSM2World/resources/shaders/shadowmap.vertex",
    "content": "/* Simple vertex shader with vertex position texture layers computation\n */\n#version 130\n\n// per vertex input\nin vec3 VertexPosition;\nin vec2 VertexTexCoord0;\nin vec2 VertexTexCoord1;\nin vec2 VertexTexCoord2;\nin vec2 VertexTexCoord3;\n\n// input for at least primitives\nuniform mat4 ModelViewProjectionMatrix;\n\n// output to fragment shader for interpolation\nout vec2 TexCoord0;\nout vec2 TexCoord1;\nout vec2 TexCoord2;\nout vec2 TexCoord3;\n\nvoid main()\n{\n\tTexCoord0 = VertexTexCoord0;\n\tTexCoord1 = VertexTexCoord1;\n\tTexCoord2 = VertexTexCoord2;\n\tTexCoord3 = VertexTexCoord3;\n\t\n\tgl_Position = ModelViewProjectionMatrix * vec4(VertexPosition, 1.0);\n} \n"
  },
  {
    "path": "OSM2World/resources/shaders/shadowvolume.fragment",
    "content": "/* Empty fragment shader. Only depth buffer will be calculated.\n */ \n#version 130\n \nvoid main()\n{\t\n\t// nothing to output, only depth buffer needed\n} \n"
  },
  {
    "path": "OSM2World/resources/shaders/shadowvolume.vertex",
    "content": "/* Simple vertex shader with vertex position\n */\n#version 130\n\n// per vertex input\nin vec4 VertexPosition;\n\n// input for at least primitives\nuniform mat4 ModelViewProjectionMatrix;\n\nvoid main()\n{\n\tgl_Position = ModelViewProjectionMatrix * VertexPosition;\n} \n"
  },
  {
    "path": "OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/DataSource.java",
    "content": "package org.openstreetmap.josm.plugins.graphview.core.data;\n\n/**\n * source of OSM data that can be used to build graphs from\n *\n * @param <N>  node type\n * @param <W>  way type\n * @param <R>  relation type\n */\npublic interface DataSource<N, W, R, M> {\n\t\n\t/** returns all nodes */\n\tpublic Iterable<N> getNodes();\n\n\t/** returns all ways */\n\tpublic Iterable<W> getWays();\n\n\t/** returns all relations */\n\tpublic Iterable<R> getRelations();\n\n\t/** returns a node's latitude */\n\tpublic double getLat(N node);\n\n\t/** returns a node's longitude */\n\tpublic double getLon(N node);\n\n\t/** returns a way's nodes */\n\tpublic Iterable<N> getNodes(W way);\n\n\t/** returns a relation's members */\n\tpublic Iterable<M> getMembers(R relation);\n\n\t/** returns a node's tags */\n\tpublic TagGroup getTagsN(N node);\n\n\t/** returns a way's tags */\n\tpublic TagGroup getTagsW(W way);\n\n\t/** returns a relation's tags */\n\tpublic TagGroup getTagsR(R relation);\n\n\t/** returns a relation member's role */\n\tpublic String getRole(M member);\n\n\t/** returns a relation member's member object */\n\tpublic Object getMember(M member);\n\t\n\t/** returns whether a relation member is a node */\n\tpublic boolean isNMember(M member);\n\t\n\t/** returns whether a relation member is a way */\n\tpublic boolean isWMember(M member);\n\t\n\t/** returns whether a relation member is a relation */\n\tpublic boolean isRMember(M member);\n\t\n\t/**\n\t * adds an observer.\n\t * Does nothing if the parameter is already an observer of this DataSource.\n\t *\n\t * @param observer  observer object, != null\n\t */\n\tpublic void addObserver(DataSourceObserver observer);\n\n\t/**\n\t * deletes an observer that has been added using {@link #addObserver(DataSourceObserver)}.\n\t * Does nothing if the parameter isn't currently an observer of this DataSource.\n\t *\n\t * @param observer  observer object, != null\n\t */\n\tpublic void deleteObserver(DataSourceObserver observer);\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/DataSourceObserver.java",
    "content": "package org.openstreetmap.josm.plugins.graphview.core.data;\n\n/**\n * observer that will be informed about changes in a DataSource\n * if it has been registered using {@link DataSource#addObserver(DataSourceObserver)}.\n * Not every DataSource will send updates, because some don't change.\n */\npublic interface DataSourceObserver {\n\n\t/**\n\t * informs this observer about changes in an observed data source\n\t * @param dataSource  observed data source that has changed; != null\n\t */\n\tpublic void update(DataSource<?, ?, ?, ?> dataSource);\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/EmptyTagGroup.java",
    "content": "package org.openstreetmap.josm.plugins.graphview.core.data;\n\nimport java.util.Collections;\nimport java.util.Iterator;\n\npublic final class EmptyTagGroup implements TagGroup {\n\t\n\tpublic static final EmptyTagGroup EMPTY_TAG_GROUP = new EmptyTagGroup();\n\t\n\tprivate EmptyTagGroup() { }\n\t\n\t@Override\n\tpublic boolean contains(Tag tag) {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean containsAny(Iterable<Tag> tag) {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean contains(String key, String value) {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean containsAny(Iterable<String> key, String value) {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean containsAny(String key, Iterable<String> values) {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean containsAny(Iterable<String> keys, Iterable<String> values) {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean containsAnyKey(Iterable<String> keys) {\n\t\treturn false;\n\t}\n\t\t\n\t@Override\n\tpublic boolean containsKey(String key) {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean containsValue(String value) {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean containsAnyValue(Iterable<String> value) {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic String getValue(String key) {\n\t\treturn null;\n\t}\n\t\n\t@Override\n\tpublic int size() {\n\t\treturn 0;\n\t}\n\t\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn true;\n\t}\n\t\n\t@Override\n\tpublic Iterator<Tag> iterator() {\n\t\treturn Collections.<Tag>emptyList().iterator();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"{}\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/MapBasedTagGroup.java",
    "content": "package org.openstreetmap.josm.plugins.graphview.core.data;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.Map;\n\n/**\n * TagGroup that uses a key-value-Map to store tags\n */\npublic class MapBasedTagGroup implements TagGroup {\n\t\n\tprivate final Map<String, String> tagMap;\n\t\n\t/**\n\t * @param tagMap  map from keys to values; != null;\n\t *                must not be modified after being used as parameter\n\t */\n\tpublic MapBasedTagGroup(Map<String, String> tagMap) {\n\t\tif (tagMap == null) {\n\t\t\tthrow new IllegalArgumentException();\n\t\t}\n\t\t\n\t\tthis.tagMap = tagMap;\n\t}\n\t\n\t/**\n\t * @param tags  tags to add to the group; != null, each != null\n\t */\n\tpublic MapBasedTagGroup(Iterable<Tag> tags) {\n\t\tif (tags == null) {\n\t\t\tthrow new IllegalArgumentException();\n\t\t}\n\t\tthis.tagMap = new HashMap<String, String>();\n\t\tfor (Tag tag : tags) {\n\t\t\tif (tag == null) {\n\t\t\t\tthrow new IllegalArgumentException();\n\t\t\t} else {\n\t\t\t\tthis.tagMap.put(tag.key, tag.value);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * @param tags  tags to add to the group; each != null\n\t */\n\tpublic MapBasedTagGroup(Tag... tags) {\n\t\tthis.tagMap = new HashMap<String, String>(tags.length);\n\t\tfor (Tag tag : tags) {\n\t\t\tif (tag == null) {\n\t\t\t\tthrow new IllegalArgumentException();\n\t\t\t} else {\n\t\t\t\tthis.tagMap.put(tag.key, tag.value);\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic String getValue(String key) {\n\t\tassert key != null;\n\t\treturn tagMap.get(key);\n\t}\n\n\t@Override\n\tpublic boolean containsKey(String key) {\n\t\tassert key != null;\n\t\treturn tagMap.containsKey(key);\n\t}\n\n\t@Override\n\tpublic boolean containsAnyKey(Iterable<String> keys) {\n\t\tfor (String key : keys) {\n\t\t\tif (this.containsKey(key)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic boolean containsValue(String value) {\n\t\tassert value != null;\n\t\treturn tagMap.containsValue(value);\n\t}\n\n\t@Override\n\tpublic boolean containsAnyValue(Iterable<String> values) {\n\t\tfor (String value : values) {\n\t\t\tif (this.containsValue(value)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic boolean contains(Tag tag) {\n\t\tassert tag != null;\n\t\treturn tag.value.equals(tagMap.get(tag.key));\n\t}\n\n\t@Override\n\tpublic boolean containsAny(Iterable<Tag> tags) {\n\t\tfor (Tag tag : tags) {\n\t\t\tif (this.contains(tag)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean contains(String key, String value) {\n\t\tassert key != null;\n\t\tassert value != null;\n\t\treturn value.equals(tagMap.get(key));\n\t}\n\t\n\t@Override\n\tpublic boolean containsAny( Iterable<String> keys, String value) {\n\t\tfor (String key : keys) {\n\t\t\tif (this.contains(key, value)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tpublic boolean containsAny(Iterable<String> keys, Iterable<String> values) {\n\t\tfor (String key : keys) {\n\t\t\tif (this.containsAny(key, values)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic boolean containsAny(String key, Iterable<String> values) {\n\t\tfor (String value : values) {\n\t\t\tif (this.contains(key, value)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic int size() {\n\t\treturn tagMap.size();\n\t}\n\t\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn tagMap.isEmpty();\n\t}\n\t\n\t/**\n\t * returns an Iterator providing access to all Tags.\n\t * The Iterator does not support the {@link Iterator#remove()} method.\n\t */\n\t@Override\n\tpublic Iterator<Tag> iterator() {\n\t\t\n\t\tCollection<Tag> tagCollection = new LinkedList<Tag>();\n\t\t\n\t\tfor (String key : tagMap.keySet()) {\n\t\t\ttagCollection.add(new Tag(key, tagMap.get(key)));\n\t\t}\n\t\t\n\t\treturn Collections.unmodifiableCollection(tagCollection).iterator();\n\t\t\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn tagMap.toString();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/Tag.java",
    "content": "package org.openstreetmap.josm.plugins.graphview.core.data;\n\n/**\n * immutable representation of an OSM tag (key-value-pair)\n */\npublic class Tag {\n\n\t/** key of the tag; != null */\n\tpublic final String key;\n\n\t/** value of the tag; != null */\n\tpublic final String value;\n\n\tpublic Tag(String key, String value) {\n\t\tassert key != null && value != null;\n\t\tthis.key = key;\n\t\tthis.value = value;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (!(obj instanceof Tag)) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\tTag otherTag = (Tag)obj;\n\t\t\treturn key.equals(otherTag.key) && value.equals(otherTag.value);\n\t\t}\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\treturn key.hashCode() + value.hashCode(); //TODO: might be a less than optimal hash function\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn key + \"=\" + value;\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/TagGroup.java",
    "content": "package org.openstreetmap.josm.plugins.graphview.core.data;\n\n/**\n * represents a group of OSM tags (e.g. all tags of a way).\n * TagGroups are expected to be immutable, so modifying the tags means creation of a new group.\n * This interface requires that keys are unique, which is guaranteed since OSM API 0.6.\n */\npublic interface TagGroup extends Iterable<Tag> {\n\t\n\t/**\n\t * returns the value for the given key or null if no tag in this group uses that key\n\t * @param key  key whose value will be returned; != null\n\t */\n\tpublic String getValue(String key);\n\t\n\t/**\n\t * returns true if this tag group contains a tag with the given key\n\t * @param key  key to check for; != null\n\t */\n\tpublic boolean containsKey(String key);\n\t\n\t/**\n\t * returns true if this tag group contains a tag with one of the given keys\n\t * @param keys  keys to check for; != null\n\t */\n\tpublic boolean containsAnyKey(Iterable<String> keys);\n\t\n\t/**\n\t * returns true if this tag group contains at least one tag with the given value\n\t * @param value  value to check for; != null\n\t */\n\tpublic boolean containsValue(String value);\n\t\n\t/**\n\t * returns true if this tag group contains at least one tag with one of the given values\n\t * @param values  values to check for; != null\n\t */\n\tpublic boolean containsAnyValue(Iterable<String> values);\n\t\n\t/**\n\t * returns true if this tag group contains the given tag\n\t * @param tag  tag to check for; != null\n\t */\n\tpublic boolean contains(Tag tag);\n\t\n\t/**\n\t * returns true if this tag group contains one of the given tags\n\t * @param tags  tags to check for; != null\n\t */\n\tpublic boolean containsAny(Iterable<Tag> tags);\n\t\n\t/**\n\t * returns true if this tag group contains the tag\n\t * @param key    key of the tag to check for; != null\n\t * @param value  value of the tag to check for; != null\n\t */\n\tpublic boolean contains(String key, String value);\n\t\n\t/**\n\t * returns true if this tag group contains one of the keys\n\t * with value\n\t * @param keys   keys of the tag to check for; != null\n\t * @param value  value of the tag to check for; != null\n\t */\n\tpublic boolean containsAny(Iterable<String> keys, String value);\n\n\t/**\n\t * returns true if this tag group contains one of the keys\n\t * with one of the values\n\t * @param keys \t   keys of the tag to check for; != null\n\t * @param values  values of the tag to check for; != null\n\t */\n\tpublic boolean containsAny(Iterable<String> keys, Iterable<String> values);\n\t\n\t/**\n\t * returns true if this tag group contains the key with\n\t * one of the values\n\t * @param key     key of the tag to check for; != null\n\t * @param values  values of the tag to check for; != null\n\t */\n\tpublic boolean containsAny(String key, Iterable<String> values);\n\t\n\t/**\n\t * returns the number of tags in this group\n\t */\n\tpublic int size();\n\t\n\t/**\n\t * returns true if this group contains any tags\n\t */\n\tpublic boolean isEmpty();\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/data/osmosis/OSMFileDataSource.java",
    "content": "package org.openstreetmap.josm.plugins.graphview.core.data.osmosis;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.DataSource;\nimport org.openstreetmap.josm.plugins.graphview.core.data.DataSourceObserver;\nimport org.openstreetmap.josm.plugins.graphview.core.data.MapBasedTagGroup;\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.openstreetmap.osmosis.core.container.v0_6.EntityContainer;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Entity;\nimport org.openstreetmap.osmosis.core.domain.v0_6.EntityType;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Node;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Relation;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Tag;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Way;\nimport org.openstreetmap.osmosis.core.domain.v0_6.WayNode;\nimport org.openstreetmap.osmosis.core.task.v0_6.Sink;\nimport org.openstreetmap.osmosis.xml.common.CompressionMethod;\nimport org.openstreetmap.osmosis.xml.v0_6.XmlReader;\n\n/**\n * DataSource providing information from a single .osm file. The file is read\n * during the constructor call, there will be no updates when the file is\n * changed later. This class uses osmosis to read the file.\n */\npublic class OSMFileDataSource implements\n\t\tDataSource<OSMFileDataSource.OwnNode, OSMFileDataSource.OwnWay,\n\t\t           OSMFileDataSource.OwnRelation, OSMFileDataSource.OwnMember> {\n\t\n\tprivate static final boolean useDebugLabels = true;\n\t\n\tprivate boolean complete = false;\n\t\n\tprivate synchronized boolean isComplete() {\n\t\treturn complete;\n\t}\n\t\n\tprivate synchronized void setCompleteTrue() {\n\t\tthis.complete = true;\n\t}\n\t\n\tprivate Map<Long, Node> nodesById = new HashMap<Long, Node>();\n\tprivate Map<Long, Way> waysById = new HashMap<Long, Way>();\n\tprivate Map<Long, Relation> relationsById = new HashMap<Long, Relation>();\n\t\n\tprivate Collection<OwnNode> ownNodes;\n\tprivate Collection<OwnWay> ownWays;\n\tprivate Collection<OwnRelation> ownRelations;\n\t\n\tprivate final Sink sinkImplementation = new Sink() {\n\t\tpublic void initialize(Map<String, Object> arg0) {\n\t\t\t/* do nothing */\n\t\t}\n\t\tpublic void release() {\n\t\t\t/* do nothing */\n\t\t}\n\t\tpublic void complete() {\n\t\t\tsetCompleteTrue();\n\t\t}\n\t\tpublic void process(EntityContainer entityContainer) {\n\t\t\tEntity entity = entityContainer.getEntity();\n\t\t\tif (entity instanceof Node) {\n\t\t\t\tnodesById.put(entity.getId(), ((Node) entity));\n\t\t\t} else if (entity instanceof Way) {\n\t\t\t\twaysById.put(entity.getId(), ((Way) entity));\n\t\t\t} else if (entity instanceof Relation) {\n\t\t\t\trelationsById.put(entity.getId(), ((Relation) entity));\n\t\t\t}\n\t\t}\n\t};\n\t\n\tpublic OSMFileDataSource(File file) throws IOException {\n\t\t\n\t\tXmlReader reader = new XmlReader(file, false, CompressionMethod.None);\n\t\treader.setSink(sinkImplementation);\n\t\t\n\t\tThread readerThread = new Thread(reader);\n\t\treaderThread.start();\n\t\t\n\t\twhile (readerThread.isAlive()) {\n\t\t\ttry {\n\t\t\t\treaderThread.join();\n\t\t\t} catch (InterruptedException e) { /* do nothing */\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (!isComplete()) {\n\t\t\tthrow new IOException(\"couldn't read from file\");\n\t\t}\n\t\t\n\t\tconvertToOwnRepresentation();\n\t\t\n\t}\n\t\n\tprivate void convertToOwnRepresentation() {\n\t\t\n\t\townNodes = new ArrayList<OwnNode>(nodesById.size());\n\t\townWays = new ArrayList<OwnWay>(waysById.size());\n\t\townRelations = new ArrayList<OwnRelation>(relationsById.size());\n\t\t\n\t\tMap<Node, OwnNode> nodeMap = new HashMap<Node, OwnNode>();\n\t\tMap<Way, OwnWay> wayMap = new HashMap<Way, OwnWay>();\n\t\tMap<Relation, OwnRelation> relationMap = new HashMap<Relation, OwnRelation>();\n\t\t\n\t\tfor (Node node : nodesById.values()) {\n\t\t\t\n\t\t\tOwnNode ownNode = new OwnNode(node.getLatitude(), node\n\t\t\t\t\t.getLongitude(), tagGroupForEntity(node));\n\t\t\t\n\t\t\townNodes.add(ownNode);\n\t\t\tnodeMap.put(node, ownNode);\n\t\t\t\n\t\t}\n\t\t\n\t\tfor (Way way : waysById.values()) {\n\t\t\t\n\t\t\tList<WayNode> origWayNodes = way.getWayNodes();\n\t\t\tList<OwnNode> wayNodes = new ArrayList<OwnNode>(origWayNodes.size());\n\t\t\tfor (WayNode origWayNode : origWayNodes) {\n\t\t\t\tNode origNode = nodesById.get(origWayNode.getNodeId());\n\t\t\t\twayNodes.add(nodeMap.get(origNode));\n\t\t\t}\n\t\t\t\n\t\t\tOwnWay ownWay = new OwnWay(tagGroupForEntity(way), wayNodes);\n\t\t\t\n\t\t\townWays.add(ownWay);\n\t\t\twayMap.put(way, ownWay);\n\t\t\t\n\t\t}\n\t\t\n\t\tfor (Relation relation : relationsById.values()) {\n\t\t\t\n\t\t\tOwnRelation ownRelation = new OwnRelation(\n\t\t\t\t\ttagGroupForEntity(relation), relation.getMembers().size());\n\t\t\t\n\t\t\townRelations.add(ownRelation);\n\t\t\trelationMap.put(relation, ownRelation);\n\t\t\t\n\t\t}\n\t\t\n\t\t// add relation members\n\t\t// (needs to be done *after* creation because relations can be members\n\t\t// of other relations)\n\t\t\n\t\tfor (Relation relation : relationMap.keySet()) {\n\t\t\t\n\t\t\tOwnRelation ownRelation = relationMap.get(relation);\n\t\t\t\n\t\t\tfor (org.openstreetmap.osmosis.core.domain.v0_6.RelationMember member : relation\n\t\t\t\t\t.getMembers()) {\n\t\t\t\t\n\t\t\t\tObject memberObject = null;\n\t\t\t\tif (member.getMemberType() == EntityType.Node) {\n\t\t\t\t\tmemberObject = nodeMap.get(nodesById.get(member\n\t\t\t\t\t\t\t.getMemberId()));\n\t\t\t\t} else if (member.getMemberType() == EntityType.Way) {\n\t\t\t\t\tmemberObject = wayMap.get(waysById\n\t\t\t\t\t\t\t.get(member.getMemberId()));\n\t\t\t\t} else if (member.getMemberType() == EntityType.Relation) {\n\t\t\t\t\tmemberObject = relationMap.get(relationsById.get(member\n\t\t\t\t\t\t\t.getMemberId()));\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tOwnMember ownMember = new OwnMember(member\n\t\t\t\t\t\t.getMemberRole(), memberObject);\n\t\t\t\t\n\t\t\t\townRelation.relationMembers.add(ownMember);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t// give up references to original collections\n\t\t\n\t\tnodesById = null;\n\t\twaysById = null;\n\t\trelationsById = null;\n\t\t\n\t}\n\t\n\tprivate TagGroup tagGroupForEntity(Entity entity) {\n\t\tMap<String, String> tagMap = new HashMap<String, String>(entity.getTags().size());\n\t\tfor (Tag tag : entity.getTags()) {\n\t\t\ttagMap.put(tag.getKey(), tag.getValue());\n\t\t}\n\t\treturn new MapBasedTagGroup(tagMap);\n\t}\n\t\n\tpublic void addObserver(DataSourceObserver observer) {\n\t\t// OSMFileDataSource doesn't check for updates\n\t}\n\t\n\tpublic void deleteObserver(DataSourceObserver observer) {\n\t\t// OSMFileDataSource doesn't check for updates\n\t}\n\t\n\tpublic double getLat(OwnNode node) {\n\t\treturn node.lat;\n\t}\n\t\n\tpublic double getLon(OwnNode node) {\n\t\treturn node.lon;\n\t}\n\t\n\tpublic List<OwnMember> getMembers(OwnRelation relation) {\n\t\treturn relation.relationMembers;\n\t}\n\t\n\tpublic Collection<OwnNode> getNodes() {\n\t\treturn ownNodes;\n\t}\n\t\n\tpublic Collection<OwnWay> getWays() {\n\t\treturn ownWays;\n\t}\n\t\n\tpublic Collection<OwnRelation> getRelations() {\n\t\treturn ownRelations;\n\t}\n\t\n\tpublic List<OwnNode> getNodes(OwnWay way) {\n\t\treturn way.nodes;\n\t}\n\t\n\tpublic TagGroup getTagsN(OwnNode node) {\n\t\treturn node.tags;\n\t}\n\t\n\tpublic TagGroup getTagsR(OwnRelation relation) {\n\t\treturn relation.tags;\n\t}\n\t\n\tpublic TagGroup getTagsW(OwnWay way) {\n\t\treturn way.tags;\n\t}\n\t\n\tpublic Object getMember(OwnMember member) {\n\t\treturn member.member;\n\t}\n\t\n\tpublic String getRole(OwnMember member) {\n\t\treturn member.role;\n\t}\n\t\n\tpublic boolean isNMember(OwnMember member) {\n\t\treturn member.member instanceof OwnNode;\n\t};\n\t\n\tpublic boolean isWMember(OwnMember member) {\n\t\treturn member.member instanceof OwnWay;\n\t};\n\t\n\tpublic boolean isRMember(OwnMember member) {\n\t\treturn member.member instanceof OwnRelation;\n\t};\n\t\n\tpublic class OwnNode {\n\t\tprivate final double lat;\n\t\tprivate final double lon;\n\t\tprivate final TagGroup tags;\n\t\t\n\t\tpublic OwnNode(double lat, double lon, TagGroup tags) {\n\t\t\tthis.lat = lat;\n\t\t\tthis.lon = lon;\n\t\t\tthis.tags = tags;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\tif (useDebugLabels && tags.containsKey(\"debug:label\")) {\n\t\t\t\treturn tags.getValue(\"debug:label\");\n\t\t\t}\n\t\t\treturn \"(\" + lat + \", \" + lon + \", \" + tags + \")\";\n\t\t}\n\t\t\n\t}\n\t\n\tpublic class OwnWay {\n\t\tprivate final TagGroup tags;\n\t\tprivate final List<OwnNode> nodes;\n\t\t\n\t\tpublic OwnWay(TagGroup tags, List<OwnNode> nodes) {\n\t\t\tthis.tags = tags;\n\t\t\tthis.nodes = nodes;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\t\n\t\t\tif (useDebugLabels && tags.containsKey(\"debug:label\")) {\n\t\t\t\treturn tags.getValue(\"debug:label\");\n\t\t\t}\n\t\t\t\n\t\t\treturn nodes.get(0)\n\t\t\t\t+ \"->[\" + (nodes.size() - 2) + \"]->\"\n\t\t\t\t+ nodes.get(nodes.size()-1);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic class OwnRelation {\n\t\t\n\t\tprivate final TagGroup tags;\n\t\t\n\t\tprivate final List<OwnMember> relationMembers;\n\t\t\t// content added after constructor call\n\t\t\n\t\tpublic OwnRelation(TagGroup tags, int initialMemberSize) {\n\t\t\tthis.tags = tags;\n\t\t\tthis.relationMembers =\n\t\t\t\tnew ArrayList<OwnMember>(initialMemberSize);\n\t\t}\n\t}\n\t\n\tpublic static class OwnMember {\n\t\tprivate final String role;\n\t\tprivate final Object member;\n\t\t\n\t\tpublic OwnMember(String role, Object member) {\n\t\t\tthis.role = role;\n\t\t\tthis.member = member;\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/openstreetmap/josm/plugins/graphview/core/util/ValueStringParser.java",
    "content": "package org.openstreetmap.josm.plugins.graphview.core.util;\n\nimport java.awt.Color;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class ValueStringParser {\n\n\t/** prevents instantiation */\n\tprivate ValueStringParser() { }\n\n\t/** pattern that splits into a part before and after a decimal point */\n\tprivate static final Pattern DEC_POINT_PATTERN = Pattern.compile(\"^(\\\\-?\\\\d+)\\\\.(\\\\d+)$\");\n\n\tpublic static final Float parseOsmDecimal(String value, boolean allowNegative) {\n\n\t\t/* positive integer */\n\n\t\ttry {\n\n\t\t\tint weight = Integer.parseInt(value);\n\t\t\tif (weight >= 0 || allowNegative) {\n\t\t\t\treturn (float)weight;\n\t\t\t}\n\n\t\t} catch (NumberFormatException nfe) {}\n\n\t\t/* positive number with decimal point */\n\n\t\tMatcher matcher = DEC_POINT_PATTERN.matcher(value);\n\n\t\tif (matcher.matches()) {\n\n\t\t\tString stringBeforePoint = matcher.group(1);\n\t\t\tString stringAfterPoint = matcher.group(2);\n\n\t\t\tif (stringBeforePoint.length() > 0 || stringAfterPoint.length() > 0) {\n\n\t\t\t\ttry {\n\n\t\t\t\t\tboolean negative = stringBeforePoint.startsWith(\"-\");\n\t\t\t\t\t\n\t\t\t\t\tfloat beforePoint = Integer.parseInt(stringBeforePoint);\n\t\t\t\t\tfloat afterPoint = Integer.parseInt(stringAfterPoint);\n\n\t\t\t\t\tdouble result = Math.abs(beforePoint)\n\t\t\t\t\t\t\t+ Math.pow(10, -stringAfterPoint.length()) * afterPoint;\n\t\t\t\t\tif (negative) { result = - result; }\n\t\t\t\t\t\n\t\t\t\t\tif (result >= 0 || allowNegative) {\n\t\t\t\t\t\treturn (float)result;\n\t\t\t\t\t}\n\n\t\t\t\t} catch (NumberFormatException nfe) {}\n\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tprivate static final Pattern KMH_PATTERN = Pattern.compile(\"^(\\\\d+)\\\\s*km/h$\");\n\tprivate static final Pattern MPH_PATTERN = Pattern.compile(\"^(\\\\d+)\\\\s*mph$\");\n\n\tprivate static final float KM_PER_MILE = 1.609344f;\n\n\t/**\n\t * parses a speed value given e.g. for the \"maxspeed\" key.\n\t *\n\t * @return  speed in km/h; null if value had syntax errors\n\t */\n\tpublic static final Float parseSpeed(String value) {\n\n\t\t/* try numeric speed (implied km/h) */\n\n\t\tFloat speed = parseOsmDecimal(value, false);\n\t\tif (speed != null) {\n\t\t\treturn speed;\n\t\t}\n\n\t\t/* try km/h speed */\n\n\t\tMatcher kmhMatcher = KMH_PATTERN.matcher(value);\n\t\tif (kmhMatcher.matches()) {\n\t\t\tString kmhString = kmhMatcher.group(1);\n\t\t\ttry {\n\t\t\t\treturn (float)Integer.parseInt(kmhString);\n\t\t\t} catch (NumberFormatException nfe) {}\n\t\t}\n\n\t\t/* try mph speed */\n\n\t\tMatcher mphMatcher = MPH_PATTERN.matcher(value);\n\t\tif (mphMatcher.matches()) {\n\t\t\tString mphString = mphMatcher.group(1);\n\t\t\ttry {\n\t\t\t\tint mph = Integer.parseInt(mphString);\n\t\t\t\treturn KM_PER_MILE * mph;\n\t\t\t} catch (NumberFormatException nfe) {}\n\t\t}\n\n\t\t/* all possibilities failed */\n\n\t\treturn null;\n\t}\n\n\tprivate static final Pattern M_PATTERN = Pattern.compile(\"^([\\\\d\\\\.]+)\\\\s*m$\");\n\tprivate static final Pattern KM_PATTERN = Pattern.compile(\"^([\\\\d\\\\.]+)\\\\s*km$\");\n\tprivate static final Pattern MI_PATTERN = Pattern.compile(\"^([\\\\d\\\\.]+)\\\\s*mi$\");\n\tprivate static final Pattern FEET_INCHES_PATTERN = Pattern.compile(\"^([\\\\d]+)'\\\\s*([\\\\d]+)\\\"\");\n\n\tprivate static final double M_PER_MI = 1609.344;\n\tprivate static final double M_PER_INCH = 0.0254f;\n\n\t/**\n\t * parses a measure value given e.g. for the \"width\" or \"length\" key.\n\t *\n\t * @return  measure in m; null if value had syntax errors\n\t */\n\tpublic static final Float parseMeasure(String value) {\n\n\t\t/* try numeric measure (implied m) */\n\n\t\tFloat measure = parseOsmDecimal(value, false);\n\t\tif (measure != null) {\n\t\t\treturn measure;\n\t\t}\n\n\t\t/* try m measure */\n\n\t\tMatcher mMatcher = M_PATTERN.matcher(value);\n\t\tif (mMatcher.matches()) {\n\t\t\tString mString = mMatcher.group(1);\n\t\t\treturn parseOsmDecimal(mString, false);\n\t\t}\n\n\t\t/* try km measure */\n\n\t\tMatcher kmMatcher = KM_PATTERN.matcher(value);\n\t\tif (kmMatcher.matches()) {\n\t\t\tString kmString = kmMatcher.group(1);\n\t\t\tfloat km = parseOsmDecimal(kmString, false);\n\t\t\treturn 1000 * km;\n\t\t}\n\n\t\t/* try mi measure */\n\n\t\tMatcher miMatcher = MI_PATTERN.matcher(value);\n\t\tif (miMatcher.matches()) {\n\t\t\tString miString = miMatcher.group(1);\n\t\t\tfloat mi = parseOsmDecimal(miString, false);\n\t\t\treturn (float)(M_PER_MI * mi);\n\t\t}\n\n\t\t/* try feet/inches measure */\n\n\t\tMatcher feetInchesMatcher = FEET_INCHES_PATTERN.matcher(value);\n\t\tif (feetInchesMatcher.matches()) {\n\t\t\tString feetString = feetInchesMatcher.group(1);\n\t\t\tString inchesString = feetInchesMatcher.group(2);\n\t\t\ttry {\n\t\t\t\tint feet = Integer.parseInt(feetString);\n\t\t\t\tint inches = Integer.parseInt(inchesString);\n\t\t\t\tif (feet >= 0 && inches >= 0 && inches < 12) {\n\t\t\t\t\treturn (float)(M_PER_INCH * (12 * feet + inches));\n\t\t\t\t}\n\t\t\t} catch (NumberFormatException nfe) {}\n\t\t}\n\n\t\t/* all possibilities failed */\n\n\t\treturn null;\n\t}\n\n\tprivate static final Pattern T_PATTERN = Pattern.compile(\"^([\\\\d\\\\.]+)\\\\s*t$\");\n\n\t/**\n\t * parses a weight value given e.g. for the \"maxweight\" or \"maxaxleload\" key.\n\t *\n\t * @return  weight in t; null if value had syntax errors\n\t */\n\tpublic static Float parseWeight(String value) {\n\n\t\t/* try numeric weight (implied t) */\n\n\t\tFloat weight = parseOsmDecimal(value, false);\n\t\tif (weight != null) {\n\t\t\treturn weight;\n\t\t}\n\n\t\t/* try t weight */\n\n\t\tMatcher tMatcher = T_PATTERN.matcher(value);\n\t\tif (tMatcher.matches()) {\n\t\t\tString tString = tMatcher.group(1);\n\t\t\treturn parseOsmDecimal(tString, false);\n\t\t}\n\n\t\t/* all possibilities failed */\n\n\t\treturn null;\n\n\t}\n\n\tprivate static final Pattern INCLINE_PATTERN = Pattern.compile(\"^(\\\\-?\\\\d+(?:\\\\.\\\\d+)?)\\\\s*%$\");\n\n\t/**\n\t * parses an incline value as given for the \"incline\" key.\n\t *\n\t * @return  incline in percents; null if value had syntax errors\n\t */\n\tpublic static final Float parseIncline(String value) {\n\n\t\tMatcher inclineMatcher = INCLINE_PATTERN.matcher(value);\n\t\tif (inclineMatcher.matches()) {\n\t\t\tString inclineString = inclineMatcher.group(1);\n\t\t\treturn parseOsmDecimal(inclineString, true);\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * parses an angular value as given for the \"direction\" key.\n\t *\n\t * @return  angle in degrees measured from north, range [0, 360[;\n\t *          null if value had syntax errors\n\t */\n\tpublic static final Float parseAngle(String value) {\n\n\t\t/* try numeric angle */\n\t\t\n\t\tFloat measure = parseOsmDecimal(value, false);\n\t\tif (measure != null) {\n\t\t\treturn measure % 360;\n\t\t}\n\t\t\n\t\t/* try cardinal directions (represented by letters) */\n\n\t\tif (\"N\"  .equals(value)) { return   0.0f; }\n\t\tif (\"NNE\".equals(value)) { return  22.5f; }\n\t\tif (\"NE\" .equals(value)) { return  45.0f; }\n\t\tif (\"ENE\".equals(value)) { return  67.5f; }\n\t\tif (\"E\"  .equals(value)) { return  90.0f; }\n\t\tif (\"ESE\".equals(value)) { return 112.5f; }\n\t\tif (\"SE\" .equals(value)) { return 135.0f; }\n\t\tif (\"SSE\".equals(value)) { return 157.5f; }\n\t\tif (\"S\"  .equals(value)) { return 180.0f; }\n\t\tif (\"SSW\".equals(value)) { return 202.5f; }\n\t\tif (\"SW\" .equals(value)) { return 225.0f; }\n\t\tif (\"WSW\".equals(value)) { return 247.5f; }\n\t\tif (\"W\"  .equals(value)) { return 270.0f; }\n\t\tif (\"WNW\".equals(value)) { return 292.5f; }\n\t\tif (\"NW\" .equals(value)) { return 315.0f; }\n\t\tif (\"NNW\".equals(value)) { return 337.5f; }\n\t\t\n\t\treturn null;\n\t}\n\n\t/**\n\t * parses an hexadecimal color value\n\t *\n\t * @return  color; null if value had syntax errors\n\t */\n\tpublic static final Color parseColor(String value) {\n\t\t\n\t\ttry {\n\t\t\treturn Color.decode(value);\n\t\t} catch (NumberFormatException e) {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/CLIArguments.java",
    "content": "package org.osm2world.console;\n\nimport java.io.File;\nimport java.util.List;\n\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil.CardinalDirection;\nimport org.osm2world.core.target.common.rendering.TileNumber;\n\nimport uk.co.flamingpenguin.jewel.cli.Option;\n\npublic interface CLIArguments {\n\t\n\tpublic static final String OUTPUT_PATTERN = \"(.*)\\\\.(?:obj|pov|png|ppm|gd)\";\n\t\n\t/* input and output */\n\t\n\t@Option(description=\"the .osm input file\", shortName=\"i\")\n\tFile getInput();\n\tboolean isInput();\n\t\n\t@Option(description=\"output files\", shortName=\"o\", pattern=OUTPUT_PATTERN)\n\tList<File> getOutput();\n\tboolean isOutput();\n\t\n\t@Option(description=\"properties file with configuration parameters\")\n\tFile getConfig();\n\tboolean isConfig();\n\t\n\t@Option(description=\"output size in pixels\", pattern=Resolution.PATTERN,\n\t\t\tdefaultValue=\"800,600\")\n\tResolution getResolution();\n\tboolean isResolution();\n\t\n\t/* camera */\n\t\n\t@Option(description=\"downwards angle of orthographic view in degrees\",\n\t\t\tlongName=\"oview.angle\", defaultValue=\"30\")\n\tdouble getOviewAngle();\n\tboolean isOviewAngle();\n\t\n\t@Option(description=\"direction from which the orthographic view is rendered\",\n\t\t\tpattern=\"[NESW]\", longName=\"oview.from\", defaultValue=\"S\")\n\tCardinalDirection getOviewFrom();\n\tboolean isOviewFrom();\n\t\n\t@Option(description=\"lat,lon pairs defining a bounding box for orthographic view\",\n\t\t\tpattern=LatLonEle.PATTERN, longName=\"oview.bbox\")\n\tList<LatLonEle> getOviewBoundingBox();\n\tboolean isOviewBoundingBox();\n\t\n\t@Option(description=\"zoom,x,y triples of tiles defining a bounding box for orthographic view\",\n\t\t\tpattern=TileNumber.PATTERN, longName=\"oview.tiles\")\n\tList<TileNumber> getOviewTiles();\n\tboolean isOviewTiles();\n\t\n\t@Option(description=\"lat,lon,ele of camera position for perspective view\",\n\t\t\tpattern=LatLonEle.PATTERN_WITH_ELE, longName=\"pview.pos\")\n\tLatLonEle getPviewPos();\n\tboolean isPviewPos();\n\t\n\t@Option(description=\"lat,lon,ele of camera look-at for perspective view\",\n\t\t\tpattern=LatLonEle.PATTERN_WITH_ELE, longName=\"pview.lookAt\")\n\tLatLonEle getPviewLookat();\n\tboolean isPviewLookat();\n\t\n\t@Option(description=\"vertical field of view angle for perspective view, in degrees\",\n\t\t\tlongName=\"pview.fovy\", defaultValue=\"45\")\n\tdouble getPviewFovy();\n\tboolean isPviewFovy();\n\t\n\t@Option(description=\"aspect ratio (width / height) for perspective view\",\n\t\t\tlongName=\"pview.aspect\")\n\tdouble getPviewAspect();\n\tboolean isPviewAspect();\n\t\n\t/* logging */\n\t\n\t@Option(description=\"writes execution times to the command line\")\n\tboolean getPerformancePrint();\n\t\n\t@Option(description=\"appends a line with execution times to a file\")\n\tFile getPerformanceTable();\n\tboolean isPerformanceTable();\n\t\t\n\t/* other parameters */\n\t\n\t@Option(description=\"start the graphical user interface\")\n\tboolean getGui();\n\t\n\t@Option(helpRequest=true, description=\"show this help\", shortName=\"?\")\n\tboolean getHelp();\n\t\n\t@Option(description=\"print software version and exit\")\n\tboolean getVersion();\n\t\n\t/* parameter files */\n\t\n\t@Option(description=\"a file containing one set of parameters per line\")\n\tFile getParameterFile();\n\tboolean isParameterFile();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/CLIArgumentsGroup.java",
    "content": "package org.osm2world.console;\n\nimport static com.google.common.base.Preconditions.*;\nimport static org.osm2world.console.CLIArgumentsUtil.ProgramMode.CONVERT;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * a group of {@link CLIArguments} that represent conversions\n * which can be performed at the same time\n * and are only distinguished by output format/parameters\n */\npublic class CLIArgumentsGroup {\n\t\n\tprivate final CLIArguments representative;\n\tprivate final List<CLIArguments> cliArgumentsList;\n\t\n\t/**\n\t * @param representative  the first member of the group.\n\t */\n\tpublic CLIArgumentsGroup(CLIArguments representative) {\n\t\t\n\t\tcheckNotNull(representative);\n\t\t\n\t\tthis.representative = representative;\n\t\t\n\t\tcliArgumentsList = new ArrayList<CLIArguments>();\n\t\tcliArgumentsList.add(representative);\n\t\t\n\t}\n\t\n\tpublic void addCLIArguments(CLIArguments cliArguments) {\n\t\t\n\t\tcheckNotNull(cliArguments);\n\t\tcheckArgument(isCompatible(cliArguments), \"argument incompatible with group\");\n\t\t\n\t\tcliArgumentsList.add(cliArguments);\n\t\t\n\t}\n\n\tpublic CLIArguments getRepresentative() {\n\t\treturn representative;\n\t}\n\t\n\tpublic List<CLIArguments> getCLIArgumentsList() {\n\t\treturn cliArgumentsList;\n\t}\n\t\n\t/**\n\t * checks whether a CLIArguments instance is compatible with this group\n\t */\n\tpublic boolean isCompatible(CLIArguments cliArguments) {\n\t\treturn isCompatible(representative, cliArguments);\n\t}\n\n\t/**\n\t * checks whether two CLIArguments instances can be put into the same group\n\t */\n\tprivate static final boolean isCompatible(\n\t\t\tCLIArguments args1, CLIArguments args2) {\n\t\t\n\t\treturn CLIArgumentsUtil.getProgramMode(args1) == CONVERT\n\t\t\t&& CLIArgumentsUtil.getProgramMode(args2) == CONVERT\n\t\t\t&& args1.getInput().equals(args2.getInput())\n\t\t\t&& ((args1.isConfig() && args1.getConfig().equals(args2.getConfig()))\n\t\t\t\t\t|| (!args1.isConfig() && !args2.isConfig()));\n\t\t\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/CLIArgumentsUtil.java",
    "content": "package org.osm2world.console;\n\nimport static org.osm2world.console.CLIArgumentsUtil.ProgramMode.*;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class CLIArgumentsUtil {\n\t\n\tpublic static enum ProgramMode {GUI, CONVERT, HELP, VERSION, PARAMFILE};\n\tpublic static enum OutputMode {OBJ, POV, PNG, PPM, GD};\n\t\n\tprivate CLIArgumentsUtil() { }\n\t\n\tpublic static final boolean isValid(CLIArguments args) {\n\t\treturn getErrorString(args) == null;\n\t}\n\t\n\tpublic static final String getErrorString(CLIArguments args) {\n\t\t\n\t\tif (getProgramMode(args) == CONVERT) {\n\t\t\t\n\t\t\tif (!args.isInput() || !args.isOutput()) {\n\t\t\t\treturn \"input and output are required arguments\" +\n\t\t\t\t\t\t\" for a conversion\";\n\t\t\t}\n\t\t\t\n\t\t\tif (args.isOviewTiles() && args.getOviewTiles().isEmpty()) {\n\t\t\t\treturn \"at least one tile required\";\n\t\t\t}\n\t\t\t\n\t\t\tif (args.isOviewBoundingBox()\n\t\t\t\t\t&& args.getOviewBoundingBox().size() < 2) {\n\t\t\t\treturn \"bounding box requires at least two lat,lon pairs\";\n\t\t\t}\n\t\t\t\n\t\t\tif (args.isOviewTiles() && args.isOviewBoundingBox()) {\n\t\t\t\treturn \"define *either* tiles or bounding box for\" +\n\t\t\t\t\t\t\" orthographic view\";\n\t\t\t}\n\t\t\t\n\t\t\tfor (File outputFile : args.getOutput()) {\n\t\t\t\tif (getOutputMode(outputFile) == null) {\n\t\t\t\t\treturn \"cannot identify file type from name \" + outputFile\n\t\t\t\t\t\t+ \"\\navailable output types: \" + OutputMode.values();\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif ((args.isPviewPos() && !args.isPviewLookat())\n\t\t\t\t\t|| (args.isPviewLookat() && !args.isPviewPos())) {\n\t\t\t\treturn \"camera position and look-at for perspective view \"\n\t\t\t\t\t+ \"cannot be used separately, both must be defined\";\n\t\t\t}\n\t\t\t\n\t\t\tif (hasOrthographicArg(args) && hasPerspectiveArg(args)) {\n\t\t\t\treturn \"you cannot combine arguments for perspective view \"\n\t\t\t\t\t+ \"and orthographic view\";\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn null;\n\t\t\t\t\n\t}\n\t\n\tprivate static final boolean hasOrthographicArg(CLIArguments args) {\n\t\treturn args.isOviewBoundingBox()\n\t\t\t|| args.isOviewTiles();\n\t}\n\t\n\tprivate static final boolean hasPerspectiveArg(CLIArguments args) {\n\t\treturn args.isPviewLookat()\n\t\t\t|| args.isPviewPos();\n\t}\n\n\tpublic static final ProgramMode getProgramMode(CLIArguments args) {\n\t\treturn args.isParameterFile() ? PARAMFILE\n\t\t\t\t: args.getHelp() ? HELP\n\t\t\t\t\t: args.getVersion() ? VERSION\n\t\t\t\t\t\t: args.getGui() ? GUI\n\t\t\t\t\t\t\t: CONVERT;\n\t}\n\t\n\tpublic static final OutputMode getOutputMode(File outputFile) {\n\t\tif (outputFile.getName().toLowerCase().endsWith(\".obj\")) {\n\t\t\treturn OutputMode.OBJ;\n\t\t} else if (outputFile.getName().toLowerCase().endsWith(\".pov\")) {\n\t\t\treturn OutputMode.POV;\n\t\t} else if (outputFile.getName().toLowerCase().endsWith(\".png\")) {\n\t\t\treturn OutputMode.PNG;\n\t\t} else if (outputFile.getName().toLowerCase().endsWith(\".ppm\")) {\n\t\t\treturn OutputMode.PPM;\n\t\t} else if (outputFile.getName().toLowerCase().endsWith(\".gd\")) {\n\t\t\treturn OutputMode.GD;\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\t\n\tpublic static final List<String[]> getUnparsedParameterGroups(\n\t\t\tFile parameterFile) throws IOException {\n\t\t\n\t\tList<String[]> result = new ArrayList<String[]>();\n\t\t\n\t\tBufferedReader in = new BufferedReader(new FileReader(parameterFile));\n\t\t\t\n\t\tString line;\n\t\t\n\t\twhile ((line = in.readLine()) != null) {\n\t\t\t\n\t\t\tif (line.startsWith(\"#\")) continue;\n\t\t\tif (line.trim().isEmpty()) continue;\n\t\t\t\n\t\t\tList<String> argList = new ArrayList<String>();\n\t\t\t\n\t\t\tPattern regex = Pattern.compile(\"[^\\\\s\\\"']+|\\\"([^\\\"]*)\\\"|'([^']*)'\");\n\t\t\tMatcher matcher = regex.matcher(line);\n\t\t\t\n\t\t\twhile (matcher.find()) {\n\t\t\t    if (matcher.group(1) != null) {\n\t\t\t        // Add double-quoted string without the quotes\n\t\t\t    \targList.add(matcher.group(1));\n\t\t\t    } else if (matcher.group(2) != null) {\n\t\t\t        // Add single-quoted string without the quotes\n\t\t\t    \targList.add(matcher.group(2));\n\t\t\t    } else {\n\t\t\t        // Add unquoted word\n\t\t\t    \targList.add(matcher.group());\n\t\t\t    }\n\t\t\t}\n\t\t\t\n\t\t\tresult.add(argList.toArray(new String[argList.size()]));\n\t\t\t\n\t\t}\n\t\tin.close();\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/ImageExporter.java",
    "content": "package org.osm2world.console;\n\nimport static java.lang.Math.*;\nimport static org.osm2world.core.target.jogl.JOGLRenderingParameters.Winding.CCW;\nimport static org.osm2world.core.util.ConfigUtil.*;\n\nimport java.awt.AlphaComposite;\nimport java.awt.Color;\nimport java.awt.Graphics2D;\nimport java.awt.image.BufferedImage;\nimport java.awt.image.DataBuffer;\nimport java.awt.image.DataBufferInt;\nimport java.io.DataOutputStream;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.FileChannel;\nimport java.nio.charset.Charset;\n\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GLAutoDrawable;\nimport javax.media.opengl.GLCapabilities;\nimport javax.media.opengl.GLDrawableFactory;\nimport javax.media.opengl.GLEventListener;\nimport javax.media.opengl.GLOffscreenAutoDrawable;\nimport javax.media.opengl.GLProfile;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.console.CLIArgumentsUtil.OutputMode;\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.target.TargetUtil;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\nimport org.osm2world.core.target.jogl.AbstractJOGLTarget;\nimport org.osm2world.core.target.jogl.JOGLRenderingParameters;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.target.jogl.JOGLTargetFixedFunction;\nimport org.osm2world.core.target.jogl.JOGLTargetShader;\nimport org.osm2world.core.target.jogl.JOGLTextureManager;\n\nimport ar.com.hjg.pngj.ImageInfo;\nimport ar.com.hjg.pngj.ImageLineByte;\nimport ar.com.hjg.pngj.PngWriter;\nimport ar.com.hjg.pngj.chunks.PngChunkTextVar;\nimport ar.com.hjg.pngj.chunks.PngMetadata;\n\nimport com.jogamp.opengl.util.awt.AWTGLReadBufferUtil;\n\npublic class ImageExporter {\n\n\t/**\n\t * the width and height of the canvas used for rendering the exported image\n\t * each must not exceed the canvas limit. If the requested image is larger,\n\t * it will be rendered in multiple passes and combined afterwards.\n\t * This is intended to avoid overwhelmingly large canvases\n\t * (which would lead to crashes)\n\t */\n\tprivate static final int DEFAULT_CANVAS_LIMIT = 1024;\n\t\n\tprivate final Results results;\n\tprivate final Configuration config;\n\t\n\tprivate File backgroundImage;\n\tprivate JOGLTextureManager backgroundTextureManager;\n\tprivate Color clearColor;\n\t\n\tprivate boolean exportAlpha = false;\n\t\n\tprivate GLOffscreenAutoDrawable drawable;\n\tprivate ImageExporterGLEventListener listener;\n\tprivate final int pBufferSizeX;\n\tprivate final int pBufferSizeY;\n\t\n\t/** target prepared in init; null for unbuffered rendering */\n\tprivate JOGLTarget bufferTarget = null;\n\t\n\tprivate boolean unbufferedRendering;\n\t\n\t\n\t/**\n\t * Creates an {@link ImageExporter} for later use.\n\t * Also performs calculations that only need to be done once for a group\n\t * of files, based on a {@link CLIArgumentsGroup}.\n\t * \n\t * @param expectedGroup  group that should contain at least the arguments\n\t *                       for the files that will later be requested.\n\t *                       Basis for optimization preparations.\n\t */\n\tpublic ImageExporter(Configuration config, Results results,\n\t\t\tCLIArgumentsGroup expectedGroup) {\n\t\t\n\t\tthis.results = results;\n\t\tthis.config = config;\n\n\t\t/* parse background color/image and other configuration options */\n\t\t\n\t\tclearColor = new Color(0, 0, 0, 0);\n\t\t\n\t\tif (config.containsKey(BG_COLOR_KEY)) {\n\t\t\tColor confClearColor = parseColor(config.getString(BG_COLOR_KEY));\n\t\t\tif (confClearColor != null) {\n\t\t\t\tclearColor = confClearColor;\n\t\t\t} else {\n\t\t\t\tSystem.err.println(\"incorrect color value: \"\n\t\t\t\t\t\t+ config.getString(BG_COLOR_KEY));\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (config.containsKey(BG_IMAGE_KEY)) {\n\t\t\tString fileString = config.getString(BG_IMAGE_KEY);\n\t\t\tif (fileString != null) {\n\t\t\t\tbackgroundImage = new File(fileString);\n\t\t\t\tif (!backgroundImage.exists()) {\n\t\t\t\t\tSystem.err.println(\"background image file doesn't exist: \"\n\t\t\t\t\t\t\t+ backgroundImage);\n\t\t\t\t\tbackgroundImage = null;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\texportAlpha = config.getBoolean(\"exportAlpha\", false);\n\t\t\n\t\tint canvasLimit = config.getInt(CANVAS_LIMIT_KEY, DEFAULT_CANVAS_LIMIT);\n\t\t\n\t\t/* find out what number and size of image file requests to expect */\n\t\t\n\t\tint expectedFileCalls = 0;\n\t\tint expectedMaxSizeX = 1;\n\t\tint expectedMaxSizeY = 1;\n\t\t\n\t\tfor (CLIArguments args : expectedGroup.getCLIArgumentsList()) {\n\t\t\t\n\t\t\tfor (File outputFile : args.getOutput()) {\n\t\t\t\tOutputMode outputMode = CLIArgumentsUtil.getOutputMode(outputFile);\n\t\t\t\tif (outputMode == OutputMode.PNG || outputMode == OutputMode.PPM || outputMode == OutputMode.GD) {\n\t\t\t\t\texpectedFileCalls += 1;\n\t\t\t\t\texpectedMaxSizeX = max(expectedMaxSizeX, args.getResolution().x);\n\t\t\t\t\texpectedMaxSizeY = max(expectedMaxSizeY, args.getResolution().y);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\tboolean onlyOneRenderPass = (expectedFileCalls <= 1\n\t\t\t\t&& expectedMaxSizeX <= canvasLimit\n\t\t\t\t&& expectedMaxSizeY <= canvasLimit);\n\n\t\tunbufferedRendering = onlyOneRenderPass\n\t\t\t\t|| config.getBoolean(\"forceUnbufferedPNGRendering\", false);\n\t\t\n\t\t/* create GL canvas and set rendering parameters */\n\n\t\tGLProfile profile;\n\t\tif (\"shader\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\tprofile = GLProfile.get(GLProfile.GL3);\n\t\t} else {\n\t\t\tprofile = GLProfile.get(GLProfile.GL2);\n\t\t}\n\t\t\n\t\tGLDrawableFactory factory = GLDrawableFactory.getFactory(profile);\n\t\t\n\t\tif (! factory.canCreateGLPbuffer(null, profile) && ! factory.canCreateFBO(null, profile)) {\n\t\t\tthrow new Error(\"Cannot create GLPbuffer or FBO for OpenGL output!\");\n\t\t}\n\t\t\n\t\tGLCapabilities cap = new GLCapabilities(profile);\n\t\tcap.setDoubleBuffered(false);\n\t\tif (exportAlpha)\n\t\t\tcap.setAlphaBits(8);\n\t\t\n\t\t// set MSAA (Multi Sample Anti-Aliasing)\n\t\tint msaa = config.getInt(\"msaa\", 0);\n\t\tif (msaa > 0) {\n\t\t\tcap.setSampleBuffers(true);\n\t\t\tcap.setNumSamples(msaa);\n\t\t}\n\t\t\n\t\tif (\"shader\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\t\n\t\t\tif (\"shadowVolumes\".equals(config.getString(\"shadowImplementation\"))\n\t\t\t\t\t|| \"both\".equals(config.getString(\"shadowImplementation\"))) {\n\t\t\t\tcap.setStencilBits(8);\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t\tpBufferSizeX = min(canvasLimit, expectedMaxSizeX);\n\t\tpBufferSizeY = min(canvasLimit, expectedMaxSizeY);\n\t\t\t\t\n\t\tdrawable = factory.createOffscreenAutoDrawable(null,\n\t\t\t\tcap, null, pBufferSizeX, pBufferSizeY, null);\n\t\tlistener = new ImageExporterGLEventListener();\n\t\tdrawable.addGLEventListener(listener);\n\n\t\tbackgroundTextureManager = new JOGLTextureManager(drawable.getGL());\n\n\t}\n\t\n\tprotected void finalize() throws Throwable {\n\t\tfreeResources();\n\t}\n\n\t/**\n\t * manually frees resources that would otherwise remain used\n\t * until the finalize call. It is no longer possible to use\n\t * {@link #writeImageFile(File, CLIArgumentsUtil.OutputMode, int, int, Camera, Projection)}\n\t * afterwards.\n\t */\n\tpublic void freeResources() {\n\n\t\tif (backgroundTextureManager != null) {\n\t\t\tbackgroundTextureManager.releaseAll();\n\t\t\tbackgroundTextureManager = null;\n\t\t}\n\t\t\n\t\tif (bufferTarget != null) {\n\t\t\tbufferTarget.freeResources();\n\t\t\tbufferTarget = null;\n\t\t}\n\t\t\n\t\tif (drawable != null) {\n\t\t\tdrawable.destroy();\n\t\t\tdrawable = null;\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * renders this ImageExporter's content to a file\n\t * \n\t * @param outputMode   one of the image output modes\n\t * @param x            horizontal resolution\n\t * @param y            vertical resolution\n\t */\n\tpublic void writeImageFile(\n\t\t\tFile outputFile, OutputMode outputMode,\n\t\t\tint x, int y,\n\t\t\tfinal Camera camera,\n\t\t\tfinal Projection projection) throws IOException {\n\t\t\n\t\t/* FIXME: this would be needed for cases where BufferSizes are so unbeliveable large that the temp images go beyond the memory limit\n\t\twhile (((1<<31)/x) <= pBufferSizeY) {\n\t\t\tpBufferSizeY /= 2;\n\t\t}\n\t\t*/\n\t\t\n\t\tlistener.prepareRendering(camera, projection, x, y);\n\t\t\n\t\t/* determine the number of \"parts\" to split the rendering in */\n\t\t\n\t\tint xParts = 1 + ((x-1) / pBufferSizeX);\n\t\tint yParts = 1 + ((y-1) / pBufferSizeY);\n\n\t\t/* generate ImageWriter */\n\t\tImageWriter imageWriter;\n\t\t\n\t\tswitch (outputMode) {\n\t\tcase PNG: imageWriter = new PNGWriter(outputFile, x, y, exportAlpha); break;\n\t\tcase PPM: imageWriter = new PPMWriter(outputFile, x, y); break;\n\t\tcase GD: imageWriter = new GDWriter(outputFile, x, y); break;\n\t\t\n\t\tdefault: throw new IllegalArgumentException(\n\t\t\t\t\"output mode not supported \" + outputMode);\n\t\t}\n\n\t\t/* create image (maybe in multiple parts) */\n\t\t\t\t\n        BufferedImage image = new BufferedImage(x, pBufferSizeY, exportAlpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);\n\n\t\tGraphics2D graphics = image.createGraphics();\n\t\tgraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));\n        \n        for (int yPart = yParts-1; yPart >=0 ; --yPart) {\n        \t\n        \tint yStart = yPart * pBufferSizeY;\n\t\t\tint yEnd   = (yPart+1 < yParts) ? (yStart + (pBufferSizeY-1)) : (y-1);\n\t\t\tint ySize  = (yEnd - yStart) + 1;\n\n        \tfor (int xPart = 0; xPart < xParts; ++xPart) {\n        \t\t\t\n\t\t\t\t/* calculate start, end and size (in pixels)\n\t\t\t\t * of the image part that will be rendered in this pass */\n\t\t\t\n\t\t\t\tint xStart = xPart * pBufferSizeX;\n\t\t\t\tint xEnd   = (xPart+1 < xParts) ? (xStart + (pBufferSizeX-1)) : (x-1);\n\t\t\t\tint xSize  = (xEnd - xStart) + 1;\n\n\t\t\t\tlistener.setPart(xStart, yStart, xEnd, yEnd, xSize, ySize);\n\t\t\t\t\n\t\t\t\t// render everything\n        \t\tdrawable.display();\n\t        \n\t\t\t\t/* make screenshot and paste into the buffer that will contain\n\t\t\t\t * pBufferSizeY entire image lines */\n\n        \t\tdrawable.getContext().makeCurrent();\n\t\t\t\tAWTGLReadBufferUtil reader = new AWTGLReadBufferUtil(drawable.getGLProfile(), exportAlpha);\n\t\t\t\tBufferedImage imagePart = reader.readPixelsToBufferedImage(drawable.getGL(), 0, 0, xSize, ySize, true);\n        \t\tdrawable.getContext().release();\n\t     \n        \t\tgraphics.drawImage(imagePart, xStart, 0, xSize, ySize, null);\n\t\t\t}\n        \t\n        \timageWriter.append(image, ySize);\n\t\t}\n\n        imageWriter.close();\n\t}\n\n\tprivate static JOGLTarget createJOGLTarget(GL gl, Results results,\n\t\t\tConfiguration config) {\n\t\t\n\t\tJOGLTarget target;\n\t\tif (\"shader\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\tboolean drawBoundingBox = config.getBoolean(\"drawBoundingBox\", false);\n\t\t\tboolean shadowVolumes = \"shadowVolumes\".equals(config.getString(\"shadowImplementation\"))\n\t\t\t\t\t|| \"both\".equals(config.getString(\"shadowImplementation\"));\n\t\t\tboolean shadowMaps = \"shadowMap\".equals(config.getString(\"shadowImplementation\"))\n\t\t\t\t\t|| \"both\".equals(config.getString(\"shadowImplementation\"));\n\t\t\tint shadowMapWidth = config.getInt(\"shadowMapWidth\", 4096);\n\t\t\tint shadowMapHeight = config.getInt(\"shadowMapHeight\", 4096);\n\t\t\tint shadowMapCameraFrustumPadding = config.getInt(\"shadowMapCameraFrustumPadding\", 8);\n\t\t\tboolean useSSAO = \"true\".equals(config.getString(\"useSSAO\"));\n\t\t\tint SSAOkernelSize = config.getInt(\"SSAOkernelSize\", 16);\n\t\t\tfloat SSAOradius = config.getFloat(\"SSAOradius\", 1);\n\t\t\tboolean overwriteProjectionClippingPlanes = \"true\".equals(config.getString(\"overwriteProjectionClippingPlanes\"));\n\t\t\ttarget = new JOGLTargetShader(gl.getGL3(),\n\t\t\t\t\tnew JOGLRenderingParameters(CCW, false, true, drawBoundingBox, shadowVolumes, shadowMaps, shadowMapWidth, shadowMapHeight,\n\t\t\t    \t\t\tshadowMapCameraFrustumPadding, useSSAO, SSAOkernelSize, SSAOradius, overwriteProjectionClippingPlanes),\n\t\t\t\t\tGlobalLightingParameters.DEFAULT);\n\t\t} else {\n\t\t\ttarget = new JOGLTargetFixedFunction(gl.getGL2(),\n\t\t\t\t\tnew JOGLRenderingParameters(CCW, false, true),\n\t\t\t\t\tGlobalLightingParameters.DEFAULT);\n\t\t}\n\t\t\n\t\t\n\t\ttarget.setConfiguration(config);\n\t\t\n\t\tboolean underground = config.getBoolean(\"renderUnderground\", true);\n\n\t\ttarget.setXZBoundary(results.getMapData().getBoundary());\n\t\tTargetUtil.renderWorldObjects(target, results.getMapData(), underground);\n\t\t\n\t\ttarget.finish();\n\t\t\n\t\treturn target;\n\t\t\n\t}\n\t\n\t\n\t/**\n\t * interface ImageWriter is used to abstract the underlaying image\n\t * format. It can be used for incremental image writes of huge images\n\t */\n\tpublic static interface ImageWriter {\n\t\tvoid append(BufferedImage img) throws IOException;\n\t\tvoid append(BufferedImage img, int lines) throws IOException;\n\t\tvoid close() throws IOException;\n\t}\n\n\t/**\n\t * Implementation of an ImageWriter to write png files\n\t */\n\tpublic static class PNGWriter implements ImageWriter {\n\n\t\tprivate ImageInfo imgInfo;\n\t\tprivate PngWriter writer;\n\t\t\n\t\tpublic PNGWriter(File outputFile, int cols, int rows, boolean alpha) {\n\t\t\timgInfo = new ImageInfo(cols, rows, 8, alpha);\n\t\t\twriter = new PngWriter(outputFile, imgInfo, true);\n\t\t\t\n\t\t\tPngMetadata metaData = writer.getMetadata();\n\t\t\tmetaData.setTimeNow();\n\t\t\tmetaData.setText(PngChunkTextVar.KEY_Software, \"OSM2World\");\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void append(BufferedImage img) throws IOException {\n\t\t\tappend(img, img.getHeight());\n\t\t}\n\n\t\t@Override\n\t\tpublic void append(BufferedImage img, int lines) throws IOException {\n\n\t\t\t/* get raw data of image */\n\t\t\tDataBuffer imageDataBuffer = img.getRaster().getDataBuffer();\n\t\t\tint[] data = (((DataBufferInt)imageDataBuffer).getData());\n\t\t\t\n\t\t\t/* create one ImageLine that will be refilled and written to png */\n\t\t\tImageLineByte bline = new ImageLineByte(imgInfo);\n\t\t\tbyte[] line = bline.getScanline();\n\t\t\tint channels = imgInfo.channels;\n\t\t\t\n\t\t\tfor (int i = 0; i < lines; i++) {\n\t\t\t\tfor (int d = 0; d < img.getWidth(); d++) {\n\t\t\t\t\tint val = data[i*img.getWidth()+d];\n\t\t\t\t\tline[channels*d+0] = (byte) (val >> 16);\n\t\t\t\t\tline[channels*d+1] = (byte) (val >> 8);\n\t\t\t\t\tline[channels*d+2] = (byte) val;\n\t\t\t\t\tif (channels > 3)\n\t\t\t\t\t\tline[channels*d+3] = (byte) (val >> 24);\n\t\t\t\t}\n\t\t\t\twriter.writeRow(bline);\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic void close() throws IOException {\n\t\t\twriter.end();\n\t\t\twriter.close();\n\t\t}\n\t}\n\t\n\t/**\n\t * Implementation of an ImageWriter to write raw ppm files\n\t */\n\tpublic static class PPMWriter implements ImageWriter {\n\n\t\tprivate FileOutputStream out;\n\t\tprivate FileChannel fc;\n\t\tprivate File outputFile;\n\t\tprivate int cols;\n\t\tprivate int rows;\n\t\t\n\t\tpublic PPMWriter(File outputFile, int cols, int rows) {\n\t\t\tthis.cols = cols;\n\t\t\tthis.rows = rows;\n\t\t\tthis.outputFile = outputFile;\n\t\t}\n\t\t\n\t\tprivate void writeHeader() throws IOException {\n\t\t\t\n\t\t\tout = new FileOutputStream(outputFile);\n\t\t\t\t\t\n\t\t\t// write header\n\t\t\tCharset charSet = Charset.forName(\"US-ASCII\");\n\t\t\tout.write(\"P6\\n\".getBytes(charSet));\n\t\t\tout.write(String.format(\"%d %d\\n\", cols, rows).getBytes(charSet));\n\t\t\tout.write(\"255\\n\".getBytes(charSet));\n\n\t\t\tfc = out.getChannel();\n\t\t}\n\t\t\n\t\t\n\t\t@Override\n\t\tpublic void append(BufferedImage img) throws IOException {\n\t\t\tappend(img, img.getHeight());\n\t\t}\n\n\t\t@Override\n\t\tpublic void append(BufferedImage img, int lines) throws IOException {\n\n\t\t\tif (fc == null) {\n\t\t\t\twriteHeader();\n\t\t\t}\n\t\t\t\n\t\t\t// collect and write content\n\n\t\t\tByteBuffer writeBuffer = ByteBuffer.allocate(\n\t\t\t\t\t3 * img.getWidth() * lines);\n\t\t\t\n\t\t\tDataBuffer imageDataBuffer = img.getRaster().getDataBuffer();\n\t\t\tint[] data = (((DataBufferInt)imageDataBuffer).getData());\n\t\t\t\n\t\t\tfor (int i = 0; i < img.getWidth() * lines; i++) {\n\t\t\t\tint value = data[i];\n\t\t\t\twriteBuffer.put((byte)(value >>> 16));\n\t\t\t\twriteBuffer.put((byte)(value >>> 8));\n\t\t\t\twriteBuffer.put((byte)(value));\n\t\t\t}\n\t\t\t\n\t\t\twriteBuffer.position(0);\n\t\t\tfc.write(writeBuffer);\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void close() throws IOException {\n\n\t\t\tif (fc != null) {\n\t\t\t\tfc.close();\n\t\t\t}\n\n\t\t\tif (out != null) {\n\t\t\t\tout.close();\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * Implementation of an ImageWriter to write the (rare) gd file format\n\t */\n\tpublic static class GDWriter implements ImageWriter {\n\n\t\t//TODO: dimensions are limited to short!\n\t\t\n\t\tprivate FileOutputStream out;\n\t\tprivate FileChannel fc;\n\t\tprivate File outputFile;\n\t\tprivate int cols;\n\t\tprivate int rows;\n\t\t\n\t\tpublic GDWriter(File outputFile, int cols, int rows) {\n\t\t\tthis.cols = cols;\n\t\t\tthis.rows = rows;\n\t\t\tthis.outputFile = outputFile;\n\t\t}\n\t\t\n\t\tprivate void writeHeader() throws IOException {\n\t\t\t\n\t\t\tout = new FileOutputStream(outputFile);\n\t\t\t\t\t\t\n\t\t\tout.write(0xff);\n\t\t\tout.write(0xfe);\n\t\t\t\n\t\t\t//write dimensions\n\t\t\tDataOutputStream dOut = new DataOutputStream(out);\n\t\t\tdOut.writeShort(cols);\n\t\t\tdOut.writeShort(rows);\n\n\t\t\tout.write(0x01);\n\t\t\tout.write(0xff);\n\t\t\tout.write(0xff);\n\t\t\tout.write(0xff);\n\t\t\tout.write(0xff);\n\t\t\tout.write(0x00);\n\t\t\t\t\t\t\n\t\t\tfc = out.getChannel();\n\t\t}\n\t\t\n\t\t\n\t\t@Override\n\t\tpublic void append(BufferedImage img) throws IOException {\n\t\t\tappend(img, img.getHeight());\n\t\t}\n\n\t\t@Override\n\t\tpublic void append(BufferedImage img, int lines) throws IOException {\n\n\t\t\tif (fc == null) {\n\t\t\t\twriteHeader();\n\t\t\t}\n\t\t\t\n\t\t\t// collect and write content\n\n\t\t\tByteBuffer writeBuffer = ByteBuffer.allocate(\n\t\t\t\t\t4 * img.getWidth() * lines);\n\t\t\t\n\t\t\tDataBuffer imageDataBuffer = img.getRaster().getDataBuffer();\n\t\t\tint[] data = (((DataBufferInt)imageDataBuffer).getData());\n\t\t\t\n\t\t\tfor (int i = 0; i < img.getWidth() * lines; i++) {\n\t\t\t\tint value = data[i];\n\t\t\t\twriteBuffer.put((byte)(value >>> 16));\n\t\t\t\twriteBuffer.put((byte)(value >>> 8));\n\t\t\t\twriteBuffer.put((byte)(value));\n\t\t\t\twriteBuffer.put((byte) 0);\n\t\t\t}\n\t\t\t\n\t\t\twriteBuffer.position(0);\n\t\t\tfc.write(writeBuffer);\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void close() throws IOException {\n\n\t\t\tif (fc != null) {\n\t\t\t\tfc.close();\n\t\t\t}\n\n\t\t\tif (out != null) {\n\t\t\t\tout.close();\n\t\t\t}\n\t\t}\n\t}\n\t\n\tpublic class ImageExporterGLEventListener implements GLEventListener {\n\t\tprivate int xStart;\n\t\tprivate int yStart;\n\t\tprivate int xEnd;\n\t\tprivate int yEnd;\n\t\tprivate int xSize;\n\t\tprivate int ySize;\n\t\tprivate int x;\n\t\tprivate int y;\n\t\tprivate Camera camera;\n\t\tprivate Projection projection;\n\t\tprivate boolean nodisplay = false;\n\n\t\t@Override\n\t\tpublic void init(GLAutoDrawable drawable) {\n\n\t\t\t/* render map data into buffer if it needs to be rendered multiple times */\n\t\t\tif (!unbufferedRendering ) {\n\t\t\t\tbufferTarget = createJOGLTarget(drawable.getGL(), results, config);\n\t\t\t}\n\t\t}\n\n\t\tpublic void setPart(int xStart, int yStart, int xEnd, int yEnd,\n\t\t\t\tint xSize, int ySize) {\n\t\t\t\n\t\t\tif (this.xSize != xSize || this.ySize != ySize) {\n\t\t\t\t// disable display while resizing. all display calls need to be from @writeImageFile\n\t\t\t\tnodisplay = true;\n\t\t\t\tdrawable.setSize(xSize, ySize);\n\t\t\t\tnodisplay = false;\n\t\t\t}\n\t\t\t\n\t\t\tthis.xStart = xStart;\n\t\t\tthis.yStart = yStart;\n\t\t\tthis.xEnd = xEnd;\n\t\t\tthis.yEnd = yEnd;\n\t\t\tthis.xSize = xSize;\n\t\t\tthis.ySize = ySize;\n\t\t}\n\n\t\tpublic void prepareRendering(Camera camera, Projection projection,\n\t\t\t\tint x, int y) {\n\t\t\tthis.camera = camera;\n\t\t\tthis.projection = projection;\n\t\t\tthis.x = x;\n\t\t\tthis.y = y;\n\t\t}\n\n\t\t@Override\n\t\tpublic void dispose(GLAutoDrawable drawable) {\n\t\t}\n\n\t\t@Override\n\t\tpublic void display(GLAutoDrawable drawable) {\n\t\t\t\n\t\t\tif (nodisplay)\n\t\t\t\treturn;\n\t\t\t\n\t\t\t/* configure rendering */\n\n\t\t\tAbstractJOGLTarget.clearGL(drawable.getGL(), clearColor);\n\n\t\t\t/* render to pBuffer */\n\n\t\t\tJOGLTarget target = (bufferTarget == null)?\n\t\t\t\t\tcreateJOGLTarget(drawable.getGL(), results, config) : bufferTarget;\n\n\t\t\t\t\tif (backgroundImage != null) {\n\t\t\t\t\t\ttarget.drawBackgoundImage(backgroundImage,\n\t\t\t\t\t\t\t\txStart, yStart, xSize, ySize,\n\t\t\t\t\t\t\t\tbackgroundTextureManager);\n\t\t\t\t\t}\n\n\t\t\t\t\ttarget.renderPart(camera, projection,\n\t\t\t\t\t\t\txStart / (double)(x-1), xEnd / (double)(x-1),\n\t\t\t\t\t\t\tyStart / (double)(y-1), yEnd / (double)(y-1));\n\n\t\t\t\t\tif (target != bufferTarget) {\n\t\t\t\t\t\ttarget.freeResources();\n\t\t\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic void reshape(GLAutoDrawable drawable, int x, int y, int width,\n\t\t\t\tint height) {\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/LatLonEle.java",
    "content": "package org.osm2world.console;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * immutable latitude/longitude/elevation coordinate triple\n */\npublic class LatLonEle {\n\n\tpublic final double lat;\n\tpublic final double lon;\n\tpublic final double ele;\n\n\tprivate static final String DOUBLE_P = \"[+-]?\\\\d+(?:\\\\.\\\\d+)?\";\n\t\n\t/**\n\t * pattern for parseable arguments\n\t */\n\tpublic static final String PATTERN = \"(\"+DOUBLE_P+\"),(\"+DOUBLE_P+\")\";\n\tpublic static final String PATTERN_WITH_ELE = PATTERN + \",(\" + DOUBLE_P + \")\";\n\n\t/**\n\t * regular constructor\n\t */\n\tpublic LatLonEle(double lat, double lon, double ele) {\n\t\tthis.lat = lat;\n\t\tthis.lon = lon;\n\t\tthis.ele = ele;\n\t\tvalidateValues();\n\t}\n\t\n\t/**\n\t * regular constructor (with default elevation of 0)\n\t */\n\tpublic LatLonEle(double lat, double lon) {\n\t\tthis(lat, lon, 0);\n\t}\n\t\n\t/**\n\t * parsing constructor\n\t * @param arg  command line argument to be parsed;\n\t *             formats see {@link #PATTERN} and {@link #PATTERN_WITH_ELE}\n\t */\n\tpublic LatLonEle(String arg) {\n\t\tMatcher mEle = Pattern.compile(PATTERN_WITH_ELE).matcher(arg);\n\t\tMatcher m = Pattern.compile(PATTERN).matcher(arg);\n\t\tif (mEle.matches()) {\n\t\t\tlat = Double.parseDouble(mEle.group(1));\n\t\t\tlon = Double.parseDouble(mEle.group(2));\n\t\t\tele = Double.parseDouble(mEle.group(3));\n\t\t\tvalidateValues();\n\t\t} else if (m.matches()) {\n\t\t\tlat = Double.parseDouble(m.group(1));\n\t\t\tlon = Double.parseDouble(m.group(2));\n\t\t\tele = 0;\n\t\t\tvalidateValues();\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"argument doesn't match: \" + arg);\n\t\t}\n\t}\n\n\t/**\n\t * @throws IllegalArgumentException  for incorrect field values\n\t */\n\tprivate void validateValues() {\n\t\tif (lat > 90 || lat < -90 || lon > 180 || lon < -180) {\n\t\t\tthrow new IllegalArgumentException(\"not valid: \" + lat + \", \" + lon);\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn lat + \",\" + lon;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/OSM2World.java",
    "content": "package org.osm2world.console;\n\nimport static org.osm2world.console.CLIArgumentsUtil.getProgramMode;\nimport static org.osm2world.core.GlobalValues.VERSION_STRING;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\nimport javax.swing.UIManager;\n\nimport org.apache.commons.configuration.BaseConfiguration;\nimport org.apache.commons.configuration.Configuration;\nimport org.apache.commons.configuration.ConfigurationException;\nimport org.apache.commons.configuration.PropertiesConfiguration;\nimport org.osm2world.console.CLIArgumentsUtil.ProgramMode;\nimport org.osm2world.core.GlobalValues;\nimport org.osm2world.viewer.view.ViewerFrame;\n\nimport uk.co.flamingpenguin.jewel.cli.ArgumentValidationException;\nimport uk.co.flamingpenguin.jewel.cli.CliFactory;\n\n/**\n * main class of the OSM2World console application\n */\npublic class OSM2World {\n\n\tpublic static void main(String[] unparsedArgs) {\n\t\t\n\t\t/* assume --gui if no parameters are given */\n\t\t\n\t\tif (unparsedArgs.length == 0) {\n\t\t \n\t\t\tSystem.out.println(\"No parameters, running graphical interface.\\n\"\n\t\t\t\t\t+ \"If you want to use the command line, use the --help\"\n\t\t\t\t\t+ \" parameter for a list of available parameters.\");\n\t\t\t\n\t\t\tunparsedArgs = new String[]{\"--gui\"};\n\t\t\t\n\t\t}\n\t\t\n\t\t/* parse command line arguments */\n\t\t\n\t\tCLIArguments args = null;\n\t\t\n\t\ttry {\n\t\t\targs = parseArguments(unparsedArgs);\n\t\t} catch (Exception e) {\n\t\t\tSystem.err.println(e.getMessage());\n\t\t\tSystem.exit(1);\n\t\t}\n\t\t\n\t\t/* parse lines from parameter file (if one exists) */\n\t\t\n\t\tList<CLIArguments> argumentsList = Collections.singletonList(args);\n\t\t\n\t\tif (args.isParameterFile()) {\n\t\t\t\n\t\t\targumentsList = new ArrayList<CLIArguments>();\n\t\t\t\n\t\t\ttry {\n\t\t\t\t\n\t\t\t\tList<String[]> unparsedArgsLines = CLIArgumentsUtil\n\t\t\t\t\t.getUnparsedParameterGroups(args.getParameterFile());\n\t\t\t\t\n\t\t\t\tfor (String[] unparsedArgsLine : unparsedArgsLines) {\n\t\t\t\t\t\n\t\t\t\t\ttry {\n\t\t\t\t\t\targumentsList.add(parseArguments(unparsedArgsLine));\n\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\tSystem.err.println(\"Could not parse parameters from file:\");\n\t\t\t\t\t\tSystem.err.println(Arrays.toString(unparsedArgsLine));\n\t\t\t\t\t\tSystem.err.println(\"Ignoring it. Reason:\");\n\t\t\t\t\t\tSystem.err.println(e.getMessage());\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t} catch (IOException e) {\n\t\t\t\tSystem.err.println(e.getMessage());\n\t\t\t\tSystem.exit(1);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* collect parameter groups into compatible groups\n\t\t * (groups of parameter groups that use the same input and config files) */\n\t\t\n\t\tList<CLIArgumentsGroup> argumentsGroups = new ArrayList<CLIArgumentsGroup>();\n\t\t\n\t\tfor (CLIArguments arguments : argumentsList) {\n\t\t\t\n\t\t\tboolean added = false;\n\t\t\t\n\t\t\tfor (CLIArgumentsGroup compatibleGroup : argumentsGroups) {\n\t\t\t\tif (compatibleGroup.isCompatible(arguments)) {\n\t\t\t\t\t// add to existing compatible group\n\t\t\t\t\tcompatibleGroup.addCLIArguments(arguments);\n\t\t\t\t\tadded = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (!added) {\n\t\t\t\t// start a new compatible group\n\t\t\t\targumentsGroups.add(new CLIArgumentsGroup(arguments));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* execute conversions */\n\t\t\n\t\tif (argumentsGroups.isEmpty()) {\n\t\t\tSystem.err.println(\"warning: empty parameter file, doing nothing\");\n\t\t}\n\t\t\n\t\tfor (CLIArgumentsGroup argumentsGroup : argumentsGroups) {\n\t\t\t\t\t\t\n\t\t\tif (argumentsList.size() > 1) {\n\t\t\t\tSystem.out.print(\"executing conversion for these parameter lines: \");\n\t\t\t\tfor (CLIArguments p : argumentsGroup.getCLIArgumentsList()) {\n\t\t\t\t\tSystem.out.print(argumentsList.indexOf(p) + \" \");\n\t\t\t\t}\n\t\t\t\tSystem.out.print(\"\\n\");\n\t\t\t}\n\t\t\t\n\t\t\texecuteArgumentsGroup(argumentsGroup);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tprivate static CLIArguments parseArguments(String[] unparsedArgs)\n\t\tthrows ArgumentValidationException, Exception {\n\t\t\n\t\tCLIArguments args = CliFactory.parseArguments(CLIArguments.class, unparsedArgs);\n\t\t\n\t\tif (!CLIArgumentsUtil.isValid(args)) {\n\t\t\tthrow new Exception(CLIArgumentsUtil.getErrorString(args));\n\t\t}\n\t\treturn args;\n\t\t\n\t}\n\n\tprivate static void executeArgumentsGroup(CLIArgumentsGroup argumentsGroup) {\n\t\t\n\t\t/* load configuration file */\n\t\t\n\t\tConfiguration config = new BaseConfiguration();\n\t\tFile configFile = null;\n\t\t\n\t\tCLIArguments representativeArgs = argumentsGroup.getRepresentative();\n\t\t\n\t\tif (representativeArgs.isConfig()) {\n\t\t\ttry {\n\t\t\t\tconfigFile = representativeArgs.getConfig();\n\t\t\t\tPropertiesConfiguration fileConfig = new PropertiesConfiguration();\n\t\t\t\tfileConfig.setListDelimiter(';');\n\t\t\t\tfileConfig.load(configFile);\n\t\t\t\tconfig = fileConfig;\n\t\t\t} catch (ConfigurationException e) {\n\t\t\t\tSystem.err.println(\"could not read config, ignoring it: \");\n\t\t\t\tSystem.err.println(e);\n\t\t\t}\n\t\t}\n\t\t\n\t\t/* run selected mode */\n\t\t\n\t\tProgramMode programMode = getProgramMode(representativeArgs);\n\t\t\n\t\tswitch (programMode) {\n\t\t\n\t\tcase HELP:\n\t\t\tSystem.out.println(\n\t\t\t\t\tCliFactory.createCli(CLIArguments.class).getHelpMessage()\n\t\t\t\t\t+ \"\\n\\nFor more information, see \" + GlobalValues.WIKI_URI);\n\t\t\tbreak;\n\t\t\t\n\t\tcase VERSION:\n\t\t\tSystem.out.println(\"OSM2World \" + VERSION_STRING);\n\t\t\tbreak;\n\t\t\n\t\tcase GUI:\n\t\t\ttry {\n\t\t\t\tUIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());\n\t\t\t} catch(Exception e) {\n\t\t\t\tSystem.out.println(\"Error setting native look and feel: \" + e);\n\t\t\t}\n\t\t\tFile input = representativeArgs.isInput() ?\n\t\t\t\t\trepresentativeArgs.getInput() : null;\n\t\t\tnew ViewerFrame(config, configFile, input).setVisible(true);\n\t\t\tbreak;\n\t\t\t\n\t\tcase CONVERT:\n\t\t\ttry {\n\t\t\t\tOutput.output(config, argumentsGroup);\n\t\t\t} catch (IOException e) {\n\t\t\t\te.printStackTrace();\n\t\t\t}\n\t\t\tbreak;\n\t\t\t\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/Output.java",
    "content": "package org.osm2world.console;\n\nimport java.io.File;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.console.CLIArgumentsUtil.OutputMode;\nimport org.osm2world.core.ConversionFacade;\nimport org.osm2world.core.ConversionFacade.Phase;\nimport org.osm2world.core.ConversionFacade.ProgressListener;\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.map_data.creation.MapProjection;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.LPEleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.LeastSquaresInterpolator;\nimport org.osm2world.core.map_elevation.creation.NaturalNeighborInterpolator;\nimport org.osm2world.core.map_elevation.creation.NoneEleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.SimpleEleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.TerrainInterpolator;\nimport org.osm2world.core.map_elevation.creation.ZeroInterpolator;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil.CardinalDirection;\nimport org.osm2world.core.target.common.rendering.Projection;\nimport org.osm2world.core.target.obj.ObjWriter;\nimport org.osm2world.core.target.povray.POVRayWriter;\nimport org.osm2world.core.util.functions.DefaultFactory;\n\npublic final class Output {\n\n\tprivate Output() {}\n\n\tpublic static void output(Configuration config,\n\t\t\tCLIArgumentsGroup argumentsGroup)\n\t\tthrows IOException {\n\t\t\n\t\tlong start = System.currentTimeMillis();\n\t\t\n\t\tConversionFacade cf = new ConversionFacade();\n\t\tPerformanceListener perfListener =\n\t\t\tnew PerformanceListener(argumentsGroup.getRepresentative());\n\t\tcf.addProgressListener(perfListener);\n\t\t\t\t\n\t\tString interpolatorType = config.getString(\"terrainInterpolator\");\n\t\tif (\"ZeroInterpolator\".equals(interpolatorType)) {\n\t\t\tcf.setTerrainEleInterpolatorFactory(\n\t\t\t\t\tnew DefaultFactory<TerrainInterpolator>(ZeroInterpolator.class));\n\t\t} else if (\"LeastSquaresInterpolator\".equals(interpolatorType)) {\n\t\t\tcf.setTerrainEleInterpolatorFactory(\n\t\t\t\t\tnew DefaultFactory<TerrainInterpolator>(LeastSquaresInterpolator.class));\n\t\t} else if (\"NaturalNeighborInterpolator\".equals(interpolatorType)) {\n\t\t\tcf.setTerrainEleInterpolatorFactory(\n\t\t\t\t\tnew DefaultFactory<TerrainInterpolator>(NaturalNeighborInterpolator.class));\n\t\t}\n\t\t\n\t\tString enforcerType = config.getString(\"eleConstraintEnforcer\");\n\t\tif (\"NoneEleConstraintEnforcer\".equals(enforcerType)) {\n\t\t\tcf.setEleConstraintEnforcerFactory(\n\t\t\t\t\tnew DefaultFactory<EleConstraintEnforcer>(NoneEleConstraintEnforcer.class));\n\t\t} else if (\"SimpleEleConstraintEnforcer\".equals(enforcerType)) {\n\t\t\tcf.setEleConstraintEnforcerFactory(\n\t\t\t\t\tnew DefaultFactory<EleConstraintEnforcer>(SimpleEleConstraintEnforcer.class));\n\t\t} else if (\"LPEleConstraintEnforcer\".equals(enforcerType)) {\n\t\t\tcf.setEleConstraintEnforcerFactory(\n\t\t\t\t\tnew DefaultFactory<EleConstraintEnforcer>(LPEleConstraintEnforcer.class));\n\t\t}\n\t\t\n\t\tResults results = cf.createRepresentations(\n\t\t\t\targumentsGroup.getRepresentative().getInput(), null, config, null);\n\t\t\n\t\tImageExporter exporter = null;\n\t\t\n\t\tfor (CLIArguments args : argumentsGroup.getCLIArgumentsList()) {\n\t\t\t\n\t\t\tCamera camera = null;\n\t\t\tProjection projection = null;\n\t\t\t\n\t\t\tif (args.isOviewTiles()) {\n\t\t\t\t\n\t\t\t\tcamera = OrthoTilesUtil.cameraForTiles(\n\t\t\t\t\t\tresults.getMapProjection(),\n\t\t\t\t\t\targs.getOviewTiles(),\n\t\t\t\t\t\targs.getOviewAngle(),\n\t\t\t\t\t\targs.getOviewFrom());\n\t\t\t\tprojection = OrthoTilesUtil.projectionForTiles(\n\t\t\t\t\t\tresults.getMapProjection(),\n\t\t\t\t\t\targs.getOviewTiles(),\n\t\t\t\t\t\targs.getOviewAngle(),\n\t\t\t\t\t\targs.getOviewFrom());\n\t\t\t\t\n\t\t\t} else if (args.isOviewBoundingBox()) {\n\t\t\t\t\n\t\t\t\tdouble angle = args.getOviewAngle();\n\t\t\t\tCardinalDirection from = args.getOviewFrom();\n\t\t\t\t\n\t\t\t\tCollection<VectorXZ> pointsXZ = new ArrayList<VectorXZ>();\n\t\t\t\tfor (LatLonEle l : args.getOviewBoundingBox()) {\n\t\t\t\t\tpointsXZ.add(results.getMapProjection().calcPos(l.lat, l.lon));\n\t\t\t\t}\n\t\t\t\tAxisAlignedBoundingBoxXZ bounds =\n\t\t\t\t\tnew AxisAlignedBoundingBoxXZ(pointsXZ);\n\t\t\t\t\t\t\t\n\t\t\t\tcamera = OrthoTilesUtil.cameraForBounds(bounds, angle, from);\n\t\t\t\tprojection = OrthoTilesUtil.projectionForBounds(bounds, angle, from);\n\t\t\t\t\n\t\t\t} else if (args.isPviewPos()) {\n\t\t\t\t\n\t\t\t\tMapProjection proj = results.getMapProjection();\n\t\t\t\t\n\t\t\t\tLatLonEle pos = args.getPviewPos();\n\t\t\t\tLatLonEle lookAt = args.getPviewLookat();\n\t\t\t\t\n\t\t\t\tcamera = new Camera();\n\t\t\t\tVectorXYZ posV = proj.calcPos(pos.lat, pos.lon).xyz(pos.ele);\n\t\t\t\tVectorXYZ laV =\tproj.calcPos(lookAt.lat, lookAt.lon).xyz(lookAt.ele);\n\t\t\t\tcamera.setCamera(posV.x, posV.y, posV.z, laV.x, laV.y, laV.z);\n\t\t\t\t\n\t\t\t\tprojection = new Projection(false,\n\t\t\t\t\t\targs.isPviewAspect() ? args.getPviewAspect() :\n\t\t\t\t\t\t\t(double)args.getResolution().x / args.getResolution().y,\n\t\t\t\t\t\t\targs.getPviewFovy(),\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t1, 50000);\n\t\t\t\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tfor (File outputFile : args.getOutput()) {\n\t\t\t\t\n\t\t\t\tOutputMode outputMode =\n\t\t\t\t\tCLIArgumentsUtil.getOutputMode(outputFile);\n\t\t\t\t\n\t\t\t\tswitch (outputMode) {\n\t\n\t\t\t\tcase OBJ:\n\t\t\t\t\tInteger primitiveThresholdOBJ =\n\t\t\t\t\t\tconfig.getInteger(\"primitiveThresholdOBJ\", null);\n\t\t\t\t\tif (primitiveThresholdOBJ == null) {\n\t\t\t\t\t\tObjWriter.writeObjFile(outputFile,\n\t\t\t\t\t\t\t\tresults.getMapData(), results.getMapProjection(),\n\t\t\t\t\t\t\t\tcamera, projection);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tObjWriter.writeObjFiles(outputFile,\n\t\t\t\t\t\t\t\tresults.getMapData(), results.getMapProjection(),\n\t\t\t\t\t\t\t\tcamera, projection, primitiveThresholdOBJ);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\tcase POV:\n\t\t\t\t\tPOVRayWriter.writePOVInstructionFile(outputFile,\n\t\t\t\t\t\t\tresults.getMapData(), camera, projection);\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\tcase PNG:\n\t\t\t\tcase PPM:\n\t\t\t\tcase GD:\n\t\t\t\t\tif (camera == null || projection == null) {\n\t\t\t\t\t\tSystem.err.println(\"camera or projection missing\");\n\t\t\t\t\t}\n\t\t\t\t\tif (exporter == null) {\n\t\t\t\t\t\texporter = new ImageExporter(\n\t\t\t\t\t\t\t\tconfig, results, argumentsGroup);\n\t\t\t\t\t}\n\t\t\t\t\texporter.writeImageFile(outputFile, outputMode,\n\t\t\t\t\t\t\targs.getResolution().x, args.getResolution().y,\n\t\t\t\t\t\t\tcamera, projection);\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tif (exporter != null) {\n\t\t\texporter.freeResources();\n\t\t\texporter = null;\n\t\t}\n\t\t\n\t\tif (argumentsGroup.getRepresentative().getPerformancePrint()) {\n\t\t\tlong timeSec = (System.currentTimeMillis() - start) / 1000;\n\t\t\tSystem.out.println(\"finished after \" + timeSec + \" s\");\n\t\t}\n\t\t\n\t\tif (argumentsGroup.getRepresentative().isPerformanceTable()) {\n\t\t\tPrintWriter w = new PrintWriter(new FileWriter(\n\t\t\t\t\targumentsGroup.getRepresentative().getPerformanceTable(), true), true);\n\t\t\tw.printf(\"|%6d |%6d |%6d |%6d |%6d |%6d |\\n\",\n\t\t\t\t(perfListener.getPhaseDuration(Phase.MAP_DATA) + 500) / 1000,\n\t\t\t\t(perfListener.getPhaseDuration(Phase.REPRESENTATION) + 500) / 1000,\n\t\t\t\t(perfListener.getPhaseDuration(Phase.ELEVATION) + 500) / 1000,\n\t\t\t\t(perfListener.getPhaseDuration(Phase.TERRAIN) + 500) / 1000,\n\t\t\t\t(System.currentTimeMillis() - perfListener.getPhaseEnd(Phase.TERRAIN) + 500) / 1000,\n\t\t\t\t(System.currentTimeMillis() - start + 500) / 1000);\n\t\t\tw.close();\n\t\t}\n\n\t}\n\t\n\tprivate static class PerformanceListener implements ProgressListener {\n\t\t\n\t\tprivate final CLIArguments args;\n\t\tpublic PerformanceListener(CLIArguments args) {\n\t\t\tthis.args = args;\n\t\t}\n\n\t\tprivate Phase currentPhase = null;\n\t\tprivate long currentPhaseStart;\n\t\t\n\t\tprivate Map<Phase, Long> phaseStarts = new HashMap<Phase, Long>();\n\t\tprivate Map<Phase, Long> phaseEnds = new HashMap<Phase, Long>();\n\t\t\n\t\tpublic Long getPhaseStart(Phase phase) {\n\t\t\treturn phaseStarts.get(phase);\n\t\t}\n\t\t\n\t\tpublic Long getPhaseEnd(Phase phase) {\n\t\t\treturn phaseEnds.get(phase);\n\t\t}\n\t\t\n\t\tpublic Long getPhaseDuration(Phase phase) {\n\t\t\treturn getPhaseEnd(phase) - getPhaseStart(phase);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void updatePhase(Phase newPhase) {\n\t\t\t\n\t\t\tphaseStarts.put(newPhase, System.currentTimeMillis());\n\t\t\t\n\t\t\tif (currentPhase != null) {\n\t\t\t\t\n\t\t\t\tphaseEnds.put(currentPhase, System.currentTimeMillis());\n\t\t\t\t\n\t\t\t\tif (args.getPerformancePrint()) {\n\t\t\t\t\tlong ms = System.currentTimeMillis() - currentPhaseStart;\n\t\t\t\t\tSystem.out.println(\"phase \" + currentPhase\n\t\t\t\t\t\t+  \" finished after \" + ms + \" ms\");\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tcurrentPhase = newPhase;\n\t\t\tcurrentPhaseStart = System.currentTimeMillis();\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/Resolution.java",
    "content": "package org.osm2world.console;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * immutable representation of an image resolution\n * (two positive integers)\n */\npublic class Resolution {\n\n\tpublic final int x;\n\tpublic final int y;\n\n\t/**\n\t * pattern for parseable arguments\n\t */\n\tpublic static final String PATTERN = \"([0-9]{1,9}),([0-9]{1,9})\";\n\t\n\t/**\n\t * regular constructor\n\t */\n\tpublic Resolution(int x, int y) {\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tvalidateValues();\n\t}\n\n\t/**\n\t * parsing constructor\n\t * @param arg  command line argument to be parsed;\n\t *             format see {@link #PATTERN}\n\t */\n\tpublic Resolution(String arg) {\n\t\tMatcher m = Pattern.compile(PATTERN).matcher(arg);\n\t\tif (m.matches()) {\n\t\t\tx = Integer.parseInt(m.group(1));\n\t\t\ty = Integer.parseInt(m.group(2));\n\t\t\tvalidateValues();\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"argument doesn't match: \" + arg);\n\t\t}\n\t}\n\n\t/**\n\t * @throws IllegalArgumentException  for incorrect field values\n\t */\n\tprivate void validateValues() {\n\t\tif (x <= 0 || y <= 0) {\n\t\t\tthrow new IllegalArgumentException(\"not positive: \" + x + \", \" + y);\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn x + \",\" + y;\n\t}\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/package-info.java",
    "content": "@ParametersAreNonnullByDefault\npackage org.osm2world.console;\n\nimport javax.annotation.ParametersAreNonnullByDefault;\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/console/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\ncommand line interface for OSM2World.\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/ConversionFacade.java",
    "content": "package org.osm2world.core;\n\nimport static java.util.Collections.emptyList;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.apache.commons.configuration.BaseConfiguration;\nimport org.apache.commons.configuration.Configuration;\nimport org.apache.commons.lang.time.StopWatch;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Bound;\nimport org.osm2world.core.map_data.creation.MapProjection;\nimport org.osm2world.core.map_data.creation.MetricMapProjection;\nimport org.osm2world.core.map_data.creation.OSMToMapDataConverter;\nimport org.osm2world.core.map_data.creation.OriginMapProjection;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.object_info.AddressGatherer;\nimport org.osm2world.core.map_data.object_info.ObjectInfoManager;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.EleConstraintValidator;\nimport org.osm2world.core.map_elevation.creation.LeastSquaresInterpolator;\nimport org.osm2world.core.map_elevation.creation.NoneEleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.SRTMData;\nimport org.osm2world.core.map_elevation.creation.TerrainElevationData;\nimport org.osm2world.core.map_elevation.creation.TerrainInterpolator;\nimport org.osm2world.core.map_elevation.creation.ZeroInterpolator;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.osm.creation.JOSMFileHack;\nimport org.osm2world.core.osm.creation.OsmosisReader;\nimport org.osm2world.core.osm.data.OSMData;\nimport org.osm2world.core.target.Renderable;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.TargetUtil;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.util.FaultTolerantIterationUtil;\nimport org.osm2world.core.util.TouchMapperProfile;\nimport org.osm2world.core.util.FaultTolerantIterationUtil.Operation;\nimport org.osm2world.core.util.functions.DefaultFactory;\nimport org.osm2world.core.util.functions.Factory;\nimport org.osm2world.core.world.creation.WorldCreator;\nimport org.osm2world.core.world.creation.WorldModule;\nimport org.osm2world.core.world.data.WorldObject;\nimport org.osm2world.core.world.modules.BarrierModule;\nimport org.osm2world.core.world.modules.BridgeModule;\nimport org.osm2world.core.world.modules.BuildingModule;\nimport org.osm2world.core.world.modules.CliffModule;\nimport org.osm2world.core.world.modules.GolfModule;\nimport org.osm2world.core.world.modules.InvisibleModule;\nimport org.osm2world.core.world.modules.ParkingModule;\nimport org.osm2world.core.world.modules.PoolModule;\nimport org.osm2world.core.world.modules.PowerModule;\nimport org.osm2world.core.world.modules.RailwayModule;\nimport org.osm2world.core.world.modules.RoadModule;\nimport org.osm2world.core.world.modules.StreetFurnitureModule;\nimport org.osm2world.core.world.modules.SurfaceAreaModule;\nimport org.osm2world.core.world.modules.TrafficSignModule;\nimport org.osm2world.core.world.modules.TreeModule;\nimport org.osm2world.core.world.modules.TunnelModule;\nimport org.osm2world.core.world.modules.WaterModule;\n\n/**\n * provides an easy way to call all steps of the conversion process\n * in the correct order\n */\npublic class ConversionFacade {\n\t\n\t/**\n\t * all results of a conversion run\n\t */\n\tpublic static final class Results {\n\t\t\n\t\tprivate final MapProjection mapProjection;\n\t\tprivate final MapData mapData;\n\t\tprivate final TerrainElevationData eleData;\n\t\t\n\t\tprivate Results(MapProjection mapProjection, MapData mapData, TerrainElevationData eleData) {\n\t\t\tthis.mapProjection = mapProjection;\n\t\t\tthis.mapData = mapData;\n\t\t\tthis.eleData = eleData;\n\t\t}\n\n\t\tpublic MapProjection getMapProjection() {\n\t\t\treturn mapProjection;\n\t\t}\n\t\t\n\t\tpublic MapData getMapData() {\n\t\t\treturn mapData;\n\t\t}\n\t\t\n\t\tpublic TerrainElevationData getEleData() {\n\t\t\treturn eleData;\n\t\t}\n\t\t\n\t\t/**\n\t\t * collects and returns all representations that implement a\n\t\t * renderableType, including terrain.\n\t\t * Convenience method.\n\t\t */\n\t\tpublic <R extends Renderable> Collection<R> getRenderables(Class<R> renderableType) {\n\t\t\treturn getRenderables(renderableType, true, true);\n\t\t}\n\t\t\n\t\t/**\n\t\t * @see #getRenderables(Class)\n\t\t */\n\t\tpublic <R extends Renderable> Collection<R> getRenderables(\n\t\t\t\tClass<R> renderableType, boolean includeGrid, boolean includeTerrain) {\n\t\t\t\n\t\t\t//TODO make use of or drop includeTerrain\n\t\t\t\n\t\t\tCollection<R> representations = new ArrayList<R>();\n\t\t\t\n\t\t\tif (includeGrid) {\n\t\t\t\tfor (R r : mapData.getWorldObjects(renderableType)) {\n\t\t\t\t\trepresentations.add(r);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\treturn representations;\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * generates a default list of modules for the conversion\n\t */\n\tprivate static final List<WorldModule> createDefaultModuleList() {\n\t\t\n\t\treturn Arrays.asList((WorldModule)\n\t\t\t\tnew RoadModule(),\n\t\t\t\tnew RailwayModule(),\n\t\t\t\tnew BuildingModule(),\n\t\t\t\t//new ParkingModule(),\n\t\t\t\t//new TreeModule(),\n\t\t\t\t//new StreetFurnitureModule(),\n\t\t\t\t//new TrafficSignModule(),\n\t\t\t\tnew WaterModule()\n\t\t\t\t//new PoolModule(),\n\t\t\t\t//new GolfModule(),\n\t\t\t\t//new CliffModule(),\n\t\t\t\t//new BarrierModule(),\n\t\t\t\t//new PowerModule(),\n\t\t\t\t//new BridgeModule(),\n\t\t\t\t//new TunnelModule(),\n\t\t\t\t//new SurfaceAreaModule(),\n\t\t\t\t//new InvisibleModule()\n\t\t);\n\t\t\n\t}\n\n\tprivate Factory<? extends OriginMapProjection> mapProjectionFactory =\n\t\tnew DefaultFactory<MetricMapProjection>(MetricMapProjection.class);\n\t\n\tprivate Factory<? extends TerrainInterpolator> terrainEleInterpolatorFactory =\n\t\tnew DefaultFactory<LeastSquaresInterpolator>(LeastSquaresInterpolator.class);\n\n\tprivate Factory<? extends EleConstraintEnforcer> eleConstraintEnforcerFactory =\n\t\tnew DefaultFactory<NoneEleConstraintEnforcer>(NoneEleConstraintEnforcer.class);\n\t\n\t/**\n\t * sets the factory that will make {@link MapProjection}\n\t * instances during subsequent calls to\n\t * {@link #createRepresentations(OSMData, List, Configuration, List)}.\n\t * \n\t * @see DefaultFactory\n\t */\n\tpublic void setMapProjectionFactory(\n\t\t\tFactory<? extends OriginMapProjection> mapProjectionFactory) {\n\t\tthis.mapProjectionFactory = mapProjectionFactory;\n\t}\n\t\n\t/**\n\t * sets the factory that will make {@link EleConstraintEnforcer}\n\t * instances during subsequent calls to\n\t * {@link #createRepresentations(OSMData, List, Configuration, List)}.\n\t * \n\t * @see DefaultFactory\n\t */\n\tpublic void setEleConstraintEnforcerFactory(\n\t\t\tFactory<? extends EleConstraintEnforcer> interpolatorFactory) {\n\t\tthis.eleConstraintEnforcerFactory = interpolatorFactory;\n\t}\n\t\n\t/**\n\t * sets the factory that will make {@link TerrainInterpolator}\n\t * instances during subsequent calls to\n\t * {@link #createRepresentations(OSMData, List, Configuration, List)}.\n\t * \n\t * @see DefaultFactory\n\t */\n\tpublic void setTerrainEleInterpolatorFactory(\n\t\t\tFactory<? extends TerrainInterpolator> enforcerFactory) {\n\t\tthis.terrainEleInterpolatorFactory = enforcerFactory;\n\t}\n\t\n\t\n\t/**\n\t * performs all necessary steps to go from\n\t * an OSM file to the renderable {@link WorldObject}s.\n\t * Sends updates to {@link ProgressListener}s.\n\t * \n\t * @param osmFile       file to read OSM data from; != null\n\t * @param worldModules  modules that will create the {@link WorldObject}s\n\t *                      in the result; null to use a default module list\n\t * @param config        set of parameters that controls various aspects\n\t *                      of the modules' behavior; null to use defaults\n\t * @param targets       receivers of the conversion results; can be null if\n\t *                      you want to handle the returned results yourself\n\t */\n\tpublic Results createRepresentations(File osmFile,\n\t\t\tList<WorldModule> worldModules, Configuration config,\n\t\t\tList<Target<?>> targets)\n\t\t\tthrows IOException {\n\n\t\tlong totalStart = TouchMapperProfile.start();\n\t\t\n\t\tif (osmFile == null) {\n\t\t\tthrow new IllegalArgumentException(\"osmFile must not be null\");\n\t\t}\n\t\t\n\t\tOSMData osmData = null;\n\t\tboolean useJOSMHack = false;\n\t\t\n\t\tif (JOSMFileHack.isJOSMGenerated(osmFile)) {\n\t\t\tuseJOSMHack = true;\n\t\t} else {\n\t\t\t\n\t\t\t/* try to read file using Osmosis */\n\t\t\t\n\t\t\ttry {\n\t\t\t\tlong readOsmStart = TouchMapperProfile.start();\n\t\t\t\tosmData = new OsmosisReader(osmFile).getData();\n\t\t\t\tTouchMapperProfile.logMillis(\"input.read_osmosis_ms\", readOsmStart);\n\t\t\t} catch (IOException e) {\n\t\t\t\t\n\t\t\t\tSystem.out.println(\"could not read file,\" +\n\t\t\t\t\t\t\" trying workaround for files created by JOSM\");\n\t\t\t\t\n\t\t\t\tuseJOSMHack = true;\n\t\t\t\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* create a temporary \"cleaned up\" file as workaround for JOSM files */\n\t\t\n\t\tif (useJOSMHack) {\n\t\t\t\n\t\t\tFile tempFile;\n\t\t\ttry {\n\t\t\t\tlong josmCleanupStart = TouchMapperProfile.start();\n\t\t\t\ttempFile = JOSMFileHack.createTempOSMFile(osmFile);\n\t\t\t\tTouchMapperProfile.logMillis(\"input.josm_cleanup_ms\", josmCleanupStart);\n\t\t\t} catch (Exception e2) {\n\t\t\t\tthrow new IOException(\"could not read OSM file\" +\n\t\t\t\t\t\t\" (not even with workaround for JOSM files)\", e2);\n\t\t\t}\n\t\t\t\n\t\t\tlong readCleanedStart = TouchMapperProfile.start();\n\t\t\tosmData = new OsmosisReader(tempFile).getData();\n\t\t\tTouchMapperProfile.logMillis(\"input.read_cleaned_osmosis_ms\", readCleanedStart);\n\t\t\t\n\t\t}\n\t\t\n\t\tResults results = createRepresentations(osmData, worldModules, config, targets);\n\t\tTouchMapperProfile.logMillis(\"conversion.from_file_total_ms\", totalStart);\n\t\treturn results;\n\t\t\n\t}\n\t\n\t\n\t/**\n\t * variant of\n\t * {@link #createRepresentations(File, List, Configuration, List)}\n\t * that accepts {@link OSMData} instead of a file.\n\t * Use this when all data is already\n\t * in memory, for example with editor applications.\n\t * \n\t * @param osmData       input data; != null\n\t * @param worldModules  modules that will create the {@link WorldObject}s\n\t *                      in the result; null to use a default module list\n\t * @param config        set of parameters that controls various aspects\n\t *                      of the modules' behavior; null to use defaults\n\t * @param targets       receivers of the conversion results; can be null if\n\t *                      you want to handle the returned results yourself\n\t * \n\t * @throws BoundingBoxSizeException  for oversized bounding boxes\n\t */\n\tpublic Results createRepresentations(OSMData osmData,\n\t\t\tList<WorldModule> worldModules, Configuration config,\n\t\t\tList<Target<?>> targets)\n\t\t\tthrows IOException, BoundingBoxSizeException {\n\n\t\tlong totalStart = TouchMapperProfile.start();\n\t\t\n\t\t/* check the inputs */\n\t\t\n\t\tif (osmData == null) {\n\t\t\tthrow new IllegalArgumentException(\"osmData must not be null\");\n\t\t}\n\t\t\n\t\tif (config == null) {\n\t\t\tconfig = new BaseConfiguration();\n\t\t}\n\t\t\n\t\tDouble maxBoundingBoxDegrees = config.getDouble(\"maxBoundingBoxDegrees\", null);\n\t\tif (maxBoundingBoxDegrees != null) {\n\t\t\tfor (Bound bound : osmData.getBounds()) {\n\t\t\t\tif (bound.getTop() - bound.getBottom() > maxBoundingBoxDegrees\n\t\t\t\t\t\t|| bound.getRight() - bound.getLeft() > maxBoundingBoxDegrees) {\n\t\t\t\t\tthrow new BoundingBoxSizeException(bound);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/* create map data from OSM data */\n\t\tupdatePhase(Phase.MAP_DATA);\n\t\t\n\t\tOriginMapProjection mapProjection = mapProjectionFactory.make();\n\t\tlong mapProjectionStart = TouchMapperProfile.start();\n\t\tmapProjection.setOrigin(osmData);\n\t\tTouchMapperProfile.logMillis(\"map_data.map_projection_origin_ms\",\n\t\t\t\tmapProjectionStart);\n\t\t\n\t\tOSMToMapDataConverter converter = new OSMToMapDataConverter(mapProjection, config);\n\t\tlong mapDataCreationStart = TouchMapperProfile.start();\n\t\tMapData mapData = converter.createMapData(osmData);\n\t\tTouchMapperProfile.logMillis(\"map_data.converter_total_ms\",\n\t\t\t\tmapDataCreationStart);\n\t\t\n\t\tAxisAlignedBoundingBoxXZ boundary = mapData.getBoundary();\n\t\t\n\t\t/* apply world modules */\n\t\tupdatePhase(Phase.REPRESENTATION);\n\t\t\n\t\tif (worldModules == null) {\n\t\t\tworldModules = createDefaultModuleList();\n\t\t}\n\t\t\n\t\tMaterials.configureMaterials(config);\n\t\t\t//this will cause problems if multiple conversions are run\n\t\t\t//at the same time, because global variables are being modified\n\t\t\n\t\tWorldCreator moduleManager =\n\t\t\tnew WorldCreator(config, worldModules);\n\t\tlong worldModulesStart = TouchMapperProfile.start();\n\t\tmoduleManager.addRepresentationsTo(mapData);\n\t\tTouchMapperProfile.logMillis(\"representation.world_creator_total_ms\",\n\t\t\t\tworldModulesStart);\n\t\t\n\t\t/* determine elevations */\n\t\tupdatePhase(Phase.ELEVATION);\n\t\t\n\t\tString srtmDir = config.getString(\"srtmDir\", null);\n\t\tTerrainElevationData eleData = null;\n\t\t\t\t\n\t\tif (srtmDir != null) {\n\t\t\teleData = new SRTMData(new File(srtmDir), mapProjection);\n\t\t}\n\t\t\n\t\tlong elevationsStart = TouchMapperProfile.start();\n\t\tcalculateElevations(mapData, eleData, config);\n\t\tTouchMapperProfile.logMillis(\"elevation.total_ms\", elevationsStart);\n\t\t\n\t\t/* create terrain */\n\t\tupdatePhase(Phase.TERRAIN); //TODO this phase may be obsolete\n\t\t\t\t\n\t\t/* supply results to targets and caller */\n\t\tupdatePhase(Phase.FINISHED);\n\t\t\n\t\tboolean underground = config.getBoolean(\"renderUnderground\", true);\n\t\t\n\t\tif (targets != null) {\n\t\t\tlong targetsStart = TouchMapperProfile.start();\n\t\t\tfor (Target<?> target : targets) {\n\t\t\t\tTargetUtil.renderWorldObjects(target, mapData, underground);\n\t\t\t\ttarget.finish();\n\t\t\t}\n\t\t\tTouchMapperProfile.logMillis(\"output.targets_render_and_finish_ms\",\n\t\t\t\t\ttargetsStart);\n\t\t}\n\n\t\tlong addressesStart = TouchMapperProfile.start();\n\t\tAddressGatherer.gather(osmData, mapData);\n\t\tTouchMapperProfile.logMillis(\"output.address_gather_ms\", addressesStart);\n\t\tTouchMapperProfile.logMillis(\"conversion.from_osm_data_total_ms\", totalStart);\n\t\t\n\t\treturn new Results(mapProjection, mapData, eleData);\n\t\t\n\t}\n\t\n\t/**\n\t * uses OSM data and an terrain elevation data (usually from an external\n\t * source) to calculate elevations for all {@link EleConnector}s of the\n\t * {@link WorldObject}s\n\t */\n\tprivate void calculateElevations(MapData mapData,\n\t\t\tTerrainElevationData eleData, Configuration config) {\n\t\t\t\t\n\t\tfinal TerrainInterpolator interpolator =\n\t\t\t\t(eleData != null)\n\t\t\t\t? terrainEleInterpolatorFactory.make()\n\t\t\t\t: new ZeroInterpolator();\n\t\t\n\t\t/* provide known elevations from eleData to the interpolator */\n\t\t\n\t\tStopWatch stopWatch = new StopWatch();\n\t\tstopWatch.start();\n\t\t\t\t\n\t\tif (!(interpolator instanceof ZeroInterpolator)) {\n\t\t\n\t\t\tCollection<VectorXYZ> sites = emptyList();\n\t\t\t\n\t\t\ttry {\n\t\t\t\t\t\t\t\n\t\t\t\tsites = eleData.getSites(mapData);\n\t\t\t\t\n\t\t\t\tSystem.out.println(\"time getSites: \" + stopWatch);\n\t\t\t\tstopWatch.reset();\n\t\t\t\tstopWatch.start();\n\t\t\t\t\n\t\t\t} catch (IOException e) {\n\t\t\t\te.printStackTrace();\n\t\t\t}\n\t\t\t\n\t\t\tinterpolator.setKnownSites(sites);\n\t\t\t\n\t\t\tSystem.out.println(\"time setKnownSites: \" + stopWatch);\n\t\t\tstopWatch.reset();\n\t\t\tstopWatch.start();\n\t\t\t\n\t\t}\n\t\t\n\t\t/* interpolate connectors' elevations */\n\t\t\n\t\tfinal List<EleConnector> connectors = new ArrayList<EleConnector>();\n\t\t\n\t\tFaultTolerantIterationUtil.iterate(mapData.getWorldObjects(),\n\t\t\t\tnew Operation<WorldObject>() {\n\t\t\t@Override public void perform(WorldObject worldObject) {\n\t\t\t\t\n\t\t\t\tfor (EleConnector conn : worldObject.getEleConnectors()) {\n\t\t\t\t\tconn.setPosXYZ(interpolator.interpolateEle(conn.pos));\n\t\t\t\t\tconnectors.add(conn);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t});\n\t\t\n\t\tSystem.out.println(\"time terrain interpolation: \" + stopWatch);\n\t\tstopWatch.reset();\n\t\tstopWatch.start();\n\t\t\n\t\t/* enforce constraints defined by WorldObjects */\n\t\t\n\t\tboolean debugConstraints = config.getBoolean(\"debugConstraints\", false);\n\t\t\n\t\tfinal EleConstraintEnforcer enforcer = debugConstraints\n\t\t\t\t? new EleConstraintValidator(mapData,\n\t\t\t\t\t\teleConstraintEnforcerFactory.make())\n\t\t\t\t: eleConstraintEnforcerFactory.make();\n\t\t\n\t\tenforcer.addConnectors(connectors);\n\t\t\n\t\tif (!(enforcer instanceof NoneEleConstraintEnforcer)) {\n\t\t\t\n\t\t\tFaultTolerantIterationUtil.iterate(mapData.getWorldObjects(),\n\t\t\t\t\tnew Operation<WorldObject>() {\n\t\t\t\t@Override public void perform(WorldObject worldObject) {\n\t\t\t\t\t\n\t\t\t\t\tworldObject.defineEleConstraints(enforcer);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t});\n\t\t\t\n\t\t}\n\t\t\n\t\tSystem.out.println(\"time add constraints: \" + stopWatch);\n\t\tstopWatch.reset();\n\t\tstopWatch.start();\n\t\t\n\t\tenforcer.enforceConstraints();\n\t\t\n\t\tSystem.out.println(\"time enforce constraints: \" + stopWatch);\n\t\tstopWatch.reset();\n\t\tstopWatch.start();\n\t\t\n\t}\n\t\n\tpublic static enum Phase {\n\t\tMAP_DATA,\n\t\tREPRESENTATION,\n\t\tELEVATION,\n\t\tTERRAIN,\n\t\tFINISHED\n\t}\n\t\n\t/**\n\t * implemented by classes that want to be informed about\n\t * a conversion run's progress\n\t */\n\tpublic static interface ProgressListener {\n\t\t\t\t\n\t\t/** announces the start of a new phase */\n\t\tpublic void updatePhase(Phase newPhase);\n\t\t\n//\t\t/** announces the fraction of the current phase that is completed */\n//\t\tpublic void updatePhaseProgress(float phaseProgress);\n\n\t}\n\t\n\tprivate List<ProgressListener> listeners = new ArrayList<ProgressListener>();\n\t\n\tpublic void addProgressListener(ProgressListener listener) {\n\t\tlisteners.add(listener);\n\t}\n\t\n\tprivate void updatePhase(Phase newPhase) {\n\t\tfor (ProgressListener listener : listeners) {\n\t\t\tlistener.updatePhase(newPhase);\n\t\t}\n\t}\n\t\n//\tprivate void updatePhaseProgress(float phaseProgress) {\n//\t\tfor (ProgressListener listener : listeners) {\n//\t\t\tlistener.updatePhaseProgress(phaseProgress);\n//\t\t}\n//\t}\n\t\n\t/**\n\t * exception to be thrown if the OSM input data covers an area\n\t * larger than the maxBoundingBoxDegrees config property\n\t */\n\tpublic static class BoundingBoxSizeException extends RuntimeException {\n\t\t\n\t\tprivate static final long serialVersionUID = 2841146365929523046L; //generated VersionID\n\t\tpublic final Bound bound;\n\n\t\tprivate BoundingBoxSizeException(Bound bound) {\n\t\t\tthis.bound = bound;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"oversized bounding box: \" + bound;\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/GlobalValues.java",
    "content": "package org.osm2world.core;\n\n/**\n * globally accessible constants\n */\npublic final class GlobalValues {\n\n\tprivate GlobalValues() {};\n\t\n\t/** version string */\n\tpublic static final String VERSION_STRING = \"0.2.0\";\n\t\n\t/** link to the OSM wiki documentation */\n\tpublic static final String WIKI_URI = \"http://wiki.osm.org/OSM2World\";\n\n\t/** link to OSM2World home page */\n\tpublic static final String OSM2WORLD_URI = \"http://osm2world.org\";\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/heightmap/creation/EmptyTerrainElevationGrid.java",
    "content": "package org.osm2world.core.heightmap.creation;\n\nimport org.osm2world.core.heightmap.data.AbstractCellularTerrainElevation;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXZ;\n\npublic class EmptyTerrainElevationGrid extends\n\t\tAbstractCellularTerrainElevation {\n\t\n\tpublic EmptyTerrainElevationGrid(AxisAlignedBoundingBoxXZ bounds,\n\t\t\tint numPointsX, int numPointsZ) {\n\t\tsuper(bounds, numPointsX, numPointsZ);\n\t}\n\t\n\t@Override\n\tprotected Float getElevation(VectorXZ pos) {\n\t\treturn null;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/heightmap/creation/FlatTerrainElevation.java",
    "content": "package org.osm2world.core.heightmap.creation;\n\nimport org.osm2world.core.heightmap.data.AbstractCellularTerrainElevation;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXZ;\n\npublic class FlatTerrainElevation extends AbstractCellularTerrainElevation {\n\t\n\tpublic FlatTerrainElevation(AxisAlignedBoundingBoxXZ boundary,\n\t\t\tint numPointsX, int numPointsZ) {\n\t\tsuper(boundary, numPointsX, numPointsZ);\n\t}\n\t\n\t@Override\n\tprotected Float getElevation(VectorXZ pos) {\n\t\treturn 0f;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/heightmap/creation/WaveTerrainElevation.java",
    "content": "package org.osm2world.core.heightmap.creation;\n\nimport org.osm2world.core.heightmap.data.AbstractCellularTerrainElevation;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXZ;\n\npublic class WaveTerrainElevation extends AbstractCellularTerrainElevation {\n\t\n\tpublic WaveTerrainElevation(AxisAlignedBoundingBoxXZ boundary,\n\t\t\tint numPointsX, int numPointsZ) {\n\t\tsuper(boundary, numPointsX, numPointsZ);\n\t}\n\t\n\t@Override\n\tprotected Float getElevation(VectorXZ pos) {\n\t\treturn (float) Math.sin(pos.x) * 10;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/heightmap/data/AbstractCellularTerrainElevation.java",
    "content": "package org.osm2world.core.heightmap.data;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.NoSuchElementException;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.PolygonXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\npublic abstract class AbstractCellularTerrainElevation implements\n\t\tCellularTerrainElevation {\n\t\n\tfinal int numPointsX;\n\tfinal int numPointsZ;\n\t\n\tfinal Collection<TerrainPoint> terrainPoints;\n\tfinal TerrainPoint[][] terrainPointGrid;\n\t\n\t@Override\n\tpublic Collection<TerrainPoint> getTerrainPoints() {\n\t\treturn terrainPoints;\n\t}\n\n\t@Override\n\tpublic TerrainPoint[][] getTerrainPointGrid() {\n\t\treturn terrainPointGrid;\n\t}\n\n\t@Override\n\tpublic PolygonXYZ getBoundaryPolygon() {\n\t\t\n\t\tList<VectorXYZ> vertices = new ArrayList<VectorXYZ>();\n\t\t\n\t\t// first row\n\t\tfor (int x = 0; x < numPointsX; x++) {\n\t\t\tvertices.add(vectorXYZForPointAt(x, 0));\n\t\t}\n\n\t\t// last column\n\t\tfor (int z = 1; z < numPointsZ - 1; z++) {\n\t\t\tvertices.add(vectorXYZForPointAt(numPointsX - 1, z));\n\t\t}\n\n\t\t// last row\n\t\tfor (int x = numPointsX - 1; x >= 0; x--) {\n\t\t\tvertices.add(vectorXYZForPointAt(x, numPointsZ - 1));\n\t\t}\n\n\t\t// first column\n\t\tfor (int z = numPointsZ - 2; z >= 0 /* [0][0] will be added again*/; z--) {\n\t\t\tvertices.add(vectorXYZForPointAt(0, z));\n\t\t}\n\t\t\n\t\treturn new PolygonXYZ(vertices);\n\t\t\n\t}\n\n\t@Override\n\tpublic PolygonXZ getBoundaryPolygonXZ() {\n\t\t\n\t\tList<VectorXZ> vertices = new ArrayList<VectorXZ>();\n\t\t\n\t\t// first row\n\t\tfor (int x = 0; x < numPointsX; x++) {\n\t\t\tvertices.add(vectorXZForPointAt(x, 0));\n\t\t}\n\n\t\t// last column\n\t\tfor (int z = 1; z < numPointsZ - 1; z++) {\n\t\t\tvertices.add(vectorXZForPointAt(numPointsX - 1, z));\n\t\t}\n\n\t\t// last row\n\t\tfor (int x = numPointsX - 1; x >= 0; x--) {\n\t\t\tvertices.add(vectorXZForPointAt(x, numPointsZ - 1));\n\t\t}\n\n\t\t// first column\n\t\tfor (int z = numPointsZ - 2; z >= 0 /* [0][0] will be added again*/; z--) {\n\t\t\tvertices.add(vectorXZForPointAt(0, z));\n\t\t}\n\t\t\n\t\treturn new PolygonXZ(vertices);\n\t\t\n\t}\n\n\tprivate VectorXYZ vectorXYZForPointAt(int x, int z) {\n\t\tTerrainPoint point = terrainPointGrid[x][z];\n\t\treturn point.getPos().xyz(point.getEle());\n\t}\n\n\tprivate VectorXZ vectorXZForPointAt(int x, int z) {\n\t\tTerrainPoint point = terrainPointGrid[x][z];\n\t\treturn point.getPos();\n\t}\n\t\n\t@Override\n\tpublic Iterable<? extends TerrainElevationCell> getCells() {\n\t\t\n\t\treturn new Iterable<CellImpl>() {\n\t\t\t@Override public Iterator<CellImpl> iterator() {\n\t\t\t\treturn new CellIterator();\n\t\t\t}\n\t\t};\n\t\t\n\t}\n\t\n\t//TODO (duplicated code): merge with independently written version from IntersectionGrid\n\tprivate final class CellIterator implements Iterator<CellImpl> {\n\n\t\tint currX = -1, currZ = 0;\n\t\t\n\t\t@Override\n\t\tpublic boolean hasNext() {\n\t\t\treturn currX + 1 < numPointsX-1 || currZ + 2 < numPointsZ-1;\n\t\t}\n\n\t\t@Override\n\t\tpublic CellImpl next() {\n\t\t\t\n\t\t\tcurrX += 1;\n\t\t\tif (currX == numPointsX-1) {\n\t\t\t\tcurrZ += 1;\n\t\t\t\tcurrX = 0;\n\t\t\t\tif (currZ == numPointsZ-1) {\n\t\t\t\t\tthrow new NoSuchElementException();\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\treturn new CellImpl(currX, currZ);\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void remove() {\n\t\t\tthrow new UnsupportedOperationException();\n\t\t}\n\t\t\n\t}\n\t\n\tprivate final class CellImpl implements TerrainElevationCell {\n\t\t\n\t\tprivate int leftXIndex;\n\t\tprivate int bottomZIndex;\n\t\t\n\t\tpublic CellImpl(int leftXIndex, int bottomZIndex) {\n\t\t\t\n\t\t\tassert leftXIndex + 1 < numPointsX\n\t\t\t\t&& bottomZIndex + 1 < numPointsZ;\n\t\t\t\n\t\t\tthis.leftXIndex = leftXIndex;\n\t\t\tthis.bottomZIndex = bottomZIndex;\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override public final TerrainPoint getBottomLeft() {\n\t\t\treturn terrainPointGrid[leftXIndex][bottomZIndex];\n\t\t}\n\t\t@Override public final TerrainPoint getTopLeft() {\n\t\t\treturn terrainPointGrid[leftXIndex][bottomZIndex+1];\n\t\t}\n\t\t@Override public final TerrainPoint getBottomRight() {\n\t\t\treturn terrainPointGrid[leftXIndex+1][bottomZIndex];\n\t\t}\n\t\t@Override public final TerrainPoint getTopRight() {\n\t\t\treturn terrainPointGrid[leftXIndex+1][bottomZIndex+1];\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Collection<TerrainPoint> getTerrainPoints() {\n\t\t\tList<TerrainPoint> terrainPoints = new ArrayList<TerrainPoint>(4);\n\t\t\tterrainPoints.add(terrainPointGrid[leftXIndex][bottomZIndex]);\n\t\t\tterrainPoints.add(terrainPointGrid[leftXIndex+1][bottomZIndex]);\n\t\t\tterrainPoints.add(terrainPointGrid[leftXIndex+1][bottomZIndex+1]);\n\t\t\tterrainPoints.add(terrainPointGrid[leftXIndex][bottomZIndex+1]);\n\t\t\treturn terrainPoints;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic final PolygonXYZ getPolygonXYZ() {\n\t\t\tList<VectorXYZ> vertices = new ArrayList<VectorXYZ>(5);\n\t\t\tvertices.add(vectorXYZForPointAt(leftXIndex, bottomZIndex));\n\t\t\tvertices.add(vectorXYZForPointAt(leftXIndex+1, bottomZIndex));\n\t\t\tvertices.add(vectorXYZForPointAt(leftXIndex+1, bottomZIndex+1));\n\t\t\tvertices.add(vectorXYZForPointAt(leftXIndex, bottomZIndex+1));\n\t\t\tvertices.add(vertices.get(0));\n\t\t\treturn new PolygonXYZ(vertices);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic final SimplePolygonXZ getPolygonXZ() {\n\t\t\tList<VectorXZ> vertices = new ArrayList<VectorXZ>(5);\n\t\t\tvertices.add(vectorXZForPointAt(leftXIndex, bottomZIndex));\n\t\t\tvertices.add(vectorXZForPointAt(leftXIndex+1, bottomZIndex));\n\t\t\tvertices.add(vectorXZForPointAt(leftXIndex+1, bottomZIndex+1));\n\t\t\tvertices.add(vectorXZForPointAt(leftXIndex, bottomZIndex+1));\n\t\t\tvertices.add(vertices.get(0));\n\t\t\treturn new SimplePolygonXZ(vertices);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\t\treturn new AxisAlignedBoundingBoxXZ(\n\t\t\t\tMath.min(getTopLeft().getPos().x, getBottomLeft().getPos().x),\n\t\t\t\tMath.min(getBottomLeft().getPos().z, getBottomRight().getPos().z),\n\t\t\t\tMath.max(getTopRight().getPos().x, getBottomRight().getPos().x),\n\t\t\t\tMath.max(getTopLeft().getPos().z, getTopRight().getPos().z));\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"cell at x: \" + leftXIndex + \", z: \" + bottomZIndex;\n\t\t}\n\n\t\t/* auto-generated hashCode and equals follow */\n\t\t\n\t\t@Override\n\t\tpublic int hashCode() {\n\t\t\tfinal int prime = 31;\n\t\t\tint result = 1;\n\t\t\tresult = prime * result + getOuterType().hashCode();\n\t\t\tresult = prime * result + bottomZIndex;\n\t\t\tresult = prime * result + leftXIndex;\n\t\t\treturn result;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean equals(Object obj) {\n\t\t\tif (this == obj)\n\t\t\t\treturn true;\n\t\t\tif (obj == null)\n\t\t\t\treturn false;\n\t\t\tif (getClass() != obj.getClass())\n\t\t\t\treturn false;\n\t\t\tCellImpl other = (CellImpl) obj;\n\t\t\tif (!getOuterType().equals(other.getOuterType()))\n\t\t\t\treturn false;\n\t\t\tif (bottomZIndex != other.bottomZIndex)\n\t\t\t\treturn false;\n\t\t\tif (leftXIndex != other.leftXIndex)\n\t\t\t\treturn false;\n\t\t\treturn true;\n\t\t}\n\n\t\tprivate AbstractCellularTerrainElevation getOuterType() {\n\t\t\treturn AbstractCellularTerrainElevation.this;\n\t\t}\n\t\t\n\t}\n\t\n\tpublic AbstractCellularTerrainElevation(AxisAlignedBoundingBoxXZ boundary,\n\t\t\tint numPointsX, int numPointsZ) {\n\n\t\tif (numPointsX < 2 || numPointsZ < 2) {\n\t\t\tthrow new IllegalArgumentException(\"need at least 2x2 points for cell grid\");\n\t\t}\n\t\t\n\t\tthis.numPointsX = numPointsX;\n\t\tthis.numPointsZ = numPointsZ;\n\t\t\n\t\tterrainPoints = new ArrayList<TerrainPoint>(numPointsX * numPointsZ);\n\t\tterrainPointGrid = new TerrainPoint[numPointsX][numPointsZ];\n\n\t\tfor (int x = 0; x < numPointsX; x++) {\n\t\t\tfor (int z = 0; z < numPointsZ; z++) {\n\n\t\t\t\tVectorXZ pos = new VectorXZ(boundary.minX + x * boundary.sizeX()\n\t\t\t\t\t\t/ (numPointsX - 1), boundary.minZ + z * boundary.sizeZ()\n\t\t\t\t\t\t/ (numPointsZ - 1));\n\n\t\t\t\tTerrainPoint terrainPoint = new TerrainPoint(pos,\n\t\t\t\t\t\tgetElevation(pos));\n\n\t\t\t\tterrainPointGrid[x][z] = terrainPoint;\n\t\t\t\tterrainPoints.add(terrainPoint);\n\n\t\t\t}\n\t\t}\n\n\t}\n\n\tprotected abstract Float getElevation(VectorXZ pos);\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/heightmap/data/CellularTerrainElevation.java",
    "content": "package org.osm2world.core.heightmap.data;\n\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.PolygonXZ;\n\n/**\n * Terrain elevation data that consists of\n * points arranged in a grid structure (2d array).\n * \n * The grid\n * <ul>\n * <li>needs to be complete (points might have unknown elevation, though)\n * <li>does <em>not</em> need to have rectangular grid cells, they may be deformed\n * </ul>\n */\npublic interface CellularTerrainElevation extends TerrainElevation {\n\n\t/**\n\t * @return  regular two-dimensional array (not jagged)\n\t */\n\tTerrainPoint[][] getTerrainPointGrid();\n\t\n\t/**\n\t * returns the boundary created from the first and last rows and columns\n\t * of the grid.\n\t * This requires that all {@link TerrainPoint}s' elevations have already\n\t * been set to non-null values.\n\t */\n\tPolygonXYZ getBoundaryPolygon();\n\t\n\t/**\n\t * returns the boundary created from the first and last rows and columns\n\t * of the grid.\n\t */\n\tPolygonXZ getBoundaryPolygonXZ();\n\t\n\t/**\n\t * returns Iterable over cells.\n\t * This is a convenience method for operations that need to be\n\t * performed for all cells. The iterator cannot be used to remove cells.\n\t */\n\tIterable<? extends TerrainElevationCell> getCells();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/heightmap/data/TerrainElevation.java",
    "content": "package org.osm2world.core.heightmap.data;\n\nimport java.util.Collection;\n\n/**\n * elevation data for the ground.\n * Rather abstract interface that doesn't impose restrictions\n * on the order of points with known elevation.\n */\npublic interface TerrainElevation {\n\t\n\tpublic Collection<TerrainPoint> getTerrainPoints();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/heightmap/data/TerrainElevationCell.java",
    "content": "package org.osm2world.core.heightmap.data;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\n\npublic interface TerrainElevationCell extends IntersectionTestObject {\n\n\tpublic TerrainPoint getTopLeft();\n\tpublic TerrainPoint getBottomLeft();\n\tpublic TerrainPoint getTopRight();\n\tpublic TerrainPoint getBottomRight();\n\t\n\tpublic Collection<TerrainPoint> getTerrainPoints();\n\t\t\n\t/** returns the counterclockwise polygon surrounding this cell. */\n\tpublic SimplePolygonXZ getPolygonXZ();\n\t\n\t/**\n\t * returns 3d polygon surrounding this cell.\n\t * ordering and XZ coordinates are the same as for {@link #getPolygonXZ()}.\n\t */\n\tpublic PolygonXYZ getPolygonXYZ();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/heightmap/data/TerrainPoint.java",
    "content": "package org.osm2world.core.heightmap.data;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\npublic class TerrainPoint {\n\t\n\tprivate final VectorXZ pos;\n\tprivate Float ele;\n\t\n\tpublic TerrainPoint(VectorXZ pos, Float ele) {\n\t\tthis.pos = pos;\n\t\tthis.ele = ele;\n\t}\n\t\n\tpublic VectorXZ getPos() {\n\t\treturn pos;\n\t}\n\t\n\tpublic VectorXYZ getPosXYZ() {\n\t\treturn pos.xyz(ele);\n\t}\n\t\n\t/**\n\t * returns the point's elevation;\n\t * null indicates an unknown elevation\n\t */\n\tpublic Float getEle() {\n\t\treturn ele;\n\t}\n\t\n\t/**\n\t * sets the point's elevation;\n\t * null indicates an unknown elevation\n\t */\n\tpublic void setEle(float ele) {\n\t\tthis.ele = ele;\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"(\" + pos + \",\" + ele + \")\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/heightmap/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nterrain heightmaps\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/EmptyTerrainBuilder.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport static java.lang.Math.min;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.EmptyTagGroup;\nimport org.openstreetmap.josm.plugins.graphview.core.data.MapBasedTagGroup;\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorGridXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.data.OSMNode;\nimport org.osm2world.core.osm.data.OSMWay;\n\n/**\n * utility class for building geometry representing empty terrain.\n */\npublic class EmptyTerrainBuilder {\n\n\t/** prevents instantiation */\n\tprivate EmptyTerrainBuilder() { }\n\t\n\t/** tag to be internally used on faked ways around \"empty terrain\" */\n\tpublic static final Tag EMPTY_SURFACE_TAG =\n\t\t\tnew Tag(\"surface\", \"osm2world:empty_terrain\");\n\n\t/** faked outline node for the terrain areas */\n\tprivate static final OSMNode EMPTY_SURFACE_NODE = new OSMNode(\n\t\t\tDouble.NaN, Double.NaN, EmptyTagGroup.EMPTY_TAG_GROUP, 0);\n\t\n\t/** faked outline way for the terrain areas */\n\tprivate static final OSMWay EMPTY_SURFACE_WAY = new OSMWay(\n\t\t\tnew MapBasedTagGroup(EMPTY_SURFACE_TAG), 0,\n\t\t\tCollections.<OSMNode>emptyList());\n\t\n\tpublic static final double POINT_GRID_DIST = 30;\n\tpublic static final int PATCH_SIZE_POINTS = 10;\n\t\n\t/**\n\t * creates a grid of square {@link MapArea}s to represent empty terrain.\n\t * The areas are connected with each other, but do not overlap,\n\t * and cover the entire data bounds.\n\t * \n\t * These areas do not come from OSM data, but they are treated the same\n\t * as mapped areas later on to avoid unnecessary special case handling.\n\t */\n\tstatic void createAreasForEmptyTerrain(List<MapNode> mapNodes,\n\t\t\tList<MapArea> mapAreas, AxisAlignedBoundingBoxXZ dataBounds) {\n\t\t\n\t\tVectorGridXZ posGrid = new VectorGridXZ(\n\t\t\t\tdataBounds.pad(POINT_GRID_DIST), POINT_GRID_DIST);\n\t\t\n\t\t/* create a grid of nodes (leaving points within the future patches blank) */\n\t\t\n\t\tMapNode[][] nodeGrid = new MapNode[posGrid.sizeX()][posGrid.sizeZ()];\n\t\t\n\t\tfor (int x = 0; x < posGrid.sizeX(); x++) {\n\t\t\tfor (int z = 0; z < posGrid.sizeZ(); z++) {\n\t\t\t\t\n\t\t\t\tif (x % PATCH_SIZE_POINTS == 0 || x == posGrid.sizeX() - 1\n\t\t\t\t\t\t|| z % PATCH_SIZE_POINTS == 0 || z == posGrid.sizeZ() - 1) {\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ pos = posGrid.get(x, z);\n\t\t\t\t\t\n\t\t\t\t\tMapNode mapNode = new MapNode(pos, EMPTY_SURFACE_NODE);\n\t\t\t\t\t\n\t\t\t\t\tnodeGrid[x][z] = mapNode;\n\t\t\t\t\tmapNodes.add(mapNode);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t\t/* create a grid of areas based on the nodes */\n\t\t\t\n\t\t// calculate the number of patches, but always round up\n\t\tint numPatchesX = (nodeGrid.length + PATCH_SIZE_POINTS - 2) / PATCH_SIZE_POINTS;\n\t\tint numPatchesZ = (nodeGrid[0].length + PATCH_SIZE_POINTS - 2) / PATCH_SIZE_POINTS;\n\n\t\tfor (int patchX = 0; patchX < numPatchesX; patchX++) {\n\t\t\tfor (int z = 0; z < numPatchesZ; z++) {\n\t\t\t\t\n\t\t\t\tmapAreas.add(createAreaForPatch(nodeGrid,\n\t\t\t\t\t\tpatchX * PATCH_SIZE_POINTS,\n\t\t\t\t\t\tz * PATCH_SIZE_POINTS));\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static MapArea createAreaForPatch(MapNode[][] nodeGrid,\n\t\t\tint startX, int startZ) {\n\t\t\n\t\tint endX = min(startX + PATCH_SIZE_POINTS + 1, nodeGrid.length);\n\t\tint endZ = min(startZ + PATCH_SIZE_POINTS + 1, nodeGrid[0].length);\n\t\t\n\t\tList<MapNode> nodes = new ArrayList<MapNode>();\n\t\t\n\t\t// first row\n\t\tfor (int x = startX; x < endX; x++) {\n\t\t\tnodes.add(nodeGrid[x][startZ]);\n\t\t}\n\t\t\n\t\t// last column\n\t\tfor (int z = startZ + 1; z < endZ - 1; z++) {\n\t\t\tnodes.add(nodeGrid[endX - 1][z]);\n\t\t}\n\t\t\n\t\t// last row\n\t\tfor (int x = endX - 1; x >= startX; x--) {\n\t\t\tnodes.add(nodeGrid[x][endZ - 1]);\n\t\t}\n\t\t\n\t\t// first column\n\t\tfor (int z = endZ - 2; z >= startZ /* start will be added again */; z--) {\n\t\t\tnodes.add(nodeGrid[startX][z]);\n\t\t}\n\t\t\n\t\treturn new MapArea(EMPTY_SURFACE_WAY, nodes);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/LatLon.java",
    "content": "package org.osm2world.core.map_data.creation;\n\n/**\n * an immutable coordinate pair with latitude and longitude\n */\npublic class LatLon {\n\t\n\tpublic final double lat;\n\tpublic final double lon;\n\t\n\tpublic LatLon(double lat, double lon) {\n\t\tthis.lat = lat;\n\t\tthis.lon = lon;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/MapProjection.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * function that converts latitude/longitude coordinates\n * to internally used x/z coordinates\n */\npublic interface MapProjection {\n\t\n\tpublic VectorXZ calcPos(double lat, double lon);\n\t\n\tpublic VectorXZ calcPos(LatLon latlon);\n\t\n\t/**\n\t * inverse for {@link #calcPos(double, double)}\n\t */\n\tpublic double calcLat(VectorXZ pos);\n\t\n\t/**\n\t * inverse for {@link #calcPos(double, double)}\n\t */\n\tpublic double calcLon(VectorXZ pos);\n\t\n\t/**\n\t * returns a vector that points one meter to the north\n\t * from the position that becomes the coordinate origin\n\t */\n\tpublic VectorXZ getNorthUnit();\n\n\t/**\n\t * returns the origin (i.e. the latlon that maps to (0,0)\n\t */\n\tpublic LatLon getOrigin();\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/MercatorProjection.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport static java.lang.Math.PI;\nimport static java.lang.Math.atan;\nimport static java.lang.Math.exp;\nimport static java.lang.Math.log;\nimport static java.lang.Math.pow;\nimport static java.lang.Math.sin;\nimport static java.lang.Math.sqrt;\nimport static java.lang.Math.tan;\nimport static java.lang.Math.toDegrees;\nimport static java.lang.Math.toRadians;\n\nfinal class MercatorProjection {\n\n\tprivate MercatorProjection() {\n\t}\n\n\tprivate static final double R_MAJOR = 6378137.0;\n\tprivate static final double R_MINOR = 6356752.3142;\n\tprivate static final double RATIO = R_MINOR / R_MAJOR;\n\tprivate static final double ECCENT = sqrt(1.0 - (RATIO * RATIO));\n\tprivate static final double COM = 0.5 * ECCENT;\n\tpublic static final double EARTH_CIRCUMFERENCE = 40075016.686;\n\n\t/**\n\t *  Calculate earth circumference at given latitude.\n\t */\n\tpublic static double earthCircumference(double latitude) {\n\t\treturn EARTH_CIRCUMFERENCE * Math.cos(toRadians(latitude));\n\t}\n\n\t/**\n\t * Convert longitude to Mercator projection (range [0..1]).\n\t */ \n\tpublic static double lonToX(double longitude) {\n\t\treturn (longitude + 180.0) / 360.0;\n\t}\n\n\t/**\n\t * Convert from Mercator projection (range [0..1]) to longitude.\n\t */\n\tpublic static double xToLon(double x) {\n\t\treturn 360.0 * (x - 0.5);\n\t}\n\n\t/**\n\t * Convert latitude to Mercator projection (range [0..1]).\n\t */ \n\tpublic static double latToY(double latitude) {\n\t\tdouble sinLat = Math.sin(toRadians(latitude));\n\t\treturn Math.log((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Math.PI) + 0.5;\n\t}\n\n\t/**\n\t * Convert from Mercator projection (range [0..1]) to latitude.\n\t */\n\tpublic static double yToLat(double y) {\n\t\treturn 360.0 * Math.atan(Math.exp((y - 0.5) * (2.0 * Math.PI))) / Math.PI - 90.0;\n\t}\n\t// This is for the Elliptical Mercator version\n\tpublic static double latToYElliptical(double lat) {\n\t\tlat = Math.min(89.5, Math.max(lat, -89.5));\n\t\tdouble phi = toRadians(lat);\n\t\tdouble sinphi = sin(phi);\n\t\tdouble con = ECCENT * sinphi;\n\t\tcon = pow(((1.0 - con) / (1.0 + con)), COM);\n\t\tdouble ts = tan(0.5 * ((PI * 0.5) - phi)) / con;\n\t\treturn 0 - R_MAJOR * log(ts);\n\t}\n\n\t// This is for the Elliptical Mercator version\n\tpublic static double yToLatElliptical(double y) {\n\t\tdouble ts = exp(-y / R_MAJOR);\n\t\tdouble phi = PI / 2 - 2 * atan(ts);\n\t\tdouble dphi = 1.0;\n\t\tint i = 0;\n\t\twhile ((Math.abs(dphi) > 0.000000001) && (i < 15))\n\t\t{\n\t\t\tdouble con = ECCENT * sin(phi);\n\t\t\tdphi = PI / 2 - 2 * atan(ts * Math.pow((1.0 - con) / (1.0 + con), COM)) - phi;\n\t\t\tphi += dphi;\n\t\t\ti++;\n\t\t}\n\t\treturn toDegrees(phi);\n\t}\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/MetricMapProjection.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport static org.osm2world.core.map_data.creation.MercatorProjection.*;\n\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * Map projection that is intended to use the \"dense\" space\n * of floating point values by making all coordinates relative to\n * the origin. 1 meter distance is roughly represented by 1 internal unit.\n */\npublic class MetricMapProjection extends OriginMapProjection {\n\t\n\tprivate double originX;\n\tprivate double originY;\n\tprivate double scaleFactor;\n\t\t\n\tpublic VectorXZ calcPos(double lat, double lon) {\n\t\tdouble x = lonToX(lon) * scaleFactor - originX;\n\t\tdouble y = latToY(lat) * scaleFactor - originY;\n\n\t\t/* snap to som cm precision, seems to reduce geometry exceptions */\n\t\tx = Math.round(x * 1000) / 1000.0d;\n\t\ty = Math.round(y * 1000) / 1000.0d;\n\n\t\treturn new VectorXZ(x, y); // x and z(!) are 2d here\n\t}\n\n\t@Override\n\tpublic VectorXZ calcPos(LatLon latlon) {\n\t\treturn calcPos(latlon.lat, latlon.lon);\n\t}\n\n\t@Override\n\tpublic double calcLat(VectorXZ pos) {\n\t\treturn yToLat((pos.z + originY) / scaleFactor);\n\t}\n\n\t@Override\n\tpublic double calcLon(VectorXZ pos) {\n\t\treturn xToLon((pos.x + originX) / scaleFactor);\n\t}\n\n\t@Override\n\tpublic VectorXZ getNorthUnit() {\n\t\treturn VectorXZ.Z_UNIT;\n\t}\n\n\t@Override\n\tpublic void setOrigin(LatLon origin) {\n\t\tsuper.setOrigin(origin);\n\n\t\tthis.scaleFactor = earthCircumference(origin.lat);\n\t\tthis.originY = latToY(origin.lat) * scaleFactor;\n\t\tthis.originX = lonToX(origin.lon) * scaleFactor;\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/MultipolygonAreaBuilder.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport static java.lang.Boolean.*;\nimport static java.lang.Double.NaN;\nimport static java.lang.Math.*;\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.*;\nimport static org.osm2world.core.math.GeometryUtil.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.MapBasedTagGroup;\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\nimport org.osm2world.core.osm.data.OSMData;\nimport org.osm2world.core.osm.data.OSMElement;\nimport org.osm2world.core.osm.data.OSMMember;\nimport org.osm2world.core.osm.data.OSMNode;\nimport org.osm2world.core.osm.data.OSMRelation;\nimport org.osm2world.core.osm.data.OSMWay;\nimport org.osm2world.core.osm.ruleset.HardcodedRuleset;\nimport org.osm2world.core.osm.ruleset.Ruleset;\n\n/**\n * utility class for creating areas from multipolygon relations,\n * including those with non-closed member ways.\n * \n * Known Limitations:<ul>\n * <li>This cannot reliably handle touching inner rings consisting\n * of non-closed ways.</li>\n * <li>Closed touching rings will not break the calculation,\n * but are represented as multiple touching holes.</li>\n * </ul>\n */\nfinal class MultipolygonAreaBuilder {\n\t\n\t/** prevents instantiation */\n\tprivate MultipolygonAreaBuilder() { }\n\t\n\t/**\n\t * Creates areas for a multipolygon relation.\n\t * Also adds this area to the adjacent nodes using\n\t * {@link MapNode#addAdjacentArea(MapArea)}.\n\t * \n\t * @param relation  the multipolygon relation\n\t * @param nodeMap   map from {@link OSMNode}s to {@link MapNode}s\n\t * \n\t * @return  constructed area(s), multiple areas will be created if there\n\t *          is more than one outer ring. Empty for invalid multipolygons.\n\t */\n\tpublic static final Collection<MapArea> createAreasForMultipolygon(\n\t\t\tOSMRelation relation, Map<OSMNode, MapNode> nodeMap) {\n\t\t\n\t\tif (isSimpleMultipolygon(relation)) {\n\t\t\treturn createAreasForSimpleMultipolygon(relation, nodeMap);\n\t\t} else {\n\t\t\treturn createAreasForAdvancedMultipolygon(relation, nodeMap);\n\t\t}\n\t\t\n\t}\n\n\tprivate static final boolean isSimpleMultipolygon(OSMRelation relation) {\n\t\t\n\t\tint numberOuters = 0;\n\t\tboolean closedWays = true;\n\t\t\n\t\tfor (OSMMember member : relation.relationMembers) {\n\t\t\t\n\t\t\tif (\"outer\".equals(member.role)\n\t\t\t\t\t&& member.member instanceof OSMWay) {\n\t\t\t\tnumberOuters += 1;\n\t\t\t}\n\t\t\t\n\t\t\tif ((\"outer\".equals(member.role) || \"inner\".equals(member.role))\n\t\t\t\t\t&& member.member instanceof OSMWay) {\n\t\t\t\t\n\t\t\t\tif (!((OSMWay)member.member).isClosed()) {\n\t\t\t\t\tclosedWays = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn numberOuters == 1 && closedWays;\n\t\t\n\t}\n\n\t/**\n\t * handles the common simple case with only one outer way.\n\t * Expected to be faster than the more general method\n\t * {@link #createAreasForAdvancedMultipolygon(OSMRelation, Map)}\n\t * \n\t * @param relation  has to be a simple multipolygon relation\n\t * @param nodeMap\n\t */\n\tprivate static final Collection<MapArea> createAreasForSimpleMultipolygon(\n\t\t\tOSMRelation relation, Map<OSMNode, MapNode> nodeMap) {\n\t\t\n\t\tassert isSimpleMultipolygon(relation);\n\t\t\n\t\tOSMElement tagSource = null;\n\t\tList<MapNode> outerNodes = null;\n\t\tList<List<MapNode>> holes = new ArrayList<List<MapNode>>();\n\t\t\n\t\tfor (OSMMember member : relation.relationMembers) {\n\t\t\tif (member.member instanceof OSMWay) {\n\t\t\t\t\n\t\t\t\tOSMWay way = (OSMWay)member.member;\n\t\t\t\t\n\t\t\t\tif (\"inner\".equals(member.role)) {\n\t\t\t\t\t\n\t\t\t\t\tList<MapNode> hole = new ArrayList<MapNode>(way.nodes.size());\n\t\t\t\t\t\n\t\t\t\t\tfor (OSMNode node : ((OSMWay)member.member).nodes) {\n\t\t\t\t\t\thole.add(nodeMap.get(node));\n\t\t\t\t\t\t//TODO: add area as adjacent to node for inners' nodes, too?\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tholes.add(hole);\n\t\t\t\t\t\n\t\t\t\t} else if (\"outer\".equals(member.role)) {\n\t\t\t\t\t\n\t\t\t\t\ttagSource = relation.tags.size() > 1 ? relation : way;\n\t\t\t\t\t\n\t\t\t\t\touterNodes = new ArrayList<MapNode>(way.nodes.size());\n\t\t\t\t\tfor (OSMNode node : way.nodes) {\n\t\t\t\t\t\touterNodes.add(nodeMap.get(node));\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn singleton(new MapArea(tagSource, outerNodes, holes));\n\t\t\n\t}\n\n\tprivate static final Collection<MapArea> createAreasForAdvancedMultipolygon(\n\t\t\tOSMRelation relation, Map<OSMNode, MapNode> nodeMap) {\n\t\t\n\t\tList<NodeSequence> innersAndOuters = new ArrayList<NodeSequence>();\n\t\t\n\t\t/* collect ways */\n\t\t\n\t\tfor (OSMMember member : relation.relationMembers) {\n\t\t\tif (member.member instanceof OSMWay\n\t\t\t\t\t&& (\"outer\".equals(member.role)\n\t\t\t\t\t\t\t|| \"inner\".equals(member.role)) ) {\n\t\t\t\t\n\t\t\t\tinnersAndOuters.add(new NodeSequence(\n\t\t\t\t\t\t(OSMWay)member.member, nodeMap));\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t\t/* build rings, then polygons from the ways */\n\t\t\n\t\tList<Ring> rings = buildRings(innersAndOuters);\n\t\t\n\t\tif (rings != null) {\n\t\t\t\t\t\t\n\t\t\treturn buildPolygonsFromRings(relation, rings);\n\t\t\t\n\t\t} else {\n\t\t\n\t\t\treturn Collections.emptyList();\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * builds closed rings from any mixture of closed and unclosed segments\n\t * \n\t * @return  null if building closed rings isn't possible\n\t */\n\tprivate static final List<Ring> buildRings(\n\t\t\tList<NodeSequence> sequences) {\n\t\t\n\t\tList<Ring> closedRings = new ArrayList<Ring>();\n\t\t\n\t\tNodeSequence currentRing = null;\n\t\t\n\t\twhile (sequences.size() > 0) {\n\t\t\t\n\t\t\tif (currentRing == null) {\n\t\t\t\t\n\t\t\t\t// start a new ring with any remaining node sequence\n\t\t\t\t\n\t\t\t\tcurrentRing = sequences.remove(sequences.size() - 1);\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\t// try to continue the ring by appending a node sequence\n\t\t\t\t\n\t\t\t\tNodeSequence assignedSequence = null;\n\t\t\t\t\n\t\t\t\tfor (NodeSequence sequence : sequences) {\n\t\t\t\t\n\t\t\t\t\tif (currentRing.tryAdd(sequence)) {\n\t\t\t\t\t\tassignedSequence = sequence;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (assignedSequence != null) {\n\t\t\t\t\tsequences.remove(assignedSequence);\n\t\t\t\t} else {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t// check whether the ring under construction is closed\n\t\t\t\n\t\t\tif (currentRing != null && currentRing.isClosed()) {\n\t\t\t\ttry {\n\t\t\t\t\tclosedRings.add(new Ring(currentRing));\n\t\t\t\t\tcurrentRing = null;\n\t\t\t\t} catch (InvalidGeometryException e) {\n\t\t\t\t\tthrow new InvalidGeometryException(String.format(\n\t\t\t\t\t\t\t\"self-intersecting ring (with %d nodes)\",\n\t\t\t\t\t\t\tcurrentRing.size() - 1), e);\n\t\t\t\t}\n\t\t\t}\n\t\t\n\t\t}\n\t\t\n\t\tif (currentRing != null) {\n\t\t\t// the last ring could not be closed\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\treturn closedRings;\n\t\t\n\t}\n\n\t/**\n\t * @param rings  rings to build polygons from; will be empty afterwards\n\t */\n\tprivate static final Collection<MapArea> buildPolygonsFromRings(\n\t\t\tOSMRelation relation, List<Ring> rings) {\n\t\t\n\t\tCollection<MapArea> finishedPolygons =\n\t\t\t\tnew ArrayList<MapArea>(rings.size() / 2);\n\t\t\n\t\t/* build polygon */\n\t\t\n\t\twhile (!rings.isEmpty()) {\n\t\t\t\n\t\t\t/* find an outer ring */\n\t\t\t\n\t\t\tRing outerRing = null;\n\t\t\t\n\t\t\tfor (Ring candidate : rings) {\n\t\t\t\t\n\t\t\t\tboolean containedInOtherRings = false;\n\t\t\t\t\n\t\t\t\tfor (Ring otherRing : rings) {\n\t\t\t\t\tif (otherRing != candidate\n\t\t\t\t\t\t\t&& otherRing.containsRing(candidate)) {\n\t\t\t\t\t\tcontainedInOtherRings = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (!containedInOtherRings) {\n\t\t\t\t\touterRing = candidate;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* find inner rings of that ring */\n\t\t\t\n\t\t\tCollection<Ring> innerRings = new ArrayList<Ring>();\n\t\t\t\n\t\t\tfor (Ring ring : rings) {\n\t\t\t\tif (ring != outerRing && outerRing.containsRing(ring)) {\n\t\t\t\t\t\n\t\t\t\t\tboolean containedInOtherRings = false;\n\t\t\t\t\t\n\t\t\t\t\tfor (Ring otherRing : rings) {\n\t\t\t\t\t\tif (otherRing != ring && otherRing != outerRing\n\t\t\t\t\t\t\t\t&& otherRing.containsRing(ring)) {\n\t\t\t\t\t\t\tcontainedInOtherRings = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (!containedInOtherRings) {\n\t\t\t\t\t\tinnerRings.add(ring);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* create a new area and remove the used rings */\n\t\t\t\n\t\t\tList<List<MapNode>> holes = new ArrayList<List<MapNode>>(innerRings.size());\n\t\t\tList<SimplePolygonXZ> holesXZ = new ArrayList<SimplePolygonXZ>(innerRings.size());\n\t\t\t\n\t\t\tfor (Ring innerRing : innerRings) {\n\t\t\t\tholes.add(innerRing.closedNodeSequence);\n\t\t\t\tholesXZ.add(innerRing.getPolygon());\n\t\t\t}\n\t\t\t\n\t\t\tMapArea area = new MapArea(relation, outerRing.getNodeLoop(), holes,\n\t\t\t\t\tnew PolygonWithHolesXZ(outerRing.getPolygon(), holesXZ));\n\t\t\t\n\t\t\tfinishedPolygons.add(area);\n\t\t\t\n\t\t\trings.remove(outerRing);\n\t\t\trings.removeAll(innerRings);\n\t\t\t\n\t\t}\n\t\t\n\t\treturn finishedPolygons;\n\t\t\n\t}\n\t\n\tprivate static final TagGroup COASTLINE_NODE_TAGS = new MapBasedTagGroup(\n\t\t\tnew Tag(\"osm2world:note\", \"fake node from coastline processing\"));\n\t\n\t/**\n\t * turns all coastline ways into {@link MapArea}s\n\t * based on an artificial natural=water multipolygon relation.\n\t * \n\t * It relies on the direction-dependent drawing of coastlines.\n\t * If coastlines are incomplete, then it is attempted to connect them\n\t * to proper rings. One assumption being used is that they are complete\n\t * within the file's bounds.\n\t * \n\t * It cannot distinguish between water and land tiles if there is no\n\t * coastline at all (it will then guess based on the tags being used),\n\t * but should be able to handle all other cases.\n\t */\n\tpublic static final Collection<MapArea> createAreasForCoastlines(\n\t\t\tOSMData osmData, Map<OSMNode, MapNode> nodeMap,\n\t\t\tCollection<MapNode> mapNodes, AxisAlignedBoundingBoxXZ fileBoundary) {\n\t\t\n\t\tlong highestRelationId = 0;\n\t\tlong highestNodeId = 0;\n\t\t\n\t\tList<OSMWay> coastlineWays = new ArrayList<OSMWay>();\n\t\t\n\t\tfor (OSMWay way : osmData.getWays()) {\n\t\t\tif (way.tags.contains(\"natural\", \"coastline\")) {\n\t\t\t\tcoastlineWays.add(way);\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (OSMRelation relation : osmData.getRelations()) {\n\t\t\tif (relation.id > highestRelationId) {\n\t\t\t\thighestRelationId = relation.id;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (OSMNode node : osmData.getNodes()) {\n\t\t\tif (node.id > highestNodeId) {\n\t\t\t\thighestNodeId = node.id;\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (fileBoundary != null) {\n\t\t\t\n\t\t\t/* build node sequences (may be closed or unclosed) */\n\t\t\t\n\t\t\tList<NodeSequence> origCoastlines = new ArrayList<NodeSequence>();\n\t\t\t\n\t\t\tfor (OSMWay coastlineWay : coastlineWays) {\n\t\t\t\torigCoastlines.add(new NodeSequence(coastlineWay, nodeMap));\n\t\t\t}\n\t\t\t\n\t\t\t/* find coastline intersections with bounding box.\n\t\t\t * They will be inserted into the rings that intersect the coastline,\n\t\t\t * and into a list (sorted counterclockwise) of intersection nodes.\n\t\t\t */\n\t\t\t\n\t\t\tList<NodeOnBBox> bBoxNodes = new ArrayList<NodeOnBBox>();\n\t\t\t\n\t\t\tfor (final LineSegmentXZ side : getSidesClockwise(fileBoundary)) {\n\t\t\t\t\n\t\t\t\tList<NodeOnBBox> intersectionsSide =\n\t\t\t\t\t\tnew ArrayList<NodeOnBBox>();\n\t\t\t\t\n\t\t\t\tfor (NodeSequence coastline : origCoastlines) {\n\t\t\t\t\n\t\t\t\t\tfor (int i = 0; i + 1 < coastline.size(); i++) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tVectorXZ r1 = coastline.get(i).getPos();\n\t\t\t\t\t\tVectorXZ r2 = coastline.get(i + 1).getPos();\n\t\t\t\t\t\t\n\t\t\t\t\t\tVectorXZ intersection = getLineSegmentIntersection(\n\t\t\t\t\t\t\t\tside.p1, side.p2, r1, r2);\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (intersection != null) {\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tMapNode intersectionNode;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (intersection.equals(r1)) {\n\t\t\t\t\t\t\t\tintersectionNode = coastline.get(i);\n\t\t\t\t\t\t\t} else if (intersection.equals(r2)) {\n\t\t\t\t\t\t\t\tintersectionNode = coastline.get(i + 1);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tintersectionNode = createFakeMapNode(intersection,\n\t\t\t\t\t\t\t\t\t\t++highestNodeId, osmData, nodeMap, mapNodes);\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tcoastline.add(i + 1, intersectionNode);\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\ti += 1;\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tintersectionsSide.add(new NodeOnBBox(intersectionNode,\n\t\t\t\t\t\t\t\t\tisRightOf(r1, side.p1, side.p2)));\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t/* add intersections for this side of the bbox,\n\t\t\t\t * sorted by distance from corner */\n\t\t\t\t\n\t\t\t\tCollections.sort(intersectionsSide, new Comparator<NodeOnBBox>() {\n\t\t\t\t\t@Override public int compare(NodeOnBBox n1, NodeOnBBox n2) {\n\t\t\t\t\t\treturn Double.compare(\n\t\t\t\t\t\t\t\tn1.node.getPos().distanceTo(side.p1),\n\t\t\t\t\t\t\t\tn2.node.getPos().distanceTo(side.p1));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\t\n\t\t\t\tbBoxNodes.addAll(intersectionsSide);\n\t\t\t\t\n\t\t\t\tMapNode cornerNode = createFakeMapNode(side.p2,\n\t\t\t\t\t\t++highestNodeId, osmData, nodeMap, mapNodes);\n\t\t\t\tbBoxNodes.add(new NodeOnBBox(cornerNode, null));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* rings are possibly shortened or split by removing all nodes\n\t\t\t * outside the bbox. */\n\t\t\t\n\t\t\tList<NodeSequence> modifiedCoastlines = new ArrayList<NodeSequence>();\n\t\t\t\n\t\t\tfor (NodeSequence origCoastline : origCoastlines) {\n\t\t\t\t\n\t\t\t\tNodeSequence modifiedCoastline = new NodeSequence();\n\t\t\t\t\n\t\t\t\tfor (MapNode node : origCoastline) {\n\t\t\t\t\n\t\t\t\t\tboolean isOnBBox = false;\n\t\t\t\t\t\n\t\t\t\t\tfor (NodeOnBBox bBoxNode : bBoxNodes) {\n\t\t\t\t\t\tif (bBoxNode.node.equals(node)) {\n\t\t\t\t\t\t\tisOnBBox = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (fileBoundary.contains(node) || isOnBBox) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tmodifiedCoastline.add(node);\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (!modifiedCoastline.isEmpty()) {\n\t\t\t\t\t\t\tmodifiedCoastlines.add(modifiedCoastline);\n\t\t\t\t\t\t\tmodifiedCoastline = new NodeSequence();\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (!modifiedCoastline.isEmpty()) {\n\t\t\t\t\tmodifiedCoastlines.add(modifiedCoastline);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t\t/* parts of the bounding box between outgoing and incoming\n\t\t\t * intersection nodes are used as additional coastline sections */\n\t\t\t\n\t\t\tList<NodeSequence> bboxSections = new ArrayList<NodeSequence>();\n\t\t\t\n\t\t\tif (bBoxNodes.size() > 4) { //more than just corners\n\t\t\t\t\n\t\t\t\tint firstIntersectionIndex = -1;\n\t\t\t\tint currentIndex = 0;\n\t\t\t\t\n\t\t\t\tList<MapNode> currentSequence = null;\n\t\t\t\t\n\t\t\t\twhile (currentIndex != firstIntersectionIndex) {\n\t\t\t\t\n\t\t\t\t\tNodeOnBBox currentBBoxNode = bBoxNodes.get(currentIndex);\n\t\t\t\t\t\n\t\t\t\t\tif (currentBBoxNode.outgoingIntersection == TRUE) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tcurrentSequence = new ArrayList<MapNode>();\n\t\t\t\t\t\tcurrentSequence.add(currentBBoxNode.node);\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (firstIntersectionIndex == -1) {\n\t\t\t\t\t\t\tfirstIntersectionIndex = currentIndex;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (currentBBoxNode.outgoingIntersection == FALSE) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (currentSequence != null) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tcurrentSequence.add(currentBBoxNode.node);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tNodeSequence finishedBboxPart = new NodeSequence();\n\t\t\t\t\t\t\tfinishedBboxPart.addAll(currentSequence);\n\t\t\t\t\t\t\tbboxSections.add(finishedBboxPart);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tcurrentSequence = null;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (currentSequence != null) {\n\t\t\t\t\t\t\tcurrentSequence.add(currentBBoxNode.node);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tcurrentIndex = (currentIndex + 1) % bBoxNodes.size();\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* construct closed rings and turn them into polygons with holes\n\t\t\t * (as if the coastlines were multipolygon member ways) */\n\t\t\t\t\t\t\n\t\t\tList<Ring> closedRings;\n\t\t\t\n\t\t\tif (!bboxSections.isEmpty()) {\n\t\t\t\t\n\t\t\t\tmodifiedCoastlines.addAll(bboxSections);\n\t\t\t\t\n\t\t\t\tclosedRings = buildRings(modifiedCoastlines);\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\n\t\t\t\tclosedRings = buildRings(modifiedCoastlines);\n\t\t\t\t\n\t\t\t\tif (closedRings != null) {\n\t\t\t\t\t\n\t\t\t\t\t/* if there is an island, but no coastline intersects\n\t\t\t\t\t * the boundary, create a boundary around the entire tile.\n\t\t\t\t\t * Do the same for water tiles (tiles without any land). */\n\t\t\t\t\t\n\t\t\t\t\tboolean hasIsland = false;\n\t\t\t\t\t\n\t\t\t\t\tfor (Ring closedRing : closedRings) {\n\t\t\t\t\t\tif (!closedRing.getPolygon().isClockwise()) {\n\t\t\t\t\t\t\thasIsland = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (hasIsland || isProbablySeaTile(osmData)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tNodeSequence boundaryRing = new NodeSequence();\n\t\t\t\t\t\t\n\t\t\t\t\t\tfor (VectorXZ pos : fileBoundary.polygonXZ().getVertices()) {\n\t\t\t\t\t\t\tboundaryRing.add(createFakeMapNode(pos,\n\t\t\t\t\t\t\t\t\t++highestNodeId, osmData, nodeMap, mapNodes));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tboundaryRing.add(boundaryRing.get(0));\n\t\t\t\t\t\t\n\t\t\t\t\t\tclosedRings.add(new Ring(boundaryRing));\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (closedRings != null) {\n\t\t\t\t\n\t\t\t\tOSMRelation relation = new OSMRelation(new MapBasedTagGroup(\n\t\t\t\t\t\tnew Tag(\"type\", \"multipolygon\"), new Tag(\"natural\", \"water\")),\n\t\t\t\t\t\thighestRelationId + 1, 0);\n\t\t\t\t\n\t\t\t\treturn buildPolygonsFromRings(relation, closedRings);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn emptyList();\n\t\t\n\t}\n\n\tprivate static final List<LineSegmentXZ> getSidesClockwise(\n\t\t\tAxisAlignedBoundingBoxXZ fileBoundary) {\n\t\t\n\t\treturn asList(\n\t\t\t\tnew LineSegmentXZ(fileBoundary.topLeft(), fileBoundary.topRight()),\n\t\t\t\tnew LineSegmentXZ(fileBoundary.topRight(), fileBoundary.bottomRight()),\n\t\t\t\tnew LineSegmentXZ(fileBoundary.bottomRight(), fileBoundary.bottomLeft()),\n\t\t\t\tnew LineSegmentXZ(fileBoundary.bottomLeft(), fileBoundary.topLeft()));\n\t\t\n\t}\n\n\tprivate static MapNode createFakeMapNode(VectorXZ pos, long nodeId,\n\t\t\tOSMData osmData, Map<OSMNode, MapNode> nodeMap,\n\t\t\tCollection<MapNode> mapNodes) {\n\t\t\n\t\tOSMNode osmNode = new OSMNode(NaN, NaN,\n\t\t\t\tCOASTLINE_NODE_TAGS, nodeId + 1);\n\t\tosmData.getNodes().add(osmNode);\n\t\t\t\t\n\t\tMapNode mapNode = new MapNode(pos, osmNode);\n\t\tmapNodes.add(mapNode);\n\t\tnodeMap.put(osmNode, mapNode);\n\t\t\n\t\treturn mapNode;\n\t\t\n\t}\n\t\n\t/**\n\t * guesses whether this is a pure sea tile (no land at all)\n\t */\n\tprivate static boolean isProbablySeaTile(OSMData osmData) {\n\t\t\n\t\tboolean anySeaTag = false;\n\t\t\n\t\tRuleset ruleset = new HardcodedRuleset();\n\t\t\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tList<Collection<? extends OSMElement>> collections = asList(\n\t\t\t\tosmData.getWays(), osmData.getNodes());\n\t\t\n\t\tfor (Collection<? extends OSMElement> collection : collections) {\n\t\t\tfor (OSMElement element : collection) {\n\t\t\t\tfor (Tag tag : element.tags) {\n\t\t\t\t\t\n\t\t\t\t\tif (ruleset.isLandTag(tag)) return false;\n\t\t\t\t\t\n\t\t\t\t\tanySeaTag |= ruleset.isSeaTag(tag);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn anySeaTag;\n\t\t\n\t}\n\t\n\tprivate static final class NodeSequence extends ArrayList<MapNode> {\n\t\t\n\t\tprivate static final long serialVersionUID = -1189277554247756781L; //generated SerialUID\n\n\t\t/**\n\t\t * creates an empty sequence\n\t\t */\n\t\tpublic NodeSequence() {\n\t\t\tsuper();\n\t\t}\n\t\t\n\t\t/**\n\t\t * creates a node sequence from an {@link OSMWay}\n\t\t */\n\t\tpublic NodeSequence(OSMWay way, Map<OSMNode, MapNode> nodeMap) {\n\t\t\t\n\t\t\tsuper(way.nodes.size());\n\t\t\t\n\t\t\tfor (OSMNode wayNode : way.nodes) {\n\t\t\t\tadd(nodeMap.get(wayNode));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * tries to add another sequence onto the start or end of this one.\n\t\t * If it succeeds, the other sequence may also be modified and\n\t\t * should be considered \"spent\".\n\t\t */\n\t\tpublic boolean tryAdd(NodeSequence other) {\n\t\t\t\n\t\t\tif (getLastNode() == other.getFirstNode()) {\n\t\t\t\t\n\t\t\t\t//add the sequence at the end\n\t\t\t\tremove(size() - 1);\n\t\t\t\taddAll(other);\n\t\t\t\treturn true;\n\t\t\t\t\n\t\t\t} else if (getLastNode() == other.getLastNode()) {\n\t\t\t\t\n\t\t\t\t//add the sequence backwards at the end\n\t\t\t\tremove(size() - 1);\n\t\t\t\tCollections.reverse(other);\n\t\t\t\taddAll(other);\n\t\t\t\treturn true;\n\t\t\t\t\n\t\t\t} else if (getFirstNode() == other.getLastNode()) {\n\t\t\t\t\n\t\t\t\t//add the sequence at the beginning\n\t\t\t\tremove(0);\n\t\t\t\taddAll(0, other);\n\t\t\t\treturn true;\n\t\t\t\t\n\t\t\t} else if (getFirstNode() == other.getFirstNode()) {\n\t\t\t\t\n\t\t\t\t//add the sequence backwards at the beginning\n\t\t\t\tremove(0);\n\t\t\t\tCollections.reverse(other);\n\t\t\t\taddAll(0, other);\n\t\t\t\treturn true;\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tprivate MapNode getFirstNode() {\n\t\t\treturn get(0);\n\t\t}\n\n\t\tprivate MapNode getLastNode() {\n\t\t\treturn get(size() - 1);\n\t\t}\n\n\t\tpublic boolean isClosed() {\n\t\t\treturn getFirstNode() == getLastNode();\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class Ring implements IntersectionTestObject {\n\t\t\n\t\tprivate final NodeSequence closedNodeSequence;\n\t\tprivate final SimplePolygonXZ polygon;\n\t\t\n\t\tpublic Ring(NodeSequence closedNodeSequence) {\n\n\t\t\tassert closedNodeSequence.isClosed();\n\t\t\t\n\t\t\tthis.closedNodeSequence = closedNodeSequence;\n\t\t\t\n\t\t\tpolygon = MapArea.polygonFromMapNodeLoop(closedNodeSequence);\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\t\t\n\t\t\tdouble minX = Double.POSITIVE_INFINITY, minZ = Double.POSITIVE_INFINITY;\n\t\t\tdouble maxX = Double.NEGATIVE_INFINITY, maxZ = Double.NEGATIVE_INFINITY;\n\t\t\t\n\t\t\tfor (MapNode n : closedNodeSequence) {\n\t\t\t\tminX = min(minX, n.getPos().x); minZ = min(minZ, n.getPos().z);\n\t\t\t\tmaxX = max(maxX, n.getPos().x); maxZ = max(maxZ, n.getPos().z);\n\t\t\t}\n\t\t\t\n\t\t\treturn new AxisAlignedBoundingBoxXZ(minX, minZ, maxX, maxZ);\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate List<MapNode> getNodeLoop() {\n\t\t\treturn closedNodeSequence;\n\t\t}\n\t\t\n\t\tpublic SimplePolygonXZ getPolygon() {\n\t\t\treturn polygon;\n\t\t}\n\t\t\n\t\tpublic boolean containsRing(Ring other) {\n\t\t\treturn this.getPolygon().contains(other.getPolygon());\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class NodeOnBBox {\n\t\t\n\t\t/** true for outgoing, false for incoming, null for other bbox nodes */\n\t\tpublic final Boolean outgoingIntersection;\n\t\t\n\t\tpublic final MapNode node;\n\t\t\n\t\tprivate NodeOnBBox(MapNode node, Boolean outgoingIntersection) {\n\t\t\tthis.node = node;\n\t\t\tthis.outgoingIntersection = outgoingIntersection;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"(\" + outgoingIntersection + \", \" + node.getOsmNode().id +\n\t\t\t\t\t\"@\" + node.getPos() + \")\";\n\t\t}\n\t\t\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/OSMToMapDataConverter.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport static java.util.Collections.emptyList;\nimport static org.osm2world.core.math.VectorXZ.distance;\nimport static org.osm2world.core.util.FaultTolerantIterationUtil.iterate;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.openstreetmap.josm.plugins.graphview.core.data.osmosis.OSMFileDataSource;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Bound;\nimport org.osm2world.core.map_data.creation.index.MapDataIndex;\nimport org.osm2world.core.map_data.creation.index.MapIntersectionGrid;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapAreaSegment;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.data.overlaps.MapIntersectionWW;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapAA;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapNA;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapType;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapWA;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.GeometryUtil;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.data.OSMData;\nimport org.osm2world.core.osm.data.OSMElement;\nimport org.osm2world.core.osm.data.OSMNode;\nimport org.osm2world.core.osm.data.OSMRelation;\nimport org.osm2world.core.osm.data.OSMWay;\nimport org.osm2world.core.osm.ruleset.HardcodedRuleset;\nimport org.osm2world.core.osm.ruleset.Ruleset;\nimport org.osm2world.core.util.TouchMapperProfile;\nimport org.osm2world.core.util.FaultTolerantIterationUtil.Operation;\n\n/**\n * converts {@link OSMData} into the internal map data representation\n */\npublic class OSMToMapDataConverter {\n\n\tprivate final Ruleset ruleset = new HardcodedRuleset();\n\t\n\tprivate final MapProjection mapProjection;\n\tprivate final Configuration config;\n\t\n\tprivate static final Tag MULTIPOLYON_TAG = new Tag(\"type\", \"multipolygon\");\n\t\n\t\t\n\tpublic OSMToMapDataConverter(MapProjection mapProjection, Configuration config) {\n\t\tthis.mapProjection = mapProjection;\n\t\tthis.config = config;\n\t}\n\n\tpublic MapData createMapData(OSMData osmData) throws IOException {\n\n\t\tlong totalStart = TouchMapperProfile.start();\n\t\t\n\t\tfinal List<MapNode> mapNodes = new ArrayList<MapNode>();\n\t\tfinal List<MapWaySegment> mapWaySegs = new ArrayList<MapWaySegment>();\n\t\tfinal List<MapArea> mapAreas = new ArrayList<MapArea>();\n\t\t\n\t\tlong createElementsStart = TouchMapperProfile.start();\n\t\tcreateMapElements(osmData, mapNodes, mapWaySegs, mapAreas);\n\t\tTouchMapperProfile.logMillis(\"map_data.create_elements_ms\", createElementsStart);\n\t\t\n\t\tMapData mapData = new MapData(mapNodes, mapWaySegs, mapAreas,\n\t\t\t\tcalculateFileBoundary(osmData.getBounds()));\n\t\t\n\t\tboolean skipIntersections = config.getBoolean(\"touchMapperSkipIntersections\", false);\n\t\tboolean skipAreaAreaOverlaps = config.getBoolean(\"touchMapperSkipAreaAreaOverlaps\", true);\n\t\tboolean skipNodeAreaOverlaps = config.getBoolean(\"touchMapperSkipNodeAreaOverlaps\", true);\n\t\tTouchMapperProfile.logValue(\"map_data.skip_area_area_overlaps\", skipAreaAreaOverlaps);\n\t\tTouchMapperProfile.logValue(\"map_data.skip_node_area_overlaps\", skipNodeAreaOverlaps);\n\t\tif (skipIntersections) {\n\t\t\tTouchMapperProfile.logValue(\"map_data.intersections_skipped\", true);\n\t\t} else {\n\t\t\tlong intersectionsStart = TouchMapperProfile.start();\n\t\t\tcalculateIntersectionsInMapData(\n\t\t\t\t\tmapData, skipAreaAreaOverlaps, skipNodeAreaOverlaps);\n\t\t\tTouchMapperProfile.logMillis(\"map_data.calculate_intersections_ms\",\n\t\t\t\t\tintersectionsStart);\n\t\t}\n\t\tTouchMapperProfile.logValue(\"map_data.nodes_count\", mapNodes.size());\n\t\tTouchMapperProfile.logValue(\"map_data.way_segments_count\", mapWaySegs.size());\n\t\tTouchMapperProfile.logValue(\"map_data.areas_count\", mapAreas.size());\n\t\tTouchMapperProfile.logMillis(\"map_data.total_ms\", totalStart);\n\n\t\treturn mapData;\n\n\t}\n\n\t/**\n\t * creates {@link MapElement}s\n\t * based on OSM data from an {@link OSMFileDataSource}\n\t * and adds them to collections\n\t */\n\tprivate void createMapElements(OSMData osmData,\n\t\t\tfinal List<MapNode> mapNodes, final List<MapWaySegment> mapWaySegs,\n\t\t\tfinal List<MapArea> mapAreas) {\n\t\t\n\t\t/* create MapNode for each OSM node */\n\n\t\tfinal Map<OSMNode, MapNode> nodeMap = new HashMap<OSMNode, MapNode>();\n\t\tlong mapNodesStart = TouchMapperProfile.start();\n\n\t\tfor (OSMNode node : osmData.getNodes()) {\n\t\t\tVectorXZ nodePos = mapProjection.calcPos(node.lat, node.lon);\n\t\t\tMapNode mapNode = new MapNode(nodePos, node);\n\t\t\tmapNodes.add(mapNode);\n\t\t\tnodeMap.put(node, mapNode);\n\t\t}\n\t\tTouchMapperProfile.logMillis(\"map_elements.nodes_ms\", mapNodesStart);\n\t\tTouchMapperProfile.logValue(\"map_elements.nodes_count\", mapNodes.size());\n\t\t\n\t\t/* create areas ... */\n\t\t\n\t\tfinal Map<OSMWay, MapArea> areaMap = new HashMap<OSMWay, MapArea>();\n\t\t\t\t\n\t\t/* ... based on multipolygons */\n\n\t\tfinal int multipolygonAreasStartCount = mapAreas.size();\n\t\tlong multipolygonsStart = TouchMapperProfile.start();\n\t\t\n\t\titerate(osmData.getRelations(), new Operation<OSMRelation>() {\n\t\t\t@Override public void perform(OSMRelation relation) {\n\t\t\t\t\n\t\t\t\tif (relation.tags.contains(MULTIPOLYON_TAG)) {\n\t\t\t\t\t\n\t\t\t\t\tfor (MapArea area : MultipolygonAreaBuilder.\n\t\t\t\t\t\t\tcreateAreasForMultipolygon(relation, nodeMap)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tmapAreas.add(area);\n\t\t\t\t\t\t\n\t\t\t\t\t\tfor (MapNode boundaryMapNode : area.getBoundaryNodes()) {\n\t\t\t\t\t\t\tboundaryMapNode.addAdjacentArea(area);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (area.getOsmObject() instanceof OSMWay) {\n\t\t\t\t\t\t\tareaMap.put((OSMWay)area.getOsmObject(), area);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t});\n\t\tTouchMapperProfile.logMillis(\"map_elements.multipolygons_ms\", multipolygonsStart);\n\t\tTouchMapperProfile.logValue(\"map_elements.multipolygon_areas_count\",\n\t\t\t\tmapAreas.size() - multipolygonAreasStartCount);\n\t\t\n\t\t/* ... based on coastline ways */\n\n\t\tfinal int coastlinesStartCount = mapAreas.size();\n\t\tlong coastlinesStart = TouchMapperProfile.start();\n\t\t\n\t\tfor (MapArea area : MultipolygonAreaBuilder.createAreasForCoastlines(\n\t\t\t\tosmData, nodeMap, mapNodes,\n\t\t\t\tcalculateFileBoundary(osmData.getBounds()))) {\n\t\t\t\n\t\t\tmapAreas.add(area);\n\t\t\t\n\t\t\tfor (MapNode boundaryMapNode : area.getBoundaryNodes()) {\n\t\t\t\tboundaryMapNode.addAdjacentArea(area);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\tTouchMapperProfile.logMillis(\"map_elements.coastlines_ms\", coastlinesStart);\n\t\tTouchMapperProfile.logValue(\"map_elements.coastline_areas_count\",\n\t\t\t\tmapAreas.size() - coastlinesStartCount);\n\t\t\n\t\t/* ... based on closed ways */\n\n\t\tfinal int closedWaysStartCount = mapAreas.size();\n\t\tlong closedWaysStart = TouchMapperProfile.start();\n\t\t\n\t\tfor (OSMWay way : osmData.getWays()) {\n\t\t\tif (way.isClosed() && !areaMap.containsKey(way)) {\n\t\t\t\t//create MapArea only if at least one tag is an area tag\n\t\t\t\tfor (Tag tag : way.tags) {\n\t\t\t\t\tif (ruleset.isAreaTag(tag)) {\n\t\t\t\t\t\t//TODO: check whether this is old-style MP outer\n\t\t\t\t\t\t\n\t\t\t\t\t\tList<MapNode> nodes = new ArrayList<MapNode>(way.nodes.size());\n\t\t\t\t\t\tfor (OSMNode boundaryOSMNode : way.nodes) {\n\t\t\t\t\t\t\tnodes.add(nodeMap.get(boundaryOSMNode));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tMapArea mapArea = new MapArea((OSMElement)way, nodes);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tmapAreas.add(mapArea);\n\t\t\t\t\t\t\tareaMap.put(way, mapArea);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t} catch (InvalidGeometryException e) {\n\t\t\t\t\t\t\tSystem.err.println(e);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tTouchMapperProfile.logMillis(\"map_elements.closed_ways_ms\", closedWaysStart);\n\t\tTouchMapperProfile.logValue(\"map_elements.closed_way_areas_count\",\n\t\t\t\tmapAreas.size() - closedWaysStartCount);\n\t\t\n\t\t/* ... for empty terrain */\n\t\t\n\t\tAxisAlignedBoundingBoxXZ terrainBoundary =\n\t\t\t\tcalculateFileBoundary(osmData.getBounds());\n\n\t\tfinal int emptyTerrainNodeStartCount = mapNodes.size();\n\t\tfinal int emptyTerrainAreaStartCount = mapAreas.size();\n\t\tlong emptyTerrainStart = TouchMapperProfile.start();\n\t\t\n\t\tif (terrainBoundary != null\n\t\t\t\t&& config.getBoolean(\"createTerrain\", true)) {\n\t\t\t\n\t\t\tEmptyTerrainBuilder.createAreasForEmptyTerrain(\n\t\t\t\t\tmapNodes, mapAreas, terrainBoundary);\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\t//TODO fall back on data boundary if file does not contain bounds\n\t\t\t\n\t\t}\n\t\tTouchMapperProfile.logMillis(\"map_elements.empty_terrain_ms\", emptyTerrainStart);\n\t\tTouchMapperProfile.logValue(\"map_elements.empty_terrain_nodes_added\",\n\t\t\t\tmapNodes.size() - emptyTerrainNodeStartCount);\n\t\tTouchMapperProfile.logValue(\"map_elements.empty_terrain_areas_added\",\n\t\t\t\tmapAreas.size() - emptyTerrainAreaStartCount);\n\t\t\t\t\n\t\t/* finish calculations */\n\n\t\tlong adjacentAreaSegmentsStart = TouchMapperProfile.start();\n\t\t\n\t\tfor (MapNode node : nodeMap.values()) {\n\t\t\tnode.calculateAdjacentAreaSegments();\n\t\t}\n\t\tTouchMapperProfile.logMillis(\"map_elements.adjacent_area_segments_ms\",\n\t\t\t\tadjacentAreaSegmentsStart);\n\t\t\n\t\t/* create way segments from remaining ways */\n\n\t\tlong waySegmentsStart = TouchMapperProfile.start();\n\t\t\n\t\tfor (OSMWay way : osmData.getWays()) {\n\t\t\tif (!way.tags.isEmpty() && !areaMap.containsKey(way)) {\n\t\t\t\t\n\t\t\t\tOSMNode previousNode = null;\n\t\t\t\tfor (OSMNode node : way.nodes) {\n\t\t\t\t\tif (previousNode != null) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tMapWaySegment mapWaySeg = new MapWaySegment(\n\t\t\t\t\t\t\t\tway, nodeMap.get(previousNode), nodeMap.get(node));\n\t\t\t\t\t\t\n\t\t\t\t\t\tmapWaySegs.add(mapWaySeg);\n\t\t\t\t\t\tnodeMap.get(previousNode).addOutboundLine(mapWaySeg);\n\t\t\t\t\t\tnodeMap.get(node).addInboundLine(mapWaySeg);\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\tpreviousNode = node;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\tTouchMapperProfile.logMillis(\"map_elements.way_segments_ms\", waySegmentsStart);\n\t\tTouchMapperProfile.logValue(\"map_elements.way_segments_count\", mapWaySegs.size());\n\t\t\n\t}\n\t\n\t/**\n\t * calculates intersections and adds the information to the\n\t * {@link MapElement}s\n\t */\n\tprivate static void calculateIntersectionsInMapData(\n\t\t\tMapData mapData,\n\t\t\tboolean skipAreaAreaOverlaps,\n\t\t\tboolean skipNodeAreaOverlaps) {\n\t\t\n\t\tMapDataIndex index = new MapIntersectionGrid(mapData.getDataBoundary());\n\t\tSet<MapElement> dedupNearbyElements = new HashSet<MapElement>();\n\t\t\n\t\tfor (MapElement e1 : mapData.getMapElements()) {\n\t\t\t\n\t\t\t/* collect all nearby elements */\n\t\t\t\n\t\t\tCollection<? extends Iterable<MapElement>> leaves\n\t\t\t\t\t= index.insertAndProbe(e1);\n\t\t\t\n\t\t\tIterable<MapElement> nearbyElements;\n\t\t\t\n\t\t\tif (leaves.size() == 1) {\n\t\t\t\tnearbyElements = leaves.iterator().next();\n\t\t\t} else {\n\t\t\t\t// collect and de-duplicate elements from all the leaves\n\t\t\t\tdedupNearbyElements.clear();\n\t\t\t\tfor (Iterable<MapElement> leaf : leaves) {\n\t\t\t\t\tfor (MapElement e : leaf) {\n\t\t\t\t\t\tdedupNearbyElements.add(e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnearbyElements = dedupNearbyElements;\n\t\t\t}\n\t\t\t\n\t\t\tfor (MapElement e2 : nearbyElements) {\n\t\t\t\n\t\t\t\tif (e1 == e2) { continue; }\n\t\t\t\t\n\t\t\t\taddOverlapBetween(\n\t\t\t\t\t\te1, e2, skipAreaAreaOverlaps, skipNodeAreaOverlaps);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * adds the overlap between two {@link MapElement}s\n\t * to both, if it exists. It calls the appropriate\n\t * subtype-specific addOverlapBetween method\n\t */\n\tprivate static void addOverlapBetween(MapElement e1, MapElement e2,\n\t\t\tboolean skipAreaAreaOverlaps, boolean skipNodeAreaOverlaps) {\n\t\t\n\t\tif (e1 instanceof MapWaySegment\n\t\t\t\t&& e2 instanceof MapWaySegment) {\n\t\t\t\n\t\t\tMapWaySegment line1 = (MapWaySegment) e1;\n\t\t\tMapWaySegment line2 = (MapWaySegment) e2;\n\t\t\tif (boundingBoxesMayOverlapOrTouch(line1, line2)) {\n\t\t\t\taddOverlapBetween(line1, line2);\n\t\t\t}\n\t\t\t\n\t\t} else if (e1 instanceof MapWaySegment\n\t\t\t\t&& e2 instanceof MapArea) {\n\t\t\t\n\t\t\tMapWaySegment line = (MapWaySegment) e1;\n\t\t\tMapArea area = (MapArea) e2;\n\t\t\tif (boundingBoxesMayOverlapOrTouch(line, area)) {\n\t\t\t\taddOverlapBetween(line, area);\n\t\t\t}\n\t\t\t\n\t\t} else if (e1 instanceof MapArea\n\t\t\t\t&& e2 instanceof MapWaySegment) {\n\t\t\t\n\t\t\tMapWaySegment line = (MapWaySegment) e2;\n\t\t\tMapArea area = (MapArea) e1;\n\t\t\tif (boundingBoxesMayOverlapOrTouch(line, area)) {\n\t\t\t\taddOverlapBetween(line, area);\n\t\t\t}\n\t\t\t\n\t\t} else if (e1 instanceof MapArea\n\t\t\t\t&& e2 instanceof MapArea) {\n\n\t\t\tif (skipAreaAreaOverlaps) { return; }\n\t\t\t\n\t\t\tMapArea area1 = (MapArea) e1;\n\t\t\tMapArea area2 = (MapArea) e2;\n\t\t\tif (boundingBoxesMayOverlapOrTouch(area1, area2)) {\n\t\t\t\taddOverlapBetween(area1, area2);\n\t\t\t}\n\t\t\t\n\t\t} else if (e1 instanceof MapNode\n\t\t\t\t&& e2 instanceof MapArea) {\n\n\t\t\tif (skipNodeAreaOverlaps) { return; }\n\t\t\t\n\t\t\tMapNode node = (MapNode) e1;\n\t\t\tMapArea area = (MapArea) e2;\n\t\t\tif (area.getAxisAlignedBoundingBoxXZ().contains(node.getPos())) {\n\t\t\t\taddOverlapBetween(node, area);\n\t\t\t}\n\t\t\t\n\t\t} else if (e1 instanceof MapArea\n\t\t\t\t&& e2 instanceof MapNode) {\n\n\t\t\tif (skipNodeAreaOverlaps) { return; }\n\t\t\t\n\t\t\tMapNode node = (MapNode) e2;\n\t\t\tMapArea area = (MapArea) e1;\n\t\t\tif (area.getAxisAlignedBoundingBoxXZ().contains(node.getPos())) {\n\t\t\t\taddOverlapBetween(node, area);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * adds the overlap between two {@link MapWaySegment}s\n\t * to both, if it exists\n\t */\n\tprivate static void addOverlapBetween(\n\t\t\tMapWaySegment line1, MapWaySegment line2) {\n\t\t\n\t\tif (line1.isConnectedTo(line2)) { return; }\n\t\t\n\t\tVectorXZ intersection = GeometryUtil.getLineSegmentIntersection(\n\t\t\t\tline1.getStartNode().getPos(),\n\t\t\t\tline1.getEndNode().getPos(),\n\t\t\t\tline2.getStartNode().getPos(),\n\t\t\t\tline2.getEndNode().getPos());\n\t\t\n\t\tif (intersection != null) {\n\t\t\t\n\t\t\t/* add the intersection */\n\t\t\t\n\t\t\tMapIntersectionWW newIntersection =\n\t\t\t\tnew MapIntersectionWW(line1, line2, intersection);\n\t\t\t\n\t\t\tline1.addOverlap(newIntersection);\n\t\t\tline2.addOverlap(newIntersection);\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t}\n\n\t}\n\t\n\t/**\n\t * adds the overlap between a {@link MapWaySegment}\n\t * and a {@link MapArea} to both, if it exists\n\t */\n\tprivate static void addOverlapBetween(\n\t\t\tMapWaySegment line, MapArea area) {\n\t\t\n\t\tfinal LineSegmentXZ segmentXZ = line.getLineSegment();\n\t\tfinal boolean connectedToArea = line.isConnectedTo(area);\n\t\t\n\t\t/* check whether the line corresponds to one of the area segments */\n\t\tif (connectedToArea) {\n\t\t\tfor (MapAreaSegment areaSegment : area.getAreaSegments()) {\n\t\t\t\tif (areaSegment.sharesBothNodes(line)) {\n\t\t\t\t\t\n\t\t\t\t\tMapOverlapWA newOverlap =\n\t\t\t\t\t\tnew MapOverlapWA(line, area, MapOverlapType.SHARE_SEGMENT,\n\t\t\t\t\t\t\t\tCollections.<VectorXZ>emptyList(),\n\t\t\t\t\t\t\t\tCollections.<MapAreaSegment>emptyList());\n\t\t\t\t\t\n\t\t\t\t\tline.addOverlap(newOverlap);\n\t\t\t\t\tarea.addOverlap(newOverlap);\n\t\t\t\t\t\n\t\t\t\t\treturn;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t/* calculate whether the line contains or intersects the area (or neither) */\n\t\t\n\t\tboolean contains;\n\t\tboolean intersects;\n\t\t\n\t\t{\n\t\t\tfinal PolygonWithHolesXZ polygon = area.getPolygon();\n\t\t\t\n\t\t\tif (!connectedToArea) {\n\t\n\t\t\t\tintersects = polygon.intersects(segmentXZ);\n\t\t\t\tcontains = !intersects && polygon.contains(segmentXZ);\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\n\t\t\t\t/* check whether the line intersects the area somewhere\n\t\t\t\t * else than just at the common node(s).\n\t\t\t\t */\n\t\t\t\t\n\t\t\t\tintersects = false;\n\t\t\t\n\t\t\t\tdouble segmentLength = distance(segmentXZ.p1, segmentXZ.p2);\n\t\t\t\t\n\t\t\t\tfor (VectorXZ pos : polygon.intersectionPositions(segmentXZ)) {\n\t\t\t\t\tif (distance(pos, segmentXZ.p1) > segmentLength / 100\n\t\t\t\t\t\t\t&& distance(pos, segmentXZ.p2) > segmentLength / 100) {\n\t\t\t\t\t\tintersects = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\t/* check whether the area contains the line's center.\n\t\t\t\t * Unless the line intersects the area outline,\n\t\t\t\t * this means that the area contains the line itself.\n\t\t\t\t */\n\t\t\t\t\t\t\t\n\t\t\t\tcontains = !intersects && polygon.contains(segmentXZ.getCenter());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* add an overlap if detected */\n\t\t\t\t\t\n\t\tif (contains || intersects) {\n\t\t\t\n\t\t\t/* find out which area segments intersect the way segment */\n\n\t\t\tList<VectorXZ> intersectionPositions = emptyList();\n\t\t\tList<MapAreaSegment> intersectingSegments = emptyList();\n\t\t\t\n\t\t\tif (intersects) {\n\n\t\t\t\tintersectionPositions = new ArrayList<VectorXZ>();\n\t\t\t\tintersectingSegments = new ArrayList<MapAreaSegment>();\n\t\t\t\t\n\t\t\t\tfor (MapAreaSegment areaSegment : area.getAreaSegments()) {\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ intersection = segmentXZ.getIntersection(\n\t\t\t\t\t\t\tareaSegment.getStartNode().getPos(),\n\t\t\t\t\t\t\tareaSegment.getEndNode().getPos());\n\t\t\t\t\t\n\t\t\t\t\tif (intersection != null) {\n\t\t\t\t\t\tintersectionPositions.add(intersection);\n\t\t\t\t\t\tintersectingSegments.add(areaSegment);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* add the overlap */\n\t\t\t\n\t\t\tMapOverlapWA newOverlap = new MapOverlapWA(line, area,\n\t\t\t\t\t\tintersects ? MapOverlapType.INTERSECT : MapOverlapType.CONTAIN,\n\t\t\t\t\t\tintersectionPositions, intersectingSegments);\n\t\t\t\n\t\t\tline.addOverlap(newOverlap);\n\t\t\tarea.addOverlap(newOverlap);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tprivate static boolean boundingBoxesMayOverlapOrTouch(\n\t\t\tMapElement element1, MapElement element2) {\n\n\t\tAxisAlignedBoundingBoxXZ b1 = element1.getAxisAlignedBoundingBoxXZ();\n\t\tAxisAlignedBoundingBoxXZ b2 = element2.getAxisAlignedBoundingBoxXZ();\n\n\t\treturn !(b1.maxX < b2.minX\n\t\t\t\t|| b1.minX > b2.maxX\n\t\t\t\t|| b1.maxZ < b2.minZ\n\t\t\t\t|| b1.minZ > b2.maxZ);\n\n\t}\n\n\t/**\n\t * adds the overlap between two {@link MapArea}s\n\t * to both, if it exists\n\t */\n\tprivate static void addOverlapBetween(\n\t\t\tMapArea area1, MapArea area2) {\n\t\t\n\t\t/* check whether the areas have a shared segment */\n\t\t\t\t\n\t\tCollection<MapAreaSegment> area1Segments = area1.getAreaSegments();\n\t\tCollection<MapAreaSegment> area2Segments = area2.getAreaSegments();\n\t\t\n\t\tfor (MapAreaSegment area1Segment : area1Segments) {\n\t\t\tfor (MapAreaSegment area2Segment : area2Segments) {\n\t\t\t\tif (area1Segment.sharesBothNodes(area2Segment)) {\n\t\t\t\t\t\n\t\t\t\t\tMapOverlapAA newOverlap =\n\t\t\t\t\t\tnew MapOverlapAA(area1, area2, MapOverlapType.SHARE_SEGMENT);\n\t\t\t\t\tarea1.addOverlap(newOverlap);\n\t\t\t\t\tarea2.addOverlap(newOverlap);\n\t\t\t\t\t\n\t\t\t\t\treturn;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t/* calculate whether one area contains the other\n\t\t * or whether their outlines intersect (or neither) */\n\t\t\n\t\tboolean contains1 = false;\n\t\tboolean contains2 = false;\n\t\tboolean intersects = false;\n\t\t\n\t\t{\n\t\t\tfinal PolygonWithHolesXZ polygon1 = area1.getPolygon();\n\t\t\tfinal PolygonWithHolesXZ polygon2 = area2.getPolygon();\n\t\t\t\n\t\t\t/* determine common nodes */\n\t\t\t\n\t\t\tSet<VectorXZ> commonNodes = new HashSet<VectorXZ>();\n\t\t\tfor (SimplePolygonXZ p : polygon1.getPolygons()) {\n\t\t\t\tcommonNodes.addAll(p.getVertices());\n\t\t\t}\n\t\t\t\n\t\t\tSet<VectorXZ> nodes2 = new HashSet<VectorXZ>();\n\t\t\tfor (SimplePolygonXZ p : polygon2.getPolygons()) {\n\t\t\t\tnodes2.addAll(p.getVertices());\n\t\t\t}\n\t\t\t\n\t\t\tcommonNodes.retainAll(nodes2);\n\t\t\t\n\t\t\t/* check whether the areas' outlines intersects somewhere\n\t\t\t * else than just at the common node(s).\n\t\t\t */\n\t\t\t\n\t\t\tintersectionPosCheck:\n\t\t\tfor (VectorXZ pos : polygon1.intersectionPositions(polygon2)) {\n\t\t\t\tboolean trueIntersection = true;\n\t\t\t\tfor (VectorXZ commonNode : commonNodes) {\n\t\t\t\t\tif (distance(pos, commonNode) < 0.01) {\n\t\t\t\t\t\ttrueIntersection = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (trueIntersection) {\n\t\t\t\t\tintersects = true;\n\t\t\t\t\tbreak intersectionPosCheck;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* check whether one area contains the other */\n\t\t\t\n\t\t\tif (polygon1.contains(polygon2.getOuter())) {\n\t\t\t\tcontains1 = true;\n\t\t\t} else if (polygon2.contains(polygon1.getOuter())) {\n\t\t\t\tcontains2 = true;\n\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t}\n\t\t\n\t\t/* add an overlap if detected */\n\t\t\t\t\t\n\t\tif (contains1 || contains2 || intersects) {\n\t\t\t\n\t\t\t/* add the overlap */\n\t\t\t\n\t\t\tMapOverlapAA newOverlap = null;\n\t\t\t\n\t\t\tif (contains1) {\n\t\t\t\tnewOverlap = new MapOverlapAA(area2, area1, MapOverlapType.CONTAIN);\n\t\t\t} else if (contains2) {\n\t\t\t\tnewOverlap = new MapOverlapAA(area1, area2, MapOverlapType.CONTAIN);\n\t\t\t} else {\n\t\t\t\tnewOverlap = new MapOverlapAA(area1, area2, MapOverlapType.INTERSECT);\n\t\t\t}\n\t\t\t\n\t\t\tarea1.addOverlap(newOverlap);\n\t\t\tarea2.addOverlap(newOverlap);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static void addOverlapBetween(MapNode node, MapArea area) {\n\t\t\n\t\tif (area.getPolygon().contains(node.getPos())) {\n\t\t\t\n\t\t\t/* add the overlap */\n\t\t\t\n\t\t\tMapOverlapNA newOverlap =\n\t\t\t\t\tnew MapOverlapNA(node, area, MapOverlapType.CONTAIN);\n\t\t\t\n\t\t\tarea.addOverlap(newOverlap);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate AxisAlignedBoundingBoxXZ calculateFileBoundary(\n\t\t\tCollection<Bound> bounds) {\n\t\t\n\t\tCollection<VectorXZ> boundedPoints = new ArrayList<VectorXZ>();\n\t\t\n\t\tfor (Bound bound : bounds) {\n\t\t\t\n\t\t\tboundedPoints.add(mapProjection.calcPos(bound.getBottom(), bound.getLeft()));\n\t\t\tboundedPoints.add(mapProjection.calcPos(bound.getBottom(), bound.getRight()));\n\t\t\tboundedPoints.add(mapProjection.calcPos(bound.getTop(), bound.getLeft()));\n\t\t\tboundedPoints.add(mapProjection.calcPos(bound.getTop(), bound.getRight()));\n\t\t\t\n\t\t}\n\t\t\n\t\tif (boundedPoints.isEmpty()) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn new AxisAlignedBoundingBoxXZ(boundedPoints);\n\t\t}\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/OriginMapProjection.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport org.openstreetmap.osmosis.core.domain.v0_6.Bound;\nimport org.osm2world.core.ConversionFacade;\nimport org.osm2world.core.osm.data.OSMData;\nimport org.osm2world.core.osm.data.OSMNode;\n\n\n/**\n * abstract map projection superclass with configurable coordinate origin\n */\npublic abstract class OriginMapProjection implements MapProjection {\n\t\n\t/**\n\t * the origin.\n\t * \n\t * TODO make this final when future Java versions offer a replacement for\n\t * current factories in {@link ConversionFacade}\n\t */\n\tprotected LatLon origin;\n\t\n\t@Override\n\tpublic LatLon getOrigin() {\n\t\treturn origin;\n\t}\n\t\n\t/**\n\t * sets a new origin.\n\t */\n\tpublic void setOrigin(LatLon origin) {\n\t\tthis.origin = origin;\n\t}\n\t\n\t/**\n\t * sets a new origin. It is placed at the center of the bounds,\n\t * or else at the first node's coordinates.\n\t */\n\tpublic void setOrigin(OSMData osmData) {\n\t\t\n\t\tif (osmData.getBounds() != null && !osmData.getBounds().isEmpty()) {\n\t\t\t\n\t\t\tBound firstBound = osmData.getBounds().iterator().next();\n\t\t\t\n\t\t\tsetOrigin(new LatLon(\n\t\t\t\t\t(firstBound.getTop() + firstBound.getBottom()) / 2,\n\t\t\t\t\t(firstBound.getLeft() + firstBound.getRight()) / 2));\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\tif (osmData.getNodes().isEmpty()) {\n\t\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\t\"OSM data must contain bounds or nodes\");\n\t\t\t}\n\t\t\t\n\t\t\tOSMNode firstNode = osmData.getNodes().iterator().next();\n\t\t\tsetOrigin(new LatLon(firstNode.lat, firstNode.lon));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/OrthographicAzimuthalMapProjection.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport static java.lang.Double.NaN;\nimport static java.lang.Math.*;\n\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.data.OSMData;\n\n/**\n * application of an orthographic projection that is intended to use\n * values in meters centered around the coordinate center (0,0).\n * It projects coordinates onto a plane touching the globe at the origin.\n * This results in sufficient accuracy\n * if the data covers only a \"small\" part of the globe.\n */\npublic class OrthographicAzimuthalMapProjection extends OriginMapProjection {\n\t\n\tprivate final double GLOBE_RADIUS = 6371000;\n\t\n\tprivate double lat0 = NaN;\n\tprivate double lon0 = NaN;\n\t\n\t@Override\n\tpublic VectorXZ calcPos(LatLon latlon) {\n\t\treturn calcPos(latlon.lat, latlon.lon);\n\t}\n\t\n\t@Override\n\tpublic void setOrigin(LatLon origin) {\n\t\t\n\t\tsuper.setOrigin(origin);\n\t\t\n\t\tlat0 = toRadians(getOrigin().lat);\n\t\tlon0 = toRadians(getOrigin().lon);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void setOrigin(OSMData osmData) {\n\t\t\n\t\tsuper.setOrigin(osmData);\n\t\t\n\t\tlat0 = toRadians(getOrigin().lat);\n\t\tlon0 = toRadians(getOrigin().lon);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic VectorXZ calcPos(double latDeg, double lonDeg) {\n\t\t\n\t\tdouble lat = toRadians(latDeg);\n\t\tdouble lon = toRadians(lonDeg);\n\t\t\n\t\tdouble x = GLOBE_RADIUS * cos(lat) * sin(lon - lon0);\n\t\tdouble y = GLOBE_RADIUS * (cos(lat0) * sin(lat) - sin(lat0) * cos(lat) * cos(lon - lon0));\n\t\t\n\t\treturn new VectorXZ(x, y);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic double calcLat(VectorXZ pos) {\n\t\t\n\t\tdouble rho = sqrt(pos.x * pos.x + pos.z * pos.z);\n\t\tdouble c = asin(rho / GLOBE_RADIUS);\n\t\t\n\t\tif (rho > 0) {\n\t\t\treturn toDegrees(asin( cos(c) * sin(lat0) + ( pos.z * sin(c) * cos(lat0) ) / rho ));\n\t\t} else {\n\t\t\treturn toDegrees(lat0);\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic double calcLon(VectorXZ pos) {\n\t\t\n\t\tdouble rho = sqrt(pos.x * pos.x + pos.z * pos.z);\n\t\tdouble c = asin(rho / GLOBE_RADIUS);\n\t\t\n\t\tdouble div = rho * cos(lat0) * cos(c) - pos.z * sin(lat0) * sin(c);\n\t\t\n\t\tif (abs(div) > 1e-5) {\n\t\t\treturn toDegrees(lon0 + atan2( pos.x * sin(c), div ));\n\t\t} else {\n\t\t\treturn toDegrees(lon0);\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic VectorXZ getNorthUnit() {\n\t\treturn VectorXZ.Z_UNIT;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/index/Map2dTree.java",
    "content": "package org.osm2world.core.map_data.creation.index;\n\nimport static java.util.Collections.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * a 2D tree (two-dimensional k-d tree) managing {@link MapElement}s of a\n * data set according on their coordinates in the XZ plane.\n * \n * Data is contained within the leafs.\n * The inner nodes split the XZ plane along parallels to the Z and X axes,\n * alternatingly.\n */\npublic class Map2dTree implements MapDataIndex {\n\t\n\tprotected static final int LEAF_SPLIT_SIZE = 11;\n\t\n\tprotected final Node root;\n\t\n\tprotected static interface Node {\n\t\t\n\t\tvoid add(MapElement element, boolean suppressSplits);\n\t\t\n\t\tList<Leaf> probe(MapElement element);\n\n\t\t/** adds all leaves in the subtree starting at this node to a list */\n\t\tvoid collectLeaves(List<Leaf> leaves);\n\t\t\n\t}\n\t\n\tprotected static class InnerNode implements Node {\n\t\t\n\t\tpublic final boolean splitAlongX;\n\t\tpublic final double splitValue;\n\t\t\n\t\tpublic Node lowerChild;\n\t\tpublic Node upperChild;\n\t\t\n\t\tprotected InnerNode(boolean splitAlongX, double splitValue) {\n\t\t\t\n\t\t\tthis.splitAlongX = splitAlongX;\n\t\t\tthis.splitValue = splitValue;\n\t\t\t\n\t\t\tthis.lowerChild = new Leaf(this);\n\t\t\tthis.upperChild = new Leaf(this);\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void add(MapElement element, boolean suppressSplits) {\n\t\t\t\n\t\t\tboolean addToLowerChild = false;\n\t\t\tboolean addToUpperChild = false;\n\t\t\t\n\t\t\tfor (MapNode node : getMapNodes(element)) {\n\t\t\t\t\n\t\t\t\tVectorXZ pos = node.getPos();\n\t\t\t\t\n\t\t\t\tif (splitAlongX) {\n\t\t\t\t\n\t\t\t\t\taddToLowerChild |= pos.x <= splitValue;\n\t\t\t\t\taddToUpperChild |= pos.x >= splitValue;\n\t\t\t\t\t\n\t\t\t\t} else {\n\n\t\t\t\t\taddToLowerChild |= pos.z <= splitValue;\n\t\t\t\t\taddToUpperChild |= pos.z >= splitValue;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (addToLowerChild) {\n\t\t\t\tlowerChild.add(element, suppressSplits);\n\t\t\t}\n\t\t\tif (addToUpperChild) {\n\t\t\t\tupperChild.add(element, suppressSplits);\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tprivate void trySplitLeaf(Leaf leaf) {\n\n\t\t\tboolean splitChildAlongX = !splitAlongX;\n\t\t\tdouble splitChildValue = 0;\n\t\t\t\n\t\t\t/* determine split value as average of all values */\n\t\t\t\n\t\t\tint numNodes = 0;\n\t\t\t\n\t\t\tfor (MapElement element : leaf) {\n\t\t\t\tfor (MapNode node : getMapNodes(element)) {\n\t\t\t\t\t\n\t\t\t\t\tif (splitChildAlongX) {\n\t\t\t\t\t\tsplitChildValue += node.getPos().x;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsplitChildValue += node.getPos().z;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tnumNodes += 1;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tsplitChildValue /= numNodes;\n\t\t\t\n\t\t\t/* create the new inner node */\n\t\t\t\n\t\t\tInnerNode newChild = new InnerNode(splitChildAlongX, splitChildValue);\n\t\t\t\n\t\t\t/* check whether splitting will reduce the maximum node size */\n\t\t\t\n\t\t\tfor (MapElement element : leaf.elements) {\n\t\t\t\tnewChild.add(element, true);\n\t\t\t}\n\t\t\t\n\t\t\tif (((Leaf)(newChild.lowerChild)).elements.size() < leaf.elements.size() - 5\n\t\t\t\t\t&& ((Leaf)(newChild.upperChild)).elements.size() < leaf.elements.size() - 5) {\n\t\t\t\t\n\t\t\t\t/* replace the leaf with the new child node */\n\t\t\t\t\n\t\t\t\tif (lowerChild == leaf) {\n\t\t\t\t\tlowerChild = newChild;\n\t\t\t\t} else if (upperChild == leaf) {\n\t\t\t\t\tupperChild = newChild;\n\t\t\t\t} else {\n\t\t\t\t\tthrow new AssertionError(\"leaf is not a child of this node\");\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic List<Leaf> probe(MapElement element) {\n\t\t\t\n\t\t\tboolean addToLowerChild = false;\n\t\t\tboolean addToUpperChild = false;\n\t\t\t\n\t\t\tfor (MapNode node : getMapNodes(element)) {\n\t\t\t\t\n\t\t\t\tVectorXZ pos = node.getPos();\n\t\t\t\t\n\t\t\t\tif (splitAlongX) {\n\t\t\t\t\t\n\t\t\t\t\taddToLowerChild |= pos.x <= splitValue;\n\t\t\t\t\taddToUpperChild |= pos.x >= splitValue;\n\t\t\t\t\t\n\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\taddToLowerChild |= pos.z <= splitValue;\n\t\t\t\t\taddToUpperChild |= pos.z >= splitValue;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (addToLowerChild && addToUpperChild) {\n\t\t\t\tList<Leaf> leaves = new ArrayList<Leaf>();\n\t\t\t\tleaves.addAll(lowerChild.probe(element));\n\t\t\t\tleaves.addAll(upperChild.probe(element));\n\t\t\t\treturn leaves;\n\t\t\t} else if (addToLowerChild) {\n\t\t\t\treturn lowerChild.probe(element);\n\t\t\t} else if (addToUpperChild) {\n\t\t\t\treturn upperChild.probe(element);\n\t\t\t} else {\n\t\t\t\tthrow new AssertionError (\"The element is not in this Node\");\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic void collectLeaves(List<Leaf> leaves) {\n\t\t\tlowerChild.collectLeaves(leaves);\n\t\t\tupperChild.collectLeaves(leaves);\n\t\t}\n\t\t\n\t}\n\t\n\tprotected static class Leaf implements Node, Iterable<MapElement> {\n\n\t\tprotected final InnerNode parent;\n\t\tprotected final ArrayList<MapElement> elements;\n\t\t\n\t\tprotected int numberWaysAndAreas = 0;\n\t\t\n\t\tprotected Leaf(InnerNode parent) {\n\t\t\tthis.parent = parent;\n\t\t\telements = new ArrayList<MapElement>(LEAF_SPLIT_SIZE);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void add(MapElement element, boolean suppressSplits) {\n\t\t\t\n\t\t\telements.add(element);\n\n\t\t\tif (!(element instanceof MapNode)) {\n\t\t\t\tnumberWaysAndAreas += 1;\n\t\t\t}\n\t\t\t\n\t\t\tif (!suppressSplits && numberWaysAndAreas >= LEAF_SPLIT_SIZE) {\n\t\t\t\tparent.trySplitLeaf(this);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Iterator<MapElement> iterator() {\n\t\t\treturn elements.iterator();\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic List<Leaf> probe(MapElement element) {\n\t\t\treturn singletonList(this);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void collectLeaves(List<Leaf> leaves) {\n\t\t\tleaves.add(this);\n\t\t}\n\t\t\n\t}\n\t\n\tpublic Map2dTree(AxisAlignedBoundingBoxXZ dataBoundary) {\n\t\t\n\t\troot = new InnerNode(true, (dataBoundary.minX + dataBoundary.maxX) / 2);\n\t\t\t\t\n\t}\n\t\n\t@Override\n\tpublic void insert(MapElement element) {\n\t\troot.add(element, false);\n\t}\n\t\n\t@Override\n\tpublic Collection<Leaf> insertAndProbe(MapElement e) {\n\t\tinsert(e);\n\t\treturn root.probe(e);\n\t}\n\t\n\tprotected static Iterable<MapNode> getMapNodes(MapElement element) {\n\t\t\n\t\tif (element instanceof MapNode) {\n\t\t\t\n\t\t\treturn singleton((MapNode)element);\n\t\t\t\n\t\t} else if (element instanceof MapWaySegment) {\n\t\t\t\n\t\t\treturn ((MapWaySegment)element).getStartEndNodes();\n\t\t\t\n\t\t} else { // element instanceof MapArea\n\t\t\t\n\t\t\treturn ((MapArea)element).getBoundaryNodes();\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic Iterable<Leaf> getLeaves() {\n\t\t\n\t\tList<Leaf> leaves = new ArrayList<Leaf>();\n\t\t\n\t\troot.collectLeaves(leaves);\n\t\t\n\t\treturn leaves;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/index/MapDataIndex.java",
    "content": "package org.osm2world.core.map_data.creation.index;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.map_data.data.MapElement;\n\n/**\n * index structure intended to speed up retrieval of candidates for\n * intersection and overlap tests\n */\npublic interface MapDataIndex {\n\t\n\t/**\n\t * inserts the element into the index structure\n\t */\n\tpublic void insert(MapElement e);\n\t\n\t/**\n\t * inserts the element into the index structure,\n\t * and returns all nearby elements contained the index structure\n\t * \n\t * @return leaves the element ends up in. A subset of {@link #getLeaves()}.\n\t * The leaves already contain the new element.\n\t */\n\tpublic Collection<? extends Iterable<MapElement>> insertAndProbe(MapElement e);\n\t\n\t/**\n\t * returns all leaves of this index structure\n\t * \n\t * @return duplicate-free groups of elements\n\t */\n\tpublic abstract Iterable<? extends Iterable<MapElement>> getLeaves();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/index/MapIntersectionGrid.java",
    "content": "package org.osm2world.core.map_data.creation.index;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.datastructures.IntersectionGrid;\n\n\npublic class MapIntersectionGrid implements MapDataIndex {\n\t\n\tprivate final IntersectionGrid<MapElement> intersectionGrid;\n\t\t\t\n\tpublic MapIntersectionGrid(AxisAlignedBoundingBoxXZ dataBoundary) {\n\t\t\n\t\tAxisAlignedBoundingBoxXZ gridBounds = dataBoundary.pad(10);\n\t\t\n\t\tintersectionGrid = new IntersectionGrid<MapElement>(\n\t\t\t\tgridBounds,\n\t\t\t\tgridBounds.sizeX() / 50,\n\t\t\t\tgridBounds.sizeZ() / 50);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void insert(MapElement e) {\n\t\tintersectionGrid.insert(e);\n\t}\n\t\n\t@Override\n\tpublic Collection<? extends Iterable<MapElement>> insertAndProbe(MapElement e) {\n\t\tinsert(e);\n\t\treturn intersectionGrid.cellsFor(e);\n\t}\n\t\n\t@Override\n\tpublic Iterable<? extends Iterable<MapElement>> getLeaves() {\n\t\treturn intersectionGrid.getCells();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/creation/index/MapQuadtree.java",
    "content": "package org.osm2world.core.map_data.creation.index;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\n\nimport com.google.common.base.Predicate;\nimport com.google.common.collect.Collections2;\n\n/**\n * a Quadtree managing {@link MapElement}s of a data set\n * according on their coordinates in the XZ plane.\n */\npublic class MapQuadtree implements MapDataIndex {\n\t\n\tstatic final int LEAF_SPLIT_SIZE = 11;\n\t\n\tfinal QuadInnerNode root;\n\t\n\tstatic abstract class QuadNode {\n\t\t\n\t\tpublic final double minX, maxX, minZ, maxZ;\n\t\t\n\t\tprivate final SimplePolygonXZ boundary;\n\t\t\n\t\tQuadNode(double minX2, double maxX2, double minZ2, double maxZ2) {\n\t\t\t\n\t\t\tthis.minX = minX2;\n\t\t\tthis.maxX = maxX2;\n\t\t\tthis.minZ = minZ2;\n\t\t\tthis.maxZ = maxZ2;\n\t\t\t\n\t\t\tList<VectorXZ> vertices = new ArrayList<VectorXZ>(5);\n\t\t\tvertices.add(new VectorXZ(minX2, minZ2));\n\t\t\tvertices.add(new VectorXZ(maxX2, minZ2));\n\t\t\tvertices.add(new VectorXZ(maxX2, maxZ2));\n\t\t\tvertices.add(new VectorXZ(minX2, maxZ2));\n\t\t\tvertices.add(vertices.get(0));\n\t\t\tboundary = new SimplePolygonXZ(vertices);\n\t\t\t\n\t\t}\n\t\t\n\t\t/** returns true if this node's bounds contain at least a part of the element */\n\t\tboolean contains(MapElement element) {\n\t\t\t\n\t\t\tif (element instanceof MapNode) {\n\t\t\t\t\n\t\t\t\treturn contains(((MapNode)element).getPos());\n\t\t\t\t\n\t\t\t} else if (element instanceof MapWaySegment) {\n\t\t\t\tMapWaySegment line = (MapWaySegment)element;\n\t\t\t\t\t\t\t\t\n\t\t\t\tVectorXZ lineStart = line.getStartNode().getPos();\n\t\t\t\tVectorXZ lineEnd = line.getEndNode().getPos();\n\t\t\t\t\n\t\t\t\tif (contains(lineStart) || contains(lineEnd)) {\n\t\t\t\t\treturn true;\n\t\t\t\t} else if (boundary.intersects(lineStart, lineEnd)) {\n\t\t\t\t\t//SUGGEST (performance): use that the box is axis-aligned?\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t\t\n\t\t\t} else { // element instanceof MapArea\n\t\t\t\tMapArea area = ((MapArea)element);\n\t\t\t\t\t\t\t\t\n\t\t\t\tfor (MapNode node : area.getBoundaryNodes()) {\n\t\t\t\t\tif (contains(node.getPos())) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (boundary.intersects(area.getPolygon().getOuter())\n\t\t\t\t\t\t|| area.getPolygon().contains(boundary)) {\n\t\t\t\t\t//SUGGEST (performance): use that the box is axis-aligned?\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\t\n\t\tboolean contains(VectorXZ pos) {\n\t\t\treturn pos.x >= minX && pos.x <= maxX\n\t\t\t\t&& pos.z >= minZ && pos.z <= maxZ;\n\t\t}\n\t\t\n\t\tabstract void add(MapElement element);\n\n\t\tabstract void addAll(Collection<MapElement> elements);\n\n\t\t/** adds all leaves in the subtree starting at this node to a list */\n\t\tabstract void collectLeaves(List<QuadLeaf> leaves);\n\t\t\n\t}\n\t\n\tstatic class QuadInnerNode extends QuadNode {\n\t\t\n\t\t/** array with four elements */\n\t\tfinal QuadNode childNodes[];\n\n\t\tQuadInnerNode(double minX, double maxX, double minZ, double maxZ) {\n\t\t\tsuper(minX, maxX, minZ, maxZ);\n\t\t\t\n\t\t\tchildNodes = new QuadNode[4];\n\t\t\t\n\t\t\tdouble halfX = (minX+maxX)/2;\n\t\t\tdouble halfZ = (minZ+maxZ)/2;\n\t\t\t\n\t\t\tchildNodes[0] = new QuadLeaf(this, minX, halfX, minZ, halfZ);\n\t\t\tchildNodes[1] = new QuadLeaf(this, halfX, maxX, minZ, halfZ);\n\t\t\tchildNodes[2] = new QuadLeaf(this, minX, halfX, halfZ, maxZ);\n\t\t\tchildNodes[3] = new QuadLeaf(this, halfX, maxX, halfZ, maxZ);\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tvoid add(MapElement element) {\n\t\t\tfor (int i=0; i<4; i++) {\n\t\t\t\tif (childNodes[i].contains(element)) {\n\t\t\t\t\tchildNodes[i].add(element);\n\t\t\t\t\t//continue loop, the element can cross leaf borders\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tvoid addAll(Collection<MapElement> elements) {\n\t\t\tfor (MapElement element : elements) {\n\t\t\t\tadd(element);\n\t\t\t}\n\t\t}\n\n\t\tvoid trySplitLeaf(QuadLeaf leaf) {\n\t\t\t\n\t\t\tQuadInnerNode newChild =\n\t\t\t\tnew QuadInnerNode(leaf.minX, leaf.maxX, leaf.minZ, leaf.maxZ);\n\t\t\t\n\t\t\t/* check whether splitting will reduce the maximum node size */\n\t\t\t\n\t\t\tboolean nodeSizeReduced = true;\n\t\t\t\t\t\t\n\t\t\tfor (int i=0; i<4; i++) {\n\t\t\t\tboolean newLeafContainsAllElements = true;\n\t\t\t\tfor (MapElement element : leaf) {\n\t\t\t\t\tif (!newChild.childNodes[i].contains(element)) {\n\t\t\t\t\t\tnewLeafContainsAllElements = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (newLeafContainsAllElements) {\n\t\t\t\t\tnodeSizeReduced = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (nodeSizeReduced) {\n\t\t\t\t\n\t\t\t\t/* replace the leaf with the new child node */\n\t\t\t\t\n\t\t\t\tfor (int i=0; i<4; i++) {\n\t\t\t\t\tif (childNodes[i] == leaf) {\n\t\t\t\t\t\tchildNodes[i] = newChild;\n\t\t\t\t\t\tchildNodes[i].addAll(leaf.elements);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tthrow new AssertionError(\"leaf is not a child of this node\");\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\t\n\t\t}\n\n\t\tvoid collectLeaves(List<QuadLeaf> leaves) {\n\t\t\tfor (int i=0; i<4; i++) {\n\t\t\t\tchildNodes[i].collectLeaves(leaves);\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\tstatic public class QuadLeaf extends QuadNode implements Iterable<MapElement> {\n\n\t\tfinal QuadInnerNode parent;\n\t\tfinal ArrayList<MapElement> elements;\n\t\t\n\t\tQuadLeaf(QuadInnerNode parent, double minX, double maxX, double minZ, double maxZ) {\n\t\t\tsuper(minX, maxX, minZ, maxZ);\n\t\t\t\n\t\t\tthis.parent = parent;\n\t\t\t\n\t\t\telements = new ArrayList<MapElement>(LEAF_SPLIT_SIZE);\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tvoid add(MapElement element) {\n\t\t\t\n\t\t\telements.add(element);\n\n\t\t\tif (elements.size() >= LEAF_SPLIT_SIZE) {\n\t\t\t\tparent.trySplitLeaf(this);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tvoid addAll(Collection<MapElement> element) {\n\t\t\t/* addAll cannot be implemented by iterating over add:\n\t\t\t * if the leaf would be \"split\"(replaced with an inner node)\n\t\t\t * during the iteration, the remaining elements would still\n\t\t\t * be added to the now-useless leaf object */\n\t\t\t\n\t\t\telements.addAll(element);\n\n\t\t\tif (elements.size() >= LEAF_SPLIT_SIZE) {\n\t\t\t\tparent.trySplitLeaf(this);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Iterator<MapElement> iterator() {\n\t\t\treturn elements.iterator();\n\t\t}\n\t\t\n\t\t@Override\n\t\tvoid collectLeaves(List<QuadLeaf> leaves) {\n\t\t\tleaves.add(this);\n\t\t}\n\t\t\n\t}\n\t\n\tpublic MapQuadtree(AxisAlignedBoundingBoxXZ dataBoundary) {\n\t\t\n\t\troot = new QuadInnerNode(\n\t\t\t\tdataBoundary.minX, dataBoundary.maxX,\n\t\t\t\tdataBoundary.minZ, dataBoundary.maxZ);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void insert(MapElement e) {\n\t\troot.add(e);\n\t}\n\t\n\t@Override\n\tpublic Collection<? extends Iterable<MapElement>> insertAndProbe(\n\t\t\tfinal MapElement e) {\n\t\t\n\t\tinsert(e);\n\t\t\n\t\treturn Collections2.<QuadLeaf>filter(getLeaves(), new Predicate<QuadLeaf>() {\n\t\t\t@Override public boolean apply(QuadLeaf leaf) {\n\t\t\t\treturn leaf.contains(e);\n\t\t\t}\n\t\t});\n\t\t\n\t}\n\t\n\t@Override\n\tpublic Collection<QuadLeaf> getLeaves() {\n\t\t\n\t\tList<QuadLeaf> leaves = new ArrayList<QuadLeaf>();\n\t\t\n\t\troot.collectLeaves(leaves);\n\t\t\n\t\treturn leaves;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/MapArea.java",
    "content": "package org.osm2world.core.map_data.data;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.data.OSMElement;\nimport org.osm2world.core.world.data.AreaWorldObject;\n\npublic class MapArea implements MapElement {\n\n\tprivate final OSMElement objectWithTags;\n\t\n\tprivate final List<MapNode> nodes;\n\tprivate final List<List<MapNode>> holes;\n\t\n\tprivate final PolygonWithHolesXZ polygon;\n\t\t\n\tprivate Collection<MapAreaSegment> areaSegments;\n\n\t@SuppressWarnings(\"unchecked\") //is later checked for EMPTY_LIST using ==\n\tprivate Collection<MapOverlap<?,?>> overlaps = Collections.EMPTY_LIST;\n\t\n\tprivate List<AreaWorldObject> representations =\n\t\tnew ArrayList<AreaWorldObject>(1);\n\t\n\t//TODO: contained / intersecting nodes/lines\n\t\n\tpublic MapArea(OSMElement objectWithTags, List<MapNode> nodes) {\n\t\tthis(objectWithTags, nodes, Collections.<List<MapNode>>emptyList());\n\t}\n\t\n\tpublic MapArea(OSMElement objectWithTags, List<MapNode> nodes,\n\t\t\tList<List<MapNode>> holes) {\n\t\t\n\t\tthis.objectWithTags = objectWithTags;\n\t\tthis.nodes = nodes;\n\t\tthis.holes = holes;\n\t\t\n\t\ttry {\n\t\t\t\n\t\t\tthis.polygon = convertToPolygon(nodes, holes);\n\t\t\t\n\t\t} catch (InvalidGeometryException e) {\n\t\t\tthrow new InvalidGeometryException(\n\t\t\t\t\t\"outer polygon is not simple for this object: \"\n\t\t\t\t\t+ objectWithTags, e);\n\t\t}\n\t\t\n\t}\n\t\n\tpublic MapArea(OSMElement objectWithTags, List<MapNode> nodes,\n\t\t\tList<List<MapNode>> holes, PolygonWithHolesXZ polygon) {\n\t\t\n\t\tthis.objectWithTags = objectWithTags;\n\t\tthis.nodes = nodes;\n\t\tthis.holes = holes;\n\t\tthis.polygon = polygon;\n\t\t\n\t}\n\t\n\tprivate static final PolygonWithHolesXZ convertToPolygon(\n\t\t\tList<MapNode> nodes, List<List<MapNode>> holes) {\n\t\t\n\t\tSimplePolygonXZ outerPolygon =\n\t\t\t\tpolygonFromMapNodeLoop(nodes);\n\t\t\n\t\tList<SimplePolygonXZ> holePolygons =\n\t\t\tnew ArrayList<SimplePolygonXZ>(holes.size());\n\t\tfor (List<MapNode> hole : holes) {\n\t\t\tholePolygons.add(polygonFromMapNodeLoop(hole));\n\t\t}\n\t\t\n\t\treturn new PolygonWithHolesXZ(outerPolygon, holePolygons);\n\t\t\t\t\n\t}\n\n\tpublic static final SimplePolygonXZ polygonFromMapNodeLoop(\n\t\t\tList<MapNode> nodes) {\n\t\t\n\t\tList<VectorXZ> vertices = new ArrayList<VectorXZ>(nodes.size());\n\t\t\n\t\tfor (MapNode node : nodes) {\n\t\t\tvertices.add(node.getPos());\n\t\t}\n\t\t\n\t\treturn new SimplePolygonXZ(vertices);\n\t\t\n\t}\n\n\tpublic List<MapNode> getBoundaryNodes() {\n\t\treturn nodes;\n\t}\n\t\n\t@Override\n\tpublic int getLayer() {\n\t\tif (objectWithTags.tags.containsKey(\"layer\")) {\n\t\t\ttry {\n\t\t\t\treturn Integer.parseInt(objectWithTags.tags.getValue(\"layer\"));\n\t\t\t} catch (NumberFormatException nfe) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}\n\t\n\tpublic Collection<List<MapNode>> getHoles() {\n\t\treturn holes;\n\t}\n\t\n\t/** returns the way or relation with the tags for this area */\n\tpublic OSMElement getOsmObject() {\n\t\treturn objectWithTags;\n\t}\n\n\t@Override\n\tpublic TagGroup getTags() {\n\t\treturn objectWithTags.tags;\n\t}\n\t\n\t/**\n\t * returns the area as a polygon.\n\t */\n\tpublic PolygonWithHolesXZ getPolygon() {\n\t\treturn polygon;\n\t}\n\n\tpublic SimplePolygonXZ getOuterPolygon() {\n\t\treturn getPolygon().getOuter();\n\t}\n\t\n\t/**\n\t * returns the segments making up this area's outer and inner boundaries\n\t */\n\tpublic Collection<MapAreaSegment> getAreaSegments() {\n\t\t\n\t\tif (areaSegments == null) {\n\t\t\n\t\t\tareaSegments = new ArrayList<MapAreaSegment>();\n\t\t\t\n\t\t\tfor (int v = 0; v+1 < nodes.size(); v++) {\n\t\t\t\t//relies on duplication of first/last node\n\t\t\t\t\n\t\t\t\tareaSegments.add(new MapAreaSegment(this,\n\t\t\t\t\t\tgetPolygon().getOuter().isClockwise(),\n\t\t\t\t\t\tnodes.get(v), nodes.get(v+1)));\n\t\t\t\t\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tfor (int h = 0; h < holes.size(); h++) {\n\t\t\t\t\n\t\t\t\tList<MapNode> holeNodes = holes.get(h);\n\t\t\t\tSimplePolygonXZ holePolygon = polygon.getHoles().get(h);\n\t\t\t\t\n\t\t\t\tfor (int v = 0; v+1 < holeNodes.size(); v++) {\n\t\t\t\t\t//relies on duplication of first/last node\n\t\t\t\t\t\n\t\t\t\t\tareaSegments.add(new MapAreaSegment(this,\n\t\t\t\t\t\t\t!holePolygon.isClockwise(),\n\t\t\t\t\t\t\tholeNodes.get(v), holeNodes.get(v+1)));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn areaSegments;\n\t\t\n\t}\n\t\n\t@Override\n\tpublic List<AreaWorldObject> getRepresentations() {\n\t\treturn representations;\n\t}\n\t\n\t@Override\n\tpublic AreaWorldObject getPrimaryRepresentation() {\n\t\tif (representations.isEmpty()) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn representations.get(0);\n\t\t}\n\t}\n\n\t/**\n\t * adds a visual representation for this area\n\t */\n\tpublic void addRepresentation(AreaWorldObject representation) {\n\t\tthis.representations.add(representation);\n\t}\n\t\n\tpublic void addOverlap(MapOverlap<?, ?> overlap) {\n\t\tassert overlap.e1 == this || overlap.e2 == this;\n\t\tif (overlaps == Collections.EMPTY_LIST) {\n\t\t\toverlaps = new ArrayList<MapOverlap<?,?>>();\n\t\t}\n\t\toverlaps.add(overlap);\n\t}\n\n\t@Override\n\tpublic Collection<MapOverlap<?,?>> getOverlaps() {\n\t\treturn overlaps;\n\t}\n\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\treturn new AxisAlignedBoundingBoxXZ(getOuterPolygon().getVertexCollection());\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn objectWithTags.toString();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/MapAreaSegment.java",
    "content": "package org.osm2world.core.map_data.data;\n\n/**\n * segment of a {@link MapArea}'s outline. This isn't a {@link MapElement}s\n * itself (for example, it has no individual representation).\n * Instead, it's simply a different way to access an area's data.\n */\npublic class MapAreaSegment extends MapSegment {\n\t\n\tprivate final MapArea area;\n\tprivate final boolean areaRight;\n\t\n\tpublic MapAreaSegment(MapArea area, boolean areaRight,\n\t\t\tMapNode startNode, MapNode endNode) {\n\t\tsuper(startNode, endNode);\n\t\tthis.area = area;\n\t\tthis.areaRight = areaRight;\n\t}\n\t\n\tpublic MapArea getArea() {\n\t\treturn area;\n\t}\n\t\n\t/**\n\t * returns true if the area is to the right of this segment\n\t */\n\tpublic boolean isAreaRight() {\n\t\treturn areaRight;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/MapData.java",
    "content": "package org.osm2world.core.map_data.data;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.data.OSMData;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.data.WorldObject;\n\nimport com.google.common.base.Function;\nimport com.google.common.collect.Iterables;\n\n/**\n * OSM2World's abstraction of {@link OSMData}, consists of {@link MapElement}s.\n * Initially contains only a slightly altered representation of OSM\n * map data. During later conversion steps, additional information is\n * added to the {@link MapElement}s.\n */\npublic class MapData {\n\n\tfinal List<MapNode> mapNodes;\n\tfinal List<MapWaySegment> mapWaySegments;\n\tfinal List<MapArea> mapAreas;\n\n\tAxisAlignedBoundingBoxXZ fileBoundary;\n\tAxisAlignedBoundingBoxXZ dataBoundary;\n\t\n\tpublic MapData(List<MapNode> mapNodes, List<MapWaySegment> mapWaySegments,\n\t\t\tList<MapArea> mapAreas, AxisAlignedBoundingBoxXZ fileBoundary) {\n\n\t\tthis.mapNodes = mapNodes;\n\t\tthis.mapWaySegments = mapWaySegments;\n\t\tthis.mapAreas = mapAreas;\n\t\tthis.fileBoundary = fileBoundary;\n\n\t\tcalculateDataBoundary();\n\t\t\n\t}\n\t\n\tprivate void calculateDataBoundary() {\n\t\t\n\t\tdouble minX = Double.POSITIVE_INFINITY;\n\t\tdouble maxX = Double.NEGATIVE_INFINITY;\n\t\tdouble minZ = Double.POSITIVE_INFINITY;\n\t\tdouble maxZ = Double.NEGATIVE_INFINITY;\n\t\t\n\t\tif (fileBoundary != null) {\n\t\t\t// use the file boundary as the minimum extent of the data boundary\n\t\t\tminX = fileBoundary.minX;\n\t\t\tmaxX = fileBoundary.maxX;\n\t\t\tminZ = fileBoundary.minZ;\n\t\t\tmaxZ = fileBoundary.maxZ;\n\t\t}\n\t\t\n\t\tfor (MapNode node : mapNodes) {\n\t\t\tfinal double nodeX = node.getPos().x;\n\t\t\tfinal double nodeZ = node.getPos().z;\n\t\t\tif (nodeX < minX) { minX = nodeX; }\n\t\t\tif (nodeX > maxX) { maxX = nodeX; }\n\t\t\tif (nodeZ < minZ) { minZ = nodeZ; }\n\t\t\tif (nodeZ > maxZ) { maxZ = nodeZ; }\n\t\t}\n\t\t\n\t\tdataBoundary = new AxisAlignedBoundingBoxXZ(minX, minZ, maxX, maxZ);\n\t\t\n\t}\n\n\tpublic Iterable<MapElement> getMapElements() {\n\t\treturn Iterables.concat(mapNodes, mapWaySegments, mapAreas);\n\t}\n\n\tpublic Collection<MapArea> getMapAreas() {\n\t\treturn mapAreas;\n\t}\n\n\tpublic Collection<MapWaySegment> getMapWaySegments() {\n\t\treturn mapWaySegments;\n\t}\n\n\tpublic Collection<MapNode> getMapNodes() {\n\t\treturn mapNodes;\n\t}\n\n\t/**\n\t * returns a rectangular boundary polygon from the minimum/maximum of\n\t * coordinates in the map data\n\t */\n\tpublic AxisAlignedBoundingBoxXZ getDataBoundary() {\n\t\treturn dataBoundary;\n\t}\n\t\n\t/**\n\t * returns a boundary based on the bounds in the input file if available,\n\t * otherwise returns the same as {@link #getDataBoundary()}\n\t */\n\tpublic AxisAlignedBoundingBoxXZ getBoundary() {\n\t\tif (fileBoundary != null) {\n\t\t\treturn fileBoundary;\n\t\t} else {\n\t\t\treturn dataBoundary;\n\t\t}\n\t}\n\n\t/**\n\t * calculates the center from the {@link MapNode}s' positions\n\t */\n\tpublic VectorXZ getCenter() {\n\n\t\tint nodeCount = getMapNodes().size();\n\n\t\tdouble avgX = 0, avgZ = 0;\n\t\tfor (MapNode node : getMapNodes()) {\n\t\t\tavgX += node.getPos().x / nodeCount; // need to divide before\n\t\t\t\t\t\t\t\t\t\t\t\t\t// numbers get too large\n\t\t\tavgZ += node.getPos().z / nodeCount;\n\t\t}\n\n\t\treturn new VectorXZ(avgX, avgZ);\n\n\t}\n\n\t/**\n\t * returns all {@link WorldObject}s from elements in this data set.\n\t */\n\tpublic Iterable<WorldObject> getWorldObjects() {\n\t\t\n\t\treturn Iterables.concat(\n\t\t\t\tIterables.transform(getMapElements(),\n\t\t\t\t\t\tnew Function<MapElement, Iterable<? extends WorldObject>>() {\n\t\t\t\t\t@Override public Iterable<? extends WorldObject> apply(MapElement e) {\n\t\t\t\t\t\treturn e.getRepresentations();\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t\n\t}\n\n\t/**\n\t * returns all {@link WorldObject}s from elements in this data set\n\t * that are instances of a certain type.\n\t * Can be used, for example, to access all\n\t * {@link TerrainBoundaryWorldObject}s in the grid.\n\t */\n\tpublic <T> Iterable<T> getWorldObjects(Class<T> type) {\n\t\treturn Iterables.filter(getWorldObjects(), type);\n\t}\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/MapElement.java",
    "content": "package org.osm2world.core.map_data.data;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\nimport org.osm2world.core.osm.data.OSMElement;\nimport org.osm2world.core.world.data.WorldObject;\n\npublic interface MapElement extends IntersectionTestObject {\n\t\n\tpublic int getLayer();\n\t\n\t/**\n\t * returns the visual representations of this element.\n\t * \n\t * The order should match the order in which they were added,\n\t * so that dependencies are preserved (elements that depend on\n\t * another element should be placed after that element).\n\t * The first element is considered the \"primary\" representation,\n\t * and for some purposes - such as elevation calculation -, only this\n\t * representation will be used.\n\t */\n\tpublic List<? extends WorldObject> getRepresentations();\n\t\n\t/**\n\t * returns the primary representation, or null if the object doesn't have any.\n\t * @see #getRepresentations()\n\t */\n\tpublic WorldObject getPrimaryRepresentation();\n\t\n\t/**\n\t * returns all overlaps between this {@link MapElement}\n\t * and other {@link MapElement}s.\n\t */\n\tpublic Collection<MapOverlap<? extends MapElement, ? extends MapElement>> getOverlaps();\n\n\t/**\n\t * returns the tags of the underlying {@link OSMElement}\n\t */\n\tTagGroup getTags();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/MapNode.java",
    "content": "package org.osm2world.core.map_data.data;\n\nimport static org.osm2world.core.math.VectorXZ.X_UNIT;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.data.OSMNode;\nimport org.osm2world.core.world.data.NodeWorldObject;\n\n/**\n * grid representation of an OSM node,\n * references inbound and outbound {@link MapWaySegment}s.\n * For each OSM node, one GridNode will be created.\n */\npublic class MapNode implements MapElement {\n\n\tprivate final VectorXZ pos;\n\tprivate final OSMNode osmNode;\n\t\n\tprivate List<NodeWorldObject> representations = new ArrayList<NodeWorldObject>(1);\n\t\n\tprivate List<MapWaySegment> connectedWaySegments = new ArrayList<MapWaySegment>();\n\tprivate List<MapSegment> connectedSegments = new ArrayList<MapSegment>();\n\t\n\tprivate List<MapWaySegment> inboundLines = new ArrayList<MapWaySegment>(); //TODO: maybe use list and sort by angle?\n\tprivate List<MapWaySegment> outboundLines = new ArrayList<MapWaySegment>();\n\t\t\n\tprivate Collection<MapArea> adjacentAreas;\n\t\n\tpublic MapNode(VectorXZ pos, OSMNode osmNode) {\n\t\t\n\t\tthis.pos = pos;\n\t\tthis.osmNode = osmNode;\n\t\tthis.adjacentAreas = new ArrayList<MapArea>();\n\t\t\n\t}\n\t\n\tpublic VectorXZ getPos() {\n\t\treturn pos;\n\t}\n\t\n\t@Override\n\tpublic int getLayer() {\n\t\tif (osmNode.tags.containsKey(\"layer\")) {\n\t\t\ttry {\n\t\t\t\treturn Integer.parseInt(osmNode.tags.getValue(\"layer\"));\n\t\t\t} catch (NumberFormatException nfe) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}\n\t\n\tpublic OSMNode getOsmNode() {\n\t\treturn osmNode;\n\t}\n\t\n\t@Override\n\tpublic TagGroup getTags() {\n\t\treturn getOsmNode().tags;\n\t}\n\t\n\tpublic Collection<MapArea> getAdjacentAreas() {\n\t\treturn adjacentAreas;\n\t}\n\t\n\tpublic void addInboundLine(MapWaySegment inboundLine) {\n\t\t\n\t\tconnectedWaySegments.add(inboundLine);\n\t\tconnectedSegments.add(inboundLine);\n\t\tinboundLines.add(inboundLine);\n\n\t\tsortLinesByAngle(connectedWaySegments);\n\t\tsortLinesByAngle(connectedSegments);\n\t\tsortLinesByAngle(inboundLines);\n\t\t\n\t}\n\t\n\tpublic void addOutboundLine(MapWaySegment outboundLine) {\n\t\t\n\t\tconnectedWaySegments.add(outboundLine);\n\t\tconnectedSegments.add(outboundLine);\n\t\toutboundLines.add(outboundLine);\n\t\t\n\t\tsortLinesByAngle(connectedWaySegments);\n\t\tsortLinesByAngle(connectedSegments);\n\t\tsortLinesByAngle(outboundLines);\n\t\t\n\t}\n\t\n\t/**\n\t * returns those connected lines that end here.\n\t * Sorting is as for {@link #getConnectedWaySegments()}.\n\t */\n\tpublic List<MapWaySegment> getInboundLines() {\n\t\treturn inboundLines;\n\t}\n\t\n\t/**\n\t * returns those connected lines that start here.\n\t * Sorting is as for {@link #getConnectedWaySegments()}.\n\t */\n\tpublic List<MapWaySegment> getOutboundLines() {\n\t\treturn outboundLines;\n\t}\n\t\n\tpublic void addAdjacentArea(MapArea adjacentArea) {\n\t\tadjacentAreas.add(adjacentArea);\n\t}\n\n\t//TODO: with all that \"needs to be called before x\" etc. stuff (also in MapArea), switch to BUILDER?\n\t/** needs to be called after adding and completing all adjacent areas */\n\tpublic void calculateAdjacentAreaSegments() {\n\t\n\t\tfor (MapArea adjacentArea : adjacentAreas) {\n\t\t\tfor (MapAreaSegment areaSegment : adjacentArea.getAreaSegments()) {\n\t\t\t\tif (areaSegment.getStartNode() == this\n\t\t\t\t\t\t|| areaSegment.getEndNode() == this) {\n\t\t\t\t\tconnectedSegments.add(areaSegment);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tsortLinesByAngle(connectedSegments);\n\t}\n\t\t\n\t/**\n\t * returns all way segments connected with this node.\n\t * They will be sorted according to the clockwise\n\t * (seen from above) angle between the vector\n\t * \"this node -> other node of the segment\"\n\t * and the positive x direction.\n\t */\n\tpublic List<MapWaySegment> getConnectedWaySegments() {\n\t\treturn connectedWaySegments;\n\t}\n\t\n\t/**\n\t * returns all way segments and area segments connected with this node.\n\t * Sorted like {@link #getConnectedWaySegments()}.\n\t */\n\tpublic List<MapSegment> getConnectedSegments() {\n\t\treturn connectedSegments;\n\t}\n\t\n\t/**\n\t * creates the ordering described for {@link #getConnectedSegments()}\n\t */\n\tprivate void sortLinesByAngle(List<? extends MapSegment> lines) {\n\t\tCollections.sort(lines, new Comparator<MapSegment>() {\n\t\t\t@Override\n\t\t\tpublic int compare(MapSegment l1, MapSegment l2) {\n\t\t\t\t\n\t\t\t\tVectorXZ d1 = l1.getDirection();\n\t\t\t\tVectorXZ d2 = l2.getDirection();\n\t\t\t\t\n\t\t\t\tif (inboundLines.contains(l1)) {\n\t\t\t\t\td1 = d1.invert();\n\t\t\t\t}\n\t\t\t\tif (inboundLines.contains(l2)) {\n\t\t\t\t\td2 = d2.invert();\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t//check whether the lines are in the first or second 180°\n\t\t\t\t//(the dot product formula will not be useful to distinguish\n\t\t\t\t//these cases - it only provides cos 0° to cos 180° - ,\n\t\t\t\t//so they need to be handled first)\n\t\t\t\t\n\t\t\t\tif (d1.z < 0 && d2.z > 0) {\n\t\t\t\t\treturn -1;\n\t\t\t\t} else if (d1.z > 0 && d2.z < 0) {\n\t\t\t\t\treturn +1;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t//check the actual angles using the dot product with (1,0,0).\n\t\t\t\t//Two simplifications apply:\n\t\t\t\t//- we don't need to divide by the vector lengths' product,\n\t\t\t\t//  as getDirection returns vectors whose length is 1\n\t\t\t\t//- we don't need to actually calculate the angle itself,\n\t\t\t\t//  because if angle a > angle b, then cos a < cos b\n\t\t\t\t//  (in [0°, 180°])\n\t\t\t\t\n\t\t\t\tdouble comparison = d1.dot(X_UNIT) - d2.dot(X_UNIT);\n\t\t\t\t\n\t\t\t\tif (comparison == 0) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (d1.z < 0) { //and d2.z < 0\n\t\t\t\t\treturn (comparison > 0) ? -1 : +1;\n\t\t\t\t} else { //d1.z > 0 and d2.z > 0\n\t\t\t\t\treturn (comparison > 0) ? +1 : -1;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t});\n\t}\n\t\n\t@Override\n\tpublic List<NodeWorldObject> getRepresentations() {\n\t\treturn representations;\n\t}\n\t\n\t@Override\n\tpublic NodeWorldObject getPrimaryRepresentation() {\n\t\tif (representations.isEmpty()) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn representations.get(0);\n\t\t}\n\t}\n\n\t/**\n\t * adds a visual representation for this node\n\t */\n\tpublic void addRepresentation(NodeWorldObject representation) {\n\t\tthis.representations.add(representation);\n\t}\n\t\t\n\t@Override\n\tpublic String toString() {\n\t\treturn osmNode.toString();\n\t}\n\n\t@Override\n\tpublic Collection<MapOverlap<?,?>> getOverlaps() {\n\t\treturn Collections.emptyList();\n\t}\n\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\treturn new AxisAlignedBoundingBoxXZ(pos.x, pos.z, pos.x, pos.z);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/MapSegment.java",
    "content": "package org.osm2world.core.map_data.data;\n\nimport java.util.List;\n\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.VectorXZ;\n\nimport com.google.common.collect.ImmutableList;\n\npublic class MapSegment {\n\t\n\tprotected final MapNode startNode;\n\tprotected final MapNode endNode;\n\t\n\tpublic MapSegment(MapNode startNode, MapNode endNode) {\n\t\tif (startNode == null || endNode == null) {\n\t\t\tthrow new IllegalArgumentException();\n\t\t}\n\t\tthis.startNode = startNode;\n\t\tthis.endNode = endNode;\n\t}\n\t\n\tpublic MapNode getStartNode() {\n\t\treturn startNode;\n\t}\n\t\n\tpublic MapNode getEndNode() {\n\t\treturn endNode;\n\t}\n\t\n\tpublic MapNode getOtherNode(MapNode node) {\n\t\tif (node == startNode) {\n\t\t\treturn endNode;\n\t\t} else if (node == endNode) {\n\t\t\treturn startNode;\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"not a node of this segment\");\n\t\t}\n\t}\n\t\n\tpublic List<MapNode> getStartEndNodes() {\n\t\treturn ImmutableList.of(startNode, endNode);\n\t}\n\t\n\tpublic LineSegmentXZ getLineSegment() {\n\t\treturn new LineSegmentXZ(startNode.getPos(), endNode.getPos());\n\t}\n\t\n\t/** caches the result for {@link #getDirection()} */\n\tprivate VectorXZ direction = null;\n\t\n\t/** caches the result for {@link #getRightNormal()} */\n\tprivate VectorXZ rightNormal = null;\n\t\n\t/**\n\t * returns a normalized vector indicating the line's horizontal direction\n\t */\n\tpublic VectorXZ getDirection() {\n\t\tif (direction == null) {\n\t\t\tdirection = endNode.getPos().subtract(startNode.getPos());\n\t\t\tdirection = direction.normalize();\n\t\t\t//TODO: (performance) getDirection method in VectorXZ?\n\t\t}\n\t\treturn direction;\n\t}\n\t\n\t/**\n\t * returns a the result of applying {@link VectorXZ#rightNormal()}\n\t * to vector returned by {@link #getDirection()}\n\t */\n\tpublic VectorXZ getRightNormal() {\n\t\tif (rightNormal == null) {\n\t\t\trightNormal = getDirection().rightNormal();\n\t\t}\n\t\treturn rightNormal;\n\t}\n\t\n\t/**\n\t * returns the center of the line\n\t */\n\tpublic VectorXZ getCenter() {\n\t\treturn (startNode.getPos().add(endNode.getPos())).mult(0.5);\n\t}\n\t\n\t/**\n\t * returns true if this MapSegment shares a node with another MapSegment\n\t */\n\tpublic boolean isConnectedTo(MapSegment other) {\n\t\treturn endNode == other.getStartNode()\n\t\t\t|| endNode == other.getEndNode()\n\t\t\t|| startNode == other.getStartNode()\n\t\t\t|| startNode == other.getEndNode();\n\t}\n\t\n\t/**\n\t * returns true if this MapSegment shares a node with a MapArea\n\t */\n\tpublic boolean isConnectedTo(MapArea other) {\n\t\treturn other.getBoundaryNodes().contains(startNode)\n\t\t\t|| other.getBoundaryNodes().contains(endNode);\n\t}\n\t\n\t/**\n\t * returns true if this MapSegment shares both nodes with another MapSegment\n\t */\n\tpublic boolean sharesBothNodes(MapSegment other) {\n\t\treturn (endNode == other.getStartNode()\n\t\t\t\t&& startNode == other.getEndNode())\n\t\t\t|| (endNode == other.getEndNode()\n\t\t\t\t&& startNode == other.getStartNode());\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/MapWaySegment.java",
    "content": "package org.osm2world.core.map_data.data;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.overlaps.MapIntersectionWW;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.osm.data.OSMWay;\nimport org.osm2world.core.world.data.WaySegmentWorldObject;\n\nimport com.google.common.collect.Iterables;\n\n/**\n * line between two nodes in the map data\n */\npublic class MapWaySegment extends MapSegment implements MapElement {\n\n\tprivate final OSMWay osmWay;\n\t\n\tprivate List<WaySegmentWorldObject> representations = new ArrayList<WaySegmentWorldObject>(1);\n\t\n\t@SuppressWarnings(\"unchecked\") //is later checked for EMPTY_LIST using ==\n\tprivate Collection<MapOverlap<?,?>> overlaps = Collections.EMPTY_LIST;\n\t\n\tpublic MapWaySegment(OSMWay osmWay, MapNode startNode, MapNode endNode) {\n\t\tsuper(startNode, endNode);\n\t\tthis.osmWay = osmWay;\n\t}\n\t\n\t@Override\n\tpublic int getLayer() {\n\t\tif (osmWay.tags.containsKey(\"layer\")) {\n\t\t\ttry {\n\t\t\t\treturn Integer.parseInt(osmWay.tags.getValue(\"layer\"));\n\t\t\t} catch (NumberFormatException nfe) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}\n\t\n\tpublic OSMWay getOsmWay() {\n\t\treturn osmWay;\n\t}\n\n\t@Override\n\tpublic TagGroup getTags() {\n\t\treturn getOsmWay().tags;\n\t}\n\t\n\tpublic void addOverlap(MapOverlap<?, ?> overlap) {\n\t\tassert overlap.e1 == this || overlap.e2 == this;\n\t\tif (overlaps == Collections.EMPTY_LIST) {\n\t\t\toverlaps = new ArrayList<MapOverlap<?,?>>();\n\t\t}\n\t\toverlaps.add(overlap);\n\t}\n\t\n\t@Override\n\tpublic Collection<MapOverlap<?,?>> getOverlaps() {\n\t\treturn overlaps;\n\t}\n\t\n\tpublic Iterable<MapIntersectionWW> getIntersectionsWW() {\n\t\treturn Iterables.filter(overlaps, MapIntersectionWW.class);\n\t}\n\t\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\treturn new AxisAlignedBoundingBoxXZ(Arrays.asList(\n\t\t\t\tstartNode.getPos(), endNode.getPos()));\n\t}\n\t\n\t@Override\n\tpublic List<WaySegmentWorldObject> getRepresentations() {\n\t\treturn representations;\n\t}\n\t\n\t@Override\n\tpublic WaySegmentWorldObject getPrimaryRepresentation() {\n\t\tif (representations.isEmpty()) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn representations.get(0);\n\t\t}\n\t}\n\n\t/**\n\t * adds a visual representation for this way segment\n\t */\n\tpublic void addRepresentation(WaySegmentWorldObject representation) {\n\t\tthis.representations.add(representation);\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn startNode + \"->\" + endNode;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/overlaps/MapIntersectionWW.java",
    "content": "package org.osm2world.core.map_data.data.overlaps;\n\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.VectorXZ;\n\n/** intersection between two {@link MapWaySegment}s (\"Way-Way\") */\npublic class MapIntersectionWW extends MapOverlap<MapWaySegment, MapWaySegment> {\n\t\n\tpublic final VectorXZ pos;\n\t\n\tpublic MapIntersectionWW(MapWaySegment line1, MapWaySegment line2, VectorXZ pos) {\n\t\tsuper(line1, line2, MapOverlapType.INTERSECT);\n\t\tthis.pos = pos;\n\t}\n\t\n\t/**\n\t * takes one of the intersecting {@link MapWaySegment}s\n\t * and returns the other one\n\t */\n\tpublic MapWaySegment getOther(MapElement line) {\n\t\treturn (MapWaySegment) super.getOther(line);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/overlaps/MapOverlap.java",
    "content": "package org.osm2world.core.map_data.data.overlaps;\n\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapWaySegment;\n\n/**\n * supertype for intersections and inclusions\n * between two {@link MapElement}s\n */\npublic abstract class MapOverlap<T1 extends MapElement, T2 extends MapElement> {\n\t\n\tpublic final T1 e1;\n\tpublic final T2 e2;\n\t\n\tpublic final MapOverlapType type;\n\t\n\tpublic MapOverlap(T1 e1, T2 e2, MapOverlapType type) {\n\t\tthis.e1 = e1;\n\t\tthis.e2 = e2;\n\t\tthis.type = type;\n\t}\n\n\t/**\n\t * takes one of the {@link MapWaySegment}s that participate\n\t * in this overlap and returns the other one\n\t */\n\tpublic MapElement getOther(MapElement element) {\n\t\tif (element == e1) {\n\t\t\treturn e2;\n\t\t} else if (element == e2) {\n\t\t\treturn e1;\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"element isn't part of this intersection\");\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"( \" + e1.toString() + \" - \" + e2.toString() + \" )\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/overlaps/MapOverlapAA.java",
    "content": "package org.osm2world.core.map_data.data.overlaps;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * overlap between two areas (\"Area-Area\").\n * The two areas' outlines either intersect\n * or one of them is completely within the other one.\n */\npublic class MapOverlapAA extends MapOverlap<MapArea, MapArea> {\n\n\tpublic MapOverlapAA(MapArea area1, MapArea area2, MapOverlapType type) {\n\t\tsuper(area1, area2, type);\n\t}\n\t\n\t@Override\n\tpublic MapArea getOther(MapElement element) {\n\t\treturn (MapArea) super.getOther(element);\n\t}\n\n\tpublic Collection<VectorXZ> getIntersectionPositions() {\n\t\treturn e1.getPolygon().intersectionPositions(e2.getPolygon());\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/overlaps/MapOverlapNA.java",
    "content": "package org.osm2world.core.map_data.data.overlaps;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\n\n/**\n * overlap between a {@link MapWaySegment} and a {@link MapArea} (\"Way-Area\").\n * The way either intersects with the area\n * or is completely contained within the area.\n */\npublic class MapOverlapNA extends MapOverlap<MapNode, MapArea> {\n\t\n\tpublic MapOverlapNA(MapNode node, MapArea area, MapOverlapType type) {\n\t\tsuper(node, area, type);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/overlaps/MapOverlapType.java",
    "content": "package org.osm2world.core.map_data.data.overlaps;\n\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapSegment;\n\n//TODO: multiple ones at same time (areas that share segments AND intersect)!\n\npublic enum MapOverlapType {\n\t\n\t/** two {@link MapElement}s intersect */\n\tINTERSECT,\n\t\n\t/** the second {@link MapElement} contains the first {@link MapElement} */\n\tCONTAIN,\n\t\n\t/**\n\t * {@link MapSegment#sharesBothNodes(MapSegment)}\n\t * is true for a pair of {@link MapSegment}s, one from each {@link MapElement}\n\t */\n\tSHARE_SEGMENT\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/data/overlaps/MapOverlapWA.java",
    "content": "package org.osm2world.core.map_data.data.overlaps;\n\nimport static java.util.Collections.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapAreaSegment;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * overlap between a {@link MapWaySegment} and a {@link MapArea} (\"Way-Area\").\n * The way either intersects with the area\n * or is completely contained within the area.\n */\npublic class MapOverlapWA extends MapOverlap<MapWaySegment, MapArea> {\n\t\n\t//TODO maybe create a MapIntersection object for each of these?\n\tprivate final List<VectorXZ> intersectionPositions;\n\tprivate final List<MapAreaSegment> intersectingAreaSegments;\n\t\n\tpublic MapOverlapWA(MapWaySegment waySegment, MapArea area,\n\t\t\tMapOverlapType type, List<VectorXZ> intersectionPositions,\n\t\t\tList<MapAreaSegment> intersectingAreaSegments) {\n\t\t\n\t\tsuper(waySegment, area, type);\n\t\t\n\t\tthis.intersectionPositions = intersectionPositions;\n\t\tthis.intersectingAreaSegments = intersectingAreaSegments;\n\t\t\n\t}\n\n\tpublic MapArea getOther(MapWaySegment waySegment) {\n\t\treturn (MapArea) super.getOther(waySegment);\n\t}\n\n\tpublic MapWaySegment getOther(MapArea area) {\n\t\treturn (MapWaySegment) super.getOther(area);\n\t}\n\n\t/** same order as {@link #getIntersectingAreaSegments()} */\n\tpublic List<VectorXZ> getIntersectionPositions() {\n\t\treturn intersectionPositions;\n\t}\n\t\n\t/** same order as {@link #getIntersectionPositions()} */\n\tpublic List<MapAreaSegment> getIntersectingAreaSegments() {\n\t\treturn intersectingAreaSegments;\n\t}\n\t\n\tpublic Collection<LineSegmentXZ> getOverlappedSegments() {\n\t\t\n\t\tif (type == MapOverlapType.CONTAIN) {\n\t\t\t\n\t\t\treturn singletonList(e1.getLineSegment());\n\t\t\t\t\t\t\n\t\t} else if (type == MapOverlapType.INTERSECT) {\n\t\t\t\n\t\t\t/* order intersections and start/end nodes\n\t\t\t * by distance from the way segments' start node */\n\t\t\t\n\t\t\tList<VectorXZ> positions = new ArrayList<VectorXZ>(getIntersectionPositions());\n\t\t\tpositions.add(e1.getStartNode().getPos());\n\t\t\tpositions.add(e1.getEndNode().getPos());\n\t\t\t\n\t\t\tsort(positions, new Comparator<VectorXZ>() {\n\t\t\t\tpublic int compare(VectorXZ v1, VectorXZ v2) {\n\t\t\t\t\treturn Double.compare(\n\t\t\t\t\t\t\tVectorXZ.distance(v1, e1.getStartNode().getPos()),\n\t\t\t\t\t\t\tVectorXZ.distance(v2, e1.getStartNode().getPos()));\n\t\t\t\t}\n\t\t\t});\n\t\t\t\t\t\t\n\t\t\t/* check for each line segments between two positions\n\t\t\t * whether it overlaps with the area */\n\t\t\t\n\t\t\tList<LineSegmentXZ> result = new ArrayList<LineSegmentXZ>();\n\t\t\t\n\t\t\tfor (int i = 0; i+1 < positions.size(); i++) {\n\t\t\t\t\n\t\t\t\tLineSegmentXZ segment =\n\t\t\t\t\tnew LineSegmentXZ(positions.get(i), positions.get(i+1));\n\t\t\t\t\n\t\t\t\tif (e2.getPolygon().contains(segment.getCenter())) {\n\t\t\t\t\tresult.add(segment);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn result;\n\t\t\t\n\t\t} else {\n\n\t\t\treturn emptyList();\n\t\t\t\n\t\t}\n\t}\n\t\n\tpublic Collection<LineSegmentXZ> getSharedSegments() {\n\t\t\n\t\tif (type == MapOverlapType.SHARE_SEGMENT) {\n\t\t\t\n\t\t\tfor (MapAreaSegment areaSegment : e2.getAreaSegments()) {\n\t\t\t\tif (areaSegment.sharesBothNodes(e1)) {\n\t\t\t\t\treturn singletonList(areaSegment.getLineSegment());\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tthrow new Error(\"no shared segment found\");\n\t\t\t\n\t\t} else {\n\n\t\t\treturn emptyList();\n\t\t\t\n\t\t}\n\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/object_info/AddressGatherer.java",
    "content": "package org.osm2world.core.map_data.object_info;\n\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.object_info.ObjectType.MainType;\nimport org.osm2world.core.osm.data.OSMData;\nimport org.osm2world.core.osm.data.OSMNode;\nimport org.osm2world.core.osm.data.OSMRelation;\n\npublic class AddressGatherer {\n\tpublic static void gather(OSMData osmData, MapData grid) {\n\t\tfor (MapNode mapNode : grid.getMapNodes()) {\n\t\t\tgatherMapNode(mapNode);\n\t\t}\n\t\tfor (OSMNode node : osmData.getNodes()) {\n\t\t\tgatherOsmNode(node);\n\t\t}\n//\t\tfor (OSMWay way : osmData.getWays()) {\n//\t\t\taddAddresses(way);\n//\t\t}\n\t\tfor (OSMRelation relation : osmData.getRelations()) {\n\t\t\t// http://wiki.openstreetmap.org/wiki/Relation:associatedStreet\n\t\t}\n\t}\n\n\tprivate static void gatherMapNode(MapNode mapNode) {\n\t\tgatherAnyNode(mapNode.getOsmNode(), mapNode);\n\n//\t\t\tBaseObject addr = ObjectInfoManager.addAddress(name);\n//\t\t\tString houseNumber = node.tags.getValue(\"addr:housenumber\");\n//\t\t\tif (! Strings.isNullOrEmpty(houseNumber)) {\n//\t\t\t\t// TODO: expand number ranges etc: https://en.wikipedia.org/wiki/House_numbering\n////\t\t\t\taddr.houseNumbers.add(houseNumber);\n//\t\t\t}\n\t}\n\n\tprivate static void gatherOsmNode(OSMNode node) {\n\t\tgatherAnyNode(node, null);\n\t}\n\n\tprivate static void gatherAnyNode(OSMNode osmNode, MapNode mapNode) {\n\t\tObjectType type = ObjectType.fromElement(osmNode);\n\t\tif (type.maintype == MainType.POI) {\n\t\t\tObjectInfoManager.addPoi(osmNode, type.subtype, mapNode);\n\t\t}\n//\t\tfor (Tag tag : osmNode.tags) {\n//\t\t\tSystem.out.println(tag.key + \": \" + tag.key + \"=\" + tag.value);\n//\t\t}\n//\t\tSystem.out.println(\"---\");\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/object_info/BaseObject.java",
    "content": "package org.osm2world.core.map_data.object_info;\n\npublic abstract class BaseObject {\n\n\tpublic final String name;\n\n\tpublic BaseObject(String name) {\n\t\tthis.name = name;\n\t}\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/object_info/MapMetaWriter.java",
    "content": "package org.osm2world.core.map_data.object_info;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport java.util.TreeSet;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.data.OSMElement;\nimport org.osm2world.core.osm.data.OSMNode;\nimport org.osm2world.core.osm.data.OSMRelation;\nimport org.osm2world.core.osm.data.OSMWay;\nimport org.osm2world.core.util.TouchMapperProfile;\nimport org.osm2world.core.world.data.WorldObject;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.ObjectWriter;\n\npublic final class MapMetaWriter {\n\n\tprivate static final String EMPTY_TERRAIN_SURFACE = \"osm2world:empty_terrain\";\n\n\tprivate MapMetaWriter() {}\n\n\tpublic static void write(File outputFile, MapData mapData) throws IOException {\n\t\tlong totalStart = TouchMapperProfile.start();\n\t\tObjectWriter writer = new ObjectMapper().writer();\n\n\t\tMap<String, Object> root = new LinkedHashMap<String, Object>();\n\t\tlong buildMetaStart = TouchMapperProfile.start();\n\t\tMap<String, Object> meta = buildMeta(mapData);\n\t\tTouchMapperProfile.logMillis(\"map_meta.build_meta_ms\", buildMetaStart);\n\n\t\tlong buildNodesStart = TouchMapperProfile.start();\n\t\tList<Map<String, Object>> nodes = buildNodes(mapData);\n\t\tTouchMapperProfile.logMillis(\"map_meta.build_nodes_ms\", buildNodesStart);\n\n\t\tlong buildWaysStart = TouchMapperProfile.start();\n\t\tList<Map<String, Object>> ways = buildWays(mapData);\n\t\tTouchMapperProfile.logMillis(\"map_meta.build_ways_ms\", buildWaysStart);\n\n\t\tlong buildAreasStart = TouchMapperProfile.start();\n\t\tList<Map<String, Object>> areas = buildAreas(mapData);\n\t\tTouchMapperProfile.logMillis(\"map_meta.build_areas_ms\", buildAreasStart);\n\n\t\troot.put(\"meta\", meta);\n\t\troot.put(\"nodes\", nodes);\n\t\troot.put(\"ways\", ways);\n\t\troot.put(\"areas\", areas);\n\t\tTouchMapperProfile.logValue(\"map_meta.nodes_count\", nodes.size());\n\t\tTouchMapperProfile.logValue(\"map_meta.ways_count\", ways.size());\n\t\tTouchMapperProfile.logValue(\"map_meta.areas_count\", areas.size());\n\n\t\tlong writeStart = TouchMapperProfile.start();\n\t\twriter.writeValue(outputFile, root);\n\t\tTouchMapperProfile.logMillis(\"map_meta.write_file_ms\", writeStart);\n\t\tTouchMapperProfile.logMillis(\"map_meta.total_ms\", totalStart);\n\t}\n\n\tprivate static Map<String, Object> buildMeta(MapData mapData) {\n\t\tMap<String, Object> meta = new LinkedHashMap<String, Object>();\n\t\tmeta.put(\"boundary\", boundsToMap(mapData.getBoundary()));\n\t\tmeta.put(\"dataBoundary\", boundsToMap(mapData.getDataBoundary()));\n\t\treturn meta;\n\t}\n\n\tprivate static List<Map<String, Object>> buildNodes(MapData mapData) {\n\t\tList<MapNode> nodes = new ArrayList<MapNode>();\n\t\tfor (MapNode node : mapData.getMapNodes()) {\n\t\t\tif (node.getRepresentations().isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isEmptyTerrain(node.getTags())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tnodes.add(node);\n\t\t}\n\n\t\tCollections.sort(nodes, new Comparator<MapNode>() {\n\t\t\t@Override\n\t\t\tpublic int compare(MapNode a, MapNode b) {\n\t\t\t\tlong aId = a.getOsmNode().id;\n\t\t\t\tlong bId = b.getOsmNode().id;\n\t\t\t\treturn (aId < bId) ? -1 : ((aId > bId) ? 1 : 0);\n\t\t\t}\n\t\t});\n\n\t\tList<Map<String, Object>> output = new ArrayList<Map<String, Object>>();\n\t\tfor (MapNode node : nodes) {\n\t\t\toutput.add(buildNodeEntry(node));\n\t\t}\n\t\treturn output;\n\t}\n\n\tprivate static List<Map<String, Object>> buildWays(MapData mapData) {\n\t\tMap<OSMNode, VectorXZ> nodePositions = new HashMap<OSMNode, VectorXZ>();\n\t\tfor (MapNode node : mapData.getMapNodes()) {\n\t\t\tnodePositions.put(node.getOsmNode(), node.getPos());\n\t\t}\n\n\t\tMap<OSMWay, List<MapWaySegment>> segmentsByWay =\n\t\t\t\tnew HashMap<OSMWay, List<MapWaySegment>>();\n\t\tfor (MapWaySegment segment : mapData.getMapWaySegments()) {\n\t\t\tif (segment.getRepresentations().isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tOSMWay way = segment.getOsmWay();\n\t\t\tList<MapWaySegment> segments = segmentsByWay.get(way);\n\t\t\tif (segments == null) {\n\t\t\t\tsegments = new ArrayList<MapWaySegment>();\n\t\t\t\tsegmentsByWay.put(way, segments);\n\t\t\t}\n\t\t\tsegments.add(segment);\n\t\t}\n\n\t\tList<OSMWay> ways = new ArrayList<OSMWay>(segmentsByWay.keySet());\n\t\tCollections.sort(ways, new Comparator<OSMWay>() {\n\t\t\t@Override\n\t\t\tpublic int compare(OSMWay a, OSMWay b) {\n\t\t\t\tlong aId = a.id;\n\t\t\t\tlong bId = b.id;\n\t\t\t\treturn (aId < bId) ? -1 : ((aId > bId) ? 1 : 0);\n\t\t\t}\n\t\t});\n\n\t\tList<Map<String, Object>> output = new ArrayList<Map<String, Object>>();\n\t\tfor (OSMWay way : ways) {\n\t\t\tList<VectorXZ> positions = new ArrayList<VectorXZ>();\n\t\t\tList<List<Double>> coords = new ArrayList<List<Double>>();\n\t\t\tfor (OSMNode node : way.nodes) {\n\t\t\t\tVectorXZ pos = nodePositions.get(node);\n\t\t\t\tif (pos == null) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tpositions.add(pos);\n\t\t\t\tcoords.add(coordToList(pos));\n\t\t\t}\n\t\t\tif (coords.isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tMap<String, Object> entry =\n\t\t\t\t\tbuildWayEntry(way, segmentsByWay.get(way), positions, coords);\n\t\t\toutput.add(entry);\n\t\t}\n\n\t\treturn output;\n\t}\n\n\tprivate static List<Map<String, Object>> buildAreas(MapData mapData) {\n\t\tList<MapArea> areas = new ArrayList<MapArea>();\n\t\tfor (MapArea area : mapData.getMapAreas()) {\n\t\t\tif (area.getRepresentations().isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isEmptyTerrain(area.getTags())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tareas.add(area);\n\t\t}\n\n\t\tCollections.sort(areas, new Comparator<MapArea>() {\n\t\t\t@Override\n\t\t\tpublic int compare(MapArea a, MapArea b) {\n\t\t\t\tlong aId = a.getOsmObject().id;\n\t\t\t\tlong bId = b.getOsmObject().id;\n\t\t\t\treturn (aId < bId) ? -1 : ((aId > bId) ? 1 : 0);\n\t\t\t}\n\t\t});\n\n\t\tList<Map<String, Object>> output = new ArrayList<Map<String, Object>>();\n\t\tfor (MapArea area : areas) {\n\t\t\toutput.add(buildAreaEntry(area));\n\t\t}\n\t\treturn output;\n\t}\n\n\tprivate static Map<String, Object> buildNodeEntry(MapNode node) {\n\t\tMap<String, Object> entry = new LinkedHashMap<String, Object>();\n\t\tOSMNode osmNode = node.getOsmNode();\n\n\t\taddCommonFields(entry, \"node\", osmNode, node.getTags());\n\t\tSet<String> repNames = addRepresentationFields(entry, node.getRepresentations());\n\t\taddTouchMapperFields(entry, repNames, node.getTags(), node);\n\n\t\tVectorXZ pos = node.getPos();\n\t\tAxisAlignedBoundingBoxXZ bounds = new AxisAlignedBoundingBoxXZ(\n\t\t\t\tpos.x, pos.z, pos.x, pos.z);\n\n\t\tentry.put(\"geometry\", pointGeometry(pos));\n\t\tentry.put(\"bounds\", boundsToMap(bounds));\n\t\tentry.put(\"center\", centerToList(bounds));\n\n\t\treturn entry;\n\t}\n\n\tprivate static Map<String, Object> buildWayEntry(OSMWay way,\n\t\t\tList<MapWaySegment> segments, List<VectorXZ> positions,\n\t\t\tList<List<Double>> coords) {\n\t\tMap<String, Object> entry = new LinkedHashMap<String, Object>();\n\n\t\taddCommonFields(entry, \"way\", way, way.tags);\n\t\tSet<String> repNames = addWayRepresentationFields(entry, segments);\n\t\taddTouchMapperFields(entry, repNames, way.tags, null);\n\n\t\tAxisAlignedBoundingBoxXZ bounds = new AxisAlignedBoundingBoxXZ(positions);\n\n\t\tMap<String, Object> geometry = new LinkedHashMap<String, Object>();\n\t\tgeometry.put(\"type\", \"line_string\");\n\t\tgeometry.put(\"coordinates\", coords);\n\t\tgeometry.put(\"closed\", Boolean.valueOf(way.isClosed()));\n\n\t\tentry.put(\"geometry\", geometry);\n\t\tentry.put(\"isClosed\", Boolean.valueOf(way.isClosed()));\n\t\tentry.put(\"bounds\", boundsToMap(bounds));\n\t\tentry.put(\"center\", centerToList(bounds));\n\n\t\treturn entry;\n\t}\n\n\tprivate static Map<String, Object> buildAreaEntry(MapArea area) {\n\t\tMap<String, Object> entry = new LinkedHashMap<String, Object>();\n\n\t\tOSMElement element = area.getOsmObject();\n\n\t\taddCommonFields(entry, \"area\", element, area.getTags());\n\t\tSet<String> repNames = addRepresentationFields(entry, area.getRepresentations());\n\t\taddTouchMapperFields(entry, repNames, area.getTags(), null);\n\n\t\tList<List<Double>> outer = new ArrayList<List<Double>>();\n\t\tfor (MapNode node : area.getBoundaryNodes()) {\n\t\t\touter.add(coordToList(node.getPos()));\n\t\t}\n\n\t\tList<List<List<Double>>> holes = new ArrayList<List<List<Double>>>();\n\t\tfor (List<MapNode> holeNodes : area.getHoles()) {\n\t\t\tList<List<Double>> hole = new ArrayList<List<Double>>();\n\t\t\tfor (MapNode node : holeNodes) {\n\t\t\t\thole.add(coordToList(node.getPos()));\n\t\t\t}\n\t\t\tholes.add(hole);\n\t\t}\n\n\t\tMap<String, Object> geometry = new LinkedHashMap<String, Object>();\n\t\tgeometry.put(\"type\", \"polygon\");\n\t\tgeometry.put(\"outer\", outer);\n\t\tgeometry.put(\"holes\", holes);\n\n\t\tAxisAlignedBoundingBoxXZ bounds = area.getAxisAlignedBoundingBoxXZ();\n\n\t\tentry.put(\"geometry\", geometry);\n\t\tentry.put(\"hasHoles\", Boolean.valueOf(!holes.isEmpty()));\n\t\tentry.put(\"bounds\", boundsToMap(bounds));\n\t\tentry.put(\"center\", centerToList(bounds));\n\n\t\treturn entry;\n\t}\n\n\tprivate static void addCommonFields(Map<String, Object> entry,\n\t\t\tString elementType, OSMElement osmElement, TagGroup tags) {\n\t\tentry.put(\"elementType\", elementType);\n\t\tentry.put(\"osmType\", osmType(osmElement));\n\t\tentry.put(\"osmId\", Long.valueOf(osmElement.id));\n\t\tentry.put(\"layer\", Integer.valueOf(getLayer(tags)));\n\t\tentry.put(\"tags\", tagsToMap(tags));\n\t}\n\n\tprivate static Set<String> addRepresentationFields(Map<String, Object> entry,\n\t\t\tList<? extends WorldObject> representations) {\n\t\tSet<String> repNames = new TreeSet<String>();\n\t\tSet<String> groundStates = new TreeSet<String>();\n\t\tfor (WorldObject representation : representations) {\n\t\t\trepNames.add(representation.getClass().getSimpleName());\n\t\t\tgroundStates.add(representation.getGroundState().name());\n\t\t}\n\n\t\tWorldObject primary = representations.isEmpty()\n\t\t\t\t? null\n\t\t\t\t: representations.get(0);\n\n\t\tentry.put(\"representations\", new ArrayList<String>(repNames));\n\t\tentry.put(\"primaryRepresentation\",\n\t\t\t\t(primary == null) ? null : primary.getClass().getSimpleName());\n\t\tentry.put(\"groundStates\", new ArrayList<String>(groundStates));\n\t\tentry.put(\"primaryGroundState\",\n\t\t\t\t(primary == null) ? null : primary.getGroundState().name());\n\t\treturn repNames;\n\t}\n\n\tprivate static Set<String> addWayRepresentationFields(Map<String, Object> entry,\n\t\t\tList<MapWaySegment> segments) {\n\t\tSet<String> repNames = new TreeSet<String>();\n\t\tSet<String> groundStates = new TreeSet<String>();\n\t\tSet<String> primaryNames = new TreeSet<String>();\n\t\tSet<String> primaryGroundStates = new TreeSet<String>();\n\n\t\tfor (MapWaySegment segment : segments) {\n\t\t\tfor (WorldObject representation : segment.getRepresentations()) {\n\t\t\t\trepNames.add(representation.getClass().getSimpleName());\n\t\t\t\tgroundStates.add(representation.getGroundState().name());\n\t\t\t}\n\t\t\tWorldObject primary = segment.getPrimaryRepresentation();\n\t\t\tif (primary != null) {\n\t\t\t\tprimaryNames.add(primary.getClass().getSimpleName());\n\t\t\t\tprimaryGroundStates.add(primary.getGroundState().name());\n\t\t\t}\n\t\t}\n\n\t\tentry.put(\"representations\", new ArrayList<String>(repNames));\n\t\tentry.put(\"groundStates\", new ArrayList<String>(groundStates));\n\t\tentry.put(\"primaryRepresentation\",\n\t\t\t\t(primaryNames.size() == 1) ? primaryNames.iterator().next() : null);\n\t\tentry.put(\"primaryGroundState\",\n\t\t\t\t(primaryGroundStates.size() == 1)\n\t\t\t\t\t\t? primaryGroundStates.iterator().next()\n\t\t\t\t\t\t: null);\n\t\treturn repNames;\n\t}\n\n\tprivate static void addTouchMapperFields(Map<String, Object> entry,\n\t\t\tSet<String> repNames, TagGroup tags, MapNode node) {\n\t\tString category = TouchMapperCategory.categoryForRepresentationNames(repNames);\n\t\tif (category != null) {\n\t\t\tentry.put(\"tmCategory\", category);\n\t\t}\n\t\tif (\"Road\".equals(category) || \"RoadArea\".equals(category)) {\n\t\t\tboolean pedestrian = (node != null)\n\t\t\t\t\t? TouchMapperCategory.isPedestrian(node)\n\t\t\t\t\t: TouchMapperCategory.isPedestrian(tags);\n\t\t\tentry.put(\"tmRoadType\", pedestrian ? \"pedestrian\" : \"car\");\n\t\t}\n\t}\n\n\tprivate static Map<String, String> tagsToMap(TagGroup tags) {\n\t\tMap<String, String> out = new TreeMap<String, String>();\n\t\tfor (Tag tag : tags) {\n\t\t\tout.put(tag.key, tag.value);\n\t\t}\n\t\treturn out;\n\t}\n\n\tprivate static Map<String, Double> boundsToMap(AxisAlignedBoundingBoxXZ bounds) {\n\t\tMap<String, Double> out = new LinkedHashMap<String, Double>();\n\t\tout.put(\"minX\", Double.valueOf(bounds.minX));\n\t\tout.put(\"minY\", Double.valueOf(bounds.minZ));\n\t\tout.put(\"maxX\", Double.valueOf(bounds.maxX));\n\t\tout.put(\"maxY\", Double.valueOf(bounds.maxZ));\n\t\treturn out;\n\t}\n\n\tprivate static List<Double> centerToList(AxisAlignedBoundingBoxXZ bounds) {\n\t\tdouble centerX = (bounds.minX + bounds.maxX) / 2.0;\n\t\tdouble centerZ = (bounds.minZ + bounds.maxZ) / 2.0;\n\t\tList<Double> center = new ArrayList<Double>(2);\n\t\tcenter.add(Double.valueOf(centerX));\n\t\tcenter.add(Double.valueOf(centerZ));\n\t\treturn center;\n\t}\n\n\tprivate static List<Double> coordToList(VectorXZ pos) {\n\t\tList<Double> coord = new ArrayList<Double>(2);\n\t\tcoord.add(Double.valueOf(pos.x));\n\t\tcoord.add(Double.valueOf(pos.z));\n\t\treturn coord;\n\t}\n\n\tprivate static Map<String, Object> pointGeometry(VectorXZ pos) {\n\t\tMap<String, Object> geometry = new LinkedHashMap<String, Object>();\n\t\tgeometry.put(\"type\", \"point\");\n\t\tgeometry.put(\"coordinates\", coordToList(pos));\n\t\treturn geometry;\n\t}\n\n\tprivate static boolean isEmptyTerrain(TagGroup tags) {\n\t\treturn tags != null && tags.contains(\"surface\", EMPTY_TERRAIN_SURFACE);\n\t}\n\n\tprivate static String osmType(OSMElement element) {\n\t\tif (element instanceof OSMNode) {\n\t\t\treturn \"node\";\n\t\t} else if (element instanceof OSMWay) {\n\t\t\treturn \"way\";\n\t\t} else if (element instanceof OSMRelation) {\n\t\t\treturn \"relation\";\n\t\t}\n\t\treturn \"unknown\";\n\t}\n\n\tprivate static int getLayer(TagGroup tags) {\n\t\tif (tags != null && tags.containsKey(\"layer\")) {\n\t\t\ttry {\n\t\t\t\treturn Integer.parseInt(tags.getValue(\"layer\"));\n\t\t\t} catch (NumberFormatException nfe) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/object_info/ObjectInfoManager.java",
    "content": "package org.osm2world.core.map_data.object_info;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.osm.data.OSMElement;\nimport org.osm2world.core.osm.data.OSMNode;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.ObjectWriter;\n\npublic class ObjectInfoManager {\n\tprivate static Map<String, WayObject> ways = new HashMap<>();\n\tprivate static Map<String, PoiObject> pois = new HashMap<>();\n\tprivate static Map<String, Map<String, PoiObject>> poiByType = new HashMap<>(); // tram_stop => Jaakkimantie => WayObject\n\t\n\tpublic static void addRoadSegment(MapWaySegment line) {\n\t\ttry {\n\t\t\tString name = line.getTags().getValue(\"name\");\n\t\t\tif (name == null || name.equals(\"\")) {\n\t\t\t\tname = \"_unnamed_roads\";\n\t\t\t}\n\t\t\tWayObject way = ways.get(name);\n\t\t\tif (way == null) {\n\t\t\t\tway = new WayObject(name);\n\t\t\t\tways.put(name, way);\n\t\t\t}\n\t\t\tway.addSegment(line.getLineSegment());\n\t\t\t\n\t\t\tfor (Tag tag : line.getTags()) {\n\t\t\t\t//System.out.println(\"tag:\" + tag.key + \"=\" + tag.value);\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tSystem.out.println(line);\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n\tpublic static void addPoi(OSMElement element, String subtype, MapNode mapNode) {\n\t\tString name = element.tags.getValue(\"name\");\n\t\tif (name == null) {\n\t\t\treturn;\n\t\t}\n\t\tif (mapNode == null && pois.containsKey(name)) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Create PoiObject\n\t\tPoiObject poi = new PoiObject(name);\n\t\tif (mapNode != null) {\n\t\t\tpoi.center = new Point(mapNode.getPos().x, mapNode.getPos().z);\n\t\t}\n\t\t\n\t\t// Store by name and type+name\n\t\tpois.put(name, poi);\n\t\tMap<String, PoiObject> byType = poiByType.get(subtype);\n\t\tif (byType == null) {\n\t\t\tbyType = new HashMap<>();\n\t\t\tpoiByType.put(subtype, byType);\n\t\t}\n\t\tbyType.put(name, poi);\n\t\t\n\t\t// If POI references a street and has a housenumber, add that to the street.\n\t\taddStreetHouseNumber(poi, element);\n\t}\n\n\tpublic WayObject getPoiStreet(PoiObject poi) {\n\t\tif (poi.street != null && ways.containsKey(poi.street)) {\n\t\t\treturn ways.get(poi.street);\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate static void addStreetHouseNumber(PoiObject poi, OSMElement element) {\n\t\tString street = element.tags.getValue(\"addr:street\");\n\t\tif (street != null) {\n\t\t\tpoi.street = street;\n\t\t\tString houseNumber = element.tags.getValue(\"addr:housenumber\");\n\t\t\tif (houseNumber != null) {\n\t\t\t\tpoi.houseNumber = houseNumber;\n\t\t\t}\n\t\t}\n\t}\n\t\n\tpublic static String getJsonLine() {\n\t\tObjectWriter writer = new ObjectMapper().writer();\n\t\ttry {\n\t\t\tHashMap<String, Object> out = new HashMap<>();\n\t\t\tout.put(\"ways\", ways);\n\t\t\tout.put(\"pois\", poiByType);\n\t\t\treturn writer.writeValueAsString(out);\n\t\t} catch (JsonProcessingException e) {\n\t\t\te.printStackTrace();\n\t\t\treturn \"{}\";\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/object_info/ObjectType.java",
    "content": "package org.osm2world.core.map_data.object_info;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.osm.data.OSMElement;\n\npublic class ObjectType {\n\tfinal MainType maintype;\n\tfinal String subtype;\n\t\n\tprivate ObjectType(MainType maintype, String subtype) {\n\t\tthis.maintype = maintype;\n\t\tthis.subtype = subtype;\n\t}\n\t\n\tpublic static ObjectType fromElement(OSMElement element) {\n\t\tTagGroup tags = element.tags;\n\t\t\n\t\tif (tags.contains(\"highway\", \"bus_stop\")) {\n\t\t\treturn poi(\"bus_stop\");\n\t\t}\n\t\tif (tags.containsKey(\"public_transport\") && tags.contains(\"bus\", \"yes\")) {\n\t\t\treturn poi(\"bus_stop\");\n\t\t}\n\t\tif (tags.contains(\"railway\", \"tram_stop\")) {\n\t\t\treturn poi(\"tram_stop\");\n\t\t}\n\t\tif (tags.containsKey(\"public_transport\") && tags.contains(\"tram\", \"yes\")) {\n\t\t\treturn poi(\"bus_stop\");\n\t\t}\n\t\tif (tags.containsKey(\"shop\")) {\n\t\t\treturn poi(\"shop\");\n\t\t}\n\t\tif (tags.containsKey(\"cuisine\") || tags.contains(\"amenity\", \"restaurant\")) {\n\t\t\treturn poi(\"restaurant\");\n\t\t}\n//\t\tif (tags.contains(\"\")) {\n//\t\t\treturn poi(\"\");\n//\t\t}\n\t\treturn new ObjectType(MainType.UNKNOWN, \"\");\n\t}\n\t\n\tprivate static ObjectType poi(String subtype) {\n\t\treturn new ObjectType(MainType.POI, subtype);\n\t}\n\tprivate static ObjectType way(String subtype) {\n\t\treturn new ObjectType(MainType.WAY, subtype);\n\t}\n\tstatic enum MainType {\n\t\tWAY,         // roads, railways, rivers\n\t\tPOI,         // bus stops, shops\n\t\tUNKNOWN }\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/object_info/PoiObject.java",
    "content": "package org.osm2world.core.map_data.object_info;\n\nimport org.osm2world.core.math.VectorXZ;\n\npublic class PoiObject extends BaseObject {\n    public String street; // if set, may match name of a WayObject\n    public String houseNumber;\n\tpublic Point center;\n\n\tpublic PoiObject(String name) {\n\t\tsuper(name);\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/object_info/Point.java",
    "content": "package org.osm2world.core.map_data.object_info;\n\npublic class Point {\n    public final double x;\n    public final double y;\n\n    public Point(double var1, double var3) {\n        this.x = var1;\n        this.y = var3;\n    }\n\n    public int hashCode() {\n        byte var2 = 1;\n        long var3 = Double.doubleToLongBits(this.x);\n        int var5 = 31 * var2 + (int)(var3 ^ var3 >>> 32);\n        var3 = Double.doubleToLongBits(this.y);\n        var5 = 31 * var5 + (int)(var3 ^ var3 >>> 32);\n        return var5;\n    }\n\n    public boolean equals(Object var1) {\n        if (this == var1) {\n            return true;\n        } else if (var1 == null) {\n            return false;\n        } else if (this.getClass() != var1.getClass()) {\n            return false;\n        } else {\n            Point var2 = (Point)var1;\n            if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(var2.x)) {\n                return false;\n            } else {\n                return Double.doubleToLongBits(this.y) == Double.doubleToLongBits(var2.y);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/object_info/TouchMapperCategory.java",
    "content": "package org.osm2world.core.map_data.object_info;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.world.data.WorldObject;\n\npublic final class TouchMapperCategory {\n\n\tprivate TouchMapperCategory() {}\n\n\tpublic static String categoryForWorldObject(WorldObject object) {\n\t\tif (object == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn categoryForRepresentationNames(\n\t\t\t\tCollections.singleton(object.getClass().getSimpleName()));\n\t}\n\n\tpublic static String categoryForRepresentationNames(Set<String> repNames) {\n\t\tif (repNames == null || repNames.isEmpty()) {\n\t\t\treturn null;\n\t\t}\n\t\tif (containsPrefix(repNames, \"RoadArea\")) {\n\t\t\treturn \"RoadArea\";\n\t\t}\n\t\tif (containsPrefix(repNames, \"Road\")) {\n\t\t\treturn \"Road\";\n\t\t}\n\t\tif (containsPrefix(repNames, \"Rail\")) {\n\t\t\treturn \"Rail\";\n\t\t}\n\t\tif (containsPrefix(repNames, \"BuildingEntrance\")) {\n\t\t\treturn \"BuildingEntrance\";\n\t\t}\n\t\tif (containsPrefix(repNames, \"Building\")) {\n\t\t\treturn \"Building\";\n\t\t}\n\t\tif (containsPrefix(repNames, \"AreaFountain\")) {\n\t\t\treturn \"AreaFountain\";\n\t\t}\n\t\tif (containsPrefix(repNames, \"Waterway\")) {\n\t\t\treturn \"Waterway\";\n\t\t}\n\t\tif (containsPrefix(repNames, \"River\")) {\n\t\t\treturn \"River\";\n\t\t}\n\t\tif (containsPrefix(repNames, \"Water\")) {\n\t\t\treturn \"Water\";\n\t\t}\n\t\treturn repNames.iterator().next();\n\t}\n\n\tpublic static boolean isPedestrian(MapNode node) {\n\t\tif (node == null) {\n\t\t\treturn false;\n\t\t}\n\t\tList<MapWaySegment> connectedWaySegments = node.getConnectedWaySegments();\n\t\tint pedestrians = 0;\n\t\tfor (MapWaySegment mapWaySegment : connectedWaySegments) {\n\t\t\tpedestrians += isPedestrian(mapWaySegment.getTags()) ? 1 : 0;\n\t\t}\n\t\treturn pedestrians >= (connectedWaySegments.size() + 1) / 2;\n\t}\n\n\tpublic static boolean isPedestrian(TagGroup tags) {\n\t\tif (tags == null) {\n\t\t\treturn false;\n\t\t}\n\t\tString highwayValue = tags.getValue(\"highway\");\n\t\tif (\"path\".equals(highwayValue)\n\t\t\t|| \"footway\".equals(highwayValue)\n\t\t\t|| \"cycleway\".equals(highwayValue)\n\t\t\t|| \"service\".equals(highwayValue)\n\t\t\t|| \"bridleway\".equals(highwayValue)\n\t\t\t|| \"living_street\".equals(highwayValue)\n\t\t\t|| \"pedestrian\".equals(highwayValue)\n\t\t\t|| \"track\".equals(highwayValue)\n\t\t\t|| \"steps\".equals(highwayValue)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (tags.containsKey(\"footway\")\n\t\t\t\t|| tags.contains(\"tourism\", \"attraction\")\n\t\t\t\t|| tags.contains(\"man_made\", \"pier\")\n\t\t\t\t|| tags.contains(\"man_made\", \"breakwater\")) {\n\t\t\treturn true;\n\t\t}\n\t\tString footValue = tags.getValue(\"foot\");\n\t\tif (\"yes\".equals(footValue)\n\t\t\t|| \"designated\".equals(footValue)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tpublic static String roadSuffix(boolean pedestrian) {\n\t\treturn pedestrian ? \"::pedestrian\" : \"\";\n\t}\n\n\tprivate static boolean containsPrefix(Set<String> repNames, String prefix) {\n\t\tfor (String name : repNames) {\n\t\t\tif (name.startsWith(prefix)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/object_info/WayObject.java",
    "content": "package org.osm2world.core.map_data.object_info;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.TreeSet;\n\nimport org.osm2world.core.math.LineSegmentXZ;\n\npublic class WayObject extends BaseObject {\n\tpublic Set<Point> points = new HashSet<>();\n\tpublic float totalLength;\n\tpublic Set<Point> borderCrossings = new HashSet<>(); // TODO\n\tpublic Set<String> intersections = new HashSet<>(); // TODO\n\n\tpublic WayObject(String name) {\n\t\tsuper(name);\n\t}\n\n\tpublic void addSegment(LineSegmentXZ segment) {\n\t\tpoints.add(new Point(segment.p1.x, segment.p1.z));\n\t\tpoints.add(new Point(segment.p2.x, segment.p2.z));\n\t\ttotalLength += segment.getLength();\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_data/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nOSM2World's abstraction of OSM map data\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/BridgeTunnelElevationCalculator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.world.modules.BridgeModule;\nimport org.osm2world.core.world.modules.TunnelModule;\n\n/**\n * sets elevations to zero, except for bridges and tunnels\n */\npublic class BridgeTunnelElevationCalculator extends TagElevationCalculator {\n\t\n\tfinal double eleBridge;\n\tfinal double eleTunnel;\n\t\t\t\n\tprivate BridgeTunnelElevationCalculator(double eleBridge, double eleTunnel) {\n\t\tsuper(0.0, false);\n\t\tthis.eleBridge = eleBridge;\n\t\tthis.eleTunnel = eleTunnel;\n\t}\n\n\tpublic BridgeTunnelElevationCalculator() {\n\t\tthis(1, 0);\n\t}\n\t\n\t@Override\n\tprotected Double getEleForTags(TagGroup tags) {\n\t\t\n\t\tif (BridgeModule.isBridge(tags)) {\n\t\t\treturn eleBridge;\n\t\t} else if (TunnelModule.isTunnel(tags)) {\n\t\t\treturn eleTunnel;\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/DelaunayTriangulation.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport static java.lang.Math.*;\nimport static org.osm2world.core.math.GeometryUtil.isRightOf;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Queue;\nimport java.util.Set;\nimport java.util.Stack;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\n\nimport com.google.common.collect.HashMultimap;\nimport com.google.common.collect.Multimap;\n\n//TODO: test performance effects of:\n// * starting point choice of the walk\n// * caching circumcircles\n// * only calculating area of triangles that are actually changed\n\n/**\n * 2d Delaunay triangulation class.\n * Built to be used as a Voronoi Diagram dual for natural neighbor\n * interpolation of the y elevation values carried by each point.\n * The triangulation is constructed by incremental insertion.\n */\npublic class DelaunayTriangulation {\n\t\n\t/**\n\t * a triangle which is the dual of a site in the Voronoi Diagram.\n\t * Must be counter-clockwise.\n\t */\n\tpublic static class DelaunayTriangle implements IntersectionTestObject {\n\t\t\n\t\t//TODO: use Site class with VectorXZ and other value - avoids all the .xz() calls\n\t\t\n\t\tpublic final VectorXYZ p0, p1, p2;\n\t\t\n\t\tprivate DelaunayTriangle neighbor0 = null;\n\t\tprivate DelaunayTriangle neighbor1 = null;\n\t\tprivate DelaunayTriangle neighbor2 = null;\n\t\t\n\t\tpublic DelaunayTriangle(VectorXYZ p0, VectorXYZ p1, VectorXYZ p2) {\n\t\t\t\n\t\t\tthis.p0 = p0;\n\t\t\tthis.p1 = p1;\n\t\t\tthis.p2 = p2;\n\t\t\t\n\t\t\tassert !asTriangleXZ().isClockwise() : \"must be counter-clockwise\";\n\t\t\t\n\t\t}\n\t\t\t\t\n\t\tpublic VectorXYZ getPoint(int i) {\n\t\t\tswitch (i) {\n\t\t\t\tcase 0: return p0;\n\t\t\t\tcase 1: return p1;\n\t\t\t\tcase 2: return p2;\n\t\t\t\tdefault: throw new Error(\"invalid index \" + i);\n\t\t\t}\n\t\t}\n\t\t\n\t\t\n\t\tpublic DelaunayTriangle getNeighbor(int i) {\n\t\t\tswitch (i) {\n\t\t\t\tcase 0: return neighbor0;\n\t\t\t\tcase 1: return neighbor1;\n\t\t\t\tcase 2: return neighbor2;\n\t\t\t\tdefault: throw new Error(\"invalid index \" + i);\n\t\t\t}\n\t\t}\n\n\t\tpublic DelaunayTriangle getLeftNeighbor(VectorXYZ atPoint) {\n\t\t\treturn getNeighbor((indexOfPoint(atPoint) + 2) % 3);\n\t\t}\n\n\t\tpublic DelaunayTriangle getRightNeighbor(VectorXYZ atPoint) {\n\t\t\treturn getNeighbor(indexOfPoint(atPoint));\n\t\t}\n\t\t\n\t\tpublic void setNeighbor(int i, DelaunayTriangle neighbor) {\n\t\t\tswitch (i) {\n\t\t\t\tcase 0: neighbor0 = neighbor; break;\n\t\t\t\tcase 1: neighbor1 = neighbor; break;\n\t\t\t\tcase 2: neighbor2 = neighbor; break;\n\t\t\t\tdefault: throw new Error(\"invalid index \" + i);\n\t\t\t}\n\t\t}\n\t\t\n\t\tpublic int indexOfPoint(VectorXYZ point) {\n\t\t\tif (point == p0) {\n\t\t\t\treturn 0;\n\t\t\t} else if (point == p1) {\n\t\t\t\treturn 1;\n\t\t\t} else if (point == p2) {\n\t\t\t\treturn 2;\n\t\t\t} else {\n\t\t\t\tthrow new IllegalArgumentException(\"not in this triangle\");\n\t\t\t}\n\t\t}\n\t\t\n\t\tpublic int indexOfNeighbor(DelaunayTriangle neighbor) {\n\t\t\tif (neighbor == neighbor0) {\n\t\t\t\treturn 0;\n\t\t\t} else if (neighbor == neighbor1) {\n\t\t\t\treturn 1;\n\t\t\t} else if (neighbor == neighbor2) {\n\t\t\t\treturn 2;\n\t\t\t} else {\n\t\t\t\tthrow new IllegalArgumentException(\"not a neighbor\");\n\t\t\t}\n\t\t}\n\t\t\n\t\tpublic void replaceNeighbor(DelaunayTriangle oldNeighbor,\n\t\t\t\tDelaunayTriangle newNeighbor) {\n\t\t\tthis.setNeighbor(indexOfNeighbor(oldNeighbor), newNeighbor);\n\t\t}\n\t\t\n\t\tpublic double angleAt(int pointIndex) {\n\t\t\t\n\t\t\tVectorXZ vecToNext = getPoint(pointIndex).xz().subtract(\n\t\t\t\t\tgetPoint((pointIndex + 2) % 3).xz());\n\t\t\tVectorXZ vecToPrev = getPoint((pointIndex + 1) % 3).xz().subtract(\n\t\t\t\t\tgetPoint(pointIndex).xz()).invert();\n\t\t\t\n\t\t\treturn VectorXZ.angleBetween(vecToNext, vecToPrev);\n\t\t\t\n\t\t}\n\n\t\tpublic double angleOppositeOf(DelaunayTriangle neighbor) {\n\t\t\t\n\t\t\treturn angleAt(((indexOfNeighbor(neighbor)) + 2) % 3);\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic VectorXZ getCircumcircleCenter() {\n\t\t\t\n\t\t\tVectorXZ b = p1.subtract(p0).xz();\n\t\t\tVectorXZ c = p2.subtract(p0).xz();\n\n\t\t\tdouble d = 2 * (b.x * c.z - b.z * c.x);\n\n\t\t\tdouble rX = (c.z * (b.x * b.x + b.z * b.z) - b.z * (c.x * c.x + c.z * c.z)) / d;\n\t\t\tdouble rZ = (b.x * (c.x * c.x + c.z * c.z) - c.x * (b.x * b.x + b.z * b.z)) / d;\n\t\t\t\n\t\t\treturn new VectorXZ(rX, rZ).add(p0.xz());\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate TriangleXZ triangleXZ = null;\n\t\t\n\t\tpublic TriangleXZ asTriangleXZ() {\n\t\t\tif (triangleXZ == null) {\n\t\t\t\ttriangleXZ = new TriangleXZ(p0.xz(), p1.xz(), p2.xz());\n\t\t\t}\n\t\t\treturn triangleXZ;\n\t\t}\n\t\t\n\t\tprivate TriangleXYZ triangleXYZ = null;\n\t\t\n\t\tpublic TriangleXYZ asTriangleXYZ() {\n\t\t\tif (triangleXYZ == null) {\n\t\t\t\ttriangleXYZ = new TriangleXYZ(p0, p1, p2);\n\t\t\t}\n\t\t\treturn triangleXYZ;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\t\treturn new AxisAlignedBoundingBoxXZ(\n\t\t\t\t\tmin(p0.x, min(p1.x, p2.x)),\n\t\t\t\t\tmin(p0.z, min(p1.z, p2.z)),\n\t\t\t\t\tmax(p0.x, max(p1.x, p2.x)),\n\t\t\t\t\tmax(p0.z, max(p1.z, p2.z)));\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn asTriangleXZ().toString();\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * operation where the triangulation is modified during an insertion\n\t */\n\tprivate interface Flip {\n\t\t\n\t\tpublic void perform();\n\t\tpublic void undo();\n\t\t\n\t\tpublic DelaunayTriangle[] getCreatedTriangles();\n\t\tpublic DelaunayTriangle[] getRemovedTriangles();\n\t\t\n\t}\n\t\n\tprivate class Flip13 implements Flip {\n\t\t\n\t\tDelaunayTriangle originalTriangle;\n\t\tVectorXYZ point;\n\t\t\n\t\tDelaunayTriangle[] createdTriangles;\n\t\t\n\t\tpublic Flip13(DelaunayTriangle triangle, VectorXYZ point) {\n\t\t\tthis.originalTriangle = triangle;\n\t\t\tthis.point = point;\n\t\t}\n\n\t\t@Override\n\t\tpublic void perform() {\n\t\t\t\n\t\t\tcreatedTriangles = new DelaunayTriangle[3];\n\t\t\t\n\t\t\tcreatedTriangles[0] = new DelaunayTriangle(\n\t\t\t\t\toriginalTriangle.p0,\n\t\t\t\t\toriginalTriangle.p1,\n\t\t\t\t\tpoint);\n\t\t\tcreatedTriangles[1] = new DelaunayTriangle(\n\t\t\t\t\toriginalTriangle.p1,\n\t\t\t\t\toriginalTriangle.p2,\n\t\t\t\t\tpoint);\n\t\t\tcreatedTriangles[2] = new DelaunayTriangle(\n\t\t\t\t\toriginalTriangle.p2,\n\t\t\t\t\toriginalTriangle.p0,\n\t\t\t\t\tpoint);\n\t\t\t\t\t\t\n\t\t\tDelaunayTriangle neighbor0 = originalTriangle.getNeighbor(0);\n\t\t\tDelaunayTriangle neighbor1 = originalTriangle.getNeighbor(1);\n\t\t\tDelaunayTriangle neighbor2 = originalTriangle.getNeighbor(2);\n\t\t\t\n\t\t\tcreatedTriangles[0].setNeighbor(0, neighbor0);\n\t\t\tcreatedTriangles[0].setNeighbor(1, createdTriangles[1]);\n\t\t\tcreatedTriangles[0].setNeighbor(2, createdTriangles[2]);\n\t\t\tif (neighbor0 != null) {\n\t\t\t\tneighbor0.replaceNeighbor(originalTriangle, createdTriangles[0]);\n\t\t\t}\n\t\t\t\n\t\t\tcreatedTriangles[1].setNeighbor(0, neighbor1);\n\t\t\tcreatedTriangles[1].setNeighbor(1, createdTriangles[2]);\n\t\t\tcreatedTriangles[1].setNeighbor(2, createdTriangles[0]);\n\t\t\tif (neighbor1 != null) {\n\t\t\t\tneighbor1.replaceNeighbor(originalTriangle, createdTriangles[1]);\n\t\t\t}\n\t\t\t\n\t\t\tcreatedTriangles[2].setNeighbor(0, neighbor2);\n\t\t\tcreatedTriangles[2].setNeighbor(1, createdTriangles[0]);\n\t\t\tcreatedTriangles[2].setNeighbor(2, createdTriangles[1]);\n\t\t\tif (neighbor2 != null) {\n\t\t\t\tneighbor2.replaceNeighbor(originalTriangle, createdTriangles[2]);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void undo() {\n\t\t\t\n\t\t\tDelaunayTriangle neighbor0 = originalTriangle.getNeighbor(0);\n\t\t\tDelaunayTriangle neighbor1 = originalTriangle.getNeighbor(1);\n\t\t\tDelaunayTriangle neighbor2 = originalTriangle.getNeighbor(2);\n\t\t\t\n\t\t\tif (neighbor0 != null) {\n\t\t\t\tneighbor0.replaceNeighbor(createdTriangles[0], originalTriangle);\n\t\t\t}\n\t\t\t\n\t\t\tif (neighbor1 != null) {\n\t\t\t\tneighbor1.replaceNeighbor(createdTriangles[1], originalTriangle);\n\t\t\t}\n\t\t\t\n\t\t\tif (neighbor2 != null) {\n\t\t\t\tneighbor2.replaceNeighbor(createdTriangles[2], originalTriangle);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\n\t\t@Override\n\t\tpublic DelaunayTriangle[] getCreatedTriangles() {\n\t\t\tassert createdTriangles != null;\n\t\t\treturn createdTriangles;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic DelaunayTriangle[] getRemovedTriangles() {\n\t\t\treturn new DelaunayTriangle[] {originalTriangle};\n\t\t\t//TODO array creation overhead :(\n\t\t}\n\t\t\n\t}\n\t\n\tprivate class Flip22 implements Flip {\n\t\t\n\t\tDelaunayTriangle[] originalTriangles;\n\t\tDelaunayTriangle[] createdTriangles;\n\t\t\n\t\t/**\n\t\t * neighbors of the quadrangle formed by both\n\t\t * {@link #originalTriangles} and {@link #createdTriangles}.\n\t\t */\n\t\tfinal DelaunayTriangle[] neighbors = new DelaunayTriangle[4];\n\t\t\n\t\tpublic Flip22(DelaunayTriangle triangle) {\n\t\t\toriginalTriangles = new DelaunayTriangle[]{\n\t\t\t\t\ttriangle,\n\t\t\t\t\ttriangle.getNeighbor(0)};\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void perform() {\n\t\t\t\n\t\t\t/* determine points and neighbors (4 each) of the quadrangle */\n\t\t\t\n\t\t\tVectorXYZ[] points = new VectorXYZ[4];\n\n\t\t\tpoints[0] = originalTriangles[0].getPoint(1);\n\t\t\tneighbors[0] = originalTriangles[0].getNeighbor(1);\n\n\t\t\tpoints[1] = originalTriangles[0].getPoint(2);\n\t\t\tneighbors[1] = originalTriangles[0].getNeighbor(2);\n\t\t\t\n\t\t\tint i = originalTriangles[1].indexOfNeighbor(originalTriangles[0]);\n\n\t\t\tpoints[2] = originalTriangles[1].getPoint((i + 1) % 3);\n\t\t\tneighbors[2] = originalTriangles[1].getNeighbor((i + 1) % 3);\n\n\t\t\tpoints[3] = originalTriangles[1].getPoint((i + 2) % 3);\n\t\t\tneighbors[3] = originalTriangles[1].getNeighbor((i + 2) % 3);\n\t\t\t\n\t\t\t/* build two new triangles for the quadrangle */\n\t\t\t\n\t\t\tcreatedTriangles = new DelaunayTriangle[2];\n\t\t\t\n\t\t\tcreatedTriangles[0] = new DelaunayTriangle(\n\t\t\t\t\tpoints[2], points[3], points[1]);\n\t\t\tcreatedTriangles[1] = new DelaunayTriangle(\n\t\t\t\t\tpoints[3], points[0], points[1]);\n\t\t\t\n\t\t\tcreatedTriangles[0].setNeighbor(0, neighbors[2]);\n\t\t\tcreatedTriangles[0].setNeighbor(1, createdTriangles[1]);\n\t\t\tcreatedTriangles[0].setNeighbor(2, neighbors[1]);\n\t\t\t\n\t\t\tcreatedTriangles[1].setNeighbor(0, neighbors[3]);\n\t\t\tcreatedTriangles[1].setNeighbor(1, neighbors[0]);\n\t\t\tcreatedTriangles[1].setNeighbor(2, createdTriangles[0]);\n\n\t\t\tif (neighbors[0] != null)\n\t\t\t\tneighbors[0].replaceNeighbor(originalTriangles[0], createdTriangles[1]);\n\t\t\tif (neighbors[1] != null)\n\t\t\t\tneighbors[1].replaceNeighbor(originalTriangles[0], createdTriangles[0]);\n\t\t\tif (neighbors[2] != null)\n\t\t\t\tneighbors[2].replaceNeighbor(originalTriangles[1], createdTriangles[0]);\n\t\t\tif (neighbors[3] != null)\n\t\t\t\tneighbors[3].replaceNeighbor(originalTriangles[1], createdTriangles[1]);\n\t\t\t\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void undo() {\n\t\t\t\n\t\t\tif (neighbors[0] != null)\n\t\t\t\tneighbors[0].replaceNeighbor(createdTriangles[1], originalTriangles[0]);\n\t\t\tif (neighbors[1] != null)\n\t\t\t\tneighbors[1].replaceNeighbor(createdTriangles[0], originalTriangles[0]);\n\t\t\tif (neighbors[2] != null)\n\t\t\t\tneighbors[2].replaceNeighbor(createdTriangles[0], originalTriangles[1]);\n\t\t\tif (neighbors[3] != null)\n\t\t\t\tneighbors[3].replaceNeighbor(createdTriangles[1], originalTriangles[1]);\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic DelaunayTriangle[] getCreatedTriangles() {\n\t\t\tassert createdTriangles != null;\n\t\t\treturn createdTriangles;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic DelaunayTriangle[] getRemovedTriangles() {\n\t\t\treturn originalTriangles;\n\t\t}\n\t\t\n\t}\n\t\t\n\tpublic class NaturalNeighbors {\n\n\t\tpublic final VectorXYZ[] neighbors;\n\t\tpublic final double[] relativeWeights;\n\t\t\n\t\tNaturalNeighbors(Collection<VectorXYZ> neighbors) {\n\t\t\t\n\t\t\tthis.neighbors = new VectorXYZ[neighbors.size()];\n\t\t\tneighbors.toArray(this.neighbors);\n\t\t\t\n\t\t\trelativeWeights = new double[neighbors.size()];\n\t\t\t\n\t\t}\n\t\t\t\t\n\t}\n\n\t/**\n\t * produces iterables for iterating over all the triangles in this\n\t * triangulation via depth-first search\n\t */\n\tprivate final Iterable<DelaunayTriangle> ITERABLE =\n\t\t\tnew Iterable<DelaunayTriangle>() {\n\t\t\n\t\t@Override\n\t\tpublic Iterator<DelaunayTriangle> iterator() {\n\t\t\t\n\t\t\tfinal DelaunayTriangle start = handleTriangle.getNeighbor(0);\n\t\t\t\n\t\t\tfinal Stack<DelaunayTriangle> startStack = new Stack<DelaunayTriangle>();\n\t\t\tstartStack.push(start);\n\t\t\t\n\t\t\tfinal Set<DelaunayTriangle> startSet = new HashSet<DelaunayTriangle>();\n\t\t\tstartSet.add(handleTriangle);\n\t\t\tstartSet.add(start);\n\t\t\t\n\t\t\treturn new Iterator<DelaunayTriangle>() {\n\t\t\t\t\n\t\t\t\tprivate final Set<DelaunayTriangle> visitedTriangles = startSet;\n\t\t\t\tprivate final Stack<DelaunayTriangle> triangleStack = startStack;\n\t\t\t\tprivate DelaunayTriangle nextTriangle = start;\n\t\t\t\tprivate int nextIndex = 0;\n\t\t\t\t\t\t\t\t\n\t\t\t\tpublic boolean hasNext() {\n\t\t\t\t\treturn nextTriangle != null;\n\t\t\t\t};\n\t\t\t\t\n\t\t\t\tpublic DelaunayTriangle next() {\n\t\t\t\t\t\n\t\t\t\t\tDelaunayTriangle currentTriangle = nextTriangle;\n\t\t\t\t\tnextTriangle = null;\n\t\t\t\t\t\n\t\t\t\t\twhile (nextTriangle == null && !triangleStack.isEmpty()) {\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\twhile (nextIndex <= 2 && nextTriangle == null) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tnextTriangle =\n\t\t\t\t\t\t\t\t\ttriangleStack.peek().getNeighbor(nextIndex);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (nextTriangle != null &&\n\t\t\t\t\t\t\t\t\t!visitedTriangles.contains(nextTriangle)) {\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\ttriangleStack.push(nextTriangle);\n\t\t\t\t\t\t\t\tvisitedTriangles.add(nextTriangle);\n\t\t\t\t\t\t\t\tnextIndex = 0;\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tnextTriangle = null;\n\t\t\t\t\t\t\t\tnextIndex ++;\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (nextTriangle == null) {\n\t\t\t\t\t\t\t// go back to previous triangle\n\t\t\t\t\t\t\ttriangleStack.pop();\n\t\t\t\t\t\t\tnextIndex = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\treturn currentTriangle;\n\t\t\t\t\t\n\t\t\t\t};\n\t\t\t\n\t\t\t\tpublic void remove() {\n\t\t\t\t\tthrow new UnsupportedOperationException();\n\t\t\t\t};\n\t\t\t\t\n\t\t\t};\n\t\t\t\n\t\t}\n\t\t\n\t};\n\n\t/**\n\t * a fake triangle outside of the bounds that is used as a start\n\t * for iterating/walking through the triangulation along neighborships\n\t */\n\tpublic final DelaunayTriangle handleTriangle;\n\n\tpublic DelaunayTriangulation(AxisAlignedBoundingBoxXZ bounds) {\n\t\t\t\t\n\t\tVectorXYZ boundV0 = bounds.bottomLeft().xyz(0);\n\t\tVectorXYZ boundV1 = bounds.bottomRight().xyz(0);\n\t\tVectorXYZ boundV2 = bounds.topRight().xyz(0);\n\t\tVectorXYZ boundV3 = bounds.topLeft().xyz(0);\n\t\t\n\t\tDelaunayTriangle t1 = new DelaunayTriangle(boundV0, boundV1, boundV3);\n\t\tDelaunayTriangle t2 = new DelaunayTriangle(boundV1, boundV2, boundV3);\n\t\t\t\t\n\t\tt1.setNeighbor(1, t2);\n\t\tt2.setNeighbor(2, t1);\n\t\t\n\t\thandleTriangle = new DelaunayTriangle(boundV1, boundV0,\n\t\t\t\tnew VectorXYZ(bounds.center().x, 0, bounds.minZ - bounds.sizeZ()));\n\t\t\t\t\n\t\tt1.setNeighbor(0, handleTriangle);\n\t\thandleTriangle.setNeighbor(0, t1);\n\t\t\n\t}\n\t\n\t/**\n\t * returns all triangles\n\t */\n\tpublic Iterable<DelaunayTriangle> getTriangles() {\n\t\treturn ITERABLE;\n\t}\n\n\tpublic Stack<Flip> insert(VectorXYZ point) { //TODO: should use <T extends Has(Immutable)Position>\n\t\t\n\t\tDelaunayTriangle triangleEnclosingPoint = getEnlosingTriangle(point.xz());\n\t\t\n\t\tif (triangleEnclosingPoint == null) {\n\t\t\tSystem.out.println(\"null\");\n\t\t\t//TODO check for null\n\t\t}\n\t\t\n\t\t/* split the enclosing triangle */\n\t\t\n\t\tStack<Flip> flipStack = new Stack<Flip>();\n\t\t\n\t\tFlip13 initialFlip = new Flip13(triangleEnclosingPoint, point);\n\t\tinitialFlip.perform();\n\t\tflipStack.push(initialFlip);\n\t\t\n\t\tQueue<DelaunayTriangle> uncheckedTriangles = new LinkedList<DelaunayTriangle>();\n\t\t\n\t\tuncheckedTriangles.offer(initialFlip.createdTriangles[0]);\n\t\tuncheckedTriangles.offer(initialFlip.createdTriangles[1]);\n\t\tuncheckedTriangles.offer(initialFlip.createdTriangles[2]);\n\t\t\n\t\t/*  */\n\t\t\n\t\twhile (!uncheckedTriangles.isEmpty()) {\n\t\t\t\n\t\t\tDelaunayTriangle triangle = uncheckedTriangles.poll();\n\t\t\t\n\t\t\t//TODO: only checks with first neighbor! Document this!\n\t\t\t\n\t\t\tif (!isDelaunay(triangle)) {\n\t\t\t\t\n\t\t\t\tFlip22 flip = new Flip22(triangle);\n\t\t\t\tflip.perform();\n\t\t\t\tflipStack.push(flip);\n\t\t\t\t\n\t\t\t\tuncheckedTriangles.offer(flip.createdTriangles[0]);\n\t\t\t\tuncheckedTriangles.offer(flip.createdTriangles[1]);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn flipStack;\n\t\t\n\t}\n\t\n\t/**\n\t * temporarily inserts a point to calculate its natural neighbors,\n\t * then undoes the insertion\n\t */\n\tpublic NaturalNeighbors probe(VectorXZ point) {\n\n\t\tVectorXYZ probePoint = point.xyz(0);\n\t\t\n\t\t/* insert the point */\n\t\t\n\t\tStack<Flip> flipStack = insert(probePoint);\n\t\t\n\t\t/* identify neighbors and modified triangles */\n\t\t\n\t\tSet<VectorXYZ> neighbors = new HashSet<VectorXYZ>();\n\t\tMultimap<VectorXYZ, DelaunayTriangle> oldTriangles = HashMultimap.create();\n\t\tMultimap<VectorXYZ, DelaunayTriangle> newTriangles = HashMultimap.create();\n\t\t\n\t\t// first loop, identifies neighbors and newly created triangles\n\t\tfor (Flip flip : flipStack) {\n\t\t\tfor (DelaunayTriangle triangle : flip.getCreatedTriangles()) {\n\t\t\t\tnewTriangles.put(triangle.p0, triangle);\n\t\t\t\tnewTriangles.put(triangle.p1, triangle);\n\t\t\t\tnewTriangles.put(triangle.p2, triangle);\n\t\t\t\tneighbors.add(triangle.p0);\n\t\t\t\tneighbors.add(triangle.p1);\n\t\t\t\tneighbors.add(triangle.p2);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// second loop, removes some newTriangles and identifies oldTriangles\n\t\tfor (Flip flip : flipStack) {\n\t\t\tfor (DelaunayTriangle triangle : flip.getRemovedTriangles()) {\n\t\t\t\toldTriangles.put(triangle.p0, triangle);\n\t\t\t\toldTriangles.put(triangle.p1, triangle);\n\t\t\t\toldTriangles.put(triangle.p2, triangle);\n\t\t\t\tnewTriangles.remove(triangle.p0, triangle);\n\t\t\t\tnewTriangles.remove(triangle.p1, triangle);\n\t\t\t\tnewTriangles.remove(triangle.p2, triangle);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// third loop, removes some oldTriangles\n\t\tfor (Flip flip : flipStack) {\n\t\t\tfor (DelaunayTriangle triangle : flip.getCreatedTriangles()) {\n\t\t\t\toldTriangles.remove(triangle.p0, triangle);\n\t\t\t\toldTriangles.remove(triangle.p1, triangle);\n\t\t\t\toldTriangles.remove(triangle.p2, triangle);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// FIXME: cannot use any oldTriangle as starting point, may have been newly created before\n\t\t\n\t\tneighbors.remove(probePoint);\n\t\t\n\t\tNaturalNeighbors result = new NaturalNeighbors(neighbors);\n\t\t\n\t\t/* calculate size of voronoi cells with the point */\n\t\t\n\t\tfor (int i = 0; i < result.neighbors.length; i++) {\n\t\t\tresult.relativeWeights[i] = getVoronoiCellSize(result.neighbors[i],\n\t\t\t\t\tnewTriangles.get(result.neighbors[i]));\n\t\t}\n\t\t\n\t\t/* undo insertion */\n\t\t\n\t\twhile (!flipStack.isEmpty()) {\n\t\t\tflipStack.pop().undo();\n\t\t}\n\n\t\t/* calculate difference of voronoi cell size with and without the point */\n\t\t\n\t\tdouble areaDifferenceSum = 0;\n\t\t\n\t\tfor (int i = 0; i < result.neighbors.length; i++) {\n\t\t\t\n\t\t\tdouble area = getVoronoiCellSize(result.neighbors[i],\n\t\t\t\t\toldTriangles.get(result.neighbors[i]));\n\t\t\t\n\t\t\tresult.relativeWeights[i] = area - result.relativeWeights[i];\n\t\t\t\n\t\t\tareaDifferenceSum += result.relativeWeights[i];\n\t\t}\n\t\t\n\t\t/* calculate relative weights of neighbors */\n\t\t\n\t\tfor (int i = 0; i < result.neighbors.length; i++) {\n\t\t\tresult.relativeWeights[i] /= areaDifferenceSum;\n\t\t\t\n\t\t\tif (result.relativeWeights[i] > 1) {\n\t\t\t\tSystem.out.println(result.relativeWeights[i]);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\tpublic List<DelaunayTriangle> getIncidentTriangles(final VectorXYZ point) {\n\n\t\tList<DelaunayTriangle> result = new ArrayList<DelaunayTriangle>();\n\t\t\n\t\tfor (DelaunayTriangle triangle : getTriangles()) {\n\t\t\tif (triangle.p0.equals(point)) {\n\t\t\t\tresult.add(triangle);\n\t\t\t} else if (triangle.p1.equals(point)) {\n\t\t\t\tresult.add(triangle);\n\t\t\t} else if (triangle.p2.equals(point)) {\n\t\t\t\tresult.add(triangle);\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\tpublic List<TriangleXZ> getVoronoiCellSectors(VectorXYZ point) {\n\t\treturn getVoronoiCellSectors(point, getIncidentTriangles(point));\n\t}\n\t\t\t\n\t/**\n\t * TODO describe effect of incident triangles\n\t */\n\tpublic List<TriangleXZ> getVoronoiCellSectors(VectorXYZ point,\n\t\t\tCollection<DelaunayTriangle> incidentTriangles) {\n\t\t\n\t\tfinal VectorXZ pointXZ = point.xz();\n\t\t\n\t\t/* build sorted list of triangles\n\t\t * by starting at any incidentTriangle and going left and right\n\t\t * around the point, appending neighbors. */\n\t\t\n\t\tList<DelaunayTriangle> triangles =\n\t\t\t\tnew ArrayList<DelaunayTriangle>(incidentTriangles.size() + 2);\n\t\t\n\t\tDelaunayTriangle startTriangle = incidentTriangles.iterator().next();\n\t\ttriangles.add(startTriangle);\n\t\t\n\t\tDelaunayTriangle currentTriangle = startTriangle;\n\t\t\n\t\twhile (/* incidentTriangles.contains(currentTriangle)  TODO re-enable\n\t\t\t\t&& */ (currentTriangle != startTriangle || triangles.size() == 1)\n\t\t\t\t&& currentTriangle.getRightNeighbor(point) != null) {\n\t\t\t\n\t\t\tcurrentTriangle = currentTriangle.getRightNeighbor(point);\n\t\t\ttriangles.add(currentTriangle);\n\t\t\t\n\t\t}\n\t\t\n\t\tCollections.reverse(triangles);\n\t\t\n\t\tif (currentTriangle != startTriangle) { //check for full circle\n\t\t\n\t\t\tList<DelaunayTriangle> leftTriangles =\n\t\t\t\t\tnew ArrayList<DelaunayTriangle>();\n\t\t\t\n\t\t\tcurrentTriangle = startTriangle;\n\t\t\n\t\t\twhile (/* incidentTriangles.contains(currentTriangle) TODO re-enable\n\t\t\t\t\t&& */ (currentTriangle != startTriangle || triangles.isEmpty())\n\t\t\t\t\t&& currentTriangle.getLeftNeighbor(point) != null) { //TODO: avoid infinite loop\n\t\t\t\t\n\t\t\t\tcurrentTriangle = currentTriangle.getLeftNeighbor(point);\n\t\t\t\tleftTriangles.add(currentTriangle);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\ttriangles.addAll(leftTriangles);\n\t\t\t\n\t\t}\n\t\t\t\t\n\t\t/* calculate the circumcircle centers */\n\t\t\n\t\tList<VectorXZ> centers = new ArrayList<VectorXZ>();\n\t\t\n\t\tfor (DelaunayTriangle t : triangles) {\n\t\t\tcenters.add(t.getCircumcircleCenter());\n\t\t}\n\n\t\t/* calculate the sectors */\n\t\t\n\t\tList<TriangleXZ> result = new ArrayList<TriangleXZ>();\n\t\t\t\t\n\t\tfor (int i = 0; i+1 < centers.size(); i++) {\n\t\t\tresult.add(new TriangleXZ(pointXZ,\n\t\t\t\t\tcenters.get(i),\n\t\t\t\t\tcenters.get(i+1)));\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\t/**\n\t * returns the size of a voronoi cell or a part of the voronoi cell.\n\t * \n\t * @param point  point corresponding to the voronoi cell\n\t * @param incidentTriangles  if this contains all triangles incident to\n\t *   point, then the size of the entire cell will be calculated.\n\t *   Otherwise, only sides that contain at least one circumcircle center\n\t *   of a triangle in this collection are taken into account.\n\t */\n\tpublic double getVoronoiCellSize(VectorXYZ point,\n\t\t\tCollection<DelaunayTriangle> incidentTriangles) {\n\t\t\n\t\tdouble size = 0;\n\t\t\n\t\tfor (TriangleXZ t : getVoronoiCellSectors(point, incidentTriangles)) {\n\t\t\tsize += t.getArea();\n\t\t}\n\t\t\n\t\treturn size;\n\t\t\n\t}\n\t\n\tprivate boolean isDelaunay(DelaunayTriangle triangle) {\n\t\t\n\t\tDelaunayTriangle neighborTriangle = triangle.getNeighbor(0);\n\t\t\n\t\tif (neighborTriangle != null && neighborTriangle != handleTriangle) {\n\t\t\t\n\t\t\tdouble a1 = triangle.angleAt(2);\n\t\t\tdouble a2 = neighborTriangle.angleOppositeOf(triangle);\n\t\t\t\n\t\t\treturn a1 + a2 <= PI;\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\treturn true;\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * returns the triangle containing the given point\n\t * \n\t * @param point  must lie within the triangulation; != null\n\t */\n\tpublic DelaunayTriangle getEnlosingTriangle(VectorXZ point) {\n\t\t\n\t\t/* use a 'visibility walk' through the triangulation,\n\t\t * starting at the handleTriangle */\n\t\t\n\t\tDelaunayTriangle currentTriangle = handleTriangle;\n\t\t\n\t\tboolean triangleContainsPoint = false;\n\t\t\n\t\twhile (!triangleContainsPoint) {\n\t\t\t\n\t\t\ttriangleContainsPoint = true;\n\t\t\t\n\t\t\tfor (int i = 0; i <= 2; i++) {\n\t\t\t\t\n\t\t\t\t// check whether the line defined by the i-th edge separates\n\t\t\t\t// the target point from the current triangle center.\n\t\t\t\t// (relies on counterclockwise winding)\n\t\t\t\t\n\t\t\t\tif (isRightOf(point,\n\t\t\t\t\t\tcurrentTriangle.getPoint(i).xz(),\n\t\t\t\t\t\tcurrentTriangle.getPoint((i + 1) % 3).xz())) { //TODO avoid xz()\n\t\t\t\t\t\n\t\t\t\t\ttriangleContainsPoint = false;\n\t\t\t\t\tcurrentTriangle = currentTriangle.getNeighbor(i);\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn currentTriangle;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/EleConstraintEnforcer.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.util.List;\n\nimport org.osm2world.core.map_elevation.data.EleConnector;\n\n\npublic interface EleConstraintEnforcer {\n\t\n\t/** whether a constraint requires a minimum, maximum or exact value */\n\tpublic static enum ConstraintType {\n\t\tMIN, MAX, EXACT\n\t}\n\t\n\t/**\n\t * makes connectors known to this enforcer. Only these connectors can be\n\t * used in constraints later on, and only they will be affected by\n\t * {@link #enforceConstraints()}.\n\t * \n\t * @param connectors  connectors with elevation values initially set to\n\t *  terrain elevation at their xz position\n\t */\n\tvoid addConnectors(Iterable<EleConnector> connectors);\n\t\n\t/**\n\t * requires two connectors to be at the same elevation\n\t */\n\tpublic void requireSameEle(EleConnector c1, EleConnector c2);\n\t\n\t/**\n\t * requires a number of connectors to be at the same elevation\n\t */\n\tpublic void requireSameEle(Iterable<EleConnector> cs);\n\t\n\t/**\n\t * requires two connectors' elevations to differ by a given distance\n\t */\n\tvoid requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector lower);\n\t\n\t/**\n\t * requires a connector to be a give distance above a line segment\n\t * defined by two other connectors.\n\t * \n\t * @param distance  distance, may be negative if 'upper' is actually below\n\t */\n\tvoid requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector base1, EleConnector base2);\n\t\n\t/**\n\t * requires an incline along a sequence of connectors.\n\t * \n\t * @param incline  incline value,\n\t *  negative values are inclines in opposite direction\n\t */\n\tvoid requireIncline(ConstraintType type, double incline,\n\t\t\tList<EleConnector> cs);\n\t\n\t/**\n\t * requires that there is a \"smooth\" transition between two line segments\n\t */\n\tvoid requireSmoothness(EleConnector from, EleConnector via, EleConnector to);\n\t\n\t/**\n\t * tries to enforce the previously added constraints\n\t * on elevations of connectors that have been added using\n\t * {@link #addConnectors(Iterable)}\n\t */\n\tvoid enforceConstraints();\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/EleConstraintValidator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport static java.lang.Math.abs;\nimport static java.util.Arrays.asList;\nimport static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.*;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.world.data.WorldObject;\n\nimport com.google.common.collect.HashMultimap;\nimport com.google.common.collect.Multimap;\n\n/**\n * a wrapper for an {@link EleConstraintEnforcer} that passes all calls though,\n * but looks for obvious contradictions in the constraints to help with debugging.\n */\npublic class EleConstraintValidator implements EleConstraintEnforcer {\n\n\tprivate final MapData mapData;\n\tprivate final EleConstraintEnforcer enforcer;\n\t\n\tprivate final Multimap<EleConnector, EleConnector> sameEleMap =\n\t\t\tHashMultimap.create();\n\tprivate final Multimap<EleConnector, EleConnector> verticalDistMap =\n\t\t\tHashMultimap.create();\n\t\n\tprivate final List<List<EleConnector>> smoothnessTriples =\n\t\t\tnew ArrayList<List<EleConnector>>();\n\t\n\tpublic EleConstraintValidator(MapData mapData, EleConstraintEnforcer enforcer) {\n\t\tthis.mapData = mapData;\n\t\tthis.enforcer = enforcer;\n\t}\n\t\n\t@Override\n\tpublic void addConnectors(Iterable<EleConnector> connectors) {\n\t\t\n\t\tenforcer.addConnectors(connectors);\n\t\t\n\t\tfor (EleConnector c : connectors) {\n\t\t\tsameEleMap.put(c, c);\n\t\t}\n\t\t\n\t\t//TODO this does not join connectors added in different addConnectors calls\n\t\tfor (EleConnector c1 : connectors) {\n\t\t\tfor (EleConnector c2 : connectors) {\n\t\t\t\t\n\t\t\t\tif (c1 != c2 && c1.connectsTo(c2)) {\n\t\t\t\t\tsameEleMap.putAll(c1, sameEleMap.get(c2));\n\t\t\t\t\tsameEleMap.putAll(c2, sameEleMap.get(c1));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\n\t@Override\n\tpublic void requireSameEle(EleConnector c1, EleConnector c2) {\n\t\t\n\t\tenforcer.requireSameEle(c1, c2);\n\n\t\tif (verticalDistMap.containsEntry(c1, c2)) {\n\t\t\tfailValidation(\"vertical distance despite same ele\", c1, c2);\n\t\t}\n\n\t\tsameEleMap.putAll(c1, sameEleMap.get(c2));\n\t\tsameEleMap.putAll(c2, sameEleMap.get(c1));\n\t\t\n\t}\n\n\t@Override\n\tpublic void requireSameEle(Iterable<EleConnector> cs) {\n\t\t\n\t\tenforcer.requireSameEle(cs);\n\t\t\n\t\tfor (EleConnector c1 : cs) {\n\t\t\tfor (EleConnector c2 : cs) {\n\t\t\t\t\n\t\t\t\tif (verticalDistMap.containsEntry(c1, c2)) {\n\t\t\t\t\tfailValidation(\"vertical distance despite same ele\", c1, c2);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tsameEleMap.putAll(c1, sameEleMap.get(c2));\n\t\t\t\tsameEleMap.putAll(c2, sameEleMap.get(c1));\n\t\t\t\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\n\t@Override\n\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector lower) {\n\t\t\n\t\tenforcer.requireVerticalDistance(type, distance, upper, lower);\n\t\t\n\t\tif ((type != MAX && distance > 0)\n\t\t\t\t|| (type != MIN && distance < 0)) {\n\t\t\t\n\t\t\tif (sameEleMap.containsEntry(upper, lower)) {\n\t\t\t\tfailValidation(\"vertical distance despite same ele\", upper, lower);\n\t\t\t}\n\t\t\t\n\t\t\tverticalDistMap.putAll(upper, sameEleMap.get(lower));\n\t\t\tverticalDistMap.putAll(lower, sameEleMap.get(upper));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector base1, EleConnector base2) {\n\t\tenforcer.requireVerticalDistance(type, distance, upper, base1, base2);\n\t}\n\n\t@Override\n\tpublic void requireIncline(ConstraintType type, double incline,\n\t\t\tList<EleConnector> cs) {\n\t\tenforcer.requireIncline(type, incline, cs);\n\t}\n\n\t@Override\n\tpublic void requireSmoothness(\n\t\t\tEleConnector v1, EleConnector v2, EleConnector v3) {\n\t\t\n\t\tenforcer.requireSmoothness(v1, v2, v3);\n\t\t\n\t\tsmoothnessTriples.add(asList(v1, v2, v3));\n\t\t\n\t}\n\n\t@Override\n\tpublic void enforceConstraints() {\n\t\t\n\t\tenforcer.enforceConstraints();\n\t\t\n\t\tprintSmoothnessLog();\n\t\t\n\t}\n\t\n\tprivate void printSmoothnessLog() {\n\t\t\n\t\tStringBuilder log = new StringBuilder(\"smoothness log:\\n\");\n\t\t\n\t\tfor (List<EleConnector> triple : smoothnessTriples) {\n\t\t\t\n\t\t\tdouble inc1 = getIncline(\n\t\t\t\t\ttriple.get(1).getPosXYZ(),\n\t\t\t\t\ttriple.get(0).getPosXYZ());\n\t\t\t\n\t\t\tdouble inc2 = getIncline(\n\t\t\t\t\ttriple.get(2).getPosXYZ(),\n\t\t\t\t\ttriple.get(1).getPosXYZ());\n\t\t\t\n\t\t\tdouble inclineDiff = abs(inc2 - inc1);\n\t\t\tdouble dist = triple.get(0).pos.distanceTo(triple.get(2).pos);\n\t\t\tdouble inclineDiffPerMeter = inclineDiff / dist;\n\t\t\t\n\t\t\tif (inclineDiffPerMeter > 200) {\n\t\t\t\tlog.append(String.format(\"%.1f%% over %.1fm at \", inclineDiff * 100,\n\t\t\t\t\t\tdist));\n\t\t\t\tappendEleConnectorString(log, triple.get(1));\n\t\t\t\tlog.append('\\n');\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tSystem.out.println(log);\n\t\t\n\t}\n\n\tprivate double getIncline(VectorXYZ v1, VectorXYZ v2) {\n\t\treturn (v2.y - v1.y) / v1.distanceToXZ(v2);\n\t}\n\n\tprivate void failValidation(String constraintText, EleConnector... cs ) {\n\t\t\n\t\tStringBuilder text = new StringBuilder(\"invalid constraint:\\n\");\n\t\ttext.append(constraintText);\n\t\t\n\t\ttext.append(\"participating connectors:\\n\");\n\t\t\n\t\tfor (EleConnector c : cs) {\n\t\t\tappendEleConnectorString(text, c);\n\t\t\ttext.append('\\n');\n\t\t}\n\t\t\n\t\tthrow new Error(text.toString());\n\t\t\n\t}\n\n\tprivate void appendEleConnectorString(StringBuilder out, EleConnector c) {\n\t\t\n\t\tout.append(c);\n\t\t\n\t\tfor (WorldObject wo : mapData.getWorldObjects()) {\n\t\t\tfor (EleConnector woConnector : wo.getEleConnectors()) {\n\t\t\t\tif (woConnector == c) {\n\t\t\t\t\tout.append(\" from \" + wo);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/EleTagElevationCalculator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseOsmDecimal;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\n\n/**\n * sets elevations based on ele tags\n */\npublic class EleTagElevationCalculator extends TagElevationCalculator {\n\t\n\t@Override\n\tprotected Double getEleForTags(TagGroup tags) {\n\t\t\n\t\tFloat value = null;\n\t\t\n\t\tif (tags.containsKey(\"ele\")) {\n\t\t\tvalue = parseOsmDecimal(tags.getValue(\"ele\"), true);\n\t\t}\n\t\t\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn (double) value;\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/ElevationCalculator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport org.osm2world.core.heightmap.data.TerrainPoint;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_elevation.data.ElevationProfile;\n\n/**\n * calculates elevations using information from {@link MapData}\n * and terrain elevation data\n * \n * TODO new documentation\n */\npublic interface ElevationCalculator {\n\n\t/**\n\t * provides elevation information for all elements in the {@link MapData}.\n\t * \n\t * Implementations of this method need to <ul>\n\t * <li>set {@link ElevationProfile}s for all {@link MapElement}s</li>\n\t * <li>determine elevation of all {@link TerrainPoint}s with unknown\n\t * elevation by using {@link TerrainPoint#setEle(float)}.</li>\n\t * </ul>\n\t * \n\t * @param mapData  map data without elevation profiles; != null\n\t * @param eleData  terrain elevation data;\n\t *                 whether null is allowed depends on the implementation\n\t */\n\tpublic void calculateElevations(MapData mapData,\n\t\t\tTerrainElevationData eleData);\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/InverseDistanceWeightingInterpolator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport static java.lang.Math.*;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.datastructures.IntersectionGrid;\n\n\npublic class InverseDistanceWeightingInterpolator implements TerrainInterpolator {\n\t\n\tprivate static final double CUTOFF = 300;\n\t\n\tprivate final double negExp;\n\tprivate Collection<VectorXYZ> sites;\n\tprivate IntersectionGrid<VectorXYZ> siteGrid; //TODO: rename IntersectionGrid to something more generic\n\n\tpublic InverseDistanceWeightingInterpolator() {\n\t\tthis(2);\n\t}\n\t\n\tpublic InverseDistanceWeightingInterpolator(double exponent) {\n\t\tthis.negExp = -exponent;\n\t}\n\t\n\t@Override\n\tpublic void setKnownSites(Collection<VectorXYZ> sites) {\n\t\t\n\t\tthis.sites = sites;\n\t\t\n\t\tsiteGrid = new IntersectionGrid<VectorXYZ>(\n\t\t\t\tnew AxisAlignedBoundingBoxXZ(sites).pad(CUTOFF/2),\n\t\t\t\tCUTOFF, CUTOFF);\n\t\t\n\t\tfor (VectorXYZ site : sites) {\n\t\t\tsiteGrid.insert(site);\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic VectorXYZ interpolateEle(VectorXZ pos) {\n\n\t\tdouble weightSum = 0;\n\t\tdouble eleSum = 0;\n\t\t\n\t\tCollection<VectorXYZ>[][] cellArray = siteGrid.getCellArray();\n\t\tint cellX = siteGrid.cellXForCoord(pos.x, pos.z);\n\t\tint cellZ = siteGrid.cellZForCoord(pos.x, pos.z);\n\t\t\n\t\tfor (int i = max(cellX-1, 0); i < min(cellX+2, cellArray.length); i++) {\n\t\t\tfor (int j = max(cellZ-1, 0); j < min(cellZ+2, cellArray[i].length); j++) {\n\t\t\t\t\n\t\t\t\tCollection<VectorXYZ> sitesInCell = cellArray[i][j];\n\t\t\t\t\n\t\t\t\tif (sitesInCell == null) continue;\n\t\t\t\t\n\t\t\t\tfor (VectorXYZ site : sitesInCell) {\n\t\t\t\t\t\n\t\t\t\t\tdouble distance = site.distanceToXZ(pos);\n\t\t\t\t\t\n\t\t\t\t\tif (distance < CUTOFF) {\n\t\t\t\t\t\tdouble weight = pow(distance, negExp);\n\t\t\t\t\t\tweightSum += weight;\n\t\t\t\t\t\teleSum += site.y * weight;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t\t//System.out.println(pos + \": \" + eleSum + \", \" + weightSum + \", \" + eleSum / weightSum);\n\t\t\n\t\treturn pos.xyz(eleSum / weightSum);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/LPEleConstraintEnforcer.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport static org.osm2world.core.map_elevation.data.GroundState.ON;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\nimport net.sf.javailp.Linear;\nimport net.sf.javailp.OptType;\nimport net.sf.javailp.Problem;\nimport net.sf.javailp.Result;\nimport net.sf.javailp.Solver;\nimport net.sf.javailp.SolverFactory;\nimport net.sf.javailp.SolverFactoryLpSolve;\n\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.map_elevation.data.LPVariablePair;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.util.FaultTolerantIterationUtil;\nimport org.osm2world.core.util.FaultTolerantIterationUtil.Operation;\n\nimport com.google.common.collect.HashMultimap;\nimport com.google.common.collect.Multimap;\n\n/**\n * enforces constraints using linear programming\n */\npublic class LPEleConstraintEnforcer implements EleConstraintEnforcer {\n\n\tprivate final Problem problem;\n\t\n\tprivate final List<LPVariablePair> variables;\n\tprivate final Map<EleConnector, LPVariablePair> variableMap;\n\n\t\n\tpublic LPEleConstraintEnforcer() {\n\t\t\n\t\tproblem = new Problem();\n\t\t\n\t\tvariables = new ArrayList<LPVariablePair>();\n\t\tvariableMap = new HashMap<EleConnector, LPVariablePair>();\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void addConnectors(Iterable<EleConnector> connectors) {\n\t\t\n\t\t//TODO: calling this multiple times does not work yet; connectors from different invocations would not be joined\n\t\t\n\t\tfor (LPVariablePair v : createVariables(connectors)) {\n\t\t\t\n\t\t\tvariables.add(v);\n\t\t\t\n\t\t\tfor (EleConnector c : v.getConnectors()) {\n\t\t\t\tvariableMap.put(c, v);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * creates variables for connectors. Joins connected connectors by\n\t * representing their elevation with the same {@link LPVariablePair}.\n\t */\n\tprivate static Collection<LPVariablePair> createVariables(\n\t\t\tIterable<EleConnector> connectors) {\n\t\t\n\t\tMultimap<VectorXZ, LPVariablePair> variablePositionMap =\n\t\t\t\tHashMultimap.create();\n\t\t\n\t\tfor (EleConnector c : connectors) {\n\t\t\t\n\t\t\tCollection<LPVariablePair> existingVariables =\n\t\t\t\t\tvariablePositionMap.get(c.pos);\n\t\t\t\n\t\t\tList<LPVariablePair> matchingVariables =\n\t\t\t\t\tnew ArrayList<LPVariablePair>();\n\t\t\t\n\t\t\tif (existingVariables != null) {\n\t\t\t\tfor (LPVariablePair v : existingVariables) {\n\t\t\t\t\tif (v.connectsTo(c)) {\n\t\t\t\t\t\tmatchingVariables.add(v);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (matchingVariables.isEmpty()) {\n\t\t\t\t\n\t\t\t\t/* create a new variable because no existing one fits */\n\t\t\t\t\n\t\t\t\tvariablePositionMap.put(c.pos, new LPVariablePair(c));\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\t/* add connector to one of the matching variables */\n\t\t\t\t\n\t\t\t\tLPVariablePair vHead = matchingVariables.get(0);\n\t\t\t\tvHead.add(c);\n\t\t\t\t\n\t\t\t\t/* merge other matching variables into that one */\n\t\t\t\t\n\t\t\t\tfor (int i = 1; i < matchingVariables.size(); i++) {\n\t\t\t\t\t\n\t\t\t\t\tLPVariablePair v = matchingVariables.get(i);\n\t\t\t\t\t\n\t\t\t\t\tvHead.addAll(v);\n\t\t\t\t\tvariablePositionMap.remove(c.pos, v);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn variablePositionMap.values();\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireSameEle(EleConnector c1, EleConnector c2) {\n\t\t\n\t\t//FIXME: this doesn't work because of the different interpolated elevations\n\t\t// - the pos/neg variables are different!\n\t\t\n\t\t/* instead of adding an actual constraint, the variables are merged.\n\t\t * This reduces the total number of variables and constraints. */\n\t\t/*\n\t\tLPVariablePair v1 = variableMap.get(c1);\n\t\tLPVariablePair v2 = variableMap.get(c2);\n\t\t\n\t\tif (v1 == v2) { return; }\n\t\t\n\t\t// move all connectors from v2 onto v1\n\t\t\n\t\tfor (EleConnector connector : v2.getConnectors()) {\n\t\t\tvariableMap.put(connector, v1);\n\t\t}\n\t\t\n\t\tv1.addAll(v2);\n\t\tvariables.remove(v1);\n\t\t\n\t\t// replace previous occurrences of v2\n\t\t\n\t\tfor (Constraint constraint : problem.getConstraints()) {\n\t\t\t\n\t\t\tList<Object> variables = constraint.getLhs().getVariables();\n\t\t\t\n\t\t\tfor (int i = 0; i < variables.size(); i++) {\n\t\t\t\tif (variables.get(i) == v2.posVar()) {\n\t\t\t\t\tvariables.set(i, v1.posVar());\n\t\t\t\t}\n\t\t\t\tif (variables.get(i) == v2.negVar()) {\n\t\t\t\t\tvariables.set(i, v1.negVar());\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t*/\n\t\t//TODO: merge joinedConnectors instead? Problem: different xz, and possibly terrain ele\n\t\t//TODO: but add appropriate weighting to the objective term!!\n\t\t\n\t\taddConstraint(\n\t\t\t\t 1, c1,\n\t\t\t\t-1, c2,\n\t\t\t\t\"=\", 0);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireSameEle(Iterable<EleConnector> cs) {\n\t\t\n\t\tIterator<EleConnector> csIterator = cs.iterator();\n\t\t\n\t\tif (csIterator.hasNext()) {\n\t\t\n\t\t\tEleConnector c = csIterator.next();\n\t\t\n\t\t\twhile (csIterator.hasNext()) {\n\t\t\t\trequireSameEle(c, csIterator.next());\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector base1, EleConnector base2) {\n\t\t\n\t\tdouble dist1 = base1.pos.distanceTo(upper.pos);\n\t\tdouble dist2 = base2.pos.distanceTo(upper.pos);\n\t\t\n\t\taddConstraint(\n\t\t\t\t 1, upper,\n\t\t\t\t-(dist2 / (dist1 + dist2)), base1,\n\t\t\t\t-(dist1 / (dist1 + dist2)), base2,\n\t\t\t\tgetOperator(type),\n\t\t\t\tdistance);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector lower) {\n\t\t\n\t\taddConstraint(\n\t\t\t\t 1, upper,\n\t\t\t\t-1, lower,\n\t\t\t\tgetOperator(type),\n\t\t\t\tdistance);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireIncline(ConstraintType type, double incline,\n\t\t\tList<EleConnector> cs) {\n\t\t\n\t\tfor (int i = 0; i+1 < cs.size(); i++) {\n\t\t\t\n\t\t\taddConstraint(\n\t\t\t\t\t 1, cs.get(i+1),\n\t\t\t\t\t-1, cs.get(i),\n\t\t\t\t\tgetOperator(type),\n\t\t\t\t\tincline * cs.get(i).pos.distanceTo(cs.get(i+1).pos));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireSmoothness(EleConnector from,\n\t\t\tEleConnector via, EleConnector to) {\n\t\t\n\t\t/* TODO restore\n\t\t\n\t\tdouble maxInclineDiffPerMeter = 0.5 / 100;\n\t\t\n\t\tdouble dist12 = from.pos.distanceTo(via.pos);\n\t\tdouble dist23 = via.pos.distanceTo(to.pos);\n\t\t\n\t\tdouble maxInclineDiff = maxInclineDiffPerMeter * (dist12 + dist23);\n\t\t\n\t\tSystem.out.println(maxInclineDiff);\n\t\t\n\t\t//| - 1/dist12 * from + (1/dist12 + 1/dist23) * via - 1/dist23 * to |\n\t\t//   <= maxInclineDiff\n\t\t\n\t\taddConstraint(\n\t\t\t\t-1/dist12, from,\n\t\t\t\t1/dist12 + 1/dist23, via,\n\t\t\t\t-1/dist23, to,\n\t\t\t\t\"<=\", maxInclineDiff);\n\t\t\n\t\taddConstraint(\n\t\t\t\t-1/dist12, from,\n\t\t\t\t1/dist12 + 1/dist23, via,\n\t\t\t\t-1/dist23, to,\n\t\t\t\t\">=\", -maxInclineDiff);\n\t\t\n\t\t*/\n\t\t\n\t}\n\t\n\tprivate void addConstraint(\n\t\t\tdouble factor1, EleConnector var1,\n\t\t\tString op, double limit) {\n\t\t\n\t\taddConstraint(\n\t\t\t\tfactor1, var1,\n\t\t\t\t0, null,\n\t\t\t\t0, null,\n\t\t\t\top, limit);\n\t\t\n\t}\n\t\n\tprivate void addConstraint(\n\t\t\tdouble factor1, EleConnector var1,\n\t\t\tdouble factor2, EleConnector var2,\n\t\t\tString op, double limit) {\n\t\t\n\t\taddConstraint(\n\t\t\t\tfactor1, var1,\n\t\t\t\tfactor2, var2,\n\t\t\t\t0, null,\n\t\t\t\top, limit);\n\t\t\n\t}\n\t\n\tprivate void addConstraint(\n\t\t\tdouble factor1, EleConnector var1,\n\t\t\tdouble factor2, EleConnector var2,\n\t\t\tdouble factor3, EleConnector var3,\n\t\t\tString op, double limit) {\n\t\t\n\t\tLinear linear = new Linear();\n\t\t\n\t\tdouble limitCorrection = 0;\n\t\t\n\t\tif (var1 != null) {\n\t\t\tLPVariablePair c1 = variableMap.get(var1);\n\t\t\tlinear.add(factor1, c1.posVar());\n\t\t\tlinear.add(-factor1, c1.negVar());\n\t\t\tlimitCorrection += factor1 * var1.getPosXYZ().y;\n\t\t}\n\t\t\n\t\tif (var2 != null) {\n\t\t\tLPVariablePair c2 = variableMap.get(var2);\n\t\t\tlinear.add(factor2, c2.posVar());\n\t\t\tlinear.add(-factor2, c2.negVar());\n\t\t\tlimitCorrection += factor2 * var2.getPosXYZ().y;\n\t\t}\n\t\t\n\t\tif (var3 != null) {\n\t\t\tLPVariablePair c3 = variableMap.get(var3);\n\t\t\tlinear.add(factor3, c3.posVar());\n\t\t\tlinear.add(-factor3, c3.negVar());\n\t\t\tlimitCorrection += factor3 * var3.getPosXYZ().y;\n\t\t}\n\t\t\n\t\tproblem.add(linear, op, limit - limitCorrection);\n\t\t\n\t}\n\t\n\tprivate static String getOperator(ConstraintType constraintType) {\n\t\t\n\t\tswitch (constraintType) {\n\t\tcase MIN: return \">=\";\n\t\tcase MAX: return \"<=\";\n\t\tcase EXACT: return \"=\";\n\t\tdefault: throw new Error(\"unhandled constraint type\");\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void enforceConstraints() {\n\t\t\n\t\tSolverFactory factory = new SolverFactoryLpSolve();\n\t\tfactory.setParameter(Solver.VERBOSE, 0);\n\t\t\n\t\tproblem.setObjective(constructObjective(), OptType.MIN);\n\t\t\n\t\t//TODO Relaxations relax = new Relaxations();\n\t\t\n\t\tfinal Solver solver = factory.get();\n\t\tfinal Result result = solver.solve(problem);\n\t\t\n\t\tif (result == null) {\n\t\t\tSystem.out.println(\"[ERROR]: cannot enforce constraints, no result for LP\");\n\t\t} else {\n\t\t\t\n\t\t\t/* apply elevation values */\n\t\t\t\n\t\t\tFaultTolerantIterationUtil.iterate(variables, new Operation<LPVariablePair>() {\n\t\t\t\t@Override public void perform(LPVariablePair v) {\n\t\t\t\t\t\n\t\t\t\t\tVectorXYZ posXYZ = v.getPosXYZ().addY(\n\t\t\t\t\t\t\t+ result.get(v.posVar()).doubleValue()\n\t\t\t\t\t\t\t- result.get(v.negVar()).doubleValue());\n\t\t\t\t\t\n\t\t\t\t\tv.setPosXYZ(posXYZ);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t});\n\t\t\t\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate Linear constructObjective() {\n\t\n\t\tLinear objectiveLinear = new Linear();\n\t\t\n\t\tfor (LPVariablePair v : variables) {\n\t\t\t\n\t\t\tif (v.getConnectors().get(0).groundState == ON) {\n\t\t\t\t\n\t\t\t\tobjectiveLinear.add(1, v.posVar());\n\t\t\t\tobjectiveLinear.add(1, v.negVar());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn objectiveLinear;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/LeastSquaresInterpolator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport static java.lang.Math.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.PriorityQueue;\n\nimport org.apache.commons.lang.time.StopWatch;\nimport org.apache.commons.math3.linear.Array2DRowRealMatrix;\nimport org.apache.commons.math3.linear.ArrayRealVector;\nimport org.apache.commons.math3.linear.QRDecomposition;\nimport org.apache.commons.math3.linear.RealMatrix;\nimport org.apache.commons.math3.linear.RealVector;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.datastructures.IntersectionGrid;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\n\n/**\n * uses least squares method to approximate a polynomial at each site,\n * and calculates elevations based on the polynomials at the nearest sites.\n */\npublic class LeastSquaresInterpolator implements TerrainInterpolator {\n\t\n\tprivate static final double CELL_SIZE = 50; //should only affect performance\n\tprivate static final int SITES_FOR_APPROX = 9;\n\tprivate static final int SITES_FOR_INTERPOL = 29;\n\t\n\tprivate Collection<SiteWithPolynomial> sites;\n\tprivate IntersectionGrid<SiteWithPolynomial> siteGrid; //TODO: rename IntersectionGrid to something more generic\n\t\n\t@Override\n\tpublic void setKnownSites(Collection<VectorXYZ> siteVectors) {\n\t\t\n\t\tStopWatch stopWatch = new StopWatch();\n\t\tstopWatch.start();\n\t\t\n\t\tsites = new ArrayList<SiteWithPolynomial>(siteVectors.size());\n\t\t\n\t\tsiteGrid = new IntersectionGrid<SiteWithPolynomial>(\n\t\t\t\tnew AxisAlignedBoundingBoxXZ(siteVectors).pad(CELL_SIZE/2),\n\t\t\t\tCELL_SIZE, CELL_SIZE);\n\t\t\n\t\tfor (VectorXYZ siteVector : siteVectors) {\n\t\t\tSiteWithPolynomial s = new SiteWithPolynomial(siteVector);\n\t\t\tsites.add(s);\n\t\t\tsiteGrid.insert(s);\n\t\t}\n\t\t\n\t\tSystem.out.println(\"  time grid: \" + stopWatch);\n\t\tstopWatch.reset();\n\t\tstopWatch.start();\n\t\t\n\t\t/* approximate a polynomial at each site */\n\n\t\tMap<SiteWithPolynomial, List<SiteWithPolynomial>> nearestSiteMap\n\t\t\t= new HashMap<SiteWithPolynomial, List<SiteWithPolynomial>>();\n\n\t\tfor (SiteWithPolynomial site : sites) {\n\t\t\t\n\t\t\tList<SiteWithPolynomial> nearestSites =\n\t\t\t\t\tfindNearestSites(site.pos.xz(), SITES_FOR_APPROX, false);\n\n\t\t\tnearestSiteMap.put(site, nearestSites);\n\t\t\t\n\t\t}\n\t\t\n\t\tSystem.out.println(\"  time nearest sites: \" + stopWatch);\n\t\tstopWatch.reset();\n\t\tstopWatch.start();\n\n\t\tcalculatePolynomials:\n\t\tfor (SiteWithPolynomial site : sites) {\n\t\t\t\n\t\t\tList<SiteWithPolynomial> nearestSites =\n\t\t\t\t\tnearestSiteMap.get(site);\n\t\t\t\n\t\t\tRealVector vector = new ArrayRealVector(SITES_FOR_APPROX);\n\t\t\tRealMatrix matrix = new Array2DRowRealMatrix(\n\t\t\t\t\tSITES_FOR_APPROX, DefaultPolynomial.NUM_COEFFS);\n\t\t\t\n\t\t\tfor (int row = 0; row < SITES_FOR_APPROX; row++) {\n\t\t\t\tSiteWithPolynomial nearSite = nearestSites.get(row);\n\t\t\t\tDefaultPolynomial.populateMatrix(matrix, row, nearSite.pos.x, nearSite.pos.z);\n\t\t\t\tvector.setEntry(row, nearSite.pos.y);\n\t\t\t}\n\t\t\t\n\t\t\tQRDecomposition qr = new QRDecomposition(matrix);\n\t\t\tRealVector solution = qr.getSolver().solve(vector);\n\t\t\t\t\n\t\t\tdouble[] coeffs = solution.toArray();\n\t\t\t\n\t\t\tfor (double coeff : coeffs) {\n\t\t\t\tif (coeff > 10e3) {\n\t\t\t\t\tcontinue calculatePolynomials;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tsite.setPolynomial(new DefaultPolynomial(coeffs));\n\t\t\t\n\t\t}\n\n\t\tSystem.out.println(\"  time polyonmials: \" + stopWatch);\n\t\tstopWatch.reset();\n\t\tstopWatch.start();\n\t\t\n\t}\n\t\n\t@Override\n\tpublic VectorXYZ interpolateEle(VectorXZ pos) {\n\t\t\n\t\tList<SiteWithPolynomial> nearestSites =\n\t\t\t\tfindNearestSites(pos, SITES_FOR_INTERPOL, true);\n\t\t\n\t\tdouble eleSum = 0;\n\t\tdouble weightSum = 0;\n\t\t\n\t\tfor (SiteWithPolynomial site : nearestSites) {\n\t\t\t\n\t\t\tdouble distance = site.pos.distanceToXZ(pos);\n\t\t\t\n\t\t\tdouble weight = max(1 - distance / 120, 0);\n\t\t\t\n\t\t\tweightSum += weight;\n\t\t\t\t\t\t\n\t\t\teleSum += weight * site.getPolynomial().evaluateAt(pos.x, pos.z);\n\t\t\t\n\t\t}\n\t\t\n\t\tdouble ele = eleSum / weightSum;\n\t\t\n\t\treturn pos.xyz(ele);\n\t\t\n\t}\n\t\n\t/**\n\t * provides access to the polynomials approximated internally.\n\t * This is usually only interesting for debugging or similar tasks.\n\t */\n\tpublic Collection<SiteWithPolynomial> getSitesWithPolynomials() {\n\t\treturn sites;\n\t}\n\t\n\tprivate List<SiteWithPolynomial> findNearestSites(\n\t\t\tfinal VectorXZ pos, int numberSites, boolean requirePolynomial) {\n\n\t\tPriorityQueue<SiteWithPolynomial> result =\n\t\t\t\tnew PriorityQueue<SiteWithPolynomial>(numberSites,\n\t\t\t\t\t\tnew Comparator<SiteWithPolynomial>() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic int compare(SiteWithPolynomial s1, SiteWithPolynomial s2) {\n\t\t\t\t\t\treturn - Double.compare(s1.pos.distanceToXZ(pos),\n\t\t\t\t\t\t\t\ts2.pos.distanceToXZ(pos));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\n\t\tCollection<SiteWithPolynomial>[][] cellArray = siteGrid.getCellArray();\n\t\tint cellX = siteGrid.cellXForCoord(pos.x, pos.z);\n\t\tint cellZ = siteGrid.cellZForCoord(pos.x, pos.z);\n\t\t\t\n\t\tint cellRange = 0;\n\t\t\t\t\n\t\tdo {\n\t\t\t\n\t\t\tfor (int i = max(cellX-cellRange, 0); i < min(cellX+cellRange+1, cellArray.length); i++) {\n\t\t\t\tfor (int j = max(cellZ-cellRange, 0); j < min(cellZ+cellRange+1, cellArray[i].length); j++) {\n\t\t\t\t\t\n\t\t\t\t\t//needs to be on the outer ring of cells (others have been checked before)\n\t\t\t\t\tif (i == cellX-cellRange || i == cellX+cellRange\n\t\t\t\t\t\t\t|| j == cellZ-cellRange || j == cellZ+cellRange) {\n\t\t\t\t\t\n\t\t\t\t\t\tCollection<SiteWithPolynomial> sitesInCell = cellArray[i][j];\n\n\t\t\t\t\t\tif (sitesInCell != null) {\n\t\t\t\t\t\t\tfor (SiteWithPolynomial site : sitesInCell) {\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tif (requirePolynomial && site.polynomial == null) continue;\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tif (result.size() < numberSites) {\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tresult.add(site);\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t} else if (site.pos.distanceToXZ(pos) <\n\t\t\t\t\t\t\t\t\t\tresult.peek().pos.distanceToXZ(pos)) {\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tresult.remove();\n\t\t\t\t\t\t\t\t\tresult.add(site);\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tcellRange ++;\n\t\t\t\n\t\t} while (result.size() < numberSites\n\t\t\t\t|| cellRange * CELL_SIZE < result.peek().pos.distanceToXZ(pos));\n\n\t\t//TODO error handling (not enough sites)\n\t\t\n\t\t/* get the result as a list with ascending distance */\n\t\t\n\t\tList<SiteWithPolynomial> resultList =\n\t\t\t\tnew ArrayList<SiteWithPolynomial>(result);\n\t\t\n\t\tCollections.reverse(resultList);\n\t\t\n\t\treturn resultList;\n\t\t\n\t}\n\t\n\tpublic static interface Polynomial {\n\t\t\n\t\tpublic double evaluateAt(double x, double z);\n\t\t\n\t}\n\t\t\n\tpublic static final class DefaultPolynomial implements Polynomial {\n\t\t\n\t\tprivate static final int NUM_COEFFS = 6;\n\t\t\n\t\tprivate final double[] coeffs;\n\t\t\n\t\tprivate DefaultPolynomial(double[] coeffs) {\n\t\t\tassert coeffs.length == NUM_COEFFS;\n\t\t\tthis.coeffs = coeffs;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic double evaluateAt(double x, double z) {\n\t\t\treturn coeffs[0]\n\t\t\t\t\t+ coeffs[1] * x\n\t\t\t\t\t+ coeffs[2] * z\n\t\t\t\t\t+ coeffs[3] * x*x\n\t\t\t\t\t+ coeffs[4] * x*z\n\t\t\t\t\t+ coeffs[5] * z*z;\n\t\t}\n\t\t\n\t\tpublic static void populateMatrix(RealMatrix matrix, int row,\n\t\t\t\tdouble x, double z) {\n\t\t\t\n\t\t\tmatrix.setEntry(row, 0, 1);\n\t\t\tmatrix.setEntry(row, 1, x);\n\t\t\tmatrix.setEntry(row, 2, z);\n\t\t\tmatrix.setEntry(row, 3, x*x);\n\t\t\tmatrix.setEntry(row, 4, x*z);\n\t\t\tmatrix.setEntry(row, 5, z*z);\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn String.format(Locale.US,\n\t\t\t\t\t\"%.3f + %.3fx + %.3fz + %.3fx^2 + %.3fxz + %.3fz^2\",\n\t\t\t\t\tcoeffs[0], coeffs[1], coeffs[2],\n\t\t\t\t\tcoeffs[3], coeffs[4], coeffs[5]);\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static final class SiteWithPolynomial implements IntersectionTestObject {\n\t\t\n\t\tpublic final VectorXYZ pos;\n\t\tprivate Polynomial polynomial;\n\t\t\t\t\n\t\tpublic SiteWithPolynomial(VectorXYZ site) {\n\t\t\tthis.pos = site;\n\t\t}\n\t\t\t\t\n\t\tpublic Polynomial getPolynomial() {\n\t\t\treturn polynomial;\n\t\t}\n\t\t\n\t\tpublic void setPolynomial(Polynomial polynomial) {\n\t\t\tthis.polynomial = polynomial;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\t\treturn pos.getAxisAlignedBoundingBoxXZ();\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn String.format(\"{%s, %s}\", pos.toString(), polynomial);\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/LevelTagElevationCalculator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseOsmDecimal;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\n\n/**\n * sets elevations based on level tags\n */\npublic class LevelTagElevationCalculator extends TagElevationCalculator {\n\t\n\tfinal double elePerLevel;\n\t\n\tpublic LevelTagElevationCalculator(double elePerLevel) {\n\t\tsuper(0.0, true);\n\t\tthis.elePerLevel = elePerLevel;\n\t}\n\t\n\tpublic LevelTagElevationCalculator() {\n\t\tthis(3.0);\n\t}\n\t\n\t@Override\n\tprotected Double getEleForTags(TagGroup tags) {\n\t\t\n\t\tFloat value = null;\n\t\t\n\t\tif (tags.containsKey(\"level\")) {\n\t\t\tvalue = parseOsmDecimal(tags.getValue(\"level\"), true);\n\t\t}\n\t\t\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn elePerLevel * value;\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/LinearInterpolator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.map_elevation.creation.DelaunayTriangulation.DelaunayTriangle;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * triangulates the point set of elevation sites,\n * then interpolates linearly within each triangle\n * (i.e. treats the triangles as flat)\n */\npublic class LinearInterpolator implements TerrainInterpolator {\n\t\n\tprivate DelaunayTriangulation triangulation;\n\t\n\t@Override\n\tpublic void setKnownSites(Collection<VectorXYZ> sites) {\n\t\t\n\t\tAxisAlignedBoundingBoxXZ boundingBox = new AxisAlignedBoundingBoxXZ(sites);\n\t\tboundingBox = boundingBox.pad(100);\n\t\t\n\t\ttriangulation = new DelaunayTriangulation(boundingBox);\n\t\t\n\t\tfor (VectorXYZ site : sites) {\n\t\t\ttriangulation.insert(site);\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic VectorXYZ interpolateEle(VectorXZ pos) {\n\t\t\n\t\tDelaunayTriangle triangle = triangulation.getEnlosingTriangle(pos);\n\t\t\n\t\tdouble ele = triangle.asTriangleXYZ().getYAt(pos);\n\t\t\n\t\treturn pos.xyz(ele);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/NNDebugViewer.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.awt.BorderLayout;\nimport java.awt.Color;\nimport java.awt.Graphics;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Random;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JFrame;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuBar;\nimport javax.swing.JMenuItem;\nimport javax.swing.JPanel;\nimport javax.swing.KeyStroke;\n\nimport org.osm2world.core.map_elevation.creation.DelaunayTriangulation.DelaunayTriangle;\nimport org.osm2world.core.map_elevation.creation.DelaunayTriangulation.NaturalNeighbors;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.PolygonXZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n\npublic class NNDebugViewer {\n\n\tpublic static void main(String[] args) {\n\t\tnew DebugViewerFrame().setVisible(true);\n\t}\n\t\n\tprivate static final double SIZE = 100;\n\t\n\tprivate static class NNDebugPanel extends JPanel {\n\t\t\n\t\tprivate static final long serialVersionUID = -6051792569759095487L; //generated SerialVersionUID\n\t\tprivate List<VectorXYZ> points;\n\t\tprivate DelaunayTriangulation triangulation;\n\t\t\n\t\t//TODO change to AxisAlignedBbox\n\t\tprivate double minX, maxX, minZ, maxZ;\n\t\t\n\t\tprivate VectorXZ probePos = null;\n\t\t\n\t\tpublic NNDebugPanel() {\n\t\t\tclear();\n\t\t\t\n\t\t\tthis.addMouseListener(new MouseAdapter() {\n\t\t\t\t@Override\n\t\t\t\tpublic void mouseClicked(MouseEvent e) {\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\tVectorXZ point = inverseCoord(e.getX(), e.getY());\n\t\t\t\t\t\n\t\t\t\t\tif (e.getButton() == MouseEvent.BUTTON1) {\n\t\t\t\t\t\tprobePos = point;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tadd(point.xyz(0));\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tNNDebugPanel.this.repaint(0);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t});\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic void clear() {\n\t\t\t\n\t\t\tprobePos = null;\n\t\t\t\n\t\t\tminX = minZ = -SIZE;\n\t\t\tmaxX = maxZ = +SIZE;\n\t\t\t\n\t\t\tpoints = new ArrayList<VectorXYZ>();\n\t\t\ttriangulation = null;\n\t\t\t\t\t\t\n\t\t\tthis.repaint(0);\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic void add(VectorXYZ p) {\n\t\t\t\n\t\t\tprobePos = null;\n\t\t\t\n\t\t\tpoints.add(p);\n\t\t\tupdateMinMax(p);\n\t\t\t\n\t\t\tthis.repaint(0);\n\t\t\t\n\t\t\tif (triangulation == null) {\n\t\t\t\t\n\t\t\t\ttriangulation = new DelaunayTriangulation(\n\t\t\t\t\t\tnew AxisAlignedBoundingBoxXZ(-SIZE, -SIZE, SIZE, SIZE));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\ttriangulation.insert(p);\n\t\t\t\t\t\t\n\t\t\tthis.repaint(0);\n\n\t\t}\n\n\t\tprivate void updateMinMax(VectorXYZ v) {\n\t\t\tif (v.x < minX) minX = v.x;\n\t\t\tif (v.x > maxX) maxX = v.x;\n\t\t\tif (v.z < minZ) minZ = v.z;\n\t\t\tif (v.z > maxZ) maxZ = v.z;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void paint(Graphics g) {\n\t\t\t\n\t\t\tsuper.paint(g);\n\t\t\t\n\t\t\tif (triangulation == null) return;\n\t\t\t\n\t\t\t/* draw Voronoi cells */\n\t\t\t\n\t\t\tRandom colorRandom = new Random(0);\n\t\t\t\n\t\t\tfor (VectorXYZ p : points) {\n\t\t\t\t\n\t\t\t\tg.setColor(new Color(\n\t\t\t\t\t\t0.5f + colorRandom.nextFloat() / 2,\n\t\t\t\t\t\t0.5f + colorRandom.nextFloat() / 2,\n\t\t\t\t\t\t0.5f + colorRandom.nextFloat() / 2));\n\t\t\t\t\n\t\t\t\tfor (TriangleXZ t : triangulation.getVoronoiCellSectors(p)) {\n\t\t\t\t\tfill(g, t);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* draw circumcircles around most recently added point */\n\t\t\t\t\t\t\n\t\t\tif (!points.isEmpty()) {\n\t\t\t\n\t\t\t\tVectorXYZ p = points.get(points.size() - 1);\n\t\t\t\t\t\t\t\n\t\t\t\tg.setColor(Color.GREEN);\n\t\t\t\t\n\t\t\t\tfor (DelaunayTriangle t : triangulation.getIncidentTriangles(p)) {\n\t\n\t\t\t\t\tVectorXZ center = t.getCircumcircleCenter();\n\t\t\t\t\tdouble radius = t.p0.distanceToXZ(center);\n\t\t\t\t\n\t\t\t\t\tdraw(g, center);\n\t\t\t\t\tdrawCircle(g, center, radius);\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t/* draw triangles */\n\t\t\t\n\t\t\tg.setColor(Color.RED);\n\t\t\t\n\t\t\tfor (DelaunayTriangle triangle : triangulation.getTriangles()) {\n\t\t\t\tdraw(g, triangle.asTriangleXZ());\n\t\t\t}\n\t\t\t\t\t\t\n\t\t\t/* draw points */\n\t\t\t\n\t\t\tg.setColor(Color.BLACK);\n\t\t\t\n\t\t\tfor (VectorXYZ p : points) {\n\t\t\t\tdraw(g, p.xz());\n\t\t\t}\n\t\t\t\n\t\t\t/* draw click data */\n\t\t\t\n\t\t\tif (probePos != null) {\n\t\t\t\t\n\t\t\t\tdraw(g, probePos);\n\t\t\t\t\t\t\t\t\n\t\t\t\tNaturalNeighbors nn = triangulation.probe(probePos);\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i < nn.neighbors.length; i++) {\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ neighbor = nn.neighbors[i].xz();\n\t\t\t\t\tdouble relativeWeight = nn.relativeWeights[i];\n\t\t\t\t\t\n\t\t\t\t\tdrawLine(g, probePos, neighbor);\n\t\t\t\t\t\n\t\t\t\t\tg.drawString(String.format(Locale.US, \"%.3f\", relativeWeight),\n\t\t\t\t\t\t\tcoordX(neighbor), coordY(neighbor));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\t\n\t\tprivate void draw(Graphics g, VectorXZ p) {\n\t\t\tdrawPoint(g, p);\n\t\t}\n\t\t\n\t\tprivate void draw(Graphics g, TriangleXZ t) {\n\t\t\tg.drawPolygon(\n\t\t\t\t\tnew int[] {coordX(t.v1), coordX(t.v2), coordX(t.v3)},\n\t\t\t\t\tnew int[] {coordY(t.v1), coordY(t.v2), coordY(t.v3)},\n\t\t\t\t\t3);\n\t\t}\n\t\t\n\tprivate void fill(Graphics g, TriangleXZ t) {\n\t\tg.fillPolygon(\n\t\t\t\tnew int[] {coordX(t.v1), coordX(t.v2), coordX(t.v3)},\n\t\t\t\tnew int[] {coordY(t.v1), coordY(t.v2), coordY(t.v3)},\n\t\t\t\t3);\n\t}\n\t\t\n\t\tprivate void draw(Graphics g, PolygonXZ p) {\n\n\t\t\tfor (VectorXZ vertex : p.getVertices()) {\n\t\t\t\tdrawPoint(g, vertex);\n\t\t\t}\n\t\t\t\n\t\t\tfor (LineSegmentXZ segment : p.getSegments()) {\n\t\t\t\tdrawLine(g, segment.p1, segment.p2);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate void drawPoint(Graphics g, VectorXZ p) {\n\t\t\tint x = coordX(p), y = coordY(p);\n\t\t\tg.fillPolygon(\n\t\t\t\t\tnew int[] {x-3, x  , x+3, x  },\n\t\t\t\t\tnew int[] {  y, y+3, y  , y-3},\n\t\t\t\t\t4);\n\t\t}\n\t\t\n\t\tprivate void drawLine(Graphics g, VectorXZ p1, VectorXZ p2) {\n\t\t\tg.drawLine(\n\t\t\t\t\tcoordX(p1), coordY(p1),\n\t\t\t\t\tcoordX(p2), coordY(p2));\n\t\t}\n\n\t\tprivate void drawCircle(Graphics g, VectorXZ center, double radius) {\n\t\t\t\n\t\t\tVectorXZ bottomLeft = center.add(new VectorXZ(-radius, radius));\n\t\t\tVectorXZ topRight = center.add(new VectorXZ(radius, -radius));\n\t\t\t\t\t\t\n\t\t\tg.drawOval(\n\t\t\t\t\tcoordX(bottomLeft), coordY(bottomLeft),\n\t\t\t\t\tcoordX(topRight) - coordX(bottomLeft),\n\t\t\t\t\tcoordY(topRight) - coordY(bottomLeft));\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate void drawArrow(Graphics g, VectorXZ p1, VectorXZ p2) {\n\t\t\t\n\t\t\tdrawLine(g, p1, p2);\n\t\t\t\t\t\t\n\t\t\tVectorXZ arrowVector = p2.subtract(p1);\n\t\t\tVectorXZ arrowDir = arrowVector.normalize();\n\t\t\tdouble headSize = (maxX - minX) / 25;\n\t\t\t\t\t\t\n\t\t\tVectorXZ headBase = p2.subtract(arrowDir.mult(headSize));\n\t\t\tVectorXZ headRight = headBase.add(arrowDir.rightNormal().mult(0.5*headSize));\n\t\t\tVectorXZ headLeft = headBase.subtract(arrowDir.rightNormal().mult(0.5*headSize));\n\t\t\t\n\t\t\tdrawLine(g, headRight, p2);\n\t\t\tdrawLine(g, headLeft, p2);\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate int coordX(VectorXZ v) {\n\t\t\treturn (int)((this.getWidth()*0.05) + (v.x - minX) / (maxX - minX) * (this.getWidth()*0.9));\n\t\t}\n\t\t\n\t\tprivate int coordY(VectorXZ v) {\n\t\t\treturn (int)((this.getHeight()*0.95) - (v.z - minZ) / (maxZ - minZ) * (this.getHeight()*0.9));\n\t\t}\n\t\t\n\t\tprivate VectorXZ inverseCoord(int x, int y) {\n\t\t\t\n\t\t\tdouble relX = ( x - coordX(new VectorXZ(minX, maxZ)) ) / (this.getWidth() * 0.9);\n\t\t\tdouble relZ = ( y - coordY(new VectorXZ(minX, maxZ)) ) / (this.getHeight() * 0.9);\n\t\t\t\n\t\t\tdouble totalX = maxX - minX;\n\t\t\tdouble totalZ = maxZ - minZ;\n\t\t\t\n\t\t\treturn new VectorXZ(\n\t\t\t\t\tminX + relX * totalX,\n\t\t\t\t\tmaxZ - relZ * totalZ);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tprivate static class DebugViewerFrame extends JFrame {\n\t\t\n\t\tprivate static final long serialVersionUID = 3058008356994265355L; //generated serialVersionUID\n\n\t\tpublic DebugViewerFrame() {\n\t\t\tsuper(\"Visual Geometry Debugger\");\n\t\t\tthis.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);\n\t\t\t\n\t\t\tthis.setSize(800, 600);\n\t\t\t\t\t\t\n\t\t\tthis.setLayout(new BorderLayout());\n\t\t\tNNDebugPanel panel = new NNDebugPanel();\n\t\t\tthis.add(panel, BorderLayout.CENTER);\n\t\t\t\n\t\t\tJMenuBar menuBar = new JMenuBar();\n\t\t\t\t\t\t\n\t\t\t{\n\t\t\t\tJMenu addMenu = new JMenu(\"add\");\n\t\t\t\taddMenu.setMnemonic(KeyEvent.VK_A);\n\t\t\t\t\n\t\t\t\taddMenu.add(new AddRandomPointAction(panel));\n\t\t\t\t\n\t\t\t\tmenuBar.add(addMenu);\n\t\t\t}\n\t\t\t\n\t\t\tmenuBar.add(new JMenuItem(new ClearAction(panel)));\n\t\t\n\t\t\tthis.setJMenuBar(menuBar);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class ClearAction extends AbstractAction {\n\n\t\tprivate static final long serialVersionUID = -355449189633652151L; //generated serialVersionUID\n\t\tprivate final NNDebugPanel panel;\n\t\t\n\t\tpublic ClearAction(NNDebugPanel panel) {\n\t\t\tsuper(\"clear\");\n\t\t\tputValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(\n\t\t\t\t\tKeyEvent.VK_C, ActionEvent.CTRL_MASK));\n\t\t\tthis.panel = panel;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void actionPerformed(ActionEvent e) {\n\t\t\tpanel.clear();\n\t\t}\n\t\t\n\t}\n\n\tprivate static final class AddRandomPointAction extends AbstractAction {\n\t\t\n\t\tprivate static final long serialVersionUID = 8243198248896477663L; //generated serialVersionUID\n\n\t\tprivate final NNDebugPanel panel;\n\t\t\n\t\tprivate static Random random = new Random();\n\t\t\n\t\tpublic AddRandomPointAction(NNDebugPanel panel) {\n\t\t\tsuper(\"random point\");\n\t\t\tputValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(\n\t\t\t\t\tKeyEvent.VK_R, ActionEvent.CTRL_MASK));\n\t\t\tthis.panel = panel;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void actionPerformed(ActionEvent e) {\n\t\t\t\n\t\t\tif (panel.points.isEmpty()) {\n\t\t\t\trandom = new Random(1);\n\t\t\t}\n\t\t\t\n\t\t\tdouble x = (random.nextDouble() * 2 * SIZE) - SIZE;\n\t\t\tdouble z = (random.nextDouble() * 2 * SIZE) - SIZE;\n\t\t\t\n\t\t\tpanel.add(new VectorXYZ(x, 0, z));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class Line {\n\t\tpublic final VectorXZ from;\n\t\tpublic final VectorXZ to;\n\t\tpublic Line(VectorXZ from, VectorXZ to) {\n\t\t\tthis.from = from;\n\t\t\tthis.to = to;\n\t\t}\n\t}\n\t\n\tprivate static class Arrow extends Line {\n\t\tpublic Arrow(VectorXZ from, VectorXZ to) {\n\t\t\tsuper(from, to);\n\t\t}\n\t}\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/NaturalNeighborInterpolator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.map_elevation.creation.DelaunayTriangulation.NaturalNeighbors;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * uses natural neighbor interpolation of heights\n */\npublic class NaturalNeighborInterpolator implements TerrainInterpolator {\n\n\tprivate DelaunayTriangulation triangulation;\n\t\n\t@Override\n\tpublic void setKnownSites(Collection<VectorXYZ> sites) {\n\t\t\n\t\tAxisAlignedBoundingBoxXZ boundingBox = new AxisAlignedBoundingBoxXZ(sites);\n\t\tboundingBox = boundingBox.pad(100);\n\t\t\n\t\ttriangulation = new DelaunayTriangulation(boundingBox);\n\t\t\n\t\tint i = 0; //TODO remove\n\t\tint total = sites.size();\n\t\tlong startTime = System.currentTimeMillis();\n\t\t\n\t\tfor (VectorXYZ site : sites) {\n\t\t\tif (++i % 1000 == 0) System.out.println(\"KS: \" + i + \"/\" + total\n\t\t\t\t\t+ \" after \" + ((System.currentTimeMillis() - startTime) / 1e3));\n\t\t\ttriangulation.insert(site);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\t@Override\n\tpublic VectorXYZ interpolateEle(VectorXZ pos) {\n\t\n\t\tNaturalNeighbors nn = triangulation.probe(pos);\n\t\t\n\t\tdouble ele = 0;\n\t\t\n\t\tfor (int i = 0; i < nn.neighbors.length; i++) {\n\t\t\tele += nn.neighbors[i].y * nn.relativeWeights[i];\n\t\t}\n\t\t\n\t\treturn pos.xyz(ele);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/NoneEleConstraintEnforcer.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.util.List;\n\nimport org.osm2world.core.map_elevation.data.EleConnector;\n\n/**\n * enforcer implementation that simply passes the interpolated terrain\n * elevations through, and does not actually enforce constraints.\n */\npublic class NoneEleConstraintEnforcer implements EleConstraintEnforcer {\n\t\n\t@Override\n\tpublic void addConnectors(Iterable<EleConnector> connectors) {}\n\t\n\t@Override\n\tpublic void requireSameEle(EleConnector c1, EleConnector c2) {}\n\t\n\t@Override\n\tpublic void requireSameEle(Iterable<EleConnector> cs) {}\n\t\n\t@Override\n\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector lower) {}\n\t\n\t@Override\n\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector base1, EleConnector base2) {}\n\t\n\t@Override\n\tpublic void requireIncline(ConstraintType type, double incline,\n\t\t\tList<EleConnector> cs) {}\n\t\n\t@Override\n\tpublic void requireSmoothness(\n\t\t\tEleConnector from, EleConnector via, EleConnector to) {}\n\t\n\t@Override\n\tpublic void enforceConstraints() {}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/SRTMData.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport static java.lang.Double.isNaN;\nimport static java.lang.Math.*;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collection;\n\nimport org.osm2world.core.map_data.creation.MapProjection;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * SRTM data for a part of the planet\n */\npublic class SRTMData implements TerrainElevationData {\n\t\n\tprivate final File tileDirectory;\n\tprivate final MapProjection projection;\n\tprivate final SRTMTile[][] tiles;\n\t\n\tpublic SRTMData(File tileDirectory, MapProjection projection) {\n\t\tthis.tileDirectory = tileDirectory;\n\t\tthis.projection = projection;\n\t\tthis.tiles = new SRTMTile[360][180];\n\t}\n\t\t\n\t@Override\n\tpublic Collection<VectorXYZ> getSites(double minLon, double minLat,\n\t\t\tdouble maxLon, double maxLat) throws IOException {\n\t\t\n\t\tCollection<VectorXYZ> result = new ArrayList<VectorXYZ>();\n\t\t\n\t\tint minLonInt = (int)floor(minLon);\n\t\tint minLatInt = (int)floor(minLat);\n\t\tint maxLonInt = (int)ceil(maxLon);\n\t\tint maxLatInt = (int)ceil(maxLat);\n\t\t\n\t\tfor (int lon = minLonInt; lon < maxLonInt; lon++) {\n\t\t\tfor (int lat = minLatInt; lat < maxLatInt; lat++) {\n\t\t\t\t\n\t\t\t\tloadTileIfNecessary(lon, lat);\n\t\t\t\t\n\t\t\t\taddTileSites(result, lon, lat,\n\t\t\t\t\t\tminLon, minLat, maxLon, maxLat);\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\t/**\n\t * variant of getSites which calculates minimum and maximum lat/lon\n\t * from the bounds of a {@link MapData} instance\n\t * \n\t * TODO: make projection reversible, then replace both getSites methods\n\t *       with a single getSite(AxisAlignedBoundingBox dataBounds) method\n\t */\n\t@Override\n\tpublic Collection<VectorXYZ> getSites(MapData mapData) throws IOException {\n\t\t\n\t\tdouble minLon = Double.POSITIVE_INFINITY;\n\t\tdouble minLat = Double.POSITIVE_INFINITY;\n\t\tdouble maxLon = Double.NEGATIVE_INFINITY;\n\t\tdouble maxLat = Double.NEGATIVE_INFINITY;\n\t\t\n\t\t/* find the minimum and maximum lat/lon in the data */\n\t\t\n\t\tfor (MapNode mapNode : mapData.getMapNodes()) {\n\t\t\t\n\t\t\tdouble lon = mapNode.getOsmNode().lon;\n\t\t\tdouble lat = mapNode.getOsmNode().lat;\n\t\t\t\n\t\t\tif (!isNaN(lat) && !isNaN(lon)) {\n\t\t\t\tminLon = min(minLon, lon);\n\t\t\t\tminLat = min(minLat, lat);\n\t\t\t\tmaxLon = max(maxLon, lon);\n\t\t\t\tmaxLat = max(maxLat, lat);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* add a small seam for robustness */\n\t\t\n\t\tminLon -= 0.02; minLat -= 0.02;\n\t\tmaxLon += 0.02; maxLat += 0.02;\n\t\t\n\t\t/* TODO: the seam could be smaller - such as this - if empty terrain nodes did have lat/lon\n\t\tminLon -= 0.005; minLat -= 0.005;\n\t\tmaxLon += 0.005; maxLat += 0.005;\n\t\t*/\n\t\t\n\t\t/* retrieve the sites for the query */\n\t\t\n\t\treturn getSites(minLon, minLat, maxLon, maxLat);\n\t\t\n\t}\n\n\tprivate void loadTileIfNecessary(int lon, int lat) throws IOException {\n\t\t\n\t\tif (getTile(lon, lat) == null) {\n\t\t\t\n\t\t\tString fileName = tileDirectory.getPath() + File.separator;\n\t\t\t\n\t\t\tif (lat >= 0) {\n\t\t\t\tfileName += String.format(\"N%02d\", lat);\n\t\t\t} else {\n\t\t\t\tfileName += String.format(\"S%02d\", -lat);\n\t\t\t}\n\n\t\t\tif (lon >= 0) {\n\t\t\t\tfileName += String.format(\"E%03d\", lon);\n\t\t\t} else {\n\t\t\t\tfileName += String.format(\"W%03d\", -lon);\n\t\t\t}\n\t\t\t\n\t\t\tfileName += \".hgt\";\n\t\t\t\n\t\t\tFile file = new File(fileName);\n\t\t\t\n\t\t\tif (file.exists()) {\n\t\t\t\tsetTile(lon, lat, new SRTMTile(file));\n\t\t\t} else {\n\t\t\t\tSystem.err.println(\"warning: missing SRTM tile \" + file.getName());\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate void addTileSites(Collection<VectorXYZ> result,\n\t\t\tint tileLon, int tileLat,\n\t\t\tdouble minLon, double minLat, double maxLon, double maxLat) {\n\t\t\n\t\tSRTMTile tile = getTile(tileLon, tileLat);\n\t\t\n\t\tif (tile == null) return;\n\t\t\n\t\t/* add a site for each SRTM pixel (except last line and column,\n\t\t * which is duplicated in adjacent tiles) */\n\t\t\n\t\tint minX = max(0,\n\t\t\t\t(int)ceil(SRTMTile.PIXELS * (minLon - tileLon)));\n\t\tint maxX = min(SRTMTile.PIXELS - 1,\n\t\t\t\t(int)floor(SRTMTile.PIXELS * (maxLon - tileLon)));\n\n\t\tint minY = max(0,\n\t\t\t\t(int)ceil(SRTMTile.PIXELS * (minLat - tileLat)));\n\t\tint maxY = min(SRTMTile.PIXELS - 1,\n\t\t\t\t(int)floor(SRTMTile.PIXELS * (maxLat - tileLat)));\n\t\t\n\t\tfor (int x = minX; x < maxX; x++) {\n\t\t\tfor (int y = minY; y < maxY; y++) {\n\t\t\t\t\n\t\t\t\tshort value = tile.getData(x, y);\n\t\t\t\t\n\t\t\t\tdouble lat = tileLat + 1.0 / SRTMTile.PIXELS * (y + 0.5);\n\t\t\t\tdouble lon = tileLon + 1.0 / SRTMTile.PIXELS * (x + 0.5);\n\t\t\t\t\n\t\t\t\tVectorXZ pos = projection.calcPos(lat, lon);\n\t\t\t\t\n\t\t\t\tif (value != SRTMTile.BLANK_VALUE &&\n\t\t\t\t\t\t!Double.isNaN(pos.x) && !Double.isNaN(pos.z)) {\n\t\t\t\t\tresult.add(pos.xyz(value));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t}\n\t\n\tprivate SRTMTile getTile(int tileLon, int tileLat) {\n\t\treturn tiles[tileLon+180][tileLat+90];\n\t}\n\t\n\tprivate void setTile(int tileLon, int tileLat, SRTMTile tile) {\n\t\ttiles[tileLon+180][tileLat+90] = tile;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/SRTMTile.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.ShortBuffer;\nimport java.nio.channels.FileChannel;\n\n/**\n * a single SRTM data tile.\n * \n * Multiple such tiles are used by {@link SRTMData} to build coverage\n * for larger regions.\n */\nclass SRTMTile {\n\n\t/** value indicating a lack of data */\n\tpublic static final short BLANK_VALUE = -32768;\n\t\n\t/** length of each dimension of an SRTM tile in pixels */\n\tstatic final int PIXELS = 1201;\n\t\t\n\tpublic final File file;\n\tprivate final ShortBuffer data;\n\t\n\tpublic SRTMTile(File file) throws IOException {\n\t\t\n\t\tthis.file = file;\n\t\t\n\t\tdata = loadDataFromFile(file);\n\t\t\t\t\n\t}\n\n\tprivate static ShortBuffer loadDataFromFile(File file) throws IOException {\n\n\t\tFileInputStream fis = new FileInputStream(file);\n\t\tFileChannel fc = fis.getChannel();\n\t\tByteBuffer bb = ByteBuffer.allocateDirect((int) fc.size());\n\t\twhile (bb.remaining() > 0) fc.read(bb);\n\t\tfc.close();\n\t\tfis.close();\n\t\t\n\t\tbb.flip();\n\t\t\n\t\t// choose the right endianness\n\t\treturn bb.order(ByteOrder.BIG_ENDIAN).asShortBuffer();\n\t\t\n\t}\n\t\n\tpublic final short getData(int x, int y) {\n\t\tassert 0 <= x && x < PIXELS && 0 <= y && y < PIXELS;\n\t\treturn data.get((1200 - y) * 1201 + x);\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn file.getName();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/SimpleEleConstraintEnforcer.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport static java.util.Arrays.asList;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport org.osm2world.core.map_elevation.data.EleConnector;\n\n/**\n * enforcer implementation that ignores many of the constraints,\n * but is much faster than the typical full implementation.\n * \n * It tries to produce an output that is \"good enough\" for some purposes,\n * and is therefore a compromise between the {@link NoneEleConstraintEnforcer}\n * and a full implementation.\n */\npublic final class SimpleEleConstraintEnforcer implements EleConstraintEnforcer {\n\t\n\tprivate Collection<EleConnector> connectors = new ArrayList<EleConnector>();\n\t\n\t/**\n\t * associates each EleConnector with the {@link StiffConnectorSet}\n\t * it is part of (if any)\n\t */\n\tprivate Map<EleConnector, StiffConnectorSet> stiffSetMap =\n\t\t\tnew HashMap<EleConnector, StiffConnectorSet>();\n\t\n\t@Override\n\tpublic void addConnectors(Iterable<EleConnector> newConnectors) {\n\t\t\n\t\tfor (EleConnector c : newConnectors) {\n\t\t\tconnectors.add(c);\n\t\t}\n\t\t\n\t\t/* connect connectors */\n\t\t\n\t\tfor (EleConnector c1 : newConnectors) {\n\t\t\tfor (EleConnector c2 : connectors) {\n\t\t\t\t\n\t\t\t\tif (c1 != c2 && c1.connectsTo(c2)) {\n\t\t\t\t\trequireSameEle(c1, c2);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireSameEle(EleConnector c1, EleConnector c2) {\n\t\t\n\t\t//SUGGEST (performance): a special case implementation would be faster\n\t\t\n\t\trequireSameEle(asList(c1, c2));\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireSameEle(Iterable<EleConnector> cs) {\n\t\t\n\t\t/* find stiff sets containing any of the affected connectors */\n\t\t\n\t\tSet<EleConnector> looseConnectors = new HashSet<EleConnector>();\n\t\tSet<StiffConnectorSet> existingStiffSets = new HashSet<StiffConnectorSet>();\n\t\t\n\t\tfor (EleConnector c : cs) {\n\t\t\t\n\t\t\tStiffConnectorSet stiffSet = stiffSetMap.get(c);\n\t\t\t\n\t\t\tif (stiffSet != null) {\n\t\t\t\texistingStiffSets.add(stiffSet);\n\t\t\t} else {\n\t\t\t\tlooseConnectors.add(c);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* return if the connectors are already in a set together */\n\t\t\n\t\tif (existingStiffSets.size() == 1 && looseConnectors.isEmpty()) return;\n\t\t\n\t\t/* merge existing sets (if any) into a single set */\n\t\t\n\t\tStiffConnectorSet commonStiffSet = null;\n\t\t\n\t\tif (existingStiffSets.isEmpty()) {\n\t\t\tcommonStiffSet = new StiffConnectorSet();\n\t\t} else {\n\t\t\t\n\t\t\tfor (StiffConnectorSet stiffSet : existingStiffSets) {\n\t\t\t\t\n\t\t\t\tif (commonStiffSet == null) {\n\t\t\t\t\tcommonStiffSet = stiffSet;\n\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\tfor (EleConnector c : stiffSet) {\n\t\t\t\t\t\tstiffSetMap.put(c, commonStiffSet);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tcommonStiffSet.mergeFrom(stiffSet);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* add remaining (loose) connectors into the common set */\n\t\t\n\t\tfor (EleConnector c : looseConnectors) {\n\t\t\t\n\t\t\tcommonStiffSet.add(c);\n\t\t\t\n\t\t\tstiffSetMap.put(c, commonStiffSet);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector lower) {\n\t\t// TODO Auto-generated method stub\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\tEleConnector upper, EleConnector base1, EleConnector base2) {\n\t\t// TODO Auto-generated method stub\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireIncline(ConstraintType type, double incline, List<EleConnector> cs) {\n\t\t// TODO Auto-generated method stub\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void requireSmoothness(EleConnector from, EleConnector via, EleConnector to) {\n\t\t// TODO Auto-generated method stub\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void enforceConstraints() {\n\t\t\n\t\t/* assign elevation to stiff sets by averaging terrain elevation */\n\t\t//TODO what for stiff sets above the ground?\n\t\t\n\t\tfor (StiffConnectorSet stiffSet : stiffSetMap.values()) {\n\t\t\t\n\t\t\tdouble averageEle = 0;\n\t\t\t\n\t\t\tfor (EleConnector connector : stiffSet) {\n\t\t\t\taverageEle += connector.getPosXYZ().y;\n\t\t\t}\n\t\t\t\n\t\t\taverageEle /= stiffSet.size();\n\t\t\t\n\t\t\tfor (EleConnector connector : stiffSet) {\n\t\t\t\tconnector.setPosXYZ(connector.pos.xyz(averageEle));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* TODO implement intended algorithm:\n\t\t * - first assign ground ele to ON\n\t\t * - then assign ele for ABOVE and BELOW based on min vertical distance constraints, and clearing\n\t\t */\n\t\t\n\t\tfor (EleConnector c : connectors) {\n\t\t\t\n\t\t\t//TODO use clearing\n\t\t\t\n\t\t\tswitch (c.groundState) {\n\t\t\tcase ABOVE: c.setPosXYZ(c.getPosXYZ().addY(5)); break;\n\t\t\tcase BELOW: c.setPosXYZ(c.getPosXYZ().addY(-5)); break;\n\t\t\tdefault: //stay at ground elevation\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * a set of connectors that are required to have the same elevation\n\t * TODO or a precise vertical offset\n\t */\n\tprivate static class StiffConnectorSet implements Iterable<EleConnector> {\n\t\t\n\t\t//TODO maybe look for a more efficient set implementation\n\t\tprivate Set<EleConnector> connectors = new HashSet<EleConnector>();\n\t\t\n\t\t/**\n\t\t * adds a connector to this set, requiring it to be at the set's\n\t\t * reference elevation\n\t\t */\n\t\tpublic void add(EleConnector connector) {\n\t\t\tconnectors.add(connector);\n\t\t}\n\t\t\n\t\t/**\n\t\t * combines this set with another, and makes the other set unusable.\n\t\t * This set will contain all {@link EleConnector}s from the other set\n\t\t * afterwards.\n\t\t */\n\t\tpublic void mergeFrom(StiffConnectorSet otherSet) {\n\t\t\t\n\t\t\tconnectors.addAll(otherSet.connectors);\n\t\t\t\n\t\t\t// make sure that the other set cannot be used anymore\n\t\t\totherSet.connectors = null;\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic double size() {\n\t\t\treturn connectors.size();\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Iterator<EleConnector> iterator() {\n\t\t\treturn connectors.iterator();\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/TagElevationCalculator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapSegment;\n\n/**\n * relies on tags that explicitly set elevation.\n * Subclasses determine the tag(s) to be used for this purpose.\n */\npublic abstract class TagElevationCalculator implements ElevationCalculator {\n\t\n\tDouble terrainElevation;\n\tboolean enableUnknownEleWarning;\n\t\n\t/**\n\t * @param terrainElevation  elevation for the terrain\n\t */\n\tpublic TagElevationCalculator(Double terrainElevation,\n\t\t\tboolean enableUnknownEleWarning) {\n\t\tthis.terrainElevation = terrainElevation;\n\t\tthis.enableUnknownEleWarning = enableUnknownEleWarning;\n\t}\n\t\n\tpublic TagElevationCalculator() {\n\t\tthis(0.0, false);\n\t}\n\t\n\t@Override\n\tpublic void calculateElevations(MapData mapData,\n\t\t\tTerrainElevationData eleData) {\n\n//\t\t//TODO replace old ElevationProfile stuff\n//\n//\t\t/* set nodes' elevation profiles */\n//\n//\t\tfor (MapNode node : mapData.getMapNodes()) {\n//\n//\t\t\tDouble ele = getEleForTags(node.getTags());\n//\n//\t\t\tif (ele == null) {\n//\n//\t\t\t\t/* use elevation information from nodes or areas containing\n//\t\t\t\t * this node. If they have contradicting information, the\n//\t\t\t\t * results will be unpredictable.\n//\t\t\t\t */\n//\n//\t\t\t\tfor (MapSegment segment : node.getConnectedSegments()) {\n//\t\t\t\t\tif (ele == null) {\n//\t\t\t\t\t\tTagGroup tags;\n//\t\t\t\t\t\tif (segment instanceof MapWaySegment) {\n//\t\t\t\t\t\t\ttags = ((MapWaySegment) segment).getTags();\n//\t\t\t\t\t\t} else {\n//\t\t\t\t\t\t\ttags = ((MapAreaSegment) segment).getArea().getTags();\n//\t\t\t\t\t\t}\n//\t\t\t\t\t\tele = getEleForTags(tags);\n//\t\t\t\t\t}\n//\t\t\t\t}\n//\n//\t\t\t}\n//\n//\t\t\tif (ele != null) {\n//\n//\t\t\t\tNodeElevationProfile profile = new NodeElevationProfile(node);\n//\t\t\t\tprofile.setEle(ele);\n//\t\t\t\tnode.setElevationProfile(profile);\n//\n//\t\t\t}\n//\n//\t\t}\n//\n//\t\t/* set elevation profiles for nodes without elevation profiles,\n//\t\t * attempt interpolation the two closest connected nodes with ele */\n//\n//\t\tfor (MapNode node : mapData.getMapNodes()) {\n//\n//\t\t\tif (node.getElevationProfile() == null) {\n//\n//\t\t\t\tList<Connection> connections = new ArrayList<Connection>();\n//\n//\t\t\t\tfor (MapSegment segment : node.getConnectedSegments()) {\n//\n//\t\t\t\t\tConnection connection =\n//\t\t\t\t\t\tfindConnectionToNodeWithEle(node, segment);\n//\n//\t\t\t\t\tif (connection != null) {\n//\t\t\t\t\t\tconnections.add(connection);\n//\t\t\t\t\t}\n//\n//\t\t\t\t}\n//\n//\t\t\t\t// sort by length\n//\t\t\t\tCollections.sort(connections, new Comparator<Connection>() {\n//\t\t\t\t\tpublic int compare(Connection c1, Connection c2) {\n//\t\t\t\t\t\treturn Double.compare(c1.getLength(), c2.getLength());\n//\t\t\t\t\t};\n//\t\t\t\t});\n//\n//\t\t\t\tdouble ele = 0;\n//\n//\t\t\t\tif (connections.size() < 2) {\n//\t\t\t\t\tif (enableUnknownEleWarning) {\n//\t\t\t\t\t\tSystem.err.println(\"node without ele information: \" + node);\n//\t\t\t\t\t}\n//\t\t\t\t} else {\n//\n//\t\t\t\t\t/* interpolate between 2 closest connected nodes */\n//\n//\t\t\t\t\tConnection cA = connections.get(0);\n//\t\t\t\t\tConnection cB = connections.get(1);\n//\t\t\t\t\tdouble eleA = cA.endNode.getElevationProfile().getEle();\n//\t\t\t\t\tdouble eleB = cB.endNode.getElevationProfile().getEle();\n//\n//\t\t\t\t\tele = ((eleA * cB.getLength()) + (eleB * cA.getLength()))\n//\t\t\t\t\t\t/ (cA.getLength() + cB.getLength());\n//\n//\t\t\t\t}\n//\n//\t\t\t\tNodeElevationProfile profile = new NodeElevationProfile(node);\n//\t\t\t\tprofile.setEle(ele);\n//\t\t\t\tnode.setElevationProfile(profile);\n//\n//\t\t\t}\n//\n//\t\t}\n//\n//\t\t/* set way segments' elevation profiles (based on nodes' elevations) */\n//\n//\t\tfor (MapWaySegment segment : mapData.getMapWaySegments()) {\n//\n//\t\t\tif (segment.getPrimaryRepresentation() == null) continue;\n//\n//\t\t\tWaySegmentElevationProfile profile =\n//\t\t\t\tnew WaySegmentElevationProfile(segment);\n//\n//\t\t\tprofile.addPointWithEle(\n//\t\t\t\tsegment.getStartNode().getElevationProfile().getPointWithEle());\n//\t\t\tprofile.addPointWithEle(\n//\t\t\t\tsegment.getEndNode().getElevationProfile().getPointWithEle());\n//\n//\t\t\tsegment.setElevationProfile(profile);\n//\n//\t\t}\n//\n//\t\t/* set areas' elevation profiles (based on nodes' elevations) */\n//\n//\t\tfor (MapArea area : mapData.getMapAreas()) {\n//\n//\t\t\tif (area.getPrimaryRepresentation() == null) continue;\n//\n//\t\t\tAreaElevationProfile profile =\n//\t\t\t\tnew AreaElevationProfile(area);\n//\n//\t\t\tfor (MapNode node : area.getBoundaryNodes()) {\n//\t\t\t\tprofile.addPointWithEle(\n//\t\t\t\t\tnode.getElevationProfile().getPointWithEle());\n//\t\t\t}\n//\n//\t\t\tfor (List<MapNode> holeOutline : area.getHoles()) {\n//\t\t\t\tfor (MapNode node : holeOutline) {\n//\t\t\t\t\tprofile.addPointWithEle(\n//\t\t\t\t\t\tnode.getElevationProfile().getPointWithEle());\n//\t\t\t\t}\n//\t\t\t}\n//\n//\t\t\tarea.setElevationProfile(profile);\n//\n//\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * a sequence of {@link MapSegment}s\n\t * that leads to a {@link MapNode} at the end\n\t */\n\tprivate static class Connection {\n\t\t\n\t\tpublic final List<MapSegment> segmentSequence;\n\t\tpublic final MapNode endNode;\n\t\t\n\t\tpublic Connection(List<MapSegment> segmentSequence, MapNode endNode) {\n\t\t\tthis.segmentSequence = segmentSequence;\n\t\t\tthis.endNode = endNode;\n\t\t}\n\t\t\n\t\tpublic double getLength() {\n\t\t\tdouble distance = 0.0;\n\t\t\tfor (MapSegment s : segmentSequence) {\n\t\t\t\tdistance += s.getLineSegment().getLength();\n\t\t\t}\n\t\t\treturn distance;\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * Tries to find the segment sequence to a node with elevation information.\n\t * More precisely, this follows the sequence of segments started by\n\t * the specified first segment until ... <ul>\n\t * <li>... a node with elevation information is found and is returned.</li>\n\t * <li>... the sequence segment branches, null is returned.</li>\n\t * <li>... the sequence returns to the start, null is returned.</li>\n\t * </ul>\n\t */\n\t//TODO replace old ElevationProfile stuff\n//\tprivate static final Connection findConnectionToNodeWithEle(\n//\t\t\tMapNode node, MapSegment firstSegmentForSequence) {\n//\n//\t\tList<MapSegment> segmentSequence = new ArrayList<MapSegment>();\n//\n//\t\tMapNode currentNode = node;\n//\t\tMapSegment currentSegment = firstSegmentForSequence;\n//\n//\t\twhile (true) { //one of the return conditions will ultimately be true\n//\n//\t\t\tsegmentSequence.add(currentSegment);\n//\n//\t\t\tcurrentNode = currentSegment.getOtherNode(currentNode);\n//\n//\t\t\tif (currentNode.getElevationProfile() != null) {\n//\t\t\t\treturn new Connection(segmentSequence, currentNode);\n//\t\t\t} else if (currentNode == node) {\n//\t\t\t\treturn null;\n//\t\t\t} else if (currentNode.getConnectedSegments().size() > 2) {\n//\t\t\t\treturn null;\n//\t\t\t}\n//\n//\t\t\t// use the one connected segment that is not the current segment\n//\t\t\t// as the next current segment\n//\n//\t\t\tfor (MapSegment s : currentNode.getConnectedSegments()) {\n//\t\t\t\tif (s != currentSegment) {\n//\t\t\t\t\tcurrentSegment = s;\n//\t\t\t\t\tbreak;\n//\t\t\t\t}\n//\t\t\t}\n//\n//\t\t}\n//\n//\t}\n\t\n\t/**\n\t * returns the elevation as set explicitly by the tags\n\t * \n\t * @return  elevation; null if the tags don't define the elevation\n\t */\n\tprotected abstract Double getEleForTags(TagGroup tags);\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/TerrainElevationData.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.io.IOException;\nimport java.util.Collection;\n\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.math.VectorXYZ;\n\n/**\n * a source of terrain elevation data. Implementations may range from raster\n * data such as SRTM to sparsely distributed points with known elevation.\n */\npublic interface TerrainElevationData {\n\n\tCollection<VectorXYZ> getSites(double minLon, double minLat,\n\t\t\tdouble maxLon, double maxLat) throws IOException;\n\n\tCollection<VectorXYZ> getSites(MapData mapData) throws IOException;\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/TerrainInterpolator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * strategy for elevation interpolation from a set of known points\n */\npublic interface TerrainInterpolator {\n\n\tvoid setKnownSites(Collection<VectorXYZ> sites);\n\t\n\tVectorXYZ interpolateEle(VectorXZ pos);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/ZeroElevationCalculator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.data.AreaElevationProfile;\nimport org.osm2world.core.map_elevation.data.NodeElevationProfile;\nimport org.osm2world.core.map_elevation.data.WaySegmentElevationProfile;\n\n/**\n * assigns an elevation of 0 to everything.\n * Useful for certain use cases, e.g. fast creation of tiled pseudo-3D tiles.\n */\npublic class ZeroElevationCalculator implements ElevationCalculator {\n\n\t@Override\n\tpublic void calculateElevations(MapData mapData,\n\t\t\tTerrainElevationData eleData) {\n\t\t\t\t\n\t\tfor (MapNode node : mapData.getMapNodes()) {\n\t\t\t\t\t\t\t\n\t\t\tNodeElevationProfile profile = new NodeElevationProfile(node);\n\t\t\tprofile.setEle(0);\n\t\t\t//TODO replace old ElevationProfile stuff\n//\t\t\tnode.setElevationProfile(profile);\n\t\t\t\t\t\t\n\t\t}\n\t\t\n\t\tfor (MapWaySegment segment : mapData.getMapWaySegments()) {\n\n\t\t\tif (segment.getPrimaryRepresentation() == null) continue;\n\t\t\t\n\t\t\tWaySegmentElevationProfile profile =\n\t\t\t\tnew WaySegmentElevationProfile(segment);\n\t\t\t\n\t\t\t//TODO replace old ElevationProfile stuff\n//\t\t\tprofile.addPointWithEle(\n//\t\t\t\tsegment.getStartNode().getElevationProfile().getPointWithEle());\n//\t\t\tprofile.addPointWithEle(\n//\t\t\t\tsegment.getEndNode().getElevationProfile().getPointWithEle());\n//\n//\t\t\tsegment.setElevationProfile(profile);\n\t\t\t\n\t\t}\n\t\t\n\t\t/* set areas' elevation profiles (based on nodes' elevations) */\n\t\t\n\t\tfor (MapArea area : mapData.getMapAreas()) {\n\t\t\t\n\t\t\tif (area.getPrimaryRepresentation() == null) continue;\n\t\t\t\n\t\t\tAreaElevationProfile profile =\n\t\t\t\tnew AreaElevationProfile(area);\n\t\t\t\n\t\t\tfor (MapNode node : area.getBoundaryNodes()) {\n\t\t\t\t//TODO replace old ElevationProfile stuff\n//\t\t\t\tprofile.addPointWithEle(\n//\t\t\t\t\tnode.getElevationProfile().getPointWithEle());\n\t\t\t}\n\t\t\t\n\t\t\tfor (List<MapNode> holeOutline : area.getHoles()) {\n\t\t\t\tfor (MapNode node : holeOutline) {\n\t\t\t\t\t//TODO replace old ElevationProfile stuff\n//\t\t\t\t\tprofile.addPointWithEle(\n//\t\t\t\t\t\tnode.getElevationProfile().getPointWithEle());\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t//TODO replace old ElevationProfile stuff\n//\t\t\tarea.setElevationProfile(profile);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/creation/ZeroInterpolator.java",
    "content": "package org.osm2world.core.map_elevation.creation;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * sets every point's elevation to 0\n */\npublic class ZeroInterpolator implements TerrainInterpolator {\n\t\n\t@Override\n\tpublic void setKnownSites(Collection<VectorXYZ> sites) {\n\t\t// do nothing\n\t}\n\t\n\t@Override\n\tpublic VectorXYZ interpolateEle(VectorXZ pos) {\n\t\treturn pos.xyz(0);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/data/AreaElevationProfile.java",
    "content": "package org.osm2world.core.map_elevation.data;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_elevation.creation.ElevationCalculator;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\nimport com.google.common.base.Function;\n\n/**\n * elevation profile for a {@link MapArea}\n */\npublic class AreaElevationProfile extends ElevationProfile {\n\n\tprivate final MapArea area;\n\t\n\tprivate Collection<VectorXYZ> pointsWithEle = null;\n\n\tprivate Function<VectorXZ, VectorXYZ> eleFunction;\n\t\n\tCollection<TriangleXYZ> triangulation = null;\n\t\n\tpublic AreaElevationProfile(MapArea area) {\n\t\tsuper();\n\t\tthis.area = area;\n\t}\n\t\n\t@Override\n\tprotected MapElement getElement() {\n\t\treturn area;\n\t}\n\n\t/**\n\t * returns all points on the area where elevation values exist.\n\t * This will at least include the outline points.\n\t * Points are not sorted in any way. Must not be used before calculation\n\t * results have been set using {@link #addPointWithEle(VectorXYZ)}\n\t */\n\t@Override\n\tpublic Collection<VectorXYZ> getPointsWithEle() {\n\t\t\n\t\tif (pointsWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t} else if (pointsWithEle.size() < 2) {\n\t\t\tthrow new IllegalStateException(\"an area must have at least two points with elevation\");\n\t\t}\n\t\t\n\t\treturn pointsWithEle;\n\t}\n\t\n\t@Override\n\tpublic double getEleAt(final VectorXZ pos) {\n\t\t\n//\t\tif (triangulation == null) {\n//\t\t\tcalculateTriangulation();\n//\t\t}\n\t\t//TODO: calculate correctly and with better performance.\n\t\t//Should be possible using a triangulation.\n\t\t\n\t\t//temporary solution: find closest point with ele\n\t\t\t\t\n//\t\tVectorXYZ closestPoint = Collections.min(pointsWithEle, new Comparator<VectorXYZ>(){\n//\t\t\tpublic int compare(VectorXYZ p1, VectorXYZ p2) {\n//\t\t\t\treturn Double.compare(\n//\t\t\t\t\t\tp1.xz().distanceTo(pos),\n//\t\t\t\t\t\tp2.xz().distanceTo(pos));\n//\t\t\t};\n//\t\t});\n//\n//\t\treturn closestPoint.y;\n\t\t\n\t\treturn eleFunction.apply(pos).y;\n\t\t\n\t}\n\t\n\t@Override\n\tpublic VectorXYZ getWithEle(VectorXZ pos) {\n\t\t//TODO keep in sync with getEleAt\n\t\treturn eleFunction.apply(pos);\n\t}\n\t\t\t\n\t/**\n\t * adds a result of {@link ElevationCalculator}.\n\t * Must be called at least once for every outline node\n\t */\n\tpublic void addPointWithEle(VectorXYZ pointWithEle) {\n\t\t\n\t\tif (pointsWithEle == null) {\n\t\t\tpointsWithEle = new ArrayList<VectorXYZ>();\n\t\t}\n\t\t\n\t\tthis.pointsWithEle.add(pointWithEle);\n\t\t\n\t}\n\t\n\tpublic void setEleFunction(Function<VectorXZ, VectorXYZ> eleFunction) {\n\t\tthis.eleFunction = eleFunction;\n\t}\n\n\t@Override\n\tpublic double getMaxEle() {\n\t\tif (pointsWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t}\n\t\tdouble maxEle = Double.MIN_VALUE;\n\t\tfor (VectorXYZ pointWithEle : pointsWithEle) {\n\t\t\tmaxEle = Math.max(maxEle, pointWithEle.y);\n\t\t}\n\t\treturn maxEle;\n\t}\n\n\t@Override\n\tpublic double getMinEle() {\n\t\tif (pointsWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t}\n\t\tdouble minEle = Double.MAX_VALUE;\n\t\tfor (VectorXYZ pointWithEle : pointsWithEle) {\n\t\t\tminEle = Math.min(minEle, pointWithEle.y);\n\t\t}\n\t\treturn minEle;\n\t}\n\t\n//  TODO: finish implementation\n//\t/**\n//\t * returns a triangulation of the associated area\n//\t * (the one returned by {@link #getElement()}),\n//\t * with elevation information\n//\t */\n//\tpublic Collection<TriangleXYZ> getTriangulation() {\n//\t\treturn triangulation;\n//\t}\n//\n//\t/**\n//\t * calculates a triangulation of this area\n//\t * and writes the result to {@link #triangulation}\n//\t */\n//\tprivate void calculateTriangulation() {\n//\n//\t\tif (pointsWithEle == null) {\n//\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n//\t\t} else if (pointsWithEle.size() < 2) {\n//\t\t\tthrow new IllegalStateException(\"an area must have at least two points with elevation\");\n//\t\t}\n//\n//\t\tCollection<TriangleXZ> trianglesXZ =\n//\t\t\tTriangulationUtil.triangulate(area.getPolygon());\n//\n//\t\tCollection<TriangleXYZ> trianglesXYZ =\n//\t\t\tnew ArrayList<TriangleXYZ>(trianglesXZ.size());\n//\n//\t\tfor (TriangleXZ triangleXZ : trianglesXZ) {\n//\t\t\tVectorXYZ v1 = eleProfile.getWithEle(triangleXZ.v1);\n//\t\t\tVectorXYZ v2 = eleProfile.getWithEle(triangleXZ.v2);\n//\t\t\tVectorXYZ v3 = eleProfile.getWithEle(triangleXZ.v3);\n//\t\t\tif (triangleXZ.isClockwise()) {\n//\t\t\t\ttrianglesXYZ.add(new TriangleXYZ(v3, v2, v1));\n//\t\t\t} else  {\n//\t\t\t\ttrianglesXYZ.add(new TriangleXYZ(v1, v2, v3));\n//\t\t\t}\n//\t\t}\n//\n//\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/data/EleConnector.java",
    "content": "package org.osm2world.core.map_elevation.data;\n\nimport static org.osm2world.core.map_elevation.data.GroundState.ON;\n\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_elevation.creation.ElevationCalculator;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * a point referenced by one or more {@link WorldObject}s, with known xz coords\n * and an elevation to be assigned by an {@link ElevationCalculator}.\n * \n * This class is a core concept of elevation calculation:\n * Because there is no injective mapping from xz coords to elevation (that\n * would not allow for bridges etc.), we need to represent whether points\n * are supposed to be \"the same\" with regards to elevation in some other manner.\n * Moreover, not all such points correspond to {@link MapNode}s.\n * Thus, this purpose is served by EleConnectors.\n */\npublic class EleConnector {\n\t\n\tpublic final VectorXZ pos;\n\t\t\n\t/** TODO document - MapNode or Intersection object, for example */\n\tpublic final Object reference;\n\t\n\t/**\n\t * indicates whether this connector should be connected to the terrain,\n\t * or is instead above or below the terrain\n\t */\n\tpublic final GroundState groundState;\n\t\n\tprivate VectorXYZ posXYZ;\n\t\n\t/**\n\t * creates an EleConnector at the given xz coordinates.\n\t * @param pos          final value for {@link #pos}; != null\n\t * @param reference    final value for {@link #reference}; may be null\n\t * @param groundState  final value for {@link #groundState}\n\t */\n\tpublic EleConnector(VectorXZ pos, Object reference, GroundState groundState) {\n\t\tassert pos != null;\n\t\tthis.pos = pos;\n\t\tthis.reference = reference;\n\t\tthis.groundState = groundState;\n\t}\n\t\n\t/**\n\t * assigns the elevation that has been calculated for this connector.\n\t * Only for use by an {@link ElevationCalculator}.\n\t * \n\t * TODO make package-visible\n\t */\n\tpublic void setPosXYZ(VectorXYZ posXYZ) {\n\t\t\t\t\n\t\tassert posXYZ.xz().equals(this.pos);\n\t\t\n\t\tthis.posXYZ = posXYZ;\n\t\t\n\t}\n\t\n\t/**\n\t * returns the 3d position after it has been calculated.\n\t * \n\t * The elevation, and therefore this {@link VectorXYZ}, is the only\n\t * property which changes (exactly once) over the lifetime of an\n\t * {@link EleConnector}: It is null before elevation calculation,\n\t * and assigned its ultimate value afterwards.\n\t */\n\tpublic VectorXYZ getPosXYZ() {\n\t\treturn posXYZ;\n\t}\n\t\n\t/**\n\t * returns true if this connector is to be joined with the other one.\n\t * It is possible that connectors are joined even if this method returns\n\t * false - that can happen when they are both joined to a third connector.\n\t */\n\tpublic boolean connectsTo(EleConnector other) {\n\t\treturn pos.equals(other.pos)\n\t\t\t\t&& ((reference != null && reference == other.reference)\n\t\t\t\t\t|| (groundState == ON && other.groundState == ON));\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn String.format(\"(%s, %s, %s)\", pos, reference, groundState);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/data/EleConnectorGroup.java",
    "content": "package org.osm2world.core.map_elevation.data;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Set;\n\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n\n/**\n * TODO document\n * \n * This only exists to make it easier for WorldObjects to manage a large\n * number of EleConnectors.\n */\npublic class EleConnectorGroup implements Iterable<EleConnector> {\n\n\t//TODO make private\n\tpublic final List<EleConnector> eleConnectors;\n\t\n\tpublic EleConnectorGroup() {\n\t\tthis(new ArrayList<EleConnector>());\n\t}\n\t\n\tprivate EleConnectorGroup(List<EleConnector> eleConnectors) {\n\t\tthis.eleConnectors = eleConnectors;\n\t}\n\t\n\tpublic void addConnectorsFor(Iterable<VectorXZ> positions,\n\t\t\tObject reference, GroundState groundState) {\n\t\t\n\t\tfor (VectorXZ pos : positions) {\n\t\t\teleConnectors.add(new EleConnector(pos, reference, groundState));\n\t\t}\n\t\t\n\t}\n\t\n\tpublic void addConnectorsFor(PolygonWithHolesXZ polygon,\n\t\t\tObject reference, GroundState groundState) {\n\t\t\n\t\taddConnectorsFor(polygon.getOuter().getVertices(), reference, groundState);\n\t\t\n\t\tfor (SimplePolygonXZ hole : polygon.getHoles()) {\n\t\t\taddConnectorsFor(hole.getVertices(), reference, groundState);\n\t\t}\n\t\t\n\t}\n\n\tpublic void addConnectorsForTriangulation(Iterable<TriangleXZ> triangles,\n\t\t\tObject reference, GroundState groundState) {\n\t\t//TODO check later whether this method is still necessary\n\t\t\n\t\tSet<VectorXZ> positions = new HashSet<VectorXZ>();\n\t\t\n\t\tfor (TriangleXZ t : triangles) {\n\t\t\tpositions.add(t.v1);\n\t\t\tpositions.add(t.v2);\n\t\t\tpositions.add(t.v3);\n\t\t}\n\t\t\n\t\taddConnectorsFor(positions, null, groundState);\n\t\t\n\t}\n\n\tpublic void add(EleConnector newConnector) {\n\t\t\n\t\teleConnectors.add(newConnector);\n\t\t\t\t\n\t}\n\t\n\tpublic void addAll(Iterable<EleConnector> newConnectors) {\n\t\t\n\t\tfor (EleConnector c : newConnectors) {\n\t\t\teleConnectors.add(c);\n\t\t}\n\t\t\n\t}\n\t\n\tpublic EleConnector getConnector(VectorXZ pos) {\n\t\t//TODO review this method (parameters sufficient? necessary at all?)\n\t\t\n\t\tfor (EleConnector eleConnector : eleConnectors) {\n\t\t\tif (eleConnector.pos.equals(pos)) {\n\t\t\t\treturn eleConnector;\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn null;\n\t\t//TODO maybe ... throw new IllegalArgumentException();\n\t\t\n\t}\n\t\n\tpublic List<EleConnector> getConnectors(Iterable<VectorXZ> positions) {\n\t\t\n\t\tList<EleConnector> connectors = new ArrayList<EleConnector>();\n\t\t\n\t\tfor (VectorXZ pos : positions) {\n\t\t\tEleConnector connector = getConnector(pos);\n\t\t\tconnectors.add(connector);\n\t\t\tif (connector == null) {\n\t\t\t\tthrow new IllegalArgumentException();\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn connectors;\n\t\t\n\t}\n\n\tpublic VectorXYZ getPosXYZ(VectorXZ pos) {\n\t\t\n\t\tEleConnector c = getConnector(pos);\n\t\t\n\t\tif (c != null) {\n\t\t\t\n\t\t\treturn c.getPosXYZ();\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\treturn pos.xyz(0);\n\t\t\t//TODO maybe ... throw new IllegalArgumentException();\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tpublic List<VectorXYZ> getPosXYZ(Collection<VectorXZ> positions) {\n\t\t\n\t\tList<VectorXYZ> result = new ArrayList<VectorXYZ>(positions.size());\n\t\t\n\t\tfor (VectorXZ pos : positions) {\n\t\t\tresult.add(getPosXYZ(pos));\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\n\tpublic PolygonXYZ getPosXYZ(SimplePolygonXZ polygon) {\n\t\treturn new PolygonXYZ(getPosXYZ(polygon.getVertexLoop()));\n\t}\n\t\n\tpublic Collection<TriangleXYZ> getTriangulationXYZ(\n\t\t\tCollection<? extends TriangleXZ> trianglesXZ) {\n\t\t\n\t\tCollection<TriangleXYZ> trianglesXYZ =\n\t\t\t\tnew ArrayList<TriangleXYZ>(trianglesXZ.size());\n\t\t\n\t\tfor (TriangleXZ triangleXZ : trianglesXZ) {\n\t\t\t\n\t\t\tVectorXYZ v1 = getPosXYZ(triangleXZ.v1);\n\t\t\tVectorXYZ v2 = getPosXYZ(triangleXZ.v2);\n\t\t\tVectorXYZ v3 = getPosXYZ(triangleXZ.v3);\n\t\t\t\n\t\t\tif (triangleXZ.isClockwise()) { //TODO: ccw test should not be in here, but maybe in triangulation util\n\t\t\t\ttrianglesXYZ.add(new TriangleXYZ(v3, v2, v1));\n\t\t\t} else  {\n\t\t\t\ttrianglesXYZ.add(new TriangleXYZ(v1, v2, v3));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn trianglesXYZ;\n\t\t\n\t}\n\t\n\t@Override\n\tpublic Iterator<EleConnector> iterator() {\n\t\treturn eleConnectors.iterator();\n\t}\n\t\n\tpublic static final EleConnectorGroup EMPTY = new EleConnectorGroup(\n\t\t\tCollections.<EleConnector>emptyList());\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/data/ElevationProfile.java",
    "content": "package org.osm2world.core.map_elevation.data;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * abstract superclass for all {@link MapElement}s' elevation profiles\n */\nabstract public class ElevationProfile {\n\n\tpublic abstract double getEleAt(VectorXZ pos);\n\t\n\tpublic VectorXYZ getWithEle(VectorXZ pos) {\n\t\treturn pos.xyz(getEleAt(pos));\n\t}\n\n\tpublic List<VectorXYZ> getWithEle(List<VectorXZ> posList) {\n\t\tArrayList<VectorXYZ> result = new ArrayList<VectorXYZ>(posList.size());\n\t\tfor (VectorXZ pos : posList) {\n\t\t\tresult.add(getWithEle(pos));\n\t\t}\n\t\treturn result;\n\t}\n\n\tpublic abstract Collection<VectorXYZ> getPointsWithEle();\n\t\n\tpublic abstract double getMinEle();\n\tpublic abstract double getMaxEle();\n\t\n\tabstract protected MapElement getElement();\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/data/GroundState.java",
    "content": "package org.osm2world.core.map_elevation.data;\n\npublic enum GroundState {\n\t\n\tON, ABOVE, BELOW;\n\t\n\tpublic boolean isHigherThan(GroundState other) {\n\t\treturn this == ABOVE && other != ABOVE\n\t\t\t|| this == ON && other == BELOW;\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/data/LPVariablePair.java",
    "content": "package org.osm2world.core.map_elevation.data;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.math.VectorXYZ;\n\n/**\n * a pair of variables for a linear program,\n * representing the elevation of a set of {@link EleConnector}s.\n */\npublic class LPVariablePair {\n\t\n\tprivate final List<EleConnector> connectors = new ArrayList<EleConnector>(2);\n\t\n\t/**\n\t * creates a variable pair for a first {@link EleConnector}.\n\t * Others may be added later.\n\t */\n\tpublic LPVariablePair(EleConnector firstMember) {\n\t\tconnectors.add(firstMember);\n\t}\n\t\n\t/**\n\t * returns all connectors currently in this set.\n\t * Must return the same object over the lifetime of this\n\t * {@link LPVariablePair} instance.\n\t */\n\tpublic List<EleConnector> getConnectors() {\n\t\treturn connectors;\n\t}\n\t\n\t/**\n\t * adds a connector to the set of {@link EleConnector}s whose elevation\n\t * is represented by this {@link LPVariablePair}\n\t */\n\tpublic void add(EleConnector c) {\n\t\tif (!connectors.contains(c)) {\n\t\t\tconnectors.add(c);\n\t\t}\n\t}\n\t\n\t/**\n\t * adds all connectors from another instance to this one\n\t */\n\tpublic void addAll(LPVariablePair other) {\n\t\tconnectors.addAll(other.getConnectors());\n\t}\n\t\n\t/**\n\t * {@link EleConnector#setPosXYZ(VectorXYZ)} for all connectors in this set\n\t */\n\tpublic void setPosXYZ(VectorXYZ posXYZ) {\n\t\tfor (EleConnector c : connectors) {\n\t\t\tc.setPosXYZ(posXYZ);\n\t\t}\n\t}\n\t\n\t/**\n\t * {@link EleConnector#getPosXYZ()} for this set\n\t */\n\tpublic VectorXYZ getPosXYZ() {\n\t\treturn connectors.get(0).getPosXYZ();\n\t}\n\t\n\t/**\n\t * {@link EleConnector#connectsTo(EleConnector)} for this set\n\t */\n\tpublic boolean connectsTo(EleConnector other) {\n\t\t\n\t\tfor (EleConnector c : connectors) {\n\t\t\tif (c.connectsTo(other)) return true;\n\t\t}\n\t\t\n\t\treturn false;\n\t\t\n\t}\n\t\n\t/**\n\t * TODO document\n\t * @see #negVar()\n\t */\n\tpublic Object posVar() {\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * TODO document\n\t * @see #posVar()\n\t */\n\tpublic Object negVar() {\n\t\treturn this.getConnectors();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn connectors.toString();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/data/NodeElevationProfile.java",
    "content": "package org.osm2world.core.map_elevation.data;\n\nimport java.util.Arrays;\nimport java.util.Collection;\n\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n\n\n/**\n * elevation profile for a {@link MapNode}\n */\npublic class NodeElevationProfile extends ElevationProfile {\n\n\tprivate final MapNode node;\n\t\n\tprivate VectorXYZ pointWithEle;\n\n\tpublic NodeElevationProfile(MapNode node) {\n\t\tthis.node = node;\n\t}\n\t\n\t@Override\n\tprotected MapElement getElement() {\n\t\treturn node;\n\t}\n\t\t\n\tpublic VectorXYZ getPointWithEle() {\n\t\tif (pointWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t}\n\t\treturn pointWithEle;\n\t}\n\t\n\t@Override\n\tpublic Collection<VectorXYZ> getPointsWithEle() {\n\t\treturn Arrays.asList(pointWithEle);\n\t}\n\t\t\n\tpublic double getEle() {\n\t\tif (pointWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t}\n\t\treturn pointWithEle.y;\n\t}\n\t\n\t@Override\n\tpublic double getEleAt(VectorXZ pos) {\n\t\treturn getEle();\n\t}\n\t\n\t@Override\n\tpublic VectorXYZ getWithEle(VectorXZ pos) {\n\t\tif (pointWithEle.x == pos.x && pointWithEle.z == pos.z) {\n\t\t\treturn pointWithEle;\n\t\t} else {\n\t\t\treturn super.getWithEle(pos);\n\t\t}\n\t}\n\n\tpublic void setEle(double ele) {\n\t\tpointWithEle = node.getPos().xyz(ele);\n\t}\n\t\n\t@Override\n\tpublic double getMaxEle() {\n\t\tif (pointWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t}\n\t\treturn pointWithEle.y;\n\t}\n\t\n\t@Override\n\tpublic double getMinEle() {\n\t\tif (pointWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t}\n\t\treturn pointWithEle.y;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/data/WaySegmentElevationProfile.java",
    "content": "package org.osm2world.core.map_elevation.data;\n\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.creation.ElevationCalculator;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * elevation profile for a {@link MapWaySegment}\n */\npublic class WaySegmentElevationProfile extends ElevationProfile {\n\n\tprivate final MapWaySegment line;\n\n\tprivate List<VectorXYZ> pointsWithEle = null;\n\n\tpublic WaySegmentElevationProfile(MapWaySegment line) {\n\t\tthis.line = line;\n\t}\n\n\t@Override\n\tprotected MapElement getElement() {\n\t\treturn line;\n\t}\n\t\n\t//TODO: is this needed?\n\t/** sort pointsWithEle by ascending (squared) distance from startPos */\n\tprivate void sortPointsWithEle() {\n\t\tCollections.sort(pointsWithEle, new Comparator<VectorXYZ>() {\n\t\t\tfinal VectorXZ startPos = line.getPrimaryRepresentation().getStartPosition();\n\t\t\t@Override\n\t\t\tpublic int compare(VectorXYZ v1, VectorXYZ v2) {\n\t\t\t\treturn Double.compare(v1.xz().subtract(startPos)\n\t\t\t\t\t\t.lengthSquared(), v2.xz().subtract(startPos)\n\t\t\t\t\t\t.lengthSquared());\n\t\t\t}\n\t\t});\n\t}\n\t\n\t/*\n\t * methods providing access to the results\n\t */\n\n\t/**\n\t * returns all points along the line where elevation values exist. This will\n\t * at least include the starting and end point of the line. Other elements\n\t * in between are ordered along the line. Must not be used before calculation\n\t * results have been set using {@link #addPointWithEle(VectorXYZ)}\n\t */\n\t@Override\n\tpublic List<VectorXYZ> getPointsWithEle() {\n\t\t\n\t\tif (pointsWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t} else if (pointsWithEle.size() < 2) {\n\t\t\tthrow new IllegalStateException(\"a line must have at least two points with elevation\");\n\t\t}\n\t\t\n\t\treturn pointsWithEle;\n\t}\n\n\t@Override\n\tpublic double getEleAt(VectorXZ pos) {\n\n\t\tif (pointsWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t} else if (pointsWithEle.size() < 2) {\n\t\t\tthrow new IllegalStateException(\"a line must have at least two points with elevation\");\n\t\t}\n\t\t\n\t\t//TODO: start pos isn't identical with first pointWithEle! can this cause problems?\n\t\t\n\t\tVectorXZ startPos = line.getPrimaryRepresentation().getStartPosition();\n\t\t\n\t\tfinal double posDistance = pos.subtract(startPos).length();\n\n\t\t// find points with known elevation directly before and after pos\n\n\t\tVectorXYZ before = null;\n\t\tVectorXYZ after = null;\n\n\t\tdouble beforeDistance = 0;\n\t\tdouble afterDistance = 0;\n\n\t\tfor (VectorXYZ v : pointsWithEle) {\n\t\t\tdouble vFraction = v.xz().subtract(startPos).length();\n\t\t\tif (vFraction < posDistance) {\n\t\t\t\tbefore = v;\n\t\t\t\tbeforeDistance = vFraction;\n\t\t\t} else if (vFraction == posDistance) {\n\t\t\t\treturn v.y;\n\t\t\t} else {\n\t\t\t\tafter = v;\n\t\t\t\tafterDistance = vFraction;\n\t\t\t\tbreak; // pointsWithEle are ordered\n\t\t\t}\n\t\t}\n\n\t\t// handle pos outside [startPos; endPos]\n\n\t\tif (before == null) {\n\t\t\treturn after.y;\n\t\t} else if (after == null) {\n\t\t\treturn before.y;\n\t\t}\n\n\t\t// interpolate between points before and after\n\t\t\n\t\tdouble influenceOfAfter = (posDistance - beforeDistance)\n\t\t\t\t/ (afterDistance - beforeDistance);\n\n\t\tdouble ele = before.y * (1-influenceOfAfter) + after.y * influenceOfAfter;\n\n\t\treturn ele;\n\n\t}\n\t\n\t/**\n\t * adds a result of {@link ElevationCalculator}.\n\t * Must be called at least twice (start and end node)\n\t */\n\tpublic void addPointWithEle(VectorXYZ pointWithEle) {\n\t\t\n\t\tif (pointsWithEle == null) {\n\t\t\tpointsWithEle = new ArrayList<VectorXYZ>();\n\t\t}\n\t\t\n\t\tthis.pointsWithEle.add(pointWithEle);\n\t\t\n\t\tsortPointsWithEle(); //TODO: (performance) don't do this every time; insert in the right place instead\n\t}\n\n\t@Override\n\tpublic double getMaxEle() {\n\t\tif (pointsWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t}\n\t\tdouble maxEle = Double.MIN_VALUE;\n\t\tfor (VectorXYZ pointWithEle : pointsWithEle) {\n\t\t\tmaxEle = Math.max(maxEle, pointWithEle.y);\n\t\t}\n\t\treturn maxEle;\n\t}\n\n\t@Override\n\tpublic double getMinEle() {\n\t\tif (pointsWithEle == null) {\n\t\t\tthrow new IllegalStateException(\"elevations have not been calculated yet\");\n\t\t}\n\t\tdouble minEle = Double.MAX_VALUE;\n\t\tfor (VectorXYZ pointWithEle : pointsWithEle) {\n\t\t\tminEle = Math.min(minEle, pointWithEle.y);\n\t\t}\n\t\treturn minEle;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/map_elevation/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nelevation information for map data, calculated during the conversion process\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/AxisAlignedBoundingBoxXYZ.java",
    "content": "package org.osm2world.core.math;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\n\nimport java.util.Arrays;\nimport java.util.Collection;\n\n/**\n * immutable representation of an axis-aligned bounding box\n * with x, y and z dimensions\n */\npublic class AxisAlignedBoundingBoxXYZ implements Cloneable {\n\n\tpublic final double minX, minY, minZ, maxX, maxY, maxZ;\n\n\tpublic AxisAlignedBoundingBoxXYZ(double minX, double minY, double minZ,\n\t\t\tdouble maxX, double maxY, double maxZ) {\n\t\tthis.minX = minX;\n\t\tthis.minY = minY;\n\t\tthis.minZ = minZ;\n\t\tthis.maxX = maxX;\n\t\tthis.maxY = maxY;\n\t\tthis.maxZ = maxZ;\n\t}\n\t\n\t/**\n\t * @param boundedPoints  must contain at least one point\n\t */\n\tpublic AxisAlignedBoundingBoxXYZ(Collection<? extends Vector3D> boundedPoints) {\n\t\t\n\t\tassert (!boundedPoints.isEmpty());\n\t\t\n\t\tdouble minX = Double.POSITIVE_INFINITY, minY = Double.POSITIVE_INFINITY, minZ = Double.POSITIVE_INFINITY;\n\t\tdouble maxX = Double.NEGATIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY, maxZ = Double.NEGATIVE_INFINITY;\n\t\t\n\t\tfor (Vector3D p : boundedPoints) {\n\t\t\tminX = min(minX, p.getX()); minY = min(minY, p.getY()); minZ = min(minZ, p.getZ());\n\t\t\tmaxX = max(maxX, p.getX()); maxY = max(maxY, p.getY()); maxZ = max(maxZ, p.getZ());\n\t\t}\n\t\t\n\t\tthis.minX = minX; this.minY = minY; this.minZ = minZ;\n\t\tthis.maxX = maxX; this.maxY = maxY; this.maxZ = maxZ;\n\t\t\n\t}\n\t\n\tpublic double sizeX() { return maxX - minX; }\n\tpublic double sizeY() { return maxY - minY; }\n\tpublic double sizeZ() { return maxZ - minZ; }\n\n\tpublic VectorXYZ center() {\n\t\treturn new VectorXYZ(minX + sizeX()/2, minY + sizeY()/2, minZ + sizeZ()/2);\n\t}\n\t\n\tpublic Collection<VectorXYZ> corners() {\n\t\treturn Arrays.asList(new VectorXYZ[]{new VectorXYZ(minX, minY, minZ),\n\t\t\t\tnew VectorXYZ(minX, minY, maxZ),\n\t\t\t\tnew VectorXYZ(minX, maxY, minZ),\n\t\t\t\tnew VectorXYZ(minX, maxY, maxZ),\n\t\t\t\tnew VectorXYZ(maxX, minY, minZ),\n\t\t\t\tnew VectorXYZ(maxX, minY, maxZ),\n\t\t\t\tnew VectorXYZ(maxX, maxY, minZ),\n\t\t\t\tnew VectorXYZ(maxX, maxY, maxZ)});\n\t}\n\t\n\t/**\n\t * returns a bounding box that is a bit larger than this one\n\t */\n\tpublic AxisAlignedBoundingBoxXYZ pad(double paddingSize) {\n\t\treturn new AxisAlignedBoundingBoxXYZ(\n\t\t\t\tminX - paddingSize,\n\t\t\t\tminY - paddingSize,\n\t\t\t\tminZ - paddingSize,\n\t\t\t\tmaxX + paddingSize,\n\t\t\t\tmaxY + paddingSize,\n\t\t\t\tmaxZ + paddingSize);\n\t}\n\n\tpublic boolean overlaps(AxisAlignedBoundingBoxXYZ otherBox) {\n\t\treturn !(maxX <= otherBox.minX\n\t\t\t\t|| minX >= otherBox.maxX\n\t\t\t\t|| maxY <= otherBox.minY\n\t\t\t\t|| minY >= otherBox.maxY\n\t\t\t\t|| maxZ <= otherBox.minZ\n\t\t\t\t|| minZ >= otherBox.maxZ);\n\t}\n\n\tpublic boolean contains(AxisAlignedBoundingBoxXYZ otherBox) {\n\t\treturn minX <= otherBox.minX\n\t\t\t\t&& minY <= otherBox.minY\n\t\t\t\t&& minZ <= otherBox.minZ\n\t\t\t\t&& maxX >= otherBox.maxX\n\t\t\t\t&& maxY >= otherBox.maxY\n\t\t\t\t&& maxZ >= otherBox.maxZ;\n\t}\n\n\tpublic boolean contains(VectorXYZ v) {\n\t\treturn v.x >= minX && v.x <= maxX && v.y >= minY && v.y <= maxY && v.z >= minZ && v.z <= maxZ;\n\t}\n\t\n\tpublic static final AxisAlignedBoundingBoxXYZ union(\n\t\t\tAxisAlignedBoundingBoxXYZ box1, AxisAlignedBoundingBoxXYZ box2) {\n\t\t\n\t\treturn new AxisAlignedBoundingBoxXYZ(\n\t\t\t\tMath.min(box1.minX, box2.minX),\n\t\t\t\tMath.min(box1.minY, box2.minY),\n\t\t\t\tMath.min(box1.minZ, box2.minZ),\n\t\t\t\tMath.max(box1.maxX, box2.maxX),\n\t\t\t\tMath.max(box1.maxY, box2.maxY),\n\t\t\t\tMath.max(box1.maxZ, box2.maxZ));\n\t\t\n\t}\n\t\n\tpublic static final AxisAlignedBoundingBoxXYZ intersect(\n\t\t\tAxisAlignedBoundingBoxXYZ box1, AxisAlignedBoundingBoxXYZ box2) {\n\t\t\n\t\treturn new AxisAlignedBoundingBoxXYZ(\n\t\t\t\tMath.max(box1.minX, box2.minX),\n\t\t\t\tMath.max(box1.minY, box2.minY),\n\t\t\t\tMath.max(box1.minZ, box2.minZ),\n\t\t\t\tMath.min(box1.maxX, box2.maxX),\n\t\t\t\tMath.min(box1.maxY, box2.maxY),\n\t\t\t\tMath.min(box1.maxZ, box2.maxZ));\n\t\t\n\t}\n\t\n\t@Override\n\tpublic AxisAlignedBoundingBoxXYZ clone() {\n\t\ttry {\n\t\t\treturn (AxisAlignedBoundingBoxXYZ) super.clone();\n\t\t} catch (CloneNotSupportedException e) {\n\t\t\tthrow new AssertionError(\"unexpected super.clone behavior\");\n\t\t}\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/AxisAlignedBoundingBoxXZ.java",
    "content": "package org.osm2world.core.math;\n\nimport static java.lang.Math.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\n\n/**\n * immutable representation of an axis-aligned bounding box\n * with x and z dimensions\n */\npublic class AxisAlignedBoundingBoxXZ implements Cloneable {\n\n\tpublic final double minX, minZ, maxX, maxZ;\n\n\tpublic AxisAlignedBoundingBoxXZ(double minX, double minZ,\n\t\t\tdouble maxX, double maxZ) {\n\t\tthis.minX = minX;\n\t\tthis.minZ = minZ;\n\t\tthis.maxX = maxX;\n\t\tthis.maxZ = maxZ;\n\t}\n\t\n\t/**\n\t * @param boundedPoints  must contain at least one point\n\t */\n\tpublic AxisAlignedBoundingBoxXZ(Collection<? extends Vector3D> boundedPoints) {\n\t\t\n\t\tassert (!boundedPoints.isEmpty());\n\t\t\n\t\tdouble minX = Double.POSITIVE_INFINITY, minZ = Double.POSITIVE_INFINITY;\n\t\tdouble maxX = Double.NEGATIVE_INFINITY, maxZ = Double.NEGATIVE_INFINITY;\n\t\t\n\t\tfor (Vector3D p : boundedPoints) {\n\t\t\tminX = min(minX, p.getX()); minZ = min(minZ, p.getZ());\n\t\t\tmaxX = max(maxX, p.getX()); maxZ = max(maxZ, p.getZ());\n\t\t}\n\t\t\n\t\tthis.minX = minX; this.minZ = minZ;\n\t\tthis.maxX = maxX; this.maxZ = maxZ;\n\t\t\n\t}\n\t\n\tpublic double sizeX() { return maxX - minX; }\n\tpublic double sizeZ() { return maxZ - minZ; }\n\n\tpublic VectorXZ center() {\n\t\treturn new VectorXZ(minX + sizeX()/2, minZ + sizeZ()/2);\n\t}\n\t\n\tprivate SimplePolygonXZ polygonXZ;\n\t\n\tpublic SimplePolygonXZ polygonXZ() {\n\t\t\n\t\tif (polygonXZ == null) {\n\t\t\tList<VectorXZ> vertexLoop = new ArrayList<VectorXZ>(5);\n\t\t\tvertexLoop.add(new VectorXZ(minX, minZ));\n\t\t\tvertexLoop.add(new VectorXZ(maxX, minZ));\n\t\t\tvertexLoop.add(new VectorXZ(maxX, maxZ));\n\t\t\tvertexLoop.add(new VectorXZ(minX, maxZ));\n\t\t\tvertexLoop.add(vertexLoop.get(0));\n\t\t\tpolygonXZ = new SimplePolygonXZ(vertexLoop);\n\t\t}\n\t\t\n\t\treturn polygonXZ;\n\t\t\n\t}\n\t\n\tpublic VectorXZ bottomLeft() {\n\t\treturn polygonXZ().getVertexCollection().get(0);\n\t}\n\t\n\tpublic VectorXZ bottomRight() {\n\t\treturn polygonXZ().getVertexCollection().get(1);\n\t}\n\t\n\tpublic VectorXZ topRight() {\n\t\treturn polygonXZ().getVertexCollection().get(2);\n\t}\n\t\n\tpublic VectorXZ topLeft() {\n\t\treturn polygonXZ().getVertexCollection().get(3);\n\t}\n\t\n\t/**\n\t * returns a bounding box that is a bit larger than this one\n\t */\n\tpublic AxisAlignedBoundingBoxXZ pad(double paddingSize) {\n\t\treturn new AxisAlignedBoundingBoxXZ(\n\t\t\t\tminX - paddingSize,\n\t\t\t\tminZ - paddingSize,\n\t\t\t\tmaxX + paddingSize,\n\t\t\t\tmaxZ + paddingSize);\n\t}\n\n\tpublic boolean overlaps(AxisAlignedBoundingBoxXZ otherBox) {\n\t\treturn !(maxX <= otherBox.minX\n\t\t\t\t|| minX >= otherBox.maxX\n\t\t\t\t|| maxZ <= otherBox.minZ\n\t\t\t\t|| minZ >= otherBox.maxZ);\n\t}\n\n\tpublic boolean contains(AxisAlignedBoundingBoxXZ otherBox) {\n\t\treturn minX <= otherBox.minX\n\t\t\t\t&& minZ <= otherBox.minZ\n\t\t\t\t&& maxX >= otherBox.maxX\n\t\t\t\t&& maxZ >= otherBox.maxZ;\n\t}\n\n\tpublic boolean contains(IntersectionTestObject object) {\n\t\treturn polygonXZ().contains(\n\t\t\t\tobject.getAxisAlignedBoundingBoxXZ().polygonXZ());\n\t}\n\n\tpublic boolean contains(VectorXZ v) {\n\t\treturn v.x >= minX && v.x <= maxX && v.z >= minZ && v.z <= maxZ;\n\t}\n\t\n\tpublic static final AxisAlignedBoundingBoxXZ union(\n\t\t\tAxisAlignedBoundingBoxXZ box1, AxisAlignedBoundingBoxXZ box2) {\n\t\t\n\t\treturn new AxisAlignedBoundingBoxXZ(\n\t\t\t\tMath.min(box1.minX, box2.minX),\n\t\t\t\tMath.min(box1.minZ, box2.minZ),\n\t\t\t\tMath.max(box1.maxX, box2.maxX),\n\t\t\t\tMath.max(box1.maxZ, box2.maxZ));\n\t\t\n\t}\n\t\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ clone() {\n\t\ttry {\n\t\t\treturn (AxisAlignedBoundingBoxXZ) super.clone();\n\t\t} catch (CloneNotSupportedException e) {\n\t\t\tthrow new AssertionError(\"unexpected super.clone behavior\");\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/GeometryUtil.java",
    "content": "package org.osm2world.core.math;\n\nimport static java.lang.Math.sqrt;\nimport static org.osm2world.core.math.VectorXZ.*;\n\nimport java.awt.geom.Line2D;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Random;\n\nimport com.vividsolutions.jts.geom.Coordinate;\nimport com.vividsolutions.jts.geom.LineSegment;\n\n/**\n * utility class for some useful calculations\n */\npublic final class GeometryUtil {\n\t\n\t/** prevents instantiation */\n\tprivate GeometryUtil() { }\n\t\n\tpublic static final List<TriangleXYZ> trianglesFromVertexList(\n\t\t\tList<? extends VectorXYZ> vs) {\n\t\t\n\t\tif (vs.size() % 3 != 0) {\n\t\t\tthrow new IllegalArgumentException(\"vertex size must be multiple of 3\");\n\t\t}\n\t\t\n\t\tList<TriangleXYZ> triangles = new ArrayList<TriangleXYZ>(vs.size() / 3);\n\t\t\n\t\tfor (int triangle = 0; triangle  < vs.size() / 3; triangle++) {\n\t\t\t\n\t\t\ttriangles.add(new TriangleXYZ(\n\t\t\t\t\tvs.get(triangle * 3),\n\t\t\t\t\tvs.get(triangle * 3 + 1),\n\t\t\t\t\tvs.get(triangle * 3 + 2)));\n\n\t\t}\n\n\t\treturn triangles;\n\t\t\n\t}\n\t\n\tpublic static final List<TriangleXYZ> trianglesFromTriangleStrip(\n\t\t\tList<? extends VectorXYZ> vs) {\n\t\t\n\t\treturn trianglesFromVertexList(\n\t\t\t\ttriangleVertexListFromTriangleStrip(vs));\n\t\t\n\t}\n\t\n\tpublic static final <V> List<V> triangleVertexListFromTriangleStrip(\n\t\t\tList<? extends V> vs) {\n\t\t\n\t\tList<V> result = new ArrayList<V>((vs.size() - 2) * 3);\n\t\t\n\t\tfor (int triangle = 0; triangle + 2 < vs.size(); triangle++) {\n\t\t\t\n\t\t\tif (triangle % 2 == 0) {\n\t\t\t\tresult.add(vs.get(triangle));\n\t\t\t\tresult.add(vs.get(triangle + 1));\n\t\t\t\tresult.add(vs.get(triangle + 2));\n\t\t\t} else {\n\t\t\t\tresult.add(vs.get(triangle));\n\t\t\t\tresult.add(vs.get(triangle + 2));\n\t\t\t\tresult.add(vs.get(triangle + 1));\n\t\t\t}\n\n\t\t}\n\n\t\treturn result;\n\t\t\n\t}\n\t\n\tpublic static final <V> List<V> triangleNormalListFromTriangleStripOrFan(\n\t\t\tList<? extends V> normals) {\n\t\t\n\t\tList<V> result = new ArrayList<V>((normals.size() - 2) * 3);\n\t\t\n\t\tfor (int triangle = 0; triangle + 2 < normals.size(); triangle++) {\n\t\t\tV normal = normals.get(triangle + 2);\n\t\t\tresult.add(normal);\n\t\t\tresult.add(normal);\n\t\t\tresult.add(normal);\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\n\tpublic static final List<TriangleXYZ> trianglesFromTriangleFan(\n\t\t\tList<? extends VectorXYZ> vs) {\n\n\t\treturn trianglesFromVertexList(\n\t\t\t\ttriangleVertexListFromTriangleFan(vs));\n\t\t\n\t}\n\n\tpublic static final <V> List<V> triangleVertexListFromTriangleFan(\n\t\t\tList<? extends V> vs) {\n\t\t\n\t\tList<V> result = new ArrayList<V>((vs.size() - 2) * 3);\n\n\t\tV center = vs.get(0);\n\t\t\n\t\tfor (int triangle = 0; triangle + 2 < vs.size(); triangle++) {\n\n\t\t\tresult.add(center);\n\t\t\tresult.add(vs.get(triangle + 1));\n\t\t\tresult.add(vs.get(triangle + 2));\n\n\t\t}\n\n\t\treturn result;\n\t\t\n\t}\n\t\n\t/**\n\t * returns the position vector where two lines intersect.\n\t * The lines are given by a point on the line and a direction vector each.\n\t * Result is null if the lines are parallel or have more than one common point.\n\t */\n\tpublic static final VectorXZ getLineIntersection(\n\t\t\tVectorXZ pointA, VectorXZ directionA,\n\t\t\tVectorXZ pointB, VectorXZ directionB) {\n\t\t\n\t\t//TODO (documentation) explain properly\n\t\t\n\t\t//calculate dot product of directionA and directionB;\n\t\t//if dot product is (approximately) 0, the lines are parallel\n\t\tdouble denom = directionA.z*directionB.x - directionA.x*directionB.z;\n\t\tif(approxZero(denom)) { return null; }\n\t\t\n\t\t//TODO: why?\n\t\tdenom = 1/denom;\n\n\t\t//calculate vector for connection between pointA and pointB\n\t\tdouble amcX = pointB.x-pointA.x;\n\t\tdouble amcZ = pointB.z-pointA.z;\n\n\t\t//calculate t so that intersection is at pointA+t*directionA\n\t\t//TODO: why this formula?\n\t\tdouble t = (amcZ*directionB.x - amcX*directionB.z)*denom;\n\n\t\treturn new VectorXZ(\n\t\t\t\tpointA.x + t * directionA.x,\n\t\t\t\tpointA.z + t * directionA.z);\n\t\t\t\t\n\t}\n\n\t/**\n\t * returns the position vector where two line segments intersect.\n\t * The lines are given by a point on the line and a direction vector each.\n\t * Result is null if the lines are parallel or have more than one common point.\n\t */\n\tpublic static final VectorXZ getLineSegmentIntersection(\n\t\t\tVectorXZ pointA1, VectorXZ pointA2,\n\t\t\tVectorXZ pointB1, VectorXZ pointB2) {\n\n\t\t//TODO: (performance): passing \"vector TO second point\", rather than point2, would avoid having to calc it here - and that information could be reused for all comparisons involving the segment\n\t\t\n\t\t//TODO (documentation) explain properly\n\t\t\n\t\tdouble vx = pointA2.x - pointA1.x;\n\t\tdouble vz = pointA2.z - pointA1.z;\n\t\tdouble qx = pointB2.x - pointB1.x;\n\t\tdouble qz = pointB2.z - pointB1.z;\n\t\t\n\t\t//calculate dot product;\n\t\t//if dot product is (approximately) 0, the lines are parallel\n\t\tdouble denom = vz*qx - vx*qz;\n\t\tif(approxZero(denom)) { return null; }\n\t\t\n\t\t//TODO: why?\n\t\tdenom = 1/denom;\n\n\t\t//calculate vector for connection between pointA1 and pointB1\n\t\tdouble amcx = pointB1.x-pointA1.x;\n\t\tdouble amcz = pointB1.z-pointA1.z;\n\n\t\t//calculate t so that intersection is at pointA1+t*v\n\t\t//TODO: why this formula?\n\t\tdouble t = (amcz*qx - amcx*qz)*denom;\n\t\tif( t < 0 || t > 1 ) { return null; }\n\n\t\t//calculate s so that intersection is at pointB1+t*q\n\t\tdouble s = (amcz*vx - amcx*vz)*denom;\n\t\tif( s < 0 || s > 1 ) { return null; }\n\t\t\n\t\treturn new VectorXZ(\n\t\t\t\tpointA1.x + t * vx,\n\t\t\t\tpointA1.z + t * vz);\n\t\t\n\t}\n\t\n\t/**\n\t * variant of {@link #getLineSegmentIntersection(VectorXZ, VectorXZ, VectorXZ, VectorXZ)}\n\t * that also returns null (= does not announce an intersection)\n\t * if the two segments share an end point\n\t */\n\tpublic static final VectorXZ getTrueLineSegmentIntersection(\n\t\t\tVectorXZ pointA1, VectorXZ pointA2,\n\t\t\tVectorXZ pointB1, VectorXZ pointB2) {\n\t\n\t\tif (pointA1.equals(pointB1) || pointA1.equals(pointB2)\n\t\t\t\t|| pointA2.equals(pointB1) || pointA2.equals(pointB2)) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn getLineSegmentIntersection(pointA1, pointA2, pointB1, pointB2);\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * returns true if the point p is on the right of the line though l1 and l2\n\t */\n\tpublic static final boolean isRightOf(VectorXZ p, VectorXZ l1, VectorXZ l2) {\n\t\t\n\t\treturn 0 > (p.z-l1.z) * (l2.x-l1.x) - (p.x-l1.x) * (l2.z-l1.z);\n\t\t\n\t}\n\t\n\t\n\t/**\n\t * returns true if p is \"between\" l1 and l2,\n\t * i.e. l1 to l2 is the longest side of the triangle p,l1,l2.\n\t * \n\t * If all three points are on a line, this means that\n\t * p is on the line segment between l1 and l2.\n\t */\n\tpublic static final boolean isBetween(VectorXZ p, VectorXZ l1, VectorXZ l2) {\n\t\t\n\t\tdouble distSqL1L2 = distanceSquared(l1, l2);\n\t\tdouble distSqPL1 = distanceSquared(p, l1);\n\t\tdouble distSqPL2 = distanceSquared(p, l2);\n\t\t\n\t\treturn distSqL1L2 > distSqPL1\n\t\t\t&& distSqL1L2 > distSqPL2;\n\t\t\n\t}\n\t\n\t/**\n\t * returns the closest distance between point p and a line defined by two points\n\t */\n\tpublic static final double distanceFromLine(VectorXZ p,\tVectorXZ v1, VectorXZ v2) {\n\t\treturn Line2D.ptLineDist(v1.x, v1.z, v2.x, v2.z, p.x, p.z);\n\t}\n\t\n\t/**\n\t * returns the closest distance between point p and line segment s\n\t */\n\tpublic static final double distanceFromLineSegment(VectorXZ p, LineSegmentXZ s) {\n\t\tLineSegment sJTS = new LineSegment(s.p1.x, s.p1.z, s.p2.x, s.p2.z);\n\t\treturn sJTS.distance(new Coordinate(p.x, p.z));\n\t}\n\n\t/**\n\t * returns a sequence of vectors at a distance above an original sequence\n\t * \n\t * @param sequence   original sequence\n\t * @param yDistance  distance in y direction between new and original sequence;\n\t *                   can be negative for creating a sequence below the original sequence.\n\t */\n\tpublic static final List<VectorXYZ> sequenceAbove(\n\t\t\tList<VectorXYZ> sequence, double yDistance) {\n\t\t\n\t\tList<VectorXYZ> newSequence = new ArrayList<VectorXYZ>(sequence.size());\n\t\t\n\t\tVectorXYZ undersideOffset = VectorXYZ.Y_UNIT.mult(yDistance);\n\t\tfor (VectorXYZ v : sequence) {\n\t\t\tnewSequence.add(v.add(undersideOffset));\n\t\t}\n\t\t\n\t\treturn newSequence;\n\t\t\n\t}\n\t\n\t/**\n\t * returns a point on a line segment between pos1 and pos2,\n\t * with parameterized placement between the two end nodes.\n\t * For example, a parameter of 0.5 would return the center\n\t * of the line segment; the interpolated point for a parameter\n\t * of 0.25 would be 1/4 of the distance between pos1 and pos2\n\t * away from pos1.\n\t */\n\tpublic static VectorXZ interpolateBetween(VectorXZ pos1, VectorXZ pos2, double influenceRatioPos2) {\n\t\treturn new VectorXZ(\n\t\t\t\tpos1.x * (1-influenceRatioPos2) + pos2.x * influenceRatioPos2,\n\t\t\t\tpos1.z * (1-influenceRatioPos2) + pos2.z * influenceRatioPos2);\n\t}\n\n\t/**\n\t * three-dimensional version of\n\t * {@link #interpolateBetween(VectorXZ, VectorXZ, double)}\n\t */\n\tpublic static VectorXYZ interpolateBetween(VectorXYZ pos1, VectorXYZ pos2, double influenceRatioPos2) {\n\t\treturn new VectorXYZ(\n\t\t\t\tpos1.x * (1-influenceRatioPos2) + pos2.x * influenceRatioPos2,\n\t\t\t\tpos1.y * (1-influenceRatioPos2) + pos2.y * influenceRatioPos2,\n\t\t\t\tpos1.z * (1-influenceRatioPos2) + pos2.z * influenceRatioPos2);\n\t}\n\t\n\t/**\n\t * performs linear interpolation of elevation information\n\t * for a position on a line segment\n\t */\n\tpublic static VectorXYZ interpolateElevation(VectorXZ posForEle,\n\t\t\tVectorXYZ pos1, VectorXYZ pos2) {\n\t\t\n\t\tdouble interpolatedElevation =\n\t\t\tinterpolateValue(posForEle, pos1.xz(), pos1.y, pos2.xz(), pos2.y);\n\t\t\n\t\treturn posForEle.xyz(interpolatedElevation);\n\t\t\n\t}\n\t\n\t/**\n\t * performs linear interpolation of any value for a position on a line segment\n\t */\n\tpublic static double interpolateValue(VectorXZ posForValue,\n\t\t\tVectorXZ pos1, double valueAt1, VectorXZ pos2, double valueAt2) {\n\t\t\n\t\tdouble distRatio =\n\t\t\tdistance(pos1, posForValue)\n\t\t\t/ (distance(pos1, posForValue) + distance(posForValue, pos2));\n\n\t\treturn valueAt1 * (1 - distRatio)\n\t\t\t+ valueAt2 * distRatio;\n\t\t\t\n\t}\n\t\n\t/**\n\t * distributes points along a line segment.\n\t * \n\t * This can be used for many different features, such as\n\t * steps along a way, street lights along a road or posts along a fence.\n\t * \n\t * @param preferredDistance  ideal distance between resulting points;\n\t * this method will try to keep the actual distance as close to this as possible\n\t * \n\t * @param pointsAtStartAndEnd  if true, there will be a point at lineStart\n\t * and lineEnd each; if false, the closest points will be half the usual\n\t * distance away from these\n\t */\n\tpublic static List<VectorXZ> equallyDistributePointsAlong(\n\t\t\tdouble preferredDistance, boolean pointsAtStartAndEnd,\n\t\t\tVectorXZ lineStart, VectorXZ lineEnd) {\n\t\t\n\t\tdouble lineLength = lineStart.subtract(lineEnd).length();\n\t\t\n\t\tint numSegments = (int) Math.round(lineLength / preferredDistance);\n\n\t\tif (numSegments == 0) {\n\t\t\treturn new ArrayList<VectorXZ>(0);\n\t\t}\n\t\t\t\t\t\t\n\t\tdouble pointDistance = lineLength / numSegments;\n\t\tVectorXZ pointDiff = lineEnd.subtract(lineStart).normalize().mult(pointDistance);\n\t\t\t\t\n\t\tint numPoints = pointsAtStartAndEnd ? numSegments + 1 : numSegments;\n\t\tList<VectorXZ> result = new ArrayList<VectorXZ>(numPoints);\n\t\t\n\t\t/* create the points, starting with the first and basing each on the previous */\n\t\t\n\t\tVectorXZ mostRecentPoint = pointsAtStartAndEnd ?\n\t\t\t\tlineStart :\n\t\t\t\tlineStart.add(pointDiff.mult(0.5f));\n\t\t\n\t\tresult.add(mostRecentPoint);\n\n\t\tfor (int point = 1; point < numPoints; point ++) {\n\t\t\t\n\t\t\tmostRecentPoint = mostRecentPoint.add(pointDiff);\n\t\t\tresult.add(mostRecentPoint);\n\t\t\t\t\t\t\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\n\t/**\n\t * distributes points along a line segment sequence.\n\t * \n\t * This can be used for many different features, such as\n\t * steps along a way, street lights along a road or posts along a fence.\n\t * \n\t * @param preferredDistance  ideal distance between resulting points;\n\t * this method will try to keep the actual distance as close to this as possible\n\t * \n\t * @param pointsAtStartAndEnd  if true, there will be a point at lineStart\n\t * and lineEnd each; if false, the closest points will be half the usual\n\t * distance away from these\n\t */\n\tpublic static List<VectorXZ> equallyDistributePointsAlong(\n\t\t\tdouble preferredDistance, boolean pointsAtStartAndEnd,\n\t\t\tList<VectorXZ> points) {\n\t\t\n\t\tdouble length = 0;\n\t\tfor (int i=0; i+1 < points.size(); i++) {\n\t\t\tlength += points.get(i+1).subtract(points.get(i)).length();\n\t\t}\n\t\t\n\t\tint numSegments = (int) Math.round(length / preferredDistance);\n\t\t\n\t\tif (numSegments == 0) {\n\t\t\treturn Collections.<VectorXZ>emptyList();\n\t\t}\n\t\t\n\t\tdouble pointDistance = length / numSegments;\n\t\t\t\t\n\t\tint numPoints = pointsAtStartAndEnd ? numSegments + 1 : numSegments;\n\t\tList<VectorXZ> result = new ArrayList<VectorXZ>(numPoints);\n\t\t\n\t\t/* create the points */\n\t\t\n\t\tdouble currentDistanceFromStart = pointsAtStartAndEnd ? 0 : pointDistance / 2;\n\n\t\tfor (int point = 1; point < numPoints; point ++) {\n\t\t\t\n\t\t\t//TODO: create and add point (to result) based on currentDistanceFromStart\n\t\t\t\n\t\t\tcurrentDistanceFromStart += pointDistance;\n\t\t\t\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t\t//TODO: support distributing along a line with corners etc,\n\t\t//to avoid \"restart\" at each node\n\t\t//(should be relatively easy, just calculate TOTAL line length first\n\t\t// and then proceed in a way similar to getEleAt)\n\t\t\t\t\n\t}\n\t\n\t/**\n\t * returns a polygon that is constructed from a given polygon\n\t * by inserting a point into one of the segments of the original polygon.\n\t * The segment chosen for this purpose is the one closest to the new node.\n\t * \n\t * @param polygon       original polygon, will not be modified by this method\n\t * @param point         the new point\n\t * @param snapDistance  minimum distance of new point from segment endpoints;\n\t *                      if the new point is closer, the unmodified\n\t *                      original polygon will be returned.\n\t */\n\tpublic static PolygonXZ insertIntoPolygon(PolygonXZ polygon,\n\t\t\tVectorXZ point, double snapDistance) {\n\t\t\n\t\tLineSegmentXZ segment = polygon.getClosestSegment(point);\n\t\t\n\t\tfor (int i = 0; i + 1 <= polygon.size() ; i++) {\n\t\t\t\n\t\t\tif (polygon.getVertex(i).equals(segment.p1)\n\t\t\t\t\t&& polygon.getVertexAfter(i).equals(segment.p2)){\n\t\t\t\t\n\t\t\t\tif (polygon.getVertex(i).distanceTo(point) <= snapDistance\n\t\t\t\t\t\t|| polygon.getVertexAfter(i).distanceTo(point) <= snapDistance) {\n\t\t\t\t\t\n\t\t\t\t\treturn polygon;\n\t\t\t\t\t\n\t\t\t\t} else {\n\n\t\t\t\t\tArrayList<VectorXZ> vertexLoop =\n\t\t\t\t\t\tnew ArrayList<VectorXZ>(polygon.getVertexLoop());\n\t\t\t\t\t\n\t\t\t\t\tvertexLoop.add(i + 1, point);\n\t\t\t\t\t\n\t\t\t\t\treturn new PolygonXZ(vertexLoop);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tthrow new IllegalArgumentException(\"segment \" + segment +\n\t\t\t\t\" was not found in polygon \" + polygon);\n\t\t\n\t}\n\t\n\t/**\n\t * constant used by {@link #distributePointsOn(long, PolygonWithHolesXZ,\n\t *  AxisAlignedBoundingBoxXZ, double, double)}\n\t */\n\tprivate static final int POINTS_PER_BOX = 100;\n\t\n\t/**\n\t * distributes points pseudo-randomly on a polygon area.\n\t * The distribution for a set of parameters will always be identical.\n\t * \n\t * This can be used for features such as trees in a forest.\n\t * \n\t * Distribution works by slicing the area's bounding box into smaller boxes\n\t * with POINTS_PER_BOX potential points each, the size of these boxes\n\t * depending on density. In each of the boxes, POINTS_PER_BOX pseudo-random\n\t * positions will be calculated. If a position is far enough from previous\n\t * ones and not inside a hole, the position will be contained in the result.\n\t * \n\t * @param seed                a seed for random number generation\n\t * @param polygonWithHolesXZ  polygon on which the points should be placed\n\t * @param boundary            boundary of the relevant area or null;\n\t *                            points outside of the boundary are optional.\n\t * @param density             desired number of points per unit of area\n\t * @param minimumDistance     minimum distance between resulting points\n\t *                            (not yet implemented)\n\t */\n\tpublic static List<VectorXZ> distributePointsOn(\n\t\t\tlong seed, PolygonWithHolesXZ polygonWithHolesXZ,\n\t\t\tAxisAlignedBoundingBoxXZ boundary,\n\t\t\tdouble density,\tdouble minimumDistance) {\n\t\t\n\t\tList<VectorXZ> result = new ArrayList<VectorXZ>();\n\n\t\tRandom rand = new Random(seed);\n\t\t\n\t\tAxisAlignedBoundingBoxXZ outerBox = new AxisAlignedBoundingBoxXZ(\n\t\t\t\tpolygonWithHolesXZ.getOuter().getVertices());\n\t\t\n\t\tdouble boxSize = sqrt(100 / density);\n\t\t\n\t\tfor (int boxZ = 0; boxZ <= (int)(outerBox.sizeZ() / boxSize); ++boxZ) {\n\t\t\tfor (int boxX = 0; boxX <= (int)(outerBox.sizeX() / boxSize); ++boxX) {\n\t\t\t\t\n\t\t\t\tAxisAlignedBoundingBoxXZ box = new AxisAlignedBoundingBoxXZ(\n\t\t\t\t\t\touterBox.minX + boxSize * boxX,\n\t\t\t\t\t\touterBox.minZ + boxSize * boxZ,\n\t\t\t\t\t\touterBox.minX + boxSize * (boxX + 1),\n\t\t\t\t\t\touterBox.minZ + boxSize * (boxZ + 1));\n\t\t\t\t\n\t\t\t\tif (boundary != null && !boundary.overlaps(box)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (!polygonWithHolesXZ.contains(box.polygonXZ())\n\t\t\t\t\t\t&& !polygonWithHolesXZ.intersects(box.polygonXZ())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i < POINTS_PER_BOX; ++i) {\n\t\t\t\t\t\n\t\t\t\t\tdouble x = box.minX + boxSize * rand.nextDouble();\n\t\t\t\t\tdouble z = box.minZ + boxSize * rand.nextDouble();\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ v = new VectorXZ(x, z);\n\t\t\t\t\t\n\t\t\t\t\tif (polygonWithHolesXZ.contains(v)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\t//TODO: check minimumDistance\n\t\t\t\t\t\t\n\t\t\t\t\t\tresult.add(v);\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\t\n\tprivate static final double EPSILON = 0.0001f;\n\t\n\tprivate static final boolean approxZero(double f) {\n\t\treturn f <= EPSILON && f >= -EPSILON;\n\t}\n\t\n\t/**\n\t * Calculate consistent tangent vectors for given vertices and vertex normals and texture coordinates.\n\t * Code inspired by Lengyel, Eric. “Computing Tangent Space Basis Vectors for an Arbitrary Mesh”. Terathon Software 3D Graphics Library, 2001. http://www.terathon.com/code/tangent.html\n\t * @param vertices list of vertices for which the tangent vectors get calculated\n\t * @param normals list of normals belonging to the vertices\n\t * @param texCoords list of texture coordinates belonging to the vertices\n\t * @return list of tangent vectors that are consistent with the vertices, normals and texture coordinates\n\t */\n\tpublic static final List<VectorXYZW> calculateTangentVectorsForTexLayer(List<VectorXYZ> vertices, List<VectorXYZ> normals,\n\t\t\tList<VectorXZ> texCoords)\n\t{\n\t\tint vertexCount = vertices.size();\n\t\tVectorXYZ[] tan1 = new VectorXYZ[vertexCount];\n\t\tVectorXYZ[] tan2 = new VectorXYZ[vertexCount];\n\t\tList<VectorXYZW> tangents = new ArrayList<VectorXYZW>();\n\t    \n\t    for (int a = 0; a < vertexCount / 3; a++)\n\t    {\n\t    \tint i = a*3+1;\n\t    \tVectorXYZ v1 = vertices.get(i-1);\n\t    \tVectorXYZ v2 = vertices.get(i);\n\t    \tVectorXYZ v3 = vertices.get(i+1);\n\n\t    \tVectorXZ w1 = texCoords.get(i-1);\n\t    \tVectorXZ w2 = texCoords.get(i);\n\t    \tVectorXZ w3 = texCoords.get(i+1);\n\t        \n\t        double x1 = v2.x - v1.x;\n\t        double x2 = v3.x - v1.x;\n\t        double y1 = v2.y - v1.y;\n\t        double y2 = v3.y - v1.y;\n\t        double z1 = v2.z - v1.z;\n\t        double z2 = v3.z - v1.z;\n\t        \n\t        double s1 = w2.x - w1.x;\n\t        double s2 = w3.x - w1.x;\n\t        double t1 = w2.z - w1.z;\n\t        double t2 = w3.z - w1.z;\n\t        \n\t        double r = 1.0 / (s1 * t2 - s2 * t1);\n\t        VectorXYZ sdir = new VectorXYZ((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r,\n\t                (t2 * z1 - t1 * z2) * r);\n\t        VectorXYZ tdir = new VectorXYZ((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r,\n\t                (s1 * z2 - s2 * z1) * r);\n\t        \n\t        tan1[i-1] = sdir;\n\t        tan1[i] = sdir;\n\t        tan1[i+1] = sdir;\n\t        \n\t        tan2[i-1] = tdir;\n\t        tan2[i] = tdir;\n\t        tan2[i+1] = tdir;\n\t    }\n\t    \n\t    for (int a = 0; a < vertexCount; a++)\n\t    {\n\t        VectorXYZ n = normals.get(a);\n\t        VectorXYZ t = tan1[a];\n\t        // Gram-Schmidt orthogonalize\n\t        VectorXYZ res = (t.subtract(n.mult(n.dot(t)))).normalize();\n\n\t        // Calculate handedness\n\t        double w = (n.cross(t).dot(tan2[a]) < 0.0F) ? -1.0F : 1.0F;\n\t        \n\t        tangents.add(new VectorXYZW(res.x, res.y, res.z, w));\n\t    }\n\t    \n\t    return tangents;\n\t}\n\t\n\t/**\n\t * Epsilon to move the shadow volumes away in light direction. Avoids self shadowing.\n\t */\n\tprivate static final double SHADOWVOLUME_EPSILON = 0.1f;\n\t\n\t/**\n\t * Calculate per triangle shadow volumes. Only light facing triangles will create a shadow volume, so objects should probably be closed for correct results.\n\t * @param vertices list of the triangles vertices. Have to be a multiple of three.\n\t * @param lightPos position of the light source. Supports point lights with lightPos.w != 0 and directional lights with lightPos.w == 0\n\t * @return list of the shadow volume vertices in homogeneous coordinates. These will contain points at infinity (w == 0) for the shadow volume back cap\n\t */\n\tpublic static final List<VectorXYZW> calculateShadowVolumesPerTriangle(List<VectorXYZ> vertices, VectorXYZW lightPos) {\n\t\t\n\t\tList<VectorXYZW> shadowVolumes = new ArrayList<VectorXYZW>();\n\n\t\tfor (int triangle = 0; triangle < vertices.size()/3; triangle++) {\n\t\t\tVectorXYZ v1 = vertices.get(triangle*3);\n\t\t\tVectorXYZ v2 = vertices.get(triangle*3+1);\n\t\t\tVectorXYZ v3 = vertices.get(triangle*3+2);\n\t\t\t\n\t\t\tVectorXYZ normal = v1.subtract(v2).crossNormalized(v3.subtract(v2));\n\t\t\t\n\t\t\tVectorXYZ lightDir;\n\t\t\tif (lightPos.w == 0)\n\t\t\t\tlightDir = lightPos.xyz().mult(-1).normalize();\n\t\t\telse\n\t\t\t\tlightDir = v1.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t\t\t\n\t\t\t// skip parallel triangles\n\t\t\tif (approxZero(normal.dot(lightDir)))\n\t\t\t\tcontinue;\n\t\t\t\n\t\t\t// only light facing triangles (not the same direction of the light rays) throw shadows\n\t\t\tif (normal.dot(lightDir) < 0) {\n\t\t\t\t\n\t\t\t\t// revert light facing triangles\n\t\t\t\tVectorXYZ tmp = v2;\n\t\t\t\tv2 = v3;\n\t\t\t\tv3 = tmp;\n\t\t\t//}\n\t\t\t\n\t\t\t\t// calculate the volume sides\n\t\t\t\tshadowVolumes.addAll(emitQuad(v1, v2, lightPos));\n\t\t\t\tshadowVolumes.addAll(emitQuad(v2, v3, lightPos));\n\t\t\t\tshadowVolumes.addAll(emitQuad(v3, v1, lightPos));\n\n\t\t\t\t// calculate the front/back cap. move front cap a little in light direction to avoid self shadowing\n\t\t\t\tVectorXYZW c1 = new VectorXYZW( v1.add(lightDir.mult(SHADOWVOLUME_EPSILON)), 1);\n\t\t\t\tVectorXYZW b1 = new VectorXYZW( lightDir, 0);\n\n\t\t\t\tif (lightPos.w != 0)\n\t\t\t\t\tlightDir = v2.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t\t\t\tVectorXYZW c2 = new VectorXYZW( v2.add(lightDir.mult(SHADOWVOLUME_EPSILON)), 1);\n\t\t\t\tVectorXYZW b2 = new VectorXYZW( lightDir, 0);\n\n\t\t\t\tif (lightPos.w != 0)\n\t\t\t\t\tlightDir = v3.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t\t\t\tVectorXYZW c3 = new VectorXYZW( v3.add(lightDir.mult(SHADOWVOLUME_EPSILON)), 1);\n\t\t\t\tVectorXYZW b3 = new VectorXYZW( lightDir, 0);\n\n\t\t\t\tshadowVolumes.add(c1);\n\t\t\t\tshadowVolumes.add(c2);\n\t\t\t\tshadowVolumes.add(c3);\n\n\t\t\t\t// reverse back cap order\n\t\t\t\tshadowVolumes.add(b1);\n\t\t\t\tshadowVolumes.add(b3);\n\t\t\t\tshadowVolumes.add(b2);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn shadowVolumes;\n\t}\n\t\n\t/**\n\t * Creates the sides of a shadow volume for a edge of the triangle casting the shadow.\n\t * @param start the start of the triangle edge\n\t * @param end the end of the triangle edge\n\t * @param lightPos position of the shadow casting light\n\t * @return the vertices making up the side of the shadow volume for the triangle edge\n\t */\n\tprivate static List<VectorXYZW> emitQuad(VectorXYZ start, VectorXYZ end, VectorXYZW lightPos) {\n\n\t\tList<VectorXYZW> result = new ArrayList<VectorXYZW>(10);\n\t\tVectorXYZ lightDir;\n\t\t\n\t\t// Vertex #1: the starting vertex (just a tiny bit below the original edge)\n\t\tif (lightPos.w == 0)\n\t\t\tlightDir = lightPos.xyz().mult(-1).normalize();\n\t\telse\n\t\t\tlightDir = start.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t\t\n\t\tVectorXYZW v1 = new VectorXYZW(start.add(lightDir.mult(SHADOWVOLUME_EPSILON)), 1);\n\t\t\n\t    // Vertex #2: the starting vertex projected to infinity\n\t\tVectorXYZW v2 = new VectorXYZW(lightDir, 0);\n\t\t\n\t    // Vertex #3: the ending vertex (just a tiny bit below the original edge)\n\t\tif (lightPos.w != 0)\n\t\t\tlightDir = end.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t    \n\t\tVectorXYZW v3 = new VectorXYZW(end.add(lightDir.mult(SHADOWVOLUME_EPSILON)), 1);\n\n\t    // Vertex #4: the ending vertex projected to infinity\n\t\tVectorXYZW v4 = new VectorXYZW(lightDir, 0);\n\t    \n\t\tresult.add(v1);\n\t\tresult.add(v2);\n\t\tresult.add(v3);\n\t\tresult.add(v3);\n\t\tresult.add(v2);\n\t\tresult.add(v4);\n//\t\tresult.add(v1);\n//\t\tresult.add(v3);\n//\t\tresult.add(v2);\n//\t\tresult.add(v3);\n//\t\tresult.add(v4);\n//\t\tresult.add(v2);\n\t\treturn result;\n\t}\n\t\n\t/*\n\tpublic static final List<VectorXYZW> calculateShadowVolumesPerTriangle(List<VectorXYZ> vertices, VectorXYZW lightPos, FloatBuffer gWVP) {\n\t\t\n\t\tList<VectorXYZW> shadowVolumes = new ArrayList<VectorXYZW>();\n\t\tfor (int triangle = 0; triangle < vertices.size()/3; triangle++) {\n\t\t\tVectorXYZ v1 = vertices.get(triangle*3);\n\t\t\tVectorXYZ v2 = vertices.get(triangle*3+1);\n\t\t\tVectorXYZ v3 = vertices.get(triangle*3+2);\n\t\t\t\n\t\t\tVectorXYZ normal = v1.subtract(v2).crossNormalized(v3.subtract(v2));\n\t\t\t\n\t\t\tVectorXYZ lightDir;\n\t\t\tif (lightPos.w == 0)\n\t\t\t\tlightDir = lightPos.xyz().mult(-1).normalize();\n\t\t\telse\n\t\t\t\tlightDir = v1.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t\t\t\n\t\t\t// Handle only light facing triangles\n\t\t\tif (normal.dot(lightDir) > 0) {\n\t\t\t\n\t\t\t\t// calculate the volume sides\n\t\t\t\tshadowVolumes.addAll(emitQuad(v1, v2, lightPos, gWVP));\n\t\t\t\tshadowVolumes.addAll(emitQuad(v2, v3, lightPos, gWVP));\n\t\t\t\tshadowVolumes.addAll(emitQuad(v3, v1, lightPos, gWVP));\n\n\t\t\t\t// calculate the front/back cap\n\t\t\t\tVectorXYZW c1 = multMatrixVec(gWVP, v1.add(lightDir.mult(EPSILON)), 1);\n\t\t\t\tVectorXYZW b1 = multMatrixVec(gWVP, lightDir, 0);\n\n\t\t\t\tif (lightPos.w != 0)\n\t\t\t\t\tlightDir = v2.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t\t\t\tVectorXYZW c2 = multMatrixVec(gWVP, v2.add(lightDir.mult(EPSILON)), 1);\n\t\t\t\tVectorXYZW b2 = multMatrixVec(gWVP, lightDir, 0);\n\n\t\t\t\tif (lightPos.w != 0)\n\t\t\t\t\tlightDir = v3.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t\t\t\tVectorXYZW c3 = multMatrixVec(gWVP, v3.add(lightDir.mult(EPSILON)), 1);\n\t\t\t\tVectorXYZW b3 = multMatrixVec(gWVP, lightDir, 0);\n\n\t\t\t\tshadowVolumes.add(c1);\n\t\t\t\tshadowVolumes.add(c2);\n\t\t\t\tshadowVolumes.add(c3);\n\n\t\t\t\t// reverse back cap order\n\t\t\t\tshadowVolumes.add(b1);\n\t\t\t\tshadowVolumes.add(b3);\n\t\t\t\tshadowVolumes.add(b2);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn shadowVolumes;\n\t}\n\t\n\tprivate static List<VectorXYZW> emitQuad(VectorXYZ start, VectorXYZ end, VectorXYZW lightPos, FloatBuffer gWVP) {\n\n\t\tList<VectorXYZW> result = new ArrayList<VectorXYZW>(10);\n\t\tVectorXYZ lightDir;\n\t\t\n\t\t// Vertex #1: the starting vertex (just a tiny bit below the original edge)\n\t\tif (lightPos.w == 0)\n\t\t\tlightDir = lightPos.xyz().mult(-1).normalize();\n\t\telse\n\t\t\tlightDir = start.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t\t\n\t\tVectorXYZW v1 = multMatrixVec(gWVP, start.add(lightDir.mult(EPSILON)), 1);\n\t\t\n\t    // Vertex #2: the starting vertex projected to infinity\n\t\tVectorXYZW v2 = multMatrixVec(gWVP, lightDir.mult(EPSILON), 0);\n\t\t\n\t    // Vertex #3: the ending vertex (just a tiny bit below the original edge)\n\t\tif (lightPos.w == 0)\n\t\t\tlightDir = lightPos.xyz().mult(-1).normalize();\n\t\telse\n\t\t\tlightDir = end.subtract(lightPos.xyz().mult(1/lightPos.w)).normalize();\n\t    \n\t\tVectorXYZW v3 = multMatrixVec(gWVP, end.add(lightDir.mult(EPSILON)), 1);\n\n\t    // Vertex #4: the ending vertex projected to infinity\n\t\tVectorXYZW v4 = multMatrixVec(gWVP, lightDir.mult(EPSILON), 0); // TODO: correct for directional light?\n\t    \n\t\tresult.add(v1);\n\t\tresult.add(v2);\n\t\tresult.add(v3);\n\t\tresult.add(v3);\n\t\tresult.add(v2);\n\t\tresult.add(v4);\n\t\treturn result;\n\t}\n\t\n\tprivate static VectorXYZW multMatrixVec(FloatBuffer mat, VectorXYZ v, float w) {\n\t\tfloat[] result = new float[4];\n\t\tFloatUtil.multMatrixVecf(mat, new float[]{(float)v.x, (float)v.y, (float)v.z, w}, result);\n\t\treturn new VectorXYZW(result[0], result[1], result[2], result[3]);\n\t}\n\t*/\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/InvalidGeometryException.java",
    "content": "package org.osm2world.core.math;\n\n/** \n * exception that is used when attempting to construct geometry that\n * does not exhibit required properties - such as self-intersecting polygons.\n * In cases where these exceptions can be caused by problems in the data,\n * it makes sense to catch and report them.\n */\npublic class InvalidGeometryException extends RuntimeException {\n\n\tprivate static final long serialVersionUID = -7755970537446437611L; //generated serialVersionUID\n\n\tpublic InvalidGeometryException() {\n\t\tsuper();\n\t}\n\n\tpublic InvalidGeometryException(String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n\tpublic InvalidGeometryException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic InvalidGeometryException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/JTSConversionUtil.java",
    "content": "package org.osm2world.core.math;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport com.vividsolutions.jts.geom.Coordinate;\nimport com.vividsolutions.jts.geom.Geometry;\nimport com.vividsolutions.jts.geom.GeometryCollection;\nimport com.vividsolutions.jts.geom.GeometryFactory;\nimport com.vividsolutions.jts.geom.LineString;\nimport com.vividsolutions.jts.geom.LinearRing;\nimport com.vividsolutions.jts.geom.Polygon;\nimport com.vividsolutions.jts.geom.impl.CoordinateArraySequence;\n\n/**\n * converts between own and JTS geometry representations.\n * \n * When handling three-dimensional coordinates, the y and z information will\n * be swapped when converting from or to JTS. JTS uses x and y as 2D\n * plane, z as elevation info.\n */\npublic class JTSConversionUtil {\n\n\tpublic static final GeometryFactory GF = new GeometryFactory();\n\t\n\tpublic static final Coordinate vectorXZToJTSCoordinate(VectorXZ v) {\n\t\treturn new Coordinate(v.x, v.z);\n\t}\n\t\n\tpublic static final Coordinate vectorXYZToJTSCoordinate(VectorXYZ v) {\n\t\treturn new Coordinate(v.x, v.z, v.y);\n\t}\n\n\tpublic static final VectorXZ vectorXZFromJTSCoordinate(Coordinate c) {\n\t\treturn new VectorXZ(c.x, c.y);\n\t}\n\t\n\tpublic static final VectorXYZ vectorXYZFromJTSCoordinate(Coordinate c) {\n\t\treturn new VectorXYZ(c.x, c.z, c.y);\n\t}\n\n\tpublic static LineString lineSegmentXZToJTSLineString(LineSegmentXZ segment) {\n\t\t\n\t\tCoordinate[] points = {\n\t\t\t\tvectorXZToJTSCoordinate(segment.p1),\n\t\t\t\tvectorXZToJTSCoordinate(segment.p2)\n\t\t};\n\t\t\n\t\treturn new LineString(new CoordinateArraySequence(points), GF);\n\t\t\n\t}\n\t\n\tpublic static final Polygon polygonXZToJTSPolygon(SimplePolygonXZ polygon) {\n\t\n\t\tList<VectorXZ> vertices = polygon.getVertexLoop();\n\t\t\n\t\tCoordinate[] array = new Coordinate[vertices.size()];\n\t\t\n\t\tfor (int i = 0; i < array.length; i++) {\n\t\t\tVectorXZ vertex = vertices.get(i);\n\t\t\tarray[i] = vectorXZToJTSCoordinate(vertex);\n\t\t}\n\t\t\n\t\treturn new Polygon(\n\t\t\t\tnew LinearRing(new CoordinateArraySequence(array), GF),\n\t\t\t\tnull, GF);\n\t\t\n\t}\n\t\n\tpublic static final PolygonWithHolesXZ\n\t\tpolygonXZFromJTSPolygon(Polygon polygon)\n\t\tthrows InvalidGeometryException {\n\t\t\n\t\t/* create outer polygon */\n\t\t\n\t\tSimplePolygonXZ outerPolygon =\n\t\t\tpolygonXZFromLineString(polygon.getExteriorRing());\n\t\t\n\t\t/* create holes */\n\t\t\n\t\tList<SimplePolygonXZ> holes = Collections.emptyList();\n\t\t\n\t\tif (polygon.getNumInteriorRing() > 0) {\n\t\t\tholes = new ArrayList<SimplePolygonXZ>();\n\t\t\tfor (int i = 0; i < polygon.getNumInteriorRing(); i++) {\n\t\t\t\tholes.add(polygonXZFromLineString(\n\t\t\t\t\t\tpolygon.getInteriorRingN(i)));\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn new PolygonWithHolesXZ(outerPolygon, holes);\n\t\t\n\t}\n\n\tprivate static final SimplePolygonXZ polygonXZFromLineString(LineString lineString) {\n\t\t\n\t\tList<VectorXZ> vertexLoop = new ArrayList<VectorXZ>(lineString.getNumPoints());\n\t\t\n\t\tfor (Coordinate coordinate : lineString.getCoordinates()) {\n\t\t\tvertexLoop.add(vectorXZFromJTSCoordinate(coordinate));\n\t\t}\n\t\t\n\t\treturn new SimplePolygonXZ(vertexLoop);\n\t}\n\t\n\tpublic static final Collection<PolygonWithHolesXZ>\n\t\tpolygonsXZFromJTSGeometry(Geometry geometry) {\n\t\t\n\t\tCollection<PolygonWithHolesXZ> result =\n\t\t\tnew ArrayList<PolygonWithHolesXZ>(1);\n\t\t\n\t\tif (geometry instanceof Polygon) {\n\t\t\tif (geometry.getNumPoints() > 2) {\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\tresult.add(polygonXZFromJTSPolygon((Polygon)geometry));\n\t\t\t\t} catch (InvalidGeometryException e) {\n\t\t\t\t\t//don't add the broken polygon\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t} else if (geometry instanceof GeometryCollection) {\n\t\t\tGeometryCollection collection = (GeometryCollection)geometry;\n\t\t\tfor (int i = 0; i < collection.getNumGeometries(); i++) {\n\t\t\t\tresult.addAll(polygonsXZFromJTSGeometry(collection.getGeometryN(i)));\n\t\t\t}\n\t\t} else {\n\t\t\tSystem.err.println(\"unhandled geometry type: \" + geometry.getClass());\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/LineSegmentXYZ.java",
    "content": "package org.osm2world.core.math;\n\npublic class LineSegmentXYZ {\n\n\tpublic final VectorXYZ p1, p2;\n\n\tpublic LineSegmentXYZ(VectorXYZ p1, VectorXYZ p2) {\n\t\tthis.p1 = p1;\n\t\tthis.p2 = p2;\n\t}\n\n\tpublic LineSegmentXZ getSegmentXZ() {\n\t\treturn new LineSegmentXZ(p1.xz(), p2.xz());\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"[\" + p1 + \", \" + p2 + \"]\";\n\t}\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/LineSegmentXZ.java",
    "content": "package org.osm2world.core.math;\n\npublic class LineSegmentXZ {\n\n\tpublic final VectorXZ p1, p2;\n\n\tpublic LineSegmentXZ(VectorXZ p1, VectorXZ p2) {\n\t\tthis.p1 = p1;\n\t\tthis.p2 = p2;\n\t}\n\n\tpublic VectorXZ getCenter() {\n\t\treturn GeometryUtil.interpolateBetween(p1, p2, 0.5);\n\t}\n\t\n\t/**\n\t * returns true if there is an intersection between this line segment and\n\t * the line segment defined by the parameters\n\t */\n\tpublic boolean intersects(VectorXZ segmentP1, VectorXZ segmentP2) {\n\t\t// TODO: (performance): passing \"vector TO second point\", rather than\n\t\t// point2, would avoid having to calc it here - and that information\n\t\t// could be reused for all comparisons involving the segment\n\t\treturn getIntersection(segmentP1, segmentP2) != null;\n\t}\n\n\t/**\n\t * returns the intersection between this line segment and\n\t * the line segment defined by the parameters;\n\t * null if none exists.\n\t */\n\tpublic VectorXZ getIntersection(VectorXZ segmentP1, VectorXZ segmentP2) {\n\t\t// TODO: (performance): passing \"vector TO second point\", rather than\n\t\t// point2, would avoid having to calc it here - and that information\n\t\t// could be reused for all comparisons involving the segment\n\n\t\treturn GeometryUtil.getTrueLineSegmentIntersection(\n\t\t\t\tsegmentP1, segmentP2, p1, p2);\n\t}\n\n\t/**\n\t * returns the distance between this segment's two end nodes\n\t */\n\tpublic double getLength() {\n\t\treturn VectorXZ.distance(p1, p2);\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"[\" + p1 + \", \" + p2 + \"]\";\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/Poly2TriUtil.java",
    "content": "package org.osm2world.core.math;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\n\nimport org.osm2world.core.util.exception.TriangulationException;\nimport org.poly2tri.Poly2Tri;\nimport org.poly2tri.triangulation.Triangulatable;\nimport org.poly2tri.triangulation.TriangulationAlgorithm;\nimport org.poly2tri.triangulation.TriangulationContext;\nimport org.poly2tri.triangulation.TriangulationMode;\nimport org.poly2tri.triangulation.TriangulationPoint;\nimport org.poly2tri.triangulation.delaunay.DelaunayTriangle;\nimport org.poly2tri.triangulation.point.TPoint;\n/**\n * @author Hannes Janetzek\n * */\n\npublic class Poly2TriUtil {\n\tstatic class CDTSet implements Triangulatable {\n\t\tList<TriangulationPoint> points = new ArrayList<TriangulationPoint>(20);\n\t\tList<DelaunayTriangle> triangles = new ArrayList<DelaunayTriangle>(20);\n\t\tArrayList<LineSegmentXZ> segmentSet = new ArrayList<LineSegmentXZ>();\n\n\t\t// it seems poly2tri requires points to be unique objects\n\t\tHashMap<VectorXZ, TriangulationPoint> pointSet\n\t\t\t= new HashMap<VectorXZ, TriangulationPoint>();\n\t\t\n\t\tpublic CDTSet(SimplePolygonXZ polygon,\n\t\t\t\tCollection<SimplePolygonXZ> holes,\n\t\t\t\tCollection<LineSegmentXZ> cSegments,\n\t\t\t\tCollection<VectorXZ> cPoints) {\n\n\t\t\tList<VectorXZ> vertices = polygon.getVertexLoop();\n\n\t\t\tsegmentSet.addAll(cSegments);\n\t\t\t\n\t\t\tfor (VectorXZ p : cPoints) {\n\t\t\t\tif (!pointSet.containsKey(p)) {\n\t\t\t\t\tTPoint tp = new TPoint(p.x, p.z);\n\t\t\t\t\tpointSet.put(p, tp);\n\t\t\t\t\tpoints.add(tp);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tfor (int i = 0, n = vertices.size() - 1; i < n; i++)\n\t\t\t\tsegmentSet.add(new LineSegmentXZ(vertices.get(i),\n\t\t\t\t\t\tvertices.get(i + 1)));\n\n\t\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\t\tvertices = hole.getVertexLoop();\n\t\t\t\tfor (int i = 0, n = vertices.size() - 1; i < n; i++)\n\t\t\t\t\tsegmentSet.add(new LineSegmentXZ(vertices.get(i),\n\t\t\t\t\t\t\tvertices.get(i + 1)));\n\t\t\t}\n\n\t\t\tremoveDuplicateSegments();\n\n\t\t\tboolean foundIntersections = false;\n\n\t\t\t// split at intersections\n\t\t\tfor (int i = 0, size = segmentSet.size(); i < size - 1; i++) {\n\t\t\t\tLineSegmentXZ l1 = segmentSet.get(i);\n\n\t\t\t\tfor (int j = i + 1; j < size; j++) {\n\t\t\t\t\tLineSegmentXZ l2 = segmentSet.get(j);\n\n\t\t\t\t\tVectorXZ crossing;\n\n\t\t\t\t\tif ((crossing = l1.getIntersection(l2.p1, l2.p2)) != null) {\n\t\t\t\t\t\tSystem.out.println(\"split \" + l1 + \" \" + l2 + \" at \"\n\t\t\t\t\t\t\t\t+ crossing);\n\t\t\t\t\t\tfoundIntersections = true;\n\n\t\t\t\t\t\tsegmentSet.remove(l1);\n\t\t\t\t\t\tsegmentSet.remove(l2);\n\n\t\t\t\t\t\tsegmentSet.add(new LineSegmentXZ(crossing, l1.p1));\n\t\t\t\t\t\tsegmentSet.add(new LineSegmentXZ(crossing, l1.p2));\n\t\t\t\t\t\tsegmentSet.add(new LineSegmentXZ(crossing, l2.p1));\n\t\t\t\t\t\tsegmentSet.add(new LineSegmentXZ(crossing, l2.p2));\n\n\t\t\t\t\t\tsize += 2;\n\n\t\t\t\t\t\t// first segment was removed\n\t\t\t\t\t\ti--;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (foundIntersections)\n\t\t\t\tremoveDuplicateSegments();\n\t\t}\n\n\t\tprivate void removeDuplicateSegments() {\n\t\t\tfor (int i = 0, size = segmentSet.size(); i < size - 1; i++) {\n\t\t\t\tLineSegmentXZ l1 = segmentSet.get(i);\n\n\t\t\t\tfor (int j = i + 1; j < size; j++) {\n\t\t\t\t\tLineSegmentXZ l2 = segmentSet.get(j);\n\n\t\t\t\t\tif ((l1.p1.equals(l2.p1) && l1.p2.equals(l2.p2))\n\t\t\t\t\t\t\t|| (l1.p1.equals(l2.p2) && l1.p2.equals(l2.p1))) {\n\t\t\t\t\t\t//System.out.println(\"remove dup \" + l1 + \" \" + l2);\n\t\t\t\t\t\tsegmentSet.remove(j);\n\t\t\t\t\t\tsize--;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tpublic TriangulationMode getTriangulationMode() {\n\t\t\treturn TriangulationMode.CONSTRAINED;\n\t\t}\n\n\t\tpublic List<TriangulationPoint> getPoints() {\n\t\t\treturn points;\n\t\t}\n\n\t\tpublic List<DelaunayTriangle> getTriangles() {\n\t\t\treturn triangles;\n\t\t}\n\n\t\tpublic void addTriangle(DelaunayTriangle t) {\n\t\t\ttriangles.add(t);\n\t\t}\n\n\t\tpublic void addTriangles(List<DelaunayTriangle> list) {\n\t\t\ttriangles.addAll(list);\n\t\t}\n\n\t\tpublic void clearTriangulation() {\n\t\t\ttriangles.clear();\n\t\t}\n\n\t\tpublic void prepareTriangulation(TriangulationContext<?> tcx) {\n\t\t\ttriangles.clear();\n\n\n\t\t\tfor (LineSegmentXZ l : segmentSet) {\n\t\t\t\tTriangulationPoint tp1, tp2;\n\t\t\t\t\n\t\t\t\tif (!pointSet.containsKey(l.p1)){\n\t\t\t\t\ttp1 = new TPoint(l.p1.x, l.p1.z);\n\t\t\t\t\tpointSet.put(l.p1, tp1);\n\t\t\t\t\tpoints.add(tp1);\n\t\t\t\t} else {\n\t\t\t\t\ttp1 = pointSet.get(l.p1);\n\t\t\t\t}\n\t\t\t\tif (!pointSet.containsKey(l.p2)){\n\t\t\t\t\ttp2 = new TPoint(l.p2.x, l.p2.z);\n\t\t\t\t\tpointSet.put(l.p2, tp2);\n\t\t\t\t\tpoints.add(tp2);\n\t\t\t\t} else {\n\t\t\t\t\ttp2 = pointSet.get(l.p2);\n\t\t\t\t}\n\n\t\t\t\ttcx.newConstraint(tp1, tp2);\n\t\t\t}\n\n\t\t\tsegmentSet.clear();\n\t\t\tpointSet.clear();\n\t\n\t\t\ttcx.addPoints(points);\n\t\t}\n\t}\n\n\tpublic static final List<TriangleXZ> triangulate(SimplePolygonXZ polygon,\n\t\t\tCollection<SimplePolygonXZ> holes,\n\t\t\tCollection<LineSegmentXZ> segments, Collection<VectorXZ> points) throws TriangulationException {\n\n\t\tCDTSet cdt = new CDTSet(polygon, holes, segments, points);\n\t\tTriangulationContext<?> tcx = Poly2Tri\n\t\t\t\t.createContext(TriangulationAlgorithm.DTSweep);\n\t\ttcx.prepareTriangulation(cdt);\n\n\t\ttry {\n\t\t\n\t\t\tPoly2Tri.triangulate(tcx);\n\t\t\t\t\t\n\t\t} catch (Exception e) {\n\t\t\tthrow new TriangulationException(e);\n\t\t} catch (StackOverflowError e) {\n\t\t\tthrow new TriangulationException(e);\n\t\t}\n\n\t\tList<TriangleXZ> triangles = new ArrayList<TriangleXZ>();\n\n\t\tList<DelaunayTriangle> result = cdt.getTriangles();\n\n\t\tif (result == null)\n\t\t\treturn triangles;\n\n\t\tfor (DelaunayTriangle t : result) {\n\n\t\t\tTriangulationPoint tCenter = t.centroid();\n\t\t\tVectorXZ center = new VectorXZ(tCenter.getX(), tCenter.getY());\n\n\t\t\tboolean triangleInHole = false;\n\t\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\t\tif (hole.contains(center)) {\n\t\t\t\t\ttriangleInHole = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (triangleInHole || !polygon.contains(center))\n\t\t\t\tcontinue;\n\n\t\t\ttriangles.add(new TriangleXZ(new VectorXZ(t.points[0].getX(),\n\t\t\t\t\tt.points[0].getY()), new VectorXZ(t.points[1].getX(),\n\t\t\t\t\tt.points[1].getY()), new VectorXZ(t.points[2].getX(),\n\t\t\t\t\tt.points[2].getY())));\n\t\t}\n\n\t\treturn triangles;\n\n\t}\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/PolygonWithHolesXZ.java",
    "content": "package org.osm2world.core.math;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class PolygonWithHolesXZ {\n\t\n\tprivate final SimplePolygonXZ outerPolygon;\n\tprivate final List<SimplePolygonXZ> holes;\n\t\n\tpublic PolygonWithHolesXZ(SimplePolygonXZ outerPolygon,\n\t\t\tList<SimplePolygonXZ> holes) {\n\t\tthis.outerPolygon = outerPolygon;\n\t\tthis.holes = holes;\n\t\t/**\n\t\t * forces the early computation of the area to avoid the creation of an invalid one. This is a temporary kludge as\n\t\t * calling a method that can be overridden in a constructor is a bad practice. Moreover, the late call of the method \n\t\t * calculateArea() was originally intended to avoid unnecessary computations but as it is necessary for intersection \n\t\t * tests even before any rendering attempts, it no longer makes sense. A better solution would consist in running the \n\t\t * computation of the area as soon as possible\n\t\t * */\n\t\tthis.getArea();\n\t}\n\n\tpublic SimplePolygonXZ getOuter() {\n\t\treturn outerPolygon;\n\t}\n\t\n\tpublic List<SimplePolygonXZ> getHoles() {\n\t\treturn holes;\n\t}\n\t\n\t/**\n\t * returns a list that contains the outer polygon and all holes\n\t */\n\tpublic List<SimplePolygonXZ> getPolygons() {\n\t\tif (holes.isEmpty()) {\n\t\t\treturn Collections.singletonList(outerPolygon);\n\t\t} else {\n\t\t\tList<SimplePolygonXZ> result = new ArrayList<SimplePolygonXZ>(holes.size()+1);\n\t\t\tresult.add(outerPolygon);\n\t\t\tresult.addAll(holes);\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tpublic TriangleXZ asTriangleXZ() {\n\t\tif (!holes.isEmpty()) {\n\t\t\tthrow new InvalidGeometryException(\"polygon has holes, it cannot be used as a triangle\");\n\t\t}\n\t\treturn outerPolygon.asTriangleXZ();\n\t}\n\n\tpublic boolean contains(SimplePolygonXZ boundary) {\n\t\t//FIXME currently returns true if boundary intersects one of the holes!\n\t\tif (!outerPolygon.contains(boundary)) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\t\tif (hole.contains(boundary)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\n\tpublic boolean contains(VectorXZ v) {\n\t\tif (!outerPolygon.contains(v)) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\t\tif (hole.contains(v)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\n\tpublic boolean contains(LineSegmentXZ lineSegment) {\n\t\tif (!this.contains(lineSegment.p1)\n\t\t\t\t|| !this.contains(lineSegment.p2)) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\t\tif (hole.intersects(lineSegment.p1, lineSegment.p2)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t//TODO (duplicate code): do something like intersects(geometricObject)\n\t\n\tpublic boolean intersects(LineSegmentXZ lineSegment) {\n\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\tif (hole.intersects(lineSegment)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn outerPolygon.intersects(lineSegment);\n\t}\n\t\n\tpublic boolean intersects(SimplePolygonXZ other) {\n\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\tif (hole.intersects(other)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn outerPolygon.intersects(other);\n\t}\n\n\tpublic List<VectorXZ> intersectionPositions(LineSegmentXZ lineSegment) {\n\t\tList<VectorXZ> intersectionPositions = new ArrayList<VectorXZ>();\n\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\tintersectionPositions.addAll(hole.intersectionPositions(lineSegment));\n\t\t}\n\t\tintersectionPositions.addAll(outerPolygon.intersectionPositions(lineSegment));\n\t\treturn intersectionPositions;\n\t}\n\n\tpublic Collection<VectorXZ> intersectionPositions(PolygonWithHolesXZ p2) {\n\t\tList<VectorXZ> intersectionPositions = new ArrayList<VectorXZ>();\n\t\tfor (SimplePolygonXZ simplePoly : p2.getPolygons()) {\n\t\t\tfor (LineSegmentXZ lineSegment : simplePoly.getSegments()) {\n\t\t\t\tintersectionPositions.addAll(\n\t\t\t\t\t\tintersectionPositions(lineSegment));\n\t\t\t}\n\t\t}\n\t\treturn intersectionPositions;\n\t}\n\t\n\tpublic double getArea() {\n\t\t//FIXME incorrect for overlapping holes\n\t\tdouble area = outerPolygon.getArea();\n\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\tarea -= hole.getArea();\n\t\t}\n\t\treturn area;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/PolygonXYZ.java",
    "content": "package org.osm2world.core.math;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * a three-dimensional polygon \n */\npublic class PolygonXYZ {\n\n\t/** polygon vertices; first and last vertex are equal */\n\tprivate final List<VectorXYZ> vertexLoop;\n\t\t\n\t/**\n\t * @param vertexLoop  vertices defining the polygon;\n\t *                  first and last vertex must be equal\n\t */\n\tpublic PolygonXYZ(List<VectorXYZ> vertexLoop) {\n\t\t\n\t\tif (!vertexLoop.get(0).equals(vertexLoop.get(vertexLoop.size() - 1))) {\n\t\t\tthrow new IllegalArgumentException(\"first and last vertex must be equal\");\n\t\t}\n\t\t\n\t\tthis.vertexLoop = vertexLoop;\t\t\n\t\t\n\t}\n\t\n\t/** \n\t * returns the polygon's vertices. \n\t * Unlike {@link #getVertexLoop()}, there is no duplication\n\t * of the first/last vertex.\n\t */\n\tpublic List<VectorXYZ> getVertices() {\n\t\treturn vertexLoop.subList(0, vertexLoop.size()-1);\n\t}\n\t\n\t/** \n\t * returns the polygon's vertices. First and last vertex are equal.\n\t */\n\tpublic List<VectorXYZ> getVertexLoop() {\n\t\treturn vertexLoop;\n\t}\n\t\n\t/**\n\t * returns the number of vertices in this polygon.\n\t * The duplicated first/last vertex is <em>not</em> counted twice,\n\t * so the result is equivalent to {@link #getVertices()}.size().\n\t */\n\tpublic int size() {\n\t\treturn vertexLoop.size()-1;\n\t}\n\t\n\t//TODO (code duplication): common polygon supertype?\n\tpublic List<LineSegmentXYZ> getSegments() {\n\t\tList<LineSegmentXYZ> segments = \n\t\t\tnew ArrayList<LineSegmentXYZ>(vertexLoop.size());\n\t\tfor (int i=0; i+1 < vertexLoop.size(); i++) {\n\t\t\tsegments.add(new LineSegmentXYZ(vertexLoop.get(i), vertexLoop.get(i+1)));\n\t\t}\n\t\treturn segments;\n\t}\n\n\tpublic PolygonXZ getXZPolygon() {\n\t\tList<VectorXZ> verticesXZ = new ArrayList<VectorXZ>(vertexLoop.size());\n\t\tfor (VectorXYZ vertex : vertexLoop) {\n\t\t\tverticesXZ.add(vertex.xz());\n\t\t}\n\t\treturn new PolygonXZ(verticesXZ);\n\t}\n\n\t/**\n\t * caller must check whether flattening will result in a simple planar polygon\n\t */\n\tpublic SimplePolygonXZ getSimpleXZPolygon() {\n\t\tList<VectorXZ> verticesXZ = new ArrayList<VectorXZ>(vertexLoop.size());\n\t\tfor (VectorXYZ vertex : vertexLoop) {\n\t\t\tverticesXZ.add(vertex.xz());\n\t\t}\n\t\treturn new SimplePolygonXZ(verticesXZ);\n\t}\n\t\n\t/**\n\t * returns a triangle with the same vertices as this polygon.\n\t * Requires that the polygon is triangular!\n\t */\n\tpublic TriangleXYZ asTriangleXYZ() {\n\t\tif (vertexLoop.size() != 4) {\n\t\t\tthrow new InvalidGeometryException(\"attempted creation of triangle \" +\n\t\t\t\t\t\"from polygon with vertex loop of size \" + vertexLoop.size() +\n\t\t\t\t\t\": \" + vertexLoop);\n\t\t} else {\n\t\t\treturn new TriangleXYZ(\n\t\t\t\t\tvertexLoop.get(0), \n\t\t\t\t\tvertexLoop.get(1), \n\t\t\t\t\tvertexLoop.get(2));\n\t\t}\n\t}\n\n\t/**\n\t * returns a reversed version of this polygon.\n\t * It consists of the same vertices, but has the other direction.\n\t */\n\tpublic PolygonXYZ reverse() {\n\t\tList<VectorXYZ> newVertexLoop = new ArrayList<VectorXYZ>(vertexLoop);\n\t\tCollections.reverse(newVertexLoop);\n\t\treturn new PolygonXYZ(newVertexLoop);\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/PolygonXZ.java",
    "content": "package org.osm2world.core.math;\n\nimport static org.osm2world.core.math.GeometryUtil.distanceFromLineSegment;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.TreeSet;\n\npublic class PolygonXZ {\n\n\t/** polygon vertices; first and last vertex are equal */\n\tprotected final List<VectorXZ> vertexLoop;\n\t\t\n\t/**\n\t * @param vertexLoop  vertices defining the polygon;\n\t *                    first and last vertex must be equal\n\t * @throws InvalidGeometryException  if the polygon is self-intersecting\n\t *                                   or produces invalid area calculation results\n\t */\n\tpublic PolygonXZ(List<VectorXZ> vertexLoop) {\n\t\t\n\t\tassertLoopProperty(vertexLoop);\n\t\t\n\t\tthis.vertexLoop = vertexLoop;\n\t\t\t\t\n\t}\n\t\n\t/**\n\t * returns the number of vertices in this polygon.\n\t * The duplicated first/last vertex is <em>not</em> counted twice,\n\t * so the result is equivalent to {@link #getVertices()}.size().\n\t */\n\tpublic int size() {\n\t\treturn vertexLoop.size()-1;\n\t}\n\n\t/**\n\t * returns the polygon's vertices.\n\t * Unlike {@link #getVertexLoop()}, there is no duplication\n\t * of the first/last vertex.\n\t */\n\tpublic List<VectorXZ> getVertices() {\n\t\treturn vertexLoop.subList(0, vertexLoop.size()-1);\n\t}\n\t\n\t/**\n\t * returns the polygon's vertices. First and last vertex are equal.\n\t */\n\tpublic List<VectorXZ> getVertexLoop() {\n\t\treturn vertexLoop;\n\t}\n\t\n\t/**\n\t * returns a collection that contains all vertices of this polygon\n\t * at least once. Can be used if you don't care about whether the first/last\n\t * vector is duplicated.\n\t */\n\tpublic List<VectorXZ> getVertexCollection() {\n\t\treturn vertexLoop;\n\t}\n\t\n\t/**\n\t * returns the vertex at a position in the vertex sequence\n\t */\n\tpublic VectorXZ getVertex(int index) {\n\t\tassert 0 <= index && index < vertexLoop.size()-1;\n\t\treturn vertexLoop.get(index);\n\t}\n\t\n\t/**\n\t * returns the successor of the vertex at a position in the vertex sequence.\n\t * This wraps around the vertex loop, so the successor of the last vertex\n\t * is the first vertex.\n\t */\n\tpublic VectorXZ getVertexAfter(int index) {\n\t\tassert 0 <= index && index < vertexLoop.size()-1;\n\t\treturn getVertex((index + 1) % size());\n\t}\n\t\n\t/**\n\t * returns the predecessor of the vertex at a position in the vertex sequence.\n\t * This wraps around the vertex loop, so the predecessor of the first vertex\n\t * is the last vertex.\n\t */\n\tpublic VectorXZ getVertexBefore(int index) {\n\t\tassert 0 <= index && index < vertexLoop.size()-1;\n\t\treturn getVertex((index + size() - 1) % size());\n\t}\n\t\n\tpublic List<LineSegmentXZ> getSegments() {\n\t\tList<LineSegmentXZ> segments = new ArrayList<LineSegmentXZ>(vertexLoop.size());\n\t\tfor (int i=0; i+1 < vertexLoop.size(); i++) {\n\t\t\tsegments.add(new LineSegmentXZ(vertexLoop.get(i), vertexLoop.get(i+1)));\n\t\t}\n\t\treturn segments;\n\t}\n\t\n\t/**\n\t * returns the polygon segment with minimum distance to a given point\n\t */\n\tpublic LineSegmentXZ getClosestSegment(VectorXZ point) {\n\t\t\n\t\tLineSegmentXZ closestSegment = null;\n\t\tdouble closestDistance = Double.MAX_VALUE;\n\t\t\n\t\tfor (LineSegmentXZ segment : getSegments()) {\n\t\t\tdouble distance = distanceFromLineSegment(point, segment);\n\t\t\tif (distance < closestDistance) {\n\t\t\t\tclosestSegment = segment;\n\t\t\t\tclosestDistance = distance;\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn closestSegment;\n\t\t\n\t}\n\t\n\t/**\n\t * returns true if there is an intersection between this polygon\n\t * and the line segment defined by the parameter\n\t */\n\tpublic boolean intersects(VectorXZ segmentP1, VectorXZ segmentP2) {\n\t\t//TODO: (performance): passing \"vector TO second point\", rather than point2, would avoid having to calc it here - and that information could be reused for all comparisons involving the segment\n\n\t\tfor (int i=0; i+1<vertexLoop.size(); i++) {\n\t\t\t\n\t\t\tVectorXZ intersection = GeometryUtil.getTrueLineSegmentIntersection(\n\t\t\t\t\tsegmentP1, segmentP2,\n\t\t\t\t\tvertexLoop.get(i), vertexLoop.get(i+1)\n\t\t\t\t\t);\n\t\t\t\n\t\t\tif (intersection != null) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn false;\n\t}\n\n\tpublic boolean intersects(LineSegmentXZ lineSegment) {\n\t\treturn intersects(lineSegment.p1, lineSegment.p2);\n\t}\n\n\t/**\n\t * returns true if there is an intersection between this polygon's\n\t * and the parameter polygon's sides\n\t */\n\tpublic boolean intersects(PolygonXZ outlinePolygonXZ) {\n\n\t\t//TODO (performance): currently performs pairwise intersection checks for sides of this and other - this might not be the fastest method\n\t\t\n\t\tfor (int i=0; i+1<vertexLoop.size(); i++) {\n\t\t\tif (outlinePolygonXZ.intersects(vertexLoop.get(i), vertexLoop.get(i+1))) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn false;\n\t}\n\n\tpublic Collection<LineSegmentXZ> intersectionSegments(\n\t\t\tLineSegmentXZ lineSegment) {\n\n\t\tList<LineSegmentXZ> intersectionSegments = new ArrayList<LineSegmentXZ>();\n\n\t\tfor (LineSegmentXZ polygonSegment : getSegments()) {\n\t\t\t\n\t\t\tVectorXZ intersection = GeometryUtil.getTrueLineSegmentIntersection(\n\t\t\t\t\tlineSegment.p1, lineSegment.p2,\n\t\t\t\t\tpolygonSegment.p1, polygonSegment.p2\n\t\t\t\t\t);\n\t\t\t\n\t\t\tif (intersection != null) {\n\t\t\t\tintersectionSegments.add(polygonSegment);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn intersectionSegments;\n\t\t\n\t}\n\n\tpublic List<VectorXZ> intersectionPositions(\n\t\t\tLineSegmentXZ lineSegment) {\n\t\t\n\t\tList<VectorXZ> intersectionPositions = new ArrayList<VectorXZ>();\n\n\t\tfor (int i=0; i+1<vertexLoop.size(); i++) {\n\t\t\t\n\t\t\tVectorXZ intersection = GeometryUtil.getTrueLineSegmentIntersection(\n\t\t\t\t\tlineSegment.p1, lineSegment.p2,\n\t\t\t\t\tvertexLoop.get(i), vertexLoop.get(i+1)\n\t\t\t\t\t);\n\t\t\t\n\t\t\tif (intersection != null) {\n\t\t\t\tintersectionPositions.add(intersection);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn intersectionPositions;\n\t\t\n\t}\n\t\n\t/**\n\t * returns whether this polygon is self-intersecting\n\t */\n\tpublic boolean isSelfIntersecting() {\n\t\treturn isSelfIntersecting(vertexLoop);\n\t}\n\t\n\t/**\n\t * returns true if the polygon defined by the polygonVertexLoop parameter\n\t * is self-intersecting.<br/>\n\t * The Code is based on Shamos-Hoey's algorithm\n\t * \n\t * TODO: if the end vertex of two line segments are the same the\n\t *       polygon is never considered as self intersecting on purpose.\n\t *       This behavior should probably be reconsidered, but currently\n\t *       left as is due to frequent cases of such polygons.\n\t */\n\tpublic static boolean isSelfIntersecting(List<VectorXZ> polygonVertexLoop) {\n\n\t\tfinal class Event {\n\t\t\tboolean start;\n\t\t\tLineSegmentXZ line;\n\t\t\t\n\t\t\tEvent(LineSegmentXZ l, boolean s) {\n\t\t\t\tthis.line = l;\n\t\t\t\tthis.start = s;\n\t\t\t}\n\t\t}\n\n\t\t// we have n-1 vertices as the first and last vertex are the same \n\t\tfinal int segments = polygonVertexLoop.size()-1;\n\n\t\t// generate an array of input events associated with their line segments\n\t\tEvent[] events = new Event[segments*2];\n\t\tfor (int i = 0; i < segments; i++) {\n\t\t\tVectorXZ v1 = polygonVertexLoop.get(i);\n\t\t\tVectorXZ v2 = polygonVertexLoop.get(i+1);\n\t\t\t\n\t\t\t// Create a line where the first vertex is left (or above) the second vertex\n\t\t\tLineSegmentXZ line;\n\t\t\tif ((v1.x < v2.x) || ((v1.x == v2.x) && (v1.z < v2.z))) {\n\t\t\t\tline = new LineSegmentXZ(v1, v2);\n\t\t\t} else {\n\t\t\t\tline = new LineSegmentXZ(v2, v1);\n\t\t\t}\n\t\t\t\n\t\t\tevents[2*i] = new Event(line, true);\n\t\t\tevents[2*i+1] = new Event(line, false);\n\t\t}\n\t\t\n\t\t// sort the input events according to the x-coordinate, then z-coordinate\n\t\tArrays.sort(events, new Comparator<Event>() {\n\t\t\tpublic int compare(Event e1, Event e2) {\n\t\t\t\t\n\t\t\t\tVectorXZ v1 = e1.start? e1.line.p1 : e1.line.p2;\n\t\t\t\tVectorXZ v2 = e2.start? e2.line.p1 : e2.line.p2;\n\n\t\t\t\tif (v1.x < v2.x) return -1;\n\t\t\t\telse if (v1.x == v2.x) {\n\t\t\t\t\tif (v1.z < v2.z) return -1;\n\t\t\t\t\telse if (v1.z == v2.z) return 0;\n\t\t\t\t}\n\t\t\t\treturn 1;\n\t\t\t}});\n\t\n\t\t// A TreeSet, used for the sweepline algorithm\n\t\tTreeSet<LineSegmentXZ> sweepLine = new TreeSet<LineSegmentXZ>(new Comparator<LineSegmentXZ>() {\n\t\t\tpublic int compare(LineSegmentXZ l1, LineSegmentXZ l2) { \n\n\t\t\t\tVectorXZ v1 = l1.p1;\n\t\t\t\tVectorXZ v2 = l2.p1;\n\n\t\t\t\tif (v1.z < v2.z) return -1;\n\t\t\t\telse if (v1.z == v2.z) {\n\t\t\t\t\tif (v1.x < v2.x) return -1;\n\t\t\t\t\telse if (v1.x == v2.x) {\n\t\t\t\t\t\tif (l1.p2.z < l2.p2.z) return -1;\n\t\t\t\t\t\telse if (l1.p2.z == l2.p2.z) {\n\t\t\t\t\t\t\tif (l1.p2.x < l2.p2.x) return -1;\n\t\t\t\t\t\t\telse if (l1.p2.x == l2.p2.x) return 0; \n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn 1;\n\t\t\t}});\n\t\t\n\t\t// start the algorithm by visiting every event\n\t\tfor (Event event : events) {\n\t\t\tLineSegmentXZ line = event.line;\n\t\t\t\n\t\t\tif (event.start) { // if it is a startpoint\n\t\t\t\n\t\t\t\tLineSegmentXZ lower = sweepLine.lower(line);\n\t\t\t\tLineSegmentXZ higher = sweepLine.higher(line);\n\n\t\t\t\tsweepLine.add(line);\n\n\t\t\t\tif (lower != null) {\n\t\t\t\t\tif (lower.intersects(line.p1, line.p2)) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (higher != null) {\t\n\t\t\t\t\tif (higher.intersects(line.p1, line.p2)) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else { // if it is an endpoint\n\t\t\t\t\n\t\t\t\tLineSegmentXZ lower = sweepLine.lower(line);\n\t\t\t\tLineSegmentXZ higher = sweepLine.higher(line);\n\n\t\t\t\tsweepLine.remove(line);\n\n\t\t\t\tif ((lower == null) || (higher == null)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (lower.intersects(higher.p1, higher.p2)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\n\t/**\n\t * checks whether this polygon is simple\n\t */\n\tpublic boolean isSimple() {\n\t\ttry {\n\t\t\tthis.asSimplePolygon();\n\t\t\treturn true;\n\t\t} catch (InvalidGeometryException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\t\n\t/**\n\t * returns a polygon with the coordinates of this polygon\n\t * that is an instance of {@link SimplePolygonXZ}.\n\t * Only works if it actually {@link #isSimple()}!\n\t */\n\tpublic SimplePolygonXZ asSimplePolygon() {\n\t\treturn new SimplePolygonXZ(vertexLoop);\n\t}\n\n\t/**\n\t * returns a triangle with the same vertices as this polygon.\n\t * Requires that the polygon is triangular!\n\t */\n\tpublic TriangleXZ asTriangleXZ() {\n\t\tif (vertexLoop.size() != 4) {\n\t\t\tthrow new InvalidGeometryException(\"attempted creation of triangle \" +\n\t\t\t\t\t\"from polygon with vertex loop of size \" + vertexLoop.size() +\n\t\t\t\t\t\": \" + vertexLoop);\n\t\t} else {\n\t\t\treturn new TriangleXZ(\n\t\t\t\t\tvertexLoop.get(0),\n\t\t\t\t\tvertexLoop.get(1),\n\t\t\t\t\tvertexLoop.get(2));\n\t\t}\n\t}\n\t\n\tpublic PolygonXYZ xyz(final double y) {\n\t\treturn new PolygonXYZ(VectorXZ.listXYZ(vertexLoop, y));\n\t}\n\n\tpublic PolygonXZ reverse() {\n\t\tList<VectorXZ> newVertexLoop = new ArrayList<VectorXZ>(vertexLoop);\n\t\tCollections.reverse(newVertexLoop);\n\t\treturn new PolygonXZ(newVertexLoop);\n\t}\n\t\n\t/**\n\t * returns the average of all vertex coordinates.\n\t * The result is not necessarily contained by this polygon.\n\t */\n\tpublic VectorXZ getCenter() {\n\t\tdouble x=0, z=0;\n\t\tint numberVertices = vertexLoop.size()-1;\n\t\tfor (VectorXZ vertex : getVertices()) {\n\t\t\tx += vertex.x / numberVertices;\n\t\t\tz += vertex.z / numberVertices;\n\t\t\t/* single division per coordinate after loop would be faster,\n\t\t\t * but might cause numbers to get too large */\n\t\t}\n\t\treturn new VectorXZ(x, z);\n\t}\n\t\n\t/**\n\t * returns the length of the polygon's outline.\n\t * (This does <em>not</em> return the number of vertices,\n\t * but the sum of distances between subsequent nodes.)\n\t */\n\tpublic double getOutlineLength() {\n\t\tdouble length = 0;\n\t\tfor (int i = 0; i+1 < vertexLoop.size(); i++) {\n\t\t\tlength += VectorXZ.distance(vertexLoop.get(i), vertexLoop.get(i+1));\n\t\t}\n\t\treturn length;\n\t}\n\n\t/**\n\t * returns true if the other polygon has the same vertices in the same order,\n\t * possibly with a different start vertex\n\t */\n\tpublic boolean isEquivalentTo(PolygonXZ other) {\n\t\t\t\t\n\t\tif (vertexLoop.size() != other.vertexLoop.size()) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\tList<VectorXZ> ownVertices = getVertices();\n\t\tList<VectorXZ> otherVertices = other.getVertices();\n\t\t\n\t\tfor (int offset = 0; offset < ownVertices.size(); offset ++) {\n\t\t\t\n\t\t\tboolean matches = true;\n\t\t\t\n\t\t\tfor (int i = 0; i < ownVertices.size(); i++) {\n\t\t\t\tint iWithOffset = (i + offset) % ownVertices.size();\n\t\t\t\tif (!otherVertices.get(i).equals(ownVertices.get(iWithOffset))) {\n\t\t\t\t\tmatches = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (matches) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\treturn false;\n\t\t\n\t}\n\t\n\t/**\n\t * checks that the first and last vertex of the vertex list are equal.\n\t * @throws IllegalArgumentException  if first and last vertex aren't equal\n\t *                                   (this is usually a programming error,\n\t *                                    therefore InvalidGeometryException is not used)\n\t */\n\tprotected static void assertLoopProperty(List<VectorXZ> vertexLoop) {\n\t\tif (!vertexLoop.get(0).equals(vertexLoop.get(vertexLoop.size() - 1))) {\n\t\t\tthrow new IllegalArgumentException(\"first and last vertex must be equal\\n\"\n\t\t\t\t\t+ \"Polygon vertices: \" + vertexLoop);\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn vertexLoop.toString();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/SimplePolygonXZ.java",
    "content": "package org.osm2world.core.math;\n\nimport static java.lang.Math.min;\nimport static org.osm2world.core.math.GeometryUtil.distanceFromLineSegment;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * a non-self-intersecting polygon in the XZ plane\n */\npublic class SimplePolygonXZ extends PolygonXZ {\n\t\n\t/** stores the signed area */\n\tprivate Double signedArea;\n\t\n\t/** stores the result for {@link #getArea()} */\n\tprivate Double area;\n\n\t/** stores the result for {@link #isClockwise()} */\n\tprivate Boolean clockwise;\n\t\n\t/**\n\t * @param vertexLoop  vertices defining the polygon;\n\t *                    first and last vertex must be equal\n\t * @throws InvalidGeometryException  if the polygon is self-intersecting\n\t *                                   or produces invalid area calculation results\n\t */\n\tpublic SimplePolygonXZ(List<VectorXZ> vertexLoop) {\n\t\t\n\t\tsuper(vertexLoop);\n\t\t\n\t\tassertLoopLength(vertexLoop);\n\t\tassertNotSelfIntersecting(vertexLoop);\n\t\t\t\t\n\t}\n\n\tprivate void calculateArea() {\n\t\tthis.signedArea = calculateSignedArea(vertexLoop);\n\t\tthis.area = Math.abs(signedArea);\n\t\tthis.clockwise = signedArea < 0;\n\t\t\n\t\tassertNonzeroArea();\n\t}\n\t\n\tpublic List<LineSegmentXZ> getSegments() {\n\t\tList<LineSegmentXZ> segments = new ArrayList<LineSegmentXZ>(vertexLoop.size());\n\t\tfor (int i=0; i+1 < vertexLoop.size(); i++) {\n\t\t\tsegments.add(new LineSegmentXZ(vertexLoop.get(i), vertexLoop.get(i+1)));\n\t\t}\n\t\treturn segments;\n\t}\n\t\n\t/** returns the polygon's area */\n\tpublic double getArea() {\n\t\tif (area == null) {\n\t\t\tcalculateArea();\n\t\t}\n\t\treturn area;\n\t}\n\t\n\t/** returns the centroid (or \"barycenter\") of the polygon */\n\tpublic VectorXZ getCentroid() {\n\t\t\n\t\tif (signedArea == null) { calculateArea(); }\n\t\t\n\t\tdouble xSum = 0, zSum = 0;\n\t\t\n\t\tint numVertices = vertexLoop.size() - 1;\n\t\tfor (int i = 0; i < numVertices; i++) {\n\t\t\t\n\t\t\tdouble factor = vertexLoop.get(i).x * vertexLoop.get(i+1).z\n\t\t\t\t- vertexLoop.get(i+1).x * vertexLoop.get(i).z;\n\t\t\t\n\t\t\txSum += (vertexLoop.get(i).x + vertexLoop.get(i+1).x) * factor;\n\t\t\tzSum += (vertexLoop.get(i).z + vertexLoop.get(i+1).z) * factor;\n\t\t\t\n\t\t}\n\t\t\n\t\tdouble areaFactor = 1 / (6 * signedArea);\n\t\treturn new VectorXZ(areaFactor * xSum, areaFactor * zSum);\n\t\t\n\t}\n\n\t/**\n\t * returns the largest distance between any pair of vertices\n\t * of this polygon\n\t */\n\tpublic double getDiameter() {\n\t\tdouble maxDistance = 0;\n\t\tfor (int i = 1; i < vertexLoop.size() - 1; i++) {\n\t\t\tfor (int j = 0; j < i; j++) {\n\t\t\t\tdouble distance = vertexLoop.get(i).distanceTo(vertexLoop.get(j));\n\t\t\t\tif (distance > maxDistance) {\n\t\t\t\t\tmaxDistance = distance;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn maxDistance;\n\t}\n\t\n\t/** returns true if the polygon has clockwise orientation */\n\tpublic boolean isClockwise() {\n\t\tif (area == null) {\n\t\t\tcalculateArea();\n\t\t}\n\t\treturn clockwise;\n\t}\n\t\n\t@Override\n\tpublic boolean isSelfIntersecting() {\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic boolean isSimple() {\n\t\treturn true;\n\t}\n\t\n\t@Override\n\tpublic SimplePolygonXZ asSimplePolygon() {\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return  a {@link PolygonWithHolesXZ}\n\t * with this polygon as the outer polygon and no holes\n\t */\n\tpublic PolygonWithHolesXZ asPolygonWithHolesXZ() {\n\t\treturn new PolygonWithHolesXZ(this, Collections.<SimplePolygonXZ>emptyList());\n\t}\n\t\n\t/**\n\t * returns this polygon if it is counterclockwise,\n\t * or the reversed polygon if it is clockwise.\n\t */\n\tpublic SimplePolygonXZ makeClockwise() {\n\t\treturn makeRotationSense(true);\n\t}\n\t\n\t/**\n\t * returns this polygon if it is clockwise,\n\t * or the reversed polygon if it is counterclockwise.\n\t */\n\tpublic SimplePolygonXZ makeCounterclockwise() {\n\t\treturn makeRotationSense(false);\n\t}\n\n\tprivate SimplePolygonXZ makeRotationSense(boolean clockwise) {\n\t\tif (this.isClockwise() ^ clockwise) {\n\t\t\treturn this.reverse();\n\t\t} else {\n\t\t\treturn this;\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic SimplePolygonXZ reverse() {\n\t\tList<VectorXZ> newVertexLoop = new ArrayList<VectorXZ>(vertexLoop);\n\t\tCollections.reverse(newVertexLoop);\n\t\treturn new SimplePolygonXZ(newVertexLoop);\n\t}\n\n\t/**\n\t * creates a new polygon by adding a shift vector to each vector of this\n\t */\n\tpublic SimplePolygonXZ shift(VectorXZ shiftVector) {\n\t\tList<VectorXZ> newVertexLoop = new ArrayList<VectorXZ>(vertexLoop.size());\n\t\tnewVertexLoop.add(vertexLoop.get(0).add(shiftVector));\n\t\tfor (VectorXZ v : vertexLoop) {\n\t\t\tif (!v.equals(vertexLoop.get(0))) {\n\t\t\t\tnewVertexLoop.add(v.add(shiftVector));\n\t\t\t}\n\t\t}\n\t\tnewVertexLoop.add(newVertexLoop.get(0));\n\t\treturn new SimplePolygonXZ(newVertexLoop);\n\t}\n\t\n\t/**\n\t * returns true if the polygon defined by the polygonVertexLoop parameter\n\t * contains a given position\n\t */\n\tpublic static boolean contains(List<VectorXZ> polygonVertexLoop, VectorXZ test) {\n\t\n\t\tassertLoopProperty(polygonVertexLoop);\n\t\t\n\t\tint i, j;\n\t\tboolean c = false;\n\n\t\tfor (i = 0, j = polygonVertexLoop.size() - 1; i < polygonVertexLoop.size(); j = i++) {\n\t\t\tif (((polygonVertexLoop.get(i).z > test.z) != (polygonVertexLoop.get(j).z > test.z))\n\t\t\t\t\t&& (test.x < (polygonVertexLoop.get(j).x - polygonVertexLoop.get(i).x)\n\t\t\t\t\t\t\t* (test.z - polygonVertexLoop.get(i).z)\n\t\t\t\t\t\t\t/ (polygonVertexLoop.get(j).z - polygonVertexLoop.get(i).z) + polygonVertexLoop.get(i).x))\n\t\t\t\tc = !c;\n\t\t}\n\n\t\treturn c;\n\t\t  \n\t}\n\t\n\t/**\n\t * returns true if the polygon contains a given position\n\t */\n\tpublic boolean contains(VectorXZ test) {\n\t\treturn SimplePolygonXZ.contains(vertexLoop, test);\n\t}\n\n\t/**\n\t * returns true if this polygon contains the parameter polygon\n\t */\n\tpublic boolean contains(PolygonXZ p) {\n\t\t//FIXME: it is possible that a polygon contains all vertices of another polygon, but still not the entire polygon\n\t\tfor (VectorXZ v : p.getVertices()) {\n\t\t\tif (!vertexLoop.contains(v) && !this.contains(v)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * returns the distance of a point to the segments this polygon.\n\t * Note that the distance can be > 0 even if the polygon contains the point\n\t */\n\tpublic double distanceToSegments(VectorXZ p) {\n\t\tdouble minDistance = Double.MAX_VALUE;\n\t\tfor (LineSegmentXZ s : getSegments()) {\n\t\t\tminDistance = min(minDistance, distanceFromLineSegment(p, s));\n\t\t}\n\t\treturn minDistance;\n\t}\n\t\n\t/**\n\t * returns a different polygon that is constructed from this polygon\n\t * by removing all vertices where this has an angle close to 180°\n\t * (i.e. where removing the vertex does not change the polygon very much).\n\t */\n\tpublic SimplePolygonXZ getSimplifiedPolygon() {\n\t\t\n\t\tboolean[] delete = new boolean[size()];\n\t\tint deleteCount = 0;\n\t\t\n\t\tfor (int i = 0; i < size(); i++) {\n\t\t\tVectorXZ segmentBefore = getVertex(i).subtract(getVertexBefore(i));\n\t\t\tVectorXZ segmentAfter = getVertexAfter(i).subtract(getVertex(i));\n\t\t\tdouble dot = segmentBefore.normalize().dot(segmentAfter.normalize());\n\t\t\tif (Math.abs(dot - 1) < 0.05) {\n\t\t\t\tdelete[i] = true;\n\t\t\t\tdeleteCount += 1;\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (deleteCount == 0 || deleteCount > size() - 3) {\n\t\t\treturn this;\n\t\t} else {\n\t\t\t\n\t\t\tList<VectorXZ> newVertexList = new ArrayList<VectorXZ>(getVertices());\n\t\t\t\n\t\t\t//iterate backwards => it doesn't matter when higher indices change\n\t\t\tfor (int i = size() - 1; i >= 0; i--) {\n\t\t\t\tif (delete[i]) {\n\t\t\t\t\tnewVertexList.remove(i);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tnewVertexList.add(newVertexList.get(0));\n\t\t\treturn new SimplePolygonXZ(newVertexList);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * calculates the area of a planar non-self-intersecting polygon.\n\t * The result is negative if the polygon is clockwise.\n\t */\n\tprivate static double calculateSignedArea(List<VectorXZ> vertexLoop) {\n\t\t\t\t\n\t\tdouble sum = 0f;\n\t\t\n\t\tfor (int i = 0; i + 1 < vertexLoop.size(); i++) {\n\t\t\tsum += vertexLoop.get(i).x * vertexLoop.get(i+1).z;\n\t\t\tsum -= vertexLoop.get(i+1).x * vertexLoop.get(i).z;\n\t\t}\n\t\t\n\t\treturn sum / 2;\n\t\t\n\t}\n\n\t/**\n\t * @throws InvalidGeometryException  if the vertex loop is too short\n\t */\n\tprivate static void assertLoopLength(List<VectorXZ> vertexLoop) {\n\t\tif (vertexLoop.size() <= 3) {\n\t\t\tthrow new InvalidGeometryException(\n\t\t\t\t\t\"polygon needs more than 2 vertices\\n\"\n\t\t\t\t\t+ \"Polygon vertex loop: \" + vertexLoop);\n\t\t}\n\t}\n\n\t/**\n\t * @throws InvalidGeometryException  if the vertex loop is self-intersecting\n\t */\n\tprivate static void assertNotSelfIntersecting(List<VectorXZ> vertexLoop) {\n\t\tif (isSelfIntersecting(vertexLoop)) {\n\t\t\tthrow new InvalidGeometryException(\n\t\t\t\t\t\"polygon must not be self-intersecting\\n\"\n\t\t\t\t\t+ \"Polygon vertices: \" + vertexLoop);\n\t\t}\n\t}\n\t\n\t/**\n\t * @throws InvalidGeometryException  if area is 0\n\t */\n\tprivate void assertNonzeroArea() {\n\t\tif (area == 0) {\n\t\t\tthrow new InvalidGeometryException(\n\t\t\t\t\t\"a polygon's area must be positive, but it's \"\n\t\t\t\t\t+ area + \" for this polygon.\\nThis problem can be caused \"\n\t\t\t\t\t+ \"by broken polygon data or imprecise calculations\"\n\t\t\t\t\t+ \"\\nPolygon vertices: \" + vertexLoop);\n\t\t}\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/TriangleXYZ.java",
    "content": "package org.osm2world.core.math;\n\nimport java.util.List;\n\nimport com.google.common.collect.ImmutableList;\n\npublic class TriangleXYZ {\n\n\tpublic final VectorXYZ v1, v2, v3;\n\n\tpublic TriangleXYZ(VectorXYZ v1, VectorXYZ v2, VectorXYZ v3) {\n\t\tthis.v1 = v1;\n\t\tthis.v2 = v2;\n\t\tthis.v3 = v3;\n\t}\n\t\n\tpublic List<VectorXYZ> getVertices() {\n\t\treturn ImmutableList.of(v1, v2, v3);\n\t}\n\t\n\t/**\n\t * returns the normalized normal vector of this triangle\n\t */\n\tpublic VectorXYZ getNormal() {\n\t\t//TODO: account for clockwise vs. counterclockwise\n\t\treturn v2.subtract(v1).crossNormalized(v2.subtract(v3));\n\t}\n\n\tpublic VectorXYZ getCenter() {\n\t\treturn new VectorXYZ(\n\t\t\t\t(v1.x + v2.x + v3.x) / 3,\n\t\t\t\t(v1.y + v2.y + v3.y) / 3,\n\t\t\t\t(v1.z + v2.z + v3.z) / 3);\n\t}\n\t\n\t/**\n\t * returns the triangle's y coord value at a {@link VectorXZ} within the\n\t * triangle's 2D footprint.\n\t * \n\t * It is obtained by linear interpolation within the triangle.\n\t */\n\tpublic double getYAt(VectorXZ pos) {\n\t\t\n\t\tdouble a = v1.z * (v2.y - v3.y) + v2.z * (v3.y - v1.y) + v3.z * (v1.y - v2.y);\n\t\tdouble b = v1.y * (v2.x - v3.x) + v2.y * (v3.x - v1.x) + v3.y * (v1.x - v2.x);\n\t\tdouble c = v1.x * (v2.z - v3.z) + v2.x * (v3.z - v1.z) + v3.x * (v1.z - v2.z);\n\t\tdouble d = -a * v1.x - b * v1.z - c * v1.y;\n\t\t\n\t\treturn -a/c * pos.x - b/c * pos.z - d/c;\n\t\t\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"[\" + v1.toString() + \", \" + v2.toString() + \", \" + v3.toString() + \"]\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/TriangleXYZWithNormals.java",
    "content": "package org.osm2world.core.math;\n\nimport java.util.List;\n\nimport com.google.common.collect.ImmutableList;\n\npublic class TriangleXYZWithNormals extends TriangleXYZ {\n\t\n\tpublic final VectorXYZ n1, n2, n3;\n\n\tpublic TriangleXYZWithNormals(TriangleXYZ t,\n\t\t\tVectorXYZ n1, VectorXYZ n2, VectorXYZ n3) {\n\t\tthis(t.v1, t.v2, t.v3, n1, n2, n3);\n\t}\n\t\n\tpublic TriangleXYZWithNormals(VectorXYZ v1, VectorXYZ v2, VectorXYZ v3,\n\t\t\tVectorXYZ n1, VectorXYZ n2, VectorXYZ n3) {\n\t\tsuper(v1, v2, v3);\n\t\tthis.n1 = n1;\n\t\tthis.n2 = n2;\n\t\tthis.n3 = n3;\n\t}\n\t\n\tpublic List<VectorXYZ> getNormals() {\n\t\treturn ImmutableList.of(n1, n2, n3);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/TriangleXZ.java",
    "content": "package org.osm2world.core.math;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport com.google.common.collect.ImmutableList;\n\npublic class TriangleXZ {\n\n\tpublic final VectorXZ v1, v2, v3;\n\n\tpublic TriangleXZ(VectorXZ v1, VectorXZ v2, VectorXZ v3) {\n\t\t\n\t\tassert v1 != null && v2 != null && v3 != null;\n\t\t\n\t\tthis.v1 = v1;\n\t\tthis.v2 = v2;\n\t\tthis.v3 = v3;\n\t\t\n\t}\n\t\n\tpublic List<VectorXZ> getVertices() {\n\t\treturn ImmutableList.of(v1, v2, v3);\n\t}\n\n\tpublic VectorXZ getCenter() {\n\t\treturn new VectorXZ(\n\t\t\t\t(v1.x + v2.x + v3.x) / 3,\n\t\t\t\t(v1.z + v2.z + v3.z) / 3);\n\t}\n\t\n\tpublic boolean contains(VectorXZ point) {\n\t\treturn SimplePolygonXZ.contains(Arrays.asList(v1, v2, v3, v1), point); //TODO: avoid creating new lists?\n\t}\n\t\n\tpublic TriangleXYZ xyz(double y) {\n\t\treturn new TriangleXYZ(v1.xyz(y), v2.xyz(y), v3.xyz(y));\n\t}\n\t\n\tpublic boolean isClockwise() {\n\t\treturn GeometryUtil.isRightOf(v3, v1, v2);\n\t}\n\t\n\t/**\n\t * returns this triangle if it is counterclockwise,\n\t * or the reversed triangle if it is clockwise.\n\t */\n\tpublic TriangleXZ makeClockwise() {\n\t\treturn makeRotationSense(true);\n\t}\n\t\n\t/**\n\t * returns this triangle if it is clockwise,\n\t * or the reversed triangle if it is counterclockwise.\n\t */\n\tpublic TriangleXZ makeCounterclockwise() {\n\t\treturn makeRotationSense(false);\n\t}\n\n\tprivate TriangleXZ makeRotationSense(boolean clockwise) {\n\t\tif (isClockwise() ^ clockwise) {\n\t\t\treturn this.reverse();\n\t\t} else {\n\t\t\treturn this;\n\t\t}\n\t}\n\n\t/**\n\t * returns the area of the triangle\n\t */\n\tpublic double getArea() {\n\t\t\n\t\tdouble sum =\n\t\t\t\t+ v1.x * v2.z\n\t\t\t\t- v2.x * v1.z\n\t\t\t\t+ v2.x * v3.z\n\t\t\t\t- v3.x * v2.z\n\t\t\t\t+ v3.x * v1.z\n\t\t\t\t- v1.x * v3.z;\n\t\t\n\t\treturn Math.abs(sum / 2);\n\t\t\n\t}\n\t\n\t/**\n\t * returns an inversed version of this triangle.\n\t * It consists of the same vertices, but has the other direction.\n\t */\n\tpublic TriangleXZ reverse() {\n\t\treturn new TriangleXZ(v3, v2, v1);\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + v1.hashCode();\n\t\tresult = prime * result + v2.hashCode();\n\t\tresult = prime * result + v3.hashCode();\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\t\n\t\tif (this == obj) {\n\t\t\treturn true;\n\t\t} else if (!(obj instanceof TriangleXZ)) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\tTriangleXZ other = (TriangleXZ)obj;\n\t\t\treturn v1.equals(other.v1)\n\t\t\t\t\t&& v2.equals(other.v2)\n\t\t\t\t\t&& v3.equals(other.v3);\n\t\t}\n\t\t\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"[\" + v1 + \", \" + v2 + \", \" + v3 + \"]\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/Vector3D.java",
    "content": "package org.osm2world.core.math;\n\npublic interface Vector3D {\n\tpublic double getX();\n\tpublic double getY();\n\tpublic double getZ();\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/Vector4D.java",
    "content": "package org.osm2world.core.math;\n\npublic interface Vector4D {\n\tpublic double getX();\n\tpublic double getY();\n\tpublic double getZ();\n\tpublic double getW();\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/VectorGridXZ.java",
    "content": "package org.osm2world.core.math;\n\nimport static java.lang.Math.*;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\n\nimport org.apache.commons.collections.iterators.ArrayIterator;\nimport org.apache.commons.collections.iterators.IteratorChain;\n\n/**\n * regular grid of {@link VectorXZ}.\n * \n * Individual points are created only when they are first accessed,\n * so no memory is wasted on unused points.\n */\npublic class VectorGridXZ implements Iterable<VectorXZ> {\n\t\n\tprivate static final VectorXZ[][] EMPTY_GRID = new VectorXZ[0][0];\n\t\n\tprivate final VectorXZ[][] grid;\n\t\n\tprivate final double sampleDistance;\n\t\n\tprivate final int startX;\n\tprivate final int startZ;\n\n\t/**\n\t * returns a regular grid of points within a bounding box.\n\t */\n\tpublic VectorGridXZ(AxisAlignedBoundingBoxXZ box, double sampleDistance) {\n\t\t\n\t\tthis.sampleDistance = sampleDistance;\n\t\t\n\t\tstartX = (int)ceil((box.minX + 0.01) / sampleDistance);\n\t\tstartZ = (int)ceil((box.minZ + 0.01) / sampleDistance);\n\t\t\n\t\tint endX = (int)floor((box.maxX - 0.01) / sampleDistance);\n\t\tint endZ = (int)floor((box.maxZ - 0.01) / sampleDistance);\n\t\t\n\t\tint numSamplesX = endX - startX + 1;\n\t\tint numSamplesZ = endZ - startZ + 1;\n\t\t\n\t\tif (numSamplesX <= 0 || numSamplesZ <= 0) {\n\t\t\t\n\t\t\tgrid = EMPTY_GRID;\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\tgrid = new VectorXZ[numSamplesX][numSamplesZ];\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic int size() {\n\t\treturn sizeX() * sizeZ();\n\t}\n\t\n\tpublic int sizeX() {\n\t\treturn grid.length;\n\t}\n\t\n\tpublic int sizeZ() {\n\t\treturn grid == EMPTY_GRID ? 0 : grid[0].length;\n\t}\n\t\n\tpublic boolean isEmpty() {\n\t\treturn grid == EMPTY_GRID;\n\t}\n\n\tpublic VectorXZ get(int indexX, int indexZ) {\n\t\t\n\t\tcreateIfNecessary(indexX, indexZ);\n\t\t\n\t\treturn grid[indexX][indexZ];\n\t\t\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic Iterator<VectorXZ> iterator() {\n\t\t\n\t\tif (isEmpty()) {\n\t\t    return Collections.EMPTY_LIST.iterator();\n\t\t} else {\n\t\t\t\n\t\t\tList<Iterator<VectorXZ>> columnIterators =\n\t\t\t\t\tnew ArrayList<Iterator<VectorXZ>>(sizeX());\n\t\t\t\n\t\t\tfor (int x = 0; x < sizeX(); x++) {\n\t\t\t\t\n\t\t\t\tfor (int z = 0; z < sizeZ(); z++) {\n\t\t\t\t\tcreateIfNecessary(x, z);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tcolumnIterators.add(new ArrayIterator(grid[x]));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn new IteratorChain(columnIterators);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate void createIfNecessary(int indexX, int indexZ) {\n\t\t\n\t\tif (grid[indexX][indexZ] == null) {\n\t\t\t\n\t\t\tgrid[indexX][indexZ] = new VectorXZ(\n\t\t\t\t\t(startX + indexX) * sampleDistance,\n\t\t\t\t\t(startZ + indexZ) * sampleDistance);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\t\n\t\tif (isEmpty()) {\n\t\t\t\n\t\t\treturn \"{0*0}\";\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\treturn \"{\" + sizeX() + \"*\" + sizeZ() + \", start \" + get(0,0) + \"}\";\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/VectorXYZ.java",
    "content": "package org.osm2world.core.math;\n\nimport static java.lang.Math.sqrt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\n\npublic class VectorXYZ implements Vector3D, IntersectionTestObject {\n\n\tpublic final double x, y, z;\n\n\tpublic VectorXYZ(double x2, double y2, double z2) {\n\t\tthis.x = x2;\n\t\tthis.y = y2;\n\t\tthis.z = z2;\n\t}\n\n\t@Override\n\tpublic double getX() {\n\t\treturn x;\n\t}\n\n\t@Override\n\tpublic double getY() {\n\t\treturn y;\n\t}\n\n\t@Override\n\tpublic double getZ() {\n\t\treturn z;\n\t}\n\n\tpublic double length() {\n\t\treturn Math.sqrt(x*x + y*y + z*z);\n\t}\n\n\tpublic double lengthSquared() {\n\t\treturn x*x + y*y + z*z;\n\t}\n\n\tpublic VectorXYZ normalize() {\n\t\tdouble length = length();\n\t\treturn new VectorXYZ(x / length, y / length, z / length);\n\t}\n\n\tpublic VectorXYZ add(VectorXYZ other) {\n\t\treturn new VectorXYZ(\n\t\t\t\tthis.x + other.x,\n\t\t\t\tthis.y + other.y,\n\t\t\t\tthis.z + other.z);\n\t}\n\t\n\tpublic VectorXYZ add(VectorXZ other) {\n\t\treturn new VectorXYZ(\n\t\t\t\tthis.x + other.x,\n\t\t\t\tthis.y,\n\t\t\t\tthis.z + other.z);\n\t}\n\n\tpublic VectorXYZ add(double x, double y, double z) {\n\t\treturn new VectorXYZ(\n\t\t\t\tthis.x + x,\n\t\t\t\tthis.y + y,\n\t\t\t\tthis.z + z);\n\t}\n\n\tpublic VectorXYZ addY(double y) {\n\t\treturn new VectorXYZ(x, this.y + y, z);\n\t}\n\n\tpublic VectorXYZ subtract(VectorXYZ other) {\n\t\treturn new VectorXYZ(\n\t\t\t\tthis.x - other.x,\n\t\t\t\tthis.y - other.y,\n\t\t\t\tthis.z - other.z);\n\t}\n\n\n\tpublic VectorXYZ subtract(VectorXZ other) {\n\t\treturn new VectorXYZ(\n\t\t\t\tthis.x - other.x,\n\t\t\t\tthis.y,\n\t\t\t\tthis.z - other.z);\n\t}\n\n\tpublic VectorXYZ cross(VectorXYZ other) {\n\t\treturn new VectorXYZ(\n\t\t\t\tthis.y * other.z - this.z * other.y,\n\t\t\t\tthis.z * other.x - this.x * other.z,\n\t\t\t\tthis.x * other.y - this.y * other.x);\n\t}\n\n\t/**\n\t * same result as calling {@link #normalize()} after\n\t * {@link #cross(VectorXYZ)}, but avoids creating a temporary vector\n\t */\n\tpublic VectorXYZ crossNormalized(VectorXYZ other) {\n\t\t\n\t\t//cross\n\t\tdouble x = this.y * other.z - this.z * other.y;\n\t\tdouble y = this.z * other.x - this.x * other.z;\n\t\tdouble z = this.x * other.y - this.y * other.x;\n\t\t\n\t\t//normalize\n\t\tdouble length = sqrt(x*x + y*y + z*z);\n\t\treturn new VectorXYZ(x / length, y / length, z / length);\n\t\t\n\t}\n\n\tpublic double dot(VectorXYZ other) {\n\t\treturn this.x * other.x + this.y * other.y + this.z * other.z;\n\t}\n\n\tpublic VectorXYZ mult(double scalar) {\n\t\treturn new VectorXYZ(x*scalar, y*scalar, z*scalar);\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"(\" + x + \", \" + y + \", \" + z + \")\";\n\t}\n\n\t/**\n\t * returns the result of rotating this vector around the x axis\n\t * @param angleRad  angle in radians\n\t */\n\tpublic VectorXYZ rotateX(double angleRad) {\n\t\tdouble sin = Math.sin(angleRad);\n\t\tdouble cos = Math.cos(angleRad);\n\t\treturn new VectorXYZ(x, y*cos - z*sin, y*sin + z*cos);\n\t}\n\n\t/**\n\t * returns the result of rotating this vector around the y axis\n\t * @param angleRad  angle in radians\n\t */\n\tpublic VectorXYZ rotateY(double angleRad) {\n\t\tdouble sin = Math.sin(angleRad);\n\t\tdouble cos = Math.cos(angleRad);\n\t\treturn new VectorXYZ(sin*z + cos*x, y, cos*z - sin*x);\n\t}\n\n\t/**\n\t * returns the result of rotating this vector around the z axis\n\t * @param angleRad  angle in radians\n\t */\n\tpublic VectorXYZ rotateZ(double angleRad) {\n\t\tdouble sin = Math.sin(angleRad);\n\t\tdouble cos = Math.cos(angleRad);\n\t\treturn new VectorXYZ(x*cos - y*sin, x*sin + y*cos, z);\n\t}\n\n\t/**\n\t * returns the result of rotating this vector around the\n\t * given normalized vector n\n\t * @param angleRad angle in radians\n\t * @param n  normalized vector\n\t */\n\tpublic VectorXYZ rotateVec(double angleRad, VectorXYZ n) {\n\t\tdouble a11 = n.x*n.x*(1 - Math.cos(angleRad)) + Math.cos(angleRad);\n\t\tdouble a12 = n.x*n.y*(1 - Math.cos(angleRad)) - n.z*Math.sin(angleRad);\n\t\tdouble a13 = n.x*n.z*(1 - Math.cos(angleRad)) + n.y*Math.sin(angleRad);\n\t\tdouble a21 = n.y*n.x*(1 - Math.cos(angleRad)) + n.z*Math.sin(angleRad);\n\t\tdouble a22 = n.y*n.y*(1 - Math.cos(angleRad)) + Math.cos(angleRad);\n\t\tdouble a23 = n.y*n.z*(1 - Math.cos(angleRad)) - n.x*Math.sin(angleRad);\n\t\tdouble a31 = n.z*n.x*(1 - Math.cos(angleRad)) - n.y*Math.sin(angleRad);\n\t\tdouble a32 = n.z*n.y*(1 - Math.cos(angleRad)) + n.x*Math.sin(angleRad);\n\t\tdouble a33 = n.z*n.z*(1 - Math.cos(angleRad)) + Math.cos(angleRad);\n\t\treturn new VectorXYZ(a11*x+a12*y+a13*z, a21*x+a22*y+a23*z, a31*x+a32*y+a33*z);\n\t}\n\t\n\t/**\n\t * returns the result of rotating this vector around a freely chosen\n\t * axis and origin\n\t * @param angleRad angle in radians\n\t * @param rotOrigin  normalized vector for the rotation origin\n\t * @param rotAxis  normalized vector for the rotation axis\n\t */\n\tpublic VectorXYZ rotateVec(double angleRad, VectorXYZ rotOrigin, VectorXYZ rotAxis) {\n\t\tVectorXYZ v = this.subtract(rotOrigin);\n\t\tv = v.rotateVec(angleRad, rotAxis);\n\t\tv = v.add(rotOrigin);\n\t\treturn v;\n\t}\n\t\n\t/**\n\t * calculates the angle between this vector and other,\n\t * but only if both are normalized!\n\t */\n\tpublic double angleTo(VectorXYZ other) {\n\t\treturn Math.acos(this.dot(other));\n\t}\n\t\n\tpublic double distanceTo(VectorXYZ other) {\n\t\t//SUGGEST (performance): don't create temporary vector\n\t\treturn (other.subtract(this)).length();\n\t}\n\t\n\tpublic double distanceToSquared(VectorXYZ other) {\n\t\t//SUGGEST (performance): don't create temporary vector\n\t\treturn (other.subtract(this)).lengthSquared();\n\t}\n\t\n\tpublic double distanceToXZ(VectorXZ other) {\n\t\t//SUGGEST (performance): don't create temporary vector\n\t\treturn VectorXZ.distance(this.xz(), other);\n\t}\n\t\n\tpublic double distanceToXZ(VectorXYZ other) {\n\t\t//SUGGEST (performance): don't create temporary vector\n\t\treturn VectorXZ.distance(this.xz(), other.xz());\n\t}\n\t\n\tpublic VectorXZ xz() {\n\t\treturn new VectorXZ(x, z);\n\t}\n\t\n\tpublic VectorXYZ x(double x) {\n\t\treturn new VectorXYZ(x, this.y, this.z);\n\t}\n\t\n\tpublic VectorXYZ y(double y) {\n\t\treturn new VectorXYZ(this.x, y, this.z);\n\t}\n\n\tpublic VectorXYZ z(double z) {\n\t\treturn new VectorXYZ(this.x, this.y, z);\n\t}\n\n\tpublic VectorXYZ invert() {\n\t\treturn new VectorXYZ(-x, -y, -z);\n\t}\n\t\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\treturn new AxisAlignedBoundingBoxXZ(x, z, x, z);\n\t}\n\t\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (!(obj instanceof VectorXYZ)) {\n\t\t\treturn false;\n\t\t}\n\t\tVectorXYZ other = (VectorXYZ) obj;\n\t\treturn x == other.x && y == other.y && z == other.z;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tlong temp;\n\t\ttemp = Double.doubleToLongBits(x);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\ttemp = Double.doubleToLongBits(y);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\ttemp = Double.doubleToLongBits(z);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\treturn result;\n\t}\n\n\t/**\n\t * creates a VectorXYZ for any Vector3D object.\n\t * If the Vector3D is already a VectorXYZ, this can return the original vector.\n\t */\n\tpublic static VectorXYZ xyz(VectorXYZ vector3D) {\n\t\tif (vector3D instanceof VectorXYZ) {\n\t\t\treturn (VectorXYZ)vector3D;\n\t\t} else {\n\t\t\treturn new VectorXYZ(vector3D.getX(), vector3D.getY(), vector3D.getZ());\n\t\t}\n\t}\n\t\n\tpublic static List<VectorXYZ> addYList(List<VectorXYZ> list, double addY) {\n\t\tList<VectorXYZ> result = new ArrayList<VectorXYZ>(list.size());\n\t\tfor (VectorXYZ listEntry : list) {\n\t\t\tresult.add(listEntry.y(listEntry.y + addY));\n\t\t}\n\t\treturn result;\n\t}\n\t\n\tpublic static final VectorXYZ NULL_VECTOR = new VectorXYZ(0, 0, 0);\n\tpublic static final VectorXYZ X_UNIT = new VectorXYZ(1, 0, 0);\n\tpublic static final VectorXYZ Y_UNIT = new VectorXYZ(0, 1, 0);\n\tpublic static final VectorXYZ Z_UNIT = new VectorXYZ(0, 0, 1);\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/VectorXYZW.java",
    "content": "package org.osm2world.core.math;\n\npublic class VectorXYZW implements Vector4D {\n\n\tpublic final double x, y, z, w;\n\n\tpublic VectorXYZW(double x2, double y2, double z2, double w2) {\n\t\tthis.x = x2;\n\t\tthis.y = y2;\n\t\tthis.z = z2;\n\t\tthis.w = w2;\n\t}\n\t\n\tpublic VectorXYZW(VectorXYZ v, double w2) {\n\t\tthis.x = v.x;\n\t\tthis.y = v.y;\n\t\tthis.z = v.z;\n\t\tthis.w = w2;\n\t}\n\n\t@Override\n\tpublic double getX() {\n\t\treturn x;\n\t}\n\n\t@Override\n\tpublic double getY() {\n\t\treturn y;\n\t}\n\n\t@Override\n\tpublic double getZ() {\n\t\treturn z;\n\t}\n\n\t@Override\n\tpublic double getW() {\n\t\treturn z;\n\t}\n\n\tpublic double length() {\n\t\treturn Math.sqrt(x*x + y*y + z*z + w*w);\n\t}\n\n\tpublic double lengthSquared() {\n\t\treturn x*x + y*y + z*z + w*w;\n\t}\n\n\tpublic VectorXYZW normalize() {\n\t\tdouble length = length();\n\t\treturn new VectorXYZW(x / length, y / length, z / length, w / length);\n\t}\n\n\tpublic VectorXYZW add(VectorXYZW other) {\n\t\treturn new VectorXYZW(\n\t\t\t\tthis.x + other.x,\n\t\t\t\tthis.y + other.y,\n\t\t\t\tthis.z + other.z,\n\t\t\t\tthis.w + other.w);\n\t}\n\n\tpublic VectorXYZW add(double x, double y, double z, double w) {\n\t\treturn new VectorXYZW(\n\t\t\t\tthis.x + x,\n\t\t\t\tthis.y + y,\n\t\t\t\tthis.z + z,\n\t\t\t\tthis.w + w);\n\t}\n\n\tpublic VectorXYZW subtract(VectorXYZW other) {\n\t\treturn new VectorXYZW(\n\t\t\t\tthis.x - other.x,\n\t\t\t\tthis.y - other.y,\n\t\t\t\tthis.z - other.z,\n\t\t\t\tthis.w - other.w);\n\t}\n\n\tpublic VectorXYZW mult(double scalar) {\n\t\treturn new VectorXYZW(x*scalar, y*scalar, z*scalar, w*scalar);\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"(\" + x + \", \" + y + \", \" + z + \", \" + w + \")\";\n\t}\n\t\n\tpublic double distanceTo(VectorXYZW other) {\n\t\t//SUGGEST (performance): don't create temporary vector\n\t\treturn (other.subtract(this)).length();\n\t}\n\t\n\tpublic double distanceToSquared(VectorXYZW other) {\n\t\t//SUGGEST (performance): don't create temporary vector\n\t\treturn (other.subtract(this)).lengthSquared();\n\t}\n\t\n\tpublic VectorXYZW x(double x) {\n\t\treturn new VectorXYZW(x, this.y, this.z, this.w);\n\t}\n\t\n\tpublic VectorXYZW y(double y) {\n\t\treturn new VectorXYZW(this.x, y, this.z, this.w);\n\t}\n\n\tpublic VectorXYZW z(double z) {\n\t\treturn new VectorXYZW(this.x, this.y, z, this.w);\n\t}\n\n\tpublic VectorXYZW invert() {\n\t\treturn new VectorXYZW(-x, -y, -z, -w);\n\t}\n\t\n\tpublic VectorXYZ xyz() {\n\t\treturn new VectorXYZ(x, y, z);\n\t}\n\t\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (!(obj instanceof VectorXYZW)) {\n\t\t\treturn false;\n\t\t}\n\t\tVectorXYZW other = (VectorXYZW) obj;\n\t\treturn x == other.x && y == other.y && z == other.z && w == other.w;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tlong temp;\n\t\ttemp = Double.doubleToLongBits(x);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\ttemp = Double.doubleToLongBits(y);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\ttemp = Double.doubleToLongBits(z);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\ttemp = Double.doubleToLongBits(w);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\treturn result;\n\t}\n\n\t/**\n\t * creates a VectorXYZ for any Vector4D object.\n\t * If the Vector4D is already a VectorXYZ, this can return the original vector.\n\t */\n\tpublic static VectorXYZW xyz(Vector4D vector4D) {\n\t\tif (vector4D instanceof VectorXYZW) {\n\t\t\treturn (VectorXYZW)vector4D;\n\t\t} else {\n\t\t\treturn new VectorXYZW(vector4D.getX(), vector4D.getY(), vector4D.getZ(), vector4D.getW());\n\t\t}\n\t}\n\t\n\tpublic static final VectorXYZW NULL_VECTOR = new VectorXYZW(0, 0, 0, 0);\n\tpublic static final VectorXYZW X_UNIT = new VectorXYZW(1, 0, 0, 0);\n\tpublic static final VectorXYZW Y_UNIT = new VectorXYZW(0, 1, 0, 0);\n\tpublic static final VectorXYZW Z_UNIT = new VectorXYZW(0, 0, 1, 0);\n\tpublic static final VectorXYZW W_UNIT = new VectorXYZW(0, 0, 0, 1);\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/VectorXZ.java",
    "content": "package org.osm2world.core.math;\n\nimport static java.lang.Math.*;\n\nimport java.util.List;\n\nimport com.google.common.base.Function;\nimport com.google.common.collect.Lists;\n\n/**\n * immutable two-dimensional vector with x and z component\n */\npublic class VectorXZ implements Vector3D {\n\n\tpublic final double x;\n\tpublic final double z;\n\t\n\t@Override\n\tpublic double getX() {\n\t\treturn x;\n\t}\n\t\n\t@Override\n\tpublic double getY() {\n\t\treturn 0;\n\t}\n\t\n\t@Override\n\tpublic double getZ() {\n\t\treturn z;\n\t}\n\t\t\n\tpublic VectorXZ(double x, double z) {\n\t\tthis.x = x;\n\t\tthis.z = z;\n\t}\n\n\tpublic double length() {\n\t\treturn Math.sqrt(x*x + z*z);\n\t}\n\n\tpublic double lengthSquared() {\n\t\treturn x*x + z*z;\n\t}\n\t\n\tpublic VectorXZ normalize() {\n\t\tdouble length = length();\n\t\treturn new VectorXZ(x / length, z / length);\n\t}\n\n\t/**\n\t * adds the parameter to this vector and returns the result\n\t */\n\tpublic VectorXZ add(VectorXZ other) {\n\t\treturn new VectorXZ(x + other.x, z + other.z);\n\t}\n\t\n\t/**\n\t * subtracts the parameter from this vector and returns the result\n\t */\n\tpublic VectorXZ subtract(VectorXZ other) {\n\t\treturn new VectorXZ(x - other.x, z - other.z);\n\t}\n\n\tpublic VectorXZ mult(double scalar) {\n\t\treturn new VectorXZ(x*scalar, z*scalar);\n\t}\n\n\tpublic VectorXZ invert() {\n\t\treturn new VectorXZ(-x, -z);\n\t}\n\n\tpublic double dot(VectorXZ other) {\n\t\treturn this.x * other.x + this.z * other.z;\n\t}\n\t\n\t/**\n\t * returns the vector that would result from calculating the\n\t * cross product of this vector (normalized and extended\n\t * to three dimensions) and (0,1,0).\n\t * \n\t * It's the vector that, seen from \"above\", points to the right\n\t * side of this vector and is orthogonal to it.\n\t * \n\t * The resulting vector's length is 1.\n\t */\n\tpublic VectorXZ rightNormal() {\n\t\tdouble length = length();\n\t\treturn new VectorXZ(z / length, -(x / length));\n\t}\n\t\n\tpublic double distanceTo(VectorXZ other) {\n\t\treturn distance(this, other);\n\t}\n\t\n\t/**\n\t * gets this vector's angle relative to (0,1).\n\t * Inverse of {@link #fromAngle(double)}.\n\t * \n\t * @return angle in radians\n\t */\n\tpublic double angle() {\n\t\tif (x == 0 && z == 0) {\n\t\t\treturn 0;\n\t\t} else {\n\t\t\tVectorXZ normalized = this.normalize();\n\t\t\tif (x >= 0) {\n\t\t\t\treturn acos(normalized.dot(Z_UNIT));\n\t\t\t} else {\n\t\t\t\treturn 2*PI - acos(normalized.dot(Z_UNIT));\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * @see #angle()\n\t */\n\tpublic double angleTo(VectorXZ other) {\n\t\treturn other.subtract(this).angle();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"(\" + x + \",\" + z + \")\";\n\t}\n\t\n\tpublic static final VectorXZ NULL_VECTOR = new VectorXZ(0, 0);\n\tpublic static final VectorXZ X_UNIT = new VectorXZ(1, 0);\n\tpublic static final VectorXZ Z_UNIT = new VectorXZ(0, 1);\n\n\n\tpublic VectorXYZ xyz(double y) {\n\t\treturn new VectorXYZ(x, y, z);\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (!(obj instanceof VectorXZ)) {\n\t\t\treturn false;\n\t\t}\n\t\tVectorXZ other = (VectorXZ) obj;\n\t\treturn x == other.x && z == other.z;\n\t}\n\t\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tlong temp;\n\t\ttemp = Double.doubleToLongBits(x);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\ttemp = Double.doubleToLongBits(z);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\treturn result;\n\t}\n\t\n\t//TODO: angle bisection as method in this class\n\t\n\t/**\n\t * returns a unit vector based on an angular direction.\n\t * For example,\n\t * angle 0    creates vector (0,1),\n\t * angle PI/2 creates vector (1,0).\n\t * \n\t * @param directionRad  direction angle in radians\n\t */\n\tpublic static VectorXZ fromAngle(double directionRad) {\n\t\treturn new VectorXZ(\n\t\t\t\tsin(directionRad),\n\t\t\t\tcos(directionRad));\n\t}\n\t\n\t/**\n\t * returns the angle between two direction vectors\n\t * @return  angle as radians, in range 0 to PI\n\t */\n\tpublic static double angleBetween(VectorXZ v1, VectorXZ v2) {\n\t\t\n\t\tdouble rawAngle = abs(v1.angle() - v2.angle());\n\t\t\n\t\tif (rawAngle < PI) {\n\t\t\treturn rawAngle;\n\t\t} else if (rawAngle == PI) {\n\t\t\treturn PI;\n\t\t} else {\n\t\t\treturn PI - (rawAngle % PI);\n\t\t}\n\t\t\n\t}\n\n\tpublic static final double distance(VectorXZ v1, VectorXZ v2) {\n\t\t//SUGGEST (performance): don't create temporary vector\n\t\treturn (v2.subtract(v1)).length();\n\t}\n\t\n\tpublic static final double distanceSquared(VectorXZ v1, VectorXZ v2) {\n\t\t//SUGGEST (performance): don't create temporary vector\n\t\treturn (v2.subtract(v1)).lengthSquared();\n\t}\n\t\n\tpublic static final List<VectorXYZ> listXYZ(List<VectorXZ> vs, final double y) {\n\t\treturn Lists.transform(vs, VectorXZ.xyzFunction(y));\n\t}\n\t\n\tpublic static final Function<VectorXZ, VectorXYZ> xyzFunction(final double y) {\n\t\treturn new Function<VectorXZ, VectorXYZ>() {\n\t\t\t@Override\n\t\t\tpublic VectorXYZ apply(VectorXZ v) {\n\t\t\t\treturn v.xyz(y);\n\t\t\t}\n\t\t};\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/algorithms/CAGUtil.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport static org.osm2world.core.math.JTSConversionUtil.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\n\nimport com.vividsolutions.jts.geom.Geometry;\nimport com.vividsolutions.jts.geom.GeometryCollection;\nimport com.vividsolutions.jts.geom.Polygon;\n\n/**\n * utility class for Constructive Area Geometry (CAG),\n * boolean operations on areas\n */\npublic final class CAGUtil {\n\t\n\tprivate CAGUtil() { }\n\t\n\t/**\n\t * takes a polygon outline, \"subtracts\" a collection of other polygon outlines,\n\t * and returns a collection of polygons that covers the difference area.\n\t * \n\t * The result polygons should cover the area that was\n\t * within the original polygon (excluding its holes),\n\t * but not within a subtracted polygon.\n\t * \n\t * @return\n\t * \t polygons without self-intersections, but maybe with holes\n\t */\n\tpublic static final Collection<PolygonWithHolesXZ> subtractPolygons(\n\t\t\tSimplePolygonXZ basePolygon,\n\t\t\tList<? extends SimplePolygonXZ> subtractPolygons) {\n\t\t\n\t\tList<Geometry> remainingGeometry = Collections.singletonList(\n\t\t\t\t(Geometry)polygonXZToJTSPolygon(basePolygon));\n\t\t\n\t\tfor (SimplePolygonXZ subtractPolygon : subtractPolygons) {\n\t\t\t\n\t\t\tPolygon jtsSubtractPolygon = polygonXZToJTSPolygon(subtractPolygon);\n\t\t\t\n\t\t\tif (!jtsSubtractPolygon.isValid()) continue;\n\t\t\t\n\t\t\tList<Geometry> newRemainingGeometry = new ArrayList<Geometry>(1);\n\t\t\t\n\t\t\tfor (Geometry g : remainingGeometry) {\n\t\t\t\t\n\t\t\t\tGeometry newG = g.difference(jtsSubtractPolygon);\n\t\t\t\t\n\t\t\t\tif (newG instanceof GeometryCollection) {\n\t\t\t\t\tfor (int i = 0; i < ((GeometryCollection)newG).getNumGeometries(); i++) {\n\t\t\t\t\t\tnewRemainingGeometry.add(((GeometryCollection)newG).getGeometryN(i));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tnewRemainingGeometry.add(newG);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tremainingGeometry = newRemainingGeometry;\n\t\t\t\t\t\t\n\t\t}\n\t\t\t\t\n\t\tCollection<PolygonWithHolesXZ> result =\n\t\t\tnew ArrayList<PolygonWithHolesXZ>();\n\t\t\n\t\tfor (Geometry g : remainingGeometry) {\n\t\t\tresult.addAll(polygonsXZFromJTSGeometry(g));\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\n\t/**\n\t * calculates the intersection area of a collection of polygons.\n\t * \n\t * The result polygons should cover the area that was\n\t * within all of the polygons.\n\t */\n\tpublic static final Collection<PolygonWithHolesXZ> intersectPolygons(\n\t\t\tList<? extends SimplePolygonXZ> intersectPolygons) {\n\t\t\n\t\tif (intersectPolygons.isEmpty()) { throw new IllegalArgumentException(); }\n\t\t\t\t\n\t\tGeometry remainingGeometry = null;\n\t\t\n\t\tfor (SimplePolygonXZ poly : intersectPolygons) {\n\t\t\t\n\t\t\tPolygon jtsPoly = polygonXZToJTSPolygon(poly);\n\t\t\t\n\t\t\tif (remainingGeometry == null) {\n\t\t\t\tremainingGeometry = jtsPoly;\n\t\t\t} else {\n\t\t\t\tremainingGeometry = remainingGeometry.intersection(jtsPoly);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn polygonsXZFromJTSGeometry(remainingGeometry);\n\t\t\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/algorithms/EarClippingTriangulationUtil.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport org.osm2world.core.math.GeometryUtil;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXZ;\n\npublic final class EarClippingTriangulationUtil {\n\n\tprivate EarClippingTriangulationUtil() { }\n\t\n\t/**\n\t * triangulates a two-dimensional polygon\n\t * by creating a simple polygon first\n\t * (integrating holes into the polygon outline),\n\t * then using Ear Clipping on that simple polygon.\n\t */\n\tpublic static final List<TriangleXZ> triangulate(\n\t\t\tSimplePolygonXZ polygon,\n\t\t\tCollection<SimplePolygonXZ> holes) {\n\t\t\n\t\t/*\n\t\t * store the outline of the polygon;\n\t\t * additional vertices from holes and terrain will be integrated later.\n\t\t * Use LinkedList for insertion performance.\n\t\t */\n\t\t\n\t\tList<VectorXZ> polygonOutline = new LinkedList<VectorXZ>(polygon.getVertexLoop());\n\t\t\n\t\t/* integrate holes */\n\t\t\n\t\tinsertHolesInPolygonOutline(polygonOutline, holes);\n\t\t\n//\t\tif (simplify) {\n//\t\t * @param simplify  if true, the polygon will be modified in a way\n//\t\t *                  that improves the robustness of the algorithm,\n//\t\t *                  but does not preserve the original shape entirely.\n//\t\t *                  This should be used if a first attempt to triangulate failed.\n//\t\t\tsimplifyOutline(polygonOutline);\n//\t\t}\n\t\t\n\t\t/* triangulate */\n\t\t\n\t\tif (polygonOutline.size() >= 3) {\n\t\t\treturn triangulateSimplePolygon(polygonOutline);\n\t\t} else {\n\t\t\treturn Collections.emptyList();\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * inserts multiple holes into a polygon outline using\n\t * {@link #insertHoleInPolygonOutline(List, SimplePolygonXZ, List)}\n\t * \n\t * TODO: public for debugging purposes\n\t */\n\tpublic static void insertHolesInPolygonOutline(\n\t\t\tList<VectorXZ> polygonOutline, Collection<SimplePolygonXZ> holes) {\n\t\t\n\t\tList<SimplePolygonXZ> remainingHoles = new LinkedList<SimplePolygonXZ>(holes);\n\t\t\n\t\t//It is possible that some holes block other holes' access to the outline.\n\t\t//However, at least one hole will be inserted during each loop execution.\n\t\twhile (!remainingHoles.isEmpty()) {\n\t\t\tfor (Iterator<SimplePolygonXZ> holeIt = remainingHoles.iterator(); holeIt.hasNext(); ) {\n\t\t\t\tSimplePolygonXZ hole = (SimplePolygonXZ) holeIt.next();\n\t\t\t\tboolean success = insertHoleInPolygonOutline(polygonOutline, hole, remainingHoles);\n\t\t\t\tif (success) {\n\t\t\t\t\tholeIt.remove();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * @param polygonOutline  polygon outline; will be modified directly to perform the insertion\n\t * @param hole   hole to be inserted\n\t * @param holes  all holes that haven't been integrated into the polygon yet, including 'hole'\n\t * @return       true if inserting the hole was successful\n\t */\n\tstatic final boolean insertHoleInPolygonOutline(\n\t\t\tList<VectorXZ> polygonOutline, SimplePolygonXZ hole, List<SimplePolygonXZ> holes) {\n\t\t\n\t\tint innerIndex;\n\t\tInteger outerIndex = null;\n\t\t\n\t\tList<VectorXZ> holeVertices = hole.getVertices();\n\t\t\n\t\tfor (innerIndex = 0; innerIndex < holeVertices.size(); innerIndex++) {\n\t\t\touterIndex = findVisibleOutlineVertex(polygonOutline,\n\t\t\t\t\tholeVertices.get(innerIndex), holes);\n\t\t\tif (outerIndex != null) { break; }\n\t\t}\n\t\t\n\t\tif (outerIndex == null) {\n\t\t\treturn false;\n\t\t} else {\n\n\t\t\tSimplePolygonXZ outerPolygon = new SimplePolygonXZ(new ArrayList<VectorXZ>(polygonOutline)); //TODO: avoid creating this every time\n\t\t\t\n\t\t\tpolygonOutline.add(outerIndex+1, polygonOutline.get(outerIndex));\n\t\t\t\n\t\t\tif (hole.isClockwise() ^ outerPolygon.isClockwise()) {\n\t\t\t\tpolygonOutline.addAll(outerIndex+1,\n\t\t\t\t\t\trearrangeOutline(hole.getVertexLoop(), innerIndex, false));\n\t\t\t} else {\n\t\t\t\tpolygonOutline.addAll(outerIndex+1,\n\t\t\t\t\t\trearrangeOutline(hole.getVertexLoop(), innerIndex, true));\n\t\t\t}\n\t\t\t\n\t\t\treturn true;\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * rearranges a polygon outline.\n\t * \n\t * @param outline  old polygon outline, will not be modified.\n\t *                 First and last vertex need to be identical.\n\t * @param newStart index of the new first/last outline vertex\n\t * @param invert   determines whether the order of vertices is inverted\n\t * @return  New polygon outline.\n\t * \t\t\tThe new first and last vertex are identical.\n\t * \n\t */\n\tstatic final List<VectorXZ> rearrangeOutline(\n\t\t\tList<VectorXZ> outline, int newStart, boolean invert) {\n\t\t\t\t\n\t\tList<VectorXZ> newOutline = new ArrayList<VectorXZ>(outline.size());\n\t\t\n\t\tboolean complete = false;\n\t\tboolean oldStartAdded = false;\n\t\tint currentIndex = newStart;\n\t\t\n\t\twhile (!complete) {\n\t\t\t\n\t\t\t/* add vertex to new outline */\n\t\t\t\n\t\t\tif (currentIndex == 0 || currentIndex == outline.size() - 1) {\n\t\t\t\tif (!oldStartAdded) {//remove original duplication of first/last node\n\t\t\t\t\tnewOutline.add(outline.get(0));\n\t\t\t\t\toldStartAdded = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnewOutline.add(outline.get(currentIndex));\n\t\t\t}\n\t\t\t\n\t\t\t/* shift current index */\n\t\t\t\n\t\t\tif (!invert) {\n\t\t\t\tcurrentIndex += 1;\n\t\t\t\tif (currentIndex >= outline.size()) {\n\t\t\t\t\tcurrentIndex = 0;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcurrentIndex -= 1;\n\t\t\t\tif (currentIndex < 0) {\n\t\t\t\t\tcurrentIndex = outline.size() - 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (currentIndex == newStart) {\n\t\t\t\tcomplete = true;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tnewOutline.add(outline.get(newStart));\n\t\t\n\t\treturn newOutline;\n\t}\n\n\tstatic final void insertVertexInPolygonOutline(\n\t\t\tList<VectorXZ> polygonOutline, VectorXZ point) {\n\t\t\n\t\tint index = findVisibleOutlineVertex(polygonOutline, point,\n\t\t\t\tCollections.<SimplePolygonXZ>emptyList());\n\t\t\n\t\tpolygonOutline.add(index+1, point);\n\t\tpolygonOutline.add(index+2, polygonOutline.get(index));\n\t\t\n\t}\n\n\t/**\n\t * finds a vertex in the polygon outline that is visible from a given point.\n\t * Visibility means that the connection between the point and the outline\n\t * vertex does not intersect the outline or any hole polygon.\n\t * \n\t * @return  index of a vertex in the polygon outline\n\t *          or null if none was found.\n\t *          The method will always find an outline vertex\n\t *          if there are no holes.\n\t */\n\tstatic final Integer findVisibleOutlineVertex(\n\t\t\tList<VectorXZ> polygonOutline, VectorXZ point,\n\t\t\tIterable<SimplePolygonXZ> holes) {\n\t\t\n\t\t// TODO (performance): replace primitive algorithm with more efficient one\n\t\t\n\t\tint outerIndex = -1;\n\t\t\n\t\tcheckOuterVertex:\n\t\tfor (VectorXZ outerVertex : polygonOutline) {\n\t\t\t\n\t\t\touterIndex += 1;\n\t\t\t\n\t\t\tfor (int i=0; i+1 < polygonOutline.size(); i++) {\n\t\t\t\tif (null != GeometryUtil.getTrueLineSegmentIntersection(\n\t\t\t\t\t\tpoint, outerVertex,\n\t\t\t\t\t\tpolygonOutline.get(i), polygonOutline.get(i+1))) {\n\t\t\t\t\tcontinue checkOuterVertex;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\t\tif (hole.intersects(point, outerVertex)) {\n\t\t\t\t\tcontinue checkOuterVertex;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\treturn outerIndex;\n\t\t\t\n\t\t}\n\t\t\n\t\treturn null;\n\t\t\n\t}\n\n\t/**\n\t * \"simplifies\" the outline by removing vertices that are (almost)\n\t * on a straight edge, or very close to each other,\n\t * because the triangulation method cannot handle these situations\n\t *  - they produce zero-area polygons.\n\t * \n\t * Using this method <em>does</em> affect the result, especially if the\n\t * elevation data associated with the eliminated vertices isn't properly\n\t * represented by linear interpolation between the adjacent vertices.\n\t */\n\tprivate static void simplifyOutline(List<VectorXZ> outline) {\n\t\t\n\t\toutline.remove(outline.size()-1);\n\t\t\n\t\tSet<Integer> removeIndizes = new HashSet<Integer>();\n\t\t\n\t\t/* find vertices on straight lines */\n\t\t\n\t\tfor (int i = 0; i < outline.size(); i++) {\n\t\t\tif (GeometryUtil.distanceFromLineSegment(\n\t\t\t\toutline.get(i),\n\t\t\t\tnew LineSegmentXZ(vertexBefore(outline, i),\n\t\t\t\t\tvertexAfter(outline, i))) < 0.001) {\n\t\t\t\tremoveIndizes.add(i);\n\t\t\t}\n\t\t}\n\t\t\n//\t\tthese didn't have a noticeable effect yet\n//\n//\t\t/* remove two vertices if they are very close and\n//\t\t * also close to each other's position in the sequence */\n//\n//\t\tfor (int i = 0; i < outline.size(); i++) {\n//\t\t\tif (VectorXZ.distance(outline.get(i),\n//\t\t\t\t\tvertexAfter(outline, i)) < 0.001) {\n//\t\t\t\tremoveIndizes.add(i);\n//\t\t\t}\n//\t\t}\n//\n//\t\tfor (int i = 0; i < outline.size(); i++) {\n//\t\t\tif (VectorXZ.distance(vertexBefore(outline, i),\n//\t\t\t\t\tvertexAfter(outline, i)) < 0.001) {\n//\t\t\t\tremoveIndizes.add(indexBefore(outline, i));\n//\t\t\t\tremoveIndizes.add(i);\n//\t\t\t}\n//\t\t}\n\t\t\t\t\n\t\t/* actually remove the vertices */\n\t\t\n\t\tfor (int i = outline.size(); i >= 0; i--) {\n\t\t\tif (removeIndizes.contains(i)) {\n\t\t\t\tSystem.out.println(\"simplify removes vertex #\" + i + \" in outline: \" + outline);\n\t\t\t\toutline.remove(i);\n\t\t\t}\n\t\t}\n\t\t\n\t\t/* restore the polygon loop property */\n\t\t\n\t\toutline.add(outline.get(0));\n\t\t\n\t}\n\t\n\t/**\n\t * Triangulates a simple polygon using Ear Clipping.\n\t * The implementation is based on the paper\n\t * \"Triangulation by Ear Clipping\" by David Eberly.\n\t * \n\t * @param outline  outline of the polygon to triangulate;\n\t *                 can be arbitrarily modified by this method.\n\t */\n\tstatic final List<TriangleXZ> triangulateSimplePolygon(\n\t\t\tList<VectorXZ> outline) {\n\t\t\n\t\tif (outline.size() == 3) {\n\t\t\treturn Collections.singletonList(\n\t\t\t\t\tnew TriangleXZ(outline.get(1), outline.get(2), outline.get(3)));\n\t\t}\n\t\t\t\t\n\t\toutline.remove(0); //TODO: only while first/last vertex is still duplicated\n\t\t\n\t\tList<TriangleXZ> triangles = new ArrayList<TriangleXZ>(outline.size() - 2);\n\t\t\n\t\tboolean progress = true;\n\t\t\n\t\twhile(outline.size() >= 3 && progress) {\n\t\t\tprogress = false;\n\t\t\tfor (int i=0; i < outline.size(); i++) {\n\t\t\t\tif (isEarTip(i, outline)) {\n\t\t\t\t\ttriangles.add(triangleAtTip(i, outline));\n\t\t\t\t\toutline.remove(i);\n\t\t\t\t\tprogress = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (outline.size() >= 3) {\n\t\t\tthrow new InvalidGeometryException(\"failed to triangulate outline.\"\n\t\t\t\t\t+ \"\\nRemaining: \" + outline\n\t\t\t\t\t+ \"\\nTriangles: \" + triangles);\n\t\t}\n\t\t\t\t\t\t\n//\t\tTODO: try to remove the need for progress check\n//      TODO (performance) use better algorithm instead of the n^3 above\n\n\t\treturn triangles;\n\t\t\n\t}\n\n\tstatic boolean isEarTip(int i, List<VectorXZ> outline) {\n\t\t\n\t\tif (isConvex(i, outline)) {\n\t\t\t\n\t\t\tTriangleXZ triangleAtTip = triangleAtTip(i, outline);\n\t\t\t\n\t\t\tfor (VectorXZ vertex : outline) {\n\t\t\t\tif (vertex != triangleAtTip.v1\n\t\t\t\t\t\t&& vertex != triangleAtTip.v2\n\t\t\t\t\t\t&& vertex != triangleAtTip.v3\n\t\t\t\t\t\t&& triangleAtTip.contains(vertex)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\treturn true;\n\t\t\t\n\t\t}\n\t\t\n\t\treturn false;\n\t\t\n\t}\n\n\t/**\n\t * //TODO: outline expects NO duplication of first/last node\n\t */\n\tstatic boolean isConvex(int i, List<VectorXZ> outline) {\n\t\t\n\t\tList<VectorXZ> tempVertices = new ArrayList<VectorXZ>(outline);\n\t\ttempVertices.add(outline.get(0));\n\t\tSimplePolygonXZ tempPolygon = new SimplePolygonXZ(tempVertices);\n\n\t\t//TODO (performance) avoid creating polygon by passing clockwise information\n\t\t\n\t\tVectorXZ segBefore = outline.get(i).subtract(vertexBefore(outline, i));\n\t\tVectorXZ segAfter = vertexAfter(outline, i).subtract(outline.get(i));\n\t\t\n\t\treturn tempPolygon.isClockwise() ^\n\t\t\tsegBefore.z * segAfter.x - segBefore.x * segAfter.z < 0;\n\t\t\n\t}\n\t\t\n\tstatic final TriangleXZ triangleAtTip(int i, List<VectorXZ> outline) {\n\t\t\n\t\treturn new TriangleXZ(vertexBefore(outline, i),\n\t\t\t\toutline.get(i), vertexAfter(outline, i));\n\t\t\n\t}\n\n\tprivate static final VectorXZ vertexBefore(List<VectorXZ> outline, int i) {\n\t\tint beforeI = indexBefore(outline, i);\n\t\treturn outline.get(beforeI);\n\t}\n\n\tprivate static int indexBefore(List<VectorXZ> outline, int i) {\n\t\treturn (outline.size() + i - 1) % outline.size();\n\t}\n\n\tprivate static final VectorXZ vertexAfter(List<VectorXZ> outline, int i) {\n\t\tint afterI = indexAfter(outline, i);\n\t\treturn outline.get(afterI);\n\t}\n\n\tprivate static int indexAfter(List<VectorXZ> outline, int i) {\n\t\treturn (i + 1) % outline.size();\n\t}\n\t\n\t/**\n\t * TODO (documentation)\n\t */\n\tstatic final Collection<SimplePolygonXZ> splitAlong(SimplePolygonXZ splitPolygon,\n\t\t\tCollection<VectorXZ[]> splitLines) {\n\t\t\n\t\tCollection<SimplePolygonXZ> polygons = new ArrayList<SimplePolygonXZ>(1);\n\t\tpolygons.add(splitPolygon);\n\t\t\n\t\tfor (VectorXZ[] splitLine : splitLines) {\n\t\t\t\n\t\t\tCollection<SimplePolygonXZ> newPolygons = new ArrayList<SimplePolygonXZ>(polygons.size());\n\t\t\t\n\t\t\tfor (SimplePolygonXZ polygon : polygons) {\n\t\t\t\t\n\t\t\t\tMap<Integer, VectorXZ> intersectionPos;\n\t\t\t\tMap<Integer, LineSegmentXZ> intersectionPartner;\n\t\t\t\t\n\t\t\t\tfor (int lineVertexI = 0; lineVertexI + 1 < splitLine.length; lineVertexI++) {\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t//TODO: implement (but check whether this method is actually needed first)\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\n\t\t\tpolygons = newPolygons;\n\t\t\t\n\t\t}\n\t\t\n\t\treturn polygons;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/algorithms/JTSTriangulationUtil.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport static java.util.Collections.emptyList;\nimport static org.osm2world.core.math.JTSConversionUtil.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXZ;\n\nimport com.vividsolutions.jts.geom.Coordinate;\nimport com.vividsolutions.jts.geom.CoordinateSequence;\nimport com.vividsolutions.jts.geom.Geometry;\nimport com.vividsolutions.jts.geom.GeometryCollection;\nimport com.vividsolutions.jts.geom.Point;\nimport com.vividsolutions.jts.geom.impl.CoordinateArraySequence;\nimport com.vividsolutions.jts.triangulate.ConformingDelaunayTriangulationBuilder;\n\n/**\n * uses the JTS library for triangulation.\n * Creates a Conforming Delaunay Triangulation with Steiner points!\n */\npublic final class JTSTriangulationUtil {\n\n\tprivate static final Geometry[] EMPTY_GEOM_ARRAY = new Geometry[0];\n\n\tprivate JTSTriangulationUtil() { }\n\t\n\t/**\n\t * triangulation of a polygon with holes, based on a\n\t * conforming delaunay triangulation\n\t */\n\tpublic static final List<TriangleXZ> triangulate(\n\t\t\tSimplePolygonXZ polygon,\n\t\t\tCollection<SimplePolygonXZ> holes) {\n\t\t\n\t\tList<VectorXZ> points = emptyList();\n\t\tList<LineSegmentXZ> segments = emptyList();\n\t\treturn triangulate(polygon, holes, segments, points);\n\t\t\n\t}\n\t\n\t/**\n\t * variant of {@link #triangulate(SimplePolygonXZ, Collection)}\n\t * that accepts some unconnected points within the polygon area\n\t * and will try to create triangle vertices at these points.\n\t * It will also accept line segment as edges that must be integrated\n\t * into the resulting triangulation.\n\t */\n\tpublic static final List<TriangleXZ> triangulate(\n\t\t\tSimplePolygonXZ polygon,\n\t\t\tCollection<SimplePolygonXZ> holes,\n\t\t\tCollection<LineSegmentXZ> segments,\n\t\t\tCollection<VectorXZ> points) {\n\n\t\tTriangulationInputSanitizer.SanitizedPolygonData sanitized =\n\t\t\t\tTriangulationInputSanitizer.sanitize(polygon, holes);\n\t\tpolygon = sanitized.getOuterPolygon();\n\t\tholes = sanitized.getHoles();\n\t\t\t\n\t\tConformingDelaunayTriangulationBuilder triangulationBuilder =\n\t\t\tnew ConformingDelaunayTriangulationBuilder();\n\t\t\n\t\tList<Geometry> constraints =\n\t\t\tnew ArrayList<Geometry>(1 + holes.size() + segments.size());\n\t\t\n\t\tconstraints.add(polygonXZToJTSPolygon(polygon));\n\t\t\n\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\tconstraints.add(polygonXZToJTSPolygon(hole));\n\t\t}\n\t\t\n\t\tfor (LineSegmentXZ segment : segments) {\n\t\t\tconstraints.add(lineSegmentXZToJTSLineString(segment));\n\t\t}\n\t\t\n\t\tArrayList<Point> jtsPoints = new ArrayList<Point>();\n\t\tfor (VectorXZ p : points) {\n\t\t\tCoordinateSequence coordinateSequence =\n\t\t\t\tnew CoordinateArraySequence(new Coordinate[] {\n\t\t\t\t\t\tvectorXZToJTSCoordinate(p)});\n\t\t\tjtsPoints.add(new Point(coordinateSequence, GF));\n\t\t}\n\t\t\n\t\ttriangulationBuilder.setSites(\n\t\t\t\tnew GeometryCollection(jtsPoints.toArray(EMPTY_GEOM_ARRAY), GF));\n\t\ttriangulationBuilder.setConstraints(\n\t\t\t\tnew GeometryCollection(constraints.toArray(EMPTY_GEOM_ARRAY), GF));\n\t\ttriangulationBuilder.setTolerance(0.01);\n\t\t\n\t\t/* run triangulation */\n\t\t\n\t\tGeometry triangulationResult = triangulationBuilder.getTriangles(GF);\n\t\t\n\t\t/* interpret the resulting polygons as triangles,\n\t\t * filter out those which are outside the polygon or in a hole */\n\t\t\n\t\tCollection<PolygonWithHolesXZ> trianglesAsPolygons =\n\t\t\tpolygonsXZFromJTSGeometry(triangulationResult);\n\t\t\n\t\tList<TriangleXZ> triangles = new ArrayList<TriangleXZ>();\n\t\t\n\t\tfor (PolygonWithHolesXZ triangleAsPolygon : trianglesAsPolygons) {\n\t\t\t\n\t\t\tboolean triangleInHole = false;\n\t\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\t\tif (hole.contains(triangleAsPolygon.getOuter().getCenter())) {\n\t\t\t\t\ttriangleInHole = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (!triangleInHole && polygon.contains(\n\t\t\t\t\ttriangleAsPolygon.getOuter().getCenter())) { //TODO: create single method for this query within PolygonWithHoles\n\t\t\t\t\n\t\t\t\ttriangles.add(triangleAsPolygon.asTriangleXZ());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\t\n\t\treturn triangles;\n\t\t\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/algorithms/NormalCalculationUtil.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport static java.util.Arrays.asList;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXYZWithNormals;\nimport org.osm2world.core.math.VectorXYZ;\n\npublic final class NormalCalculationUtil {\n\n\t/** prevents instantiation */\n\tprivate NormalCalculationUtil() {}\n\t\n\t/**\n\t * calculates normals for a collection of triangles\n\t */\n\tpublic static final List<VectorXYZ> calculateTriangleNormals(\n\t\t\tList<VectorXYZ> vertices, boolean smooth) {\n\n\t\tassert vertices.size() % 3 == 0;\n\t\t\n\t\tVectorXYZ[] normals = new VectorXYZ[vertices.size()];\n\t\t\n\t\t//TODO: implement smooth case\n\t\tif (/*!smooth*/ true) { //flat\n\t\t\t\t\n\t\t\tfor (int triangle = 0; triangle < vertices.size() / 3; triangle++) {\n\t\t\t\t\n\t\t\t\tint i = triangle * 3 + 1;\n\t\t\t\t\n\t\t\t\tVectorXYZ vBefore = vertices.get(i-1);\n\t\t\t\tVectorXYZ vAt = vertices.get(i);\n\t\t\t\tVectorXYZ vAfter = vertices.get(i+1);\n\t\t\t\t\n\t\t\t\tVectorXYZ toBefore = vBefore.subtract(vAt);\n\t\t\t\tVectorXYZ toAfter = vAfter.subtract(vAt);\n\t\t\t\t\n\t\t\t\tnormals[i] = toBefore.crossNormalized(toAfter);\n\t\t\t\t\n\t\t\t\tnormals[i-1] = normals[i];\n\t\t\t\tnormals[i+1] = normals[i];\n\t\t\t\t\t\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn asList(normals);\n\t\t\n\t}\n\n\n\tpublic static final List<VectorXYZ> calculateTriangleStripNormals(\n\t\t\tList<VectorXYZ> vertices, boolean smooth) {\n\t\t\n\t\tassert vertices.size() >= 3;\n\t\t\n\t\tVectorXYZ[] normals = calculatePerTriangleNormals(vertices, false);\n\t\treturn asList(normals);\n\t\t\n\t\t//TODO: implement smooth case\n\t\t\n\t}\n\t\n\tpublic static final List<VectorXYZ> calculateTriangleFanNormals(\n\t\t\tList<VectorXYZ> vertices, boolean smooth) {\n\t\t\n\t\tassert vertices.size() >= 3;\n\t\t\n\t\tVectorXYZ[] normals = calculatePerTriangleNormals(vertices, true);\n\t\treturn asList(normals);\n\n\t\t//TODO: implement smooth case\n\t\t\n\t}\n\n\t/**\n\t * calculates \"flat\" lighting normals for triangle strips and triangle fans\n\t * \n\t * @param vertices  fan/strip vertices\n\t * @param fan       true for fans, false for strips\n\t */\n\tprivate static VectorXYZ[] calculatePerTriangleNormals(\n\t\t\tList<VectorXYZ> vertices, boolean fan) {\n\t\t\n\t\tVectorXYZ[] normals = new VectorXYZ[vertices.size()];\n\t\t\n\t\tfor (int triangle = 0; triangle < vertices.size() - 2; triangle++) {\n\t\t\t\n\t\t\tint i = triangle + 1;\n\t\t\t\n\t\t\tVectorXYZ vBefore = vertices.get( fan ? 0 : (i-1) );\n\t\t\tVectorXYZ vAt = vertices.get(i);\n\t\t\tVectorXYZ vAfter = vertices.get(i+1);\n\t\t\t\n\t\t\tVectorXYZ toBefore = vBefore.subtract(vAt);\n\t\t\tVectorXYZ toAfter = vAfter.subtract(vAt);\n\t\t\t\n\t\t\tif (triangle % 2 == 0 || fan) {\n\t\t\t\tnormals[i+1] = toBefore.crossNormalized(toAfter);\n\t\t\t} else {\n\t\t\t\tnormals[i+1] = toAfter.crossNormalized(toBefore);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tnormals[0] = normals[2];\n\t\tnormals[1] = normals[2];\n\t\t\n\t\treturn normals;\n\t\t\n\t}\n\n\tprivate static final double MAX_ANGLE_RADIANS = Math.toRadians(75);\n\t\n\t/**\n\t * calculates normals for vertices that are shared by multiple triangles.\n\t */\n\tpublic static final Collection<TriangleXYZWithNormals> calculateTrianglesWithNormals(\n\t\t\tCollection<TriangleXYZ> triangles) {\n\t\t\n\t\tMap<VectorXYZ, List<TriangleXYZ>> adjacentTriangles =\n\t\t\tcalculateAdjacentTriangles(triangles);\n\t\t\n\t\tCollection<TriangleXYZWithNormals> result =\n\t\t\tnew ArrayList<TriangleXYZWithNormals>(triangles.size());\n\t\t\n\t\tfor (TriangleXYZ triangle : triangles) {\n\t\t\t\n\t\t\tresult.add(new TriangleXYZWithNormals(triangle,\n\t\t\t\t\tcalculateNormal(triangle.v1, triangle, adjacentTriangles),\n\t\t\t\t\tcalculateNormal(triangle.v2, triangle, adjacentTriangles),\n\t\t\t\t\tcalculateNormal(triangle.v3, triangle, adjacentTriangles)));\n\t\t\t\n\t\t}\n\t\t\n\t\treturn result;\n\t\n\t}\n\n\tprivate static VectorXYZ calculateNormal(VectorXYZ v, TriangleXYZ triangle,\n\t\t\tMap<VectorXYZ, List<TriangleXYZ>> adjacentTrianglesMap) {\n\t\t\n\t\t/* find adjacent triangles whose normals are close enough to that of t\n\t\t * and save their normal vectors */\n\n\t\tList<VectorXYZ> relevantNormals = new ArrayList<VectorXYZ>();\n\t\t\n\t\tfor (TriangleXYZ t2 : adjacentTrianglesMap.get(v)) {\n\t\t\t\n\t\t\tif (triangle == t2 ||\n\t\t\t\t\ttriangle.getNormal().angleTo(t2.getNormal()) <= MAX_ANGLE_RADIANS) {\n\n\t\t\t\t//add, unless one of the existing normals is very similar\n\n\t\t\t\tboolean notCoplanar = true;\n\t\t\t\tfor (VectorXYZ n : relevantNormals) {\n\t\t\t\t\tif (n.angleTo(t2.getNormal()) < 0.01) {\n\t\t\t\t\t\tnotCoplanar = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (notCoplanar) {\n\t\t\t\t\trelevantNormals.add(t2.getNormal());\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t\t\n\t\t/* calculate sum of relevant normals,\n\t\t * normalize it and set the result as normal for the vertex */\n\n\t\tVectorXYZ normal = new VectorXYZ(0, 0, 0);\n\t\tfor (VectorXYZ addNormal : relevantNormals) {\n\t\t\tnormal = normal.add(addNormal);\n\t\t}\n\n\t\treturn normal.normalize();\n\n\t}\n\n\tprivate static Map<VectorXYZ, List<TriangleXYZ>> calculateAdjacentTriangles(\n\t\t\tCollection<TriangleXYZ> triangles) {\n\t\t\n\t\tMap<VectorXYZ, List<TriangleXYZ>> result =\n\t\t\tnew HashMap<VectorXYZ, List<TriangleXYZ>>();\n\t\t\n\t\tfor (TriangleXYZ triangle : triangles) {\n\t\t\tfor (VectorXYZ vertex : triangle.getVertices()) {\n\t\t\t\tList<TriangleXYZ> triangleList = result.get(vertex);\n\t\t\t\tif (triangleList == null) {\n\t\t\t\t\ttriangleList = new ArrayList<TriangleXYZ>();\n\t\t\t\t\tresult.put(vertex, triangleList);\n\t\t\t\t}\n\t\t\t\ttriangleList.add(triangle);\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn result;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/algorithms/Poly2TriTriangulationUtil.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport static java.util.Collections.disjoint;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Set;\n\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.util.exception.TriangulationException;\nimport org.poly2tri.Poly2Tri;\nimport org.poly2tri.geometry.polygon.Polygon;\nimport org.poly2tri.geometry.polygon.PolygonPoint;\nimport org.poly2tri.triangulation.TriangulationPoint;\nimport org.poly2tri.triangulation.delaunay.DelaunayTriangle;\nimport org.poly2tri.triangulation.point.TPoint;\n\n/**\n * uses the poly2tri library for triangulation.\n * Creates a Constrained Delaunay Triangulation, not true Delaunay!\n */\npublic final class Poly2TriTriangulationUtil {\n\n\tprivate Poly2TriTriangulationUtil() { }\n\t\t\n\t/**\n\t * triangulates of a polygon with holes.\n\t * \n\t * Accepts some unconnected points within the polygon area\n\t * and will create triangle vertices at these points.\n\t * It will also accept line segments as edges that must be integrated\n\t * into the resulting triangulation.\n\t * @throws TriangulationException if triangulation fails\n\t */\n\tpublic static final List<TriangleXZ> triangulate(\n\t\t\tSimplePolygonXZ outerPolygon,\n\t\t\tCollection<SimplePolygonXZ> holes,\n\t\t\tCollection<LineSegmentXZ> segments,\n\t\t\tCollection<VectorXZ> points) throws TriangulationException {\n\n\t\tTriangulationInputSanitizer.SanitizedPolygonData sanitized =\n\t\t\t\tTriangulationInputSanitizer.sanitize(outerPolygon, holes);\n\t\touterPolygon = sanitized.getOuterPolygon();\n\t\tholes = sanitized.getHoles();\n\t\t\n\t\t/* remove any problematic data (duplicate points) from the input */\n\t\t\n\t\tSet<VectorXZ> knownVectors =\n\t\t\t\tnew HashSet<VectorXZ>(outerPolygon.getVertexCollection());\n\t\t\n\t\tList<SimplePolygonXZ> filteredHoles = new ArrayList<SimplePolygonXZ>();\n\t\t\n\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\t\n\t\t\tif (disjoint(hole.getVertexCollection(), knownVectors)) {\n\t\t\t\tfilteredHoles.add(hole);\n\t\t\t\tknownVectors.addAll(hole.getVertices());\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t//TODO filter segments\n\t\t\n\t\tSet<VectorXZ> filteredPoints = new HashSet<VectorXZ>(points);\n\t\tfilteredPoints.removeAll(knownVectors);\n\t\t\n\t\t// remove points that are *almost* the same as a known vector\n\t\tIterator<VectorXZ> filteredPointsIterator = filteredPoints.iterator();\n\t\twhile (filteredPointsIterator.hasNext()) {\n\t\t\tVectorXZ filteredPoint = filteredPointsIterator.next();\n\t\t\tfor (VectorXZ knownVector : knownVectors) {\n\t\t\t\tif (knownVector.distanceTo(filteredPoint) < 0.2) {\n\t\t\t\t\tfilteredPointsIterator.remove();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t/* run the actual triangulation */\n\t\t\n\t\treturn triangulateFast(outerPolygon, filteredHoles, segments, filteredPoints);\n\t\t\n\t}\n\n\t/**\n\t * variant of {@link #triangulate(SimplePolygonXZ, Collection, Collection, Collection)}\n\t * that does not validate the input. This is obviously faster,\n\t * but the caller needs to make sure that there are no problems.\n\t * @throws TriangulationException if triangulation fails\n\t */\n\tpublic static final List<TriangleXZ> triangulateFast(\n\t\t\tSimplePolygonXZ outerPolygon,\n\t\t\tCollection<SimplePolygonXZ> holes,\n\t\t\tCollection<LineSegmentXZ> segments,\n\t\t\tCollection<VectorXZ> points) throws TriangulationException {\n\t\t\n\t\t/* prepare data for triangulation */\n\t\t\n\t\tPolygon triangulationPolygon = toPolygon(outerPolygon);\n\t\t\n\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\ttriangulationPolygon.addHole(toPolygon(hole));\n\t\t}\n\t\t\n\t\t//TODO collect points and constraints from segments\n\t\t\n\t\tfor (VectorXZ p : points) {\n\t\t\ttriangulationPolygon.addSteinerPoint(toTPoint(p));\n\t\t}\n\t\t\n\t\ttry {\n\t\t\t\n\t\t\t/* run triangulation */\n\t\t\t\n\t\t\tPoly2Tri.triangulate(triangulationPolygon);\n\t\t\t\n\t\t\t/* convert the result to the desired format */\n\t\t\t\n\t\t\tList<DelaunayTriangle> triangles = triangulationPolygon.getTriangles();\n\t\t\t\n\t\t\tList<TriangleXZ> result = new ArrayList<TriangleXZ>(triangles.size());\n\t\t\t\n\t\t\tfor (DelaunayTriangle triangle : triangles) {\n\t\t\t\tresult.add(toTriangleXZ(triangle));\n\t\t\t}\n\t\t\t\t\n\t\t\treturn result;\n\t\t\t\n\t\t} catch (Exception e) {\n\t\t\tthrow new TriangulationException(e);\n\t\t} catch (StackOverflowError e) {\n\t\t\tthrow new TriangulationException(e);\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final TPoint toTPoint(VectorXZ v) {\n\t\treturn new TPoint(v.x, v.z);\n\t}\n\n\tprivate static final VectorXZ toVectorXZ(TriangulationPoint points) {\n\t\treturn new VectorXZ(points.getX(), points.getY());\n\t}\n\n\tprivate static final Polygon toPolygon(SimplePolygonXZ polygon) {\n\t\t\t\t\n\t\tList<PolygonPoint> points = new ArrayList<PolygonPoint>(polygon.size());\n\t\t\n\t\tfor (VectorXZ v : polygon.getVertices()) {\n\t\t\tpoints.add(new PolygonPoint(v.x, v.z));\n\t\t}\n\t\t\n\t\treturn new Polygon(points);\n\t\t\n\t}\n\n\tprivate static final TriangleXZ toTriangleXZ(DelaunayTriangle triangle) {\n\t\t\n\t\treturn new TriangleXZ(\n\t\t\t\ttoVectorXZ(triangle.points[0]),\n\t\t\t\ttoVectorXZ(triangle.points[1]),\n\t\t\t\ttoVectorXZ(triangle.points[2]));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/algorithms/TriangulationInputSanitizer.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport static org.osm2world.core.math.GeometryUtil.distanceFromLineSegment;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.util.TouchMapperProfile;\nimport org.osm2world.core.util.TriangulationConfig;\n\n/**\n * Removes near-collinear vertices from triangulation inputs.\n *\n * This improves triangulation robustness for source data that contains\n * very small kinks or almost-straight vertex chains.\n */\npublic final class TriangulationInputSanitizer {\n\n\tpublic static final class SanitizedPolygonData {\n\t\tprivate final SimplePolygonXZ outerPolygon;\n\t\tprivate final List<SimplePolygonXZ> holes;\n\t\tprivate final int removedOuterVertices;\n\t\tprivate final int removedHoleVertices;\n\t\tprivate final int fallbackRings;\n\n\t\tprivate SanitizedPolygonData(\n\t\t\t\tSimplePolygonXZ outerPolygon,\n\t\t\t\tList<SimplePolygonXZ> holes,\n\t\t\t\tint removedOuterVertices,\n\t\t\t\tint removedHoleVertices,\n\t\t\t\tint fallbackRings) {\n\t\t\tthis.outerPolygon = outerPolygon;\n\t\t\tthis.holes = holes;\n\t\t\tthis.removedOuterVertices = removedOuterVertices;\n\t\t\tthis.removedHoleVertices = removedHoleVertices;\n\t\t\tthis.fallbackRings = fallbackRings;\n\t\t}\n\n\t\tpublic SimplePolygonXZ getOuterPolygon() {\n\t\t\treturn outerPolygon;\n\t\t}\n\n\t\tpublic List<SimplePolygonXZ> getHoles() {\n\t\t\treturn holes;\n\t\t}\n\n\t\tpublic int getRemovedOuterVertices() {\n\t\t\treturn removedOuterVertices;\n\t\t}\n\n\t\tpublic int getRemovedHoleVertices() {\n\t\t\treturn removedHoleVertices;\n\t\t}\n\n\t\tpublic int getRemovedTotalVertices() {\n\t\t\treturn removedOuterVertices + removedHoleVertices;\n\t\t}\n\n\t\tpublic int getFallbackRingCount() {\n\t\t\treturn fallbackRings;\n\t\t}\n\t}\n\n\tprivate static final class RingSimplifyResult {\n\t\tprivate final SimplePolygonXZ polygon;\n\t\tprivate final int removedVertices;\n\t\tprivate final boolean fellBackToOriginal;\n\n\t\tprivate RingSimplifyResult(\n\t\t\t\tSimplePolygonXZ polygon,\n\t\t\t\tint removedVertices,\n\t\t\t\tboolean fellBackToOriginal) {\n\t\t\tthis.polygon = polygon;\n\t\t\tthis.removedVertices = removedVertices;\n\t\t\tthis.fellBackToOriginal = fellBackToOriginal;\n\t\t}\n\t}\n\n\tprivate TriangulationInputSanitizer() { }\n\n\tpublic static SanitizedPolygonData sanitize(\n\t\t\tSimplePolygonXZ outerPolygon,\n\t\t\tCollection<SimplePolygonXZ> holes) {\n\n\t\tdouble toleranceMeters = TriangulationConfig.getCollinearToleranceMeters();\n\t\tSanitizedPolygonData result =\n\t\t\t\tsanitize(outerPolygon, holes, toleranceMeters);\n\n\t\tif (result.getRemovedTotalVertices() > 0\n\t\t\t\t|| result.getFallbackRingCount() > 0) {\n\t\t\tTouchMapperProfile.logValue(\n\t\t\t\t\t\"triangulation.collinear_tolerance_m\",\n\t\t\t\t\ttoleranceMeters);\n\t\t\tTouchMapperProfile.logValue(\n\t\t\t\t\t\"triangulation.collinear_removed_outer_vertices\",\n\t\t\t\t\tresult.getRemovedOuterVertices());\n\t\t\tTouchMapperProfile.logValue(\n\t\t\t\t\t\"triangulation.collinear_removed_hole_vertices\",\n\t\t\t\t\tresult.getRemovedHoleVertices());\n\t\t\tTouchMapperProfile.logValue(\n\t\t\t\t\t\"triangulation.collinear_fallback_rings\",\n\t\t\t\t\tresult.getFallbackRingCount());\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tstatic SanitizedPolygonData sanitize(\n\t\t\tSimplePolygonXZ outerPolygon,\n\t\t\tCollection<SimplePolygonXZ> holes,\n\t\t\tdouble toleranceMeters) {\n\n\t\tif (!(toleranceMeters > 0)) {\n\t\t\tList<SimplePolygonXZ> unchangedHoles =\n\t\t\t\t\tnew ArrayList<SimplePolygonXZ>(holes);\n\t\t\treturn new SanitizedPolygonData(\n\t\t\t\t\touterPolygon, unchangedHoles, 0, 0, 0);\n\t\t}\n\n\t\tRingSimplifyResult simplifiedOuter =\n\t\t\t\tsimplifyRing(outerPolygon, toleranceMeters);\n\n\t\tList<SimplePolygonXZ> simplifiedHoles =\n\t\t\t\tnew ArrayList<SimplePolygonXZ>(holes.size());\n\n\t\tint removedHoles = 0;\n\t\tint fallbackRings = simplifiedOuter.fellBackToOriginal ? 1 : 0;\n\n\t\tfor (SimplePolygonXZ hole : holes) {\n\t\t\tRingSimplifyResult simplifiedHole = simplifyRing(hole, toleranceMeters);\n\t\t\tsimplifiedHoles.add(simplifiedHole.polygon);\n\t\t\tremovedHoles += simplifiedHole.removedVertices;\n\t\t\tif (simplifiedHole.fellBackToOriginal) {\n\t\t\t\tfallbackRings += 1;\n\t\t\t}\n\t\t}\n\n\t\treturn new SanitizedPolygonData(\n\t\t\t\tsimplifiedOuter.polygon,\n\t\t\t\tsimplifiedHoles,\n\t\t\t\tsimplifiedOuter.removedVertices,\n\t\t\t\tremovedHoles,\n\t\t\t\tfallbackRings);\n\t}\n\n\tprivate static RingSimplifyResult simplifyRing(\n\t\t\tSimplePolygonXZ originalRing,\n\t\t\tdouble toleranceMeters) {\n\n\t\tList<VectorXZ> vertices =\n\t\t\t\tnew ArrayList<VectorXZ>(originalRing.getVertices());\n\n\t\tif (vertices.size() <= 3) {\n\t\t\treturn new RingSimplifyResult(originalRing, 0, false);\n\t\t}\n\n\t\tint removed = 0;\n\t\tboolean removedInThisPass = true;\n\n\t\twhile (removedInThisPass && vertices.size() > 3) {\n\t\t\tremovedInThisPass = false;\n\t\t\tint size = vertices.size();\n\n\t\t\tfor (int i = 0; i < size; i++) {\n\t\t\t\tVectorXZ previous = vertices.get((size + i - 1) % size);\n\t\t\t\tVectorXZ current = vertices.get(i);\n\t\t\t\tVectorXZ next = vertices.get((i + 1) % size);\n\n\t\t\t\tdouble distance = distanceFromLineSegment(\n\t\t\t\t\t\tcurrent, new LineSegmentXZ(previous, next));\n\n\t\t\t\tif (distance >= toleranceMeters) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tList<VectorXZ> candidate =\n\t\t\t\t\t\tnew ArrayList<VectorXZ>(vertices);\n\t\t\t\tcandidate.remove(i);\n\n\t\t\t\tif (candidate.size() < 3) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif (!isValidSimplePolygon(candidate)) {\n\t\t\t\t\t// If simplification would break polygon validity,\n\t\t\t\t\t// keep the original ring unchanged.\n\t\t\t\t\treturn new RingSimplifyResult(originalRing, 0, true);\n\t\t\t\t}\n\n\t\t\t\tvertices = candidate;\n\t\t\t\tremoved += 1;\n\t\t\t\tremovedInThisPass = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (removed == 0) {\n\t\t\treturn new RingSimplifyResult(originalRing, 0, false);\n\t\t}\n\n\t\ttry {\n\t\t\treturn new RingSimplifyResult(\n\t\t\t\t\tnew SimplePolygonXZ(toLoop(vertices)),\n\t\t\t\t\tremoved,\n\t\t\t\t\tfalse);\n\t\t} catch (InvalidGeometryException e) {\n\t\t\treturn new RingSimplifyResult(originalRing, 0, true);\n\t\t}\n\t}\n\n\tprivate static boolean isValidSimplePolygon(List<VectorXZ> vertices) {\n\t\ttry {\n\t\t\tnew SimplePolygonXZ(toLoop(vertices));\n\t\t\treturn true;\n\t\t} catch (InvalidGeometryException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate static List<VectorXZ> toLoop(List<VectorXZ> vertices) {\n\t\tif (vertices.isEmpty()) {\n\t\t\treturn Collections.emptyList();\n\t\t}\n\t\tList<VectorXZ> loop = new ArrayList<VectorXZ>(vertices.size() + 1);\n\t\tloop.addAll(vertices);\n\t\tloop.add(vertices.get(0));\n\t\treturn loop;\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/algorithms/TriangulationUtil.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.util.TriangulationConfig;\n\nimport com.vividsolutions.jts.triangulate.ConstraintEnforcementException;\n\n/**\n * triangulation utility class that uses {@link EarClippingTriangulationUtil},\n * and, as a fallback, {@link JTSTriangulationUtil}\n */\npublic class TriangulationUtil {\n\t\n\t/**\n\t * triangulates a two-dimensional polygon with holes and unconnected points.\n\t */\n\tpublic static final List<TriangleXZ> triangulate(\n\t\t\tSimplePolygonXZ outerPolygon,\n\t\t\tCollection<SimplePolygonXZ> holes,\n\t\t\tCollection<VectorXZ> points) {\n\n\t\treturn triangulate(outerPolygon, holes, points,\n\t\t\t\tTriangulationConfig.getCollinearToleranceMeters());\n\n\t}\n\n\tstatic final List<TriangleXZ> triangulate(\n\t\t\tSimplePolygonXZ outerPolygon,\n\t\t\tCollection<SimplePolygonXZ> holes,\n\t\t\tCollection<VectorXZ> points,\n\t\t\tdouble collinearToleranceMeters) {\n\n\t\tTriangulationInputSanitizer.SanitizedPolygonData sanitized =\n\t\t\t\tTriangulationInputSanitizer.sanitize(\n\t\t\t\t\t\touterPolygon, holes, collinearToleranceMeters);\n\t\touterPolygon = sanitized.getOuterPolygon();\n\t\tholes = sanitized.getHoles();\n\t\t\n\t\tif (points.isEmpty() && outerPolygon.size() <= 100) {\n\t\t\t\n\t\t\ttry {\n\t\t\t\t\n\t\t\t\treturn EarClippingTriangulationUtil.triangulate(outerPolygon, holes);\n\t\t\t\t\n\t\t\t} catch (InvalidGeometryException e) {\n\t\t\t\t\n\t\t\t\t//TODO (error handling): log failed triangulations properly (as info)\n\t\t\t\t\n\t\t\t\t// e.printStackTrace();\n\t\t\t\t// System.err.println(\"outer: \" + outerPolygon);\n\t\t\t\t// System.err.println(\"holes: \" + holes);\n\t\t\t\t// System.err.println(\"using JTS triangulation instead\");\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* use JTS if there are unconnected points, or as a fallback */\n\t\t\n\t\ttry {\n\n\t\t\treturn JTSTriangulationUtil.triangulate(outerPolygon, holes,\n\t\t\t\t\tCollections.<LineSegmentXZ>emptyList(), points);\n\t\t\t\n\t\t} catch (ConstraintEnforcementException e2) {\n\t\t\t\n\t\t\te2.printStackTrace();\n\t\t\tSystem.err.println(\"outer: \" + outerPolygon);\n\t\t\tSystem.err.println(\"holes: \" + holes);\n\t\t\tSystem.err.println(\"JTS triangulation failed, returning empty list\");\n\t\t\t\n\t\t\treturn Collections.emptyList();\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * triangulates a two-dimensional polygon with holes.\n\t */\n\tpublic static final List<TriangleXZ> triangulate(\n\t\t\tSimplePolygonXZ outerPolygon,\n\t\t\tCollection<SimplePolygonXZ> holes) {\n\t\t\n\t\treturn triangulate(outerPolygon, holes,\n\t\t\t\tCollections.<VectorXZ>emptyList());\n\t\t\n\t}\n\t\n\t/**\n\t * @see #triangulate(SimplePolygonXZ, Collection)\n\t */\n\tpublic static final List<TriangleXZ> triangulate(\n\t\t\tPolygonWithHolesXZ polygon,\n\t\t\tCollection<VectorXZ> points) {\n\t\t\n\t\treturn triangulate(polygon.getOuter(), polygon.getHoles(), points);\n\t\t\n\t}\n\t\n\t/**\n\t * @see #triangulate(SimplePolygonXZ, Collection)\n\t */\n\tpublic static final List<TriangleXZ> triangulate(\n\t\t\tPolygonWithHolesXZ polygon) {\n\t\t\n\t\treturn triangulate(polygon.getOuter(), polygon.getHoles());\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/datastructures/IntersectionGrid.java",
    "content": "package org.osm2world.core.math.datastructures;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Iterator;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\n\n/**\n * a data structure that can be used to speed up intersection tests.\n * \n * An IntersectionTestObject is added to all grid cells that are at least\n * partially covered by the object's axis-aligned bounding box.\n * When testing for intersections or inclusions, only elements in the same\n * cell need to be compared.\n */\npublic class IntersectionGrid<T extends IntersectionTestObject> {\n\t\n\tprivate final AxisAlignedBoundingBoxXZ gridBounds;\n\tprivate Collection<T>[][] cells;\n\t\n\tprivate final int cellCountX, cellCountZ;\n\t\n\tprivate final double cellSizeX, cellSizeZ;\n\t\n\tpublic IntersectionGrid(AxisAlignedBoundingBoxXZ gridBounds,\n\t\t\tint cellCountX, int cellCountZ) {\n\t\t\n\t\tthis.gridBounds = gridBounds;\n\n\t\t@SuppressWarnings(\"unchecked\") //cannot create generic array\n\t\tCollection<T>[][] newCells\n\t\t\t= new Collection[cellCountX][cellCountZ];\n\t\t\n\t\tthis.cells = newCells;\n\t\t\n\t\tthis.cellCountX = cellCountX;\n\t\tthis.cellCountZ = cellCountZ;\n\t\t\n\t\tthis.cellSizeX = gridBounds.sizeX() / cellCountX;\n\t\tthis.cellSizeZ = gridBounds.sizeZ() / cellCountZ;\n\t\t\t\t\t\t\n\t}\n\t\n\t/**\n\t * alternative constructor that uses a target cell size to calculate\n\t * the number of cells\n\t */\n\tpublic IntersectionGrid(AxisAlignedBoundingBoxXZ gridBounds,\n\t\t\tdouble approxCellSizeX, double approxCellSizeZ) {\n\t\tthis(gridBounds,\n\t\t\t\t((int) (gridBounds.sizeX() / approxCellSizeX)) + 1,\n\t\t\t\t((int) (gridBounds.sizeZ() / approxCellSizeZ)) + 1);\n\t}\n\t\n\tpublic Collection<T>[][] getCellArray() {\n\t\treturn cells;\n\t}\n\n\t/**\n\t * returns the content object collections for all non-empty cells\n\t */\n\tpublic Iterable<Collection<T>> getCells() {\n\t\treturn new Iterable<Collection<T>>() {\n\t\t\t@Override public Iterator<Collection<T>> iterator() {\n\t\t\t\treturn new CellIterator();\n\t\t\t}\n\t\t};\n\t}\n\t\n\t/**\n\t * read-only iterator for non-null cells\n\t */\n\tprivate class CellIterator implements Iterator<Collection<T>> {\n\n\t\tint x =  0;\n\t\tint z = -1;\n\t\t\n\t\tpublic CellIterator() {\n\t\t\ttoNext();\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic boolean hasNext() {\n\t\t\treturn x < cellCountX;\n\t\t}\n\n\t\t@Override\n\t\tpublic Collection<T> next() {\n\t\t\tCollection<T> result = cells[x][z];\n\t\t\ttoNext();\n\t\t\treturn result;\n\t\t}\n\n\t\t@Override\n\t\tpublic void remove() {\n\t\t\tthrow new UnsupportedOperationException();\n\t\t}\n\t\t\n\t\tprivate void toNext() {\n\t\t\tdo {\n\t\t\t\tz ++;\n\t\t\t\tif (z >= cellCountZ) {\n\t\t\t\t\tz = 0;\n\t\t\t\t\tx ++;\n\t\t\t\t}\n\t\t\t} while (x < cellCountX && cells[x][z] == null);\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * returns all non-empty cells that would contain the object.\n\t * Will not modify the intersection grid, and it doesn't matter\n\t * whether the object has been inserted or not.\n\t */\n\tpublic Collection<Collection<T>> cellsFor(\n\t\t\tIntersectionTestObject object) {\n\n\t\tassert(gridBounds.contains(object));\n\n\t\tAxisAlignedBoundingBoxXZ objectAABB = object.getAxisAlignedBoundingBoxXZ();\n\t\t\n\t\tint minCellX = cellXForCoord(objectAABB.minX, objectAABB.minZ);\n\t\tint minCellZ = cellZForCoord(objectAABB.minX, objectAABB.minZ);\n\t\tint maxCellX = cellXForCoord(objectAABB.maxX, objectAABB.maxZ);\n\t\tint maxCellZ = cellZForCoord(objectAABB.maxX, objectAABB.maxZ);\n\t\t\n\t\tCollection<Collection<T>> result =\n\t\t\tnew ArrayList<Collection<T>>(\n\t\t\t\t\t(maxCellX - minCellX) * (maxCellZ - minCellZ));\n\t\t\n\t\tfor (int cellX = minCellX; cellX <= maxCellX; cellX ++) {\n\t\t\tfor (int cellZ = minCellZ; cellZ <= maxCellZ; cellZ ++) {\n\t\t\t\tif (cells[cellX][cellZ] != null) {\n\t\t\t\t\tresult.add(cells[cellX][cellZ]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\tpublic void insert(T object) {\n\t\t\n\t\tassert(gridBounds.contains(object));\n\t\t\n\t\tAxisAlignedBoundingBoxXZ objectAABB = object.getAxisAlignedBoundingBoxXZ();\n\t\t\n\t\tint minCellX = cellXForCoord(objectAABB.minX, objectAABB.minZ);\n\t\tint minCellZ = cellZForCoord(objectAABB.minX, objectAABB.minZ);\n\t\tint maxCellX = cellXForCoord(objectAABB.maxX, objectAABB.maxZ);\n\t\tint maxCellZ = cellZForCoord(objectAABB.maxX, objectAABB.maxZ);\n\t\t\n\t\tfor (int cellX = minCellX; cellX <= maxCellX; cellX ++) {\n\t\t\tfor (int cellZ = minCellZ; cellZ <= maxCellZ; cellZ ++) {\n\t\t\t\taddToCell(cellX, cellZ, object);\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\n\tprivate void addToCell(int cellX, int cellZ, T object) {\n\t\tif (cells[cellX][cellZ] == null) {\n\t\t\tcells[cellX][cellZ] = new ArrayList<T>();\n\t\t}\n\t\tcells[cellX][cellZ].add(object);\n\t}\n\t\n\tpublic void remove(T object) {\n\t\t\n\t\tassert(gridBounds.contains(object));\n\t\t\n\t\tAxisAlignedBoundingBoxXZ objectAABB = object.getAxisAlignedBoundingBoxXZ();\n\t\t\n\t\tint minCellX = cellXForCoord(objectAABB.minX, objectAABB.minZ);\n\t\tint minCellZ = cellZForCoord(objectAABB.minX, objectAABB.minZ);\n\t\tint maxCellX = cellXForCoord(objectAABB.maxX, objectAABB.maxZ);\n\t\tint maxCellZ = cellZForCoord(objectAABB.maxX, objectAABB.maxZ);\n\t\t\n\t\tfor (int cellX = minCellX; cellX <= maxCellX; cellX ++) {\n\t\t\tfor (int cellZ = minCellZ; cellZ <= maxCellZ; cellZ ++) {\n\t\t\t\tif (cells[cellX][cellZ] != null) {\n\t\t\t\t\tcells[cellX][cellZ].remove(object);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * returns the x index of the cell that contains the coordinate\n\t */\n\tpublic final int cellXForCoord(double x, double z) {\n\t\treturn (int) ((x - gridBounds.minX) / cellSizeX);\n\t}\n\t\n\t/**\n\t * returns the z index of the cell that contains the coordinate\n\t */\n\tpublic final int cellZForCoord(double x, double z) {\n\t\treturn (int) ((z - gridBounds.minZ) / cellSizeZ);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/datastructures/IntersectionTestObject.java",
    "content": "package org.osm2world.core.math.datastructures;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\n\n/**\n * object which can be inserted into data structures\n * that speed up intersection tests\n */\npublic interface IntersectionTestObject {\n\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/math/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nclasses for geometric objects that are used throughout OSM2World\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/creation/JOSMFileHack.java",
    "content": "package org.osm2world.core.osm.creation;\n\nimport static java.lang.Double.parseDouble;\nimport static java.lang.Math.*;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.parsers.ParserConfigurationException;\nimport javax.xml.transform.Transformer;\nimport javax.xml.transform.TransformerException;\nimport javax.xml.transform.TransformerFactory;\nimport javax.xml.transform.dom.DOMSource;\nimport javax.xml.transform.stream.StreamResult;\n\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport org.xml.sax.SAXException;\n\n/**\n * workaround for the inability of Osmosis to read the JOSM XML format.\n */\npublic final class JOSMFileHack {\n\t\n\tprivate JOSMFileHack() {}\n\t\n\t/**\n\t * Returns true if the file was identified as being generated by JOSM.\n\t * This method peeks into the first lines of the file,\n\t * trying to find the generator tag.\n\t */\n\tpublic static final boolean isJOSMGenerated(File file) {\n\t\t\n\t\ttry {\n\t\t\t\n\t\t\tBufferedReader reader = new BufferedReader(new FileReader(file));\n\t\t\t\n\t\t\tfor (int i=0; i<100; i++) {\n\t\t\t\tString line = reader.readLine();\n\t\t\t\tif (line != null) {\n\t\t\t\t\tif (line.contains(\"generator='JOSM'\")) {\n\t\t\t\t\t\treader.close();\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\treader.close();\n\t\t\t\n\t\t} catch (IOException e) { }\n\t\t\n\t\treturn false;\n\t\t\n\t}\n\t\n\t/**\n\t * creates a temporary file in the .osm format. This removes some\n\t * JOSM-specific attributes present in the original file,\n\t * sets fake versions for unversioned elements,\n\t * and merges multiple bound elements.\n\t * \n\t * The generated file should <em>not</em> be used for anything except\n\t * feeding it to OSM2World.\n\t */\n\tpublic static final File createTempOSMFile(File josmFile) throws\n\t\t\tIOException, ParserConfigurationException, SAXException,\n\t\t\tTransformerException {\n\t\t\n\t\t/* parse original file */\n\t\t\n\t\tDocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();\n\t\tDocumentBuilder db = dbf.newDocumentBuilder();\n\t\tDocument doc = db.parse(josmFile);\n\t\t\n\t\t/* modify DOM */\n\t\t\n\t\tNodeList nodes = doc.getDocumentElement().getChildNodes();\n\t\tList<Node> nodesToDelete = new ArrayList<Node>();\n\t\tList<Element> boundsElements = new ArrayList<Element>();\n\t\t\n\t\tfor (int i = 0; i < nodes.getLength(); i++) {\n\t\t\tif (nodes.item(i) instanceof Element) {\n\t\t\t\tElement element = (Element) nodes.item(i);\n\t\t\t\tif (\"node\".equals(element.getNodeName())\n\t\t\t\t\t\t|| \"way\".equals(element.getNodeName())\n\t\t\t\t\t\t|| \"relation\".equals(element.getNodeName())) {\n\t\t\t\t\tif (\"delete\".equals(element.getAttribute(\"action\"))) {\n\t\t\t\t\t\tnodesToDelete.add(element);\n\t\t\t\t\t} else if (!element.hasAttribute(\"version\")) {\n\t\t\t\t\t\telement.setAttribute(\"version\", \"424242\");\n\t\t\t\t\t}\n\t\t\t\t} else if (\"bounds\".equals(element.getNodeName())) {\n\t\t\t\t\tboundsElements.add(element);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (boundsElements.size() > 1) {\n\t\t\t\n\t\t\tdouble minLat = Double.POSITIVE_INFINITY;\n\t\t\tdouble minLon = Double.POSITIVE_INFINITY;\n\t\t\tdouble maxLat = Double.NEGATIVE_INFINITY;\n\t\t\tdouble maxLon = Double.NEGATIVE_INFINITY;\n\t\t\t\n\t\t\tfor (Element bounds : boundsElements) {\n\t\t\t\tminLat = min(minLat, parseDouble(bounds.getAttribute(\"minlat\")));\n\t\t\t\tminLon = min(minLon, parseDouble(bounds.getAttribute(\"minlon\")));\n\t\t\t\tmaxLat = max(maxLat, parseDouble(bounds.getAttribute(\"maxlat\")));\n\t\t\t\tmaxLon = max(maxLon, parseDouble(bounds.getAttribute(\"maxlon\")));\n\t\t\t}\n\t\t\t\n\t\t\tElement firstBounds = boundsElements.remove(0);\n\t\t\tfirstBounds.setAttribute(\"minlat\", Double.toString(minLat));\n\t\t\tfirstBounds.setAttribute(\"minlon\", Double.toString(minLon));\n\t\t\tfirstBounds.setAttribute(\"maxlat\", Double.toString(maxLat));\n\t\t\tfirstBounds.setAttribute(\"maxlon\", Double.toString(maxLon));\n\t\t\t\n\t\t\tnodesToDelete.addAll(boundsElements);\n\t\t\t\n\t\t\tSystem.out.println(\"WARNING: input file contains multiple <bounds>.\" +\n\t\t\t\t\t\" This can lead to wrong coastlines and other issues.\"); //TODO proper logging\n\t\t\t\n\t\t}\n\t\t\n\t\tfor (Node node : nodesToDelete) {\n\t\t\tdoc.getDocumentElement().removeChild(node);\n\t\t}\n\t\t\n\t\t/* write result */\n\t\t\n\t\tFile tempFile = File.createTempFile(\"workaround\", \".osm\", null);\n\t\ttempFile.deleteOnExit();\n\t\t\n\t\tTransformerFactory tFactory = TransformerFactory.newInstance();\n\t\tTransformer transformer = tFactory.newTransformer();\n\t\t\n\t\tDOMSource source = new DOMSource(doc);\n\t\tStreamResult result = new StreamResult(new FileOutputStream(tempFile));\n\t\ttransformer.transform(source, result);\n\t\t\n\t\treturn tempFile;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/creation/OSMDataReader.java",
    "content": "package org.osm2world.core.osm.creation;\n\nimport org.osm2world.core.osm.data.OSMData;\n\npublic interface OSMDataReader {\n\tpublic OSMData getData();\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/creation/OsmosisReader.java",
    "content": "package org.osm2world.core.osm.creation;\n\nimport static org.openstreetmap.josm.plugins.graphview.core.data.EmptyTagGroup.EMPTY_TAG_GROUP;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.MapBasedTagGroup;\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.openstreetmap.osmosis.core.container.v0_6.EntityContainer;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Bound;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Entity;\nimport org.openstreetmap.osmosis.core.domain.v0_6.EntityType;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Node;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Relation;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Tag;\nimport org.openstreetmap.osmosis.core.domain.v0_6.Way;\nimport org.openstreetmap.osmosis.core.domain.v0_6.WayNode;\nimport org.openstreetmap.osmosis.core.task.v0_6.RunnableSource;\nimport org.openstreetmap.osmosis.core.task.v0_6.Sink;\nimport org.openstreetmap.osmosis.xml.common.CompressionMethod;\nimport org.openstreetmap.osmosis.xml.v0_6.XmlReader;\nimport org.osm2world.core.osm.data.OSMData;\nimport org.osm2world.core.osm.data.OSMElement;\nimport org.osm2world.core.osm.data.OSMMember;\nimport org.osm2world.core.osm.data.OSMNode;\nimport org.osm2world.core.osm.data.OSMRelation;\nimport org.osm2world.core.osm.data.OSMWay;\n\n/**\n * DataSource providing information from a single .osm file. The file is read\n * during the constructor call, there will be no updates when the file is\n * changed later. This class uses osmosis to read the file.\n */\npublic class OsmosisReader implements OSMDataReader {\n\t\n\tprivate boolean complete = false;\n\t\n\tprivate synchronized boolean isComplete() {\n\t\treturn complete;\n\t}\n\t\n\tprivate synchronized void setCompleteTrue() {\n\t\tthis.complete = true;\n\t}\n\t\n\tprivate List<Bound> bounds = new ArrayList<Bound>();\n\tprivate Map<Long, Node> nodesById = new HashMap<Long, Node>();\n\tprivate Map<Long, Way> waysById = new HashMap<Long, Way>();\n\tprivate Map<Long, Relation> relationsById = new HashMap<Long, Relation>();\n\t\n\tprivate Collection<OSMNode> ownNodes;\n\tprivate Collection<OSMWay> ownWays;\n\tprivate Collection<OSMRelation> ownRelations;\n\t\n\tprivate final Sink sinkImplementation = new Sink() {\n\t\tpublic void initialize(Map<String, Object> arg0) {\n\t\t\t/* do nothing */\n\t\t}\n\t\tpublic void release() {\n\t\t\t/* do nothing */\n\t\t}\n\t\tpublic void complete() {\n\t\t\tsetCompleteTrue();\n\t\t}\n\t\tpublic void process(EntityContainer entityContainer) {\n\t\t\tEntity entity = entityContainer.getEntity();\n\t\t\tif (entity instanceof Node) {\n\t\t\t\tnodesById.put(entity.getId(), ((Node) entity));\n\t\t\t} else if (entity instanceof Way) {\n\t\t\t\twaysById.put(entity.getId(), ((Way) entity));\n\t\t\t} else if (entity instanceof Relation) {\n\t\t\t\trelationsById.put(entity.getId(), ((Relation) entity));\n\t\t\t} else if (entity instanceof Bound) {\n\t\t\t\tbounds.add((Bound) entity);\n\t\t\t}\n\t\t}\n\t};\n\t\n\tpublic OsmosisReader(File file) throws IOException {\n\t\t\n\t\tRunnableSource reader = createReaderForFile(file);\n\t\t\n\t\treader.setSink(sinkImplementation);\n\t\t\n\t\tThread readerThread = new Thread(reader);\n\t\treaderThread.start();\n\t\t\n\t\twhile (readerThread.isAlive()) {\n\t\t\ttry {\n\t\t\t\treaderThread.join();\n\t\t\t} catch (InterruptedException e) { /* do nothing */\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (!isComplete()) {\n\t\t\tthrow new IOException(\"couldn't read from file\");\n\t\t}\n\t\t\n\t\tconvertToOwnRepresentation();\n\t\t\n\t}\n\n\tpublic static final RunnableSource createReaderForFile(File file)\n\t\t\tthrows FileNotFoundException {\n\t\t\n\t\tboolean pbf = false;\n\t\tCompressionMethod compression = CompressionMethod.None;\n\t\t\n\t\tif (file.getName().endsWith(\".pbf\")) {\n\t\t\tpbf = true;\n\t\t} else if (file.getName().endsWith(\".gz\")) {\n\t\t\tcompression = CompressionMethod.GZip;\n\t\t} else if (file.getName().endsWith(\".bz2\")) {\n\t\t\tcompression = CompressionMethod.BZip2;\n\t\t}\n\t\t\n\t\tRunnableSource reader;\n\t\t\n\t\tif (pbf) {\n\t\t\treader = new crosby.binary.osmosis.OsmosisReader(\n\t\t\t\t\tnew FileInputStream(file));\n\t\t} else {\n\t\t\treader = new XmlReader(file, false, compression);\n\t\t}\n\t\t\n\t\treturn reader;\n\t\t\n\t}\n\t\n\tprivate void convertToOwnRepresentation() {\n\t\t\n\t\townNodes = new ArrayList<OSMNode>(nodesById.size());\n\t\townWays = new ArrayList<OSMWay>(waysById.size());\n\t\townRelations = new ArrayList<OSMRelation>(relationsById.size());\n\t\t\n\t\tMap<Node, OSMNode> nodeMap = new HashMap<Node, OSMNode>();\n\t\tMap<Way, OSMWay> wayMap = new HashMap<Way, OSMWay>();\n\t\tMap<Relation, OSMRelation> relationMap = new HashMap<Relation, OSMRelation>();\n\t\t\n\t\tfor (Node node : nodesById.values()) {\n\t\t\t\n\t\t\tOSMNode ownNode = new OSMNode(node.getLatitude(), node\n\t\t\t\t\t.getLongitude(), tagGroupForEntity(node), node.getId());\n\t\t\t\n\t\t\townNodes.add(ownNode);\n\t\t\tnodeMap.put(node, ownNode);\n\t\t\t\n\t\t}\n\t\t\n\t\tfor (Way way : waysById.values()) {\n\t\t\t\n\t\t\tList<WayNode> origWayNodes = way.getWayNodes();\n\t\t\tList<OSMNode> wayNodes = new ArrayList<OSMNode>(origWayNodes.size());\n\t\t\tfor (WayNode origWayNode : origWayNodes) {\n\t\t\t\tNode origNode = nodesById.get(origWayNode.getNodeId());\n\t\t\t\tif (origNode != null) {\n\t\t\t\t\twayNodes.add(nodeMap.get(origNode));\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tOSMWay ownWay = new OSMWay(tagGroupForEntity(way),\n\t\t\t\t\tway.getId(), wayNodes);\n\t\t\t\n\t\t\townWays.add(ownWay);\n\t\t\twayMap.put(way, ownWay);\n\t\t\t\n\t\t}\n\t\t\n\t\tfor (Relation relation : relationsById.values()) {\n\t\t\t\n\t\t\tOSMRelation ownRelation = new OSMRelation(\n\t\t\t\t\ttagGroupForEntity(relation), relation.getId(),\n\t\t\t\t\trelation.getMembers().size());\n\t\t\t\n\t\t\townRelations.add(ownRelation);\n\t\t\trelationMap.put(relation, ownRelation);\n\t\t\t\n\t\t}\n\t\t\n\t\t// add relation members\n\t\t// (needs to be done *after* creation because relations can be members\n\t\t// of other relations)\n\t\t\n\t\tfor (Relation relation : relationMap.keySet()) {\n\t\t\t\n\t\t\tOSMRelation ownRelation = relationMap.get(relation);\n\t\t\t\n\t\t\tfor (org.openstreetmap.osmosis.core.domain.v0_6.RelationMember member : relation\n\t\t\t\t\t.getMembers()) {\n\t\t\t\t\t\t\t\t\n\t\t\t\tOSMElement memberObject = null;\n\t\t\t\tif (member.getMemberType() == EntityType.Node) {\n\t\t\t\t\tmemberObject = nodeMap.get(nodesById.get(member\n\t\t\t\t\t\t\t.getMemberId()));\n\t\t\t\t} else if (member.getMemberType() == EntityType.Way) {\n\t\t\t\t\tmemberObject = wayMap.get(waysById\n\t\t\t\t\t\t\t.get(member.getMemberId()));\n\t\t\t\t} else if (member.getMemberType() == EntityType.Relation) {\n\t\t\t\t\tmemberObject = relationMap.get(relationsById.get(member\n\t\t\t\t\t\t\t.getMemberId()));\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (memberObject != null) {\n\t\t\t\t\t\n\t\t\t\t\tOSMMember ownMember = new OSMMember(member\n\t\t\t\t\t\t\t.getMemberRole(), memberObject);\n\t\t\t\t\t\n\t\t\t\t\townRelation.relationMembers.add(ownMember);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t// give up references to original collections\n\t\t\n\t\tnodesById = null;\n\t\twaysById = null;\n\t\trelationsById = null;\n\t\t\n\t}\n\t\n\tprivate TagGroup tagGroupForEntity(Entity entity) {\n\t\tif (entity.getTags().isEmpty()) {\n\t\t\treturn EMPTY_TAG_GROUP;\n\t\t} else {\n\t\t\tMap<String, String> tagMap = new HashMap<String, String>(entity.getTags().size());\n\t\t\tfor (Tag tag : entity.getTags()) {\n\t\t\t\ttagMap.put(tag.getKey(), tag.getValue());\n\t\t\t}\n\t\t\treturn new MapBasedTagGroup(tagMap);\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic OSMData getData() {\n\t\treturn new OSMData(bounds, ownNodes, ownWays, ownRelations);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/data/OSMData.java",
    "content": "package org.osm2world.core.osm.data;\n\nimport java.util.Collection;\n\nimport org.openstreetmap.osmosis.core.domain.v0_6.Bound;\n\n/**\n * OSM dataset containing nodes, areas and relations\n */\npublic class OSMData {\n\n\tprivate final Collection<Bound> bounds;\n\tprivate final Collection<OSMNode> nodes;\n\tprivate final Collection<OSMWay> ways;\n\tprivate final Collection<OSMRelation> relations;\n\t\t\n\tpublic OSMData(Collection<Bound> bounds, Collection<OSMNode> nodes,\n\t\t\tCollection<OSMWay> ways, Collection<OSMRelation> relations) {\n\t\t\n\t\tthis.bounds = bounds;\n\t\tthis.nodes = nodes;\n\t\tthis.ways = ways;\n\t\tthis.relations = relations;\n\t\t\n\t}\n\n\tpublic Collection<OSMNode> getNodes() {\n\t\treturn nodes;\n\t}\n\t\n\tpublic Collection<OSMWay> getWays() {\n\t\treturn ways;\n\t}\n\t\n\tpublic Collection<OSMRelation> getRelations() {\n\t\treturn relations;\n\t}\n\t\t\n\tpublic Collection<Bound> getBounds() {\n\t\treturn bounds;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/data/OSMElement.java",
    "content": "package org.osm2world.core.osm.data;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\n\npublic abstract class OSMElement {\n\n\tpublic final TagGroup tags;\n\tpublic final long id;\n\n\tpublic OSMElement(TagGroup tags, long id) {\n\t\tassert tags != null;\n\t\tthis.tags = tags;\n\t\tthis.id = id;\n\t}\n\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + (int) (id ^ (id >>> 32));\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj)\n\t\t\treturn true;\n\t\tif (obj == null)\n\t\t\treturn false;\n\t\tif (getClass() != obj.getClass())\n\t\t\treturn false;\n\t\tOSMElement other = (OSMElement) obj;\n\t\tif (id != other.id)\n\t\t\treturn false;\n\t\treturn true;\n\t}\n\t\n\t/**\n\t * returns the id, plus an one-letter prefix for the element type\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn \"?\" + id;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/data/OSMMember.java",
    "content": "package org.osm2world.core.osm.data;\n\npublic class OSMMember {\n\t\n\tstatic final boolean useDebugLabels = true;\n\t\n\tpublic final String role;\n\tpublic final OSMElement member;\n\t\n\tpublic OSMMember(String role, OSMElement member) {\n\t\tassert role != null && member != null;\n\t\tthis.role = role;\n\t\tthis.member = member;\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn role + \":\" + member;\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/data/OSMNode.java",
    "content": "package org.osm2world.core.osm.data;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\n\npublic class OSMNode extends OSMElement {\n\tpublic final double lat;\n\tpublic final double lon;\n\t\n\tpublic OSMNode(double lat, double lon, TagGroup tags, long id) {\n\t\tsuper(tags, id);\n\t\tthis.lat = lat;\n\t\tthis.lon = lon;\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"n\" + id;\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/data/OSMRelation.java",
    "content": "package org.osm2world.core.osm.data;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\n\npublic class OSMRelation extends OSMElement {\n\t\t\n\tpublic final List<OSMMember> relationMembers;\n\t\t// content added after constructor call\n\t\n\tpublic OSMRelation(TagGroup tags, long id, int initialMemberSize) {\n\t\tsuper(tags, id);\n\t\tthis.relationMembers =\n\t\t\tnew ArrayList<OSMMember>(initialMemberSize);\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"r\" + id;\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/data/OSMWay.java",
    "content": "package org.osm2world.core.osm.data;\n\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\n\npublic class OSMWay extends OSMElement {\n\t\n\tpublic final List<OSMNode> nodes;\n\t\n\tpublic OSMWay(TagGroup tags, long id, List<OSMNode> nodes) {\n\t\tsuper(tags, id);\n\t\tfor (OSMNode node : nodes) assert node != null;\n\t\tthis.nodes = nodes;\n\t}\n\t\n\tpublic boolean isClosed() {\n\t\treturn nodes.size() > 0 &&\n\t\t\tnodes.get(0).equals(nodes.get(nodes.size()-1));\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"w\" + id;\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nunprocessed OSM map data\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/ruleset/HardcodedRuleset.java",
    "content": "package org.osm2world.core.osm.ruleset;\n\nimport java.util.Collection;\nimport java.util.HashSet;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\n\npublic class HardcodedRuleset implements Ruleset {\n\n\tprivate static Collection<Tag> areaTags = new HashSet<Tag>();\n\tprivate static Collection<String> areaKeys = new HashSet<String>();\n\t\n\tprivate static Collection<Tag> landTags = new HashSet<Tag>();\n\tprivate static Collection<Tag> seaTags = new HashSet<Tag>();\n\t\n\tstatic {\n\t\tareaTags.add(new Tag(\"area\", \"yes\"));\n\t\tareaTags.add(new Tag(\"amenity\", \"fountain\"));\n\t\tareaTags.add(new Tag(\"amenity\", \"parking\"));\n\t\tareaTags.add(new Tag(\"amenity\", \"swimming_pool\"));\n\t\tareaTags.add(new Tag(\"leisure\", \"pitch\"));\n\t\tareaTags.add(new Tag(\"leisure\", \"swimming_pool\"));\n\t\tareaTags.add(new Tag(\"natural\", \"beach\"));\n\t\tareaTags.add(new Tag(\"natural\", \"bay\"));\n\t\tareaTags.add(new Tag(\"natural\", \"sand\"));\n\t\tareaTags.add(new Tag(\"natural\", \"water\"));\n\t\tareaTags.add(new Tag(\"natural\", \"wood\"));\n\t\tareaTags.add(new Tag(\"power\", \"generator\"));\n\t\tareaTags.add(new Tag(\"waterway\", \"riverbank\"));\n\t\tareaTags.add(new Tag(\"ocean\", \"yes\"));\n\t\t\n\t\tareaKeys.add(\"building\");\n\t\tareaKeys.add(\"building:part\");\n\t\tareaKeys.add(\"golf\");\n\t\tareaKeys.add(\"landuse\");\n\n\t\tlandTags.add(new Tag(\"landuse\", \"forest\"));\n\t\tlandTags.add(new Tag(\"landuse\", \"basin\"));\n\t\tlandTags.add(new Tag(\"landuse\", \"reservoir\"));\n\t\tlandTags.add(new Tag(\"natural\", \"bay\"));\n\t\tlandTags.add(new Tag(\"natural\", \"water\"));\n\t\tlandTags.add(new Tag(\"natural\", \"wood\"));\n\t\tlandTags.add(new Tag(\"waterway\", \"river\"));\n\t\tlandTags.add(new Tag(\"waterway\", \"stream\"));\n\t\tlandTags.add(new Tag(\"ocean\", \"yes\"));\n\n\t\tseaTags.add(new Tag(\"maritime\", \"yes\"));\n\t\tseaTags.add(new Tag(\"route\", \"ferry\"));\n\t\tseaTags.add(new Tag(\"seamark\", \"buoy\"));\n\t\tseaTags.add(new Tag(\"seamark:type\", \"buoy_cardinal\"));\n\t\tseaTags.add(new Tag(\"seamark:type\", \"buoy_isolated_danger\"));\n\t\tseaTags.add(new Tag(\"seamark:type\", \"buoy_lateral\"));\n\t\tseaTags.add(new Tag(\"seamark:type\", \"buoy_safe_water\"));\n\t\tseaTags.add(new Tag(\"seamark:type\", \"buoy_special_purpose\"));\n\t\tseaTags.add(new Tag(\"seamark:type\", \"cable_submarine\"));\n\t\tseaTags.add(new Tag(\"submarine\", \"yes\"));\n\t\tseaTags.add(new Tag(\"wetland\", \"tidalflat\"));\n\t}\n\t\n\t@Override\n\tpublic boolean isAreaTag(Tag tag) {\n\t\treturn areaKeys.contains(tag.key)\n\t\t\t|| areaTags.contains(tag);\n\t}\n\t\n\t@Override\n\tpublic boolean isLandTag(Tag tag) {\n\t\treturn landTags.contains(tag);\n\t}\n\t\n\t@Override\n\tpublic boolean isSeaTag(Tag tag) {\n\t\treturn seaTags.contains(tag);\n\t}\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/osm/ruleset/Ruleset.java",
    "content": "package org.osm2world.core.osm.ruleset;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\n\npublic interface Ruleset {\n\t\n\t/** identifies tags that indicate that a closed way represents an area */\n\tpublic boolean isAreaTag(Tag tag);\n\t\n\t/**\n\t * identifies tags which (almost) exclusively appear outside the sea.\n\t * This lets us make a guess whether a tile is a land tile.\n\t */\n\tpublic boolean isLandTag(Tag tag);\n\t\n\t/**\n\t * identifies tags which (almost) exclusively appear on the sea.\n\t * This lets us make a guess whether a tile is a land tile.\n\t */\n\tpublic boolean isSeaTag(Tag tag);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/Renderable.java",
    "content": "package org.osm2world.core.target;\n\n/**\n * object that can be rendered/exported to a target T\n */\npublic interface Renderable {\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/RenderableToAllTargets.java",
    "content": "package org.osm2world.core.target;\n\n/**\n * representation that only uses methods from {@link Target}\n * and can therefore render to all targets supporting these features\n */\npublic interface RenderableToAllTargets extends Renderable {\n\t\n\tpublic void renderTo(Target<?> target);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/Target.java",
    "content": "package org.osm2world.core.target;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXYZWithNormals;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * A sink for rendering/writing {@link WorldObject}s to.\n * \n * @param <R>  subtype of {@link Renderable} designed for visualization\n *             with this target\n */\npublic interface Target<R extends Renderable> {\n\t\n\t/**\n\t * returns the renderable type designed for this target\n\t */\n\tClass<R> getRenderableType();\n\n\tvoid setConfiguration(Configuration config);\n\t\n\t/**\n\t * renders a renderable object to this target.\n\t * Usually, this means calling a \"renderTo\" method on that renderable,\n\t * with this target as a parameter.\n\t */\n\tvoid render(R renderable);\n\t\n\t/**\n\t * announces the begin of the draw* calls for a {@link WorldObject}.\n\t * This allows targets to group them, if desired.\n\t * Otherwise, this can be ignored.\n\t */\n\tvoid beginObject(WorldObject object);\n\t\n\t/**\n\t * draws triangles.\n\t * \n\t * @param texCoordLists  one texture coordinate list per texture.\n\t *          Each must have three coordinates per triangle.\n\t *          Can be null if no texturing information is available.\n\t */\n\tvoid drawTriangles(Material material,\n\t\t\tCollection<? extends TriangleXYZ> triangles,\n\t\t\tList<List<VectorXZ>> texCoordLists);\n\t\n\t/**\n\t * draws triangles with explicitly defined normal vectors.\n\t * \n\t * @see #drawTriangles(Material, Collection, List)\n\t */\n\tvoid drawTrianglesWithNormals(Material material,\n\t\t\tCollection<? extends TriangleXYZWithNormals> triangles,\n\t\t\tList<List<VectorXZ>> texCoordLists);\n\t\n\t/**\n\t * draws a triangle strip.\n\t * \n\t * @param vs             vertices of the triangle strip\n\t * @param texCoordLists  one texture coordinate list per texture.\n\t *          Each must have the same length as the \"vs\" parameter.\n\t *          Can be null if no texturing information is available.\n\t */\n\tvoid drawTriangleStrip(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists);\n\t\n\t/**\n\t * draws a triangle fan.\n\t * \n\t * @see #drawTriangleStrip(Material, List, List)\n\t */\n\tvoid drawTriangleFan(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists);\n\n\t/**\n\t * draws a <em>convex</em> polygon\n\t * \n\t * @see #drawTriangleStrip(Material, List, List)\n\t */\n\tvoid drawConvexPolygon(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists);\n\t\n\t/**\n\t * draws a box with outward-facing polygons.\n\t * \n\t * @param faceDirection  direction for the \"front\" of the box\n\t */\n\tvoid drawBox(Material material,\n\t\t\tVectorXYZ bottomCenter, VectorXZ faceDirection,\n\t\t\tdouble height, double width, double depth);\n\t\n\t/**\n\t * draws a column with outward-facing polygons around a point.\n\t * A column is a polygon with > 3 corners extruded upwards.\n\t * \n\t * The implementation may decide to reduce the number of corners\n\t * in order to improve performance (or make rendering possible\n\t * when a perfect cylinder isn't supported).\n\t * @param corners  number of corners; null creates a cylinder\n\t *  for radiusBottom == radiusTop or (truncated) cone otherwise\n\t */\n\tvoid drawColumn(Material material, Integer corners,\n\t\t\tVectorXYZ base, double height, double radiusBottom,\n\t\t\tdouble radiusTop, boolean drawBottom, boolean drawTop);\n\n\t/**\n\t * gives the target the chance to perform finish/cleanup operations\n\t * after all objects have been drawn.\n\t */\n\tvoid finish();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/TargetUtil.java",
    "content": "package org.osm2world.core.target;\n\nimport static org.osm2world.core.target.statistics.StatisticsTarget.Stat.PRIMITIVE_COUNT;\nimport static org.osm2world.core.util.FaultTolerantIterationUtil.iterate;\n\nimport java.util.Iterator;\n\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.target.common.RenderableToPrimitiveTarget;\nimport org.osm2world.core.target.statistics.StatisticsTarget;\nimport org.osm2world.core.util.IgnoredExceptionLog;\nimport org.osm2world.core.util.TouchMapperProfile;\nimport org.osm2world.core.util.FaultTolerantIterationUtil.Operation;\nimport org.osm2world.core.world.data.WorldObject;\n\npublic final class TargetUtil {\n\n\tprivate TargetUtil() {}\n\t\n\t/**\n\t * render all world objects to a target instance\n\t * that are compatible with that target type\n\t */\n\tpublic static <R extends Renderable> void renderWorldObjects(\n\t\t\tfinal Target<R> target, final MapData mapData,\n\t\t\tfinal boolean renderUnderground) {\n\n\t\tlong totalStart = TouchMapperProfile.start();\n\t\tint renderedCandidates = 0;\n\t\tint ignoredExceptions = 0;\n\t\tint suppressedExceptions = 0;\n\t\tlong exceptionLoggingNanos = 0;\n\t\t\n\t\tfor (MapElement mapElement : mapData.getMapElements()) {\n\t\t\tfor (WorldObject r : mapElement.getRepresentations()) {\n\t\t\t\tif (renderUnderground || r.getGroundState() != GroundState.BELOW) {\n\t\t\t\t\trenderedCandidates++;\n\t\t\t\t\n\t\t\t\t\ttry {\n\t\t\t\t\t\trenderObject(target, r);\n\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\tlong catchStartNanos = TouchMapperProfile.start();\n\t\t\t\t\t\tignoredExceptions++;\n\t\t\t\t\t\tif (IgnoredExceptionLog.shouldLogDetailed(ignoredExceptions)) {\n\t\t\t\t\t\t\tIgnoredExceptionLog.logDetailed(e, mapElement);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif (suppressedExceptions == 0) {\n\t\t\t\t\t\t\t\tIgnoredExceptionLog.logSuppressionNotice(\n\t\t\t\t\t\t\t\t\t\t\"TargetUtil.renderWorldObjects\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsuppressedExceptions++;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (TouchMapperProfile.isEnabled()) {\n\t\t\t\t\t\t\texceptionLoggingNanos += (System.nanoTime() - catchStartNanos);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tIgnoredExceptionLog.logSummary(\"TargetUtil.renderWorldObjects\",\n\t\t\t\tignoredExceptions, suppressedExceptions);\n\t\tTouchMapperProfile.logValue(\"render.world_objects.candidate_count\",\n\t\t\t\trenderedCandidates);\n\t\tTouchMapperProfile.logValue(\"render.world_objects.ignored_exceptions\",\n\t\t\t\tignoredExceptions);\n\t\tTouchMapperProfile.logValue(\"render.world_objects.suppressed_exceptions\",\n\t\t\t\tsuppressedExceptions);\n\t\tTouchMapperProfile.logNanosAsMillis(\n\t\t\t\t\"render.world_objects.exception_logging_ms\", exceptionLoggingNanos);\n\t\tTouchMapperProfile.logMillis(\"render.world_objects.total_ms\", totalStart);\n\t}\n\n\t/**\n\t * render all world objects to a target instances\n\t * that are compatible with that target type.\n\t * World objects are added to a target until the number of primitives\n\t * reaches the primitive threshold, then the next target is retrieved\n\t * from the iterator.\n\t */\n\tpublic static <R extends RenderableToPrimitiveTarget> void renderWorldObjects(\n\t\t\tfinal Iterator<? extends Target<R>> targetIterator,\n\t\t\tfinal MapData mapData, final int primitiveThresholdPerTarget) {\n\t\t\t\t\n\t\tfinal StatisticsTarget primitiveCounter = new StatisticsTarget();\n\t\t\n\t\titerate(mapData.getMapElements(), new Operation<MapElement>() {\n\n\t\t\tTarget<R> currentTarget = targetIterator.next();\n\t\t\t\n\t\t\t@Override public void perform(MapElement e) {\n\t\t\t\tfor (WorldObject r : e.getRepresentations()) {\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\trenderObject(primitiveCounter, r);\n\t\t\t\t\t\n\t\t\t\t\trenderObject(currentTarget, r);\n\t\t\t\t\t\n\t\t\t\t\tif (primitiveCounter.getGlobalCount(PRIMITIVE_COUNT)\n\t\t\t\t\t\t\t>= primitiveThresholdPerTarget) {\n\t\t\t\t\t\tcurrentTarget = targetIterator.next();\n\t\t\t\t\t\tprimitiveCounter.clear();\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t});\n\t\t\n\t}\n\t\n\t/**\n\t * renders any object to a target instance\n\t * if it is a renderable compatible with that target type.\n\t * Also sends {@link Target#beginObject(WorldObject)} calls.\n\t */\n\tpublic static final <R extends Renderable> void renderObject(\n\t\t\tfinal Target<R> target, Object object) {\n\t\t\n\t\tClass<R> renderableType = target.getRenderableType();\n\t\t\n\t\tif (renderableType.isInstance(object)) {\n\t\t\t\n\t\t\tif (object instanceof WorldObject) {\n\t\t\t\ttarget.beginObject((WorldObject)object);\n\t\t\t} else {\n\t\t\t\ttarget.beginObject(null);\n\t\t\t}\n\t\t\t\n\t\t\ttarget.render(renderableType.cast(object));\n\t\t\t\n\t\t} else if (object instanceof RenderableToAllTargets) {\n\t\t\t\n\t\t\tif (object instanceof WorldObject) {\n\t\t\t\ttarget.beginObject((WorldObject)object);\n\t\t\t} else {\n\t\t\t\ttarget.beginObject(null);\n\t\t\t}\n\t\t\t\n\t\t\t((RenderableToAllTargets)object).renderTo(target);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/AbstractTarget.java",
    "content": "package org.osm2world.core.target.common;\n\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.*;\nimport static org.osm2world.core.math.GeometryUtil.*;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.*;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.texCoordLists;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.Renderable;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * superclass for {@link Target} implementations that defines some\n * of the required methods using others. Extending it reduces the number of\n * methods that have to be provided by the implementation\n */\npublic abstract class AbstractTarget<R extends Renderable>\n\t\timplements Target<R> {\n\t\n\tprotected Configuration config;\n\t\n\t@Override\n\tpublic void setConfiguration(Configuration config) {\n\t\tthis.config = config;\n\t}\n\t\n\t@Override\n\tpublic void beginObject(WorldObject object) {}\n\t\t\n\t@Override\n\tpublic void drawBox(Material material,\n\t\t\tVectorXYZ bottomCenter, VectorXZ faceDirection,\n\t\t\tdouble height, double width, double depth) {\n\t\t\n\t\tfinal VectorXYZ backVector = faceDirection.mult(-depth).xyz(0);\n\t\tfinal VectorXYZ rightVector = faceDirection.rightNormal().mult(-width).xyz(0);\n\t\tfinal VectorXYZ upVector = new VectorXYZ(0, height, 0);\n\t\t\n\t\tfinal VectorXYZ frontLowerLeft = bottomCenter\n\t\t\t\t.add(rightVector.mult(-0.5))\n\t\t\t\t.add(backVector.mult(-0.5));\n\t\t\n\t\tfinal VectorXYZ frontLowerRight = frontLowerLeft.add(rightVector);\n\t\tfinal VectorXYZ frontUpperLeft  = frontLowerLeft.add(upVector);\n\t\tfinal VectorXYZ frontUpperRight = frontLowerRight.add(upVector);\n\t\t\n\t\tfinal VectorXYZ backLowerLeft   = frontLowerLeft.add(backVector);\n\t\tfinal VectorXYZ backLowerRight  = frontLowerRight.add(backVector);\n\t\tfinal VectorXYZ backUpperLeft   = frontUpperLeft.add(backVector);\n\t\tfinal VectorXYZ backUpperRight  = frontUpperRight.add(backVector);\n\t\t\n\t\tList<VectorXYZ> vsStrip1 = asList(\n\t\t\t\tbackLowerLeft, backLowerRight,\n\t\t\t\tfrontLowerLeft, frontLowerRight,\n\t\t\t\tfrontUpperLeft, frontUpperRight,\n\t\t\t\tbackUpperLeft, backUpperRight\n\t\t);\n\t\t\n\t\tList<VectorXYZ> vsStrip2 = asList(\n\t\t\t\tfrontUpperRight, frontLowerRight,\n\t\t\t\tbackUpperRight, backLowerRight,\n\t\t\t\tbackUpperLeft, backLowerLeft,\n\t\t\t\tfrontUpperLeft, frontLowerLeft\n\t\t);\n\t\t\n\t\tList<List<VectorXZ>> texCoords1 = null, texCoords2 = null;\n\t\t\n\t\tif (material.getTextureDataList() != null) {\n\t\t\ttexCoords1 = nCopies(material.getTextureDataList().size(), BOX_TEX_COORDS_1);\n\t\t\ttexCoords2 = nCopies(material.getTextureDataList().size(), BOX_TEX_COORDS_2);\n\t\t}\n\t\t\n\t\tdrawTriangleStrip(material, vsStrip1, texCoords1);\n\t\tdrawTriangleStrip(material, vsStrip2, texCoords2);\n\t\t\n\t}\n\t\n\tprotected static final List<VectorXZ> BOX_TEX_COORDS_1 = asList(\n\t\tnew VectorXZ(0,     0), new VectorXZ(0.25,     0),\n\t\tnew VectorXZ(0, 1.0/3), new VectorXZ(0.25, 1.0/3),\n\t\tnew VectorXZ(0, 2.0/3), new VectorXZ(0.25, 2.0/3),\n\t\tnew VectorXZ(0,     1), new VectorXZ(0.25,     1)\n\t);\n\t\n\tprotected static final List<VectorXZ> BOX_TEX_COORDS_2 = asList(\n\t\tnew VectorXZ(0.25, 2.0/3), new VectorXZ(0.25, 1.0/3),\n\t\tnew VectorXZ(0.50, 2.0/3), new VectorXZ(0.50, 1.0/3),\n\t\tnew VectorXZ(0.75, 2.0/3), new VectorXZ(0.75, 1.0/3),\n\t\tnew VectorXZ(1.00, 2.0/3), new VectorXZ(1.00, 1.0/3)\n\t);\n\n\tprivate static final int EDGES_FOR_CYLINDER = 16;\n\t\n\t@Override\n\tpublic void drawColumn(Material material, Integer corners, VectorXYZ base,\n\t\t\tdouble height, double radiusBottom, double radiusTop,\n\t\t\tboolean drawBottom, boolean drawTop) {\n\t\t\n\t\tif (corners == null) {\n\t\t\tcorners = EDGES_FOR_CYLINDER;\n\t\t\tmaterial = material.makeSmooth();\n\t\t}\n\n\t\tfloat angleInterval = (float) (2 * Math.PI / corners);\n\n\t\t/* prepare vector lists for the 3 primitives */\n\t\t\n\t\tList<VectorXYZ> bottomFan = new ArrayList<VectorXYZ>(corners + 2);\n\t\tList<VectorXYZ> topFan = new ArrayList<VectorXYZ>(corners + 2);\n\t\tList<VectorXYZ> mantleStrip = new ArrayList<VectorXYZ>(corners + 2);\n\t\t\n\t\t/* fill vectors into lists */\n\t\t\n\t\tbottomFan.add(base);\n\t\ttopFan.add(base.add(0, height, 0));\n\t\t\n\t\tfor (int i = 0; i <= corners; i++) {\n\t\t\t\n\t\t\tdouble angle = - i * angleInterval;\n\t\t\tdouble sin = Math.sin(angle);\n\t\t\tdouble cos = Math.cos(angle);\n\t\t\t\n\t\t\tVectorXYZ topV = base.add(\n\t\t\t\t\tradiusTop * sin, height, radiusTop * cos);\n\t\t\tVectorXYZ bottomV = base.add(\n\t\t\t\t\tradiusBottom * sin, 0, radiusBottom * cos);\n\n\t\t\tbottomFan.add(bottomV);\n\t\t\ttopFan.add(topV);\n\n\t\t\tmantleStrip.add(topV);\n\t\t\tmantleStrip.add(bottomV);\n\t\t\t\n\t\t}\n\t\t\n\t\tCollections.reverse(bottomFan);\n\t\t\n\t\t/* draw the 3 primitives */\n\n\t\tif (drawBottom) { drawTriangleFan(material, bottomFan,\n\t\t\t\ttexCoordLists(bottomFan, material, GLOBAL_X_Z)); }\n\t\t\n\t\tif (drawTop) { drawTriangleFan(material, topFan,\n\t\t\t\ttexCoordLists(bottomFan, material, GLOBAL_X_Z)); }\n\t\t\n\t\tdrawTriangleStrip(material, mantleStrip,\n\t\t\t\ttexCoordLists(mantleStrip, material, STRIP_WALL));\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void drawTriangleStrip(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tList<List<VectorXZ>> newTexCoordLists = emptyList();\n\t\tif (texCoordLists != null && !texCoordLists.isEmpty()) {\n\t\t\tnewTexCoordLists = new ArrayList<List<VectorXZ>>(texCoordLists.size());\n\t\t\tfor (List<VectorXZ> texCoordList : texCoordLists) {\n\t\t\t\tnewTexCoordLists.add(\n\t\t\t\t\t\ttriangleVertexListFromTriangleStrip(texCoordList));\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t\tdrawTriangles(material, trianglesFromTriangleStrip(vs), newTexCoordLists);\n\t}\n\t\n\t@Override\n\tpublic void drawTriangleFan(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tList<List<VectorXZ>> newTexCoordLists = emptyList();\n\t\tif (texCoordLists != null && !texCoordLists.isEmpty()) {\n\t\t\tnewTexCoordLists = new ArrayList<List<VectorXZ>>(texCoordLists.size());\n\t\t\tfor (List<VectorXZ> texCoordList : texCoordLists) {\n\t\t\t\tnewTexCoordLists.add(\n\t\t\t\t\t\ttriangleVertexListFromTriangleFan(texCoordList));\n\t\t\t}\n\t\t}\n\t\t\n\t\tdrawTriangles(material, trianglesFromTriangleFan(vs), newTexCoordLists);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void drawConvexPolygon(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\tdrawTriangleFan(material, vs, texCoordLists);\n\t}\n\t\n\t@Override\n\tpublic void finish() {}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/FaceTarget.java",
    "content": "package org.osm2world.core.target.common;\n\nimport static java.lang.Math.abs;\nimport static java.util.Collections.nCopies;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXYZWithNormals;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.Renderable;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.world.data.WorldObject;\n\nimport com.google.common.collect.HashMultimap;\nimport com.google.common.collect.Multimap;\n\n/**\n * a target that relies on faces to represent geometry.\n * The faces used by this target are polygons\n * with three or more coplanar vertices.\n * \n * TODO: this currently produces faces that are not convex\n */\npublic abstract class FaceTarget<R extends Renderable>\n\textends AbstractTarget<R> {\n\t\n\tabstract public void drawFace(Material material, List<VectorXYZ> vs,\n\t\t\tList<VectorXYZ> normals, List<List<VectorXZ>> texCoordLists);\n\t\n\t/**\n\t * decides whether faces should be reconstructed from triangulations\n\t * and other primitives.\n\t */\n\tabstract public boolean reconstructFaces();\n\t\n\t/**\n\t * prevents triangles from before the call to be connected with triangles\n\t * after this call when faces are reconstructed.\n\t * \n\t * This is automatically done at the beginning of each new object.\n\t * It only has any effect if {@link #reconstructFaces()} is enabled.\n\t * \n\t * Calling this method at appropriate times can also help to speed up\n\t * performance by lowering the number of candidates for merging.\n\t */\n\tpublic void flushReconstructedFaces() {\n\t\tdrawAndClearCurrentFaces();\n\t}\n\t\n\t/**\n\t * mutable representation of a face\n\t */\n\tprotected final static class Face {\n\t\t\n\t\tpublic final List<VectorXYZ> vs;\n\t\tpublic final List<List<VectorXZ>> texCoordLists;\n\t\tpublic final VectorXYZ normal;\n\t\t\n\t\tpublic Face(List<VectorXYZ> vs,\n\t\t\t\tList<List<VectorXZ>> texCoordLists, VectorXYZ normal) {\n\t\t\t\n\t\t\tthis.vs = vs;\n\t\t\tthis.texCoordLists = texCoordLists;\n\t\t\tthis.normal = normal;\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * @return  true if the triangle has been successfully inserted\n\t\t */\n\t\tpublic boolean tryInsert(IsolatedTriangle t) {\n\t\t\t\n\t\t\tfor (int i = 0; i < vs.size(); i++) {\n\t\t\t\tint j = (i+1) % vs.size();\n\t\t\t\tint k = (i+2) % vs.size();\n\t\t\t\t\n\t\t\t\tif (vs.get(i).equals(t.triangle.v3)\n\t\t\t\t\t\t&& vs.get(j).equals(t.triangle.v2)) { /* TODO tex coords equal */\n\t\t\t\t\t\n\t\t\t\t\tif (vs.get(k).equals(t.triangle.v1)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tremoveVertex(j);\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\tinsertVertex(j, t.triangle.v1,\n\t\t\t\t\t\t\t\ttexCoordLists, t.texCoordOffset + 0);\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\treturn true;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (vs.get(i).equals(t.triangle.v1)\n\t\t\t\t\t\t&& vs.get(j).equals(t.triangle.v3)) { /* TODO tex coords equal */\n\t\t\t\t\t\n\t\t\t\t\tif (vs.get(k).equals(t.triangle.v2)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tremoveVertex(j);\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\tinsertVertex(j, t.triangle.v2,\n\t\t\t\t\t\t\t\ttexCoordLists, t.texCoordOffset + 1);\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\treturn true;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (vs.get(i).equals(t.triangle.v2)\n\t\t\t\t\t\t&& vs.get(j).equals(t.triangle.v1)) { /* TODO tex coords equal */\n\t\t\t\t\t\n\t\t\t\t\tif (vs.get(k).equals(t.triangle.v3)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tremoveVertex(j);\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\tinsertVertex(j, t.triangle.v3,\n\t\t\t\t\t\t\t\ttexCoordLists, t.texCoordOffset + 2);\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\treturn true;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn false;\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic void removeDuplicateEdges() {\n\t\t\t\n\t\t\tboolean repeat = true;\n\t\t\t\n\t\t\twhile (repeat) {\n\t\t\t\t\n\t\t\t\trepeat = false;\n\t\t\t\t\n\t\t\t\tassert vs.size() >= 3;\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i < vs.size(); i++) {\n\t\t\t\t\tint j = (i+1) % vs.size();\n\t\t\t\t\tint k = (i+2) % vs.size();\n\t\t\t\t\t\n\t\t\t\t\t//TODO: what about tex coords?\n\t\t\t\t\tif (vs.get(i).equals(vs.get(k))) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (k > j) {\n\t\t\t\t\t\t\tremoveVertex(k);\n\t\t\t\t\t\t\tremoveVertex(j);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tremoveVertex(j);\n\t\t\t\t\t\t\tremoveVertex(k);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\trepeat = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate void insertVertex(int i, VectorXYZ vertex,\n\t\t\t\tList<List<VectorXZ>> insTexCoordLists, int texCoordPos) {\n\t\t\t\n\t\t\tthis.vs.add(i, vertex);\n\t\t\t\n\t\t\tfor (int list = 0; list < texCoordLists.size(); list++) {\n\t\t\t\t\n\t\t\t\tthis.texCoordLists.get(list).add(i,\n\t\t\t\t\t\tinsTexCoordLists.get(list).get(texCoordPos));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate void removeVertex(int i) {\n\t\t\t\n\t\t\tthis.vs.remove(i);\n\t\t\t\n\t\t\tfor (int list = 0; list < texCoordLists.size(); list++) {\n\t\t\t\tthis.texCoordLists.get(list).remove(i);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn vs.toString();\n\t\t}\n\t\t\n\t}\n\t\n\tprotected final static class IsolatedTriangle {\n\t\t\n\t\tpublic final TriangleXYZ triangle;\n\t\tpublic final VectorXYZ normal;\n\t\tpublic final int texCoordOffset;\n\t\tpublic final List<List<VectorXZ>> texCoordLists;\n\t\t\n\t\tpublic IsolatedTriangle(TriangleXYZ triangle, VectorXYZ normal,\n\t\t\t\tint texCoordOffset, List<List<VectorXZ>> texCoordLists) {\n\t\t\t\n\t\t\tthis.triangle = triangle;\n\t\t\tthis.normal = normal;\n\t\t\tthis.texCoordOffset = texCoordOffset;\n\t\t\tthis.texCoordLists = texCoordLists;\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn triangle.toString();\n\t\t}\n\t\t\n\t}\n\t\n\tprivate final Multimap<Material, IsolatedTriangle> isolatedTriangles =\n\t\t\tHashMultimap.create();\n\t\n\t@Override\n\tpublic void drawTriangles(Material material,\n\t\t\tCollection<? extends TriangleXYZ> triangles,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tint i = 0;\n\t\t\n\t\tfor (TriangleXYZ triangle : triangles) {\n\t\t\t\n\t\t\tif (reconstructFaces()) {\n\t\t\t\t\n\t\t\t\tVectorXYZ n = triangle.getNormal();\n\t\t\t\t\n\t\t\t\tif (Double.isNaN(n.x) || Double.isNaN(n.y) || Double.isNaN(n.z)) {\n\t\t\t\t\tcontinue; //TODO log\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tisolatedTriangles.put(material,\n\t\t\t\t\t\tnew IsolatedTriangle(triangle, n, i*3, texCoordLists));\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tList<List<VectorXZ>> subLists = new ArrayList<List<VectorXZ>>();\n\t\t\t\tfor (List<VectorXZ> list : texCoordLists) {\n\t\t\t\t\tsubLists.add(list.subList(3*i, 3*(i+1)));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tdrawFace(material, triangle.getVertices(), null, subLists);\n\t\t\t}\n\t\t\t\n\t\t\ti++;\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic void drawTrianglesWithNormals(Material material,\n\t\t\tCollection<? extends TriangleXYZWithNormals> triangles,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tdrawTriangles(material, triangles, texCoordLists);\n\t\t\n\t\t//TODO keep normals information\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void drawConvexPolygon(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tif (reconstructFaces()) {\n\t\t\tsuper.drawConvexPolygon(material, vs, texCoordLists);\n\t\t} else {\n\t\t\tdrawFace(material, vs, null, texCoordLists);\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void beginObject(WorldObject object) {\n\t\t\n\t\tdrawAndClearCurrentFaces();\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void finish() {\n\t\t\n\t\tdrawAndClearCurrentFaces();\n\t\t\n\t}\n\t\n\t/**\n\t * integrates all {@link #isolatedTriangles} into faces,\n\t * then draws all faces and clears the collection.\n\t */\n\tprivate void drawAndClearCurrentFaces() {\n\t\t\n\t\tfor (Material material : isolatedTriangles.keySet()) {\n\t\t\t\n\t\t\tCollection<Face> faces = combineTrianglesToFaces(isolatedTriangles.get(material));\n\t\t\t\n\t\t\t/* draw faces */\n\t\t\t\n\t\t\tfor (Face face : faces) {\n\t\t\t\tdrawFace(material, face.vs,\n\t\t\t\t\t\tnCopies(face.vs.size(), face.normal),\n\t\t\t\t\t\tface.texCoordLists);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tisolatedTriangles.clear();\n\t\t\n\t}\n\t\n\t/**\n\t * @param isolatedTriangles  non-empty collection of triangles\n\t */\n\tprotected static Collection<Face> combineTrianglesToFaces(\n\t\t\tCollection<IsolatedTriangle> isolatedTriangles) {\n\t\t\n\t\tList<IsolatedTriangle> triangles =\n\t\t\t\tnew LinkedList<IsolatedTriangle>(isolatedTriangles);\n\t\t\n\t\tCollection<Face> faces = new ArrayList<Face>();\n\t\t\n\t\t/* turn one triangle into a face */\n\t\t\n\t\tfaces.add(createFaceFromTriangle(triangles.remove(0)));\n\t\t\n\t\t/* turn remaining triangles into faces or insert them into existing ones */\n\t\t\n\t\ttrianglesToFacesLoop: while (!triangles.isEmpty()) {\n\t\t\t\n\t\t\t/* try to insert triangles into existing faces */\n\t\t\t\n\t\t\tfor (IsolatedTriangle triangle : triangles) {\n\t\t\t\tfor (Face face : faces) {\n\t\t\t\t\t\n\t\t\t\t\tif (normalAlmostEquals(face.normal, triangle.normal)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tboolean inserted = face.tryInsert(triangle);\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (inserted) {\n\t\t\t\t\t\t\ttriangles.remove(triangle);\n\t\t\t\t\t\t\tcontinue trianglesToFacesLoop;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* could not extend existing faces, start a new face instead */\n\t\t\t\n\t\t\tfaces.add(createFaceFromTriangle(triangles.remove(0)));\n\t\t\t\n\t\t}\n\t\t\n\t\t/* eliminate duplicate edges */\n\t\t\n\t\tfor (Face face : faces) {\n\t\t\tface.removeDuplicateEdges();\n\t\t}\n\t\t\n\t\treturn faces;\n\t\t\n\t}\n\t\n\tprotected static boolean normalAlmostEquals(VectorXYZ n1, VectorXYZ n2) {\n\t\t\n\t\treturn abs(n1.x - n2.x) <= 0.01\n\t\t\t\t&& abs(n1.y - n2.y) <= 0.01\n\t\t\t\t&& abs(n1.z - n2.z) <= 0.01;\n\t\t\n\t}\n\t\n\tprotected static Face createFaceFromTriangle(IsolatedTriangle t) {\n\t\t\n\t\tList<VectorXYZ> newFaceVs =\n\t\t\t\tnew ArrayList<VectorXYZ>(t.triangle.getVertices());\n\t\t\n\t\tList<List<VectorXZ>> newFaceTCLists = new ArrayList<List<VectorXZ>>();\n\t\t\n\t\tfor (int list = 0; list < t.texCoordLists.size(); list++) {\n\t\t\tnewFaceTCLists.add(new ArrayList<VectorXZ>(\n\t\t\t\t\tt.texCoordLists.get(list).subList(\n\t\t\t\t\t\t\tt.texCoordOffset, t.texCoordOffset + 3)));\n\t\t}\n\t\t\n\t\treturn new Face(newFaceVs,\tnewFaceTCLists, t.normal);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/Primitive.java",
    "content": "package org.osm2world.core.target.common;\n\nimport java.util.List;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\npublic class Primitive {\n\n\tpublic static enum Type {\n\t\tCONVEX_POLYGON,\n\t\tTRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN\n\t}\n\t\n\tpublic final Type type;\n\t\n\tpublic final List<VectorXYZ> vertices;\n\t\t\n\tpublic final List<VectorXYZ> normals;\n\t\n\tpublic final List<List<VectorXZ>> texCoordLists;\n\t\n\tpublic Primitive(Type type, List<VectorXYZ> vertices,\n\t\t\tList<VectorXYZ> normals, List<List<VectorXZ>> texCoordLists) {\n\t\tthis.type = type;\n\t\tthis.vertices = vertices;\n\t\tthis.normals = normals;\n\t\tthis.texCoordLists = texCoordLists;\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"{\" + type + \", \" + vertices + \"}\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/PrimitiveTarget.java",
    "content": "package org.osm2world.core.target.common;\n\nimport static org.osm2world.core.math.algorithms.NormalCalculationUtil.*;\nimport static org.osm2world.core.target.common.Primitive.Type.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXYZWithNormals;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.Renderable;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Interpolation;\n\n/**\n * superclass for targets that are based on OpenGL primitives.\n * These targets will treat different primitives similarly:\n * They convert them all to a list of vertices\n * and represent the primitive type using an enum or flags.\n */\npublic abstract class PrimitiveTarget<R extends Renderable>\n\t\textends AbstractTarget<R> {\n\n\t/**\n\t * @param vs       vertices that form the primitive\n\t * @param normals  normal vector for each vertex; same size as vs\n\t * @param texCoordLists  texture coordinates for each texture layer,\n\t *                       each list has the same size as vs\n\t */\n\tabstract protected void drawPrimitive(Primitive.Type type, Material material,\n\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\tList<List<VectorXZ>> texCoordLists);\n\t\n\t@Override\n\tpublic void drawTriangleStrip(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\tboolean smooth = (material.getInterpolation() == Interpolation.SMOOTH);\n\t\tdrawPrimitive(TRIANGLE_STRIP, material, vs,\n\t\t\t\tcalculateTriangleStripNormals(vs, smooth),\n\t\t\t\ttexCoordLists);\n\t}\n\n\t@Override\n\tpublic void drawTriangleFan(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\tboolean smooth = (material.getInterpolation() == Interpolation.SMOOTH);\n\t\tdrawPrimitive(TRIANGLE_FAN, material, vs,\n\t\t\t\tcalculateTriangleFanNormals(vs, smooth),\n\t\t\t\ttexCoordLists);\n\t}\n\t\n\t@Override\n\tpublic void drawTriangles(Material material,\n\t\t\tCollection<? extends TriangleXYZ> triangles,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tList<VectorXYZ> vectors = new ArrayList<VectorXYZ>(triangles.size()*3);\n\t\t\n\t\tfor (TriangleXYZ triangle : triangles) {\n\t\t\tvectors.add(triangle.v1);\n\t\t\tvectors.add(triangle.v2);\n\t\t\tvectors.add(triangle.v3);\n\t\t}\n\t\t\n\t\tdrawPrimitive(TRIANGLES, material, vectors,\n\t\t\t\tcalculateTriangleNormals(vectors,\n\t\t\t\t\t\tmaterial.getInterpolation() == Interpolation.SMOOTH),\n\t\t\t\t\t\ttexCoordLists);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void drawTrianglesWithNormals(Material material,\n\t\t\tCollection<? extends TriangleXYZWithNormals> triangles,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\n\t\tList<VectorXYZ> vectors = new ArrayList<VectorXYZ>(triangles.size()*3);\n\t\tList<VectorXYZ> normals = new ArrayList<VectorXYZ>(triangles.size()*3);\n\t\t\t\t\n\t\tfor (TriangleXYZWithNormals triangle : triangles) {\n\t\t\tvectors.add(triangle.v1);\n\t\t\tvectors.add(triangle.v2);\n\t\t\tvectors.add(triangle.v3);\n\t\t\tnormals.add(triangle.n1);\n\t\t\tnormals.add(triangle.n2);\n\t\t\tnormals.add(triangle.n3);\n\t\t}\n\t\t\n\t\tdrawPrimitive(TRIANGLES, material, vectors, normals, texCoordLists);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/RenderableToFaceTarget.java",
    "content": "package org.osm2world.core.target.common;\n\nimport org.osm2world.core.target.Renderable;\n\n/**\n * a renderable with specialized output code for {@link FaceTarget}s\n */\npublic interface RenderableToFaceTarget extends Renderable {\n\t\n\tpublic void renderTo(FaceTarget<?> target);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/RenderableToPrimitiveTarget.java",
    "content": "package org.osm2world.core.target.common;\n\nimport org.osm2world.core.target.Renderable;\n\npublic interface RenderableToPrimitiveTarget extends Renderable {\n\t\n\tpublic void renderTo(PrimitiveTarget<?> target);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/TextureData.java",
    "content": "package org.osm2world.core.target.common;\n\nimport java.io.File;\n\nimport org.osm2world.core.target.common.material.TexCoordFunction;\n\n/**\n * a texture with all information necessary for applying it to an object\n * that has texture coordinates\n */\npublic class TextureData {\n\t\n\tpublic static enum Wrap { REPEAT, CLAMP, CLAMP_TO_BORDER }\n\t\n\t/** path to the texture file */\n\tpublic final File file;\n\t\n\t/** width of a single tile of the texture */\n\tpublic final double width;\n\t\n\t/** height of a single tile of the texture */\n\tpublic final double height;\n\t\n\t/** wrap style of the texture */\n\tpublic final Wrap wrap;\n\t\n\t/** calculation rule for texture coordinates */\n\tpublic final TexCoordFunction coordFunction;\n\t\n\t/**\n\t * whether the texture is modulated with the material color.\n\t * Otherwise, a plain white base color is used, resulting in the texture's\n\t * colors appearing unaltered (except for lighting)\n\t */\n\tpublic final boolean colorable;\n\t\n\tpublic final boolean isBumpMap;\n\n\tpublic TextureData(File file, double width, double height, Wrap wrap,\n\t\t\tTexCoordFunction texCoordFunction, boolean colorable, boolean isBumpMap) {\n\t\t\n\t\tthis.file = file;\n\t\tthis.width = width;\n\t\tthis.height = height;\n\t\tthis.wrap = wrap;\n\t\tthis.coordFunction = texCoordFunction;\n\t\tthis.colorable = colorable;\n\t\tthis.isBumpMap = isBumpMap;\n\t\t\n\t}\n\n\t//auto-generated\n\t@Override\n\tpublic String toString() {\n\t\treturn \"TextureData [file=\" + file + \", width=\" + width + \", height=\" + height + \", wrap=\" + wrap\n\t\t\t\t+ \", texCoordFunction=\" + coordFunction + \", colorable=\" + colorable + \", isBumpMap=\" + isBumpMap + \"]\";\n\t}\n\n\t// auto-generated\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + (colorable ? 1231 : 1237);\n\t\tresult = prime * result\n\t\t\t\t+ ((coordFunction == null) ? 0 : coordFunction.hashCode());\n\t\tresult = prime * result + ((file == null) ? 0 : file.hashCode());\n\t\tlong temp;\n\t\ttemp = Double.doubleToLongBits(height);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\tresult = prime * result + (isBumpMap ? 1231 : 1237);\n\t\ttemp = Double.doubleToLongBits(width);\n\t\tresult = prime * result + (int) (temp ^ (temp >>> 32));\n\t\tresult = prime * result + ((wrap == null) ? 0 : wrap.hashCode());\n\t\treturn result;\n\t}\n\n\t// auto-generated\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj)\n\t\t\treturn true;\n\t\tif (obj == null)\n\t\t\treturn false;\n\t\tif (getClass() != obj.getClass())\n\t\t\treturn false;\n\t\tTextureData other = (TextureData) obj;\n\t\tif (colorable != other.colorable)\n\t\t\treturn false;\n\t\tif (coordFunction == null) {\n\t\t\tif (other.coordFunction != null)\n\t\t\t\treturn false;\n\t\t} else if (!coordFunction.equals(other.coordFunction))\n\t\t\treturn false;\n\t\tif (file == null) {\n\t\t\tif (other.file != null)\n\t\t\t\treturn false;\n\t\t} else if (!file.equals(other.file))\n\t\t\treturn false;\n\t\tif (Double.doubleToLongBits(height) != Double\n\t\t\t\t.doubleToLongBits(other.height))\n\t\t\treturn false;\n\t\tif (isBumpMap != other.isBumpMap)\n\t\t\treturn false;\n\t\tif (Double.doubleToLongBits(width) != Double\n\t\t\t\t.doubleToLongBits(other.width))\n\t\t\treturn false;\n\t\tif (wrap != other.wrap)\n\t\t\treturn false;\n\t\treturn true;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/lighting/GlobalLightingParameters.java",
    "content": "package org.osm2world.core.target.common.lighting;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.math.VectorXYZ;\n\n/**\n * parameters that describe lighting affecting the entire scene; immutable\n */\npublic class GlobalLightingParameters {\n\t\n\tpublic final Color globalAmbientColor;\n\t\n\t/**\n\t * source of the scene's directional lighting;\n\t * null disables it and leaves only ambient lighting\n\t */\n\tpublic final VectorXYZ lightFromDirection;\n\t\n\tpublic final Color lightColorDiffuse;\n\tpublic final Color lightColorSpecular;\n\t\n\tprivate GlobalLightingParameters(\n\t\t\tColor globalAmbientLight, VectorXYZ lightFromDirection,\n\t\t\tColor lightColorDiffuse, Color lightColorSpecular) {\n\t\t\n\t\tthis.globalAmbientColor = globalAmbientLight;\n\t\tthis.lightFromDirection = lightFromDirection;\n\t\tthis.lightColorDiffuse = lightColorDiffuse;\n\t\tthis.lightColorSpecular = lightColorSpecular;\n\t\t\n\t}\n\t\n\tpublic static final GlobalLightingParameters DEFAULT =\n\t\t\tnew GlobalLightingParameters(\n\t\t\t\t\tnew Color(1.0f, 1.0f, 1.0f),\n\t\t\t\t\tnew VectorXYZ(1.0, 1.5, -1.0),\n\t\t\t\t\tColor.WHITE,\n\t\t\t\t\tColor.WHITE);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/material/ConfMaterial.java",
    "content": "package org.osm2world.core.target.common.material;\n\nimport java.awt.Color;\nimport java.util.List;\n\nimport org.osm2world.core.target.common.TextureData;\n\n\n/**\n * a material whose attributes can be configured at runtime.\n */\npublic class ConfMaterial extends Material {\n\n\tpublic ConfMaterial(Interpolation interpolation, Color color,\n\t\t\tfloat ambientFactor, float diffuseFactor, float specularFactor, int shininess,\n\t\t\tTransparency transparency, Shadow shadow, AmbientOcclusion ao, List<TextureData> textureDataList) {\n\t\tsuper(interpolation, color, ambientFactor, diffuseFactor, specularFactor, shininess,\n\t\t\t\ttransparency, shadow, ao, textureDataList);\n\t}\n\t\n\tpublic ConfMaterial(Interpolation interpolation, Color color,\n\t\t\tfloat ambientFactor, float diffuseFactor,\n\t\t\tTransparency transparency, List<TextureData> textureDataList) {\n\t\tsuper(interpolation, color, ambientFactor, diffuseFactor, 0.0f, 1,\n\t\t\t\ttransparency, Shadow.TRUE, AmbientOcclusion.TRUE, textureDataList);\n\t}\n\t\n\tpublic ConfMaterial(Interpolation interpolation, Color color,\n\t\t\tTransparency transparency, List<TextureData> textureDataList) {\n\t\tsuper(interpolation, color, transparency, textureDataList);\n\t}\n\t\n\tpublic ConfMaterial(Interpolation interpolation, Color color) {\n\t\tsuper(interpolation, color);\n\t}\n\t\n\tpublic void setInterpolation(Interpolation interpolation) {\n\t\tthis.interpolation = interpolation;\n\t}\n\t\n\tpublic void setColor(Color color) {\n\t\tthis.color = color;\n\t}\n\t\n\tpublic void setAmbientFactor(float ambientFactor) {\n\t\tthis.ambientFactor = ambientFactor;\n\t}\n\t\n\tpublic void setDiffuseFactor(float diffuseFactor) {\n\t\tthis.diffuseFactor = diffuseFactor;\n\t}\n\t\n\tpublic void setSpecularFactor(float specularFactor) {\n\t\tthis.specularFactor = specularFactor;\n\t}\n\t\n\tpublic void setShininess(int shininess) {\n\t\tthis.shininess = shininess;\n\t}\n\t\n\tpublic void setTransparency(Transparency transparency) {\n\t\tthis.transparency = transparency;\n\t}\t\n\t\n\tpublic void setShadow(Shadow shadow) {\n\t\tthis.shadow = shadow;\n\t}\n\t\n\tpublic void setAmbientOcclusion(AmbientOcclusion ao) {\n\t\tthis.ambientOcclusion = ao;\n\t}\n\t\n\tpublic void setTextureDataList(List<TextureData> textureDataList) {\n\t\tthis.textureDataList = textureDataList;\n\t\tthis.updateBumpMap();\n\t}\n\t\n\t/*\n\t * unlike ImmutableMaterial, this has no equals method.\n\t * It should not equal another material just because that one currently (!)\n\t * has the same visual parameters.\n\t */\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/material/ImmutableMaterial.java",
    "content": "package org.osm2world.core.target.common.material;\n\nimport java.awt.Color;\nimport java.util.List;\n\nimport org.osm2world.core.target.common.TextureData;\n\n/**\n * a simple material class that offers no capabilities beyond the minimum\n * requirements of {@link Material}\n */\npublic final class ImmutableMaterial extends Material {\n\t\n\tpublic ImmutableMaterial(Interpolation interpolation, Color color,\n\t\t\tfloat ambientFactor, float diffuseFactor, float specularFactor, int shininess,\n\t\t\tTransparency transparency, Shadow shadow, AmbientOcclusion ao, List<TextureData> textureDataList) {\n\t\tsuper(interpolation, color, ambientFactor, diffuseFactor, specularFactor, shininess,\n\t\t\t\ttransparency, shadow, ao, textureDataList);\n\t}\n\t\n\tpublic ImmutableMaterial(Interpolation interpolation, Color color,\n\t\t\tTransparency transparency, List<TextureData> textureDataList) {\n\t\tsuper(interpolation, color, transparency, textureDataList);\n\t}\n\t\n\tpublic ImmutableMaterial(Interpolation interpolation, Color color) {\n\t\tsuper(interpolation, color);\n\t}\n\n\t// auto-generated\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + Float.floatToIntBits(ambientFactor);\n\t\tresult = prime\n\t\t\t\t* result\n\t\t\t\t+ ((ambientOcclusion == null) ? 0 : ambientOcclusion.hashCode());\n\t\tresult = prime * result + ((bumpMap == null) ? 0 : bumpMap.hashCode());\n\t\tresult = prime * result + bumpMapInd;\n\t\tresult = prime * result + ((color == null) ? 0 : color.hashCode());\n\t\tresult = prime * result + Float.floatToIntBits(diffuseFactor);\n\t\tresult = prime * result\n\t\t\t\t+ ((interpolation == null) ? 0 : interpolation.hashCode());\n\t\tresult = prime * result + ((shadow == null) ? 0 : shadow.hashCode());\n\t\tresult = prime * result + Float.floatToIntBits(shininess);\n\t\tresult = prime * result + Float.floatToIntBits(specularFactor);\n\t\tresult = prime * result\n\t\t\t\t+ ((textureDataList == null) ? 0 : textureDataList.hashCode());\n\t\tresult = prime * result\n\t\t\t\t+ ((transparency == null) ? 0 : transparency.hashCode());\n\t\treturn result;\n\t}\n\n\t// auto-generated\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj)\n\t\t\treturn true;\n\t\tif (obj == null)\n\t\t\treturn false;\n\t\tif (getClass() != obj.getClass())\n\t\t\treturn false;\n\t\tMaterial other = (Material) obj;\n\t\tif (Float.floatToIntBits(ambientFactor) != Float\n\t\t\t\t.floatToIntBits(other.ambientFactor))\n\t\t\treturn false;\n\t\tif (ambientOcclusion != other.ambientOcclusion)\n\t\t\treturn false;\n\t\tif (bumpMap == null) {\n\t\t\tif (other.bumpMap != null)\n\t\t\t\treturn false;\n\t\t} else if (!bumpMap.equals(other.bumpMap))\n\t\t\treturn false;\n\t\tif (bumpMapInd != other.bumpMapInd)\n\t\t\treturn false;\n\t\tif (color == null) {\n\t\t\tif (other.color != null)\n\t\t\t\treturn false;\n\t\t} else if (!color.equals(other.color))\n\t\t\treturn false;\n\t\tif (Float.floatToIntBits(diffuseFactor) != Float\n\t\t\t\t.floatToIntBits(other.diffuseFactor))\n\t\t\treturn false;\n\t\tif (interpolation != other.interpolation)\n\t\t\treturn false;\n\t\tif (shadow != other.shadow)\n\t\t\treturn false;\n\t\tif (Float.floatToIntBits(shininess) != Float\n\t\t\t\t.floatToIntBits(other.shininess))\n\t\t\treturn false;\n\t\tif (Float.floatToIntBits(specularFactor) != Float\n\t\t\t\t.floatToIntBits(other.specularFactor))\n\t\t\treturn false;\n\t\tif (textureDataList == null) {\n\t\t\tif (other.textureDataList != null)\n\t\t\t\treturn false;\n\t\t} else if (!textureDataList.equals(other.textureDataList))\n\t\t\treturn false;\n\t\tif (transparency != other.transparency)\n\t\t\treturn false;\n\t\treturn true;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/material/Material.java",
    "content": "package org.osm2world.core.target.common.material;\n\nimport java.awt.Color;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.osm2world.core.target.common.TextureData;\n\n/**\n * describes the material/surface properties of an object for lighting\n */\npublic abstract class Material {\n\t\n\tpublic static enum Interpolation {FLAT, SMOOTH};\n\t\n\tpublic static enum Transparency {\n\t\t/** arbitrary transparency, including partially transparent pixels */\n\t\tTRUE,\n\t\t/** only allow pixels to be either fully transparent or fully opaque */\n\t\tBINARY,\n\t\t/** all pixels are opaque */\n\t\tFALSE\n\t}\n\t\n\tpublic static enum Shadow {\n\t\t/** casts shadows */\n\t\tTRUE,\n\t\t/** casts no shadows */\n\t\tFALSE\n\t}\n\t\n\tpublic static enum AmbientOcclusion {\n\t\t/** casts AO */\n\t\tTRUE,\n\t\t/** casts no AO */\n\t\tFALSE\n\t}\n\t\n\t/**\n\t * Interpolation of normals\n\t */\n\tprotected Interpolation interpolation;\n\tprotected Color color;\n\tprotected float ambientFactor;\n\tprotected float diffuseFactor;\n\tprotected float specularFactor;\n\tprotected int shininess;\n\tprotected Transparency transparency;\n\tprotected Shadow shadow;\n\tprotected AmbientOcclusion ambientOcclusion;\n\t\n\tprotected List<TextureData> textureDataList;\n\tprotected TextureData bumpMap;\n\tprotected int bumpMapInd;\n\n\tpublic Material(Interpolation interpolation, Color color,\n\t\t\tfloat ambientFactor, float diffuseFactor, float specularFactor, int shininess, \n\t\t\tTransparency transparency, Shadow shadow, AmbientOcclusion ao, List<TextureData> textureDataList) {\n\t\tthis.interpolation = interpolation;\n\t\tthis.color = color;\n\t\tthis.ambientFactor = ambientFactor;\n\t\tthis.diffuseFactor = diffuseFactor;\n\t\tthis.specularFactor = specularFactor;\n\t\tthis.shininess = shininess;\n\t\tthis.transparency = transparency;\n\t\tthis.shadow = shadow;\n\t\tthis.ambientOcclusion = ao;\n\t\tthis.textureDataList = textureDataList;\n\t\tupdateBumpMap();\n\t}\n\t\n\tprotected void updateBumpMap() {\n\t\tthis.bumpMap = null;\n\t\tthis.bumpMapInd = -1;\n\t\tif (textureDataList == null) {\n\t\t\treturn;\n\t\t}\n\t\tint i = 0;\n\t\tfor (TextureData t : textureDataList) {\n\t\t\tif (t.isBumpMap) {\n\t\t\t\tthis.bumpMap = t;\n\t\t\t\tthis.bumpMapInd = i;\n\t\t\t}\n\t\t\ti++;\n\t\t}\n\t}\n\t\n\tpublic Material(Interpolation interpolation, Color color,\n\t\t\tTransparency transparency, List<TextureData> textureDataList) {\n\t\tthis(interpolation, color, 0.5f, 0.5f, 0.0f, 1, transparency,\n\t\t\t\tShadow.TRUE, AmbientOcclusion.TRUE, textureDataList);\n\t}\n\t\n\tpublic Material(Interpolation interpolation, Color color) {\n\t\tthis(interpolation, color, Transparency.FALSE,\n\t\t\t\tCollections.<TextureData>emptyList());\n\t}\n\t\t\n\tpublic Interpolation getInterpolation() {\n\t\treturn interpolation;\n\t}\n\t\n\tpublic Color getColor() {\n\t\treturn color;\n\t}\n\t\n\tpublic float getAmbientFactor() {\n\t\treturn ambientFactor;\n\t}\n\t\n\tpublic float getDiffuseFactor() {\n\t\treturn diffuseFactor;\n\t}\n\t\n\tpublic float getSpecularFactor() {\n\t\treturn specularFactor;\n\t}\n\t\n\tpublic int getShininess() {\n\t\treturn shininess;\n\t}\n\t\t\n\tpublic Color ambientColor() {\n\t\treturn multiplyColor(getColor(), getAmbientFactor());\n\t}\n\t\n\tpublic Color diffuseColor() {\n\t\treturn multiplyColor(getColor(), getDiffuseFactor());\n\t}\n\t\n\tpublic Material brighter() {\n\t\treturn new ImmutableMaterial(interpolation, getColor().brighter(),\n\t\t\t\tgetAmbientFactor(), getDiffuseFactor(), getSpecularFactor(), getShininess(),\n\t\t\t\tgetTransparency(), getShadow(), getAmbientOcclusion(), getTextureDataList());\n\t}\n\t\n\tpublic Material darker() {\n\t\treturn new ImmutableMaterial(interpolation, getColor().darker(),\n\t\t\t\tgetAmbientFactor(), getDiffuseFactor(), getSpecularFactor(), getShininess(),\n\t\t\t\tgetTransparency(), getShadow(), getAmbientOcclusion(), getTextureDataList());\n\t}\n\t\n\tpublic static final Color multiplyColor(Color c, float factor) {\n\t\tfloat[] colorComponents = new float[3];\n\t\tc.getColorComponents(colorComponents);\n\t\t\n\t\treturn new Color(\n\t\t\t\tcolorComponents[0] * factor,\n\t\t\t\tcolorComponents[1] * factor,\n\t\t\t\tcolorComponents[2] * factor);\n\t}\n\n\tpublic Material makeSmooth() {\n\t\treturn new ImmutableMaterial(Interpolation.SMOOTH, getColor(),\n\t\t\t\tgetAmbientFactor(), getDiffuseFactor(), getSpecularFactor(), getShininess(),\n\t\t\t\tgetTransparency(), getShadow(), getAmbientOcclusion(), getTextureDataList());\n\t}\n\t\n\t/**\n\t * returns a material that is the same as this one,\n\t * except with additional texture data layers stacked on top\n\t */\n\tpublic Material withAddedLayers(List<TextureData> textureLayers) {\n\t\t\n\t\tif (textureLayers.isEmpty()) return this;\n\t\t\n\t\tList<TextureData> textureDataList =\n\t\t\t\tnew ArrayList<TextureData>(getTextureDataList());\n\t    \n\t\ttextureDataList.addAll(textureLayers);\n\t    \n\t    return new ImmutableMaterial(getInterpolation(), getColor(),\n\t    \t\tgetAmbientFactor(), getDiffuseFactor(), getSpecularFactor(), getShininess(),\n\t    \t\tgetTransparency(), getShadow(), getAmbientOcclusion(), textureDataList);\n\t    \n\t}\n\t\n\tpublic Transparency getTransparency() {\n\t\treturn transparency;\n\t}\n\t\n\tpublic Shadow getShadow() {\n\t\treturn shadow;\n\t}\n\t\n\tpublic AmbientOcclusion getAmbientOcclusion() {\n\t\treturn ambientOcclusion;\n\t}\n\t\t\n\tpublic List<TextureData> getTextureDataList() {\n\t\treturn textureDataList;\n\t}\n\t\n\tpublic int getNumTextureLayers() {\n\t\tif (textureDataList == null) {\n\t\t\treturn 0;\n\t\t} else {\n\t\t\treturn textureDataList.size();\n\t\t}\n\t}\n\n\tpublic boolean hasBumpMap() {\n\t\treturn this.bumpMap != null;\n\t}\n\t\n\tpublic TextureData getBumpMap() {\n\t\treturn this.bumpMap;\n\t}\n\t\n\tpublic int getBumpMapInd() {\n\t\treturn this.bumpMapInd;\n\t}\n\t\n\tpublic String toString() {\n\t\treturn String.format(\"{%s, #%06x, a%3f, d%3f, s%3f, sh%3f, %d tex\",\n\t\t\t\tinterpolation, color.getRGB() & 0x00ffffff, ambientFactor,\n\t\t\t\tdiffuseFactor, specularFactor, shininess, textureDataList.size())\n\t\t\t\t+ transparency + shadow + ambientOcclusion\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t/*\n\t * some possible later additions: specular (obvious ...),\n\t * as well as brilliance, phong, metallic, reflection, crand and iridescence for POVRay\n\t */\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/material/Materials.java",
    "content": "package org.osm2world.core.target.common.material;\n\nimport java.awt.Color;\nimport java.io.File;\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.TextureData.Wrap;\nimport org.osm2world.core.target.common.material.Material.AmbientOcclusion;\nimport org.osm2world.core.target.common.material.Material.Interpolation;\nimport org.osm2world.core.target.common.material.Material.Shadow;\nimport org.osm2world.core.target.common.material.Material.Transparency;\nimport org.osm2world.core.util.ConfigUtil;\nimport org.osm2world.core.world.creation.WorldModule;\n\n/**\n * this class defines materials that can be used by all {@link WorldModule}s\n */\npublic final class Materials {\n\t\n\t/** prevents instantiation */\n\tprivate Materials() {}\n\t\n\t/** material for \"empty\" ground */\n\tpublic static final ConfMaterial TERRAIN_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.SMOOTH, Color.GREEN);\n\t\n\tpublic static final ConfMaterial WATER =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.BLUE);\n\tpublic static final ConfMaterial PURIFIED_WATER =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, Color.BLUE);\n\t\n\tpublic static final ConfMaterial ASPHALT =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.3f, 0.3f));\n\tpublic static final ConfMaterial BRICK =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(1.0f, 0.5f, 0.25f));\n\tpublic static final ConfMaterial COBBLESTONE =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.3f, 0.3f));\n\tpublic static final ConfMaterial CONCRETE =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.55f, 0.55f, 0.55f));\n\tpublic static final ConfMaterial EARTH =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0, 0));\n\tpublic static final ConfMaterial GLASS =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial GRASS =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.0f, 0.8f, 0.0f));\n\tpublic static final ConfMaterial GRASS_PAVER =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.5f, 0.3f));\n\tpublic static final ConfMaterial GRAVEL =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.4f, 0.4f, 0.4f));\n\tpublic static final ConfMaterial PAVING_STONE =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.4f, 0.4f, 0.4f));\n\tpublic static final ConfMaterial PEBBLESTONE =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.4f, 0.4f, 0.4f));\n\tpublic static final ConfMaterial PLASTIC =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0, 0, 0));\n\tpublic static final ConfMaterial PLASTIC_GREY =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(184, 184, 184));\n\tpublic static final ConfMaterial SAND =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(241, 233, 80));\n\tpublic static final ConfMaterial STEEL =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(200, 200, 200));\n\tpublic static final ConfMaterial WOOD =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.2f, 0.2f));\n\tpublic static final ConfMaterial WOOD_WALL =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.2f, 0.2f));\n\tpublic static final ConfMaterial TARTAN =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(206, 109, 90));\n\t\n\tpublic static final ConfMaterial ROAD_MARKING =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial ROAD_MARKING_DASHED =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial ROAD_MARKING_ZEBRA =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial ROAD_MARKING_CROSSING =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial ROAD_MARKING_ARROW_THROUGH =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial ROAD_MARKING_ARROW_THROUGH_RIGHT =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial ROAD_MARKING_ARROW_RIGHT =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial ROAD_MARKING_ARROW_RIGHT_LEFT =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial RED_ROAD_MARKING =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.6f, 0.3f, 0.3f));\n\tpublic static final ConfMaterial KERB =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.4f, 0.4f, 0.4f));\n\tpublic static final ConfMaterial STEPS_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.DARK_GRAY);\n\tpublic static final ConfMaterial HANDRAIL_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.LIGHT_GRAY);\n\t\t\n\tpublic static final ConfMaterial RAIL_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.LIGHT_GRAY);\n\tpublic static final ConfMaterial RAIL_SLEEPER_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.2f, 0.2f));\n\tpublic static final ConfMaterial RAIL_BALLAST_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.DARK_GRAY);\n\t\n\tpublic static final ConfMaterial BUILDING_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(1f, 0.9f, 0.55f));\n\tpublic static final ConfMaterial BUILDING_WINDOWS =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(1f, 0.9f, 0.55f));\n\tpublic static final ConfMaterial ROOF_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.8f, 0, 0));\n\tpublic static final ConfMaterial GLASS_ROOF =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.9f, 0.9f, 0.9f));\n\tpublic static final ConfMaterial ENTRANCE_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.2f, 0, 0));\n\tpublic static final ConfMaterial GARAGE_DOORS =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(1f, 0.9f, 0.55f));\n\t\n\tpublic static final ConfMaterial WALL_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.GRAY);\n\tpublic static final ConfMaterial WALL_GABION =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.GRAY);\n\t\n\tpublic static final ConfMaterial HEDGE =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0,0.5f,0));\n\t\n\tpublic static final ConfMaterial FENCE_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.2f, 0.2f));\n\tpublic static final ConfMaterial SPLIT_RAIL_FENCE =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.2f, 0.2f));\n\tpublic static final ConfMaterial CHAIN_LINK_FENCE =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, Color.LIGHT_GRAY);\n\tpublic static final ConfMaterial CHAIN_LINK_FENCE_POST =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.1f, 0.5f, 0.1f));\n\t\t\n\tpublic static final ConfMaterial BRIDGE_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.GRAY);\n\tpublic static final ConfMaterial BRIDGE_PILLAR_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.GRAY);\n\t\n\tpublic static final ConfMaterial TUNNEL_DEFAULT =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.GRAY, 0.2f, 0.5f,\n\t\t\t\tTransparency.FALSE, Collections.<TextureData>emptyList());\n\t\n\tpublic static final ConfMaterial TREE_TRUNK =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.2f, 0.2f));\n\tpublic static final ConfMaterial TREE_CROWN =\n\t\tnew ConfMaterial(Interpolation.SMOOTH, new Color(0, 0.5f, 0));\n\tpublic static final ConfMaterial TREE_BILLBOARD_BROAD_LEAVED =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0, 0.5f, 0), 1f, 0f,\n\t\t\t\tTransparency.FALSE, Collections.<TextureData>emptyList());\n\tpublic static final ConfMaterial TREE_BILLBOARD_BROAD_LEAVED_FRUIT =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0, 0.5f, 0), 1f, 0f,\n\t\t\t\tTransparency.FALSE, Collections.<TextureData>emptyList());\n\tpublic static final ConfMaterial TREE_BILLBOARD_CONIFEROUS =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0, 0.5f, 0), 1f, 0f,\n\t\t\t\tTransparency.FALSE, Collections.<TextureData>emptyList());\n\t\n\tpublic static final ConfMaterial POWER_TOWER_VERTICAL =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(.7f, .7f, .7f), 1f, 0f,\n\t\t\t\tTransparency.BINARY, Collections.<TextureData>emptyList());\n\tpublic static final ConfMaterial POWER_TOWER_HORIZONTAL =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(.7f, .7f, .7f), 1f, 0f,\n\t\t\t\t\tTransparency.BINARY, Collections.<TextureData>emptyList());\n\t\n\tpublic static final ConfMaterial ADVERTISING_POSTER =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(1, 1, 0.8f));\n\t\n\tpublic static final ConfMaterial BUS_STOP_SIGN =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.98f, 0.90f, 0.05f));\n\t\n\tpublic static final ConfMaterial SIGN_DE_250 =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.RED);\n\n\tpublic static final ConfMaterial SIGN_DE_206 =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.RED);\n\t\n\tpublic static final ConfMaterial SIGN_DE_625_11 =\n\t\tnew ConfMaterial(Interpolation.FLAT, Color.WHITE);\n\t\n\tpublic static final ConfMaterial SIGN_DE_625_21 =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, Color.WHITE);\n\t\n\tpublic static final ConfMaterial GRITBIN_DEFAULT =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.3f, 0.5f, 0.4f));\n\t\n\tpublic static final ConfMaterial POSTBOX_DEUTSCHEPOST =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(1f, 0.8f, 0f));\n\tpublic static final ConfMaterial POSTBOX_ROYALMAIL =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.8f, 0, 0));\n\tpublic static final ConfMaterial TELEKOM_MANGENTA =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.883f, 0f, 0.453f));\n\t\n\tpublic static final ConfMaterial FIREHYDRANT =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0.8f, 0, 0));\n\n\tpublic static final ConfMaterial SOLAR_PANEL =\n\t\t\tnew ConfMaterial(Interpolation.FLAT, Color.BLUE);\n\t\n\tpublic static final ConfMaterial SKYBOX =\n\t\tnew ConfMaterial(Interpolation.FLAT, new Color(0, 0, 1),\n\t\t\t\t1, 0, Transparency.FALSE, null);\n\t\n\tprivate static final Map<String, ConfMaterial> surfaceMaterialMap =\n\t\tnew HashMap<String, ConfMaterial>();\n\tprivate static final Map<ConfMaterial, String> fieldNameMap =\n\t\tnew HashMap<ConfMaterial, String>();\n\t\n\tstatic {\n\t\t\n\t\tsurfaceMaterialMap.put(\"asphalt\", ASPHALT);\n\t\tsurfaceMaterialMap.put(\"cobblestone\", COBBLESTONE);\n\t\tsurfaceMaterialMap.put(\"compacted\", GRAVEL);\n\t\tsurfaceMaterialMap.put(\"concrete\", CONCRETE);\n\t\tsurfaceMaterialMap.put(\"grass\", GRASS);\n\t\tsurfaceMaterialMap.put(\"gravel\", GRAVEL);\n\t\tsurfaceMaterialMap.put(\"grass_paver\", GRASS_PAVER);\n\t\tsurfaceMaterialMap.put(\"ground\", EARTH);\n\t\tsurfaceMaterialMap.put(\"paved\", ASPHALT);\n\t\tsurfaceMaterialMap.put(\"paving_stones\", PAVING_STONE);\n\t\tsurfaceMaterialMap.put(\"pebblestone\", PEBBLESTONE);\n\t\tsurfaceMaterialMap.put(\"sand\", SAND);\n\t\tsurfaceMaterialMap.put(\"tartan\", TARTAN);\n\t\tsurfaceMaterialMap.put(\"unpaved\", EARTH);\n\t\tsurfaceMaterialMap.put(\"wood\", WOOD);\n\n\t\ttry {\n\t\t\tfor (Field field : Materials.class.getFields()) {\n\t\t\t\tif (field.getType().equals(ConfMaterial.class)) {\n\t\t\t\t\tfieldNameMap.put(\n\t\t\t\t\t\t\t(ConfMaterial)field.get(null),\n\t\t\t\t\t\t\tfield.getName());\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tthrow new Error(e);\n\t\t}\n\t\t\n\t}\n\n\t/** returns all materials defined here */\n\tpublic static final Collection<ConfMaterial> getMaterials() {\n\t\treturn fieldNameMap.keySet();\n\t}\n\n\t/** returns a material defined here based on its field name */\n\tpublic static final ConfMaterial getMaterial(String fieldName) {\n\t\tfor (Entry<ConfMaterial, String> entry : fieldNameMap.entrySet()) {\n\t\t\tif (entry.getValue().equals(fieldName)) {\n\t\t\t\treturn entry.getKey();\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\t\n\t/** returns a material for a surface value; null if none is found */\n\tpublic static final Material getSurfaceMaterial(String value) {\n\t\treturn getSurfaceMaterial(value, null);\n\t}\n\t\n\t/** same as {@link #getSurfaceMaterial(String)}, but with fallback value */\n\tpublic static final Material getSurfaceMaterial(String value,\n\t\t\tMaterial fallback) {\n\t\tMaterial material = surfaceMaterialMap.get(value);\n\t\tif (material != null) {\n\t\t\treturn material;\n\t\t} else {\n\t\t\treturn fallback;\n\t\t}\n\t}\n\t\n\t/**\n\t * returns a human-readable, unique name for a material defined\n\t * within this class, null for all other materials.\n\t */\n\tpublic static final String getUniqueName(Material material) {\n\t\treturn fieldNameMap.get(material);\n\t}\n\t\n\tprivate static final String CONF_KEY_REGEX =\n\t\t\t\"material_(.+)_(color|specular|shininess|shadow|ssao|transparency|texture\\\\d*_(?:file|width|height|bumpmap))\";\n\t\n\t/**\n\t * configures the attributes of the materials within this class\n\t * based on external configuration settings\n\t */\n\tpublic static final void configureMaterials(Configuration config) {\n\t\t\n\t\t// unchecked type parameter necessary due to Apache libs' old interface\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tIterator<String> keyIterator = config.getKeys();\n\t\t\n\t\twhile (keyIterator.hasNext()) {\n\t\t\t\n\t\t\tString key = keyIterator.next();\n\t\t\t\n\t\t\tMatcher matcher = Pattern.compile(CONF_KEY_REGEX).matcher(key);\n\t\t\t\n\t\t\tif (matcher.matches()) {\n\t\t\t\t\n\t\t\t\tString materialName = matcher.group(1);\n\t\t\t\tConfMaterial material = getMaterial(materialName);\n\t\t\t\t\n\t\t\t\tif (material != null) {\n\t\t\t\t\n\t\t\t\t\tString attribute = matcher.group(2);\n\t\t\t\t\t\n\t\t\t\t\tif (\"color\".equals(attribute)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tColor color = ConfigUtil.parseColor(\n\t\t\t\t\t\t\t\tconfig.getString(key));\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (color != null) {\n\t\t\t\t\t\t\tmaterial.setColor(color);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tSystem.err.println(\"incorrect color value: \"\n\t\t\t\t\t\t\t\t\t+ config.getString(key));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (\"specular\".equals(attribute)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tfloat specular = config.getFloat(key);\n\t\t\t\t\t\tmaterial.setSpecularFactor(specular);\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (\"shininess\".equals(attribute)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tint shininess = config.getInt(key);\n\t\t\t\t\t\tmaterial.setShininess(shininess);\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (\"shadow\".equals(attribute)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tString value = config.getString(key).toUpperCase();\n\t\t\t\t\t\tShadow shadow = Shadow.valueOf(value);\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (shadow != null) {\n\t\t\t\t\t\t\tmaterial.setShadow(shadow);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (\"ssao\".equals(attribute)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tString value = config.getString(key).toUpperCase();\n\t\t\t\t\t\tAmbientOcclusion ao = AmbientOcclusion.valueOf(value);\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (ao != null) {\n\t\t\t\t\t\t\tmaterial.setAmbientOcclusion(ao);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (\"transparency\".equals(attribute)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tString value = config.getString(key).toUpperCase();\n\t\t\t\t\t\tTransparency transparency = Transparency.valueOf(value);\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (transparency != null) {\n\t\t\t\t\t\t\tmaterial.setTransparency(transparency);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (attribute.startsWith(\"texture\")) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tList<TextureData> textureDataList =\n\t\t\t\t\t\t\tnew ArrayList<TextureData>();\n\t\t\t\t\t\t\n\t\t\t\t\t\tfor (int i = 0; i < 32; i++) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tString fileKey = \"material_\" + materialName + \"_texture\" + i + \"_file\";\n\t\t\t\t\t\t\tString widthKey = \"material_\" + materialName + \"_texture\" + i + \"_width\";\n\t\t\t\t\t\t\tString heightKey = \"material_\" + materialName + \"_texture\" + i + \"_height\";\n\t\t\t\t\t\t\tString wrapKey = \"material_\" + materialName + \"_texture\" + i + \"_wrap\";\n\t\t\t\t\t\t\tString coordFunctionKey = \"material_\" + materialName + \"_texture\" + i + \"_coord_function\";\n\t\t\t\t\t\t\tString colorableKey = \"material_\" + materialName + \"_texture\" + i + \"_colorable\";\n\t\t\t\t\t\t\tString bumpmapKey = \"material_\" + materialName + \"_texture\" + i + \"_bumpmap\";\n\n\t\t\t\t\t\t\tif (config.getString(fileKey) == null) break;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tFile file = new File(config.getString(fileKey));\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tdouble width = config.getDouble(widthKey, 1);\n\t\t\t\t\t\t\tdouble height = config.getDouble(heightKey, 1);\n\t\t\t\t\t\t\tboolean colorable = config.getBoolean(colorableKey, false);\n\t\t\t\t\t\t\tboolean isBumpMap = config.getBoolean(bumpmapKey, false);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tString wrapString = config.getString(wrapKey);\n\t\t\t\t\t\t\tWrap wrap = Wrap.REPEAT;\n\t\t\t\t\t\t\tif (\"clamp_to_border\".equalsIgnoreCase(wrapString)) {\n\t\t\t\t\t\t\t\twrap = Wrap.CLAMP_TO_BORDER;\n\t\t\t\t\t\t\t} else if (\"clamp\".equalsIgnoreCase(wrapString)) {\n\t\t\t\t\t\t\t\twrap = Wrap.CLAMP;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tString coordFunctionString = config.getString(coordFunctionKey);\n\t\t\t\t\t\t\tTexCoordFunction coordFunction = null;\n\t\t\t\t\t\t\tif (coordFunctionString != null) {\n\t\t\t\t\t\t\t\tcoordFunction = NamedTexCoordFunction.valueOf(\n\t\t\t\t\t\t\t\t\t\tcoordFunctionString.toUpperCase());\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// bumpmaps are only supported in the shader implementation, skip for others\n\t\t\t\t\t\t\tif (!isBumpMap || \"shader\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\t\t\t\t\t\tTextureData textureData = new TextureData(\n\t\t\t\t\t\t\t\t\t\tfile, width, height, wrap, coordFunction, colorable, isBumpMap);\n\t\t\t\t\t\t\t\ttextureDataList.add(textureData);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tmaterial.setTextureDataList(textureDataList);\n\t\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\tSystem.err.println(\"unknown material attribute: \"\n\t\t\t\t\t\t\t\t+ attribute);\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t} else {\n\t\t\t\t\tSystem.err.println(\"unknown material: \" + materialName);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/material/NamedTexCoordFunction.java",
    "content": "package org.osm2world.core.target.common.material;\n\nimport static java.lang.Math.abs;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.TextureData;\n\n/**\n * several useful {@link TexCoordFunction} implementations.\n * They can be referenced by name in style definition files.\n * {@link Target}s may also provide special implementations for them\n * (e.g. as a specialized shader).\n */\npublic enum NamedTexCoordFunction implements TexCoordFunction {\n\t\n\t/**\n\t * uses x and z vertex coords together with the texture's width and height\n\t * to place a texture. This function works for all geometries,\n\t * but steep inclines or even vertical walls produce odd-looking results.\n\t */\n\tGLOBAL_X_Z,\n\n\t/**\n\t * like {@link #GLOBAL_X_Z}, but uses y instead of z dimension.\n\t * Better suited for certain vertical surfaces.\n\t */\n\tGLOBAL_X_Y,\n\n\t/**\n\t * creates texture coordinates for individual triangles that\n\t * orient the texture based on each triangle's downward slope.\n\t * \n\t * TODO: introduce face requirement?\n\t */\n\tSLOPED_TRIANGLES,\n\t\n\t/**\n\t * creates texture coordinates for a triangle strip (alternating between\n\t * upper and lower vertex), based on the length along a wall from the\n\t * starting point, height of the vertex, and texture size.\n\t * \n\t * This only works for vertices forming a triangle strip,\n\t * alternating between upper and lower vertex.\n\t */\n\tSTRIP_WALL,\n\t\n\t/**\n\t * creates texture coordinates for a triangle strip (alternating between\n\t * upper and lower vertex), based on the length along a wall from the\n\t * starting point.\n\t * \n\t * Similar to {@link #STRIP_WALL}, except that one texture coordinate\n\t * dimension alternates between 1 and 0 instead of being based on height.\n\t */\n\tSTRIP_FIT_HEIGHT,\n\t\n\t/**\n\t * stretches the texture exactly once onto a triangle strip (alternating\n\t * between upper and lower vertex).\n\t * \n\t * Most commonly used to texture a rectangle represented as a\n\t * triangle strip with 2 triangles.\n\t */\n\tSTRIP_FIT;\n\t\n\t@Override\n\tpublic List<VectorXZ> apply(List<VectorXYZ> vs, TextureData textureData) {\n\t\t\n\t\tList<VectorXZ> result = new ArrayList<VectorXZ>(vs.size());\n\t\t\n\t\tswitch (this) {\n\t\t\n\t\tcase GLOBAL_X_Z:\n\t\tcase GLOBAL_X_Y:\n\t\t\t\n\t\t\tfor (VectorXYZ v : vs) {\n\t\t\t\tresult.add(new VectorXZ(\n\t\t\t\t\t\tv.x / textureData.width,\n\t\t\t\t\t\t(this == GLOBAL_X_Y ? v.y : v.z) / textureData.height));\n\t\t\t}\n\t\t\t\n\t\t\tbreak;\n\t\t\t\n\t\tcase SLOPED_TRIANGLES:\n\t\t\t\n\t\t\tif (vs.size() % 3 != 0) {\n\t\t\t\tthrow new IllegalArgumentException(\"not a set of triangles\");\n\t\t\t}\n\t\t\t\n\t\t\tList<Double> knownAngles = new ArrayList<Double>();\n\t\t\t\n\t\t\tfor (int i = 0; i < vs.size() / 3; i++) {\n\t\t\t\t\n\t\t\t\t//TODO avoid creating a temporary triangle\n\t\t\t\tTriangleXYZ triangle = new TriangleXYZ(vs.get(3*i), vs.get(3*i+1), vs.get(3*i+2));\n\t\t\t\t\n\t\t\t\tVectorXZ normalXZProjection = triangle.getNormal().xz();\n\t\t\t\t\n\t\t\t\tdouble downAngle = 0;\n\t\t\t\t\n\t\t\t\tif (normalXZProjection.x != 0 || normalXZProjection.z != 0) {\n\t\t\t\t\t\n\t\t\t\t\tdownAngle = normalXZProjection.angle();\n\t\t\t\t\t\n\t\t\t\t\t//try to avoid differences between triangles of the same face\n\t\t\t\t\t\n\t\t\t\t\tDouble similarKnownAngle = null;\n\t\t\t\t\t\n\t\t\t\t\tfor (double knownAngle : knownAngles) {\n\t\t\t\t\t\tif (abs(downAngle - knownAngle) < 0.02) {\n\t\t\t\t\t\t\tsimilarKnownAngle = knownAngle;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (similarKnownAngle == null) {\n\t\t\t\t\t\tknownAngles.add(downAngle);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdownAngle = similarKnownAngle;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tfor (VectorXYZ v : triangle.getVertices()) {\n\t\t\t\t\tVectorXZ baseTexCoord = v.rotateY(-downAngle).xz();\n\t\t\t\t\tresult.add(new VectorXZ(\n\t\t\t\t\t\t\t-baseTexCoord.x / textureData.width,\n\t\t\t\t\t\t\t-baseTexCoord.z / textureData.height));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tbreak;\n\t\t\t\n\t\tcase STRIP_WALL:\n\t\tcase STRIP_FIT_HEIGHT:\n\t\tcase STRIP_FIT:\n\t\t\t\n\t\t\tif (vs.size() % 2 == 1) {\n\t\t\t\tthrow new IllegalArgumentException(\"not a triangle strip wall\");\n\t\t\t}\n\t\t\t\n\t\t\t/* calculate length of the wall (if needed later) */\n\t\t\t\n\t\t\tdouble totalLength = 0;\n\t\t\t\n\t\t\tif (this == STRIP_FIT) {\n\t\t\t\tfor (int i = 0; i+1 < vs.size(); i++) {\n\t\t\t\t\ttotalLength += vs.get(i).distanceToXZ(vs.get(i+1));\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* calculate texture coordinate list */\n\t\t\t\n\t\t\tdouble accumulatedLength = 0;\n\t\t\t\n\t\t\tfor (int i = 0; i < vs.size(); i++) {\n\t\t\t\t\n\t\t\t\tVectorXYZ v = vs.get(i);\n\t\t\t\t\n\t\t\t\t// increase accumulated length after every second vector\n\t\t\t\t\n\t\t\t\tif (i > 0 && i % 2 == 0) {\n\t\t\t\t\taccumulatedLength += v.xz().distanceTo(vs.get(i-2).xz());\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// calculate texture coords\n\t\t\t\t\n\t\t\t\tdouble s, t;\n\t\t\t\t\n\t\t\t\tif (this != STRIP_FIT) {\n\t\t\t\t\ts = accumulatedLength / textureData.width;\n\t\t\t\t} else {\n\t\t\t\t\ts = accumulatedLength / totalLength;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (this == STRIP_WALL) {\n\t\t\t\t\tt = (i % 2 == 0) ? (v.distanceTo(vs.get(i+1))) / textureData.height : 0;\n\t\t\t\t} else {\n\t\t\t\t\tt = (i % 2 == 0) ? 1 : 0;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tresult.add(new VectorXZ(s, t));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tbreak;\n\t\t\n\t\tdefault:\n\t\t\t\n\t\t\tthrow new Error(\"unimplemented texture coordinate function\");\n\t\t\t\t\n\t\t}\n\t\t\n\t\treturn result;\n\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/material/TexCoordFunction.java",
    "content": "package org.osm2world.core.target.common.material;\n\nimport java.util.List;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.TextureData;\n\n/**\n * the function used to calculate texture coordinates for each vertex from\n * a collection. Some implementations only make sense for certain geometries\n * (e.g. vertices forming triangle strips).\n */\npublic interface TexCoordFunction {\n\t\n\t/**\n\t * calculates a texture coordinate for each vertex\n\t */\n\tpublic List<VectorXZ> apply(\n\t\t\tList<VectorXYZ> vs, TextureData textureData);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/material/TexCoordUtil.java",
    "content": "package org.osm2world.core.target.common.material;\n\nimport static java.util.Collections.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.TextureData;\n\n/**\n * utility class for texture coordinate calculation\n */\npublic final class TexCoordUtil {\n\t\n\t/** prevents instantiation */\n\tprivate TexCoordUtil() {}\n\t\n\t/**\n\t * calculates the texture coordinate lists based on the\n\t * {@link TexCoordFunction} associated with each {@link TextureData} layer\n\t */\n\tpublic static final List<List<VectorXZ>> texCoordLists(\n\t\t\tList<VectorXYZ> vs, Material material,\n\t\t\tTexCoordFunction defaultCoordFunction) {\n\t\t\n\t\tList<TextureData> textureDataList = material.getTextureDataList();\n\t\t\n\t\tif (textureDataList.size() == 0) {\n\t\t\t\n\t\t\treturn emptyList();\n\t\t\t\n\t\t} else if (textureDataList.size() == 1) {\n\t\t\t\n\t\t\tTextureData textureData = textureDataList.get(0);\n\t\t\tTexCoordFunction coordFunction = textureData.coordFunction;\n\t\t\tif (coordFunction == null) { coordFunction = defaultCoordFunction; }\n\t\t\t\n\t\t\treturn singletonList(coordFunction.apply(vs, textureData));\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\tList<List<VectorXZ>> result = new ArrayList<List<VectorXZ>>();\n\t\t\t\n\t\t\tfor (TextureData textureData : textureDataList) {\n\t\t\t\t\n\t\t\t\tTexCoordFunction coordFunction = textureData.coordFunction;\n\t\t\t\tif (coordFunction == null) { coordFunction = defaultCoordFunction; }\n\t\t\t\t\n\t\t\t\tresult.add(coordFunction.apply(vs, textureData));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn result;\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * equivalent of {@link #texCoordLists(List, Material, TexCoordFunction)}\n\t * for a collection of triangle objects.\n\t */\n\tpublic static final List<List<VectorXZ>> triangleTexCoordLists(\n\t\t\tCollection<TriangleXYZ> triangles, Material material,\n\t\t\tTexCoordFunction defaultCoordFunction) {\n\t\t\n\t\tList<VectorXYZ> vs = new ArrayList<VectorXYZ>(triangles.size() * 3);\n\t\t\n\t\tfor (TriangleXYZ triangle : triangles) {\n\t\t\tvs.add(triangle.v1);\n\t\t\tvs.add(triangle.v2);\n\t\t\tvs.add(triangle.v3);\n\t\t}\n\t\t\n\t\treturn texCoordLists(vs, material, defaultCoordFunction);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/rendering/Camera.java",
    "content": "package org.osm2world.core.target.common.rendering;\n\nimport static org.osm2world.core.math.VectorXYZ.Y_UNIT;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\npublic class Camera {\n\t\n\tVectorXYZ up;\n\tVectorXYZ pos;\n\tVectorXYZ lookAt;\n\t\t\n\t/** returns the view direction vector with length 1 */\n\tpublic VectorXYZ getViewDirection() {\n\t\t//TODO: (performance)? cache viewDirection\n\t\treturn lookAt.subtract(pos).normalize();\n\t}\n\t\n\t/**\n\t * returns the vector that is orthogonal to the connection\n\t * between pos and lookAt and points to the right of it.\n\t * The result has length 1.\n\t */\n\tpublic VectorXYZ getRight() {\n\t\treturn getViewDirection().crossNormalized(up);\n\t}\n\t\n\tpublic VectorXYZ getPos() {\n\t\treturn pos;\n\t}\n\t\n\tpublic VectorXYZ getLookAt() {\n\t\treturn lookAt;\n\t}\n\t\n\tpublic VectorXYZ getUp() {\n\t\treturn up;\n\t}\n\t\n\tpublic void setPos(VectorXYZ pos) {\n\t\tthis.pos = pos;\n\t}\n\t\n\tpublic void setCamera(double posX, double posY, double posZ,\n\t\t\tdouble lookAtX, double lookAtY, double lookAtZ) {\n\t\tsetPos(posX, posY, posZ);\n\t\tup = Y_UNIT; // some initial setup value\n\t\tsetLookAt(lookAtX, lookAtY, lookAtZ);\n\t}\n\n\tpublic void setCamera(double posX, double posY, double posZ,\n\t\t\tdouble lookAtX, double lookAtY, double lookAtZ,\n\t\t\tdouble upX, double upY, double upZ) {\n\t\tsetPos(posX, posY, posZ);\n\t\tup = new VectorXYZ(upX, upY, upZ);\n\t\tsetLookAt(lookAtX, lookAtY, lookAtZ);\n\t}\n\n\tprivate void setPos(double x, double y, double z) {\n\t\tthis.setPos(new VectorXYZ(x, y, z));\n\t}\n\n\tprivate void setLookAt(VectorXYZ lookAt) {\n\t\tthis.lookAt = lookAt;\n\n\t\tVectorXYZ right = getRight();\n\t\tup = right.crossNormalized(getViewDirection());\n\t}\n\n\tprivate void setLookAt(double x, double y, double z) {\n\t\tthis.setLookAt(new VectorXYZ(x, y, z));\n\t}\n\n\t/**\n\t * moves pos and lookAt in the view direction\n\t * @param step  units to move forward\n\t */\n\tpublic void moveForward(double step) {\n\t\tVectorXYZ d = getViewDirection();\n\t\tmove(d.x * step, d.y * step, d.z * step);\n\t}\n\n\t/**\n\t * moves pos and lookAt forward in the map plane\n\t * @param step  units to move forward\n\t */\n\tpublic void moveMapForward(double step) {\n\t\tVectorXYZ d = getViewDirection();\n\t\tVectorXZ md = new VectorXZ(d.x, d.z).normalize();\n\t\tmove(md.x * step, 0, md.z * step);\n\t}\n\n\t/**\n\t * moves pos and lookAt to the right, orthogonally to the view direction\n\t * \n\t * @param step  units to move right, negative units move to the left\n\t */\n\tpublic void moveRight(double step) {\n\t\tVectorXYZ right = getRight();\n\t\tmove(right.x * step, right.y * step, right.z * step);\n\t}\n\n\t/**\n\t * moves pos and lookAt to the right in the map plane\n\t * \n\t * @param step  units to move right, negative units move to the left\n\t */\n\tpublic void moveMapRight(double step) {\n\t\tVectorXYZ right = getRight();\n\t\tVectorXZ md = new VectorXZ(right.x, right.z).normalize();\n\t\tmove(md.x * step, 0, md.z * step);\n\t}\n\n\t/**\n\t * move pos and lookAt upwards, orthogonally to the view direction\n\t * \n\t * @param step units to move up, negative units move down\n\t */\n\tpublic void moveUp(double step) {\n\t\tmove(up.x * step, up.y * step, up.z * step);\n\t}\n\n\t/**\n\t * move pos and lookAt upwards in respect to the map plane\n\t * \n\t * @param step units to move up, negative units move down\n\t */\n\tpublic void moveMapUp(double step) {\n\t\tmove(0, step, 0);\n\t}\n\n\t\n\t/** moves both pos and lookAt by the given vector */\n\tpublic void move(VectorXYZ move) {\n\t\tpos = pos.add(move);\n\t\tlookAt = lookAt.add(move);\n\t}\n\n\t/** moves both pos and lookAt by the given vector */\n\tpublic void move(double moveX, double moveY, double moveZ) {\n\t\tpos = pos.add(moveX, moveY, moveZ);\n\t\tlookAt = lookAt.add(moveX, moveY, moveZ);\n\t}\n\n\t/**\n\t * moves lookAt to represent a rotation counterclockwise\n\t * around the y axis on pos\n\t * \n\t * @param d  angle in radians\n\t */\n\tpublic void rotateY(double d) {\n\t\t\n\t\tup = up.rotateY(d);\n\t\tVectorXYZ toOldLookAt = lookAt.subtract(pos);\n\t\tVectorXYZ toNewLookAt = toOldLookAt.rotateY(d);\n\t\t\n\t\tlookAt = pos.add(toNewLookAt);\n\t}\n\t\n\t/**\n\t * rotates the camera around the yaw axis\n\t * \n\t * @param d  angle in radians\n\t */\n\tpublic void yaw(double d) {\n\t\t\n\t\tVectorXYZ toOldLookAt = lookAt.subtract(pos);\n\t\tVectorXYZ toNewLookAt = toOldLookAt.rotateVec(d, up);\n\t\t\n\t\tlookAt = pos.add(toNewLookAt);\n\t}\n\n\t/**\n\t * rolls the camera\n\t * \n\t * @param d  angle in radians\n\t */\n\tpublic void roll(double d) {\n\t\tVectorXYZ view = getViewDirection();\n\t\tup = up.rotateVec(d, view);\n\t}\n\t\n\t/**\n\t * rotates the camera around the pitch axis\n\t * \n\t * @param d  angle in radians\n\t */\n\tpublic void pitch(double d) {\n\t\tVectorXYZ right = getRight();\n\t\t\n\t\tup = up.rotateVec(d, right);\n\t\t\n\t\tVectorXYZ toOldLookAt = lookAt.subtract(pos);\n\t\tVectorXYZ toNewLookAt = toOldLookAt.rotateVec(d, right);\n\t\tlookAt = pos.add(toNewLookAt);\n\t}\n\n\t/**\n\t * rotates the camera around an axis orthogonal to the y axis\n\t * and {@link #getViewDirection()}.\n\t * The effect is similar to {@link #pitch(double)}, but independent\n\t * from the current roll angle.\n\t * \n\t * @param d  angle in radians\n\t */\n\tpublic void mapPitch(double d) {\n\t\t\n\t\tVectorXYZ right = getViewDirection().crossNormalized(Y_UNIT);\n\t\t\n\t\tup = up.rotateVec(d, right);\n\t\t\n\t\tVectorXYZ toOldLookAt = lookAt.subtract(pos);\n\t\tVectorXYZ toNewLookAt = toOldLookAt.rotateVec(d, right);\n\t\tlookAt = pos.add(toNewLookAt);\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"{pos=\" + pos + \", lookAt=\" + lookAt + \", up=\" + up + \"}\";\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/rendering/OrthoTilesUtil.java",
    "content": "package org.osm2world.core.target.common.rendering;\n\nimport static java.lang.Math.PI;\nimport static org.osm2world.core.math.AxisAlignedBoundingBoxXZ.union;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.creation.MapProjection;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n/**\n * calculates camera and projection information for orthographic tiles.\n */\npublic final class OrthoTilesUtil {\n\n\t/** 4 cardinal directions, can be used for camera placement */\n\tpublic static enum CardinalDirection {\n\t\t\n\t\tN, E, S, W;\n\t\t\n\t\t/**\n\t\t * returns the closest cardinal direction for an angle\n\t\t * @param angle  angle to north direction in radians;\n\t\t *               consistent with {@link VectorXZ#angle()}\n\t\t */\n\t\tpublic static CardinalDirection closestCardinal(double angle) {\n\t\t\tangle = angle % (2 * PI);\n\t\t\tif (angle < PI / 4) { return N; }\n\t\t\telse if (angle < 3 * PI / 4) { return E; }\n\t\t\telse if (angle < 5 * PI / 4) { return S; }\n\t\t\telse if (angle < 7 * PI / 4) { return W; }\n\t\t\telse { return N; }\n\t\t}\n\t\t\n\t\tpublic boolean isOppositeOf(CardinalDirection other) {\n\t\t\treturn this == N && other == S\n\t\t\t\t\t|| this == E && other == W\n\t\t\t\t\t|| this == S && other == N\n\t\t\t\t\t|| this == W && other == E;\n\t\t}\n\t\n\t};\n\t\n\t/** prevents instantiation */\n\tprivate OrthoTilesUtil() { }\n\t\n\tpublic static final Camera cameraForTile(MapProjection mapProjection,\n\t\t\tTileNumber tile, double angleDeg, CardinalDirection from) {\n\t\treturn cameraForBounds(boundsForTile(mapProjection, tile),\n\t\t\t\tangleDeg, from);\n\t}\n\t\n\tpublic static final Camera cameraForTiles(MapProjection mapProjection,\n\t\t\tList<TileNumber> tiles, double angleDeg, CardinalDirection from) {\n\t\t\n\t\tif (tiles.isEmpty()) { throw new IllegalArgumentException(\"empty tiles list\"); }\n\t\t\n\t\tAxisAlignedBoundingBoxXZ result = boundsForTiles(mapProjection, tiles);\n\t\t\n\t\treturn cameraForBounds(result, angleDeg, from);\n\t\t\n\t}\n\n\tpublic static final Camera cameraForBounds(\n\t\t\tAxisAlignedBoundingBoxXZ bounds, double angleDeg,\n\t\t\tCardinalDirection from) {\n\t\t\n\t\tCamera result = new Camera();\n\t\t\n\t\tVectorXYZ lookAt = new VectorXYZ(\n\t\t\t\tbounds.minX + bounds.sizeX() / 2,\n\t\t\t\t0,\n\t\t\t\tbounds.minZ + bounds.sizeZ() / 2);\n\t\t\n\t\t// calculate camera position (start with position for view from south,\n\t\t// then modify it depending on parameters)\n\t\t\n\t\tdouble cameraDistance = Math.max(bounds.sizeX(), bounds.sizeZ());\n\t\t\t\n\t\tdouble cameraOffsetX = 0;\n\t\tdouble cameraOffsetZ = - cameraDistance * Math.cos(Math.toRadians(angleDeg));\n\t\t\t\t\n\t\tif (from == CardinalDirection.W || from == CardinalDirection.E) {\n\t\t\tdouble temp = cameraOffsetX;\n\t\t\tcameraOffsetX = cameraOffsetZ;\n\t\t\tcameraOffsetZ = temp;\n\t\t}\n\t\t\n\t\tif (from == CardinalDirection.N || from == CardinalDirection.E) {\n\t\t\tcameraOffsetX = -cameraOffsetX;\n\t\t\tcameraOffsetZ = -cameraOffsetZ;\n\t\t}\n\t\t\n\t\tresult.setCamera(lookAt.x + cameraOffsetX,\n\t\t\t\t\t\t cameraDistance * Math.sin(Math.toRadians(angleDeg)),\n\t\t\t\t\t\t lookAt.z + cameraOffsetZ,\n\t\t\t\t\t\t lookAt.x, lookAt.y, lookAt.z);\n\t\t\n\t\treturn result;\n\t}\n\t\n\tpublic static final Projection projectionForTile(MapProjection mapProjection,\n\t\t\tTileNumber tile, double angleDeg, CardinalDirection from) {\n\t\tAxisAlignedBoundingBoxXZ tileBounds = boundsForTile(mapProjection, tile);\n\t\treturn projectionForBounds(tileBounds, angleDeg, from);\n\t}\n\n\tpublic static final Projection projectionForTiles(MapProjection mapProjection,\n\t\t\tList<TileNumber> tiles, double angleDeg, CardinalDirection from) {\n\t\t\n\t\tif (tiles.isEmpty()) { throw new IllegalArgumentException(\"empty tiles list\"); }\n\t\t\n\t\tAxisAlignedBoundingBoxXZ result = boundsForTiles(mapProjection, tiles);\n\t\t\n\t\treturn projectionForBounds(result, angleDeg, from);\n\t\t\n\t}\n\t\n\tpublic static final Projection projectionForBounds(\n\t\t\tAxisAlignedBoundingBoxXZ bounds, double angleDeg,\n\t\t\tCardinalDirection from) {\n\t\t\n\t\tdouble sin = Math.sin(Math.toRadians(angleDeg));\n\t\t\n\t\tdouble sizeX = bounds.sizeX();\n\t\tdouble sizeZ = bounds.sizeZ();\n\t\t\n\t\tif (from == CardinalDirection.W || from == CardinalDirection.E) {\n\t\t\tdouble temp = sizeX;\n\t\t\tsizeX = sizeZ;\n\t\t\tsizeZ = temp;\n\t\t}\n\t\t\n\t\treturn new Projection(true,\n\t\t\t\t sizeX / (sizeZ * sin),\n\t\t\t\t Double.NaN,\n\t\t\t\t sizeZ * sin,\n\t\t\t\t -10000, 10000);\n\t\t\n\t}\n\t\n\tprivate static final AxisAlignedBoundingBoxXZ boundsForTile(\n\t\t\tMapProjection mapProjection, TileNumber tile) {\n\t\t\n\t\tVectorXZ tilePos1 = mapProjection.calcPos(\n\t\t\t\ttile2lat(tile.y, tile.zoom), tile2lon(tile.x, tile.zoom));\n\t\n\t\tVectorXZ tilePos2 = mapProjection.calcPos(\n\t\t\t\ttile2lat(tile.y+1, tile.zoom), tile2lon(tile.x+1, tile.zoom));\n\t\t\n\t\treturn new AxisAlignedBoundingBoxXZ(Arrays.asList(tilePos1, tilePos2));\n\t\t\n\t}\n\n\tprivate static final AxisAlignedBoundingBoxXZ boundsForTiles(\n\t\t\tMapProjection mapProjection, List<TileNumber> tiles) {\n\t\t\n\t\tAxisAlignedBoundingBoxXZ result = boundsForTile(mapProjection, tiles.get(0));\n\t\t\n\t\tfor (int i=1; i<tiles.size(); i++) {\n\t\t\tAxisAlignedBoundingBoxXZ newBox = boundsForTile(mapProjection, tiles.get(i));\n\t\t\tresult = union(result, newBox);\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\tprivate static final double tile2lon(int x, int z) {\n\t\treturn x / Math.pow(2.0, z) * 360.0 - 180;\n\t}\n\t\n\tprivate static final double tile2lat(int y, int z) {\n\t\tdouble n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, z);\n\t\treturn Math.toDegrees(Math.atan(Math.sinh(n)));\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/rendering/Projection.java",
    "content": "package org.osm2world.core.target.common.rendering;\n\n/**\n * configuration of the projection from 3D world to screen\n * (<em>not</em> the map projection)\n */\npublic class Projection {\n\t\n\tprivate final boolean orthographic;\n\t\n\t/**\n\t * width / height\n\t */\n\tprivate final double aspectRatio;\n\t\n\t/**\n\t * vertical viewing volume angle;\n\t * only relevant for <em>perspective</em> projection\n\t */\n\tprivate final double vertAngle;\n\t\n\t/**\n\t * height of the viewing volume;\n\t * only relevant for <em>orthographic</em> projection\n\t */\n\tprivate final double volumeHeight;\n\t\n\tprivate final double nearClippingDistance;\n\tprivate final double farClippingDistance;\n\t\t\t\n\tpublic Projection(boolean orthographic, double aspectRatio,\n\t\t\tdouble vertAngle, double volumeHeight, double nearClippingDistance,\n\t\t\tdouble farClippingDistance) {\n\t\tthis.orthographic = orthographic;\n\t\tthis.aspectRatio = aspectRatio;\n\t\tthis.vertAngle = vertAngle;\n\t\tthis.volumeHeight = volumeHeight;\n\t\tthis.nearClippingDistance = nearClippingDistance;\n\t\tthis.farClippingDistance = farClippingDistance;\n\t}\n\t\n\tpublic boolean isOrthographic() {\n\t\treturn orthographic;\n\t}\n\n\tpublic double getAspectRatio() {\n\t\treturn aspectRatio;\n\t}\n\n\tpublic double getVertAngle() {\n\t\treturn vertAngle;\n\t}\n\n\tpublic double getVolumeHeight() {\n\t\treturn volumeHeight;\n\t}\n\n\tpublic double getNearClippingDistance() {\n\t\treturn nearClippingDistance;\n\t}\n\n\tpublic double getFarClippingDistance() {\n\t\treturn farClippingDistance;\n\t}\n\n\tpublic Projection withVolumeHeight(double newVolumeHeight) {\n\t\treturn new Projection(orthographic, aspectRatio, vertAngle,\n\t\t\t\tnewVolumeHeight, nearClippingDistance, farClippingDistance);\n\t}\n\n\tpublic Projection withAspectRatio(double newAspectRatio) {\n\t\treturn new Projection(orthographic, newAspectRatio, vertAngle,\n\t\t\t\tvolumeHeight, nearClippingDistance, farClippingDistance);\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"{orthographic=\" + orthographic\n\t\t+ \", aspectRatio=\" + aspectRatio\n\t\t+ \", vertAngle=\" + vertAngle\n\t\t+ \", volumeHeight=\" + volumeHeight\n\t\t+ \", nearClippingDistance=\" + nearClippingDistance\n\t\t+ \", farClippingDistance=\" + farClippingDistance\n\t\t+ \"}\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/common/rendering/TileNumber.java",
    "content": "package org.osm2world.core.target.common.rendering;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * immutable tile number with zoom level\n */\npublic class TileNumber {\n\n\tpublic final int zoom;\n\tpublic final int x;\n\tpublic final int y;\n\n\t/**\n\t * pattern for parsing constructor\n\t */\n\tpublic static final String PATTERN = \"([0-9]{1,2}),([0-9]{1,9}),([0-9]{1,9})\";\n\t\n\t/**\n\t * regular constructor\n\t */\n\tpublic TileNumber(int zoom, int x, int y) {\n\t\tthis.zoom = zoom;\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tvalidateValues();\n\t}\n\n\t/**\n\t * parsing constructor\n\t * @param arg  string to be parsed;\n\t *             format see {@link #PATTERN}\n\t */\n\tpublic TileNumber(String arg) {\n\t\tMatcher m = Pattern.compile(PATTERN).matcher(arg);\n\t\tif (m.matches()) {\n\t\t\tzoom = Integer.parseInt(m.group(1));\n\t\t\tx = Integer.parseInt(m.group(2));\n\t\t\ty = Integer.parseInt(m.group(3));\n\t\t\tvalidateValues();\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"argument doesn't match: \" + arg);\n\t\t}\n\t}\n\n\t/**\n\t * @throws IllegalArgumentException  for incorrect field values\n\t */\n\tprivate void validateValues() {\n\t\tif (zoom <= 0 || x <= 0 || y <= 0) {\n\t\t\t//TODO (robustness): more validation\n\t\t\tthrow new IllegalArgumentException(\"not positive: \" + x + \", \" + y);\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic String toString() {\t\t\n\t\treturn zoom + \",\" + x + \",\" + y;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/AbstractJOGLTarget.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static java.util.Arrays.asList;\nimport static javax.media.opengl.GL.*;\nimport static javax.media.opengl.GL2.GL_POLYGON;\nimport static org.osm2world.core.target.jogl.NonAreaPrimitive.Type.*;\n\nimport java.awt.Color;\nimport java.nio.FloatBuffer;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.media.opengl.GL;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.Primitive.Type;\nimport org.osm2world.core.target.common.PrimitiveTarget;\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\n\n/**\n * Common implementation base for the new shader based {@link JOGLTargetShader} and the old {@link JOGLTargetFixedFunction}\n */\npublic abstract class AbstractJOGLTarget extends PrimitiveTarget<RenderableToJOGL> implements JOGLTarget {\n\tprotected PrimitiveBuffer primitiveBuffer;\n\tprotected List<NonAreaPrimitive> nonAreaPrimitives;\n\tprotected JOGLRenderer renderer;\n\tprotected JOGLTextureManager textureManager;\n\tprotected JOGLRenderingParameters renderingParameters;\n\tprotected GlobalLightingParameters globalLightingParameters;\n\t\n\tpublic AbstractJOGLTarget(GL gl, JOGLRenderingParameters renderingParameters,\n\t\t\tGlobalLightingParameters globalLightingParameters) {\n\t\tthis.textureManager = new JOGLTextureManager(gl);\n\t\tthis.renderingParameters = renderingParameters;\n\t\tthis.globalLightingParameters = globalLightingParameters;\n\t}\n\n\t@Override\n\tpublic Class<RenderableToJOGL> getRenderableType() {\n\t\treturn RenderableToJOGL.class;\n\t}\n\n\t@Override\n\tpublic void render(RenderableToJOGL renderable) {\n\t\trenderable.renderTo(this);\n\t}\n\n\t@Override\n\tpublic void render(Camera camera, Projection projection) {\n\t\trenderPart(camera, projection, 0, 1, 0, 1);\n\t}\n\n\t@Override\n\tprotected void drawPrimitive(Primitive.Type type, Material material,\n\t\t\tList<VectorXYZ> vertices, List<VectorXYZ> normals,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tprimitiveBuffer.drawPrimitive(type, material, vertices, normals, texCoordLists);\n\t\t\n\t\t// cache textures. they should not be loaded in the render function (see https://www.opengl.org/wiki/Common_Mistakes#glGenTextures_in_render_function)\n\t\t// in some situations even errors were encountered\n\t\tif (material.getNumTextureLayers() > 0) {\n\t\t\tfor (TextureData t : material.getTextureDataList()) {\n\t\t\t\ttextureManager.getTextureForFile(t.file, true);\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\tprivate void drawNonAreaPrimitive(NonAreaPrimitive.Type type,\n\t\t\tColor color, int width, List<VectorXYZ> vs) {\n\t\t\n\t\tnonAreaPrimitives.add(new NonAreaPrimitive(\n\t\t\t\ttype, color, width, vs));\n        \n\t}\n\t\n\t@Override\n\tpublic void drawPoints(Color color, VectorXYZ... vs) {\n\t\tdrawNonAreaPrimitive(POINTS, color, 1, asList(vs));\n\t}\n\t\n\t@Override\n\tpublic void drawLineStrip(Color color, int width, VectorXYZ... vs) {\n\t\tdrawNonAreaPrimitive(LINE_STRIP, color, width, asList(vs));\n\t}\n\t\n\t@Override\n\tpublic void drawLineStrip(Color color, int width, List<VectorXYZ> vs) {\n\t\tdrawNonAreaPrimitive(LINE_STRIP, color, width, vs);\n\t}\n\t\n\t@Override\n\tpublic void drawLineLoop(Color color, int width, List<VectorXYZ> vs) {\n\t\tdrawNonAreaPrimitive(LINE_LOOP, color, width, vs);\n\t}\n\t\n\t/**\n\t * set global lighting parameters. Using this method affects all primitives\n\t * (even those from previous draw calls).\n\t * \n\t * @param parameters  parameter object; null disables lighting\n\t */\n\tpublic void setGlobalLightingParameters(\n\t\t\tGlobalLightingParameters parameters) {\n\t\t\n\t\tthis.globalLightingParameters = parameters;\n\t\t\n\t}\n\t\n\t/**\n\t * set global rendering parameters. Using this method affects all primitives\n\t * (even those from previous draw calls).\n\t */\n\tpublic void setRenderingParameters(\n\t\t\tJOGLRenderingParameters renderingParameters) {\n\t\t\n\t\tthis.renderingParameters = renderingParameters;\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void reset() {\n\t\tthis.primitiveBuffer = new PrimitiveBuffer();\n\t\tthis.nonAreaPrimitives = new ArrayList<NonAreaPrimitive>();\n\t\t\n\t\tif (renderer != null) {\n\t\t\trenderer.freeResources();\n\t\t\trenderer = null;\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic void freeResources() {\n\t\t\n\t\ttextureManager.releaseAll();\n\t\t\n\t\treset();\n\t\t\n\t}\n\n\t@Override\n\tpublic boolean isFinished() {\n\t\treturn renderer != null;\n\t}\n\t\n\tstatic final int getGLConstant(Type type) {\n\t\tswitch (type) {\n\t\tcase TRIANGLE_STRIP: return GL_TRIANGLE_STRIP;\n\t\tcase TRIANGLE_FAN: return GL_TRIANGLE_FAN;\n\t\tcase TRIANGLES: return GL_TRIANGLES;\n\t\tcase CONVEX_POLYGON: return GL_POLYGON;\n\t\tdefault: throw new Error(\"programming error: unhandled primitive type\");\n\t\t}\n\t}\n\n\tstatic final int getGLConstant(NonAreaPrimitive.Type type) {\n\t\tswitch (type) {\n\t\tcase POINTS: return GL_POINTS;\n\t\tcase LINES: return GL_LINES;\n\t\tcase LINE_STRIP: return GL_LINE_STRIP;\n\t\tcase LINE_LOOP: return GL_LINE_LOOP;\n\t\tdefault: throw new Error(\"programming error: unhandled primitive type\");\n\t\t}\n\t}\n\t\n\t/**\n\t * clears the rendering surface, the z buffer and the stencil buffer\n\t * \n\t * @param clearColor  background color before rendering any primitives;\n\t *                     null uses a previously defined clear color\n\t */\n\tpublic static final void clearGL(GL gl, Color clearColor) {\n\t\t\n\t\tif (clearColor != null) {\n\t\t\tfloat[] c = {0f, 0f, 0f};\n\t\t\tclearColor.getColorComponents(c);\n\t\t\tgl.glClearColor(c[0], c[1], c[2], clearColor.getAlpha()/255f);\n\t\t}\n\t\t\n\t\tgl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL.GL_STENCIL_BUFFER_BIT);\n        \n\t}\n\t\n\tstatic final FloatBuffer getFloatBuffer(Color color) {\n\t\tfloat colorArray[] = {0, 0, 0, color.getAlpha() / 255f};\n\t\tcolor.getRGBColorComponents(colorArray);\n\t\treturn FloatBuffer.wrap(colorArray);\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/AbstractPrimitiveShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport javax.media.opengl.GL3;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Transparency;\n\n/**\n * Base class for shaders that render primitives with materials. Is used by {@link JOGLRendererVBOShader}.\n */\npublic abstract class AbstractPrimitiveShader extends AbstractShader {\n\n\t/**\n\t * Render objects that are semi transparent (see {@link Transparency.TRUE})\n\t */\n\tprotected boolean renderSemiTransparent = true;\n\t\n\t/**\n\t * Render only objects that are semi transparent (see {@link Transparency.TRUE})\n\t */\n\tprotected boolean renderOnlySemiTransparent = false;\n\n\tpublic AbstractPrimitiveShader(GL3 gl, String name) {\n\t\tsuper(gl, name);\n\t}\n\n\t/**\n\t * Prepare the shader to use the given material when rendering the primitives later.\n\t * @param material the material to apply\n\t * @param textureManager the texture manager to use if the material has textures\n\t * @return <code>true</code> if this shader wants to render primitives with the given material at all, <code>false</code> otherwise.\n\t */\n\tpublic boolean setMaterial(Material material, JOGLTextureManager textureManager) {\n\t\t\n\t\tif (!renderSemiTransparent && material.getTransparency() == Transparency.TRUE) {\n\t\t\treturn false;\n\t\t} else if (renderOnlySemiTransparent && material.getTransparency() != Transparency.TRUE) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t\t\n\t}\n\t\n\t/**\n\t * see {@link #renderSemiTransparent}\n\t */\n\tpublic void setRenderSemiTransparent(boolean renderSemiTransparent) {\n\t\tthis.renderSemiTransparent  = renderSemiTransparent;\n\t}\n\t\n\t/**\n\t * see {@link #renderOnlySemiTransparent}\n\t */\n\tpublic void setRenderOnlySemiTransparent(boolean renderOnlySemiTransparent) {\n\t\tthis.renderOnlySemiTransparent  = renderOnlySemiTransparent;\n\t}\n\n\t/**\n\t * Returns the id to use by {@link JOGLRendererVBOShader} to bind the vertex position attribute.\n\t * May be -1 if the attribute is unused.\n\t */\n\tpublic abstract int getVertexPositionID();\n\n\t/**\n\t * Returns the id to use by {@link JOGLRendererVBOShader} to bind the vertex normal attribute.\n\t * May be -1 if the attribute is unused.\n\t */\n\tpublic abstract int getVertexNormalID();\n\n\t/**\n\t * Returns the id to use by {@link JOGLRendererVBOShader} to bind the vertex texture coordinate attribute.\n\t * May be -1 if the attribute is unused.\n\t */\n\tpublic abstract int getVertexTexCoordID(int i);\n\t\n\t/**\n\t * Returns the id to use by {@link JOGLRendererVBOShader} to bind the vertex bumpmap coordinate attribute.\n\t * May be -1 if the attribute is unused.\n\t */\n\tpublic abstract int getVertexBumpMapCoordID();\n\n\t/**\n\t * Returns the id to use by {@link JOGLRendererVBOShader} to bind the vertex tangent attribute.\n\t * May be -1 if the attribute is unused.\n\t */\n\tpublic abstract int getVertexTangentID();\n\t\n\t/**\n\t * Enable a vertex attribute. Attributes with -1 are ignored.\n\t * @param index the index of the attribute. Can safely be -1\n\t */\n\tpublic void glEnableVertexAttribArray(int index) {\n\t\tif (index != -1) {\n\t\t\tgl.glEnableVertexAttribArray(index);\n\t\t}\n\t}\n\t\n\t/**\n\t * Disable a vertex attribute. Attributes with -1 are ignored.\n\t * @param index the index of the attribute. Can safely be -1\n\t */\n\tpublic void glDisableVertexAttribArray(int index) {\n\t\tif (index != -1) {\n\t\t\tgl.glDisableVertexAttribArray(index);\n\t\t}\n\t}\n\t\n\t/**\n\t * Setup the vertex attribute pointer. Attributes with index -1 are ignores.\n\t */\n\tpublic void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, long offset) {\n\t\tif (index != -1) {\n\t\t\tgl.glVertexAttribPointer(index, size, type, normalized, stride, offset);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/AbstractShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport java.nio.IntBuffer;\n\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GL3;\n\n/**\n * Simple base class for a shader program. Manages vertex and fragment shaders, links and validates them.\n */\npublic abstract class AbstractShader {\n\t\n\tprotected GL3 gl;\n\tprotected int vertexShader;\n\tprotected int fragmentShader;\n\tprotected int shaderProgram;\n\t\n\t/**\n\t * Loads the vertex and fragment shaders with the basename name and ending <i>vertex</i> and <i>fragment</i>\n\t * and creates a shader program for them. The shader is linked but not validated.\n\t * {@link #validateShader()} needs to be called manually.\n\t * @param name basename of the shader to load\n\t */\n\tpublic AbstractShader(GL3 gl, String name) {\n\t\tthis.gl = gl;\n\t\tshaderProgram = gl.glCreateProgram();\n\n\t\tif (shaderProgram < 1)\n\t\t\tthrow new RuntimeException(\"Unable to create shader program.\");\n\n\t\t/*\n\t\t * set up the vertex and fragment shaders. use static methods\n\t\t * for setting up the shaders. These methods return the unique int\n\t\t * identifiers GL assigns to each shader\n\t\t */\n\t\tvertexShader = ShaderManager.createVertexShader(gl, name+\".vertex\");\n\t\tfragmentShader = ShaderManager.createFragmentShader(gl, name+\".fragment\");\n\t\t//System.out.printf(\"Vertex: %d, Fragment: %d\\n\",vertexShader,fragmentShader);\n\n\t\t// attach the shaders to the shader program and link\n\t\tgl.glAttachShader(shaderProgram, vertexShader);\n\t\tgl.glAttachShader(shaderProgram, fragmentShader);\n\n\t\tgl.glLinkProgram(shaderProgram);\n\t\t// validate linking\n\t\tIntBuffer linkStatus = IntBuffer.allocate(1);\n\t\tgl.glGetProgramiv(shaderProgram, GL3.GL_LINK_STATUS, linkStatus);\n\t\tif (linkStatus.get() == GL.GL_FALSE) {\n\t\t\tShaderManager.printProgramInfoLog(gl, shaderProgram);\n\t\t\tthrow new RuntimeException(\"could not link shader\");\n\t\t}\n\t\tShaderManager.printProgramInfoLog(gl, shaderProgram);\n\t}\n\t\n\t/**\n\t * Validates the linked shader program. An exception is raised if the validation fails.\n\t */\n\tprotected void validateShader() {\n\t\t// tell GL to validate the shader program and grab the created log\n\t\tgl.glValidateProgram(shaderProgram);\n\t\t// perform general validation that the program is usable\n\t\tIntBuffer validateStatus = IntBuffer.allocate(1);\n\t\tgl.glGetProgramiv(shaderProgram, GL3.GL_VALIDATE_STATUS, validateStatus);\n\t\tif (validateStatus.get() == GL.GL_FALSE) {\n\t\t\tShaderManager.printProgramInfoLog(gl, shaderProgram);\n\t\t\tthrow new RuntimeException(\"could not validate shader\");\n\t\t}\n\t\tShaderManager.printProgramInfoLog(gl, shaderProgram);\n\t}\n\t\n\t/**\n\t * Make this shader program active.\n\t */\n\tpublic void useShader() {\n\t\tgl.glUseProgram(this.getProgram());\n\t}\n\t\n\tpublic void disableShader() {\n\t\tgl.glUseProgram(0);\n\t}\n\t\n\tpublic int getProgram() {\n\t\treturn shaderProgram;\n\t}\n\n\tpublic void freeResources() {\n\t    gl.glDetachShader(shaderProgram, vertexShader);\n\t    gl.glDetachShader(shaderProgram, fragmentShader);\n\t    gl.glDeleteProgram(shaderProgram);\n\t    gl.glDeleteShader(vertexShader);\n\t    gl.glDeleteShader(fragmentShader);\n\t    gl = null;\n\t}\n\t\n\t@Override\n\tprotected void finalize() {\n\t\tfreeResources();\n\t}\n\n\t/**\n\t * Load default values for the shader. Should be called after every {@link #useShader()} and before any draw calls.\n\t */\n\tpublic void loadDefaults() { }\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/BackgroundShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport java.nio.FloatBuffer;\n\nimport javax.media.opengl.GL3;\n\nimport com.jogamp.opengl.math.FloatUtil;\nimport com.jogamp.opengl.util.PMVMatrix;\n\n/**\n * Shader to render a single texture on screen.\n */\npublic class BackgroundShader extends AbstractShader {\n\n\tprivate int modelViewProjectionMatrixID;\n\tprivate int vertexPositionID;\n\tprivate int vertexTexCoord;\n\tprivate int textureID;\n\t\n\tpublic BackgroundShader(GL3 gl) {\n\t\tsuper(gl, \"/shaders/background\");\n\t\t\n\t\t// get indices of named attributes\n\t\tvertexPositionID = gl.glGetAttribLocation(shaderProgram, \"VertexPosition\");\n\t\tvertexTexCoord = gl.glGetAttribLocation(shaderProgram, \"VertexTexCoord\");\n\t\t\n\t\t// get indices of uniform variables\n\t\tmodelViewProjectionMatrixID = gl.glGetUniformLocation(shaderProgram, \"ModelViewProjectionMatrix\");\n\t\ttextureID = gl.glGetUniformLocation(shaderProgram, \"Tex\");\n\t\t\n\t\tthis.validateShader();\n\t}\n\t\n\t/**\n\t * Send uniform matrices \"ProjectionMatrix, ModelViewMatrix and ModelViewProjectionMatrix\" to vertex shader\n\t * @param pmvMatrix the PMVMatrix containing all matrices\n\t */\n\tpublic void setPMVMatrix(PMVMatrix pmvMatrix) {\n\t\tFloatBuffer pmvMat = FloatBuffer.allocate(16);\n\t\tFloatUtil.multMatrixf(pmvMatrix.glGetPMatrixf(), pmvMatrix.glGetMvMatrixf(), pmvMat);\n\t\tgl.glUniformMatrix4fv(this.getModelViewProjectionMatrixID(), 1, false, pmvMat);\n\t}\n\t\n\t/**\n\t * Returns the id to use to bind the vertex position attribute.\n\t */\n\tpublic int getVertexPositionID() {\n\t\treturn vertexPositionID;\n\t}\n\t\n\t/**\n\t * Returns the id to use to bind the vertex texture coordinate attribute.\n\t */\n\tpublic int getVertexTexCoordID() {\n\t\treturn vertexTexCoord;\n\t}\n\t\n\tpublic int getModelViewProjectionMatrixID() {\n\t\treturn modelViewProjectionMatrixID;\n\t}\n\t\n\t/**\n\t * Returns the id to use to bind the texture attribute.\n\t */\n\tpublic int getTextureID() {\n\t\treturn textureID;\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/DefaultShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT;\nimport static javax.media.opengl.GL.GL_REPEAT;\nimport static javax.media.opengl.GL.GL_TEXTURE_2D;\nimport static javax.media.opengl.GL.GL_TEXTURE_MAX_ANISOTROPY_EXT;\nimport static javax.media.opengl.GL.GL_TEXTURE_WRAP_S;\nimport static javax.media.opengl.GL.GL_TEXTURE_WRAP_T;\nimport static javax.media.opengl.GL2GL3.GL_CLAMP_TO_BORDER;\nimport static javax.media.opengl.GL2GL3.GL_TEXTURE_BORDER_COLOR;\nimport static org.osm2world.core.target.jogl.AbstractJOGLTarget.getFloatBuffer;\n\nimport java.awt.Color;\nimport java.nio.FloatBuffer;\n\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.TextureData.Wrap;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Transparency;\n\nimport com.jogamp.opengl.math.FloatUtil;\nimport com.jogamp.opengl.util.PMVMatrix;\nimport com.jogamp.opengl.util.texture.Texture;\n\n/**\n * Complex shader with support for complex materials and various graphic effects:\n * <ul>\n * <li> Shadow Volumes\n * <li> Shadow Maps\n * <li> Screen Space Ambient Occlusion\n * <li> Phong shading\n * <li> Bumpmaps / Normalmaps\n * </ul>\n */\npublic class DefaultShader extends AbstractPrimitiveShader {\n\t\n\t/** maximum number of texture layers any material can use */\n\tpublic static final int MAX_TEXTURE_LAYERS = 4;\n\n\t/** globally controls anisotropic filtering for all textures */\n\tpublic static final boolean ANISOTROPIC_FILTERING = true;\n\t\n\t/**\n\t * Parameters for SSAO\n\t */\n\tprivate int kernelSize = 16;\n\tprivate float[] kernel;\n\tprivate float ssaoRadius = 1;\n\tprivate int noiseTextureHandle;\n\tprivate static final int NOISE_TEXTURE_WIDTH=4;\n\tprivate static final int NOISE_TEXTURE_HEIGHT=4;\n\t\n\t/**\n\t * Threshold for the dot product of a SSAO kernel sample and the vertex\n\t * normal. this prevents samples from being too near to the vertex plane and\n\t * so prevents self shadowing\n\t */\n\tprivate static final double SSAO_SAMPLE_THRESHOLD = 0.15;\n\t\n\tprivate int projectionMatrixID;\n\tprivate int modelViewMatrixID;\n\tprivate int modelViewProjectionMatrixID;\n\tprivate int normalMatrixID;\n\tprivate int shadowMatrixID;\n\tprivate int vertexPositionID;\n\tprivate int vertexNormalID;\n\tprivate int[] vertexTexCoordID = new int[MAX_TEXTURE_LAYERS];\n\tprivate int vertexBumpMapCoordID;\n\tprivate int vertexTangentID;\n\t\n\tpublic DefaultShader(GL3 gl) {\n\t\tsuper(gl, \"/shaders/default\");\n\t\t\n\t\t// get indices of named attributes\n\t\tvertexPositionID = gl.glGetAttribLocation(shaderProgram, \"VertexPosition\");\n\t\tvertexNormalID = gl.glGetAttribLocation(shaderProgram, \"VertexNormal\");\n\t\tfor (int i=0; i<MAX_TEXTURE_LAYERS; i++)\n\t\t\tvertexTexCoordID[i] = gl.glGetAttribLocation(shaderProgram, \"VertexTexCoord\"+i+\"\");\n\t\tvertexBumpMapCoordID = gl.glGetAttribLocation(shaderProgram, \"VertexBumpMapCoord\");\n\t\tvertexTangentID = gl.glGetAttribLocation(shaderProgram, \"VertexTangent\");\n\t\t\n\t\t// get indices of uniform variables\n\t\tprojectionMatrixID = gl.glGetUniformLocation(shaderProgram, \"ProjectionMatrix\");\n\t\tmodelViewMatrixID = gl.glGetUniformLocation(shaderProgram, \"ModelViewMatrix\");\n\t\tmodelViewProjectionMatrixID = gl.glGetUniformLocation(shaderProgram, \"ModelViewProjectionMatrix\");\n\t\tnormalMatrixID = gl.glGetUniformLocation(shaderProgram, \"NormalMatrix\");\n\t\tshadowMatrixID = gl.glGetUniformLocation(shaderProgram, \"ShadowMatrix\");\n\t\t\n\t\tgenerateSamplingMatrix();\n\t\tgenerateNoiseTexture();\n\n\t\tthis.prepareValidation();\n\t\tthis.validateShader();\n\t}\n\t\n\t/**\n\t * Executes necessary preparation steps for shader validation.\n\t * Sets default texture units for samplers. This is needed if there are\n\t * different sampler types, as they need to point to different texture units.\n\t */\n\tprivate void prepareValidation() {\n\t\t\n\t\tthis.useShader();\n\t\t// set default texture units\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"ShadowMap\"), 0);\n        gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"DepthMap\"), 1);\n        gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"NoiseTex\"), 2);\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"BumpMap\"), getGLTextureNumber(0));\n\t\tfor (int i=0; i<MAX_TEXTURE_LAYERS; i++)\n\t\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"Tex[\"+i+\"]\"), getGLTextureNumber(i));\n\t\tthis.disableShader();\n\t}\n\t\n\t@Override\n\tpublic void loadDefaults() {\n\t\trenderSemiTransparent = true;\n\t\trenderOnlySemiTransparent = false;\n\t\t\n\t\t// set default material values\n\t\tgl.glUniform3f(gl.glGetUniformLocation(shaderProgram, \"Material.Color\"), 0, 0, 0);\n\t\tgl.glUniform1f(gl.glGetUniformLocation(shaderProgram, \"Material.Ka\"), 0);\n\t\tgl.glUniform1f(gl.glGetUniformLocation(shaderProgram, \"Material.Kd\"), 0);\n\t\tgl.glUniform1f(gl.glGetUniformLocation(shaderProgram, \"Material.Ks\"), 0);\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"Material.Shininess\"), 0);\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"isShadowed\"), 0);\n\t\t\n\t\t// reset optional parts\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useShadowMap\"), 0);\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useSSAO\"), 0);\n\t}\n\t\n\t/**\n\t * Send uniform matrices \"ProjectionMatrix, ModelViewMatrix and ModelViewProjectionMatrix\" to vertex shader\n\t * @param pmvMatrix\n\t */\n\tpublic void setPMVMatrix(PMVMatrix pmvMatrix) {\n\t\tgl.glUniformMatrix4fv(this.getProjectionMatrixID(), 1, false, pmvMatrix.glGetPMatrixf());\n\t\tgl.glUniformMatrix4fv(this.getModelViewMatrixID(), 1, false, pmvMatrix.glGetMvMatrixf());\n\t\tFloatBuffer pmvMat = FloatBuffer.allocate(16);\n\t\tFloatUtil.multMatrixf(pmvMatrix.glGetPMatrixf(), pmvMatrix.glGetMvMatrixf(), pmvMat);\n\t\tgl.glUniformMatrix4fv(this.getModelViewProjectionMatrixID(), 1, false, pmvMat);\n\t\tgl.glUniformMatrix4fv(this.getNormalMatrixID(), 1, false, pmvMatrix.glGetMvitMatrixf());\n\t}\n\t\n\t/**\n\t * Prepares the shader to do lighting.\n\t * @param lighting the global lighting to apply. Can be <code>null</code> to disable lighting.\n\t */\n\tpublic void setGlobalLighting(GlobalLightingParameters lighting) {\n\t\t\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useLighting\"), lighting != null ? 1 : 0);\n\t\tif (lighting != null) {\n\t\t\tgl.glUniform4f(gl.glGetUniformLocation(shaderProgram, \"Light.Position\"), (float)lighting.lightFromDirection.getX(),\n\t\t\t\t\t(float)lighting.lightFromDirection.getY(), -(float)lighting.lightFromDirection.getZ(), 0f);\n\t\t\tgl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, \"Light.La\"), 1, getFloatBuffer(lighting.globalAmbientColor));\n\t\t\tgl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, \"Light.Ld\"), 1, getFloatBuffer(lighting.lightColorDiffuse));\n\t\t\tgl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, \"Light.Ls\"), 1, getFloatBuffer(lighting.lightColorSpecular));\t\t\t\n\t\t}\n\t}\n\t\n\t/**\n\t * Sets whether to Render everything as in shadow.\n\t */\n\tpublic void setShadowed(boolean isShadowed) {\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"isShadowed\"), isShadowed ? 1 : 0);\n\t}\n\t\n\t@Override\n\tpublic boolean setMaterial(Material material, JOGLTextureManager textureManager) {\n\t\tif (!super.setMaterial(material, textureManager))\n\t\t\treturn false;\n\n\t\tint numTexLayers = 0;\n\t\tif (material.getTextureDataList() != null) {\n\t\t\tnumTexLayers = material.getTextureDataList().size();\n\t\t}\n\t\t\n\t\t/* set color / lighting */\n\t\t\n\t\tif (numTexLayers == 0 || material.getTextureDataList().get(0).colorable) {\n\t\t\tgl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, \"Material.Color\"), 1, getFloatBuffer(material.getColor()));\n\t\t} else {\n\t\t\tgl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, \"Material.Color\"), 1, getFloatBuffer(Color.WHITE));\n\t\t}\n\t\tgl.glUniform1f(gl.glGetUniformLocation(shaderProgram, \"Material.Ka\"), material.getAmbientFactor());\n\t\tgl.glUniform1f(gl.glGetUniformLocation(shaderProgram, \"Material.Kd\"), material.getDiffuseFactor());\n\t\tgl.glUniform1f(gl.glGetUniformLocation(shaderProgram, \"Material.Ks\"), material.getSpecularFactor());\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"Material.Shininess\"), material.getShininess());\n\t\n\t\t/* set textures and associated parameters */\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useBumpMaps\"), material.hasBumpMap() ? 1 : 0);\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useAlphaTreshold\"), material.getTransparency() == Transparency.BINARY ? 1 : 0);\n\t\tif (material.getTransparency() == Transparency.FALSE) {\n\t\t\tgl.glDisable(GL.GL_BLEND);\n\t\t} else {\n\t\t\tgl.glEnable(GL.GL_BLEND);\n\t\t\t/* GL.GL_SRC_ALPHA and GL.GL_ONE_MINUS_SRC_ALPHA for color blending produces correct results for color, while\n\t\t\t * GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA produces correct alpha blended results: the blendfunction is in fact equal to 1-(1-SRC_APLHA)*(1-DST_APLHA)\n\t\t\t */\n\t\t\tgl.glBlendFuncSeparate(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA, GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA);\n\t\t\tif (material.getTransparency() == Transparency.BINARY) {\n\t\t\t\tgl.glUniform1f(gl.glGetUniformLocation(shaderProgram, \"alphaTreshold\"), 0.5f );\n\t\t\t}\n\t\t}\n\t    for (int i = 0; i < MAX_TEXTURE_LAYERS; i++) {\n\t    \tif (i < numTexLayers) {\n\t\t\t\tgl.glActiveTexture(getGLTextureConstant(i));\n\t\t\t\tTextureData textureData = material.getTextureDataList().get(i);\n\t\t\t\tTexture texture = textureManager.getTextureForFile(textureData.file);\n\n\t\t\t\ttexture.bind(gl);\n\t\t\t\t\n\t\t\t\tif (textureData.isBumpMap) {\n\t\t    \t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useTexture[\"+i+\"]\"), 0);\n\t\t\t\t} else {\n\t\t    \t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useTexture[\"+i+\"]\"), 1);\n\t\t\t\t\n\t\t\t\t\t/* enable anisotropic filtering (note: this could be a\n\t\t\t\t\t * per-texture setting, but currently isn't) */\n\t\t\t\t\t\n\t\t\t        if (gl.isExtensionAvailable(\"GL_EXT_texture_filter_anisotropic\")) {\n\t\t\t\t\t\t\n\t\t\t        \tif (ANISOTROPIC_FILTERING) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tfloat max[] = new float[1];\n\t\t\t\t\t\t\tgl.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, max, 0);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tgl.glTexParameterf(GL_TEXTURE_2D,\n\t\t\t\t\t\t\t\t\tGL_TEXTURE_MAX_ANISOTROPY_EXT,\n\t\t\t\t\t\t\t\t\tmax[0]);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tgl.glTexParameterf(GL_TEXTURE_2D,\n\t\t\t\t\t\t\t\t\tGL_TEXTURE_MAX_ANISOTROPY_EXT,\n\t\t\t\t\t\t\t\t\t1.0f);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t        }\n\t\t\t\t}\n\t\t        \n\t\t\t\t/* wrapping behavior */\n\t\t        \n\t\t\t\tint wrap = 0;\n\t\t\t\t\n\t\t\t\tswitch (textureData.wrap) {\n\t\t\t\tcase CLAMP: System.out.println(\"Warning: CLAMP is no longer supported. Using CLAMP_TO_BORDER instead.\"); wrap = GL_CLAMP_TO_BORDER; break;\n\t\t\t\tcase REPEAT: wrap = GL_REPEAT; break;\n\t\t\t\tcase CLAMP_TO_BORDER: wrap = GL_CLAMP_TO_BORDER; break;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap);\n\t\t        gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap);\n\t\t        \n\t\t        \n\t\t        if (textureData.wrap == Wrap.CLAMP_TO_BORDER) {\n\t\t        \t\n\t\t        \t/* TODO: make the RGB configurable -  for some reason,\n\t\t        \t * it shows up in lowzoom even if fully transparent */\n\t\t        \tgl.glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR,\n\t\t        \t\t\tgetFloatBuffer(new Color(1f, 1f, 1f, 0f)));\n\t\t        \t\n\t\t        }\n\n\t\t        int loc;\n\t\t        if (textureData.isBumpMap) {\n\t\t        \tloc = gl.glGetUniformLocation(shaderProgram, \"BumpMap\");\n\t\t\t        if (loc < 0) {\n\t\t\t        \t//throw new RuntimeException(\"BumpMap not found in shader program.\");\n\t\t\t        }\n\t\t\t\t} else {\n\t\t\t\t\tloc = gl.glGetUniformLocation(shaderProgram, \"Tex[\"+i+\"]\");\n\t\t\t\t\tif (loc < 0) {\n\t\t\t\t\t\t//throw new RuntimeException(\"Tex[\"+i+\"] not found in shader program.\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t        gl.glUniform1i(loc, getGLTextureNumber(i));\n\t    \t} else {\n\t    \t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useTexture[\"+i+\"]\"), 0);\n\t    \t}\n\t    }\n\t   return true;\n\t}\n\t\n\tstatic final int getGLTextureConstant(int textureNumber) {\n\t\tswitch (getGLTextureNumber(textureNumber)) {\n\t\t//case 0: return GL.GL_TEXTURE0; // reserved for shadow map\n\t\t//case 1: return GL.GL_TEXTURE1; // reserved for ssao depth map\n\t\t//case 2: return GL.GL_TEXTURE2; // reserved for ssao noise texture\n\t\tcase 3: return GL.GL_TEXTURE3;\n\t\tcase 4: return GL.GL_TEXTURE4;\n\t\tcase 5: return GL.GL_TEXTURE5;\n\t\tcase 6: return GL.GL_TEXTURE6;\n\t\tdefault: throw new Error(\"programming error: unhandled texture number\");\n\t\t}\n\t}\n\t\n\tstatic final int getGLTextureNumber(int textureNumber) {\n\t\treturn textureNumber + 3; // texture id 0-2 are reserved for shadow map, ssao depth map and ssao noise texture\n\t}\n\t\n\t/**\n\t * Binds the specified texture as shadow map and uses it for rendering shadows.\n\t */\n\tpublic void bindShadowMap(int shadowMapHandle) {\n\t\tgl.glActiveTexture(GL.GL_TEXTURE0);\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, shadowMapHandle);\n        gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"ShadowMap\"), 0);\n        gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useShadowMap\"), 1);\n\t}\n\t\n\t/**\n\t * Creates random sampling kernel data for SSAO.\n\t */\n\tprivate void generateSamplingMatrix() {\n        kernel = new float[kernelSize*3];\n        for (int i = 0; i < kernelSize; ++i) {\n        \tfloat scale = (float)i / (float)kernelSize;\n        \tscale = lerp(0.2f, 1.0f, scale*scale);\n        \tVectorXYZ v = new VectorXYZ(\n        \t\t\tMath.random()*2-1,\n        \t\t\tMath.random()*2-1,\n        \t\t\tMath.random()\n        \t\t\t).normalize().mult(scale); //.mult(Math.random()*scale);\n        \twhile (v.dot(new VectorXYZ(0, 0, 1)) < SSAO_SAMPLE_THRESHOLD) {\n        \t\tv = new VectorXYZ(\n        \t\t\t\tMath.random()*2-1,\n        \t\t\t\tMath.random()*2-1,\n        \t\t\t\tMath.random()\n        \t\t\t\t).normalize().mult(scale);\n        \t}\n        \t\n        \tkernel[i*3] = (float)v.x;\n        \tkernel[i*3+1] = (float)v.y;\n        \tkernel[i*3+2] = (float)v.z;\n        \t\n        }\n\t}\n\t\n\t/**\n\t * Generates and binds the noise texture used for SSAO.\n\t */\n\tprivate void generateNoiseTexture() {\n\t\tfloat[] noise = new float[NOISE_TEXTURE_WIDTH*NOISE_TEXTURE_HEIGHT*3];\n\t\tfor (int i = 0; i < NOISE_TEXTURE_WIDTH*NOISE_TEXTURE_HEIGHT; i++)\n\t\t{\n\t\t\tnoise[i*3] = (float)Math.random()*2-1;\n\t\t\tnoise[i*3+1] = (float)Math.random()*2-1;\n\t\t\tnoise[i*3+2] = 0;\n\t\t}  \n\t\tint[] noiseTexture = new int[1]; \n\t\tgl.glGenTextures(1, noiseTexture, 0);\n\t\tnoiseTextureHandle = noiseTexture[0];\n\t\tgl.glBindTexture(GL_TEXTURE_2D, noiseTextureHandle);\n\t\tgl.glTexImage2D(GL_TEXTURE_2D, 0, GL.GL_RGB16F, NOISE_TEXTURE_WIDTH, NOISE_TEXTURE_HEIGHT, 0, GL.GL_RGB, GL.GL_FLOAT, FloatBuffer.wrap(noise));\n\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);\n\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);\n\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);\n\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);  \n\t}\n\t\n\tprivate float lerp(float a, float b, float f) {\n\t\treturn a + f * (b - a);\n\t}\n\t\n\t/**\n\t * Binds a texture as depth map and uses it for SSAO. Also sets up the rest of the SSAO settings.\n\t */\n\tpublic void enableSSAOwithDepthMap(int depthMapHandle) {\n\t\t// bind depth map\n\t\tgl.glActiveTexture(GL.GL_TEXTURE1);\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, depthMapHandle);\n        gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"DepthMap\"), 1);\n        \n        // send SSAO parameters\n        gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useSSAO\"), 1);\n        int[] viewport = new int[4];\n\t\tgl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);\n\t\tint width = viewport[2], height = viewport[3];\n        gl.glUniform2f(gl.glGetUniformLocation(shaderProgram, \"uNoiseScale\"), (float)width/(float)NOISE_TEXTURE_WIDTH, (float)height/(float)NOISE_TEXTURE_HEIGHT);\n        \n        // TODO: may be enough to only send once when initializing?\n        gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"uKernelSize\"), kernelSize);\n        gl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, \"uKernelOffsets\"), kernelSize, kernel, 0);\n        gl.glUniform1f(gl.glGetUniformLocation(shaderProgram, \"uRadius\"), ssaoRadius);\n        gl.glActiveTexture(GL.GL_TEXTURE2);\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, noiseTextureHandle);\n        gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"NoiseTex\"), 2);\n\t}\n\t\n\t/**\n\t * Change the size of the SSAO kernel and recomputes it. If the size already\n\t * matches nothing is done. This should be called before\n\t * {@link #enableSSAOwithDepthMap(int)} to have any effect.\n\t */\n\tpublic void setSSAOkernelSize(int kernelSize) {\n\t\tif (this.kernelSize != kernelSize) {\n\t\t\tthis.kernelSize = kernelSize;\n\t\t\tgenerateSamplingMatrix();\n\t\t}\n\t}\n\t\n\t/**\n\t * Change the radius used for SSAO. This should be called before\n\t * {@link #enableSSAOwithDepthMap(int)} to have any effect.\n\t */\n\tpublic void setSSAOradius(float radius) {\n\t\tthis.ssaoRadius = radius;\n\t}\n\t\n\t@Override\n\tpublic int getVertexPositionID() {\n\t\treturn vertexPositionID;\n\t}\n\n\t@Override\n\tpublic int getVertexNormalID() {\n\t\treturn vertexNormalID;\n\t}\n\n\t@Override\n\tpublic int getVertexTexCoordID(int i) {\n\t\treturn vertexTexCoordID[i];\n\t}\n\n\t@Override\n\tpublic int getVertexBumpMapCoordID() {\n\t\treturn vertexBumpMapCoordID;\n\t}\n\n\tpublic int getProjectionMatrixID() {\n\t\treturn projectionMatrixID;\n\t}\n\n\tpublic int getModelViewMatrixID() {\n\t\treturn modelViewMatrixID;\n\t}\n\n\tpublic int getModelViewProjectionMatrixID() {\n\t\treturn modelViewProjectionMatrixID;\n\t}\n\n\tpublic int getNormalMatrixID() {\n\t\treturn normalMatrixID;\n\t}\n\t\n\tpublic int getShadowMatrixID() {\n\t\treturn shadowMatrixID;\n\t}\n\n\t@Override\n\tpublic int getVertexTangentID() {\n\t\treturn vertexTangentID;\n\t}\n\n\t/**\n\t * Sets the PMVMatrix that was used to render the shadow map set with {@link #bindShadowMap(int)}.\n\t * This is needed to correctly compare the shadow map depth values with the fragment depth value.\n\t * @param pmvMatrix\n\t */\n\tpublic void setShadowMatrix(PMVMatrix pmvMatrix) {\n\t\t// S = B*MPV\n\t\t\n\t\t// bias matrix\n\t\tfloat[] b = {0.5f, 0,    0,    0,\n\t\t\t\t\t 0,    0.5f, 0,    0,\n\t\t\t\t\t 0,    0,    0.5f, 0f,\n\t\t\t\t\t 0.5f, 0.5f, 0.5f, 1.0f};\n\t\tFloatBuffer bb = FloatBuffer.wrap(b);\n\t\t\n\t\t// PMV of light source\n\t\tFloatBuffer pmvMat = FloatBuffer.allocate(16);\n\t\tFloatUtil.multMatrixf(pmvMatrix.glGetPMatrixf(), pmvMatrix.glGetMvMatrixf(), pmvMat);\n\t\t\n\t\tFloatBuffer shadowMat = FloatBuffer.allocate(16);\n\t\tFloatUtil.multMatrixf(bb, pmvMat, shadowMat);\n\t\t\n\t\tgl.glUniformMatrix4fv(this.getShadowMatrixID(), 1, false, shadowMat);\n\t}\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/DepthBufferShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_REPEAT;\nimport static javax.media.opengl.GL.GL_TEXTURE_2D;\nimport static javax.media.opengl.GL.GL_TEXTURE_WRAP_S;\nimport static javax.media.opengl.GL.GL_TEXTURE_WRAP_T;\nimport static javax.media.opengl.GL2GL3.GL_CLAMP_TO_BORDER;\nimport static javax.media.opengl.GL2GL3.GL_TEXTURE_BORDER_COLOR;\nimport static org.osm2world.core.target.jogl.AbstractJOGLTarget.getFloatBuffer;\n\nimport java.awt.Color;\nimport java.nio.FloatBuffer;\n\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.TextureData.Wrap;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Transparency;\n\nimport com.jogamp.opengl.math.FloatUtil;\nimport com.jogamp.opengl.util.PMVMatrix;\nimport com.jogamp.opengl.util.texture.Texture;\n\n/**\n * Shader that renders the depth buffer only. Supports transparent texture layers with {@link #USE_TRANSPARENCY}.\n */\npublic class DepthBufferShader extends AbstractPrimitiveShader {\n\n\tpublic static final boolean USE_TRANSPARENCY = true;\n\t\n\tprivate int modelViewProjectionMatrixID;\n\tprivate int vertexPositionID;\n\tprivate int[] vertexTexCoordID = new int[DefaultShader.MAX_TEXTURE_LAYERS];\n\t\n\tpublic DepthBufferShader(GL3 gl) {\n\t\tsuper(gl, \"/shaders/shadowmap\");\n\t\t\n\t\t// get indices of named attributes\n\t\tvertexPositionID = gl.glGetAttribLocation(shaderProgram, \"VertexPosition\");\n\t\tfor (int i=0; i<DefaultShader.MAX_TEXTURE_LAYERS; i++)\n\t\t\tvertexTexCoordID[i] = gl.glGetAttribLocation(shaderProgram, \"VertexTexCoord\"+i+\"\");\n\t\t\n\t\t// get indices of uniform variables\n\t\tmodelViewProjectionMatrixID = gl.glGetUniformLocation(shaderProgram, \"ModelViewProjectionMatrix\");\n\t\t\n\t\tthis.validateShader();\n\t}\n\t\n\t/**\n\t * Send uniform matrices \"ProjectionMatrix, ModelViewMatrix and ModelViewProjectionMatrix\" to vertex shader\n\t * @param pmvMatrix\n\t */\n\tpublic void setPMVMatrix(PMVMatrix pmvMatrix) {\n\t\tFloatBuffer pmvMat = FloatBuffer.allocate(16);\n\t\tFloatUtil.multMatrixf(pmvMatrix.glGetPMatrixf(), pmvMatrix.glGetMvMatrixf(), pmvMat);\n\t\tgl.glUniformMatrix4fv(this.getModelViewProjectionMatrixID(), 1, false, pmvMat);\n\t}\n\t\n\t@Override\n\tpublic int getVertexPositionID() {\n\t\treturn vertexPositionID;\n\t}\n\t\n\t/**\n\t * Returns the id to use to bind the ModelViewProjectionMatrix attribute.\n\t */\n\tpublic int getModelViewProjectionMatrixID() {\n\t\treturn modelViewProjectionMatrixID;\n\t}\n\n\t@Override\n\tpublic boolean setMaterial(Material material, JOGLTextureManager textureManager) {\n\t\t\n\t\tif (!USE_TRANSPARENCY) {\n\t\t\treturn true;\n\t\t}\n\t\t\n\t\t/*\n\t\t * only set textures (needed for transparency)\n\t\t */\n\t\tint numTexLayers = 0;\n\t\tif (material.getTextureDataList() != null) {\n\t\t\tnumTexLayers = material.getTextureDataList().size();\n\t\t}\n\t\t\n\t\t/* set textures and associated parameters */\n\t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useAlphaTreshold\"), material.getTransparency() == Transparency.BINARY ? 1 : 0);\n\t\tif (material.getTransparency() == Transparency.BINARY) {\n\t\t\tgl.glUniform1f(gl.glGetUniformLocation(shaderProgram, \"alphaTreshold\"), 0.5f );\n\t\t}\n\n\t    for (int i = 0; i < DefaultShader.MAX_TEXTURE_LAYERS; i++) {\n\t    \tif (i < numTexLayers) {\n\t\t\t\tgl.glActiveTexture(getGLTextureConstant(i));\n\t\t\t\tTextureData textureData = material.getTextureDataList().get(i);\n\t\t\t\tif (textureData.isBumpMap) {\n\t\t    \t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useTexture[\"+i+\"]\"), 0);\n\t\t    \t\tcontinue;\n\t\t\t\t} else {\n\t\t    \t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useTexture[\"+i+\"]\"), 1);\n\t\t\t\t}\n\t\t\t\tTexture texture = textureManager.getTextureForFile(textureData.file);\n\n\t\t\t\ttexture.bind(gl);\n\t\t        \n\t\t\t\t/* wrapping behavior */\n\t\t        \n\t\t\t\tint wrap = 0;\n\t\t\t\t\n\t\t\t\tswitch (textureData.wrap) {\n\t\t\t\tcase CLAMP: System.out.println(\"Warning: CLAMP is no longer supported. Using CLAMP_TO_BORDER instead.\"); wrap = GL_CLAMP_TO_BORDER; break;\n\t\t\t\tcase REPEAT: wrap = GL_REPEAT; break;\n\t\t\t\tcase CLAMP_TO_BORDER: wrap = GL_CLAMP_TO_BORDER; break;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap);\n\t\t        gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap);\n\t\t        \n\t\t        \n\t\t        if (textureData.wrap == Wrap.CLAMP_TO_BORDER) {\n\t\t        \t\n\t\t        \t/* TODO: make the RGB configurable -  for some reason,\n\t\t        \t * it shows up in lowzoom even if fully transparent */\n\t\t        \tgl.glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR,\n\t\t        \t\t\tgetFloatBuffer(new Color(1f, 1f, 1f, 0f)));\n\t\t        \t\n\t\t        }\n\n\t\t        int loc = gl.glGetUniformLocation(shaderProgram, \"Tex[\"+i+\"]\");\n\t\t        if (loc < 0) {\n\t\t        \t//throw new RuntimeException(\"Tex[\"+i+\"] not found in shader program.\");\n\t\t        }\n\t\t        gl.glUniform1i(loc, getGLTextureNumber(i));\n\t    \t} else {\n\t    \t\tgl.glUniform1i(gl.glGetUniformLocation(shaderProgram, \"useTexture[\"+i+\"]\"), 0);\n\t    \t}\n\t    }\n\t   \n\t    return true;\n\t}\n\n\tstatic final int getGLTextureConstant(int textureNumber) {\n\t\tswitch (getGLTextureNumber(textureNumber)) {\n\t\t//case 0: return GL.GL_TEXTURE0;\n\t\tcase 1: return GL.GL_TEXTURE1;\n\t\tcase 2: return GL.GL_TEXTURE2;\n\t\tcase 3: return GL.GL_TEXTURE3;\n\t\tcase 4: return GL.GL_TEXTURE4;\n\t\tdefault: throw new Error(\"programming error: unhandled texture number\");\n\t\t}\n\t}\n\t\n\tstatic final int getGLTextureNumber(int textureNumber) {\n\t\treturn textureNumber + 1;\n\t}\n\t\n\t@Override\n\tpublic int getVertexNormalID() {\n\t\treturn -1; // unused\n\t}\n\n\t@Override\n\tpublic int getVertexTexCoordID(int i) {\n\t\treturn vertexTexCoordID[i];\n\t}\n\n\t@Override\n\tpublic int getVertexBumpMapCoordID() {\n\t\treturn -1; // unused\n\t}\n\n\t@Override\n\tpublic int getVertexTangentID() {\n\t\treturn -1; // unused\n\t}\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLRenderer.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\n\n/**\n * Renders the contents of a {@link PrimitiveBuffer} using JOGL.\n */\nabstract class JOGLRenderer {\n\t\n\tprotected JOGLTextureManager textureManager;\n\t\n\tprotected JOGLRenderer(JOGLTextureManager textureManager) {\n\t\t\n\t\tthis.textureManager = textureManager;\n\t\t\n\t}\n\n\t/**\n\t * Render all primitives. Transparent objects need to get sorted first back to front\n\t * relative to the given camera and projection.\n\t */\n\tpublic abstract void render(Camera camera, Projection projection);\n\t\n\t/**\n\t * frees all OpenGL resources associated with this object.\n\t * Rendering will no longer be possible afterwards!\n\t */\n\tpublic void freeResources() {\n\t\t\n\t\ttextureManager = null;\n\t\t\n\t}\n\t\n\t@Override\n\tprotected void finalize() {\n\t\tfreeResources();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLRendererDisplayList.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static java.lang.Math.PI;\nimport static java.lang.Math.abs;\nimport static javax.media.opengl.GL2.GL_COMPILE;\nimport static org.osm2world.core.target.common.rendering.OrthoTilesUtil.CardinalDirection.closestCardinal;\nimport static org.osm2world.core.target.jogl.JOGLTargetFixedFunction.drawPrimitive;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport javax.media.opengl.GL2;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Transparency;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil.CardinalDirection;\nimport org.osm2world.core.target.common.rendering.Projection;\n\n/**\n * renders the contents of a {@link PrimitiveBuffer} using JOGL.\n * Uses display lists to speed up the process.\n * \n * If you don't need the renderer anymore, it's recommended to manually call\n * {@link #freeResources()} to delete the display lists. Otherwise, this will\n * not be done before a destructor call.\n */\nclass JOGLRendererDisplayList extends JOGLRenderer {\n\t\n\tprotected GL2 gl;\n\t\n\t/** pointer to the display list with static, non-transparent geometry */\n\tprivate Integer displayListPointer;\n\t\n\t/** transparent primitives, need to be sorted by distance from camera */\n\tprivate List<PrimitiveWithMaterial> transparentPrimitives =\n\t\t\tnew ArrayList<PrimitiveWithMaterial>();\n\t\n\t/**\n\t * the camera direction that was the basis for the previous sorting\n\t * of {@link #transparentPrimitives}.\n\t */\n\tprivate CardinalDirection currentPrimitiveSortDirection = null;\n\t\n\tprivate static final class PrimitiveWithMaterial {\n\t\t\n\t\tpublic final Primitive primitive;\n\t\tpublic final Material material;\n\t\t\n\t\tprivate PrimitiveWithMaterial(Primitive primitive, Material material) {\n\t\t\tthis.primitive = primitive;\n\t\t\tthis.material = material;\n\t\t}\n\t\t\n\t}\n\t\n\tpublic JOGLRendererDisplayList(GL2 gl, JOGLTextureManager textureManager,\n\t\t\tPrimitiveBuffer primitiveBuffer) {\n\t\t\n\t\tsuper(textureManager);\n\t\tthis.gl = gl;\n\t\t\n\t\tdisplayListPointer = gl.glGenLists(1);\n\t\t\n\t\tgl.glNewList(displayListPointer, GL_COMPILE);\n\n\t\tfor (Material material : primitiveBuffer.getMaterials()) {\n\t\t\t\n\t\t\tif (material.getTransparency() == Transparency.TRUE) {\n\t\t\t\t\n\t\t\t\tfor (Primitive primitive : primitiveBuffer.getPrimitives(material)) {\n\t\t\t\t\ttransparentPrimitives.add(\n\t\t\t\t\t\t\tnew PrimitiveWithMaterial(primitive, material));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tJOGLTargetFixedFunction.setMaterial(gl, material, textureManager);\n\t\n\t\t\t\tfor (Primitive primitive : primitiveBuffer.getPrimitives(material)) {\n\t\t\t\t\tdrawPrimitive(gl, AbstractJOGLTarget.getGLConstant(primitive.type),\n\t\t\t\t\t\t\tprimitive.vertices, primitive.normals,\n\t\t\t\t\t\t\tprimitive.texCoordLists);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tgl.glEndList();\n\t\t\n\t}\n\n\t@Override\n\tpublic void render(final Camera camera, final Projection projection) {\n\t\t\n\t\t/* render static geometry */\n\t\t\n\t\tif (displayListPointer == null)\n\t\t\tthrow new IllegalStateException(\"display list has been deleted\");\n\t\t\n\t\tgl.glCallList(displayListPointer);\n\t\t\n\t\t/* render transparent primitives back-to-front */\n\n\t\tsortPrimitivesBackToFront(camera, projection);\n\t\t\n\t\tMaterial previousMaterial = null;\n\t\t\n\t\tfor (PrimitiveWithMaterial p : transparentPrimitives) {\n\t\t\t\n\t\t\tif (!p.material.equals(previousMaterial)) {\n\t\t\t\tJOGLTargetFixedFunction.setMaterial(gl, p.material, textureManager);\n\t\t\t\tpreviousMaterial = p.material;\n\t\t\t}\n\t\t\t\n\t\t\tdrawPrimitive(gl, AbstractJOGLTarget.getGLConstant(p.primitive.type),\n\t\t\t\t\tp.primitive.vertices, p.primitive.normals,\n\t\t\t\t\tp.primitive.texCoordLists);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tprivate void sortPrimitivesBackToFront(final Camera camera,\n\t\t\tfinal Projection projection) {\n\t\t\n\t\tif (projection.isOrthographic() &&\n\t\t\t\tabs(camera.getViewDirection().xz().angle() % (PI/2)) < 0.01 ) {\n\t\t\t\n\t\t\t/* faster sorting for cardinal directions */\n\t\t\t\n\t\t\tCardinalDirection closestCardinal = closestCardinal(camera.getViewDirection().xz().angle());\n\t\t\t\n\t\t\tif (closestCardinal.isOppositeOf(currentPrimitiveSortDirection)) {\n\t\t\t\n\t\t\t\tCollections.reverse(transparentPrimitives);\n\t\t\t\t\n\t\t\t} else if (closestCardinal != currentPrimitiveSortDirection) {\n\t\t\t\t\t\n\t\t\t\tComparator<PrimitiveWithMaterial> comparator = null;\n\t\t\t\t\n\t\t\t\tswitch(closestCardinal) {\n\t\t\t\t\n\t\t\t\tcase N:\n\t\t\t\t\tcomparator = new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\t\t\treturn Double.compare(primitivePos(p2).z, primitivePos(p1).z);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\t\n\t\t\t\tcase E:\n\t\t\t\t\tcomparator = new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\t\t\treturn Double.compare(primitivePos(p2).x, primitivePos(p1).x);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\tcase S:\n\t\t\t\t\tcomparator = new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\t\t\treturn Double.compare(primitivePos(p1).z, primitivePos(p2).z);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\tcase W:\n\t\t\t\t\tcomparator = new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\t\t\treturn Double.compare(primitivePos(p1).x, primitivePos(p2).x);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tCollections.sort(transparentPrimitives, comparator);\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\n\t\t\tcurrentPrimitiveSortDirection = closestCardinal;\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\t/* sort based on distance to camera */\n\t\t\t\n\t\t\tCollections.sort(transparentPrimitives, new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t@Override\n\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\treturn Double.compare(\n\t\t\t\t\t\t\tdistanceToCameraSq(camera, p2),\n\t\t\t\t\t\t\tdistanceToCameraSq(camera, p1));\n\t\t\t\t}\n\t\t\t});\n\t\t\t\n\t\t\tcurrentPrimitiveSortDirection = null;\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tprivate double distanceToCameraSq(Camera camera, PrimitiveWithMaterial p) {\n\t\treturn primitivePos(p).distanceToSquared(camera.getPos());\n\t}\n\t\n\tprivate VectorXYZ primitivePos(PrimitiveWithMaterial p) {\n\t\t\n\t\tdouble sumX = 0, sumY = 0, sumZ = 0;\n\t\t\n\t\tfor (VectorXYZ v : p.primitive.vertices) {\n\t\t\tsumX += v.x;\n\t\t\tsumY += v.y;\n\t\t\tsumZ += v.z;\n\t\t}\n\t\t\n\t\treturn new VectorXYZ(sumX / p.primitive.vertices.size(),\n\t\t\t\tsumY / p.primitive.vertices.size(),\n\t\t\t\tsumZ / p.primitive.vertices.size());\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void freeResources() {\n\t\t\n\t\tif (displayListPointer != null) {\n\t\t\tgl.glDeleteLists(displayListPointer, 1);\n\t\t\tdisplayListPointer = null;\n\t\t}\n\t\tgl = null;\n\n\t\tsuper.freeResources();\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLRendererVBO.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static java.lang.Math.PI;\nimport static java.lang.Math.abs;\nimport static org.osm2world.core.target.common.rendering.OrthoTilesUtil.CardinalDirection.closestCardinal;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Transparency;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil.CardinalDirection;\nimport org.osm2world.core.target.common.rendering.Projection;\n\n/**\n * Base class for renderer that use vertex buffer objects (VBO) to speed up the process.\n * \n * If you don't need the renderer anymore, it's recommended to manually call\n * {@link #freeResources()} to delete the VBOs and other resources.\n */\npublic abstract class JOGLRendererVBO extends JOGLRenderer {\n\n\tprotected static final boolean DOUBLE_PRECISION_RENDERING = false;\n\n\t/** VBOs with static, non-alphablended geometry for each material */\n\tprotected List<VBOData<?>> vbos = new ArrayList<VBOData<?>>();\n\t\n\t/** alphablended primitives, need to be sorted by distance from camera */\n\tprotected List<PrimitiveWithMaterial> transparentPrimitives =\n\t\t\tnew ArrayList<PrimitiveWithMaterial>();\n\t\n\t/**\n\t * the camera direction that was the basis for the previous sorting\n\t * of {@link #transparentPrimitives}.\n\t */\n\tprivate CardinalDirection currentPrimitiveSortDirection = null;\n\n\tprotected static final class PrimitiveWithMaterial {\n\t\t\n\t\tpublic final Primitive primitive;\n\t\tpublic final Material material;\n\t\tpublic final VBOData<?> vbo;\n\t\t\n\t\tprivate PrimitiveWithMaterial(Primitive primitive, Material material, VBOData<?>vbo) {\n\t\t\tthis.primitive = primitive;\n\t\t\tthis.material = material;\n\t\t\tthis.vbo = vbo;\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * returns the number of values for each vertex\n\t * in the vertex buffer layout appropriate for a given material.\n\t */\n\tpublic static int getValuesPerVertex(Material material) {\n\t\t\n\t\tint numValues = 6; // vertex coordinates and normals\n\t\t\n\t\tif (material.getTextureDataList() != null) {\n\t\t\tnumValues += 2 * material.getTextureDataList().size();\n\t\t}\n\t\tif (material.hasBumpMap()) {\n\t\t\tnumValues += 4; // tangent vectors are 4D\n\t\t}\n\t\t\n\t\treturn numValues;\n\t\t\n\t}\n\t\n\tJOGLRendererVBO(JOGLTextureManager textureManager) {\n\t\tsuper(textureManager);\n\t}\n\t\n\t/**\n\t * Create the VBOs from a {@link PrimitiveBuffer}.\n\t * @param primitiveBuffer the source for the VBOs\n\t */\n\tprotected void init(PrimitiveBuffer primitiveBuffer) {\n\t\t\n\t\tfor (Material material : primitiveBuffer.getMaterials()) {\n\t\t\t\n\t\t\tif (material.getTransparency() == Transparency.TRUE) {\n\t\t\t\t\n\t\t\t\tfor (Primitive primitive : primitiveBuffer.getPrimitives(material)) {\n\t\t\t\t\ttransparentPrimitives.add(new PrimitiveWithMaterial(\n\t\t\t\t\t\t\tprimitive, material, this.createVBOData(\n\t\t\t\t\t\t\t\t\ttextureManager, material,\n\t\t\t\t\t\t\t\t\tArrays.asList(primitive))));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tCollection<Primitive> primitives = primitiveBuffer.getPrimitives(material);\n\t\t\t\tvbos.add(this.createVBOData(textureManager, material, primitives));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * Sort all transparent primitives back to front relative to the camera.\n\t * The projection can be used to speed up sorting if it is orthographic.\n\t */\n\tprotected void sortPrimitivesBackToFront(final Camera camera,\n\t\t\tfinal Projection projection) {\n\t\t\n\t\tif (projection.isOrthographic() &&\n\t\t\t\tabs(camera.getViewDirection().xz().angle() % (PI/2)) < 0.01 ) {\n\t\t\t\n\t\t\t/* faster sorting for cardinal directions */\n\t\t\t\n\t\t\tCardinalDirection closestCardinal = closestCardinal(camera.getViewDirection().xz().angle());\n\t\t\t\n\t\t\tif (closestCardinal.isOppositeOf(currentPrimitiveSortDirection)) {\n\t\t\t\t\n\t\t\t\tCollections.reverse(transparentPrimitives);\n\t\t\t\t\n\t\t\t} else if (closestCardinal != currentPrimitiveSortDirection) {\n\t\t\t\t\n\t\t\t\tComparator<PrimitiveWithMaterial> comparator = null;\n\t\t\t\t\n\t\t\t\tswitch(closestCardinal) {\n\t\t\t\t\n\t\t\t\tcase N:\n\t\t\t\t\tcomparator = new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\t\t\treturn Double.compare(primitivePos(p2).z, primitivePos(p1).z);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\tcase E:\n\t\t\t\t\tcomparator = new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\t\t\treturn Double.compare(primitivePos(p2).x, primitivePos(p1).x);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\tcase S:\n\t\t\t\t\tcomparator = new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\t\t\treturn Double.compare(primitivePos(p1).z, primitivePos(p2).z);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\tcase W:\n\t\t\t\t\tcomparator = new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\t\t\treturn Double.compare(primitivePos(p1).x, primitivePos(p2).x);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tCollections.sort(transparentPrimitives, comparator);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tcurrentPrimitiveSortDirection = closestCardinal;\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\t/* sort based on distance to camera */\n\t\t\t\n\t\t\tCollections.sort(transparentPrimitives, new Comparator<PrimitiveWithMaterial>() {\n\t\t\t\t@Override\n\t\t\t\tpublic int compare(PrimitiveWithMaterial p1, PrimitiveWithMaterial p2) {\n\t\t\t\t\treturn Double.compare(\n\t\t\t\t\t\t\tdistanceToCameraSq(camera, p2),\n\t\t\t\t\t\t\tdistanceToCameraSq(camera, p1));\n\t\t\t\t}\n\t\t\t});\n\t\t\t\n\t\t\tcurrentPrimitiveSortDirection = null;\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate double distanceToCameraSq(Camera camera, PrimitiveWithMaterial p) {\n\t\treturn primitivePos(p).distanceToSquared(camera.getPos());\n\t}\n\t\n\tprivate VectorXYZ primitivePos(PrimitiveWithMaterial p) {\n\t\t\n\t\tdouble sumX = 0, sumY = 0, sumZ = 0;\n\t\t\n\t\tfor (VectorXYZ v : p.primitive.vertices) {\n\t\t\tsumX += v.x;\n\t\t\tsumY += v.y;\n\t\t\tsumZ += v.z;\n\t\t}\n\t\t\n\t\treturn new VectorXYZ(sumX / p.primitive.vertices.size(),\n\t\t\t\tsumY / p.primitive.vertices.size(),\n\t\t\t\tsumZ / p.primitive.vertices.size());\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void freeResources() {\n\t\t\n\t\tif (vbos != null) {\n\t\t\tfor (VBOData<?> vbo : vbos) {\n\t\t\t\tvbo.delete();\n\t\t\t}\n\t\t\tvbos = null;\n\t\t}\n\t\t\n\t\tsuper.freeResources();\n\t\t\n\t}\n\t\n\t/**\n\t * Create a new vertex buffer object for a bunch of primitives with the same material.\n\t * @param textureManager the texture manager used if the material contains texture layers.\n\t * @param material the material that applies to all primitives\n\t * @param primitives the primitives to create the VBO for\n\t * @return a vertex buffer object matching the primitives\n\t */\n\tabstract VBOData<?> createVBOData(JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives);\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLRendererVBOFixedFunction.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_ARRAY_BUFFER;\nimport static javax.media.opengl.GL.GL_FLOAT;\nimport static javax.media.opengl.GL2GL3.GL_DOUBLE;\nimport static javax.media.opengl.fixedfunc.GLPointerFunc.GL_NORMAL_ARRAY;\nimport static javax.media.opengl.fixedfunc.GLPointerFunc.GL_TEXTURE_COORD_ARRAY;\nimport static javax.media.opengl.fixedfunc.GLPointerFunc.GL_VERTEX_ARRAY;\nimport static org.osm2world.core.target.jogl.JOGLTargetFixedFunction.drawPrimitive;\n\nimport java.nio.DoubleBuffer;\nimport java.nio.FloatBuffer;\nimport java.util.Collection;\n\nimport javax.media.opengl.GL2;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\n\nimport com.jogamp.common.nio.Buffers;\n\n/**\n * Renders the contents of a {@link PrimitiveBuffer} using JOGL and the old fixed function OpengGL pipeline.\n * Uses vertex buffer objects (VBO) to speed up the process.\n * \n * If you don't need the renderer anymore, it's recommended to manually call\n * {@link #freeResources()} to delete the VBOs and other resources.\n */\npublic class JOGLRendererVBOFixedFunction extends JOGLRendererVBO {\n\t\n\tprotected GL2 gl;\n\t\n\tprivate final class VBODataDouble extends VBODataFixedFunction<DoubleBuffer> {\n\n\t\tpublic VBODataDouble(GL2 gl, JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives) {\n\t\t\tsuper(gl, textureManager, material, primitives);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected DoubleBuffer createBuffer(int numValues) {\n\t\t\treturn Buffers.newDirectDoubleBuffer(numValues);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(DoubleBuffer buffer, VectorXZ texCoord) {\n\t\t\tbuffer.put(texCoord.x);\n\t\t\tbuffer.put(texCoord.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(DoubleBuffer buffer, VectorXYZ v) {\n\t\t\tbuffer.put(v.x);\n\t\t\tbuffer.put(v.y);\n\t\t\tbuffer.put(-v.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int valueTypeSize() {\n\t\t\treturn Buffers.SIZEOF_DOUBLE;\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int glValueType() {\n\t\t\treturn GL_DOUBLE;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate final class VBODataFloat extends VBODataFixedFunction<FloatBuffer> {\n\n\t\tpublic VBODataFloat(GL2 gl, JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives) {\n\t\t\tsuper(gl, textureManager, material, primitives);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected FloatBuffer createBuffer(int numValues) {\n\t\t\treturn Buffers.newDirectFloatBuffer(numValues);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(FloatBuffer buffer, VectorXZ texCoord) {\n\t\t\tbuffer.put((float)texCoord.x);\n\t\t\tbuffer.put((float)texCoord.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(FloatBuffer buffer, VectorXYZ v) {\n\t\t\tbuffer.put((float)v.x);\n\t\t\tbuffer.put((float)v.y);\n\t\t\tbuffer.put((float)-v.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int valueTypeSize() {\n\t\t\treturn Buffers.SIZEOF_FLOAT;\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int glValueType() {\n\t\t\treturn GL_FLOAT;\n\t\t}\n\t\t\n\t}\n\t\n\tJOGLRendererVBOFixedFunction(GL2 gl, JOGLTextureManager textureManager,\n\t\t\tPrimitiveBuffer primitiveBuffer) {\n\t\t\n\t\tsuper(textureManager);\n\t\tthis.gl = gl;\n\t\tthis.init(primitiveBuffer);\n\t}\n\t\n\t@Override\n\tVBOData<?> createVBOData(JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives) {\n\t\tif (DOUBLE_PRECISION_RENDERING)\n\t\t\treturn new VBODataDouble(gl, textureManager, material, primitives);\n\t\telse\n\t\t\treturn new VBODataFloat(gl, textureManager, material, primitives);\n\t}\n\t\n\t@Override\n\tpublic void render(final Camera camera, final Projection projection) {\n\t\t\n\t\t/* render static geometry */\n\t\t\n\t\tgl.glEnableClientState(GL_VERTEX_ARRAY);\n\t\tgl.glEnableClientState(GL_NORMAL_ARRAY);\n\t\t\n\t\tfor (VBOData<?> vboData : vbos) {\n\t\t\tvboData.render();\n\t\t}\n\t\t\n\t\tgl.glDisableClientState(GL_VERTEX_ARRAY);\n\t\tgl.glDisableClientState(GL_NORMAL_ARRAY);\n\t\t\n\t\tfor (int t = 0; t < JOGLTargetFixedFunction.MAX_TEXTURE_LAYERS; t++) {\n\t\t\tgl.glClientActiveTexture(JOGLTargetFixedFunction.getGLTextureConstant(t));\n\t\t\tgl.glDisableClientState(GL_TEXTURE_COORD_ARRAY);\n\t\t}\n\t\t\n\t\tgl.glBindBuffer(GL_ARRAY_BUFFER, 0);\n\t\t\n\t\t\n\t\t/* render transparent primitives back-to-front */\n\t\t\n\t\tsortPrimitivesBackToFront(camera, projection);\n\t\t\n\t\tMaterial previousMaterial = null;\n\t\t\n\t\tfor (PrimitiveWithMaterial p : transparentPrimitives) {\n\t\t\t\n\t\t\tif (!p.material.equals(previousMaterial)) {\n\t\t\t\tJOGLTargetFixedFunction.setMaterial(gl, p.material, textureManager);\n\t\t\t\tpreviousMaterial = p.material;\n\t\t\t}\n\t\t\t\n\t\t\tdrawPrimitive(gl, AbstractJOGLTarget.getGLConstant(p.primitive.type),\n\t\t\t\t\tp.primitive.vertices, p.primitive.normals,\n\t\t\t\t\tp.primitive.texCoordLists);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void freeResources() {\n\t\tgl = null;\n\t\tsuper.freeResources();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLRendererVBONonAreaShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_FLOAT;\nimport static javax.media.opengl.GL2GL3.GL_DOUBLE;\n\nimport java.awt.Color;\nimport java.nio.DoubleBuffer;\nimport java.nio.FloatBuffer;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.math.VectorXYZ;\n\nimport com.jogamp.common.nio.Buffers;\n\n/**\n * Renders the a bunch of {@link NonAreaPrimitive} objects using JOGL and the new shader based OpengGL pipeline.\n * Uses vertex buffer objects (VBO) to speed up the process.\n * \n * If you don't need the renderer anymore, it's recommended to manually call\n * {@link #freeResources()} to delete the VBOs and other resources.\n */\npublic class JOGLRendererVBONonAreaShader {\n\t\n\tprotected static final boolean DOUBLE_PRECISION_RENDERING = false;\n\t\n\tprotected GL3 gl;\n\tprivate NonAreaShader shader;\n\tprotected List<VBODataNonAreaShader<?>> vbos = new ArrayList<VBODataNonAreaShader<?>>();\n\t\n\tprivate final class VBODataDouble extends VBODataNonAreaShader<DoubleBuffer> {\n\n\t\tpublic VBODataDouble(GL3 gl, NonAreaPrimitive primitive) {\n\t\t\tsuper(gl, shader, primitive);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected DoubleBuffer createBuffer(int numValues) {\n\t\t\treturn Buffers.newDirectDoubleBuffer(numValues);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(DoubleBuffer buffer, Color color) {\n\t\t\tbuffer.put(color.getRed()/255d);\n\t\t\tbuffer.put(color.getGreen()/255d);\n\t\t\tbuffer.put(color.getBlue()/255d);\n\t\t\tbuffer.put(color.getAlpha()/255d);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(DoubleBuffer buffer, VectorXYZ v) {\n\t\t\tbuffer.put(v.x);\n\t\t\tbuffer.put(v.y);\n\t\t\tbuffer.put(-v.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int valueTypeSize() {\n\t\t\treturn Buffers.SIZEOF_DOUBLE;\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int glValueType() {\n\t\t\treturn GL_DOUBLE;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate final class VBODataFloat extends VBODataNonAreaShader<FloatBuffer> {\n\n\t\tpublic VBODataFloat(GL3 gl, NonAreaPrimitive primitive) {\n\t\t\tsuper(gl, shader, primitive);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected FloatBuffer createBuffer(int numValues) {\n\t\t\treturn Buffers.newDirectFloatBuffer(numValues);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(FloatBuffer buffer, Color color) {\n\t\t\tbuffer.put(color.getRed()/255f);\n\t\t\tbuffer.put(color.getGreen()/255f);\n\t\t\tbuffer.put(color.getBlue()/255f);\n\t\t\tbuffer.put(color.getAlpha()/255f);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(FloatBuffer buffer, VectorXYZ v) {\n\t\t\tbuffer.put((float)v.x);\n\t\t\tbuffer.put((float)v.y);\n\t\t\tbuffer.put((float)-v.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int valueTypeSize() {\n\t\t\treturn Buffers.SIZEOF_FLOAT;\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int glValueType() {\n\t\t\treturn GL_FLOAT;\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * Creates vertex buffer objects for all primitives.\n\t * @param shader the shader used for rendering\n\t * @param primitives the primitives to render\n\t */\n\tpublic JOGLRendererVBONonAreaShader(GL3 gl, NonAreaShader shader, Collection<NonAreaPrimitive> primitives) {\n\t\tthis.gl = gl;\n\t\tthis.shader = shader;\n\t\tfor (NonAreaPrimitive nonAreaPrimitive : primitives) {\n\t\t\tVBODataNonAreaShader<?> vbo;\n\t\t\tif (DOUBLE_PRECISION_RENDERING)\n\t\t\t\tvbo = new VBODataDouble(gl, nonAreaPrimitive);\n\t\t\telse\n\t\t\t\tvbo = new VBODataFloat(gl, nonAreaPrimitive);\n\t\t\tvbos.add(vbo);\n\t\t}\n\t}\n\t\n\t/**\n\t * Renders all VBOs for the {@link NonAreaPrimitive} objects.\n\t */\n\tpublic void render() {\n\t\t\n\t\tgl.glEnableVertexAttribArray(shader.getVertexPositionID());\n\t\tgl.glEnableVertexAttribArray(shader.getVertexColorID());\n\t\t\n\t\t// render non area primitives\n\t\tfor (VBODataNonAreaShader<?> vbo : vbos) {\n\t\t\t\n\t\t\tvbo.render();\n\t\t\t\n\t\t}\n\t\t\n\t\tgl.glDisableVertexAttribArray(shader.getVertexPositionID());\n\t\tgl.glDisableVertexAttribArray(shader.getVertexColorID());\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLRendererVBOShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_FLOAT;\nimport static javax.media.opengl.GL2GL3.GL_DOUBLE;\n\nimport java.nio.DoubleBuffer;\nimport java.nio.FloatBuffer;\nimport java.util.ArrayList;\nimport java.util.Collection;\n\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXYZ;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXYZW;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\n\nimport com.jogamp.common.nio.Buffers;\n\n/**\n * Renders the contents of a {@link PrimitiveBuffer} using JOGL and the new shader based OpenGL pipeline.\n * Uses vertex buffer objects (VBO) to speed up the process.\n * \n * If you don't need the renderer anymore, it's recommended to manually call\n * {@link #freeResources()} to delete the VBOs and other resources.\n */\npublic class JOGLRendererVBOShader extends JOGLRendererVBO {\n\t\n\tprotected GL3 gl;\n\tprotected AbstractPrimitiveShader shader;\n\tprotected AxisAlignedBoundingBoxXYZ boundingBox = null;\n\t\n\tprivate final class VBODataDouble extends VBODataShader<DoubleBuffer> {\n\n\t\tpublic VBODataDouble(GL3 gl, JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives) {\n\t\t\tsuper(gl, textureManager, material, primitives);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected DoubleBuffer createBuffer(int numValues) {\n\t\t\treturn Buffers.newDirectDoubleBuffer(numValues);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(DoubleBuffer buffer, VectorXZ texCoord) {\n\t\t\tbuffer.put(texCoord.x);\n\t\t\tbuffer.put(texCoord.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(DoubleBuffer buffer, VectorXYZ v) {\n\t\t\tbuffer.put(v.x);\n\t\t\tbuffer.put(v.y);\n\t\t\tbuffer.put(-v.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(DoubleBuffer buffer, VectorXYZW t) {\n\t\t\tbuffer.put(t.x);\n\t\t\tbuffer.put(t.y);\n\t\t\tbuffer.put(-t.z);\n\t\t\tbuffer.put(t.w);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int valueTypeSize() {\n\t\t\treturn Buffers.SIZEOF_DOUBLE;\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int glValueType() {\n\t\t\treturn GL_DOUBLE;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate final class VBODataFloat extends VBODataShader<FloatBuffer> {\n\n\t\tpublic VBODataFloat(GL3 gl, JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives) {\n\t\t\tsuper(gl, textureManager, material, primitives);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected FloatBuffer createBuffer(int numValues) {\n\t\t\treturn Buffers.newDirectFloatBuffer(numValues);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(FloatBuffer buffer, VectorXZ texCoord) {\n\t\t\tbuffer.put((float)texCoord.x);\n\t\t\tbuffer.put((float)texCoord.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(FloatBuffer buffer, VectorXYZ v) {\n\t\t\tbuffer.put((float)v.x);\n\t\t\tbuffer.put((float)v.y);\n\t\t\tbuffer.put((float)-v.z);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(FloatBuffer buffer, VectorXYZW t) {\n\t\t\tbuffer.put((float)t.x);\n\t\t\tbuffer.put((float)t.y);\n\t\t\tbuffer.put((float)-t.z);\n\t\t\tbuffer.put((float)t.w);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int valueTypeSize() {\n\t\t\treturn Buffers.SIZEOF_FLOAT;\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int glValueType() {\n\t\t\treturn GL_FLOAT;\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * Creates vertex buffer objects for all primitives and computes a bounding box around them.\n\t * @param primitiveBuffer the primitives to create the VBOs for\n\t * @param xzBoundary the boundary of the OSM file. Used to tighten the bounding box to only primitives within these bounds.\n\t */\n\tJOGLRendererVBOShader(GL3 gl, JOGLTextureManager textureManager,\n\t\t\tPrimitiveBuffer primitiveBuffer, AxisAlignedBoundingBoxXZ xzBoundary) {\n\t\t\n\t\tsuper(textureManager);\n\t\tthis.gl = gl;\n\t\tthis.init(primitiveBuffer);\n\t\t\n\t\tArrayList<VectorXYZ> boundedVertices = new ArrayList<VectorXYZ>();\n\t\tfor (Material m : primitiveBuffer.getMaterials()) {\n\t\t\tfor (Primitive p : primitiveBuffer.getPrimitives(m)) {\n\t\t\t\tfor (VectorXYZ v : p.vertices) {\n\t\t\t\t\tif (xzBoundary == null || xzBoundary.contains(v.xz())) {\n\t\t\t\t\t\tboundedVertices.add(new VectorXYZ(v.x, v.y, -v.z));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tboundingBox = new AxisAlignedBoundingBoxXYZ(boundedVertices);\n\t}\n\t\n\t@Override\n\tVBOData<?> createVBOData(JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives) {\n\t\tif (DOUBLE_PRECISION_RENDERING)\n\t\t\treturn new VBODataDouble(gl, textureManager, material, primitives);\n\t\telse\n\t\t\treturn new VBODataFloat(gl, textureManager, material, primitives);\n\t}\n\t\n\t/**\n\t * Render the stored VBOS. Uses the currently set shader. Transparent primitives are not sorted.\n\t * If they have to be sorted for the set shader then use {@link #render(Camera, Projection)}.\n\t */\n\tpublic void render() {\n\n\t\t/* render static geometry */\n\t\t\n\t\tshader.glEnableVertexAttribArray(shader.getVertexPositionID());\n\t\tshader.glEnableVertexAttribArray(shader.getVertexNormalID());\n\t\t\n\t\tfor (VBOData<?> vboData : vbos) {\n\t\t\t((VBODataShader<?>)vboData).setShader(shader);\n\t\t\tvboData.render();\n\t\t}\n\t\t\n\t\t/* render transparent primitives unsorted */\n\t\t\n\t\tfor (PrimitiveWithMaterial p : transparentPrimitives) {\n\t\t\t((VBODataShader<?>)p.vbo).setShader(shader);\n\t\t\tp.vbo.render();\n\t\t}\n\t\t\n\t\tshader.glDisableVertexAttribArray(shader.getVertexPositionID());\n\t\tshader.glDisableVertexAttribArray(shader.getVertexNormalID());\n\t\t\n\t}\n\t\n\t/**\n\t * Render the stored VBOs. Uses the currently set shader. Transparent objects get sorted first back to front\n\t * relative to the given camera and projection.\n\t */\n\t@Override\n\tpublic void render(final Camera camera, final Projection projection) {\n\t\t\n\t\t/* render static geometry */\n\t\t\n\t\tshader.glEnableVertexAttribArray(shader.getVertexPositionID());\n\t\tshader.glEnableVertexAttribArray(shader.getVertexNormalID());\n\t\t\n\t\tfor (VBOData<?> vboData : vbos) {\n\t\t\t((VBODataShader<?>)vboData).setShader(shader);\n\t\t\tvboData.render();\n\t\t}\n\t\t\n\t\t/* render transparent primitives back-to-front */\n\t\t\n\t\tsortPrimitivesBackToFront(camera, projection);\n\t\t\n\t\tfor (PrimitiveWithMaterial p : transparentPrimitives) {\n\t\t\t((VBODataShader<?>)p.vbo).setShader(shader);\n\t\t\tp.vbo.render();\n\t\t}\n\t\t\n\t\tshader.glDisableVertexAttribArray(shader.getVertexPositionID());\n\t\tshader.glDisableVertexAttribArray(shader.getVertexNormalID());\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void freeResources() {\n\t\tgl = null;\n\t\tsuper.freeResources();\n\t}\n\t\n\t/**\n\t * Change the shader used to render the VBOs\n\t */\n\tpublic void setShader(AbstractPrimitiveShader shader) {\n\t\tthis.shader = shader;\n\t}\n\t\n\t/**\n\t * Get the bounding box around all relevant primitives computed at {@link #JOGLRendererVBOShader(GL3, JOGLTextureManager, PrimitiveBuffer, AxisAlignedBoundingBoxXZ)}\n\t */\n\tpublic AxisAlignedBoundingBoxXYZ getBoundingBox() {\n\t\treturn boundingBox;\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLRendererVBOShadowVolume.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_FLOAT;\nimport static javax.media.opengl.GL2GL3.GL_DOUBLE;\n\nimport java.nio.DoubleBuffer;\nimport java.nio.FloatBuffer;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.math.VectorXYZW;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Shadow;\nimport org.osm2world.core.target.common.material.Material.Transparency;\n\nimport com.jogamp.common.nio.Buffers;\n\n/**\n * Renders the shadow volumes for the contents of a {@link PrimitiveBuffer} using JOGL.\n * Uses vertex buffer objects (VBO) to speed up the process.\n * \n * If you don't need the renderer anymore, it's recommended to manually call\n * {@link #freeResources()} to delete the VBOs and other resources.\n */\npublic class JOGLRendererVBOShadowVolume  {\n\t\n\tprotected GL3 gl;\n\tprotected AbstractPrimitiveShader shader;\n\tprotected VectorXYZW lightPos;\n\n\tprotected static final boolean DOUBLE_PRECISION_RENDERING = false;\n\n\t/** VBOs with static, non-alphablended geometry for each material */\n\tprotected List<VBODataShadowVolume<?>> vbos = new ArrayList<VBODataShadowVolume<?>>();\n\t\n\tprivate final class VBODataDouble extends VBODataShadowVolume<DoubleBuffer> {\n\n\t\tpublic VBODataDouble(GL3 gl, Collection<Primitive> primitives, VectorXYZW lightPos) {\n\t\t\tsuper(gl, primitives, lightPos);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected DoubleBuffer createBuffer(int numValues) {\n\t\t\treturn Buffers.newDirectDoubleBuffer(numValues);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(DoubleBuffer buffer, VectorXYZW t) {\n\t\t\tbuffer.put(t.x);\n\t\t\tbuffer.put(t.y);\n\t\t\tbuffer.put(-t.z);\n\t\t\tbuffer.put(t.w);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int valueTypeSize() {\n\t\t\treturn Buffers.SIZEOF_DOUBLE;\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int glValueType() {\n\t\t\treturn GL_DOUBLE;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate final class VBODataFloat extends VBODataShadowVolume<FloatBuffer> {\n\n\t\tpublic VBODataFloat(GL3 gl, Collection<Primitive> primitives, VectorXYZW lightPos) {\n\t\t\tsuper(gl, primitives, lightPos);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected FloatBuffer createBuffer(int numValues) {\n\t\t\treturn Buffers.newDirectFloatBuffer(numValues);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected void put(FloatBuffer buffer, VectorXYZW t) {\n\t\t\tbuffer.put((float)t.x);\n\t\t\tbuffer.put((float)t.y);\n\t\t\tbuffer.put((float)-t.z);\n\t\t\tbuffer.put((float)t.w);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int valueTypeSize() {\n\t\t\treturn Buffers.SIZEOF_FLOAT;\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected int glValueType() {\n\t\t\treturn GL_FLOAT;\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * Creates the shadow volumes (and VBOs) for all primitives in a {@link PrimitiveBuffer}.\n\t * @param primitiveBuffer the primitives to create the shadow volumes for\n\t * @param lightPos the position of the light source to create the shadow volumes for. If ligthPose.w == 0, the light is directional.\n\t */\n\tJOGLRendererVBOShadowVolume(GL3 gl, PrimitiveBuffer primitiveBuffer, VectorXYZW lightPos) {\n\t\t\n\t\tthis.gl = gl;\n\t\tthis.lightPos = lightPos;\n\t\tthis.init(primitiveBuffer);\n\t}\n\t\n\tprotected void init(PrimitiveBuffer primitiveBuffer) {\n\t\t\n\t\tCollection<Primitive> combinedPrimitives = new ArrayList<Primitive>();\n\t\tfor (Material material : primitiveBuffer.getMaterials()) {\n\t\t\t\n\t\t\tif (material.getTransparency() == Transparency.FALSE && material.getShadow() == Shadow.TRUE) {\n\t\t\t\tCollection<Primitive> primitives = primitiveBuffer.getPrimitives(material);\n\t\t\t\tcombinedPrimitives.addAll(primitives);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\tvbos.add(this.createVBOData(combinedPrimitives));\n\t\t\n\t}\n\t\n\tVBODataShadowVolume<?> createVBOData(Collection<Primitive> primitives) {\n\t\tif (DOUBLE_PRECISION_RENDERING)\n\t\t\treturn new VBODataDouble(gl, primitives, lightPos);\n\t\telse\n\t\t\treturn new VBODataFloat(gl, primitives, lightPos);\n\t}\n\t\n\t/**\n\t * Render all shadow volume VBOs\n\t */\n\tpublic void render() {\n\t\t\n\t\t/* render static geometry */\n\t\t\n\t\tshader.glEnableVertexAttribArray(shader.getVertexPositionID());\n\t\t\n\t\tfor (VBODataShadowVolume<?> vboData : vbos) {\n\t\t\tvboData.setShader(shader);\n\t\t\tvboData.render();\n\t\t}\n\t\t\n\t\tshader.glDisableVertexAttribArray(shader.getVertexPositionID());\n\t\t\n\t}\n\t\n\t/**\n\t * frees all OpenGL resources associated with this object. Rendering will no longer be possible afterwards!\n\t */\n\tpublic void freeResources() {\n\t\tgl = null;\n\t\tif (vbos != null) {\n\t\t\tfor (VBODataShadowVolume<?> vbo : vbos) {\n\t\t\t\tvbo.delete();\n\t\t\t}\n\t\t\tvbos = null;\n\t\t}\n\t}\n\t\n\t/**\n\t * Set the shader used for rendering.\n\t */\n\tpublic void setShader(AbstractPrimitiveShader shader) {\n\t\tthis.shader = shader;\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLRenderingParameters.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_CCW;\nimport static javax.media.opengl.GL.GL_CW;\n\n/**\n * global parameters for rendering a JOGL scene\n */\npublic class JOGLRenderingParameters {\n\t\n\tpublic static enum Winding {\n\t\t\n\t\tCW(GL_CW), CCW(GL_CCW);\n\t\t\n\t\tint glConstant;\n\t\t\n\t\tprivate Winding(int glConstant) {\n\t\t\tthis.glConstant = glConstant;\n\t\t}\n\t\t\n\t};\n\t\n\tfinal Winding frontFace;\n\tfinal boolean wireframe;\n\tfinal boolean useZBuffer;\n\tfinal boolean drawBoundingBox;\n\tfinal boolean useShadowVolumes;\n\tfinal boolean useShadowMaps;\n\tfinal int shadowMapWidth;\n\tfinal int shadowMapHeight;\n\tfinal int shadowMapCameraFrustumPadding;\n\tfinal boolean useSSAO;\n\tfinal int SSAOkernelSize;\n\tfinal float SSAOradius;\n\tfinal boolean overwriteProjectionClippingPlanes;\n\t\n\t/**\n\t * @param frontFace\n\t *            winding of the front face for backface culling; null disables\n\t *            backface culling\n\t * @param wireframe\n\t *            renders just a wireframe instead of filled surfaces\n\t * @param useZBuffer\n\t *            enables the z buffer, should usually be true\n\t * @param drawBoundingBox\n\t *            draw the bounding box used when rendering to determine all\n\t *            relevant primitives\n\t * @param useShadowVolumes\n\t *            renders only shadows casted by non-transparent objects with\n\t *            shadow volumes\n\t * @param useShadowMaps\n\t *            renders the shadows of all objects with a shadow map, but only\n\t *            the back faces (from light source view) cast a shadow. if\n\t *            shadow volumes are activated too, the shadow map is only used\n\t *            for non opaque objects.\n\t * @param shadowMapWidth\n\t *            resolution of the shadow map\n\t * @param shadowMapHeight\n\t *            resolution of the shadow map\n\t * @param shadowMapCameraFrustumPadding\n\t *            padding in meter for the camera frustum to use for the shadow\n\t *            map camera. Increase here if objects outside the current\n\t *            camera view frustum, that should throw a shadow won't do so.\n\t * @param useSSAO\n\t *            use screen space ambient occlusion\n\t * @param SSAOkernelSize\n\t *            size of the sampling kernel (number of samples)\n\t * @param SSAOradius\n\t *            sampling radius in meter\n\t * @param overwriteProjectionClippingPlanes\n\t *            optimize the clipping planes of the camera: reduce them to\n\t *            match the world bounding box\n\t */\n\tpublic JOGLRenderingParameters(\n\t\t\tWinding frontFace, boolean wireframe, boolean useZBuffer, boolean drawBoundingBox, boolean useShadowVolumes,\n\t\t\tboolean useShadowMaps, int shadowMapWidth, int shadowMapHeight, int shadowMapCameraFrustumPadding, \n\t\t\tboolean useSSAO, int SSAOkernelSize, float SSAOradius, boolean overwriteProjectionClippingPlanes) {\n\t\t\n\t\tthis.frontFace = frontFace;\n\t\tthis.wireframe = wireframe;\n\t\tthis.useZBuffer = useZBuffer;\n\t\tthis.drawBoundingBox = drawBoundingBox;\n\t\tthis.useShadowVolumes = useShadowVolumes;\n\t\tthis.useShadowMaps = useShadowMaps;\n\t\tthis.shadowMapWidth = shadowMapWidth;\n\t\tthis.shadowMapHeight = shadowMapHeight;\n\t\tthis.shadowMapCameraFrustumPadding = shadowMapCameraFrustumPadding;\n\t\tthis.useSSAO = useSSAO;\n\t\tthis.SSAOkernelSize = SSAOkernelSize;\n\t\tthis.SSAOradius = SSAOradius;\n\t\tthis.overwriteProjectionClippingPlanes = overwriteProjectionClippingPlanes;\n\t}\n\t\n\t/**\n\t * @param frontFace   winding of the front face for backface culling;\n\t *                     null disables backface culling\n\t * @param wireframe   renders just a wireframe instead of filled surfaces\n\t * @param useZBuffer  enables the z buffer, should usually be true\n\t */\n\tpublic JOGLRenderingParameters(\n\t\t\tWinding frontFace, boolean wireframe, boolean useZBuffer) {\n\t\t\n\t\tthis.frontFace = frontFace;\n\t\tthis.wireframe = wireframe;\n\t\tthis.useZBuffer = useZBuffer;\n\t\tthis.drawBoundingBox = false;\n\t\tthis.useShadowVolumes = false;\n\t\tthis.useShadowMaps = false;\n\t\tthis.shadowMapWidth = 0;\n\t\tthis.shadowMapHeight = 0;\n\t\tthis.shadowMapCameraFrustumPadding = 0;\n\t\tthis.useSSAO = false;\n\t\tthis.SSAOkernelSize = 0;\n\t\tthis.SSAOradius = 0;\n\t\tthis.overwriteProjectionClippingPlanes = false;\n\t}\n\t\n\tpublic JOGLRenderingParameters() {\n\t\tthis.frontFace = null;\n\t\tthis.wireframe = false;\n\t\tthis.useZBuffer = true;\n\t\tthis.drawBoundingBox = false;\n\t\tthis.useShadowVolumes = false;\n\t\tthis.useShadowMaps = false;\n\t\tthis.shadowMapWidth = 0;\n\t\tthis.shadowMapHeight = 0;\n\t\tthis.shadowMapCameraFrustumPadding = 0;\n\t\tthis.useSSAO = false;\n\t\tthis.SSAOkernelSize = 0;\n\t\tthis.SSAOradius = 0;\n\t\tthis.overwriteProjectionClippingPlanes = false;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLTarget.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport java.awt.Color;\nimport java.io.File;\nimport java.util.List;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\n\npublic interface JOGLTarget extends Target<RenderableToJOGL> {\n\t\t\n\t/**\n\t * discards all accumulated draw calls\n\t */\n\tpublic void reset();\n\t\n\tpublic void drawPoints(Color color, VectorXYZ... vs);\n\t\n\tpublic void drawLineStrip(Color color, int width, VectorXYZ... vs);\n\t\n\tpublic void drawLineStrip(Color color, int width, List<VectorXYZ> vs);\n\t\n\tpublic void drawLineLoop(Color color, int width, List<VectorXYZ> vs);\n\t\n\t/**\n\t * set global lighting parameters. Using this method affects all primitives\n\t * (even those from previous draw calls).\n\t * \n\t * @param parameters  parameter object; null disables lighting\n\t */\n\tpublic void setGlobalLightingParameters(\n\t\t\tGlobalLightingParameters parameters);\n\n\t/**\n\t * set global rendering parameters. Using this method affects all primitives\n\t * (even those from previous draw calls).\n\t */\n\tpublic void setRenderingParameters(\n\t\t\tJOGLRenderingParameters renderingParameters);\n\t\n\tpublic void setConfiguration(Configuration config);\n\t\n\tpublic boolean isFinished();\n\t\n\tpublic void render(Camera camera, Projection projection);\n\t\n\t/**\n\t * similar to {@link #render(Camera, Projection)},\n\t * but allows rendering only a part of the \"normal\" image.\n\t * For example, with xStart=0, xEnd=0.5, yStart=0 and yEnd=1,\n\t * only the left half of the full image will be rendered,\n\t * but it will be stretched to cover the available space.\n\t * \n\t * Only supported for orthographic projections!\n\t */\n\tpublic void renderPart(Camera camera, Projection projection,\n\t\t\tdouble xStart, double xEnd, double yStart, double yEnd);\n\t\n\tpublic void freeResources();\n\t\n\tpublic void drawBackgoundImage(File backgroundImage,\n\t\t\tint startPixelX, int startPixelY,\n\t\t\tint pixelWidth, int pixelHeight,\n\t\t\tJOGLTextureManager textureManager);\n\n\t/**\n\t * Set the boundary for the relevant data. All data outside of this boundary may be ignored by the target\n\t * to improve quality and performance.\n\t */\n\tpublic void setXZBoundary(AxisAlignedBoundingBoxXZ boundary);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLTargetFixedFunction.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_BACK;\nimport static javax.media.opengl.GL.GL_CCW;\nimport static javax.media.opengl.GL.GL_CULL_FACE;\nimport static javax.media.opengl.GL.GL_DEPTH_TEST;\nimport static javax.media.opengl.GL.GL_FRONT;\nimport static javax.media.opengl.GL.GL_FRONT_AND_BACK;\nimport static javax.media.opengl.GL.GL_GREATER;\nimport static javax.media.opengl.GL.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT;\nimport static javax.media.opengl.GL.GL_REPEAT;\nimport static javax.media.opengl.GL.GL_REPLACE;\nimport static javax.media.opengl.GL.GL_SRC_ALPHA;\nimport static javax.media.opengl.GL.GL_SRC_COLOR;\nimport static javax.media.opengl.GL.GL_TEXTURE;\nimport static javax.media.opengl.GL.GL_TEXTURE0;\nimport static javax.media.opengl.GL.GL_TEXTURE1;\nimport static javax.media.opengl.GL.GL_TEXTURE2;\nimport static javax.media.opengl.GL.GL_TEXTURE3;\nimport static javax.media.opengl.GL.GL_TEXTURE_2D;\nimport static javax.media.opengl.GL.GL_TEXTURE_MAX_ANISOTROPY_EXT;\nimport static javax.media.opengl.GL.GL_TEXTURE_WRAP_S;\nimport static javax.media.opengl.GL.GL_TEXTURE_WRAP_T;\nimport static javax.media.opengl.GL2.GL_CLAMP;\nimport static javax.media.opengl.GL2.GL_SOURCE0_RGB;\nimport static javax.media.opengl.GL2.GL_SOURCE1_RGB;\nimport static javax.media.opengl.GL2.GL_SOURCE2_RGB;\nimport static javax.media.opengl.GL2ES1.GL_ALPHA_TEST;\nimport static javax.media.opengl.GL2ES1.GL_COMBINE;\nimport static javax.media.opengl.GL2ES1.GL_COMBINE_RGB;\nimport static javax.media.opengl.GL2ES1.GL_INTERPOLATE;\nimport static javax.media.opengl.GL2ES1.GL_LIGHT_MODEL_AMBIENT;\nimport static javax.media.opengl.GL2ES1.GL_MODULATE;\nimport static javax.media.opengl.GL2ES1.GL_OPERAND0_RGB;\nimport static javax.media.opengl.GL2ES1.GL_OPERAND1_RGB;\nimport static javax.media.opengl.GL2ES1.GL_OPERAND2_RGB;\nimport static javax.media.opengl.GL2ES1.GL_PREVIOUS;\nimport static javax.media.opengl.GL2ES1.GL_TEXTURE_ENV;\nimport static javax.media.opengl.GL2ES1.GL_TEXTURE_ENV_MODE;\nimport static javax.media.opengl.GL2GL3.GL_CLAMP_TO_BORDER;\nimport static javax.media.opengl.GL2GL3.GL_FILL;\nimport static javax.media.opengl.GL2GL3.GL_LINE;\nimport static javax.media.opengl.GL2GL3.GL_QUADS;\nimport static javax.media.opengl.GL2GL3.GL_TEXTURE_BORDER_COLOR;\nimport static javax.media.opengl.fixedfunc.GLLightingFunc.GL_AMBIENT;\nimport static javax.media.opengl.fixedfunc.GLLightingFunc.GL_DIFFUSE;\nimport static javax.media.opengl.fixedfunc.GLLightingFunc.GL_LIGHT0;\nimport static javax.media.opengl.fixedfunc.GLLightingFunc.GL_LIGHTING;\nimport static javax.media.opengl.fixedfunc.GLLightingFunc.GL_POSITION;\nimport static javax.media.opengl.fixedfunc.GLLightingFunc.GL_SPECULAR;\nimport static javax.media.opengl.fixedfunc.GLMatrixFunc.GL_MODELVIEW;\nimport static javax.media.opengl.fixedfunc.GLMatrixFunc.GL_PROJECTION;\nimport static org.osm2world.core.target.common.material.Material.multiplyColor;\nimport static org.osm2world.core.target.common.material.Material.Transparency.BINARY;\nimport static org.osm2world.core.target.common.material.Material.Transparency.TRUE;\n\nimport java.awt.Color;\nimport java.io.File;\nimport java.util.List;\n\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GL2;\nimport javax.media.opengl.glu.GLU;\n\nimport org.apache.commons.configuration.BaseConfiguration;\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.TextureData.Wrap;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\n\nimport com.jogamp.opengl.util.texture.Texture;\n\n/**\n * JOGL target using the old fixed function OpenGL pipeline.\n */\npublic final class JOGLTargetFixedFunction extends AbstractJOGLTarget implements JOGLTarget {\n\t\n\t/** maximum number of texture layers any material can use */\n\tpublic static final int MAX_TEXTURE_LAYERS = 4;\n\t\n\t/** globally controls anisotropic filtering for all textures */\n\tprivate static final boolean ANISOTROPIC_FILTERING = true;\n\t\n\tprivate final GL2 gl;\n\t\n\tprivate Configuration config = new BaseConfiguration();\n\t\n\t/**\n\t * creates a new JOGLTarget for a given {@link GL2} interface. It is\n\t * possible to have multiple targets that render to the same gl object.\n\t * \n\t * @param renderingParameters  global parameters for rendering;\n\t *   see {@link #setRenderingParameters(JOGLRenderingParameters)}\n\t * @param globalLightingParameters  global parameters for lighting;\n\t *   see {@link #setGlobalLightingParameters(GlobalLightingParameters)}\n\t */\n\tpublic JOGLTargetFixedFunction(GL2 gl, JOGLRenderingParameters renderingParameters,\n\t\t\tGlobalLightingParameters globalLightingParameters) {\n\t\t\n\t\tsuper(gl, renderingParameters, globalLightingParameters);\n\t\tthis.gl = gl;\n\t\t\n\t\treset();\n\t\t\n\t}\n\n\tpublic static void drawPrimitive(GL2 gl, int glPrimitiveType,\n\t\t\tList<VectorXYZ> vertices, List<VectorXYZ> normals,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tassert vertices.size() == normals.size();\n\t\t\n\t\tgl.glBegin(glPrimitiveType);\n\t\t\n\t\tfor (int i = 0; i < vertices.size(); i++) {\n\t\t\t\n\t\t\tif (texCoordLists != null) {\n\t\t\t\tfor (int texLayer = 0; texLayer < texCoordLists.size(); texLayer++) {\n\t\t\t\t\tVectorXZ textureCoord =\ttexCoordLists.get(texLayer).get(i);\n\t\t\t\t\tgl.glMultiTexCoord2d(getGLTextureConstant(texLayer),\n\t\t\t\t\t\t\ttextureCoord.x, textureCoord.z);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tVectorXYZ n = normals.get(i);\n\t\t\tgl.glNormal3d(n.x, n.y,\t-n.z);\n\t\t\t\n\t\t\tVectorXYZ v = vertices.get(i);\n\t\t\tgl.glVertex3d(v.x, v.y, -v.z);\n\t\t\t\n\t\t}\n\t\t\n\t\tgl.glEnd();\n\t\t\n\t}\n\t\n\t/**\n\t * prepares a scene, based on the accumulated draw calls, for rendering.\n\t */\n\t@Override\n\tpublic void finish() {\n\t\t\n\t\tif (isFinished()) return;\n\t\t\n\t\tif (\"DisplayList\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\trenderer = new JOGLRendererDisplayList(\n\t\t\t\t\tgl, textureManager, primitiveBuffer);\n\t\t} else {\n\t\t\trenderer = new JOGLRendererVBOFixedFunction(\n\t\t\t\t\tgl, textureManager, primitiveBuffer);\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * similar to {@link #render(Camera, Projection)},\n\t * but allows rendering only a part of the \"normal\" image.\n\t * For example, with xStart=0, xEnd=0.5, yStart=0 and yEnd=1,\n\t * only the left half of the full image will be rendered,\n\t * but it will be stretched to cover the available space.\n\t * \n\t * Only supported for orthographic projections!\n\t */\n\tpublic void renderPart(Camera camera, Projection projection,\n\t\t\tdouble xStart, double xEnd, double yStart, double yEnd) {\n\t\t\n\t\tif (renderer == null) {\n\t\t\tthrow new IllegalStateException(\"finish must be called first\");\n\t\t}\n\t\t\n\t\t/* apply camera and projection information */\n\t\t\n\t\tapplyProjectionMatricesForPart(gl, projection,\n\t\t\t\txStart, xEnd, yStart, yEnd);\n\t\t\n\t\tapplyCameraMatrices(gl, camera);\n\t\t\n\t\t/* apply global rendering parameters */\n\t\t\n\t\tapplyRenderingParameters(gl, renderingParameters);\n\t\tapplyLightingParameters(gl, globalLightingParameters, projection.isOrthographic());\n\t\t\n\t\t/* render primitives */\n\t\t\n\t\trenderer.render(camera, projection);\n\t\t\n\t\tfor (NonAreaPrimitive nonAreaPrimitive : nonAreaPrimitives) {\n\t\t\t\n\t\t\tgl.glLineWidth(nonAreaPrimitive.width);\n\t\t\t\n\t\t\tColor c = nonAreaPrimitive.color;\n\t\t\tgl.glColor3f(c.getRed()/255f, c.getGreen()/255f, c.getBlue()/255f);\n\t\t\t\n\t\t\tgl.glBegin(AbstractJOGLTarget.getGLConstant(nonAreaPrimitive.type));\n\t        \n\t\t\tfor (VectorXYZ v : nonAreaPrimitive.vs) {\n\t\t        gl.glVertex3d(v.getX(), v.getY(), -v.getZ());\n\t\t\t}\n\t\t\t\n\t        gl.glEnd();\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tstatic final void applyCameraMatrices(GL2 gl, Camera camera) {\n\t\t\n    \tgl.glLoadIdentity();\n\t\t\n\t\tVectorXYZ pos = camera.getPos();\n\t\tVectorXYZ lookAt = camera.getLookAt();\n\t\tVectorXYZ up = camera.getUp();\n\t\tnew GLU().gluLookAt(\n\t\t\t\tpos.x, pos.y, -pos.z,\n\t\t\t\tlookAt.x, lookAt.y, -lookAt.z,\n\t\t\t\tup.x, up.y, -up.z);\n\t\t\n\t}\n\t\n\tstatic final void applyProjectionMatrices(GL2 gl, Projection projection) {\n\t\tapplyProjectionMatricesForPart(gl, projection, 0, 1, 0, 1);\n\t}\n\n\t/**\n\t * similar to {@link #applyProjectionMatrices(GL2, Projection)},\n\t * but allows rendering only a part of the \"normal\" image.\n\t */\n\tstatic final void applyProjectionMatricesForPart(GL2 gl, Projection projection,\n\t\t\tdouble xStart, double xEnd, double yStart, double yEnd) {\n\t\t\n\t\tif ((xStart != 0 || xEnd != 1 || yStart != 0 || yEnd != 1)\n\t\t\t\t&& !projection.isOrthographic()) {\n\t\t\tthrow new IllegalArgumentException(\"section rendering only supported \"\n\t\t\t\t\t+ \"for orthographic projections\");\n\t\t}\n\t\t\n\t\tgl.glMatrixMode(GL_PROJECTION);\n\t\tgl.glLoadIdentity();\n\t\t\n\t\tif (projection.isOrthographic()) {\n\n\t\t\tdouble volumeWidth = projection.getAspectRatio() * projection.getVolumeHeight();\n\t\t\t\n\t\t\tgl.glOrtho(\n\t\t\t\t\t(-0.5 + xStart) * volumeWidth,\n\t\t\t\t\t(-0.5 + xEnd  ) * volumeWidth,\n\t\t\t\t\t(-0.5 + yStart) * projection.getVolumeHeight(),\n\t\t\t\t\t(-0.5 + yEnd  ) * projection.getVolumeHeight(),\n\t\t\t\t\tprojection.getNearClippingDistance(),\n\t\t\t\t\tprojection.getFarClippingDistance());\n\t\t\t\n\t\t} else { //perspective\n\n\t\t\tnew GLU().gluPerspective(\n\t\t\t\t\tprojection.getVertAngle(),\n\t\t\t\t\tprojection.getAspectRatio(),\n\t\t\t\t\tprojection.getNearClippingDistance(),\n\t\t\t\t\tprojection.getFarClippingDistance());\n\t\t\t\n\t\t}\n\n\t\tgl.glMatrixMode(GL_MODELVIEW);\n\t\t\n\t}\n\t\n\tstatic final void applyRenderingParameters(GL2 gl,\n\t\t\tJOGLRenderingParameters parameters) {\n\t\t\n\t\t/* backface culling */\n\t\t\n\t\tif (parameters.frontFace == null) {\n\t\t\tgl.glDisable(GL_CULL_FACE);\n\t\t} else {\n\t\t\tgl.glFrontFace(GL_CCW);\n\t\t\tgl.glCullFace(GL_BACK);\n\t\t\tgl.glEnable (GL_CULL_FACE);\n\t\t}\n\t\t\n\t\t/* wireframe mode */\n\t\t\n\t\tif (parameters.wireframe) {\n    \t\tgl.glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);\n    \t} else {\n    \t\tgl.glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);\n    \t}\n\t\t\n\t\t/* z buffer */\n\t\t\n\t\tif (parameters.useZBuffer) {\n\t\t\tgl.glEnable(GL_DEPTH_TEST);\n\t\t} else {\n\t\t\tgl.glDisable(GL_DEPTH_TEST);\n\t\t}\n\t\t\t\t\n\t}\n\t\n\tstatic final void applyLightingParameters(GL2 gl,\n\t\t\tGlobalLightingParameters lighting, boolean orthographicProjection) {\n\t\t\n\t\tif (lighting == null) {\n\n\t\t\tgl.glDisable(GL_LIGHT0);\n\t\t\tgl.glDisable(GL_LIGHTING);\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\tgl.glLightModelfv(GL_LIGHT_MODEL_AMBIENT,\n\t\t\t\t\tgetFloatBuffer(lighting.globalAmbientColor));\n\t\t\t\n\t\t\tgl.glLightfv(GL_LIGHT0, GL_AMBIENT,\n\t\t\t\t\tgetFloatBuffer(Color.BLACK));\n\t\t\tgl.glLightfv(GL_LIGHT0, GL_DIFFUSE,\n\t\t\t\t\tgetFloatBuffer(lighting.lightColorDiffuse));\n\t\t\tgl.glLightfv(GL_LIGHT0, GL_SPECULAR,\n\t\t\t\t\tgetFloatBuffer(lighting.lightColorSpecular));\n\t\t\t\n\t\t\t// make specular lighting computation more realistic\n\t\t\tgl.glLightModeli(GL2.GL_LIGHT_MODEL_LOCAL_VIEWER, orthographicProjection ? GL.GL_FALSE : GL.GL_TRUE);\n\t\t\tgl.glLightModeli(GL2.GL_LIGHT_MODEL_COLOR_CONTROL, GL2.GL_SEPARATE_SPECULAR_COLOR);\n\t\t\t\n\t\t\tgl.glLightfv(GL_LIGHT0, GL_POSITION, new float[] {\n\t\t\t\t\t\t(float)lighting.lightFromDirection.x,\n\t\t\t\t\t\t(float)lighting.lightFromDirection.y,\n\t\t\t\t\t\t-(float)lighting.lightFromDirection.z,\n\t\t\t\t\t\t0.0f}, 0);\n\t\t\t\n\t\t\tgl.glEnable(GL_LIGHT0);\n\t\t\tgl.glEnable(GL_LIGHTING);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tstatic final void setMaterial(GL2 gl, Material material,\n\t\t\tJOGLTextureManager textureManager) {\n\t\t\n\t\tint numTexLayers = 0;\n\t\tif (material.getTextureDataList() != null) {\n\t\t\tnumTexLayers = material.getTextureDataList().size();\n\t\t}\n\t\t\n\t\t/* set lighting */\n\t\t// wrong as interpolation is meant for normals? light shading should always be smooth\n\t\t/*\n\t\tif (material.getInterpolation() == Interpolation.SMOOTH) {\n\t\t\tgl.glShadeModel(GL_SMOOTH);\n\t\t} else {\n\t\t\tgl.glShadeModel(GL_FLAT);\n\t\t}\n\t\t*/\n\t\t\n\t\t/* set color */\n\t\t\n\t\tif (numTexLayers == 0 || material.getTextureDataList().get(0).colorable) {\n\t\t\t\n\t\t\t//TODO: glMaterialfv could be redundant if color was used for ambient and diffuse\n\t\t\tColor c = material.getColor();\n\t\t\tgl.glColor3f(c.getRed()/255f, c.getGreen()/255f, c.getBlue());\n\t\t\t\n\t\t\tgl.glMaterialfv(GL_FRONT, GL_AMBIENT,\n\t\t\t\t\tgetFloatBuffer(material.ambientColor()));\n\t\t\tgl.glMaterialfv(GL_FRONT, GL_DIFFUSE,\n\t\t\t\t\tgetFloatBuffer(material.diffuseColor()));\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\tgl.glColor3f(1, 1, 1);\n\t\t\t\n\t\t\tgl.glMaterialfv(GL_FRONT, GL_AMBIENT, getFloatBuffer(\n\t\t\t\t\tmultiplyColor(Color.WHITE, material.getAmbientFactor())));\n\t\t\tgl.glMaterialfv(GL_FRONT, GL_DIFFUSE, getFloatBuffer(\n\t\t\t\t\tmultiplyColor(Color.WHITE, material.getDiffuseFactor())));\n\t\t\t\n\t\t}\n\t\t\n\t\t// specular lighting\n\t\tgl.glMaterialfv(GL.GL_FRONT, GL2.GL_SPECULAR, getFloatBuffer(\n\t\t\t\tmultiplyColor(Color.WHITE, material.getSpecularFactor())));\n\t\tgl.glMateriali(GL.GL_FRONT, GL2.GL_SHININESS, material.getShininess());\n\t\t\n\t\t/* set textures and associated parameters */\n\t\t\n\t\tfor (int i = 0; i < MAX_TEXTURE_LAYERS; i++) {\n\t\t\t\n\t\t\tgl.glActiveTexture(getGLTextureConstant(i));\n\t\t\t\t\t\t\n\t\t\tif (i >= numTexLayers) {\n\t\t\t\t\n\t\t\t\tgl.glDisable(GL_TEXTURE_2D);\n\t\t\t\t\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tgl.glEnable(GL_TEXTURE_2D);\n\t\t\t\t\n\t\t\t\tif (material.getTransparency() == TRUE) {\n\t\t\t\t\tgl.glEnable(GL.GL_BLEND);\n\t\t\t\t\t/* GL.GL_SRC_ALPHA and GL.GL_ONE_MINUS_SRC_ALPHA for color blending produces correct results for color, while\n\t\t\t\t\t * GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA produces correct alpha blended results: the blendfunction is in fact equal to 1-(1-SRC_APLHA)*(1-DST_APLHA)\n\t\t\t\t\t */\n\t\t\t\t\tgl.glBlendFuncSeparate(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA, GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA);\n\t\t\t\t} else if (material.getTransparency() == BINARY) {\n\t\t\t\t\tgl.glAlphaFunc(GL_GREATER, 0.5f);\n\t\t\t\t\tgl.glEnable(GL_ALPHA_TEST);\n\t\t\t\t} else {\n\t\t\t\t\tgl.glDisable(GL.GL_BLEND);\n\t\t\t\t\tgl.glDisable(GL_ALPHA_TEST);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tTextureData textureData = material.getTextureDataList().get(i);\n\t\t\t\t\n\t\t\t\tTexture texture = textureManager.getTextureForFile(textureData.file);\n\t\t        texture.enable(gl); //TODO: should this be called every time?\n\t\t        texture.bind(gl);\n\t\t        \n\t\t\t\t/* enable anisotropic filtering (note: this could be a\n\t\t\t\t * per-texture setting, but currently isn't) */\n\t\t\t\t\n\t\t        if (gl.isExtensionAvailable(\"GL_EXT_texture_filter_anisotropic\")) {\n\t\t\t\t\t\n\t\t        \tif (ANISOTROPIC_FILTERING) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tfloat max[] = new float[1];\n\t\t\t\t\t\tgl.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, max, 0);\n\t\t\t\t\t\t\n\t\t\t\t\t\tgl.glTexParameterf(GL_TEXTURE_2D,\n\t\t\t\t\t\t\t\tGL_TEXTURE_MAX_ANISOTROPY_EXT,\n\t\t\t\t\t\t\t\tmax[0]);\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\tgl.glTexParameterf(GL_TEXTURE_2D,\n\t\t\t\t\t\t\t\tGL_TEXTURE_MAX_ANISOTROPY_EXT,\n\t\t\t\t\t\t\t\t1.0f);\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t        }\n\t\t\t\t\n\t\t\t\t/* wrapping behavior */\n\t\t        \n\t\t\t\tint wrap = 0;\n\t\t\t\t\n\t\t\t\tswitch (textureData.wrap) {\n\t\t\t\tcase CLAMP: wrap = GL_CLAMP; break;\n\t\t\t\tcase REPEAT: wrap = GL_REPEAT; break;\n\t\t\t\tcase CLAMP_TO_BORDER: wrap = GL_CLAMP_TO_BORDER; break;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap);\n\t\t        gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap);\n\t\t        \n\t\t        \n\t\t        if (textureData.wrap == Wrap.CLAMP_TO_BORDER) {\n\t\t        \t\n\t\t        \t/* TODO: make the RGB configurable -  for some reason,\n\t\t        \t * it shows up in lowzoom even if fully transparent */\n\t\t        \tgl.glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR,\n\t\t        \t\t\tgetFloatBuffer(new Color(1f, 1f, 1f, 0f)));\n\t\t        \t\n\t\t        }\n\t\t\t\t\n\t\t        /* combination of texture layers */\n\t        \tif (i == 0) {\n\t\t        \t/* Cv = Cp × Cs (Cp = ?)\n\t\t        \t * Av = Ap × As (Ap = 1?)\n\t\t        \t */\n\t\t        \tgl.glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);\n\t\t        } else {\n\t\t        \t\n\t\t        \t/* Cv = Arg0×Arg2 + Arg1×(1-Arg2)\n\t\t        \t * Cv = Cs × As + Cp × (1-As)\n\t\t        \t * Arg0 = Cs\n\t\t        \t * Arg1 = Cp\n\t\t        \t * Arg2 = As\n\t\t        \t */\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE );\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE );\n\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE );\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR );\n\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PREVIOUS );\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR );\n\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE2_RGB, GL_TEXTURE );\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_SRC_ALPHA );\n\n\t\t        \t/* Av = Arg0×Arg2 + Arg1×(1-Arg2)\n\t\t        \t * Av = 1 × As + Ad × (1-As)\n\t\t        \t * Arg0 = 1\n\t\t        \t * Arg1 = Ad\n\t\t        \t * Arg2 = As\n\t\t        \t */\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL2.GL_COMBINE_ALPHA, GL_INTERPOLATE ); \n\t\t        \t\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL2.GL_SOURCE0_ALPHA, GL2.GL_CONSTANT );\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL2.GL_OPERAND0_ALPHA, GL_SRC_ALPHA );\n\t\t        \t\n\t\t        \tfloat[] mycolor = {0.0f, 0.0f, 0.0f, 1.0f}; //RGB doesn't matter since its not used\n\t\t        \tgl.glTexEnvfv(GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_COLOR, mycolor, 0);\n\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL2.GL_SOURCE1_ALPHA, GL_PREVIOUS );\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL2.GL_OPERAND1_ALPHA, GL_SRC_ALPHA );\n\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL2.GL_SOURCE2_ALPHA, GL_TEXTURE );\n\t\t        \tgl.glTexEnvi( GL_TEXTURE_ENV, GL2.GL_OPERAND2_ALPHA, GL_SRC_ALPHA );\n\t\t        }\n\t\t        \n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\n\n\tstatic final int getGLTextureConstant(int textureNumber) {\n\t\tswitch (textureNumber) {\n\t\tcase 0: return GL_TEXTURE0;\n\t\tcase 1: return GL_TEXTURE1;\n\t\tcase 2: return GL_TEXTURE2;\n\t\tcase 3: return GL_TEXTURE3;\n\t\tdefault: throw new Error(\"programming error: unhandled texture number\");\n\t\t}\n\t}\n\t\n\tpublic final void drawBackgoundImage(File backgroundImage,\n\t\t\tint startPixelX, int startPixelY,\n\t\t\tint pixelWidth, int pixelHeight,\n\t\t\tJOGLTextureManager textureManager) {\n\t\t\n\t\tgl.glMatrixMode(GL_PROJECTION);\n\t\tgl.glPushMatrix();\n\t\tgl.glLoadIdentity();\n\t\tgl.glOrtho(0, 1, 0, 1, 0, 1);\n\t\t\n\t\tgl.glMatrixMode(GL_MODELVIEW);\n\t\tgl.glPushMatrix();\n\t\tgl.glLoadIdentity();\n\t\t\t\t\n\t\tgl.glDepthMask( false );\n\t\t\n\t\t/* texture binding */\n\n\t\tgl.glEnable(GL_TEXTURE_2D);\n\t\tgl.glActiveTexture(GL_TEXTURE0);\n\t\t\n\t\tgl.glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);\n\t\t\n\t\tTexture backgroundTexture =\n\t\t\t\ttextureManager.getTextureForFile(backgroundImage);\n\n\t\tbackgroundTexture.enable(gl);\n\t\tbackgroundTexture.bind(gl);\n\t\t\n\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);\n\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);\n\t\t\n\t\tint texWidth = backgroundTexture.getImageWidth();\n\t\tint texHeight = backgroundTexture.getImageHeight();\n\t\t\n\t\t/* draw quad */\n\t\t\t\t\n\t\tgl.glBegin( GL_QUADS ); {\n\t\t\tgl.glTexCoord2f(\n\t\t\t\t\t(float) startPixelX / texWidth,\n\t\t\t\t\t(float) startPixelY / texHeight );\n\t\t\tgl.glVertex2f( 0, 0 );\n\t\t\tgl.glTexCoord2f(\n\t\t\t\t\t(float) (startPixelX + pixelWidth) / texWidth,\n\t\t\t\t\t(float) startPixelY / texHeight );\n\t\t\tgl.glVertex2f( 1f, 0 );\n\t\t\tgl.glTexCoord2f(\n\t\t\t\t\t(float) (startPixelX + pixelWidth) / texWidth,\n\t\t\t\t\t(float) (startPixelY + pixelHeight) / texHeight );\n\t\t\tgl.glVertex2f( 1f, 1f );\n\t\t\tgl.glTexCoord2f(\n\t\t\t\t\t(float) startPixelX / texWidth,\n\t\t\t\t\t(float) (startPixelY + pixelHeight) / texHeight );\n\t\t\tgl.glVertex2f( 0, 1f );\n\t\t} gl.glEnd();\n\t\t\n\t\t/* restore some settings */\n\t\t\n\t\tgl.glDepthMask( true );\n\t\t\n\t\tgl.glPopMatrix();\n\t\tgl.glMatrixMode(GL_PROJECTION);\n\t\tgl.glPopMatrix();\n\t\tgl.glMatrixMode(GL_MODELVIEW);\n\t\t\n\t}\n\n\t@Override\n\tpublic void setXZBoundary(AxisAlignedBoundingBoxXZ boundary) {}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLTargetShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static java.util.Arrays.asList;\nimport static javax.media.opengl.GL.GL_ARRAY_BUFFER;\nimport static javax.media.opengl.GL.GL_BACK;\nimport static javax.media.opengl.GL.GL_CCW;\nimport static javax.media.opengl.GL.GL_CULL_FACE;\nimport static javax.media.opengl.GL.GL_DEPTH_TEST;\nimport static javax.media.opengl.GL.GL_FRONT_AND_BACK;\nimport static javax.media.opengl.GL.GL_REPEAT;\nimport static javax.media.opengl.GL.GL_STATIC_DRAW;\nimport static javax.media.opengl.GL.GL_TEXTURE0;\nimport static javax.media.opengl.GL.GL_TEXTURE_2D;\nimport static javax.media.opengl.GL.GL_TEXTURE_WRAP_S;\nimport static javax.media.opengl.GL.GL_TEXTURE_WRAP_T;\nimport static javax.media.opengl.GL2GL3.GL_FILL;\nimport static javax.media.opengl.GL2GL3.GL_LINE;\nimport static javax.media.opengl.fixedfunc.GLMatrixFunc.GL_MODELVIEW;\nimport static javax.media.opengl.fixedfunc.GLMatrixFunc.GL_PROJECTION;\n\nimport java.awt.Color;\nimport java.io.File;\nimport java.nio.FloatBuffer;\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GL2;\nimport javax.media.opengl.GL2GL3;\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXYZ;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXYZW;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\n\nimport com.jogamp.common.nio.Buffers;\nimport com.jogamp.opengl.math.FloatUtil;\nimport com.jogamp.opengl.util.PMVMatrix;\nimport com.jogamp.opengl.util.texture.Texture;\n\npublic class JOGLTargetShader extends AbstractJOGLTarget implements JOGLTarget {\n\tprivate DefaultShader defaultShader;\n\tprivate ShadowMapShader shadowMapShader;\n\tprivate ShadowVolumeShader shadowVolumeShader;\n\t//private DepthBufferShader depthBufferShader;\n\tprivate SSAOShader ssaoShader;\n\tprivate NonAreaShader nonAreaShader;\n\tprivate BackgroundShader backgroundShader;\n\tprivate GL3 gl;\n\t\n\t/**\n\t * PMVMatrix for rendering the world from camera perspective.\n\t */\n\tprivate PMVMatrix pmvMatrix;\n\tprivate JOGLRendererVBONonAreaShader nonAreaRenderer;\n\tprivate JOGLRendererVBOShader rendererShader;\n\tprivate JOGLRendererVBOShadowVolume rendererShadowVolume;\n\tprivate AxisAlignedBoundingBoxXZ xzBoundary;\n\tprivate boolean showShadowPerspective;\n\t\n\tpublic JOGLTargetShader(GL3 gl, JOGLRenderingParameters renderingParameters,\n\t\t\tGlobalLightingParameters globalLightingParameters) {\n\t\tsuper(gl, renderingParameters, globalLightingParameters);\n\t\tdefaultShader = new DefaultShader(gl);\n\t\tshadowMapShader = new ShadowMapShader(gl);\n\t\t//depthBufferShader = new DepthBufferShader(gl);\n\t\tssaoShader = new SSAOShader(gl);\n\t\tshadowVolumeShader = new ShadowVolumeShader(gl);\n\t\tnonAreaShader = new NonAreaShader(gl);\n\t\tbackgroundShader = new BackgroundShader(gl);\n\t\tthis.gl = gl;\n\t\tpmvMatrix = new PMVMatrix();\n\t\treset();\n\t}\n\n\t@Override\n\tpublic void drawBackgoundImage(File backgroundImage,\n\t\t\tint startPixelX, int startPixelY, int pixelWidth, int pixelHeight,\n\t\t\tJOGLTextureManager textureManager) {\n\t\t\n\t\tbackgroundShader.useShader();\n\t\t\n\t\tPMVMatrix backgroundPMVMatrix = new PMVMatrix();\n\t\tbackgroundPMVMatrix.glMatrixMode(GL_PROJECTION);\n\t\tbackgroundPMVMatrix.glLoadIdentity();\n\t\tbackgroundPMVMatrix.glOrthof(0, 1, 0, 1, 0, 1);\n\t\t\n\t\tbackgroundPMVMatrix.glMatrixMode(GL_MODELVIEW);\n\t\tbackgroundPMVMatrix.glLoadIdentity();\n\t\t\n\t\tbackgroundShader.setPMVMatrix(backgroundPMVMatrix);\n\t\t\n\t\tgl.glDepthMask( false );\n\n\t\t/* texture binding */\n\n\t\tgl.glActiveTexture(GL_TEXTURE0);\n\t\t\n\t\tTexture backgroundTexture =\n\t\t\t\ttextureManager.getTextureForFile(backgroundImage);\n\t\t\n\t\tbackgroundTexture.bind(gl);\n\t\t\n\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);\n\t\tgl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);\n\n        gl.glUniform1i(backgroundShader.getTextureID(), 0);\n\t\t\n\t\tint texWidth = backgroundTexture.getImageWidth();\n\t\tint texHeight = backgroundTexture.getImageHeight();\n\t\t\n\t\t/* draw quad */\n\t\t\n\t\t/* create the buffer */\n\t\t\n\t\tint[] id = new int[1];\n\t\tgl.glGenBuffers(1, id, 0);\n\t\t\n\t\t/* collect the data for the buffer */\n\t\tint verticeCount = 4;\n\t\tint valueCount = 2;\n\t\t\n\t\tFloatBuffer valueBuffer = Buffers.newDirectFloatBuffer(verticeCount*(2*valueCount));\n\t\tvalueBuffer.put(0);\n\t\tvalueBuffer.put(0);\n\t\t\n\t\tvalueBuffer.put(1f);\n\t\tvalueBuffer.put(0);\n\t\t\n\t\tvalueBuffer.put(0);\n\t\tvalueBuffer.put(1f);\n\t\t\n\t\tvalueBuffer.put(1f);\n\t\tvalueBuffer.put(1f);\n\t\t\n\t\tvalueBuffer.put((float) startPixelX / texWidth);\n\t\tvalueBuffer.put((float) startPixelY / texHeight);\n\t\t\n\t\tvalueBuffer.put((float) (startPixelX + pixelWidth) / texWidth);\n\t\tvalueBuffer.put((float) startPixelY / texHeight);\n\t\t\n\t\tvalueBuffer.put((float) startPixelX / texWidth);\n\t\tvalueBuffer.put((float) (startPixelY + pixelHeight) / texHeight);\n\t\t\n\t\tvalueBuffer.put((float) (startPixelX + pixelWidth) / texWidth);\n\t\tvalueBuffer.put((float) (startPixelY + pixelHeight) / texHeight);\n\t\t\n\t\tvalueBuffer.rewind();\n\t\t\n\t\t/* write the data into the buffer */\n\t\t\n\t\tgl.glBindBuffer(GL_ARRAY_BUFFER, id[0]);\n\t\t\n\t\tgl.glBufferData(\n\t\t\t\tGL_ARRAY_BUFFER,\n\t\t\t\tvalueBuffer.capacity() * Buffers.SIZEOF_FLOAT,\n\t\t\t\tvalueBuffer,\n\t\t\t\tGL_STATIC_DRAW);\n\t\t\n\t\tgl.glEnableVertexAttribArray(backgroundShader.getVertexPositionID());\n\t\tgl.glEnableVertexAttribArray(backgroundShader.getVertexTexCoordID());\n\t\t\n\t\tint stride = 0;\n\t\tgl.glVertexAttribPointer(backgroundShader.getVertexPositionID(), valueCount, GL.GL_FLOAT, false, stride, 0);\n\t\tgl.glVertexAttribPointer(backgroundShader.getVertexTexCoordID(), valueCount, GL.GL_FLOAT, false, stride, Buffers.SIZEOF_FLOAT * valueCount * verticeCount);\n\t\t\n\t\tgl.glDrawArrays(GL.GL_TRIANGLE_STRIP, 0, 4);\n\t\t\n\t\tgl.glDisableVertexAttribArray(backgroundShader.getVertexPositionID());\n\t\tgl.glDisableVertexAttribArray(backgroundShader.getVertexTexCoordID());\n\t\t\n\t\t/* restore some settings */\n\t\t\n\t\tgl.glDepthMask( true );\n\t\t\n\t\tbackgroundShader.disableShader();\n\t}\n\t\n\t/**\n\t * Calculate tighter near and far planes for the boundingBox around the visible world objects.\n\t * @param camera the current camera for which the planes are calculated\n\t * @param projection the current projection\n\t * @param boundingBox the bounding box around all visible world objects\n\t * @return a new projection with near and far planes as tight as possible\n\t */\n\tpublic static Projection updateClippingPlanesForCamera(Camera camera, Projection projection, AxisAlignedBoundingBoxXYZ boundingBox) {\n\t\t\n\t\tdouble nearPlane = Double.POSITIVE_INFINITY, farPlane = 0;\n\t\t\n\t\tPMVMatrix camMat = new PMVMatrix();\n\t\tVectorXYZ pos = camera.getPos();\n\t\tVectorXYZ lookAt = camera.getLookAt();\n\t\tVectorXYZ up = camera.getUp();\n\t\tcamMat.gluLookAt(\n\t\t\t\t(float)pos.x, (float)pos.y, (float)-pos.z,\n\t\t\t\t(float)lookAt.x, (float)lookAt.y, (float)-lookAt.z,\n\t\t\t\t(float)up.x, (float)up.y, (float)-up.z);\n\t\t\n\t\t\n\t\tfor (VectorXYZ corner : boundingBox.corners()) {\n\t\t\tfloat[] result = new float[4];\n\t\t\tFloatUtil.multMatrixVecf(camMat.glGetMvMatrixf(),\n\t\t\t\t\tnew float[]{(float)corner.x, (float)corner.y, (float)corner.z, 1}, result);\n\t\t\tVectorXYZ cornerCam = new VectorXYZ(result[0]/result[3], result[1]/result[3], result[2]/result[3]);\n\t\t\tdouble depth = -cornerCam.z;\n\t\t\tnearPlane = Math.min(depth, nearPlane);\n\t\t\tfarPlane = Math.max(depth, farPlane);\n\t\t}\n\t\t\n\t\tif (nearPlane == Double.POSITIVE_INFINITY)\n\t\t\tnearPlane = projection.getNearClippingDistance();\n\t\telse\n\t\t\tnearPlane = Math.max(projection.getNearClippingDistance(), nearPlane);\n\t\tif (farPlane == 0)\n\t\t\tfarPlane = projection.getFarClippingDistance();\n\t\telse\n\t\t\tfarPlane = Math.min(projection.getFarClippingDistance(), farPlane);\n\t\t\n\t\treturn new Projection(projection.isOrthographic(), projection.getAspectRatio(),\n\t\t\t\tprojection.getVertAngle(), projection.getVolumeHeight(), nearPlane, farPlane);\n\t}\n\n\t@Override\n\tpublic void renderPart(Camera camera, Projection projection, double xStart,\n\t\t\tdouble xEnd, double yStart, double yEnd) {\n\t\tif (renderer == null) {\n\t\t\tthrow new IllegalStateException(\"finish must be called first\");\n\t\t}\n\t\t\n\t\tif (renderingParameters.overwriteProjectionClippingPlanes) {\n\t\t\tprojection = updateClippingPlanesForCamera(camera, projection, rendererShader.getBoundingBox());\n\t\t}\n\t\t\n\t\tapplyProjectionMatricesForPart(pmvMatrix, projection,\n\t\t\t\txStart, xEnd, yStart, yEnd);\n\t\t\n\t\tapplyCameraMatrices(pmvMatrix, camera);\n\t\t\n\t\tif (renderingParameters.useSSAO) {\n\t\t\tdefaultShader.setSSAOkernelSize(renderingParameters.SSAOkernelSize);\n\t\t\tdefaultShader.setSSAOradius(renderingParameters.SSAOradius);\n\t\t\t\n\t\t\t// based on http://john-chapman-graphics.blogspot.de/2013/01/ssao-tutorial.html\n\t\t\t// render depth buffer only\n\t\t\tssaoShader.useShader();\n\t\t\tssaoShader.setPMVMatrix(pmvMatrix);\n\t\t\tapplyRenderingParameters(gl, renderingParameters);\n\t\t\trendererShader.setShader(ssaoShader);\n\t\t\trendererShader.render();\n\t\t\tssaoShader.disableShader();\n\t\t}\n\t\t\n\t\tif (renderingParameters.useShadowMaps) {\n\t\t\t\n\t\t\t// TODO: render only part?\n\t\t\tshadowMapShader.setCameraFrustumPadding(renderingParameters.shadowMapCameraFrustumPadding);\n\t\t\tshadowMapShader.setShadowMapSize(renderingParameters.shadowMapWidth, renderingParameters.shadowMapHeight);\n\t\t\tshadowMapShader.useShader();\n\t\t\tshadowMapShader.preparePMVMatrix(globalLightingParameters, pmvMatrix, rendererShader.getBoundingBox());\n\t\t\t// render opaque shadow casters only when not using shadow volumes simultaneously, as those will be rendered there\n\t\t\tshadowMapShader.setRenderOpaque(!renderingParameters.useShadowVolumes);\n\t\t\t//shadowMapShader.setPMVMatrix(pmvMatrix);\n\t\t\t\n\t\t\t/* render primitives to shadow map*/\n\t\t\trendererShader.setShader(shadowMapShader);\n\t\t\trendererShader.render();\n\t\t\t\n\t\t\t//ShaderManager.saveDepthBuffer(new File(\"/home/sebastian/shadowmap\"+xStart+\"_\"+yStart+\".png\"), shadowMapShader.getShadowMapHandle(), shadowMapShader.shadowMapWidth, shadowMapShader.shadowMapHeight, gl);\n\t\t\t//shadowMapShader.saveShadowMap(new File(\"/home/sebastian/shadowmap.bmp\"));\n\t\t\t//shadowMapShader.saveColorBuffer(new File(\"/home/sebastian/shadowmap_color\"+xStart+\"_\"+yStart+\".png\"));\n\t\t\t\n\t\t\tshadowMapShader.disableShader();\n\t\t}\n\t\t\n\t\t/* apply camera and projection information */\n\t\tdefaultShader.useShader();\n\t\tdefaultShader.loadDefaults();\n\t\t\n\t\tif (showShadowPerspective)\n\t\t\tdefaultShader.setPMVMatrix(shadowMapShader.getPMVMatrix());\n\t\telse\n\t\t\tdefaultShader.setPMVMatrix(pmvMatrix);\n\t\t\n\t\t/* apply global rendering parameters */\n\t\t\n\t\tapplyRenderingParameters(gl, renderingParameters);\n\t\tapplyLightingParameters(defaultShader, globalLightingParameters);\n\t\t\n\t\tif (renderingParameters.useShadowMaps) {\n\t\t\tdefaultShader.bindShadowMap(shadowMapShader.getShadowMapHandle());\n\t\t\tdefaultShader.setShadowMatrix(shadowMapShader.getPMVMatrix());\n\t\t}\n\t\tif (!showShadowPerspective && renderingParameters.useSSAO) {\n\t\t\tdefaultShader.enableSSAOwithDepthMap(ssaoShader.getDepthBuferHandle());\n\t\t}\n\t\t\n\t\t// if using shadow volumes render semi-transparent objects later\n\t\tdefaultShader.setRenderSemiTransparent(!renderingParameters.useShadowVolumes);\n\t\t\n\t\t/* render primitives */\n\n\t\trendererShader.setShader(defaultShader);\n\t\trendererShader.render(camera, projection);\n\t\t\n\t\tdefaultShader.disableShader();\n\t\t\n\t\t/* non area primitives */\n\t\tnonAreaShader.useShader();\n\t\tnonAreaShader.loadDefaults();\n\t\tif (showShadowPerspective)\n\t\t\tnonAreaShader.setPMVMatrix(shadowMapShader.getPMVMatrix());\n\t\telse\n\t\t\tnonAreaShader.setPMVMatrix(pmvMatrix);\n\t\t\n\t\tnonAreaRenderer.render();\n\t\t\n\t\tnonAreaShader.disableShader();\n\t\t\n\t\t/* render shadows with shadow volumes on top of world with lighting */\n\t\tif (renderingParameters.useShadowVolumes) {\n\t\t\t\n\t\t\t/* Render shadow volumes with depth-fail algorithm. Uses the previously filled depth buffer */\n\t\t\tint[] drawbuffer = new int[1];\n\t\t\tgl.glGetIntegerv(GL2GL3.GL_DRAW_BUFFER, drawbuffer, 0);\n\t\t\tgl.glDrawBuffer(GL.GL_NONE);\n\t\t\tgl.glEnable(GL.GL_STENCIL_TEST);\n\t\t\tgl.glDepthMask(false);\n\t\t    gl.glEnable(GL3.GL_DEPTH_CLAMP); // used to clamp the infinity big shadow volumes\n\t\t    gl.glDisable(GL_CULL_FACE);\n\n\t\t    // We need the stencil test to be enabled but we want it\n\t\t    // to succeed always. Only the depth test matters.\n\t\t    gl.glStencilFunc(GL.GL_ALWAYS, 0, 0xff);\n\n\t\t    // Set the stencil test per the depth fail algorithm\n\t\t    gl.glStencilOpSeparate(GL.GL_BACK, GL.GL_KEEP, GL.GL_INCR_WRAP, GL.GL_KEEP);\n\t\t    gl.glStencilOpSeparate(GL.GL_FRONT, GL.GL_KEEP, GL.GL_DECR_WRAP, GL.GL_KEEP);\n\t\t    \n\t\t    // relax depth test to prevent z-fighting with self shadowing\n\t\t    //gl.glDepthFunc(GL.GL_LEQUAL);\n\t\t    \n\t\t\t// if using shadow volumes render semi-transparent objects later\n\t\t    shadowVolumeShader.setRenderSemiTransparent(!renderingParameters.useShadowVolumes);\n\t\t    \n\t\t    shadowVolumeShader.useShader();\n\t\t    shadowVolumeShader.setPMVMatrix(pmvMatrix);\n\t\t    rendererShadowVolume.setShader(shadowVolumeShader);\n\t\t    rendererShadowVolume.render();\n\t\t    shadowVolumeShader.disableShader();\n\n\t\t    // Restore local stuff\n\t\t    gl.glDepthMask(true);\n\t\t    gl.glDisable(GL3.GL_DEPTH_CLAMP);\n\t\t    gl.glEnable(GL_CULL_FACE);\n\t\t    \n\t\t    /* Render scene in shadow */\n\t\t    gl.glDrawBuffer(drawbuffer[0]);\n\t\t    // Draw only if the corresponding stencil value is NOT zero\n\t\t    gl.glStencilFunc(GL.GL_NOTEQUAL, 0x0, 0xFF);\n\t\t    // prevent update to the stencil buffer\n\t\t    gl.glStencilOpSeparate(GL.GL_BACK, GL.GL_KEEP, GL.GL_KEEP, GL.GL_KEEP);\n\t\t    gl.glStencilOpSeparate(GL.GL_FRONT, GL.GL_KEEP, GL.GL_KEEP, GL.GL_KEEP);\n\t\t    // Draw on top of the already rendered world with lighting\n\t\t    gl.glDepthFunc(GL.GL_LEQUAL);\n\t\t    \n\t\t    applyRenderingParameters(gl, renderingParameters);\n\t\t    defaultShader.useShader();\n\t\t\tdefaultShader.loadDefaults();\n\t\t\tdefaultShader.setPMVMatrix(pmvMatrix);\n\t\t\tdefaultShader.setShadowed(true);\n\t\t\t\n\t\t\tif (!showShadowPerspective && renderingParameters.useSSAO) {\n\t\t\t\tdefaultShader.enableSSAOwithDepthMap(ssaoShader.getDepthBuferHandle());\n\t\t\t}\n\n\t\t\t// if using shadow volumes render semi-transparent objects later\n\t\t\tdefaultShader.setRenderSemiTransparent(!renderingParameters.useShadowVolumes);\n\t\t\t\n//\t\t\t/* render primitives */\n\t\t\trendererShader.setShader(defaultShader);\n\t\t\trendererShader.render(camera, projection);\n\t\t\tdefaultShader.setShadowed(false);\n\t\t\tdefaultShader.disableShader();\n\t\t\t\n\t\t\t/* non area primitives */\n\t\t\tnonAreaShader.useShader();\n\t\t\tnonAreaShader.loadDefaults();\n\t\t\t\n\t\t\tnonAreaShader.setPMVMatrix(pmvMatrix);\n\t\t\t\n\t\t\tnonAreaRenderer.render();\n\t\t\t\n\t\t\tnonAreaShader.disableShader();\n\n\t\t\t// Render shadow volumes for debug\n\t\t\t/*gl.glDisable(GL.GL_STENCIL_TEST);\n\t\t\t//gl.glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);\n\t\t    shadowVolumeShader.useShader();\n\t\t    shadowVolumeShader.setPMVMatrix(pmvMatrix);\n\t\t    rendererShadowVolume.setShader(shadowVolumeShader);\n\t\t    rendererShadowVolume.render(camera, projection);\n\t\t    shadowVolumeShader.disableShader();*/\n\t\t\t\n\t\t\t// reset\n\t\t    gl.glDisable(GL.GL_STENCIL_TEST);\n\t\t    gl.glDepthFunc(GL.GL_LESS);\n\t\t    \n\t\t\t/* render semi-transparent objects now */\n\t\t    // NOTE: results could be improved slightly, if the depth-fail algorithm is executed here as well\n\t\t    //       the result would be that the topmost semi-transparent pixel would receive shadow volume shadows\n\t\t    //       (needs to be investigated, if the difference is noticeable in practice)\n\t\t    defaultShader.useShader();\n\t\t\tdefaultShader.loadDefaults();\n\t\t\tdefaultShader.setPMVMatrix(pmvMatrix);\n\t\t\t\n\t\t\t/* apply global rendering parameters */\n\t\t\t\n\t\t\tapplyRenderingParameters(gl, renderingParameters);\n\t\t\tapplyLightingParameters(defaultShader, globalLightingParameters);\n\t\t\t\n\t\t\tif (renderingParameters.useShadowMaps) {\n\t\t\t\tdefaultShader.bindShadowMap(shadowMapShader.getShadowMapHandle());\n\t\t\t\tdefaultShader.setShadowMatrix(shadowMapShader.getPMVMatrix());\n\t\t\t}\n\t\t\tif (!showShadowPerspective && renderingParameters.useSSAO) {\n\t\t\t\tdefaultShader.enableSSAOwithDepthMap(ssaoShader.getDepthBuferHandle());\n\t\t\t}\n\t\t\tdefaultShader.setRenderOnlySemiTransparent(true);\n\t\t\t\n\t\t\t/* render primitives */\n\n\t\t\trendererShader.setShader(defaultShader);\n\t\t\trendererShader.render(camera, projection);\n\t\t\t\n\t\t\tdefaultShader.disableShader();\n\t\t}\n\t}\n\t\n\tstatic final void applyRenderingParameters(GL3 gl,\n\t\t\tJOGLRenderingParameters parameters) {\n\t\t\n\t\t/* backface culling */\n\t\t\n\t\tif (parameters.frontFace == null) {\n\t\t\tgl.glDisable(GL_CULL_FACE);\n\t\t} else {\n\t\t\tgl.glFrontFace(GL_CCW);\n\t\t\tgl.glCullFace(GL_BACK);\n\t\t\tgl.glEnable (GL_CULL_FACE);\n\t\t}\n\t\t\n\t\t/* wireframe mode */\n\t\t\n\t\tif (parameters.wireframe) {\n    \t\tgl.glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);\n    \t} else {\n    \t\tgl.glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);\n    \t}\n\t\t\n\t\t/* z buffer */\n\t\t\n\t\tif (parameters.useZBuffer) {\n\t\t\tgl.glEnable(GL_DEPTH_TEST);\n\t\t} else {\n\t\t\tgl.glDisable(GL_DEPTH_TEST);\n\t\t}\n\t\t\t\t\n\t}\n\t\n\tstatic final void applyCameraMatrices(PMVMatrix pmvMatrix, Camera camera) {\n\t\t\n\t\tpmvMatrix.glMatrixMode(GL_MODELVIEW);\n\t\tpmvMatrix.glLoadIdentity();\n\t\t\n\t\tVectorXYZ pos = camera.getPos();\n\t\tVectorXYZ lookAt = camera.getLookAt();\n\t\tVectorXYZ up = camera.getUp();\n\t\tpmvMatrix.gluLookAt(\n\t\t\t\t(float)pos.x, (float)pos.y, (float)-pos.z,\n\t\t\t\t(float)lookAt.x, (float)lookAt.y, (float)-lookAt.z,\n\t\t\t\t(float)up.x, (float)up.y, (float)-up.z);\n\t\t\n\t}\n\t\n\tstatic final void applyProjectionMatrices(PMVMatrix pmvMatrix, Projection projection) {\n\t\tapplyProjectionMatricesForPart(pmvMatrix, projection, 0, 1, 0, 1);\n\t}\n\n\t/**\n\t * similar to {@link #applyProjectionMatrices(GL2, Projection)},\n\t * but allows rendering only a part of the \"normal\" image.\n\t */\n\tstatic final void applyProjectionMatricesForPart(PMVMatrix pmvMatrix, Projection projection,\n\t\t\tdouble xStart, double xEnd, double yStart, double yEnd) {\n\t\t\n\t\tif ((xStart != 0 || xEnd != 1 || yStart != 0 || yEnd != 1)\n\t\t\t\t&& !projection.isOrthographic()) {\n\t\t\tthrow new IllegalArgumentException(\"section rendering only supported \"\n\t\t\t\t\t+ \"for orthographic projections\");\n\t\t}\n\t\t\n\t\tpmvMatrix.glMatrixMode(GL_PROJECTION);\n\t\tpmvMatrix.glLoadIdentity();\n\t\t\n\t\tif (projection.isOrthographic()) {\n\n\t\t\tdouble volumeWidth = projection.getAspectRatio() * projection.getVolumeHeight();\n\t\t\t\n\t\t\tpmvMatrix.glOrthof(\n\t\t\t\t\t(float)((-0.5 + xStart) * volumeWidth),\n\t\t\t\t\t(float)((-0.5 + xEnd  ) * volumeWidth),\n\t\t\t\t\t(float)((-0.5 + yStart) * projection.getVolumeHeight()),\n\t\t\t\t\t(float)((-0.5 + yEnd  ) * projection.getVolumeHeight()),\n\t\t\t\t\t(float)(projection.getNearClippingDistance()),\n\t\t\t\t\t(float)(projection.getFarClippingDistance()));\n\t\t\t\n\t\t} else { //perspective\n\n\t\t\tpmvMatrix.gluPerspective(\n\t\t\t\t\t(float)(projection.getVertAngle()),\n\t\t\t\t\t(float)(projection.getAspectRatio()),\n\t\t\t\t\t(float)(projection.getNearClippingDistance()),\n\t\t\t\t\t(float)(projection.getFarClippingDistance()));\n\t\t\t\n\t\t}\n\n\t\tpmvMatrix.glMatrixMode(GL_MODELVIEW);\n\t\t\n\t}\n\t\n\tstatic final void applyLightingParameters(DefaultShader shader,\n\t\t\tGlobalLightingParameters lighting) {\n\t\t\n\t\tshader.setGlobalLighting(lighting);\n\t\t\n\t}\n\t\n//\tpublic void drawPrimitive(GL3 gl, int glPrimitiveType,\n//\t\t\tList<VectorXYZ> vertices, List<VectorXYZ> normals,\n//\t\t\tList<List<VectorXZ>> texCoordLists) {\n//\t\tassert vertices.size() == normals.size();\n//\t\t\n//\t\tgl.glBegin(glPrimitiveType);\n//\t\t\n//\t\tfor (int i = 0; i < vertices.size(); i++) {\n//\t\t\t\n//\t\t\tif (texCoordLists != null) {\n//\t\t\t\tfor (int texLayer = 0; texLayer < texCoordLists.size(); texLayer++) {\n//\t\t\t\t\tVectorXZ textureCoord =\ttexCoordLists.get(texLayer).get(i);\n//\t\t\t\t\tif (i==0) {\n//\t\t\t\t\t\tgl.glVertexAttrib2d(shader.getVertexTexCoordID(), textureCoord.x, textureCoord.z);\n//\t\t\t\t\t}\n//\t\t\t\t}\n//\t\t\t}\n//\n//\t\t\tVectorXYZ n = normals.get(i);\n//\t\t\tgl.glVertexAttrib3d(shader.getVertexNormalID(), n.x, n.y, -n.z);\n//\t\t\t\n//\t\t\tVectorXYZ v = vertices.get(i);\n//\t\t\tgl.glVertexAttrib3d(shader.getVertexPositionID(), v.x, v.y, -v.z);\n//\t\t\t\n//\t\t}\n//\t\t\n//\t\tgl.glEnd();\n//\t}\n\t\n\t/**\n\t * Draw the corners of a bounding box as colored lines.\n\t * @param color the color of the lines\n\t * @param bb the bounding box to draw\n\t */\n\tprotected final void drawBoundingBox(Color color, AxisAlignedBoundingBoxXYZ bb) {\n\t\t// bottom\n\t\tdrawBox(color, new VectorXYZ(bb.minX, bb.minY, bb.minZ), new VectorXYZ(\n\t\t\t\tbb.minX, bb.minY, bb.maxZ), new VectorXYZ(bb.maxX, bb.minY,\n\t\t\t\tbb.maxZ), new VectorXYZ(bb.maxX, bb.minY, bb.minZ));\n\t\t// top\n\t\tdrawBox(color, new VectorXYZ(bb.minX, bb.maxY, bb.minZ), new VectorXYZ(\n\t\t\t\tbb.minX, bb.maxY, bb.maxZ), new VectorXYZ(bb.maxX, bb.maxY,\n\t\t\t\tbb.maxZ), new VectorXYZ(bb.maxX, bb.maxY, bb.minZ));\n\t\t// bottom/top connections\n\t\tdrawLineStrip(color, 1, new VectorXYZ(bb.minX, bb.minY, bb.minZ),\n\t\t\t\tnew VectorXYZ(bb.minX, bb.maxY, bb.minZ));\n\t\tdrawLineStrip(color, 1, new VectorXYZ(bb.minX, bb.minY, bb.maxZ),\n\t\t\t\tnew VectorXYZ(bb.minX, bb.maxY, bb.maxZ));\n\t\tdrawLineStrip(color, 1, new VectorXYZ(bb.maxX, bb.minY, bb.maxZ),\n\t\t\t\tnew VectorXYZ(bb.maxX, bb.maxY, bb.maxZ));\n\t\tdrawLineStrip(color, 1, new VectorXYZ(bb.maxX, bb.minY, bb.minZ),\n\t\t\t\tnew VectorXYZ(bb.maxX, bb.maxY, bb.minZ));\n\t}\n\t\n\t/**\n\t * Draw a colored line between two points.\n\t */\n\tprotected final void drawLine(Color color,\n\t\t\tVectorXYZ v1, VectorXYZ v2) {\n\t\tdrawLineLoop(color, 1, asList(v1, v2));\n\t}\n\t\n\t/**\n\t * Draw a colored 2D box as line loop.\n\t */\n\tprotected final void drawBox(Color color,\n\t\t\tVectorXYZ v1, VectorXYZ v2, VectorXYZ v3, VectorXYZ v4) {\n\t\tdrawLineLoop(color, 1, asList(v1, v2, v3, v4));\n\t}\n\t\n\t@Override\n\tpublic void finish() {\n\t\tif (isFinished()) return;\n\t\t\n\t\t//this.drawLineLoop(Color.WHITE, 1, Arrays.asList(new VectorXYZ[]{xzBoundary.topLeft().xyz(0.1), xzBoundary.topRight().xyz(0.1), xzBoundary.bottomRight().xyz(0.1), xzBoundary.bottomLeft().xyz(0.1)}));\n\t\trendererShader = new JOGLRendererVBOShader(gl, textureManager, primitiveBuffer, xzBoundary);\n\t\trenderer = rendererShader;\n\t\tif (renderingParameters.drawBoundingBox) {\n\t\t\tthis.drawBoundingBox(Color.RED, rendererShader.getBoundingBox());\n\t\t}\n\t\tnonAreaRenderer = new JOGLRendererVBONonAreaShader(gl, nonAreaShader, nonAreaPrimitives);\n\t\tif (renderingParameters.useShadowVolumes)\n\t\t\trendererShadowVolume = new JOGLRendererVBOShadowVolume(gl, primitiveBuffer, new VectorXYZW(globalLightingParameters.lightFromDirection, 0));\n\t}\n\t\n\t@Override\n\tpublic void reset() {\n\t\tsuper.reset();\n\t\tif (rendererShadowVolume != null) {\n\t\t\trendererShadowVolume.freeResources();\n\t\t\trendererShadowVolume = null;\n\t\t}\n\t}\n\n\t@Override\n\tpublic void setXZBoundary(AxisAlignedBoundingBoxXZ boundary) {\n\t\tthis.xzBoundary = boundary;\n\t}\n\t\n\t/**\n\t * Set whether to use the real camera PMVMatrix or the PMVMatrix normally\n\t * used for drawing the shadow map when rendering the world.\n\t */\n\tpublic void setShowShadowPerspective(boolean s) {\n\t\tthis.showShadowPerspective = s;\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/JOGLTextureManager.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport javax.imageio.ImageIO;\nimport javax.media.opengl.GL;\n\nimport com.jogamp.opengl.util.awt.ImageUtil;\nimport com.jogamp.opengl.util.texture.Texture;\nimport com.jogamp.opengl.util.texture.TextureIO;\nimport com.jogamp.opengl.util.texture.awt.AWTTextureIO;\n\n/**\n * loads textures from files to JOGL and keeps them available for future use\n */\npublic class JOGLTextureManager {\n\n\tprivate final GL gl;\n\t\n\tprivate final Map<File, Texture> availableTextures = new HashMap<File, Texture>();\n\t\n\tpublic JOGLTextureManager(GL gl) {\n\t\tthis.gl = gl;\n\t}\n\t\n\tpublic Texture getTextureForFile(File file) {\n\t\treturn getTextureForFile(file, true);\n\t}\n\t\n\tpublic Texture getTextureForFile(File file, boolean createMipmaps) {\n\t\t\n\t\tTexture result = availableTextures.get(file);\n\t\t\n\t\tif (result == null) {\n\t\t\t\n\t\t\tsynchronized (this) {\n\t\t\t\t\n\t\t\t\t//try again\n\t\t\t\t\n\t\t\t\tif (availableTextures.containsKey(file)) {\n\t\t\t\t\treturn availableTextures.get(file);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\t\n\t\t\t\t\tif (!file.getName().toLowerCase().endsWith(\"png\")) {\n\t\t\t\t\t\t\n\t\t\t\t\t\t//flip to ensure consistent tex coords with png images\n\t\t\t\t\t\tBufferedImage bufferedImage = ImageIO.read(file);\n\t\t\t\t\t\tImageUtil.flipImageVertically(bufferedImage);\n\t\t\t\t\t\t\n\t\t\t\t\t\tresult = AWTTextureIO.newTexture(\n\t\t\t\t\t\t\t\tgl.getGLProfile(), bufferedImage, createMipmaps);\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\t\tresult = TextureIO.newTexture(file, createMipmaps);\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t/* workaround for OpenGL 3: call to glGenerateMipmap is missing in [AWT]TextureIO.newTexture()\n\t\t\t\t\t * May be fixed in new versions of JOGL.\n\t\t\t\t\t */\n\t\t\t\t\tif (createMipmaps && gl.isGL3()) {\n\t\t\t\t\t\tgl.glGenerateMipmap(result.getTarget());\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tavailableTextures.put(file, result);\n\t\t\t\t\t\n\t\t\t\t} catch (IOException exc) {\n\t\t\t\t\t\n\t\t\t\t\texc.printStackTrace();\n\t\t\t\t\tSystem.exit(2);\n\t\t\t\t\t//TODO error handling\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\t\n\tpublic void releaseAll() {\n\t\t\t\t\n\t\tfor (Texture texture : availableTextures.values()) {\n\t\t\ttexture.destroy(gl);\n\t\t}\n\t\t\n\t\tavailableTextures.clear();\n\t\t\n\t}\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/NonAreaPrimitive.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport java.awt.Color;\nimport java.util.List;\n\nimport org.osm2world.core.math.VectorXYZ;\n\n/**\n * represents points and lines to be drawn by a {@link JOGLTarget}.\n */\nclass NonAreaPrimitive {\n\t\n\tstatic enum Type {\n\t\tPOINTS, LINES, LINE_STRIP, LINE_LOOP\n\t}\n\t\n\tpublic final Type type;\n\tpublic final Color color;\n\tpublic final int width;\n\tpublic final List<VectorXYZ> vs;\n\t\n\tpublic NonAreaPrimitive(Type type, Color color, int width,\n\t\t\tList<VectorXYZ> vs) {\n\t\t\n\t\tthis.type = type;\n\t\tthis.color = color;\n\t\tthis.width = width;\n\t\tthis.vs = vs;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/NonAreaShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport java.nio.FloatBuffer;\n\nimport javax.media.opengl.GL3;\n\nimport com.jogamp.opengl.math.FloatUtil;\nimport com.jogamp.opengl.util.PMVMatrix;\n\n/**\n * Shader for {@link NonAreaPrimitive}.\n */\npublic class NonAreaShader extends AbstractShader {\n\n\tprivate int modelViewProjectionMatrixID;\n\tprivate int vertexPositionID;\n\tprivate int vertexColorID;\n\t\n\tpublic NonAreaShader(GL3 gl) {\n\t\tsuper(gl, \"/shaders/nonarea\");\n\t\t\n\t\t// get indices of named attributes\n\t\tvertexPositionID = gl.glGetAttribLocation(shaderProgram, \"VertexPosition\");\n\t\tvertexColorID = gl.glGetAttribLocation(shaderProgram, \"VertexColor\");\n\t\t\n\t\t// get indices of uniform variables\n\t\tmodelViewProjectionMatrixID = gl.glGetUniformLocation(shaderProgram, \"ModelViewProjectionMatrix\");\n\t\t\n\t\tthis.validateShader();\n\t}\n\t\n\t/**\n\t * Send uniform matrices \"ProjectionMatrix, ModelViewMatrix and ModelViewProjectionMatrix\" to vertex shader\n\t * @param pmvMatrix the PMVMatrix containing all matrices\n\t */\n\tpublic void setPMVMatrix(PMVMatrix pmvMatrix) {\n\t\tFloatBuffer pmvMat = FloatBuffer.allocate(16);\n\t\tFloatUtil.multMatrixf(pmvMatrix.glGetPMatrixf(), pmvMatrix.glGetMvMatrixf(), pmvMat);\n\t\tgl.glUniformMatrix4fv(this.getModelViewProjectionMatrixID(), 1, false, pmvMat);\n\t}\n\t\n\t/**\n\t * Returns the id to use to bind the vertex position attribute.\n\t */\n\tpublic int getVertexPositionID() {\n\t\treturn vertexPositionID;\n\t}\n\t\n\t/**\n\t * Returns the id to use to bind the vertex color attribute.\n\t */\n\tpublic int getVertexColorID() {\n\t\treturn vertexColorID;\n\t}\n\t\n\tpublic int getModelViewProjectionMatrixID() {\n\t\treturn modelViewProjectionMatrixID;\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/PrimitiveBuffer.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.Primitive.Type;\nimport org.osm2world.core.target.common.PrimitiveTarget;\nimport org.osm2world.core.target.common.RenderableToPrimitiveTarget;\nimport org.osm2world.core.target.common.material.Material;\n\nimport com.google.common.collect.HashMultimap;\nimport com.google.common.collect.Multimap;\n\n/**\n * Storage for low-level rendering information (vertex and primitive data)\n * that can be displayed using graphics APIs, e.g. OpenGL.\n * Higher-level information, such as object coherence, OSM attributes\n * or representations, isn't present in a PrimitiveBuffer.\n */\npublic class PrimitiveBuffer extends\n\t\tPrimitiveTarget<RenderableToPrimitiveTarget> {\n\n\t@Override\n\tpublic Class<RenderableToPrimitiveTarget> getRenderableType() {\n\t\treturn RenderableToPrimitiveTarget.class;\n\t}\n\t\n\t@Override\n\tpublic void render(RenderableToPrimitiveTarget renderable) {\n\t\trenderable.renderTo(this);\n\t}\n\t\n\tprivate Multimap<Material, Primitive> primitiveMap = HashMultimap.create();\n\t\n\t@Override\n\tprotected void drawPrimitive(Type type, Material material,\n\t\t\tList<VectorXYZ> vertices, List<VectorXYZ> normals,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\tprimitiveMap.put(material,\n\t\t\t\tnew Primitive(type, vertices, normals, texCoordLists));\n\t}\n\t\n\t/**\n\t * returns all materials used in the buffer\n\t */\n\tpublic Set<Material> getMaterials() {\n\t\treturn primitiveMap.keySet();\n\t}\n\t\n\t/**\n\t * returns all primitives that use a given material\n\t */\n\tpublic Collection<Primitive> getPrimitives(Material material) {\n\t\treturn primitiveMap.get(material);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/RenderableToJOGL.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport org.osm2world.core.target.Renderable;\n\npublic interface RenderableToJOGL extends Renderable {\n\n\tpublic void renderTo(JOGLTarget target);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/SSAOShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT;\nimport static javax.media.opengl.GL.GL_DEPTH_TEST;\n\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.AmbientOcclusion;\n\n/**\n * Shader to render the depth buffer into a texture that can be used to implement SSAO later.\n */\npublic class SSAOShader extends DepthBufferShader {\n\t\n\tprivate int depthBufferHandle;\n\tprivate int frameBufferHandle;\n\tprivate int width;\n\tprivate int height;\n\t\n\tpublic SSAOShader(GL3 gl) {\n\t\tsuper(gl);\n\t\tinitialize();\n\t}\n\t\n\t/**\n\t * Setup the framebuffer and texture.\n\t */\n\tprivate void initialize() {\n\t\tint[] viewport = new int[4];\n\t\tgl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);\n\t\twidth = viewport[2];\n\t\theight = viewport[3];\n\n\t\t// create the shadow map texture / depth buffer\n\t\tint[] tmp = new int[1];\n\t\tgl.glGenTextures(1,tmp,0);\n\t\tdepthBufferHandle = tmp[0];\n\t\tgl.glActiveTexture(GL.GL_TEXTURE1);\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);\n\n\t\tgl.glTexImage2D(GL.GL_TEXTURE_2D,          // target texture type\n\t\t        0,                                  // mipmap LOD level\n\t\t        GL3.GL_DEPTH_COMPONENT,         // internal pixel format\n\t\t                                            //GL_DEPTH_COMPONENT\n\t\t        width,                     // width of generated image\n\t\t        height,                    // height of generated image\n\t\t        0,                          // border of image\n\t\t        GL3.GL_DEPTH_COMPONENT,     // external pixel format \n\t\t        GL.GL_UNSIGNED_BYTE,        // datatype for each value\n\t\t        null);  // buffer to store the texture in memory\n\n\t\t// some settings for the shadow map texture\n\t\t// GL_LINEAR might produce better results, but is slower. GL_NEAREST shows aliasing artifacts clearly\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);\n\t\t\n\t\t/* For texture access outside the shadow map use the highest depth value possible (1.0).\n\t\t * This means the fragment lies outside of the lights frustum and no shadow should be applied.\n\t\t * Therefore we use CLAMP_TO_BORDER with a border of (1.0, 0.0, 0.0, 0.0)\n\t\t */\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);\n\t\t//float [] border = {1.0f, 0.0f, 0.0f, 0.0f};\n\t\t//gl.glTexParameterfv(GL.GL_TEXTURE_2D, GL3.GL_TEXTURE_BORDER_COLOR, border, 0);\n\t\t\n\t\t/* special for depth textures: do not retrieve the texture values, but the result of a comparison.\n\t\t * compare the third value (r) of the texture coordinate against the depth value stored at the texture coordinate (s,t)\n\t\t * result will be 1.0 if r is less than the texture value (which means the fragment is nearer) and 0.0 otherwise\n\t\t */\n\t\t//gl.glTexParameteri(GL.GL_TEXTURE_2D, GL3.GL_TEXTURE_COMPARE_MODE, GL3.GL_COMPARE_REF_TO_TEXTURE);\n\t\t//gl.glTexParameteri(GL.GL_TEXTURE_2D, GL3.GL_TEXTURE_COMPARE_FUNC, GL.GL_LESS);\n\n\t\tgl.glActiveTexture(GL.GL_TEXTURE1);\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);\n\t\t\n\t\t\n\t\t/*gl.glGenTextures(1,tmp,0);\n\t\tcolorBufferHandle = tmp[0];\n\t\tgl.glActiveTexture(GL.GL_TEXTURE1);\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, colorBufferHandle);\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);\n\n\t\tgl.glTexImage2D(GL.GL_TEXTURE_2D,          // target texture type\n\t\t        0,                                  // mipmap LOD level\n\t\t        GL.GL_RGBA,         // internal pixel format\n\t\t                                            //GL_DEPTH_COMPONENT\n\t\t        shadowMapWidth,                     // width of generated image\n\t\t        shadowMapHeight,                    // height of generated image\n\t\t        0,                          // border of image\n\t\t        GL.GL_RGBA,     // external pixel format \n\t\t        GL.GL_UNSIGNED_BYTE,        // datatype for each value\n\t\t        null);  // buffer to store the texture in memory\n\t\t*/\n\t\t\n\t\t// create the frame buffer object (FBO)\n\t\tgl.glGenFramebuffers(1, tmp, 0);\n\t\tframeBufferHandle = tmp[0];\n\t\tgl.glBindFramebuffer(GL.GL_FRAMEBUFFER, frameBufferHandle);\n\n\t\t//Attach 2D texture to this FBO\n\t\tgl.glFramebufferTexture2D(GL.GL_FRAMEBUFFER,\n\t\t        GL.GL_DEPTH_ATTACHMENT,\n\t\t        GL.GL_TEXTURE_2D,\n\t\t        depthBufferHandle,0);\n\t\t/*gl.glFramebufferTexture(GL.GL_FRAMEBUFFER,\n\t\t        GL.GL_DEPTH_ATTACHMENT,\n\t\t        depthBufferHandle,0);*/\n\t\t/*gl.glFramebufferTexture2D(GL.GL_FRAMEBUFFER,\n\t\t        GL.GL_COLOR_ATTACHMENT0,\n\t\t        GL.GL_TEXTURE_2D,\n\t\t        colorBufferHandle,0);*/\n\t\t\n\t\t// set target for fragment shader output: not used, we only need the depth buffer\n\t\t//int[] drawBuffers = {GL.GL_NONE};\n\t\t//gl.glDrawBuffers(1, drawBuffers, 0);\n\t\t//gl.glDrawBuffer(GL.GL_COLOR_ATTACHMENT0);\n\t\tgl.glDrawBuffer(GL.GL_NONE);\n\t\tgl.glReadBuffer(GL.GL_NONE);\n\n\t\t//gl.glBindTexture(GL.GL_TEXTURE_2D, 0);\n\n\t\t//Disable color buffer\n\t\t//http://stackoverflow.com/questions/12546368/render-the-depth-buffer-into-a-texture-using-a-frame-buffer\n\t\t//gl.glDrawBuffer(GL2.GL_NONE);\n\t\t//gl.glReadBuffer(GL2.GL_NONE);\n\n\t\t//Set pixels ((width*2)* (height*2))\n\t\t//It has to have twice the size of shadowmap size\n\t\t//pixels = GLBuffers.newDirectByteBuffer(shadowMapWidth*shadowMapHeight*4);\n\n\t\t//Set default frame buffer before doing the check\n\t\t//http://www.opengl.org/wiki/FBO#Completeness_Rules\n\t\t//gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);\n\n\t\tint status = gl.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER);\n\n\t\t// Always check that our framebuffer is ok\n\t\tif(gl.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER) != GL.GL_FRAMEBUFFER_COMPLETE)\n\t\t{\n\t\t\tthrow new RuntimeException(\"Can not use FBO! Status error:\" + status);\n\t\t}\n\t\tgl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);\n\t}\n\t\n\t/**\n\t * Resize the framebuffer backing texture, if size doesn't match.\n\t */\n\tprivate void resizeBuffer(int width, int height) {\n\t\tif (width != this.width || height != this.height) {\n\t\t\tthis.width = width;\n\t\t\tthis.height = height;\n\t\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);\n\t\n\t\t\tgl.glTexImage2D(GL.GL_TEXTURE_2D,          // target texture type\n\t\t\t        0,                                  // mipmap LOD level\n\t\t\t        GL3.GL_DEPTH_COMPONENT,         // internal pixel format\n\t\t\t                                            //GL_DEPTH_COMPONENT\n\t\t\t        width,                     // width of generated image\n\t\t\t        height,                    // height of generated image\n\t\t\t        0,                          // border of image\n\t\t\t        GL3.GL_DEPTH_COMPONENT,     // external pixel format \n\t\t\t        GL.GL_UNSIGNED_BYTE,        // datatype for each value\n\t\t\t        null);  // buffer to store the texture in memory\n\t\t}\n\t}\n\t\n\t/**\n\t * Prepares rendering of the depth map. Binds the framebuffer and clears it.\n\t * The size of the framebuffer is automatically adjusted to match the current viewport.\n\t */\n\tprivate void prepareDepthMapGeneration() {\n\t\tint[] viewport = new int[4];\n\t\tgl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);\n\t\tresizeBuffer(viewport[2], viewport[3]);\n\t\t\n\t\t// bind FBO\n\t\tgl.glBindFramebuffer(GL.GL_FRAMEBUFFER, frameBufferHandle);\n\t\t\n\t\t// clear shadow map\n\t\tgl.glClear(GL_DEPTH_BUFFER_BIT);\n\t\t\n\t\tgl.glEnable(GL_DEPTH_TEST);\n\t}\n\t\n\t/**\n\t * {@inheritDoc}\n\t * Only primitives that support ambient occlusion will get rendered.\n\t */\n\t@Override\n\tpublic boolean setMaterial(Material material, JOGLTextureManager textureManager) {\n\t\t\n\t\tif (material.getAmbientOcclusion() == AmbientOcclusion.FALSE) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\treturn super.setMaterial(material, textureManager);\n\t}\n\t\n\t/**\n\t * Returns the handle of the texture containing the rendered depth map.\n\t */\n\tpublic int getDepthBuferHandle() {\n\t\treturn depthBufferHandle;\n\t}\n\n\t/**\n\t * Prepares rendering of the depth map. This changes the current framebuffer.\n\t * {@link #disableShader()} should be called after the rendering is complete to bind the default framebuffer again.\n\t */\n\t@Override\n\tpublic void useShader() {\n\t\tsuper.useShader();\n\t\tprepareDepthMapGeneration();\n\t}\n\t\n\t/**\n\t * Completes the rendering of the depth map. The default framebuffer gets restored.\n\t */\n\t@Override\n\tpublic void disableShader() {\n\t\t\n\t\t//ShaderManager.saveDepthBuffer(new File(\"/home/sebastian/ssao_depth.png\"), depthBufferHandle, width, height, gl);\n\t\t\n\t\t// bind default framebuffer\n\t\tgl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);\n\t\tsuper.disableShader();\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/ShaderManager.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport java.awt.image.BufferedImage;\nimport java.awt.image.WritableRaster;\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.ByteBuffer;\nimport java.nio.FloatBuffer;\nimport java.nio.IntBuffer;\n\nimport javax.imageio.ImageIO;\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GL2GL3;\nimport javax.media.opengl.GL3;\n\n/**\n * Utility class to manage low level shader creation.\n */\npublic class ShaderManager {\n\n\t/**\n\t * Loads the vertex shader from a resource file, compiles it and does error checking.\n\t * @param filename path to the resource containing the shader code\n\t * @return handle of the created vertex shader\n\t */\n\tpublic static int createVertexShader(GL3 gl, String filename) {\n\n\t\t// get the unique id\n\t\tint vertShader = gl.glCreateShader(GL3.GL_VERTEX_SHADER);\n\t\tif (vertShader == 0)\n\t\t\tthrow new RuntimeException(\"Unable to create vertex shader.\");\n\n\t\t// create a single String array index to hold the shader code\n\t\tString[] vertCode = new String[1];\n\t\tvertCode[0] = \"\";\n\t\tString line;\n\n\t\t// open the file and read the contents into the String array.\n\t\tInputStream stream = System.class.getResourceAsStream(filename);\n\t\tif (stream == null) {\n\t\t\tthrow new RuntimeException(\"Vertex shader not found in classpath: \\\"\"+ filename +\"\\\"\");\n\t\t}\n\t\tBufferedReader reader = new BufferedReader(new InputStreamReader(stream));\n\t\ttry {\n\t\t\twhile ((line = reader.readLine()) != null) {\n\t\t\t\tvertCode[0] += line + \"\\n\";\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new RuntimeException(\"Failed reading vertex shader \\\"\" + filename + \"\\\".\",e);\n\t\t}\n\n\t\t// Associate the code string with the unique id\n\t\tgl.glShaderSource(vertShader, 1, vertCode, null);\n\t\t// compile the vertex shader\n\t\tgl.glCompileShader(vertShader);\n\n\t\t// acquire compilation status\n\t\tIntBuffer shaderStatus = IntBuffer.allocate(1);\n\t\tgl.glGetShaderiv(vertShader, GL3.GL_COMPILE_STATUS, shaderStatus);\n\n\t\t// check whether compilation was successful\n\t\tif (shaderStatus.get() == GL.GL_FALSE) {\n\t\t\tprintShaderInfoLog(gl, vertShader);\n\t\t\tthrow new IllegalStateException(\"compilation error for shader [\"\n\t\t\t\t\t+ filename + \"].\");\n\t\t}\n\t\tprintShaderInfoLog(gl, vertShader);\n\n\t\t// the int returned is now associated with the compiled shader\n\t\treturn vertShader;\n\t}\n\n\t/**\n\t * Loads the fragment shader from a resource file, compiles it and does error checking.\n\t * @param filename path to the resource containing the shader code\n\t * @return handle of the created fragment shader\n\t */\n\tpublic static int createFragmentShader(GL3 gl, String filename) {\n\n\t\tint fragShader = gl.glCreateShader(GL3.GL_FRAGMENT_SHADER);\n\t\tif (fragShader == 0)\n\t\t\treturn 0;\n\n\t\tString[] fragCode = new String[1];\n\t\tfragCode[0] = \"\";\n\t\tString line;\n\n\t\tInputStream stream = System.class.getResourceAsStream(filename);\n\t\tif (stream == null) {\n\t\t\tthrow new RuntimeException(\"Fragment shader not found in classpath: \\\"\"+ filename +\"\\\"\");\n\t\t}\n\t\tBufferedReader reader = new BufferedReader(new InputStreamReader(stream));\n\t\ttry {\n\t\t\twhile ((line = reader.readLine()) != null) {\n\t\t\t\tfragCode[0] += line + \"\\n\";\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new RuntimeException(\"Failed reading fragment shader \\\"\" + filename + \"\\\".\",e);\n\t\t}\n\n\t\tgl.glShaderSource(fragShader, 1, fragCode, null);\n\t\tgl.glCompileShader(fragShader);\n\t\t\n\t\t// acquire compilation status\n\t\tIntBuffer shaderStatus = IntBuffer.allocate(1);\n\t\tgl.glGetShaderiv(fragShader, GL3.GL_COMPILE_STATUS, shaderStatus);\n\n\t\t// check whether compilation was successful\n\t\tif (shaderStatus.get() == GL.GL_FALSE) {\n\t\t\tprintShaderInfoLog(gl, fragShader);\n\t\t\tthrow new IllegalStateException(\"compilation error for shader [\"\n\t\t\t\t\t+ filename + \"].\");\n\t\t}\n\t\tprintShaderInfoLog(gl, fragShader);\n\n\t\treturn fragShader;\n\t}\n\t\n\t/**\n\t * Prints the shader log to System.out\n\t */\n\tpublic static boolean printShaderInfoLog(GL3 gl, int shader) {\n\t\tIntBuffer ival = IntBuffer.allocate(1);\n\t\tgl.glGetShaderiv(shader, GL3.GL_INFO_LOG_LENGTH,\n\t\t\t\tival);\n\n\t\tint size = ival.get();\n\t\tif (size > 1) {\n\t\t\tByteBuffer log = ByteBuffer.allocate(size);\n\t\t\tival.flip();\n\t\t\tgl.glGetShaderInfoLog(shader, size, ival, log);\n\t\t\tbyte[] infoBytes = new byte[size];\n\t\t\tlog.get(infoBytes);\n\t\t\tSystem.out.println(\"Info log: \" + new String(infoBytes));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t }\n\t\n\t/**\n\t * Reads the program log into a String.\n\t * @param prog handle to the shader program\n\t * @return the program log as String\n\t */\n\tpublic static String getProgramInfoLog(GL3 gl, int prog) {\n\t\tIntBuffer ival = IntBuffer.allocate(1);\n\t\tgl.glGetProgramiv(prog, GL3.GL_INFO_LOG_LENGTH,\n\t\t\t\tival);\n\n\t\tint size = ival.get();\n\t\tif (size > 1) {\n\t\t\tByteBuffer log = ByteBuffer.allocate(size);\n\t\t\tival.flip();\n\t\t\tgl.glGetProgramInfoLog(prog, size, ival, log);\n\t\t\tbyte[] infoBytes = new byte[size];\n\t\t\tlog.get(infoBytes);\n\t\t\treturn new String(infoBytes);\n\t\t}\n\t\treturn \"\";\n\t}\n\t\n\t/**\n\t * Prints the program log to System.out\n\t */\n\tpublic static boolean printProgramInfoLog(GL3 gl, int prog) {\n\t\tIntBuffer ival = IntBuffer.allocate(1);\n\t\tgl.glGetProgramiv(prog, GL3.GL_INFO_LOG_LENGTH,\n\t\t\t\tival);\n\n\t\tint size = ival.get();\n\t\tif (size > 1) {\n\t\t\tByteBuffer log = ByteBuffer.allocate(size);\n\t\t\tival.flip();\n\t\t\tgl.glGetProgramInfoLog(prog, size, ival, log);\n\t\t\tbyte[] infoBytes = new byte[size];\n\t\t\tlog.get(infoBytes);\n\t\t\tSystem.out.println(\"Info log: \" + new String(infoBytes));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t }\n\t\n\t/**\n\t * Save a depth buffer texture to a file as png.\n\t */\n\tpublic static void saveDepthBuffer(File file, int depthBufferHandle, int width, int height, GL2GL3 gl) {\n\t\t// create buffer to store image\n\t\tFloatBuffer buffer=FloatBuffer.allocate(width*height);//ByteBuffer.allocate(shadowMapWidth*shadowMapHeight*4).asFloatBuffer(); \n\t\t\n\t\t// load image in buffer\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);\n\t\tgl.glGetTexImage(GL.GL_TEXTURE_2D, 0, GL3.GL_DEPTH_COMPONENT, GL.GL_FLOAT, buffer);\n\t\tbuffer.rewind();\n\n\t\t// create buffered image\n\t\tBufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);\n\t\t\n\t\t// copy data to buffered image\n\t\tWritableRaster wr = img.getRaster();\n\t\tfor (int col=0; col<img.getWidth(); col++) {\n\t\t\tfor (int row=0; row<img.getHeight(); row++) {\n\t\t\t\tfloat f = buffer.get(row*width+col);\n\t\t\t\tint v = (int)(f*255);\n\t\t\t\twr.setPixel(col, height-1-row, new int[] {v});\n\t\t\t}\n\t\t}\n\t\t\n\t\t// save to file\n\t\ttry {\n\t\t\tImageIO.write(img, \"png\", file);\n\t\t} catch (IOException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/ShadowMapShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_CCW;\nimport static javax.media.opengl.GL.GL_COLOR_BUFFER_BIT;\nimport static javax.media.opengl.GL.GL_CULL_FACE;\nimport static javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT;\nimport static javax.media.opengl.GL.GL_DEPTH_TEST;\nimport static javax.media.opengl.fixedfunc.GLMatrixFunc.GL_MODELVIEW;\nimport static javax.media.opengl.fixedfunc.GLMatrixFunc.GL_PROJECTION;\n\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.FloatBuffer;\nimport java.util.ArrayList;\nimport javax.imageio.ImageIO;\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GL3;\n\nimport jogamp.opengl.ProjectFloat;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXYZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Shadow;\nimport org.osm2world.core.target.common.material.Material.Transparency;\nimport org.osm2world.core.target.common.rendering.Projection;\nimport org.osm2world.viewer.model.Defaults;\n\nimport com.jogamp.opengl.math.FloatUtil;\nimport com.jogamp.opengl.util.PMVMatrix;\n\n/**\n * Shader to render the depth buffer into a texture that can be used to implement shadow maps later.\n */\npublic class ShadowMapShader extends DepthBufferShader {\n\t\n\tprotected int shadowMapWidth = 1024;\n\tprotected int shadowMapHeight = 1024;\n\t\n\t/**\n\t * Padding for the calculated bounding box around the camera frustum.\n\t * This is needed to not cut away shadow casters outside but nearby the camera frustum\n\t * that may cast shadows which lay within the camera frustum.\n\t */\n\tprivate int cameraFrustumPadding = 8;\n\t\n\tpublic int depthBufferHandle;\n\tpublic int colorBufferHandle;\n\tprivate int frameBufferHandle;\n\t\n\tprivate int[] viewport = new int[4];\n\t\n\tprivate boolean renderOpaque = true;\n\t\n\t/**\n\t *  model view projection matrix of the shadow casting light source\n\t */\n\tprivate PMVMatrix pmvMat;\n\t\n\tpublic ShadowMapShader(GL3 gl) {\n\t\tsuper(gl);\n\t\t\n\t\tpmvMat = new PMVMatrix();\n\t\tinitializeShadowMap();\n\t}\n\t\n\t/**\n\t * Setup the framebuffer and texture.\n\t */\n\tprivate void initializeShadowMap() {\n\n\t\t// create the shadow map texture / depth buffer\n\t\tint[] tmp = new int[1];\n\t\tgl.glGenTextures(1,tmp,0);\n\t\tdepthBufferHandle = tmp[0];\n\t\tgl.glActiveTexture(GL.GL_TEXTURE0);\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);\n\n\t\tgl.glTexImage2D(GL.GL_TEXTURE_2D,          // target texture type\n\t\t        0,                                  // mipmap LOD level\n\t\t        GL3.GL_DEPTH_COMPONENT,         // internal pixel format\n\t\t                                            //GL_DEPTH_COMPONENT\n\t\t        shadowMapWidth,                     // width of generated image\n\t\t        shadowMapHeight,                    // height of generated image\n\t\t        0,                          // border of image\n\t\t        GL3.GL_DEPTH_COMPONENT,     // external pixel format \n\t\t        GL.GL_UNSIGNED_BYTE,        // datatype for each value\n\t\t        null);  // buffer to store the texture in memory\n\n\t\t// some settings for the shadow map texture\n\t\t// GL_LINEAR might produce better results, but is slower. GL_NEAREST shows aliasing artifacts clearly\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);\n\t\t\n\t\t/* For texture access outside the shadow map use the highest depth value possible (1.0).\n\t\t * This means the fragment lies outside of the lights frustum and no shadow should be applied.\n\t\t * Therefore we use CLAMP_TO_BORDER with a border of (1.0, 0.0, 0.0, 0.0)\n\t\t */\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL3.GL_CLAMP_TO_BORDER);\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL3.GL_CLAMP_TO_BORDER);\n\t\tfloat [] border = {1.0f, 0.0f, 0.0f, 0.0f};\n\t\tgl.glTexParameterfv(GL.GL_TEXTURE_2D, GL3.GL_TEXTURE_BORDER_COLOR, border, 0);\n\t\t\n\t\t/* special for depth textures: do not retrieve the texture values, but the result of a comparison.\n\t\t * compare the third value (r) of the texture coordinate against the depth value stored at the texture coordinate (s,t)\n\t\t * result will be 1.0 if r is less than the texture value (which means the fragment is nearer) and 0.0 otherwise\n\t\t */\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL3.GL_TEXTURE_COMPARE_MODE, GL3.GL_COMPARE_REF_TO_TEXTURE);\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL3.GL_TEXTURE_COMPARE_FUNC, GL.GL_LESS);\n\n\t\tgl.glActiveTexture(GL.GL_TEXTURE0);\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);\n\t\t\n\t\t\n\t\t/*gl.glGenTextures(1,tmp,0);\n\t\tcolorBufferHandle = tmp[0];\n\t\tgl.glActiveTexture(GL.GL_TEXTURE1);\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, colorBufferHandle);\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);\n\t\tgl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);\n\n\t\tgl.glTexImage2D(GL.GL_TEXTURE_2D,          // target texture type\n\t\t        0,                                  // mipmap LOD level\n\t\t        GL.GL_RGBA,         // internal pixel format\n\t\t                                            //GL_DEPTH_COMPONENT\n\t\t        shadowMapWidth,                     // width of generated image\n\t\t        shadowMapHeight,                    // height of generated image\n\t\t        0,                          // border of image\n\t\t        GL.GL_RGBA,     // external pixel format \n\t\t        GL.GL_UNSIGNED_BYTE,        // datatype for each value\n\t\t        null);  // buffer to store the texture in memory\n\t\t*/\n\t\t\n\t\t\n\t\t// create the frame buffer object (FBO)\n\t\tgl.glGenFramebuffers(1, tmp, 0);\n\t\tframeBufferHandle = tmp[0];\n\t\tgl.glBindFramebuffer(GL.GL_FRAMEBUFFER, frameBufferHandle);\n\n\t\t//Attach 2D texture to this FBO\n\t\tgl.glFramebufferTexture2D(GL.GL_FRAMEBUFFER,\n\t\t        GL.GL_DEPTH_ATTACHMENT,\n\t\t        GL.GL_TEXTURE_2D,\n\t\t        depthBufferHandle,0);\n\t\t/*gl.glFramebufferTexture(GL.GL_FRAMEBUFFER,\n\t\t        GL.GL_DEPTH_ATTACHMENT,\n\t\t        depthBufferHandle,0);*/\n\t\t/*gl.glFramebufferTexture2D(GL.GL_FRAMEBUFFER,\n\t\t        GL.GL_COLOR_ATTACHMENT0,\n\t\t        GL.GL_TEXTURE_2D,\n\t\t        colorBufferHandle,0);*/\n\t\t\n\t\t// set target for fragment shader output: not used, we only need the depth buffer\n\t\t//int[] drawBuffers = {GL.GL_NONE};\n\t\t//gl.glDrawBuffers(1, drawBuffers, 0);\n\t\t//gl.glDrawBuffer(GL.GL_COLOR_ATTACHMENT0);\n\t\tgl.glDrawBuffer(GL.GL_NONE);\n\t\tgl.glReadBuffer(GL.GL_NONE);\n\n\t\t//gl.glBindTexture(GL.GL_TEXTURE_2D, 0);\n\n\t\t//Disable color buffer\n\t\t//http://stackoverflow.com/questions/12546368/render-the-depth-buffer-into-a-texture-using-a-frame-buffer\n\t\t//gl.glDrawBuffer(GL2.GL_NONE);\n\t\t//gl.glReadBuffer(GL2.GL_NONE);\n\n\t\t//Set pixels ((width*2)* (height*2))\n\t\t//It has to have twice the size of shadowmap size\n\t\t//pixels = GLBuffers.newDirectByteBuffer(shadowMapWidth*shadowMapHeight*4);\n\n\t\t//Set default frame buffer before doing the check\n\t\t//http://www.opengl.org/wiki/FBO#Completeness_Rules\n\t\t//gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);\n\n\t\tint status = gl.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER);\n\n\t\t// Always check that our framebuffer is ok\n\t\tif(gl.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER) != GL.GL_FRAMEBUFFER_COMPLETE)\n\t\t{\n\t\t\tthrow new RuntimeException(\"Can not use FBO! Status error:\" + status);\n\t\t}\n\t\tgl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);\n\t}\n\t\n\t/**\n\t * @see #cameraFrustumPadding\n\t */\n\tpublic void setCameraFrustumPadding(int padding) {\n\t\tthis.cameraFrustumPadding = padding;\n\t}\n\t\n\t/**\n\t * Change the size of the shadow map texture. This needs to be called before {@link #useShader()},\n\t * as otherwise the viewport may be wrong.\n\t * @param width the new texture width\n\t * @param height the new texture height\n\t */\n\tpublic void setShadowMapSize(int width, int height) {\n\t\tresizeBuffer(width, height);\n\t}\n\t\n\t/**\n\t * Resize the framebuffer backing texture, if size doesn't match.\n\t */\n\tprivate void resizeBuffer(int width, int height) {\n\t\tif (width != this.shadowMapWidth || height != this.shadowMapHeight) {\n\t\t\tthis.shadowMapWidth = width;\n\t\t\tthis.shadowMapHeight = height;\n\t\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);\n\t\n\t\t\tgl.glTexImage2D(GL.GL_TEXTURE_2D,          // target texture type\n\t\t\t        0,                                  // mipmap LOD level\n\t\t\t        GL3.GL_DEPTH_COMPONENT,         // internal pixel format\n\t\t\t                                            //GL_DEPTH_COMPONENT\n\t\t\t        shadowMapWidth,                     // width of generated image\n\t\t\t        shadowMapHeight,                    // height of generated image\n\t\t\t        0,                          // border of image\n\t\t\t        GL3.GL_DEPTH_COMPONENT,     // external pixel format \n\t\t\t        GL.GL_UNSIGNED_BYTE,        // datatype for each value\n\t\t\t        null);  // buffer to store the texture in memory\n\n\t\t\t/*gl.glBindTexture(GL.GL_TEXTURE_2D, colorBufferHandle);\n\t\n\t\t\tgl.glTexImage2D(GL.GL_TEXTURE_2D,          // target texture type\n\t\t\t        0,                                  // mipmap LOD level\n\t\t\t        GL.GL_RGBA,         // internal pixel format\n\t\t\t                                            //GL_DEPTH_COMPONENT\n\t\t\t        shadowMapWidth,                     // width of generated image\n\t\t\t        shadowMapHeight,                    // height of generated image\n\t\t\t        0,                          // border of image\n\t\t\t        GL.GL_RGBA,     // external pixel format \n\t\t\t        GL.GL_UNSIGNED_BYTE,        // datatype for each value\n\t\t\t        null);  // buffer to store the texture in memory\n\t\t\t*/\n\t\t}\n\t}\n\t\n\t/**\n\t * Prepare everything to render the shadow map (bind framebuffer, update viewport, clear buffer, etc.)\n\t */\n\tprivate void prepareShadowMapGeneration() {\n\t\t\n\t\t// bind FBO\n\t\tgl.glBindFramebuffer(GL.GL_FRAMEBUFFER, frameBufferHandle);\n\t\t\n\t\t// set right viewport for the framebuffer size, store old to reset later\n\t\tgl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);\n        gl.glViewport(0, 0, shadowMapWidth, shadowMapHeight);\n\t\t\n\t\t// clear shadow map\n\t\tgl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\n\t\t\n\t\t// enable front face culling\n\t\tgl.glFrontFace(GL_CCW);\n\t\tgl.glCullFace(GL.GL_FRONT);\n\t\tgl.glEnable (GL_CULL_FACE);\n\t\t//gl.glDisable (GL_CULL_FACE);\n\t\t\n\t\tgl.glEnable(GL_DEPTH_TEST);\n\t}\n\t\n\tpublic void saveColorBuffer(File file) {\n\t\t// create buffer to store image\n\t\tByteBuffer buffer = ByteBuffer.allocate(shadowMapWidth*shadowMapHeight*4);\n\t\t\n\t\t// load image in buffer\n\t\tgl.glBindTexture(GL.GL_TEXTURE_2D, colorBufferHandle);\n\t\tgl.glGetTexImage(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, buffer);\n\t\tbuffer.rewind();\n\n\t\t// create buffered image\n\t\tBufferedImage img = new BufferedImage(shadowMapWidth, shadowMapHeight, BufferedImage.TYPE_INT_RGB);\n\t\t\n\t\t// copy data to buffered image\n\t\tfor (int col=0; col<img.getWidth(); col++) {\n\t\t\tfor (int row=0; row<img.getHeight(); row++) {\n\t\t\t\tbyte r = buffer.get(row*shadowMapHeight*4+col*4+0);\n\t\t\t\tbyte g = buffer.get(row*shadowMapHeight*4+col*4+1);\n\t\t\t\tbyte b = buffer.get(row*shadowMapHeight*4+col*4+2);\n\t\t\t\tbyte a = buffer.get(row*shadowMapHeight*4+col*4+3);\n\t\t\t\tint alpha = a & 0xFF;\n\t\t\t\tint red =   r & 0xFF;\n\t\t\t\tint green = g & 0xFF;\n\t\t\t\tint blue =  b & 0xFF;\n\t\t\t\tint rgb = (alpha << 24 | red << 16 | green << 8 | blue);\n\t\t\t\timg.setRGB(col, shadowMapHeight-1-row, rgb);\n\t\t\t}\n\t\t}\n\t\t\n\n\t\t// save to file\n\t\ttry {\n\t\t\tImageIO.write(img, \"png\", file);\n\t\t} catch (IOException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\t\n\t/**\n\t * prepare and use PMVMatrix for rendering shadows from global lighting perspective using \"Perspective Shadow Maps\"\n\t * (see http://www-sop.inria.fr/reves/Marc.Stamminger/psm/)\n\t * @param lighting\n\t */\n\tpublic void preparePMVMatrixPSM(GlobalLightingParameters lighting, PMVMatrix cameraPMV, AxisAlignedBoundingBoxXYZ primitivesBoundingBox) {\n\t\t\n\t\t// camera PMV Matrix:\n\t\tFloatBuffer camPMvMat = FloatBuffer.allocate(16);\n\t\tFloatUtil.multMatrixf(cameraPMV.glGetPMatrixf(), cameraPMV.glGetMvMatrixf(), camPMvMat);\n\t\t\n\t\t// transform light into camera space (unit cube)\n\t\tfloat[] lightPos = {(float)lighting.lightFromDirection.x, (float)lighting.lightFromDirection.y, -(float)lighting.lightFromDirection.z, 0};\n\t\tfloat[] lightPosCam = new float[4];\n\t\tFloatUtil.multMatrixVecf(camPMvMat, lightPos, lightPosCam);\n\t\t\n\t\t// set view and projection matrices to light source\n\t\tPMVMatrix pmvMatL = new PMVMatrix();\n\t\tpmvMatL.glMatrixMode(GL_MODELVIEW);\n\t\tpmvMatL.glLoadIdentity();\n\t\tpmvMatL.gluLookAt(lightPosCam[0], lightPosCam[1], lightPosCam[2],\n\t\t\t\t0f, 0f, 0f,\n\t\t\t\t0f, 1f, 0f);\n\t\t\n\t\tpmvMatL.glMatrixMode(GL_PROJECTION);\n\t\tpmvMatL.glLoadIdentity();\n\t\tProjection projection = Defaults.PERSPECTIVE_PROJECTION;\n\t\tpmvMatL.gluPerspective(\n\t\t\t\t(float)(projection.getVertAngle()),\n\t\t\t\t(float)(projection.getAspectRatio()),\n\t\t\t\t(float)(projection.getNearClippingDistance()),\n\t\t\t\t(float)(projection.getFarClippingDistance()));\n\t\t//pmvMat.glOrthof(-1000,1000,-1000,1000,-1000,1500);\n\t\t\n\t\t//float[] frustum;\n\t\t/*frustum = intersectFrustum(calculateCameraLightFrustum(pmvMat, cameraPMV),\n\t\t\t\tcalculatePrimitivesLightFrustum(pmvMat, primitivesBoundingBox));*/\n\t\t//frustum = calculatePrimitivesLightFrustum(pmvMat, primitivesBoundingBox);\n\t\t//System.out.println(\"shadow map frustum: \" + Arrays.toString(frustum));\n\t\t//pmvMatL.glOrthof(frustum[0], frustum[1], frustum[2], frustum[3], frustum[4], frustum[5]);\n\t\t\n\t\t// M = M_cam\n\t\tpmvMat.glMatrixMode(GL_MODELVIEW);\n\t\tpmvMat.glLoadMatrixf(cameraPMV.glGetMvMatrixf());\n\t\t\n\t\t// P = P_light*Mv_light*P_cam\n\t\tpmvMat.glMatrixMode(GL_PROJECTION);\n\t\tpmvMat.glLoadMatrixf(pmvMatL.glGetPMatrixf());\n\t\tpmvMat.glMultMatrixf(pmvMatL.glGetMvMatrixf());\n\t\tpmvMat.glMultMatrixf(cameraPMV.glGetPMatrixf());\n\t\t\n\t\tsetPMVMatrix(pmvMat);\n\t}\n\t\n\t/**\n\t * Prepare and use PMVMatrix for rendering shadows from global lighting perspective\n\t * @param lighting contains the lights direction\n\t * @param cameraPMV the current camera PMVMatrix used to tighten the lights view frustum on the visible part of the world\n\t * @param primitivesBoundingBox bounding box around all relevant primitives in world coordinates. Also used to tighten the lights view frustum\n\t */\n\tpublic void preparePMVMatrix(GlobalLightingParameters lighting, PMVMatrix cameraPMV, AxisAlignedBoundingBoxXYZ primitivesBoundingBox) {\n\t\t\n\t\t// set view and projection matrices to light source\n\n\t\t\n\t\tpmvMat.glMatrixMode(GL_MODELVIEW);\n\t\tpmvMat.glLoadIdentity();\n\t\tpmvMat.gluLookAt((float)lighting.lightFromDirection.x, (float)lighting.lightFromDirection.y, -(float)lighting.lightFromDirection.z,\n\t\t\t\t0f, 0f, 0f,\n\t\t\t\t0f, 1f, 0f);\n\t\t\n\t\tpmvMat.glMatrixMode(GL_PROJECTION);\n\t\tpmvMat.glLoadIdentity();\n\t\t/*Projection projection = Defaults.PERSPECTIVE_PROJECTION;\n\t\tpmvMat.gluPerspective(\n\t\t\t\t(float)(projection.getVertAngle()),\n\t\t\t\t(float)(projection.getAspectRatio()),\n\t\t\t\t(float)(projection.getNearClippingDistance()),\n\t\t\t\t(float)(projection.getFarClippingDistance()));*/\n\t\t//pmvMat.glOrthof(-1000,1000,-1000,1000,-1000,1500);\n\t\t\n\t\tAxisAlignedBoundingBoxXYZ frustum;\n\t\tfrustum = AxisAlignedBoundingBoxXYZ.intersect(calculateCameraLightFrustum(pmvMat, cameraPMV),\n\t\t\t\tcalculatePrimitivesLightFrustum(pmvMat, primitivesBoundingBox));\n\t\t//frustum = calculatePrimitivesLightFrustum(pmvMat, primitivesBoundingBox);\n\t\t//frustum = calculateCameraLightFrustum(pmvMat, cameraPMV);\n\t\tpmvMat.glOrthof((float)frustum.minX, (float)frustum.maxX, (float)frustum.minY, (float)frustum.maxY, (float)frustum.minZ, (float)frustum.maxZ);\n\t\tpmvMat.glMatrixMode(GL_MODELVIEW);\n\t\t\n\t\tsetPMVMatrix(pmvMat);\n\t}\n\t\n\t/**\n\t * Calculate the frustum for the light projection matrix based on the bounding box of all primitives.\n\t * Transforms the bounding box into lightspace and draws an axis aligned bounding box around it.\n\t * @param lightPMV contains the lights ModelView matrix\n\t * @param primitivesBoundingBox bounding box around all relevant primitives in world coordinates\n\t * @return the optimal frustum for the light\n\t */\n\tprivate AxisAlignedBoundingBoxXYZ calculatePrimitivesLightFrustum(PMVMatrix lightPMV, AxisAlignedBoundingBoxXYZ primitivesBoundingBox) {\n\n\t\tArrayList<VectorXYZ> corners = new ArrayList<VectorXYZ>();\n\t\tfor (VectorXYZ corner : primitivesBoundingBox.corners()) {\n\t\t\tfloat[] result = new float[4];\n\t\t\tFloatUtil.multMatrixVecf(lightPMV.glGetMvMatrixf(), new float[]{(float)corner.x, (float)corner.y, (float)corner.z, 1}, result);\n\t\t\tcorners.add(new VectorXYZ(result[0]/result[3], result[1]/result[3], result[2]/result[3]));\n\t\t}\n\t\tAxisAlignedBoundingBoxXYZ frustum = new AxisAlignedBoundingBoxXYZ(corners);\n\n\t\treturn frustum;\n\t}\n\t\n\t/**\n\t * Calculate the frustum for the light projection matrix based on the frustum of the camera\n\t * @param lightPMV contains the lights ModelView matrix\n\t * @param cameraPMV the cameras PMVMatrix that defines the camera view frustum\n\t * @return the optimal frustum for the light\n\t */\n\tprivate AxisAlignedBoundingBoxXYZ calculateCameraLightFrustum(PMVMatrix lightPMV, PMVMatrix cameraPMV) {\n\t\t/*\n\t\t * calculate transform from screen space bounding box to light space:\n\t\t * inverse projection -> inverse modelview -> modelview of light\n\t\t */\n\t\tFloatBuffer cameraP_inverse = FloatBuffer.allocate(16);\n\t\tFloatBuffer cameraPMV_inverse = FloatBuffer.allocate(16);\n\t\tProjectFloat p = new ProjectFloat();\n\t\tp.gluInvertMatrixf(cameraPMV.glGetPMatrixf(), cameraP_inverse);\n\t\tFloatUtil.multMatrixf(cameraPMV.glGetMviMatrixf(), cameraP_inverse, cameraPMV_inverse);\n\t\tFloatBuffer NDC2light = FloatBuffer.allocate(16);\n\t\tFloatUtil.multMatrixf(lightPMV.glGetMvMatrixf(), cameraPMV_inverse, NDC2light);\n\t\t\n\t\t/*\n\t\t * transform screen space bounding box to light space\n\t\t * and calculate axis aligned bounding box\n\t\t */\n\t\tArrayList<VectorXYZ> corners = new ArrayList<VectorXYZ>();\n\t\tfor (int x = -1; x<=1; x+=2) {\n\t\t\tfor (int y = -1; y<=1; y+=2) {\n\t\t\t\tfor (int z = -1; z<=1; z+=2) {\n\t\t\t\t\tfloat[] NDCcorner = {x, y, z, 1};\n\t\t\t\t\tfloat[] result = new float[4];\n\t\t\t\t\tFloatUtil.multMatrixVecf(NDC2light, NDCcorner, result);\n\t\t\t\t\tcorners.add(new VectorXYZ(result[0]/result[3], result[1]/result[3], -result[2]/result[3]));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tAxisAlignedBoundingBoxXYZ frustum = new AxisAlignedBoundingBoxXYZ(corners).pad(cameraFrustumPadding);\n\t\treturn frustum;\n\t}\n\t\n\t/**\n\t * {@inheritDoc}\n\t * Only primitives that support shadow will get rendered. For opaque objects see {@link #setRenderOpaque(boolean)}\n\t */\n\t@Override\n\tpublic boolean setMaterial(Material material, JOGLTextureManager textureManager) {\n\t\tif (!renderOpaque && material.getTransparency() == Transparency.FALSE) {\n\t\t\treturn false;\n\t\t}\n\t\tif (material.getShadow() == Shadow.FALSE) {\n\t\t\treturn false;\n\t\t}\n\t\treturn super.setMaterial(material, textureManager);\n\t}\n\t\n\t/**\n\t * Sets whether to render opaque objects or not. Useful if the shadow map is only needed for transparent objects.\n\t */\n\tpublic void setRenderOpaque(boolean renderOpaque) {\n\t\tthis.renderOpaque = renderOpaque;\n\t}\n\n\t/**\n\t * Returns the PMVMatrix that was used to render the shadow map.\n\t * Should be called at least after {@link #useShader()}\n\t */\n\tpublic PMVMatrix getPMVMatrix() {\n\t\treturn pmvMat;\n\t}\n\t\n\t/**\n\t * Returns the handle of the texture containing the rendered shadow map.\n\t */\n\tpublic int getShadowMapHandle() {\n\t\treturn depthBufferHandle;\n\t}\n\t\n\t/**\n\t * Prepares rendering of the shadow map. This changes the current framebuffer and viewport.\n\t * {@link #disableShader()} should be called after the rendering is complete to bind the default framebuffer again\n\t * and restore the original viewport.\n\t */\n\t@Override\n\tpublic void useShader() {\n\t\tsuper.useShader();\n\t\tprepareShadowMapGeneration();\n\t}\n\t\n\t/**\n\t * Completes the rendering of the shadow map. The default framebuffer and viewport get restored.\n\t */\n\t@Override\n\tpublic void disableShader() {\n\t\t\n\t\t// bind default framebuffer\n\t\tgl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);\n\t\t\n\t\t// reset viewport\n\t\tgl.glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);\n\t\tsuper.disableShader();\n\t\t\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/ShadowVolumeShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport java.nio.FloatBuffer;\n\nimport javax.media.opengl.GL3;\n\nimport com.jogamp.opengl.math.FloatUtil;\nimport com.jogamp.opengl.util.PMVMatrix;\n\n/**\n * Simple shader to render shadow volumes.\n * @see JOGLRendererVBOShadowVolume\n */\npublic class ShadowVolumeShader extends AbstractPrimitiveShader {\n\t\n\tprivate int modelViewProjectionMatrixID;\n\tprivate int vertexPositionID;\n\t\n\tpublic ShadowVolumeShader(GL3 gl) {\n\t\tsuper(gl, \"/shaders/shadowvolume\");\n\t\t\n\t\t// get indices of named attributes\n\t\tvertexPositionID = gl.glGetAttribLocation(shaderProgram, \"VertexPosition\");\n\t\t\n\t\t// get indices of uniform variables\n\t\tmodelViewProjectionMatrixID = gl.glGetUniformLocation(shaderProgram, \"ModelViewProjectionMatrix\");\n\t\t\n\t\tthis.validateShader();\n\t}\n\t\n\t/**\n\t * Send uniform matrices \"ProjectionMatrix, ModelViewMatrix and ModelViewProjectionMatrix\" to vertex shader\n\t * @param pmvMatrix the PMVMatrix containing all matrices\n\t */\n\tpublic void setPMVMatrix(PMVMatrix pmvMatrix) {\n\t\tFloatBuffer pmvMat = FloatBuffer.allocate(16);\n\t\tFloatUtil.multMatrixf(pmvMatrix.glGetPMatrixf(), pmvMatrix.glGetMvMatrixf(), pmvMat);\n\t\tgl.glUniformMatrix4fv(this.getModelViewProjectionMatrixID(), 1, false, pmvMat);\n\t}\n\t\n\t/**\n\t * Returns the id to use to bind the vertex position attribute.\n\t */\n\tpublic int getVertexPositionID() {\n\t\treturn vertexPositionID;\n\t}\n\t\n\tpublic int getModelViewProjectionMatrixID() {\n\t\treturn modelViewProjectionMatrixID;\n\t}\n\t\n\t@Override\n\tpublic int getVertexNormalID() {\n\t\treturn -1; // unused\n\t}\n\n\t@Override\n\tpublic int getVertexTexCoordID(int i) {\n\t\treturn -1; // unused\n\t}\n\n\t@Override\n\tpublic int getVertexBumpMapCoordID() {\n\t\treturn -1; // unused\n\t}\n\n\t@Override\n\tpublic int getVertexTangentID() {\n\t\treturn -1; // unused\n\t}\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/VBOData.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_ARRAY_BUFFER;\nimport static javax.media.opengl.GL.GL_STATIC_DRAW;\nimport static org.osm2world.core.math.GeometryUtil.triangleNormalListFromTriangleStripOrFan;\nimport static org.osm2world.core.math.GeometryUtil.triangleVertexListFromTriangleFan;\nimport static org.osm2world.core.math.GeometryUtil.triangleVertexListFromTriangleStrip;\n\nimport java.nio.Buffer;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport javax.media.opengl.GL;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.Primitive.Type;\nimport org.osm2world.core.target.common.material.Material;\n\n/**\n * Base class that keeps a VBO id along with associated information.\n */\npublic abstract class VBOData<BufferT extends Buffer> {\n\t\n\t/** material associated with this VBO, determines VBO layout */\n\tprotected Material material;\n\t\n\t/** array with one element containing the VBO id */\n\tprotected final int[] id;\n\t\n\t/** number of vertices in the vbo */\n\tprotected final int vertexCount;\n\t\n\t/** size of each value in the vbo */\n\tprotected final int valueTypeSize;\n\t\n\t/** gl constant for the value type in the vbo */\n\tprotected final int glValueType;\n\n\t/** create a buffer to store the vbo data for upload to graphics memory */\n\tprotected abstract BufferT createBuffer(int numValues);\n\t\n\t/** add a texture coordinate to the vbo buffer */\n\tprotected abstract void put(BufferT buffer, VectorXZ texCoord);\n\t\n\t/** add 3d vertex data to the vbo buffer */\n\tprotected abstract void put(BufferT buffer, VectorXYZ v);\n\t\n\t/** returns the size of each value in the vbo */\n\tprotected abstract int valueTypeSize();\n\t\n\t/** returns the gl constant for the value type in the vbo */\n\tprotected abstract int glValueType();\n\n\tprivate GL gl;\n\tprotected JOGLTextureManager textureManager;\n\t\n\t/**\n\t * Creates a new vertex buffer object, adds all primitives to the buffer and uploads it to graphics memory.\n\t */\n\tpublic VBOData(GL gl, JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives) {\n\t\t\n\t\tthis.gl = gl;\n\t\tthis.textureManager = textureManager;\n\t\tthis.material = material;\n\t\t\n\t\tvalueTypeSize = valueTypeSize();\n\t\tglValueType = glValueType();\n\t\t\n\t\tvertexCount = countVertices(primitives);\n\t\t\n\t\t/* create the buffer */\n\t\t\n\t\tid = new int[1];\n\t\tgl.glGenBuffers(1, id, 0);\n\t\t\n\t\t/* collect the data for the buffer */\n\t\t\n\t\tBufferT valueBuffer = createBuffer(\n\t\t\t\tvertexCount * getValuesPerVertex(material));\n\t\t\t\t\t\n\t\tfor (Primitive primitive : primitives) {\n\t\t\taddPrimitiveToValueBuffer(valueBuffer, primitive);\n\t\t}\n\t\t\n\t\tvalueBuffer.rewind();\n\t\t\n\t\t/* write the data into the buffer */\n\t\t\n\t\tgl.glBindBuffer(GL_ARRAY_BUFFER, id[0]);\n\t\t\n\t\tgl.glBufferData(\n\t\t\t\tGL_ARRAY_BUFFER,\n\t\t\t\tvalueBuffer.capacity() * valueTypeSize,\n\t\t\t\tvalueBuffer,\n\t\t\t\tGL_STATIC_DRAW);\n\t\t\n\t}\n\t\n\t/**\n\t * returns the number of vertices required to represent a collection\n\t * of primitives with individual triangles\n\t */\n\tstatic int countVertices(Collection<Primitive> primitives) {\n\t\t\n\t\tint vertexCount = 0;\n\t\t\n\t\tfor (Primitive primitive : primitives) {\n\t\t\tif (primitive.type == Type.TRIANGLES) {\n\t\t\t\tvertexCount += primitive.vertices.size();\n\t\t\t} else {\n\t\t\t\tvertexCount += 3 * (primitive.vertices.size() - 2);\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn vertexCount;\n\t\t\n\t}\n\t\n\t/**\n\t * put the values for a primitive's vertices into the buffer\n\t */\n\tprotected void addPrimitiveToValueBuffer(BufferT buffer,\n\t\t\tPrimitive primitive) {\n\t\t\t\t\t\n\t\t/*\n\t\t * rearrange the lists of vertices, normals and texture coordinates\n\t\t * to turn triangle strips and triangle fans into separate triangles\n\t\t */\n\n\t\tList<VectorXYZ> primVertices = primitive.vertices;\n\t\tList<VectorXYZ> primNormals = primitive.normals;\n\t\tList<List<VectorXZ>> primTexCoordLists = primitive.texCoordLists;\n\t\t\n\t\tif (primitive.type == Type.TRIANGLE_STRIP) {\n\n\t\t\t// TODO: support smooth interpolation of normals\n\t\t\tprimVertices = triangleVertexListFromTriangleStrip(primVertices);\n\t\t\tprimNormals = triangleNormalListFromTriangleStripOrFan(primNormals);\n\t\t\t\n\t\t\tif (primTexCoordLists != null) {\n\t\t\t\tList<List<VectorXZ>> newPrimTexCoordLists = new ArrayList<List<VectorXZ>>();\n\t\t\t\tfor (List<VectorXZ> primTexCoordList : primTexCoordLists) {\n\t\t\t\t\tnewPrimTexCoordLists.add(triangleVertexListFromTriangleStrip(primTexCoordList));\n\t\t\t\t}\n\t\t\t\tprimTexCoordLists = newPrimTexCoordLists;\n\t\t\t}\n\t\t\t\n\t\t} else if (primitive.type == Type.TRIANGLE_FAN) {\n\n\t\t\t// TODO: support smooth interpolation of normals\n\t\t\tprimVertices = triangleVertexListFromTriangleFan(primVertices);\n\t\t\tprimNormals = triangleNormalListFromTriangleStripOrFan(primNormals);\n\t\t\t\n\t\t\tif (primTexCoordLists != null) {\n\t\t\t\tList<List<VectorXZ>> newPrimTexCoordLists = new ArrayList<List<VectorXZ>>();\n\t\t\t\tfor (List<VectorXZ> primTexCoordList : primTexCoordLists) {\n\t\t\t\t\tnewPrimTexCoordLists.add(triangleVertexListFromTriangleFan(primTexCoordList));\n\t\t\t\t}\n\t\t\t\tprimTexCoordLists = newPrimTexCoordLists;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* put the values into the buffer, in the right order */\n\t\t\n\t\tfor (int i = 0; i < primVertices.size(); i++) {\n\t\t\t\n\t\t\tassert (primTexCoordLists == null\n\t\t\t\t\t&& material.getNumTextureLayers() == 0)\n\t\t\t\t|| (primTexCoordLists != null\n\t\t\t\t\t&& primTexCoordLists.size() == material.getNumTextureLayers())\n\t\t\t\t: \"WorldModules need to provide the correct number of tex coords\";\n\t\t\t\n\t\t\tif (primTexCoordLists == null && material.getNumTextureLayers() > 0) {\n\t\t\t\tSystem.out.println(material);\n\t\t\t}\n\t\t\t\t\n\t\t\tfor (int t = 0; t < material.getNumTextureLayers(); t++) {\n\t\t\t\tVectorXZ textureCoord =\tprimTexCoordLists.get(t).get(i);\n\t\t\t\tput(buffer, textureCoord);\n\t\t\t}\n\t\t\t\n\t\t\tput(buffer, primNormals.get(i));\n\t\t\tput(buffer, primVertices.get(i));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * Bind and render this vertex buffer object.\n\t */\n\tpublic abstract void render();\n\t\n\t/**\n\t * Returns the number of values for each vertex in the vertex buffer layout appropriate for a given material.\n\t */\n\tprotected abstract int getValuesPerVertex(Material material);\n\n\t/**\n\t * Delete the vertex buffer object from graphics memory.\n\t */\n\tpublic void delete() {\n\t\tgl.glDeleteBuffers(id.length, id, 0);\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/VBODataFixedFunction.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_ARRAY_BUFFER;\nimport static javax.media.opengl.GL.GL_TRIANGLES;\nimport static javax.media.opengl.fixedfunc.GLPointerFunc.GL_TEXTURE_COORD_ARRAY;\n\nimport java.nio.Buffer;\nimport java.util.Collection;\n\nimport javax.media.opengl.GL2;\n\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.material.Material;\n\n/**\n * class that keeps a VBO id along with associated information for the fixed function OpenGL pipeline\n */\nabstract class VBODataFixedFunction<BufferT extends Buffer> extends VBOData<BufferT> {\n\t\n\tprotected GL2 gl;\n\t\n\t/**\n\t * @see VBOData#VBOData(javax.media.opengl.GL, JOGLTextureManager, Material, Collection)\n\t */\n\tpublic VBODataFixedFunction(GL2 gl, JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives) {\n\t\tsuper(gl, textureManager, material, primitives);\n\t\tthis.gl = gl;\n\t}\n\t\n\t@Override\n\tpublic void render() {\n\t\t\n\t\tfor (int i = 0; i < JOGLTargetFixedFunction.MAX_TEXTURE_LAYERS; i++) {\n\t\t\t\n\t\t\tgl.glClientActiveTexture(JOGLTargetFixedFunction.getGLTextureConstant(i));\n\t\t\t\n\t\t\tif (i >= material.getNumTextureLayers()) {\n\t\t\t\t\n\t\t\t\tgl.glDisableClientState(GL_TEXTURE_COORD_ARRAY);\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tgl.glEnableClientState(GL_TEXTURE_COORD_ARRAY);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tgl.glClientActiveTexture(JOGLTargetFixedFunction.getGLTextureConstant(0));\n\t\t\n\t\tJOGLTargetFixedFunction.setMaterial(gl, material, textureManager);\n\t\t\n\t\tgl.glBindBuffer(GL_ARRAY_BUFFER, id[0]);\n\t\t\n\t\tsetPointerLayout();\n\t\t\n\t\tgl.glDrawArrays(GL_TRIANGLES, 0, vertexCount);\n\t\t\n\t}\n\n\tprivate void setPointerLayout() {\n\t\t\n\t\tint stride = valueTypeSize * JOGLRendererVBO.getValuesPerVertex(material);\n\t\t\n\t\tint offset = 0;\n\t\t\n\t\tfor (int i = 0; i < material.getNumTextureLayers(); i++) {\n\t\t\t\n\t\t\tgl.glClientActiveTexture(JOGLTargetFixedFunction.getGLTextureConstant(i));\n\t\t\tgl.glEnableClientState(GL_TEXTURE_COORD_ARRAY);\n\t\t\t\n\t\t\tgl.glActiveTexture(JOGLTargetFixedFunction.getGLTextureConstant(i));\n\t\t\tgl.glTexCoordPointer(2, glValueType, stride, offset);\n\t\t\t\n\t\t\toffset += 2 * valueTypeSize;\n\t\t\t\n\t\t}\n\t\t\n\t\tgl.glVertexPointer(3, glValueType, stride, offset + valueTypeSize() * 3);\n\t\tgl.glNormalPointer(glValueType, stride, offset);\n\t\t\n\t}\n\t\n\t@Override\n\tprotected int getValuesPerVertex(Material material) {\n\t\treturn JOGLRendererVBO.getValuesPerVertex(material);\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/VBODataNonAreaShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_ARRAY_BUFFER;\nimport static javax.media.opengl.GL.GL_STATIC_DRAW;\n\nimport java.awt.Color;\nimport java.nio.Buffer;\n\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.math.VectorXYZ;\n\n/**\n * class that keeps a VBO id along with associated information for {@link NonAreaPrimitive} objects.\n */\nabstract class VBODataNonAreaShader<BufferT extends Buffer> {\n\t\n\t/** array with one element containing the VBO id */\n\tprotected final int[] id;\n\t\n\t/** number of vertices in the vbo */\n\tprotected final int vertexCount;\n\t\n\t/** size of each value in the vbo */\n\tprotected final int valueTypeSize;\n\t\n\t/** gl constant for the value type in the vbo */\n\tprotected final int glValueType;\n\n\t/** create a buffer to store the vbo data for upload to graphics memory */\n\tprotected abstract BufferT createBuffer(int numValues);\n\t\n\t/** add vertex color to the vbo buffer */\n\tprotected abstract void put(BufferT buffer, Color color);\n\t\n\t/** add 3d vertex data to the vbo buffer */\n\tprotected abstract void put(BufferT buffer, VectorXYZ v);\n\t\n\t/** returns the size of each value in the vbo */\n\tprotected abstract int valueTypeSize();\n\t\n\t/** returns the gl constant for the value type in the vbo */\n\tprotected abstract int glValueType();\n\t\n\tprotected GL3 gl;\n\tprivate NonAreaShader shader;\n\t\n\t/** the OpenGL constant for the type of the {@link NonAreaPrimitive} */\n\tprivate int primitiveType;\n\t\n\t/** width of the {@link NonAreaPrimitive} */\n\tprivate int width;\n\t\n\t/**\n\t * Creates a new vertex buffer object, adds all primitives to the buffer and uploads it to graphics memory.\n\t */\n\tpublic VBODataNonAreaShader(GL3 gl, NonAreaShader shader, NonAreaPrimitive nonAreaPrimitive) {\n\t\tthis.gl = gl;\n\t\tthis.shader = shader;\n\t\tprimitiveType= AbstractJOGLTarget.getGLConstant(nonAreaPrimitive.type);\n\t\twidth = nonAreaPrimitive.width;\n\t\t\n\t\tvalueTypeSize = valueTypeSize();\n\t\tglValueType = glValueType();\n\t\t\n\t\tvertexCount = nonAreaPrimitive.vs.size();\n\t\t\n\t\t/* create the buffer */\n\t\t\n\t\tid = new int[1];\n\t\tgl.glGenBuffers(1, id, 0);\n\t\t\n\t\t/* collect the data for the buffer */\n\t\t\n\t\tBufferT valueBuffer = createBuffer(\n\t\t\t\tvertexCount * getValuesPerVertex());\n\t\t\t\t\t\n\t\t/* put the values into the buffer, in the right order */\n\t\t\n\t\tfor (int i = 0; i < nonAreaPrimitive.vs.size(); i++) {\n\t\t\t\n\t\t\tput(valueBuffer, nonAreaPrimitive.vs.get(i));\n\t\t\tput(valueBuffer, nonAreaPrimitive.color);\n\t\t\t\n\t\t}\n\t\t\n\t\tvalueBuffer.rewind();\n\t\t\n\t\t/* write the data into the buffer */\n\t\t\n\t\tgl.glBindBuffer(GL_ARRAY_BUFFER, id[0]);\n\t\t\n\t\tgl.glBufferData(\n\t\t\t\tGL_ARRAY_BUFFER,\n\t\t\t\tvalueBuffer.capacity() * valueTypeSize,\n\t\t\t\tvalueBuffer,\n\t\t\t\tGL_STATIC_DRAW);\n\t\t\n\t}\n\t\n\t/**\n\t * Bind and render this vertex buffer object.\n\t */\n\tpublic void render() {\n\t\tgl.glBindBuffer(GL_ARRAY_BUFFER, id[0]);\n\n\t\tsetPointerLayout();\n\n\t\tgl.glLineWidth(width);\n\t\t\n\t\tgl.glDrawArrays(primitiveType, 0, vertexCount);\n\t}\n\t\n\tprivate void setPointerLayout() {\n\t\t\n\t\tint stride = valueTypeSize * getValuesPerVertex();\n\t\t\n\t\tint offset = 0;\n\t\t\n\t\tgl.glVertexAttribPointer(shader.getVertexPositionID(), 3, glValueType(), false, stride, offset);\n\t\tgl.glVertexAttribPointer(shader.getVertexColorID(), 4, glValueType(), false, stride, offset + valueTypeSize() * 3);\n\t\t\n\t}\n\t\n\t/**\n\t * Returns the number of values for each vertex in the vertex buffer layout.\n\t */\n\tprivate int getValuesPerVertex() {\n\t\t\n\t\t// 3 for vertex position, 4 for vertex color\n\t\treturn 7;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/VBODataShader.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_ARRAY_BUFFER;\nimport static javax.media.opengl.GL.GL_TRIANGLES;\nimport static org.osm2world.core.math.GeometryUtil.calculateTangentVectorsForTexLayer;\nimport static org.osm2world.core.math.GeometryUtil.triangleNormalListFromTriangleStripOrFan;\nimport static org.osm2world.core.math.GeometryUtil.triangleVertexListFromTriangleFan;\nimport static org.osm2world.core.math.GeometryUtil.triangleVertexListFromTriangleStrip;\n\nimport java.nio.Buffer;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXYZW;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.Primitive.Type;\nimport org.osm2world.core.target.common.material.Material;\n\n/**\n * class that keeps a VBO id along with associated information for the shader based OpenGL pipeline\n */\nabstract class VBODataShader<BufferT extends Buffer> extends VBOData<BufferT> {\n\t\n\tprotected GL3 gl;\n\tprotected AbstractPrimitiveShader shader;\n\t\n\t/**\n\t * @see VBOData#VBOData(javax.media.opengl.GL, JOGLTextureManager, Material, Collection)\n\t */\n\tpublic VBODataShader(GL3 gl, JOGLTextureManager textureManager, Material material, Collection<Primitive> primitives) {\n\t\tsuper(gl, textureManager, material, primitives);\n\t\tthis.gl = gl;\n\t}\n\t\n\t/**\n\t * Set the shader this VBO uses when rendering (e.g. calls\n\t * {@link AbstractPrimitiveShader#setMaterial(Material, JOGLTextureManager)}.\n\t */\n\tpublic void setShader(AbstractPrimitiveShader shader) {\n\t\tthis.shader = shader;\n\t}\n\t\n\t@Override\n\tprotected void addPrimitiveToValueBuffer(BufferT buffer,\n\t\t\tPrimitive primitive) {\n\t\t\t\t\t\n\t\t/*\n\t\t * rearrange the lists of vertices, normals and texture coordinates\n\t\t * to turn triangle strips and triangle fans into separate triangles\n\t\t */\n\n\t\tList<VectorXYZ> primVertices = primitive.vertices;\n\t\tList<VectorXYZ> primNormals = primitive.normals;\n\t\tList<List<VectorXZ>> primTexCoordLists = primitive.texCoordLists;\n\t\t\n\t\tif (primitive.type == Type.TRIANGLE_STRIP) {\n\t\t\t\n\t\t\t// TODO: support smooth interpolation of normals\n\t\t\tprimVertices = triangleVertexListFromTriangleStrip(primVertices);\n\t\t\tprimNormals = triangleNormalListFromTriangleStripOrFan(primNormals);\n\t\t\t\n\t\t\tif (primTexCoordLists != null) {\n\t\t\t\tList<List<VectorXZ>> newPrimTexCoordLists = new ArrayList<List<VectorXZ>>();\n\t\t\t\tfor (List<VectorXZ> primTexCoordList : primTexCoordLists) {\n\t\t\t\t\tnewPrimTexCoordLists.add(triangleVertexListFromTriangleStrip(primTexCoordList));\n\t\t\t\t}\n\t\t\t\tprimTexCoordLists = newPrimTexCoordLists;\n\t\t\t}\n\t\t\t\n\t\t} else if (primitive.type == Type.TRIANGLE_FAN) {\n\n\t\t\t// TODO: support smooth interpolation of normals\n\t\t\tprimVertices = triangleVertexListFromTriangleFan(primVertices);\n\t\t\tprimNormals = triangleNormalListFromTriangleStripOrFan(primNormals);\n\t\t\t\n\t\t\tif (primTexCoordLists != null) {\n\t\t\t\tList<List<VectorXZ>> newPrimTexCoordLists = new ArrayList<List<VectorXZ>>();\n\t\t\t\tfor (List<VectorXZ> primTexCoordList : primTexCoordLists) {\n\t\t\t\t\tnewPrimTexCoordLists.add(triangleVertexListFromTriangleFan(primTexCoordList));\n\t\t\t\t}\n\t\t\t\tprimTexCoordLists = newPrimTexCoordLists;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tList<VectorXYZW> primTangents = null;\n\t\tif (material.hasBumpMap()) {\n\t\t\tprimTangents = calculateTangentVectorsForTexLayer(primVertices, primNormals, primTexCoordLists.get(material.getBumpMapInd()));\n\t\t}\n\t\t\t\n\t\t/* put the values into the buffer, in the right order */\n\t\t\n\t\tfor (int i = 0; i < primVertices.size(); i++) {\n\t\t\t\n\t\t\tint count = 0;\n\t\t\tassert (primTexCoordLists == null\n\t\t\t\t\t&& material.getNumTextureLayers() == 0)\n\t\t\t\t|| (primTexCoordLists != null\n\t\t\t\t\t&& primTexCoordLists.size() == material.getNumTextureLayers())\n\t\t\t\t: \"WorldModules need to provide the correct number of tex coords\";\n\t\t\t\n\t\t\tif (primTexCoordLists == null && material.getNumTextureLayers() > 0) {\n\t\t\t\tSystem.out.println(material);\n\t\t\t}\n\t\t\t\t\n\t\t\tfor (int t = 0; t < material.getNumTextureLayers(); t++) {\n\t\t\t\tif (!material.hasBumpMap() || t != material.getBumpMapInd()) {\n\t\t\t\t\tVectorXZ textureCoord =\tprimTexCoordLists.get(t).get(i);\n\t\t\t\t\tput(buffer, textureCoord);\n\t\t\t\t\t//System.out.println(\"put tex coord\");\n\t\t\t\t\tcount += 2;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tput(buffer, primNormals.get(i));\n\t\t\tcount += 3;\n\t\t\tif (material.hasBumpMap()) {\n\t\t\t\tput(buffer, primTangents.get(i));\n\t\t\t\tcount += 4;\n\t\t\t\tput(buffer, primTexCoordLists.get(material.getBumpMapInd()).get(i));\n\t\t\t\tcount += 2;\n\t\t\t}\n\t\t\tput(buffer, primVertices.get(i));\n\t\t\tcount += 3;\n\t\t\t\n\t\t\tif (count != JOGLRendererVBO.getValuesPerVertex(material)) {\n\t\t\t\tthrow new RuntimeException(\"put: \"+count +\" values:\" + JOGLRendererVBO.getValuesPerVertex(material));\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void render() {\n\t\tgl.glBindBuffer(GL_ARRAY_BUFFER, id[0]);\n\n\t\tsetPointerLayout();\n\t\tif (shader.setMaterial(material, textureManager))\n\t\t\tgl.glDrawArrays(GL_TRIANGLES, 0, vertexCount);\n\t\t\n\t\tfor (int i=1; i<DefaultShader.MAX_TEXTURE_LAYERS; i++) {\n\t\t\tshader.glDisableVertexAttribArray(shader.getVertexTexCoordID(i));\n\t\t}\n\t\tshader.glDisableVertexAttribArray(shader.getVertexBumpMapCoordID());\n\t\tshader.glDisableVertexAttribArray(shader.getVertexTangentID());\n\t}\n\t\n\tprivate void setPointerLayout() {\n\t\t\n\t\tint stride = valueTypeSize * JOGLRendererVBO.getValuesPerVertex(material);\n\t\t\n\t\tint offset = 0;\n\t\t\n\t\tfor (int i = 0; i < material.getNumTextureLayers(); i++) {\n\n\t\t\tif (!material.hasBumpMap() || i != material.getBumpMapInd()) {\n\t\t\t\tshader.glEnableVertexAttribArray(shader.getVertexTexCoordID(i));\n\t\t\t\tshader.glVertexAttribPointer(shader.getVertexTexCoordID(i), 2, glValueType(), false, stride, offset);\n\t\t\t\toffset += 2 * valueTypeSize;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tshader.glVertexAttribPointer(shader.getVertexNormalID(), 3, glValueType(), false, stride, offset);\n\t\toffset += valueTypeSize() * 3;\n\t\t\n\t\tif (material.hasBumpMap()) {\n\t\t\tshader.glEnableVertexAttribArray(shader.getVertexTangentID());\n\t\t\tshader.glVertexAttribPointer(shader.getVertexTangentID(), 4, glValueType(), false, stride, offset);\n\t\t\toffset += valueTypeSize() * 4;\n\t\t\tshader.glEnableVertexAttribArray(shader.getVertexBumpMapCoordID());\n\t\t\tshader.glVertexAttribPointer(shader.getVertexBumpMapCoordID(), 2, glValueType(), false, stride, offset);\n\t\t\toffset += valueTypeSize() * 2;\n\t\t}\n\t\tif (offset != stride - 3*valueTypeSize()) {\n\t\t\tthrow new RuntimeException(\"offset: \"+offset + \" stride:\"+stride +\" valueTypeSize:\"+valueTypeSize());\n\t\t}\n\t\tshader.glVertexAttribPointer(shader.getVertexPositionID(), 3, glValueType(), false, stride, offset);\n\t}\n\t\n\t@Override\n\tprotected int getValuesPerVertex(Material material) {\n\t\treturn JOGLRendererVBO.getValuesPerVertex(material);\n\t}\n\t\n\t/** add 4d vertex data to the vbo buffer */\n\tprotected abstract void put(BufferT buffer, VectorXYZW t);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/jogl/VBODataShadowVolume.java",
    "content": "package org.osm2world.core.target.jogl;\n\nimport static javax.media.opengl.GL.GL_ARRAY_BUFFER;\nimport static javax.media.opengl.GL.GL_STATIC_DRAW;\nimport static javax.media.opengl.GL.GL_TRIANGLES;\nimport static org.osm2world.core.math.GeometryUtil.triangleVertexListFromTriangleFan;\nimport static org.osm2world.core.math.GeometryUtil.triangleVertexListFromTriangleStrip;\n\nimport java.nio.Buffer;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport javax.media.opengl.GL3;\n\nimport org.osm2world.core.math.GeometryUtil;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXYZW;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.Primitive.Type;\n\n/**\n * class that keeps a VBO id along with associated information for shadow volumes\n */\nabstract class VBODataShadowVolume<BufferT extends Buffer> {\n\t\n\tprotected GL3 gl;\n\tprotected AbstractPrimitiveShader shader;\n\t\n\t/** position of the light source casting the shadow volumes */\n\tprotected VectorXYZW lightPos;\n\t\n\t/** array with one element containing the VBO id */\n\tprotected final int[] id;\n\t\n\t/** number of vertices in the vbo */\n\tprotected final int vertexCount;\n\t\n\t/** size of each value in the vbo */\n\tprotected final int valueTypeSize;\n\t\n\t/** gl constant for the value type in the vbo */\n\tprotected final int glValueType;\n\n\t/** create a buffer to store the vbo data for upload to graphics memory */\n\tprotected abstract BufferT createBuffer(int numValues);\n\t\n\t/** returns the size of each value in the vbo */\n\tprotected abstract int valueTypeSize();\n\t\n\t/** returns the gl constant for the value type in the vbo */\n\tprotected abstract int glValueType();\n\t\n\t/** add 4d shadow volume vertex data to the vbo buffer */\n\tprotected abstract void put(BufferT buffer, VectorXYZW sv);\n\n\t/**\n\t * Creates a new vertex buffer object, calculates the shadow volumes for all\n\t * primitives and adds them to the buffer and uploads it to graphics memory.\n\t */\n\tpublic VBODataShadowVolume(GL3 gl, Collection<Primitive> primitives, VectorXYZW lightPos) {\n\t\t\n\t\tthis.gl = gl;\n\t\tthis.lightPos = lightPos;\n\t\t\n\t\tvalueTypeSize = valueTypeSize();\n\t\tglValueType = glValueType();\n\t\t\n\t\t//vertexCount = VBOData.countVertices(primitives)*8;\n\t\t\n\t\t/* create the buffer */\n\t\t\n\t\tid = new int[1];\n\t\tgl.glGenBuffers(1, id, 0);\n\t\t\n\t\t/* collect the data for the buffer */\n\t\t\n\t\tList<VectorXYZW> shadowVolumeVertices = new ArrayList<VectorXYZW>();\n\t\tfor (Primitive primitive : primitives) {\n\t\t\tshadowVolumeVertices.addAll(getPrimitivesShadowVolumes(primitive));\n\t\t}\n\t\tvertexCount = shadowVolumeVertices.size();\n\t\t\n\t\tBufferT valueBuffer = createBuffer(\n\t\t\t\tvertexCount * getValuesPerVertex());\n\t\t\t\t\t\n\t\taddVerticesToValueBuffer(valueBuffer, shadowVolumeVertices);\n\t\t\n\t\tvalueBuffer.rewind();\n\t\t\n\t\t/* write the data into the buffer */\n\t\t\n\t\tgl.glBindBuffer(GL_ARRAY_BUFFER, id[0]);\n\t\t\n\t\tgl.glBufferData(\n\t\t\t\tGL_ARRAY_BUFFER,\n\t\t\t\tvalueBuffer.capacity() * valueTypeSize,\n\t\t\t\tvalueBuffer,\n\t\t\t\tGL_STATIC_DRAW);\n\t\t\n\t}\n\t\n\t/**\n\t * Set the shader this VBO uses when rendering the shadow volumes.\n\t */\n\tpublic void setShader(AbstractPrimitiveShader shader) {\n\t\tthis.shader = shader;\n\t}\n\t\n\t/**\n\t * Put the values of a shadow volume into the buffer.\n\t */\n\tprotected void addVerticesToValueBuffer(BufferT buffer,\n\t\t\tList<VectorXYZW> shadowVolumeVertices) {\n\t\t\n\t\t/* put the values into the buffer, in the right order */\n\t\tfor (VectorXYZW v : shadowVolumeVertices) {\n\t\t\tput(buffer, v);\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * Calculate the shadow volume for a primitive.\n\t */\n\tprotected List<VectorXYZW> getPrimitivesShadowVolumes(Primitive primitive) {\n\t\t\t\t\t\n\t\t/*\n\t\t * rearrange the lists of vertices, normals and texture coordinates\n\t\t * to turn triangle strips and triangle fans into separate triangles\n\t\t */\n\n\t\tList<VectorXYZ> primVertices = primitive.vertices;\n\t\t\n\t\tif (primitive.type == Type.TRIANGLE_STRIP) {\n\t\t\t\n\t\t\tprimVertices = triangleVertexListFromTriangleStrip(primVertices);\n\t\t\t\n\t\t} else if (primitive.type == Type.TRIANGLE_FAN) {\n\t\t\t\n\t\t\tprimVertices = triangleVertexListFromTriangleFan(primVertices);\n\t\t\t\n\t\t}\n\t\t\n\t\t/*\n\t\t *  NOTE: performance could be improved a lot if shadow volume geometry would be minimized.\n\t\t *  Suggestions:\n\t\t *   * calculate volumes only for silhouette\n\t\t *   * skip back facing triangles (from light perspective). All shadow casting objectes have to be closed then.\n\t\t *   * use low poly model for shadow volume generation (only useful for high poly objects)\n\t\t */\n\t\tList<VectorXYZW> shadowVolumeVertices = GeometryUtil.calculateShadowVolumesPerTriangle(primVertices, lightPos);\n\t\treturn shadowVolumeVertices;\n\t\t\n\t}\n\t\n\t/**\n\t * Bind and render this vertex buffer object.\n\t */\n\tpublic void render() {\n\t\tgl.glBindBuffer(GL_ARRAY_BUFFER, id[0]);\n\n\t\tsetPointerLayout();\n\t\t\n\t\tgl.glDrawArrays(GL_TRIANGLES, 0, vertexCount);\n\t}\n\t\n\tprivate void setPointerLayout() {\n\t\t\n\t\tint stride = valueTypeSize * getValuesPerVertex();\n\t\t\n\t\tint offset = 0;\n\t\t\n\t\tshader.glVertexAttribPointer(shader.getVertexPositionID(), 4, glValueType(), false, stride, offset);\n\t}\n\t\n\t/**\n\t * Returns the number of values for each vertex in the vertex buffer layout.\n\t */\n\tprotected int getValuesPerVertex() {\n\t\treturn 4;\n\t}\n\t\n\t/**\n\t * Delete the vertex buffer object from graphics memory.\n\t */\n\tpublic void delete() {\n\t\tgl.glDeleteBuffers(id.length, id, 0);\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/obj/ObjTarget.java",
    "content": "package org.osm2world.core.target.obj;\n\nimport static java.awt.Color.WHITE;\nimport static java.lang.Math.max;\nimport static java.util.Collections.nCopies;\nimport static org.osm2world.core.target.common.material.Material.multiplyColor;\n\nimport java.awt.Color;\nimport java.io.PrintStream;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXYZWithNormals;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.data.OSMElement;\nimport org.osm2world.core.target.common.FaceTarget;\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.WorldObject;\n\npublic class ObjTarget extends FaceTarget<RenderableToObj> {\n\n\tprivate final PrintStream objStream;\n\tprivate final PrintStream mtlStream;\n\t\n\tprivate final Map<VectorXYZ, Integer> vertexIndexMap = new HashMap<VectorXYZ, Integer>();\n\tprivate final Map<VectorXYZ, Integer> normalsIndexMap = new HashMap<VectorXYZ, Integer>();\n\tprivate final Map<VectorXZ, Integer> texCoordsIndexMap = new HashMap<VectorXZ, Integer>();\n\tprivate final Map<Material, String> materialMap = new HashMap<Material, String>();\n\t\n\tprivate Class<? extends WorldObject> currentWOGroup = null;\n\tprivate int anonymousWOCounter = 0;\n\t\n\tprivate Material currentMaterial = null;\n\tprivate int currentMaterialLayer = 0;\n\tprivate static int anonymousMaterialCounter = 0;\n\t\n\t// this is approximatly one millimeter\n\tprivate static final double SMALL_OFFSET = 1e-3;\n\t\n\tpublic ObjTarget(PrintStream objStream, PrintStream mtlStream) {\n\t\t\n\t\tthis.objStream = objStream;\n\t\tthis.mtlStream = mtlStream;\n\t\t\t\t\n\t}\n\t\n\t@Override\n\tpublic Class<RenderableToObj> getRenderableType() {\n\t\treturn RenderableToObj.class;\n\t}\n\t\n\t@Override\n\tpublic void render(RenderableToObj renderable) {\n\t\trenderable.renderTo(this);\n\t}\n\t\n\t@Override\n\tpublic boolean reconstructFaces() {\n\t\treturn config != null && config.getBoolean(\"reconstructFaces\", false);\n\t}\n\n\t@Override\n\tpublic void beginObject(WorldObject object) {\n\t\t\n\t\tif (object == null) {\n\t\t\t\n\t\t\tcurrentWOGroup = null;\n\t\t\tobjStream.println(\"g null\");\n\t\t\tobjStream.println(\"o null\");\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\t/* maybe start a group depending on the object's class */\n\t\t\t\n\t\t\tif (!object.getClass().equals(currentWOGroup)) {\n\t\t\t\tcurrentWOGroup = object.getClass();\n\t\t\t\tobjStream.println(\"g \" + currentWOGroup.getSimpleName());\n\t\t\t}\n\t\t\t\n\t\t\t/* start an object with the object's class\n\t\t\t * and the underlying OSM element's name/ref tags */\n\t\t\t\n\t\t\tString roadSuffix = null;\n\t\t\t\n\t\t\tMapElement element = object.getPrimaryMapElement();\n\t\t\tOSMElement osmElement;\n\t\t\tif (element instanceof MapNode) {\n\t\t\t\tosmElement = ((MapNode) element).getOsmNode();\n\t\t\t} else if (element instanceof MapWaySegment) {\n\t\t\t\tosmElement = ((MapWaySegment) element).getOsmWay();\n\t\t\t} else if (element instanceof MapArea) {\n\t\t\t\tosmElement = ((MapArea) element).getOsmObject();\n\t\t\t} else {\n\t\t\t\tosmElement = null;\n\t\t\t}\n\n\t\t\tif (element instanceof MapNode) {\n\t\t\t\tList<MapWaySegment> connectedWaySegments = ((MapNode) element).getConnectedWaySegments();\n\t\t\t\tint pedestrians = 0;\n\t\t\t\tfor (MapWaySegment mapWaySegment : connectedWaySegments) {\n\t\t\t\t\tpedestrians += isPath(mapWaySegment.getTags()) ? 1 : 0;\n\t\t\t\t}\n\t\t\t\tif (pedestrians >= (connectedWaySegments.size()+1) / 2) {\n\t\t\t\t\troadSuffix = \"::pedestrian\";\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (roadSuffix == null) {\n\t\t\t\troadSuffix = isPath(osmElement.tags) ? \"::pedestrian\" : \"\";\n\t\t\t}\n\t\t\t\n\t\t\tif (osmElement != null && osmElement.tags.containsKey(\"name\")) {\n\t\t\t\tobjStream.println(\"o \" + object.getClass().getSimpleName() + \" \" + osmElement.tags.getValue(\"name\") + roadSuffix);\n\t\t\t} else if (osmElement != null && osmElement.tags.containsKey(\"ref\")) {\n\t\t\t\tobjStream.println(\"o \" + object.getClass().getSimpleName() + \" \" + osmElement.tags.getValue(\"ref\") + roadSuffix);\n\t\t\t} else {\n\t\t\t\tobjStream.println(\"o \" + object.getClass().getSimpleName() + anonymousWOCounter ++ + roadSuffix);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\n\tprivate static boolean isPath(TagGroup tags) {\n\t\tString highwayValue = tags.getValue(\"highway\");\n\t\tif (\"path\".equals(highwayValue)\n\t\t\t|| \"footway\".equals(highwayValue)\n\t\t\t|| \"cycleway\".equals(highwayValue)\n\t\t\t|| \"service\".equals(highwayValue)\n\t\t\t|| \"bridleway\".equals(highwayValue)\n\t\t\t|| \"living_street\".equals(highwayValue)\n\t\t\t|| \"pedestrian\".equals(highwayValue)\n\t\t\t|| \"track\".equals(highwayValue)\n\t\t\t|| \"steps\".equals(highwayValue)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (tags.containsKey(\"footway\")\n\t\t\t\t|| tags.contains(\"tourism\", \"attraction\")\n\t\t\t\t|| tags.contains(\"man_made\", \"pier\")\n\t\t\t\t|| tags.contains(\"man_made\", \"breakwater\")) {\n\t\t\treturn true;\n\t\t}\n\t\tString footValue = tags.getValue(\"foot\");\n\t\tif (\"yes\".equals(footValue)\n\t\t\t|| \"designated\".equals(footValue)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\t@Override\n\tpublic void drawFace(Material material, List<VectorXYZ> vs,\n\t\t\tList<VectorXYZ> normals, List<List<VectorXZ>> texCoordLists) {\n\n\t\tint[] normalIndices = null;\n\t\tif (normals != null) {\n\t\t\tnormalIndices = normalsToIndices(normals);\n\t\t}\n\t\t\n\t\tVectorXYZ faceNormal = new TriangleXYZ(vs.get(0), vs.get(1), vs.get(2)).getNormal();\n\t\t\n\t\tfor (int layer = 0; layer < max(1, material.getNumTextureLayers()); layer++) {\n\t\t\t\n\t\t\tuseMaterial(material, layer);\n\t\t\n\t\t\tint[] texCoordIndices = null;\n\t\t\tif (texCoordLists != null && !texCoordLists.isEmpty()) {\n\t\t\t\ttexCoordIndices = texCoordsToIndices(texCoordLists.get(layer));\n\t\t\t}\n\t\t\t\n\t\t\twriteFace(verticesToIndices((layer == 0)? vs : offsetVertices(vs, nCopies(vs.size(), faceNormal), layer * SMALL_OFFSET)),\n\t\t\t\t\tnormalIndices, texCoordIndices);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void drawTrianglesWithNormals(Material material,\n\t\t\tCollection<? extends TriangleXYZWithNormals> triangles,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tfor (int layer = 0; layer < max(1, material.getNumTextureLayers()); layer++) {\n\t\t\t\n\t\t\tuseMaterial(material, layer);\n\n\t\t\tint triangleNumber = 0;\n\t\t\tfor (TriangleXYZWithNormals t : triangles) {\n\t\t\t\n\t\t\t\tint[] texCoordIndices = null;\n\t\t\t\tif (texCoordLists != null && !texCoordLists.isEmpty()) {\n\t\t\t\t\tList<VectorXZ> texCoords = texCoordLists.get(layer);\n\t\t\t\t\ttexCoordIndices = texCoordsToIndices(\n\t\t\t\t\t\t\ttexCoords.subList(3*triangleNumber, 3*triangleNumber + 3));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\twriteFace(verticesToIndices((layer == 0)? t.getVertices() : offsetVertices(t.getVertices(), t.getNormals(), layer * SMALL_OFFSET)),\n\t\t\t\t\t\tnormalsToIndices(t.getNormals()), texCoordIndices);\n\t\t\t\n\t\t\t\ttriangleNumber ++;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tprivate void useMaterial(Material material, int layer) {\n\t\tif (!material.equals(currentMaterial) || (layer != currentMaterialLayer)) {\n\t\t\t\n\t\t\tString name = materialMap.get(material);\n\t\t\tif (name == null) {\n\t\t\t\tname = Materials.getUniqueName(material);\n\t\t\t\tif (name == null) {\n\t\t\t\t\tname = \"MAT_\" + anonymousMaterialCounter;\n\t\t\t\t\tanonymousMaterialCounter += 1;\n\t\t\t\t}\n\t\t\t\tmaterialMap.put(material, name);\n\t\t\t\twriteMaterial(material, name);\n\t\t\t}\n\t\t\t\n\t\t\tobjStream.println(\"usemtl \" + name + \"_\" + layer);\n\t\t\t\n\t\t\tcurrentMaterial = material;\n\t\t\tcurrentMaterialLayer = layer;\n\t\t}\n\t}\n\t\n\tprivate List<? extends VectorXYZ> offsetVertices(List<? extends VectorXYZ> vs, List<VectorXYZ> directions, double offset) {\n\t\t\n\t\tList<VectorXYZ> result = new ArrayList<VectorXYZ>(vs.size());\n\t\t\n\t\tfor (int i = 0; i < vs.size(); i++) {\n\t\t\tresult.add(vs.get(i).add(directions.get(i).mult(offset)));\n\t\t}\n\t\t\n\t\treturn result;\n\t}\n\t\n\tprivate int[] verticesToIndices(List<? extends VectorXYZ> vs) {\n\t\treturn vectorsToIndices(vertexIndexMap, \"v \", vs);\n\t}\n\n\tprivate int[] normalsToIndices(List<? extends VectorXYZ> normals) {\n\t\treturn vectorsToIndices(normalsIndexMap, \"vn \", normals);\n\t}\n\n\tprivate int[] texCoordsToIndices(List<VectorXZ> texCoords) {\n\t\treturn vectorsToIndices(texCoordsIndexMap, \"vt \", texCoords);\n\t}\n\t\n\tprivate <V> int[] vectorsToIndices(Map<V, Integer> indexMap,\n\t\t\tString objLineStart, List<? extends V> vectors) {\n\t\t\n\t\tint[] indices = new int[vectors.size()];\n\t\t\n\t\tfor (int i=0; i<vectors.size(); i++) {\n\t\t\tfinal V v = vectors.get(i);\n\t\t\tInteger index = indexMap.get(v);\n\t\t\tif (index == null) {\n\t\t\t\tindex = indexMap.size();\n\t\t\t\tobjStream.println(objLineStart + \" \" + formatVector(v));\n\t\t\t\tindexMap.put(v, index);\n\t\t\t}\n\t\t\tindices[i] = index;\n\t\t}\n\t\t\n\t\treturn indices;\n\t\t\n\t}\n\t\n\tprivate String formatVector(Object v) {\n\t\t\n\t\tif (v instanceof VectorXYZ) {\n\t\t\tVectorXYZ vXYZ = (VectorXYZ)v;\n\t\t\treturn vXYZ.x + \" \" + vXYZ.y + \" \" + (-vXYZ.z);\n\t\t} else {\n\t\t\tVectorXZ vXZ = (VectorXZ)v;\n\t\t\treturn vXZ.x + \" \" + vXZ.z;\n\t\t}\n\t\t\n\t}\n\n\tprivate void writeFace(int[] vertexIndices, int[] normalIndices,\n\t\t\tint[] texCoordIndices) {\n\n\t\tassert normalIndices == null\n\t\t\t\t|| vertexIndices.length == normalIndices.length;\n\n\t\tobjStream.print(\"f\");\n\n\t\tfor (int i = 0; i < vertexIndices.length; i++) {\n\n\t\t\tobjStream.print(\" \" + (vertexIndices[i]+1));\n\n\t\t\tif (texCoordIndices != null && normalIndices == null) {\n\t\t\t\tobjStream.print(\"/\" + (texCoordIndices[i]+1));\n\t\t\t} else if (texCoordIndices == null && normalIndices != null) {\n\t\t\t\tobjStream.print(\"//\" + (normalIndices[i]+1));\n\t\t\t} else if (texCoordIndices != null && normalIndices != null) {\n\t\t\t\tobjStream.print(\"/\" + (texCoordIndices[i]+1)\n\t\t\t\t\t\t+ \"/\" + (normalIndices[i]+1));\n\t\t\t}\n\n\t\t}\n\n\t\tobjStream.println();\n\t}\n\t\n\tprivate void writeMaterial(Material material, String name) {\n\n\t\tfor (int i = 0; i < max(1, material.getNumTextureLayers()); i++) {\n\t\t\t\n\t\t\tTextureData textureData = null;\n\t\t\tif (material.getNumTextureLayers() > 0) {\n\t\t\t\ttextureData = material.getTextureDataList().get(i);\n\t\t\t}\n\t\t\n\t\t\tmtlStream.println(\"newmtl \" + name + \"_\" + i);\n\t\t\t\n\t\t\tif (textureData == null || textureData.colorable) {\n\t\t\t\twriteColorLine(\"Ka\", material.ambientColor());\n\t\t\t\twriteColorLine(\"Kd\", material.diffuseColor());\n\t\t\t\t//Ks\n\t\t\t\t//Ns\n\t\t\t} else {\n\t\t\t\twriteColorLine(\"Ka\", multiplyColor(WHITE, material.getAmbientFactor()));\n\t\t\t\twriteColorLine(\"Kd\", multiplyColor(WHITE, 1 - material.getAmbientFactor()));\n\t\t\t\t//Ks\n\t\t\t\t//Ns\n\t\t\t}\n\t\t\n\t\t\tif (textureData != null) {\n\t\t\t\tmtlStream.println(\"map_Ka \" + textureData.file);\n\t\t\t\tmtlStream.println(\"map_Kd \" + textureData.file);\n\t\t\t}\n\t\t\tmtlStream.println();\n\t\t}\n\t}\n\t\n\tprivate void writeColorLine(String lineStart, Color color) {\n\t\t\n\t\tmtlStream.println(lineStart\n\t\t\t\t+ \" \" + color.getRed() / 255f\n\t\t\t\t+ \" \" + color.getGreen() / 255f\n\t\t\t\t+ \" \" + color.getBlue() / 255f);\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/obj/ObjWriter.java",
    "content": "package org.osm2world.core.target.obj;\n\nimport static com.google.common.base.Preconditions.checkArgument;\nimport static java.lang.String.format;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.PrintStream;\nimport java.util.Iterator;\n\nimport org.osm2world.core.GlobalValues;\nimport org.osm2world.core.map_data.object_info.MapMetaWriter;\nimport org.osm2world.core.map_data.creation.MapProjection;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.TargetUtil;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\nimport org.osm2world.core.util.TouchMapperProfile;\n\n/**\n * utility class for creating an Wavefront OBJ file\n */\npublic final class ObjWriter {\n\n\t/** prevents instantiation */\n\tprivate ObjWriter() { }\n\t\n\tpublic static final void writeObjFile(\n\t\t\tFile objFile, MapData mapData,\n\t\t\tMapProjection mapProjection,\n\t\t\tCamera camera, Projection projection)\n\t\t\tthrows IOException {\n\n\t\tlong totalStart = TouchMapperProfile.start();\n\t\t\n\t\tif (!objFile.exists()) {\n\t\t\tobjFile.createNewFile();\n\t\t}\n\t\t\n\t\tFile mtlFile = new File(objFile.getAbsoluteFile() + \".mtl\");\n\t\tif (!mtlFile.exists()) {\n\t\t\tmtlFile.createNewFile();\n\t\t}\n\t\t\n\t\tPrintStream objStream = new PrintStream(objFile);\n\t\tPrintStream mtlStream = new PrintStream(mtlFile);\n\t\t\n\t\t/* write comments at the beginning of both files */\n\t\t\n\t\twriteObjHeader(objStream, mapProjection);\n\t\t\n\t\twriteMtlHeader(mtlStream);\n\t\t\t\t\n\t\t/* write path of mtl file to obj file */\n\t\t\n\t\tobjStream.println(\"mtllib \" + mtlFile.getName() + \"\\n\");\n\t\t\n\t\t/* write actual file content */\n\t\t\n\t\tObjTarget target = new ObjTarget(objStream, mtlStream);\n\t\t\n\t\tlong renderStart = TouchMapperProfile.start();\n\t\tTargetUtil.renderWorldObjects(target, mapData, true);\n\t\tTouchMapperProfile.logMillis(\"output.obj.render_world_objects_ms\",\n\t\t\t\trenderStart);\n\t\t\n\t\tobjStream.close();\n\t\tmtlStream.close();\n\n\t\tFile metaFile = new File(objFile.getAbsoluteFile().getParentFile(),\n\t\t\t\t\"map-meta-raw.json\");\n\t\tlong writeMetaStart = TouchMapperProfile.start();\n\t\tMapMetaWriter.write(metaFile, mapData);\n\t\tTouchMapperProfile.logMillis(\"output.obj.write_map_meta_ms\", writeMetaStart);\n\t\tTouchMapperProfile.logMillis(\"output.obj.total_ms\", totalStart);\n\t\t\n\t}\n\t\n\tpublic static final void writeObjFiles(\n\t\t\tfinal File objDirectory, MapData mapData,\n\t\t\tfinal MapProjection mapProjection,\n\t\t\tCamera camera, Projection projection,\n\t\t\tint primitiveThresholdPerFile)\n\t\t\tthrows IOException {\n\n\t\tlong totalStart = TouchMapperProfile.start();\n\t\t\t\t\t\n\t\tif (!objDirectory.exists()) {\n\t\t\tobjDirectory.mkdir();\n\t\t}\n\t\t\n\t\tcheckArgument(objDirectory.isDirectory());\n\t\t\n\t\tfinal File mtlFile = new File(objDirectory.getPath()\n\t\t\t\t+ File.separator + \"materials.mtl\");\n\t\tif (!mtlFile.exists()) {\n\t\t\tmtlFile.createNewFile();\n\t\t}\n\t\t\n\t\tfinal PrintStream mtlStream = new PrintStream(mtlFile);\n\t\t\n\t\twriteMtlHeader(mtlStream);\n\t\t\n\t\t/* create iterator which creates and wraps .obj files as needed */\n\t\t\t\t\n\t\tIterator<ObjTarget> objIterator = new Iterator<ObjTarget>() {\n\n\t\t\tprivate int fileCounter = 0;\n\t\t\tPrintStream objStream = null;\n\n\t\t\t@Override\n\t\t\tpublic boolean hasNext() {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic ObjTarget next() {\n\n\t\t\t\ttry {\n\t\t\t\t\t\n\t\t\t\t\tif (objStream != null) {\n\t\t\t\t\t\tobjStream.close();\n\t\t\t\t\t\tfileCounter ++;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tFile objFile = new File(objDirectory.getPath() + File.separator\n\t\t\t\t\t\t\t+ \"part\" + format(\"%04d\", fileCounter) + \".obj\");\n\t\t\t\t\t\n\t\t\t\t\tif (!objFile.exists()) {\n\t\t\t\t\t\tobjFile.createNewFile();\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tobjStream = new PrintStream(objFile);\n\t\t\t\t\t\n\t\t\t\t\twriteObjHeader(objStream, mapProjection);\n\t\n\t\t\t\t\tobjStream.println(\"mtllib \" + mtlFile.getName() + \"\\n\");\n\t\t\t\t\t\n\t\t\t\t\treturn new ObjTarget(objStream, mtlStream);\n\t\t\t\t\t\n\t\t\t\t} catch (FileNotFoundException e) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void remove() {\n\t\t\t\tthrow new UnsupportedOperationException();\n\t\t\t}\n\t\t\t\n\t\t};\n\t\t\n\t\t/* write file content */\n\t\t\n\t\tlong renderStart = TouchMapperProfile.start();\n\t\tTargetUtil.renderWorldObjects(objIterator, mapData, primitiveThresholdPerFile);\n\t\tTouchMapperProfile.logMillis(\"output.obj_split.render_world_objects_ms\",\n\t\t\t\trenderStart);\n\t\t\n\t\tmtlStream.close();\n\n\t\tFile metaFile = new File(objDirectory, \"map-meta-raw.json\");\n\t\tlong writeMetaStart = TouchMapperProfile.start();\n\t\tMapMetaWriter.write(metaFile, mapData);\n\t\tTouchMapperProfile.logMillis(\"output.obj_split.write_map_meta_ms\",\n\t\t\t\twriteMetaStart);\n\t\tTouchMapperProfile.logMillis(\"output.obj_split.total_ms\", totalStart);\n\t\t\n\t}\n\n\tprivate static final void writeObjHeader(PrintStream objStream,\n\t\t\tMapProjection mapProjection) {\n\t\t\n\t\tobjStream.println(\"# This file was created by OSM2World \"\n\t\t\t\t+ GlobalValues.VERSION_STRING + \" - \"\n\t\t\t\t+ GlobalValues.OSM2WORLD_URI + \"\\n\");\n\t\tobjStream.println(\"# Projection information:\");\n\t\tobjStream.println(\"# Coordinate origin (0,0,0): \"\n\t\t\t\t+ \"lat \" + mapProjection.calcLat(VectorXZ.NULL_VECTOR) + \", \"\n\t\t\t\t+ \"lon \" + mapProjection.calcLon(VectorXZ.NULL_VECTOR) + \", \"\n\t\t\t\t+ \"ele 0\");\n\t\tobjStream.println(\"# North direction: \" + new VectorXYZ(\n\t\t\t\t\t\tmapProjection.getNorthUnit().x, 0,\n\t\t\t\t\t\t- mapProjection.getNorthUnit().z));\n\t\tobjStream.println(\"# 1 coordinate unit corresponds to roughly \"\n\t\t\t\t+ \"1 m in reality\\n\");\n\t\t\n\t}\n\n\tprivate static final void writeMtlHeader(PrintStream mtlStream) {\n\t\t\n\t\tmtlStream.println(\"# This file was created by OSM2World \"\n\t\t\t\t+ GlobalValues.VERSION_STRING + \" - \"\n\t\t\t\t+ GlobalValues.OSM2WORLD_URI + \"\\n\");\n\t\t\n\t}\n\t\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/obj/RenderableToObj.java",
    "content": "package org.osm2world.core.target.obj;\n\nimport org.osm2world.core.target.common.RenderableToPrimitiveTarget;\n\npublic interface RenderableToObj extends RenderableToPrimitiveTarget {\n\n\tpublic void renderTo(ObjTarget target);\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/obj/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\ntarget that creates Wavefront .obj and .mtl files\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nexport targets for the generated models\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/povray/POVRayTarget.java",
    "content": "package org.osm2world.core.target.povray;\n\nimport java.awt.Color;\nimport java.io.PrintStream;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXYZWithNormals;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.AbstractTarget;\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\n\npublic class POVRayTarget extends AbstractTarget<RenderableToPOVRay> {\n\t\n\tprivate static final String INDENT = \"  \";\n\t\n\t// this is approximatly one millimeter\n\tprivate static final double SMALL_OFFSET = 1e-3;\n\t\n\tprivate final PrintStream output;\n\t\n\tprivate Map<TextureData, String> textureNames = new HashMap<TextureData, String>();\n\t\n\tpublic POVRayTarget(PrintStream output) {\n\t\tthis.output = output;\n\t}\n\t\n\t@Override\n\tpublic Class<RenderableToPOVRay> getRenderableType() {\n\t\treturn RenderableToPOVRay.class;\n\t}\n\t\n\t@Override\n\tpublic void render(RenderableToPOVRay renderable) {\n\t\trenderable.renderTo(this);\n\t}\n\t\n//\tint openBrackets = 0;\n//\n//\t/**\n//\t * appends indentation based on {@link #INDENT} and {@link #openBrackets}\n//\t */\n//\tprivate void appendIndent() {\n//\t\tfor (int i=0; i<openBrackets; i++) {\n//\t\t\tappend(INDENT);\n//\t\t}\n//\t}\n\t\n\t/**\n\t * provides direct write access to the generated source code.\n\t * This is intended for Renderables using special POVRay features.\n\t */\n\tpublic void append(String code) {\n\t\toutput.print(code);\n//\t\tif (code.contains(\"union\") && openBrackets > 0) {\n//\t\t\tSystem.out.println(openBrackets);\n//\t\t}\n//\t\tfor (int i=0; i<code.length(); i++) {\n//\t\t\tchar c = code.charAt(i);\n//\t\t\tif (c == '{') {\n//\t\t\t\topenBrackets++;\n//\t\t\t} else if (c == '}') {\n//\t\t\t\topenBrackets--;\n//\t\t\t}\n//\t\t}\n\t}\n\n\tpublic void append(int value) {\n\t\toutput.print(value);\n\t}\n\t\n\tpublic void append(double value) {\n\t\toutput.print(value);\n\t}\n\t\n//\tprivate final LinkedList<StringBuilder> stack = new LinkedList<StringBuilder>();\n//\n//\tint openBrackets = 0;\n//\n//\tpublic void append(int value) {\n//\t\tstack.peek().append(value);\n//\t}\n//\n//\tpublic void append(double value) {\n//\t\tstack.peek().append(value);\n//\t}\n//\n//\tpublic void startBlock(String s) {\n//\t\tStringBuilder newBlock = new StringBuilder(s + \"{\");\n//\t\tstack.push(newBlock);\n//\t}\n//\n//\tpublic void endBlock(String block) {\n//\t\tStringBuilder closedBlock = stack.poll();\n//\t\tif (stack.isEmpty()) {\n//\t\t\toutput.append(closedBlock);\n//\t\t} else {\n//\t\t\tstack.peek().append(closedBlock);\n//\t\t}\n//\t}\n\n\tpublic void appendDefaultParameterValue(String name, String value) {\n\t\t\n\t\tappend(\"#ifndef (\" + name + \")\\n\");\n\t\tappend(\"#declare \" + name + \" = \" + value);\n\t\tappend(\"\\n#end\\n\\n\");\n\t\t\n\t}\n\t\n\tpublic void appendMaterialDefinitions() {\n\t\t\n\t\tfor (Material material : Materials.getMaterials()) {\n\t\t\t\n\t\t\tString uniqueName = Materials.getUniqueName(material);\n\t\t\tString name = \"texture_\" + uniqueName;\n\t\t\t\n\t\t\tappend(\"#ifndef (\" + name + \")\\n\");\n\t\t\tappend(\"#declare \" + name + \" = \");\n\t\t\tappendMaterial(material);\n\t\t\tappend(\"#end\\n\\n\");\n\t\t\t\n\t\t\tif (material.getNumTextureLayers() == 1) {\n\t\t\t\t\n\t\t\t\tTextureData td = material.getTextureDataList().get(0);\n\t\t\t\t\n\t\t\t\tif (!td.colorable) {\n\t\t\t\t\ttextureNames.put(td, uniqueName);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\t@Override\n\tpublic void drawTriangles(Material material,\n\t\t\tCollection<? extends TriangleXYZ> triangles,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\n\t\tif (!checkMeshValidity(triangles))\n\t\t\treturn;\n\t\t\n\t\tfor (TriangleXYZ triangle : triangles) {\n\t\t\tperformNaNCheck(triangle);\n\t\t}\n\t\t\n\t\tif (material.getNumTextureLayers() > 1) {\n\t\t\t\n\t\t\tint count = 0;\n\t\t\t\n\t\t\tfor (TextureData textureData : material.getTextureDataList()) {\n\n\t\t\t\tappend(\"mesh {\\n\");\n\n\t\t\t\tdrawTriangleMesh(triangles, texCoordLists.get(count), count);\n\n\t\t\t\tappend(\"  uv_mapping \");\n\t\t\t\tappendMaterial(material, textureData);\n\t\t\t\t\n\t\t\t\tif (count > 0)\n\t\t\t\t\tappend(\"  no_shadow\");\n\t\t\t\tappend(\"}\\n\");\n\t\t\t\tcount++;\n\t\t\t}\n\t\t} else {\n\n\t\t\tappend(\"mesh {\\n\");\n\t\t\n\t\t\tif (texCoordLists.size() > 0) {\n\t\t\t\tdrawTriangleMesh(triangles, texCoordLists.get(0), 0);\n\t\t\t} else {\n\t\t\t\tfor (TriangleXYZ triangle : triangles) {\n\t\t\t\t\tappend(INDENT);\n\t\t\t\t\tappendTriangle(triangle.v1, triangle.v2, triangle.v3);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tappend(\" uv_mapping \");\n\t\t\tappendMaterialOrName(material);\n\t\t\n\t\t\tappend(\"}\\n\");\n\t\t}\n\t}\n\t\t\n\t@Override\n\tpublic void drawTrianglesWithNormals(Material material,\n\t\t\tCollection<? extends TriangleXYZWithNormals> triangles,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\n\t\tif (!checkMeshValidity(triangles))\n\t\t\treturn;\n\n\t\tif (material.getNumTextureLayers() > 1) {\n\t\t\t\n\t\t\tint count = 0;\n\t\t\t\n\t\t\tfor (TextureData textureData : material.getTextureDataList()) {\n\t\t\t\t\n\t\t\t\tappend(\"mesh {\\n\");\n\n\t\t\t\tdrawTriangleNormalMesh(triangles, texCoordLists.get(count), count);\n\n\t\t\t\tappend(\"  uv_mapping \");\n\t\t\t\tappendMaterial(material, textureData);\n\t\t\t\t\n\t\t\t\tif (count > 0)\n\t\t\t\t\tappend(\"  no_shadow\");\n\t\t\t\tappend(\"}\\n\");\n\t\t\t\tcount++;\n\t\t\t}\n\t\t} else {\n\t\t\t\n\t\t\tappend(\"mesh {\\n\");\n\n\t\t\tif (texCoordLists.size() > 0) {\n\t\t\t\tdrawTriangleNormalMesh(triangles, texCoordLists.get(0), 0);\n\t\t\t} else {\n\t\t\t\tfor (TriangleXYZWithNormals triangle : triangles) {\n\t\t\t\t\tappend(INDENT);\n\t\t\t\t\tappendTriangle(triangle.v1, triangle.v2, triangle.v3,\n\t\t\t\t\t\t\ttriangle.n1, triangle.n2, triangle.n3, true);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tappend(\" uv_mapping \");\n\t\t\tappendMaterialOrName(material);\n\t\t\tappend(\"}\\n\");\n\t\t}\n\t}\n\n\tprivate void drawTriangleNormalMesh(Collection<? extends TriangleXYZWithNormals> triangles,\n\t\t\tList<VectorXZ> texCoordList, int depth) {\n\n\t\tIterator<? extends TriangleXYZWithNormals> itr1 = triangles.iterator();\n\t\tIterator<VectorXZ> itr2 = texCoordList.iterator();\n\t\t\n\t\twhile (itr1.hasNext()) {\n\t\t\n\t\t\tTriangleXYZWithNormals triangle = itr1.next();\n\t\t\tVectorXYZ n1 = triangle.n1;\n\t\t\tVectorXYZ n2 = triangle.n2;\n\t\t\tVectorXYZ n3 = triangle.n3;\n\t\t\tVectorXZ tex1 = itr2.next();\n\t\t\tVectorXZ tex2 = itr2.next();\n\t\t\tVectorXZ tex3 = itr2.next();\n\t\t\t\n\t\t\tappend(INDENT);\n\t\t\n\t\t\tif (depth > 0) {\n\t\t\t\n\t\t\t\tVectorXYZ d1 = n1.mult(depth*SMALL_OFFSET);\n\t\t\t\tVectorXYZ d2 = n2.mult(depth*SMALL_OFFSET);\n\t\t\t\tVectorXYZ d3 = n3.mult(depth*SMALL_OFFSET);\n\t\t\t\t\n\t\t\t\tappendTriangle(\n\t\t\t\t\t\ttriangle.v1.add(d1),\n\t\t\t\t\t\ttriangle.v2.add(d2),\n\t\t\t\t\t\ttriangle.v3.add(d3),\n\t\t\t\t\t\tn1, n2, n3, tex1, tex2, tex3, false, true);\n\t\t\t\n\t\t\t} else {\n\t\t\t\n\t\t\t\tappendTriangle(triangle.v1, triangle.v2, triangle.v3,\n\t\t\t\t\t\tnull, null, null, tex1, tex2, tex3, false, true);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void drawTriangleMesh(Collection<? extends TriangleXYZ> triangles,\n\t\t\tList<VectorXZ> texCoordList, int depth) {\n\n\t\tIterator<? extends TriangleXYZ> itr1 = triangles.iterator();\n\t\tIterator<VectorXZ> itr2 = texCoordList.iterator();\n\t\t\n\t\twhile (itr1.hasNext()) {\n\t\t\n\t\t\tTriangleXYZ triangle = itr1.next();\n\t\t\tVectorXYZ normal = triangle.getNormal();\n\t\t\tVectorXZ tex1 = itr2.next();\n\t\t\tVectorXZ tex2 = itr2.next();\n\t\t\tVectorXZ tex3 = itr2.next();\n\t\t\n\t\t\tappend(INDENT);\n\t\t\n\t\t\tif (depth > 0) {\n\t\t\t\n\t\t\t\tnormal = normal.mult(depth*SMALL_OFFSET);\n\t\t\t\tappendTriangle(\n\t\t\t\t\t\ttriangle.v1.add(normal),\n\t\t\t\t\t\ttriangle.v2.add(normal),\n\t\t\t\t\t\ttriangle.v3.add(normal),\n\t\t\t\t\t\tnull, null, null, tex1, tex2, tex3, false, true);\n\t\t\t\n\t\t\t} else {\n\t\t\t\n\t\t\t\tappendTriangle(triangle.v1, triangle.v2, triangle.v3,\n\t\t\t\t\t\tnull, null, null, tex1, tex2, tex3, false, true);\n\t\t\t}\n\t\t}\n\t}\n\t\n//\t@Override\n//\tpublic void drawTriangleStrip(Material material, VectorXYZ... vectors) {\n//\n//\t\tfor (VectorXYZ vector : vectors) {\n//\t\t\tperformNaNCheck(vector);\n//\t\t}\n//\n//\t\tappend(\"union {\\n\");\n//\n//\t\tfor (int triangle = 0; triangle + 2 < vectors.length; triangle++) {\n//\n//\t\t\tappend(INDENT);\n//\n//\t\t\tappendTriangle(\n//\t\t\t\t\tvectors[triangle],\n//\t\t\t\t\tvectors[triangle + 1],\n//\t\t\t\t\tvectors[triangle + 2]);\n//\n//\t\t}\n//\n//\t\tappendMaterial(material);\n//\n//\t\tappend(\"}\\n\");\n//\n//\t}\n\t\n//\t@Override\n//\tpublic void drawTriangleFan(Material material, List<? extends VectorXYZ> vs) {\n//\t\tfor (VectorXYZ vector : vs) {\n//\t\t\tperformNaNCheck(vector);\n//\t\t}\n//\n//\t\tappend(\"union {\\n\");\n//\n//\t\tVectorXYZ center = vs.get(0);\n//\n//\t\tfor (int triangle = 0; triangle + 2 < vs.size(); triangle ++) {\n//\n//\t\t\tappend(INDENT);\n//\n//\t\t\tappendTriangle(\n//\t\t\t\t\tcenter,\n//\t\t\t\t\tvs.get(triangle + 1),\n//\t\t\t\t\tvs.get(triangle + 2));\n//\n//\t\t}\n//\n//\t\tappendMaterial(material);\n//\n//\t\tappend(\"}\\n\");\n//\n//\t}\n\n\t@Override\n\tpublic void drawConvexPolygon(Material material, List<VectorXYZ> vs,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tfor (VectorXYZ vector : vs) {\n\t\t\tperformNaNCheck(vector);\n\t\t}\n\n\t\tappend(\"polygon {\\n  \");\n\t\tappend(vs.size());\n\t\tappend(\", \");\n\t\tfor (VectorXYZ v : vs) {\n\t\t\tappendVector(v);\n\t\t}\n\t\t\n\t\tappendMaterialOrName(material);\n\t\t\n\t\tappend(\"}\\n\");\n\n\t}\n\n\t@Override\n\tpublic void drawColumn(Material material, Integer corners, VectorXYZ base,\n\t\t\tdouble height, double radiusBottom, double radiusTop,\n\t\t\tboolean drawBottom, boolean drawTop) {\n\n\t\tperformNaNCheck(base);\n\t\t\n\t\tif (height <= 0) return;\n\n\t\tif (corners == null) {\n\n\t\t\tif (radiusBottom == radiusTop) { // cylinder\n\n\t\t\t\tappend(\"cylinder {\\n  \");\n\t\t\t\tappendVector(base);\n\t\t\t\tappend(\", \");\n\t\t\t\tappendVector(base.y(base.y + height));\n\t\t\t\tappend(\", \");\n\t\t\t\tappend(radiusTop);\n\n\t\t\t} else { // (truncated) cone\n\n\t\t\t\tappend(\"cone {\\n  \");\n\t\t\t\tappendVector(base);\n\t\t\t\tappend(\", \"); append(radiusBottom); append(\", \");\n\t\t\t\tappendVector(base.y(base.y + height));\n\t\t\t\tappend(\", \"); append(radiusTop);\n\n\t\t\t}\n\n\t\t\tif (!drawBottom && !drawTop) {\n\t\t\t\t// TODO: incorrect if only one is false\n\t\t\t\tappend(\" open\");\n\t\t\t}\n\t\t\t\n\t\t\tappendMaterialOrName(material);\n\t\t\t\n\t\t\tappend(\"}\\n\");\n\n\t\t} else { // not round\n\n\t\t\tsuper.drawColumn(material, corners, base, height, radiusBottom, radiusTop, drawBottom, drawTop);\n\n\t\t}\n\n\t}\n\n\t/**\n\t * variant of {@link #drawColumn(Material, Integer, VectorXYZ, double, double, double, boolean, boolean)}\n\t * that allows arbitrarily placed columns\n\t */\n\tpublic void drawColumn(Material material, Integer corners, VectorXYZ base,\n\t\t\tVectorXYZ cap, double radiusBottom, double radiusTop,\n\t\t\tboolean drawBottom, boolean drawTop) {\n\n\t\tperformNaNCheck(base);\n\t\tperformNaNCheck(cap);\n\t\t\n\t\tif (cap.equals(base))\n\t\t\treturn;\n\n\t\tif (corners == null) {\n\n\t\t\tif (radiusBottom == radiusTop) { // cylinder\n\n\t\t\t\tappend(\"cylinder {\\n  \");\n\t\t\t\tappendVector(base);\n\t\t\t\tappend(\", \");\n\t\t\t\tappendVector(cap);\n\t\t\t\tappend(\", \"); append(radiusTop);\n\n\t\t\t} else { // (truncated) cone\n\n\t\t\t\tappend(\"cone {\\n  \");\n\t\t\t\tappendVector(base);\n\t\t\t\tappend(\", \"); append(radiusBottom); append(\", \");\n\t\t\t\tappendVector(cap);\n\t\t\t\tappend(\", \"); append(radiusTop);\n\n\t\t\t}\n\n\t\t\tif (!drawBottom && !drawTop) {\n\t\t\t\t// TODO: incorrect if only one is false\n\t\t\t\tappend(\" open\");\n\t\t\t}\n\n\t\t} else { // not round\n\n\t\t\tthrow new UnsupportedOperationException(\n\t\t\t\t\t\"drawing non-round columns isn't implemented yet\");\n\n\t\t}\n\t\t\n\t\tappendMaterialOrName(material);\n\t\t\n\t\tappend(\"}\\n\");\n\n\t}\n\t\n\tprivate boolean checkMeshValidity(Collection<? extends TriangleXYZ> triangles) {\n\t\t\n\t\tif (triangles.size() == 0)\n\t\t\treturn false;\n\t\t\n\t\tboolean result = false;\n\t\tfor (TriangleXYZ triangle : triangles) {\n\n\t\t\tresult |= !isDegenerated(triangle);\n\t\t}\n\t\t\n\t\treturn result;\n\t}\n\t\n\tprivate boolean isDegenerated(TriangleXYZ triangle) {\n\n\t\tVectorXYZ a = triangle.v1;\n\t\tVectorXYZ b = triangle.v2;\n\t\tVectorXYZ c = triangle.v3;\n\t\t\n\t\tif (a.equals(b) || a.equals(c) || b.equals(c)) {\n\t\t\treturn true;\n\t\t} else if (a.x == b.x && b.x == c.x\n\t\t\t\t&& a.y == b.y && b.y == c.y) {\n\t\t\treturn true;\n\t\t} else if (a.x == b.x && b.x == c.x\n\t\t\t\t&& a.z == b.z && b.z == c.z) {\n\t\t\treturn true;\n\t\t} else if (a.y == b.y && b.y == c.y\n\t\t\t\t&& a.z == b.z && b.z == c.z) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t\n\tpublic void appendTriangle(VectorXYZ a, VectorXYZ b, VectorXYZ c) {\n\n\t\tappendTriangle(a, b, c, null, null, null, false);\n\t}\n\n\tpublic void appendTriangle(\n\t\t\tVectorXYZ a, VectorXYZ b, VectorXYZ c,\n\t\t\tVectorXYZ na, VectorXYZ nb, VectorXYZ nc,\n\t\t\tboolean smooth) {\n\n\t\tappendTriangle(a, b, c, na, nb, nc, null, null, null, smooth, false);\n\t}\n\t\t\n\tpublic void appendTriangle(\n\t\t\tVectorXYZ a, VectorXYZ b, VectorXYZ c,\n\t\t\tVectorXYZ na, VectorXYZ nb, VectorXYZ nc,\n\t\t\tVectorXZ ta, VectorXZ tb, VectorXZ tc,\n\t\t\tboolean smooth, boolean texture) {\n\n\t\t// append the triangle\n\t\t\n\t\tif (smooth) append(\"smooth_\");\n\t\tappend(\"triangle { \");\n\t\tappendVector(a);\n\t\tif (smooth) {\n\t\t\tappend(\", \");\n\t\t\tappendVector(na);\n\t\t}\n\t\tappend(\", \");\n\t\tappendVector(b);\n\t\tif (smooth) {\n\t\t\tappend(\", \");\n\t\t\tappendVector(nb);\n\t\t}\n\t\tappend(\", \");\n\t\tappendVector(c);\n\t\tif (smooth) {\n\t\t\tappend(\", \");\n\t\t\tappendVector(nc);\n\t\t}\n\n\t\tif (texture) {\n\t\t\t/*\n\t\t\tappend(\" uv_vectors \");\n\t\t\tappendInverseVector(ta);\n\t\t\tappend(\", \");\n\t\t\tappendInverseVector(tb);\n\t\t\tappend(\", \");\n\t\t\tappendInverseVector(tc);\n\t\t\t*/\n\t\t\t\n\t\t\tappend(\" uv_vectors \");\n\t\t\tappendVector(ta);\n\t\t\tappend(\", \");\n\t\t\tappendVector(tb);\n\t\t\tappend(\", \");\n\t\t\tappendVector(tc);\n\t\t}\n\n\t\tappend(\"}\\n\");\n\t}\n\t\n\n\t/**\n\t * adds a color. Syntax is \"color rgb <x, y, z>\".\n\t */\n\tpublic void appendRGBColor(Color color) {\n\t\t\n\t\tappend(\"color rgb \");\n\t\tappendVector(\n\t\t\t\tcolor.getRed()/255f,\n\t\t\t\tcolor.getGreen()/255f,\n\t\t\t\tcolor.getBlue()/255f);\n\t\t\n\t}\n\t\n\tpublic void appendMaterialOrName(Material material) {\n\t\t\n\t\tString materialName = Materials.getUniqueName(material);\n\n\t\tif (materialName != null) {\n\t\t\tappend(\" texture { texture_\" + materialName + \" }\");\n\t\t} else {\n\t\t\tappendMaterial(material);\n\t\t}\n\t\t\n\t}\n\n\tprivate void appendMaterial(Material material) {\n\n\t\tif (material.getNumTextureLayers() == 0) {\n\n\t\t\tappend(\"  texture {\\n\");\n\t\t\tappend(\"    pigment { \");\n\t\t\tappendRGBColor(material.getColor());\n\t\t\tappend(\" }\\n    finish {\\n\");\n\t\t\tappend(\"      ambient \" + material.getAmbientFactor() + \"\\n\");\n\t\t\tappend(\"      diffuse \" + material.getDiffuseFactor() + \"\\n\");\n\t\t\tappend(\"    }\\n\");\n\t\t\tappend(\"  }\\n\");\n\n\t\t} else {\n\n\t\t\tfor (TextureData textureData : material.getTextureDataList()) {\n\n\t\t\t\tappendMaterial(material, textureData);\n\t\t\t}\n\t\t}\n\t}\n\n\t\n\tprivate void appendMaterial(Material material, TextureData textureData) {\n\n\t\tString textureName = textureNames.get(textureData);\n\t\t\n\t\tif (textureName == null) {\n\n\t\t\tif (textureData.colorable) {\n\t\t\t\tappend(\"  texture {\\n\");\n\t\t\t\tappend(\"    pigment { \");\n\t\t\t\tappendRGBColor(material.getColor());\n\t\t\t\tappend(\" }\\n    finish {\\n\");\n\t\t\t\tappend(\"      ambient \" + material.getAmbientFactor() + \"\\n\");\n\t\t\t\tappend(\"      diffuse \" + material.getDiffuseFactor() + \"\\n\");\n\t\t\t\tappend(\"    }\\n\");\n\t\t\t\tappend(\"  }\\n\");\n\t\t\t}\n\n\t\t\tappend(\"  texture {\\n\");\n\t\t\tappend(\"    pigment { \");\n\t\t\tappendImageMap(textureData);\n\t\t\tappend(\" }\\n    finish {\\n\");\n\t\t\tappend(\"      ambient \" + material.getAmbientFactor() + \"\\n\");\n\t\t\tappend(\"      diffuse \" + material.getDiffuseFactor() + \"\\n\");\n\t\t\tappend(\"    }\\n\");\n\t\t\tappend(\"  }\\n\");\n\t\n\t\t} else {\n\n\t\t\tappend(\"  texture { texture_\" + textureName + \"}\");\n\t\t}\n\t}\n\n\n\tprivate void appendImageMap(TextureData textureData) {\n\n\t\tappend(\"        image_map {\\n\");\n\n\t\tif (textureData.file.getName().toLowerCase().endsWith(\"png\")) {\n\t\t\tappend(\"             png \\\"\" + textureData.file + \"\\\"\\n\");\n\t\t} else {\n\t\t\tappend(\"             jpeg \\\"\" + textureData.file + \"\\\"\\n\");\n\t\t}\n\n\t\tif (textureData.colorable) {\n\t\t\tappend(\"             filter all 1.0\\n\");\n\t\t}\n\t\tappend(\"\\n          }\");\n\t}\n\t\n\t/**\n\t * adds a vector to the String built by a StringBuilder.\n\t * Syntax is \"<x, y, z>\".\n\t */\n\tpublic void appendVector(float x, float y, float z) {\n\n\t\tif (Float.isNaN(x) || Float.isNaN(y) || Float.isNaN(z)) {\n\t\t\tthrow new IllegalArgumentException(\"NaN vector \" + x + \", \" + y + \", \" + z);\n\t\t}\n\t\t\n\t\tappend(\"<\");\n\t\tappend(x);\n\t\tappend(\", \");\n\t\tappend(y);\n\t\tappend(\", \");\n\t\tappend(z);\n\t\tappend(\">\");\n\t\t\n\t}\n\n\tpublic void appendVector(double x, double y, double z) {\n\t\t\n\t\tif (Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z)) {\n\t\t\tthrow new IllegalArgumentException(\"NaN vector \" + x + \", \" + y + \", \" + z);\n\t\t}\n\t\t\n\t\tappend(\"<\");\n\t\tappend(x);\n\t\tappend(\", \");\n\t\tappend(y);\n\t\tappend(\", \");\n\t\tappend(z);\n\t\tappend(\">\");\n\t\t\n\t}\n\t\n\t/**\n\t * alternative to {@link #appendVector(double, double)}\n\t * using a vector object as parameter instead of individual coordinates\n\t */\n\tpublic void appendVector(VectorXYZ vector) {\n\t\tappendVector(vector.getX(), vector.getY(), vector.getZ());\n\t}\n\t\n\t/**\n\t * adds a vector to the String built by a StringBuilder.\n\t * Syntax is \"<v1, v2>\".\n\t */\n\tpublic void appendVector(double x, double z) {\n\t\t\n\t\tappend(\"<\");\n\t\tappend(x);\n\t\tappend(\", \");\n\t\tappend(z);\n\t\tappend(\">\");\n\t\t\n\t}\n\t\n\t/**\n\t * alternative to {@link #appendVector(double, double)}\n\t * using a vector object as parameter instead of individual coordinates\n\t */\n\tpublic void appendVector(VectorXZ vector) {\n\t\tappendVector(vector.x, vector.z);\n\t}\n\n\t/**\n\t * append a vector with inverted coordinates\n\t */\n\tpublic void appendInverseVector(VectorXZ vector) {\n\t\tappendVector(-vector.x, -vector.z);\n\t}\n\n\t/**\n\t * \n\t * @param vs  polygon vertices; first and last should be equal\n\t */\n\tpublic void appendPolygon(VectorXYZ... vs) {\n\t\t\n\t\tassert !vs[0].equals(vs[vs.length-1]) : \"polygon not closed\";\n\n\t\tappend(\"polygon {\\n  \");\n\t\tappend(vs.length);\n\t\tappend(\", \");\n\t\tfor (VectorXYZ v : vs) {\n\t\t\tappendVector(v);\n\t\t}\n\t\tappend(\"}\\n\");\n\t\t\n\t}\n\t\n\tpublic void appendPrism(float y1, float y2, VectorXZ... vs) {\n\t\t\n\t\tappend(\"prism {\\n  \");\n\t\tappend(y1);\n\t\tappend(\", \");\n\t\tappend(y2);\n\t\tappend(\", \");\n\t\tappend(vs.length);\n\t\tappend(\",\\n  \");\n\t\tfor (VectorXZ v : vs) {\n\t\t\tappendVector(v);\n\t\t}\n\t\tappend(\"\\n}\");\n\t\t\t\t\n\t}\n\t\n\t//TODO: avoid having to do this\n\n\tprivate void performNaNCheck(TriangleXYZWithNormals triangle) {\n\t\tperformNaNCheck(triangle.v1);\n\t\tperformNaNCheck(triangle.v2);\n\t\tperformNaNCheck(triangle.v3);\n\t\tperformNaNCheck(triangle.n1);\n\t\tperformNaNCheck(triangle.n2);\n\t\tperformNaNCheck(triangle.n3);\n\t}\n\t\n\tprivate void performNaNCheck(TriangleXYZ triangle) {\n\t\tperformNaNCheck(triangle.v1);\n\t\tperformNaNCheck(triangle.v2);\n\t\tperformNaNCheck(triangle.v3);\n\t}\n\n\tprivate void performNaNCheck(VectorXYZ v) {\n\t\tif (Double.isNaN(v.x) || Double.isNaN(v.y) || Double.isNaN(v.z)) {\n\t\t\tthrow new IllegalArgumentException(\"NaN vector \" + v.x + \", \" + v.y + \", \" + v.z);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/povray/POVRayWriter.java",
    "content": "package org.osm2world.core.target.povray;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.PrintStream;\nimport java.util.Locale;\n\nimport org.osm2world.core.GlobalValues;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.TargetUtil;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * utility class for creating a POVRay file\n */\npublic final class POVRayWriter {\n\n\t/** prevents instantiation */\n\tprivate POVRayWriter() { }\n\t\n\tpublic static final void writePOVInstructionFile(File file, MapData mapData,\n\t\t\tCamera camera, Projection projection)\n\t\t\tthrows IOException {\n\t\t\n\t\tif (!file.exists()) {\n\t\t\tfile.createNewFile();\n\t\t}\n\t\t\n\t\tPrintStream printStream = new PrintStream(file);\n\t\t\n\t\twritePOVInstructionStringToStream(printStream,\n\t\t\t\tmapData, camera, projection);\n\t\t\n\t\tprintStream.close();\n\t\t\n\t}\n\n\tprivate static final void writePOVInstructionStringToStream(\n\t\t\tPrintStream stream, MapData mapData,\n\t\t\tCamera camera, Projection projection) {\n\t\t\t\t\n\t\tPOVRayTarget target = new POVRayTarget(stream);\n\t\t\n\t\taddCommentHeader(target);\n\t\t\n\t\ttarget.append(\"\\n#include \\\"textures.inc\\\"\\n#include \\\"colors.inc\\\"\\n\");\n\t\ttarget.append(\"#include \\\"osm2world_definitions.inc\\\"\\n\\n\");\n\t\t\n\t\tif (camera != null && projection != null) {\n\t\t\taddCameraDefinition(target, camera, projection);\n\t\t}\n\n\t\ttarget.append(\"//\\n// global scene parameters\\n//\\n\\n\");\n\n\t\taddLightingDefinition(target, GlobalLightingParameters.DEFAULT);\n\t\t\n\t\ttarget.appendDefaultParameterValue(\"season\", \"summer\");\n\t\ttarget.appendDefaultParameterValue(\"time\", \"day\");\n\t\t\n\t\ttarget.append(\"//\\n// material and object definitions\\n//\\n\\n\");\n\t\t\n\t\ttarget.appendDefaultParameterValue(\"sky_sphere_def\",\n\t\t\t\t\"sky_sphere {\\n pigment { Blue_Sky3 }\\n} \");\n\t\ttarget.append(\"sky_sphere {sky_sphere_def}\\n\\n\");\n\t\t\n\t\ttarget.appendMaterialDefinitions();\n\t\t\n\t\tfor (MapElement element : mapData.getMapElements()) {\n\t\t\tfor (WorldObject r : element.getRepresentations()) {\n\t\t\t\tif (r instanceof RenderableToPOVRay) {\n\t\t\t\t\t((RenderableToPOVRay)r).addDeclarationsTo(target);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t//TODO get terrain boundary elsewhere\n//\t\tif (terrain != null) {\n//\n//\t\t\ttarget.append(\"//\\n// empty ground around the scene\\n//\\n\\n\");\n//\n//\t\t\ttarget.append(\"difference {\\n\");\n//\t\t\ttarget.append(\"  plane { y, -0.001 }\\n  \");\n//\t\t\tVectorXZ[] boundary = eleData.getBoundaryPolygon().getXZPolygon()\n//\t\t\t\t.getVertexLoop().toArray(new VectorXZ[0]);\n//\t\t\ttarget.appendPrism( -100, 1, boundary);\n//\t\t\ttarget.append(\"\\n\");\n//\t\t\ttarget.appendMaterialOrName(Materials.TERRAIN_DEFAULT);\n//\t\t\ttarget.append(\"\\n}\\n\\n\");\n//\n//\t\t}\n\t\t\t\n\t\ttarget.append(\"\\n\\n//\\n//Map data\\n//\\n\\n\");\n\t\t\t\n\t\tTargetUtil.renderWorldObjects(target, mapData, true);\n\t\t\t\t\n\t}\n\n\tprivate static final void addLightingDefinition(POVRayTarget target,\n\t\t\tGlobalLightingParameters parameters) {\n\t\t\n\t\ttarget.append(String.format(Locale.ENGLISH,\n\t\t\t\t\"global_settings { ambient_light rgb <%f,%f,%f> }\\n\",\n\t\t\t\tparameters.globalAmbientColor.getRed() / 255f,\n\t\t\t\tparameters.globalAmbientColor.getGreen() / 255f,\n\t\t\t\tparameters.globalAmbientColor.getBlue() / 255f));\n\t\t\n\t\ttarget.append(String.format(Locale.ENGLISH,\n\t\t\t\t\"light_source{ <%f,%f,%f> color rgb <%f,%f,%f> parallel point_at <0,0,0> fade_power 0 }\\n\\n\",\n\t\t\t\tparameters.lightFromDirection.x * 100000,\n\t\t\t\tparameters.lightFromDirection.y * 100000,\n\t\t\t\tparameters.lightFromDirection.z * 100000,\n\t\t\t\tparameters.lightColorDiffuse.getRed() / 255f,\n\t\t\t\tparameters.lightColorDiffuse.getGreen() / 255f,\n\t\t\t\tparameters.lightColorDiffuse.getBlue() / 255f));\n\t\t\n\t}\n\t\n\tprivate static final void addCommentHeader(POVRayTarget target) {\n\t\t\n\t\ttarget.append(\"/*\\n\"\n\t\t\t\t+ \" * This file was created by OSM2World \"\n\t\t\t\t+ GlobalValues.VERSION_STRING + \" - \"\n\t\t\t\t+ GlobalValues.OSM2WORLD_URI + \"\\n\"\n\t\t\t\t+ \" * \\n\"\n\t\t\t\t+ \" * Make sure that a \\\"osm2world_definitions.inc\\\" file exists!\\n\"\n\t\t\t\t+ \" * You can start with the one in the \\\"resources\\\" folder from your\\n\"\n\t\t\t\t+ \" * OSM2World installation or even just create an empty file.\\n\"\n\t\t\t\t+ \" */\\n\");\n\t\t\n\t}\n\t\t\n\tprivate static final void addCameraDefinition(POVRayTarget target,\n\t\t\tCamera camera, Projection projection) {\n\t\t\n\t\ttarget.append(\"camera {\");\n\t\t\n\t\tif (projection.isOrthographic()) {\n\t\t\ttarget.append(\"\\n  orthographic\");\n\t\t}\n\t\t\n\t\ttarget.append(\"\\n  location \");\n\t\ttarget.appendVector(camera.getPos());\n\t\t\n\t\tif (projection.isOrthographic()) {\n\t\t\t\n\t\t\ttarget.append(\"\\n  right \");\n\t\t\tdouble width = projection.getVolumeHeight()\n\t\t\t\t* projection.getAspectRatio();\n\t\t\ttarget.appendVector(camera.getRight().mult(width).invert()); //invert compensates for left-handed vs. right handed coordinates\n\t\t\t\n\t\t\ttarget.append(\"\\n  up \");\n\t\t\tVectorXYZ up = camera.getUp();\n\t\t\ttarget.appendVector(up.mult(projection.getVolumeHeight()));\n\t\t\t\t\t\t\n\t\t\ttarget.append(\"\\n  look_at \");\n\t\t\ttarget.appendVector(camera.getLookAt());\n\t\t\t\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\ttarget.append(\"\\n  look_at  \");\n\t\t\ttarget.appendVector(camera.getLookAt());\n\t\t\t\n\t\t\ttarget.append(\"\\n  sky \");\n\t\t\ttarget.appendVector(camera.getUp());\n\t\t\t\n\t\t}\n\t\t\n\t\ttarget.append(\"\\n}\\n\\n\");\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/povray/RenderableToPOVRay.java",
    "content": "package org.osm2world.core.target.povray;\n\nimport org.osm2world.core.target.Renderable;\n\npublic interface RenderableToPOVRay extends Renderable {\n\n\t/**\n\t * lets the Renderable add global declarations.\n\t * This is called before the renderTo calls.\n\t */\n\tpublic void addDeclarationsTo(POVRayTarget target);\n\t\n\tpublic void renderTo(POVRayTarget target);\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/povray/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\ntarget that creates .pov files for the POVRay raytracer\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/target/statistics/StatisticsTarget.java",
    "content": "package org.osm2world.core.target.statistics;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.PrimitiveTarget;\nimport org.osm2world.core.target.common.RenderableToPrimitiveTarget;\nimport org.osm2world.core.target.common.Primitive.Type;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.jogl.JOGLRendererVBO;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * a target that simply counts the primitives that are sent to it\n * to create statistics.\n */\npublic class StatisticsTarget extends\n\t\tPrimitiveTarget<RenderableToPrimitiveTarget> {\n\t\n\tprivate long[] globalCounts = new long[Stat.values().length];\n\tprivate Map<Material, long[]> countsPerMaterial = new HashMap<Material, long[]>();\n\tprivate Map<Class<?>, long[]> countsPerClass = new HashMap<Class<?>, long[]>();\n\t\n\tprivate WorldObject currentObject = null;\n\t\n\tprivate static abstract class StatImpl {\n\t\t\n\t\tpublic long countObject(WorldObject object) {\n\t\t\treturn 0;\n\t\t}\n\t\t\n\t\tpublic long countPrimitive(Type type, Material material,\n\t\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\treturn 0;\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static enum Stat {\n\t\t\n\t\tOBJECT_COUNT(new StatImpl() {\n\t\t\t@Override public long countObject(WorldObject object) {\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t}),\n\t\t\n\t\tPRIMITIVE_COUNT(new StatImpl() {\n\t\t\t@Override public long countPrimitive(Type type, Material material,\n\t\t\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t}),\n\t\t\n\t\tTOTAL_TRIANGLE_COUNT(new StatImpl() {\n\t\t\t@Override public long countPrimitive(Type type, Material material,\n\t\t\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\t\tif (type == Type.TRIANGLES) {\n\t\t\t\t\treturn vs.size() / 3;\n\t\t\t\t} else {\n\t\t\t\t\treturn vs.size() - 2;\n\t\t\t\t}\n\t\t\t}\n\t\t}),\n\t\t\n\t\tTRIANGLES_COUNT(new StatImpl() {\n\t\t\t@Override public long countPrimitive(Type type, Material material,\n\t\t\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\t\treturn type == Type.TRIANGLES ? 1 : 0;\n\t\t\t}\n\t\t}),\n\t\t\n\t\tTRIANGLE_STRIP_COUNT(new StatImpl() {\n\t\t\t@Override public long countPrimitive(Type type, Material material,\n\t\t\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\t\treturn type == Type.TRIANGLE_STRIP ? 1 : 0;\n\t\t\t}\n\t\t}),\n\t\t\n\t\tTRIANGLE_FAN_COUNT(new StatImpl() {\n\t\t\t@Override public long countPrimitive(Type type, Material material,\n\t\t\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\t\treturn type == Type.TRIANGLE_FAN ? 1 : 0;\n\t\t\t}\n\t\t}),\n\t\t\n\t\tCONVEX_POLYGON_COUNT(new StatImpl() {\n\t\t\t@Override public long countPrimitive(Type type, Material material,\n\t\t\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\t\treturn type == Type.CONVEX_POLYGON ? 1 : 0;\n\t\t\t}\n\t\t}),\n\t\t\n\t\tVBO_VALUE_COUNT(new StatImpl() {\n\t\t\t@Override public long countPrimitive(Type type, Material material,\n\t\t\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\t\t\n\t\t\t\tint vertexCount;\n\t\t\t\t\n\t\t\t\tif (type == Type.TRIANGLES) {\n\t\t\t\t\tvertexCount = vs.size();\n\t\t\t\t} else {\n\t\t\t\t\tvertexCount = 3 * (vs.size() - 2);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\treturn vertexCount *\n\t\t\t\t\tJOGLRendererVBO.getValuesPerVertex(material);\n\t\t\t\t\n\t\t\t}\n\t\t});\n\t\t\n\t\tprivate final StatImpl impl;\n\n\t\tprivate Stat(StatImpl impl) {\n\t\t\tthis.impl = impl;\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic Class<RenderableToPrimitiveTarget> getRenderableType() {\n\t\treturn RenderableToPrimitiveTarget.class;\n\t}\n\n\t@Override\n\tpublic void render(RenderableToPrimitiveTarget renderable) {\n\t\trenderable.renderTo(this);\n\t}\n\n\t@Override\n\tpublic void beginObject(WorldObject object) {\n\t\t\n\t\tcurrentObject = object;\n\t\t\n\t\tif (currentObject != null) {\n\t\t\t\n\t\t\tClass<?> currentClass = currentObject.getClass();\n\t\t\t\n\t\t\tif (!countsPerClass.containsKey(currentClass)) {\n\t\t\t\tcountsPerClass.put(currentClass,\n\t\t\t\t\t\tnew long[Stat.values().length]);\n\t\t\t}\n\t\t\t\n\t\t\tfor (Stat stat : Stat.values()) {\n\t\t\t\t\n\t\t\t\tlong count = stat.impl.countObject(object);\n\t\t\t\t\n\t\t\t\tglobalCounts[stat.ordinal()] += count;\n\t\t\t\t\t\t\t\n\t\t\t\tif (currentObject != null) {\n\t\t\t\t\tcountsPerClass.get(currentClass)[stat.ordinal()] += count;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t}\n\t\t\n\t\tsuper.beginObject(object);\n\t\t\t\n\t}\n\t\n\t@Override\n\tprotected void drawPrimitive(Type type, Material material,\n\t\t\tList<VectorXYZ> vs, List<VectorXYZ> normals,\n\t\t\tList<List<VectorXZ>> texCoordLists) {\n\t\t\n\t\tif (!countsPerMaterial.containsKey(material)) {\n\t\t\tcountsPerMaterial.put(material, new long[Stat.values().length]);\n\t\t}\n\t\t\n\t\tfor (Stat stat : Stat.values()) {\n\t\t\t\n\t\t\tlong count = stat.impl.countPrimitive(\n\t\t\t\t\ttype, material, vs, normals, texCoordLists);\n\t\t\t\n\t\t\tglobalCounts[stat.ordinal()] += count;\n\t\t\t\n\t\t\tif (material != null) {\n\t\t\t\tcountsPerMaterial.get(material)[stat.ordinal()] += count;\n\t\t\t}\n\t\t\t\n\t\t\tif (currentObject != null) {\n\t\t\t\tcountsPerClass.get(currentObject.getClass())[stat.ordinal()] += count;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic void clear() {\n\t\tfor (int i=0; i < globalCounts.length; ++i) {\n\t\t\tglobalCounts[i] = 0;\n\t\t}\n\t\tcountsPerMaterial.clear();\n\t\tcountsPerClass.clear();\n\t\tcurrentObject = null;\n\t}\n\t\n\tpublic long getGlobalCount(Stat stat) {\n\t\treturn globalCounts[stat.ordinal()];\n\t}\n\t\n\tpublic Set<Material> getKnownMaterials() {\n\t\treturn countsPerMaterial.keySet();\n\t}\n\t\n\tpublic long getCountForMaterial(Material material, Stat stat) {\n\t\treturn countsPerMaterial.get(material)[stat.ordinal()];\n\t}\n\t\n\tpublic Set<Class<?>> getKnownRenderableClasses() {\n\t\treturn countsPerClass.keySet();\n\t}\n\t\n\tpublic long getCountForClass(Class<?> c, Stat stat) {\n\t\treturn countsPerClass.get(c)[stat.ordinal()];\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/ConfigUtil.java",
    "content": "package org.osm2world.core.util;\n\nimport java.awt.Color;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * utility class for parsing configuration values\n */\nfinal public class ConfigUtil {\n\n\tprivate ConfigUtil() { }\n\t\n\tpublic static final String BG_COLOR_KEY = \"backgroundColor\";\n\tpublic static final String BG_IMAGE_KEY = \"backgroundImage\";\n\tpublic static final String CANVAS_LIMIT_KEY = \"canvasLimit\";\n\t\n\tpublic static final Color parseColor(String colorString) {\n\t\t\n\t\tif (colorString == null) {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\tColor color = parseColorTuple(colorString);\n\t\t\n\t\tif (color != null) {\n\t\t\treturn color;\n\t\t} else {\n\t\t\n\t\t\ttry {\n\t\t\t\treturn Color.decode(colorString);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final Pattern hsvTuplePattern = Pattern.compile(\n\t\t\"^hsv\\\\s*\\\\(\\\\s*(\\\\d{1,3})\\\\s*,\" +\n\t\t\t\"\\\\s*(\\\\d{1,3})\\\\s*%\\\\s*,\" +\n\t\t\t\"\\\\s*(\\\\d{1,3})\\\\s*%\\\\s*\\\\)\");\n\t\t\n\t/**\n\t * parses colors that are given as a color scheme identifier\n\t * with a value tuple in brackets.\n\t * \n\t * Currently only supports hsv.\n\t * \n\t * @return color; null on parsing errors\n\t */\n\tpublic static final Color parseColorTuple(String colorString) {\n\t\t\n\t\tMatcher matcher = hsvTuplePattern.matcher(colorString);\n\t\t\n\t\tif (matcher.matches()) {\n\t\t\t\n\t\t\ttry {\n\t\t\t\t\n\t\t\t\tint v1 = Integer.parseInt(matcher.group(1));\n\t\t\t\tint v2 = Integer.parseInt(matcher.group(2));\n\t\t\t\tint v3 = Integer.parseInt(matcher.group(3));\n\t\t\t\t\n\t\t\t\treturn Color.getHSBColor(v1 / 360f, v2 / 100f, v3 / 100f);\n\t\t\t\t\n\t\t\t} catch (NumberFormatException nfe) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn null;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/FaultTolerantIterationUtil.java",
    "content": "package org.osm2world.core.util;\n\n/**\n * utility class that allows iterations where Exceptions in the processing\n * of a single element don't cause program failure\n */\nfinal public class FaultTolerantIterationUtil {\n\n\tprivate FaultTolerantIterationUtil() { }\n\t\n\tpublic static interface Operation<T> {\n\t\tpublic void perform(T input);\n\t}\n\t\n\tpublic static final <T> void iterate(\n\t\t\tIterable<? extends T> collection, Operation<T> operation) {\n\n\t\tlong totalStart = TouchMapperProfile.start();\n\t\tint itemCount = 0;\n\t\tint exceptionCount = 0;\n\t\tint suppressedExceptionCount = 0;\n\t\tlong exceptionLoggingNanos = 0;\n\t\t\n\t\tfor (T input : collection) {\n\t\t\titemCount++;\n\t\t\ttry {\n\t\t\t\toperation.perform(input);\n\t\t\t} catch (Exception e) {\n\t\t\t\tlong catchStart = TouchMapperProfile.start();\n\t\t\t\texceptionCount++;\n\t\t\t\tif (IgnoredExceptionLog.shouldLogDetailed(exceptionCount)) {\n\t\t\t\t\tIgnoredExceptionLog.logDetailed(e, input);\n\t\t\t\t} else {\n\t\t\t\t\tif (suppressedExceptionCount == 0) {\n\t\t\t\t\t\tIgnoredExceptionLog.logSuppressionNotice(\n\t\t\t\t\t\t\t\t\"FaultTolerantIterationUtil.iterate\");\n\t\t\t\t\t}\n\t\t\t\t\tsuppressedExceptionCount++;\n\t\t\t\t}\n\t\t\t\tif (TouchMapperProfile.isEnabled()) {\n\t\t\t\t\texceptionLoggingNanos += (System.nanoTime() - catchStart);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tIgnoredExceptionLog.logSummary(\"FaultTolerantIterationUtil.iterate\",\n\t\t\t\texceptionCount, suppressedExceptionCount);\n\n\t\tif (TouchMapperProfile.isEnabled() && exceptionCount > 0) {\n\t\t\tTouchMapperProfile.logValue(\"fault_tolerant_iterate.items\", itemCount);\n\t\t\tTouchMapperProfile.logValue(\"fault_tolerant_iterate.exceptions\",\n\t\t\t\t\texceptionCount);\n\t\t\tTouchMapperProfile.logValue(\n\t\t\t\t\t\"fault_tolerant_iterate.suppressed_exceptions\",\n\t\t\t\t\tsuppressedExceptionCount);\n\t\t\tTouchMapperProfile.logNanosAsMillis(\n\t\t\t\t\t\"fault_tolerant_iterate.exception_logging_ms\",\n\t\t\t\t\texceptionLoggingNanos);\n\t\t\tTouchMapperProfile.logMillis(\"fault_tolerant_iterate.total_ms\", totalStart);\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/IgnoredExceptionLog.java",
    "content": "package org.osm2world.core.util;\n\n/**\n * Helper for controlled logging of repeatedly ignored exceptions.\n *\n * By default, only the first few stack traces are printed and the rest are\n * summarized to reduce stderr overhead in large conversion runs.\n */\npublic final class IgnoredExceptionLog {\n\n\tprivate static final String MAX_DETAILED_ENV =\n\t\t\t\"TOUCH_MAPPER_MAX_IGNORED_EXCEPTIONS_LOGGED\";\n\n\tprivate static final int DEFAULT_MAX_DETAILED = 3;\n\n\tprivate static final int maxDetailedExceptions = parseMaxDetailed(\n\t\t\tSystem.getenv(MAX_DETAILED_ENV));\n\n\tprivate IgnoredExceptionLog() { }\n\n\tprivate static int parseMaxDetailed(String rawValue) {\n\t\tif (rawValue == null || rawValue.trim().isEmpty()) {\n\t\t\treturn DEFAULT_MAX_DETAILED;\n\t\t}\n\t\ttry {\n\t\t\treturn Integer.parseInt(rawValue.trim());\n\t\t} catch (NumberFormatException nfe) {\n\t\t\treturn DEFAULT_MAX_DETAILED;\n\t\t}\n\t}\n\n\tpublic static int getMaxDetailedExceptions() {\n\t\treturn maxDetailedExceptions;\n\t}\n\n\tpublic static boolean shouldLogDetailed(int oneBasedExceptionIndex) {\n\t\treturn maxDetailedExceptions < 0\n\t\t\t\t|| oneBasedExceptionIndex <= maxDetailedExceptions;\n\t}\n\n\tpublic static void logDetailed(Exception e, Object input) {\n\t\tSystem.err.println(\"ignored exception:\");\n\t\te.printStackTrace();\n\t\tSystem.err.println(\"this exception occurred for the following input:\\n\"\n\t\t\t\t+ input);\n\t}\n\n\tpublic static void logSuppressionNotice(String context) {\n\t\tif (maxDetailedExceptions == 0) {\n\t\t\tSystem.err.println(\n\t\t\t\t\t\"ignored exceptions in \" + context\n\t\t\t\t\t+ \" are summarized (no stack traces).\");\n\t\t} else {\n\t\t\tSystem.err.println(\n\t\t\t\t\t\"ignored exceptions in \" + context\n\t\t\t\t\t+ \": showing first \" + maxDetailedExceptions\n\t\t\t\t\t+ \" stack traces; suppressing the rest.\");\n\t\t}\n\t}\n\n\tpublic static void logSummary(String context, int totalExceptions,\n\t\t\tint suppressedExceptions) {\n\t\tif (totalExceptions <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (suppressedExceptions > 0) {\n\t\t\tSystem.err.println(\n\t\t\t\t\t\"ignored exception summary (\" + context + \"): total=\"\n\t\t\t\t\t+ totalExceptions + \", suppressed=\" + suppressedExceptions);\n\t\t} else {\n\t\t\tSystem.err.println(\n\t\t\t\t\t\"ignored exception summary (\" + context + \"): total=\"\n\t\t\t\t\t+ totalExceptions);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/MinMaxUtil.java",
    "content": "package org.osm2world.core.util;\n\nimport com.google.common.base.Function;\n\npublic final class MinMaxUtil {\n\t\t\t\n\t/** prevents instantiation */\n\tprivate MinMaxUtil() { }\n\t\n\t/**\n\t * from an Iterable of objects, this returns the one\n\t * with the lowest associated value of a function f\n\t * \n\t * @return  any of the inputs that generate the minimum function value,\n\t *          null if objects is empty\n\t */\n\tpublic static final <T> T min(Iterable<T> objects, Function<T, Double> f) {\n\t\t\n\t\tT currentMinObject = null;\n\t\tDouble currentMinValue = null;\n\t\t\n\t\tfor (T object : objects) {\n\t\t\tif (currentMinObject == null\n\t\t\t\t\t|| f.apply(object) < currentMinValue) {\n\t\t\t\tcurrentMinObject = object;\n\t\t\t\tcurrentMinValue = f.apply(object);\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn currentMinObject;\n\t\t\n\t}\n\t\n\n\t/**\n\t * from an Iterable of objects, this returns the one\n\t * with the highest associated value of a function f\n\t * \n\t * @return  any of the inputs that generate the maximum function value,\n\t *          null if objects is empty\n\t */\n\tpublic static final <T> T max(Iterable<T> objects, Function<T, Double> f) {\n\t\t\n\t\tT currentMaxObject = null;\n\t\tDouble currentMaxValue = null;\n\t\t\n\t\tfor (T object : objects) {\n\t\t\tif (currentMaxObject == null\n\t\t\t\t\t|| f.apply(object) > currentMaxValue) {\n\t\t\t\tcurrentMaxObject = object;\n\t\t\t\tcurrentMaxValue = f.apply(object);\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn currentMaxObject;\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/Predicates.java",
    "content": "package org.osm2world.core.util;\n\nimport com.google.common.base.Predicate;\n\npublic final class Predicates {\n\t\n\t/** prevents instantiation */\n\tprivate Predicates() { }\n\t\n\t/**\n\t * returns a predicate that is true for objects that are instance of a type\n\t */\n\tpublic static final Predicate<Object> hasType(final Class<?> type) {\n\t\t\n\t\treturn new Predicate<Object>() {\t\t\t\n\t\t\t@Override\n\t\t\tpublic boolean apply(Object o) {\n\t\t\t\treturn type.isInstance(o);\n\t\t\t}\n\t\t};\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/TouchMapperProfile.java",
    "content": "package org.osm2world.core.util;\n\nimport java.util.Locale;\n\n/**\n * Minimal opt-in profiling helper for Touch Mapper performance analysis.\n *\n * Enable by setting TOUCH_MAPPER_PROFILE=1 (or true/yes/on).\n */\npublic final class TouchMapperProfile {\n\n\tprivate static final boolean ENABLED = parseEnabled(\n\t\t\tSystem.getenv(\"TOUCH_MAPPER_PROFILE\"));\n\n\tprivate TouchMapperProfile() { }\n\n\tprivate static boolean parseEnabled(String rawValue) {\n\t\tif (rawValue == null) {\n\t\t\treturn false;\n\t\t}\n\t\tString value = rawValue.trim().toLowerCase(Locale.US);\n\t\treturn \"1\".equals(value)\n\t\t\t\t|| \"true\".equals(value)\n\t\t\t\t|| \"yes\".equals(value)\n\t\t\t\t|| \"on\".equals(value);\n\t}\n\n\tpublic static boolean isEnabled() {\n\t\treturn ENABLED;\n\t}\n\n\tpublic static long start() {\n\t\treturn ENABLED ? System.nanoTime() : 0L;\n\t}\n\n\tpublic static void logMillis(String label, long startNanos) {\n\t\tif (!ENABLED) {\n\t\t\treturn;\n\t\t}\n\t\tlong elapsedNanos = System.nanoTime() - startNanos;\n\t\tlogNanosAsMillis(label, elapsedNanos);\n\t}\n\n\tpublic static void logNanosAsMillis(String label, long nanos) {\n\t\tif (!ENABLED) {\n\t\t\treturn;\n\t\t}\n\t\tSystem.err.println(String.format(Locale.US,\n\t\t\t\t\"TM_PROFILE %s %.3f\", label, nanos / 1000000.0));\n\t}\n\n\tpublic static void logValue(String label, Object value) {\n\t\tif (!ENABLED) {\n\t\t\treturn;\n\t\t}\n\t\tSystem.err.println(\"TM_PROFILE \" + label + \" \" + String.valueOf(value));\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/TriangulationConfig.java",
    "content": "package org.osm2world.core.util;\n\n/**\n * Shared triangulation-related runtime configuration.\n */\npublic final class TriangulationConfig {\n\n\tpublic static final String COLLINEAR_TOLERANCE_ENV =\n\t\t\t\"TOUCH_MAPPER_TRIANGULATION_COLLINEAR_TOLERANCE_M\";\n\n\t// Set to 0.0 to disable geometry cleanup.\n\t// 0.01 (1 cm real-world scale) is a reasonable middle ground.\n\tpublic static final double DEFAULT_COLLINEAR_TOLERANCE_M = 0.01;\n\n\tprivate static final double collinearToleranceMeters =\n\t\t\tparseCollinearToleranceMeters(System.getenv(COLLINEAR_TOLERANCE_ENV));\n\n\tprivate TriangulationConfig() { }\n\n\t/**\n\t * @return tolerance in meters for removing near-collinear vertices before\n\t *         triangulation. Returns 0 if simplification is disabled.\n\t */\n\tpublic static double getCollinearToleranceMeters() {\n\t\treturn collinearToleranceMeters;\n\t}\n\n\tstatic double parseCollinearToleranceMeters(String rawValue) {\n\t\tif (rawValue == null || rawValue.trim().isEmpty()) {\n\t\t\treturn DEFAULT_COLLINEAR_TOLERANCE_M;\n\t\t}\n\n\t\ttry {\n\t\t\tdouble parsed = Double.parseDouble(rawValue.trim());\n\t\t\treturn parsed > 0 ? parsed : 0.0;\n\t\t} catch (NumberFormatException nfe) {\n\t\t\treturn 0.0;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/exception/TriangulationException.java",
    "content": "package org.osm2world.core.util.exception;\n\n/**\n * error caused by an unsuccessful triangulation attempt\n */\npublic class TriangulationException extends Exception {\n\t\n\tpublic TriangulationException(String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n\tpublic TriangulationException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic TriangulationException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/functions/DefaultFactory.java",
    "content": "package org.osm2world.core.util.functions;\n\n\n/**\n * creates new instances of a class by calling the standard constructor.\n * In many cases, this is the simplest way of obtaining a {@link Factory}\n * for a given class.\n */\npublic class DefaultFactory<T> implements Factory<T> {\n\t\n\tprivate final Class<? extends T> c;\n\t\n\tpublic DefaultFactory(Class<? extends T> c) {\n\t\tthis.c = c;\n\t}\n\t\n\t@Override\n\tpublic T make() {\n\t\ttry {\n\t\t\treturn c.newInstance();\n\t\t} catch (InstantiationException e) {\n\t\t\tthrow new Error(e);\n\t\t} catch (IllegalAccessException e) {\n\t\t\tthrow new Error(e);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/functions/Factory.java",
    "content": "package org.osm2world.core.util.functions;\n\n/**\n * Creates instances of another object type.\n *\n * @param <T>  the type of objects created by this factory\n */\npublic interface Factory<T> {\n\t\n\t/** creates a new instance */\n\tT make();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/util/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nutility classes that are used throughout OSM2World\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/creation/NetworkCalculator.java",
    "content": "package org.osm2world.core.world.creation;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapSegment;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.world.data.NodeWorldObject;\nimport org.osm2world.core.world.network.JunctionNodeWorldObject;\nimport org.osm2world.core.world.network.NetworkWaySegmentWorldObject;\nimport org.osm2world.core.world.network.VisibleConnectorNodeWorldObject;\n\n/**\n * class that will calculate the information for those {@link MapElement}s\n * that implement an interface from org.o3dw.representation.network and set it.\n * Calling this is necessary for those representations to work properly!\n */\npublic class NetworkCalculator {\n\t\n\tprivate NetworkCalculator() {}\n\t\n\t\n\t\n\t//FIXME: must be able to handle connectors with lines of opposite direction!\n\t\n\t\n\t\n\tprivate static final float ROAD_PUSHING_STEP = 0.01f;\n\t\n\t\n\t/**\n\t * calculates cut and offset information for all\n\t * NetworkNode/Line/AreaRepresentations of elements in a grid.\n\t */\n\tpublic static void calculateNetworkInformationInGrid(MapData grid) {\n\t\t\n\t\tfor (MapNode node : grid.getMapNodes()) {\n\t\t\t\n\t\t\t\n\t\t\t//TODO: also work with nodes that aren't Network*NodeRepresentations,\n\t\t\t//  but connect two NetworkWaySegmentRep.s (invisible connectors)\n\t\t\t\n\t\t\tfinal List<MapWaySegment> inboundNLines = new ArrayList<MapWaySegment>();\n\t\t\tList<MapWaySegment> outboundNLines = new ArrayList<MapWaySegment>();\n\t\t\t\n\t\t\tfor (MapWaySegment line : node.getInboundLines()) {\n\t\t\t\tif (line.getPrimaryRepresentation() instanceof NetworkWaySegmentWorldObject) {\n\t\t\t\t\tinboundNLines.add(line);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tfor (MapWaySegment line : node.getOutboundLines()) {\n\t\t\t\tif (line.getPrimaryRepresentation() instanceof NetworkWaySegmentWorldObject) {\n\t\t\t\t\toutboundNLines.add(line);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t\n\t\t\tif (node.getPrimaryRepresentation() instanceof JunctionNodeWorldObject) {\n\n\t\t\t\t/* junctions */\n\t\t\t\t\n\t\t\t\tcalculateJunctionNodeEffects(node,\n\t\t\t\t\t\t(JunctionNodeWorldObject)node.getPrimaryRepresentation(),\n\t\t\t\t\t\tinboundNLines, outboundNLines);\n\t\t\t\t\n\t\t\t} else if (inboundNLines.size() + outboundNLines.size() == 2) {\n\t\t\t\t\n\t\t\t\t/* visible or invisible connectors */\n\t\t\t\t\n\t\t\t\tList<MapWaySegment> connectedNLines = new ArrayList<MapWaySegment>(2);\n\t\t\t\tconnectedNLines.addAll(inboundNLines);\n\t\t\t\tconnectedNLines.addAll(outboundNLines);\n\t\t\t\t\n\t\t\t\tMapWaySegment line1 = connectedNLines.get(0);\n\t\t\t\tMapWaySegment line2 = connectedNLines.get(1);\n\t\t\t\t\n\t\t\t\tcalculateConnectorNodeEffects(node.getPrimaryRepresentation(),\n\t\t\t\t\t\tline1, line2,\n\t\t\t\t\t\tinboundNLines.contains(line1),\n\t\t\t\t\t\tinboundNLines.contains(line2));\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tfor (MapWaySegment outboundNLine : outboundNLines) {\n\t\t\t\t\tsetOrthogonalCutVector(outboundNLine, true);\n\t\t\t\t}\n\n\t\t\t\tfor (MapWaySegment inboundNLine : inboundNLines) {\n\t\t\t\t\tsetOrthogonalCutVector(inboundNLine, false);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * calculates the effects of both visible and invisible connector nodes.\n\t */\n\tprivate static void calculateConnectorNodeEffects(\n\t\t\tNodeWorldObject nodeRepresentation,\n\t\t\tMapWaySegment line1, MapWaySegment line2,\n\t\t\tboolean inbound1, boolean inbound2) {\n\t\t\t\t\t\n\t\tNetworkWaySegmentWorldObject renderable1 =\n\t\t\t((NetworkWaySegmentWorldObject)line1.getPrimaryRepresentation());\n\t\tNetworkWaySegmentWorldObject renderable2 =\n\t\t\t((NetworkWaySegmentWorldObject)line2.getPrimaryRepresentation());\n\t\t\n\t\tVisibleConnectorNodeWorldObject visibleConnectorRep = null;\n\t\t\n\t\tif (nodeRepresentation instanceof VisibleConnectorNodeWorldObject) {\n\t\t\tvisibleConnectorRep =\n\t\t\t\t(VisibleConnectorNodeWorldObject)nodeRepresentation;\n\t\t}\n\t\t\n\t\t/* calculate cut as angle bisector between the two lines */\n\t\t\n\t\tVectorXZ inVector = line1.getDirection();\n\t\tVectorXZ outVector = line2.getDirection();\n\n\t\tif (!inbound1) { inVector = inVector.invert(); }\n\t\tif (inbound2) { outVector = outVector.invert(); }\n\t\t\n\t\tVectorXZ cutVector;\n\t\t\n\t\tif (inVector.equals(outVector)) { //TODO: allow for some small difference?\n\t\t\tcutVector = outVector.rightNormal();\n\t\t} else {\n\t\t\tcutVector = outVector.subtract(inVector);\n\t\t\tcutVector = cutVector.normalize();\n\t\t}\n\t\t\n\t\t//make sure that cutVector points to the right, which is equivalent to:\n\t\t//y component of the cross product (inVector x cutVector) is positive.\n\t\t//If this isn't the case, invert the cut vector.\n\t\tif (inVector.z * cutVector.x - inVector.x * cutVector.z <= 0) {\n\t\t\tcutVector = cutVector.invert();\n\t\t}\n\t\t\n\t\t/* set calculated cut vector */\n\t\t\n\t\tif (inbound1) {\n\t\t\trenderable1.setEndCutVector(cutVector);\n\t\t} else {\n\t\t\trenderable1.setStartCutVector(cutVector.invert());\n\t\t}\n\t\t\n\t\tif (inbound2) {\n\t\t\trenderable2.setEndCutVector(cutVector.invert());\n\t\t} else {\n\t\t\trenderable2.setStartCutVector(cutVector);\n\t\t}\n\t\t\n\t\t/* perform calculations necessary for connectors\n\t\t * whose representation requires space */\n\t\t\n\t\tdouble connectorLength = 0;\n\t\t\n\t\tif (visibleConnectorRep != null) {\n\t\t\tconnectorLength = visibleConnectorRep.getLength();\n\t\t}\n\t\t\n\t\tif (connectorLength > 0) {\n\t\t\t\t\t\t\t\t\t\n\t\t\t/* move connected lines to make room for the node's representation */\n\t\t\t\n\t\t\t//connected node of line1 is moved orthogonally to the cut vector\n\t\t\tVectorXZ offset1 = cutVector.rightNormal();\n\t\t\toffset1 = offset1.mult(connectorLength / 2);\n\t\t\tif (inbound1) {\n\t\t\t\trenderable1.setEndOffset(offset1);\n\t\t\t} else {\n\t\t\t\trenderable1.setStartOffset(offset1);\n\t\t\t}\n\t\t\t\n\t\t\t//node of line2 is moved into the opposite direction\n\t\t\tVectorXZ offset2 = offset1.invert();\n\t\t\tif (inbound2) {\n\t\t\t\trenderable2.setEndOffset(offset2);\n\t\t\t} else {\n\t\t\t\trenderable2.setStartOffset(offset2);\n\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t/* provide information to node's representation */\n\t\t\t\n\t\t\tif (nodeRepresentation instanceof VisibleConnectorNodeWorldObject) {\n\t\t\t\t\n\t\t\t\tVisibleConnectorNodeWorldObject connectorRep =\n\t\t\t\t\t(VisibleConnectorNodeWorldObject)nodeRepresentation;\n\t\t\t\t\n\t\t\t\tVectorXZ connectedPos1;\n\t\t\t\tVectorXZ connectedPos2;\n\t\t\t\t\n\t\t\t\tif (inbound1) {\n\t\t\t\t\tconnectedPos1 = line1.getEndNode().getPos();\n\t\t\t\t} else {\n\t\t\t\t\tconnectedPos1 = line1.getStartNode().getPos();\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (inbound2) {\n\t\t\t\t\tconnectedPos2 = line2.getEndNode().getPos();\n\t\t\t\t} else {\n\t\t\t\t\tconnectedPos2 = line2.getStartNode().getPos();\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tconnectorRep.setInformation(\n\t\t\t\t\t\tcutVector,\n\t\t\t\t\t\tconnectedPos1.add(offset1),\n\t\t\t\t\t\tconnectedPos2.add(offset2),\n\t\t\t\t\t\trenderable1.getWidth(),\n\t\t\t\t\t\trenderable2.getWidth());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t//TODO: if done properly, this might affect NOT ONLY the directly adjacent lines\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tprivate static void calculateJunctionNodeEffects(\n\t\t\tMapNode node, JunctionNodeWorldObject nodeRepresentation,\n\t\t\tfinal List<MapWaySegment> inboundNLines, List<MapWaySegment> outboundNLines) {\n\t\t\n\t\t/* create list of all connected roads.\n\t\t * Order of adds is important, it needs to match\n\t\t * the order of cutVectors, coords and widths adds. */\n\t\t\n\t\tList<MapWaySegment> connectedNSegments = new ArrayList<MapWaySegment>();\n\t\tconnectedNSegments.addAll(inboundNLines);\n\t\tconnectedNSegments.addAll(outboundNLines);\n\t\t\t\t\n\t\t//all cut vectors in here will point to the right from the junctions pov!\n\t\tList<VectorXZ> cutVectors = new ArrayList<VectorXZ>(connectedNSegments.size());\n\t\tList<VectorXZ> coords = new ArrayList<VectorXZ>(connectedNSegments.size());\n\t\tList<Float> widths = new ArrayList<Float>(connectedNSegments.size());\n\t\t\n\t\t/* determine cut angles:\n\t\t * always orthogonal to the connected line */\n\t\t\t\t\n\t\tfor (MapWaySegment in : inboundNLines) {\n\t\t\t\n\t\t\tNetworkWaySegmentWorldObject inRenderable =\n\t\t\t\t((NetworkWaySegmentWorldObject)in.getPrimaryRepresentation());\n\t\t\t\n\t\t\tVectorXZ cutVector = in.getRightNormal();\n\t\t\tinRenderable.setEndCutVector(cutVector);\n\t\t\tcutVectors.add(cutVector.invert());\n\t\t\t\n\t\t\tcoords.add(in.getEndNode().getPos());\n\t\t\twidths.add(inRenderable.getWidth());\n\t\t\t\n\t\t}\n\n\t\tfor (MapWaySegment out : outboundNLines) {\n\t\t\t\n\t\t\tNetworkWaySegmentWorldObject outRenderable =\n\t\t\t\t((NetworkWaySegmentWorldObject)out.getPrimaryRepresentation());\n\t\t\t\n\t\t\tVectorXZ cutVector = out.getRightNormal();\n\t\t\toutRenderable.setStartCutVector(cutVector);\n\t\t\tcutVectors.add(cutVector);\n\t\t\t\n\t\t\tcoords.add(out.getStartNode().getPos());\n\t\t\twidths.add(outRenderable.getWidth());\n\t\t\t\n\t\t}\n\t\t\n\t\t/* move roads away from the intersection until they cannot overlap anymore,\n\t\t * this is certain if the distance between their ends' center points\n\t\t * is greater than the sum of their half-widths */\n\t\t\n\t\t//TODO (performance) if roads were ordered by angle here already, this would be much faster -> only neighbors checked\n\t\t\n\t\tboolean overlapPossible;\n\t\t\n\t\tdo {\n\t\t\t\n\t\t\toverlapPossible = false;\n\t\t\t\n\t\t\toverlapCheck:\n\t\t\tfor (int r1=0; r1 < coords.size(); r1++) {\n\t\t\t\tfor (int r2=r1+1; r2 < coords.size(); r2++) {\n\t\t\t\t\t\n\t\t\t\t\t/* ignore overlapping (or almost overlapping) way segments\n\t\t\t\t\t * as no reasonable amount of pushing would separate these */\n\t\t\t\t\tif (VectorXZ.distance(connectedNSegments.get(r1).getDirection(),\n\t\t\t\t\t\t\tconnectedNSegments.get(r2).getDirection()) < 0.1\n\t\t\t\t\t\t||\tVectorXZ.distance(connectedNSegments.get(r1).getDirection(),\n\t\t\t\t\t\t\t\tconnectedNSegments.get(r2).getDirection().invert()) < 0.1) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tdouble distance = Math.abs(coords.get(r1).subtract(coords.get(r2)).length());\n\t\t\t\t\t\n\t\t\t\t\tif (distance > 200) {\n\t\t\t\t\t\t//TODO: proper error handling\n\t\t\t\t\t\tSystem.err.println(\"distance has exceeded 200 at node \" + node\n\t\t\t\t\t\t\t\t+ \"\\n (representation: \" + nodeRepresentation + \")\");\n\t\t\t\t\t\t// overlapCheck will remain false, no further size increase\n\t\t\t\t\t\tbreak overlapCheck;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (distance <= widths.get(r1)*0.5 + widths.get(r2)*0.5) {\n\t\t\t\t\t\toverlapPossible = true;\n\t\t\t\t\t\tbreak overlapCheck;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\n\t\t\tif (overlapPossible) {\n\t\t\t\t\n\t\t\t\t/* push outwards */\n\t\t\t\t\n\t\t\t\tcoords.clear();\n\t\t\t\t\n\t\t\t\tfor (MapWaySegment in : inboundNLines) {\n\t\t\t\t\t\n\t\t\t\t\tNetworkWaySegmentWorldObject inRenderable =\n\t\t\t\t\t\t((NetworkWaySegmentWorldObject)in.getPrimaryRepresentation());\n\t\t\t\t\tVectorXZ inVector = in.getDirection();\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ offsetModification = inVector.mult(-ROAD_PUSHING_STEP);\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ newEndOffset = inRenderable.getEndOffset().add(offsetModification);\n\t\t\t\t\tinRenderable.setEndOffset(newEndOffset);\n\t\t\t\t\tcoords.add(in.getEndNode().getPos().add(newEndOffset));\n\t\t\t\t\t\n\t\t\t\t}\n\n\t\t\t\tfor (MapWaySegment out : outboundNLines) {\n\t\t\t\t\t\n\t\t\t\t\tNetworkWaySegmentWorldObject outRenderable =\n\t\t\t\t\t\t((NetworkWaySegmentWorldObject)out.getPrimaryRepresentation());\n\t\t\t\t\tVectorXZ outVector = out.getDirection();\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ offsetModification = outVector.mult(ROAD_PUSHING_STEP);\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ newStartOffset = outRenderable.getStartOffset().add(offsetModification);\n\t\t\t\t\toutRenderable.setStartOffset(newStartOffset);\n\t\t\t\t\tcoords.add(out.getStartNode().getPos().add(newStartOffset));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t} while(overlapPossible);\n\n\t\t/* set calculated information using the correct order */\n\t\t\n\t\tList<MapSegment> segments = node.getConnectedSegments();\n\t\t\t\t\n\t\tArrayList<VectorXZ> junctionCutCenters = new ArrayList<VectorXZ>(segments.size());\n\t\tArrayList<VectorXZ> junctionCutVectors = new ArrayList<VectorXZ>(segments.size());\n\t\tArrayList<Float> junctionWidths = new ArrayList<Float>(segments.size());\n\t\t\n\t\tfor (MapSegment segment : segments) {\n\t\t\t\n\t\t\tif (connectedNSegments.contains(segment)) {\n\n\t\t\t\tint index = connectedNSegments.indexOf(segment);\n\t\t\t\t\n\t\t\t\tjunctionCutCenters.add(coords.get(index));\n\t\t\t\tjunctionCutVectors.add(cutVectors.get(index));\n\t\t\t\tjunctionWidths.add(widths.get(index));\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tjunctionCutCenters.add(null);\n\t\t\t\tjunctionCutVectors.add(null);\n\t\t\t\tjunctionWidths.add(null);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tnodeRepresentation.setInformation(\n\t\t\t\tjunctionCutCenters, junctionCutVectors, junctionWidths);\n\t\t\n\t}\n\n\t/**\n\t * @param l  line with {@link NetworkWaySegmentWorldObject} as representation\n\t */\n\tprivate static void setOrthogonalCutVector(MapWaySegment l, boolean setStartVector) {\n\n\t\tVectorXZ cutVector = l.getRightNormal();\n\t\t\n\t\tNetworkWaySegmentWorldObject lRepresentation =\n\t\t\t(NetworkWaySegmentWorldObject)l.getPrimaryRepresentation();\n\t\t\n\t\tif (setStartVector) {\n\t\t\tlRepresentation.setStartCutVector(cutVector);\n\t\t} else {\n\t\t\tlRepresentation.setEndCutVector(cutVector);\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/creation/WorldCreator.java",
    "content": "package org.osm2world.core.world.creation;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.util.TouchMapperProfile;\n\npublic class WorldCreator {\n\n\tprivate Collection<WorldModule> modules;\n\t\t\n\tpublic WorldCreator(Configuration config, WorldModule... modules) {\n\t\tthis(config, Arrays.asList(modules));\n\t}\n\t\n\tpublic WorldCreator(Configuration config, List<WorldModule> modules) {\n\t\tthis.modules = modules;\n\t\tfor (WorldModule module : modules) {\n\t\t\tmodule.setConfiguration(config);\n\t\t}\n\t}\n\t\n\tpublic void addRepresentationsTo(MapData mapData) {\n\n\t\tlong totalStart = TouchMapperProfile.start();\n\t\t\n\t\tfor (WorldModule module : modules) {\n\t\t\tlong moduleStart = TouchMapperProfile.start();\n\t\t\tmodule.applyTo(mapData);\n\t\t\tTouchMapperProfile.logMillis(\"representation.module.\"\n\t\t\t\t\t+ module.getClass().getSimpleName() + \"_ms\", moduleStart);\n\t\t}\n\t\t\n\t\tlong networkStart = TouchMapperProfile.start();\n\t\tNetworkCalculator.calculateNetworkInformationInGrid(mapData);\n\t\tTouchMapperProfile.logMillis(\"representation.network_calculation_ms\",\n\t\t\t\tnetworkStart);\n\t\tTouchMapperProfile.logMillis(\"representation.total_ms\", totalStart);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/creation/WorldModule.java",
    "content": "package org.osm2world.core.world.creation;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.world.data.WorldObject;\n\npublic interface WorldModule {\n\t\n\t/**\n\t * provides a {@link Configuration} that can be used to control aspects\n\t * of a WorldModule's behavior.\n\t * \n\t * This is guaranteed to be called before {@link #applyTo(MapData)},\n\t * but not all parameters might be explicitly set in the configuration,\n\t * so defaults need to be available.\n\t */\n\tpublic void setConfiguration(Configuration config);\n\t\n\t/**\n\t * adds {@link WorldObject}s to {@link MapElement}s\n\t */\n\tpublic void applyTo(MapData mapData);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/AbstractAreaWorldObject.java",
    "content": "package org.osm2world.core.world.data;\n\nimport java.util.Collection;\nimport java.util.Collections;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnectorGroup;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.algorithms.JTSTriangulationUtil;\nimport org.osm2world.core.math.algorithms.Poly2TriTriangulationUtil;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\nimport org.osm2world.core.util.exception.TriangulationException;\n\n/**\n * implementation of {@link AreaWorldObject} that offers some basic features:\n * < ul><li> providing the object outline based on the {@link MapArea}\n * </li><li> providing bounding geometry for intersection tests\n * </li><li> calculating a triangulation of the surface for rendering\n * </ul>\n */\npublic abstract class AbstractAreaWorldObject\n\timplements WorldObjectWithOutline, AreaWorldObject,\n\t\tIntersectionTestObject {\n\t\n\tprotected final MapArea area;\n\t\n\tprivate final SimplePolygonXZ outlinePolygonXZ;\n\t\n\tprivate EleConnectorGroup connectors;\n\t\n\tprotected AbstractAreaWorldObject(MapArea area) {\n\t\t\n\t\tthis.area = area;\n\t\t\n\t\toutlinePolygonXZ = area.getPolygon().getOuter().makeCounterclockwise();\n\t\t\n\t}\n\t\n\t@Override\n\tpublic EleConnectorGroup getEleConnectors() {\n\t\t\n\t\tif (connectors == null) {\n\t\t\t\n\t\t\tconnectors = new EleConnectorGroup();\n\t\t\t\n\t\t\tconnectors.addConnectorsForTriangulation(\n\t\t\t\t\tgetTriangulationXZ(), null, getGroundState());\n\t\t\t\n\t\t}\n\t\t\n\t\treturn connectors;\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {}\n\t\n\t@Override\n\tpublic SimplePolygonXZ getOutlinePolygonXZ() {\n\t\treturn outlinePolygonXZ;\n\t}\n\n\t@Override\n\tpublic PolygonXYZ getOutlinePolygon() {\n\t\treturn connectors.getPosXYZ(outlinePolygonXZ);\n\t}\n\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\treturn new AxisAlignedBoundingBoxXZ(\n\t\t\t\tarea.getOuterPolygon().getVertexCollection());\n\t}\n\t\n\t@Override\n\tpublic final MapArea getPrimaryMapElement() {\n\t\treturn area;\n\t}\n\n\t/**\n\t * decompose this area into counterclockwise triangles.\n\t */\n\tprotected Collection<TriangleXZ> getTriangulationXZ() {\n\t\t\n\t\ttry {\n\t\t\n\t\t\treturn Poly2TriTriangulationUtil.triangulate(\n\t\t\t\tarea.getPolygon().getOuter(),\n\t\t\t\tarea.getPolygon().getHoles(),\n\t\t\t\tCollections.<LineSegmentXZ>emptyList(),\n\t\t\t\tCollections.<VectorXZ>emptyList());\n\t\t\t\n\t\t} catch (TriangulationException e) {\n\t\t\t\n\t\t\tSystem.err.println(\"Poly2Tri exception for \" + this + \":\");\n\t\t\te.printStackTrace();\n\t\t\tSystem.err.println(\"... falling back to JTS triangulation.\");\n\t\t\t\n\t\t\treturn JTSTriangulationUtil.triangulate(\n\t\t\t\t\tarea.getPolygon().getOuter(),\n\t\t\t\t\tarea.getPolygon().getHoles(),\n\t\t\t\t\tCollections.<LineSegmentXZ>emptyList(),\n\t\t\t\t\tCollections.<VectorXZ>emptyList());\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * decompose this area into counterclockwise 3d triangles.\n\t * Only available after elevation calculation.\n\t */\n\tprotected Collection<TriangleXYZ> getTriangulation() {\n\t\treturn connectors.getTriangulationXYZ(getTriangulationXZ());\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn this.getClass().getSimpleName() + \"(\" + area + \")\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/AreaWorldObject.java",
    "content": "package org.osm2world.core.world.data;\n\nimport org.osm2world.core.map_data.data.MapArea;\n\npublic interface AreaWorldObject extends WorldObject {\n\t\n\t@Override\n\tpublic MapArea getPrimaryMapElement();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/NoOutlineNodeWorldObject.java",
    "content": "package org.osm2world.core.world.data;\n\nimport static java.util.Collections.singleton;\n\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\n\n/**\n * superclass for {@link NodeWorldObject}s that don't have an outline,\n * and are not part of a network.\n * Instead, they are located at a single point on the terrain or other areas\n * and not connected to other features.\n * \n * @see OutlineNodeWorldObject\n */\npublic abstract class NoOutlineNodeWorldObject implements NodeWorldObject,\n\t\tIntersectionTestObject {\n\t\n\tprotected final MapNode node;\n\t\n\tprivate final EleConnector connector;\n\t\n\tpublic NoOutlineNodeWorldObject(MapNode node) {\n\t\tthis.node = node;\n\t\tthis.connector = new EleConnector(node.getPos(), node,\n\t\t\t\tgetGroundState());\n\t}\n\t\n\t@Override\n\tpublic final MapNode getPrimaryMapElement() {\n\t\treturn node;\n\t}\n\t\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\treturn new AxisAlignedBoundingBoxXZ(singleton(node.getPos()));\n\t}\n\t\n\t@Override\n\tpublic Iterable<EleConnector> getEleConnectors() {\n\t\treturn singleton(connector);\n\t}\n\n\t@Override\n\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn this.getClass().getSimpleName() + \"(\" + node + \")\";\n\t}\n\n\t/**\n\t * provides subclasses with the 3d position of the {@link MapNode}.\n\t * Only works during rendering (i.e. after elevation calculation).\n\t */\n\tprotected VectorXYZ getBase() {\n\t\treturn connector.getPosXYZ();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/NoOutlineWaySegmentWorldObject.java",
    "content": "package org.osm2world.core.world.data;\n\nimport static java.util.Arrays.asList;\n\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\n\n/**\n * superclass for {@link WaySegmentWorldObject}s that don't have an outline,\n * and are not part of a network.\n * Instead, they can be considered infinitely thin.\n */\npublic abstract class NoOutlineWaySegmentWorldObject\n\t\timplements WaySegmentWorldObject, IntersectionTestObject {\n\t\n\tprotected final MapWaySegment segment;\n\t\n\tprivate final EleConnector startConnector;\n\tprivate final EleConnector endConnector;\n\t\n\tpublic NoOutlineWaySegmentWorldObject(MapWaySegment segment) {\n\t\t\n\t\tthis.segment = segment;\n\t\t\n\t\tstartConnector = new EleConnector(getStartPosition(),\n\t\t\t\tsegment.getStartNode(), getGroundState());\n\t\tendConnector = new EleConnector(getEndPosition(),\n\t\t\t\tsegment.getEndNode(), getGroundState());\n\t\t\n\t}\n\t\n\t@Override\n\tpublic final MapWaySegment getPrimaryMapElement() {\n\t\treturn segment;\n\t}\n\t\n\t@Override\n\tpublic Iterable<EleConnector> getEleConnectors() {\n\t\treturn asList(startConnector, endConnector);\n\t}\n\t\n\t@Override\n\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {}\n\t\n\t@Override\n\tpublic VectorXZ getStartPosition() {\n\t\treturn segment.getStartNode().getPos();\n\t}\n\t\n\t@Override\n\tpublic VectorXZ getEndPosition() {\n\t\treturn segment.getEndNode().getPos();\n\t}\n\t\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\treturn new AxisAlignedBoundingBoxXZ(asList(\n\t\t\t\tgetStartPosition(), getEndPosition()));\n\t}\n\t\n\t/**\n\t * returns the 3d start position.\n\t * Only available after elevation calculation.\n\t */\n\tprotected VectorXYZ getStartXYZ() {\n\t\treturn startConnector.getPosXYZ();\n\t}\n\t\n\t/**\n\t * returns the 3d end position.\n\t * Only available after elevation calculation.\n\t */\n\tprotected VectorXYZ getEndXYZ() {\n\t\treturn endConnector.getPosXYZ();\n\t}\n\t\n\t/**\n\t * returns the 3d vertex sequence running along the segment.\n\t * Only available after elevation calculation.\n\t */\n\tprotected List<VectorXYZ> getBaseline() {\n\t\treturn asList(getStartXYZ(), getEndXYZ());\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/NodeWorldObject.java",
    "content": "package org.osm2world.core.world.data;\n\nimport org.osm2world.core.map_data.data.MapNode;\n\npublic interface NodeWorldObject extends WorldObject {\n\t\n\t@Override\n\tpublic MapNode getPrimaryMapElement();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/OutlineNodeWorldObject.java",
    "content": "package org.osm2world.core.world.data;\n\nimport static java.util.Collections.emptyList;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\n\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnectorGroup;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.algorithms.TriangulationUtil;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\n\n/**\n * superclass for {@link NodeWorldObject}s that do have an outline\n * and are not just treated as an infinitely small point.\n * \n * @see NoOutlineNodeWorldObject\n */\npublic abstract class OutlineNodeWorldObject implements NodeWorldObject,\n\t\tIntersectionTestObject, WorldObjectWithOutline {\n\n\tprotected final MapNode node;\n\n\tprivate EleConnectorGroup connectors = null;\n\t\n\tprotected OutlineNodeWorldObject(MapNode node) {\n\t\tthis.node = node;\n\t}\n\t\n\t@Override\n\tpublic abstract SimplePolygonXZ getOutlinePolygonXZ();\n\t\n\t@Override\n\tpublic final MapNode getPrimaryMapElement() {\n\t\treturn node;\n\t}\n\t\n\t@Override\n\tpublic EleConnectorGroup getEleConnectors() {\n\t\t\n\t\tif (connectors == null) {\n\t\t\t\n\t\t\tSimplePolygonXZ outlinePolygonXZ = getOutlinePolygonXZ();\n\t\t\t\n\t\t\tif (outlinePolygonXZ == null) {\n\t\t\t\t\n\t\t\t\tconnectors = EleConnectorGroup.EMPTY;\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tconnectors = new EleConnectorGroup();\n\t\t\t\tconnectors.addConnectorsFor(outlinePolygonXZ.getVertices(),\n\t\t\t\t\t\tnode, getGroundState());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn connectors;\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {}\n\t\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\tif (getOutlinePolygonXZ() != null) {\n\t\t\treturn new AxisAlignedBoundingBoxXZ(\n\t\t\t\t\tgetOutlinePolygonXZ().getVertexCollection());\n\t\t} else {\n\t\t\treturn new AxisAlignedBoundingBoxXZ(\n\t\t\t\t\tnode.getPos().x, node.getPos().z,\n\t\t\t\t\tnode.getPos().x, node.getPos().z);\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic PolygonXYZ getOutlinePolygon() {\n\t\treturn connectors.getPosXYZ(getOutlinePolygonXZ());\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn this.getClass().getSimpleName() + \"(\" + node + \")\";\n\t}\n\t\n\t/**\n\t * @return  a triangulation of the area covered by this junction\n\t */\n\tprotected Collection<TriangleXYZ> getTriangulation() {\n\t\t\n\t\tif (getOutlinePolygonXZ() == null) return emptyList();\n\t\t\n\t\tCollection<TriangleXZ> trianglesXZ = TriangulationUtil.triangulate(\n\t\t\t\tgetOutlinePolygonXZ(),\n\t\t\t\tCollections.<SimplePolygonXZ>emptyList());\n\t\t\n\t\tCollection<TriangleXYZ> trianglesXYZ =\n\t\t\tnew ArrayList<TriangleXYZ>(trianglesXZ.size());\n\t\t\n\t\tfor (TriangleXZ triangleXZ : trianglesXZ) {\n\t\t\tVectorXYZ v1 = connectors.getPosXYZ(triangleXZ.v1);\n\t\t\tVectorXYZ v2 = connectors.getPosXYZ(triangleXZ.v2);\n\t\t\tVectorXYZ v3 = connectors.getPosXYZ(triangleXZ.v3);\n\t\t\tif (triangleXZ.isClockwise()) {\n\t\t\t\ttrianglesXYZ.add(new TriangleXYZ(v3, v2, v1));\n\t\t\t} else  {\n\t\t\t\ttrianglesXYZ.add(new TriangleXYZ(v1, v2, v3));\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn trianglesXYZ;\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/TerrainBoundaryWorldObject.java",
    "content": "package org.osm2world.core.world.data;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\n\n/**\n * area covered by representations with this interface will not be\n * covered by terrain if the representation is on the ground\n * (according to {@link WorldObject#getGroundState()}).\n */\npublic interface TerrainBoundaryWorldObject\n\textends WorldObjectWithOutline, IntersectionTestObject {\n\t\n\t//TODO: multipolygon support -> requires retrieving the inner polygons\n\t// (preferably, the supertype WorldObjectWithOutline should be modified)\n\t\n\t/**\n\t * returns the axis aligned bounding box that contains the entire object\n\t */\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/WaySegmentWorldObject.java",
    "content": "package org.osm2world.core.world.data;\n\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.VectorXZ;\n\npublic interface WaySegmentWorldObject extends WorldObject {\n\n\t@Override\n\tpublic MapWaySegment getPrimaryMapElement();\n\t\n\t/**\n\t * returns the start position.\n\t * Might be different from {@link MapWaySegment}'s start position;\n\t * as node features such as crossings require space, too.\n\t */\n\tpublic VectorXZ getStartPosition();\n\t\n\t/**\n\t * returns the end position.\n\t * See {@link #getStartPosition()} for details.\n\t */\n\tpublic VectorXZ getEndPosition();\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/WorldObject.java",
    "content": "package org.osm2world.core.world.data;\n\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.map_elevation.data.GroundState;\n\npublic interface WorldObject {\n\t\n\t/**\n\t * returns the \"primary\" {@link MapElement} for this WorldObject;\n\t * i.e. the one it is most strongly associated with.\n\t * Can be null if there is no (clear) primary element for this feature.\n\t */\n\tpublic MapElement getPrimaryMapElement();\n\t\n\t/**\n\t * returns whether this feature is on, above or below the ground.\n\t * This is relevant for elevation calculations,\n\t * because the elevation of features o.t.g. is directly\n\t * determined by terrain elevation data.\n\t * Elevation of features above/below t.g. depends on elevation of\n\t * features o.t.g. as well as other features above/below t.g.\n\t */\n\tpublic GroundState getGroundState();\n\t\n\t/**\n\t * returns all {@link EleConnector}s used by this WorldObject\n\t */\n\tpublic Iterable<EleConnector> getEleConnectors();\n\t\n\t/**\n\t * lets this object add constraints for the relative elevations of its\n\t * {@link EleConnector}s. Called after {@link #getEleConnectors()}.\n\t */\n\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer);\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/data/WorldObjectWithOutline.java",
    "content": "package org.osm2world.core.world.data;\n\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\n\n/**\n * world object that has a defined outline that can be used for purposes\n * such as cutting holes into the terrain (if this is instance of\n * {@link TerrainBoundaryWorldObject}), cutting tunnels through buildings,\n * preventing bridge pillars from piercing through this WorldObject ...\n */\npublic interface WorldObjectWithOutline extends WorldObject {\n\n\t/**\n\t * returns a counterclockwise polygon defining the object's ground footprint.\n\t * \n\t * @return outline polygon; null if this world object doesn't cover any area\n\t */\n\tpublic PolygonXYZ getOutlinePolygon();\n\n\t/**\n\t * returns a counterclockwise polygon defining the object's ground footprint,\n\t * projected onto the XZ plane.\n\t * \n\t * @return outline polygon; null if this world object doesn't cover any area\n\t */\n\tpublic SimplePolygonXZ getOutlinePolygonXZ();\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/BarrierModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.nCopies;\nimport static org.osm2world.core.target.common.material.Materials.CHAIN_LINK_FENCE;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.*;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.texCoordLists;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.*;\nimport static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.GeometryUtil;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.NoOutlineNodeWorldObject;\nimport org.osm2world.core.world.modules.common.AbstractModule;\nimport org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject;\n\n/**\n * adds barriers to the world\n */\npublic class BarrierModule extends AbstractModule {\n\t\n\t@Override\n\tprotected void applyToWaySegment(MapWaySegment line) {\n\n\t\tTagGroup tags = line.getTags();\n\t\tif (!tags.containsKey(\"barrier\")) return; //fast exit for common case\n\t\t\n\t\tif (Wall.fits(tags)) {\n\t\t\tline.addRepresentation(new Wall(line));\n\t\t} else if (CityWall.fits(tags)) {\n\t\t\tline.addRepresentation(new CityWall(line));\n\t\t} else if (Hedge.fits(tags)) {\n\t\t\tline.addRepresentation(new Hedge(line));\n\t\t} else if (ChainLinkFence.fits(tags)) {\n\t\t\tline.addRepresentation(new ChainLinkFence(line, tags));\n\t\t} else if (Fence.fits(tags)) {\n\t\t\tline.addRepresentation(new Fence(line, tags));\n\t\t}\n\t\t\t\n\t}\n\t\n\t@Override\n\tprotected void applyToNode(MapNode node) {\n\n\t\tTagGroup tags = node.getTags();\n\t\tif (!tags.containsKey(\"barrier\") && !tags.containsKey(\"power\")) return; //fast exit for common case\n\n\t\tif (Bollard.fits(tags)) {\n\t\t\tnode.addRepresentation(new Bollard(node, tags));\n\t\t}\n\n\t\t\n\t}\n\t\n\tprivate static abstract class LinearBarrier\n\t\textends AbstractNetworkWaySegmentWorldObject\n\t\timplements RenderableToAllTargets {\n\t\t\t\t\n\t\tprotected final float height;\n\t\tprotected final float width;\n\t\t\n\t\tpublic LinearBarrier(MapWaySegment waySegment,\n\t\t\t\tfloat defaultHeight, float defaultWidth) {\n\t\t\t\n\t\t\tsuper(waySegment);\n\t\t\t\t\t\t\n\t\t\theight = parseHeight(waySegment.getOsmWay().tags, defaultHeight);\n\t\t\twidth = parseWidth(waySegment.getOsmWay().tags, defaultWidth);\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic VectorXZ getStartPosition() {\n\t\t\treturn segment.getStartNode().getPos();\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic VectorXZ getEndPosition() {\n\t\t\treturn segment.getEndNode().getPos();\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON; //TODO: flexible ground states\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic float getWidth() {\n\t\t\treturn width;\n\t\t}\n\t\t\t\t\n\t}\n\t\t\n\t/**\n\t * superclass for linear barriers that are a simple colored \"wall\"\n\t * (use width and height to create vertical sides and a flat top)\n\t */\n\tprivate static abstract class ColoredWall extends LinearBarrier {\n\t\t\n\t\tprivate final Material material;\n\t\t\n\t\tpublic ColoredWall(Material material, MapWaySegment segment,\n\t\t\t\tfloat defaultHeight, float defaultWidth) {\n\t\t\tsuper(segment, 1, 0.5f);\n\t\t\tthis.material = material;\n\t\t}\n\t\t\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t//TODO: join ways back together to reduce the number of caps\n\t\t\t\n\t\t\tList<VectorXYZ> wallShape = asList(\n\t\t\t\tnew VectorXYZ(-width/2, 0, 0),\n\t\t\t\tnew VectorXYZ(-width/2, height, 0),\n\t\t\t\tnew VectorXYZ(+width/2, height, 0),\n\t\t\t\tnew VectorXYZ(+width/2, 0, 0)\n\t\t\t);\n\t\t\t\n\t\t\tList<VectorXYZ> path = getCenterline();\n\t\t\t\n\t\t\tList<List<VectorXYZ>> strips = createShapeExtrusionAlong(wallShape,\n\t\t\t\t\tpath, nCopies(path.size(), VectorXYZ.Y_UNIT));\n\t\t\t\n\t\t\tfor (List<VectorXYZ> strip : strips) {\n\t\t\t\ttarget.drawTriangleStrip(material, strip,\n\t\t\t\t\t\ttexCoordLists(strip, material, STRIP_WALL));\n\t\t\t}\n\t\t\t\n\t\t\t/* draw caps */\n\t\t\t\n\t\t\tList<VectorXYZ> startCap = transformShape(wallShape,\n\t\t\t\t\tpath.get(0),\n\t\t\t\t\tsegment.getDirection().xyz(0),\n\t\t\t\t\tVectorXYZ.Y_UNIT);\n\t\t\tList<VectorXYZ> endCap = transformShape(wallShape,\n\t\t\t\t\tpath.get(path.size()-1),\n\t\t\t\t\tsegment.getDirection().invert().xyz(0),\n\t\t\t\t\tVectorXYZ.Y_UNIT);\n\t\t\t\n\t\t\tList<List<VectorXZ>> texCoordLists =\n\t\t\t\ttexCoordLists(wallShape, material, GLOBAL_X_Y);\n\t\t\t\n\t\t\ttarget.drawConvexPolygon(material, startCap, texCoordLists);\n\t\t\ttarget.drawConvexPolygon(material, endCap, texCoordLists);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class Wall extends ColoredWall {\n\t\t\n\t\tpublic static boolean fits(TagGroup tags) {\n\t\t\treturn tags.contains(\"barrier\", \"wall\");\n\t\t}\n\t\t\n\t\tprivate static Material getMaterial(TagGroup tags) {\n\t\t\t\n\t\t\tMaterial material = null;\n\t\t\t\n\t\t\tif (\"gabion\".equals(tags.getValue(\"wall\"))) {\n\t\t\t\tmaterial = Materials.WALL_GABION;\n\t\t\t} else if ( tags.containsKey(\"material\") ) {\n\t\t\t\tmaterial = Materials.getMaterial(tags.getValue(\"material\").toUpperCase());\n\t\t\t}\n\t\t\t\n\t\t\tif (material != null) {\n\t\t\t\treturn material;\n\t\t\t} else {\n\t\t\t\treturn Materials.WALL_DEFAULT;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic Wall(MapWaySegment segment) {\n\t\t\tsuper(getMaterial(segment.getTags()), segment, 1f, 0.25f);\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class CityWall extends ColoredWall {\n\t\tpublic static boolean fits(TagGroup tags) {\n\t\t\treturn tags.contains(\"barrier\", \"city_wall\");\n\t\t}\n\t\tpublic CityWall(MapWaySegment segment) {\n\t\t\tsuper(Materials.WALL_DEFAULT, segment, 10, 2);\n\t\t}\n\t}\n\t\n\tprivate static class Hedge extends ColoredWall {\n\t\tpublic static boolean fits(TagGroup tags) {\n\t\t\treturn tags.contains(\"barrier\", \"hedge\");\n\t\t}\n\t\tpublic Hedge(MapWaySegment segment) {\n\t\t\tsuper(Materials.HEDGE, segment, 1f, 0.5f);\n\t\t}\n\t}\n\t\n\tprivate static class ChainLinkFence extends LinearBarrier {\n\t\t\n\t\tpublic static boolean fits(TagGroup tags) {\n\t\t\treturn tags.contains(\"barrier\", \"fence\")\n\t\t\t\t\t&& tags.contains(\"fence_type\", \"chain_link\");\n\t\t}\n\t\t\n\t\tpublic ChainLinkFence(MapWaySegment segment, TagGroup tags) {\n\t\t\tsuper(segment, 1f, 0.02f);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t/* render fence */\n\t\t\t\n\t\t\tList<VectorXYZ> pointsWithEle = getCenterline();\n\t\t\t\n\t\t\tList<VectorXYZ> vsFence = createVerticalTriangleStrip(\n\t\t\t\t\tpointsWithEle, 0, height);\n\t\t\tList<List<VectorXZ>> texCoordListsFence = texCoordLists(\n\t\t\t\t\tvsFence, CHAIN_LINK_FENCE, STRIP_WALL);\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(CHAIN_LINK_FENCE, vsFence, texCoordListsFence);\n\n\t\t\tList<VectorXYZ> pointsWithEleBack =\n\t\t\t\t\tnew ArrayList<VectorXYZ>(pointsWithEle);\n\t\t\tCollections.reverse(pointsWithEleBack);\n\t\t\t\n\t\t\tList<VectorXYZ> vsFenceBack = createVerticalTriangleStrip(\n\t\t\t\t\tpointsWithEleBack, 0, height);\n\t\t\tList<List<VectorXZ>> texCoordListsFenceBack = texCoordLists(\n\t\t\t\t\tvsFenceBack, CHAIN_LINK_FENCE, STRIP_WALL);\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(CHAIN_LINK_FENCE, vsFenceBack,\n\t\t\t\t\ttexCoordListsFenceBack);\n\t\t\t\t\t\t\n\t\t\t/* render poles */\n\t\t\t\n\t\t\t//TODO connect the poles to the ground independently\n\t\t\t\n\t\t\tList<VectorXZ> polePositions = GeometryUtil.equallyDistributePointsAlong(2f, false,\n\t\t\t\t\tsegment.getStartNode().getPos(), segment.getEndNode().getPos());\n\t\t\t\n\t\t\tfor (VectorXZ polePosition : polePositions) {\n//\t\t\t\tTODO draw poles again\n//\t\t\t\tVectorXYZ base = polePosition.xyz(segment.getElevationProfile().getEleAt(polePosition));\n//\t\t\t\ttarget.drawColumn(CHAIN_LINK_FENCE_POST, null, base,\n//\t\t\t\t\t\theight, width, width, false, true);\n//\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class Fence extends LinearBarrier {\n\t\t\n\t\tpublic static boolean fits(TagGroup tags) {\n\t\t\treturn tags.contains(\"barrier\", \"fence\");\n\t\t}\n\t\t\n\t\tprivate static final Map<String, Material> MATERIAL_MAP;\n\t\tstatic {\n\t\t\tMATERIAL_MAP = new HashMap<String, Material>();\n\t\t\tMATERIAL_MAP.put(\"split_rail\", Materials.SPLIT_RAIL_FENCE);\n\t\t}\n\t\t\n\t\tprivate final Material material;\n\t\t\n\t\tpublic Fence(MapWaySegment segment, TagGroup tags) {\n\t\t\tsuper(segment, 0.5f, 0.1f);\n\t\t\t\n\t\t\tMaterial materialFromMap = MATERIAL_MAP.get(tags.getValue(\"fence_type\"));\n\t\t\tif (materialFromMap != null) {\n\t\t\t\tmaterial = materialFromMap;\n\t\t\t} else {\n\t\t\t\tmaterial = Materials.FENCE_DEFAULT;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tList<VectorXYZ> baseline = getCenterline();\n\t\t\t\n\t\t\t/* render bars */\n\t\t\t\n\t\t\tList<VectorXYZ> vsLowFront = createVerticalTriangleStrip(\n\t\t\t\t\tbaseline,\n\t\t\t\t\t0.2f * height, 0.5f * height);\n\t\t\tList<VectorXYZ> vsLowBack = createVerticalTriangleStrip(\n\t\t\t\t\tbaseline,\n\t\t\t\t\t0.5f * height, 0.2f * height);\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(material, vsLowFront, null);\n\t\t\ttarget.drawTriangleStrip(material, vsLowBack, null);\n\n\t\t\tList<VectorXYZ> vsHighFront = createVerticalTriangleStrip(\n\t\t\t\t\tbaseline,\n\t\t\t\t\t0.65f * height, 0.95f * height);\n\t\t\tList<VectorXYZ> vsHighBack = createVerticalTriangleStrip(\n\t\t\t\t\tbaseline,\n\t\t\t\t\t0.95f * height, 0.65f * height);\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(material, vsHighFront, null);\n\t\t\ttarget.drawTriangleStrip(material, vsHighBack, null);\n\t\t\t\n\t\t\t/* render poles */\n\t\t\t\n\t\t\tList<VectorXZ> polePositions = GeometryUtil.equallyDistributePointsAlong(1f, false,\n\t\t\t\t\tsegment.getStartNode().getPos(), segment.getEndNode().getPos());\n\t\t\t\n\t\t\tfor (VectorXZ polePosition : polePositions) {\n//\t\t\t\tTODO draw poles again\n//\t\t\t\tVectorXYZ base = polePosition.xyz(segment.getElevationProfile().getEleAt(polePosition));\n//\t\t\t\ttarget.drawColumn(material, null , base, height, width, width, false, true);\n\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class Bollard extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\n\t\tprivate static final float DEFAULT_HEIGHT = 1;\n\n\t\tpublic static boolean fits(TagGroup tags) {\n\t\t\treturn tags.contains(\"barrier\", \"bollard\");\n\t\t}\n\t\t\n\t\tprivate final float height;\n\t\t\n\t\tpublic Bollard(MapNode node, TagGroup tags) {\n\t\t\t\n\t\t\tsuper(node);\n\t\t\t\n\t\t\theight = parseHeight(tags, DEFAULT_HEIGHT);\n\t\t\t\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON; //TODO: flexible ground states\n\t\t}\n\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\ttarget.drawColumn(Materials.CONCRETE,\n\t\t\t\t\tnull, getBase(), height, 0.15f, 0.15f, false, true);\n\t\t}\n\t\t\n\t}\n\t\n\t//TODO: bollard_count or similar tag exists? create \"Bollards\" rep.\n\t//just as lift gates etc, this should use the line.getRightNormal and the road width\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/BridgeModule.java",
    "content": "package org.osm2world.core.world.modules;\n\n\nimport static org.osm2world.core.math.GeometryUtil.*;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.data.overlaps.MapIntersectionWW;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.WaySegmentWorldObject;\nimport org.osm2world.core.world.data.WorldObject;\nimport org.osm2world.core.world.modules.WaterModule.Water;\nimport org.osm2world.core.world.modules.common.AbstractModule;\nimport org.osm2world.core.world.modules.common.BridgeOrTunnel;\nimport org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject;\n\n/**\n * adds bridges to the world.\n * \n * Needs to be applied <em>after</em> all the modules that generate\n * whatever runs over the bridge.\n */\npublic class BridgeModule extends AbstractModule {\n\n\tpublic static final boolean isBridge(TagGroup tags) {\n\t\treturn tags.containsKey(\"bridge\")\n\t\t\t&& !\"no\".equals(tags.getValue(\"bridge\"))\n\t\t\t&& !\"proposed\".equals(tags.getValue(\"bridge\"));\n\t}\n\t\n\tpublic static final boolean isBridge(MapWaySegment segment) {\n\t\treturn isBridge(segment.getTags());\n\t}\n\t\n\t@Override\n\tprotected void applyToWaySegment(MapWaySegment segment) {\n\t\t\n\t\tWaySegmentWorldObject primaryRepresentation =\n\t\t\tsegment.getPrimaryRepresentation();\n\t\t\n\t\tif (primaryRepresentation instanceof AbstractNetworkWaySegmentWorldObject\n\t\t\t\t&& isBridge(segment)) {\n\t\t\t\n\t\t\tsegment.addRepresentation(new Bridge(segment,\n\t\t\t\t\t(AbstractNetworkWaySegmentWorldObject) primaryRepresentation));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static final double BRIDGE_UNDERSIDE_HEIGHT = 0.2f;\n\t\t\n\tprivate static class Bridge extends BridgeOrTunnel\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic Bridge(MapWaySegment segment,\n\t\t\t\tAbstractNetworkWaySegmentWorldObject primaryWO) {\n\t\t\tsuper(segment, primaryWO);\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ABOVE;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Iterable<EleConnector> getEleConnectors() {\n\t\t\t// TODO EleConnectors for pillars\n\t\t\treturn super.getEleConnectors();\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\n\t\t\tdrawBridgeUnderside(target);\n\t\t\t\t\t\t\n\t\t\tdrawBridgePillars(target);\n\t\t\t\n\t\t}\n\n\t\tprivate void drawBridgeUnderside(Target<?> target) {\n\t\t\t\n\t\t\tList<VectorXYZ> leftOutline = primaryRep.getOutline(false);\n\t\t\tList<VectorXYZ> rightOutline = primaryRep.getOutline(true);\n\t\t\t\n\t\t\tList<VectorXYZ> belowLeftOutline = sequenceAbove(leftOutline, -BRIDGE_UNDERSIDE_HEIGHT);\n\t\t\tList<VectorXYZ> belowRightOutline = sequenceAbove(rightOutline, -BRIDGE_UNDERSIDE_HEIGHT);\n\t\t\t\n\t\t\tList<VectorXYZ> strip1 = createTriangleStripBetween(\n\t\t\t\t\tbelowLeftOutline, leftOutline);\n\t\t\tList<VectorXYZ> strip2 = createTriangleStripBetween(\n\t\t\t\t\tbelowRightOutline, belowLeftOutline);\n\t\t\tList<VectorXYZ> strip3 = createTriangleStripBetween(\n\t\t\t\t\trightOutline, belowRightOutline);\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(Materials.BRIDGE_DEFAULT, strip1, null);\n\t\t\ttarget.drawTriangleStrip(Materials.BRIDGE_DEFAULT, strip2, null);\n\t\t\ttarget.drawTriangleStrip(Materials.BRIDGE_DEFAULT, strip3, null);\n\t\t\t\n\t\t}\n\n\t\tprivate void drawBridgePillars(Target<?> target) {\n\t\t\t\n\t\t\tList<VectorXZ> pillarPositions = equallyDistributePointsAlong(\n\t\t\t\t\t2f, false,\n\t\t\t\t\tprimaryRep.getStartPosition(),\n\t\t\t\t\tprimaryRep.getEndPosition());\n\t\t\t\n\t\t\t//make sure that the pillars doesn't pierce anything on the ground\n\t\t\t\n\t\t\tCollection<WorldObject> avoidedObjects = new ArrayList<WorldObject>();\n\t\t\t\n\t\t\tfor (MapIntersectionWW i : segment.getIntersectionsWW()) {\n\t\t\t\tfor (WorldObject otherRep : i.getOther(segment).getRepresentations()) {\n\t\t\t\t\t\n\t\t\t\t\tif (otherRep.getGroundState() == GroundState.ON\n\t\t\t\t\t\t\t&& !(otherRep instanceof Water) //TODO: choose better criterion!\n\t\t\t\t\t) {\n\t\t\t\t\t\tavoidedObjects.add(otherRep);\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tfilterWorldObjectCollisions(pillarPositions, avoidedObjects);\n\t\t\t\n\t\t\t//draw the pillars\n\t\t\t\n\t\t\tfor (VectorXZ pos : pillarPositions) {\n\t\t\t\tdrawBridgePillarAt(target, pos);\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tprivate void drawBridgePillarAt(Target<?> target, VectorXZ pos) {\n\t\t\n\t\t\t/* determine the bridge elevation at that point */\n\t\t\t\n\t\t\tVectorXYZ top = null;\n\t\t\t\n\t\t\tList<VectorXYZ> vs = primaryRep.getCenterline();\n\t\t\t\n\t\t\tfor (int i = 0; i + 1 < vs.size(); i++) {\n\t\t\t\t\n\t\t\t\tif (isBetween(pos, vs.get(i).xz(), vs.get(i+1).xz())) {\n\t\t\t\t\ttop = interpolateElevation(pos, vs.get(i), vs.get(i+1));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\n\t\t\t/* draw the pillar */\n\t\t\t\n\t\t\t// TODO: start pillar at ground instead of just 100 meters below the bridge\n\t\t\ttarget.drawColumn(Materials.BRIDGE_PILLAR_DEFAULT, null,\n\t\t\t\t\ttop.addY(-100),\n\t\t\t\t\t100,\n\t\t\t\t\t0.2, 0.2, false, false);\n\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/BuildingModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static com.google.common.base.Preconditions.checkArgument;\nimport static java.lang.Double.POSITIVE_INFINITY;\nimport static java.lang.Math.*;\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.*;\nimport static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.*;\nimport static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.*;\nimport static org.osm2world.core.map_elevation.data.GroundState.*;\nimport static org.osm2world.core.math.GeometryUtil.*;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.*;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.*;\nimport static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*;\n\nimport java.awt.Color;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapWA;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.map_elevation.data.EleConnectorGroup;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.Poly2TriUtil;\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.PolygonXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.algorithms.CAGUtil;\nimport org.osm2world.core.math.algorithms.JTSTriangulationUtil;\nimport org.osm2world.core.math.algorithms.TriangulationUtil;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.material.ImmutableMaterial;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.util.MinMaxUtil;\nimport org.osm2world.core.util.exception.TriangulationException;\nimport org.osm2world.core.world.data.AreaWorldObject;\nimport org.osm2world.core.world.data.NodeWorldObject;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.data.WaySegmentWorldObject;\nimport org.osm2world.core.world.data.WorldObjectWithOutline;\nimport org.osm2world.core.world.modules.common.ConfigurableWorldModule;\nimport org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject;\n\nimport com.google.common.base.Function;\n\n/**\n * adds buildings to the world\n */\npublic class BuildingModule extends ConfigurableWorldModule {\n\t\n\tprivate static String TM_EXCLUDE_BUILDINGS = System.getenv(\"TOUCH_MAPPER_EXCLUDE_BUILDINGS\");\n\n\t@Override\n\tpublic void applyTo(MapData mapData) {\n\t\t\n\t\tif (TM_EXCLUDE_BUILDINGS.equals(\"true\")) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tboolean useBuildingColors = config.getBoolean(\"useBuildingColors\", false);\n\t\tboolean drawBuildingWindows = config.getBoolean(\"drawBuildingWindows\", false);\n\t\t\n\t\tfor (MapArea area : mapData.getMapAreas()) {\n\t\t\t\n\t\t\tif (!area.getRepresentations().isEmpty()) continue;\n\t\t\t\n\t\t\tString buildingValue = area.getTags().getValue(\"building\");\n\t\t\t\n\t\t\tif (buildingValue != null && !buildingValue.equals(\"no\")) {\n\t\t\t\t\n\t\t\t\tBuilding building = new Building(area,\n\t\t\t\t\t\tuseBuildingColors, drawBuildingWindows);\n\t\t\t\tarea.addRepresentation(building);\n\t\t\t\t\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class Building implements AreaWorldObject,\n\t\tWorldObjectWithOutline, RenderableToAllTargets {\n\t\t\n\t\tprivate final MapArea area;\n\t\tprivate final List<BuildingPart> parts =\n\t\t\t\tnew ArrayList<BuildingModule.BuildingPart>();\n\t\t\n\t\tprivate final EleConnectorGroup outlineConnectors;\n\t\t\n\t\tpublic Building(MapArea area, boolean useBuildingColors,\n\t\t\t\tboolean drawBuildingWindows) {\n\t\t\t\n\t\t\tthis.area = area;\n\t\t\t\n//\t\t\tfor (MapOverlap<?,?> overlap : area.getOverlaps()) {\n//\t\t\t\tMapElement other = overlap.getOther(area);\n//\t\t\t\tif (other instanceof MapArea\n//\t\t\t\t\t\t&& other.getTags().containsKey(\"building:part\")) {\n//\t\t\t\t\t\n//\t\t\t\t\tMapArea otherArea = (MapArea)other;\n//\t\t\t\t\t\n//\t\t\t\t\t//TODO: check whether the building contains the part (instead of just touching it)\n//\t\t\t\t\tif (area.getPolygon().contains(\n//\t\t\t\t\t\t\totherArea.getPolygon().getOuter())) {\n//\t\t\t\t\t\tparts.add(new BuildingPart(this, otherArea,\n//\t\t\t\t\t\t\totherArea.getPolygon(), useBuildingColors,\n//\t\t\t\t\t\t\tdrawBuildingWindows));\n//\t\t\t\t\t}\n//\t\t\t\t\t\n//\t\t\t\t}\n//\t\t\t}\n\t\t\t\n\t\t\t/* add part(s) for area not covered by building:part polygons */\n\t\t\tboolean isBuildingPart = false;\n\t\t\tif (area.getTags().containsKey(\"building:part\"))\n\t\t\t\tisBuildingPart = !(\"no\".equals(area.getTags().getValue(\"building:part\")));\n\t\t\t\n\t\t\tif (parts.isEmpty() || isBuildingPart) {\n\t\t\t\tparts.add(new BuildingPart(this, area,\n\t\t\t\t\t\tarea.getPolygon(), useBuildingColors, drawBuildingWindows));\n\t\t\t} else {\n//\t\t\t\tList<SimplePolygonXZ> subtractPolygons = new ArrayList<SimplePolygonXZ>();\n//\t\t\t\t\n//\t\t\t\tfor (BuildingPart part : parts) {\n//\t\t\t\t\tsubtractPolygons.add(part.getPolygon().getOuter());\n//\t\t\t\t}\n//\t\t\t\tsubtractPolygons.addAll(area.getPolygon().getHoles());\n//\t\t\t\t\n//\t\t\t\tCollection<PolygonWithHolesXZ> remainingPolys =\n//\t\t\t\t\tCAGUtil.subtractPolygons(\n//\t\t\t\t\t\t\tarea.getPolygon().getOuter(),\n//\t\t\t\t\t\t\tsubtractPolygons);\n//\t\t\t\t\n//\t\t\t\tfor (PolygonWithHolesXZ remainingPoly : remainingPolys) {\n//\t\t\t\t\tparts.add(new BuildingPart(this, area, remainingPoly,\n//\t\t\t\t\t\t\tuseBuildingColors, drawBuildingWindows));\n//\t\t\t\t}\n//\t\t\t\t\n\t\t\t}\n\n\t\t\t/* create connectors along the outline.\n\t\t\t * Because the ground around buildings is not necessarily plane,\n\t\t\t * they aren't directly used for ele, but instead their minimum.\n\t\t\t */\n\t\t\t\n\t\t\toutlineConnectors = new EleConnectorGroup();\n\t\t\toutlineConnectors.addConnectorsFor(area.getPolygon(), null, ON);\n\t\t\t\n\t\t}\n\n\t\tpublic MapArea getArea() { //TODO: redundant because of getPrimaryMapElement\n\t\t\treturn area;\n\t\t}\n\t\t\n\t\tpublic List<BuildingPart> getParts() {\n\t\t\treturn parts;\n\t\t}\n\n\t\t@Override\n\t\tpublic MapArea getPrimaryMapElement() {\n\t\t\treturn area;\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic EleConnectorGroup getEleConnectors() {\n\t\t\treturn outlineConnectors;\n\t\t}\n\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\t\n\t\t\tList<EleConnector> groundLevelEntrances = new ArrayList<EleConnector>();\n\t\t\t\n\t\t\t/* add constraints between entrances with different levels */\n\t\t\t\n\t\t\tfor (BuildingPart part : parts) {\n\t\t\t\t\n\t\t\t\t// add vertical distances\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i < part.entrances.size(); i++) {\n\t\t\t\t\t\n\t\t\t\t\tBuildingEntrance e1 = part.entrances.get(i);\n\t\t\t\t\t\n\t\t\t\t\tfor (int j = i+1; j < part.entrances.size(); j++) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tBuildingEntrance e2 = part.entrances.get(j);\n\t\t\t\t\t\t\n\t\t\t\t\t\tdouble heightPerLevel = part.heightWithoutRoof / part.buildingLevels;\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (e1.getLevel() > e2.getLevel()) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tenforcer.requireVerticalDistance(EXACT,\n\t\t\t\t\t\t\t\t\theightPerLevel * (e1.getLevel() - e2.getLevel()),\n\t\t\t\t\t\t\t\t\te1.connector, e2.connector);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t} else if (e1.getLevel() < e2.getLevel()) {\n\n\t\t\t\t\t\t\tenforcer.requireVerticalDistance(EXACT,\n\t\t\t\t\t\t\t\t\theightPerLevel * (e2.getLevel() - e1.getLevel()),\n\t\t\t\t\t\t\t\t\te2.connector, e1.connector);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// collect entrances for next step\n\t\t\t\t\t\n\t\t\t\t\tif (e1.getLevel() == 0 && e1.getGroundState() == ON) {\n\t\t\t\t\t\tgroundLevelEntrances.add(e1.connector);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* make sure that a level=0 ground entrance is the building's lowest point */\n\t\t\t\n\t\t\tfor (EleConnector outlineConnector : outlineConnectors) {\n\t\t\t\tfor (EleConnector entranceConnector : groundLevelEntrances) {\n\t\t\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\t\t\tMIN, 0, outlineConnector, entranceConnector);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t//TODO\n//\t\t@Override\n//\t\tpublic double getClearingAbove(VectorXZ pos) {\n//\t\t\tdouble maxClearingAbove = 0;\n//\t\t\tfor (BuildingPart part : parts) {\n//\t\t\t\tdouble clearing = part.getClearingAbove(pos);\n//\t\t\t\tmaxClearingAbove = max(clearing, maxClearingAbove);\n//\t\t\t}\n//\t\t\treturn maxClearingAbove;\n//\t\t}\n\n\t\t@Override\n\t\tpublic SimplePolygonXZ getOutlinePolygonXZ() {\n\t\t\treturn area.getPolygon().getOuter().makeCounterclockwise();\n\t\t}\n\n\t\tpublic double getGroundLevelEle() {\n\t\t\t\n\t\t\tdouble minEle = POSITIVE_INFINITY;\n\t\t\t\n\t\t\tfor (EleConnector c : outlineConnectors) {\n\t\t\t\tif (c.getPosXYZ().y < minEle) {\n\t\t\t\t\tminEle = c.getPosXYZ().y;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\treturn minEle;\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic PolygonXYZ getOutlinePolygon() {\n\t\t\treturn getOutlinePolygonXZ().xyz(getGroundLevelEle());\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\tfor (BuildingPart part : parts) {\n\t\t\t\tpart.renderTo(target);\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class BuildingPart implements RenderableToAllTargets {\n\n\t\tprivate final Building building;\n\t\tprivate final MapArea area;\n\t\tprivate final PolygonWithHolesXZ polygon;\n\t\t\n\t\tprivate int buildingLevels;\n\t\tprivate int minLevel;\n\t\t\n\t\tprivate double heightWithoutRoof;\n\t\t\n\t\tprivate Material materialWall;\n\t\tprivate Material materialWallWithWindows;\n\t\tprivate Material materialRoof;\n\t\t\n\t\tprivate List<BuildingEntrance> entrances = new ArrayList<BuildingEntrance>();\n\t\t\n\t\tprivate Roof roof;\n\t\t\n\t\tpublic BuildingPart(Building building,\n\t\t\t\tMapArea area, PolygonWithHolesXZ polygon,\n\t\t\t\tboolean useBuildingColors, boolean drawBuildingWindows) {\n\n\t\t\tthis.building = building;\n\t\t\tthis.area = area;\n\t\t\tthis.polygon = polygon;\n\n\t\t\tsetAttributes(false, false);\n\t\t\t\n//\t\t\tfor (MapNode node : area.getBoundaryNodes()) {\n//\t\t\t\tif ((node.getTags().contains(\"building\", \"entrance\")\n//\t\t\t\t\t\t|| node.getTags().containsKey(\"entrance\"))\n//\t\t\t\t\t\t&& node.getRepresentations().isEmpty()) {\n//\t\t\t\t\t\n//\t\t\t\t\tBuildingEntrance entrance = new BuildingEntrance(this, node);\n//\t\t\t\t\tentrances.add(entrance);\n//\t\t\t\t\tnode.addRepresentation(entrance);\n//\t\t\t\t\t\n//\t\t\t\t}\n//\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\treturn polygon;\n\t\t}\n\t\t\t\t\n\t\tpublic Roof getRoof() {\n\t\t\treturn roof;\n\t\t}\n\n\t\tpublic double getClearingAbove(VectorXZ pos) {\n\t\t\treturn heightWithoutRoof + roof.getRoofHeight();\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\trenderFloor(target, 0);\n\t\t\t\n//\t\t\trenderWalls(target, roof);\n\t\t\t\n//\t\t\troof.renderTo(target);\n\t\t\t\n\t\t}\n\n\t\tprivate void renderFloor(Target<?> target, double floorEle) {\n\t\t\t\n\t\t\tCollection<TriangleXZ> triangles =\n\t\t\t\tTriangulationUtil.triangulate(polygon);\n\n\t\t\tList<TriangleXYZ> trianglesXYZ =\n\t\t\t\tnew ArrayList<TriangleXYZ>(triangles.size());\n\t\t\t\t\n\t\t\tfor (TriangleXZ triangle : triangles) {\n\t\t\t\ttrianglesXYZ.add(triangle.makeClockwise().xyz(floorEle));\n\t\t\t}\n\t\t\t\t\n\t\t\ttarget.drawTriangles(materialWall, trianglesXYZ,\n\t\t\t\t\ttriangleTexCoordLists(trianglesXYZ, materialWall, GLOBAL_X_Z));\n\t\t\t\n\t\t}\n\n\t\tprivate void renderWalls(Target<?> target, Roof roof) {\n\t\t\t\n\t\t\tdouble baseEle = building.getGroundLevelEle();\n\t\t\t\n\t\t\tdouble floorHeight = calculateFloorHeight(roof);\n\t\t\tboolean renderFloor = (floorHeight > 0);\n\t\t\t\n\t\t\tif (true || area.getOverlaps().isEmpty()) {\n\t\t\t\t// Always draw buildings whole. Otherwise there may be\n\t\t\t\t// eg. ground level tunnels through them, which would screw things up.\n\t\t\t\trenderWalls(target, roof.getPolygon(), false,\n\t\t\t\t\t\tbaseEle, floorHeight, roof);\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\t\t\t\n\t\t\t\t/* find terrain boundaries on the ground\n\t\t\t\t * that overlap with the building */\n\t\t\t\t\n\t\t\t\tList<TerrainBoundaryWorldObject> tbWorldObjects = new ArrayList<TerrainBoundaryWorldObject>();\n\t\t\t\t\t\t\t\t\n\t\t\t\tfor (MapOverlap<?,?> overlap : area.getOverlaps()) {\n\t\t\t\t\tMapElement other = overlap.getOther(area);\n\t\t\t\t\tif (other.getPrimaryRepresentation() instanceof TerrainBoundaryWorldObject\n\t\t\t\t\t\t\t&& other.getPrimaryRepresentation().getGroundState() == GroundState.ON\n\t\t\t\t\t\t\t&& (other.getTags().contains(\"tunnel\", \"passage\")\n\t\t\t\t\t\t\t\t\t|| other.getTags().contains(\"tunnel\", \"building_passage\"))) {\n\t\t\t\t\t\ttbWorldObjects.add((TerrainBoundaryWorldObject)\n\t\t\t\t\t\t\t\tother.getPrimaryRepresentation());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t/* render building parts where the building polygon does not overlap with terrain boundaries */\n\t\t\t\t\n\t\t\t\tList<SimplePolygonXZ> subtractPolygons = new ArrayList<SimplePolygonXZ>();\n\t\t\t\t\n\t\t\t\tfor (TerrainBoundaryWorldObject o : tbWorldObjects) {\n\t\t\t\t\t\n\t\t\t\t\tSimplePolygonXZ subtractPoly = o.getOutlinePolygonXZ();\n\t\t\t\t\t\n\t\t\t\t\tsubtractPolygons.add(subtractPoly);\n\t\t\t\t\t\n\t\t\t\t\tif (o instanceof WaySegmentWorldObject) {\n\t\t\t\t\t\t\n\t\t\t\t\t\t// extend the subtract polygon for segments that end\n\t\t\t\t\t\t// at a common node with this building part's outline.\n\t\t\t\t\t\t// (otherwise, the subtract polygon will probably\n\t\t\t\t\t\t// not exactly line up with the polygon boundary)\n\t\t\t\t\t\t\n\t\t\t\t\t\tWaySegmentWorldObject waySegmentWO = (WaySegmentWorldObject)o;\n\t\t\t\t\t\tVectorXZ start = waySegmentWO.getStartPosition();\n\t\t\t\t\t\tVectorXZ end = waySegmentWO.getEndPosition();\n\t\t\t\t\t\t\n\t\t\t\t\t\tboolean startCommonNode = false;\n\t\t\t\t\t\tboolean endCommonNode = false;\n\t\t\t\t\t\t\n\t\t\t\t\t\tfor (SimplePolygonXZ p : polygon.getPolygons()) {\n\t\t\t\t\t\t\tstartCommonNode |= p.getVertexCollection().contains(start);\n\t\t\t\t\t\t\tendCommonNode |= p.getVertexCollection().contains(end);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tVectorXZ direction = end.subtract(start).normalize();\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (startCommonNode) {\n\t\t\t\t\t\t\tsubtractPolygons.add(subtractPoly.shift(direction));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (endCommonNode) {\n\t\t\t\t\t\t\tsubtractPolygons.add(subtractPoly.shift(direction.invert()));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tsubtractPolygons.addAll(roof.getPolygon().getHoles());\n\t\t\t\t\n\t\t\t\tCollection<PolygonWithHolesXZ> buildingPartPolys =\n\t\t\t\t\tCAGUtil.subtractPolygons(\n\t\t\t\t\t\t\troof.getPolygon().getOuter(),\n\t\t\t\t\t\t\tsubtractPolygons);\n\t\t\t\t\n\t\t\t\tfor (PolygonWithHolesXZ p : buildingPartPolys) {\n\t\t\t\t\trenderWalls(target, p, false, baseEle, floorHeight, roof);\n\t\t\t\t\tif (renderFloor) {\n\t\t\t\t\t\trenderFloor(target, baseEle + floorHeight);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t/* render building parts above the terrain boundaries */\n\t\t\t\t\n\t\t\t\tfor (TerrainBoundaryWorldObject o : tbWorldObjects) {\n\n\t\t\t\t\tCollection<PolygonWithHolesXZ> raisedBuildingPartPolys;\n\t\t\t\t\t\n\t\t\t\t\tCollection<PolygonWithHolesXZ> polysAboveTBWOs =\n\t\t\t\t\t\tCAGUtil.intersectPolygons(Arrays.asList(\n\t\t\t\t\t\t\t\tpolygon.getOuter(), o.getOutlinePolygon().getSimpleXZPolygon()));\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\tif (polygon.getHoles().isEmpty()) {\n\t\t\t\t\t\traisedBuildingPartPolys = polysAboveTBWOs;\n\t\t\t\t\t} else {\n\t\t\t\t\t\traisedBuildingPartPolys = new ArrayList<PolygonWithHolesXZ>();\n\t\t\t\t\t\tfor (PolygonWithHolesXZ p : polysAboveTBWOs) {\n\t\t\t\t\t\t\tList<SimplePolygonXZ> subPolys = new ArrayList<SimplePolygonXZ>();\n\t\t\t\t\t\t\tsubPolys.addAll(polygon.getHoles());\n\t\t\t\t\t\t\tsubPolys.addAll(p.getHoles());\n\t\t\t\t\t\t\traisedBuildingPartPolys.addAll(\n\t\t\t\t\t\t\t\t\tCAGUtil.subtractPolygons(p.getOuter(), subPolys));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tfor (PolygonWithHolesXZ p : raisedBuildingPartPolys) {\n\t\t\t\t\t\tdouble newFloorHeight = 3;\n\t\t\t\t\t\t\t//TODO restore clearing - o.getClearingAbove(o.getOutlinePolygon().getSimpleXZPolygon().getCenter());\n\t\t\t\t\t\tif (newFloorHeight < floorHeight) {\n\t\t\t\t\t\t\tnewFloorHeight = floorHeight;\n\t\t\t\t\t\t}\n\t\t\t\t\t\trenderWalls(target, p, false, baseEle, newFloorHeight, roof);\n\t\t\t\t\t\trenderFloor(target, baseEle);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\n\t\t}\n\n\t\tprivate double calculateFloorHeight(Roof roof) {\n\t\t\t\n\t\t\tif (getValue(\"min_height\") != null) {\n\t\t\t\t\n\t\t\t\tFloat minHeight = parseMeasure(getValue(\"min_height\"));\n\t\t\t\tif (minHeight != null) {\n\t\t\t\t\treturn minHeight;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (minLevel > 0) {\n\t\t\t\t\n\t\t\t\treturn (heightWithoutRoof / buildingLevels) * minLevel;\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (area.getTags().contains(\"building\", \"roof\")\n\t\t\t\t\t|| area.getTags().contains(\"building:part\", \"roof\")) {\n\t\t\t\t\n\t\t\t\treturn heightWithoutRoof - 0.3;\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn 0;\n\t\t\t\t\t\t\n\t\t}\n\n\t\tprivate void renderWalls(Target<?> target, PolygonWithHolesXZ p,\n\t\t\t\tboolean renderFloor, double baseEle, double floorHeight,\n\t\t\t\tRoof roof) {\n\t\t\t\n\t\t\tdrawWallOnPolygon(target, baseEle, floorHeight,\n\t\t\t\t\troof, p.getOuter().makeCounterclockwise());\n\t\t\t\n\t\t\tfor (SimplePolygonXZ polygon : p.getHoles()) {\n\t\t\t\tdrawWallOnPolygon(target, baseEle, floorHeight,\n\t\t\t\t\t\troof, polygon.makeClockwise());\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tprivate void drawWallOnPolygon(Target<?> target, double baseEle,\n\t\t\t\tdouble floorHeight, Roof roof, SimplePolygonXZ polygon) {\n\t\t\t\n\t\t\tdouble floorEle = baseEle + floorHeight;\n\t\t\t\n\t\t\tList<TextureData> textureDataList = materialWallWithWindows.getTextureDataList();\n\t\t\tList<VectorXZ> vertices = polygon.getVertexLoop();\n\t\t\t\n\t\t\tList<VectorXYZ> mainWallVectors = new ArrayList<VectorXYZ>(vertices.size() * 2);\n\t\t\tList<VectorXYZ> roofWallVectors = new ArrayList<VectorXYZ>(vertices.size() * 2);\n\t\t\t\n\t\t\tList<List<VectorXZ>> mainWallTexCoordLists = new ArrayList<List<VectorXZ>>(\n\t\t\t\t\ttextureDataList.size());\n\t\t\t\n\t\t\tfor (int texLayer = 0; texLayer < textureDataList.size(); texLayer ++) {\n\t\t\t\tmainWallTexCoordLists.add(new ArrayList<VectorXZ>());\n\t\t\t}\n\t\t\t\n\t\t\tdouble accumulatedLength = 0;\n\t\t\tdouble[] previousS = new double[textureDataList.size()];\n\t\t\t\n\t\t\tfor (int i = 0; i < vertices.size(); i++) {\n\t\t\t\t\n\t\t\t\tfinal VectorXZ coord = vertices.get(i);\n\t\t\t\t\n\t\t\t\t/* update accumulated wall length */\n\t\t\t\t\n\t\t\t\tif (i > 0) {\n\t\t\t\t\taccumulatedLength += coord.distanceTo(vertices.get(i-1));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t/* add wall vectors */\n\t\t\t\t\n\t\t\t\tfinal VectorXYZ upperVector = coord.xyz(roof.getRoofEleAt(coord));\n\t\t\t\tfinal VectorXYZ middleVector = coord.xyz(baseEle + heightWithoutRoof);\n\t\t\t\t\n\t\t\t\tdouble upperEle = upperVector.y;\n\t\t\t\tdouble middleEle = middleVector.y;\n\t\t\t\t\n\t\t\t\tmainWallVectors.add(middleVector);\n\t\t\t\tmainWallVectors.add(new VectorXYZ(coord.x,\n\t\t\t\t\t\tmin(floorEle, middleEle), coord.z));\n\t\t\t\t\n\t\t\t\troofWallVectors.add(upperVector);\n\t\t\t\troofWallVectors.add(new VectorXYZ(coord.x,\n\t\t\t\t\t\tmin(middleEle, upperEle), coord.z));\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t/* add texture coordinates */\n\t\t\t\t\n\t\t\t\tfor (int texLayer = 0; texLayer < textureDataList.size(); texLayer ++) {\n\t\t\t\t\t\n\t\t\t\t\tTextureData textureData = textureDataList.get(texLayer);\n\t\t\t\t\tList<VectorXZ> texCoordList = mainWallTexCoordLists.get(texLayer);\n\t\t\t\t\t\n\t\t\t\t\tdouble s, lowerT, middleT;\n\t\t\t\t\t\n\t\t\t\t\t// determine s (width dimension) coordinate\n\t\t\t\t\t\n\t\t\t\t\tif (textureData.height > 0) {\n\t\t\t\t\t\ts = accumulatedLength / textureData.width;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (i == 0) {\n\t\t\t\t\t\t\ts = 0;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ts = previousS[texLayer] + round(vertices.get(i-1)\n\t\t\t\t\t\t\t\t\t.distanceTo(coord) / textureData.width);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tpreviousS[texLayer] = s;\n\t\t\t\t\t\n\t\t\t\t\t// determine t (height dimension) coordinates\n\t\t\t\t\t\n\t\t\t\t\tif (textureData.height > 0) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tlowerT = (floorEle - baseEle) / textureData.height;\n\t\t\t\t\t\tmiddleT = (middleEle - baseEle) / textureData.height;\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\tlowerT = buildingLevels *\n\t\t\t\t\t\t\t(floorEle - baseEle) / (middleEle - baseEle);\n\t\t\t\t\t\tmiddleT = buildingLevels;\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// set texture coordinates\n\t\t\t\t\t\n\t\t\t\t\ttexCoordList.add(new VectorXZ(s, middleT));\n\t\t\t\t\ttexCoordList.add(new VectorXZ(s, lowerT));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t}\n\n\t\t\ttarget.drawTriangleStrip(materialWallWithWindows, mainWallVectors,\n\t\t\t\t\tmainWallTexCoordLists);\n\t\t\ttarget.drawTriangleStrip(materialWall, roofWallVectors,\n\t\t\t\t\ttexCoordLists(roofWallVectors, materialWall, STRIP_WALL));\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * sets the building part attributes (height, colors) depending on\n\t\t * the building's and building part's tags.\n\t\t * If available, explicitly tagged data is used,\n\t\t * with tags of the building part overriding building tags.\n\t\t * Otherwise, the values depend on indirect assumptions\n\t\t * (level height) or ultimately the building class as determined\n\t\t * by the \"building\" key.\n\t\t */\n\t\tprivate void setAttributes(boolean useBuildingColors,\n\t\t\t\tboolean drawBuildingWindows) {\n\t\t\t\n\t\t\tTagGroup tags = area.getTags();\n\t\t\tTagGroup buildingTags = building.area.getTags();\n\t\t\t\n\t\t\t/* determine defaults for building type */\n\t\t\t\n\t\t\tint defaultLevels = 3;\n\t\t\tdouble defaultHeightPerLevel = 2.5;\n\t\t\tMaterial defaultMaterialWall = Materials.BUILDING_DEFAULT;\n\t\t\tMaterial defaultMaterialRoof = Materials.ROOF_DEFAULT;\n\t\t\tMaterial defaultMaterialWindows = Materials.BUILDING_WINDOWS;\n\t\t\tString defaultRoofShape = \"flat\";\n\t\t\t\n\t\t\tString buildingValue = getValue(\"building\");\n\t\t\t\n\t\t\tif (\"greenhouse\".equals(buildingValue)) {\n\t\t\t\tdefaultLevels = 1;\n\t\t\t\tdefaultMaterialWall = Materials.GLASS;\n\t\t\t\tdefaultMaterialRoof = Materials.GLASS_ROOF;\n\t\t\t\tdefaultMaterialWindows = null;\n\t\t\t} else if (\"garage\".equals(buildingValue)\n\t\t\t\t\t|| \"garages\".equals(buildingValue)) {\n\t\t\t\tdefaultLevels = 1;\n\t\t\t\tdefaultMaterialWall = Materials.CONCRETE;\n\t\t\t\tdefaultMaterialRoof = Materials.CONCRETE;\n\t\t\t\tdefaultMaterialWindows = Materials.GARAGE_DOORS;\n\t\t\t} else if (\"hut\".equals(buildingValue)\n\t\t\t\t\t|| \"shed\".equals(buildingValue)) {\n\t\t\t\tdefaultLevels = 1;\n\t\t\t} else if (\"cabin\".equals(buildingValue)) {\n\t\t\t\tdefaultLevels = 1;\n\t\t\t\tdefaultMaterialWall = Materials.WOOD_WALL;\n\t\t\t\tdefaultMaterialRoof = Materials.WOOD;\n\t\t\t} else if (\"roof\".equals(buildingValue)) {\n\t\t\t\tdefaultLevels = 1;\n\t\t\t\tdefaultMaterialWindows = null;\n\t\t\t} else if (\"church\".equals(buildingValue)\n\t\t\t\t\t|| \"hangar\".equals(buildingValue)\n\t\t\t\t\t|| \"industrial\".equals(buildingValue)) {\n\t\t\t\tdefaultMaterialWindows = null;\n\t\t\t} else {\n\t\t\t\tif (getValue(\"building:levels\") == null) {\n\t\t\t\t\tdefaultMaterialWindows = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* determine levels */\n\t\t\t\n\t\t\tbuildingLevels = defaultLevels;\n\t\t\t\n\t\t\tFloat parsedLevels = null;\n\t\t\t\n\t\t\tif (getValue(\"building:levels\") != null) {\n\t\t\t\tparsedLevels = parseOsmDecimal(\n\t\t\t\t\t\tgetValue(\"building:levels\"), false);\n\t\t\t}\n\t\t\t\n\t\t\tif (parsedLevels != null) {\n\t\t\t\tbuildingLevels = (int)(float)parsedLevels;\n\t\t\t} else if (parseHeight(tags, parseHeight(buildingTags, -1)) > 0) {\n\t\t\t\tbuildingLevels = max(1, (int)(parseHeight(tags, parseHeight(\n\t\t\t\t\t\tbuildingTags, -1)) / defaultHeightPerLevel));\n\t\t\t}\n\t\t\t\n\t\t\tminLevel = 0;\n\t\t\t\n\t\t\tif (getValue(\"building:min_level\") != null) {\n\t\t\t\tFloat parsedMinLevel = parseOsmDecimal(\n\t\t\t\t\t\tgetValue(\"building:min_level\"), false);\n\t\t\t\tif (parsedMinLevel != null) {\n\t\t\t\t\tminLevel = (int)(float)parsedMinLevel;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* determine roof shape */\n\t\t\t\n//\t\t\tboolean explicitRoofTagging = true;\n//\t\t\t\n//\t\t\tif (!(\"no\".equals(area.getTags().getValue(\"roof:lines\"))) && hasComplexRoof(area)) {\n//\t\t\t\troof = new ComplexRoof();\n//\t\t\t} else {\n//\t\t\t\t\n//\t\t\t\tString roofShape = getValue(\"roof:shape\");\n//\t\t\t\tif (roofShape == null) { roofShape = getValue(\"building:roof:shape\"); }\n//\t\t\t\t\n//\t\t\t\tif (roofShape == null) {\n//\t\t\t\t\troofShape = defaultRoofShape;\n//\t\t\t\t\texplicitRoofTagging = false;\n//\t\t\t\t}\n//\t\t\t\t\n//\t\t\t\ttry {\n//\t\t\t\t\t\n//\t\t\t\t\tif (\"pyramidal\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new PyramidalRoof();\n//\t\t\t\t\t} else if (\"onion\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new OnionRoof();\n//\t\t\t\t\t} else if (\"skillion\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new SkillionRoof();\n//\t\t\t\t\t} else if (\"gabled\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new GabledRoof();\n//\t\t\t\t\t} else if (\"hipped\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new HippedRoof();\n//\t\t\t\t\t} else if (\"half-hipped\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new HalfHippedRoof();\n//\t\t\t\t\t} else if (\"gambrel\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new GambrelRoof();\n//\t\t\t\t\t} else if (\"mansard\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new MansardRoof();\n//\t\t\t\t\t} else if (\"dome\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new DomeRoof();\n//\t\t\t\t\t} else if (\"round\".equals(roofShape)) {\n//\t\t\t\t\t\troof = new RoundRoof();\n//\t\t\t\t\t} else {\n//\t\t\t\t\t\troof = new FlatRoof();\n//\t\t\t\t\t}\n//\t\t\t\t\t\n//\t\t\t\t} catch (InvalidGeometryException e) {\n//\t\t\t\t\tSystem.err.println(\"falling back to FlatRoof: \" + e);\n//\t\t\t\t\troof = new FlatRoof();\n//\t\t\t\t\texplicitRoofTagging = false;\n//\t\t\t\t}\n//\t\t\t\t\n//\t\t\t}\n\n\t\t\troof = new FlatRoof();\n\t\t\tboolean explicitRoofTagging = false;\n\n\t\t\t/* determine height */\n\t\t\t\n//\t\t\tdouble fallbackHeight = buildingLevels * defaultHeightPerLevel;\n//\t\t\t\n//\t\t\tfallbackHeight += roof.getRoofHeight();\n//\t\t\t\n//\t\t\tfallbackHeight = parseHeight(buildingTags, (float)fallbackHeight);\n//\t\t\t\n//\t\t\tdouble height = parseHeight(tags, (float)fallbackHeight);\n\t\t\tdouble height = 100f;\n\t\t\theightWithoutRoof = height - roof.getRoofHeight();\n\t\t\t\n\t\t\t/* determine materials */\n\t\t    \n\t\t    if (defaultMaterialRoof == Materials.ROOF_DEFAULT\n\t\t    \t\t&& explicitRoofTagging && roof instanceof FlatRoof) {\n\t\t    \tdefaultMaterialRoof = Materials.CONCRETE;\n\t\t    }\n\t\t    \n\t\t    if (useBuildingColors) {\n\t\t    \t\n\t\t    \tmaterialWall = buildMaterial(\n\t\t    \t\t\tgetValue(\"building:material\"),\n\t\t    \t\t\tgetValue(\"building:colour\"),\n\t\t    \t\t\tdefaultMaterialWall, false);\n\t\t    \tmaterialRoof = buildMaterial(\n\t\t    \t\t\tgetValue(\"roof:material\"),\n\t\t    \t\t\tgetValue(\"roof:colour\"),\n\t\t    \t\t\tdefaultMaterialRoof, true);\n\t\t    \t\n\t\t    } else {\n\t\t    \t\n\t\t    \tmaterialWall = defaultMaterialWall;\n\t\t    \tmaterialRoof = defaultMaterialRoof;\n\t\t    \t\n\t\t    }\n\t\t    \n\t\t    if (materialWall == Materials.GLASS) {\n\t\t\t\t// avoid placing windows into a glass front\n\t\t\t\t// TODO: the == currently only works if GLASS is not colorable\n\t\t\t\tdefaultMaterialWindows = null;\n\t\t    }\n\t\t    \n\t\t    materialWallWithWindows = materialWall;\n\t\t    \n\t\t    if (drawBuildingWindows) {\n\n\t\t    \tMaterial materialWindows = defaultMaterialWindows;\n\t\t    \t\n\t\t    \tif (materialWindows != null) {\n\t\t    \t\t\n\t\t    \t\tmaterialWallWithWindows = materialWallWithWindows.\n\t\t    \t\t\t\twithAddedLayers(materialWindows.getTextureDataList());\n\t\t\t    \t\n\t\t    \t}\n\t\t    \t\n\t\t    }\n\t\t    \n\t\t}\n\t\t\n\t\tprivate Material buildMaterial(String materialString,\n\t\t\t\tString colorString, Material defaultMaterial,\n\t\t\t\tboolean roof) {\n\t\t\t\n\t\t\tMaterial material = defaultMaterial;\n\t\t\t\n\t\t\tif (materialString != null) {\n\t\t\t\tif (\"brick\".equals(materialString)) {\n\t\t\t\t\tmaterial = Materials.BRICK;\n\t\t\t\t} else if (\"glass\".equals(materialString)) {\n\t\t\t\t\tmaterial = roof ? Materials.GLASS_ROOF : Materials.GLASS;\n\t\t\t\t} else if (\"wood\".equals(materialString)) {\n\t\t\t\t\tmaterial = Materials.WOOD_WALL;\n\t\t\t\t} else if (Materials.getSurfaceMaterial(materialString) != null) {\n\t\t\t\t\tmaterial = Materials.getSurfaceMaterial(materialString);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tboolean colorable = material.getNumTextureLayers() == 0\n\t\t\t\t\t|| material.getTextureDataList().get(0).colorable;\n\t\t\t\n\t\t\tif (colorString != null && colorable) {\n\t\t\t\t\n\t\t\t\tColor color;\n\t\t\t\t\n\t\t\t\tif (\"white\".equals(colorString)) {\n\t\t\t\t\tcolor = new Color(240, 240, 240);\n\t\t\t\t} else if (\"black\".equals(colorString)) {\n\t\t\t\t\tcolor = new Color(76, 76, 76);\n\t\t\t\t} else if (\"grey\".equals(colorString) || \"gray\".equals(colorString)) {\n\t\t\t\t\tcolor = new Color(100, 100, 100);\n\t\t\t\t} else if (\"red\".equals(colorString)) {\n\t\t\t\t\tif (roof) {\n\t\t\t\t\t\tcolor = new Color(204, 0, 0);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolor = new Color(255, 190, 190);\n\t\t\t\t\t}\n\t\t\t\t} else if (\"green\".equals(colorString)) {\n\t\t\t\t\tif (roof) {\n\t\t\t\t\t\tcolor = new Color(150, 200, 130);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolor = new Color(190, 255, 190);\n\t\t\t\t\t}\n\t\t\t\t} else if (\"blue\".equals(colorString)) {\n\t\t\t\t\tif (roof) {\n\t\t\t\t\t\tcolor = new Color(100, 50, 200);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolor = new Color(190, 190, 255);\n\t\t\t\t\t}\n\t\t\t\t} else if (\"yellow\".equals(colorString)) {\n\t\t\t\t\tcolor = new Color(255, 255, 175);\n\t\t\t\t} else if (\"pink\".equals(colorString)) {\n\t\t\t\t\tcolor = new Color(225, 175, 225);\n\t\t\t\t} else if (\"orange\".equals(colorString)) {\n\t\t\t\t\tcolor = new Color(255, 225, 150);\n\t\t\t\t} else if (\"brown\".equals(colorString)) {\n\t\t\t\t\tif (roof) {\n\t\t\t\t\t\tcolor = new Color(120, 110, 110);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolor = new Color(170, 130, 80);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tcolor = parseColor(colorString);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (color != null) {\n\t\t\t\t\tmaterial = new ImmutableMaterial(\n\t\t\t\t\t\t\tmaterial.getInterpolation(), color,\n\t\t\t\t\t\t\tmaterial.getAmbientFactor(),\n\t\t\t\t\t\t\tmaterial.getDiffuseFactor(),\n\t\t\t\t\t\t\tmaterial.getSpecularFactor(),\n\t\t\t\t\t\t\tmaterial.getShininess(),\n\t\t\t\t\t\t\tmaterial.getTransparency(),\n\t\t\t\t\t\t\tmaterial.getShadow(),\n\t\t\t\t\t\t\tmaterial.getAmbientOcclusion(),\n\t\t\t\t\t\t\tmaterial.getTextureDataList());\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn material;\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * returns the value for a key from the building part's tags or the\n\t\t * building's tags (if the part doesn't have a tag with this key)\n\t\t */\n\t\tprivate String getValue(String key) {\n\n\t\t\tif (area.getTags().containsKey(key)) {\n\t\t\t\treturn area.getTags().getValue(key);\n\t\t\t} else {\n\t\t\t\treturn building.area.getTags().getValue(key);\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tprivate static final float DEFAULT_RIDGE_HEIGHT = 5;\n\t\t\n\t\tpublic static interface Roof extends RenderableToAllTargets {\n\t\t\t\t\t\t\n\t\t\t/**\n\t\t\t * returns the outline (with holes) of the roof.\n\t\t\t * The shape will be generally identical to that of the\n\t\t\t * building itself, but additional vertices might have\n\t\t\t * been inserted into segments.\n\t\t\t */\n\t\t\tPolygonWithHolesXZ getPolygon();\n\n\t\t\t/**\n\t\t\t * returns roof elevation at a position.\n\t\t\t */\n\t\t\tdouble getRoofEleAt(VectorXZ coord);\n\t\t\t\n\t\t\t/**\n\t\t\t * returns maximum roof height\n\t\t\t */\n\t\t\tdouble getRoofHeight();\n\n\t\t\t/**\n\t\t\t * returns maximum roof elevation\n\t\t\t */\n\t\t\tdouble getMaxRoofEle();\n\t\t\t\n\t\t}\n\n\t\t/**\n\t\t * superclass for roofs based on roof:type tags.\n\t\t * Contains common functionality, such as roof height parsing.\n\t\t */\n\t\tabstract private class TaggedRoof implements Roof {\n\t\t\n\t\t\tprotected final double roofHeight;\n\t\t\t\n\t\t\t/*\n\t\t\t * default roof height if no value is tagged explicitly.\n\t\t\t * Can optionally be overwritten by subclasses.\n\t\t\t */\n\t\t\tprotected float getDefaultRoofHeight() {\n\t\t\t\tif (buildingLevels == 1) {\n\t\t\t\t\treturn 1;\n\t\t\t\t} else {\n\t\t\t\t\treturn DEFAULT_RIDGE_HEIGHT;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tTaggedRoof() {\n\t\t\t\t\n\t\t\t\tFloat taggedHeight = null;\n\t\t\t\t\n\t\t\t\tif (area.getTags().containsKey(\"roof:height\")) {\n\t\t\t\t\tString valueString = getValue(\"roof:height\");\n\t\t\t\t\ttaggedHeight = parseMeasure(valueString);\n\t\t\t\t} else if (getValue(\"roof:levels\") != null) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\ttaggedHeight = 2.5f * Integer.parseInt(getValue(\"roof:levels\"));\n\t\t\t\t\t} catch (NumberFormatException e) {}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\troofHeight =\n\t\t\t\t\ttaggedHeight != null ? taggedHeight : getDefaultRoofHeight();\n\t\t\t\t\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic double getRoofHeight() {\n\t\t\t\treturn roofHeight;\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic double getMaxRoofEle() {\n\t\t\t\treturn building.getGroundLevelEle() +\n\t\t\t\t\t\theightWithoutRoof + roofHeight;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate abstract class SpindleRoof extends TaggedRoof {\n\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\treturn polygon;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic double getRoofEleAt(VectorXZ pos) {\n\t\t\t\treturn getMaxRoofEle() - getRoofHeight();\n\t\t\t}\n\t\t\t\n\t\t\tprotected void renderSpindle(\n\t\t\t\t\tTarget<?> target, Material material,\n\t\t\t\t\tSimplePolygonXZ polygon,\n\t\t\t\t\tList<Double> heights, List<Double> scaleFactors) {\n\t\t\t\t\n\t\t\t\tcheckArgument(heights.size() == scaleFactors.size(),\n\t\t\t\t\t\t\"heights and scaleFactors must have same size\");\n\t\t\t\t\n\t\t\t\tint numRings = heights.size();\n\t\t\t\tVectorXZ center = polygon.getCenter();\n\t\t\t\t\n\t\t\t\t/* calculate the vertex rings */\n\t\t\t\t\n\t\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\t\tList<VectorXYZ>[] rings = new List[numRings];\n\n\t\t\t\tfor (int i = 0; i < numRings; i++) {\n\t\t\t\t\t\n\t\t\t\t\tdouble y = heights.get(i);\n\t\t\t\t\tdouble scale = scaleFactors.get(i);\n\t\t\t\t\t\n\t\t\t\t\tif (scale == 0) {\n\t\t\t\t\t\t\n\t\t\t\t\t\trings[i] = nCopies(polygon.size() + 1, center.xyz(y));\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\trings[i] = new ArrayList<VectorXYZ>();\n\t\t\t\t\t\tfor (VectorXZ v : polygon.getVertexLoop()) {\n\t\t\t\t\t\t\trings[i].add(interpolateBetween(center, v, scale).xyz(y));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t/* draw the triangle strips (or fans) between the rings */\n\t\t\t\t\n\t\t\t\tList<List<VectorXZ>> texCoordData[] = spindleTexCoordLists(\n\t\t\t\t\t\trings, polygon.getOutlineLength(), material);\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i+1 < numRings; i++) {\n\t\t\t\t\t\n\t\t\t\t\tList<VectorXYZ> vs = new ArrayList<VectorXYZ>();\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\tfor (int v = 0; v < rings[i].size(); v ++) {\n\t\t\t\t\t\tvs.add(rings[i].get(v));\n\t\t\t\t\t\tvs.add(rings[i+1].get(v));\n\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\ttarget.drawTriangleStrip(material, vs, texCoordData[i]);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tprotected List<List<VectorXZ>>[] spindleTexCoordLists(\n\t\t\t\t\tList<VectorXYZ>[] rings, double polygonLength,\n\t\t\t\t\tMaterial material) {\n\t\t\t\t\n\t\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\t\tList<List<VectorXZ>>[] result = new List[rings.length - 1];\n\t\t\t\t\n\t\t\t\tdouble accumulatedTexHeight = 0;\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i+1 < rings.length; i++) {\n\t\t\t\t\t\n\t\t\t\t\tdouble texHeight =\n\t\t\t\t\t\trings[i].get(0).distanceTo(rings[i+1].get(0));\n\t\t\t\t\t\n\t\t\t\t\tList<TextureData> textureDataList =\n\t\t\t\t\t\tmaterial.getTextureDataList();\n\t\t\t\t\t\n\t\t\t\t\tif (textureDataList.size() == 0) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tresult[i] = emptyList();\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (textureDataList.size() == 1) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tresult[i] = singletonList(spindleTexCoordList(\n\t\t\t\t\t\t\t\trings[i], rings[i+1], polygonLength,\n\t\t\t\t\t\t\t\taccumulatedTexHeight, textureDataList.get(0)));\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\tresult[i] = new ArrayList<List<VectorXZ>>();\n\t\t\t\t\t\t\n\t\t\t\t\t\tfor (TextureData textureData : textureDataList) {\n\t\t\t\t\t\t\tresult[i].add(spindleTexCoordList(\n\t\t\t\t\t\t\t\t\trings[i], rings[i+1], polygonLength,\n\t\t\t\t\t\t\t\t\taccumulatedTexHeight, textureData));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\taccumulatedTexHeight += texHeight;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\treturn result;\n\t\t\t\t\n\t\t\t}\n\n\t\t\tprivate List<VectorXZ> spindleTexCoordList(\n\t\t\t\t\tList<VectorXYZ> lowerRing, List<VectorXYZ> upperRing,\n\t\t\t\t\tdouble polygonLength, double accumulatedTexHeight,\n\t\t\t\t\tTextureData textureData) {\n\t\t\t\t\n\t\t\t\tdouble textureRepeats = max(1,\n\t\t\t\t\t\tround(polygonLength / textureData.width));\n\t\t\t\t\n\t\t\t\tdouble texWidthSteps = textureRepeats / (lowerRing.size() - 1);\n\t\t\t\tdouble texHeight = lowerRing.get(0).distanceTo(upperRing.get(0));\n\t\t\t\t\n\t\t\t\tdouble texZ1 = accumulatedTexHeight / textureData.height;\n\t\t\t\tdouble texZ2 = (accumulatedTexHeight + texHeight) / textureData.height;\n\t\t\t\t\n\t\t\t\tVectorXZ[] texCoords = new VectorXZ[2 * lowerRing.size()];\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i < lowerRing.size(); i++) {\n\t\t\t\t\ttexCoords[2*i] = new VectorXZ(i*texWidthSteps, -texZ1);\n\t\t\t\t\ttexCoords[2*i+1] = new VectorXZ(i*texWidthSteps, -texZ2);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\treturn asList(texCoords);\n\t\t\t\t\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tprotected float getDefaultRoofHeight() {\n\t\t\t\treturn (float)polygon.getOuter().getDiameter() / 2;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class OnionRoof extends SpindleRoof {\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\t\n\t\t\t\tdouble roofY = getMaxRoofEle() - getRoofHeight();\n\t\t\t\t\n\t\t\t\trenderSpindle(target, materialRoof,\n\t\t\t\t\t\tpolygon.getOuter().makeClockwise(),\n\t\t\t\t\t\tasList(roofY,\n\t\t\t\t\t\t\t\troofY + 0.15 * roofHeight,\n\t\t\t\t\t\t\t\troofY + 0.52 * roofHeight,\n\t\t\t\t\t\t\t\troofY + 0.72 * roofHeight,\n\t\t\t\t\t\t\t\troofY + 0.82 * roofHeight,\n\t\t\t\t\t\t\t\troofY + 1.0 * roofHeight),\n\t\t\t\t\t\tasList(1.0, 0.8, 1.0, 0.7, 0.15, 0.0));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tprotected float getDefaultRoofHeight() {\n\t\t\t\treturn (float)polygon.getOuter().getDiameter();\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class DomeRoof extends SpindleRoof {\n\n\t\t\t/**\n\t\t\t * number of height rings to approximate the round dome shape\n\t\t\t */\n\t\t\tprivate static final int HEIGHT_RINGS = 10;\n\t\t\t\t\t\t\n\t\t\t@Override\n\t\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\t\n\t\t\t\tdouble roofY = getMaxRoofEle() - getRoofHeight();\n\n\t\t\t\tList<Double> heights = new ArrayList<Double>();\n\t\t\t\tList<Double> scales = new ArrayList<Double>();\n\t\t\t\t\n\t\t\t\tfor (int ring = 0; ring < HEIGHT_RINGS; ++ring) {\n\t\t\t\t\tdouble relativeHeight = (double)ring / (HEIGHT_RINGS - 1);\n\t\t\t\t\theights.add(roofY + relativeHeight * roofHeight);\n\t\t\t\t\tscales.add(sqrt(1.0 - relativeHeight * relativeHeight));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\trenderSpindle(target, materialRoof,\n\t\t\t\t\t\tpolygon.getOuter().makeClockwise(),\n\t\t\t\t\t\theights, scales);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * superclass for roofs that have exactly one height value\n\t\t * for each point within their XZ polygon\n\t\t */\n\t\tpublic abstract class HeightfieldRoof extends TaggedRoof {\n\n\t\t\t/**\n\t\t\t * returns segments within the roof polygon\n\t\t\t * that define ridges or edges of the roof\n\t\t\t */\n\t\t\tpublic abstract Collection<LineSegmentXZ> getInnerSegments();\n\n\t\t\t/**\n\t\t\t * returns segments within the roof polygon\n\t\t\t * that define apex nodes of the roof\n\t\t\t */\n\t\t\tpublic abstract Collection<VectorXZ> getInnerPoints();\n\t\t\t\n\t\t\t/**\n\t\t\t * returns roof elevation at a position.\n\t\t\t * Only required to work for positions that are part of the\n\t\t\t * polygon, segments or points for the roof.\n\t\t\t * \n\t\t\t * @return  elevation, null if unknown\n\t\t\t */\n\t\t\tprotected abstract Double getRoofEleAt_noInterpolation(VectorXZ pos);\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic double getRoofEleAt(VectorXZ v) {\n\t\t\t\t\n\t\t\t\tDouble ele = getRoofEleAt_noInterpolation(v);\n\t\t\t\t\n\t\t\t\tif (ele != null) {\n\t\t\t\t\treturn ele;\n\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\t// get all segments from the roof\n\t\t\t\t\t\n\t\t\t\t\t//TODO (performance): avoid doing this for every node\n\t\t\t\t\t\n\t\t\t\t\tCollection<LineSegmentXZ> segments =\n\t\t\t\t\t\tnew ArrayList<LineSegmentXZ>();\n\t\t\t\t\t\n\t\t\t\t\tsegments.addAll(this.getInnerSegments());\n\t\t\t\t\tsegments.addAll(this.getPolygon().getOuter().getSegments());\n\t\t\t\t\tfor (SimplePolygonXZ hole : this.getPolygon().getHoles()) {\n\t\t\t\t\t\tsegments.addAll(hole.getSegments());\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// find the segment with the closest distance to the node\n\t\t\t\t\t\n\t\t\t\t\tLineSegmentXZ closestSegment = null;\n\t\t\t\t\tdouble closestSegmentDistance = Double.MAX_VALUE;\n\t\t\t\t\t\n\t\t\t\t\tfor (LineSegmentXZ segment : segments) {\n\t\t\t\t\t\tdouble segmentDistance = distanceFromLineSegment(v, segment);\n\t\t\t\t\t\tif (segmentDistance < closestSegmentDistance) {\n\t\t\t\t\t\t\tclosestSegment = segment;\n\t\t\t\t\t\t\tclosestSegmentDistance = segmentDistance;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// use that segment for height interpolation\n\t\t\t\t\t\n\t\t\t\t\treturn interpolateValue(v,\n\t\t\t\t\t\t\tclosestSegment.p1,\n\t\t\t\t\t\t\tgetRoofEleAt_noInterpolation(closestSegment.p1),\n\t\t\t\t\t\t\tclosestSegment.p2,\n\t\t\t\t\t\t\tgetRoofEleAt_noInterpolation(closestSegment.p2));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\t\n\t\t\t\t/* create the triangulation of the roof */\n\t\t\t\t\n\t\t\t\tCollection<TriangleXZ> triangles;\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\t\n\t\t\t\t\ttriangles = Poly2TriUtil.triangulate(\n\t\t\t\t\t\t\tgetPolygon().getOuter(),\n\t\t\t\t\t\t    getPolygon().getHoles(),\n\t\t\t\t\t\t    getInnerSegments(),\n\t\t\t\t\t\t    getInnerPoints());\n\t\t\t\t\n\t\t\t\t} catch (TriangulationException e) {\n\t\t\t\t\n\t\t\t\t\t\ttriangles = JTSTriangulationUtil.triangulate(\n\t\t\t\t\t\t\t\tgetPolygon().getOuter(),\n\t\t\t\t\t\t\t\tgetPolygon().getHoles(),\n\t\t\t\t\t\t\t\tgetInnerSegments(),\n\t\t\t\t\t\t\t\tgetInnerPoints());\n\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tList<TriangleXYZ> trianglesXYZ =\n\t\t\t\t\t\tnew ArrayList<TriangleXYZ>(triangles.size());\n\t\t\t\t\n\t\t\t\tfor (TriangleXZ triangle : triangles) {\n\t\t\t\t\tTriangleXZ tCCW = triangle.makeCounterclockwise();\n\t\t\t\t\ttrianglesXYZ.add(new TriangleXYZ(\n\t\t\t\t\t\t\twithRoofEle(tCCW.v1),\n\t\t\t\t\t\t\twithRoofEle(tCCW.v2),\n\t\t\t\t\t\t\twithRoofEle(tCCW.v3)));\n\t\t\t\t\t//TODO: avoid duplicate objects for points in more than one triangle\n\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t/* draw triangles */\n\t\t\t\t\n\t\t\t\ttarget.drawTriangles(materialRoof, trianglesXYZ,\n\t\t\t\t\t\ttriangleTexCoordLists(trianglesXYZ,\n\t\t\t\t\t\t\t\tmaterialRoof, SLOPED_TRIANGLES));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tprivate VectorXYZ withRoofEle(VectorXZ v) {\n\t\t\t\treturn v.xyz(getRoofEleAt(v));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class FlatRoof extends HeightfieldRoof {\n\t\t\t\t\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\treturn polygon;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic double getRoofHeight() {\n\t\t\t\treturn 0;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\t\t\t\treturn getMaxRoofEle();\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic double getMaxRoofEle() {\n\t\t\t\treturn building.getGroundLevelEle() + heightWithoutRoof;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class PyramidalRoof extends HeightfieldRoof {\n\t\t\t\n\t\t\tprivate final VectorXZ apex;\n\t\t\tprivate final List<LineSegmentXZ> innerSegments;\n\t\t\t\n\t\t\tpublic PyramidalRoof() {\n\t\t\t\n\t\t\t\tsuper();\n\t\t\t\t\n\t\t\t\tSimplePolygonXZ outerPoly = polygon.getOuter();\n\t\t\t\t\n\t\t\t\tapex = outerPoly.getCentroid();\n\t\t\t\t\n\t\t\t\tinnerSegments = new ArrayList<LineSegmentXZ>();\n\t\t\t\tfor (VectorXZ v : outerPoly.getVertices()) {\n\t\t\t\t\tinnerSegments.add(new LineSegmentXZ(v, apex));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\treturn polygon;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn singletonList(apex);\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\t\t\t\treturn innerSegments;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\t\t\t\tif (apex.equals(pos)) {\n\t\t\t\t\treturn getMaxRoofEle();\n\t\t\t\t} else if (polygon.getOuter().getVertices().contains(pos)) {\n\t\t\t\t\treturn getMaxRoofEle() - roofHeight;\n\t\t\t\t} else {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class SkillionRoof extends HeightfieldRoof {\n\t\t\t\n\t\t\tprivate final LineSegmentXZ ridge;\n\t\t\tprivate final double roofLength;\n\t\t\t\n\t\t\tpublic SkillionRoof() {\n\t\t\t\t\n\t\t\t\t/* parse slope direction */\n\t\t\t\t\n\t\t\t\tVectorXZ slopeDirection = null;\n\t\t\t\t\n\t\t\t\tif (getValue(\"roof:direction\") != null) {\n\t\t\t\t\tFloat angle = parseAngle(\n\t\t\t\t\t\t\tgetValue(\"roof:direction\"));\n\t\t\t\t\tif (angle != null) {\n\t\t\t\t\t\tslopeDirection = VectorXZ.fromAngle(toRadians(angle));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// fallback from roof:direction to roof:slope:direction\n\t\t\t\tif (slopeDirection == null\n\t\t\t\t\t\t&& getValue(\"roof:slope:direction\") != null) {\n\t\t\t\t\tFloat angle = parseAngle(\n\t\t\t\t\t\t\tgetValue(\"roof:slope:direction\"));\n\t\t\t\t\tif (angle != null) {\n\t\t\t\t\t\tslopeDirection = VectorXZ.fromAngle(toRadians(angle));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (slopeDirection != null) {\n\t\t\t\t\t\n\t\t\t\t\tSimplePolygonXZ simplifiedOuter =\n\t\t\t\t\t\t\tpolygon.getOuter().getSimplifiedPolygon();\n\t\t\t\t\t\n\t\t\t\t\t/* find ridge by calculating the outermost intersections of\n\t\t\t\t\t * the quasi-infinite slope \"line\" towards the centroid vector\n\t\t\t\t\t * with segments of the polygon */\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ center = simplifiedOuter.getCentroid();\n\t\t\t\t\t\n\t\t\t\t\tCollection<LineSegmentXZ> intersections =\n\t\t\t\t\t\t\tsimplifiedOuter.intersectionSegments(new LineSegmentXZ(\n\t\t\t\t\t\t\t\tcenter.add(slopeDirection.mult(-1000)), center));\n\t\t\t\t\t\n\t\t\t\t\tLineSegmentXZ outermostIntersection = null;\n\t\t\t\t\tdouble distanceOutermostIntersection = -1;\n\t\t\t\t\t\n\t\t\t\t\tfor (LineSegmentXZ i : intersections) {\n\t\t\t\t\t\tdouble distance = distanceFromLineSegment(center, i);\n\t\t\t\t\t\tif (distance > distanceOutermostIntersection) {\n\t\t\t\t\t\t\toutermostIntersection = i;\n\t\t\t\t\t\t\tdistanceOutermostIntersection = distance;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tridge = outermostIntersection;\n\t\t\t\t\t\n\t\t\t\t\t/* calculate maximum distance from ridge */\n\t\t\t\t\t\n\t\t\t\t\tdouble maxDistance = 0.1;\n\t\t\t\t\t\n\t\t\t\t\tfor (VectorXZ v : polygon.getOuter().getVertexLoop()) {\n\t\t\t\t\t\tdouble distance = distanceFromLine(v, ridge.p1, ridge.p2);\n\t\t\t\t\t\tif (distance > maxDistance) {\n\t\t\t\t\t\t\tmaxDistance = distance;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\troofLength = maxDistance;\n\t\t\t\t\t\n\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\tridge = null;\n\t\t\t\t\troofLength = Double.NaN;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\treturn polygon;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tprotected Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\t\t\t\tif (ridge == null) {\n\t\t\t\t\treturn getMaxRoofEle();\n\t\t\t\t} else {\n\t\t\t\t\tdouble distance = distanceFromLineSegment(pos, ridge);\n\t\t\t\t\tdouble relativeDistance = distance / roofLength;\n\t\t\t\t\treturn getMaxRoofEle() - relativeDistance * roofHeight;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t}\n\t\t\t\t\n\t\t/**\n\t\t * tagged roof with a ridge.\n\t\t * Deals with ridge calculation for various subclasses.\n\t\t */\n\t\tabstract private class RoofWithRidge extends HeightfieldRoof {\n\t\t\t\n\t\t\t/** absolute distance of ridge to outline */\n\t\t\tprotected final double ridgeOffset;\n\t\t\t\n\t\t\tprotected final LineSegmentXZ ridge;\n\t\t\t\n\t\t\t/** the roof cap that is closer to the first vertex of the ridge */\n\t\t\tprotected final LineSegmentXZ cap1;\n\t\t\t/** the roof cap that is closer to the second vertex of the ridge */\n\t\t\tprotected final LineSegmentXZ cap2;\n\t\t\t\n\t\t\t/** maximum distance of any outline vertex to the ridge */\n\t\t\tprotected final double maxDistanceToRidge;\n\t\t\t\n\t\t\t/**\n\t\t\t * creates an instance and calculates the final fields\n\t\t\t * \n\t\t\t * @param relativeRoofOffset  distance of ridge to outline\n\t\t\t *    relative to length of roof cap; 0 if ridge ends at outline\n\t\t\t */\n\t\t\tpublic RoofWithRidge(double relativeRoofOffset) {\n\t\t\t\n\t\t\t\tsuper();\n\t\t\t\t\n\t\t\t\tSimplePolygonXZ outerPoly = polygon.getOuter();\n\t\t\t\t\n\t\t\t\tSimplePolygonXZ simplifiedPolygon =\n\t\t\t\t\touterPoly.getSimplifiedPolygon();\n\t\t\t\t\n\t\t\t\t/* determine ridge direction based on tag if it exists,\n\t\t\t\t * otherwise choose direction of longest polygon segment */\n\t\t\t\t\n\t\t\t\tVectorXZ ridgeDirection = null;\n\n\t\t\t\tif (getValue(\"roof:direction\") != null) {\n\t\t\t\t\tFloat angle = parseAngle(\n\t\t\t\t\t\t\tgetValue(\"roof:direction\"));\n\t\t\t\t\tif (angle != null) {\n\t\t\t\t\t\tridgeDirection = VectorXZ.fromAngle(toRadians(angle)).rightNormal();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (ridgeDirection == null && getValue(\"roof:ridge:direction\") != null) {\n\t\t\t\t\tFloat angle = parseAngle(\n\t\t\t\t\t\t\tgetValue(\"roof:ridge:direction\"));\n\t\t\t\t\tif (angle != null) {\n\t\t\t\t\t\tridgeDirection = VectorXZ.fromAngle(toRadians(angle));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (ridgeDirection == null) {\n\t\t\t\t\t\n\t\t\t\t\tLineSegmentXZ longestSeg = MinMaxUtil.max(\n\t\t\t\t\t\t\tsimplifiedPolygon.getSegments(),\n\t\t\t\t\t\t\tnew Function<LineSegmentXZ, Double>() {\n\t\t\t\t\t\t\t\tpublic Double apply(LineSegmentXZ s) {\n\t\t\t\t\t\t\t\t\treturn s.getLength();\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\n\t\t\t\t\tridgeDirection =\n\t\t\t\t\t\tlongestSeg.p2.subtract(longestSeg.p1).normalize();\n\t\t\t\t\t\n\t\t\t\t\tif (area.getTags().contains(\"roof:orientation\", \"across\")) {\n\t\t\t\t\t\tridgeDirection = ridgeDirection.rightNormal();\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t/* calculate the two outermost intersections of the\n\t\t\t\t * quasi-infinite ridge line with segments of the polygon */\n\t\t\t\t\n\t\t\t\tVectorXZ p1 = outerPoly.getCentroid();\n\t\t\t\t\n\t\t\t\tCollection<LineSegmentXZ> intersections =\n\t\t\t\t\tsimplifiedPolygon.intersectionSegments(new LineSegmentXZ(\n\t\t\t\t\t\t\tp1.add(ridgeDirection.mult(-1000)),\n\t\t\t\t\t\t\tp1.add(ridgeDirection.mult(1000))\n\t\t\t\t\t));\n\n\t\t\t\tif (intersections.size() < 2) {\n\t\t\t\t\tthrow new InvalidGeometryException(\n\t\t\t\t\t\t\t\"cannot handle roof geometry for id \"\n\t\t\t\t\t\t\t\t\t+ area.getOsmObject().id);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t//TODO choose outermost instead of any pair of intersections\n\t\t\t\tIterator<LineSegmentXZ> it = intersections.iterator();\n\t\t\t\tcap1 = it.next();\n\t\t\t\tcap2 = it.next();\n\t\t\t\t\n\t\t\t\t/* base ridge on the centers of the intersected segments\n\t\t\t\t * (the intersections itself are not used because the\n\t\t\t\t * tagged ridge direction is likely not precise)       */\n\n\t\t\t\tVectorXZ c1 = cap1.getCenter();\n\t\t\t\tVectorXZ c2 = cap2.getCenter();\n\t\t\t\t\n\t\t\t\tridgeOffset = min(\n\t\t\t\t\t\tcap1.getLength() * relativeRoofOffset,\n\t\t\t\t\t\t0.4 * c1.distanceTo(c2));\n\t\t\t\t\n\t\t\t\tif (relativeRoofOffset == 0) {\n\t\t\t\t\t\n\t\t\t\t\tridge = new LineSegmentXZ(c1, c2);\n\t\t\t\t\t\n\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\tridge = new LineSegmentXZ(\n\t\t\t\t\t\t\tc1.add( p1.subtract(c1).normalize().mult(ridgeOffset) ),\n\t\t\t\t\t\t\tc2.add( p1.subtract(c2).normalize().mult(ridgeOffset) ));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t/* calculate maxDistanceToRidge */\n\t\t\t\t\n\t\t\t\tdouble maxDistance = 0;\n\t\t\t\t\n\t\t\t\tfor (VectorXZ v : outerPoly.getVertices()) {\n\t\t\t\t\tmaxDistance = max (maxDistance,\n\t\t\t\t\t\t\tdistanceFromLineSegment(v, ridge));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tmaxDistanceToRidge = maxDistance;\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class GabledRoof extends RoofWithRidge {\n\n\t\t\tpublic GabledRoof() {\n\t\t\t\tsuper(0);\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\t\n\t\t\t\tPolygonXZ newOuter = polygon.getOuter();\n\t\t\t\t\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, ridge.p1, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, ridge.p2, 0.2);\n\t\t\t\t\n\t\t\t\treturn new PolygonWithHolesXZ(\n\t\t\t\t\t\tnewOuter.asSimplePolygon(),\n\t\t\t\t\t\tpolygon.getHoles());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\t\t\t\treturn singleton(ridge);\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\t\t\t\tdouble distRidge = distanceFromLineSegment(pos, ridge);\n\t\t\t\tdouble relativePlacement = distRidge / maxDistanceToRidge;\n\t\t\t\treturn getMaxRoofEle() - roofHeight * relativePlacement;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class HippedRoof extends RoofWithRidge {\n\n\t\t\tpublic HippedRoof() {\n\t\t\t\tsuper(1/3.0);\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\treturn polygon;\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\t\t\t\treturn asList(\n\t\t\t\t\t\tridge,\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p1, cap1.p1),\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p1, cap1.p2),\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p2, cap2.p1),\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p2, cap2.p2));\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\t\t\t\tif (ridge.p1.equals(pos) || ridge.p2.equals(pos)) {\n\t\t\t\t\treturn getMaxRoofEle();\n\t\t\t\t} else if (getPolygon().getOuter().getVertexLoop().contains(pos)) {\n\t\t\t\t\treturn getMaxRoofEle() - roofHeight;\n\t\t\t\t} else {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class HalfHippedRoof extends RoofWithRidge {\n\n\t\t\tprivate final LineSegmentXZ cap1part, cap2part;\n\t\t\t\n\t\t\tpublic HalfHippedRoof() {\n\t\t\t\t\n\t\t\t\tsuper(1/6.0);\n\t\t\t\t\n\t\t\t\tcap1part = new LineSegmentXZ(\n\t\t\t\t\t\tinterpolateBetween(cap1.p1, cap1.p2,\n\t\t\t\t\t\t\t\t0.5 - ridgeOffset / cap1.getLength()),\n\t\t\t\t\t\tinterpolateBetween(cap1.p1, cap1.p2,\n\t\t\t\t\t\t\t\t0.5 + ridgeOffset / cap1.getLength()));\n\t\t\t\t\n\t\t\t\tcap2part = new LineSegmentXZ(\n\t\t\t\t\t\tinterpolateBetween(cap2.p1, cap2.p2,\n\t\t\t\t\t\t\t\t0.5 - ridgeOffset / cap1.getLength()),\n\t\t\t\t\t\tinterpolateBetween(cap2.p1, cap2.p2,\n\t\t\t\t\t\t\t\t0.5 + ridgeOffset / cap1.getLength()));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\t\n\t\t\t\tPolygonXZ newOuter = polygon.getOuter();\n\t\t\t\t\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, cap1part.p1, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, cap1part.p2, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, cap2part.p1, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, cap2part.p2, 0.2);\n\t\t\t\t\n\t\t\t\treturn new PolygonWithHolesXZ(\n\t\t\t\t\t\tnewOuter.asSimplePolygon(),\n\t\t\t\t\t\tpolygon.getHoles());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\t\t\t\treturn asList(ridge,\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p1, cap1part.p1),\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p1, cap1part.p2),\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p2, cap2part.p1),\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p2, cap2part.p2));\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\t\t\t\tif (ridge.p1.equals(pos) || ridge.p2.equals(pos)) {\n\t\t\t\t\treturn getMaxRoofEle();\n\t\t\t\t} else if (getPolygon().getOuter().getVertexLoop().contains(pos)) {\n\t\t\t\t\tif (distanceFromLineSegment(pos, cap1part) < 0.05) {\n\t\t\t\t\t\treturn getMaxRoofEle()\n\t\t\t\t\t\t\t- roofHeight * ridgeOffset / (cap1.getLength()/2);\n\t\t\t\t\t} else if (distanceFromLineSegment(pos, cap2part) < 0.05) {\n\t\t\t\t\t\treturn getMaxRoofEle()\n\t\t\t\t\t\t\t- roofHeight * ridgeOffset / (cap2.getLength()/2);\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn getMaxRoofEle() - roofHeight;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class GambrelRoof extends RoofWithRidge {\n\n\t\t\tprivate final LineSegmentXZ cap1part, cap2part;\n\t\t\t\n\t\t\tpublic GambrelRoof() {\n\t\t\t\t\n\t\t\t\tsuper(0);\n\t\t\t\t\n\t\t\t\tcap1part = new LineSegmentXZ(\n\t\t\t\t\t\tinterpolateBetween(cap1.p1, cap1.p2, 1/6.0),\n\t\t\t\t\t\tinterpolateBetween(cap1.p1, cap1.p2, 5/6.0));\n\n\t\t\t\tcap2part = new LineSegmentXZ(\n\t\t\t\t\t\tinterpolateBetween(cap2.p1, cap2.p2, 1/6.0),\n\t\t\t\t\t\tinterpolateBetween(cap2.p1, cap2.p2, 5/6.0));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\t\n\t\t\t\tPolygonXZ newOuter = polygon.getOuter();\n\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, ridge.p1, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, ridge.p2, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, cap1part.p1, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, cap1part.p2, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, cap2part.p1, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, cap2part.p2, 0.2);\n\t\t\t\t\n\t\t\t\t//TODO: add intersections of additional edges with outline?\n\t\t\t\t\n\t\t\t\treturn new PolygonWithHolesXZ(\n\t\t\t\t\t\tnewOuter.asSimplePolygon(),\n\t\t\t\t\t\tpolygon.getHoles());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\t\t\t\treturn asList(ridge,\n\t\t\t\t\t\tnew LineSegmentXZ(cap1part.p1, cap2part.p2),\n\t\t\t\t\t\tnew LineSegmentXZ(cap1part.p2, cap2part.p1));\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\n\t\t\t\tdouble distRidge = distanceFromLineSegment(pos, ridge);\n\t\t\t\tdouble relativePlacement = distRidge / maxDistanceToRidge;\n\t\t\t\t\n\t\t\t\tif (relativePlacement < 2/3.0) {\n\t\t\t\t\treturn getMaxRoofEle()\n\t\t\t\t\t\t- 1/2.0 * roofHeight * relativePlacement;\n\t\t\t\t} else {\n\t\t\t\t\treturn getMaxRoofEle() - 1/3.0 * roofHeight\n\t\t\t\t\t\t- 2 * roofHeight * (relativePlacement - 2/3.0);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate class RoundRoof extends RoofWithRidge {\n\t\t\tprivate final static double ROOF_SUBDIVISION_METER = 2.5;\n\t\t\t\n\t\t\tprivate final List<LineSegmentXZ> capParts;\n\t\t\tprivate final int rings;\n\t\t\tprivate final double radius;\n\t\t\t\n\t\t\tpublic RoundRoof() {\n\n\t\t\t\tsuper(0);\n\n\t\t\t\tif (roofHeight < maxDistanceToRidge) {\n\t\t\t\t\tdouble squaredHeight = roofHeight * roofHeight;\n\t\t\t\t\tdouble squaredDist = maxDistanceToRidge * maxDistanceToRidge;\n\t\t\t\t\tdouble centerY =  (squaredDist - squaredHeight) / (2 * roofHeight);\n\t\t\t\t\tradius = sqrt(squaredDist + centerY * centerY);\n\t\t\t\t} else {\n\t\t\t\t\tradius = 0;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\trings = (int)Math.max(3, roofHeight/ROOF_SUBDIVISION_METER);\n\t\t\t\tcapParts = new ArrayList<LineSegmentXZ>(rings*2);\n\t\t\t\t// TODO: would be good to vary step size with slope\n\t\t\t\tfloat step = 0.5f / (rings + 1);\n\t\t\t\tfor (int i = 1; i <= rings; i++) {\n\t\t\t\t\tcapParts.add(new LineSegmentXZ(\n\t\t\t\t\t\t\tinterpolateBetween(cap1.p1, cap1.p2, i * step),\n\t\t\t\t\t\t\tinterpolateBetween(cap1.p1, cap1.p2, 1 - i * step)));\n\n\t\t\t\t\tcapParts.add(new LineSegmentXZ(\n\t\t\t\t\t\t\tinterpolateBetween(cap2.p1, cap2.p2, i * step),\n\t\t\t\t\t\t\tinterpolateBetween(cap2.p1, cap2.p2, 1 - i * step)));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\n\t\t\t\tPolygonXZ newOuter = polygon.getOuter();\n\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, ridge.p1, 0.2);\n\t\t\t\tnewOuter = insertIntoPolygon(newOuter, ridge.p2, 0.2);\n\n\t\t\t\tfor (LineSegmentXZ capPart : capParts){\n\t\t\t\t\tnewOuter = insertIntoPolygon(newOuter, capPart.p1, 0.2);\n\t\t\t\t\tnewOuter = insertIntoPolygon(newOuter, capPart.p2, 0.2);\n\t\t\t\t}\n\n\t\t\t\t//TODO: add intersections of additional edges with outline?\n\t\t\t\treturn new PolygonWithHolesXZ(\n\t\t\t\t\t\tnewOuter.asSimplePolygon(),\n\t\t\t\t\t\tpolygon.getHoles());\n\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\n\t\t\t\tList<LineSegmentXZ> innerSegments = new ArrayList<LineSegmentXZ>(rings * 2 + 1);\n\t\t\t\tinnerSegments.add(ridge);\n\t\t\t\tfor (int i = 0; i < rings * 2; i += 2) {\n\t\t\t\t\tLineSegmentXZ cap1part = capParts.get(i);\n\t\t\t\t\tLineSegmentXZ cap2part = capParts.get(i+1);\n\t\t\t\t\tinnerSegments.add(new LineSegmentXZ(cap1part.p1, cap2part.p2));\n\t\t\t\t\tinnerSegments.add(new LineSegmentXZ(cap1part.p2, cap2part.p1));\n\t\t\t\t}\n\n\t\t\t\treturn innerSegments;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\t\t\t\tdouble distRidge = distanceFromLineSegment(pos, ridge);\n\t\t\t\tdouble ele;\n\t\t\t\t\n\t\t\t\tif (radius > 0) {\n\t\t\t\t\tdouble relativePlacement = distRidge / radius;\n\t\t\t\t\tele = getMaxRoofEle() - radius\n\t\t\t\t\t\t+ sqrt(1.0 - relativePlacement * relativePlacement) * radius;\n\t\t\t\t} else {\n\t\t\t\t\t// This could be any interpolator\n\t\t\t\t\tdouble relativePlacement = distRidge / maxDistanceToRidge;\n\t\t\t\t\tele = getMaxRoofEle() - roofHeight +\n\t\t\t\t\t(1 - (Math.pow(relativePlacement, 2.5))) * roofHeight;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\treturn Math.max(ele, getMaxRoofEle() - roofHeight);\n\t\t\t}\n\t\t}\n\n\t\tprivate class MansardRoof extends RoofWithRidge {\n\n\t\t\tprivate final LineSegmentXZ mansardEdge1, mansardEdge2;\n\t\t\t\n\t\t\tpublic MansardRoof() {\n\t\t\t\t\n\t\t\t\tsuper(1/3.0);\n\t\t\t\t\n\t\t\t\tmansardEdge1 = new LineSegmentXZ(\n\t\t\t\t\t\tinterpolateBetween(cap1.p1, ridge.p1, 1/3.0),\n\t\t\t\t\t\tinterpolateBetween(cap2.p2, ridge.p2, 1/3.0));\n\n\t\t\t\tmansardEdge2 = new LineSegmentXZ(\n\t\t\t\t\t\tinterpolateBetween(cap1.p2, ridge.p1, 1/3.0),\n\t\t\t\t\t\tinterpolateBetween(cap2.p1, ridge.p2, 1/3.0));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\treturn polygon;\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\t\t\t\treturn asList(ridge,\n\t\t\t\t\t\tmansardEdge1,\n\t\t\t\t\t\tmansardEdge2,\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p1, mansardEdge1.p1),\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p1, mansardEdge2.p1),\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p2, mansardEdge1.p2),\n\t\t\t\t\t\tnew LineSegmentXZ(ridge.p2, mansardEdge2.p2),\n\t\t\t\t\t\tnew LineSegmentXZ(cap1.p1, mansardEdge1.p1),\n\t\t\t\t\t\tnew LineSegmentXZ(cap2.p2, mansardEdge1.p2),\n\t\t\t\t\t\tnew LineSegmentXZ(cap1.p2, mansardEdge2.p1),\n\t\t\t\t\t\tnew LineSegmentXZ(cap2.p1, mansardEdge2.p2),\n\t\t\t\t\t\tnew LineSegmentXZ(mansardEdge1.p1, mansardEdge2.p1),\n\t\t\t\t\t\tnew LineSegmentXZ(mansardEdge1.p2, mansardEdge2.p2));\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\n\t\t\t\tif (ridge.p1.equals(pos) || ridge.p2.equals(pos)) {\n\t\t\t\t\treturn getMaxRoofEle();\n\t\t\t\t} else if (getPolygon().getOuter().getVertexLoop().contains(pos)) {\n\t\t\t\t\treturn getMaxRoofEle() - roofHeight;\n\t\t\t\t} else if (mansardEdge1.p1.equals(pos)\n\t\t\t\t\t\t|| mansardEdge1.p2.equals(pos)\n\t\t\t\t\t\t|| mansardEdge2.p1.equals(pos)\n\t\t\t\t\t\t|| mansardEdge2.p2.equals(pos)) {\n\t\t\t\t\treturn getMaxRoofEle() - 1/3.0 * roofHeight;\n\t\t\t\t} else {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * roof that has been mapped with explicit roof edge/ridge/apex elements\n\t\t */\n\t\tprivate class ComplexRoof extends HeightfieldRoof {\n\t\t\t\n\t\t\tprivate double roofHeight = 0;\n\t\t\tprivate final Map<VectorXZ, Double> roofHeightMap;\n\t\t\tprivate PolygonWithHolesXZ simplePolygon;\n\t\t\tprivate final Collection<LineSegmentXZ> ridgeAndEdgeSegments;\n\t\t\t\n\t\t\tpublic ComplexRoof() {\n\n\t\t\t\t/* find ridge and/or edges\n\t\t\t\t * (apex nodes don't need to be handled separately\n\t\t\t\t *  as they should always be part of an edge segment) */\n\t\t\t\t\n\t\t\t\troofHeightMap = new HashMap<VectorXZ, Double>();\n\t\t\t\tSet<VectorXZ> nodeSet = new HashSet<VectorXZ>();\n\t\t\t\t\n\t\t\t\tridgeAndEdgeSegments = new ArrayList<LineSegmentXZ>();\n\t\t\t\t\n\t\t\t\tList<MapNode> nodes = area.getBoundaryNodes();\n\t\t\t\tboolean usePartRoofHeight = false;\n\t\t\t\t\n\t\t\t\tif (area.getTags().containsKey(\"roof:height\")){\n\t\t\t\t\troofHeight = parseMeasure(area.getTags().getValue(\"roof:height\"));\n\t\t\t\t\tusePartRoofHeight = true;\n\t\t\t\t} else\n\t\t\t\t\troofHeight = DEFAULT_RIDGE_HEIGHT;\n\t\t\t\t\n\t\t\t\tList<MapWaySegment> edges = new ArrayList<MapWaySegment>();\n\t\t\t\tList<MapWaySegment> ridges = new ArrayList<MapWaySegment>();\n\t\t\t\t\n\t\t\t\tfor (MapOverlap<?,?> overlap : area.getOverlaps()) {\n\t\t\t\t\t\n\t\t\t\t\tif (overlap instanceof MapOverlapWA) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tMapWaySegment waySegment = ((MapOverlapWA)overlap).e1;\n\t\t\t\t\t\t\n\t\t\t\t\t\tboolean isRidge = waySegment.getTags().contains(\"roof:ridge\", \"yes\");\n\t\t\t\t\t\tboolean isEdge = waySegment.getTags().contains(\"roof:edge\", \"yes\");\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (!(isRidge || isEdge))\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\n\t\t\t\t\t\tboolean inside = polygon.contains(waySegment.getCenter());\n\n\t\t\t\t\t\t// check also endpoints as pnpoly algo is not reliable when\n\t\t\t\t\t\t// segment lies on the polygon edge\n\t\t\t\t\t\tboolean containsStart = nodes.contains(waySegment.getStartNode());\n\t\t\t\t\t\tboolean containsEnd = nodes.contains(waySegment.getEndNode());\n\n\t\t\t\t\t\tif (!inside && !(containsStart && containsEnd))\n\t\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t\tif (isEdge)\n\t\t\t\t\t\t\tedges.add(waySegment);\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tridges.add(waySegment);\n\t\t\t\t\t\t\n\t\t\t\t\t\tridgeAndEdgeSegments.add(waySegment.getLineSegment());\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor (MapWaySegment waySegment : edges){\n\t\t\t\t\tfor (MapNode node : waySegment.getStartEndNodes()) {\n\n\t\t\t\t\t\t// height of node (above roof base)\n\t\t\t\t\t\tFloat nodeHeight = null;\n\n\t\t\t\t\t\tif (node.getTags().containsKey(\"roof:height\")) {\n\t\t\t\t\t\t\tnodeHeight = parseMeasure(node.getTags()\n\t\t\t\t\t\t\t\t\t.getValue(\"roof:height\"));\n\t\t\t\t\t\t// hmm, shouldnt edges be interpolated? some seem to think they dont\n\t\t\t\t\t\t} else if (waySegment.getTags().containsKey(\"roof:height\")) {\n\t\t\t\t\t\t\tnodeHeight = parseMeasure(waySegment.getTags()\n\t\t\t\t\t\t\t        .getValue(\"roof:height\"));\n\t\t\t\t\t\t} else if (node.getTags().contains(\"roof:apex\",\t\"yes\")) {\n\t\t\t\t\t\t\tnodeHeight = (float)roofHeight;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (nodeHeight == null) {\n\t\t\t\t\t\t\tnodeSet.add(node.getPos());\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\troofHeightMap.put(node.getPos(), (double) nodeHeight);\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (usePartRoofHeight)\n\t\t\t\t\t\t\troofHeight = max(roofHeight, nodeHeight);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\tfor (MapWaySegment waySegment : ridges){\n\t\t\t\t\t// height of node (above roof base)\n\t\t\t\t\tFloat nodeHeight = null;\n\n\t\t\t\t\tif (waySegment.getTags().containsKey(\"roof:height\")) {\n\t\t\t\t\t\tnodeHeight = parseMeasure(waySegment.getTags()\n\t\t\t\t\t\t\t\t.getValue(\"roof:height\"));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnodeHeight = (float) roofHeight;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (usePartRoofHeight)\n\t\t\t\t\t\troofHeight = max(roofHeight, nodeHeight);\n\n\t\t\t\t\tfor (MapNode node : waySegment.getStartEndNodes())\n\t\t\t\t\t\t\troofHeightMap.put(node.getPos(), (double) nodeHeight);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t/* join colinear segments, but not the nodes that are connected to ridge/edges\n\t\t\t\t * often there are nodes that are only added to join one building to another\n\t\t\t\t * but these interfere with proper triangulation.\n\t\t\t\t * TODO: do the same for holes */\n\t\t\t\tList<VectorXZ> vertices = polygon.getOuter().getVertexLoop();\n\t\t\t\tList<VectorXZ> simplified = new ArrayList<VectorXZ>();\n\t\t\t\tVectorXZ vPrev = vertices.get(vertices.size() - 2);\n\t\t\t\t\n\t\t\t\tfor (int i = 0, size = vertices.size() - 1; i < size; i++ ){\n\t\t\t\t\tVectorXZ v = vertices.get(i);\n\t\t\t\t\t\n\t\t\t\t\tif (i == 0 || roofHeightMap.containsKey(v) || nodeSet.contains(v)) {\n\t\t\t\t\t\tsimplified.add(v);\n\t\t\t\t\t\tvPrev = v;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tVectorXZ vNext = vertices.get(i + 1);\n\t\t\t\t\tLineSegmentXZ l = new LineSegmentXZ(vPrev, vNext);\n\t\t\t\t\t\n\t\t\t\t\t// TODO define as static somewhere: 10 cm tolerance\n\t\t\t\t\tif (distanceFromLineSegment(v, l) < 0.01){\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\troofHeightMap.put(v, 0.0);\n\t\t\t\t\tsimplified.add(v);\n\t\t\t\t\tvPrev = v;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (simplified.size() > 2) {\n\t\t\t\t\ttry{\n\t\t\t\t\t\tsimplified.add(simplified.get(0));\n\t\t\t\t\t\tsimplePolygon = new PolygonWithHolesXZ(new SimplePolygonXZ(simplified),\n\t\t\t\t\t\t\t\tpolygon.getHoles());\n\t\t\t\t\t} catch (InvalidGeometryException e) {\n\t\t\t\t\t\tSystem.err.print(e.getMessage());\n\t\t\t\t\t\tsimplePolygon = polygon;\n\t\t\t\t\t}\n\t\t\t\t} else\n\t\t\t\t\tsimplePolygon = polygon;\n\n\t\t\t\t/* add heights for outline nodes that don't have one yet */\n\n\t\t\t\tfor (VectorXZ v : simplePolygon.getOuter().getVertices()) {\n\t\t\t\t\tif (!roofHeightMap.containsKey(v)) {\n\t\t\t\t\t\troofHeightMap.put(v, 0.0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tfor (SimplePolygonXZ hole : simplePolygon.getHoles()) {\n\t\t\t\t\tfor (VectorXZ v : hole.getVertices()) {\n\t\t\t\t\t\tif (!roofHeightMap.containsKey(v)) {\n\t\t\t\t\t\t\troofHeightMap.put(v, 0.0);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t/* add heights for edge nodes that are not also\n\t\t\t\t * ridge/outline/apex nodes. This will just use base height\n\t\t\t\t * for them instead of trying to interpolate heights along\n\t\t\t\t * chains of edge segments. Results are therefore wrong,\n\t\t\t\t * but there's no reason to map them like that anyway. */\n\t\t\t\t\n\t\t\t\tfor (LineSegmentXZ segment : ridgeAndEdgeSegments) {\n\t\t\t\t\tif (!roofHeightMap.containsKey(segment.p1)) {\n\t\t\t\t\t\troofHeightMap.put(segment.p1, 0.0);\n\t\t\t\t\t}\n\t\t\t\t\tif (!roofHeightMap.containsKey(segment.p2)) {\n\t\t\t\t\t\troofHeightMap.put(segment.p2, 0.0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic PolygonWithHolesXZ getPolygon() {\n\t\t\t\treturn simplePolygon;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Collection<VectorXZ> getInnerPoints() {\n\t\t\t\treturn emptyList();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Collection<LineSegmentXZ> getInnerSegments() {\n\t\t\t\treturn ridgeAndEdgeSegments;\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic double getRoofHeight() {\n\t\t\t\treturn roofHeight;\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic Double getRoofEleAt_noInterpolation(VectorXZ pos) {\n\t\t\t\tif (roofHeightMap.containsKey(pos)) {\n\t\t\t\t\treturn building.getGroundLevelEle()\n\t\t\t\t\t\t+ heightWithoutRoof + roofHeightMap.get(pos);\n\t\t\t\t} else {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic double getMaxRoofEle() {\n\t\t\t\treturn building.getGroundLevelEle()\n\t\t\t\t\t+ heightWithoutRoof + roofHeight;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\t\t\n\t\tpublic static boolean hasComplexRoof(MapArea area) {\n\t\t\tfor (MapOverlap<?,?> overlap : area.getOverlaps()) {\n\t\t\t\tif (overlap instanceof MapOverlapWA) {\n\t\t\t\t\tTagGroup tags = overlap.e1.getTags();\n\t\t\t\t\tif (tags.contains(\"roof:ridge\", \"yes\")\n\t\t\t\t\t\t\t|| tags.contains(\"roof:edge\", \"yes\")) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class BuildingEntrance implements NodeWorldObject,\n\t\tRenderableToAllTargets {\n\t\t\n\t\tprivate final BuildingPart buildingPart;\n\t\tprivate final MapNode node;\n\t\t\n\t\tprivate final EleConnector connector;\n\t\t\n\t\tpublic BuildingEntrance(BuildingPart buildingPart, MapNode node) {\n\t\t\tthis.buildingPart = buildingPart;\n\t\t\tthis.node = node;\n\t\t\tthis.connector = new EleConnector(node.getPos(), node, getGroundState());\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic MapNode getPrimaryMapElement() {\n\t\t\treturn node;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Iterable<EleConnector> getEleConnectors() {\n\t\t\treturn singleton(connector);\n\t\t}\n\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\t\n\t\t\t/* TODO for level != null and ABOVE/BELO, add vertical distance to ground */\n\t\t\t\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\t\n\t\t\tboolean onlyOn = true;\n\t\t\tboolean onlyAbove = true;\n\t\t\tboolean onlyBelow = true;\n\t\t\t\n\t\t\tfor (MapWaySegment waySegment : node.getConnectedWaySegments()) {\n\t\t\t\t\n\t\t\t\tif (waySegment.getPrimaryRepresentation() instanceof\n\t\t\t\t\t\tAbstractNetworkWaySegmentWorldObject) {\n\t\t\t\t\t\n\t\t\t\t\tswitch (waySegment.getPrimaryRepresentation().getGroundState()) {\n\t\t\t\t\tcase ABOVE: onlyOn = false; onlyBelow = false; break;\n\t\t\t\t\tcase BELOW: onlyOn = false; onlyAbove = false; break;\n\t\t\t\t\tcase ON: onlyBelow = false; onlyAbove = false; break;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (onlyOn) {\n\t\t\t\treturn ON;\n\t\t\t} else if (onlyAbove) {\n\t\t\t\treturn ABOVE;\n\t\t\t} else if (onlyBelow) {\n\t\t\t\treturn BELOW;\n\t\t\t} else {\n\t\t\t\treturn ON;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic int getLevel() {\n\n\t\t\ttry {\n\t\t\t\treturn Integer.parseInt(node.getTags().getValue(\"level\"));\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t/* calculate a vector that points out of the building */\n\t\t\t\n\t\t\tVectorXZ outOfBuilding = VectorXZ.Z_UNIT;\n\t\t\t\n\t\t\tfor (SimplePolygonXZ polygon :\n\t\t\t\tbuildingPart.polygon.getPolygons()) {\n\t\t\t\t\n\t\t\t\tfinal List<VectorXZ> vs = polygon.getVertexLoop();\n\t\t\t\tint entranceI = vs.indexOf(node.getPos());\n\t\t\t\t\n\t\t\t\tif (entranceI != -1) {\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ posBefore = vs.get((vs.size() + entranceI - 1) % vs.size());\n\t\t\t\t\tVectorXZ posAfter = vs.get((vs.size() + entranceI + 1) % vs.size());\n\t\t\t\t\t\n\t\t\t\t\toutOfBuilding = posBefore.subtract(posAfter).rightNormal();\n\t\t\t\t\tif (!polygon.isClockwise()) {\n\t\t\t\t\t\toutOfBuilding = outOfBuilding.invert();\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* draw the entrance as a box protruding from the building */\n\t\t\t\n\t\t\tVectorXYZ center = connector.getPosXYZ();\n\t\t\t\n\t\t\tfloat height = parseHeight(node.getTags(), 2);\n\t\t\tfloat width = parseWidth(node.getTags(), 1);\n\t\t\t\n\t\t\ttarget.drawBox(Materials.ENTRANCE_DEFAULT,\n\t\t\t\t\tcenter, outOfBuilding, height, width, 0.1);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/CliffModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static com.google.common.collect.Iterables.any;\nimport static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.MIN;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.GLOBAL_X_Z;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.texCoordLists;\nimport static org.osm2world.core.util.Predicates.hasType;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.createTriangleStripBetween;\nimport static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*;\n\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.modules.common.ConfigurableWorldModule;\nimport org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject;\n\n/**\n * adds cliffs and retaining walls to the world.\n * Their common property is that they offset terrain elevation.\n */\npublic class CliffModule extends ConfigurableWorldModule {\n\t\n\t@Override\n\tpublic void applyTo(MapData grid) {\n\t\t\n\t\tfor (MapWaySegment segment : grid.getMapWaySegments()) {\n\t\t\t\n\t\t\tif (segment.getTags().contains(\"natural\", \"cliff\")) {\n\t\t\t\tsegment.addRepresentation(new Cliff(segment));\n\t\t\t} else if (segment.getTags().contains(\"barrier\", \"retaining_wall\")) {\n\t\t\t\tsegment.addRepresentation(new RetainingWall(segment));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static int getConnectedCliffs(MapNode node) {\n\t\t\n\t\tint result = 0;\n\t\t\n\t\tfor (MapWaySegment segment : node.getConnectedWaySegments()) {\n\t\t\tif (any(segment.getRepresentations(), hasType(Cliff.class))) {\n\t\t\t\tresult += 1;\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\tprivate abstract static class AbstractCliff\n\t\t\textends AbstractNetworkWaySegmentWorldObject\n\t\t\timplements TerrainBoundaryWorldObject, RenderableToAllTargets {\n\t\t\n\t\tprotected AbstractCliff(MapWaySegment segment) {\n\t\t\tsuper(segment);\n\t\t}\n\n\t\tprotected abstract float getDefaultWidth();\n\t\t\n\t\tprotected abstract Material getMaterial();\n\t\t\n\t\t@Override\n\t\tpublic float getWidth() {\n\t\t\treturn parseWidth(segment.getTags(), getDefaultWidth());\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\t\n\t\t\tdouble height = parseHeight(segment.getTags(), 5);\n\t\t\t\n\t\t\tif (isBroken()) return;\n\t\t\t\n\t\t\t/* add vertical offset between left and right connectors */\n\t\t\t\n\t\t\tList<EleConnector> center = getCenterlineEleConnectors();\n\t\t\tList<EleConnector> left = connectors.getConnectors(getOutlineXZ(false));\n\t\t\tList<EleConnector> right = connectors.getConnectors(getOutlineXZ(true));\n\t\t\t\n\t\t\tfor (int i = 0; i < center.size(); i++) {\n\t\t\t\t\n\t\t\t\t// the ends of the cliff may be much lower\n\t\t\t\tif ((i != 0 || getConnectedCliffs(segment.getStartNode()) > 1)\n\t\t\t\t\t\t&& (i != center.size() - 1 || getConnectedCliffs(segment.getEndNode()) > 1)) {\n\t\t\t\t\t\n\t\t\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\t\t\tMIN, height, left.get(i), right.get(i));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tList<VectorXYZ> groundVs = createTriangleStripBetween(\n\t\t\t\t\tgetOutline(false), getOutline(true));\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(getMaterial(), groundVs,\n\t\t\t\t\ttexCoordLists(groundVs, Materials.RAIL_BALLAST_DEFAULT, GLOBAL_X_Z));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class Cliff extends AbstractCliff {\n\n\t\tprotected Cliff(MapWaySegment segment) {\n\t\t\tsuper(segment);\n\t\t}\n\n\t\t@Override\n\t\tprotected float getDefaultWidth() {\n\t\t\treturn 1.0f;\n\t\t}\n\n\t\t@Override\n\t\tprotected Material getMaterial() {\n\t\t\treturn Materials.EARTH;\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class RetainingWall extends AbstractCliff {\n\n\t\tprotected RetainingWall(MapWaySegment segment) {\n\t\t\tsuper(segment);\n\t\t}\n\n\t\t@Override\n\t\tprotected float getDefaultWidth() {\n\t\t\treturn 1.0f;\n\t\t}\n\n\t\t@Override\n\t\tprotected Material getMaterial() {\n\t\t\treturn Materials.CONCRETE;\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/GolfModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.lang.Math.PI;\nimport static java.util.Arrays.asList;\nimport static org.osm2world.core.math.VectorXZ.fromAngle;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.*;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.*;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.createTriangleStripBetween;\n\nimport java.awt.Color;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.map_elevation.data.EleConnectorGroup;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.algorithms.JTSTriangulationUtil;\nimport org.osm2world.core.math.algorithms.Poly2TriTriangulationUtil;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.ImmutableMaterial;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Interpolation;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.util.MinMaxUtil;\nimport org.osm2world.core.util.exception.TriangulationException;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.modules.SurfaceAreaModule.SurfaceArea;\nimport org.osm2world.core.world.modules.common.AbstractModule;\n\nimport com.google.common.base.Function;\n\n/**\n * adds golf courses to the map\n */\npublic class GolfModule extends AbstractModule {\n\t\n\tprivate static final int HOLE_CIRCLE_VERTICES = 8;\n\tprivate static final double HOLE_RADIUS = 0.108 / 2;\n\tprivate static final double HOLE_DEPTH = 0.102;\n\t\n\t@Override\n\tpublic void applyToArea(MapArea area) {\n\t\t\n\t\tif (!area.getTags().containsKey(\"golf\")) return;\n\t\t\n\t\tif (area.getTags().contains(\"golf\", \"tee\")) {\n\t\t\tarea.addRepresentation(new Tee(area));\n\t\t} else if (area.getTags().contains(\"golf\", \"fairway\")) {\n\t\t\tarea.addRepresentation(new Fairway(area));\n\t\t} else if (area.getTags().contains(\"golf\", \"green\")) {\n\t\t\tarea.addRepresentation(new Green(area));\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class Tee extends SurfaceArea {\n\n\t\tprivate Tee(MapArea area) {\n\t\t\t\n\t\t\tsuper(area, area.getTags().containsKey(\"surface\")\n\t\t\t\t\t? area.getTags().getValue(\"surface\")\n\t\t\t\t\t: \"grass\");\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class Fairway extends SurfaceArea {\n\n\t\tprivate Fairway(MapArea area) {\n\t\t\t\n\t\t\tsuper(area, area.getTags().containsKey(\"surface\")\n\t\t\t\t\t? area.getTags().getValue(\"surface\")\n\t\t\t\t\t: \"grass\");\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class Green extends AbstractAreaWorldObject\n\t\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\t\t\n\t\tprivate final VectorXZ pinPosition;\n\t\tprivate final SimplePolygonXZ pinHoleLoop;\n\t\tprivate final EleConnectorGroup pinConnectors;\n\t\t\n\t\tprivate Green(MapArea area) {\n\t\t\t\n\t\t\tsuper(area);\n\t\t\t\n\t\t\t/* check whether a pin has been explicitly mapped */\n\t\t\t\n\t\t\tVectorXZ explicitPinPosition = null;\n\t\t\t\n\t\t\tfor (MapOverlap<?, ?> overlap : area.getOverlaps()) {\n\t\t\t\t\n\t\t\t\tMapElement other = overlap.getOther(area);\n\t\t\t\t\n\t\t\t\tif (other.getTags().contains(\"golf\",\"pin\")\n\t\t\t\t\t\t&& other instanceof MapNode) {\n\t\t\t\t\t\n\t\t\t\t\texplicitPinPosition = ((MapNode)other).getPos();\n\t\t\t\t\t\n\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* place an implicit pin if none has been mapped */\n\t\t\t\n\t\t\tif (explicitPinPosition != null) {\n\t\t\t\t\n\t\t\t\tpinPosition = explicitPinPosition;\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tpinPosition = area.getOuterPolygon().getCenter();\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\n\t\t\t/* create circle around the hole */\n\t\t\t\t\n\t\t\tList<VectorXZ> holeRing = new ArrayList<VectorXZ>(HOLE_CIRCLE_VERTICES);\n\t\t\t\n\t\t\tfor (int i = 0; i < HOLE_CIRCLE_VERTICES; i++) {\n\t\t\t\tVectorXZ direction = fromAngle(2 * PI * ((double)i / HOLE_CIRCLE_VERTICES));\n\t\t\t\tVectorXZ vertex = pinPosition.add(direction.mult(HOLE_RADIUS));\n\t\t\t\tholeRing.add(vertex);\n\t\t\t}\n\t\t\t\n\t\t\tholeRing.add(holeRing.get(0));\n\t\t\t\n\t\t\tpinHoleLoop = new SimplePolygonXZ(holeRing);\n\t\t\t\n\t\t\tpinConnectors = new EleConnectorGroup();\n\t\t\tpinConnectors.addConnectorsFor(pinHoleLoop.getVertexCollection(), area, GroundState.ON);\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic EleConnectorGroup getEleConnectors() {\n\t\t\t\n\t\t\tEleConnectorGroup eleConnectors = super.getEleConnectors();\n\t\t\t\n\t\t\tif (pinConnectors != null) {\n\t\t\t\teleConnectors.addAll(pinConnectors);\n\t\t\t}\n\t\t\t\n\t\t\treturn eleConnectors;\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t/* render green surface */\n\t\t\t\n\t\t\tString surfaceValue = area.getTags().getValue(\"surface\");\n\t\t\t\n\t\t\tMaterial material = Materials.GRASS;\n\t\t\t\n\t\t\tif (surfaceValue != null && !\"grass\".equals(surfaceValue)) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(surfaceValue, material);\n\t\t\t}\n\n\t\t\tCollection<TriangleXZ> trianglesXZ = getGreenTriangulation();\n\t\t\tCollection<TriangleXYZ> triangles = getEleConnectors().getTriangulationXYZ(trianglesXZ);\n\t\t\t\n\t\t\ttarget.drawTriangles(material, triangles,\n\t\t\t\t\ttriangleTexCoordLists(triangles , material, GLOBAL_X_Z));\n\t\t\t\n\t\t\t/* render pin */\n\t\t\t\t\t\n\t\t\tPolygonXYZ upperHoleRing = pinConnectors.getPosXYZ(pinHoleLoop);\n\t\t\t\t\n\t\t\tdrawPin(target, pinPosition, upperHoleRing.getVertexLoop());\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate List<TriangleXZ> getGreenTriangulation() {\n\t\t\t\n\t\t\tList<SimplePolygonXZ> holes = area.getPolygon().getHoles();\n\t\t\t\n\t\t\tholes.add(pinHoleLoop);\n\t\t\t\n\t\t\ttry {\n\t\t\t\t\n\t\t\t\treturn Poly2TriTriangulationUtil.triangulate(\n\t\t\t\t\tarea.getPolygon().getOuter(),\n\t\t\t\t\tholes,\n\t\t\t\t\tCollections.<LineSegmentXZ>emptyList(),\n\t\t\t\t\tCollections.<VectorXZ>emptyList());\n\t\t\t\t\n\t\t\t} catch (TriangulationException e) {\n\t\t\t\t\n\t\t\t\tSystem.err.println(\"Poly2Tri exception for \" + this + \":\");\n\t\t\t\te.printStackTrace();\n\t\t\t\tSystem.err.println(\"... falling back to JTS triangulation.\");\n\t\t\t\t\n\t\t\t\treturn JTSTriangulationUtil.triangulate(\n\t\t\t\t\t\tarea.getPolygon().getOuter(),\n\t\t\t\t\t\tholes,\n\t\t\t\t\t\tCollections.<LineSegmentXZ>emptyList(),\n\t\t\t\t\t\tCollections.<VectorXZ>emptyList());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tprivate static void drawPin(Target<?> target,\n\t\t\t\tVectorXZ pos, List<VectorXYZ> upperHoleRing) {\n\t\t\t\n\t\t\tdouble minHoleEle = MinMaxUtil.<VectorXYZ>min(upperHoleRing,\n\t\t\t\t\tnew Function<VectorXYZ, Double>() {\n\t\t\t\t@Override public Double apply(VectorXYZ v) {\n\t\t\t\t\treturn v.y;\n\t\t\t\t}\n\t\t\t}).y;\n\t\t\t\n\t\t\tdouble holeBottomEle = minHoleEle - HOLE_DEPTH;\n\t\t\t\t\t\t\n\t\t\t/* draw hole */\n\t\t\t\n\t\t\tList<VectorXYZ> lowerHoleRing = new ArrayList<VectorXYZ>();\n\t\t\tfor (VectorXYZ v : upperHoleRing) {\n\t\t\t\tlowerHoleRing.add(v.y(holeBottomEle));\n\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t\tList<VectorXYZ> vs = createTriangleStripBetween(\n\t\t\t\t\tupperHoleRing, lowerHoleRing);\n\t\t\t\n\t\t\tMaterial groundMaterial = Materials.EARTH.makeSmooth();\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(groundMaterial, vs,\n\t\t\t\t\ttexCoordLists(vs, groundMaterial, STRIP_WALL));\n\t\t\t\n\t\t\ttarget.drawConvexPolygon(groundMaterial, lowerHoleRing,\n\t\t\t\t\ttexCoordLists(vs, groundMaterial, GLOBAL_X_Z));\n\t\t\t\n\t\t\t/* draw flag */\n\t\t\t\n\t\t\ttarget.drawColumn(Materials.PLASTIC_GREY.makeSmooth(), null,\n\t\t\t\t\tpos.xyz(holeBottomEle), 1.5, 0.007, 0.007, false, true);\n\t\t\t\n\t\t\tImmutableMaterial flagcloth = new ImmutableMaterial(Interpolation.SMOOTH, Color.YELLOW);\n\t\t\t\n\t\t\tList<VectorXYZ> flagVertices = asList(\n\t\t\t\t\tnew VectorXYZ(pos.x, 1.5, pos.z),\n\t\t\t\t\tnew VectorXYZ(pos.x, 1.2, pos.z),\n\t\t\t\t\tnew VectorXYZ(pos.x + 0.4, 1.5, pos.z),\n\t\t\t\t\tnew VectorXYZ(pos.x + 0.4, 1.2, pos.z));\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(flagcloth, flagVertices,\n\t\t\t\t\ttexCoordLists(flagVertices, flagcloth, STRIP_WALL));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/InvisibleModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapAreaSegment;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapSegment;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\nimport org.osm2world.core.world.data.NoOutlineNodeWorldObject;\nimport org.osm2world.core.world.data.NoOutlineWaySegmentWorldObject;\nimport org.osm2world.core.world.modules.common.AbstractModule;\n\n/**\n * creates invisible world objects that are not rendered,\n * but nevertheless contain important information (such as elevation at a point)\n */\npublic class InvisibleModule extends AbstractModule {\n\t\n\t@Override\n\tprotected void applyToNode(MapNode node) {\n\t\tif (node.getTags().containsKey(\"ele\")\n\t\t\t\t&& node.getRepresentations().isEmpty()) {\n\t\t\t\n\t\t\tboolean isInGroundSegment = false;\n\t\t\tfor (MapSegment segment : node.getConnectedSegments()) {\n\t\t\t\tMapElement element;\n\t\t\t\tif (segment instanceof MapWaySegment) {\n\t\t\t\t\telement = (MapWaySegment)segment;\n\t\t\t\t} else {\n\t\t\t\t\telement = ((MapAreaSegment)segment).getArea();\n\t\t\t\t}\n\t\t\t\tif (element.getPrimaryRepresentation() != null\n\t\t\t\t\t\t&& element.getPrimaryRepresentation().getGroundState()\n\t\t\t\t\t\t\t== GroundState.ON) {\n\t\t\t\t\tisInGroundSegment = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (node.getConnectedSegments().isEmpty() || isInGroundSegment) {\n\t\t\t\tnode.addRepresentation(new InvisibleEleNode(node));\n\t\t\t}\n\t\t\t\n\t\t}\n\t}\n\t\n\t@Override\n\tprotected void applyToWaySegment(MapWaySegment segment) {\n\t\tif (segment.getTags().containsKey(\"ele\")\n\t\t\t\t&& segment.getRepresentations().isEmpty()) {\n\t\t\tsegment.addRepresentation(new InvisibleEleWaySegment(segment));\n\t\t}\n\t}\n\t\n\t@Override\n\tprotected void applyToArea(MapArea area) {\n\t\tif (area.getTags().containsKey(\"ele\")\n\t\t\t\t&& area.getRepresentations().isEmpty()) {\n\t\t\tarea.addRepresentation(new InvisibleEleArea(area));\n\t\t}\n\t}\n\t\n\tprivate static class InvisibleEleNode extends NoOutlineNodeWorldObject {\n\t\t\n\t\tpublic InvisibleEleNode(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class InvisibleEleWaySegment\n\t\t\textends NoOutlineWaySegmentWorldObject {\n\t\t\n\t\tpublic InvisibleEleWaySegment(MapWaySegment segment) {\n\t\t\tsuper(segment);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class InvisibleEleArea\n\t\t\textends AbstractAreaWorldObject {\n\t\t\n\t\tprotected InvisibleEleArea(MapArea area) {\n\t\t\tsuper(area);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t}\n\t\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/ParkingModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static org.osm2world.core.target.common.material.Materials.*;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.GLOBAL_X_Z;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.triangleTexCoordLists;\n\nimport java.util.Collection;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.modules.common.AbstractModule;\n\n/**\n * adds parking spaces to the world\n */\npublic class ParkingModule extends AbstractModule {\n\t\n\t@Override\n\tprotected void applyToArea(MapArea area) {\n\t\tif (area.getTags().contains(\"amenity\",\"parking\")) {\n\t\t\n\t\t\tString parkingValue = area.getTags().getValue(\"parking\");\n\t\t\t\n\t\t\tif (\"surface\".equals(parkingValue) || parkingValue == null) {\n\t\t\t\tarea.addRepresentation(new SurfaceParking(area));\n\t\t\t}\n\t\t\t\t\t\t\n\t\t}\n\t}\n\t\n\tprivate static class SurfaceParking extends AbstractAreaWorldObject\n\timplements TerrainBoundaryWorldObject, RenderableToAllTargets {\n\t\t\n\t\tpublic SurfaceParking(MapArea area) {\n\t\t\tsuper(area);\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tString surface = area.getTags().getValue(\"surface\");\n\t\t\tMaterial material = getSurfaceMaterial(surface, ASPHALT);\n\t\t\t\n\t\t\tCollection<TriangleXYZ> triangles = getTriangulation();\n\t\t\t\n\t\t\ttarget.drawTriangles(material, triangles,\n\t\t\t\t\ttriangleTexCoordLists(triangles, material, GLOBAL_X_Z));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/PoolModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.nCopies;\nimport static org.osm2world.core.target.common.material.Materials.PURIFIED_WATER;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.GLOBAL_X_Z;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.*;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.createShapeExtrusionAlong;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.modules.common.AbstractModule;\n\n/**\n * adds swimming pools and water parks to the world\n */\npublic class PoolModule extends AbstractModule {\n\n\tprivate final boolean isPool(TagGroup tags) {\n\t\tboolean pool = tags.contains(\"amenity\", \"swimming_pool\");\n\t\tpool |= tags.contains(\"leisure\", \"swimming_pool\");\n\t\t\n\t\treturn pool;\n\t}\n\t\n\tprivate final boolean isPool(MapArea area) {\n\t\treturn isPool(area.getTags());\n\t}\n\t\n\t\n\t@Override\n\tprotected void applyToArea(MapArea area) {\n\t\tif (isPool(area))\n\t\t\tarea.addRepresentation(new Pool(area));\n\t}\n\n\t\n\tprivate static class Pool extends AbstractAreaWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\t\n\t\tpublic Pool(MapArea area) {\n\t\t\tsuper(area);\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\n\t\t\t/* render water */\n\t\t\t\n\t\t\tCollection<TriangleXYZ> triangles = getTriangulation();\n\t\t\t\n\t\t\ttarget.drawTriangles(PURIFIED_WATER, triangles,\n\t\t\t\t\ttriangleTexCoordLists(triangles, PURIFIED_WATER, GLOBAL_X_Z));\n\n\t\t\t/* draw a small area around the pool */\n\n\t\t\tdouble width=1;\n\t\t\tdouble height=0.1;\n\t\t\t\n\t\t\tList<VectorXYZ> wallShape = asList(\n\t\t\t\t\tnew VectorXYZ(-width/2, 0, 0),\n\t\t\t\t\tnew VectorXYZ(-width/2, height, 0),\n\t\t\t\t\tnew VectorXYZ(+width/2, height, 0),\n\t\t\t\t\tnew VectorXYZ(+width/2, 0, 0)\n\t\t\t);\n\n\t\t\tList<VectorXYZ> path = getOutlinePolygon().getVertexLoop();\n\t\t\t\n\t\t\tList<List<VectorXYZ>> strips = createShapeExtrusionAlong(\n\t\t\t\t\twallShape, path,\n\t\t\t\t\tnCopies(path.size(), VectorXYZ.Y_UNIT));\n\t\t\t\n\t\t\tfor (List<VectorXYZ> strip : strips) {\n\t\t\t\ttarget.drawTriangleStrip(Materials.CONCRETE, strip,\n\t\t\t\t\t\ttexCoordLists(strip, Materials.CONCRETE, GLOBAL_X_Z));\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/PowerModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.lang.Math.PI;\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.*;\nimport static org.osm2world.core.math.VectorXYZ.Z_UNIT;\nimport static org.osm2world.core.target.common.material.Materials.PLASTIC_GREY;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.STRIP_WALL;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.texCoordLists;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.*;\nimport static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\nimport org.osm2world.core.world.data.NoOutlineNodeWorldObject;\nimport org.osm2world.core.world.data.NoOutlineWaySegmentWorldObject;\nimport org.osm2world.core.world.data.WorldObject;\nimport org.osm2world.core.world.data.WorldObjectWithOutline;\nimport org.osm2world.core.world.modules.common.AbstractModule;\n\n/**\n * module for power infrastructure\n */\npublic final class PowerModule extends AbstractModule {\n\t\t\n\tprivate static TowerConfig generateTowerConfig(MapNode node) {\n\n\t\tList<MapWaySegment> powerLines = new ArrayList<MapWaySegment>();\n\t\tfor (MapWaySegment way : node.getConnectedWaySegments()) {\n\t\t\tif (way.getTags().contains(\"power\", \"line\")) {\n\t\t\t\tpowerLines.add(way);\n\t\t\t}\n\t\t}\n\n\t\tVectorXZ dir = VectorXZ.NULL_VECTOR;\n\t\tint cables = -1;\n\t\tint voltage = -1;\n\t\tfor (MapWaySegment powerLine : powerLines) {\n\t\t\tdir = dir.add(powerLine.getDirection());\n\t\t\t\n\t\t\ttry {\n\t\t\t\tcables = Integer.valueOf(powerLine.getTags().getValue(\"cables\"));\n\t\t\t} catch (NumberFormatException e) {}\n\t\t\ttry {\n\t\t\t\tvoltage = Integer.valueOf(powerLine.getTags().getValue(\"voltage\"));\n\t\t\t} catch (NumberFormatException e) {}\n\t\t}\n\t\tdir = dir.mult(1.0/powerLines.size());\n\n\t\treturn new TowerConfig(node, cables, voltage, dir);\n\t}\n\t\n\t@Override\n\tprotected void applyToNode(MapNode node) {\n\t\t\n\t\tif (node.getTags().contains(\"power\", \"cable_distribution_cabinet\")) {\n\t\t\tnode.addRepresentation(new PowerCabinet(node));\n\t\t}\n\t\t\n\t\tif (node.getTags().contains(\"power\", \"pole\")) {\n\t\t\tnode.addRepresentation(new Powerpole(node));\n\t\t}\n\t\tif (node.getTags().contains(\"power\", \"generator\")\n\t\t\t&& node.getTags().contains(\"generator:source\", \"wind\")) {\n\t\t\tnode.addRepresentation(new WindTurbine(node));\n\t\t}\n\t\t\n\t\tif (node.getTags().contains(\"power\", \"tower\")) {\n\n\t\t\tTowerConfig config = generateTowerConfig(node);\n\t\t\t\n\t\t\tif (node.getPrimaryRepresentation() == null) {\n\t\t\t\tif (config.isHighVoltagePowerTower()) {\n\t\t\t\t\tnode.addRepresentation(new HighVoltagePowerTower(node, config));\n\t\t\t\t} else {\n\t\t\t\t\tnode.addRepresentation(new PowerTower(node, config));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t@Override\n\tprotected void applyToWaySegment(MapWaySegment segment) {\n\t\tif (segment.getTags().contains(\"power\", \"minor_line\")) {\n\t\t\tsegment.addRepresentation(new PowerMinorLine(segment));\n\t\t}\n\t\t\n\t\tif (segment.getTags().contains(\"power\", \"line\")) {\n\t\t\tsegment.addRepresentation(new PowerLine(segment));\n\t\t}\n\t}\n\t\n\t@Override\n\tprotected void applyToArea(MapArea area) {\n\t\tif (area.getTags().contains(\"power\", \"generator\")\n\t\t\t\t&& area.getTags().contains(\"generator:method\", \"photovoltaic\")) {\n\t\t\tarea.addRepresentation(new PhotovoltaicPlant(area));\n\t\t}\n\t}\n\t\n\tprivate static final class PowerCabinet extends NoOutlineNodeWorldObject\n\timplements RenderableToAllTargets {\n\n\t\tpublic PowerCabinet(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\t\t\t\t\n\t\t\ttarget.drawBox(PLASTIC_GREY, getBase(),\n\t\t\t\t\tfaceVector, 1.5, 0.8, 0.3);\n\t\t\t\n\t\t}\n\t\t\n\t\t}\n\t\n\tprivate final static class TowerConfig {\n\t\tMapNode pos;\n\t\tint cables;\n\t\tint voltage;\n\t\tVectorXZ direction;\n\t\t\n\t\tpublic TowerConfig(MapNode pos, int cables, int voltage, VectorXZ direction) {\n\t\t\tsuper();\n\t\t\tthis.pos = pos;\n\t\t\tthis.cables = cables;\n\t\t\tthis.voltage = voltage;\n\t\t\tthis.direction = direction;\n\t\t}\n\t\t\n\t\tpublic boolean isHighVoltagePowerTower() {\n\t\t\treturn voltage >= 50000 || cables >= 6;\n\t\t}\n\t}\n\t\n\tprivate static final class Powerpole extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic Powerpole(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t/* determine material */\n\t\t\t\n\t\t\tMaterial material = null;\n\t\t\t\n\t\t\t//TODO parse color\n\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"material\"));\n\t\t\t}\n\t\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"surface\"), Materials.WOOD);\n\t\t\t}\n\t\t\t\n\t\t\ttarget.drawColumn(material, null, getBase(),\n\t\t\t\t\tparseHeight(node.getTags(), 8f),\n\t\t\t\t\t0.15, 0.15, false, true);\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class WindTurbine extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic WindTurbine(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tfloat poleHeight = parseHeight(node.getTags(), 100f);\n\t\t\tfloat poleRadiusBottom = parseWidth(node.getTags(), 5) / 2;\n\t\t\tfloat poleRadiusTop = poleRadiusBottom / 2;\n\t\t\tfloat nacelleHeight = poleHeight * 0.05f;\n\t\t\tfloat nacelleDepth = poleHeight * 0.1f;\n\t\t\tfloat bladeLength = poleHeight / 2;\n\t\t\t\n\t\t\t/* determine material */\n\t\t\t\n\t\t\tMaterial poleMaterial = null;\n\t\t\tMaterial nacelleMaterial = Materials.STEEL;\n\t\t\tMaterial bladeMaterial = Materials.STEEL; // probably fibre, but color matches roughly :)\n\t\t\t\n\t\t\t//TODO parse color\n\t\t\t\n\t\t\tif (poleMaterial == null) {\n\t\t\t\tpoleMaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"material\"));\n\t\t\t}\n\t\t\t\t\n\t\t\tif (poleMaterial == null) {\n\t\t\t\tpoleMaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"surface\"), Materials.STEEL);\n\t\t\t}\n\t\t\t\n\t\t\t/* draw pole */\n\t\t\ttarget.drawColumn(poleMaterial, null,\n\t\t\t\t\tgetBase(),\n\t\t\t\t\tpoleHeight,\n\t\t\t\t\tpoleRadiusBottom, poleRadiusTop, false, false);\n\t\t\t\n\t\t\t/* draw nacelle */\n\t\t\tVectorXZ nacelleVector = VectorXZ.X_UNIT;\n\t\t\ttarget.drawBox(nacelleMaterial,\n\t\t\t\t\tgetBase().addY(poleHeight).add(nacelleDepth/2 - poleRadiusTop*2, 0f, 0f),\n\t\t\t\t\tnacelleVector, nacelleHeight, nacelleHeight, nacelleDepth);\n\t\t\t\n\t\t\t/* draw blades */\n\t\t\t\n\t\t\t// define first blade\n\t\t\tList<VectorXYZ> bladeFront = asList(\n\t\t\t\tgetBase().addY(poleHeight).add(-poleRadiusTop*2, nacelleHeight/2, +nacelleHeight/2),\n\t\t\t\tgetBase().addY(poleHeight).add(-poleRadiusTop*2, nacelleHeight/2 - bladeLength, 0f),\n\t\t\t\tgetBase().addY(poleHeight).add(-poleRadiusTop*2, nacelleHeight/2, -nacelleHeight/2)\n\t\t\t);\n\t\t\tList<VectorXYZ> bladeBack = asList(\n\t\t\t\tbladeFront.get(0),\n\t\t\t\tbladeFront.get(2),\n\t\t\t\tbladeFront.get(1)\n\t\t\t);\n\t\t\t\n\t\t\t// rotate and draw blades\n\t\t\tdouble rotCenterY = getBase().y + poleHeight + nacelleHeight/2;\n\t\t\tdouble rotCenterZ = node.getPos().getZ();\n\t\t\t\n\t\t\tbladeFront = rotateShapeX(bladeFront, 60, rotCenterY, rotCenterZ);\n\t\t\tbladeBack  = rotateShapeX(bladeBack, 60, rotCenterY, rotCenterZ);\n\t\t\ttarget.drawTriangleStrip(bladeMaterial, bladeFront, null);\n\t\t\ttarget.drawTriangleStrip(bladeMaterial, bladeBack, null);\n\t\t\tbladeFront = rotateShapeX(bladeFront, 120, rotCenterY, rotCenterZ);\n\t\t\tbladeBack  = rotateShapeX(bladeBack, 120, rotCenterY, rotCenterZ);\n\t\t\ttarget.drawTriangleStrip(bladeMaterial, bladeFront, null);\n\t\t\ttarget.drawTriangleStrip(bladeMaterial, bladeBack, null);\n\t\t\tbladeFront = rotateShapeX(bladeFront, 120, rotCenterY, rotCenterZ);\n\t\t\tbladeBack  = rotateShapeX(bladeBack, 120, rotCenterY, rotCenterZ);\n\t\t\ttarget.drawTriangleStrip(bladeMaterial, bladeFront, null);\n\t\t\ttarget.drawTriangleStrip(bladeMaterial, bladeBack, null);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class PowerMinorLine\n\t\textends NoOutlineWaySegmentWorldObject\n\t\timplements RenderableToAllTargets {\n\t\t\n\t\tprivate static final float DEFAULT_THICKN = 0.05f; // width and height\n\t\tprivate static final float DEFAULT_CLEARING_BL = 7.5f; // power pole height is 8\n\t\tprivate static final Material material = Materials.PLASTIC;\n\t\t\n\t\tpublic PowerMinorLine(MapWaySegment segment) {\n\t\t\tsuper(segment);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tList<VectorXYZ> powerlineShape = asList(\n\t\t\t\tnew VectorXYZ(-DEFAULT_THICKN/2, DEFAULT_CLEARING_BL, 0),\n\t\t\t\tnew VectorXYZ(-DEFAULT_THICKN/2, DEFAULT_CLEARING_BL + DEFAULT_THICKN, 0),\n\t\t\t\tnew VectorXYZ(+DEFAULT_THICKN/2, DEFAULT_CLEARING_BL + DEFAULT_THICKN, 0),\n\t\t\t\tnew VectorXYZ(+DEFAULT_THICKN/2, DEFAULT_CLEARING_BL, 0)\n\t\t\t);\n\t\t\t\n\t\t\tList<VectorXYZ> path = getBaseline();\n\t\t\t\n\t\t\tList<List<VectorXYZ>> strips = createShapeExtrusionAlong(\n\t\t\t\t\tpowerlineShape,\n\t\t\t\t\tpath,\n\t\t\t\t\tnCopies(path.size(), VectorXYZ.Y_UNIT));\n\t\t\t\n\t\t\tfor (List<VectorXYZ> strip : strips) {\n\t\t\t\ttarget.drawTriangleStrip(material, strip, null);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tprivate final static class PowerLine\n\t\textends NoOutlineWaySegmentWorldObject\n\t\timplements RenderableToAllTargets {\n\n\t\tprivate static final float CABLE_THICKNESS = 0.05f;\n\t\t// TODO: we need black plastic for cable material\n\t\tprivate final static Material CABLE_MATERIAL = Materials.PLASTIC;\n\t\tprivate static final double SLACK_SPAN = 6;\n\t\tprivate static final double INTERPOLATION_STEPS = 10;\n\t\t// TODO: Once createShapeExtrusionAlong supports arbitrary shapes, we want to switch to a circle instead of a polygon\n\t\tprivate static final double diag = Math.sqrt(2)*CABLE_THICKNESS/2;\n\t\tprivate static final List<VectorXYZ> powerlineShape = asList(\n\t\t\t\tnew VectorXYZ(-diag, 0, 0),\n\t\t\t\tnew VectorXYZ(-CABLE_THICKNESS/2, CABLE_THICKNESS/2, 0),\n\t\t\t\tnew VectorXYZ(0, diag, 0),\n\t\t\t\tnew VectorXYZ(CABLE_THICKNESS/2, CABLE_THICKNESS/2, 0),\n\t\t\t\tnew VectorXYZ(diag, 0, 0),\n\t\t\t\tnew VectorXYZ(+CABLE_THICKNESS/2, -CABLE_THICKNESS/2, 0),\n\t\t\t\tnew VectorXYZ(0, -diag, 0),\n\t\t\t\tnew VectorXYZ(-CABLE_THICKNESS/2, -CABLE_THICKNESS/2, 0),\n\t\t\t\tnew VectorXYZ(-diag, 0, 0)\n\t\t\t);\n\t\t\n\t\tprivate int cables = -1;\n\t\tprivate int voltage = -1;\n\t\tprivate TowerConfig start;\n\t\tprivate TowerConfig end;\n\t\tprivate List<VectorXYZ> startPos = null;\n\t\tprivate List<VectorXYZ> endPos = null;\n\t\t\n\t\t\n\t\tpublic PowerLine(MapWaySegment line) {\n\t\t\tsuper(line);\n\t\t}\n\t\t\n\t\tprivate void addPos(VectorXYZ baseStart, VectorXYZ baseEnd, double gotoRight, double up) {\n\t\t\tstartPos.add(baseStart.add(start.direction.rightNormal().mult(gotoRight)).add(0, up, 0));\n\t\t\tendPos.add(baseEnd.add(end.direction.rightNormal().mult(gotoRight)).add(0, up, 0));\n\t\t}\n\n\t\tprivate void addPos(VectorXYZ baseStart, VectorXYZ baseEnd, double gotoRight, double upStart, double upEnd) {\n\t\t\tstartPos.add(baseStart.add(start.direction.rightNormal().mult(gotoRight)).add(0, upStart, 0));\n\t\t\tendPos.add(baseEnd.add(end.direction.rightNormal().mult(gotoRight)).add(0, upEnd, 0));\n\t\t}\n\n\t\tprivate void setup() {\n\t\t\t\n\t\t\tstartPos = new ArrayList<VectorXYZ>();\n\t\t\tendPos = new ArrayList<VectorXYZ>();\n\t\t\t\n\t\t\t// check number of power lines\n\t\t\ttry {\n\t\t\t\tcables = Integer.valueOf(segment.getTags().getValue(\"cables\"));\n\t\t\t} catch (NumberFormatException e) {}\n\n\t\t\t// check voltage\n\t\t\ttry {\n\t\t\t\tvoltage = Integer.valueOf(segment.getTags().getValue(\"voltage\"));\n\t\t\t} catch (NumberFormatException e) {}\n\n\t\t\tif (cables <= 0) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// get the tower configurations for start and end tower\n\t\t\tstart = generateTowerConfig(segment.getStartNode());\n\t\t\tend = generateTowerConfig(segment.getEndNode());\n\t\t\t\t\t\t\n\t\t\tif (!start.isHighVoltagePowerTower() && !end.isHighVoltagePowerTower()) {\n\n\t\t\t\t// normal PowerTower...\n\n\t\t\t\tdouble startHeight = parseHeight(start.pos.getTags(), 14) + 0.25;\n\t\t\t\tdouble endHeight = parseHeight(end.pos.getTags(), 14) + 0.25;\n\t\t\t\t\n\t\t\t\tVectorXYZ baseStart = getStartXYZ().addY(startHeight - 0.5);\n\t\t\t\tVectorXYZ baseEnd = getEndXYZ().addY(endHeight - 0.5);\n\t\t\t\n\t\t\t\t// power lines at the top left and right\n\t\t\t\taddPos(baseStart, baseEnd, 2, 0.5);\n\t\t\t\taddPos(baseStart, baseEnd, -2, 0.5);\n\t\t\t\t\n\t\t\t\tif (cables >= 3) {\n\t\t\t\t\t// additional power line at the top center\n\t\t\t\t\taddPos(baseStart, baseEnd, 0, 0.5);\n\t\t\t\t}\n\t\t\t\tif (cables >= 5) {\n\t\t\t\t\t// further power lines at the left and right below the column\n\t\t\t\t\taddPos(baseStart, baseEnd, 1.5, -0.5);\n\t\t\t\t\taddPos(baseStart, baseEnd, -1.5, -0.5);\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t// High voltage PowerTower ...\n\t\t\t\t\n\t\t\t\tfloat default_height = voltage > 150000 ? 40 : 30;\n\t\t\t\tfloat pole_width = voltage > 150000 ? 16 : 13;\n\t\t\t\t\n\t\t\t\tdouble startHeight = parseHeight(start.pos.getTags(), default_height);\n\t\t\t\tdouble endHeight = parseHeight(end.pos.getTags(), default_height);\n\n\t\t\t\tdouble heightS = 2.5 * (((int) (startHeight / 2.5)) / 5);\n\t\t\t\tdouble heightE = 2.5 * (((int) (endHeight / 2.5)) / 5);\n\n\t\t\t\tVectorXYZ baseStart = getStartXYZ().addY(-0.5);\n\t\t\t\tVectorXYZ baseEnd = getEndXYZ().addY(-0.5);\n\n\t\t\t\t// power line at the tower's top\n\t\t\t\taddPos(baseStart, baseEnd, 0, 5*heightS, 5*heightE);\n\n\t\t\t\t// power lines start a little bit below the tower's columns\n\t\t\t\tbaseStart = baseStart.add(0, -0.2, 0);\n\t\t\t\tbaseEnd = baseEnd.add(0, -0.2, 0);\n\n\t\t\t\t// power lines at the base column\n\t\t\t\taddPos(baseStart, baseEnd, 0.9*pole_width, startHeight/2, endHeight/2);\n\t\t\t\taddPos(baseStart, baseEnd, -0.9*pole_width, startHeight/2, endHeight/2);\n\t\t\t\tif ((cables > 3) && (cables <= 9)) {\n\t\t\t\t\taddPos(baseStart, baseEnd, 0.45*pole_width, startHeight/2, endHeight/2);\n\t\t\t\t\taddPos(baseStart, baseEnd, -0.45*pole_width, startHeight/2, endHeight/2);\n\t\t\t\t} else if (cables > 9) {\n\t\t\t\t\taddPos(baseStart, baseEnd, 0.6*pole_width, startHeight/2, endHeight/2);\n\t\t\t\t\taddPos(baseStart, baseEnd, -0.6*pole_width, startHeight/2, endHeight/2);\n\t\t\t\t\taddPos(baseStart, baseEnd, 0.3*pole_width, startHeight/2, endHeight/2);\n\t\t\t\t\taddPos(baseStart, baseEnd, -0.3*pole_width, startHeight/2, endHeight/2);\n\t\t\t\t}\n\n\t\t\t\t// additional power lines at the upper column\n\t\t\t\tif (cables >= 7) {\n\t\t\t\t\taddPos(baseStart, baseEnd, 0.9*0.6*pole_width, 4*heightS, 4*heightE);\n\t\t\t\t\taddPos(baseStart, baseEnd, -0.9*0.6*pole_width, 4*heightS, 4*heightE);\n\t\t\t\t\tif (cables >= 9) {\n\t\t\t\t\t\taddPos(baseStart, baseEnd, 0.45*0.6*pole_width, 4*heightS, 4*heightE);\n\t\t\t\t\t\taddPos(baseStart, baseEnd, -0.45*0.6*pole_width, 4*heightS, 4*heightE);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ABOVE;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t// do initial setup for height and position calculation, if necessary\n\t\t\tif (startPos == null) {\n\t\t\t\tsetup();\n\t\t\t}\n\t\t\t\n\t\t\tfor (int i = 0; i < startPos.size(); i++) {\n\n\t\t\t\tVectorXYZ start = startPos.get(i);\n\t\t\t\tVectorXYZ end = endPos.get(i);\n\t\t\t\t\n\t\t\t\tdouble lenToEnd = end.distanceToXZ(start);\n\t\t\t\tdouble heightDiff = end.y - start.y;\n\n\t\t\t\tdouble stepSize = lenToEnd / INTERPOLATION_STEPS;\n\t\t\t\tVectorXZ dir = end.xz().subtract(start.xz()).normalize();\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> path = new ArrayList<VectorXYZ>();\n\t\t\t\tfor (int x = 0; x <= INTERPOLATION_STEPS; x++) {\n\t\t\t\t\tdouble ratio = x / INTERPOLATION_STEPS;\n\t\t\t\t\t\n\t\t\t\t\t// distance from start to position x\n\t\t\t\t\tdouble dx = stepSize * x;\n\t\t\t\t\t\n\t\t\t\t\t// use a simple parabola between two towers\n\t\t\t\t\tdouble height = (1 - Math.pow(2.0*(ratio - 0.5), 2)) * -SLACK_SPAN;\n\t\t\t\t\t// add a linear function to account for different tower/terrain heights\n\t\t\t\t\theight += ratio * heightDiff;\n\t\t\t\t\t\n\t\t\t\t\tpath.add(start.add(dir.mult(dx)).add(0, height, 0));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tList<List<VectorXYZ>> strips = createShapeExtrusionAlong(\n\t\t\t\t\t\tpowerlineShape, path,\n\t\t\t\t\t\tnCopies(path.size(), VectorXYZ.Y_UNIT));\n\t\t\t\t\n\t\t\t\tfor (List<VectorXYZ> strip : strips) {\n\t\t\t\t\ttarget.drawTriangleStrip(CABLE_MATERIAL, strip, null);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\t\n\tprivate static final class PowerTower extends NoOutlineNodeWorldObject\n\t\timplements RenderableToAllTargets {\n\n\t\tprivate TowerConfig config;\n\t\n\t\tpublic PowerTower(MapNode node, TowerConfig config) {\n\t\t\tsuper(node);\n\t\t\tthis.config = config;\n\t\t}\n\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\n\t\t// TODO we're missing the ceramics to hold the power lines\n\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\n\t\t\tVectorXYZ base = getBase().addY(-0.5);\n\t\t\tdouble height = parseHeight(node.getTags(), 14);\n\n\t\t\tMaterial material = Materials.getSurfaceMaterial(node.getTags().getValue(\"material\"));\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(node.getTags().getValue(\"surface\"), Materials.STEEL);\n\t\t\t}\n\t\t\t\n\t\t\t// draw base column\n\t\t\ttarget.drawColumn(material, null, base, height, 0.5, 0.25, true, true);\n\t\t\t\n\t\t\t// draw cross \"column\"\n\t\t\ttarget.drawBox(material, base.add(0, height, 0), config.direction, 0.25, 5, 0.25);\n\t\t\n\t\t\t// draw pieces holding the power lines\n\t\t\tbase = base.add(0, height + 0.25, 0);\n\t\t\ttarget.drawColumn(Materials.CONCRETE, null, base.add(config.direction.rightNormal().mult(2)), 0.5, 0.1, 0.1, true, true);\n\t\t\ttarget.drawColumn(Materials.CONCRETE, null, base.add(config.direction.rightNormal().mult(-2)), 0.5, 0.1, 0.1, true, true);\n\t\t\tif (config.cables >= 3) {\n\t\t\t\ttarget.drawColumn(Materials.CONCRETE, null, base, 0.5, 0.1, 0.1, true, true);\n\t\t\t}\n\t\t\tif (config.cables >= 5) {\n\t\t\t\ttarget.drawColumn(Materials.CONCRETE, null, base.add(config.direction.rightNormal().mult(1.5)), -0.5, 0.1, 0.1, true, true);\n\t\t\t\ttarget.drawColumn(Materials.CONCRETE, null, base.add(config.direction.rightNormal().mult(-1.5)), -0.5, 0.1, 0.1, true, true);\n\t\t\t}\n\t\t}\n\t}\n\n\n\tprivate static final class HighVoltagePowerTower extends NoOutlineNodeWorldObject\n\t\timplements RenderableToAllTargets {\n\n\t\tprivate TowerConfig config;\n\t\tprivate VectorXZ direction;\n\t\t\n\t\tpublic HighVoltagePowerTower(MapNode node, TowerConfig config) {\n\t\t\tsuper(node);\n\t\t\tthis.config = config;\n\t\t\tthis.direction = config.direction;\n\t\t}\n\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\n\t\t/**\n\t\t * parse height tag or provide a reasonable default based on the tower's voltage\n\t\t */\n\t\tprivate double getTowerHeight() {\n\n\t\t\tfloat default_height = config.voltage > 150000 ? 40 : 30;\n\t\t\tdouble height = parseHeight(node.getTags(), default_height);\n\n\t\t\treturn height;\n\t\t}\n\t\t\n\t\tprivate VectorXZ[][] getCorners(VectorXZ center, double diameter) {\n\t\t\tdouble half = diameter/2;\n\t\t\tVectorXZ ortho = direction.rightNormal();\n\t\t\t\n\t\t\tVectorXZ right_in = center.add(direction.mult(half));\n\t\t\tVectorXZ left_in = center.add(direction.mult(-half));\n\t\t\tVectorXZ right_out = center.add(direction.mult(half));\n\t\t\tVectorXZ left_out = center.add(direction.mult(-half));\n\n\t\t\t// TODO: if we can switch off backface culling we'd only need one face here\n\t\t\treturn new VectorXZ[][]{\n\t\t\t\t\tnew VectorXZ[]{\n\t\t\t\t\t\t\tright_in.add(ortho.mult(-half)),\n\t\t\t\t\t\t\tright_in.add(ortho.mult(half)),\n\t\t\t\t\t\t\tleft_in.add(ortho.mult(half)),\n\t\t\t\t\t\t\tleft_in.add(ortho.mult(-half))\n\t\t\t\t\t},\n\t\t\t\t\tnew VectorXZ[]{\n\t\t\t\t\t\t\tright_out.add(ortho.mult(half)),\n\t\t\t\t\t\t\tright_out.add(ortho.mult(-half)),\n\t\t\t\t\t\t\tleft_out.add(ortho.mult(-half)),\n\t\t\t\t\t\t\tleft_out.add(ortho.mult(half))\n\t\t\t\t\t}};\n\t\t}\n\t\n\t\tprivate void drawSegment(Target<?> target,\n\t\t\t\tVectorXZ[] low, VectorXZ[] high, double base, double height) {\n\t\t\t\n\t\t\tfor (int a = 0; a < 4; a++) {\n\t\t\t\n\t\t\t\tList<VectorXYZ> vs = new ArrayList<VectorXYZ>();\n\t\t\t\tList<VectorXZ> tex = new ArrayList<VectorXZ>();\n\t\t\t\tList<List<VectorXZ>> texList =\n\t\t\t\t\tnCopies(Materials.POWER_TOWER_VERTICAL.getNumTextureLayers(), tex);\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i < 2; i++) {\n\t\t\t\t\tint idx = (a+i)%4;\n\t\t\t\t\tvs.add(high[idx].xyz(height));\n\t\t\t\t\tvs.add(low[idx].xyz(base));\n\t\t\t\t\ttex.add(new VectorXZ(i, 1));\n\t\t\t\t\ttex.add(new VectorXZ(i, 0));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\ttarget.drawTriangleStrip(Materials.POWER_TOWER_VERTICAL, vs, texList);\n\t\t\t}\n\t\t}\n\t\n\t\tprivate void drawHorizontalSegment(Target<?> target,\n\t\t\t\tVectorXZ left, VectorXZ right, double base,\n\t\t\t\tdouble left_height, double right_height) {\n\n\t\t\tList<VectorXYZ> vs = new ArrayList<VectorXYZ>();\n\t\t\tList<VectorXZ> tex = new ArrayList<VectorXZ>();\n\t\t\tList<List<VectorXZ>> texList =\n\t\t\t\t\tnCopies(Materials.POWER_TOWER_HORIZONTAL.getNumTextureLayers(), tex);\n\t\t\n\t\t\tvs.add(right.xyz(base));\n\t\t\tvs.add(left.xyz(base));\n\t\t\tvs.add(right.xyz(base+right_height));\n\t\t\tvs.add(left.xyz(base+left_height));\n\t\t\n\t\t\ttex.add(new VectorXZ(1, 1));\n\t\t\ttex.add(new VectorXZ(0, 1));\n\t\t\ttex.add(new VectorXZ(1, 0));\n\t\t\ttex.add(new VectorXZ(0, 0));\n\t\t\n\t\t\ttarget.drawTriangleStrip(Materials.POWER_TOWER_HORIZONTAL, vs, texList);\n\t\t}\n\n\t\tprivate void drawHorizontalTop(Target<?> target, VectorXZ[][] points,\n\t\t\t\tdouble base, double border, double middle, double center) {\n\t\t\t\n\t\t\tdouble[] height = new double[]{border, middle, center, center, middle, border};\n\t\t\t\n\t\t\tint len = height.length;\n\t\t\tfor (int a = 0; a < len-1; a++) {\n\t\t\t\n\t\t\t\tList<VectorXYZ> vs = new ArrayList<VectorXYZ>();\n\t\t\t\tList<VectorXZ> tex = new ArrayList<VectorXZ>();\n\t\t\t\tList<List<VectorXZ>> texList =\n\t\t\t\t\t\tnCopies(Materials.POWER_TOWER_VERTICAL.getNumTextureLayers(), tex);\n\n\t\t\t\tfor (int i = 0; i < 2; i++) {\n\t\t\t\t\tvs.add(points[1][a+i].xyz(base + height[a+i]));\n\t\t\t\t\tvs.add(points[2][a+i].xyz(base + height[a+i]));\n\t\t\t\t\ttex.add(new VectorXZ(0, i));\n\t\t\t\t\ttex.add(new VectorXZ(1, i));\n\t\t\t\t}\n\t\t\t\ttarget.drawTriangleStrip(Materials.POWER_TOWER_VERTICAL, vs, texList);\n\t\t\t}\n\t\t}\n\t\t\n\t\t\n\t\tprivate double drawPart(Target<?> target, double elevation,\n\t\t\t\tint nr_segments, double segment_height, double ground_size,\n\t\t\t\tdouble top_size) {\n\n\t\t\tfor (int i = 0; i < nr_segments; i++) {\n\n\t\t\t\tdouble bottom = ground_size + i * (top_size - ground_size) / nr_segments;\n\t\t\t\tdouble top = ground_size + (i + 1) * (top_size - ground_size) / nr_segments;\n\n\t\t\t\tVectorXZ[][] low = getCorners(node.getPos(), bottom);\n\t\t\t\tVectorXZ[][] high = getCorners(node.getPos(), top);\n\n\t\t\t\tdrawSegment(target, low[0], high[0], elevation, elevation + segment_height);\n\t\t\t\tdrawSegment(target, low[1], high[1], elevation, elevation + segment_height);\n\n\t\t\t\televation += segment_height;\n\t\t\t}\n\n\t\t\treturn elevation;\n\t\t}\n\n\t\tprivate VectorXZ[] getPoleCoordinates(VectorXZ base,\n\t\t\t\tVectorXZ direction, double width, double size) {\n\t\t\t\n\t\t\treturn new VectorXZ[] {\n\t\t\t\t\tbase.add(direction.mult(-width - size)),\n\t\t\t\t\tbase.add(direction.mult(-width / 2 - size)),\n\t\t\t\t\tbase.add(direction.mult(-size)),\n\t\t\t\t\tbase.add(direction.mult(size)),\n\t\t\t\t\tbase.add(direction.mult(width / 2 + size)),\n\t\t\t\t\tbase.add(direction.mult(width + size))\n\t\t\t};\n\t\t}\n\n\t\tprivate void drawHorizontalPole(Target<?> target, double elevation,\n\t\t\t\tdouble diameter, double width) {\n\n\t\t\tdouble half = diameter / 2;\n\t\t\tVectorXZ ortho = direction.rightNormal();\n\n\t\t\t// TODO: if we can switch off backface culling we'd only need one face here\n\t\t\tVectorXZ[][] draw = new VectorXZ[][] {\n\t\t\t\t\tgetPoleCoordinates(node.getPos().add(direction.mult(-half)), ortho, width, half),\n\t\t\t\t\tgetPoleCoordinates(node.getPos().add(direction.mult(-half)), ortho.invert(), width, half),\n\t\t\t\t\tgetPoleCoordinates(node.getPos().add(direction.mult(half)), ortho.invert(), width, half),\n\t\t\t\t\tgetPoleCoordinates(node.getPos().add(direction.mult(half)), ortho, width, half)\n\t\t\t};\n\n\t\t\tfor (int i = 0; i < 4; i++) {\n\t\t\t\tdrawHorizontalSegment(target, draw[i][0], draw[i][1], elevation, 0.1, diameter/2);\n\t\t\t\tdrawHorizontalSegment(target, draw[i][1], draw[i][2], elevation, diameter/2, diameter);\n\t\t\t\tdrawHorizontalSegment(target, draw[i][2], draw[i][3], elevation, diameter, diameter);\n\t\t\t\tdrawHorizontalSegment(target, draw[i][3], draw[i][4], elevation, diameter, diameter/2);\n\t\t\t\tdrawHorizontalSegment(target, draw[i][4], draw[i][5], elevation, diameter/2, 0.1);\n\t\t\t}\n\t\t\t\n\t\t\tdrawHorizontalTop(target, draw, elevation, 0.1, diameter/2, diameter);\n\t\t}\n\n\t\t// TODO we're missing the ceramics to hold the power lines\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\n\t\t\tfloat pole_width = config.voltage > 150000 ? 16 : 13;\n\t\t\tfloat[] tower_width = config.voltage > 150000 ? new float[]{11,6,4f,0} : new float[]{8,5,3,0};\n\t\t\tdouble height = getTowerHeight();\n\n\t\t\tdouble segment_height = 2.5;\n\t\t\tdouble base = getBase().y - 0.5;\n\n\t\t\tint parts = (int) (height / segment_height);\n\t\t\tint low_parts = parts / 5;\n\n\t\t\t// draw the tower itself\n\t\t\tdouble ele = drawPart(target, base, low_parts, segment_height, tower_width[0], tower_width[1]);\n\t\t\tele = drawPart(target, ele, 3 * low_parts, segment_height, tower_width[1], tower_width[2]);\n\t\t\tdrawPart(target, ele, low_parts, segment_height, tower_width[2], tower_width[3]);\n\n\t\t\t// draw the vertical poles\n\t\t\tdrawHorizontalPole(target, base + height/2, 0.7*tower_width[1], pole_width);\n\t\t\tif (config.cables > 6) {\n\t\t\t\tdrawHorizontalPole(target, ele, 0.55*tower_width[2], 0.6*pole_width);\n\t\t\t}\n\t\t}\n\t}\n\t\n\tprivate static final class PhotovoltaicPlant extends AbstractAreaWorldObject\n\t\timplements RenderableToAllTargets {\n\n\t\t//TODO create individual EleConnector for panels\n\t\t\n\t\t/** compares vectors by x coordinate */\n\t\tprivate static final Comparator<VectorXZ> X_COMPARATOR = new Comparator<VectorXZ>() {\n\t\t\t@Override public int compare(VectorXZ v1, VectorXZ v2) {\n\t\t\t\treturn Double.compare(v1.x, v2.x);\n\t\t\t}\n\t\t};\n\n\t\tprotected PhotovoltaicPlant(MapArea area) {\n\t\t\tsuper(area);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t/* construct panel geometry */\n\n\t\t\tdouble panelAngle = PI / 4;\n\t\t\tdouble panelHeight = 5;\n\t\t\t\n\t\t\tVectorXYZ upVector = Z_UNIT.mult(panelHeight).rotateX(-panelAngle);\n\t\t\t\t\t\t\n\t\t\t/* place and draw rows of panels */\n\t\t\t\n\t\t\tAxisAlignedBoundingBoxXZ box = this.getAxisAlignedBoundingBoxXZ();\n\t\t\t\n\t\t\tList<SimplePolygonXZ> obstacles = getGroundObstacles();\n\t\t\t\n\t\t\tdouble posZ = box.minZ;\n\t\t\t\n\t\t\twhile (posZ + upVector.z < box.maxZ) {\n\t\t\t\t\n\t\t\t\tLineSegmentXZ rowLine = new LineSegmentXZ(\n\t\t\t\t\t\t new VectorXZ(box.minX - 10, posZ),\n\t\t\t\t\t\t new VectorXZ(box.maxX + 10, posZ));\n\t\t\t\t\n\t\t\t\t// calculate start and end points (maybe more than one each)\n\t\t\t\t\n\t\t\t\tList<VectorXZ> intersections =\n\t\t\t\t\t\tarea.getPolygon().intersectionPositions(rowLine);\n\t\t\t\t\n\t\t\t\tassert intersections.size() % 2 == 0;\n\t\t\t\t\n\t\t\t\tsort(intersections, X_COMPARATOR);\n\t\t\t\t\n\t\t\t\t// add more start/end points at ground-level obstacles\n\t\t\t\t\n\t\t\t\tfor (SimplePolygonXZ obstacle : obstacles) {\n\t\t\t\t\t\n\t\t\t\t\tList<VectorXZ> obstacleIntersections =\n\t\t\t\t\t\t\tobstacle.intersectionPositions(rowLine);\n\t\t\t\t\t\n\t\t\t\t\tfor (int i = 0; i + 1 < obstacleIntersections.size(); i += 2) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tint insertionIndexA = -binarySearch(intersections,\n\t\t\t\t\t\t\t\tobstacleIntersections.get(i), X_COMPARATOR) - 1;\n\t\t\t\t\t\tint insertionIndexB = -binarySearch(intersections,\n\t\t\t\t\t\t\t\tobstacleIntersections.get(i + 1), X_COMPARATOR) - 1;\n\t\t\t\t\t\t\n\t\t\t\t\t\tsort(obstacleIntersections, X_COMPARATOR);\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (insertionIndexA == insertionIndexB\n\t\t\t\t\t\t\t\t&& insertionIndexA >= 0\n\t\t\t\t\t\t\t\t&& insertionIndexA % 2 == 1) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tintersections.add(insertionIndexA,\n\t\t\t\t\t\t\t\t\tobstacleIntersections.get(i+1));\n\t\t\t\t\t\t\tintersections.add(insertionIndexA,\n\t\t\t\t\t\t\t\t\tobstacleIntersections.get(i));\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// draw row of panels between each start/end pair\n\t\t\t\t\n\t\t\t\tassert intersections.size() % 2 == 0;\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i + 1 < intersections.size(); i += 2) {\n\t\t\t\t\t\n\t\t\t\t\t// TODO: take elevation into account\n\t\t\t\t\t// Might necessitate individual panels or shorter strips.\n\t\t\t\t\t\n//\t\t\t\t\trenderPanelsTo(target,\n//\t\t\t\t\t\t\teleProfile.getWithEle(intersections.get(i)),\n//\t\t\t\t\t\t\teleProfile.getWithEle(intersections.get(i+1)),\n//\t\t\t\t\t\t\tupVector);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tposZ += upVector.z * 1.5;\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\t/**\n\t\t * returns outlines from ground objects overlapping this area\n\t\t */\n\t\tprivate List<SimplePolygonXZ> getGroundObstacles() {\n\t\t\t\n\t\t\tList<SimplePolygonXZ> obstacles = new ArrayList<SimplePolygonXZ>();\n\t\t\t\n\t\t\tfor (MapOverlap<?, ?> overlap : area.getOverlaps()) {\n\t\t\t\tfor (WorldObject otherWO : overlap.getOther(area).getRepresentations()) {\n\t\t\t\t\t\n\t\t\t\t\tif (otherWO.getGroundState() == GroundState.ON\n\t\t\t\t\t\t\t&& otherWO instanceof WorldObjectWithOutline) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tobstacles.add(((WorldObjectWithOutline)otherWO).getOutlinePolygonXZ());\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\treturn obstacles;\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate void renderPanelsTo(Target<?> target, VectorXYZ bottomLeft,\n\t\t\t\tVectorXYZ bottomRight, VectorXYZ upVector) {\n\t\t\t\n\t\t\t/* draw front */\n\t\t\t\n\t\t\tList<VectorXYZ> vs = asList(\n\t\t\t\t\tbottomLeft.add(upVector),\n\t\t\t\t\tbottomLeft,\n\t\t\t\t\tbottomRight.add(upVector),\n\t\t\t\t\tbottomRight);\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(Materials.SOLAR_PANEL, vs,\n\t\t\t\t\ttexCoordLists(vs, Materials.SOLAR_PANEL, STRIP_WALL));\n\t\t\t\n\t\t\t/* draw back */\n\t\t\t\n\t\t\tvs = asList(vs.get(2), vs.get(3), vs.get(0), vs.get(1));\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(Materials.PLASTIC_GREY, vs,\n\t\t\t\t\ttexCoordLists(vs, Materials.PLASTIC_GREY, STRIP_WALL));\n\t\t\t\t\t\t\n\t\t}\n\t\t\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/RailwayModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static com.google.common.collect.Iterables.any;\nimport static java.util.Arrays.asList;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.GLOBAL_X_Z;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.texCoordLists;\nimport static org.osm2world.core.util.Predicates.hasType;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.GeometryUtil;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.modules.common.ConfigurableWorldModule;\nimport org.osm2world.core.world.modules.common.WorldModuleGeometryUtil;\nimport org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject;\nimport org.osm2world.core.world.network.JunctionNodeWorldObject;\n\n/**\n * adds rails to the world\n */\npublic class RailwayModule extends ConfigurableWorldModule {\n\n\t/** accepted values of the railway key */\n\tprivate static final List<String> RAILWAY_VALUES = asList(\n\t\t\t\"rail\", \"light_rail\", \"tram\", \"subway\", \"disused\");\n\t\n\t@Override\n\tpublic void applyTo(MapData grid) {\n\t\t\n\t\tfor (MapWaySegment segment : grid.getMapWaySegments()) {\n\t\t\tif (segment.getTags().containsAny(\"railway\", RAILWAY_VALUES)) {\n\t\t\t\tif (! TunnelModule.isTunnel(segment.getTags())) {\n\t\t\t\t\tsegment.addRepresentation(new Rail(segment));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t//TODO: the following for loop is copied from water module and should be in a common superclass\n\t\tfor (MapNode node : grid.getMapNodes()) {\n\t\t\t\n\t\t\tint connectedRails = 0;\n\t\t\t\n\t\t\tfor (MapWaySegment line : node.getConnectedWaySegments()) {\n\t\t\t\tif (any(line.getRepresentations(), hasType(Rail.class))) {\n\t\t\t\t\tconnectedRails += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (connectedRails > 2) {\n\t\t\t\t// node.addRepresentation(new RailJunction(node));\n\t\t\t\t// TODO: reactivate after implementing proper rendering for rail junctions\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class Rail extends AbstractNetworkWaySegmentWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\n\t\tprivate static float TM_SCALE = Float.parseFloat(System.getenv(\"TOUCH_MAPPER_SCALE\"));\n\t\tprivate static float TM_EXTRUDER = Float.parseFloat(System.getenv(\"TOUCH_MAPPER_EXTRUDER_WIDTH\"));\n\t\t\n\t\tprivate static float sizesScaling = TM_SCALE / 3100; // historically things were tuned for scale 3100\n\n\t\tprivate static final float GROUND_WIDTH = 2.25f * sizesScaling;\n\t\tprivate static final float RAIL_DIST = 1.5f * sizesScaling;\n\t\n\t\tprivate static final float SLEEPER_HEIGHT = 0.125f * sizesScaling;\n\t\t\n\t\tprivate static final List<VectorXYZ> RAIL_SHAPE = asList(\n\t\t\tnew VectorXYZ(-0.45f, 0, 0), new VectorXYZ(-0.1f, 0.1f, 0),\n\t\t\tnew VectorXYZ(-0.1f, 0.5f, 0), new VectorXYZ(-0.25f, 0.55f, 0),\n\t\t\tnew VectorXYZ(-0.25f, 0.75f, 0), new VectorXYZ(+0.25f, 0.75f, 0),\n\t\t\tnew VectorXYZ(+0.25f, 0.55f, 0), new VectorXYZ(+0.1f, 0.5f, 0),\n\t\t\tnew VectorXYZ(+0.1f, 0.1f, 0), new VectorXYZ(+0.45f, 0, 0)\n\t\t);\n\t\t\n\t\tstatic {\n\t\t\tfor (int i=0; i < RAIL_SHAPE.size(); i++) {\n\t\t\t\tVectorXYZ v = RAIL_SHAPE.get(i);\n\t\t\t\tv = v.mult(0.25f);\n\t\t\t\tv = v.y(v.y + SLEEPER_HEIGHT);\n\t\t\t\tRAIL_SHAPE.set(i, v);\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t\tpublic Rail(MapWaySegment segment) {\n\t\t\tsuper(segment);\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\t\n\t\t\tif (segment.getTags().contains(\"railway\", \"subway\")\n\t\t\t\t\t&& !segment.getTags().contains(\"tunnel\", \"no\")){\n\t\t\t\treturn GroundState.BELOW;\n\t\t\t}\n\t\t\t\n\t\t\treturn super.getGroundState();\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\n\t\t\t/* draw ground */\n\t\t\t\n\t\t\tList<VectorXYZ> groundVs = WorldModuleGeometryUtil.createTriangleStripBetween(\n\t\t\t\t\tgetOutline(false), getOutline(true));\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(Materials.RAIL_BALLAST_DEFAULT, groundVs,\n\t\t\t\t\ttexCoordLists(groundVs, Materials.RAIL_BALLAST_DEFAULT, GLOBAL_X_Z));\n\t\t\t\n\t\t\t\n\t\t\t/* draw rails */\n\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tList<VectorXYZ>[] railLines = new List[2];\n\t\t\t\n\t\t\trailLines[0] = WorldModuleGeometryUtil.createLineBetween(\n\t\t\t\t\tgetOutline(false), getOutline(true),\n\t\t\t\t\t((GROUND_WIDTH - RAIL_DIST) / GROUND_WIDTH) / 2);\n\n\t\t\trailLines[1] = WorldModuleGeometryUtil.createLineBetween(\n\t\t\t\t\tgetOutline(false), getOutline(true),\n\t\t\t\t\t1 - ((GROUND_WIDTH - RAIL_DIST) / GROUND_WIDTH) / 2);\n\n//\t\t\tfor (List<VectorXYZ> railLine : railLines) {\n//\t\t\t\t\n//\t\t\t\tList<List<VectorXYZ>> stripVectors =\n//\t\t\t\t\tWorldModuleGeometryUtil.createShapeExtrusionAlong(\n//\t\t\t\t\tRAIL_SHAPE, railLine,\n//\t\t\t\t\tCollections.nCopies(railLine.size(), VectorXYZ.Y_UNIT));\n//\t\t\t\t\t\n//\t\t\t\tfor (List<VectorXYZ> stripVector : stripVectors) {\n//\t\t\t\t\ttarget.drawTriangleStrip(Materials.RAIL_DEFAULT, stripVector, null);\n//\t\t\t\t}\n//\t\t\t\n//\t\t\t}\n//\t\t\t\n//\t\t\t\n//\t\t\t/* draw railway ties/sleepers */\n//\t\t\t\n//\t\t\tList<VectorXZ> sleeperPositions = GeometryUtil.equallyDistributePointsAlong(3, false,\n//\t\t\t\t\tgetStartWithOffset(), getEndWithOffset());\n//\t\t\t\t\t\t\n//\t\t\tfor (VectorXZ sleeperPosition : sleeperPositions) {\n//\t\t\t\n//\t\t\t\t//TODO interpolate ele, also using additional points inbetween\n//\t\t\t\t\n////\t\t\t\tVectorXYZ sleeperPositionXYZ =\n////\t\t\t\t\t\tsegment.getElevationProfile().getWithEle(sleeperPosition);\n////\n////\t\t\t\ttarget.drawBox(Materials.RAIL_SLEEPER_DEFAULT,\n////\t\t\t\t\t\tsleeperPositionXYZ, segment.getDirection(),\n////\t\t\t\t\t\tSLEEPER_HEIGHT, SLEEPER_WIDTH, SLEEPER_LENGTH);\n//\t\t\t\t\n//\t\t\t}\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic float getWidth() {\n\t\t\treturn GROUND_WIDTH;\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class RailJunction\n\t\textends JunctionNodeWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\t\t\n\t\tpublic RailJunction(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\t//TODO (code duplication): copied from RoadModule\n\t\t\tGroundState currentGroundState = null;\n\t\t\tcheckEachLine: {\n\t\t\t\tfor (MapWaySegment line : this.node.getConnectedWaySegments()) {\n\t\t\t\t\tif (line.getPrimaryRepresentation() == null) continue;\n\t\t\t\t\tGroundState lineGroundState = line.getPrimaryRepresentation().getGroundState();\n\t\t\t\t\tif (currentGroundState == null) {\n\t\t\t\t\t\tcurrentGroundState = lineGroundState;\n\t\t\t\t\t} else if (currentGroundState != lineGroundState) {\n\t\t\t\t\t\tcurrentGroundState = GroundState.ON;\n\t\t\t\t\t\tbreak checkEachLine;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn currentGroundState;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tif (getOutlinePolygon() == null) return;\n\t\t\t\n\t\t\t/* draw ground */\n\n\t\t\tList<VectorXYZ> vectors = getOutlinePolygon().getVertexLoop();\n\n\t\t\tMaterial material = Materials.RAIL_BALLAST_DEFAULT;\n\t\t\t\n\t\t\ttarget.drawConvexPolygon(material, vectors,\n\t\t\t\t\ttexCoordLists(vectors, material, GLOBAL_X_Z));\n\n\t\t\t/* draw connection between each pair of rails */\n\n\t\t\t/* TODO: use node.getConnectedLines() instead?\n\t\t\t * (allows access to information from there,\n\t\t\t *  such as getOutline!)\n\t\t\t */\n\n\t\t\tfor (int i=0; i<cutCenters.size(); i++) {\n\t\t\t\tfor (int j=0; j<i; j++) {\n\n\t\t\t\t\t/* connect those rails with an obtuse angle between them */\n\n\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/RoadModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.lang.Math.*;\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.reverse;\nimport static org.openstreetmap.josm.plugins.graphview.core.data.EmptyTagGroup.EMPTY_TAG_GROUP;\nimport static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseOsmDecimal;\nimport static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.*;\nimport static org.osm2world.core.math.GeometryUtil.interpolateElevation;\nimport static org.osm2world.core.math.VectorXYZ.*;\nimport static org.osm2world.core.target.common.material.Materials.*;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.*;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.*;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.*;\nimport static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.MapBasedTagGroup;\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.object_info.ObjectInfoManager;\nimport org.osm2world.core.map_data.object_info.ObjectType;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.GeometryUtil;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.target.common.material.TexCoordFunction;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.modules.common.ConfigurableWorldModule;\nimport org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject;\nimport org.osm2world.core.world.network.JunctionNodeWorldObject;\nimport org.osm2world.core.world.network.NetworkAreaWorldObject;\nimport org.osm2world.core.world.network.VisibleConnectorNodeWorldObject;\n\n/**\n * adds roads to the world\n */\npublic class RoadModule extends ConfigurableWorldModule {\n\t\n\t/** determines whether right-hand or left-hand traffic is the default */\n\tprivate static final boolean RIGHT_HAND_TRAFFIC_BY_DEFAULT = true;\n\t\n\t@Override\n\tpublic void applyTo(MapData grid) {\n\t\t\n\t\tfor (MapWaySegment line : grid.getMapWaySegments()) {\n\t\t\tif (isRoad(line.getTags())) {\n\t\t\t\tif (! isTunnelOrSimilar(line.getTags())) {\n\t\t\t\t\tline.addRepresentation(new Road(line, line.getTags()));\n\t\t\t\t\tObjectInfoManager.addRoadSegment(line);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (MapArea area : grid.getMapAreas()) {\n\t\t\t\t\n\t\t\tif (isRoad(area.getTags())) {\n\t\t\t\tif (! isTunnelOrSimilar(area.getTags())) {\n\t\t\t\t\tList<VectorXZ> coords = new ArrayList<VectorXZ>();\n\t\t\t\t\tfor (MapNode node : area.getBoundaryNodes()) {\n\t\t\t\t\t\tcoords.add(node.getPos());\n\t\t\t\t\t}\n\t\t\t\t\tcoords.remove(coords.size()-1);\n\t\t\t\t\t\n\t\t\t\t\tarea.addRepresentation(new RoadArea(area));\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tfor (MapNode node : grid.getMapNodes()) {\n\n\t\t\tTagGroup tags = node.getOsmNode().tags;\n\t\t\t\n\t\t\tList<Road> connectedRoads = getConnectedRoads(node, false);\n\t\t\t\n\t\t\tif (connectedRoads.size() == 3) {\n\t\t\t\t\n\t\t\t\tnode.addRepresentation(new RoadJunction(node));\n\t\t\t\t\n\t\t\t} else if (connectedRoads.size() == 2\n\t\t\t\t\t&& tags.contains(\"highway\", \"crossing\")\n\t\t\t\t\t&& !tags.contains(\"crossing\", \"no\")) {\n\t\t\t\t\n\t\t\t\tnode.addRepresentation(new RoadCrossingAtConnector(node));\n\t\t\t\t\n\t\t\t} else if (connectedRoads.size() == 2) {\n\t\t\t\t\n\t\t\t\tRoad road1 = connectedRoads.get(0);\n\t\t\t\tRoad road2 = connectedRoads.get(1);\n\t\t\t\t\n\t\t\t\tif (road1.getWidth() != road2.getWidth()\n\t\t\t\t\t\t/* TODO: || lane layouts not identical */) {\n\t\t\t\t\tnode.addRepresentation(new RoadConnector(node));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tpublic static final boolean isTunnelOrSimilar(TagGroup tags) {\n\t\tif (tags.containsKey(\"tunnel\")\n\t\t\t\t&& !\"no\".equals(tags.getValue(\"tunnel\"))\n\t\t\t\t/* Touch Mapper: keep building passages as tactilely continuous roads. */\n\t\t\t\t&& !\"building_passage\".equals(tags.getValue(\"tunnel\"))) {\n\t\t\treturn true;\n\t\t};\n\t\tif (tags.containsKey(\"indoor\")\n\t\t\t\t&& !\"no\".equals(tags.getValue(\"indoor\"))) {\n\t\t\treturn true;\n\t\t};\n\t\treturn false;\n\t}\n\n\tprivate static boolean isRoad(TagGroup tags) {\n\t\tif (tags.containsKey(\"highway\")\n\t\t\t\t&& !tags.contains(\"highway\", \"construction\")\n\t\t\t\t&& !tags.contains(\"highway\", \"proposed\")\n\t\t\t\t/* Touch Mapper: do not materialize explicitly proposed bridges as tactile roads. */\n\t\t\t\t&& !tags.contains(\"bridge\", \"proposed\")) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn tags.contains(\"railway\", \"platform\")\n\t\t\t\t|| tags.contains(\"leisure\", \"track\")\n\t\t\t\t|| tags.contains(\"tourism\", \"attraction\")\n\t\t\t\t|| tags.contains(\"man_made\", \"pier\")\n\t\t\t\t|| tags.contains(\"man_made\", \"breakwater\");\n\t\t}\n\t}\n\t\n\tprivate static boolean isSteps(TagGroup tags) {\n\t\treturn false;\n//\t\treturn tags.contains(new Tag(\"highway\",\"steps\"));\n\t}\n\n\t// See also TouchMapperCategory.isPedestrian()\n\tprivate static boolean isPath(TagGroup tags) {\n\t\tif (tags.contains(\"man_made\", \"pier\")\n\t\t\t\t|| tags.contains(\"man_made\", \"breakwater\")) {\n\t\t\treturn true;\n\t\t}\n\t\tString highwayValue = tags.getValue(\"highway\");\n\t\treturn \"path\".equals(highwayValue)\n\t\t\t|| \"footway\".equals(highwayValue)\n\t\t\t|| \"cycleway\".equals(highwayValue)\n\t\t\t|| \"bridleway\".equals(highwayValue)\n\t\t\t|| \"steps\".equals(highwayValue);\n\t}\n\n\tprivate static boolean isOneway(TagGroup tags) {\n\t\treturn tags.contains(\"oneway\", \"yes\")\n\t\t\t\t|| (!tags.contains(\"oneway\", \"no\")\n\t\t\t\t\t&& (tags.contains(\"highway\", \"motorway\")\n\t\t\t\t\t|| (tags.contains(\"highway\", \"motorway_link\"))));\n\t}\n\n\tprivate static int getDefaultLanes(TagGroup tags) {\n\t\tString highwayValue = tags.getValue(\"highway\");\n\t\tif (highwayValue == null\n\t\t\t\t|| isPath(tags)\n\t\t\t\t|| highwayValue.endsWith(\"_link\")\n\t\t\t\t|| \"service\".equals(highwayValue)\n\t\t\t\t|| \"track\".equals(highwayValue)\n\t\t\t\t|| \"residential\".equals(highwayValue)\n\t\t\t\t|| \"living_street\".equals(highwayValue)\n\t\t\t\t|| \"pedestrian\".equals(highwayValue)\n\t\t\t\t|| \"platform\".equals(highwayValue)) {\n\t\t\treturn 1;\n\t\t} else if (\"motorway\".equals(highwayValue)){\n\t\t\treturn 2;\n\t\t} else {\n\t\t\treturn isOneway(tags) ? 1 : 2;\n\t\t}\n\t}\n\n\t/**\n\t * determines surface for a junction or connector/crossing.\n\t * If the node has an explicit surface tag, this is evaluated.\n\t * Otherwise, the result depends on the surface values of adjacent roads.\n\t */\n\tprivate static Material getSurfaceForNode(MapNode node) {\n\t\t\n\t\tMaterial surface = getSurfaceMaterial(\n\t\t\t\tnode.getTags().getValue(\"surface\"), null);\n\t\t\n\t\tif (surface == null) {\n\t\t\t\n\t\t\t/* choose the surface of any adjacent road */\n\t\t\t\n\t\t\tfor (MapWaySegment segment : node.getConnectedWaySegments()) {\n\t\t\t\t\n\t\t\t\tif (segment.getPrimaryRepresentation() instanceof Road) {\n\t\t\t\t\tRoad road = (Road)segment.getPrimaryRepresentation();\n\t\t\t\t\tsurface = road.getSurface();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn surface;\n\t\t\n\t}\n\t\n\tprivate static Material getSurfaceForRoad(TagGroup tags,\n\t\t\tMaterial defaultSurface) {\n\n\t\tMaterial result;\n\t\t\n\t\tif (tags.containsKey(\"tracktype\")) {\n\t\t\tif (tags.contains(\"tracktype\", \"grade1\")) {\n\t\t\t\tresult = ASPHALT;\n\t\t\t} else if (tags.contains(\"tracktype\", \"grade2\")) {\n\t\t\t\tresult = GRAVEL;\n\t\t\t} else {\n\t\t\t\tresult = EARTH;\n\t\t\t}\n\t\t} else {\n\t\t\tresult = defaultSurface;\n\t\t}\n\t\t\n\t\treturn getSurfaceMaterial(tags.getValue(\"surface\"), result);\n\t\t\n\t}\n\t\n\tprivate static Material getSurfaceMiddleForRoad(TagGroup tags,\n\t\t\tMaterial defaultSurface) {\n\n\t\tMaterial result;\n\t\t\n\t\tif (tags.contains(\"tracktype\", \"grade4\")\n\t\t\t\t|| tags.contains(\"tracktype\", \"grade5\")) {\n\t\t\tresult = TERRAIN_DEFAULT;\n\t\t\t// ideally, this would be the terrain type surrounds the track...\n\t\t} else {\n\t\t\tresult = defaultSurface;\n\t\t}\n\t\t\n\t\tresult = getSurfaceMaterial(tags.getValue(\"surface:middle\"), result);\n\t\t\n\t\tif (result == GRASS) {\n\t\t\tresult = TERRAIN_DEFAULT;\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\n\t/**\n\t * returns all roads connected to a node\n\t * @param requireLanes  only include roads that are not paths and have lanes\n\t */\n\tprivate static List<Road> getConnectedRoads(MapNode node,\n\t\t\tboolean requireLanes) {\n\t\t\n\t\tList<Road> connectedRoadsWithLanes = new ArrayList<Road>();\n\t\t\t\t\n\t\tfor (MapWaySegment segment : node.getConnectedWaySegments()) {\n\t\t\t\n\t\t\tif (segment.getPrimaryRepresentation() instanceof Road) {\n\t\t\t\tRoad road = (Road)segment.getPrimaryRepresentation();\n\t\t\t\tif (!requireLanes ||\n\t\t\t\t\t\t(road.getLaneLayout() != null && !isPath(road.tags))) {\n\t\t\t\t\tconnectedRoadsWithLanes.add(road);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn connectedRoadsWithLanes;\n\t\t\n\t}\n\t\n\t/**\n\t * find matching lane pairs\n\t * (lanes that can be connected at a junction or connector)\n\t */\n\tprivate static Map<Integer, Integer> findMatchingLanes(\n\t\t\tList<Lane> lanes1, List<Lane> lanes2,\n\t\t\tboolean isJunction, boolean isCrossing) {\n\t\t\n\t\tMap<Integer, Integer> matches = new HashMap<Integer, Integer>();\n\t\t\n\t\t/*\n\t\t * iterate from inside to outside\n\t\t * (only for connectors, where it will lead to desirable connections\n\t\t * between straight motorcar lanes e.g. at highway exits)\n\t\t */\n\t\t\n\t\tif (!isJunction) {\n\t\t\t\n\t\t\tfor (int laneI = 0; laneI < lanes1.size()\n\t\t\t\t\t&& laneI < lanes2.size(); ++laneI) {\n\t\t\t\t\n\t\t\t\tfinal Lane lane1 = lanes1.get(laneI);\n\t\t\t\tfinal Lane lane2 = lanes2.get(laneI);\n\t\t\t\t\n\t\t\t\tif (isCrossing && !lane1.type.isConnectableAtCrossings) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (isJunction && !lane1.type.isConnectableAtJunctions) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (lane2.type.equals(lane1.type)) {\n\t\t\t\t\t\n\t\t\t\t\tmatches.put(laneI, laneI);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* iterate from outside to inside.\n\t\t * Mostly intended to gather sidewalks and other non-car lanes. */\n\t\t\n\t\tfor (int laneI = 0; laneI < lanes1.size()\n\t\t\t\t&& laneI < lanes2.size(); ++laneI) {\n\t\t\t\n\t\t\tint lane1Index = lanes1.size() - 1 - laneI;\n\t\t\tint lane2Index = lanes2.size() - 1 - laneI;\n\t\t\t\n\t\t\tfinal Lane lane1 = lanes1.get(lane1Index);\n\t\t\tfinal Lane lane2 = lanes2.get(lane2Index);\n\t\t\t\t\t\t\n\t\t\tif (isCrossing && !lane1.type.isConnectableAtCrossings) {\n\t\t\t\tcontinue;\n\t\t\t} else if (isJunction && !lane1.type.isConnectableAtJunctions) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t\n\t\t\tif (matches.containsKey(lane1Index)\n\t\t\t\t\t|| matches.containsKey(lane2Index)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t\n\t\t\tif (lane2.type.equals(lane1.type)) {\n\t\t\t\tmatches.put(lane1Index,\tlane2Index);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn matches;\n\t\t\n\t}\n\n\t/**\n\t * determines connected lanes at a junction, crossing or connector\n\t */\n\tprivate static List<LaneConnection> buildLaneConnections(\n\t\t\tMapNode node, boolean isJunction, boolean isCrossing) {\n\t\tif (true) {\n\t\t\treturn new ArrayList<LaneConnection>();\n\t\t}\n\t\t\n\t\tList<Road> roads = getConnectedRoads(node, true);\n\t\t\n\t\t/* check whether the oneway special case applies */\n\n\t\tif (isJunction) {\n\t\t\t\t\t\t\n\t\t\tboolean allOneway = true;\n\t\t\tint firstInboundIndex = -1;\n\t\t\t\n\t\t\tfor (int i = 0; i < roads.size(); i++) {\n\t\t\t\t\n\t\t\t\tRoad road = roads.get(i);\n\t\t\t\t\n\t\t\t\tif (!isOneway(road.tags)) {\n\t\t\t\t\tallOneway = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (firstInboundIndex == -1 && road.segment.getEndNode() == node) {\n\t\t\t\t\tfirstInboundIndex = i;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (firstInboundIndex != -1) {\n\t\t\t\n\t\t\t\t// sort into inbound and outbound oneways\n\t\t\t\t// (need to be sequential blocks in the road list)\n\t\t\t\t\n\t\t\t\tList<Road> inboundOnewayRoads = new ArrayList<Road>();\n\t\t\t\tList<Road> outboundOnewayRoads = new ArrayList<Road>();\n\t\t\t\t\n\t\t\t\tint i = 0;\n\t\t\t\t\n\t\t\t\tfor (i = firstInboundIndex; i < roads.size(); i++) {\n\t\t\t\t\t\n\t\t\t\t\tif (roads.get(i).segment.getEndNode() != node) {\n\t\t\t\t\t\tbreak; //not inbound\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tinboundOnewayRoads.add(roads.get(i));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\treverse(inboundOnewayRoads);\n\t\t\t\t\n\t\t\t\tfor (/* continue previous loop */;\n\t\t\t\t\t\ti % roads.size() != firstInboundIndex; i++) {\n\t\t\t\t\t\n\t\t\t\t\toutboundOnewayRoads.add(roads.get(i % roads.size()));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\n\t\t\t\tif (allOneway) {\n\t\t\t\t\treturn buildLaneConnections_allOneway(node,\n\t\t\t\t\t\t\tinboundOnewayRoads, outboundOnewayRoads);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* apply normal treatment (not oneway-specific) */\n\t\t\n\t\tList<LaneConnection> result = new ArrayList<LaneConnection>();\n\t\t\n\t\tfor (int i = 0; i < roads.size(); i++) {\n\n\t\t\tfinal Road road1 = roads.get(i);\n\t\t\tfinal Road road2 = roads.get(\n\t\t\t\t\t(i+1) % roads.size());\n\n\t\t\taddLaneConnectionsForRoadPair(result,\n\t\t\t\t\tnode, road1, road2,\n\t\t\t\t\tisJunction, isCrossing);\n\t\t\t\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\n\t/**\n\t * builds lane connections at a junction of just oneway roads.\n\t * Intended to handle motorway merges and splits well.\n\t * Inbound and outbound roads must not be mixed,\n\t * but build two separate continuous blocks instead.\n\t * \n\t * @param inboundOnewayRoadsLTR  inbound roads, left to right\n\t * @param outboundOnewayRoadsLTR  outbound roads, left to right\n\t */\n\tprivate static List<LaneConnection> buildLaneConnections_allOneway(\n\t\t\tMapNode node, List<Road> inboundOnewayRoadsLTR,\n\t\t\tList<Road> outboundOnewayRoadsLTR) {\n\t\tif (true) {\n\t\t\treturn new ArrayList<LaneConnection>();\n\t\t}\n\n\t\tList<Lane> inboundLanes = new ArrayList<Lane>();\n\t\tList<Lane> outboundLanes = new ArrayList<Lane>();\n\t\t\n\t\tfor (Road road : inboundOnewayRoadsLTR) {\n\t\t\tinboundLanes.addAll(road.getLaneLayout().getLanesLeftToRight());\n\t\t}\n\t\tfor (Road road : outboundOnewayRoadsLTR) {\n\t\t\toutboundLanes.addAll(road.getLaneLayout().getLanesLeftToRight());\n\t\t}\n\t\t\n\t\tMap<Integer, Integer> matches = findMatchingLanes(inboundLanes,\n\t\t\t\toutboundLanes, false, false);\n\t\t\n\t\t/* build connections */\n\t\t\n\t\tList<LaneConnection> result = new ArrayList<RoadModule.LaneConnection>();\n\t\t\n\t\tfor (int lane1Index : matches.keySet()) {\n\t\t\t\n\t\t\tfinal Lane lane1 = inboundLanes.get(lane1Index);\n\t\t\tfinal Lane lane2 = outboundLanes.get(matches.get(lane1Index));\n\t\t\t\n\t\t\tresult.add(buildLaneConnection(lane1, lane2,\n\t\t\t\t\tRoadPart.LEFT, //TODO: road part is not always the same\n\t\t\t\t\tfalse, true));\n\t\t\t\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\n\t/**\n\t * determines connected lanes at a junction, crossing or connector\n\t * for a pair of two of the junction's roads.\n\t * Only connections between the left part of road1 with the right part of\n\t * road2 will be taken into account.\n\t */\n\tprivate static void addLaneConnectionsForRoadPair(\n\t\t\tList<LaneConnection> result,\n\t\t\tMapNode node, Road road1, Road road2,\n\t\t\tboolean isJunction, boolean isCrossing) {\n\t\t\n\t\t/* get some basic info about the roads */\n\t\t\n\t\tfinal boolean isRoad1Inbound = road1.segment.getEndNode() == node;\n\t\tfinal boolean isRoad2Inbound = road2.segment.getEndNode() == node;\n\t\t\n\t\tfinal List<Lane> lanes1, lanes2;\n\t\t\n\t\tlanes1 = road1.getLaneLayout().getLanes(\n\t\t\t\tisRoad1Inbound ? RoadPart.LEFT : RoadPart.RIGHT);\n\n\t\tlanes2 = road2.getLaneLayout().getLanes(\n\t\t\t\tisRoad2Inbound ? RoadPart.RIGHT : RoadPart.LEFT);\n\t\t\n\t\t/* determine which lanes are connected */\n\t\t\n\t\tMap<Integer, Integer> matches =\n\t\t\t\tfindMatchingLanes(lanes1, lanes2, isJunction, isCrossing);\n\t\t\n\t\t/* build the lane connections */\n\t\t\n\t\tfor (int lane1Index : matches.keySet()) {\n\t\t\t\n\t\t\tfinal Lane lane1 = lanes1.get(lane1Index);\n\t\t\tfinal Lane lane2 = lanes2.get(matches.get(lane1Index));\n\t\t\t\n\t\t\tresult.add(buildLaneConnection(lane1, lane2, RoadPart.LEFT,\n\t\t\t\t\t!isRoad1Inbound, !isRoad2Inbound));\n\t\t\t\n\t\t}\n\t\t\n\t\t//TODO: connect \"disappearing\" lanes to a point on the other side\n\t\t//      or draw caps (only for connectors)\n\t\t\n\t}\n\t\n\tprivate static LaneConnection buildLaneConnection(\n\t\t\tLane lane1, Lane lane2, RoadPart roadPart,\n\t\t\tboolean atLane1Start, boolean atLane2Start) {\n\t\t\t\t\n\t\tList<VectorXYZ> leftLaneBorder = new ArrayList<VectorXYZ>();\n\t\tleftLaneBorder.add(lane1.getBorderNode(\n\t\t\t\tatLane1Start, atLane1Start));\n\t\tleftLaneBorder.add(lane2.getBorderNode(\n\t\t\t\tatLane2Start, !atLane2Start));\n\t\t\n\t\tList<VectorXYZ> rightLaneBorder = new ArrayList<VectorXYZ>();\n\t\trightLaneBorder.add(lane1.getBorderNode(\n\t\t\t\tatLane1Start, !atLane1Start));\n\t\trightLaneBorder.add(lane2.getBorderNode(\n\t\t\t\tatLane2Start, atLane2Start));\n\t\t\n\t\treturn new LaneConnection(lane1.type, RoadPart.LEFT,\n\t\t\t\tlane1.road.rightHandTraffic,\n\t\t\t\tleftLaneBorder, rightLaneBorder);\n\t\t\n\t}\n\n\t/**\n\t * representation for junctions between roads.\n\t */\n\tpublic static class RoadJunction\n\t\textends JunctionNodeWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\t\t\t\t\t\t\n\t\tpublic RoadJunction(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tMaterial material = getSurfaceForNode(node);\n\t\t\tCollection<TriangleXYZ> triangles = super.getTriangulation();\n\t\t\t\n\t\t\ttarget.drawTriangles(material, triangles,\n\t\t\t\t\ttriangleTexCoordLists(triangles, material, GLOBAL_X_Z));\n\t\t\t\n\t\t\t/* connect some lanes such as sidewalks between adjacent roads */\n\t\t\t\n\t\t\tList<LaneConnection> connections = buildLaneConnections(\n\t\t\t\t\tnode, true, false);\n\t\t\t\n\t\t\tfor (LaneConnection connection : connections) {\n\t\t\t\tconnection.renderTo(target);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\tGroundState currentGroundState = null;\n\t\t\tcheckEachLine: {\n\t\t\t\tfor (MapWaySegment line : this.node.getConnectedWaySegments()) {\n\t\t\t\t\tif (line.getPrimaryRepresentation() == null) continue;\n\t\t\t\t\tGroundState lineGroundState = line.getPrimaryRepresentation().getGroundState();\n\t\t\t\t\tif (currentGroundState == null) {\n\t\t\t\t\t\tcurrentGroundState = lineGroundState;\n\t\t\t\t\t} else if (currentGroundState != lineGroundState) {\n\t\t\t\t\t\tcurrentGroundState = GroundState.ON;\n\t\t\t\t\t\tbreak checkEachLine;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn currentGroundState;\n\t\t}\n\n\t}\n\t\n\t/* TODO: crossings at junctions - when there is, e.g., a footway connecting to the road!\n\t * (ideally, this would be implemented using more flexibly configurable\n\t * junctions which can have \"preferred\" segments that influence\n\t * the junction shape more/exclusively)\n\t */\n\t\n\t/**\n\t * visible connectors where a road changes width or lane layout\n\t */\n\tpublic static class RoadConnector\n\t\textends VisibleConnectorNodeWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\t\t\n\t\tprivate static final double MAX_CONNECTOR_LENGTH = 5;\n\t\t\n\t\tpublic RoadConnector(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic float getLength() {\n\t\t\t\n\t\t\t// length is at most a third of the shorter road segment's length\n\t\t\t\t\t\t\n\t\t\tList<Road> roads = getConnectedRoads(node, false);\n\t\t\t\n\t\t\treturn (float)Math.min(Math.min(\n\t\t\t\t\troads.get(0).segment.getLineSegment().getLength() / 3,\n\t\t\t\t\troads.get(1).segment.getLineSegment().getLength() / 3),\n\t\t\t\t\tMAX_CONNECTOR_LENGTH);\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tList<LaneConnection> connections = buildLaneConnections(\n\t\t\t\t\tnode, false, false);\n\t\t\t\n\t\t\t/* render connections */\n\t\t\t\n\t\t\tfor (LaneConnection connection : connections) {\n\t\t\t\tconnection.renderTo(target);\n\t\t\t}\n\t\t\t\n\t\t\t/* render area not covered by connections */\n\t\t\t\n\t\t\t//TODO: subtract area covered by connections\n\t\t\t\n\t\t\tMaterial material = getSurfaceForNode(node);\n\t\t\t\n\t\t\tCollection<TriangleXYZ> trianglesXYZ = getTriangulation();\n\t\t\t\n\t\t\ttarget.drawTriangles(material, trianglesXYZ,\n\t\t\t\t\ttriangleTexCoordLists(trianglesXYZ, material, GLOBAL_X_Z));\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\tGroundState currentGroundState = null;\n\t\t\tcheckEachLine: {\n\t\t\t\tfor (MapWaySegment line : this.node.getConnectedWaySegments()) {\n\t\t\t\t\tif (line.getPrimaryRepresentation() == null) continue;\n\t\t\t\t\tGroundState lineGroundState = line.getPrimaryRepresentation().getGroundState();\n\t\t\t\t\tif (currentGroundState == null) {\n\t\t\t\t\t\tcurrentGroundState = lineGroundState;\n\t\t\t\t\t} else if (currentGroundState != lineGroundState) {\n\t\t\t\t\t\tcurrentGroundState = GroundState.ON;\n\t\t\t\t\t\tbreak checkEachLine;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn currentGroundState;\n\t\t}\n\t\n\t}\n\t\n\t/**\n\t * representation for crossings (zebra crossing etc.) on roads\n\t */\n\tpublic static class RoadCrossingAtConnector\n\t\textends VisibleConnectorNodeWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\t\t\n\t\tprivate static final float CROSSING_WIDTH = 3f;\n\t\t\n\t\tpublic RoadCrossingAtConnector(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic float getLength() {\n\t\t\treturn parseWidth(node.getTags(), CROSSING_WIDTH);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tVectorXYZ startLeft = getEleConnectors().getPosXYZ(\n\t\t\t\t\tstartPos.subtract(cutVector.mult(0.5 * startWidth)));\n\t\t\tVectorXYZ startRight = getEleConnectors().getPosXYZ(\n\t\t\t\t\tstartPos.add(cutVector.mult(0.5 * startWidth)));\n\t\t\t\n\t\t\tVectorXYZ endLeft = getEleConnectors().getPosXYZ(\n\t\t\t\t\tendPos.subtract(cutVector.mult(0.5 * endWidth)));\n\t\t\tVectorXYZ endRight = getEleConnectors().getPosXYZ(\n\t\t\t\t\tendPos.add(cutVector.mult(0.5 * endWidth)));\n\t\t\t\n\t\t\t/* determine surface material */\n\t\t\t\n\t\t\tMaterial surface = getSurfaceForNode(node);\n\t\t\t\n\t\t\tif (node.getTags().contains(\"crossing\", \"zebra\")\n\t\t\t\t\t|| node.getTags().contains(\"crossing_ref\", \"zebra\")) {\n\t\t\t\t\n\t\t\t\tsurface = surface.withAddedLayers(\n\t\t\t\t\t\tROAD_MARKING_ZEBRA.getTextureDataList());\n\t\t\t\t\n\t\t\t} else if (!node.getTags().contains(\"crossing\", \"unmarked\")) {\n\n\t\t\t\tsurface = surface.withAddedLayers(\n\t\t\t\t\t\tROAD_MARKING_CROSSING.getTextureDataList());\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* draw crossing */\n\t\t\t\n\t\t\tList<VectorXYZ> vs = asList(endLeft, startLeft, endRight, startRight);\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(surface, vs,\n\t\t\t\t\ttexCoordLists(vs, surface, GLOBAL_X_Z));\n\t\t\t\n\t\t\t/* draw lane connections */\n\t\t\t\n\t\t\tList<LaneConnection> connections = buildLaneConnections(\n\t\t\t\t\tnode, false, true);\n\t\t\t\n\t\t\tfor (LaneConnection connection : connections) {\n\t\t\t\tconnection.renderTo(target);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\tGroundState currentGroundState = null;\n\t\t\tcheckEachLine: {\n\t\t\t\tfor (MapWaySegment line : this.node.getConnectedWaySegments()) {\n\t\t\t\t\tif (line.getPrimaryRepresentation() == null) continue;\n\t\t\t\t\tGroundState lineGroundState = line.getPrimaryRepresentation().getGroundState();\n\t\t\t\t\tif (currentGroundState == null) {\n\t\t\t\t\t\tcurrentGroundState = lineGroundState;\n\t\t\t\t\t} else if (currentGroundState != lineGroundState) {\n\t\t\t\t\t\tcurrentGroundState = GroundState.ON;\n\t\t\t\t\t\tbreak checkEachLine;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn currentGroundState;\n\t\t}\n\n\t}\n\t\t\n\t/** representation of a road */\n\tpublic static class Road\n\t\textends AbstractNetworkWaySegmentWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\n\t\tprivate static float TM_SCALE = Float.parseFloat(System.getenv(\"TOUCH_MAPPER_SCALE\"));\n\t\tprivate static float TM_EXTRUDER = Float.parseFloat(System.getenv(\"TOUCH_MAPPER_EXTRUDER_WIDTH\"));\n\n\t\t// Using exactly extruder width will leave many roads unprinted. The ideal road width may\n\t\t// be much less than the goal because edges where roads connect are not perpendicular\n\t\t// to road direction. Further unevenness in road width is caused by fattening in Blender.\n\t\t// Further, Simplify3D often uses value eg. 1.2x extruder width, depending on printer-dependent configuration.\n\t\tprivate static final float ROAD_WIDTH_MULTIPLIER = 1.6f;\n\n\t\tprotected static final float DEFAULT_LANE_WIDTH = 3.5f;\n\t\t\n\t\tprotected static final float DEFAULT_ROAD_CLEARING = 5;\n\t\tprotected static final float DEFAULT_PATH_CLEARING = 2;\n\t\t\n\t\tprotected static final List<VectorXYZ> HANDRAIL_SHAPE = asList(\n\t\t\tnew VectorXYZ(-0.02f, -0.05f, 0), new VectorXYZ(-0.02f,     0f, 0),\n\t\t\tnew VectorXYZ(+0.02f,     0f, 0), new VectorXYZ(+0.02f, -0.05f, 0));\n\t\t\n\t\tpublic final boolean rightHandTraffic;\n\t\t\n\t\tpublic final LaneLayout laneLayout;\n\t\tpublic final float width;\n\t\t\n\t\tfinal private TagGroup tags;\n\t\tfinal public VectorXZ startCoord, endCoord;\n\t\n\t\tfinal private boolean steps;\n\t\t\t\t\t\t\n\t\tpublic Road(MapWaySegment line, TagGroup tags) {\n\t\t\t\n\t\t\tsuper(line);\n\t\t\t\n\t\t\tthis.tags = tags;\n\t\t\tthis.startCoord = line.getStartNode().getPos();\n\t\t\tthis.endCoord = line.getEndNode().getPos();\n\t\t\t\n\t\t\tif (RIGHT_HAND_TRAFFIC_BY_DEFAULT) {\n\t\t\t\tif (tags.contains(\"driving_side\", \"left\")) {\n\t\t\t\t\trightHandTraffic = false;\n\t\t\t\t} else {\n\t\t\t\t\trightHandTraffic = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (tags.contains(\"driving_side\", \"right\")) {\n\t\t\t\t\trightHandTraffic = true;\n\t\t\t\t} else {\n\t\t\t\t\trightHandTraffic = false;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tthis.steps = isSteps(tags);\n\t\t\t\t\t\t\n\t\t\tif (steps) {\n\t\t\t\t// Never executed\n\t\t\t\tthis.laneLayout = null;\n\t\t\t\tthis.width = parseWidth(tags, 1.0f);\n\t\t\t} else {\n\t\t\t\tthis.laneLayout = buildBasicLaneLayout();\n\t\t\t\tfloat mmToUnits = TM_SCALE / 1000;\n\t\t\t\tfloat minRoadWidth = TM_EXTRUDER * ROAD_WIDTH_MULTIPLIER * mmToUnits;\n\t\t\t\tfloat sizesScaling = TM_SCALE / 3100; // historically things were tuned for scale 3100\n\t\t\t\tthis.width = Math.max(minRoadWidth, calculateWidth() * 0.8f * sizesScaling);\n\t\t\t\tlaneLayout.setCalculatedValues(width);\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\t/**\n\t\t * creates a lane layout from several basic tags.\n\t\t */\n\t\tprivate LaneLayout buildBasicLaneLayout() {\n\t\t\t\n\t\t\tboolean isOneway = isOneway(tags);\n\t\t\t\n\t\t\t/* determine which special lanes and attributes exist */\n\t\t\t\n\t\t\tString divider = tags.getValue(\"divider\");\n\t\t\t\n\t\t\tString sidewalk = tags.containsKey(\"sidewalk\") ?\n\t\t\t\t\ttags.getValue(\"sidewalk\") : tags.getValue(\"footway\");\n\t\t\t\n\t\t\tboolean leftSidewalk = \"left\".equals(sidewalk)\n\t\t\t\t\t|| \"both\".equals(sidewalk);\n\t\t\tboolean rightSidewalk = \"right\".equals(sidewalk)\n\t\t\t\t\t|| \"both\".equals(sidewalk);\n\t\t\t\n\t\t\tboolean leftCycleway = tags.contains(\"cycleway:left\", \"lane\")\n\t\t\t\t\t|| tags.contains(\"cycleway\", \"lane\");\n\t\t\tboolean rightCycleway = tags.contains(\"cycleway:right\", \"lane\")\n\t\t\t\t\t|| tags.contains(\"cycleway\", \"lane\");\n\t\t\t\n\t\t\t/* get individual values for each lane */\n\t\t\t\n\t\t\tTagGroup[] laneTagsRight = getPerLaneTags(RoadPart.RIGHT);\n\t\t\tTagGroup[] laneTagsLeft = getPerLaneTags(RoadPart.LEFT);\n\t\t\t\n\t\t\t/* determine the number of lanes */\n\t\t\t\t\t\t\n\t\t\tFloat lanes = null;\n\t\t\t\n\t\t\tif (tags.containsKey(\"lanes\")) {\n\t\t\t\tlanes = parseOsmDecimal(tags.getValue(\"lanes\"), false);\n\t\t\t}\n\t\t\t\n\t\t\tFloat lanesRight = null;\n\t\t\tFloat lanesLeft = null;\n\t\t\t\n\t\t\t//TODO handle oneway case\n\t\t\t\n\t\t\tString rightKey = rightHandTraffic ? \"lanes:forward\" : \"lanes:backward\";\n\t\t\t\n\t\t\tif (laneTagsRight != null) {\n\t\t\t\tlanesRight = (float)laneTagsRight.length;\n\t\t\t} else if (tags.containsKey(rightKey)) {\n\t\t\t\tlanesRight = parseOsmDecimal(tags.getValue(rightKey), false);\n\t\t\t}\n\n\t\t\tString leftKey = rightHandTraffic ? \"lanes:backward\" : \"lanes:forward\";\n\t\t\t\n\t\t\tif (laneTagsLeft != null) {\n\t\t\t\tlanesLeft = (float)laneTagsLeft.length;\n\t\t\t} else if (tags.containsKey(leftKey)) {\n\t\t\t\tlanesLeft = parseOsmDecimal(tags.getValue(leftKey), false);\n\t\t\t}\n\t\t\t\n\t\t\tint vehicleLaneCount;\n\t\t\tint vehicleLaneCountRight;\n\t\t\tint vehicleLaneCountLeft;\n\t\t\t\n\t\t\tif (lanesRight != null && lanesLeft != null) {\n\t\t\t\t\n\t\t\t\tvehicleLaneCountRight = (int)(float)lanesRight;\n\t\t\t\tvehicleLaneCountLeft = (int)(float)lanesLeft;\n\t\t\t\t\n\t\t\t\tvehicleLaneCount = vehicleLaneCountRight + vehicleLaneCountLeft;\n\t\t\t\t\n\t\t\t\t//TODO incorrect in case of center lanes\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tif (lanes == null) {\n\t\t\t\t\tvehicleLaneCount = getDefaultLanes(tags);\n\t\t\t\t} else {\n\t\t\t\t\tvehicleLaneCount = (int)(float) lanes;\n\t\t\t\t}\n\n\t\t\t\tif (lanesRight != null) {\n\t\t\t\t\t\n\t\t\t\t\tvehicleLaneCountRight = (int)(float)lanesRight;\n\t\t\t\t\tvehicleLaneCount = max(vehicleLaneCount, vehicleLaneCountRight);\n\t\t\t\t\tvehicleLaneCountLeft = vehicleLaneCount - vehicleLaneCountRight;\n\t\t\t\t\t\n\t\t\t\t} else if (lanesLeft != null) {\n\t\t\t\t\t\n\t\t\t\t\tvehicleLaneCountLeft = (int)(float)lanesLeft;\n\t\t\t\t\tvehicleLaneCount = max(vehicleLaneCount, vehicleLaneCountLeft);\n\t\t\t\t\tvehicleLaneCountRight = vehicleLaneCount - vehicleLaneCountLeft;\n\t\t\t\t\t\n\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\tvehicleLaneCountLeft = vehicleLaneCount / 2;\n\t\t\t\t\tvehicleLaneCountRight = vehicleLaneCount - vehicleLaneCountLeft;\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* create the layout */\n\t\t\t\n\t\t\tLaneLayout layout = new LaneLayout();\n\t\t\t\n\t\t\t// central divider\n\t\t\t\n\t\t\tif (vehicleLaneCountRight > 0 && vehicleLaneCountLeft > 0) {\n\t\t\t\n\t\t\t\tLaneType dividerType = DASHED_LINE;\n\t\t\t\t\n\t\t\t\tif (\"dashed_line\".equals(divider)) {\n\t\t\t\t\tdividerType = DASHED_LINE;\n\t\t\t\t} else if (\"solid_line\".equals(divider)) {\n\t\t\t\t\tdividerType = SOLID_LINE;\n\t\t\t\t} else if (\"no\".equals(divider)) {\n\t\t\t\t\tdividerType = null;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (dividerType != null) {\n\t\t\t\t\tlayout.getLanes(RoadPart.RIGHT).add(new Lane(this,\n\t\t\t\t\t\t\tdividerType, RoadPart.RIGHT, EMPTY_TAG_GROUP));\n\t\t\t\t}\n\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t// left and right road part\n\t\t\t\n\t\t\tfor (RoadPart roadPart : RoadPart.values()) {\n\t\t\t\n\t\t\t\tint lanesPart = (roadPart == RoadPart.RIGHT)\n\t\t\t\t\t\t? vehicleLaneCountRight\n\t\t\t\t\t\t: vehicleLaneCountLeft;\n\t\t\t\t\n\t\t\t\tTagGroup[] laneTags = (roadPart == RoadPart.RIGHT)\n\t\t\t\t\t\t? laneTagsRight\n\t\t\t\t\t\t: laneTagsLeft;\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i < lanesPart; ++ i) {\n\t\t\t\t\t\n\t\t\t\t\tif (i > 0) {\n\t\t\t\t\t\t\n\t\t\t\t\t\t// divider between lanes in the same direction\n\t\t\t\t\t\t\n\t\t\t\t\t\tlayout.getLanes(roadPart).add(new Lane(this,\n\t\t\t\t\t\t\t\tDASHED_LINE, roadPart, EMPTY_TAG_GROUP));\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\n\t\t\t\t\t//lane itself\n\t\t\t\t\t\n\t\t\t\t\tTagGroup tags = (laneTags != null)\n\t\t\t\t\t\t\t? laneTags[i]\n\t\t\t\t\t\t\t: EMPTY_TAG_GROUP;\n\t\t\t\t\t\n\t\t\t\t\tlayout.getLanes(roadPart).add(new Lane(this,\n\t\t\t\t\t\t\tVEHICLE_LANE, roadPart, tags));\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t//special lanes\n\t\t\t\n\t\t\tif (leftCycleway) {\n\t\t\t\tlayout.leftLanes.add(new Lane(this,\n\t\t\t\t\t\tCYCLEWAY, RoadPart.LEFT, EMPTY_TAG_GROUP));\n\t\t\t}\n\t\t\tif (rightCycleway) {\n\t\t\t\tlayout.rightLanes.add(new Lane(this,\n\t\t\t\t\t\tCYCLEWAY, RoadPart.RIGHT, EMPTY_TAG_GROUP));\n\t\t\t}\n\t\t\t\n\t\t\tif (leftSidewalk) {\n\t\t\t\tlayout.leftLanes.add(new Lane(this,\n\t\t\t\t\t\tKERB, RoadPart.LEFT, EMPTY_TAG_GROUP));\n\t\t\t\tlayout.leftLanes.add(new Lane(this,\n\t\t\t\t\t\tSIDEWALK, RoadPart.LEFT, EMPTY_TAG_GROUP));\n\t\t\t}\n\t\t\tif (rightSidewalk) {\n\t\t\t\tlayout.rightLanes.add(new Lane(this,\n\t\t\t\t\t\tKERB, RoadPart.RIGHT, EMPTY_TAG_GROUP));\n\t\t\t\tlayout.rightLanes.add(new Lane(this,\n\t\t\t\t\t\tSIDEWALK, RoadPart.RIGHT, EMPTY_TAG_GROUP));\n\t\t\t}\n\t\t\t\n\t\t\treturn layout;\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * evaluates tags using the :lanes key suffix\n\t\t * \n\t\t * @return  array with values; null if the tag isn't used\n\t\t */\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tprivate TagGroup[] getPerLaneTags(RoadPart roadPart) {\n\t\t\t\n\t\t\t/* determine which of the suffixes :lanes[:forward|:backward] matter */\n\t\t\t\n\t\t\tList<String> relevantSuffixes;\n\t\t\t\n\t\t\tif (roadPart == RoadPart.RIGHT ^ !rightHandTraffic) {\n\t\t\t\t// the forward part\n\t\t\t\t\n\t\t\t\tif (isOneway(tags)) {\n\t\t\t\t\trelevantSuffixes = asList(\":lanes\", \":lanes:forward\");\n\t\t\t\t} else {\n\t\t\t\t\trelevantSuffixes = asList(\":lanes:forward\");\n\t\t\t\t}\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t// the backward part\n\t\t\t\t\n\t\t\t\trelevantSuffixes = asList(\":lanes:backward\");\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* evaluate tags with one of the relevant suffixes */\n\t\t\t\n\t\t\tMap<String, String>[] resultMaps = null;\n\t\t\t\n\t\t\tfor (String suffix : relevantSuffixes) {\n\t\t\t\t\n\t\t\t\tfor (Tag tag : tags) {\n\t\t\t\t\tif (tag.key.endsWith(suffix)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tString baseKey = tag.key.substring(0,\n\t\t\t\t\t\t\t\ttag.key.lastIndexOf(suffix));\n\t\t\t\t\t\t\n\t\t\t\t\t\tString[] values = tag.value.split(\"\\\\|\");\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (resultMaps == null) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tresultMaps = new Map[values.length];\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tfor (int i = 0; i < resultMaps.length; i++) {\n\t\t\t\t\t\t\t\tresultMaps[i] = new HashMap<String, String>();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t} else if (values.length != resultMaps.length) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// inconsistent number of lanes\n\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tfor (int i = 0; i < values.length; i++) {\n\t\t\t\t\t\t\tresultMaps[i].put(baseKey, values[i].trim());\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* build a TagGroup for each lane from the result */\n\t\t\t\n\t\t\tif (resultMaps == null) {\n\t\t\t\treturn null;\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tTagGroup[] result = new TagGroup[resultMaps.length];\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i < resultMaps.length; i++) {\n\t\t\t\t\tresult[i] = new MapBasedTagGroup(resultMaps[i]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\treturn result;\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tprivate float calculateWidth() {\n\t\t\t\n\t\t\t// if the width of all lanes is known, use the sum as the road's width\n\t\t\t\n\t\t\tFloat sumWidth = calculateLaneBasedWidth(false, false);\n\t\t\t\n\t\t\tif (sumWidth != null) return sumWidth;\n\t\t\t\n\t\t\t// if the width of the road is explicitly tagged, use that value\n\t\t\t// (note that this has lower priority than the sum of lane widths,\n\t\t\t// to avoid errors when the two values don't match)\n\t\t\t\n\t\t\tfloat explicitWidth = parseWidth(tags, -1);\n\t\t\t\n\t\t\tif (explicitWidth != -1) return explicitWidth;\n\n\t\t\t// if there is some basic info on lanes, use that\n\t\t\t\n\t\t\tif (tags.containsKey(\"lanes\") || tags.containsKey(\"divider\")) {\n\t\t\t\t\n\t\t\t\treturn calculateLaneBasedWidth(true, false);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t// if all else fails, make a guess\n\t\t\t\n\t\t\treturn calculateLaneBasedWidth(true, true)\n\t\t\t\t\t+ estimateVehicleLanesWidth();\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * calculates the width of the road as the sum of the widths\n\t\t * of its lanes\n\t\t * \n\t\t * @param useDefaults  whether to use a default for unknown widths\n\t\t * @param ignoreVehicleLanes  ignoring full-width lanes,\n\t\t * \twhich means that only sidewalks, cycleways etc. will be counted\n\t\t * \n\t\t * @return  the estimated width, or null if a lane has unknown width\n\t\t * \tand no defaults are permitted\n\t\t */\n\t\tprivate Float calculateLaneBasedWidth(boolean useDefaults,\n\t\t\t\tboolean ignoreVehicleLanes) {\n\t\t\t\n\t\t\tfloat width = 0;\n\t\t\t\n\t\t\tfor (Lane lane : laneLayout.getLanesLeftToRight()) {\n\t\t\t\t\n\t\t\t\tif (lane.type == VEHICLE_LANE && ignoreVehicleLanes) continue;\n\t\t\t\t\n\t\t\t\tif (lane.getAbsoluteWidth() == null) {\n\t\t\t\t\tif (useDefaults) {\n\t\t\t\t\t\twidth += DEFAULT_LANE_WIDTH;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\twidth += lane.getAbsoluteWidth();\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn width;\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * calculates a rough estimate of the road's vehicle lanes' total width\n\t\t * based on road type and oneway\n\t\t */\n\t\tprivate float estimateVehicleLanesWidth() {\n\t\t\t\n\t\t\tString highwayValue = tags.getValue(\"highway\");\n\t\t\t\n\t\t\tfloat width = 0;\n\t\t\t\n\t\t\t/* guess the combined width of all vehicle lanes */\n\t\t\t\n\t\t\tif (!tags.containsKey(\"lanes\") && !tags.containsKey(\"divider\")) {\n\t\t\t\t\t\t\t\t\n\t\t\t\tif (isPath(tags)) {\n\t\t\t\t\twidth = 1f;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\telse if (\"service\".equals(highwayValue)\n\t\t\t\t\t\t|| \"track\".equals(highwayValue)) {\n\t\t\t\t\tif (tags.contains(\"service\", \"parking_aisle\")) {\n\t\t\t\t\t\twidth = DEFAULT_LANE_WIDTH * 0.8f;\n\t\t\t\t\t} else {\n\t\t\t\t\t\twidth = DEFAULT_LANE_WIDTH;\n\t\t\t\t\t}\n\t\t\t\t} else if (\"primary\".equals(highwayValue) || \"secondary\".equals(highwayValue)) {\n\t\t\t\t\twidth = 2 * DEFAULT_LANE_WIDTH;\n\t\t\t\t} else if (\"motorway\".equals(highwayValue)) {\n\t\t\t\t\twidth = 2.5f * DEFAULT_LANE_WIDTH;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\telse if (tags.containsKey(\"oneway\") && !tags.getValue(\"oneway\").equals(\"no\")) {\n\t\t\t\t\twidth = DEFAULT_LANE_WIDTH;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\telse {\n\t\t\t\t\twidth = 4;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\n\t\t\treturn width;\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\t\n\t\t\tsuper.defineEleConstraints(enforcer);\n\t\t\t\n\t\t\t/* impose sensible maximum incline (35% is \"the world's steepest residential street\") */\n\t\t\t\n\t\t\tif (!isPath(tags) && !isSteps(tags) && !tags.containsKey(\"incline\")) {\n\t\t\t\tenforcer.requireIncline(MAX, +0.35, getCenterlineEleConnectors());\n\t\t\t\tenforcer.requireIncline(MIN, -0.35, getCenterlineEleConnectors());\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic float getWidth() {\n\t\t\treturn width;\n\t\t}\n\t\t\t\t\n\t\tpublic Material getSurface() {\n\t\t\treturn getSurfaceForRoad(tags, ASPHALT);\n\t\t}\n\t\t\n\t\tpublic LaneLayout getLaneLayout() {\n\t\t\treturn laneLayout;\n\t\t}\n\t\t\n\t\tprivate void renderStepsTo(Target<?> target) {\n\t\t\t\n\t\t\tfinal VectorXZ startWithOffset = getStartPosition();\n\t\t\tfinal VectorXZ endWithOffset = getEndPosition();\n\t\t\n\t\t\tList<VectorXYZ> leftOutline = getOutline(false);\n\t\t\tList<VectorXYZ> rightOutline = getOutline(true);\n\t\t\t\n\t\t\t\n\t\t\tdouble lineLength = VectorXZ.distance (\n\t\t\t\t\tsegment.getStartNode().getPos(), segment.getEndNode().getPos());\n\t\t\t\n\t\t\t/* render ground first (so gaps between the steps look better) */\n\t\t\t\n\t\t\tList<VectorXYZ> vs = createTriangleStripBetween(\n\t\t\t\t\tleftOutline, rightOutline);\n\n\t\t\ttarget.drawTriangleStrip(ASPHALT, vs,\n\t\t\t\t\ttexCoordLists(vs, ASPHALT, GLOBAL_X_Z));\n\t\t\t\n\t\t\t/* determine the length of each individual step */\n\t\t\t\n\t\t\tfloat stepLength = 0.3f;\n\t\t\t\n\t\t\tif (tags.containsKey(\"step_count\")) {\n\t\t\t\ttry {\n\t\t\t\t\tint stepCount = Integer.parseInt(tags.getValue(\"step_count\"));\n\t\t\t\t\tstepLength = (float)lineLength / stepCount;\n\t\t\t\t} catch (NumberFormatException e) { /* don't overwrite default length */ }\n\t\t\t}\n\t\t\t\n\t\t\t/* locate the position on the line at the beginning/end of each step\n\t\t\t * (positions on the line spaced by step length),\n\t\t\t * interpolate heights between adjacent points with elevation */\n\t\t\t\n\t\t\tList<VectorXYZ> centerline = getCenterline();\n\t\t\t\n\t\t\tList<VectorXZ> stepBorderPositionsXZ =\n\t\t\t\tGeometryUtil.equallyDistributePointsAlong(\n\t\t\t\t\tstepLength, true, startWithOffset, endWithOffset);\n\t\t\t\n\t\t\tList<VectorXYZ> stepBorderPositions = new ArrayList<VectorXYZ>();\n\t\t\tfor (VectorXZ posXZ : stepBorderPositionsXZ) {\n\t\t\t\tVectorXYZ posXYZ = interpolateElevation(posXZ,\n\t\t\t\t\t\tcenterline.get(0),\n\t\t\t\t\t\tcenterline.get(centerline.size() - 1));\n\t\t\t\tstepBorderPositions.add(posXYZ);\n\t\t\t}\n\t\t\t\n\t\t\t/* draw steps */\n\t\t\t\n\t\t\tfor (int step = 0; step < stepBorderPositions.size() - 1; step++) {\n\t\t\t\t\n\t\t\t\tVectorXYZ frontCenter = stepBorderPositions.get(step);\n\t\t\t\tVectorXYZ backCenter = stepBorderPositions.get(step+1);\n\t\t\t\t\n\t\t\t\tdouble height = abs(frontCenter.y - backCenter.y);\n\t\t\t\t\t\t\t\t\n\t\t\t\tVectorXYZ center = (frontCenter.add(backCenter)).mult(0.5);\n\t\t\t\tcenter = center.subtract(Y_UNIT.mult(0.5 * height));\n\t\t\t\t\n\t\t\t\tVectorXZ faceDirection = segment.getDirection();\n\t\t\t\tif (frontCenter.y < backCenter.y) {\n\t\t\t\t\t//invert if upwards\n\t\t\t\t\tfaceDirection = faceDirection.invert();\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\ttarget.drawBox(Materials.STEPS_DEFAULT,\n\t\t\t\t\t\tcenter, faceDirection,\n\t\t\t\t\t\theight, width, backCenter.distanceToXZ(frontCenter));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* draw handrails */\n\t\t\t\n\t\t\tList<List<VectorXYZ>> handrailFootprints =\n\t\t\t\tnew ArrayList<List<VectorXYZ>>();\n\n\t\t\tif (segment.getTags().contains(\"handrail:left\",\"yes\")) {\n\t\t\t\thandrailFootprints.add(leftOutline);\n\t\t\t}\n\t\t\tif (segment.getTags().contains(\"handrail:right\",\"yes\")) {\n\t\t\t\thandrailFootprints.add(rightOutline);\n\t\t\t}\n\t\t\t\n\t\t\tint centerHandrails = 0;\n\t\t\tif (segment.getTags().contains(\"handrail:center\",\"yes\")) {\n\t\t\t\tcenterHandrails = 1;\n\t\t\t} else if (segment.getTags().containsKey(\"handrail:center\")) {\n\t\t\t\ttry {\n\t\t\t\t\tcenterHandrails = Integer.parseInt(\n\t\t\t\t\t\t\tsegment.getTags().getValue(\"handrail:center\"));\n\t\t\t\t} catch (NumberFormatException e) {}\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t\tfor (int i = 0; i < centerHandrails; i++) {\n\t\t\t\thandrailFootprints.add(createLineBetween(\n\t\t\t\t\t\tleftOutline, rightOutline,\n\t\t\t\t\t\t(i + 1.0f) / (centerHandrails + 1)));\n\t\t\t}\n\t\t\t\n\t\t\tfor (List<VectorXYZ> handrailFootprint : handrailFootprints) {\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> handrailLine = new ArrayList<VectorXYZ>();\n\t\t\t\tfor (VectorXYZ v : handrailFootprint) {\n\t\t\t\t\thandrailLine.add(v.y(v.y + 1));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tList<List<VectorXYZ>> strips = createShapeExtrusionAlong(\n\t\t\t\t\tHANDRAIL_SHAPE, handrailLine,\n\t\t\t\t\tCollections.nCopies(handrailLine.size(), VectorXYZ.Y_UNIT));\n\t\t\t\t\n\t\t\t\tfor (List<VectorXYZ> strip : strips) {\n\t\t\t\t\ttarget.drawTriangleStrip(HANDRAIL_DEFAULT, strip,\n\t\t\t\t\t\t\ttexCoordLists(strip, HANDRAIL_DEFAULT, STRIP_WALL));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\ttarget.drawColumn(HANDRAIL_DEFAULT, 4,\n\t\t\t\t\t\thandrailFootprint.get(0),\n\t\t\t\t\t\t1, 0.03, 0.03, false, true);\n\t\t\t\ttarget.drawColumn(HANDRAIL_DEFAULT, 4,\n\t\t\t\t\t\thandrailFootprint.get(handrailFootprint.size()-1),\n\t\t\t\t\t\t1, 0.03, 0.03, false, true);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tprivate void renderLanesTo(Target<?> target) {\n\n\t\t\tList<Lane> lanesLeftToRight = laneLayout.getLanesLeftToRight();\n\t\t\t\n\t\t\t/* draw lanes themselves */\n\t\t\t\n\t\t\tfor (Lane lane : lanesLeftToRight) {\n\t\t\t\tlane.renderTo(target);\n\t\t\t}\n\t\t\t\n\t\t\t/* close height gaps at left and right border of the road */\n\t\t\t\n\t\t\tLane firstLane = lanesLeftToRight.get(0);\n\t\t\tLane lastLane = lanesLeftToRight.get(lanesLeftToRight.size() - 1);\n\t\t\t\n\t\t\tif (firstLane.getHeightAboveRoad() > 0) {\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> vs = createTriangleStripBetween(\n\t\t\t\t\t\tgetOutline(false),\n\t\t\t\t\t\taddYList(getOutline(false), firstLane.getHeightAboveRoad()));\n\t\t\t\t\n\t\t\t\ttarget.drawTriangleStrip(getSurface(), vs,\n\t\t\t\t\t\ttexCoordLists(vs, getSurface(), STRIP_WALL));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (lastLane.getHeightAboveRoad() > 0) {\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> vs = createTriangleStripBetween(\n\t\t\t\t\t\taddYList(getOutline(true), lastLane.getHeightAboveRoad()),\n\t\t\t\t\t\tgetOutline(true));\n\t\t\t\t\n\t\t\t\ttarget.drawTriangleStrip(getSurface(), vs,\n\t\t\t\t\t\ttexCoordLists(vs, getSurface(), STRIP_WALL));\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\n\t\t\tif (steps) {\n\t\t\t\trenderStepsTo(target);\n\t\t\t} else {\n\t\t\t\trenderLanesTo(target);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class RoadArea extends NetworkAreaWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\n\t\tprivate static final float DEFAULT_CLEARING = 5f;\n\t\t\n\t\tpublic RoadArea(MapArea area) {\n\t\t\tsuper(area);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tString surface = area.getTags().getValue(\"surface\");\n\t\t\tMaterial material = getSurfaceMaterial(surface, ASPHALT);\n\t\t\tCollection<TriangleXYZ> triangles = getTriangulation();\n\t\t\t\n\t\t\ttarget.drawTriangles(material, triangles,\n\t\t\t\t\ttriangleTexCoordLists(triangles, material, GLOBAL_X_Z));\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\tif (BridgeModule.isBridge(area.getTags())) {\n\t\t\t\treturn GroundState.ABOVE;\n\t\t\t} else if (TunnelModule.isTunnel(area.getTags())) {\n\t\t\t\treturn GroundState.BELOW;\n\t\t\t} else {\n\t\t\t\treturn GroundState.ON;\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static enum RoadPart {\n\t\tLEFT, RIGHT\n\t\t//TODO add CENTRE lane support\n\t}\n\t\n\tprivate static class LaneLayout {\n\t\n\t\tpublic final List<Lane> leftLanes = new ArrayList<Lane>();\n\t\tpublic final List<Lane> rightLanes = new ArrayList<Lane>();\n\t\t\t\t\n\t\tpublic List<Lane> getLanes(RoadPart roadPart) {\n\t\t\tswitch (roadPart) {\n\t\t\tcase LEFT: return leftLanes;\n\t\t\tcase RIGHT: return rightLanes;\n\t\t\tdefault: throw new Error(\"unhandled road part value\");\n\t\t\t}\n\t\t}\n\t\t\n\t\tpublic List<Lane> getLanesLeftToRight() {\n\t\t\tList<Lane> result = new ArrayList<Lane>();\n\t\t\tresult.addAll(leftLanes);\n\t\t\tCollections.reverse(result);\n\t\t\tresult.addAll(rightLanes);\n\t\t\treturn result;\n\t\t}\n\t\t\n\t\t/**\n\t\t * calculates and sets all lane attributes\n\t\t * that are not known during lane creation\n\t\t */\n\t\tpublic void setCalculatedValues(double totalRoadWidth) {\n\t\t\t\n\t\t\t/* determine width of lanes without explicitly assigned width */\n\t\t\t\n\t\t\tint lanesWithImplicitWidth = 0;\n\t\t\tdouble remainingWidth = totalRoadWidth;\n\t\t\t\n\t\t\tfor (RoadPart part : RoadPart.values()) {\n\t\t\t\tfor (Lane lane : getLanes(part)) {\n\t\t\t\t\tif (lane.getAbsoluteWidth() == null) {\n\t\t\t\t\t\tlanesWithImplicitWidth += 1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tremainingWidth -= lane.getAbsoluteWidth();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tdouble implicitLaneWidth = remainingWidth / lanesWithImplicitWidth;\n\t\t\t\n\t\t\t/* calculate a factor to reduce all lanes' width\n\t\t\t * if the sum of their widths would otherwise\n\t\t\t * be larger than that of the road */\n\t\t\t\n\t\t\tdouble laneWidthScaling = 1.0;\n\t\t\t\n\t\t\tif (remainingWidth < 0) {\n\t\t\t\t\n\t\t\t\tdouble widthSum = totalRoadWidth - remainingWidth;\n\t\t\t\t\n\t\t\t\timplicitLaneWidth = 1;\n\t\t\t\twidthSum += lanesWithImplicitWidth * implicitLaneWidth;\n\t\t\t\t\n\t\t\t\tlaneWidthScaling = totalRoadWidth / widthSum;\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* assign values */\n\t\t\t\n\t\t\tfor (RoadPart part : asList(RoadPart.LEFT, RoadPart.RIGHT)) {\n\t\t\t\t\t\t\t\n\t\t\t\tdouble heightAboveRoad = 0;\n\t\t\t\t\n\t\t\t\tfor (Lane lane : getLanes(part)) {\n\t\t\t\t\t\n\t\t\t\t\tdouble relativeWidth;\n\t\t\t\t\t\n\t\t\t\t\tif (lane.getAbsoluteWidth() == null) {\n\t\t\t\t\t\trelativeWidth = laneWidthScaling *\n\t\t\t\t\t\t\t\t(implicitLaneWidth / totalRoadWidth);\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativeWidth = laneWidthScaling *\n\t\t\t\t\t\t\t\t(lane.getAbsoluteWidth() / totalRoadWidth);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tlane.setCalculatedValues1(relativeWidth, heightAboveRoad);\n\t\t\t\t\t\n\t\t\t\t\theightAboveRoad += lane.getHeightOffset();\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* calculate relative lane positions based on relative width */\n\t\t\t\n\t\t\tdouble accumulatedWidth = 0;\n\t\t\t\n\t\t\tfor (Lane lane : getLanesLeftToRight()) {\n\t\t\t\t\n\t\t\t\tdouble relativePositionLeft = accumulatedWidth;\n\t\t\t\t\n\t\t\t\taccumulatedWidth += lane.getRelativeWidth();\n\t\t\t\t\n\t\t\t\tdouble relativePositionRight = accumulatedWidth;\n\t\t\t\t\n\t\t\t\tif (relativePositionRight > 1) { //avoids precision problems\n\t\t\t\t\trelativePositionRight = 1;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tlane.setCalculatedValues2(relativePositionLeft,\n\t\t\t\t\t\trelativePositionRight);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * calculates and sets all lane attributes\n\t\t * that are not known during lane creation\n\t\t */\n\t\t\n\t\t\n\t}\n\t\n\t/**\n\t * a lane or lane divider of the road segment.\n\t * \n\t * Field values depend on neighboring lanes and are therefore calculated\n\t * and defined in two phases. Results are then set using\n\t * {@link #setCalculatedValues1(double, double)} and\n\t * {@link #setCalculatedValues2(double, double)}, respectively.\n\t */\n\tprivate static final class Lane implements RenderableToAllTargets {\n\t\t\n\t\tpublic final Road road;\n\t\tpublic final LaneType type;\n\t\tpublic final RoadPart roadPart;\n\t\tpublic final TagGroup laneTags;\n\t\t\n\t\tprivate int phase = 0;\n\t\t\n\t\tprivate double relativeWidth;\n\t\tprivate double heightAboveRoad;\n\t\t\n\t\tprivate double relativePositionLeft;\n\t\tprivate double relativePositionRight;\n\t\t\t\t\n\t\tpublic Lane(Road road, LaneType type, RoadPart roadPart,\n\t\t\t\tTagGroup laneTags) {\n\t\t\tthis.road = road;\n\t\t\tthis.type = type;\n\t\t\tthis.roadPart = roadPart;\n\t\t\tthis.laneTags = laneTags;\n\t\t}\n\n\t\t/** returns width in meters or null for undefined width */\n\t\tpublic Double getAbsoluteWidth() {\n\t\t\treturn type.getAbsoluteWidth(road.tags, laneTags);\n\t\t}\n\n\t\t/** returns height increase relative to inner neighbor */\n\t\tpublic double getHeightOffset() {\n\t\t\treturn type.getHeightOffset(road.tags, laneTags);\n\t\t}\n\t\t\n\t\tpublic void setCalculatedValues1(double relativeWidth,\n\t\t\t\tdouble heightAboveRoad) {\n\t\t\t\n\t\t\tassert phase == 0;\n\t\t\t\n\t\t\tthis.relativeWidth = relativeWidth;\n\t\t\tthis.heightAboveRoad = heightAboveRoad;\n\t\t\t\n\t\t\tphase = 1;\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic void setCalculatedValues2(double relativePositionLeft,\n\t\t\t\tdouble relativePositionRight) {\n\t\t\t\n\t\t\tassert phase == 1;\n\t\t\t\n\t\t\tthis.relativePositionLeft = relativePositionLeft;\n\t\t\tthis.relativePositionRight = relativePositionRight;\n\t\t\t\n\t\t\tphase = 2;\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic Double getRelativeWidth() {\n\t\t\tassert phase > 0;\n\t\t\treturn relativeWidth;\n\t\t}\n\t\t\n\t\tpublic double getHeightAboveRoad() {\n\t\t\tassert phase > 0;\n\t\t\treturn heightAboveRoad;\n\t\t}\n\t\t\n\t\t/**\n\t\t * provides access to the first and last node\n\t\t * of the lane's left and right border\n\t\t */\n\t\tpublic VectorXYZ getBorderNode(boolean start, boolean right) {\n\t\t\t\n\t\t\tassert phase > 1;\n\t\t\t\n\t\t\tdouble relativePosition = right\n\t\t\t\t\t? relativePositionRight\n\t\t\t\t\t: relativePositionLeft;\n\t\t\t\n\t\t\tif (relativePosition < 0 || relativePosition > 1) {\n\t\t\t\tSystem.out.println(\"PROBLEM\");\n\t\t\t}\n\t\t\t\n\t\t\tVectorXYZ roadPoint = road.getPointOnCut(start, relativePosition);\n\t\t\t\n\t\t\treturn roadPoint.add(0, getHeightAboveRoad(), 0);\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tassert phase > 1;\n\t\t\t\n\t\t\tif (road.isBroken()) return;\n\t\t\t\n\t\t\tList<VectorXYZ> leftLaneBorder = createLineBetween(\n\t\t\t\t\troad.getOutline(false), road.getOutline(true),\n\t\t\t\t\t(float)relativePositionLeft);\n\t\t\tleftLaneBorder = addYList(leftLaneBorder, getHeightAboveRoad());\n\t\t\t\n\t\t\tList<VectorXYZ> rightLaneBorder = createLineBetween(\n\t\t\t\t\troad.getOutline(false), road.getOutline(true),\n\t\t\t\t\t(float)relativePositionRight);\n\t\t\trightLaneBorder = addYList(rightLaneBorder, getHeightAboveRoad());\n\t\t\t\n\t\t\ttype.render(target, roadPart, road.rightHandTraffic,\n\t\t\t\t\troad.tags, laneTags, leftLaneBorder, rightLaneBorder);\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn \"{\" + type + \", \" + roadPart + \"}\";\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * a connection between two lanes (e.g. at a junction)\n\t */\n\tprivate static class LaneConnection implements RenderableToAllTargets {\n\t\t\n\t\tpublic final LaneType type;\n\t\tpublic final RoadPart roadPart;\n\t\tpublic final boolean rightHandTraffic;\n\t\t\n\t\tprivate final List<VectorXYZ> leftBorder;\n\t\tprivate final List<VectorXYZ> rightBorder;\n\t\t\n\t\tprivate LaneConnection(LaneType type, RoadPart roadPart,\n\t\t\t\tboolean rightHandTraffic,\n\t\t\t\tList<VectorXYZ> leftBorder, List<VectorXYZ> rightBorder) {\n\t\t\tthis.type = type;\n\t\t\tthis.roadPart = roadPart;\n\t\t\tthis.rightHandTraffic = rightHandTraffic;\n\t\t\tthis.leftBorder = leftBorder;\n\t\t\tthis.rightBorder = rightBorder;\n\t\t}\n\n\t\t/**\n\t\t * returns the outline of this connection.\n\t\t * For determining the total terrain covered by junctions and connectors.\n\t\t */\n\t\tpublic PolygonXYZ getOutline() {\n\t\t\t\n\t\t\tList<VectorXYZ> outline = new ArrayList<VectorXYZ>();\n\n\t\t\toutline.addAll(leftBorder);\n\t\t\t\n\t\t\tList<VectorXYZ>rOutline = new ArrayList<VectorXYZ>(rightBorder);\n\t\t\tCollections.reverse(rOutline);\n\t\t\toutline.addAll(rOutline);\n\t\t\t\n\t\t\toutline.add(outline.get(0));\n\t\t\t\n\t\t\treturn new PolygonXYZ(outline);\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\ttype.render(target, roadPart, rightHandTraffic,\n\t\t\t\t\tEMPTY_TAG_GROUP, EMPTY_TAG_GROUP, leftBorder, rightBorder);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * a type of lanes. Determines visual appearance,\n\t * and contains the intelligence for evaluating type-specific tags.\n\t */\n\tprivate static abstract class LaneType {\n\t\t\n\t\tprivate final String typeName;\n\t\tpublic final boolean isConnectableAtCrossings;\n\t\tpublic final boolean isConnectableAtJunctions;\n\t\t\t\t\n\t\tprivate LaneType(String typeName,\n\t\t\t\tboolean isConnectableAtCrossings,\n\t\t\t\tboolean isConnectableAtJunctions) {\n\t\t\t\n\t\t\tthis.typeName = typeName;\n\t\t\tthis.isConnectableAtCrossings = isConnectableAtCrossings;\n\t\t\tthis.isConnectableAtJunctions = isConnectableAtJunctions;\n\t\t\t\n\t\t}\n\n\t\tpublic abstract void render(Target<?> target, RoadPart roadPart,\n\t\t\t\tboolean rightHandTraffic,\n\t\t\t\tTagGroup roadTags, TagGroup laneTags,\n\t\t\t\tList<VectorXYZ> leftLaneBorder,\n\t\t\t\tList<VectorXYZ> rightLaneBorder);\n\t\t\n\t\tpublic abstract Double getAbsoluteWidth(\n\t\t\t\tTagGroup roadTags, TagGroup laneTags);\n\n\t\tpublic abstract double getHeightOffset(\n\t\t\t\tTagGroup roadTags, TagGroup laneTags);\n\t\n\t\t@Override\n\t\tpublic String toString() {\n\t\t\treturn typeName;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static abstract class FlatTexturedLane extends LaneType {\n\t\t\t\t\n\t\tprivate FlatTexturedLane(String typeName,\n\t\t\t\tboolean isConnectableAtCrossings,\n\t\t\t\tboolean isConnectableAtJunctions) {\n\t\t\t\n\t\t\tsuper(typeName, isConnectableAtCrossings, isConnectableAtJunctions);\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void render(Target<?> target, RoadPart roadPart,\n\t\t\t\tboolean rightHandTraffic,\n\t\t\t\tTagGroup roadTags, TagGroup laneTags,\n\t\t\t\tList<VectorXYZ> leftLaneBorder,\n\t\t\t\tList<VectorXYZ> rightLaneBorder) {\n\t\t\t\n\t\t\tMaterial surface = getSurface(roadTags, laneTags);\n\t\t\tMaterial surfaceMiddle = getSurfaceMiddle(roadTags, laneTags);\n\t\t\t\t\t\t\n\t\t\t/* draw lane triangle strips */\n\t\t\t\n\t\t\tif (surfaceMiddle == null || surfaceMiddle.equals(surface)) {\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> vs = createTriangleStripBetween(\n\t\t\t\t\t\tleftLaneBorder, rightLaneBorder);\n\t\t\t\t\n\t\t\t\tboolean mirrorLeftRight = laneTags.containsKey(\"turn\")\n\t\t\t\t\t\t&& laneTags.getValue(\"turn\").contains(\"left\");\n\t\t\t\t\n\t\t\t\t\n\t\t\t\tif (!roadTags.contains(\"highway\", \"motorway\")) {\n\t\t\t\t\tsurface = addTurnArrows(surface, laneTags);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\ttarget.drawTriangleStrip(surface, vs,\n\t\t\t\t\t\ttexCoordLists(vs, surface, new ArrowTexCoordFunction(\n\t\t\t\t\t\t\t\troadPart, rightHandTraffic, mirrorLeftRight)));\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> leftMiddleBorder =\n\t\t\t\t\tcreateLineBetween(leftLaneBorder, rightLaneBorder, 0.3f);\n\t\t\t\tList<VectorXYZ> rightMiddleBorder =\n\t\t\t\t\tcreateLineBetween(leftLaneBorder, rightLaneBorder, 0.7f);\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> vsLeft = createTriangleStripBetween(\n\t\t\t\t\t\tleftLaneBorder, leftMiddleBorder);\n\t\t\t\tList<VectorXYZ> vsMiddle = createTriangleStripBetween(\n\t\t\t\t\t\tleftMiddleBorder, rightMiddleBorder);\n\t\t\t\tList<VectorXYZ> vsRight = createTriangleStripBetween(\n\t\t\t\t\t\trightMiddleBorder, rightLaneBorder);\n\t\t\t\t\n\t\t\t\ttarget.drawTriangleStrip(surface, vsLeft,\n\t\t\t\t\t\ttexCoordLists(vsLeft, surface, GLOBAL_X_Z));\n\t\t\t\ttarget.drawTriangleStrip(surfaceMiddle, vsMiddle,\n\t\t\t\t\t\ttexCoordLists(vsMiddle, surfaceMiddle, GLOBAL_X_Z));\n\t\t\t\ttarget.drawTriangleStrip(surface, vsRight,\n\t\t\t\t\t\ttexCoordLists(vsRight, surface, GLOBAL_X_Z));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic double getHeightOffset(TagGroup roadTags, TagGroup laneTags) {\n\t\t\treturn 0;\n\t\t}\n\t\t\n\t\tprotected Material getSurface(TagGroup roadTags, TagGroup laneTags) {\n\t\t\t\n\t\t\treturn getSurfaceMaterial(laneTags.getValue(\"surface\"),\n\t\t\t\t\tgetSurfaceForRoad(roadTags, ASPHALT));\n\t\t\t\n\t\t}\n\t\t\n\t\tprotected Material getSurfaceMiddle(TagGroup roadTags, TagGroup laneTags) {\n\t\t\t\n\t\t\treturn getSurfaceMaterial(laneTags.getValue(\"surface:middle\"),\n\t\t\t\t\tgetSurfaceMiddleForRoad(roadTags, null));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final LaneType VEHICLE_LANE = new FlatTexturedLane(\n\t\t\t\"VEHICLE_LANE\", false, false) {\n\t\t\n\t\tpublic Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) {\n\t\t\t\n\t\t\tdouble width = parseWidth(laneTags, -1);\n\t\t\t\n\t\t\tif (width == -1) {\n\t\t\t\treturn null;\n\t\t\t} else {\n\t\t\t\treturn width;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t};\n\t\n\tprivate static final LaneType CYCLEWAY = new FlatTexturedLane(\n\t\t\t\"CYCLEWAY\", false, false) {\n\t\t\n\t\tpublic Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) {\n\t\t\treturn (double)parseWidth(laneTags, 0.5f);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected Material getSurface(TagGroup roadTags, TagGroup laneTags) {\n\t\t\tMaterial material = super.getSurface(roadTags, laneTags);\n\t\t\tif (material == ASPHALT) return RED_ROAD_MARKING;\n\t\t\telse return material;\n\t\t}\n\t\t\n\t};\n\t\n\tprivate static final LaneType SIDEWALK = new FlatTexturedLane(\n\t\t\t\"SIDEWALK\", true, true) {\n\t\t\t\t\n\t\tpublic Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) {\n\t\t\treturn (double)parseWidth(laneTags, 1.0f);\n\t\t}\n\t\t\n\t};\n\n\tprivate static final LaneType SOLID_LINE = new FlatTexturedLane(\n\t\t\t\"SOLID_LINE\", false, false) {\n\n\t\t@Override\n\t\tpublic Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) {\n\t\t\treturn (double)parseWidth(laneTags, 0.1f);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected Material getSurface(TagGroup roadTags, TagGroup laneTags) {\n\t\t\treturn ROAD_MARKING;\n\t\t}\n\t\t\n\t};\n\n\tprivate static final LaneType DASHED_LINE = new FlatTexturedLane(\n\t\t\t\"DASHED_LINE\", false, false) {\n\n\t\t@Override\n\t\tpublic Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) {\n\t\t\treturn (double)parseWidth(laneTags, 0.1f);\n\t\t}\n\t\t\n\t\t@Override\n\t\tprotected Material getSurface(TagGroup roadTags, TagGroup laneTags) {\n\t\t\treturn ROAD_MARKING_DASHED;\n\t\t}\n\t\t\n\t};\n\t\n\tprivate static final LaneType KERB = new LaneType(\n\t\t\t\"KERB\", true, true) {\n\t\t\n\t\t@Override\n\t\tpublic void render(Target<?> target, RoadPart roadPart,\n\t\t\t\tboolean rightHandTraffic, TagGroup roadTags, TagGroup laneTags,\n\t\t\t\tList<VectorXYZ> leftLaneBorder,\n\t\t\t\tList<VectorXYZ> rightLaneBorder) {\n\n\t\t\tList<VectorXYZ> border1, border2, border3;\n\t\t\tdouble height = getHeightOffset(roadTags, laneTags);\n\t\t\t\n\t\t\tif (roadPart == RoadPart.LEFT) {\n\t\t\t\tborder1 = addYList(leftLaneBorder, height);\n\t\t\t\tborder2 = addYList(rightLaneBorder, height);\n\t\t\t\tborder3 = rightLaneBorder;\n\t\t\t} else {\n\t\t\t\tborder1 = leftLaneBorder;\n\t\t\t\tborder2 = addYList(leftLaneBorder, height);\n\t\t\t\tborder3 = addYList(rightLaneBorder, height);\n\t\t\t}\n\n\t\t\tList<VectorXYZ> vs1_2 = createTriangleStripBetween(\n\t\t\t\t\tborder1, border2);\n\t\t\ttarget.drawTriangleStrip(Materials.KERB, vs1_2,\n\t\t\t\t\ttexCoordLists(vs1_2, Materials.KERB, STRIP_FIT_HEIGHT));\n\n\t\t\tList<VectorXYZ> vs2_3 = createTriangleStripBetween(\n\t\t\t\t\tborder2, border3);\n\t\t\ttarget.drawTriangleStrip(Materials.KERB, vs2_3,\n\t\t\t\t\ttexCoordLists(vs2_3, Materials.KERB, STRIP_FIT_HEIGHT));\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) {\n\t\t\treturn (double)parseWidth(laneTags, 0.15f);\n\t\t}\n\n\t\t@Override\n\t\tpublic double getHeightOffset(TagGroup roadTags, TagGroup laneTags) {\n\t\t\treturn (double)parseHeight(laneTags, 0.12f);\n\t\t}\n\n\t};\n\t\n\t/**\n\t * adds a texture layer for turn arrows (if any) to a material\n\t * \n\t * @return  a material based on the input, possibly with added turn arrows\n\t */\n\tprivate static Material addTurnArrows(Material material,\n\t\t\tTagGroup laneTags) {\n\t\t\n\t\tMaterial arrowMaterial = null;\n\t\t\n\t\t/* find the right material  */\n\t\t\n\t\tString turn = laneTags.getValue(\"turn\");\n\t\t\n\t\tif (turn != null) {\n\t\t\t\n\t\t\tif (turn.contains(\"through\") && turn.contains(\"right\")) {\n\t\t\t\t\n\t\t\t\tarrowMaterial = ROAD_MARKING_ARROW_THROUGH_RIGHT;\n\t\t\t\t\n\t\t\t} else if (turn.contains(\"through\") && turn.contains(\"left\")) {\n\t\t\t\t\n\t\t\t\tarrowMaterial = ROAD_MARKING_ARROW_THROUGH_RIGHT;\n\t\t\t\t\n\t\t\t} else if (turn.contains(\"through\")) {\n\t\t\t\t\n\t\t\t\tarrowMaterial = ROAD_MARKING_ARROW_THROUGH;\n\t\t\t\t\n\t\t\t} else if (turn.contains(\"right\") && turn.contains(\"left\")) {\n\t\t\t\t\n\t\t\t\tarrowMaterial = ROAD_MARKING_ARROW_RIGHT_LEFT;\n\t\t\t\t\n\t\t\t} else if (turn.contains(\"right\")) {\n\t\t\t\t\n\t\t\t\tarrowMaterial = ROAD_MARKING_ARROW_RIGHT;\n\t\t\t\t\n\t\t\t} else if (turn.contains(\"left\")) {\n\t\t\t\t\n\t\t\t\tarrowMaterial = ROAD_MARKING_ARROW_RIGHT;\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* apply the results */\n\t\t\n\t\tif (arrowMaterial != null) {\n\t\t\tmaterial = material.withAddedLayers(arrowMaterial.getTextureDataList());\n\t\t}\n\t\t\n\t\treturn material;\n\t\t\n\t}\n\n\t/**\n\t * a texture coordinate function for arrow road markings on turn lanes.\n\t * Has special features including centering the arrow, placing it at an\n\t * offset from the end of the road, and taking available space into account.\n\t * \n\t * To reduce the number of necessary textures, it uses mirrored versions of\n\t * the various right-pointing arrows for left-pointing arrows.\n\t */\n\tprivate static class ArrowTexCoordFunction implements TexCoordFunction {\n\t\t\n\t\tprivate final RoadPart roadPart;\n\t\tprivate final boolean rightHandTraffic;\n\t\tprivate final boolean mirrorLeftRight;\n\t\t\t\t\n\t\tprivate ArrowTexCoordFunction(RoadPart roadPart,\n\t\t\t\tboolean rightHandTraffic, boolean mirrorLeftRight) {\n\t\t\t\n\t\t\tthis.roadPart = roadPart;\n\t\t\tthis.rightHandTraffic = rightHandTraffic;\n\t\t\tthis.mirrorLeftRight = mirrorLeftRight;\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic List<VectorXZ> apply(List<VectorXYZ> vs, TextureData textureData) {\n\t\t\t\n\t\t\tif (vs.size() % 2 == 1) {\n\t\t\t\tthrow new IllegalArgumentException(\"not a triangle strip lane\");\n\t\t\t}\n\t\t\t\n\t\t\tList<VectorXZ> result = new ArrayList<VectorXZ>(vs.size());\n\t\t\t\n\t\t\tboolean forward = roadPart == RoadPart.LEFT ^ rightHandTraffic;\n\t\t\t\n\t\t\t/* calculate length of the lane */\n\t\t\t\n\t\t\tdouble totalLength = 0;\n\t\t\t\n\t\t\tfor (int i = 0; i+2 < vs.size(); i += 2) {\n\t\t\t\ttotalLength += vs.get(i).distanceToXZ(vs.get(i+2));\n\t\t\t}\n\t\t\t\n\t\t\t/* calculate texture coordinate list */\n\t\t\t\n\t\t\tdouble accumulatedLength = forward ? totalLength : 0;\n\t\t\t\n\t\t\tfor (int i = 0; i < vs.size(); i++) {\n\t\t\t\t\n\t\t\t\tVectorXYZ v = vs.get(i);\n\t\t\t\t\n\t\t\t\t// increase accumulated length after every second vector\n\t\t\t\t\n\t\t\t\tif (i > 0 && i % 2 == 0) {\n\t\t\t\t\t\n\t\t\t\t\tdouble segmentLength = v.xz().distanceTo(vs.get(i-2).xz());\n\t\t\t\t\t\n\t\t\t\t\tif (forward) {\n\t\t\t\t\t\taccumulatedLength -= segmentLength;\n\t\t\t\t\t} else {\n\t\t\t\t\t\taccumulatedLength += segmentLength;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// determine width of the lane at that point\n\t\t\t\t\n\t\t\t\tdouble width = (i % 2 == 0)\n\t\t\t\t\t\t? v.distanceTo(vs.get(i+1))\n\t\t\t\t\t\t: v.distanceTo(vs.get(i-1));\n\t\t\t\t\n\t\t\t\t// determine whether this vertex should get the higher or\n\t\t\t\t// lower t coordinate from the vertex pair\n\t\t\t\t\n\t\t\t\tboolean higher = i % 2 == 0;\n\t\t\t\t\n\t\t\t\tif (!forward) {\n\t\t\t\t\thigher = !higher;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (mirrorLeftRight) {\n\t\t\t\t\thigher = !higher;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// calculate texture coords\n\t\t\t\t\n\t\t\t\tdouble s, t;\n\t\t\t\t\n\t\t\t\ts = accumulatedLength / textureData.width;\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\tif (width > textureData.height) {\n\t\t\t\t\tdouble padding = ((width / textureData.height) - 1)  / 2;\n\t\t\t\t\tt = higher ? 0 - padding : 1 + padding;\n\t\t\t\t} else {\n\t\t\t\t\tt = higher ? 0 : 1;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tresult.add(new VectorXZ(s, t));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn result;\n\t\t\t\n\t\t}\n\t\t\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/StreetFurnitureModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.lang.Math.*;\nimport static java.util.Arrays.asList;\nimport static org.osm2world.core.target.common.material.Materials.*;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.*;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.texCoordLists;\nimport static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*;\n\nimport java.awt.Color;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.ImmutableMaterial;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Interpolation;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.NoOutlineNodeWorldObject;\nimport org.osm2world.core.world.modules.common.AbstractModule;\n\n/**\n * adds various types of street furniture to the world\n */\npublic class StreetFurnitureModule extends AbstractModule {\n\t\n\t@Override\n\tprotected void applyToNode(MapNode node) {\n\t\tif (node.getTags().contains(\"man_made\", \"flagpole\")) {\n\t\t\tnode.addRepresentation(new Flagpole(node));\n\t\t}\n\t\tif (node.getTags().contains(\"advertising\", \"column\")) {\n\t\t\tnode.addRepresentation(new AdvertisingColumn(node));\n\t\t}\n\t\tif (node.getTags().contains(\"advertising\", \"billboard\")) {\n\t\t\tnode.addRepresentation(new Billboard(node));\n\t\t}\n\t\tif (node.getTags().contains(\"amenity\", \"bench\")) {\n\t\t\tnode.addRepresentation(new Bench(node));\n\t\t}\n\t\tif (node.getTags().contains(\"highway\", \"bus_stop\")\n\t\t\t\t|| node.getTags().contains(\"public_transport\", \"platform\")\n\t\t\t\t&& node.getTags().contains(\"bus\", \"yes\")) {\n\t\t\tnode.addRepresentation(new BusStop(node));\n\t\t}\n\t\tif (node.getTags().contains(\"man_made\", \"cross\")\n\t\t\t\t|| node.getTags().contains(\"summit:cross\", \"yes\")\n\t\t\t\t|| node.getTags().contains(\"historic\", \"wayside_cross\")) {\n\t\t\tnode.addRepresentation(new Cross(node));\n\t\t}\n\t\tif (node.getTags().contains(\"amenity\", \"waste_basket\")) {\n\t\t\tnode.addRepresentation(new WasteBasket(node));\n\t\t}\n\t\tif (node.getTags().contains(\"amenity\", \"grit_bin\")) {\n\t\t\tnode.addRepresentation(new GritBin(node));\n\t\t}\n\t\tif (node.getTags().contains(\"amenity\", \"post_box\")\n\t\t\t&& (node.getTags().containsAnyKey(asList(\"operator\", \"brand\")))) {\n\t\t\tnode.addRepresentation(new PostBox(node));\n\t\t}\n\t\tif (node.getTags().contains(\"amenity\", \"telephone\")\n\t\t\t&& (node.getTags().containsAnyKey(asList(\"operator\", \"brand\")))) {\n\t\t\tnode.addRepresentation(new Phone(node));\n\t\t}\n\t\tif (node.getTags().contains(\"amenity\", \"vending_machine\")\n\t\t\t&& (node.getTags().containsAny(\"vending\", asList(\"parcel_pickup;parcel_mail_in\", \"parcel_mail_in\") ))) {\n\t\t\tnode.addRepresentation(new ParcelMachine(node));\n\t\t}\n\t\tif (node.getTags().contains(\"amenity\", \"vending_machine\")\n\t\t\t\t&& (node.getTags().containsAny(\"vending\", asList(\"bicycle_tube\", \"cigarettes\", \"condoms\")))) {\n\t\t\t\tnode.addRepresentation(new VendingMachineVice(node));\n\t\t}\n\t\tif (node.getTags().contains(\"amenity\", \"recycling\")\n\t\t\t\t&& (node.getTags().contains(\"recycling_type\", \"container\"))) {\n\t\t\t\tnode.addRepresentation(new RecyclingContainer(node));\n\t\t}\n\t\tif (node.getTags().contains(\"emergency\", \"fire_hydrant\")\n\t\t\t&& node.getTags().contains(\"fire_hydrant:type\", \"pillar\")) {\n\t\t\tnode.addRepresentation(new FireHydrant(node));\n\t\t}\n\t\tif (node.getTags().contains(\"highway\", \"street_lamp\")) {\n\t\t\tnode.addRepresentation(new StreetLamp(node));\n\t\t}\n\t\tif (node.getTags().contains(\"tourism\", \"information\")\n\t\t\t\t&& node.getTags().contains(\"information\", \"board\")) {\n\t\t\t\tnode.addRepresentation(new Board(node));\n\t\t}\n\t}\n\t\n\tprivate static boolean isInWall(MapNode node){\n\t\tif (node.getAdjacentAreas().size()>0){\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\t\n\tprivate static boolean isInHighway(MapNode node){\n\t\tif (node.getConnectedWaySegments().size()>0){\n\t\t\tfor(MapWaySegment way: node.getConnectedWaySegments()){\n\t\t\t\tif( way.getTags().containsKey(\"highway\") && !way.getTags().containsAny(\"highway\", asList(\"path\", \"footway\", \"platform\") ) ){\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\tprivate static final class Flagpole extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic Flagpole(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\ttarget.drawColumn(STEEL, null, getBase(),\n\t\t\t\t\tparseHeight(node.getTags(), 10f),\n\t\t\t\t\t0.15, 0.15, false, true);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class AdvertisingColumn extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic AdvertisingColumn(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tfloat height = parseHeight(node.getTags(), 3f);\n\t\t\t\n\t\t\t/* draw socket, poster and cap */\n\t\t\t\n\t\t\ttarget.drawColumn(CONCRETE, null,\n\t\t\t\t\tgetBase(),\n\t\t\t\t\t0.15 * height,\n\t\t\t\t\t0.5, 0.5, false, false);\n\t\t\t\n\t\t\ttarget.drawColumn(ADVERTISING_POSTER, null,\n\t\t\t\t\tgetBase(),\n\t\t\t\t\t0.98 * height,\n\t\t\t\t\t0.48, 0.48, false, false);\n\t\t\t\n\t\t\ttarget.drawColumn(CONCRETE, null,\n\t\t\t\t\tgetBase().add(0, 0.95 * height, 0),\n\t\t\t\t\t0.05 * height,\n\t\t\t\t\t0.5, 0.5, false, true);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class Billboard extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic Billboard(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tfloat width = parseWidth(node.getTags(), 4);\n\t\t\tfloat height = parseHeight(node.getTags(), 3.5f);\n\t\t\tfloat minHeight = height / 5;\n\t\t\t\t\t\t\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\t\t\t\t\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\tVectorXZ boardVector = faceVector.rightNormal();\n\t\t\t\n\t\t\t\n\t\t\t/* draw board */\n\t\t\t\n\t\t\tVectorXYZ[] vsPoster = {\n\t\t\t\tgetBase().add(boardVector.mult(width/2)).addY(height),\n\t\t\t\tgetBase().add(boardVector.mult(width/2)).addY(minHeight),\n\t\t\t\tgetBase().add(boardVector.mult(-width/2)).addY(height),\n\t\t\t\tgetBase().add(boardVector.mult(-width/2)).addY(minHeight)\n\t\t\t};\n\t\t\t\n\t\t\tList<VectorXYZ> vsListPoster = asList(vsPoster);\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(ADVERTISING_POSTER, vsListPoster,\n\t\t\t\t\ttexCoordLists(vsListPoster, ADVERTISING_POSTER, STRIP_FIT));\n\t\t\t\n\t\t\tVectorXYZ[] vsBoard = {\n\t\t\t\t\tvsPoster[2],\n\t\t\t\t\tvsPoster[3],\n\t\t\t\t\tvsPoster[0],\n\t\t\t\t\tvsPoster[1]\n\t\t\t};\n\t\t\t\n\t\t\tList<VectorXYZ> vsListBoard = asList(vsBoard);\n\t\t\t\t\t\t\t\t\t\n\t\t\ttarget.drawTriangleStrip(CONCRETE, vsListBoard,\n\t\t\t\t\ttexCoordLists(vsListBoard, CONCRETE, STRIP_WALL));\n\t\t\t\n\t\t\t\n\t\t\t/* draw poles */\n\t\t\t\n\t\t\tVectorXZ[] poles = {\n\t\t\t\t\tnode.getPos().add(boardVector.mult(-width/4)),\n\t\t\t\t\tnode.getPos().add(boardVector.mult(+width/4))\n\t\t\t};\n\t\t\t\n\t\t\tfor (VectorXZ pole : poles) {\n\t\t\t\ttarget.drawBox(CONCRETE, pole.xyz(getBase().y),\n\t\t\t\t\t\tfaceVector, minHeight, 0.2, 0.1);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class Bench extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic Bench(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\t\t\t\n\t\t\tfloat width = parseWidth(node.getTags(), 1.5f);\n\t\t\t\n\t\t\t/* determine material */\n\t\t\t\n\t\t\tMaterial material = null;\n\t\t\t\n\t\t\t//TODO parse color\n\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"material\"));\n\t\t\t}\n\t\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"surface\"), Materials.WOOD);\n\t\t\t}\n\t\t\t\n\t\t\t/* calculate vectors and corners */\n\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\t\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\tVectorXZ boardVector = faceVector.rightNormal();\n\t\t\t\n\t\t\tList<VectorXZ> cornerOffsets = new ArrayList<VectorXZ>(4);\n\t\t\tcornerOffsets.add(faceVector.mult(+0.25).add(boardVector.mult(+width/2)));\n\t\t\tcornerOffsets.add(faceVector.mult(+0.25).add(boardVector.mult(-width/2)));\n\t\t\tcornerOffsets.add(faceVector.mult(-0.25).add(boardVector.mult(+width/2)));\n\t\t\tcornerOffsets.add(faceVector.mult(-0.25).add(boardVector.mult(-width/2)));\n\t\t\t\n\t\t\t/* draw seat and backrest */\n\t\t\t\n\t\t\ttarget.drawBox(material, getBase().addY(0.5),\n\t\t\t\t\tfaceVector, 0.05, width, 0.5);\n\t\t\t\n\t\t\tif (!node.getTags().contains(\"backrest\", \"no\")) {\n\t\t\t\t\n\t\t\t\ttarget.drawBox(material,\n\t\t\t\t\t\tgetBase().add(faceVector.mult(-0.23)).addY(0.5),\n\t\t\t\t\t\tfaceVector, 0.5, width, 0.04);\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\t\t\n\t\t\t/* draw poles */\n\t\t\t\t\t\t\n\t\t\tfor (VectorXZ cornerOffset : cornerOffsets) {\n\t\t\t\tVectorXZ polePos = node.getPos().add(cornerOffset.mult(0.8));\n\t\t\t\ttarget.drawBox(material, polePos.xyz(getBase().y),\n\t\t\t\t\t\tfaceVector, 0.5, 0.08, 0.08);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * a summit cross or wayside cross\n\t */\n\tprivate static final class Cross extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic Cross(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tboolean summit = node.getTags().containsKey(\"summit:cross\")\n\t\t\t\t\t|| node.getTags().contains(\"natural\", \"peak\");\n\t\t\t\n\t\t\tfloat height = parseHeight(node.getTags(), summit ? 4f : 2f);\n\t\t\tfloat width = parseHeight(node.getTags(), height * 2/3);\n\t\t\t\n\t\t\tdouble thickness = min(height, width) / 8;\n\t\t\t\n\t\t\t/* determine material and direction */\n\t\t\t\n\t\t\tMaterial material = null;\n\t\t\t\n\t\t\t//TODO parse color\n\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"material\"));\n\t\t\t}\n\t\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"surface\"), Materials.WOOD);\n\t\t\t}\n\t\t\t\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\t\n\t\t\t/* draw cross */\n\t\t\t\n\t\t\ttarget.drawBox(material, getBase(),\n\t\t\t\t\tfaceVector, height, thickness, thickness);\n\t\t\t\n\t\t\ttarget.drawBox(material, getBase().addY(height - width/2 - thickness/2),\n\t\t\t\t\tfaceVector, thickness, width, thickness);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class RecyclingContainer extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\n\t\tpublic RecyclingContainer(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tfloat distanceX = 3f;\n\t\t\tfloat distanceZ = 1.6f;\n\t\t\t\n\t\t\tint n = -1;\n\t\t\tint m = 0;\n\t\t\t\n\t\t\tif (node.getTags().containsAny( asList(\"recycling:glass_bottles\",\"recycling:glass\"), \"yes\")){\n\t\t\t\tn++;\n\t\t\t}\n\t\t\tif (node.getTags().contains( \"recycling:paper\", \"yes\")){\n\t\t\t\tn++;\n\t\t\t}\n\t\t\tif (node.getTags().contains( \"recycling:clothes\", \"yes\")){\n\t\t\t\tn++;\n\t\t\t}\n\t\t\t\n\t\t\tif (node.getTags().contains( \"recycling:paper\", \"yes\")){\n\t\t\t\tdrawContainer(target, \"paper\", getBase().add(new VectorXYZ((distanceX*(-n/2 + m) ), 0f,  (distanceZ/2)).rotateY(faceVector.angle())));\n\t\t\t\tdrawContainer(target, \"paper\", getBase().add(new VectorXYZ((distanceX*(-n/2 + m) ), 0f,  -(distanceZ/2)).rotateY(faceVector.angle())));\n\t\t\t\tm++;\n\t\t\t}\n\t\t\tif (node.getTags().containsAny( asList(\"recycling:glass_bottles\",\"recycling:glass\" ), \"yes\")){\n\t\t\t\tdrawContainer(target, \"white_glass\", getBase().add(new VectorXYZ((distanceX*(-n/2 + m) ), 0f,  (distanceZ/2)).rotateY(faceVector.angle())));\n\t\t\t\tdrawContainer(target, \"coloured_glass\", getBase().add(new VectorXYZ((distanceX*(-n/2 + m) ), 0f,  -(distanceZ/2)).rotateY(faceVector.angle())));\n\t\t\t\tm++;\n\t\t\t}\n\t\t\tif (node.getTags().contains( \"recycling:clothes\", \"yes\")){\n\t\t\t\tdrawContainer(target, \"clothes\", getBase().add(new VectorXYZ((distanceX*(-n/2 + m) ), 0f,  0).rotateY(faceVector.angle())));\n\t\t\t}\n\t\t\t\n\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate void drawContainer(Target<?> target, String trash, VectorXYZ pos){\n\t\t\t\n\t\t\tif (\"clothes\".equals(trash)) {\n\t\t\t\ttarget.drawBox(new ImmutableMaterial(Interpolation.FLAT, new Color(0.82f, 0.784f, 0.75f)),\n\t\t\t\t\t\tpos,\n\t\t\t\t\t\tfaceVector, 2, 1, 1);\n\t\t\t} else { // \"paper\" || \"white_glass\" || \"coloured_glass\"\n\t\t\t\tfloat width = 1.5f;\n\t\t\t\tfloat height = 1.6f;\n\t\t\t\t\n\t\t\t\tMaterial colourFront = null;\n\t\t\t\tMaterial colourBack = null;\n\t\t\t\t\n\t\t\t\tif (\"paper\".equals(trash)) {\n\t\t\t\t\tcolourFront = new ImmutableMaterial(Interpolation.FLAT, Color.BLUE);\n\t\t\t\t\tcolourBack = new ImmutableMaterial(Interpolation.FLAT, Color.BLUE);\n\t\t\t\t} else if (\"white_glass\".equals(trash)) {\n\t\t\t\t\tcolourFront = new ImmutableMaterial(Interpolation.FLAT, Color.WHITE);\n\t\t\t\t\tcolourBack = new ImmutableMaterial(Interpolation.FLAT, Color.WHITE);\n\t\t\t\t} else { // \"coloured_glass\"\n\t\t\t\t\tcolourFront = new ImmutableMaterial(Interpolation.FLAT, new Color(0.18f, 0.32f, 0.14f));\n\t\t\t\t\tcolourBack = new ImmutableMaterial(Interpolation.FLAT, new Color(0.39f, 0.15f, 0.11f));\n\t\t\t\t}\n\t\t\t\n\t\t\t\ttarget.drawBox(STEEL,\n\t\t\t\t\t\tpos,\n\t\t\t\t\t\tfaceVector, height, width, width);\n\t\t\t\ttarget.drawBox(colourFront,\n\t\t\t\t\t\tpos.add(new VectorXYZ((width/2-0.10),0.1f, (width/2-0.1)).rotateY(directionAngle )),\n\t\t\t\t\t\tfaceVector, height-0.2, 0.202, 0.202);\n\t\t\t\ttarget.drawBox(colourBack,\n\t\t\t\t\t\tpos.add(new VectorXYZ(-(width/2-0.10),0.1f, (width/2-0.1)).rotateY(directionAngle )),\n\t\t\t\t\t\tfaceVector, height-0.2, 0.202, 0.202);\n\t\t\t\ttarget.drawBox(colourFront,\n\t\t\t\t\t\tpos.add(new VectorXYZ((width/2-0.10),0.1f, -(width/2-0.1)).rotateY(directionAngle )),\n\t\t\t\t\t\tfaceVector, height-0.2, 0.202, 0.202);\n\t\t\t\ttarget.drawBox(colourBack,\n\t\t\t\t\t\tpos.add(new VectorXYZ(-(width/2-0.10),0.1f, -(width/2-0.1)).rotateY(directionAngle )),\n\t\t\t\t\t\tfaceVector, height-0.2, 0.202, 0.202);\n\t\t\t}\n\t\t}\n\n\t\t\n\t}\n\t\n\tprivate static final class WasteBasket extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic WasteBasket(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t/* determine material */\n\t\t\t\n\t\t\tMaterial material = null;\n\t\t\t\n\t\t\t//TODO parse color\n\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"material\"));\n\t\t\t}\n\t\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"surface\"), STEEL);\n\t\t\t}\n\t\t\t\n\t\t\t/* draw pole */\n\t\t\ttarget.drawColumn(material, null, getBase(),\n\t\t\t\t\t1.2, 0.06, 0.06, false, true);\n\t\t\t\n\t\t\t/* draw basket */\n\t\t\ttarget.drawColumn(material, null,\n\t\t\t\t\tgetBase().addY(0.5).add(0.25, 0f, 0f),\n\t\t\t\t\t0.5, 0.2, 0.2, true, true);\n\t\t}\n\t\t\n\t}\n\n\tprivate static final class GritBin extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\n\t\tpublic GritBin(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tfloat height = parseHeight(node.getTags(), 0.5f);\n\t\t\tfloat width = parseWidth(node.getTags(), 1);\n\t\t\tfloat depth = width / 2f;\n\t\t\t\n\t\t\t/* determine material */\n\t\t\t\n\t\t\tMaterial material = null;\n\t\t\t\n\t\t\t//TODO parse color\n\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"material\"));\n\t\t\t}\n\t\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"surface\"), Materials.GRITBIN_DEFAULT);\n\t\t\t}\n\t\t\t\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\t\t\t\t\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\tVectorXZ boardVector = faceVector.rightNormal();\n\t\t\t\n\t\t\t/* draw box */\n\t\t\ttarget.drawBox(material, getBase(),\n\t\t\t\t\tfaceVector, height, width, depth);\n\t\t\t\n\t\t\t/* draw lid */\n\t\t\tList<VectorXYZ> vs = new ArrayList<VectorXYZ>();\n\t\t\tvs.add(getBase().addY(height + 0.2));\n\t\t\tvs.add(getBase().add(boardVector.mult(width/2)).add(faceVector.mult(depth/2)).addY(height));\n\t\t\tvs.add(getBase().add(boardVector.mult(-width/2)).add(faceVector.mult(depth/2)).addY(height));\n\t\t\tvs.add(getBase().add(boardVector.mult(-width/2)).add(faceVector.mult(-depth/2)).addY(height));\n\t\t\tvs.add(getBase().add(boardVector.mult(width/2)).add(faceVector.mult(-depth/2)).addY(height));\n\t\t\tvs.add(getBase().add(boardVector.mult(width/2)).add(faceVector.mult(depth/2)).addY(height));\n\t\t\t\n\t\t\ttarget.drawTriangleFan(material.brighter(), vs, null);\n\t\t\t\n\t\t}\n\t\n\t}\n\t\n\tprivate static final class Phone extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tprivate static enum Type {WALL, PILLAR, CELL, HALFCELL}\n\t\t\n\t\tpublic Phone(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\t\n\t\t\tMaterial roofMaterial = null;\n\t\t\tMaterial poleMaterial = null;\n\t\t\tType type = null;\n\t\t\t\n\t\t\t// get Type of Phone\n\t\t\tif(isInWall(node)){\n\t\t\t\ttype = Type.WALL;\n\t\t\t} else {\n\t\t\t\ttype =  Type.CELL;\n\t\t\t}\n\t\t\t\n\t\t\t// Phones differ widely in appearance, hence we draw them only for known operators or brands\n\t\t\tif (node.getTags().containsAny(asList(\"operator\", \"brand\"), asList(\"Deutsche Telekom AG\", \"Deutsche Telekom\", \"Telekom\") )) {\n\t\t\t\troofMaterial = TELEKOM_MANGENTA;\n\t\t\t\tpoleMaterial = STEEL;\n\t\t\t} else if (node.getTags().containsAny(asList(\"operator\", \"brand\"), \"British Telecom\")) {\n\t\t\t\troofMaterial = POSTBOX_ROYALMAIL;\n\t\t\t\tpoleMaterial = POSTBOX_ROYALMAIL;\n\t\t\t} else {\n\t\t\t\t//no rendering, unknown operator or brand //TODO log info\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t\n\t\t\t// default dimensions may differ depending on the phone type\n\t\t\tfloat height = 0f;\n\t\t\tfloat width = 0f;\n\t\t\t\n\t\t\tswitch(type) {\n\t\t\t\tcase WALL:\n\t\t\t\t\t\n\t\t\t\t\tbreak;\n\t\t\t\tcase CELL:\n\t\t\t\t\theight = parseHeight(node.getTags(), 2.1f);\n\t\t\t\t\twidth = parseWidth(node.getTags(), 0.8f);\n\t\n\t\t\t\t\ttarget.drawBox(GLASS,\n\t\t\t\t\t\t\tgetBase(),\n\t\t\t\t\t\t\tfaceVector, height-0.2, width-0.06, width-0.06);\n\t\t\t\t\ttarget.drawBox(roofMaterial,\n\t\t\t\t\t\t\tgetBase().addY(height-0.2),\n\t\t\t\t\t\t\tfaceVector, 0.2, width, width);\n\t\t\t\t\ttarget.drawBox(poleMaterial,\n\t\t\t\t\t\t\tgetBase().add(new VectorXYZ((width/2-0.05),0, (width/2-0.05) ).rotateY( directionAngle )),\n\t\t\t\t\t\t\tfaceVector, height-0.2, 0.1, 0.1);\n\t\t\t\t\ttarget.drawBox(poleMaterial,\n\t\t\t\t\t\t\tgetBase().add(new VectorXYZ(-(width/2-0.05),0, (width/2-0.05) ).rotateY( directionAngle )),\n\t\t\t\t\t\t\tfaceVector, height-0.2, 0.1, 0.1);\n\t\t\t\t\ttarget.drawBox(poleMaterial,\n\t\t\t\t\t\t\tgetBase().add(new VectorXYZ(0,0, -(width/2-0.05) ).rotateY( directionAngle )),\n\t\t\t\t\t\t\tfaceVector, height-0.2, width, 0.1);\n\t\t\t\t\t\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tassert false : \"unknown or unsupported phone type\";\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class VendingMachineVice extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\n\t\tprivate static enum Type {WALL, PILLAR}\n\t\t\n\t\tpublic VendingMachineVice(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\t\n\t\t\tMaterial machineMaterial = null;\n\t\t\tMaterial poleMaterial = STEEL;\n\t\t\tType type = null;\n\t\t\t\n\t\t\tif (node.getTags().contains(\"vending\", \"bicycle_tube\") && node.getTags().containsAny(\"operator\", asList(\"Continental\", \"continental\"))){\n\t\t\t\tmachineMaterial = new ImmutableMaterial(Interpolation.FLAT, Color.ORANGE );\n\t\t\t} else if(node.getTags().contains(\"vending\", \"bicycle_tube\")){\n\t\t\t\tmachineMaterial = new ImmutableMaterial(Interpolation.FLAT, Color.BLUE );\n\t\t\t} else if(node.getTags().contains(\"vending\", \"cigarettes\")){\n\t\t\t\tmachineMaterial = new ImmutableMaterial(Interpolation.FLAT, new Color(0.8f, 0.73f, 0.5f));\n\t\t\t} else if(node.getTags().contains(\"vending\", \"condoms\")){\n\t\t\t\tmachineMaterial = new ImmutableMaterial(Interpolation.FLAT, new Color(0.39f, 0.15f, 0.11f) );\n\t\t\t}\n\t\t\t\n\t\t\t// get Type of vending machine\n\t\t\tif(isInWall(node)){\n\t\t\t\ttype = Type.WALL;\n\t\t\t} else {\n\t\t\t\ttype =  Type.PILLAR;\n\t\t\t}\n\t\t\t\n\t\t\t// default dimensions will differ depending on the post box type\n\t\t\tfloat height = 0f;\n\t\t\t\n\t\t\tswitch(type) {\n\t\t\t\tcase WALL:\n\t\t\t\t\t\n\t\t\t\t\tbreak;\n\t\t\t\tcase PILLAR:\n\t\t\t\t\theight = parseHeight(node.getTags(), 1.8f);\n\t\t\t\t\t\n\t\t\t\t\ttarget.drawBox(poleMaterial,\n\t\t\t\t\t\t\tgetBase().add(new VectorXYZ(0,0, -0.05).rotateY(faceVector.angle())),\n\t\t\t\t\t\t\tfaceVector, height-0.3, 0.1, 0.1);\n\t\t\t\t\ttarget.drawBox(machineMaterial,\n\t\t\t\t\t\t\tgetBase().addY(height-1).add(new VectorXYZ(0,0, 0.1 ).rotateY(directionAngle)),\n\t\t\t\t\t\t\tfaceVector, 1, 1, 0.2);\n\t\t\t\t\t\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tassert false : \"unknown or unsupported Vending machine Type\";\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class PostBox extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tprivate static enum Type {WALL, PILLAR}\n\t\n\t\tpublic PostBox(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\t\n\t\t\tMaterial boxMaterial = null;\n\t\t\tMaterial poleMaterial = null;\n\t\t\tType type = null;\n\t\t\t\n\t\t\t\n\t\t\t// post boxes differ widely in appearance, hence we draw them only for known operators or brands\n\t\t\tif (node.getTags().containsAny(asList(\"operator\", \"brand\"), asList(\"Deutsche Post AG\", \"Deutsche Post\"))) {\n\t\t\t\tboxMaterial = POSTBOX_DEUTSCHEPOST;\n\t\t\t\tpoleMaterial = STEEL;\n\t\t\t\ttype = Type.WALL;\n\t\t\t} else if (node.getTags().contains(\"operator\", \"Royal Mail\")) {\n\t\t\t\tboxMaterial = POSTBOX_ROYALMAIL;\n\t\t\t\ttype = Type.PILLAR;\n\t\t\t} else {\n\t\t\t\t//no rendering, unknown operator or brand for post box //TODO log info\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t\tassert (type != Type.WALL || poleMaterial != null) : \"post box of type wall requires a pole material\";\n\t\t\t\n\t\t\t// default dimensions will differ depending on the post box type\n\t\t\tfloat height = 0f;\n\t\t\tfloat width = 0f;\n\t\t\t\n\t\t\tswitch(type) {\n\t\t\t\tcase WALL:\n\t\t\t\t\theight = parseHeight(node.getTags(), 0.8f);\n\t\t\t\t\twidth = parseWidth(node.getTags(), 0.3f);\n\t\t\t\t\t\n\t\t\t\t\ttarget.drawBox(poleMaterial,\n\t\t\t\t\t\tgetBase(),\n\t\t\t\t\t\tfaceVector, height, 0.08, 0.08);\n\t\t\t\t\t\n\t\t\t\t\ttarget.drawBox(boxMaterial,\n\t\t\t\t\t\tgetBase().add(faceVector.mult(width/2 - 0.08/2)).addY(height),\n\t\t\t\t\t\tfaceVector, width, width, width);\n\t\t\t\t\tbreak;\n\t\t\t\tcase PILLAR:\n\t\t\t\t\theight = parseHeight(node.getTags(), 2f);\n\t\t\t\t\twidth = parseWidth(node.getTags(), 0.5f);\n\t\t\t\t\t\n\t\t\t\t\ttarget.drawColumn(boxMaterial, null,\n\t\t\t\t\t\tgetBase(),\n\t\t\t\t\t\theight - 0.1, width, width, false, false);\n\t\t\t\t\ttarget.drawColumn(boxMaterial, null,\n\t\t\t\t\t\tgetBase().addY(height - 0.1),\n\t\t\t\t\t\t0.1, width + 0.1, 0, true, true);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tassert false : \"unknown post box type\";\n\t\t\t}\n\t\t\t\n\t\t}\n\t\n\t}\n\t\n\tprivate static final class BusStop extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic BusStop(MapNode node) {\n\t\t\tsuper(node);\n\t\t\t\n\t\t\tif(node.getTags().contains(\"bin\", \"yes\")){\n\t\t\t\tnode.addRepresentation(new WasteBasket(node));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\tif(!isInHighway(node)){\n\t\t\t\tfloat height = parseHeight(node.getTags(), 3f);\n\t\t\t\tfloat signHeight = 0.7f;\n\t\t\t\tfloat signWidth = 0.4f;\n\t\t\t\t\n\t\t\t\tMaterial poleMaterial = STEEL;\n\t\t\t\t\n\t\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\t\t\n\t\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\t\t\n\t\t\t\ttarget.drawColumn(poleMaterial, null,\n\t\t\t\t\tgetBase(),\n\t\t\t\t\theight-signHeight, 0.05, 0.05, true, true);\n\t\t\t\t/* draw sign */\n\t\t\t\ttarget.drawBox(BUS_STOP_SIGN,\n\t\t\t\t\tgetBase().addY(height - signHeight),\n\t\t\t\t\tfaceVector, signHeight, signWidth, 0.02);\n\t\t\t\t/*  draw timetable */\n\t\t\t\ttarget.drawBox(poleMaterial,\n\t\t\t\t\tgetBase().addY(1.2f).add(new VectorXYZ(0.055f,0,0f).rotateY(directionAngle)),\n\t\t\t\t\tfaceVector, 0.31, 0.01, 0.43);\n\t\t\t\t\n\t\t\t\t//TODO Add Shelter and bench\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\t\n\tprivate static final class ParcelMachine extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\n\t\tpublic ParcelMachine(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tdouble ele = getBase().y;\n\t\t\t\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\t\n\t\t\tMaterial boxMaterial = POSTBOX_DEUTSCHEPOST;\n\t\t\tMaterial otherMaterial = STEEL;\n\t\t\t\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\tVectorXZ rightVector = faceVector.rightNormal();\n\t\t\t\n\t\t\t// shape depends on type\n\t\t\tif (node.getTags().contains(\"type\", \"Rondell\")) {\n\t\t\t\t\n\t\t\t\tfloat height = parseHeight(node.getTags(), 2.2f);\n\t\t\t\tfloat width = parseWidth(node.getTags(), 3f);\n\t\t\t\tfloat rondelWidth  = width * 2/3;\n\t\t\t\tfloat boxWidth     = width * 1/3;\n\t\t\t\tfloat roofOverhang = 0.3f;\n\t\t\t\t\n\t\t\t\t/* draw rondel */\n\t\t\t\ttarget.drawColumn(boxMaterial, null,\n\t\t\t\t\tgetBase().add(rightVector.mult(-rondelWidth/2)),\n\t\t\t\t\theight,\trondelWidth/2, rondelWidth/2, false, true);\n\t\t\t\t/* draw box */\n\t\t\t\ttarget.drawBox(boxMaterial,\n\t\t\t\t\tgetBase().add(rightVector.mult(boxWidth/2)).add(faceVector.mult(-boxWidth/2)),\n\t\t\t\t\tfaceVector, height, boxWidth, boxWidth);\n\t\t\t\t/* draw roof */\n\t\t\t\ttarget.drawColumn(otherMaterial, null,\n\t\t\t\t\tgetBase().addY(height),\n\t\t\t\t\t0.1, rondelWidth/2 + roofOverhang/2, rondelWidth/2 + roofOverhang/2, true, true);\n\t\t\t\t\n\t\t\t} else if (node.getTags().contains(\"type\", \"Paketbox\")) {\n\t\t\t\t\n\t\t\t\tfloat height = parseHeight(node.getTags(), 1.5f);\n\t\t\t\tfloat width = parseHeight(node.getTags(), 1.0f);\n\t\t\t\tfloat depth = width;\n\t\t\t\t\n\t\t\t\ttarget.drawBox(boxMaterial,\tgetBase(),\n\t\t\t\t\t\tfaceVector, height, width * 2, depth * 2);\n\t\t\t\t\n\t\t\t} else { // type=Schrank or type=24/7 Station (they look roughly the same) or no type (fallback)\n\t\t\t\t\n\t\t\t\tfloat height = parseHeight(node.getTags(), 2.2f);\n\t\t\t\tfloat width = parseWidth(node.getTags(), 3.5f);\n\t\t\t\tfloat depth = width/3;\n\t\t\t\tfloat roofOverhang = 0.3f;\n\t\t\t\t\n\t\t\t\t/* draw box */\n\t\t\t\ttarget.drawBox(boxMaterial,\n\t\t\t\t\tgetBase(),\n\t\t\t\t\tfaceVector, height, width, depth);\n\t\t\t\t/*  draw small roof */\n\t\t\t\ttarget.drawBox(otherMaterial,\n\t\t\t\t\tnode.getPos().add(faceVector.mult(roofOverhang)).xyz(ele + height),\n\t\t\t\t\tfaceVector, 0.1, width, depth + roofOverhang*2);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\n\t}\n\t\n\tprivate static final class FireHydrant extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic FireHydrant(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tfloat height = parseHeight(node.getTags(), 1f);\n\t\t\t\n\t\t\t/* draw main pole */\n\t\t\ttarget.drawColumn(FIREHYDRANT, null,\n\t\t\t\t\tgetBase(),\n\t\t\t\t\theight,\n\t\t\t\t\t0.15, 0.15, false, true);\n\t\t\t\n\t\t\t/* draw two small and one large valve */\n\t\t\tVectorXYZ valveBaseVector = getBase().addY(height - 0.3);\n\t\t\tVectorXZ smallValveVector = VectorXZ.X_UNIT;\n\t\t\tVectorXZ largeValveVector = VectorXZ.Z_UNIT;\n\t\t\t\n\t\t\ttarget.drawBox(FIREHYDRANT,\n\t\t\t\tvalveBaseVector,\n\t\t\t\tsmallValveVector, 0.1f, 0.5f, 0.1f);\n\t\t\ttarget.drawBox(FIREHYDRANT,\n\t\t\t\tvalveBaseVector.add(0.2f, -0.1f, 0f),\n\t\t\t\tlargeValveVector, 0.15f, 0.15f, 0.15f);\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class StreetLamp extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic StreetLamp(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tfloat lampHeight = 0.8f;\n\t\t\tfloat lampHalfWidth = 0.4f;\n\t\t\tfloat poleHeight = parseHeight(node.getTags(), 5f) - lampHeight;\n\t\t\t\n\t\t\t/* determine material */\n\t\t\t\n\t\t\tMaterial material = null;\n\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"material\"));\n\t\t\t}\n\t\t\t\n\t\t\tif (material == null) {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(\n\t\t\t\t\t\tnode.getTags().getValue(\"surface\"), STEEL);\n\t\t\t}\n\t\t\t\n\t\t\t/* draw pole */\n\t\t\ttarget.drawColumn(material, null,\n\t\t\t\t\tgetBase(),\n\t\t\t\t\t0.5, 0.16, 0.08, false, false);\n\t\t\ttarget.drawColumn(material, null,\n\t\t\t\t\tgetBase().addY(0.5),\n\t\t\t\t\tpoleHeight, 0.08, 0.08, false, false);\n\t\t\t\n\t\t\t/* draw lamp */\n\t\t\t\t\t\n\t\t\t// lower part\n\t\t\tList<VectorXYZ> vs = new ArrayList<VectorXYZ>();\n\t\t\tvs.add(getBase().addY(poleHeight));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(lampHalfWidth, 0, lampHalfWidth));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(lampHalfWidth, 0, -lampHalfWidth));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(-lampHalfWidth, 0, -lampHalfWidth));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(-lampHalfWidth, 0, lampHalfWidth));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(lampHalfWidth, 0, lampHalfWidth));\n\t\t\t\n\t\t\ttarget.drawTriangleFan(material, vs, null);\n\t\t\t\n\t\t\t// upper part\n\t\t\tvs = new ArrayList<VectorXYZ>();\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(lampHalfWidth, 0, lampHalfWidth));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(-lampHalfWidth, 0, lampHalfWidth));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(-lampHalfWidth, 0, -lampHalfWidth));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(lampHalfWidth, 0, -lampHalfWidth));\n\t\t\tvs.add(getBase().addY(poleHeight + lampHeight * 0.8).add(lampHalfWidth, 0, lampHalfWidth));\n\t\t\t\n\t\t\ttarget.drawTriangleFan(material, vs, null);\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final class Board extends NoOutlineNodeWorldObject\n\timplements RenderableToAllTargets {\n\n\t\tpublic Board(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tdouble directionAngle = parseDirection(node.getTags(), PI);\n\t\t\tVectorXZ faceVector = VectorXZ.fromAngle(directionAngle);\n\t\t\ttarget.drawColumn(WOOD, null,\n\t\t\t\t\tgetBase(),\n\t\t\t\t\t1.5, 0.05, 0.05, false, true);\n\t\t\ttarget.drawBox(WOOD,\n\t\t\t\t\tgetBase().addY(1.2),\n\t\t\t\t\tfaceVector, 0.4, 0.4, 0.1);\n\t\t}\n\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/SurfaceAreaModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.util.Collections.*;\nimport static org.osm2world.core.map_data.creation.EmptyTerrainBuilder.EMPTY_SURFACE_TAG;\nimport static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.MIN;\nimport static org.osm2world.core.map_elevation.data.GroundState.*;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.GLOBAL_X_Z;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.triangleTexCoordLists;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.creation.EmptyTerrainBuilder;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapType;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorGridXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.algorithms.CAGUtil;\nimport org.osm2world.core.math.algorithms.JTSTriangulationUtil;\nimport org.osm2world.core.math.algorithms.Poly2TriTriangulationUtil;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.util.exception.TriangulationException;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.data.WorldObject;\nimport org.osm2world.core.world.data.WorldObjectWithOutline;\nimport org.osm2world.core.world.modules.common.AbstractModule;\n\n/**\n * adds generic areas with surface information to the world.\n * Is based on surface information on otherwise unknown/unspecified areas.\n */\npublic class SurfaceAreaModule extends AbstractModule {\n\t\n\t/** assumptions about default surfaces for certain tags */\n\tprivate static final Map<Tag, String> defaultSurfaceMap\n\t\t= new HashMap<Tag, String>();\n\t\n\tstatic {\n\t\tdefaultSurfaceMap.put(new Tag(\"leisure\", \"pitch\"), \"ground\");\n\t\tdefaultSurfaceMap.put(new Tag(\"landuse\", \"construction\"), \"ground\");\n\t\tdefaultSurfaceMap.put(new Tag(\"golf\", \"bunker\"), \"sand\");\n\t\tdefaultSurfaceMap.put(new Tag(\"golf\", \"green\"), \"grass\");\n\t\tdefaultSurfaceMap.put(new Tag(\"natural\", \"sand\"), \"sand\");\n\t\tdefaultSurfaceMap.put(new Tag(\"natural\", \"beach\"), \"sand\");\n\t\tdefaultSurfaceMap.put(new Tag(\"landuse\", \"meadow\"), \"grass\");\n\t\tdefaultSurfaceMap.put(new Tag(\"landuse\", \"grass\"), \"grass\");\n\t}\n\t\n\t@Override\n\tprotected void applyToArea(MapArea area) {\n\t\t\n\t\tif (!area.getRepresentations().isEmpty()) return;\n\t\t\n\t\tTagGroup tags = area.getTags();\n\t\t\n\t\tif (tags.containsKey(\"surface\")) {\n\t\t\tarea.addRepresentation(new SurfaceArea(area, tags.getValue(\"surface\")));\n\t\t} else {\n\t\t\n\t\t\tfor (Tag tagWithDefault : defaultSurfaceMap.keySet()) {\n\t\t\t\tif (tags.contains(tagWithDefault)) {\n\t\t\t\t\tarea.addRepresentation(new SurfaceArea(\n\t\t\t\t\t\t\tarea, defaultSurfaceMap.get(tagWithDefault)));\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class SurfaceArea extends AbstractAreaWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\t\t\n\t\tprivate final String surface;\n\t\t\n\t\tprivate Collection<TriangleXZ> triangulationXZ;\n\t\t\n\t\tpublic SurfaceArea(MapArea area, String surface) {\n\t\t\tsuper(area);\n\t\t\tthis.surface = surface;\n\t\t}\n\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tMaterial material = null;\n\t\t\t\n\t\t\tif (surface.equals(EMPTY_SURFACE_TAG.value)) {\n\t\t\t\tmaterial = Materials.TERRAIN_DEFAULT;\n\t\t\t} else {\n\t\t\t\tmaterial = Materials.getSurfaceMaterial(surface);\n\t\t\t}\n\t\t\t\n\t\t\tif (material != null) {\n\t\t\t\t\n\t\t\t\tCollection<TriangleXYZ> triangles = getTriangulation();\n\t\t\t\ttarget.drawTriangles(material, triangles,\n\t\t\t\t\t\ttriangleTexCoordLists(triangles, material, GLOBAL_X_Z));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * calculates the true ground footprint of this area by removing\n\t\t * area covered by other overlapping features, then triangulates it\n\t\t * into counterclockwise triangles.\n\t\t */\n\t\t@Override\n\t\tprotected Collection<TriangleXZ> getTriangulationXZ() {\n\t\t\t\n\t\t\tif (triangulationXZ != null) {\n\t\t\t\treturn triangulationXZ;\n\t\t\t}\n\t\t\t\n\t\t\tboolean isEmptyTerrain = surface.equals(EMPTY_SURFACE_TAG.value);\n\t\t\t\n\t\t\t/* collect the outlines of overlapping ground polygons and other polygons,\n\t\t\t * and EleConnectors within the area */\n\t\t\t\n\t\t\tList<SimplePolygonXZ> subtractPolys = new ArrayList<SimplePolygonXZ>();\n\t\t\tList<SimplePolygonXZ> allPolys = new ArrayList<SimplePolygonXZ>();\n\t\t\t\n\t\t\tList<VectorXZ> eleConnectorPoints = new ArrayList<VectorXZ>();\n\t\t\t\n\t\t\tfor (MapOverlap<?, ?> overlap : area.getOverlaps()) {\n\t\t\tfor (WorldObject otherWO : overlap.getOther(area).getRepresentations()) {\n\n\t\t\t\tif (otherWO instanceof TerrainBoundaryWorldObject\n\t\t\t\t\t\t&& otherWO.getGroundState() == GroundState.ON) {\n\t\t\t\t\t\n\t\t\t\t\tif (otherWO instanceof SurfaceArea && !isEmptyTerrain) {\n\t\t\t\t\t\t// empty terrain has lowest priority\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (overlap.type == MapOverlapType.CONTAIN\n\t\t\t\t\t\t\t&& overlap.e1 == this.area) {\n\t\t\t\t\t\t// completely within other element, no ground area left\n\t\t\t\t\t\treturn emptyList();\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tTerrainBoundaryWorldObject terrainBoundary =\n\t\t\t\t\t\t(TerrainBoundaryWorldObject)otherWO;\n\t\t\t\t\t\n\t\t\t\t\tSimplePolygonXZ outlinePolygon = terrainBoundary.getOutlinePolygonXZ();\n\t\t\t\t\t\n\t\t\t\t\tif (outlinePolygon != null) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tsubtractPolys.add(outlinePolygon);\n\t\t\t\t\t\tallPolys.add(outlinePolygon);\n\t\t\t\t\t\t\n\t\t\t\t\t\tfor (EleConnector eleConnector : otherWO.getEleConnectors()) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (!outlinePolygon.getVertexCollection().contains(eleConnector.pos)) {\n\t\t\t\t\t\t\t\teleConnectorPoints.add(eleConnector.pos);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\tfor (EleConnector eleConnector : otherWO.getEleConnectors()) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (eleConnector.reference == null) {\n\t\t\t\t\t\t\t/* workaround to avoid using connectors at intersections,\n\t\t\t\t\t\t\t * which might fall on area segments\n\t\t\t\t\t\t\t * //TODO cleaner solution\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\teleConnectorPoints.add(eleConnector.pos);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (otherWO instanceof WorldObjectWithOutline) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tSimplePolygonXZ outlinePolygon =\n\t\t\t\t\t\t\t\t((WorldObjectWithOutline)otherWO).getOutlinePolygonXZ();\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (outlinePolygon != null) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tallPolys.add(outlinePolygon);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* add a grid of points within the area for smoother surface shapes */\n\t\t\t\n\t\t\tVectorGridXZ pointGrid = new VectorGridXZ(\n\t\t\t\t\tarea.getAxisAlignedBoundingBoxXZ(),\n\t\t\t\t\tEmptyTerrainBuilder.POINT_GRID_DIST);\n\t\t\t\n\t\t\tfor (VectorXZ point : pointGrid) {\n\t\t\t\t\n\t\t\t\t//don't insert if it is e.g. on top of a tunnel;\n\t\t\t\t//otherwise there would be no minimum vertical distance\n\t\t\t\t\n\t\t\t\tboolean safe = true;\n\t\t\t\t\n\t\t\t\tfor (SimplePolygonXZ polygon : allPolys) {\n\t\t\t\t\tif (polygon.contains(point)) {\n\t\t\t\t\t\tsafe = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (safe) {\n\t\t\t\t\teleConnectorPoints.add(point);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* create \"leftover\" polygons by subtracting the existing ones */\n\t\t\t\n\t\t\tCollection<PolygonWithHolesXZ> polygons;\n\t\t\t\n\t\t\tif (subtractPolys.isEmpty()) {\n\t\t\t\t\n\t\t\t\t/* SUGGEST (performance) handle the common \"empty terrain cell\"\n\t\t\t\t * special case more efficiently, also regarding point raster? */\n\t\t\t\t\n\t\t\t\tpolygons = singleton(area.getPolygon());\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tpolygons = CAGUtil.subtractPolygons(\n\t\t\t\t\t\tarea.getOuterPolygon(), subtractPolys);\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\t\t\n\t\t\t/* triangulate, using elevation information from all participants */\n\t\t\t\n\t\t\ttriangulationXZ = new ArrayList<TriangleXZ>();\n\t\t\t\n\t\t\tfor (PolygonWithHolesXZ polygon : polygons) {\n\t\t\t\t\n\t\t\t\tList<VectorXZ> points = new ArrayList<VectorXZ>();\n\t\t\t\t\n\t\t\t\tfor (VectorXZ point : eleConnectorPoints) {\n\t\t\t\t\tif (polygon.contains(point)) {\n\t\t\t\t\t\tpoints.add(point);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\t\n\t\t\t\t\ttriangulationXZ.addAll(Poly2TriTriangulationUtil.triangulate(\n\t\t\t\t\t\t\tpolygon.getOuter(),\n\t\t\t\t\t\t\tpolygon.getHoles(),\n\t\t\t\t\t\t\tCollections.<LineSegmentXZ>emptyList(),\n\t\t\t\t\t\t\tpoints));\n\t\t\t\t\t\n\t\t\t\t} catch (TriangulationException e) {\n\t\t\t\t\t\n\t\t\t\t\tSystem.err.println(\"Poly2Tri exception for \" + this + \":\");\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t\tSystem.err.println(\"... falling back to JTS triangulation.\");\n\t\t\t\t\t\n\t\t\t\t\ttriangulationXZ.addAll(JTSTriangulationUtil.triangulate(\n\t\t\t\t\t\t\tpolygon.getOuter(),\n\t\t\t\t\t\t\tpolygon.getHoles(),\n\t\t\t\t\t\t\tCollections.<LineSegmentXZ>emptyList(),\n\t\t\t\t\t\t\tpoints));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn triangulationXZ;\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\t\n\t\t\tsuper.defineEleConstraints(enforcer);\n\t\t\t\n\t\t\t/** add vertical distance to connectors above and below */\n\t\t\t\n\t\t\tfor (MapOverlap<?, ?> overlap : area.getOverlaps()) {\n\t\t\tfor (WorldObject otherWO : overlap.getOther(area).getRepresentations()) {\n\t\t\t\t\n\t\t\t\tfor (EleConnector eleConnector : otherWO.getEleConnectors()) {\n\t\t\t\t\t\n\t\t\t\t\tEleConnector ownConnector = getEleConnectors().getConnector(eleConnector.pos);\n\t\t\t\t\t\n\t\t\t\t\tif (ownConnector == null) continue;\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\tif (eleConnector.groundState == ABOVE) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\t\t\t\tMIN, 1,\n\t\t\t\t\t\t\t\teleConnector, ownConnector); //TODO actual clearing\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (eleConnector.groundState == BELOW) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\t\t\t\tMIN, 10,\n\t\t\t\t\t\t\t\townConnector, eleConnector); //TODO actual clearing\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic PolygonXYZ getOutlinePolygon() {\n\t\t\tif (surface.equals(EMPTY_SURFACE_TAG.value)) {\n\t\t\t\t// avoid interfering with e.g. tree placement\n\t\t\t\treturn null;\n\t\t\t} else {\n\t\t\t\treturn super.getOutlinePolygon();\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic SimplePolygonXZ getOutlinePolygonXZ() {\n\t\t\tif (surface.equals(EMPTY_SURFACE_TAG.value)) {\n\t\t\t\t// avoid interfering with e.g. tree placement\n\t\t\t\treturn null;\n\t\t\t} else {\n\t\t\t\treturn super.getOutlinePolygonXZ();\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\tif (BridgeModule.isBridge(area.getTags())) {\n\t\t\t\treturn GroundState.ABOVE;\n\t\t\t} else if (TunnelModule.isTunnel(area.getTags())) {\n\t\t\t\treturn GroundState.BELOW;\n\t\t\t} else {\n\t\t\t\treturn GroundState.ON;\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/TrafficSignModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.lang.Math.PI;\nimport static java.util.Arrays.asList;\nimport static org.osm2world.core.math.VectorXYZ.X_UNIT;\nimport static org.osm2world.core.target.common.material.Materials.*;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.STRIP_FIT;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.texCoordLists;\nimport static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.TextureData;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.world.data.NoOutlineNodeWorldObject;\nimport org.osm2world.core.world.modules.common.AbstractModule;\n\n\n/**\n * adds traffic signs to the world\n */\npublic class TrafficSignModule extends AbstractModule {\n\t\n\t@Override\n\tprotected void applyToNode(MapNode node) {\n\t\t\n\t\tif (!node.getTags().containsKey(\"traffic_sign\")) return;\n\t\t\n\t\tif (isInHighway(node)) return; //only exact positions (next to highway)\n\t\t\n\t\t/* split the traffic sign value into its components */\n\t\t\n\t\tString tagValue = node.getTags().getValue(\"traffic_sign\");\n\t\tString[] countryAndSigns = tagValue.split(\":\", 2);\n\t\t\n\t\tif (countryAndSigns.length != 2) return;\n\t\t\n\t\tString country = countryAndSigns[0];\n\t\t\n\t\tString[] signs = countryAndSigns[1].split(\"[;,]\");\n\t\t\n\t\t/* match each individual sign to the list of supported types */\n\t\t\n\t\tList<TrafficSignType> types = new ArrayList<TrafficSignType>(signs.length);\n\t\t\n\t\tfor (String sign : signs) {\n\t\t\t\n\t\t\tsign = sign.trim();\n\t\t\tsign = sign.replace('-', '_');\n\t\t\t\n\t\t\ttry {\n\t\t\t\ttypes.add(TrafficSignType.valueOf(country + '_' + sign));\n\t\t\t} catch (IllegalArgumentException e) {\n\t\t\t\t// not a supported traffic sign type\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* create a visual representation for the traffic sign */\n\t\t\n\t\tif (types.size() > 0) {\n\t\t\tnode.addRepresentation(new TrafficSign(node, types));\n\t\t}\n\t\t\t\t\n\t}\n\t\n\tprivate enum TrafficSignType {\n\n\t\tSTOP(SIGN_DE_206, 1, 2),\n\t\t\n\t\tDE_206(SIGN_DE_206, 1, 2),\n\t\tDE_250(SIGN_DE_250, 1, 2),\n\t\tDE_625(SIGN_DE_625_11, 2, .80),\n\t\tDE_625_11(SIGN_DE_625_11, 2, .80),\n\t\tDE_625_21(SIGN_DE_625_21, 2, .80);\n\t\t\n\t\tpublic final Material material;\n\t\t\n\t\tpublic final int numPosts;\n\n\t\tprivate final double defaultHeight;\n\t\t\n\t\tTrafficSignType(Material material, int numPosts, double defaultHeight) {\n\t\t\tthis.material = material;\n\t\t\tthis.numPosts = numPosts;\n\t\t\tthis.defaultHeight = defaultHeight;\n\t\t}\n\t\t\t\t\n\t}\n\t\n\tprivate static boolean isInHighway(MapNode node){\n\t\tif (node.getConnectedWaySegments().size()>0){\n\t\t\tfor(MapWaySegment way: node.getConnectedWaySegments()){\n\t\t\t\tif( way.getTags().containsKey(\"highway\") && !way.getTags().containsAny(\"highway\", asList(\"path\", \"footway\", \"platform\") ) ){\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\tprivate static final class TrafficSign extends NoOutlineNodeWorldObject\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tprivate final List<TrafficSignType> types;\n\t\t\n\t\tpublic TrafficSign(MapNode node, List<TrafficSignType> types) {\n\t\t\t\n\t\t\tsuper(node);\n\t\t\t\n\t\t\tthis.types = types;\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t/* get basic parameters */\n\n\t\t\tdouble height = parseHeight(node.getTags(), (float)types.get(0).defaultHeight);\n\t\t\tdouble postRadius = 0.05;\n\t\t\t\n\t\t\tdouble[] signHeights = new double[types.size()];\n\t\t\tdouble[] signWidths = new double[types.size()];\n\t\t\t\n\t\t\tfor (int sign = 0; sign < types.size(); sign++) {\n\t\t\t\t\n\t\t\t\tTextureData textureData = null;\n\t\t\t\t\n\t\t\t\tif (types.get(sign).material.getNumTextureLayers() != 0) {\n\t\t\t\t\ttextureData = types.get(sign).material.getTextureDataList().get(0);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (textureData == null) {\n\t\t\t\t\tsignHeights[sign] = 0.6;\n\t\t\t\t\tsignWidths[sign] = 0.6;\n\t\t\t\t} else {\n\t\t\t\t\tsignHeights[sign] = textureData.height;\n\t\t\t\t\tsignWidths[sign] = textureData.width;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* position the post(s) */\n\t\t\t\n\t\t\tint numPosts = types.get(0).numPosts;\n\t\t\t\n\t\t\tList<VectorXYZ> positions = new ArrayList<VectorXYZ>(numPosts);\n\t\t\t\n\t\t\tfor (int i = 0; i < numPosts; i++) {\n\t\t\t\tdouble relativePosition = 0.5 - (i+1)/(double)(numPosts+1);\n\t\t\t\tpositions.add(getBase().add(X_UNIT.mult(relativePosition * signWidths[0])));\n\t\t\t}\n\t\t\t\t\t\t\n\t\t\t/* create the front and back side of the sign */\n\t\t\t\n\t\t\tList<List<VectorXYZ>> signGeometries = new ArrayList<List<VectorXYZ>>();\n\n\t\t\tdouble distanceBetweenSigns = 0.1;\n\t\t\tdouble upperHeight = height;\n\t\t\t\n\t\t\tfor (int sign = 0; sign < types.size(); sign++) {\n\t\t\t\t\n\t\t\t\tdouble signHeight = signHeights[sign];\n\t\t\t\tdouble signWidth = signWidths[sign];\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> vs = asList(\n\t\t\t\t\t\tgetBase().add(+signWidth/2, upperHeight, postRadius),\n\t\t\t\t\t\tgetBase().add(+signWidth/2, upperHeight-signHeight, postRadius),\n\t\t\t\t\t\tgetBase().add(-signWidth/2, upperHeight, postRadius),\n\t\t\t\t\t\tgetBase().add(-signWidth/2, upperHeight-signHeight, postRadius)\n\t\t\t\t\t\t);\n\t\t\t\t\n\t\t\t\tsignGeometries.add(vs);\n\t\t\t\t\n\t\t\t\tupperHeight -= signHeight + distanceBetweenSigns;\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* rotate the sign around the base to match the direction tag */\n\t\t\t\n\t\t\tdouble direction = parseDirection(node.getTags(), PI);\n\t\t\t\n\t\t\tfor (List<VectorXYZ> vs : signGeometries) {\n\t\t\t\n\t\t\t\tfor (int i = 0; i < vs.size(); i++) {\n\t\t\t\t\tVectorXYZ v = vs.get(i);\n\t\t\t\t\tv = v.rotateVec(direction, getBase(), VectorXYZ.Y_UNIT);\n\t\t\t\t\tvs.set(i, v);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (positions.size() > 1) { // if 1, the post is exactly on the base\n\t\t\t\tfor (int i = 0; i < positions.size(); i++) {\n\t\t\t\t\tVectorXYZ v = positions.get(i);\n\t\t\t\t\tv = v.rotateVec(direction, getBase(), VectorXYZ.Y_UNIT);\n\t\t\t\t\tpositions.set(i, v);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* render the post(s) */\n\t\t\t\n\t\t\tfor (VectorXYZ position : positions) {\n\t\t\t\ttarget.drawColumn(STEEL, null, position,\n\t\t\t\t\t\theight, postRadius, postRadius,\n\t\t\t\t\t\tfalse, true);\n\t\t\t}\n\t\t\t\n\t\t\t/* render the sign (front, then back) */\n\t\t\t\n\t\t\tfor (int sign = 0; sign < types.size(); sign++) {\n\t\t\t\t\n\t\t\t\tTrafficSignType type = types.get(sign);\n\t\t\t\tList<VectorXYZ> vs = signGeometries.get(sign);\n\t\t\t\t\n\t\t\t\ttarget.drawTriangleStrip(type.material, vs,\n\t\t\t\t\t\ttexCoordLists(vs, type.material, STRIP_FIT));\n\t\t\t\t\n\t\t\t\tvs = asList(vs.get(2), vs.get(3), vs.get(0), vs.get(1));\n\t\t\t\t\n\t\t\t\ttarget.drawTriangleStrip(STEEL, vs,\n\t\t\t\t\t\ttexCoordLists(vs, STEEL, STRIP_FIT));\n\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/TreeModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.util.Arrays.asList;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.filterWorldObjectCollisions;\nimport static org.osm2world.core.world.modules.common.WorldModuleParseUtil.parseHeight;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.GeometryUtil;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.FaceTarget;\nimport org.osm2world.core.target.common.RenderableToFaceTarget;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.target.povray.POVRayTarget;\nimport org.osm2world.core.target.povray.RenderableToPOVRay;\nimport org.osm2world.core.world.data.AreaWorldObject;\nimport org.osm2world.core.world.data.NoOutlineNodeWorldObject;\nimport org.osm2world.core.world.data.WaySegmentWorldObject;\nimport org.osm2world.core.world.data.WorldObject;\nimport org.osm2world.core.world.modules.common.ConfigurableWorldModule;\nimport org.osm2world.core.world.modules.common.WorldModuleBillboardUtil;\n\n/**\n * adds trees, tree rows, tree groups and forests to the world\n */\npublic class TreeModule extends ConfigurableWorldModule {\n\t\n\tprivate static final List<String> LEAF_TYPE_KEYS =\n\t\t\tasList(\"leaf_type\", \"wood\", \"type\");\n\t\n\tprivate static enum LeafType {\n\t\t\n\t\tBROADLEAVED(\"broadleaved\", \"broad_leaved\", \"broad_leafed\", \"deciduous\"),\n\t\tNEEDLELEAVED(\"needleleaved\", \"coniferous\", \"conifer\");\n\t\t\n\t\tprivate final List<String> values;\n\n\t\tprivate LeafType(String... values) {\n\t\t\tthis.values = asList(values);\n\t\t}\n\t\t\n\t\tpublic static LeafType getValue(TagGroup tags) {\n\t\t\tfor (LeafType type : values()) {\n\t\t\t\tif (tags.containsAny(LEAF_TYPE_KEYS, type.values)) {\n\t\t\t\t\treturn type;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final List<String> LEAF_CYCLE_KEYS =\n\t\t\tasList(\"leaf_cycle\", \"wood\", \"type\");\n\t\n\tprivate static enum LeafCycle {\n\t\t\n\t\tEVERGREEN(\"evergreen\"),\n\t\tDECIDUOUS(\"deciduous\", \"broad_leaved\"),\n\t\tSEMI_EVERGREEN(\"semi_evergreen\"),\n\t\tSEMI_DECIDUOUS(\"semi_deciduous\");\n\t\t\n\t\tprivate final List<String> values;\n\n\t\tprivate LeafCycle(String... values) {\n\t\t\tthis.values = asList(values);\n\t\t}\n\t\t\n\t\tpublic static LeafCycle getValue(TagGroup tags) {\n\t\t\tfor (LeafCycle type : values()) {\n\t\t\t\tif (tags.containsAny(LEAF_CYCLE_KEYS, type.values)) {\n\t\t\t\t\treturn type;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static enum TreeSpecies {\n\t\t\n\t\tAPPLE_TREE(\"malus\");\n\t\t\n\t\tprivate final String value;\n\t\t\n\t\tprivate TreeSpecies(String value) {\n\t\t\tthis.value = value;\n\t\t}\n\t\t\n\t\tpublic static TreeSpecies getValue(TagGroup tags) {\n\t\t\t\n\t\t\tString speciesString = tags.getValue(\"species\");\n\t\t\t\n\t\t\tif (speciesString != null) {\n\t\t\t\t\n\t\t\t\tfor (TreeSpecies species : values()) {\n\t\t\t\t\tif (speciesString.contains(species.value)) {\n\t\t\t\t\t\treturn species;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t// default to apple trees for orchards\n\t\t\t\n\t\t\tif (tags.contains(\"landuse\", \"orchard\")) {\n\t\t\t\treturn APPLE_TREE;\n\t\t\t} else {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate boolean useBillboards = false;\n\tprivate double defaultTreeHeight = 10;\n\tprivate double defaultTreeHeightForest = 20;\n\t\n\t@Override\n\tpublic void setConfiguration(Configuration config) {\n\t\tsuper.setConfiguration(config);\n\t\tuseBillboards = config.getBoolean(\"useBillboards\", false);\n\t\tdefaultTreeHeight = config.getDouble(\"defaultTreeHeight\", 10);\n\t\tdefaultTreeHeightForest = config.getDouble(\"defaultTreeHeightForest\", 20);\n\t}\n\t\n\t@Override\n\tpublic final void applyTo(MapData mapData) {\n\t\t\n\t\tfor (MapNode node : mapData.getMapNodes()) {\n\t\t\t\n\t\t\tif (node.getTags().contains(\"natural\", \"tree\")) {\n\t\t\t\tnode.addRepresentation(new Tree(node));\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tfor (MapWaySegment segment : mapData.getMapWaySegments()) {\n\t\t\t\n\t\t\tif (segment.getTags().contains(new Tag(\"natural\", \"tree_row\"))) {\n\t\t\t\tsegment.addRepresentation(new TreeRow(segment));\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tfor (MapArea area : mapData.getMapAreas()) {\n\t\t\t\n\t\t\tif (area.getTags().contains(\"natural\", \"wood\")\n\t\t\t\t\t|| area.getTags().contains(\"landuse\", \"forest\")\n\t\t\t\t\t|| area.getTags().containsKey(\"wood\")\n\t\t\t\t\t|| area.getTags().contains(\"landuse\", \"orchard\")) {\n\t\t\t\tarea.addRepresentation(new Forest(area, mapData));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tprivate static final float TREE_RADIUS_PER_HEIGHT = 0.2f;\n\t\n\tprivate void renderTree(Target<?> target, MapElement element, VectorXYZ pos,\n\t\t\tLeafType leafType, LeafCycle leafCycle, TreeSpecies species) {\n\t\t\n\t\t// if leaf type is unknown, make \"random\" decision based on x coord\n\t\tif (leafType == null) {\n\t\t\tif ((long)(pos.getX()) % 2 == 0) {\n\t\t\t\tleafType = LeafType.NEEDLELEAVED;\n\t\t\t} else {\n\t\t\t\tleafType = LeafType.BROADLEAVED;\n\t\t\t}\n\t\t}\n\t\t\n\t\tdouble height = getTreeHeight(element, leafType == LeafType.NEEDLELEAVED, species != null);\n\t\t\n\t\tif (useBillboards) {\n\t\t\t\n\t\t\t//\"random\" decision based on x coord\n\t\t\tboolean mirrored = (long)(pos.getX()) % 2 == 0;\n\t\t\t\n\t\t\tMaterial material = species == TreeSpecies.APPLE_TREE\n\t\t\t\t\t? Materials.TREE_BILLBOARD_BROAD_LEAVED_FRUIT\n\t\t\t\t\t: leafType == LeafType.NEEDLELEAVED\n\t\t\t\t\t? Materials.TREE_BILLBOARD_CONIFEROUS\n\t\t\t\t\t: Materials.TREE_BILLBOARD_BROAD_LEAVED;\n\t\t\t\n\t\t\tWorldModuleBillboardUtil.renderCrosstree(target, material, pos,\n\t\t\t\t\t(species != null ? 1.0 : 0.5 ) * height, height, mirrored);\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\trenderTreeGeometry(target, pos, leafType, height);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static void renderTreeGeometry(Target<?> target,\n\t\t\tVectorXYZ posXYZ, LeafType leafType, double height) {\n\t\t\n\t\tboolean coniferous = (leafType == LeafType.NEEDLELEAVED);\n\t\t\n\t\tdouble stemRatio = coniferous?0.3:0.5;\n\t\tdouble radius = height*TREE_RADIUS_PER_HEIGHT;\n\t\t\n\t\ttarget.drawColumn(Materials.TREE_TRUNK,\n\t\t\t\tnull, posXYZ, height*stemRatio,\n\t\t\t\tradius / 4, radius / 5, false, true);\n\t\t\n\t\ttarget.drawColumn(Materials.TREE_CROWN,\n\t\t\t\tnull, posXYZ.y(posXYZ.y+height*stemRatio),\n\t\t\t\theight*(1-stemRatio),\n\t\t\t\tradius,\n\t\t\t\tconiferous ? 0 : radius,\n\t\t\t\ttrue, true);\n\t}\n\t\n\t/**\n\t * parse height (for forests, add some random factor)\n\t */\n\tprivate double getTreeHeight(MapElement element,\n\t\t\tboolean isConiferousTree, boolean isFruitTree) {\n\t\t\n\t\tfloat heightFactor = 1;\n\t\tif (element instanceof MapArea) {\n\t\t\theightFactor = 0.5f + 0.75f * (float)Math.random();\n\t\t}\n\t\t\n\t\tdouble defaultHeight = defaultTreeHeight;\n\t\tif (element instanceof MapArea && !isFruitTree) {\n\t\t\tdefaultHeight = defaultTreeHeightForest;\n\t\t}\n\t\t\n\t\treturn heightFactor *\n\t\t\t\tparseHeight(element.getTags(), (float)defaultHeight);\n\t\t\n\t}\n\t\n\tprivate POVRayTarget previousDeclarationTarget = null;\n\t\n\tprivate void addTreeDeclarationsTo(POVRayTarget target) {\n\t\tif (target != previousDeclarationTarget) {\n\t\t\n\t\t\t//TODO support any combination of leaf type and leaf cycle\n\t\t\t\n\t\t\tpreviousDeclarationTarget = target;\n\t\t\t\n\t\t\ttarget.append(\"#ifndef (broad_leaved_tree)\\n\");\n\t\t\ttarget.append(\"#declare broad_leaved_tree = object { union {\\n\");\n\t\t\trenderTreeGeometry(target, VectorXYZ.NULL_VECTOR, LeafType.BROADLEAVED, 1);\n\t\t\ttarget.append(\"} }\\n#end\\n\\n\");\n\t\t\t\n\t\t\ttarget.append(\"#ifndef (coniferous_tree)\\n\");\n\t\t\ttarget.append(\"#declare coniferous_tree = object { union {\\n\");\n\t\t\trenderTreeGeometry(target, VectorXYZ.NULL_VECTOR, LeafType.NEEDLELEAVED, 1);\n\t\t\ttarget.append(\"} }\\n#end\\n\\n\");\n\t\t\t\n\t\t}\n\t}\n\n\tprivate void renderTree(POVRayTarget target, MapElement element, VectorXYZ pos,\n\t\t\tLeafType leafType, LeafCycle leafCycle, TreeSpecies species) {\n\t\t\n\t\tboolean isConiferousTree = (leafType == LeafType.NEEDLELEAVED);\n\t\t\n\t\tdouble height = getTreeHeight(element, isConiferousTree, false);\n\t\t\n\t\t//rotate randomly for variation\n\t\tfloat yRotation = (float) Math.random() * 360;\n\t\t\n\t\t//add union of stem and leaves\n\t\tif (isConiferousTree) {\n\t\t\ttarget.append(\"object { coniferous_tree rotate \");\n\t\t} else {\n\t\t\ttarget.append(\"object { broad_leaved_tree rotate \");\n\t\t}\n\t\t\n\t\ttarget.append(Float.toString(yRotation));\n\t\ttarget.append(\"*y scale \");\n\t\ttarget.append(height);\n\t\ttarget.append(\" translate \");\n\t\ttarget.appendVector(pos.x, 0, pos.z);\n\t\ttarget.append(\" }\\n\");\n\t\t\n\t}\n\t\n\tpublic class Tree extends NoOutlineNodeWorldObject\n\t\timplements RenderableToAllTargets, RenderableToPOVRay {\n\t\t\n\t\tprivate final LeafType leafType;\n\t\tprivate final LeafCycle leafCycle;\n\t\tprivate final TreeSpecies species;\n\t\t\n\t\tpublic Tree(MapNode node) {\n\t\t\t\n\t\t\tsuper(node);\n\t\t\t\n\t\t\tleafType = LeafType.getValue(node.getTags());\n\t\t\tleafCycle = LeafCycle.getValue(node.getTags());\n\t\t\tspecies = TreeSpecies.getValue(node.getTags());\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\t\treturn new AxisAlignedBoundingBoxXZ(Collections.singleton(node.getPos()));\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\trenderTree(target, node, getBase(), leafType, leafCycle, species);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void addDeclarationsTo(POVRayTarget target) {\n\t\t\taddTreeDeclarationsTo(target);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(POVRayTarget target) {\n\t\t\trenderTree(target, node, getBase(), leafType, leafCycle, species);\n\t\t}\n\t\t\n\t}\n\n\tpublic class TreeRow implements WaySegmentWorldObject,\n\t\tRenderableToPOVRay, RenderableToFaceTarget, RenderableToAllTargets {\n\n\t\tprivate final MapWaySegment segment;\n\n\t\tprivate final List<EleConnector> treeConnectors;\n\n\t\tprivate final LeafType leafType;\n\t\tprivate final LeafCycle leafCycle;\n\t\tprivate final TreeSpecies species;\n\t\t\n\t\tpublic TreeRow(MapWaySegment segment) {\n\t\t\t\n\t\t\tthis.segment = segment;\n\t\t\t\n\t\t\t/* determine details about the trees in the row */\n\t\t\t\n\t\t\tleafType = LeafType.getValue(segment.getTags());\n\t\t\tleafCycle = LeafCycle.getValue(segment.getTags());\n\t\t\tspecies = TreeSpecies.getValue(segment.getTags());\n\t\t\t\n\t\t\t/* add connectors for the trees' positions */\n\t\t\t\n\t\t\t//TODO: spread along a full way\n\t\t\t\n\t\t\tList<VectorXZ> treePositions = GeometryUtil.equallyDistributePointsAlong(\n\t\t\t\t\t4 /* TODO: derive from tree count */ ,\n\t\t\t\t\tfalse /* TODO: should be true once a full way is covered */,\n\t\t\t\t\tsegment.getStartNode().getPos(), segment.getEndNode().getPos());\n\t\t\t\n\t\t\ttreeConnectors = new ArrayList<EleConnector>(treePositions.size());\n\t\t\t\n\t\t\tfor (VectorXZ treePosition : treePositions) {\n\t\t\t\ttreeConnectors.add(\n\t\t\t\t\t\tnew EleConnector(treePosition, null, getGroundState()));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic MapWaySegment getPrimaryMapElement() {\n\t\t\treturn segment;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Iterable<EleConnector> getEleConnectors() {\n\t\t\treturn treeConnectors;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {}\n\t\t\n\t\t@Override\n\t\tpublic VectorXZ getEndPosition() {\n\t\t\treturn segment.getEndNode().getPos();\n\t\t}\n\n\t\t@Override\n\t\tpublic VectorXZ getStartPosition() {\n\t\t\treturn segment.getStartNode().getPos();\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(POVRayTarget target) {\n\t\t\tfor (EleConnector treeConnector : treeConnectors) {\n\t\t\t\trenderTree(target, segment, treeConnector.getPosXYZ(),\n\t\t\t\t\t\tleafType, leafCycle, species);\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void addDeclarationsTo(POVRayTarget target) {\n\t\t\taddTreeDeclarationsTo(target);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(FaceTarget<?> target) {\n\t\t\tfor (EleConnector treeConnector : treeConnectors) {\n\t\t\t\trenderTree(target, segment, treeConnector.getPosXYZ(),\n\t\t\t\t\t\tleafType, leafCycle, species);\n\t\t\t\ttarget.flushReconstructedFaces();\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\tfor (EleConnector treeConnector : treeConnectors) {\n\t\t\t\trenderTree(target, segment, treeConnector.getPosXYZ(),\n\t\t\t\t\t\tleafType, leafCycle, species);\n\t\t\t}\n\t\t}\n\t\t\n\t\t//TODO: there is significant code duplication with Forest...\n\t\t\n\t}\n\t\n\n\tpublic class Forest implements AreaWorldObject,\n\t\tRenderableToPOVRay, RenderableToFaceTarget, RenderableToAllTargets {\n\n\t\tprivate final MapArea area;\n\t\tprivate final MapData mapData;\n\t\t\n\t\tprivate Collection<EleConnector> treeConnectors = null;\n\n\t\tprivate final LeafType leafType;\n\t\tprivate final LeafCycle leafCycle;\n\t\tprivate final TreeSpecies species;\n\t\t\n\t\tpublic Forest(MapArea area, MapData mapData) {\n\t\t\t\n\t\t\tthis.area = area;\n\t\t\tthis.mapData = mapData;\n\n\t\t\tleafType = LeafType.getValue(area.getTags());\n\t\t\tleafCycle = LeafCycle.getValue(area.getTags());\n\t\t\tspecies = TreeSpecies.getValue(area.getTags());\n\t\t\t\n\t\t}\n\n\t\tprivate void createTreeConnectors(double density) {\n\t\t\t\n\t\t\t/* collect other objects that the trees should not be placed on */\n\t\t\t\n\t\t\tCollection<WorldObject> avoidedObjects = new ArrayList<WorldObject>();\n\t\t\t\n\t\t\tfor (MapOverlap<?, ?> overlap : area.getOverlaps()) {\n\t\t\t\tfor (WorldObject otherRep : overlap.getOther(area).getRepresentations()) {\n\t\t\t\t\t\n\t\t\t\t\tif (otherRep.getGroundState() == GroundState.ON) {\n\t\t\t\t\t\tavoidedObjects.add(otherRep);\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* place the trees */\n\t\t\t\n\t\t\tList<VectorXZ> treePositions =\n\t\t\t\tGeometryUtil.distributePointsOn(area.getOsmObject().id,\n\t\t\t\t\t\tarea.getPolygon(), mapData.getBoundary(),\n\t\t\t\t\t\tdensity, 0.3f);\n\t\t\t\n\t\t\tfilterWorldObjectCollisions(treePositions, avoidedObjects);\n\t\t\t\n\t\t\t/* create a terrain connector for each tree */\n\t\t\t\n\t\t\ttreeConnectors = new ArrayList<EleConnector>(treePositions.size());\n\t\t\t\n\t\t\tfor (VectorXZ treePosition : treePositions) {\n\t\t\t\ttreeConnectors.add(new EleConnector(\n\t\t\t\t\t\ttreePosition, null, getGroundState()));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic MapArea getPrimaryMapElement() {\n\t\t\treturn area;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Iterable<EleConnector> getEleConnectors() {\n\t\t\t\n\t\t\tif (treeConnectors == null) {\n\t\t\t\tcreateTreeConnectors(config.getDouble(\"treesPerSquareMeter\", 0.01f));\n\t\t\t}\n\t\t\t\n\t\t\treturn treeConnectors;\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {}\n\t\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(POVRayTarget target) {\n\t\t\tfor (EleConnector treeConnector : treeConnectors) {\n\t\t\t\trenderTree(target, area, treeConnector.getPosXYZ(),\n\t\t\t\t\t\tleafType, leafCycle, species);\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void addDeclarationsTo(POVRayTarget target) {\n\t\t\taddTreeDeclarationsTo(target);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(FaceTarget<?> target) {\n\t\t\tfor (EleConnector treeConnector : treeConnectors) {\n\t\t\t\trenderTree(target, area, treeConnector.getPosXYZ(),\n\t\t\t\t\t\tleafType, leafCycle, species);\n\t\t\t\ttarget.flushReconstructedFaces();\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\tfor (EleConnector treeConnector : treeConnectors) {\n\t\t\t\trenderTree(target, area, treeConnector.getPosXYZ(),\n\t\t\t\t\t\tleafType, leafCycle, species);\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/TunnelModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.emptyList;\nimport static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.EXACT;\nimport static org.osm2world.core.map_elevation.data.GroundState.ON;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.createTriangleStripBetween;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.osm2world.core.map_data.data.MapAreaSegment;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapSegment;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.map_elevation.data.EleConnectorGroup;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.NodeWorldObject;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.data.WaySegmentWorldObject;\nimport org.osm2world.core.world.modules.common.AbstractModule;\nimport org.osm2world.core.world.modules.common.BridgeOrTunnel;\nimport org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject;\nimport org.osm2world.core.world.network.JunctionNodeWorldObject;\nimport org.osm2world.core.world.network.VisibleConnectorNodeWorldObject;\n\n/**\n * adds tunnels to the world.\n * \n * Needs to be applied <em>after</em> all the modules that generate\n * whatever runs through the tunnels.\n */\npublic class TunnelModule extends AbstractModule {\n\n\tpublic static final boolean isTunnel(TagGroup tags) {\n\t\treturn tags.containsKey(\"tunnel\")\n\t\t\t\t&& !\"no\".equals(tags.getValue(\"tunnel\"))\n\t\t\t\t&& !\"building_passage\".equals(tags.getValue(\"tunnel\"));\n\t}\n\n\tpublic static final boolean isTunnel(MapSegment segment) {\n\t\tif (segment instanceof MapWaySegment) {\n\t\t\treturn isTunnel(((MapWaySegment)segment).getTags());\n\t\t} else {\n\t\t\treturn isTunnel(((MapAreaSegment)segment).getArea().getTags());\n\t\t}\n\t}\n\t\n\t@Override\n\tprotected void applyToWaySegment(MapWaySegment segment) {\n\t\t\n\t\tWaySegmentWorldObject primaryRepresentation =\n\t\t\tsegment.getPrimaryRepresentation();\n\t\t\n\t\tif (primaryRepresentation instanceof AbstractNetworkWaySegmentWorldObject\n\t\t\t\t&& isTunnel(segment)) {\n\t\t\t\n\t\t\tsegment.addRepresentation(new Tunnel(segment,\n\t\t\t\t\t(AbstractNetworkWaySegmentWorldObject) primaryRepresentation));\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tprotected void applyToNode(MapNode node) {\n\t\t\n\t\t/* entrances */\n\t\t\n\t\tif (node.getConnectedWaySegments().size() == 2) {\n\t\t\t\n\t\t\tMapWaySegment segmentA = node.getConnectedWaySegments().get(0);\n\t\t\tMapWaySegment segmentB = node.getConnectedWaySegments().get(1);\n\t\t\t\n\t\t\tif (isTunnel(segmentA) && !isTunnel(segmentB)\n\t\t\t\t\t&& segmentA.getPrimaryRepresentation() instanceof AbstractNetworkWaySegmentWorldObject) {\n\t\t\t\t\n\t\t\t\tnode.addRepresentation(new TunnelEntrance(node,\n\t\t\t\t\t\t(AbstractNetworkWaySegmentWorldObject)\n\t\t\t\t\t\tsegmentA.getPrimaryRepresentation()));\n\t\t\t\t\n\t\t\t} else if (isTunnel(segmentB) && !isTunnel(segmentA)\n\t\t\t\t\t&& segmentB.getPrimaryRepresentation() instanceof AbstractNetworkWaySegmentWorldObject) {\n\t\t\t\t\n\t\t\t\tnode.addRepresentation(new TunnelEntrance(node,\n\t\t\t\t\t\t(AbstractNetworkWaySegmentWorldObject)\n\t\t\t\t\t\tsegmentB.getPrimaryRepresentation()));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* tunnel nodes and junctions */\n\t\t\n\t\tboolean onlyTunnelConnected = true;\n\t\tfor (MapWaySegment segment : node.getConnectedWaySegments()) {\n\t\t\tif (!isTunnel(segment)) {\n\t\t\t\tonlyTunnelConnected = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t\t\n\t\tif (onlyTunnelConnected) {\n\n\t\t\tif (node.getPrimaryRepresentation() instanceof VisibleConnectorNodeWorldObject) {\n\t\t\t\t//TODO: TunnelConnector\n\t\t\t} else if (node.getPrimaryRepresentation() instanceof JunctionNodeWorldObject) {\n\t\t\t\tnode.addRepresentation(new TunnelJunction(node,\n\t\t\t\t\t\t(JunctionNodeWorldObject) node.getPrimaryRepresentation()));\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\t\n\t}\n\t\n\tpublic static class Tunnel extends BridgeOrTunnel\n\t\t\timplements RenderableToAllTargets {\n\t\t\n\t\tpublic Tunnel(MapWaySegment segment,\n\t\t\t\tAbstractNetworkWaySegmentWorldObject primaryWO) {\n\t\t\tsuper(segment, primaryWO);\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.BELOW;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\tList<VectorXYZ> leftOutline = primaryRep.getOutline(false);\n\t\t\tList<VectorXYZ> rightOutline = primaryRep.getOutline(true);\n\t\t\t\n\t\t\tList<VectorXYZ> aboveLeftOutline =\n\t\t\t\tnew ArrayList<VectorXYZ>(leftOutline.size());\n\t\t\tList<VectorXYZ> aboveRightOutline =\n\t\t\t\tnew ArrayList<VectorXYZ>(rightOutline.size());\n\t\t\t\n\t\t\tfor (int i=0; i < leftOutline.size(); i++) {\n\t\t\t\n\t\t\t\tVectorXYZ clearingOffset = VectorXYZ.Y_UNIT.mult(\n\t\t\t\t\t\t10); //TODO restore clearing\n//\t\t\t\t\t\tprimaryRep.getClearingAbove(leftOutline.get(i).xz()));\n\t\t\t\t\n\t\t\t\taboveLeftOutline.add(leftOutline.get(i).add(clearingOffset));\n\t\t\t\taboveRightOutline.add(rightOutline.get(i).add(clearingOffset));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tList<VectorXYZ> strip1 = createTriangleStripBetween(\n\t\t\t\t\trightOutline, aboveRightOutline);\n\t\t\tList<VectorXYZ> strip2 = createTriangleStripBetween(\n\t\t\t\t\taboveRightOutline, aboveLeftOutline);\n\t\t\tList<VectorXYZ> strip3 = createTriangleStripBetween(\n\t\t\t\t\taboveLeftOutline, leftOutline);\n\t\t\t\n\t\t\ttarget.drawTriangleStrip(Materials.TUNNEL_DEFAULT, strip1, null);\n\t\t\ttarget.drawTriangleStrip(Materials.TUNNEL_DEFAULT, strip2, null);\n\t\t\ttarget.drawTriangleStrip(Materials.TUNNEL_DEFAULT, strip3, null);\n\t\t\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class TunnelEntrance implements NodeWorldObject,\n\t\tTerrainBoundaryWorldObject {\n\t\t\n\t\tprivate final MapNode node;\n\t\tprivate final AbstractNetworkWaySegmentWorldObject tunnelContent;\n\n\t\t/**\n\t\t * counterclockwise outline composed of\n\t\t * {@link #lowerLeft}, {@link #lowerCenter}, {@link #lowerRight},\n\t\t * {@link #upperRight}, {@link #upperCenter} and {@link #upperLeft}.\n\t\t */\n\t\tprivate SimplePolygonXZ outline;\n\t\t\n\t\tprivate VectorXZ lowerLeft;\n\t\tprivate VectorXZ lowerCenter;\n\t\tprivate VectorXZ lowerRight;\n\t\tprivate VectorXZ upperLeft;\n\t\tprivate VectorXZ upperCenter;\n\t\tprivate VectorXZ upperRight;\n\t\t\n\t\tprivate EleConnectorGroup connectors;\n\t\t\n\t\tpublic TunnelEntrance(MapNode node,\n\t\t\t\tAbstractNetworkWaySegmentWorldObject tunnelContent) {\n\t\t\t\n\t\t\tthis.node = node;\n\t\t\tthis.tunnelContent = tunnelContent;\n\t\t\t\n\t\t}\n\t\t\n\t\t/**\n\t\t * creates outline as \"ring\" around the entrance\n\t\t */\n\t\tprivate void calculateOutlineIfNecessary() {\n\t\t\t\n\t\t\tif (outline != null) return;\n\n\t\t\tlowerCenter = node.getPos();\n\t\t\t\n\t\t\tVectorXZ toRight = tunnelContent.getStartCutVector()\n\t\t\t\t\t.mult(tunnelContent.getWidth() * 0.5f);\n\t\t\t\n\t\t\tlowerLeft = lowerCenter.subtract(toRight);\n\t\t\tlowerRight = lowerCenter.add(toRight);\n\t\t\t\n\t\t\tVectorXZ toBack = tunnelContent.segment.getDirection().mult(0.1);\n\t\t\tif (tunnelContent.segment.getEndNode() == node) {\n\t\t\t\ttoBack = toBack.invert();\n\t\t\t}\n\t\t\t\n\t\t\tupperLeft = lowerLeft.add(toBack);\n\t\t\tupperCenter = lowerCenter.add(toBack);\n\t\t\tupperRight = lowerRight.add(toBack);\n\t\t\t\n\t\t\toutline = new SimplePolygonXZ(asList(\n\t\t\t\t\tlowerLeft, lowerCenter, lowerRight,\n\t\t\t\t\tupperRight, upperCenter, upperLeft,\n\t\t\t\t\tlowerLeft));\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic MapNode getPrimaryMapElement() {\n\t\t\treturn node;\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Iterable<EleConnector> getEleConnectors() {\n\t\t\t\n\t\t\tcalculateOutlineIfNecessary();\n\t\t\t\n\t\t\tif (connectors == null) {\n\t\t\t\tconnectors = new EleConnectorGroup();\n\t\t\t\tconnectors.add(new EleConnector(lowerLeft, node, ON));\n\t\t\t\tconnectors.add(new EleConnector(lowerCenter, node, ON));\n\t\t\t\tconnectors.add(new EleConnector(lowerRight, node, ON));\n\t\t\t\tconnectors.add(new EleConnector(upperLeft, null, ON));\n\t\t\t\tconnectors.add(new EleConnector(upperCenter, null, ON));\n\t\t\t\tconnectors.add(new EleConnector(upperRight, null, ON));\n\t\t\t}\n\t\t\t\n\t\t\treturn connectors;\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\t\n\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\tEXACT,\n\t\t\t\t\t10,\n\t\t\t\t\tconnectors.getConnector(upperLeft), connectors.getConnector(lowerLeft));\n\t\t\t\n\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\tEXACT,\n\t\t\t\t\t10,\n\t\t\t\t\tconnectors.getConnector(upperCenter), connectors.getConnector(lowerCenter));\n\n\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\tEXACT,\n\t\t\t\t\t10,\n\t\t\t\t\tconnectors.getConnector(upperRight), connectors.getConnector(lowerRight));\n\t\t\t\n\t\t\t//TODO restore original clearing\n\t\t\t//tunnelPrimaryRep.getClearingAbove(node.getPos()));\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\t\t\n\t\t\tcalculateOutlineIfNecessary();\n\t\t\t\n\t\t\treturn new AxisAlignedBoundingBoxXZ(getOutlinePolygon().getVertices());\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic SimplePolygonXZ getOutlinePolygonXZ() {\n\t\t\t\n\t\t\tcalculateOutlineIfNecessary();\n\t\t\t\n\t\t\treturn outline;\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic PolygonXYZ getOutlinePolygon() {\n\t\t\t\n\t\t\tcalculateOutlineIfNecessary();\n\t\t\t\n\t\t\treturn connectors.getPosXYZ(getOutlinePolygonXZ());\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class TunnelJunction implements NodeWorldObject,\n\t\tRenderableToAllTargets {\n\t\n\t\tprivate final MapNode node;\n\t\tprivate final JunctionNodeWorldObject primaryRep;\n\t\t\n\t\tpublic TunnelJunction(MapNode node, JunctionNodeWorldObject primaryRep) {\n\t\t\tthis.node = node;\n\t\t\tthis.primaryRep = primaryRep;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic MapNode getPrimaryMapElement() {\n\t\t\treturn node;\n\t\t}\n\t\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.BELOW;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic Iterable<EleConnector> getEleConnectors() {\n\t\t\t// TODO EleConnectors for tunnels\n\t\t\treturn emptyList();\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t//TODO port to new elevation model\n\t\t\t\n//\t\t\tList<VectorXYZ> topOutline = new ArrayList<VectorXYZ>();\n//\n//\t\t\tint segCount = node.getConnectedSegments().size();\n//\t\t\tfor (int i=0; i<segCount; i++) {\n//\n//\t\t\t\tList<VectorXYZ> line = primaryRep.getOutline((i+1)%segCount, i);\n//\n//\t\t\t\tList<VectorXYZ> lineTop =\n//\t\t\t\t\tnew ArrayList<VectorXYZ>(line.size());\n//\n//\t\t\t\tfor (VectorXYZ lineV : line) {\n//\n//\t\t\t\t\tdouble clearing;\n//\n//\t\t\t\t\tif (line.indexOf(lineV) == 0) {\n//\t\t\t\t\t\tMapSegment segment = node.getConnectedSegments().get((i+1)%segCount);\n//\t\t\t\t\t\tclearing = 10; //TODO clearingAboveMapSegment(lineV, segment);\n//\t\t\t\t\t} else {\n//\t\t\t\t\t\tMapSegment segment = node.getConnectedSegments().get(i);\n//\t\t\t\t\t\tclearing = 10; //TODO clearingAboveMapSegment(lineV, segment);\n//\t\t\t\t\t}\n//\n//\t\t\t\t\tlineTop.add(lineV.y(lineV.y + clearing));\n//\n//\t\t\t\t}\n//\n//\t\t\t\t// draw wall\n//\n//\t\t\t\ttarget.drawTriangleStrip(Materials.TUNNEL_DEFAULT,\n//\t\t\t\t\t\tcreateTriangleStripBetween(line, lineTop), null);\n//\n//\t\t\t\t//collect nodes for top outline\n//\n//\t\t\t\ttopOutline.addAll(lineTop);\n//\n//\t\t\t}\n//\n//\t\t\t// draw top\n//\n//\t\t\ttarget.drawConvexPolygon(Materials.TUNNEL_DEFAULT, topOutline,\n//\t\t\t\t\tglobalTexCoordLists(\n//\t\t\t\t\t\t\ttopOutline, Materials.TUNNEL_DEFAULT, false));\n//\n\t\t}\n\n\t\t//TODO update or delete\n//\t\tprivate static double clearingAboveMapSegment(VectorXYZ lineV, MapSegment segment) {\n//\n//\t\t\tWorldObject segmentRep;\n//\t\t\tif (segment instanceof MapWaySegment) {\n//\t\t\t\tsegmentRep = ((MapWaySegment)segment)\n//\t\t\t\t\t.getPrimaryRepresentation();\n//\t\t\t} else {\n//\t\t\t\tsegmentRep = ((MapAreaSegment)segment)\n//\t\t\t\t\t.getArea().getPrimaryRepresentation();\n//\t\t\t}\n//\n//\t\t\tif (segmentRep != null) {\n//\t\t\t\treturn segmentRep.getClearingAbove(lineV.xz());\n//\t\t\t} else {\n//\t\t\t\treturn 0;\n//\t\t\t}\n//\n//\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/WaterModule.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static com.google.common.collect.Iterables.any;\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.nCopies;\nimport static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.MAX;\nimport static org.osm2world.core.target.common.material.Materials.*;\nimport static org.osm2world.core.target.common.material.NamedTexCoordFunction.*;\nimport static org.osm2world.core.target.common.material.TexCoordUtil.*;\nimport static org.osm2world.core.util.Predicates.hasType;\nimport static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.*;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.RenderableToAllTargets;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.modules.common.ConfigurableWorldModule;\nimport org.osm2world.core.world.modules.common.WorldModuleParseUtil;\nimport org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject;\nimport org.osm2world.core.world.network.JunctionNodeWorldObject;\nimport org.osm2world.core.world.network.NetworkAreaWorldObject;\n\n/**\n * adds water bodies, streams, rivers and fountains to the world\n */\npublic class WaterModule extends ConfigurableWorldModule {\n\n\t//TODO: add canal, ditch, drain\n\n\tprivate static final float MIN_WATER_WIDTH = 2.2f;\n\tprivate static final Tag WATER_TAG = new Tag(\"natural\", \"water\");\n\tprivate static final Tag RIVERBANK_TAG = new Tag(\"waterway\", \"riverbank\");\n\tprivate static final Tag BAY_TAG = new Tag(\"natural\", \"bay\");\n\tprivate static final Tag RESERVOIR_TAG = new Tag(\"landuse\", \"reservoir\");\n\tprivate static final Tag BASIN_TAG = new Tag(\"landuse\", \"basin\");\n\tprivate static final Tag OCEAN_TAG = new Tag(\"ocean\", \"yes\");\n\t\n\tprivate static final Map<String, Float> WATERWAY_WIDTHS;\n\t\n\tstatic {\n\t\tWATERWAY_WIDTHS = new HashMap<String, Float>();\n\t\tWATERWAY_WIDTHS.put(\"river\", MIN_WATER_WIDTH * 1.5f);\n\t\tWATERWAY_WIDTHS.put(\"stream\", MIN_WATER_WIDTH);\n\t\tWATERWAY_WIDTHS.put(\"canal\", MIN_WATER_WIDTH * 1.2f);\n\t\tWATERWAY_WIDTHS.put(\"ditch\", MIN_WATER_WIDTH);\n\t\tWATERWAY_WIDTHS.put(\"drain\", MIN_WATER_WIDTH);\n\t}\n\t\n\t//TODO: apply to is almost always the same! create a superclass handling this!\n\t\n\t@Override\n\tpublic void applyTo(MapData mapData) {\n\t\t\n\t\tfor (MapWaySegment line : mapData.getMapWaySegments()) {\n\t\t\tfor (String value : WATERWAY_WIDTHS.keySet()) {\n\t\t\t\tif (line.getTags().contains(\"waterway\", value)) {\n\t\t\t\t\tline.addRepresentation(new Waterway(line));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (MapNode node : mapData.getMapNodes()) {\n\t\t\t\n\t\t\tint connectedRivers = 0;\n\t\t\t\n\t\t\tfor (MapWaySegment line : node.getConnectedWaySegments()) {\n\t\t\t\tif (any(line.getRepresentations(), hasType(Waterway.class))) {\n\t\t\t\t\tconnectedRivers += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\t\t\t\n\t\t\tif (connectedRivers > 2) {\n\t\t\t\tnode.addRepresentation(new RiverJunction(node));\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tfor (MapArea area : mapData.getMapAreas()) {\n\t\t\tif (isWaterArea(area)) {\n\t\t\t\tarea.addRepresentation(new Water(area));\n\t\t\t}\n\t\t\tif (area.getTags().contains(\"amenity\", \"fountain\")) {\n\t\t\t\tarea.addRepresentation(new AreaFountain(area));\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\n\tprivate static boolean isWaterArea(MapArea area) {\n\t\treturn area.getTags().contains(WATER_TAG)\n\t\t\t\t|| area.getTags().contains(RIVERBANK_TAG)\n\t\t\t\t|| area.getTags().contains(BAY_TAG)\n\t\t\t\t|| area.getTags().contains(RESERVOIR_TAG)\n\t\t\t\t|| area.getTags().contains(BASIN_TAG)\n\t\t\t\t|| area.getTags().contains(OCEAN_TAG);\n\t}\n\t\n\tpublic static class Waterway extends AbstractNetworkWaySegmentWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\t\t\n\t\tpublic Waterway(MapWaySegment line) {\n\t\t\tsuper(line);\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\t\n\t\t\tsuper.defineEleConstraints(enforcer);\n\t\t\t\n\t\t\t/* enforce downhill flow */\n\t\t\t\n\t\t\tif (!segment.getTags().containsKey(\"incline\")) {\n\t\t\t\tenforcer.requireIncline(MAX, 0, getCenterlineEleConnectors());\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tpublic float getWidth() {\n\t\t\treturn 2 * Math.max(\n\t\t\t\t\t1,\n\t\t\t\t\tWorldModuleParseUtil.parseWidth(segment.getTags(),\n\t\t\t\t\tWATERWAY_WIDTHS.get(segment.getTags().getValue(\"waterway\"))));\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic PolygonXYZ getOutlinePolygon() {\n\t\t\tif (isContainedWithinRiverbank()) {\n\t\t\t\treturn null;\n\t\t\t} else {\n\t\t\t\treturn super.getOutlinePolygon();\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic SimplePolygonXZ getOutlinePolygonXZ() {\n\t\t\tif (isContainedWithinRiverbank()) {\n\t\t\t\treturn null;\n\t\t\t} else {\n\t\t\t\treturn super.getOutlinePolygonXZ();\n\t\t\t}\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t//note: simply \"extending\" a river cannot work - unlike streets -\n\t\t\t//      because there can be islands within the riverbank polygon.\n\t\t\t//      That's why rivers will be *replaced* with Water areas instead.\n\t\t\t\n\t\t\t/* only draw the river if it doesn't have a riverbank */\n\t\t\t\n\t\t\t//TODO: handle case where a river is completely within riverbanks, but not a *single* riverbank\n\t\t\t\t\t\t\n\t\t\tif (! isContainedWithinRiverbank()) {\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> leftOutline = getOutline(false);\n\t\t\t\tList<VectorXYZ> rightOutline = getOutline(true);\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> leftWaterBorder = createLineBetween(\n\t\t\t\t\t\tleftOutline, rightOutline, 0.05f);\n\t\t\t\tList<VectorXYZ> rightWaterBorder = createLineBetween(\n\t\t\t\t\t\tleftOutline, rightOutline, 0.95f);\n\t\t\t\t\n//\t\t\t\tmodifyLineHeight(leftWaterBorder, -0.2f);\n//\t\t\t\tmodifyLineHeight(rightWaterBorder, -0.2f);\n//\t\n//\t\t\t\tList<VectorXYZ> leftGround = createLineBetween(\n//\t\t\t\t\t\tleftOutline, rightOutline, 0.35f);\n//\t\t\t\tList<VectorXYZ> rightGround = createLineBetween(\n//\t\t\t\t\t\tleftOutline, rightOutline, 0.65f);\n//\t\t\t\t\n//\t\t\t\tmodifyLineHeight(leftGround, -1);\n//\t\t\t\tmodifyLineHeight(rightGround, -1);\n//\t\t\t\t\n//\t\t\t\t/* render ground */\n//\t\t\t\t\n//\t\t\t\t@SuppressWarnings(\"unchecked\") // generic vararg is intentional\n//\t\t\t\tList<List<VectorXYZ>> strips = asList(\n//\t\t\t\t\tcreateTriangleStripBetween(\n//\t\t\t\t\t\t\tleftOutline, leftWaterBorder),\n//\t\t\t\t\tcreateTriangleStripBetween(\n//\t\t\t\t\t\t\tleftWaterBorder, leftGround),\n//\t\t\t\t\tcreateTriangleStripBetween(\n//\t\t\t\t\t\t\tleftGround, rightGround),\n//\t\t\t\t\tcreateTriangleStripBetween(\n//\t\t\t\t\t\t\trightGround, rightWaterBorder),\n//\t\t\t\t\tcreateTriangleStripBetween(\n//\t\t\t\t\t\t\trightWaterBorder, rightOutline)\n//\t\t\t\t);\n//\t\t\t\t\n//\t\t\t\tfor (List<VectorXYZ> strip : strips) {\n//\t\t\t\t\ttarget.drawTriangleStrip(TERRAIN_DEFAULT, strip,\n//\t\t\t\t\t\ttexCoordLists(strip, TERRAIN_DEFAULT, GLOBAL_X_Z));\n//\t\t\t\t}\n\t\t\t\t\n\t\t\t\t/* render water */\n\t\t\t\t\n\t\t\t\tList<VectorXYZ> vs = createTriangleStripBetween(\n\t\t\t\t\t\tleftWaterBorder, rightWaterBorder);\n\t\t\t\t\n\t\t\t\ttarget.drawTriangleStrip(WATER, vs,\n\t\t\t\t\t\ttexCoordLists(vs, WATER, GLOBAL_X_Z));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\tprivate boolean isContainedWithinRiverbank() {\n\t\t\tboolean containedWithinRiverbank = false;\n\t\t\t\n\t\t\tfor (MapOverlap<?,?> overlap : segment.getOverlaps()) {\n\t\t\t\tif (overlap.getOther(segment) instanceof MapArea) {\n\t\t\t\t\tMapArea area = (MapArea)overlap.getOther(segment);\n\t\t\t\t\tif (area.getPrimaryRepresentation() instanceof Water &&\n\t\t\t\t\t\t\tarea.getPolygon().contains(segment.getLineSegment())) {\n\t\t\t\t\t\tcontainedWithinRiverbank = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn containedWithinRiverbank;\n\t\t}\n\n\t\tprivate static void modifyLineHeight(List<VectorXYZ> leftWaterBorder, float yMod) {\n\t\t\tfor (int i = 0; i < leftWaterBorder.size(); i++) {\n\t\t\t\tVectorXYZ v = leftWaterBorder.get(i);\n\t\t\t\tleftWaterBorder.set(i, v.y(v.y+yMod));\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\n\tpublic static class RiverJunction\n\t\textends JunctionNodeWorldObject\n\t\timplements TerrainBoundaryWorldObject, RenderableToAllTargets {\n\n\t\tpublic RiverJunction(MapNode node) {\n\t\t\tsuper(node);\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\t\n\t\t\t//TODO: check whether it's within a riverbank (as with Waterway)\n\t\t\t\n\t\t\tList<VectorXYZ> vertices = getOutlinePolygon().getVertices();\n\t\t\t\n\t\t\ttarget.drawConvexPolygon(WATER, vertices,\n\t\t\t\t\ttexCoordLists(vertices, WATER, GLOBAL_X_Z));\n\t\t\t\n\t\t\t//TODO: only cover with water to 0.95 * distance to center; add land below\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class Water extends NetworkAreaWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\t\t\n\t\t//TODO: only cover with water to 0.95 * distance to center; add land below.\n\t\t// possible algorithm: for each node of the outer polygon, check whether it\n\t\t// connects to another water surface. If it doesn't move it inwards,\n\t\t// where \"inwards\" is calculated based on the two adjacent polygon segments.\n\t\t\n\t\tpublic Water(MapArea area) {\n\t\t\tsuper(area);\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\tenforcer.requireSameEle(getEleConnectors());\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\t\t\tCollection<TriangleXYZ> triangles = getTriangulation();\n\t\t\ttarget.drawTriangles(WATER, triangles,\n\t\t\t\t\ttriangleTexCoordLists(triangles, WATER, GLOBAL_X_Z));\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class AreaFountain extends AbstractAreaWorldObject\n\t\timplements RenderableToAllTargets, TerrainBoundaryWorldObject {\n\n\t\tpublic AreaFountain(MapArea area) {\n\t\t\tsuper(area);\n\t\t}\n\n\t\t@Override\n\t\tpublic GroundState getGroundState() {\n\t\t\treturn GroundState.ON;\n\t\t}\n\n\t\t@Override\n\t\tpublic void renderTo(Target<?> target) {\n\n\t\t\t/* render water */\n\t\t\t\t\n\t\t\tCollection<TriangleXYZ> triangles = getTriangulation();\n\t\t\ttarget.drawTriangles(PURIFIED_WATER, triangles,\n\t\t\t\t\ttriangleTexCoordLists(triangles, PURIFIED_WATER, GLOBAL_X_Z));\n\t\t\t\n\t\t\t/* render walls */\n\t\t\t//note: mostly copy-pasted from BarrierModule\n\t\t\t\n\t\t\tdouble width=0.1;\n\t\t\tdouble height=0.5;\n\t\t\t\n\t\t\tList<VectorXYZ> wallShape = asList(\n\t\t\t\t\tnew VectorXYZ(-width/2, 0, 0),\n\t\t\t\t\tnew VectorXYZ(-width/2, height, 0),\n\t\t\t\t\tnew VectorXYZ(+width/2, height, 0),\n\t\t\t\t\tnew VectorXYZ(+width/2, 0, 0)\n\t\t\t);\n\t\t\t\n\t\t\tList<VectorXYZ> path = getOutlinePolygon().getVertexLoop();\n\t\t\t\n\t\t\tList<List<VectorXYZ>> strips = createShapeExtrusionAlong(\n\t\t\t\t\twallShape,\n\t\t\t\t\tpath,\n\t\t\t\t\tnCopies(path.size(), VectorXYZ.Y_UNIT));\n\t\t\t\n\t\t\tfor (List<VectorXYZ> strip : strips) {\n\t\t\t\ttarget.drawTriangleStrip(Materials.CONCRETE, strip,\n\t\t\t\t\t\ttexCoordLists(strip, Materials.CONCRETE, STRIP_WALL));\n\t\t\t}\n\t\t\t\t\t\t\t\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/common/AbstractModule.java",
    "content": "package org.osm2world.core.world.modules.common;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.world.creation.WorldModule;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * skeleton implementation for {@link WorldModule}s.\n * \n * Subclasses need to be able to create {@link WorldObject}s\n * for each {@link MapElement} in isolation.\n * This can make parallel application of the module possible.\n */\npublic abstract class AbstractModule extends ConfigurableWorldModule {\n\t\t\n\t@Override\n\tpublic final void applyTo(MapData grid) {\n\t\t\n\t\tfor (MapNode node : grid.getMapNodes()) {\n\t\t\tapplyToNode(node);\n\t\t}\n\n\t\tfor (MapWaySegment segment : grid.getMapWaySegments()) {\n\t\t\tapplyToWaySegment(segment);\n\t\t}\n\n\t\tfor (MapArea area : grid.getMapAreas()) {\n\t\t\tapplyToArea(area);\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * create {@link WorldObject}s for a {@link MapElement}.\n\t * Can be overwritten by subclasses.\n\t * The default implementation does not create any objects.\n\t */\n\tprotected void applyToElement(MapElement element) {}\n\t\n\t/**\n\t * create {@link WorldObject}s for a {@link MapNode}.\n\t * Can be overwritten by subclasses.\n\t * The default implementation calls {@link #applyToElement(MapElement)}.\n\t */\n\tprotected void applyToNode(MapNode node) {\n\t\tapplyToElement(node);\n\t}\n\t\n\t/**\n\t * create {@link WorldObject}s for a {@link MapWaySegment}.\n\t * Can be overwritten by subclasses.\n\t * The default implementation calls {@link #applyToElement(MapElement)}.\n\t */\n\tprotected void applyToWaySegment(MapWaySegment segment) {\n\t\tapplyToElement(segment);\n\t}\n\t\n\t/**\n\t * create {@link WorldObject}s for a {@link MapArea}.\n\t * Can be overwritten by subclasses.\n\t * The default implementation calls {@link #applyToElement(MapElement)}.\n\t */\n\tprotected void applyToArea(MapArea area) {\n\t\tapplyToElement(area);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/common/BridgeOrTunnel.java",
    "content": "package org.osm2world.core.world.modules.common;\n\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.emptyList;\nimport static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.*;\nimport static org.osm2world.core.map_elevation.data.GroundState.*;\nimport static org.osm2world.core.math.GeometryUtil.isBetween;\n\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapAreaSegment;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.data.overlaps.MapIntersectionWW;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapType;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapWA;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\nimport org.osm2world.core.world.data.WaySegmentWorldObject;\nimport org.osm2world.core.world.data.WorldObject;\nimport org.osm2world.core.world.modules.TreeModule.Forest;\nimport org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject;\n\n/**\n * common superclass for bridges and tunnels\n */\npublic abstract class BridgeOrTunnel implements WaySegmentWorldObject {\n\n\tprotected final MapWaySegment segment;\n\tprotected final AbstractNetworkWaySegmentWorldObject primaryRep;\n\t\n\tpublic BridgeOrTunnel(MapWaySegment segment,\n\t\t\tAbstractNetworkWaySegmentWorldObject primaryRepresentation) {\n\t\tthis.segment = segment;\n\t\tthis.primaryRep = primaryRepresentation;\n\t}\n\n\t@Override\n\tpublic MapWaySegment getPrimaryMapElement() {\n\t\treturn segment;\n\t}\n\n\t@Override\n\tpublic VectorXZ getEndPosition() {\n\t\treturn primaryRep.getEndPosition();\n\t}\n\n\t@Override\n\tpublic VectorXZ getStartPosition() {\n\t\treturn primaryRep.getStartPosition();\n\t}\n\t\n\t@Override\n\tpublic Iterable<EleConnector> getEleConnectors() {\n\t\treturn emptyList();\n\t}\n\t\n\t@Override\n\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tList<List<VectorXZ>> lines = asList(\n\t\t\t\tprimaryRep.getCenterlineXZ(),\n\t\t\t\tprimaryRep.getOutlineXZ(true),\n\t\t\t\tprimaryRep.getOutlineXZ(false));\n\t\t\n\t\tSimplePolygonXZ outlinePolygonXZ = primaryRep.getOutlinePolygonXZ();\n\t\t\n\t\t/* ensure a minimum vertical distance to ways and areas below,\n\t\t * at intersections */\n\t\t\n\t\tfor (MapOverlap<?,?> overlap : segment.getOverlaps()) {\n\t\t\t\n\t\t\tMapElement other = overlap.getOther(segment);\n\t\t\tWorldObject otherWO = other.getPrimaryRepresentation();\n\t\t\t\n\t\t\tif (otherWO == null\n\t\t\t\t\t|| otherWO.getGroundState() != ON)  //TODO remove the ground state check\n\t\t\t\tcontinue;\n\t\t\t\n\t\t\tboolean thisIsUpper = this.getGroundState() == ABOVE; //TODO check layers\n\t\t\t\n\t\t\tdouble distance = 10.0; //TODO base on clearing\n\t\t\t\n\t\t\tif (overlap instanceof MapIntersectionWW) {\n\t\t\t\t\n\t\t\t\tMapIntersectionWW intersection = (MapIntersectionWW) overlap;\n\t\t\t\t\n\t\t\t\tif (otherWO instanceof AbstractNetworkWaySegmentWorldObject) {\n\t\t\t\t\t\n\t\t\t\t\tAbstractNetworkWaySegmentWorldObject otherANWSWO =\n\t\t\t\t\t\t\t((AbstractNetworkWaySegmentWorldObject)otherWO);\n\t\t\t\t\t\n\t\t\t\t\tEleConnector thisConn = primaryRep.getEleConnectors()\n\t\t\t\t\t\t\t.getConnector(intersection.pos);\n\t\t\t\t\tEleConnector otherConn = otherANWSWO.getEleConnectors()\n\t\t\t\t\t\t\t.getConnector(intersection.pos);\n\t\t\t\t\t\n\t\t\t\t\tif (thisIsUpper) {\n\t\t\t\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\t\t\t\tMIN, distance, thisConn, otherConn);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\t\t\t\tMIN, distance, otherConn, thisConn);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t} else if (overlap instanceof MapOverlapWA) {\n\t\t\t\t\n\t\t\t\t/*\n\t\t\t\t * require minimum distance at intersection points\n\t\t\t\t * (these have been inserted into this segment,\n\t\t\t\t * but not into the area)\n\t\t\t\t */\n\t\t\t\t\n\t\t\t\tMapOverlapWA overlapWA = (MapOverlapWA) overlap;\n\t\t\t\t\n\t\t\t\tif (overlap.type == MapOverlapType.INTERSECT\n\t\t\t\t\t\t&& otherWO instanceof AbstractAreaWorldObject) {\n\t\t\t\t\t\n\t\t\t\t\tAbstractAreaWorldObject otherAAWO =\n\t\t\t\t\t\t\t((AbstractAreaWorldObject)otherWO);\n\t\t\t\t\t\n\t\t\t\t\tfor (int i = 0; i < overlapWA.getIntersectionPositions().size(); i++) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tVectorXZ pos =\n\t\t\t\t\t\t\t\toverlapWA.getIntersectionPositions().get(i);\n\t\t\t\t\t\tMapAreaSegment areaSegment =\n\t\t\t\t\t\t\t\toverlapWA.getIntersectingAreaSegments().get(i);\n\t\t\t\t\t\t\n\t\t\t\t\t\tEleConnector thisConn = primaryRep.getEleConnectors()\n\t\t\t\t\t\t\t\t.getConnector(pos);\n\t\t\t\t\t\t\n\t\t\t\t\t\tEleConnector base1 = otherAAWO.getEleConnectors()\n\t\t\t\t\t\t\t\t.getConnector(areaSegment.getStartNode().getPos());\n\t\t\t\t\t\tEleConnector base2 = otherAAWO.getEleConnectors()\n\t\t\t\t\t\t\t\t.getConnector(areaSegment.getEndNode().getPos());\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\tif (thisConn != null && base1 != null && base2 != null) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (thisIsUpper) {\n\t\t\t\t\t\t\t\tenforcer.requireVerticalDistance(MIN, distance,\n\t\t\t\t\t\t\t\t\t\tthisConn, base1, base2);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tenforcer.requireVerticalDistance(MAX, -distance,\n\t\t\t\t\t\t\t\t\t\tthisConn, base1, base2);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t/*\n\t\t\t\t * require minimum distance to the area's elevation connectors.\n\t\t\t\t * There is usually no direct counterpart for these in this segment.\n\t\t\t\t * Examples include trees on terrain above tunnels.\n\t\t\t\t */\n\t\t\t\t\n\t\t\t\tif (!(otherWO instanceof Forest)) continue; //TODO enable and debug for other WO classes\n\t\t\t\t\n\t\t\t\teleConnectors:\n\t\t\t\tfor (EleConnector c : otherWO.getEleConnectors()) {\n\t\t\t\t\t\n\t\t\t\t\tif (outlinePolygonXZ == null ||\n\t\t\t\t\t\t\t!outlinePolygonXZ.contains(c.pos))\n\t\t\t\t\t\tcontinue eleConnectors;\n\t\t\t\t\t\n\t\t\t\t\tfor (List<VectorXZ> line : lines) {\n\t\t\t\t\t\tfor (int i = 0; i+1 < line.size(); i++) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tVectorXZ v1 = line.get(i);\n\t\t\t\t\t\t\tVectorXZ v2 = line.get(i+1);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (isBetween(c.pos, v1, v2)) {\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tEleConnector base1 = primaryRep.getEleConnectors().getConnector(v1);\n\t\t\t\t\t\t\t\tEleConnector base2 = primaryRep.getEleConnectors().getConnector(v2);\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tif (base1 != null && base2 != null) {\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tif (thisIsUpper) {\n\t\t\t\t\t\t\t\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\t\t\t\t\t\t\t\tMAX, -distance,\n\t\t\t\t\t\t\t\t\t\t\t\tc, base1, base2);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tenforcer.requireVerticalDistance(\n\t\t\t\t\t\t\t\t\t\t\t\tMIN, distance,\n\t\t\t\t\t\t\t\t\t\t\t\tc, base1, base2);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tcontinue eleConnectors;\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn this.getClass().getSimpleName() + \"(\" + segment + \")\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/common/ConfigurableWorldModule.java",
    "content": "package org.osm2world.core.world.modules.common;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.world.creation.WorldModule;\n\n/**\n * simple superclass for {@link WorldModule}s that stores a configuration set by\n * {@link #setConfiguration(org.apache.commons.configuration.Configuration)}\n */\npublic abstract class ConfigurableWorldModule implements WorldModule {\n\t\n\tprotected Configuration config;\n\t\n\t@Override\n\tpublic void setConfiguration(Configuration config) {\n\t\tthis.config = config;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/common/WorldModuleBillboardUtil.java",
    "content": "package org.osm2world.core.world.modules.common;\n\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.nCopies;\n\nimport java.util.List;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.common.material.Material;\n\n/**\n * utility class for drawing billboards, particularly \"cross tree\" shapes\n */\npublic final class WorldModuleBillboardUtil {\n\n\tprivate WorldModuleBillboardUtil() { }\n\t\n\t/**\n\t * renders a \"cross tree\" shape.\n\t * \n\t * This shape is composed from two rectangular billboards, each with front\n\t * and back side, intersecting orthogonally. Each billboard consists of\n\t * two halves, separated at the billboard's intersection axis,\n\t * to allow sorting of primitives for transparent rendering.\n\t */\n\tpublic static final void renderCrosstree(Target<?> target,\n\t\t\tMaterial material, VectorXYZ pos,\n\t\t\tdouble width, double height,\n\t\t\tboolean mirroredTextures) {\n\n\t\tdouble halfWidth = 0.5 * width;\n\t\t\n\t\tVectorXYZ xPosBottom = pos.add(halfWidth, 0, 0);\n\t\tVectorXYZ xNegBottom = pos.add(-halfWidth, 0, 0);\n\t\tVectorXYZ zPosBottom = pos.add(0, 0, halfWidth);\n\t\tVectorXYZ zNegBottom = pos.add(0, 0, -halfWidth);\n\n\t\tVectorXYZ xPosTop = xPosBottom.add(0, height, 0);\n\t\tVectorXYZ xNegTop = xNegBottom.add(0, height, 0);\n\t\tVectorXYZ zPosTop = zPosBottom.add(0, height, 0);\n\t\tVectorXYZ zNegTop = zNegBottom.add(0, height, 0);\n\t\tVectorXYZ posTop = pos.add(0, height, 0);\n\t\t\n\t\ttarget.drawTriangleStrip(material, asList(\n\t\t\t\txNegTop, xNegBottom, posTop, pos),\n\t\t\t\tbuildBillboardTexCoordLists(false, mirroredTextures,\n\t\t\t\t\t\tmaterial.getTextureDataList().size()));\n\t\t\n\t\ttarget.drawTriangleStrip(material, asList(\n\t\t\t\txPosBottom, xPosTop, pos, posTop),\n\t\t\t\tbuildBillboardTexCoordLists(true, mirroredTextures,\n\t\t\t\t\t\tmaterial.getTextureDataList().size()));\n\n\t\ttarget.drawTriangleStrip(material, asList(\n\t\t\t\tzNegTop, zNegBottom, posTop, pos),\n\t\t\t\tbuildBillboardTexCoordLists(false, mirroredTextures,\n\t\t\t\t\t\tmaterial.getTextureDataList().size()));\n\t\t\n\t\ttarget.drawTriangleStrip(material, asList(\n\t\t\t\tzPosBottom, zPosTop, pos, posTop),\n\t\t\t\tbuildBillboardTexCoordLists(true, mirroredTextures,\n\t\t\t\t\t\tmaterial.getTextureDataList().size()));\n\n\t\ttarget.drawTriangleStrip(material, asList(\n\t\t\t\txPosTop, xPosBottom, posTop, pos),\n\t\t\t\tbuildBillboardTexCoordLists(false, mirroredTextures,\n\t\t\t\t\t\tmaterial.getTextureDataList().size()));\n\t\t\n\t\ttarget.drawTriangleStrip(material, asList(\n\t\t\t\txNegBottom, xNegTop, pos, posTop),\n\t\t\t\tbuildBillboardTexCoordLists(true, mirroredTextures,\n\t\t\t\t\t\tmaterial.getTextureDataList().size()));\n\n\t\ttarget.drawTriangleStrip(material, asList(\n\t\t\t\tzPosTop, zPosBottom, posTop, pos),\n\t\t\t\tbuildBillboardTexCoordLists(false, mirroredTextures,\n\t\t\t\t\t\tmaterial.getTextureDataList().size()));\n\t\t\n\t\ttarget.drawTriangleStrip(material, asList(\n\t\t\t\tzNegBottom, zNegTop, pos, posTop),\n\t\t\t\tbuildBillboardTexCoordLists(true, mirroredTextures,\n\t\t\t\t\t\tmaterial.getTextureDataList().size()));\n\t\t\n\t}\n\t\n\tprivate static final List<List<VectorXZ>> buildBillboardTexCoordLists(\n\t\t\tboolean right, boolean mirrored, int copies) {\n\t\t\n\t\tif (right) {\n\t\t\t\n\t\t\tif (mirrored) {\n\t\t\t\tif (copies <= 3) { return BILLBOARD_RIGHT_TEX_COORDS_MIRRORED_COPIES[copies]; }\n\t\t\t\telse { return nCopies(copies, BILLBOARD_RIGHT_TEX_COORDS_MIRRORED); }\n\t\t\t} else {\n\t\t\t\tif (copies <= 3) { return BILLBOARD_RIGHT_TEX_COORDS_COPIES[copies]; }\n\t\t\t\telse { return nCopies(copies, BILLBOARD_RIGHT_TEX_COORDS); }\n\t\t\t}\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\tif (mirrored) {\n\t\t\t\tif (copies <= 3) { return BILLBOARD_LEFT_TEX_COORDS_MIRRORED_COPIES[copies]; }\n\t\t\t\telse { return nCopies(copies, BILLBOARD_LEFT_TEX_COORDS_MIRRORED); }\n\t\t\t} else {\n\t\t\t\tif (copies <= 3) { return BILLBOARD_LEFT_TEX_COORDS_COPIES[copies]; }\n\t\t\t\telse { return nCopies(copies, BILLBOARD_LEFT_TEX_COORDS); }\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final List<VectorXZ> BILLBOARD_LEFT_TEX_COORDS = asList(\n\t\t\tVectorXZ.Z_UNIT, VectorXZ.NULL_VECTOR,\n\t\t\tnew VectorXZ(0.5, 1), new VectorXZ(0.5, 0));\n\t\n\tprivate static final List<VectorXZ> BILLBOARD_LEFT_TEX_COORDS_MIRRORED = asList(\n\t\t\tnew VectorXZ(1, 1), VectorXZ.X_UNIT,\n\t\t\tnew VectorXZ(0.5, 1), new VectorXZ(0.5, 0));\n\t\t\n\tprivate static final List<VectorXZ> BILLBOARD_RIGHT_TEX_COORDS = asList(\n\t\t\tVectorXZ.X_UNIT, new VectorXZ(1, 1),\n\t\t\tnew VectorXZ(0.5, 0), new VectorXZ(0.5, 1));\n\t\n\tprivate static final List<VectorXZ> BILLBOARD_RIGHT_TEX_COORDS_MIRRORED = asList(\n\t\t\tVectorXZ.NULL_VECTOR, VectorXZ.Z_UNIT,\n\t\t\tnew VectorXZ(0.5, 0), new VectorXZ(0.5, 1));\n\t\n\t@SuppressWarnings(\"unchecked\")\n\tprivate static final List<List<VectorXZ>>[] BILLBOARD_LEFT_TEX_COORDS_COPIES = new List[]{\n\t\tnCopies(0, BILLBOARD_LEFT_TEX_COORDS),\n\t\tnCopies(1, BILLBOARD_LEFT_TEX_COORDS),\n\t\tnCopies(2, BILLBOARD_LEFT_TEX_COORDS),\n\t\tnCopies(3, BILLBOARD_LEFT_TEX_COORDS)\n\t};\n\t\n\t@SuppressWarnings(\"unchecked\")\n\tprivate static final List<List<VectorXZ>>[] BILLBOARD_LEFT_TEX_COORDS_MIRRORED_COPIES = new List[]{\n\t\tnCopies(0, BILLBOARD_LEFT_TEX_COORDS_MIRRORED),\n\t\tnCopies(1, BILLBOARD_LEFT_TEX_COORDS_MIRRORED),\n\t\tnCopies(2, BILLBOARD_LEFT_TEX_COORDS_MIRRORED),\n\t\tnCopies(3, BILLBOARD_LEFT_TEX_COORDS_MIRRORED)\n\t};\n\t\n\t@SuppressWarnings(\"unchecked\")\n\tprivate static final List<List<VectorXZ>>[] BILLBOARD_RIGHT_TEX_COORDS_COPIES = new List[]{\n\t\tnCopies(0, BILLBOARD_RIGHT_TEX_COORDS),\n\t\tnCopies(1, BILLBOARD_RIGHT_TEX_COORDS),\n\t\tnCopies(2, BILLBOARD_RIGHT_TEX_COORDS),\n\t\tnCopies(3, BILLBOARD_RIGHT_TEX_COORDS)\n\t};\n\t\n\t@SuppressWarnings(\"unchecked\")\n\tprivate static final List<List<VectorXZ>>[] BILLBOARD_RIGHT_TEX_COORDS_MIRRORED_COPIES = new List[]{\n\t\tnCopies(0, BILLBOARD_RIGHT_TEX_COORDS_MIRRORED),\n\t\tnCopies(1, BILLBOARD_RIGHT_TEX_COORDS_MIRRORED),\n\t\tnCopies(2, BILLBOARD_RIGHT_TEX_COORDS_MIRRORED),\n\t\tnCopies(3, BILLBOARD_RIGHT_TEX_COORDS_MIRRORED)\n\t};\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/common/WorldModuleGeometryUtil.java",
    "content": "package org.osm2world.core.world.modules.common;\n\nimport static java.lang.Math.toRadians;\nimport static java.util.Arrays.asList;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\n\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.GeometryUtil;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.world.creation.WorldModule;\nimport org.osm2world.core.world.data.WorldObject;\nimport org.osm2world.core.world.data.WorldObjectWithOutline;\n\n/**\n * offers some geometry-related utility functions for {@link WorldModule}s\n */\npublic final class WorldModuleGeometryUtil {\n\n\tprivate WorldModuleGeometryUtil() { }\n\t\n\t/**\n\t * creates the vectors for a vertical triangle strip\n\t * at a given elevation above a line of points\n\t */\n\tpublic static final List<VectorXYZ> createVerticalTriangleStrip(\n\t\t\tList<? extends VectorXYZ> baseLine, float stripLowerYBound, float stripUpperYBound) {\n\n\t\tVectorXYZ[] result = new VectorXYZ[baseLine.size() * 2];\n\n\t\tfor (int i = 0; i < baseLine.size(); i++) {\n\n\t\t\tVectorXYZ basePos = baseLine.get(i);\n\t\t\t\n\t\t\tresult[i*2] = new VectorXYZ(\n\t\t\t\t\tbasePos.getX(),\n\t\t\t\t\tbasePos.getY() + stripLowerYBound,\n\t\t\t\t\tbasePos.getZ());\n\n\t\t\tresult[i*2+1] = new VectorXYZ(\n\t\t\t\t\tbasePos.getX(),\n\t\t\t\t\tbasePos.getY() + stripUpperYBound,\n\t\t\t\t\tbasePos.getZ());\n\n\t\t}\n\t\t\n\t\treturn asList(result);\n\t\t\n\t}\n\n\t/**\n\t * creates a triangle strip between two outlines with identical number of vectors\n\t */\n\tpublic static final List<VectorXYZ> createTriangleStripBetween(\n\t\t\tList<VectorXYZ> leftOutline, List<VectorXYZ> rightOutline) {\n\n\t\tassert leftOutline.size() == rightOutline.size();\n\t\t\n\t\tVectorXYZ[] vs = new VectorXYZ[leftOutline.size() * 2];\n\t\t\n\t\tfor (int i = 0; i < leftOutline.size(); i++) {\n\t\t\tvs[i*2] = leftOutline.get(i);\n\t\t\tvs[i*2+1] = rightOutline.get(i);\n\t\t}\n\t\t\n\t\treturn asList(vs);\n\t\t\n\t}\n\t\t\t\n\t\n\t/**\n\t * @param ratio  0 is at left outline, 1 at right outline\n\t */\n\tpublic static final List<VectorXYZ> createLineBetween(\n\t\t\tList<VectorXYZ> leftOutline, List<VectorXYZ> rightOutline, float ratio) {\n\t\t\n\t\tassert leftOutline.size() == rightOutline.size();\n\t\t\n\t\tList<VectorXYZ> result = new ArrayList<VectorXYZ>(leftOutline.size());\n\t\t\n\t\tfor (int i = 0; i < leftOutline.size(); i++) {\n\t\t\tresult.add(GeometryUtil.interpolateBetween(\n\t\t\t\t\tleftOutline.get(i), rightOutline.get(i), ratio));\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\n\t//TODO: many uses of VisualizationUtil#drawColumn are a special case of this\n\t/**\n\t * creates triangle strip vectors for a shape extruded along a line of coordinates\n\t * \n\t * @param shape          shape relative to origin\n\t * @param extrusionPath  nodes to extrude the shape along; needs at least 2 nodes\n\t * @param upVectors      vector for \"up\" direction at each extrusion path node.\n\t *                       You can use {@link Collections#nCopies(int, Object)}\n\t *                       if you want the same up vector for all nodes.\n\t * @return               list of triangle strip vertex lists\n\t */\n\tpublic static final List<List<VectorXYZ>> createShapeExtrusionAlong(\n\t\t\tList<VectorXYZ> shape,\n\t\t\tList<VectorXYZ> extrusionPath,\n\t\t\tList<VectorXYZ> upVectors) {\n\t\t\n\t\tif (extrusionPath.size() < 2) {\n\t\t\tthrow new IllegalArgumentException(\"extrusion path needs at least 2 nodes\");\n\t\t} else if (extrusionPath.size() != upVectors.size()) {\n\t\t\tthrow new IllegalArgumentException(\"extrusionPath and upVectors must have same size\");\n\t\t}\n\t\t\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tList<VectorXYZ>[] shapeVectors = new List[extrusionPath.size()];\n\n\t\t/*\n\t\t * create shape at each node of the extrusion path.\n\t\t * Special handling for first and last node,\n\t\t * where calculation of \"forward\" vector is different.\n\t\t */\n\t\t\n\t\tshapeVectors[0] = transformShape(shape,\n\t\t\t\textrusionPath.get(0),\n\t\t\t\textrusionPath.get(1).subtract(extrusionPath.get(0)).normalize(),\n\t\t\t\tupVectors.get(0));\n\t\t\n\t\tfor (int pathI = 1; pathI < extrusionPath.size()-1; pathI ++) {\n\t\t\t\t\t\t\n\t\t\tVectorXYZ forwardVector =\n\t\t\t\textrusionPath.get(pathI+1).subtract(extrusionPath.get(pathI-1));\n\t\t\tforwardVector = forwardVector.normalize();\n\t\t\t\n\t\t\tshapeVectors[pathI] = transformShape(shape,\n\t\t\t\t\textrusionPath.get(pathI),\n\t\t\t\t\tforwardVector,\n\t\t\t\t\tupVectors.get(pathI));\n\t\t\t\n\t\t}\n\n\t\tint last = extrusionPath.size()-1;\n\t\tshapeVectors[last] = transformShape(shape,\n\t\t\t\textrusionPath.get(last),\n\t\t\t\textrusionPath.get(last).subtract(extrusionPath.get(last-1)).normalize(),\n\t\t\t\tupVectors.get(last));\n\t\t\n\t\t/* draw triangle strips */\n\t\t\n\t\tList<List<VectorXYZ>> triangleStripList =\n\t\t\t\tnew ArrayList<List<VectorXYZ>>(shape.size()-1);\n\t\t\n\t\tfor (int i = 0; i+1 < shape.size(); i++) {\n\t\t\t\n\t\t\tVectorXYZ[] triangleStripVectors = new VectorXYZ[2*shapeVectors.length];\n\t\t\t\n\t\t\tfor (int j=0; j < shapeVectors.length; j++) {\n\t\t\t\t\n\t\t\t\ttriangleStripVectors[j*2+1] = shapeVectors[j].get(i);\n\t\t\t\ttriangleStripVectors[j*2+0] = shapeVectors[j].get(i+1);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\ttriangleStripList.add(asList(triangleStripVectors));\n\t\t\t\n\t\t}\n\t\t\t\t\n\t\treturn triangleStripList;\n\t\t\n\t}\n\t\n\t/**\n\t * creates an rotated version of a list of vectors\n\t * by rotating them by the given angle around the parallel of the x axis\n\t * defined by the given Y and Z coordinates\n\t * \n\t * @param angle  rotation angle in degrees\n\t */\n\tpublic static final List<VectorXYZ> rotateShapeX(List<VectorXYZ> shape,\n\t\t\tdouble angle, double posY, double posZ) {\n\t\t\n\t\tVectorXYZ[] result = new VectorXYZ[shape.size()];\n\n\t\tfor (int i = 0; i < shape.size(); ++i) {\n\t\t\tresult[i] = shape.get(i).add(0f, -posY, -posZ);\n\t\t\tresult[i] = result[i].rotateX(toRadians(angle));\n\t\t\tresult[i] = result[i].add(0f, posY, posZ);\n\t\t}\n\t\t\n\t\treturn asList(result);\n\t\t\n\t}\n\t\n\t/**\n\t * moves a shape that was defined at the origin to a new position.\n\t * This is used by {@link #createShapeExtrusionAlong(List, List, List)}\n\t * \n\t * @param center   new center coordinate\n\t * @param forward  new forward direction (unit vector)\n\t * @param up       new up direction (unit vector)\n\t * @return         list of 3d vectors; same length as shape\n\t */\n\tpublic static final List<VectorXYZ> transformShape (List<VectorXYZ> shape,\n\t\t\tVectorXYZ center, VectorXYZ forward, VectorXYZ up) {\n\n\t\tVectorXYZ[] result = new VectorXYZ[shape.size()];\n\t\t\n\t\tVectorXYZ right = forward.cross(up);\n\t\t\n\t\tfinal double[][] m = { //rotation matrix\n\t\t\t\t{right.x,   right.y,   right.z},\n\t\t\t\t{up.x,      up.y,      up.z},\n\t\t\t\t{forward.x, forward.y, forward.z}\n\t\t};\n\t\t\n\t\tfor (int i = 0; i < shape.size(); i++) {\n\t\t\t\n\t\t\tVectorXYZ v = shape.get(i);\n\t\n\t\t\tv = new VectorXYZ(\n\t\t\t\t\tm[0][0] * v.x + m[1][0] * v.y + m[2][0] * v.z,\n\t\t\t\t\tm[0][1] * v.x + m[1][1] * v.y + m[2][1] * v.z,\n\t\t\t\t\tm[0][2] * v.x + m[1][2] * v.y + m[2][2] * v.z\n\t\t\t\t\t);\n\t\t\t\n\t\t\tv = v.add(center);\n\t\t\t\n\t\t\tresult[i] = v;\n\t\t\t\n\t\t}\n\t\t\n\t\treturn asList(result);\n\t\t\n\t}\n\t\n\n\t/**\n\t * removes positions from a collection if they are on the area covered by a\n\t * {@link WorldObjectWithOutline} from a collection of {@link WorldObject}s.\n\t * \n\t * This can be used to avoid placing trees, bridge pillars\n\t * and other randomly distributed features on roads, rails\n\t * or other similar places where they don't belong.\n\t */\n\tpublic static final void filterWorldObjectCollisions(\n\t\t\tCollection<VectorXZ> positions,\n\t\t\tCollection<WorldObject> worldObjects) {\n\t\t\n\t\t//TODO: add support for avoiding a radius around the position, too.\n\t\t//this is easily possible once \"inflating\"/\"shrinking\" polygons is supported [would also be useful for water bodies etc.]\n\t\t\n\t\t/*\n\t\t * prepare filter polygons.\n\t\t * It improves performance to construct the outline polygons only once\n\t\t * instead of doing this within the loop iterating over positions.\n\t\t */\n\t\t\n\t\tList<SimplePolygonXZ> filterPolygons = new ArrayList<SimplePolygonXZ>();\n\t\t\n\t\tfor (WorldObject worldObject : worldObjects) {\n\t\t\t\n\t\t\tif (worldObject.getGroundState() == GroundState.ON\n\t\t\t\t&& (worldObject instanceof WorldObjectWithOutline)) {\n\t\t\t\t\n\t\t\t\tSimplePolygonXZ outline = null;\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\toutline = ((WorldObjectWithOutline)worldObject).\n\t\t\t\t\t\t\tgetOutlinePolygonXZ();\n\t\t\t\t} catch (InvalidGeometryException e) {\n\t\t\t\t\t//ignore this outline\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (outline != null) {\n\t\t\t\t\tfilterPolygons.add(outline);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\n\t\t}\n\t\t\n\t\t/* perform filtering of positions */\n\t\t\n\t\tIterator<VectorXZ> positionIterator = positions.iterator();\n\t\t\n\t\twhile (positionIterator.hasNext()) {\n\t\t\t\n\t\t\tVectorXZ pos = positionIterator.next();\n\t\t\t\n\t\t\tfor (SimplePolygonXZ filterPolygon : filterPolygons) {\n\t\t\t\tif (filterPolygon.contains(pos)) {\n\t\t\t\t\tpositionIterator.remove();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/modules/common/WorldModuleParseUtil.java",
    "content": "package org.osm2world.core.world.modules.common;\n\nimport static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseAngle;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;\nimport org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser;\nimport org.osm2world.core.world.creation.WorldModule;\n\n/**\n * utility class that can be used by {@link WorldModule}s\n * to interpret frequently used value formats.\n */\npublic class WorldModuleParseUtil {\n\n\tprivate WorldModuleParseUtil() { }\n\n\t/**\n\t * returns the value of the first key that exists,\n\t * or the fallback value if none of the keys exists\n\t */\n\tpublic static final String getValueWithFallback(String fallback,\n\t\t\tTagGroup tags, String... keys) {\n\t\tfor (String key : keys) {\n\t\t\tif (tags.containsKey(key)) {\n\t\t\t\treturn tags.getValue(key);\n\t\t\t}\n\t\t}\n\t\treturn fallback;\n\t}\n\t\n\t/**\n\t * retrieves width using (in this priority order)\n\t * width tag, est_width tag, defaultValue parameter\n\t */\n\tpublic static final float parseWidth(TagGroup tags, float defaultValue) {\n\t\treturn parseMeasure(tags, defaultValue, \"width\", \"est_width\");\n\t}\n\n\t/**\n\t * retrieves height using (in this priority order)\n\t * height tag, building:height tag, est_height tag, defaultValue parameter\n\t */\n\tpublic static final float parseHeight(TagGroup tags, float defaultValue) {\n\t\treturn parseMeasure(tags, defaultValue, \"height\", \"building:height\", \"est_height\");\n\t}\n\n\t/**\n\t * retrieves clearing using (in this priority order)\n\t * practical:maxheight tag, maxheight tag, defaultValue parameter\n\t */\n\tpublic static final float parseClearing(TagGroup tags, float defaultValue) {\n\t\treturn parseMeasure(tags, defaultValue, \"maxheight:physical\", \"maxheight\");\n\t}\n\t\n\t/**\n\t * parses the direction tag and returns the direction\n\t * (or a default value) as radians\n\t */\n\tpublic static final double parseDirection(TagGroup tags, double defaultValue) {\n\t\t\n\t\tFloat directionAngle = null;\n\t\t\n\t\tif (tags.containsKey(\"direction\")) {\n\t\t\tdirectionAngle = parseAngle(tags.getValue(\"direction\"));\n\t\t}\n\t\t\n\t\tif (directionAngle != null) {\n\t\t\treturn Math.toRadians(directionAngle);\n\t\t} else {\n\t\t\treturn defaultValue;\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static final float parseMeasure(TagGroup tags, float defaultValue,\n\t\t\tString... keys) {\n\n\t\tfor (String key : keys) {\n\t\t\tif (tags.containsKey(key)) {\n\t\t\t\tFloat value = ValueStringParser.parseMeasure(tags.getValue(key));\n\t\t\t\tif (value != null) {\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn defaultValue;\n\t\t\n\t}\n\t\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/network/AbstractNetworkWaySegmentWorldObject.java",
    "content": "package org.osm2world.core.world.network;\n\nimport static java.lang.Double.*;\nimport static java.util.Collections.emptyList;\nimport static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseIncline;\nimport static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.*;\nimport static org.osm2world.core.map_elevation.data.GroundState.*;\nimport static org.osm2world.core.math.GeometryUtil.interpolateBetween;\nimport static org.osm2world.core.math.VectorXZ.distanceSquared;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.data.overlaps.MapIntersectionWW;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapType;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapWA;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.map_elevation.data.EleConnectorGroup;\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.map_elevation.data.WaySegmentElevationProfile;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.GeometryUtil;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.datastructures.IntersectionTestObject;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\nimport org.osm2world.core.world.data.WaySegmentWorldObject;\nimport org.osm2world.core.world.data.WorldObject;\nimport org.osm2world.core.world.data.WorldObjectWithOutline;\nimport org.osm2world.core.world.modules.BridgeModule;\nimport org.osm2world.core.world.modules.TunnelModule;\n\npublic abstract class AbstractNetworkWaySegmentWorldObject\n\timplements NetworkWaySegmentWorldObject, WaySegmentWorldObject,\n\t           IntersectionTestObject, WorldObjectWithOutline {\n\n\tpublic final MapWaySegment segment;\n\t\n\tprivate VectorXZ startCutVector = null;\n\tprivate VectorXZ endCutVector = null;\n\t\n\tprivate VectorXZ startOffset = VectorXZ.NULL_VECTOR;\n\tprivate VectorXZ endOffset = VectorXZ.NULL_VECTOR;\n\t\n\tprotected EleConnectorGroup connectors;\n\t\n\tprivate List<VectorXZ> centerlineXZ = null;\n\t\n\tprivate List<VectorXZ> leftOutlineXZ = null;\n\tprivate List<VectorXZ> rightOutlineXZ = null;\n\t\n\tprivate SimplePolygonXZ outlinePolygonXZ = null;\n\t\n\tprivate Boolean broken = null;\n\t\n\tprotected AbstractNetworkWaySegmentWorldObject(MapWaySegment segment) {\n\t\tthis.segment = segment;\n\t}\n\t\n\t@Override\n\tpublic final MapWaySegment getPrimaryMapElement() {\n\t\treturn segment;\n\t}\n\t\n\t@Override\n\tpublic void setStartCutVector(VectorXZ cutVector) {\n\t\tthis.startCutVector = cutVector;\n\t}\n\t\n\t@Override\n\tpublic void setEndCutVector(VectorXZ cutVector) {\n\t\tthis.endCutVector = cutVector;\n\t}\n\n\t@Override\n\tpublic VectorXZ getStartCutVector() {\n\t\treturn startCutVector;\n\t}\n\n\t@Override\n\tpublic VectorXZ getEndCutVector() {\n\t\treturn endCutVector;\n\t}\n\t\n\tpublic VectorXZ getCutVectorAt(MapNode node) {\n\t\tif (node == segment.getStartNode()) {\n\t\t\treturn getStartCutVector();\n\t\t} else if (node == segment.getEndNode()) {\n\t\t\treturn getEndCutVector();\n\t\t} else {\n\t\t\tthrow new IllegalArgumentException(\"node is not part of the line\");\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic void setStartOffset(VectorXZ offsetVector) {\n\t\tthis.startOffset = offsetVector;\n\t}\n\t\n\t@Override\n\tpublic void setEndOffset(VectorXZ offsetVector) {\n\t\tthis.endOffset = offsetVector;\n\t}\n\t\n\tprotected VectorXZ getStartWithOffset() {\n\t\treturn segment.getStartNode().getPos().add(startOffset); //SUGGEST (performance): cache? [also getEnd*]\n\t}\n\t\n\tprotected VectorXZ getEndWithOffset() {\n\t\treturn segment.getEndNode().getPos().add(endOffset);\n\t}\n\t\n\t/**\n\t * calculates centerline and outlines, along with their connectors\n\t * TODO: perform before construction, to simplify the object and avoid {@link #broken}\n\t */\n\tprivate void calculateXZGeometry() {\n\t\n\t\tif (startCutVector == null || endCutVector == null) {\n\t\t\tthrow new IllegalStateException(\"cannot calculate outlines before cut vectors\");\n\t\t}\n\t\t\n\t\tconnectors = new EleConnectorGroup();\n\t\t\n\t\t{ /* calculate centerline */\n\t\t\t\t\t\t\n\t\t\tcenterlineXZ = new ArrayList<VectorXZ>();\n\t\t\t\n\t\t\tfinal VectorXZ start = getStartWithOffset();\n\t\t\tfinal VectorXZ end = getEndWithOffset();\n\t\t\t\n\t\t\tcenterlineXZ.add(start);\n\t\t\t\n\t\t\tconnectors.add(new EleConnector(start,\n\t\t\t\t\tsegment.getStartNode(), getGroundState(segment.getStartNode())));\n\t\t\t\n\t\t\t// add intersections along the centerline\n\t\t\t\n\t\t\tfor (MapOverlap<?,?> overlap : segment.getOverlaps()) {\n\t\t\t\t\n\t\t\t\tif (overlap.getOther(segment).getPrimaryRepresentation() == null)\n\t\t\t\t\tcontinue;\n\t\t\t\t\n\t\t\t\tif (overlap instanceof MapIntersectionWW) {\n\t\t\t\t\t\n\t\t\t\t\tMapIntersectionWW intersection = (MapIntersectionWW) overlap;\n\t\t\t\t\t\n\t\t\t\t\tif (GeometryUtil.isBetween(intersection.pos, start, end)) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tcenterlineXZ.add(intersection.pos);\n\t\t\t\t\t\t\n\t\t\t\t\t\tconnectors.add(new EleConnector(intersection.pos,\n\t\t\t\t\t\t\t\tnull, getGroundState()));\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t} else if (overlap instanceof MapOverlapWA\n\t\t\t\t\t\t&& overlap.type == MapOverlapType.INTERSECT) {\n\t\t\t\t\t\n\t\t\t\t\tif (!(overlap.getOther(segment).getPrimaryRepresentation()\n\t\t\t\t\t\t\tinstanceof AbstractAreaWorldObject)) continue;\n\t\t\t\t\t\n\t\t\t\t\tMapOverlapWA overlapWA = (MapOverlapWA) overlap;\n\t\t\t\t\t\n\t\t\t\t\tfor (int i = 0; i < overlapWA.getIntersectionPositions().size(); i++) {\n\t\t\t\t\t\n\t\t\t\t\t\tVectorXZ pos = overlapWA.getIntersectionPositions().get(i);\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (GeometryUtil.isBetween(pos, start, end)) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tcenterlineXZ.add(pos);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tconnectors.add(new EleConnector(pos,\n\t\t\t\t\t\t\t\t\tnull, getGroundState()));\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t// finish the centerline\n\t\t\t\n\t\t\tcenterlineXZ.add(end);\n\n\t\t\tconnectors.add(new EleConnector(end,\n\t\t\t\t\tsegment.getEndNode(), getGroundState(segment.getEndNode())));\n\t\t\t\n\t\t\tif (centerlineXZ.size() > 3) {\n\t\t\t\t\n\t\t\t\t// sort by distance from start\n\t\t\t\tCollections.sort(centerlineXZ, new Comparator<VectorXZ>() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic int compare(VectorXZ v1, VectorXZ v2) {\n\t\t\t\t\t\treturn Double.compare(\n\t\t\t\t\t\t\t\tdistanceSquared(v1, start),\n\t\t\t\t\t\t\t\tdistanceSquared(v2, start));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t{ /* calculate left and right outlines */\n\t\t\t\n\t\t\tleftOutlineXZ = new ArrayList<VectorXZ>(centerlineXZ.size());\n\t\t\trightOutlineXZ = new ArrayList<VectorXZ>(centerlineXZ.size());\n\t\t\t\n\t\t\tassert centerlineXZ.size() >= 2;\n\t\t\t\n\t\t\tdouble halfWidth = getWidth() * 0.5f;\n\t\t\t\n\t\t\tVectorXZ centerStart = centerlineXZ.get(0);\n\t\t\tleftOutlineXZ.add(centerStart.add(startCutVector.mult(-halfWidth)));\n\t\t\trightOutlineXZ.add(centerStart.add(startCutVector.mult(halfWidth)));\n\t\t\t\n\t\t\tconnectors.add(new EleConnector(leftOutlineXZ.get(0),\n\t\t\t\t\tsegment.getStartNode(), getGroundState(segment.getStartNode())));\n\t\t\tconnectors.add(new EleConnector(rightOutlineXZ.get(0),\n\t\t\t\t\tsegment.getStartNode(), getGroundState(segment.getStartNode())));\n\t\t\t\n\t\t\tfor (int i = 1; i < centerlineXZ.size() - 1; i++) {\n\t\t\t\t\n\t\t\t\tleftOutlineXZ.add(centerlineXZ.get(i).add(segment.getRightNormal().mult(-halfWidth)));\n\t\t\t\trightOutlineXZ.add(centerlineXZ.get(i).add(segment.getRightNormal().mult(halfWidth)));\n\t\t\t\t\n\t\t\t\tconnectors.add(new EleConnector(leftOutlineXZ.get(i),\n\t\t\t\t\t\tnull, getGroundState()));\n\t\t\t\tconnectors.add(new EleConnector(rightOutlineXZ.get(i),\n\t\t\t\t\t\tnull, getGroundState()));\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tVectorXZ centerEnd = centerlineXZ.get(centerlineXZ.size() - 1);\n\t\t\tleftOutlineXZ.add(centerEnd.add(endCutVector.mult(-halfWidth)));\n\t\t\trightOutlineXZ.add(centerEnd.add(endCutVector.mult(halfWidth)));\n\t\t\t\n\t\t\tconnectors.add(new EleConnector(leftOutlineXZ.get(leftOutlineXZ.size() - 1),\n\t\t\t\t\tsegment.getEndNode(), getGroundState(segment.getEndNode())));\n\t\t\tconnectors.add(new EleConnector(rightOutlineXZ.get(rightOutlineXZ.size() - 1),\n\t\t\t\t\tsegment.getEndNode(), getGroundState(segment.getEndNode())));\n\t\t\t\n\t\t}\n\t\t\n\t\t{ /* calculate the outline loop */\n\t\t\t\n\t\t\tList<VectorXZ> outlineLoopXZ =\n\t\t\t\t\tnew ArrayList<VectorXZ>(centerlineXZ.size() * 2 + 1);\n\t\t\t\n\t\t\toutlineLoopXZ.addAll(rightOutlineXZ);\n\t\t\t\n\t\t\tList<VectorXZ> left = new ArrayList<VectorXZ>(leftOutlineXZ);\n\t\t\tCollections.reverse(left);\n\t\t\toutlineLoopXZ.addAll(left);\n\t\t\t\n\t\t\toutlineLoopXZ.add(outlineLoopXZ.get(0));\n\t\t\t\n\t\t\t// check for brokenness\n\t\t\t\n\t\t\ttry {\n\t\t\t\toutlinePolygonXZ = new SimplePolygonXZ(outlineLoopXZ);\n\t\t\t\tbroken = outlinePolygonXZ.isClockwise();\n\t\t\t} catch (InvalidGeometryException e) {\n\t\t\t\tbroken = true;\n\t\t\t\tconnectors = EleConnectorGroup.EMPTY;\n\t\t\t}\n\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * determines whether the node is connected to the terrain based on the\n\t * segments connected to it\n\t * \n\t * @param node  one of the nodes of {@link #segment}\n\t */\n\tprivate GroundState getGroundState(MapNode node) {\n\t\t\n\t\tWorldObject primaryWO = node.getPrimaryRepresentation();\n\t\t\n\t\tif (primaryWO != null) {\n\t\t\t\n\t\t\treturn primaryWO.getGroundState();\n\t\t\t\n\t\t} else if (this.getGroundState() == ON) {\n\t\t\t\n\t\t\treturn ON;\n\t\t\t\n\t\t} else {\n\t\t\t\n\t\t\tboolean allAbove = true;\n\t\t\tboolean allBelow = true;\n\t\t\t\n\t\t\tfor (MapWaySegment segment : node.getConnectedWaySegments()) {\n\t\t\t\tif (segment.getPrimaryRepresentation() != null) {\n\t\t\t\t\tswitch (segment.getPrimaryRepresentation().getGroundState()) {\n\t\t\t\t\tcase ABOVE: allBelow = false; break;\n\t\t\t\t\tcase BELOW: allAbove = false; break;\n\t\t\t\t\tcase ON: return ON;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (allAbove) {\n\t\t\t\treturn ABOVE;\n\t\t\t} else if (allBelow) {\n\t\t\t\treturn BELOW;\n\t\t\t} else {\n\t\t\t\treturn ON;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * implementation of {@link WorldObject#getGroundState()}.\n\t * This version checks for bridge and tunnel tags to make the decision.\n\t * If that is not desired, subclasses may override the method.\n\t */\n\t@Override\n\tpublic GroundState getGroundState() {\n\t\tif (BridgeModule.isBridge(segment.getTags())) {\n\t\t\treturn GroundState.ABOVE;\n\t\t} else if (TunnelModule.isTunnel(segment.getTags())) {\n\t\t\treturn GroundState.BELOW;\n\t\t} else {\n\t\t\treturn GroundState.ON;\n\t\t}\n\t}\n\n\t@Override\n\tpublic EleConnectorGroup getEleConnectors() {\n\t\t\n\t\tif (connectors == null) {\n\t\t\tcalculateXZGeometry();\n\t\t}\n\t\t\n\t\treturn connectors;\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void defineEleConstraints(EleConstraintEnforcer enforcer) {\n\t\t\n\t\tif (isBroken()) return;\n\t\t\n\t\t//TODO: maybe save connectors separately right away\n\t\t\n\t\tList<EleConnector> center = getCenterlineEleConnectors();\n\t\tList<EleConnector> left = connectors.getConnectors(getOutlineXZ(false));\n\t\tList<EleConnector> right = connectors.getConnectors(getOutlineXZ(true));\n\t\t\n\t\t/* left and right connectors have the same ele as their center conn. */\n\t\t\n\t\tfor (int i = 0; i < center.size(); i++) {\n\t\t\t\n\t\t\tenforcer.requireSameEle(center.get(i), left.get(i));\n\t\t\tenforcer.requireSameEle(center.get(i), right.get(i));\n\t\t\t\n\t\t}\n\t\t\n\t\t/* incline should be honored */\n\t\t\n\t\tString inclineValue = segment.getTags().getValue(\"incline\");\n\t\t\t\t\n\t\tif (inclineValue != null) {\n\t\t\t\n\t\t\tdouble minIncline = NaN;\n\t\t\tdouble maxIncline = NaN;\n\t\t\t\n\t\t\tif (\"up\".equals(inclineValue)) {\n\t\t\t\t\n\t\t\t\tminIncline = 0.1;\n\t\t\t\t\n\t\t\t} else if (\"down\".equals(inclineValue)) {\n\t\t\t\t\n\t\t\t\tmaxIncline = -0.1;\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tFloat incline = parseIncline(inclineValue);\n\t\t\t\t\n\t\t\t\tif (incline != null) {\n\t\t\t\t\tif (incline > 0) {\n\t\t\t\t\t\tminIncline = incline / 100.0 * 0.5;\n\t\t\t\t\t\tmaxIncline = incline / 100.0 * 1.1;\n\t\t\t\t\t} else if (incline < 0) {\n\t\t\t\t\t\tmaxIncline = incline / 100.0 * 0.5;\n\t\t\t\t\t\tminIncline = incline / 100.0 * 1.1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t\n\t\t\t\t\t\tenforcer.requireSameEle(center);\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tif (!isNaN(minIncline)) {\n\t\t\t\tenforcer.requireIncline(MIN, minIncline, center);\n\t\t\t}\n\t\t\t\n\t\t\tif (!isNaN(maxIncline)) {\n\t\t\t\tenforcer.requireIncline(MAX, maxIncline, center);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t//TODO sensible maximum incline for road and rail; and waterway down-incline\n\t\t// ... take incline differences, steps etc. into account => move into Road, Rail separately\n\t\t\n\t\t/* ensure a smooth transition from previous segment */\n\t\t\n\t\t//TODO this might be more elegant with an \"Invisible Connector WO\"\n\t\t\n\t\tList<MapWaySegment> connectedSegments =\n\t\t\t\tsegment.getStartNode().getConnectedWaySegments();\n\t\t\n\t\tif (connectedSegments.size() == 2) {\n\t\t\t\n\t\t\tMapWaySegment previousSegment = null;\n\t\t\t\n\t\t\tfor (MapWaySegment connectedSegment : connectedSegments) {\n\t\t\t\tif (connectedSegment != this.segment) {\n\t\t\t\t\tpreviousSegment = connectedSegment;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tWorldObject previousWO = previousSegment.getPrimaryRepresentation();\n\t\t\t\n\t\t\tif (previousWO instanceof AbstractNetworkWaySegmentWorldObject) {\n\t\t\t\t\n\t\t\t\tAbstractNetworkWaySegmentWorldObject previous =\n\t\t\t\t\t\t(AbstractNetworkWaySegmentWorldObject)previousWO;\n\t\t\t\t\n\t\t\t\tif (!previous.isBroken()) {\n\t\t\t\t\t\n\t\t\t\t\tList<EleConnector> previousCenter =\n\t\t\t\t\t\t\tprevious.getCenterlineEleConnectors();\n\t\t\t\t\t\n\t\t\t\t\tenforcer.requireSmoothness(\n\t\t\t\t\t\t\tpreviousCenter.get(previousCenter.size() - 2),\n\t\t\t\t\t\t\tcenter.get(0),\n\t\t\t\t\t\t\tcenter.get(1));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\n\t\t}\n\t\t\n\t\t/* ensure smooth transitions within the way itself */\n\t\t\n\t\tfor (int i = 0; i + 2 < center.size(); i++) {\n\t\t\tenforcer.requireSmoothness(\n\t\t\t\t\tcenter.get(i),\n\t\t\t\t\tcenter.get(i+1),\n\t\t\t\t\tcenter.get(i+2));\n\t\t}\n\t\t\n\t}\n\t\n\tprotected List<EleConnector> getCenterlineEleConnectors() {\n\n\t\tif (isBroken()) return emptyList();\n\t\t\n\t\treturn connectors.getConnectors(getCenterlineXZ());\n\t\t\n\t}\n\t\n\t/**\n\t * returns a sequence of node running along the center of the\n\t * line from start to end (each with offset).\n\t * Uses the {@link WaySegmentElevationProfile} for adding\n\t * elevation information.\n\t */\n\tpublic List<VectorXZ> getCenterlineXZ() {\n\t\t\n\t\tif (centerlineXZ == null) {\n\t\t\tcalculateXZGeometry();\n\t\t}\n\t\t\n\t\treturn centerlineXZ;\n\t\t\n\t}\n\t\n\t/**\n\t * 3d version of {@link #getCenterlineXZ()}.\n\t * Only available after elevation calculation.\n\t */\n\tpublic List<VectorXYZ> getCenterline() {\n\t\treturn connectors.getPosXYZ(getCenterlineXZ());\n\t}\n\t\n\t/**\n\t * Variant of {@link #getOutline(boolean)}.\n\t * This one is already available before elevation calculation.\n\t */\n\tpublic List<VectorXZ> getOutlineXZ(boolean right) {\n\t\t\n\t\tif (right) {\n\t\t\t\n\t\t\tif (rightOutlineXZ == null) {\n\t\t\t\tcalculateXZGeometry();\n\t\t\t}\n\t\t\t\n\t\t\treturn rightOutlineXZ;\n\t\t\t\n\t\t} else { //left\n\t\t\t\n\t\t\tif (leftOutlineXZ == null) {\n\t\t\t\tcalculateXZGeometry();\n\t\t\t}\n\t\t\t\n\t\t\treturn leftOutlineXZ;\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * provides the left or right border (a line at an appropriate distance\n\t * from the center line), taking into account cut vectors, offsets and\n\t * elevation information.\n\t * Available after cut vectors, offsets and elevation information\n\t * have been calculated.\n\t * \n\t * Left and right border have the same number of nodes as the elevation\n\t * profile's {@link WaySegmentElevationProfile#getPointsWithEle()}.\n\t * //TODO: compatible with future offset/clearing influences?\n\t */\n\tpublic List<VectorXYZ> getOutline(boolean right) {\n\t\treturn connectors.getPosXYZ(getOutlineXZ(right));\n\t}\n\t\n\t@Override\n\tpublic SimplePolygonXZ getOutlinePolygonXZ() {\n\t\t\n\t\tif (outlinePolygonXZ == null) {\n\t\t\tcalculateXZGeometry();\n\t\t}\n\t\t\n\t\tif (isBroken()) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn outlinePolygonXZ;\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic PolygonXYZ getOutlinePolygon() {\n\t\t\n\t\tif (isBroken()) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn connectors.getPosXYZ(outlinePolygonXZ);\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * checks whether this segment has a broken outline.\n\t * That can happen e.g. if it lies between two junctions that are too close\n\t * together.\n\t */\n\tpublic boolean isBroken() {\n\t\t\n\t\tif (broken == null) {\n\t\t\tcalculateXZGeometry();\n\t\t}\n\t\t\n\t\t//TODO filter out broken objects during creation in the world module\n\t\treturn broken;\n\t\t\n\t}\n\t\n\t/**\n\t * returns a point on the start or end cut line.\n\t * \n\t * @param start  point is on the start cut if true, on the end cut if false\n\t * @param relativePosFromLeft  0 is the leftmost point, 1 the rightmost.\n\t *                             Values in between are for interpolation.\n\t */\n\tpublic VectorXYZ getPointOnCut(boolean start, double relativePosFromLeft) {\n\t\t\n\t\tassert 0 <= relativePosFromLeft && relativePosFromLeft <= 1;\n\t\t\n\t\tVectorXZ position = start ? getStartWithOffset() : getEndWithOffset();\n\t\tVectorXZ cutVector = start ? getStartCutVector() : getEndCutVector();\n\t\t\n\t\tVectorXYZ left = connectors.getPosXYZ(position.add(\n\t\t\t\tcutVector.mult(-0.5 * getWidth())));\n\t\tVectorXYZ right = connectors.getPosXYZ(position.add(\n\t\t\t\tcutVector.mult(+0.5 * getWidth())));\n\t\t\n\t\tif (relativePosFromLeft == 0) {\n\t\t\treturn left;\n\t\t} else if (relativePosFromLeft == 1) {\n\t\t\treturn right;\n\t\t} else {\n\t\t\treturn interpolateBetween(left, right, relativePosFromLeft);\n\t\t}\n\t\t\n\t}\n\n\t@Override\n\tpublic VectorXZ getStartOffset() {\n\t\treturn startOffset;\n\t}\n\n\t@Override\n\tpublic VectorXZ getEndOffset() {\n\t\treturn endOffset;\n\t}\n\t\n\t@Override\n\tpublic VectorXZ getStartPosition() {\n\t\treturn segment.getStartNode().getPos().add(getStartOffset());\n\t}\n\t\n\t@Override\n\tpublic VectorXZ getEndPosition() {\n\t\treturn segment.getEndNode().getPos().add(getEndOffset());\n\t}\n\t\n\t@Override\n\tpublic AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {\n\t\t\n\t\tif (isBroken() || getOutlinePolygonXZ() == null) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn new AxisAlignedBoundingBoxXZ(\n\t\t\t\t\tgetOutlinePolygonXZ().getVertexCollection());\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn this.getClass().getSimpleName() + \"(\" + segment + \")\";\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/network/JunctionNodeWorldObject.java",
    "content": "package org.osm2world.core.world.network;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapSegment;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.PolygonXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.world.creation.NetworkCalculator;\nimport org.osm2world.core.world.data.OutlineNodeWorldObject;\n\npublic abstract class JunctionNodeWorldObject extends OutlineNodeWorldObject {\n\t\n\tprotected boolean informationProvided = false;\n\tprotected List<VectorXZ> cutVectors;\n\tprotected List<VectorXZ> cutCenters;\n\tprotected List<Float> widths;\n\t\t\n\t/**\n\t * sets the results of {@link NetworkCalculator}'s calculations.\n\t * \n\t * Cut information will not be created for all way/area segments.\n\t * The lists can therefore contain null entries.\n\t * \n\t * @param cutCenters\n\t * centers of the cuts to each;\n\t * indices are the same as for the GridNode's {@link MapNode#getConnectedSegments()}\n\t * @param cutVectors\n\t * vectors describing indicating the cut line,\n\t * pointing to the right from the node's pov;\n\t * for indices see junctionCutCenters\n\t * @param widths\n\t * widths of the junction cut;\n\t * for indices see junctionCutCenters\n\t */\n\tpublic void setInformation(List<VectorXZ> cutCenters,\n\t\t\tList<VectorXZ> cutVectors, List<Float> widths) {\n\n\t\tthis.informationProvided = true;\n\n\t\tthis.cutCenters = cutCenters;\n\t\tthis.cutVectors = cutVectors;\n\t\tthis.widths = widths;\n\n\t}\n\t\n\tpublic JunctionNodeWorldObject(MapNode node) {\n\t\tsuper(node);\n\t}\n\t\n\t\n\t\n\t//TODO formerly @Override, currently useless\n//\tpublic double getClearingAbove(VectorXZ pos) {\n//\t\t// current solution: maximum of connected segments' clearings.\n//\t\t// Could probably find a more intelligent method.\n//\n//\t\tdouble max = 0;\n//\t\tfor (MapWaySegment waySegment : node.getConnectedWaySegments()) {\n//\t\t\tWaySegmentWorldObject rep = waySegment.getPrimaryRepresentation();\n//\t\t\tif (rep != null) {\n//\t\t\t\tdouble clearing = rep.getClearingAbove(node.getPos());\n//\t\t\t\tif (clearing > max) {\n//\t\t\t\t\tmax = clearing;\n//\t\t\t\t}\n//\t\t\t}\n//\t\t}\n//\t\treturn max;\n//\t}\n\t\n\t//TODO formerly @Override, currently useless\n//\tpublic double getClearingBelow(VectorXZ pos) {\n//\t\t// current solution: maximum of connected segments' clearings.\n//\t\t// Could probably find a more intelligent method.\n//\n//\t\tdouble max = 0;\n//\t\tfor (MapWaySegment waySegment : node.getConnectedWaySegments()) {\n//\t\t\tWaySegmentWorldObject rep = waySegment.getPrimaryRepresentation();\n//\t\t\tif (rep != null) {\n//\t\t\t\tdouble clearing = rep.getClearingBelow(node.getPos());\n//\t\t\t\tif (clearing > max) {\n//\t\t\t\t\tmax = clearing;\n//\t\t\t\t}\n//\t\t\t}\n//\t\t}\n//\t\treturn max;\n//\t}\n\t\n\t@Override\n\tpublic SimplePolygonXZ getOutlinePolygonXZ() {\n\t\t\n\t\tList<VectorXZ> vectors = new ArrayList<VectorXZ>(cutCenters.size()*2+1);\n\t\t\n\t\tfor (int i=0; i < cutCenters.size(); i++) {\n\t\t\t\n\t\t\tif (cutCenters.get(i) == null) continue;\n\t\t\t\n\t\t\tVectorXZ left = getCutNode(i, false);\n\t\t\tVectorXZ right = getCutNode(i, true);\n\t\t\t\n\t\t\tif (left != null) {\n\t\t\t\tvectors.add(left);\n\t\t\t}\n\t\t\tif (right != null) {\n\t\t\t\tvectors.add(right);\n\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t}\n\t\t\n\t\t/* try to convert into a valid, counterclockwise simple polygon */\n\t\t\n\t\tif (vectors.size() > 2) {\n\t\t\t\n\t\t\tvectors.add(vectors.get(0)); //close polygon\n\t\t\t\n\t\t\tPolygonXZ poly = new PolygonXZ(vectors);\n\t\t\t\n\t\t\ttry {\n\t\t\t\t\n\t\t\t\tSimplePolygonXZ simplePoly = poly.asSimplePolygon();\n\t\t\t\t\n\t\t\t\tif (simplePoly.isClockwise()) {\n\t\t\t\t\treturn simplePoly.reverse();\n\t\t\t\t} else {\n\t\t\t\t\treturn simplePoly;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t} catch (InvalidGeometryException e) {\n\t\t\t\t//deal with non-simple polygons\n\t\t\t\t//TODO: this should be prevented from ever happening\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t} else {\n\t\t\t\n\t\t\treturn null;\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * calculates the left or right node of a cut\n\t * (Only available if junction information for this representation has been\n\t * provided using {@link #setInformation(List, List, List)}).\n\t * \n\t * @return cut node position; null if connected section #i has no outline\n\t */\n\tprotected VectorXZ getCutNode(int i, boolean right) {\n\n\t\tcheckInformationProvided();\n\t\t\t\t\n\t\tVectorXZ cutCenter = cutCenters.get(i);\n\t\tVectorXZ cutVector = cutVectors.get(i);\n\t\tFloat width = widths.get(i);\n\t\t\n\t\tif (cutCenter == null) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\t\n\t\t\tif (right) {\n\t\t\t\treturn cutCenter.add(cutVector.mult(width * 0.5f));\n\t\t\t} else {\n\t\t\t\treturn cutCenter.subtract(cutVector.mult(width * 0.5f));\n\t\t\t}\n\t\t\n\t\t}\n\t\t\t\n\t}\n\t\n\t/**\n\t * provides outline for the areas covered by the junction.\n\t * \n\t * The from and to indices refer to the list\n\t * returned by the underlying {@link MapNode}'s\n\t * {@link MapNode#getConnectedSegments()} method.\n\t */\n\tpublic List<VectorXZ> getOutline(int from, int to) {\n\t\n\t\tcheckInformationProvided();\n\t\t\n\t\tList<VectorXZ> outline = new ArrayList<VectorXZ>();\n\t\n\t\tList<MapSegment> segments = node.getConnectedSegments();\n\t\n\t\tassert from >= 0 && from < segments.size();\n\t\tassert to >= 0 && to < segments.size();\n\t\t\t\n\t\tint i = from;\n\t\t\n\t\twhile (i != to) {\n\t\t\t   \n\t\t\tVectorXZ newNodeA = getCutNode(i, false);\n\t\t\tif (newNodeA != null) {\n\t\t\t\toutline.add(newNodeA);\n\t\t\t}\n\t\n\t\t\tint nextI = i - 1;\n\t\t\tif (nextI < 0) { nextI = segments.size() - 1; }\n\t\n\t\t\tVectorXZ newNodeB = getCutNode(nextI, true);\n\t\t\tif (newNodeB != null) {\n\t\t\t\toutline.add(newNodeB);\n\t\t\t}\n\t\n\t\t\ti = nextI;\n\t\n\t\t}\n\t\t\n\t\treturn outline;\n\t\t\n\t}\n\t\n\t/**\n\t * throws an IllegalStateException if information hasn't been\n\t * provided by a {@link NetworkCalculator}\n\t */\n\tprivate void checkInformationProvided() throws IllegalStateException {\n\t\tif (!informationProvided) {\n\t\t\tthrow new IllegalStateException(\"no junction information\" +\n\t\t\t\t\t\" has been set for this representation\");\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/network/NetworkAreaWorldObject.java",
    "content": "package org.osm2world.core.world.network;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.world.data.AbstractAreaWorldObject;\n\npublic abstract class NetworkAreaWorldObject extends AbstractAreaWorldObject {\n\n\tpublic NetworkAreaWorldObject(MapArea area) {\n\t\tsuper(area);\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/network/NetworkWaySegmentWorldObject.java",
    "content": "package org.osm2world.core.world.network;\n\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.world.creation.NetworkCalculator;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * \"networks\" are sets of {@link WorldObject}s that have certain\n * frequently required characteristics. Most importantly, a network\n * consists of nodes, lines and areas linked with each other.\n * \n * Other characteristics include\n * - cut angles where lines (or lines and areas) connect\n * - junctions at nodes that occupy some area and push back connecting lines\n *  //TODO (documentation): explain more\n * \n * Features using these types of representation include roads,\n * railways and rivers.\n */\npublic interface NetworkWaySegmentWorldObject extends WorldObject {\n\n\t/**\n\t * returns the line's width\n\t */\n\tpublic float getWidth();\n\t\n\t/**\n\t * returns the cut vector for the start of the line.\n\t * Only available after {@link #setStartCutVector(VectorXZ)}.\n\t */\n\tpublic VectorXZ getStartCutVector();\n\t\n\t/**\n\t * Sets the cut vector for the start of the line. //TODO: explain \"cut vectors\"\n\t * To be used by {@link NetworkCalculator}.\n\t */\n\tpublic void setStartCutVector(VectorXZ cutVector);\n\n\t/**\n\t * returns the cut vector for the end of the line.\n\t * Only available after {@link #setStartCutVector(VectorXZ)}.\n\t */\n\tpublic VectorXZ getEndCutVector();\n\t\n\t/**\n\t * Sets the cut vector for the end of the line. //TODO: explain \"cut vectors\"\n\t * To be used by {@link NetworkCalculator}.\n\t */\n\tpublic void setEndCutVector(VectorXZ cutVector);\n\n\t/**\n\t * returns the current offset for the end of the line.\n\t * Should already be usable before first {@link #setEndOffset(VectorXZ)}\n\t * call, returning (0,0).\n\t */\n\tpublic VectorXZ getStartOffset();\n\t\n\t/**\n\t * Sets the offset for the start of the line.\n\t * To be used by {@link NetworkCalculator}.\n\t */\n\tpublic void setStartOffset(VectorXZ offsetVector);\n\n\t/**\n\t * returns the current offset for the end of the line.\n\t * Should already be usable before first {@link #setEndOffset(VectorXZ)}\n\t * call, returning (0,0).\n\t */\n\tpublic VectorXZ getEndOffset();\n\t\n\t/**\n\t * Sets the offset for the end of the line.\n\t * To be used by {@link NetworkCalculator}.\n\t */\n\tpublic void setEndOffset(VectorXZ offsetVector);\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/network/VisibleConnectorNodeWorldObject.java",
    "content": "package org.osm2world.core.world.network;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapSegment;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.world.creation.NetworkCalculator;\nimport org.osm2world.core.world.data.OutlineNodeWorldObject;\n\npublic abstract class VisibleConnectorNodeWorldObject\n\textends OutlineNodeWorldObject {\n\n\tprotected boolean informationProvided;\n\t\n\tprotected VectorXZ cutVector;\n\tprotected VectorXZ startPos;\n\tprotected VectorXZ endPos;\n\tprotected float startWidth;\n\tprotected float endWidth;\n\t\n\t/**\n\t * returns the length required by this node representation.\n\t * Adjacent lines will be pushed back accordingly.\n\t * \n\t * If this is 0, this has the same effect as an invisible\n\t * connector node (adjacent line representations\n\t * directly touching each other). Examples where non-zero values\n\t * are needed include crossings at nodes in roads.\n\t * \n\t * Needs to be provided by the implementing class before the\n\t * calculation in {@link NetworkCalculator} starts.\n\t */\n\tabstract public float getLength();\n\t\n\t/**\n\t * sets the results of {@link NetworkCalculator}'s calculations.\n\t * Most methods in this class cannot be used until this method\n\t * has provided the required information!\n\t */\n\tpublic void setInformation(VectorXZ cutVector,\n\t\t\tVectorXZ startPos, VectorXZ endPos,\n\t\t\tfloat startWidth, float endWidth) {\n\t\t\n\t\tthis.informationProvided = true;\n\t\t\n\t\tthis.cutVector = cutVector;\n\t\tthis.startPos = startPos;\n\t\tthis.endPos = endPos;\n\t\tthis.startWidth = startWidth;\n\t\tthis.endWidth = endWidth;\n\t\t\n\t}\n\t\n\tpublic VisibleConnectorNodeWorldObject(MapNode node) {\n\t\tsuper(node);\n\t}\n\t\n\t@Override\n\tpublic SimplePolygonXZ getOutlinePolygonXZ() {\n\t\t\n\t\tList<VectorXZ> outlineXZ = new ArrayList<VectorXZ>(getOutlineXZ(0, 0));\n\t\toutlineXZ.addAll(getOutlineXZ(1, 1));\n\t\t\n\t\tif (outlineXZ.size() > 2) {\n\t\t\n\t\t\ttry { //TODO better handling of broken outlines\n\t\t\t\toutlineXZ.add(outlineXZ.get(0));\n\t\t\t\treturn new SimplePolygonXZ(outlineXZ);\n\t\t\t} catch (InvalidGeometryException e) {}\n\t\t\t\n\t\t\t}\n\t\t\n\t\treturn null;\n\t\t\n\t}\n\t\n\t/**\n\t * provides outline for the areas covered by the connector.\n\t * \n\t * The from and to indices refer to the list\n\t * returned by the underlying {@link MapNode}'s\n\t * {@link MapNode#getConnectedSegments()} method.\n\t */\n\tprotected List<VectorXZ> getOutlineXZ(int from, int to) {\n\t\n\t\tcheckInformationProvided();\n\t\t\n\t\tList<VectorXZ> outline = new ArrayList<VectorXZ>();\n\t\n\t\tList<MapSegment> segments = node.getConnectedSegments();\n\t\n\t\tassert from >= 0 && from < segments.size();\n\t\tassert to >= 0 && to < segments.size();\n\t\n\t\tif (((from == 1 && to == 0) || (from == 0 && to == 1))) {\n\t\t\t\t\t\t\n\t\t\tif (from == 0) {\n\t\t\t\t\n\t\t\t\tVectorXZ pos1 = startPos\n\t\t\t\t\t.add(cutVector.mult(startWidth));\n\t\n\t\t\t\tVectorXZ pos2 = endPos\n\t\t\t\t\t.add(cutVector.mult(endWidth));\n\t\t\t\t\n\t\t\t\toutline.add(pos1);\n\t\t\t\toutline.add(pos2);\n\t\t\t\t\n\t\t\t} else {\n\t\n\t\t\t\tVectorXZ pos1 = endPos\n\t\t\t\t\t.subtract(cutVector.mult(endWidth));\n\t\n\t\t\t\tVectorXZ pos2 = startPos\n\t\t\t\t\t.subtract(cutVector.mult(startWidth));\n\t\t\t\t\n\t\t\t\toutline.add(pos1);\n\t\t\t\toutline.add(pos2);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t} else if (from == to\n\t\t\t\t&& segments.get(from) instanceof MapWaySegment) { //usually at the end of a noexit road\n\t\t\t\n\t\t\tMapWaySegment segment = (MapWaySegment) segments.get(from);\n\t\t\t\n\t\t\tif (segment.getPrimaryRepresentation() instanceof NetworkWaySegmentWorldObject) {\n\t\t\t\t\n\t\t\t\tNetworkWaySegmentWorldObject rep =\n\t\t\t\t\t(NetworkWaySegmentWorldObject) segment.getPrimaryRepresentation();\n\t\t\t\t\n\t\t\t\t//TODO: the calculations for pos1/2 should be part of the NetworkLineRepresentation (it's used quite often)\n\t\t\t\t\n\t\t\t\tif (segment.getEndNode() == node) { //inbound segment\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ pos1 = node.getPos()\n\t\t\t\t\t\t.add(rep.getEndOffset())\n\t\t\t\t\t\t.add(rep.getEndCutVector().mult(rep.getWidth()/2));\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ pos2 = node.getPos()\n\t\t\t\t\t\t.add(rep.getEndOffset())\n\t\t\t\t\t\t.subtract(rep.getEndCutVector().mult(rep.getWidth()/2));\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\toutline.add(pos1);\n\t\t\t\t\toutline.add(pos2);\n\t\t\t\t\t\n\t\t\t\t} else { //outbound segment\n\t\n\t\t\t\t\tVectorXZ pos1 = node.getPos()\n\t\t\t\t\t\t.add(rep.getStartOffset())\n\t\t\t\t\t\t.subtract(rep.getStartCutVector().mult(rep.getWidth()/2));\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ pos2 = node.getPos()\n\t\t\t\t\t\t.add(rep.getStartOffset())\n\t\t\t\t\t\t.add(rep.getStartCutVector().mult(rep.getWidth()/2));\n\t\n\t\t\t\t\toutline.add(pos1);\n\t\t\t\t\toutline.add(pos2);\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\treturn outline;\n\t\t\n\t}\n\t\n\t/**\n\t * throws an IllegalStateException if information hasn't been\n\t * provided by a {@link NetworkCalculator}\n\t */\n\tprivate void checkInformationProvided() throws IllegalStateException {\n\t\tif (!informationProvided) {\n\t\t\tthrow new IllegalStateException(\"no connector information\"\n\t\t\t\t\t+ \" has been set for this representation.\\n\"\n\t\t\t\t\t+ \"node: \" + node);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/network/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nfunctionality used by groups of features that aren't isolated, but form a \"network\" of nodes, ways and areas\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/core/world/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nthe three-dimensional \"world\" created by OSM2World\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/Viewer.java",
    "content": "package org.osm2world.viewer;\n\nimport javax.swing.UIManager;\n\nimport org.apache.commons.configuration.BaseConfiguration;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic class Viewer {\n\t\n\tpublic static void main(String[] args) {\n\t\tnew Viewer(args);\n\t}\n\t\n\tprivate Viewer(String[] args) {\n\t\t\n\t\ttry {\n\t\t\tUIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());\n\t\t} catch(Exception e) {\n\t\t\tSystem.out.println(\"Error setting native look and feel: \" + e);\n\t\t}\n\t\t\n\t\tnew ViewerFrame(new BaseConfiguration(), null, null).setVisible(true);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/AboutAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JOptionPane;\nimport static org.osm2world.core.GlobalValues.*;\n\npublic class AboutAction extends AbstractAction {\n\n\tprivate static final long serialVersionUID = -6717063896933933005L; //generated serialVersionUID\n\n\tpublic AboutAction() {\n\t\tsuper(\"About OSM2World\");\n\t}\n\t\n\t@Override\n\tpublic void actionPerformed(ActionEvent arg0) {\n\t\tJOptionPane.showMessageDialog(null, \"OSM2World\\nVersion \" + VERSION_STRING\n\t\t\t\t+ \"\\n\\nFor more information, visit\\n\"\n\t\t\t\t+ WIKI_URI + \"\\n\" + OSM2WORLD_URI, \n\t\t\t\t\"About OSM2World\", JOptionPane.INFORMATION_MESSAGE);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/AbstractExportAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.Cursor;\nimport java.awt.event.ActionEvent;\nimport java.io.File;\nimport java.util.Observable;\nimport java.util.Observer;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JFileChooser;\nimport javax.swing.filechooser.FileNameExtensionFilter;\n\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.MessageManager;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ProgressDialog;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic abstract class AbstractExportAction\n\textends AbstractAction implements Observer {\n\n\tprivate static final long serialVersionUID = 76411168278378356L; //generated serialVersionUID\n\tprotected final ViewerFrame viewerFrame;\n\tprotected final Data data;\n\tprotected final MessageManager messageManager;\n\tprotected final RenderOptions renderOptions;\n\t\n\tprivate File lastPath = null;\n\n\tprotected AbstractExportAction(String name,\n\t\t\tViewerFrame viewerFrame, Data data,\n\t\t\tMessageManager messageManager,\n\t\t\tRenderOptions renderOptions) {\n\t\t\n\t\tsuper(name);\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.messageManager = messageManager;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\t\n\t\tsetEnabled(false);\n\t\tdata.addObserver(this);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void update(Observable o, Object arg) {\n\t\tsetEnabled(data.getConversionResults() != null);\n\t}\n\n\tprotected boolean chooseDirectory() { return false; }\n\t\n\tabstract protected FileNameExtensionFilter getFileNameExtensionFilter();\n\n\tabstract protected void performExport(File file);\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent arg0) {\n\t\n\t\tFile file = askFile();\n\t\t\n\t\tif (file != null) {\n\t\t\tnew ExportFileThread(file).start();\n\t\t}\n\t\t\n\t}\n\t\n\tprivate File askFile() {\n\t\t\n\t\tJFileChooser chooser = new JFileChooser(lastPath);\n\t\tchooser.setDialogTitle(\"Export file\");\n\t\t\n\t\tif (chooseDirectory()) {\n\t\t\tchooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);\n\t\t} else {\n\t\t\tchooser.setFileFilter(getFileNameExtensionFilter());\n\t\t}\n\t\t\n\t\tint returnVal = chooser.showSaveDialog(null);\n\t\n\t\tif (returnVal == JFileChooser.APPROVE_OPTION) {\n\t\t\t\n\t\t\tFile selectedFile = chooser.getSelectedFile();\n\t\t\t\n\t\t\tlastPath = selectedFile.getParentFile();\n\t\t\t\n\t\t\t/* make sure that file uses correct extension */\n\t\t\t\t\t\n\t\t\tif (getFileNameExtensionFilter() != null &&\n\t\t\t\t\tgetFileNameExtensionFilter().getExtensions().length == 1) {\n\t\t\t\t\n\t\t\t\tString extension = getFileNameExtensionFilter().getExtensions()[0];\n\t\t\t\t\t\t\t\t\n\t\t\t\tif (!selectedFile.getPath().endsWith(\".\" + extension)) {\n\t\t\t\t\tselectedFile = new File(selectedFile.getPath() + \".\" + extension);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\treturn selectedFile;\n\t\t\t\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t}\n\tprivate class ExportFileThread extends Thread {\n\n\t\tprivate final File file;\n\t\tprivate ProgressDialog progressDialog;\n\n\t\tpublic ExportFileThread(File file) {\n\t\t\tsuper(\"ExportFileThread\");\n\t\t\tthis.file = file;\n\t\t}\n\n\t\t@Override\n\t\tpublic void run() {\n\n\t\t\tviewerFrame.setCursor(Cursor\n\t\t\t\t\t.getPredefinedCursor(Cursor.WAIT_CURSOR));\n\t\t\tviewerFrame.setEnabled(false);\n\n\t\t\tString progressDescription = \"Export file\";\n\t\t\tif (getFileNameExtensionFilter() != null) {\n\t\t\t\tprogressDescription += \" of type \"\n\t\t\t\t\t\t+ getFileNameExtensionFilter().getDescription();\n\t\t\t}\n\t\t\t\n\t\t\tprogressDialog = new ProgressDialog(viewerFrame, progressDescription);\n\t\t\tprogressDialog.setProgress(null);\n\t\t\tprogressDialog.setText(\"Writing file: \" + file.getAbsolutePath());\n\t\t\t\n\t\t\tperformExport(file);\n\n\t\t\tprogressDialog.dispose();\n\n\t\t\tviewerFrame.setCursor(Cursor\n\t\t\t\t\t.getPredefinedCursor(Cursor.DEFAULT_CURSOR));\n\t\t\tviewerFrame.setEnabled(true);\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ExitAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.KeyStroke;\n\n/**\n * closes the application */\npublic class ExitAction extends AbstractAction {\n\t\t\n\tprivate static final long serialVersionUID = -7239839993534668987L; //generated serialVersionUID\n\n\tpublic ExitAction() {\n\t\tsuper(\"Exit\");\n\t\tputValue(SHORT_DESCRIPTION, \"Closes the application\");\n\t\tputValue(MNEMONIC_KEY, KeyEvent.VK_X);\n\t\tputValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(\n\t\t\t\tKeyEvent.VK_Q, ActionEvent.CTRL_MASK));\n\t}\n\t\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\t\tSystem.exit(0);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ExportObjAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.HeadlessException;\nimport java.awt.event.KeyEvent;\nimport java.io.File;\nimport java.io.IOException;\n\nimport javax.swing.JOptionPane;\nimport javax.swing.filechooser.FileNameExtensionFilter;\n\nimport org.osm2world.core.target.obj.ObjWriter;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.MessageManager;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic class ExportObjAction extends AbstractExportAction {\n\n\tprivate static final long serialVersionUID = -6233943695461766122L;  //generated serialVersionUID\n\n\tpublic ExportObjAction(ViewerFrame viewerFrame, Data data,\n\t\t\tMessageManager messageManager, RenderOptions renderOptions) {\n\n\t\tsuper(\"Export OBJ file\", viewerFrame, data, messageManager, renderOptions);\n\t\tputValue(SHORT_DESCRIPTION, \"Writes a Wavefront .obj file\");\n\t\tputValue(MNEMONIC_KEY, KeyEvent.VK_O);\n\n\t}\n\n\tprotected FileNameExtensionFilter getFileNameExtensionFilter() {\n\t\treturn new FileNameExtensionFilter(\"Wavefront .obj files\", \"obj\");\n\t}\n\n\t@Override\n\tprotected void performExport(File file) throws HeadlessException {\n\n\t\ttry {\n\t\t\t\n\t\t\t/* write the file */\n\n\t\t\tObjWriter.writeObjFile(\n\t\t\t\t\tfile,\n\t\t\t\t\tdata.getConversionResults().getMapData(),\n\t\t\t\t\tdata.getConversionResults().getMapProjection(),\n\t\t\t\t\tnull, renderOptions.projection);\n\n\t\t\tmessageManager.addMessage(\"exported Wavefront .obj file \" + file);\n\n\t\t} catch (IOException e) {\n\t\t\tJOptionPane.showMessageDialog(viewerFrame,\n\t\t\t\t\te.toString(),\n\t\t\t\t\t\"Could not export Wavefront .obj file\",\n\t\t\t\t\tJOptionPane.ERROR_MESSAGE);\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ExportObjDirAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.HeadlessException;\nimport java.io.File;\nimport java.io.IOException;\n\nimport javax.swing.JOptionPane;\nimport javax.swing.filechooser.FileNameExtensionFilter;\n\nimport org.osm2world.core.target.obj.ObjWriter;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.MessageManager;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic class ExportObjDirAction extends AbstractExportAction {\n\n\tprivate static final long serialVersionUID = 6380889966390760664L; //generated serialVersionUID\n\n\tpublic ExportObjDirAction(ViewerFrame viewerFrame, Data data,\n\t\t\tMessageManager messageManager, RenderOptions renderOptions) {\n\n\t\tsuper(\"Export OBJ directory\", viewerFrame, data, messageManager, renderOptions);\n\t\tputValue(SHORT_DESCRIPTION, \"Writes several smaller Wavefront .obj files to a directory\");\n\t\t\n\t}\n\t\n\t@Override\n\tprotected boolean chooseDirectory() {\n\t\treturn true;\n\t}\n\t\n\tprotected FileNameExtensionFilter getFileNameExtensionFilter() {\n\t\treturn null;\n\t}\n\n\t@Override\n\tprotected void performExport(File file) throws HeadlessException {\n\n\t\ttry {\n\t\t\t\n\t\t\tString thresholdString = JOptionPane.showInputDialog(\n\t\t\t\t\tviewerFrame, \"Graphics primitives per file\", 10000);\n\t\t\t\n\t\t\tint primitiveThresholdPerFile = Integer.parseInt(thresholdString);\n\t\t\t\n\t\t\t/* write the file */\n\n\t\t\tObjWriter.writeObjFiles(\n\t\t\t\t\tfile,\n\t\t\t\t\tdata.getConversionResults().getMapData(),\n\t\t\t\t\tdata.getConversionResults().getMapProjection(),\n\t\t\t\t\tnull, renderOptions.projection,\n\t\t\t\t\tprimitiveThresholdPerFile);\n\n\t\t\tmessageManager.addMessage(\"exported Wavefront .obj file \" + file);\n\n\t\t} catch (IOException e) {\n\t\t\tJOptionPane.showMessageDialog(viewerFrame,\n\t\t\t\t\te.toString(),\n\t\t\t\t\t\"Could not export Wavefront .obj file\",\n\t\t\t\t\tJOptionPane.ERROR_MESSAGE);\n\t\t\te.printStackTrace();\n\t\t} catch (NumberFormatException e) {\n\t\t\tJOptionPane.showMessageDialog(viewerFrame,\n\t\t\t\t\te.toString(),\n\t\t\t\t\t\"please enter a valid number of primitives per file\",\n\t\t\t\t\tJOptionPane.ERROR_MESSAGE);\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ExportPOVRayAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.HeadlessException;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\nimport java.io.File;\nimport java.io.IOException;\n\nimport javax.swing.JOptionPane;\nimport javax.swing.KeyStroke;\nimport javax.swing.filechooser.FileNameExtensionFilter;\n\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.povray.POVRayWriter;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.MessageManager;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n\npublic class ExportPOVRayAction extends AbstractExportAction {\n\n\tprivate static final long serialVersionUID = -6019526579344140850L; //generated serialVersionUID\n\n\tpublic ExportPOVRayAction(ViewerFrame viewerFrame, Data data,\n\t\t\tMessageManager messageManager, RenderOptions renderOptions) {\n\n\t\tsuper(\"Export POVRay file\", viewerFrame, data, messageManager, renderOptions);\n\t\tputValue(SHORT_DESCRIPTION, \"Writes a source file for the POVRay raytracer\");\n\t\tputValue(MNEMONIC_KEY, KeyEvent.VK_P);\n\t\tputValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(\n\t\t\t\tKeyEvent.VK_P, ActionEvent.CTRL_MASK));\n\n\t}\n\n\tprotected FileNameExtensionFilter getFileNameExtensionFilter() {\n\t\treturn new FileNameExtensionFilter(\"POVRay source files\", \"pov\");\n\t}\n\n\t@Override\n\tprotected void performExport(File file) throws HeadlessException {\n\n\t\ttry {\n\n\t\t\t/* create camera and perspective\n\t\t\t   compensating for left- vs. right-handed coords */\n\n\t\t\tCamera povRayCamera = new Camera();\n\t\t\tpovRayCamera.setCamera(\n\t\t\t\t\trenderOptions.camera.getPos().x, \n\t\t\t\t\trenderOptions.camera.getPos().y, \n\t\t\t\t\trenderOptions.camera.getPos().z,\n\t\t\t\t\trenderOptions.camera.getLookAt().x, \n\t\t\t\t\trenderOptions.camera.getLookAt().y, \n\t\t\t\t\trenderOptions.camera.getLookAt().z,\n\t\t\t\t\trenderOptions.camera.getUp().x,\n\t\t\t\t\trenderOptions.camera.getUp().y,\n\t\t\t\t\trenderOptions.camera.getUp().z);\n\n\t\t\t/* write the file */\n\n\t\t\tPOVRayWriter.writePOVInstructionFile(\n\t\t\t\t\tfile, \n\t\t\t\t\tdata.getConversionResults().getMapData(),\n\t\t\t\t\tpovRayCamera, renderOptions.projection);\n\n\t\t\tmessageManager.addMessage(\"exported POVRay file \" + file);\n\n\t\t} catch (IOException e) {\n\t\t\tJOptionPane.showMessageDialog(viewerFrame,\n\t\t\t\t\te.toString(),\n\t\t\t\t\t\"Could not export POVRay file\", JOptionPane.ERROR_MESSAGE);\n\t\t\te.printStackTrace();\t\t\t\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ExportScreenshotAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.HeadlessException;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.io.IOException;\n\nimport javax.imageio.ImageIO;\nimport javax.swing.JOptionPane;\nimport javax.swing.KeyStroke;\nimport javax.swing.filechooser.FileNameExtensionFilter;\n\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.MessageManager;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\nimport com.jogamp.opengl.util.awt.AWTGLReadBufferUtil;\n\npublic class ExportScreenshotAction extends AbstractExportAction {\n\n\tprivate static final long serialVersionUID = 8777435425813342813L; //generated serialVersionUID\n\n\tpublic ExportScreenshotAction(ViewerFrame viewerFrame, Data data,\n\t\t\tMessageManager messageManager, RenderOptions renderOptions) {\n\n\t\tsuper(\"Export Screenshot\", viewerFrame, data, messageManager, renderOptions);\n\t\tputValue(SHORT_DESCRIPTION, \"Writes the current display content to an image file\");\n\t\tputValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(\n\t\t\t\tKeyEvent.VK_PRINTSCREEN, ActionEvent.CTRL_MASK));\n\n\t}\n\n\tprotected FileNameExtensionFilter getFileNameExtensionFilter() {\n\t\treturn new FileNameExtensionFilter(\"PNG image files\", \"png\");\n\t}\n\n\t@Override\n\tprotected void performExport(File file) throws HeadlessException {\n\n\t\ttry {\n\t\t\t\n\t\t\tviewerFrame.glCanvas.getContext().makeCurrent();\n\t\t\t\n\t\t\tboolean exportAlpha = data.getConfig().getBoolean(\"exportAlpha\", false);\n\t\t\tAWTGLReadBufferUtil reader = new AWTGLReadBufferUtil(viewerFrame.glCanvas.getGLProfile(), exportAlpha);\n\t\t\tBufferedImage img = reader.readPixelsToBufferedImage(viewerFrame.glCanvas.getGL(), true);\n\t\t    ImageIO.write(img, \"png\", file);\n\n\t\t\tviewerFrame.glCanvas.getContext().release();\n\t\t\t\n\t\t\tmessageManager.addMessage(\"exported PNG file \" + file);\n\n\t\t} catch (IOException e) {\n\t\t\tJOptionPane.showMessageDialog(viewerFrame,\n\t\t\t\t\te.toString(),\n\t\t\t\t\t\"Could not export PNG file\", JOptionPane.ERROR_MESSAGE);\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/HelpControlsAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JOptionPane;\n\npublic class HelpControlsAction extends AbstractAction {\n\t\n\tprivate static final long serialVersionUID = 9195787581575616092L; //generated serialVersionUID\n\n\tpublic HelpControlsAction() {\n\t\tsuper(\"Controls\");\n\t}\n\t\n\t@Override\n\tpublic void actionPerformed(ActionEvent arg0) {\n\t\tJOptionPane.showMessageDialog(null, \"OSM2World GUI Controls:\\n\"\n\t\t\t\t+ \"* left mouse button moves the camera position\\n\"\n\t\t\t\t+ \"* right mouse button rotates the camera direction\\n\"\n\t\t\t\t+ \"* mouse wheel moves the camera closer to\" +\n\t\t\t\t\t\t\" or away from the ground\\n\",\n\t\t\t\t\"About OSM2World\", JOptionPane.INFORMATION_MESSAGE);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/OpenOSMAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.Cursor;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\nimport java.io.File;\nimport java.io.IOException;\nimport java.lang.Thread.UncaughtExceptionHandler;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JFileChooser;\nimport javax.swing.JOptionPane;\nimport javax.swing.KeyStroke;\nimport javax.swing.SwingUtilities;\nimport javax.swing.filechooser.FileNameExtensionFilter;\n\nimport org.osm2world.core.ConversionFacade.BoundingBoxSizeException;\nimport org.osm2world.core.ConversionFacade.Phase;\nimport org.osm2world.core.ConversionFacade.ProgressListener;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.TerrainInterpolator;\nimport org.osm2world.core.math.InvalidGeometryException;\nimport org.osm2world.core.util.functions.DefaultFactory;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ProgressDialog;\nimport org.osm2world.viewer.view.RecentFilesUpdater;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic class OpenOSMAction extends AbstractAction {\n\n\tprivate static final long serialVersionUID = -71970104383745134L; //generated serialVersionUID\n\tViewerFrame viewerFrame;\n\tData data;\n\tRenderOptions renderOptions;\n\n\tprivate File lastPath = null;\n\n\tpublic OpenOSMAction(ViewerFrame viewerFrame, Data data, RenderOptions renderOptions) {\n\n\t\tsuper(\"Open OSM file\");\n\t\tputValue(SHORT_DESCRIPTION, \"Opens a file with OpenStreetMap data\");\n\t\tputValue(MNEMONIC_KEY, KeyEvent.VK_O);\n\t\tputValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(\n\t\t\t\tKeyEvent.VK_O, ActionEvent.CTRL_MASK));\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\t\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent arg0) {\n\n\t\tFile osmFile = askFile();\n\n\t\tif (osmFile != null) {\n\n\t\t\topenOSMFile(osmFile, true);\n\t\t\t\n\t\t}\n\n\t}\n\n\tpublic void openOSMFile(File osmFile, boolean resetCamera) {\n\t\t\n\t\tOpenOSMThread thread = new OpenOSMThread(osmFile, resetCamera);\n\t\tthread.setUncaughtExceptionHandler(\n\t\t\t\tnew ConversionExceptionHandler(viewerFrame));\n\t\tthread.start();\n\t\t\n\t\tRecentFilesUpdater.addRecentFile(osmFile);\n\t\t\n\t}\n\n\tprivate File askFile() {\n\n\t\tJFileChooser chooser = new JFileChooser(lastPath);\n\t\tchooser.setDialogTitle(\"Open OSM file\");\n\t\tchooser.setFileFilter(new FileNameExtensionFilter(\n\t\t\t\t\"OpenStreetMap data files\", \"osm\", \"gz\", \"bz2\", \"pbf\"));\n\n\t\tint returnVal = chooser.showOpenDialog(null);\n\n\t\tif (returnVal == JFileChooser.APPROVE_OPTION) {\n\t\t\tFile selectedFile = chooser.getSelectedFile();\n\t\t\tlastPath = selectedFile.getParentFile();\n\t\t\treturn selectedFile;\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\n\t\t//\t\t/* alternative implementation using FileDialog */\n\t\t//\n\t\t//\t\tFileDialog dialog =\n\t\t//            new FileDialog(viewerFrame, \"Open OSM file\", FileDialog.LOAD);\n\t\t//\n\t\t//\t\tdialog.setVisible(true);\n\t\t//\n\t\t//\t\treturn new File(dialog.getFile());\n\n\t}\n\n\tprivate class OpenOSMThread extends Thread implements ProgressListener {\n\n\t\tprivate final File osmFile;\n\t\tprivate final boolean resetCamera;\n\t\t\n\t\tprivate ProgressDialog progressDialog;\n\n\t\tpublic OpenOSMThread(File osmFile, boolean resetCamera) {\n\t\t\tsuper(\"OpenOSMThread\");\n\t\t\tthis.osmFile = osmFile;\n\t\t\tthis.resetCamera = resetCamera;\n\t\t}\n\n\t\t@Override\n\t\tpublic void run() {\n\n\t\t\tviewerFrame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));\n\t\t\tviewerFrame.setEnabled(false);\n\t\t\n\t\t\tboolean failOnLargeBBox = true;\n\t\t\tboolean runAgain = true;\n\t\t\t\n\t\t\t/* check file size */\n\t\t\t\n\t\t\tif (osmFile.length() > 1e7) {\n\t\t\t\t\n\t\t\t\tString[] options = new String[] {\"Try anyway\" ,\"Cancel\"};\n\t\t\t\t\n\t\t\t\tint answer = JOptionPane.showOptionDialog(viewerFrame,\n\t\t\t\t\t\t\"The input file is probably too big.\\n\"\n\t\t\t\t\t\t+ \"This viewer can only handle relatively small areas well.\\n\",\n\t\t\t\t\t\t\"Large input file size\",\n\t\t\t\t\t\tJOptionPane.OK_CANCEL_OPTION,\n\t\t\t\t\t\tJOptionPane.WARNING_MESSAGE,\n\t\t\t\t\t\tnull, options, options[1]);\n\t\t\t\t\n\t\t\t\trunAgain = (answer != 1);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t/* attempt to open the file */\n\t\t\t\t\t\t\n\t\t\twhile (runAgain) {\n\t\t\t\t\n\t\t\t\trunAgain = false;\n\t\t\t\t\n\t\t\t\tprogressDialog = new ProgressDialog(viewerFrame, \"Open OSM File\");\n\t\t\t\t\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\t\n\t\t\t\t\tdata.loadOSMFile(osmFile, failOnLargeBBox,\n\t\t\t\t\t\t\tnew DefaultFactory<TerrainInterpolator>(\n\t\t\t\t\t\t\t\t\trenderOptions.getInterpolatorClass()),\n\t\t\t\t\t\t\tnew DefaultFactory<EleConstraintEnforcer>(\n\t\t\t\t\t\t\t\t\trenderOptions.getEnforcerClass()),\n\t\t\t\t\t\t\tthis);\n\t\n\t\t\t\t\tif (resetCamera) {\n\t\t\t\t\t\tnew ResetCameraAction(viewerFrame, data, renderOptions).actionPerformed(null);\n\t\t\t\t\t}\n\t\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\t\n\t\t\t\t\tJOptionPane.showMessageDialog(viewerFrame,\n\t\t\t\t\t\t\te.toString() + \"\\nCause: \" +\n\t\t\t\t\t\t\t(e.getCause() == null ? \"unknown\" : e.getCause()),\n\t\t\t\t\t\t\t\"Could not open OSM file\", JOptionPane.ERROR_MESSAGE);\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t\t\n\t\t\t\t} catch (InvalidGeometryException e) {\n\t\t\t\t\t\n\t\t\t\t\tJOptionPane.showMessageDialog(viewerFrame,\n\t\t\t\t\t\t\t\"The OSM data contains broken geometry.\\n\"\n\t\t\t\t\t\t\t+ \"Make sure you are not using an extract with\"\n\t\t\t\t\t\t\t+ \" incomplete areas!\\nIf you are e.g. using Osmosis,\"\n\t\t\t\t\t\t\t+ \" you need to use its completeWays=yes parameter.\\n\"\n\t\t\t\t\t\t\t+ \"See command line output for error details.\",\n\t\t\t\t\t\t\t\"Could not perform conversion\", JOptionPane.ERROR_MESSAGE);\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t\t\n\t\t\t\t} catch (BoundingBoxSizeException e) {\n\t\t\t\t\t\n\t\t\t\t\tString[] options = new String[] {\"Try anyway\" ,\"Cancel\"};\n\t\t\t\t\t\n\t\t\t\t\tint answer = JOptionPane.showOptionDialog(viewerFrame,\n\t\t\t\t\t\t\t\"The input file covers a large bounding box.\\n\"\n\t\t\t\t\t\t\t+ \"This viewer can only handle relatively small areas well.\\n\",\n\t\t\t\t\t\t\t\"Large bounding box\",\n\t\t\t\t\t\t\tJOptionPane.OK_CANCEL_OPTION,\n\t\t\t\t\t\t\tJOptionPane.WARNING_MESSAGE,\n\t\t\t\t\t\t\tnull, options, options[1]);\n\t\t\t\t\t\n\t\t\t\t\tfailOnLargeBBox = false;\n\t\t\t\t\trunAgain = (answer != 1);\n\t\t\t\t\t\n\t\t\t\t}\n\n\t\t\t\tprogressDialog.dispose();\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tviewerFrame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));\n\t\t\tviewerFrame.setEnabled(true);\n\n\t\t}\n\n\t\t@Override\n\t\tpublic void updatePhase(Phase newPhase) {\n\t\t\tswitch (newPhase) {\n\t\t\tcase MAP_DATA:\n\t\t\t\tprogressDialog.setProgress(0);\n\t\t\t\tprogressDialog.setText(\"1/5: Organize information from .osm file...\");\n\t\t\t\tbreak;\n\t\t\tcase REPRESENTATION:\n\t\t\t\tprogressDialog.setProgress(20);\n\t\t\t\tprogressDialog.setText(\"2/5: Choose visual representations for OSM objects...\");\n\t\t\t\tbreak;\n\t\t\tcase ELEVATION:\n\t\t\t\tprogressDialog.setProgress(40);\n\t\t\t\tprogressDialog.setText(\"3/5: Guess elevations from available information...\");\n\t\t\t\tbreak;\n\t\t\tcase TERRAIN:\n\t\t\t\tprogressDialog.setProgress(60);\n\t\t\t\tprogressDialog.setText(\"4/5: Generate terrain...\");\n\t\t\t\tbreak;\n\t\t\tcase FINISHED:\n\t\t\t\tprogressDialog.setProgress(80);\n\t\t\t\tprogressDialog.setText(\"5/5: Represent objects by 3D primitives...\");\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t}\n\n\t}\n\t\n\tprivate static class ConversionExceptionHandler\n\t\t\timplements UncaughtExceptionHandler {\n\t\t\n\t\tprivate final ViewerFrame viewerFrame;\n\t\t\n\t\tpublic ConversionExceptionHandler(ViewerFrame viewerFrame) {\n\t\t\tthis.viewerFrame = viewerFrame;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void uncaughtException(final Thread t, final Throwable e) {\n\t\t\tif (SwingUtilities.isEventDispatchThread()) {\n\t\t\t\tshowException(t, e);\n\t\t\t} else {\n\t\t\t\tSwingUtilities.invokeLater(new Runnable() {\n\t\t\t\t\tpublic void run() {\n\t\t\t\t\t\tshowException(t, e);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tprivate void showException(Thread t, Throwable e) {\n\t\t\t\n\t\t\t// TODO log\n\t\t\te.printStackTrace();\n\t\t\t\n\t\t\tString msg = String.format(\n\t\t\t\t\t\"Unexpected problem on thread %s:\\n%s\\n\\n\"\n\t\t\t\t\t+ \"OSM2World will be closed now.\\n\\nLocation:\\n%s\\n%s\",\n\t\t\t\t\tt.getName(), e.toString(),\n\t\t\t\t\te.getStackTrace()[0], e.getStackTrace()[1]);\n\t\t\t\n\t\t\tJOptionPane.showMessageDialog(viewerFrame, msg,\n\t\t\t\t\t\"Error\", JOptionPane.ERROR_MESSAGE);\n\t\t\t\n\t\t\tSystem.exit(1);\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/OrthoBoundsAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Observable;\nimport java.util.Observer;\n\nimport javax.swing.AbstractAction;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil.CardinalDirection;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n\npublic class OrthoBoundsAction extends AbstractAction implements Observer {\n\n\tprivate static final long serialVersionUID = 3506647787633942591L; //generated serialVersionUID\n\tViewerFrame viewerFrame;\n\tData data;\n\tRenderOptions renderOptions;\n\n\tpublic OrthoBoundsAction(ViewerFrame viewerFrame, Data data, RenderOptions renderOptions) {\n\n\t\tsuper(\"Ortho bounds\");\n\t\tputValue(SHORT_DESCRIPTION, \"Switch to orthographic view of \"\n\t\t\t\t+ \"the entire map data from south.\");\n\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\n\t\tsetEnabled(false);\n\t\tdata.addObserver(this);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void update(Observable o, Object arg) {\n\t\tsetEnabled(data.getConversionResults() != null);\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\n\t\tAxisAlignedBoundingBoxXZ bounds =\n\t\t\tdata.getConversionResults().getMapData().getDataBoundary();\n\t\t\n\t\trenderOptions.camera =\n\t\t\tOrthoTilesUtil.cameraForBounds(bounds, 30, CardinalDirection.S);\n\n\t\trenderOptions.projection =\n\t\t\tOrthoTilesUtil.projectionForBounds(bounds, 30, CardinalDirection.S);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/OrthoTileAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Observable;\nimport java.util.Observer;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JOptionPane;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil.CardinalDirection;\nimport org.osm2world.core.target.common.rendering.TileNumber;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic class OrthoTileAction extends AbstractAction implements Observer {\n\n\tprivate static final long serialVersionUID = 4494511160535468154L; //generated serialVersionUID\n\tViewerFrame viewerFrame;\n\tData data;\n\tRenderOptions renderOptions;\n\n\tpublic OrthoTileAction(ViewerFrame viewerFrame, Data data, RenderOptions renderOptions) {\n\n\t\tsuper(\"Ortho tile\");\n\t\tputValue(SHORT_DESCRIPTION, \"Switch to orthographic view of a tile.\");\n\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\n\t\tsetEnabled(false);\n\t\tdata.addObserver(this);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void update(Observable o, Object arg) {\n\t\tsetEnabled(data.getConversionResults() != null);\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\n\t\ttry {\n\n\t\t\tint zoom = Integer.parseInt(\n\t\t\t\t\tJOptionPane.showInputDialog(viewerFrame, \"zoom level\"));\n\t\t\tint tileX = Integer.parseInt(\n\t\t\t\t\tJOptionPane.showInputDialog(viewerFrame, \"tile x\"));\n\t\t\tint tileY = Integer.parseInt(\n\t\t\t\t\tJOptionPane.showInputDialog(viewerFrame, \"tile y\"));\n\t\t\tint angle = Integer.parseInt(\n\t\t\t\t\tJOptionPane.showInputDialog(viewerFrame, \"view angle\"));\n\t\t\tCardinalDirection from = CardinalDirection.valueOf(\n\t\t\t\t\tJOptionPane.showInputDialog(viewerFrame, \"from cardinal direction\"));\n\t\t\t\n\t\t\tAxisAlignedBoundingBoxXZ tileBounds =\n\t\t\t\tdata.getConversionResults().getMapData().getDataBoundary();\n\t\t\t\n\t\t\trenderOptions.camera = OrthoTilesUtil.cameraForTile(\n\t\t\t\t\tdata.getConversionResults().getMapProjection(),\n\t\t\t\t\tnew TileNumber(zoom, tileX, tileY), angle, from);\n\n\t\t\trenderOptions.projection = OrthoTilesUtil.projectionForTile(\n\t\t\t\t\tdata.getConversionResults().getMapProjection(),\n\t\t\t\t\tnew TileNumber(zoom, tileX, tileY), angle, from);\n\t\t\t\t\t\t\n\t\t} catch (NumberFormatException nfe) {\n\t\t\tJOptionPane.showMessageDialog(viewerFrame, \"invalid input\");\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ReloadOSMAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\nimport java.io.File;\nimport java.util.Observable;\nimport java.util.Observer;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JOptionPane;\nimport javax.swing.KeyStroke;\n\nimport org.apache.commons.configuration.ConfigurationException;\nimport org.apache.commons.configuration.PropertiesConfiguration;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n/**\n * reloads the previously opened OSM file\n */\npublic class ReloadOSMAction extends AbstractAction implements Observer {\n\n\tprivate static final long serialVersionUID = 1162049141590529184L; //generated serialVersionUID\n\tprivate final ViewerFrame viewerFrame;\n\tprivate final Data data;\n\tprivate final RenderOptions renderOptions;\n\tprivate final File configFile;\n\t\n\tpublic ReloadOSMAction(ViewerFrame viewerFrame, Data data,\n\t\t\tRenderOptions renderOptions, File configFile) {\n\t\t\n\t\tsuper(\"Reload OSM file\");\n\t\tputValue(SHORT_DESCRIPTION, \"Reloads the most recently opened OSM file\" +\n\t\t\t\t\" and the configuration file\");\n\t\tputValue(MNEMONIC_KEY, KeyEvent.VK_R);\n\t\tputValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(\n\t\t\t\tKeyEvent.VK_R, ActionEvent.CTRL_MASK));\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\tthis.configFile = configFile;\n\t\t\n\t\tthis.setEnabled(false);\n\t\t\n\t\tdata.addObserver(this);\n\t\t\n\t}\n\t\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent arg0) {\n\t\t\n\t\t/* reload config file */\n\t\t\n\t\tif (configFile != null) {\n\t\t\n\t\t\ttry {\n\t\t\t\t\n\t\t\t\tPropertiesConfiguration fileConfig = new PropertiesConfiguration();\n\t\t\t\tfileConfig.setListDelimiter(';');\n\t\t\t\tfileConfig.load(configFile);\n\t\t\t\tdata.setConfig(fileConfig);\n\t\t\t\t\n\t\t\t} catch (ConfigurationException e) {\n\t\t\t\t\n\t\t\t\tJOptionPane.showMessageDialog(viewerFrame,\n\t\t\t\t\t\t\"Could not reload the properties configuration file:\\n\"\n\t\t\t\t\t\t+ e.getMessage(),\n\t\t\t\t\t\t\"Error reloading configuration\",\n\t\t\t\t\t\tJOptionPane.WARNING_MESSAGE);\n\t\t\t\t\n\t\t\t\tSystem.err.println(e);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\t/* reload OSM file */\n\t\t\n\t\tnew OpenOSMAction(viewerFrame, data, renderOptions)\n\t\t\t\t.openOSMFile(data.getOsmFile(), false);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void update(Observable o, Object arg) {\n\t\tthis.setEnabled(data.getOsmFile() != null);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ResetCameraAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Observable;\nimport java.util.Observer;\n\nimport javax.swing.AbstractAction;\n\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.Defaults;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n\npublic class ResetCameraAction extends AbstractAction implements Observer {\n\n\tprivate static final long serialVersionUID = 9163911330093863591L; //generated serialVersionUID\n\tViewerFrame viewerFrame;\n\tData data;\n\tRenderOptions renderOptions;\n\n\tpublic ResetCameraAction(ViewerFrame viewerFrame, Data data, RenderOptions renderOptions) {\n\n\t\tsuper(\"Reset Camera\");\n\t\tputValue(SHORT_DESCRIPTION, \"Use this if you have messed up the camera\");\n\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\n\t\tsetEnabled(false);\n\t\tdata.addObserver(this);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void update(Observable o, Object arg) {\n\t\tsetEnabled(data.getConversionResults() != null);\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\n\t\tMapData mapData = data.getConversionResults().getMapData();\n\n\t\tVectorXZ camLookAt = mapData.getCenter();\n\n\t\trenderOptions.camera = new Camera();\n\t\trenderOptions.camera.setCamera(camLookAt.x, 1000, camLookAt.z-1000,\n                                       camLookAt.x, 0, camLookAt.z);\n\t\t\n\t\trenderOptions.projection = Defaults.PERSPECTIVE_PROJECTION;\n\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/SetCameraToCoordinateAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Observable;\nimport java.util.Observer;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JOptionPane;\n\nimport org.osm2world.core.map_data.creation.MapProjection;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic class SetCameraToCoordinateAction extends AbstractAction implements Observer {\n\n\tprivate static final long serialVersionUID = 8587163475874776786L; //generated serialVersionUID\n\tViewerFrame viewerFrame;\n\tData data;\n\tRenderOptions renderOptions;\n\n\tpublic SetCameraToCoordinateAction(ViewerFrame viewerFrame, Data data, RenderOptions renderOptions) {\n\n\t\tsuper(\"Set camera to coordinate\");\n\t\tputValue(SHORT_DESCRIPTION, \"Precisely position the camera\");\n\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\n\t\tsetEnabled(false);\n\t\tdata.addObserver(this);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void update(Observable o, Object arg) {\n\t\tsetEnabled(data.getConversionResults() != null);\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\n\t\tdouble lat = Double.parseDouble(\n\t\t\t\tJOptionPane.showInputDialog(viewerFrame, \"lat\"));\n\t\tdouble lon = Double.parseDouble(\n\t\t\t\tJOptionPane.showInputDialog(viewerFrame, \"lon\"));\n\t\tdouble height = Double.parseDouble(\n\t\t\t\tJOptionPane.showInputDialog(viewerFrame, \"height\"));\n\t\t\n\t\tMapProjection projection = data.getConversionResults().getMapProjection();\n\n\t\tVectorXZ newPosXZ = projection.calcPos(lat, lon);\n\t\tVectorXYZ newPos = newPosXZ.xyz(height);\n\t\t\n\t\trenderOptions.camera.move(newPos.subtract(renderOptions.camera.getPos()));\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/SetEleConstraintEnforcerAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JOptionPane;\n\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic class SetEleConstraintEnforcerAction extends AbstractAction {\n\n\tprivate static final long serialVersionUID = -5241031810160447221L; //generated serialVersionUID\n\tClass<? extends EleConstraintEnforcer> enforcerClass;\n\tViewerFrame viewerFrame;\n\tData data;\n\tRenderOptions renderOptions;\n\n\tpublic SetEleConstraintEnforcerAction(\n\t\t\tClass<? extends EleConstraintEnforcer> enforcerClass,\n\t\t\tViewerFrame viewerFrame, Data data, RenderOptions renderOptions) {\n\t\t\n\t\tsuper(enforcerClass.getSimpleName().replace(\"EleConstraintEnforcer\", \"\"));\n\t\t\n\t\tputValue(SELECTED_KEY, enforcerClass.equals(\n\t\t\t\trenderOptions.getEnforcerClass()));\n\t\t\n\t\tthis.enforcerClass = enforcerClass;\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\n\t\trenderOptions.setEnforcerClass(enforcerClass);\n\t\tputValue(SELECTED_KEY,\n\t\t\t\trenderOptions.getEnforcerClass().equals(enforcerClass));\n\t\t\n\t\tif (data.getConversionResults() != null) {\n\t\t\tJOptionPane.showMessageDialog(viewerFrame, \"You need to reload or\" +\n\t\t\t\t\t\" open a new OSM file for this option to have any effect!\",\n\t\t\t\t\t\"Reload required\", JOptionPane.INFORMATION_MESSAGE);\n\t\t}\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/SetTerrainInterpolatorAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JOptionPane;\n\nimport org.osm2world.core.map_elevation.creation.TerrainInterpolator;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic class SetTerrainInterpolatorAction extends AbstractAction {\n\n\tprivate static final long serialVersionUID = 6034246138682723498L; //generated serialVersionUID\n\tClass<? extends TerrainInterpolator> interpolatorClass;\n\tViewerFrame viewerFrame;\n\tData data;\n\tRenderOptions renderOptions;\n\n\tpublic SetTerrainInterpolatorAction(\n\t\t\tClass<? extends TerrainInterpolator> interpolatorClass,\n\t\t\tViewerFrame viewerFrame, Data data, RenderOptions renderOptions) {\n\t\t\n\t\tsuper(interpolatorClass.getSimpleName().replaceAll(\"Interpolator\", \"\"));\n\t\t\n\t\tputValue(SELECTED_KEY, interpolatorClass.equals(\n\t\t\t\trenderOptions.getInterpolatorClass()));\n\t\t\n\t\tthis.interpolatorClass = interpolatorClass;\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\n\t\trenderOptions.setInterpolatorClass(interpolatorClass);\n\t\tputValue(SELECTED_KEY,\n\t\t\t\trenderOptions.getInterpolatorClass().equals(interpolatorClass));\n\t\t\n\t\tif (data.getConversionResults() != null) {\n\t\t\tJOptionPane.showMessageDialog(viewerFrame, \"You need to reload or\" +\n\t\t\t\t\t\" open a new OSM file for this option to have any effect!\",\n\t\t\t\t\t\"Reload required\", JOptionPane.INFORMATION_MESSAGE);\n\t\t}\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ShowCameraConfigurationAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Observable;\nimport java.util.Observer;\n\nimport javax.swing.AbstractAction;\nimport javax.swing.JOptionPane;\n\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.map_data.creation.MapProjection;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\n\npublic class ShowCameraConfigurationAction\n\textends AbstractAction implements Observer {\n\t\n\tprivate static final long serialVersionUID = -3461617949419339009L;\n\tprivate final Data data;\n\tprivate final RenderOptions renderOptions;\n\t\n\tpublic ShowCameraConfigurationAction(Data data, RenderOptions renderOptions) {\n\t\t\n\t\tsuper(\"Show current camera configuration\");\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\t\n\t\tsetEnabled(false);\n\t\tdata.addObserver(this);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void actionPerformed(ActionEvent arg0) {\n\t\t\n\t\tResults r = data.getConversionResults();\n\t\t\n\t\tif (r == null) {\n\t\t\tJOptionPane.showMessageDialog(null, \"no Camera defined\");\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tMapProjection mapProjection = data.getConversionResults().getMapProjection();\n\t\t\n\t\tVectorXYZ pos = renderOptions.camera.getPos();\n\t\tVectorXYZ lookAt = renderOptions.camera.getLookAt();\n\t\t\n\t\tJOptionPane.showMessageDialog(null,\n\t\t\t\t\"posLat = \" + mapProjection.calcLat(pos.xz())\n\t\t\t\t+ \"\\nposLon = \" + mapProjection.calcLon(pos.xz())\n\t\t\t\t+ \"\\nposEle = \" + pos.y\n\t\t\t\t+ \"\\nlookAtLat = \" + mapProjection.calcLat(lookAt.xz())\n\t\t\t\t+ \"\\nlookAtLon = \" + mapProjection.calcLon(lookAt.xz())\n\t\t\t\t+ \"\\nlookAtEle = \" + lookAt.y,\n\t\t\t\t\"Current camera configuration\", JOptionPane.INFORMATION_MESSAGE);\n\t}\n\t\n\t@Override\n\tpublic void update(Observable o, Object arg) {\n\t\tsetEnabled(data.getConversionResults() != null);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/StatisticsAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Observable;\nimport java.util.Observer;\n\nimport javax.swing.AbstractAction;\n\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.target.TargetUtil;\nimport org.osm2world.core.target.statistics.StatisticsTarget;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.view.StatisticsDialog;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n\npublic class StatisticsAction extends AbstractAction implements Observer {\n\n\tprivate static final long serialVersionUID = -7894095901533692645L;\n\tprivate final ViewerFrame viewerFrame;\n\tprivate final Data data;\n\t\n\tpublic StatisticsAction(ViewerFrame viewerFrame, Data data) {\n\t\t\n\t\tsuper(\"Statistics\");\n\t\tputValue(SHORT_DESCRIPTION, \"Shows statistics for the current scene\");\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\t\n\t\tsetEnabled(false);\n\t\tdata.addObserver(this);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void update(Observable o, Object arg) {\n\t\tsetEnabled(data.getConversionResults() != null);\n\t}\n\t\n\t@Override\n\tpublic void actionPerformed(ActionEvent arg0) {\n\t\t\n\t\tResults conversionResults = data.getConversionResults();\n\t\t\n\t\tStatisticsTarget stats = new StatisticsTarget();\n\t\t\n\t\tTargetUtil.renderWorldObjects(stats, conversionResults.getMapData(), true);\n\t\tnew StatisticsDialog(viewerFrame, stats).setVisible(true);\n\t\t\n\t}\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ToggleBackfaceCullingAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\n\nimport javax.swing.AbstractAction;\n\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\npublic class ToggleBackfaceCullingAction extends AbstractAction {\n\n\tprivate static final long serialVersionUID = 3993313015641228064L;\n\tprivate final ViewerFrame viewerFrame;\n\tprivate final Data data;\n\tprivate final RenderOptions renderOptions;\n\t\t\n\tpublic ToggleBackfaceCullingAction(ViewerFrame viewerFrame, Data data,\n\t\t\tRenderOptions renderOptions) {\n\t\t\n\t\tsuper(\"Backface culling\");\n\t\tputValue(SHORT_DESCRIPTION, \"Switches backface culling on and off\");\t\n\t\tputValue(SELECTED_KEY, renderOptions.isBackfaceCulling());\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\t\t\t\t\n\t\trenderOptions.setBackfaceCulling(!renderOptions.isBackfaceCulling());\n\t\tputValue(SELECTED_KEY, renderOptions.isBackfaceCulling());\n\t\t\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ToggleDebugViewAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.util.Observable;\nimport java.util.Observer;\n\nimport javax.swing.AbstractAction;\n\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\nimport org.osm2world.viewer.view.debug.DebugView;\n\n\n\npublic class ToggleDebugViewAction extends AbstractAction implements Observer {\n\t\n\tprivate static final long serialVersionUID = -7415950981091744016L;\n\n\tprivate final DebugView debugView;\n\t\n\tprivate final ViewerFrame viewerFrame;\n\tprivate final Data data;\n\tprivate final RenderOptions renderOptions;\n\t\n\tprivate boolean enabled = false;\n\t\n\tpublic ToggleDebugViewAction(DebugView debugView, int mnemonicKey,\n\t\t\tboolean enabled, ViewerFrame viewerFrame,\n\t\t\tData data, RenderOptions renderOptions) {\n\t\t\n\t\tsuper(debugView.getClass().getSimpleName());\n\t\t\n\t\tputValue(SHORT_DESCRIPTION, debugView.getDescription());\n\t\tputValue(MNEMONIC_KEY, mnemonicKey);\n\t\tputValue(SELECTED_KEY, enabled);\n\t\t\n\t\tthis.debugView = debugView;\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\tthis.enabled = enabled;\n\t\t\n\t\tif (enabled) {\n\t\t\trenderOptions.activeDebugViews.add(debugView);\n\t\t}\n\t\t\n\t\tdata.addObserver(this);\n\t\t\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\t\t\n\t\tif (enabled) {\n\t\t\trenderOptions.activeDebugViews.remove(debugView);\n\t\t} else {\n\t\t\trenderOptions.activeDebugViews.add(debugView);\n\t\t}\n\t\t\t\n\t\tenabled = !enabled;\n\t\tputValue(SELECTED_KEY, enabled);\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void update(Observable o, Object arg) {\n\t\t\n\t\tviewerFrame.setConfiguration(data.getConfig());\n\t\t\n\t\tdebugView.setConfiguration(data.getConfig());\n\t\t\n\t\tdebugView.setConversionResults(\n\t\t\t\tdata.getConversionResults());\n\t\t\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ToggleOrthographicProjectionAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\n\nimport javax.swing.AbstractAction;\n\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.Defaults;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n\npublic class ToggleOrthographicProjectionAction extends AbstractAction {\n\n\tprivate static final long serialVersionUID = 8546764815038965935L;\n\tprivate final ViewerFrame viewerFrame;\n\tprivate final Data data;\n\tprivate final RenderOptions renderOptions;\n\t\t\n\tpublic ToggleOrthographicProjectionAction(ViewerFrame viewerFrame, Data data,\n\t\t\tRenderOptions renderOptions) {\n\t\t\n\t\tsuper(\"Orthographic projection\");\n\t\tputValue(SHORT_DESCRIPTION, \"Switches between orthographic and perspective projection\");\n\t\tputValue(MNEMONIC_KEY, KeyEvent.VK_C);\n\t\tputValue(SELECTED_KEY, renderOptions.projection.isOrthographic());\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\n\t\tif (renderOptions.projection.isOrthographic()) {\n\t\t\trenderOptions.projection = Defaults.PERSPECTIVE_PROJECTION;\n\t\t} else {\n\t\t\trenderOptions.projection = Defaults.ORTHOGRAPHIC_PROJECTION;\n\t\t}\n\t\t\n\t\tputValue(SELECTED_KEY, renderOptions.projection.isOrthographic());\n\t\t\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ToggleTerrainAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\n\nimport javax.swing.AbstractAction;\n\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n\npublic class ToggleTerrainAction extends AbstractAction {\n\n\tprivate static final long serialVersionUID = 3659085143967426625L;\n\tprivate final ViewerFrame viewerFrame;\n\tprivate final Data data;\n\tprivate final RenderOptions renderOptions;\n\t\t\n\tpublic ToggleTerrainAction(ViewerFrame viewerFrame, Data data,\n\t\t\tRenderOptions renderOptions) {\n\t\t\n\t\tsuper(\"Terrain\");\n\t\tputValue(SHORT_DESCRIPTION, \"Controls whether terrain is displayed\");\n\t\tputValue(MNEMONIC_KEY, KeyEvent.VK_N);\n\t\tputValue(SELECTED_KEY, renderOptions.isShowTerrain());\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\t\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\t\t\t\t\n\t\trenderOptions.setShowTerrain(!renderOptions.isShowTerrain());\n\t\tputValue(SELECTED_KEY, renderOptions.isShowTerrain());\n\t\t\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ToggleWireframeAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\n\nimport javax.swing.AbstractAction;\n\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n\npublic class ToggleWireframeAction extends AbstractAction {\n\n\tprivate static final long serialVersionUID = 6710342251037183143L;\n\tprivate final ViewerFrame viewerFrame;\n\tprivate final Data data;\n\tprivate final RenderOptions renderOptions;\n\t\t\n\tpublic ToggleWireframeAction(ViewerFrame viewerFrame, Data data,\n\t\t\tRenderOptions renderOptions) {\n\t\t\n\t\tsuper(\"Wireframe view\");\n\t\tputValue(SHORT_DESCRIPTION, \"Switches between wireframe and solid view\");\n\t\tputValue(MNEMONIC_KEY, KeyEvent.VK_F);\t\t\n\t\tputValue(SELECTED_KEY, renderOptions.isWireframe());\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\t\t\t\t\n\t\trenderOptions.setWireframe(!renderOptions.isWireframe());\n\t\tputValue(SELECTED_KEY, renderOptions.isWireframe());\n\t\t\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/actions/ToggleWorldObjectsAction.java",
    "content": "package org.osm2world.viewer.control.actions;\n\nimport java.awt.event.ActionEvent;\nimport java.awt.event.KeyEvent;\n\nimport javax.swing.AbstractAction;\n\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n\npublic class ToggleWorldObjectsAction extends AbstractAction {\n\n\tprivate static final long serialVersionUID = -5751259322056033212L;\n\tprivate final ViewerFrame viewerFrame;\n\tprivate final Data data;\n\tprivate final RenderOptions renderOptions;\n\t\t\n\tpublic ToggleWorldObjectsAction(ViewerFrame viewerFrame, Data data,\n\t\t\tRenderOptions renderOptions) {\n\t\t\n\t\tsuper(\"World objects\");\n\t\tputValue(SHORT_DESCRIPTION, \"Controls whether world objects are displayed\");\n\t\tputValue(MNEMONIC_KEY, KeyEvent.VK_W);\n\t\tputValue(SELECTED_KEY, renderOptions.isShowGrid());\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.data = data;\n\t\tthis.renderOptions = renderOptions;\n\t\t\n\t}\n\n\t@Override\n\tpublic void actionPerformed(ActionEvent e) {\n\t\t\t\t\n\t\trenderOptions.setShowGrid(!renderOptions.isShowGrid());\n\t\tputValue(SELECTED_KEY, renderOptions.isShowGrid());\n\t\t\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/control/navigation/DefaultNavigation.java",
    "content": "package org.osm2world.viewer.control.navigation;\n\nimport java.awt.Cursor;\nimport java.awt.Point;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.KeyEvent;\nimport java.awt.event.KeyListener;\nimport java.awt.event.MouseAdapter;\nimport java.awt.event.MouseEvent;\nimport java.awt.event.MouseWheelEvent;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport javax.swing.Timer;\nimport javax.swing.event.MouseInputListener;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.ViewerFrame;\n\n\npublic class DefaultNavigation extends MouseAdapter implements KeyListener, MouseInputListener {\n\n\tprivate final static double ANGLE_INCREMENT = Math.PI/200;\n\tprivate final static double MOVEMENT_INCREMENT = 2.0;\n\t\n\tprivate final RenderOptions renderOptions;\n\tprivate final ViewerFrame viewerFrame;\n\n\tprivate final Timer timer;\n\t\n\tpublic DefaultNavigation(ViewerFrame viewerFrame, RenderOptions renderOptions) {\n\t\t\n\t\tthis.viewerFrame = viewerFrame;\n\t\tthis.renderOptions = renderOptions;\n\t\t\n\t\ttimer = new Timer(20, KEYBOARD_TASK);\n\t}\n\t\n\tprivate boolean translationDrag = false;\n\tprivate boolean rotationDrag = false;\n\tprivate boolean movementDrag = false;\n\t\n\tprivate Point previousMousePoint;\n\n\tprivate final Set<Integer> pressedKeys = new HashSet<Integer>();\n\n\t@Override\n\tpublic void keyPressed(KeyEvent e) {\n\t\tsynchronized (pressedKeys) {\n\t\t\tpressedKeys.add(e.getKeyCode());\n\t\t\ttimer.start();\n\t\t}\n\t}\n\n\t@Override\n\tpublic void keyReleased(KeyEvent e) {\n\t\tsynchronized (pressedKeys) {\n\t\t\tpressedKeys.remove(e.getKeyCode());\n\t\t\tif (pressedKeys.isEmpty())\n\t\t\t\ttimer.stop();\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic void keyTyped(KeyEvent e) {}\n\t\n\t@Override\n\tpublic void mouseDragged(MouseEvent e) {\n\t\t\n\t\tPoint currentMousePoint = e.getPoint();\n\t\t\n\t\tfloat movementX = currentMousePoint.x - previousMousePoint.x;\n\t\tfloat movementY = currentMousePoint.y - previousMousePoint.y;\n\t\t\n\t\tCamera camera = renderOptions.camera;\n\t\t\n\t\tif (camera != null) {\n\t\n\t\t\tif (translationDrag) {\n\t\t\t\t\n\t\t\t\tcamera.moveMapForward(movementY);\n\t\t\t\tcamera.moveMapRight(movementX);\n\t\t\t\t\n\t\t\t} else if (rotationDrag) {\n\t\t\t\t\t\n\t\t\t\t/* view left/right */\n\t\t\t\tcamera.rotateY(movementX/100);\n\t\t\t\t\n\t\t\t\t/* view up/down */\n\t\t\t\tcamera.mapPitch(movementY/-100);\n\t\t\t\n\t\t\t} else if (movementDrag) {\n\t\t\t\t\n\t\t\t\t/* roll left/right */\n\t\t\t\tcamera.roll(movementX/100);\n\t\t\t\t\n\t\t\t\t/* move up/down */\n\t\t\t\tcamera.moveMapUp(movementY);\n\t\t\t}\n\t\t}\n\t\t\t\n\t\tpreviousMousePoint = currentMousePoint;\n\t\t\t\n\t}\n\t\n\t@Override\n\tpublic void mousePressed(MouseEvent e) {\n\t\tif (e.getButton() == MouseEvent.BUTTON1) {\n\t\t\tviewerFrame.setCursor(new Cursor(Cursor.MOVE_CURSOR));\n\t\t\ttranslationDrag = true;\n\t\t} else if (e.getButton() == MouseEvent.BUTTON2) {\n\t\t\tviewerFrame.setCursor(new Cursor(Cursor.MOVE_CURSOR));\n\t\t\tmovementDrag = true;\n\t\t} else {\n\t\t\tviewerFrame.setCursor(new Cursor(Cursor.MOVE_CURSOR));\n\t\t\trotationDrag = true;\n\t\t}\n\t\tpreviousMousePoint = e.getPoint();\n\t}\n\n\t@Override\n\tpublic void mouseReleased(MouseEvent e) {\n\t\tviewerFrame.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));\n\t\ttranslationDrag = false;\n\t\trotationDrag = false;\n\t\tmovementDrag = false;\n\t}\n\t\n\t@Override\n\tpublic void mouseWheelMoved(MouseWheelEvent e) {\n\n\t\tzoom(e.getWheelRotation() < 0, 1);\n\t}\n\n\tprivate void zoom(boolean zoomIn, double scale) {\n\t\tCamera c = renderOptions.camera;\n\t\t\n\t\tif (c != null) {\n\n\t\t\tVectorXYZ toLookAt = c.getLookAt().subtract(c.getPos());\n\t\t\tVectorXYZ move = toLookAt.mult(scale * (zoomIn ? 0.2f : -0.25f));\n\t\t\tVectorXYZ newPos = c.getPos().add(move);\n\n\t\t\tc.setPos(newPos);\n\t\t}\n\t}\n\t\n\tprivate final ActionListener KEYBOARD_TASK = new ActionListener() {\n\t\t\n\t\t@Override\n\t\tpublic void actionPerformed(ActionEvent e) {\n\n\t\t\tCamera c = renderOptions.camera;\n\n\t\t\tif (c != null) {\n\n\t\t\t\tsynchronized (pressedKeys) {\n\n\t\t\t\t\tfor (int key : pressedKeys) {\n\n\t\t\t\t\t\tswitch (key) {\n\t\t\t\t\t\tcase KeyEvent.VK_Q:\n\t\t\t\t\t\t\tc.roll(ANGLE_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_E:\n\t\t\t\t\t\t\tc.roll(-ANGLE_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_W:\n\t\t\t\t\t\t\tc.moveForward(MOVEMENT_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_S:\n\t\t\t\t\t\t\tc.moveForward(-MOVEMENT_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_A:\n\t\t\t\t\t\t\tc.moveRight(MOVEMENT_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_D:\n\t\t\t\t\t\t\tc.moveRight(-MOVEMENT_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_PAGE_UP:\n\t\t\t\t\t\t\tc.moveUp(MOVEMENT_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_PAGE_DOWN:\n\t\t\t\t\t\t\tc.moveUp(-MOVEMENT_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_UP:\n\t\t\t\t\t\t\tc.pitch(ANGLE_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_DOWN:\n\t\t\t\t\t\t\tc.pitch(-ANGLE_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_RIGHT:\n\t\t\t\t\t\t\tc.yaw(ANGLE_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_LEFT:\n\t\t\t\t\t\t\tc.yaw(-ANGLE_INCREMENT);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_PLUS:\n\t\t\t\t\t\tcase KeyEvent.VK_I:\n\t\t\t\t\t\t\tzoom(true, 0.5);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase KeyEvent.VK_MINUS:\n\t\t\t\t\t\tcase KeyEvent.VK_O:\n\t\t\t\t\t\t\tzoom(false, 0.5);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/model/Data.java",
    "content": "package org.osm2world.viewer.model;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Observable;\n\nimport org.apache.commons.configuration.BaseConfiguration;\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.ConversionFacade;\nimport org.osm2world.core.ConversionFacade.BoundingBoxSizeException;\nimport org.osm2world.core.ConversionFacade.ProgressListener;\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.TerrainInterpolator;\nimport org.osm2world.core.util.functions.Factory;\n\npublic class Data extends Observable {\n\t\n\tprivate Configuration config = new BaseConfiguration();\n\tprivate File osmFile = null;\n\tprivate Results conversionResults = null;\n\t\n\tpublic Configuration getConfig() {\n\t\treturn config;\n\t}\n\n\tpublic void setConfig(Configuration config) {\n\t\t\n\t\tthis.config = config;\n\t\t\n\t\tthis.setChanged();\n\t\tthis.notifyObservers();\n\t\t\n\t}\n\t\n\t/**\n\t * @param interpolatorFactory\n\t * @param enforcerFactory\n\t * \n\t */\n\tpublic void loadOSMFile(File osmFile, boolean failOnLargeBBox,\n\t\t\tFactory<? extends TerrainInterpolator> interpolatorFactory,\n\t\t\tFactory<? extends EleConstraintEnforcer> enforcerFactory,\n\t\t\tProgressListener listener)\n\t\t\t\t\tthrows IOException, BoundingBoxSizeException {\n\t\t\n\t\ttry {\n\t\t\t\n\t\t\tthis.osmFile = osmFile;\n\t\t\t\n\t\t\tConversionFacade converter = new ConversionFacade();\n\t\t\tconverter.setTerrainEleInterpolatorFactory(interpolatorFactory);\n\t\t\tconverter.setEleConstraintEnforcerFactory(enforcerFactory);\n\t\t\t\n\t\t\tconverter.addProgressListener(listener);\n\t\t\t\n\t\t\tif (failOnLargeBBox) {\n\t\t\t\tconfig.addProperty(\"maxBoundingBoxDegrees\", 1);\n\t\t\t}\n\t\t\t\n\t\t\tconversionResults = converter.createRepresentations(\n\t\t\t\t\tosmFile, null, config, null);\n\t\t\t\n\t\t} catch (IOException e) {\n\t\t\t\n\t\t\tosmFile = null;\n\t\t\tconversionResults = null;\n\t\t\t\n\t\t\tthrow e;\n\t\t\t\n\t\t} catch (BoundingBoxSizeException e) {\n\t\t\t\n\t\t\tosmFile = null;\n\t\t\tconversionResults = null;\n\t\t\t\n\t\t\tthrow e;\n\t\t\t\n\t\t} finally {\n\t\t\t\n\t\t\tconfig.clearProperty(\"maxBoundingBoxDegrees\");\n\t\t\t\n\t\t}\n\t\t\n\t\tthis.setChanged();\n\t\tthis.notifyObservers();\n\t\t\n\t}\n\t\n\tpublic File getOsmFile() {\n\t\treturn osmFile;\n\t}\n\t\n\tpublic Results getConversionResults() {\n\t\treturn conversionResults;\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/model/Defaults.java",
    "content": "package org.osm2world.viewer.model;\n\nimport org.osm2world.core.target.common.rendering.Projection;\n\npublic final class Defaults {\n\t\n\tprivate Defaults() { }\n\t\n\tpublic static final Projection PERSPECTIVE_PROJECTION\n\t\t= new Projection(false, 4/3.0, 45, 50, 1, 100000);\n\n\tpublic static final Projection ORTHOGRAPHIC_PROJECTION\n\t\t= new Projection(true, 4/3.0, 45, 50, -100000, 100000);\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/model/MessageManager.java",
    "content": "package org.osm2world.viewer.model;\n\nimport java.util.Iterator;\nimport java.util.LinkedList;\n\n/**\n * receives and stores the messages that are displayed to the user\n */\npublic class MessageManager { //TODO: isn't this a *model* class...?\n\t\n\tpublic static final long DEFAULT_MILLISECONDS_TO_LIVE = 3000;\n\t\n\tpublic static class Message {\n\t\tpublic final String messageString;\n\t\tpublic final long expiration;\n\t\tpublic Message(String messageString, long expiration) {\n\t\t\tthis.messageString = messageString;\n\t\t\tthis.expiration = expiration;\n\t\t}\n\t}\n\t\n\tprivate LinkedList<Message> messages = new LinkedList<Message>();\n\t\n\tprivate void addMessage(String message, long millisecondsToLive) {\n\t\tlong expiration = System.currentTimeMillis() + millisecondsToLive;\n\t\tmessages.add(new Message(message, expiration ));\n\t}\n\t\n\tpublic void addMessage(String message) {\n\t\taddMessage(message, DEFAULT_MILLISECONDS_TO_LIVE);\n\t}\n\t\n\tpublic LinkedList<Message> getLiveMessages() {\n\t\tremoveExpiredMessages();\n\t\treturn messages;\n\t}\n\n\tprivate void removeExpiredMessages() {\n\t\tlong now = System.currentTimeMillis();\n\t\tfor (Iterator<Message> messageIterator = messages.iterator(); messageIterator.hasNext();) {\n\t\t\tMessage message = messageIterator.next();\n\t\t\tif (now > message.expiration) { \n\t\t\t\tmessageIterator.remove();\n\t\t\t}\n\t\t}\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/model/RenderOptions.java",
    "content": "package org.osm2world.viewer.model;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.LeastSquaresInterpolator;\nimport org.osm2world.core.map_elevation.creation.NoneEleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.TerrainInterpolator;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\nimport org.osm2world.viewer.view.debug.DebugView;\n\npublic class RenderOptions {\n\n\tpublic Camera camera = null;\n\tpublic Projection projection = Defaults.PERSPECTIVE_PROJECTION;\n\t\n\tpublic Set<DebugView> activeDebugViews = new HashSet<DebugView>();\n\t\n\tprivate boolean showGrid = true;\n\tprivate boolean showTerrain = true;\n\tprivate boolean wireframe = false;\n\tprivate boolean backfaceCulling = true;\n\t\n\tClass<? extends TerrainInterpolator> interpolatorClass =\n\t\t\tLeastSquaresInterpolator.class;\n\tClass<? extends EleConstraintEnforcer> enforcerClass =\n\t\t\tNoneEleConstraintEnforcer.class;\n\t\n\tpublic boolean isShowGrid() {\n\t\treturn showGrid;\n\t}\n\tpublic void setShowGrid(boolean showGrid) {\n\t\tthis.showGrid = showGrid;\n\t}\n\tpublic boolean isShowTerrain() {\n\t\treturn showTerrain;\n\t}\n\tpublic void setShowTerrain(boolean showTerrain) {\n\t\tthis.showTerrain = showTerrain;\n\t}\n\tpublic boolean isWireframe() {\n\t\treturn wireframe;\n\t}\n\tpublic void setWireframe(boolean wireframe) {\n\t\tthis.wireframe = wireframe;\n\t}\n\tpublic boolean isBackfaceCulling() {\n\t\treturn backfaceCulling;\n\t}\n\tpublic void setBackfaceCulling(boolean backfaceCulling) {\n\t\tthis.backfaceCulling = backfaceCulling;\n\t}\n\t\n\tpublic Class<? extends TerrainInterpolator> getInterpolatorClass() {\n\t\treturn interpolatorClass;\n\t}\n\t\n\tpublic void setInterpolatorClass(Class<? extends TerrainInterpolator> interpolatorClass) {\n\t\tthis.interpolatorClass = interpolatorClass;\n\t}\n\t\n\tpublic Class<? extends EleConstraintEnforcer> getEnforcerClass() {\n\t\treturn enforcerClass;\n\t}\n\t\n\tpublic void setEnforcerClass(Class<? extends EleConstraintEnforcer> enforcerClass) {\n\t\tthis.enforcerClass = enforcerClass;\n\t}\n\t\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/package.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n<!--\n\n    Copyright (C) 2010–2011  OSM2World contributors\n  \n    This file is part of OSM2World.\n\n    OSM2World is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Lesser General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    OSM2World is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public License\n    along with OSM2World.  If not, see <http://www.gnu.org/licenses/>.\n\n-->\n</head><body>\n\nGUI (based on Swing and JOGL) for testing OSM2World.\n\n</body></html>"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/FileDrop.java",
    "content": "package org.osm2world.viewer.view;\r\n\r\nimport java.awt.datatransfer.DataFlavor;\r\nimport java.io.BufferedReader;\r\nimport java.io.File;\r\nimport java.io.IOException;\r\nimport java.io.PrintStream;\r\nimport java.io.Reader;\r\n\r\nimport org.osm2world.viewer.view.FileDrop.TransferableObject;\r\n\r\n/*\r\n * this class has been copied from the Public Domain resource\r\n * http://www.iharder.net/current/java/filedrop/\r\n */\r\n\r\n/**\r\n * This class makes it easy to drag and drop files from the operating\r\n * system to a Java program. Any <tt>java.awt.Component</tt> can be\r\n * dropped onto, but only <tt>javax.swing.JComponent</tt>s will indicate\r\n * the drop event with a changed border.\r\n * <p/>\r\n * To use this class, construct a new <tt>FileDrop</tt> by passing\r\n * it the target component and a <tt>Listener</tt> to receive notification\r\n * when file(s) have been dropped. Here is an example:\r\n * <p/>\r\n * <code><pre>\r\n *      JPanel myPanel = new JPanel();\r\n *      new FileDrop( myPanel, new FileDrop.Listener()\r\n *      {   public void filesDropped( java.io.File[] files )\r\n *          {\r\n *              // handle file drop\r\n *              ...\r\n *          }   // end filesDropped\r\n *      }); // end FileDrop.Listener\r\n * </pre></code>\r\n * <p/>\r\n * You can specify the border that will appear when files are being dragged by\r\n * calling the constructor with a <tt>javax.swing.border.Border</tt>. Only\r\n * <tt>JComponent</tt>s will show any indication with a border.\r\n * <p/>\r\n * You can turn on some debugging features by passing a <tt>PrintStream</tt>\r\n * object (such as <tt>System.out</tt>) into the full constructor. A <tt>null</tt>\r\n * value will result in no extra debugging information being output.\r\n * <p/>\r\n *\r\n * <p>I'm releasing this code into the Public Domain. Enjoy.\r\n * </p>\r\n * <p><em>Original author: Robert Harder, rharder@usa.net</em></p>\r\n * <p>2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.</p>\r\n *\r\n * @author  Robert Harder\r\n * @author  rharder@users.sf.net\r\n * @version 1.0.1\r\n */\r\nclass FileDrop\r\n{\r\n    private transient javax.swing.border.Border normalBorder;\r\n    private transient java.awt.dnd.DropTargetListener dropListener;\r\n    \r\n    \r\n    /** Discover if the running JVM is modern enough to have drag and drop. */\r\n    private static Boolean supportsDnD;\r\n    \r\n    // Default border color\r\n    private static java.awt.Color defaultBorderColor = new java.awt.Color( 0f, 0f, 1f, 0.25f );\r\n    \r\n    /**\r\n     * Constructs a {@link FileDrop} with a default light-blue border\r\n     * and, if <var>c</var> is a {@link java.awt.Container}, recursively\r\n     * sets all elements contained within as drop targets, though only\r\n     * the top level container will change borders.\r\n     *\r\n     * @param c Component on which files will be dropped.\r\n     * @param listener Listens for <tt>filesDropped</tt>.\r\n     * @since 1.0\r\n     */\r\n    public FileDrop(\r\n    final java.awt.Component c,\r\n    final Listener listener )\r\n    {   this( null,  // Logging stream\r\n              c,     // Drop target\r\n              javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), // Drag border\r\n              true, // Recursive\r\n              listener );\r\n    }   // end constructor\r\n    \r\n    \r\n    \r\n    \r\n    /**\r\n     * Constructor with a default border and the option to recursively set drop targets.\r\n     * If your component is a <tt>java.awt.Container</tt>, then each of its children\r\n     * components will also listen for drops, though only the parent will change borders.\r\n     *\r\n     * @param c Component on which files will be dropped.\r\n     * @param recursive Recursively set children as drop targets.\r\n     * @param listener Listens for <tt>filesDropped</tt>.\r\n     * @since 1.0\r\n     */\r\n    public FileDrop(\r\n    final java.awt.Component c,\r\n    final boolean recursive,\r\n    final Listener listener )\r\n    {   this( null,  // Logging stream\r\n              c,     // Drop target\r\n              javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), // Drag border\r\n              recursive, // Recursive\r\n              listener );\r\n    }   // end constructor\r\n    \r\n    \r\n    /**\r\n     * Constructor with a default border and debugging optionally turned on.\r\n     * With Debugging turned on, more status messages will be displayed to\r\n     * <tt>out</tt>. A common way to use this constructor is with\r\n     * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for\r\n     * the parameter <tt>out</tt> will result in no debugging output.\r\n     *\r\n     * @param out PrintStream to record debugging info or null for no debugging.\r\n     * @param c Component on which files will be dropped.\r\n     * @param listener Listens for <tt>filesDropped</tt>.\r\n     * @since 1.0\r\n     */\r\n    public FileDrop(\r\n    final java.io.PrintStream out,\r\n    final java.awt.Component c,\r\n    final Listener listener )\r\n    {   this( out,  // Logging stream\r\n              c,    // Drop target\r\n              javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ),\r\n              false, // Recursive\r\n              listener );\r\n    }   // end constructor\r\n    \r\n        \r\n    \r\n    /**\r\n     * Constructor with a default border, debugging optionally turned on\r\n     * and the option to recursively set drop targets.\r\n     * If your component is a <tt>java.awt.Container</tt>, then each of its children\r\n     * components will also listen for drops, though only the parent will change borders.\r\n     * With Debugging turned on, more status messages will be displayed to\r\n     * <tt>out</tt>. A common way to use this constructor is with\r\n     * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for\r\n     * the parameter <tt>out</tt> will result in no debugging output.\r\n     *\r\n     * @param out PrintStream to record debugging info or null for no debugging.\r\n     * @param c Component on which files will be dropped.\r\n     * @param recursive Recursively set children as drop targets.\r\n     * @param listener Listens for <tt>filesDropped</tt>.\r\n     * @since 1.0\r\n     */\r\n    public FileDrop(\r\n    final java.io.PrintStream out,\r\n    final java.awt.Component c,\r\n    final boolean recursive,\r\n    final Listener listener)\r\n    {   this( out,  // Logging stream\r\n              c,    // Drop target\r\n              javax.swing.BorderFactory.createMatteBorder( 2, 2, 2, 2, defaultBorderColor ), // Drag border\r\n              recursive, // Recursive\r\n              listener );\r\n    }   // end constructor\r\n    \r\n    \r\n    \r\n    \r\n    /**\r\n     * Constructor with a specified border\r\n     *\r\n     * @param c Component on which files will be dropped.\r\n     * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs.\r\n     * @param listener Listens for <tt>filesDropped</tt>.\r\n     * @since 1.0\r\n     */\r\n    public FileDrop(\r\n    final java.awt.Component c,\r\n    final javax.swing.border.Border dragBorder,\r\n    final Listener listener)\r\n    {   this(\r\n            null,   // Logging stream\r\n            c,      // Drop target\r\n            dragBorder, // Drag border\r\n            false,  // Recursive\r\n            listener );\r\n    }   // end constructor\r\n    \r\n    \r\n        \r\n    \r\n    /**\r\n     * Constructor with a specified border and the option to recursively set drop targets.\r\n     * If your component is a <tt>java.awt.Container</tt>, then each of its children\r\n     * components will also listen for drops, though only the parent will change borders.\r\n     *\r\n     * @param c Component on which files will be dropped.\r\n     * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs.\r\n     * @param recursive Recursively set children as drop targets.\r\n     * @param listener Listens for <tt>filesDropped</tt>.\r\n     * @since 1.0\r\n     */\r\n    public FileDrop(\r\n    final java.awt.Component c,\r\n    final javax.swing.border.Border dragBorder,\r\n    final boolean recursive,\r\n    final Listener listener)\r\n    {   this(\r\n            null,\r\n            c,\r\n            dragBorder,\r\n            recursive,\r\n            listener );\r\n    }   // end constructor\r\n    \r\n            \r\n    \r\n    /**\r\n     * Constructor with a specified border and debugging optionally turned on.\r\n     * With Debugging turned on, more status messages will be displayed to\r\n     * <tt>out</tt>. A common way to use this constructor is with\r\n     * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for\r\n     * the parameter <tt>out</tt> will result in no debugging output.\r\n     *\r\n     * @param out PrintStream to record debugging info or null for no debugging.\r\n     * @param c Component on which files will be dropped.\r\n     * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs.\r\n     * @param listener Listens for <tt>filesDropped</tt>.\r\n     * @since 1.0\r\n     */\r\n    public FileDrop(\r\n    final java.io.PrintStream out,\r\n    final java.awt.Component c,\r\n    final javax.swing.border.Border dragBorder,\r\n    final Listener listener)\r\n    {   this(\r\n            out,    // Logging stream\r\n            c,      // Drop target\r\n            dragBorder, // Drag border\r\n            false,  // Recursive\r\n            listener );\r\n    }   // end constructor\r\n    \r\n    \r\n    \r\n    \r\n    \r\n    /**\r\n     * Full constructor with a specified border and debugging optionally turned on.\r\n     * With Debugging turned on, more status messages will be displayed to\r\n     * <tt>out</tt>. A common way to use this constructor is with\r\n     * <tt>System.out</tt> or <tt>System.err</tt>. A <tt>null</tt> value for\r\n     * the parameter <tt>out</tt> will result in no debugging output.\r\n     *\r\n     * @param out PrintStream to record debugging info or null for no debugging.\r\n     * @param c Component on which files will be dropped.\r\n     * @param dragBorder Border to use on <tt>JComponent</tt> when dragging occurs.\r\n     * @param recursive Recursively set children as drop targets.\r\n     * @param listener Listens for <tt>filesDropped</tt>.\r\n     * @since 1.0\r\n     */\r\n    public FileDrop(\r\n    final java.io.PrintStream out,\r\n    final java.awt.Component c,\r\n    final javax.swing.border.Border dragBorder,\r\n    final boolean recursive,\r\n    final Listener listener)\r\n    {\r\n        \r\n        if( supportsDnD() )\r\n        {   // Make a drop listener\r\n            dropListener = new java.awt.dnd.DropTargetListener()\r\n            {   public void dragEnter( java.awt.dnd.DropTargetDragEvent evt )\r\n                {       log( out, \"FileDrop: dragEnter event.\" );\r\n\r\n                    // Is this an acceptable drag event?\r\n                    if( isDragOk( out, evt ) )\r\n                    {\r\n                        // If it's a Swing component, set its border\r\n                        if( c instanceof javax.swing.JComponent )\r\n                        {   javax.swing.JComponent jc = (javax.swing.JComponent) c;\r\n                            normalBorder = jc.getBorder();\r\n                            log( out, \"FileDrop: normal border saved.\" );\r\n                            jc.setBorder( dragBorder );\r\n                            log( out, \"FileDrop: drag border set.\" );\r\n                        }   // end if: JComponent\r\n\r\n                        // Acknowledge that it's okay to enter\r\n                        //evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );\r\n                        evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY );\r\n                        log( out, \"FileDrop: event accepted.\" );\r\n                    }   // end if: drag ok\r\n                    else\r\n                    {   // Reject the drag event\r\n                        evt.rejectDrag();\r\n                        log( out, \"FileDrop: event rejected.\" );\r\n                    }   // end else: drag not ok\r\n                }   // end dragEnter\r\n\r\n                public void dragOver( java.awt.dnd.DropTargetDragEvent evt )\r\n                {   // This is called continually as long as the mouse is\r\n                    // over the drag target.\r\n                }   // end dragOver\r\n\r\n                public void drop( java.awt.dnd.DropTargetDropEvent evt )\r\n                {   log( out, \"FileDrop: drop event.\" );\r\n                    try\r\n                    {   // Get whatever was dropped\r\n                        java.awt.datatransfer.Transferable tr = evt.getTransferable();\r\n\r\n                        // Is it a file list?\r\n                        if (tr.isDataFlavorSupported (java.awt.datatransfer.DataFlavor.javaFileListFlavor))\r\n                        {\r\n                            // Say we'll take it.\r\n                            //evt.acceptDrop ( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );\r\n                            evt.acceptDrop ( java.awt.dnd.DnDConstants.ACTION_COPY );\r\n                            log( out, \"FileDrop: file list accepted.\" );\r\n\r\n                            // Get a useful list\r\n                            java.util.List fileList = (java.util.List)\r\n                                tr.getTransferData(java.awt.datatransfer.DataFlavor.javaFileListFlavor);\r\n                            java.util.Iterator iterator = fileList.iterator();\r\n\r\n                            // Convert list to array\r\n                            java.io.File[] filesTemp = new java.io.File[ fileList.size() ];\r\n                            fileList.toArray( filesTemp );\r\n                            final java.io.File[] files = filesTemp;\r\n\r\n                            // Alert listener to drop.\r\n                            if( listener != null )\r\n                                listener.filesDropped( files );\r\n\r\n                            // Mark that drop is completed.\r\n                            evt.getDropTargetContext().dropComplete(true);\r\n                            log( out, \"FileDrop: drop complete.\" );\r\n                        }   // end if: file list\r\n                        else // this section will check for a reader flavor.\r\n                        {\r\n                            // Thanks, Nathan!\r\n                            // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\r\n                            DataFlavor[] flavors = tr.getTransferDataFlavors();\r\n                            boolean handled = false;\r\n                            for (int zz = 0; zz < flavors.length; zz++) {\r\n                                if (flavors[zz].isRepresentationClassReader()) {\r\n                                    // Say we'll take it.\r\n                                    //evt.acceptDrop ( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );\r\n                                    evt.acceptDrop(java.awt.dnd.DnDConstants.ACTION_COPY);\r\n                                    log(out, \"FileDrop: reader accepted.\");\r\n\r\n                                    Reader reader = flavors[zz].getReaderForText(tr);\r\n\r\n                                    BufferedReader br = new BufferedReader(reader);\r\n                                    \r\n                                    if(listener != null)\r\n                                        listener.filesDropped(createFileArray(br, out));\r\n                                    \r\n                                    // Mark that drop is completed.\r\n                                    evt.getDropTargetContext().dropComplete(true);\r\n                                    log(out, \"FileDrop: drop complete.\");\r\n                                    handled = true;\r\n                                    break;\r\n                                }\r\n                            }\r\n                            if(!handled){\r\n                                log( out, \"FileDrop: not a file list or reader - abort.\" );\r\n                                evt.rejectDrop();\r\n                            }\r\n                            // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\r\n                        }   // end else: not a file list\r\n                    }   // end try\r\n                    catch ( java.io.IOException io)\r\n                    {   log( out, \"FileDrop: IOException - abort:\" );\r\n                        io.printStackTrace( out );\r\n                        evt.rejectDrop();\r\n                    }   // end catch IOException\r\n                    catch (java.awt.datatransfer.UnsupportedFlavorException ufe)\r\n                    {   log( out, \"FileDrop: UnsupportedFlavorException - abort:\" );\r\n                        ufe.printStackTrace( out );\r\n                        evt.rejectDrop();\r\n                    }   // end catch: UnsupportedFlavorException\r\n                    finally\r\n                    {\r\n                        // If it's a Swing component, reset its border\r\n                        if( c instanceof javax.swing.JComponent )\r\n                        {   javax.swing.JComponent jc = (javax.swing.JComponent) c;\r\n                            jc.setBorder( normalBorder );\r\n                            log( out, \"FileDrop: normal border restored.\" );\r\n                        }   // end if: JComponent\r\n                    }   // end finally\r\n                }   // end drop\r\n\r\n                public void dragExit( java.awt.dnd.DropTargetEvent evt )\r\n                {   log( out, \"FileDrop: dragExit event.\" );\r\n                    // If it's a Swing component, reset its border\r\n                    if( c instanceof javax.swing.JComponent )\r\n                    {   javax.swing.JComponent jc = (javax.swing.JComponent) c;\r\n                        jc.setBorder( normalBorder );\r\n                        log( out, \"FileDrop: normal border restored.\" );\r\n                    }   // end if: JComponent\r\n                }   // end dragExit\r\n\r\n                public void dropActionChanged( java.awt.dnd.DropTargetDragEvent evt )\r\n                {   log( out, \"FileDrop: dropActionChanged event.\" );\r\n                    // Is this an acceptable drag event?\r\n                    if( isDragOk( out, evt ) )\r\n                    {   //evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );\r\n                        evt.acceptDrag( java.awt.dnd.DnDConstants.ACTION_COPY );\r\n                        log( out, \"FileDrop: event accepted.\" );\r\n                    }   // end if: drag ok\r\n                    else\r\n                    {   evt.rejectDrag();\r\n                        log( out, \"FileDrop: event rejected.\" );\r\n                    }   // end else: drag not ok\r\n                }   // end dropActionChanged\r\n            }; // end DropTargetListener\r\n\r\n            // Make the component (and possibly children) drop targets\r\n            makeDropTarget( out, c, recursive );\r\n        }   // end if: supports dnd\r\n        else\r\n        {   log( out, \"FileDrop: Drag and drop is not supported with this JVM\" );\r\n        }   // end else: does not support DnD\r\n    }   // end constructor\r\n\r\n    \r\n    private static boolean supportsDnD()\r\n    {   // Static Boolean\r\n        if( supportsDnD == null )\r\n        {\r\n            boolean support = false;\r\n            try\r\n            {   Class arbitraryDndClass = Class.forName( \"java.awt.dnd.DnDConstants\" );\r\n                support = true;\r\n            }   // end try\r\n            catch( Exception e )\r\n            {   support = false;\r\n            }   // end catch\r\n            supportsDnD = new Boolean( support );\r\n        }   // end if: first time through\r\n        return supportsDnD.booleanValue();\r\n    }   // end supportsDnD\r\n    \r\n    \r\n     // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\r\n     private static String ZERO_CHAR_STRING = \"\" + (char)0;\r\n     private static File[] createFileArray(BufferedReader bReader, PrintStream out)\r\n     {\r\n        try {\r\n            java.util.List list = new java.util.ArrayList();\r\n            java.lang.String line = null;\r\n            while ((line = bReader.readLine()) != null) {\r\n                try {\r\n                    // kde seems to append a 0 char to the end of the reader\r\n                    if(ZERO_CHAR_STRING.equals(line)) continue;\r\n                    \r\n                    java.io.File file = new java.io.File(new java.net.URI(line));\r\n                    list.add(file);\r\n                } catch (Exception ex) {\r\n                    log(out, \"Error with \" + line + \": \" + ex.getMessage());\r\n                }\r\n            }\r\n\r\n            return (java.io.File[]) list.toArray(new File[list.size()]);\r\n        } catch (IOException ex) {\r\n            log(out, \"FileDrop: IOException\");\r\n        }\r\n        return new File[0];\r\n     }\r\n     // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\r\n     \r\n    \r\n    private void makeDropTarget( final java.io.PrintStream out, final java.awt.Component c, boolean recursive )\r\n    {\r\n        // Make drop target\r\n        final java.awt.dnd.DropTarget dt = new java.awt.dnd.DropTarget();\r\n        try\r\n        {   dt.addDropTargetListener( dropListener );\r\n        }   // end try\r\n        catch( java.util.TooManyListenersException e )\r\n        {   e.printStackTrace();\r\n            log(out, \"FileDrop: Drop will not work due to previous error. Do you have another listener attached?\" );\r\n        }   // end catch\r\n        \r\n        // Listen for hierarchy changes and remove the drop target when the parent gets cleared out.\r\n        c.addHierarchyListener( new java.awt.event.HierarchyListener()\r\n        {   public void hierarchyChanged( java.awt.event.HierarchyEvent evt )\r\n            {   log( out, \"FileDrop: Hierarchy changed.\" );\r\n                java.awt.Component parent = c.getParent();\r\n                if( parent == null )\r\n                {   c.setDropTarget( null );\r\n                    log( out, \"FileDrop: Drop target cleared from component.\" );\r\n                }   // end if: null parent\r\n                else\r\n                {   new java.awt.dnd.DropTarget(c, dropListener);\r\n                    log( out, \"FileDrop: Drop target added to component.\" );\r\n                }   // end else: parent not null\r\n            }   // end hierarchyChanged\r\n        }); // end hierarchy listener\r\n        if( c.getParent() != null )\r\n            new java.awt.dnd.DropTarget(c, dropListener);\r\n        \r\n        if( recursive && (c instanceof java.awt.Container ) )\r\n        {\r\n            // Get the container\r\n            java.awt.Container cont = (java.awt.Container) c;\r\n            \r\n            // Get it's components\r\n            java.awt.Component[] comps = cont.getComponents();\r\n            \r\n            // Set it's components as listeners also\r\n            for( int i = 0; i < comps.length; i++ )\r\n                makeDropTarget( out, comps[i], recursive );\r\n        }   // end if: recursively set components as listener\r\n    }   // end dropListener\r\n    \r\n    \r\n    \r\n    /** Determine if the dragged data is a file list. */\r\n    private boolean isDragOk( final java.io.PrintStream out, final java.awt.dnd.DropTargetDragEvent evt )\r\n    {   boolean ok = false;\r\n        \r\n        // Get data flavors being dragged\r\n        java.awt.datatransfer.DataFlavor[] flavors = evt.getCurrentDataFlavors();\r\n        \r\n        // See if any of the flavors are a file list\r\n        int i = 0;\r\n        while( !ok && i < flavors.length )\r\n        {\r\n            // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\r\n            // Is the flavor a file list?\r\n            final DataFlavor curFlavor = flavors[i];\r\n            if( curFlavor.equals( java.awt.datatransfer.DataFlavor.javaFileListFlavor ) ||\r\n                curFlavor.isRepresentationClassReader()){\r\n                ok = true;\r\n            }\r\n            // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.\r\n            i++;\r\n        }   // end while: through flavors\r\n        \r\n        // If logging is enabled, show data flavors\r\n        if( out != null )\r\n        {   if( flavors.length == 0 )\r\n                log( out, \"FileDrop: no data flavors.\" );\r\n            for( i = 0; i < flavors.length; i++ )\r\n                log( out, flavors[i].toString() );\r\n        }   // end if: logging enabled\r\n        \r\n        return ok;\r\n    }   // end isDragOk\r\n    \r\n    \r\n    /** Outputs <tt>message</tt> to <tt>out</tt> if it's not null. */\r\n    private static void log( java.io.PrintStream out, String message )\r\n    {   // Log message if requested\r\n        if( out != null )\r\n            out.println( message );\r\n    }   // end log\r\n\r\n    \r\n    \r\n    \r\n    /**\r\n     * Removes the drag-and-drop hooks from the component and optionally\r\n     * from the all children. You should call this if you add and remove\r\n     * components after you've set up the drag-and-drop.\r\n     * This will recursively unregister all components contained within\r\n     * <var>c</var> if <var>c</var> is a {@link java.awt.Container}.\r\n     *\r\n     * @param c The component to unregister as a drop target\r\n     * @since 1.0\r\n     */\r\n    public static boolean remove( java.awt.Component c)\r\n    {   return remove( null, c, true );\r\n    }   // end remove\r\n    \r\n    \r\n    \r\n    /**\r\n     * Removes the drag-and-drop hooks from the component and optionally\r\n     * from the all children. You should call this if you add and remove\r\n     * components after you've set up the drag-and-drop.\r\n     *\r\n     * @param out Optional {@link java.io.PrintStream} for logging drag and drop messages\r\n     * @param c The component to unregister\r\n     * @param recursive Recursively unregister components within a container\r\n     * @since 1.0\r\n     */\r\n    public static boolean remove( java.io.PrintStream out, java.awt.Component c, boolean recursive )\r\n    {   // Make sure we support dnd.\r\n        if( supportsDnD() )\r\n        {   log( out, \"FileDrop: Removing drag-and-drop hooks.\" );\r\n            c.setDropTarget( null );\r\n            if( recursive && ( c instanceof java.awt.Container ) )\r\n            {   java.awt.Component[] comps = ((java.awt.Container)c).getComponents();\r\n                for( int i = 0; i < comps.length; i++ )\r\n                    remove( out, comps[i], recursive );\r\n                return true;\r\n            }   // end if: recursive\r\n            else return false;\r\n        }   // end if: supports DnD\r\n        else return false;\r\n    }   // end remove\r\n    \r\n    \r\n\r\n    \r\n/* ********  I N N E R   I N T E R F A C E   L I S T E N E R  ******** */\r\n    \r\n    \r\n    /**\r\n     * Implement this inner interface to listen for when files are dropped. For example\r\n     * your class declaration may begin like this:\r\n     * <code><pre>\r\n     *      public class MyClass implements FileDrop.Listener\r\n     *      ...\r\n     *      public void filesDropped( java.io.File[] files )\r\n     *      {\r\n     *          ...\r\n     *      }   // end filesDropped\r\n     *      ...\r\n     * </pre></code>\r\n     *\r\n     * @since 1.1\r\n     */\r\n    public static interface Listener {\r\n       \r\n        /**\r\n         * This method is called when files have been successfully dropped.\r\n         *\r\n         * @param files An array of <tt>File</tt>s that were dropped.\r\n         * @since 1.0\r\n         */\r\n        public abstract void filesDropped( java.io.File[] files );\r\n        \r\n        \r\n    }   // end inner-interface Listener\r\n    \r\n    \r\n/* ********  I N N E R   C L A S S  ******** */\r\n    \r\n    \r\n    /**\r\n     * This is the event that is passed to the\r\n     * {@link Listener#filesDropped filesDropped(...)} method in\r\n     * your {@link Listener} when files are dropped onto\r\n     * a registered drop target.\r\n     *\r\n     * <p>I'm releasing this code into the Public Domain. Enjoy.</p>\r\n     * \r\n     * @author  Robert Harder\r\n     * @author  rob@iharder.net\r\n     * @version 1.2\r\n     */\r\n    public static class Event extends java.util.EventObject {\r\n\n\t\tprivate static final long serialVersionUID = -4180860113403394986L; //generated serialVersionUID\r\n\t\tprivate java.io.File[] files;\r\n\r\n        /**\r\n         * Constructs an {@link Event} with the array\r\n         * of files that were dropped and the\r\n         * {@link FileDrop} that initiated the event.\r\n         *\r\n         * @param files The array of files that were dropped\r\n         * @source The event source\r\n         * @since 1.1\r\n         */\r\n        public Event( java.io.File[] files, Object source ) {\r\n            super( source );\r\n            this.files = files;\r\n        }   // end constructor\r\n\r\n        /**\r\n         * Returns an array of files that were dropped on a\r\n         * registered drop target.\r\n         *\r\n         * @return array of files that were dropped\r\n         * @since 1.1\r\n         */\r\n        public java.io.File[] getFiles() {\r\n            return files;\r\n        }   // end getFiles\r\n    \r\n    }   // end inner class Event\r\n    \r\n    \r\n    \r\n/* ********  I N N E R   C L A S S  ******** */\r\n    \r\n\r\n    /**\r\n     * At last an easy way to encapsulate your custom objects for dragging and dropping\r\n     * in your Java programs!\r\n     * When you need to create a {@link java.awt.datatransfer.Transferable} object,\r\n     * use this class to wrap your object.\r\n     * For example:\r\n     * <pre><code>\r\n     *      ...\r\n     *      MyCoolClass myObj = new MyCoolClass();\r\n     *      Transferable xfer = new TransferableObject( myObj );\r\n     *      ...\r\n     * </code></pre>\r\n     * Or if you need to know when the data was actually dropped, like when you're\r\n     * moving data out of a list, say, you can use the {@link TransferableObject.Fetcher}\r\n     * inner class to return your object Just in Time.\r\n     * For example:\r\n     * <pre><code>\r\n     *      ...\r\n     *      final MyCoolClass myObj = new MyCoolClass();\r\n     *\r\n     *      TransferableObject.Fetcher fetcher = new TransferableObject.Fetcher()\r\n     *      {   public Object getObject(){ return myObj; }\r\n     *      }; // end fetcher\r\n     *\r\n     *      Transferable xfer = new TransferableObject( fetcher );\r\n     *      ...\r\n     * </code></pre>\r\n     *\r\n     * The {@link java.awt.datatransfer.DataFlavor} associated with\r\n     * {@link TransferableObject} has the representation class\r\n     * <tt>net.iharder.dnd.TransferableObject.class</tt> and MIME type\r\n     * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\r\n     * This data flavor is accessible via the static\r\n     * {@link #DATA_FLAVOR} property.\r\n     *\r\n     *\r\n     * <p>I'm releasing this code into the Public Domain. Enjoy.</p>\r\n     * \r\n     * @author  Robert Harder\r\n     * @author  rob@iharder.net\r\n     * @version 1.2\r\n     */\r\n    public static class TransferableObject implements java.awt.datatransfer.Transferable\r\n    {\r\n        /**\r\n         * The MIME type for {@link #DATA_FLAVOR} is\r\n         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\r\n         *\r\n         * @since 1.1\r\n         */\r\n        public final static String MIME_TYPE = \"application/x-net.iharder.dnd.TransferableObject\";\r\n\r\n\r\n        /**\r\n         * The default {@link java.awt.datatransfer.DataFlavor} for\r\n         * {@link TransferableObject} has the representation class\r\n         * <tt>net.iharder.dnd.TransferableObject.class</tt>\r\n         * and the MIME type\r\n         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\r\n         *\r\n         * @since 1.1\r\n         */\r\n        public final static java.awt.datatransfer.DataFlavor DATA_FLAVOR =\r\n            new java.awt.datatransfer.DataFlavor( FileDrop.TransferableObject.class, MIME_TYPE );\r\n\r\n\r\n        private Fetcher fetcher;\r\n        private Object data;\r\n\r\n        private java.awt.datatransfer.DataFlavor customFlavor;\r\n\r\n\r\n\r\n        /**\r\n         * Creates a new {@link TransferableObject} that wraps <var>data</var>.\r\n         * Along with the {@link #DATA_FLAVOR} associated with this class,\r\n         * this creates a custom data flavor with a representation class\r\n         * determined from <code>data.getClass()</code> and the MIME type\r\n         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\r\n         *\r\n         * @param data The data to transfer\r\n         * @since 1.1\r\n         */\r\n        public TransferableObject( Object data )\r\n        {   this.data = data;\r\n            this.customFlavor = new java.awt.datatransfer.DataFlavor( data.getClass(), MIME_TYPE );\r\n        }   // end constructor\r\n\r\n\r\n\r\n        /**\r\n         * Creates a new {@link TransferableObject} that will return the\r\n         * object that is returned by <var>fetcher</var>.\r\n         * No custom data flavor is set other than the default\r\n         * {@link #DATA_FLAVOR}.\r\n         *\r\n         * @see Fetcher\r\n         * @param fetcher The {@link Fetcher} that will return the data object\r\n         * @since 1.1\r\n         */\r\n        public TransferableObject( Fetcher fetcher )\r\n        {   this.fetcher = fetcher;\r\n        }   // end constructor\r\n\r\n\r\n\r\n        /**\r\n         * Creates a new {@link TransferableObject} that will return the\r\n         * object that is returned by <var>fetcher</var>.\r\n         * Along with the {@link #DATA_FLAVOR} associated with this class,\r\n         * this creates a custom data flavor with a representation class <var>dataClass</var>\r\n         * and the MIME type\r\n         * <tt>application/x-net.iharder.dnd.TransferableObject</tt>.\r\n         *\r\n         * @see Fetcher\r\n         * @param dataClass The {@link java.lang.Class} to use in the custom data flavor\r\n         * @param fetcher The {@link Fetcher} that will return the data object\r\n         * @since 1.1\r\n         */\r\n        public TransferableObject( Class dataClass, Fetcher fetcher )\r\n        {   this.fetcher = fetcher;\r\n            this.customFlavor = new java.awt.datatransfer.DataFlavor( dataClass, MIME_TYPE );\r\n        }   // end constructor\r\n\r\n        /**\r\n         * Returns the custom {@link java.awt.datatransfer.DataFlavor} associated\r\n         * with the encapsulated object or <tt>null</tt> if the {@link Fetcher}\r\n         * constructor was used without passing a {@link java.lang.Class}.\r\n         *\r\n         * @return The custom data flavor for the encapsulated object\r\n         * @since 1.1\r\n         */\r\n        public java.awt.datatransfer.DataFlavor getCustomDataFlavor()\r\n        {   return customFlavor;\r\n        }   // end getCustomDataFlavor\r\n\r\n\r\n    /* ********  T R A N S F E R A B L E   M E T H O D S  ******** */\r\n\r\n\r\n        /**\r\n         * Returns a two- or three-element array containing first\r\n         * the custom data flavor, if one was created in the constructors,\r\n         * second the default {@link #DATA_FLAVOR} associated with\r\n         * {@link TransferableObject}, and third the\r\n         * {@code java.awt.datatransfer.DataFlavor.stringFlavor}.\r\n         *\r\n         * @return An array of supported data flavors\r\n         * @since 1.1\r\n         */\r\n        public java.awt.datatransfer.DataFlavor[] getTransferDataFlavors()\r\n        {\r\n            if( customFlavor != null )\r\n                return new java.awt.datatransfer.DataFlavor[]\r\n                {   customFlavor,\r\n                    DATA_FLAVOR,\r\n                    java.awt.datatransfer.DataFlavor.stringFlavor\r\n                };  // end flavors array\r\n            else\r\n                return new java.awt.datatransfer.DataFlavor[]\r\n                {   DATA_FLAVOR,\r\n                    java.awt.datatransfer.DataFlavor.stringFlavor\r\n                };  // end flavors array\r\n        }   // end getTransferDataFlavors\r\n\r\n\r\n\r\n        /**\r\n         * Returns the data encapsulated in this {@link TransferableObject}.\r\n         * If the {@link Fetcher} constructor was used, then this is when\r\n         * the {@link Fetcher#getObject getObject()} method will be called.\r\n         * If the requested data flavor is not supported, then the\r\n         * {@link Fetcher#getObject getObject()} method will not be called.\r\n         *\r\n         * @param flavor The data flavor for the data to return\r\n         * @return The dropped data\r\n         * @since 1.1\r\n         */\r\n        public Object getTransferData( java.awt.datatransfer.DataFlavor flavor )\r\n        throws java.awt.datatransfer.UnsupportedFlavorException, java.io.IOException\r\n        {\r\n            // Native object\r\n            if( flavor.equals( DATA_FLAVOR ) )\r\n                return fetcher == null ? data : fetcher.getObject();\r\n\r\n            // String\r\n            if( flavor.equals( java.awt.datatransfer.DataFlavor.stringFlavor ) )\r\n                return fetcher == null ? data.toString() : fetcher.getObject().toString();\r\n\r\n            // We can't do anything else\r\n            throw new java.awt.datatransfer.UnsupportedFlavorException(flavor);\r\n        }   // end getTransferData\r\n\r\n\r\n\r\n\r\n        /**\r\n         * Returns <tt>true</tt> if <var>flavor</var> is one of the supported\r\n         * flavors. Flavors are supported using the <code>equals(...)</code> method.\r\n         *\r\n         * @param flavor The data flavor to check\r\n         * @return Whether or not the flavor is supported\r\n         * @since 1.1\r\n         */\r\n        public boolean isDataFlavorSupported( java.awt.datatransfer.DataFlavor flavor )\r\n        {\r\n            // Native object\r\n            if( flavor.equals( DATA_FLAVOR ) )\r\n                return true;\r\n\r\n            // String\r\n            if( flavor.equals( java.awt.datatransfer.DataFlavor.stringFlavor ) )\r\n                return true;\r\n\r\n            // We can't do anything else\r\n            return false;\r\n        }   // end isDataFlavorSupported\r\n\r\n\r\n    /* ********  I N N E R   I N T E R F A C E   F E T C H E R  ******** */\r\n\r\n        /**\r\n         * Instead of passing your data directly to the {@link TransferableObject}\r\n         * constructor, you may want to know exactly when your data was received\r\n         * in case you need to remove it from its source (or do anyting else to it).\r\n         * When the {@link #getTransferData getTransferData(...)} method is called\r\n         * on the {@link TransferableObject}, the {@link Fetcher}'s\r\n         * {@link #getObject getObject()} method will be called.\r\n         *\r\n         * @author Robert Harder\r\n         * @copyright 2001\r\n         * @version 1.1\r\n         * @since 1.1\r\n         */\r\n        public static interface Fetcher\r\n        {\r\n            /**\r\n             * Return the object being encapsulated in the\r\n             * {@link TransferableObject}.\r\n             *\r\n             * @return The dropped object\r\n             * @since 1.1\r\n             */\r\n            public abstract Object getObject();\r\n        }   // end inner interface Fetcher\r\n\r\n\r\n\r\n    }   // end class TransferableObject\r\n\r\n    \r\n    \r\n    \r\n    \r\n}   // end class FileDrop\r\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/ProgressDialog.java",
    "content": "package org.osm2world.viewer.view;\n\nimport java.awt.BorderLayout;\n\nimport javax.swing.JDialog;\nimport javax.swing.JPanel;\nimport javax.swing.JProgressBar;\nimport javax.swing.JTextField;\n\npublic class ProgressDialog extends JDialog {\n\n\tprivate static final long serialVersionUID = -1016979607905010257L; //generated serialVersionUID\n\tprivate final JProgressBar progressBar;\n\tprivate final JTextField statusField;\n\t\n\tpublic ProgressDialog(ViewerFrame viewerFrame, String title) {\n\t\t\n\t\tsuper(viewerFrame, title);\n\t\t\n\t\tJPanel panel = new JPanel();\n\t\tBorderLayout layout = new BorderLayout();\n\t\tpanel.setLayout(layout);\n\t\t\n\t\tprogressBar = new JProgressBar();\n\t\tpanel.add(progressBar, BorderLayout.CENTER);\n\t\t\n\t\tstatusField = new JTextField(50);\n\t\tstatusField.setEditable(false);\n\t\tpanel.add(statusField, BorderLayout.SOUTH);\n\n\t\tthis.add(panel);\n\t\t\n\t\tsetProgress(null);\n\t\t\n\t\tthis.pack();\n\t\tthis.setVisible(true);\n\t\t\n\t}\n\t\n\tpublic void setText(String text) {\n\t\tstatusField.setText(text);\n\t}\n\t\n\t/**\n\t * @param progress  progress percentage or null for indeterminate progress\n\t */\n\tpublic void setProgress(Integer progress) {\n\t\tif (progress == null) {\n\t\t\tprogressBar.setIndeterminate(true);\n\t\t} else {\n\t\t\tprogressBar.setIndeterminate(false);\n\t\t\tprogressBar.setValue(progress);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/RecentFilesUpdater.java",
    "content": "package org.osm2world.viewer.view;\n\nimport static java.lang.Math.min;\nimport static java.util.Arrays.sort;\n\nimport java.awt.event.ActionListener;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.prefs.BackingStoreException;\nimport java.util.prefs.PreferenceChangeEvent;\nimport java.util.prefs.PreferenceChangeListener;\nimport java.util.prefs.Preferences;\n\nimport javax.swing.JMenu;\nimport javax.swing.JMenuItem;\n\nimport org.osm2world.viewer.Viewer;\n\nimport com.google.common.base.Function;\n\n/**\n * keeps a \"recent files\" menu current based on a persistently stored list\n */\npublic class RecentFilesUpdater implements PreferenceChangeListener {\n\t\n\tprivate static final int MAX_ENTRIES = 10;\n\t\n\tprivate final JMenu recentFilesMenu;\n\tprivate final Function<File, ActionListener> actionForFile;\n\t\n\t/**\n\t * creates the updater, registers it as a listener\n\t * and builds the initial content of the menu\n\t * \n\t * @param actionForFile\n\t *   function creating the listener for opening a particular file\n\t */\n\tpublic RecentFilesUpdater(JMenu recentFilesMenu,\n\t\t\tFunction<File, ActionListener> actionForFile) {\n\t\t\n\t\tthis.recentFilesMenu = recentFilesMenu;\n\t\tthis.actionForFile = actionForFile;\n\t\t\n\t\tprefs().addPreferenceChangeListener(this);\n\t\t\n\t\tupdateRecentFilesMenu();\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void preferenceChange(PreferenceChangeEvent e) {\n\t\tupdateRecentFilesMenu();\n\t}\n\t\n\t/**\n\t * builds (or rebuilds) the menu items in the {@link #recentFilesMenu}\n\t * based on the current state of the preferences\n\t */\n\tprivate void updateRecentFilesMenu() {\n\t\t\n\t\trecentFilesMenu.removeAll();\n\t\t\n\t\ttry {\n\t\t\t\n\t\t\tfor (File file : fileListFromPrefs()) {\n\t\t\t\tJMenuItem menuItem = new JMenuItem(file.getName());\n\t\t\t\tmenuItem.addActionListener(actionForFile.apply(file));\n\t\t\t\trecentFilesMenu.add(menuItem);\n\t\t\t}\n\t\t\t\n\t\t} catch (BackingStoreException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t\t\n\t\trecentFilesMenu.setEnabled(recentFilesMenu.getItemCount() > 0);\n\t\t\n\t}\n\t\n\t/**\n\t * returns the {@link Preferences} node used to persistently store the\n\t * recent files list\n\t */\n\tprivate static final Preferences prefs() {\n\t\treturn Preferences.userNodeForPackage(Viewer.class).node(\"recentFiles\");\n\t}\n\t\n\t/**\n\t * returns the list of recent files from user preferences.\n\t * The list is sorted from newest to oldest entry.\n\t */\n\tprivate static final List<File> fileListFromPrefs()\n\t\t\tthrows BackingStoreException {\n\t\t\n\t\tList<File> result = new ArrayList<File>();\n\t\t\n\t\tString[] keys = prefs().keys();\n\t\tsort(keys);\n\t\t\n\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\tString pathname = prefs().get(keys[i], null);\n\t\t\tresult.add(new File(pathname));\n\t\t}\n\t\t\n\t\treturn result;\n\t\t\n\t}\n\t\n\t/**\n\t * adds a file to the persistently stored list of recent files.\n\t * May replace an existing entry if maximum capacity is reached.\n\t */\n\tpublic static void addRecentFile(File newFile) {\n\t\t\n\t\tnewFile = newFile.getAbsoluteFile();\n\t\t\n\t\ttry {\n\t\t\t\n\t\t\tList<File> files = fileListFromPrefs();\n\t\t\t\n\t\t\tfiles.remove(newFile);\n\t\t\tfiles.add(0, newFile);\n\t\t\t\n\t\t\tprefs().clear();\n\t\t\t\n\t\t\tfor (int i = 0; i < min(files.size(), MAX_ENTRIES); i++) {\n\t\t\t\tprefs().put(\"entry_\" + (char)('a' + i),\n\t\t\t\t\t\tfiles.get(i).getAbsolutePath());\n\t\t\t}\n\t\t\t\n\t\t} catch (BackingStoreException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t\t\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/StatisticsDialog.java",
    "content": "package org.osm2world.viewer.view;\n\nimport static org.osm2world.core.target.statistics.StatisticsTarget.Stat.*;\n\nimport java.awt.event.KeyEvent;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.swing.BoxLayout;\nimport javax.swing.JComponent;\nimport javax.swing.JDialog;\nimport javax.swing.JFrame;\nimport javax.swing.JLabel;\nimport javax.swing.JPanel;\nimport javax.swing.JScrollPane;\nimport javax.swing.JTabbedPane;\nimport javax.swing.JTable;\nimport javax.swing.table.TableModel;\nimport javax.swing.table.TableRowSorter;\n\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.target.statistics.StatisticsTarget;\nimport org.osm2world.core.target.statistics.StatisticsTarget.Stat;\n\n\npublic class StatisticsDialog extends JDialog {\n\t\n\tprivate static final long serialVersionUID = -2724106939639635472L;\n\tprivate static final Map<Stat, String> statNames = new HashMap<Stat, String>();\n\tstatic {\n\t\tstatNames.put(OBJECT_COUNT, \"objects\");\n\t\tstatNames.put(PRIMITIVE_COUNT, \"primitives\");\n\t\tstatNames.put(TOTAL_TRIANGLE_COUNT, \"triangles\");\n\t\tstatNames.put(TRIANGLES_COUNT, \"t. groups\");\n\t\tstatNames.put(TRIANGLE_STRIP_COUNT, \"t. strips\");\n\t\tstatNames.put(TRIANGLE_FAN_COUNT, \"t. fans\");\n\t\tstatNames.put(CONVEX_POLYGON_COUNT, \"polygons\");\n\t\tstatNames.put(VBO_VALUE_COUNT, \"vbo values\");\n\t}\n\t\n\tpublic StatisticsDialog(JFrame owner, StatisticsTarget stats) {\n\t\tsuper(owner, \"Statistics\");\n\t\t\n\t\t/* collect content for tables */\n\t\t\n\t\tList<Material> materialList =\n\t\t\t\tnew ArrayList<Material>(stats.getKnownMaterials());\n\t\tList<Class<?>> classList =\n\t\t\t\tnew ArrayList<Class<?>>(stats.getKnownRenderableClasses());\n\t\t\n\t\tint numColumns = Stat.values().length + 1;\n\t\tint numMaterials = materialList.size();\n\t\tint numClasses = classList.size();\n\t\t\n\t\tString[] columnNames = new String[numColumns];\n\t\t\n\t\tObject[][] perMaterialData =\n\t\t\t\tnew Object[numMaterials + 1][numColumns];\n\t\tObject[][] perClassData =\n\t\t\t\tnew Object[numClasses + 1][numColumns];\n\t\t\n\t\tComparator<?>[] comparators = new Comparator<?>[numColumns];\n\t\t\n\t\tcolumnNames[0] = \"name\";\n\t\tperMaterialData[numMaterials][0] = \"TOTAL\";\n\t\tperClassData[numClasses][0] = \"TOTAL\";\n\t\tcomparators[0] = null;\n\t\t\n\t\tfor (int col = 0; col < numColumns; ++col) {\n\t\t\t\n\t\t\tStat stat = (col == 0) ? null : Stat.values()[col-1];\n\t\t\t\n\t\t\tif (col != 0) {\n\t\t\t\tcolumnNames[col] = statNames.get(stat);\n\t\t\t\tcomparators[col] = LONG_COMPARATOR;\n\t\t\t}\n\t\t\t\n\t\t\tfor (int row = 0; row < numMaterials; ++row) {\n\t\t\t\t\n\t\t\t\tif (col == 0) {\n\t\t\t\t\tString name = Materials.getUniqueName(materialList.get(row));\n\t\t\t\t\tif (name == null) {\n\t\t\t\t\t\tname = materialList.get(row).toString();\n\t\t\t\t\t}\n\t\t\t\t\tperMaterialData[row][0] = name;\n\t\t\t\t} else {\n\t\t\t\t\tperMaterialData[row][col] =\n\t\t\t\t\t\tstats.getCountForMaterial(materialList.get(row), stat);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\t\n\t\t\tfor (int row = 0; row < numClasses; ++row) {\n\t\t\t\t\n\t\t\t\tif (col == 0) {\n\t\t\t\t\tperClassData[row][0] = classList.get(row).getSimpleName();\n\t\t\t\t} else {\n\t\t\t\t\tperClassData[row][col] =\n\t\t\t\t\t\tstats.getCountForClass(classList.get(row), stat);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (col != 0) {\n\t\t\t\tperMaterialData[numMaterials][col] = stats.getGlobalCount(stat);\n\t\t\t\tperClassData[numClasses][col] = stats.getGlobalCount(stat);\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\t/* create tabs */\n\t\t\n\t\tJTabbedPane tabs = new JTabbedPane();\n\t\tthis.add(tabs);\n\t\t\t\t\n\t\t{ // global statistics\n\t\t\t\n\t\t\tJPanel panel = new JPanel();\n\t\t\tpanel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));\n\t\t\t\n\t\t\tpanel.add(new JLabel(\"object types: \" + classList.size()));\n\t\t\tpanel.add(new JLabel(\"materials: \" + materialList.size()));\n\t\t\t\n\t\t\tfor (Stat stat : Stat.values()) {\n\t\t\t\tpanel.add(new JLabel(statNames.get(stat) + \": \"\n\t\t\t\t\t\t+ stats.getGlobalCount(stat)));\n\t\t\t}\n\t\t\t\n\t\t\ttabs.addTab(\"Global\", null, panel, \"Global statistics\");\n\t\t\ttabs.setMnemonicAt(0, KeyEvent.VK_G);\n\t\t\t\n\t\t} { // statistics per material\n\t\t\t\t\t\t\n\t\t\tJComponent component = createTableComponent(\n\t\t\t\t\tcolumnNames, perMaterialData, comparators);\n\t\t\t\n\t\t\ttabs.addTab(\"Materials\", null, component,\n\t\t\t\t\t\"Statistics per material\");\n\t\t\t\n\t\t\ttabs.setMnemonicAt(1, KeyEvent.VK_M);\n\t\t\t\n\t\t} { // statistics per class\n\t\t\t\n\t\t\tJComponent component = createTableComponent(\n\t\t\t\t\tcolumnNames, perClassData, comparators);\n\t\t\t\t\t\t\n\t\t\ttabs.addTab(\"Classes\", null, component,\n\t\t\t\t\t\"Statistics per WorldObject class\");\n\t\t\t\n\t\t\ttabs.setMnemonicAt(2, KeyEvent.VK_C);\n\t\t\t\n\t\t}\n\t\t\n\t\tsetSize(800, 500);\n\t\t\n\t}\n\n\t\n\t\n\tprivate static final Comparator<?> LONG_COMPARATOR = new Comparator<Object>() {\n\t\t@Override public int compare(Object o1, Object o2) {\n\t\t\treturn ((Long)o1).compareTo((Long)o2);\n\t\t}\n\t};\n\t\n\tprivate static JComponent createTableComponent(String[] columnNames,\n\t\t\tObject[][] perMaterialData, Comparator<?>[] comparators) {\n\t\t\n\t\tJTable table = new JTable(perMaterialData, columnNames);\n\t\ttable.setFillsViewportHeight(true);\n\t\t\n\t\tTableRowSorter<TableModel> sorter =\n\t\t\t\tnew TableRowSorter<TableModel>(table.getModel());\n\t\t\n\t\tfor (int col = 0; col < columnNames.length; ++col) {\n\t\t\tsorter.setComparator(col, comparators[col]);\n\t\t}\n\t\t\n\t\ttable.setRowSorter(sorter);\n\n\t\treturn new JScrollPane(table);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/TextRenderer.java",
    "content": "package org.osm2world.viewer.view;\n\nimport java.awt.Color;\n\npublic interface TextRenderer {\n\t\n\t//public abstract void drawText(String string, Vector3D pos, Color color);\n\n\tpublic abstract void drawText(String string, int x, int y,\n\t\t\tint screenWidth, int screenHeight, Color color);\n\n\tpublic abstract void destroy();\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/TextRendererFixedFunction.java",
    "content": "package org.osm2world.viewer.view;\n\nimport java.awt.Color;\nimport java.awt.Font;\n\nimport com.jogamp.opengl.util.awt.TextRenderer;\n\npublic class TextRendererFixedFunction implements org.osm2world.viewer.view.TextRenderer {\n\t\n\tprivate TextRenderer textRenderer = new TextRenderer(\n\t\t\tnew Font(\"SansSerif\", Font.PLAIN, 12), true, false);\n\t//needs quite a bit of memory, so it must not create an instance for each use!\n\t\n//\t@Override\n//\tpublic final void drawText(String string, Vector3D pos, Color color) {\n//\t\ttextRenderer.setColor(color);\n//\t\ttextRenderer.begin3DRendering();\n//\t\ttextRenderer.draw3D(string,\n//\t\t\t\t(float)pos.getX(), (float)pos.getY(), -(float)pos.getZ(),\n//\t\t\t\t0.05f);\n//\t}\n\n\t@Override\n\tpublic final void drawText(String string, int x, int y,\n\t\t\tint screenWidth, int screenHeight, Color color) {\n\t\ttextRenderer.beginRendering(screenWidth, screenHeight);\n\t\ttextRenderer.setColor(color);\n\t\ttextRenderer.draw(string, x, y);\n\t\ttextRenderer.endRendering();\n\t}\n\t\n\t@Override\n\tpublic void destroy() {\n\t\ttextRenderer.dispose();\n\t\ttextRenderer = null;\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/TextRendererShader.java",
    "content": "package org.osm2world.viewer.view;\n\nimport java.awt.Color;\nimport java.io.IOException;\n\nimport javax.media.opengl.GL2ES2;\n\nimport com.jogamp.graph.curve.opengl.RenderState;\nimport com.jogamp.graph.curve.opengl.TextRenderer;\nimport com.jogamp.graph.font.Font;\nimport com.jogamp.graph.font.FontFactory;\nimport com.jogamp.graph.font.FontSet;\nimport com.jogamp.graph.geom.opengl.SVertex;\nimport com.jogamp.opengl.util.glsl.ShaderState;\n\npublic class TextRendererShader implements org.osm2world.viewer.view.TextRenderer {\n\tprivate TextRenderer textRenderer;\n\tprivate Font textRendererFont = null;\n\tprivate GL2ES2 gl;\n\t\n\tpublic TextRendererShader(GL2ES2 gl) {\n\t\tthis.gl = gl;\n\t\ttry {\n\t\t\ttextRendererFont = FontFactory.getDefault().get(FontSet.FAMILY_REGULAR, FontSet.STYLE_SERIF);\n\t\t} catch (IOException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t\tRenderState renderState = RenderState.createRenderState(new ShaderState(), SVertex.factory());\n\t\ttextRenderer = TextRenderer.create(renderState, 0);\n\t\ttextRenderer.init(gl);\n\t\tif (!textRenderer.isInitialized()) {\n\t\t\tthrow new IllegalStateException(\"Text renderer not initlialized.\");\n\t\t}\n\n\t\t// Workaround to get shader initialized properly (fixes problems at first drawText call in some situations)\n\t\ttextRenderer.enable(gl, true);\n\t\ttextRenderer.enable(gl, false);\n\t}\n\n//\t@Override\n//\tpublic void drawText(String string, Vector3D pos, Color color) {\n//\t\ttextRenderer.resetModelview(gl);\n//\t\ttextRenderer.setColorStatic(gl, color.getRed(), color.getGreen(), color.getBlue());\n//\t\tfloat[] posF = {(float)pos.getX(), (float)pos.getY(), -(float)pos.getZ()};\n//\t\tint[] texSize = {0};\n//\t\ttextRenderer.drawString3D(gl.getGL2ES2(), textRendererFont, string, posF, 12, texSize);\n//\t}\n\n\t@Override\n\tpublic void drawText(String string, int x, int y, int screenWidth,\n\t\t\tint screenHeight, Color color) {\n\t\ttextRenderer.enable(gl, true);\n\t\ttextRenderer.setColorStatic(gl, color.getRed(), color.getGreen(), color.getBlue());\n\t\ttextRenderer.resetModelview(gl);\n\t\ttextRenderer.reshapeOrtho(gl, screenWidth, screenHeight, -100000, 100000);\n\t\ttextRenderer.translate(gl, x, y, 0);\n\t\tfloat[] posF = {0, 0, 0}; // not used in TextRendererImpl01\n\t\tint[] texSize = {0};\n\t\ttextRenderer.drawString3D(gl, textRendererFont, string, posF, 12, texSize);\n\t\ttextRenderer.enable(gl, false);\n\t}\n\t\n\t@Override\n\tpublic void destroy() {\n\t\ttextRenderer.destroy(gl);\n\t\ttextRenderer = null;\n\t\ttextRendererFont = null;\n\t\tgl = null;\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/ViewerFrame.java",
    "content": "package org.osm2world.viewer.view;\n\nimport static java.awt.event.KeyEvent.*;\nimport static java.util.Arrays.asList;\n\nimport java.awt.BorderLayout;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.media.opengl.GLCapabilities;\nimport javax.media.opengl.GLProfile;\nimport javax.swing.ButtonGroup;\nimport javax.swing.JCheckBoxMenuItem;\nimport javax.swing.JFrame;\nimport javax.swing.JMenu;\nimport javax.swing.JMenuBar;\nimport javax.swing.JRadioButtonMenuItem;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.InverseDistanceWeightingInterpolator;\nimport org.osm2world.core.map_elevation.creation.LPEleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.LeastSquaresInterpolator;\nimport org.osm2world.core.map_elevation.creation.LinearInterpolator;\nimport org.osm2world.core.map_elevation.creation.NaturalNeighborInterpolator;\nimport org.osm2world.core.map_elevation.creation.NoneEleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.SimpleEleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.creation.TerrainInterpolator;\nimport org.osm2world.core.map_elevation.creation.ZeroInterpolator;\nimport org.osm2world.viewer.control.actions.AboutAction;\nimport org.osm2world.viewer.control.actions.ExitAction;\nimport org.osm2world.viewer.control.actions.ExportObjAction;\nimport org.osm2world.viewer.control.actions.ExportObjDirAction;\nimport org.osm2world.viewer.control.actions.ExportPOVRayAction;\nimport org.osm2world.viewer.control.actions.ExportScreenshotAction;\nimport org.osm2world.viewer.control.actions.HelpControlsAction;\nimport org.osm2world.viewer.control.actions.OpenOSMAction;\nimport org.osm2world.viewer.control.actions.OrthoBoundsAction;\nimport org.osm2world.viewer.control.actions.OrthoTileAction;\nimport org.osm2world.viewer.control.actions.ReloadOSMAction;\nimport org.osm2world.viewer.control.actions.ResetCameraAction;\nimport org.osm2world.viewer.control.actions.SetCameraToCoordinateAction;\nimport org.osm2world.viewer.control.actions.SetEleConstraintEnforcerAction;\nimport org.osm2world.viewer.control.actions.SetTerrainInterpolatorAction;\nimport org.osm2world.viewer.control.actions.ShowCameraConfigurationAction;\nimport org.osm2world.viewer.control.actions.StatisticsAction;\nimport org.osm2world.viewer.control.actions.ToggleBackfaceCullingAction;\nimport org.osm2world.viewer.control.actions.ToggleDebugViewAction;\nimport org.osm2world.viewer.control.actions.ToggleOrthographicProjectionAction;\nimport org.osm2world.viewer.control.actions.ToggleWireframeAction;\nimport org.osm2world.viewer.control.navigation.DefaultNavigation;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.MessageManager;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.debug.ClearingDebugView;\nimport org.osm2world.viewer.view.debug.DebugView;\nimport org.osm2world.viewer.view.debug.EleConnectorDebugView;\nimport org.osm2world.viewer.view.debug.EleConstraintDebugView;\nimport org.osm2world.viewer.view.debug.EleDebugView;\nimport org.osm2world.viewer.view.debug.FaceDebugView;\nimport org.osm2world.viewer.view.debug.InternalCoordsDebugView;\nimport org.osm2world.viewer.view.debug.InverseDistanceWeightingInterpolatorDebugView;\nimport org.osm2world.viewer.view.debug.LatLonDebugView;\nimport org.osm2world.viewer.view.debug.LeastSquaresInterpolatorDebugView;\nimport org.osm2world.viewer.view.debug.LinearInterpolatorDebugView;\nimport org.osm2world.viewer.view.debug.Map2dTreeDebugView;\nimport org.osm2world.viewer.view.debug.MapDataBoundsDebugView;\nimport org.osm2world.viewer.view.debug.MapDataDebugView;\nimport org.osm2world.viewer.view.debug.NaturalNeighborInterpolatorDebugView;\nimport org.osm2world.viewer.view.debug.NetworkDebugView;\nimport org.osm2world.viewer.view.debug.OrthoBoundsDebugView;\nimport org.osm2world.viewer.view.debug.QuadtreeDebugView;\nimport org.osm2world.viewer.view.debug.RoofDataDebugView;\nimport org.osm2world.viewer.view.debug.ShadowView;\nimport org.osm2world.viewer.view.debug.SkyboxView;\nimport org.osm2world.viewer.view.debug.TerrainBoundaryAABBDebugView;\nimport org.osm2world.viewer.view.debug.TerrainBoundaryDebugView;\nimport org.osm2world.viewer.view.debug.WorldObjectNormalsDebugView;\nimport org.osm2world.viewer.view.debug.WorldObjectView;\n\nimport com.google.common.base.Function;\n\npublic class ViewerFrame extends JFrame {\n\n\tprivate static final long serialVersionUID = 5807635150399807163L;\n\n\t/**\n\t * Number of bits for stencil buffer\n\t */\n\tprivate static final int STENCIL_BITS = 8;\n\n\tpublic ViewerGLCanvas glCanvas;\n\t\n\tprivate final Data data = new Data();\n\tprivate final RenderOptions renderOptions = new RenderOptions();\n\tprivate final MessageManager messageManager = new MessageManager();\n\tprivate final List<DebugView> debugViews = new ArrayList<DebugView>();\n\t\n\tprivate final File configFile;\n\t\n\t/**\n\t * \n\t * @param config  configuration object, != null\n\t * @param configFile  properties (where config was loaded from), can be null\n\t * @param inputFile  osm data file to be loaded at viewer start, can be null\n\t */\n\tpublic ViewerFrame(final Configuration config,\n\t\t\tfinal File configFile, File inputFile) {\n\t\t\n\t\tsuper(\"OSM2World Viewer\");\n\t\t\n\t\tthis.configFile = configFile;\n\t\tdata.setConfig(config);\n\t\t\n\t\tcreateMenuBar();\n\t\t\n\t\tcreateCanvas(config);\n\t\t\n\t\tsetDefaultCloseOperation(EXIT_ON_CLOSE);\n\t\t\n\t\tpack();\n\t\t\n\t\tif (inputFile != null) {\n\t\t\tnew OpenOSMAction(this, data, renderOptions).openOSMFile(inputFile, true);\n\t\t}\n\t\t\n\t}\n\t\n\tprivate final Function<File, ActionListener> actionForFileFunction =\n\t\t\tnew Function<File, ActionListener>() {\n\t\t\n\t\tpublic ActionListener apply(final File file) {\n\t\t\t\n\t\t\treturn new ActionListener() {\n\t\t\t\t@Override public void actionPerformed(ActionEvent e) {\n\t\t\t\t\tnew OpenOSMAction(ViewerFrame.this, data,\n\t\t\t\t\t\t\trenderOptions).openOSMFile(file, true);\n\t\t\t\t}\n\t\t\t};\n\t\t\t\n\t\t}\n\t\t\n\t};\n\n\tprivate void createMenuBar() {\n\n\t\tJMenuBar menu = new JMenuBar();\n\n\t\t{ //\"File\"\n\n\t\t\tJMenu recentFilesMenu = new JMenu(\"Recent files\");\n\t\t\t\n\t\t\tnew RecentFilesUpdater(recentFilesMenu, actionForFileFunction);\n\t\t\t\n\t\t\tJMenu subMenu = new JMenu(\"File\");\n\t\t\tsubMenu.setMnemonic(VK_F);\n\t\t\tsubMenu.add(new OpenOSMAction(this, data, renderOptions));\n\t\t\tsubMenu.add(new ReloadOSMAction(this, data, renderOptions, configFile));\n\t\t\tsubMenu.add(recentFilesMenu);\n\t\t\tsubMenu.add(new ExportObjAction(this, data, messageManager, renderOptions));\n\t\t\tsubMenu.add(new ExportObjDirAction(this, data, messageManager, renderOptions));\n\t\t\tsubMenu.add(new ExportPOVRayAction(this, data, messageManager, renderOptions));\n\t\t\tsubMenu.add(new ExportScreenshotAction(this, data, messageManager, renderOptions));\n\t\t\tsubMenu.add(new StatisticsAction(this, data));\n\t\t\tsubMenu.add(new ExitAction());\n\t\t\tmenu.add(subMenu);\n\n\t\t} { //\"View\"\n\t\t\t\n\t\t\tJMenu subMenu = new JMenu(\"View\");\n\t\t\tsubMenu.setMnemonic(VK_V);\n\t\t\t\n\t\t\tsubMenu.add(new JCheckBoxMenuItem(new ToggleWireframeAction(this, data, renderOptions)));\n\t\t\tsubMenu.add(new JCheckBoxMenuItem(new ToggleBackfaceCullingAction(this, data, renderOptions)));\n\t\t\t\n\t\t\tinitAndAddDebugView(subMenu, VK_W, true,\n\t\t\t\t\tnew WorldObjectView(renderOptions));\n\t\t\tinitAndAddDebugView(subMenu, -1, true,\n\t\t\t\t\tnew SkyboxView());\n\t\t\t\n\t\t\tsubMenu.addSeparator();\n\t\t\t\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew TerrainBoundaryAABBDebugView());\n\t\t\tinitAndAddDebugView(subMenu, VK_L, false,\n\t\t\t\t\tnew ClearingDebugView());\n\t\t\tinitAndAddDebugView(subMenu, VK_D, false,\n\t\t\t\t\tnew MapDataDebugView());\n\t\t\tinitAndAddDebugView(subMenu, VK_E, false,\n\t\t\t\t\tnew EleConnectorDebugView());\n\t\t\tinitAndAddDebugView(subMenu, VK_C, false,\n\t\t\t\t\tnew EleConstraintDebugView());\n\t\t\tinitAndAddDebugView(subMenu, VK_R, false,\n\t\t\t\t\tnew RoofDataDebugView());\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew FaceDebugView());\n\t\t\tinitAndAddDebugView(subMenu, VK_X, false,\n\t\t\t\t\tnew NetworkDebugView());\n\t\t\tinitAndAddDebugView(subMenu, VK_Q, false,\n\t\t\t\t\tnew QuadtreeDebugView());\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew Map2dTreeDebugView());\n\t\t\tinitAndAddDebugView(subMenu, VK_B, false,\n\t\t\t\t\tnew TerrainBoundaryDebugView());\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew WorldObjectNormalsDebugView());\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew MapDataBoundsDebugView());\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew OrthoBoundsDebugView());\n\t\t\tsubMenu.add(new JCheckBoxMenuItem(new ToggleDebugViewAction(\n\t\t\t\t\tnew InternalCoordsDebugView(), -1, false,\n\t\t\t\t\tthis, data, renderOptions)));\n\t\t\tsubMenu.add(new JCheckBoxMenuItem(new ToggleDebugViewAction(\n\t\t\t\t\tnew LatLonDebugView(), -1, false,\n\t\t\t\t\tthis, data, renderOptions)));\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew EleDebugView());\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew NaturalNeighborInterpolatorDebugView(renderOptions));\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew LeastSquaresInterpolatorDebugView(renderOptions));\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew InverseDistanceWeightingInterpolatorDebugView(renderOptions));\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew LinearInterpolatorDebugView(renderOptions));\n\t\t\tinitAndAddDebugView(subMenu, -1, false,\n\t\t\t\t\tnew ShadowView(renderOptions));\n\t\t\t\n\t\t\tmenu.add(subMenu);\n\t\t\t\n\t\t} { //\"Camera\"\n\n\t\t\tJMenu subMenu = new JMenu(\"Camera\");\n\t\t\tsubMenu.setMnemonic(VK_C);\n\t\t\tsubMenu.add(new ResetCameraAction(this, data, renderOptions));\n\t\t\tsubMenu.add(new SetCameraToCoordinateAction(this, data, renderOptions));\n\t\t\tsubMenu.add(new OrthoTileAction(this, data, renderOptions));\n\t\t\tsubMenu.add(new OrthoBoundsAction(this, data, renderOptions));\n\t\t\tsubMenu.add(new JCheckBoxMenuItem(\n\t\t\t\t\tnew ToggleOrthographicProjectionAction(this, data, renderOptions)));\n\t\t\tsubMenu.add(new ShowCameraConfigurationAction(data, renderOptions));\n\t\t\tmenu.add(subMenu);\n\n\t\t} { //\"Options\"\n\n\t\t\tJMenu subMenu = new JMenu(\"Options\");\n\t\t\tsubMenu.setMnemonic(VK_O);\n\t\t\t\n\t\t\tJMenu interpolatorMenu = new JMenu(\"TerrainInterpolator\");\n\t\t\tsubMenu.add(interpolatorMenu);\n\t\t\t\n\t\t\tButtonGroup interpolatorGroup = new ButtonGroup();\n\t\t\t\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tList<Class<? extends TerrainInterpolator>> interpolatorClasses = asList(\n\t\t\t\t\tZeroInterpolator.class,\n\t\t\t\t\tLinearInterpolator.class,\n\t\t\t\t\tInverseDistanceWeightingInterpolator.class,\n\t\t\t\t\tLeastSquaresInterpolator.class,\n\t\t\t\t\tNaturalNeighborInterpolator.class);\n\t\t\t\n\t\t\tfor (Class<? extends TerrainInterpolator> c : interpolatorClasses) {\n\t\t\t\t\n\t\t\t\tJRadioButtonMenuItem item = new JRadioButtonMenuItem(\n\t\t\t\t\t\tnew SetTerrainInterpolatorAction(c,\n\t\t\t\t\t\t\t\tthis, data, renderOptions));\n\t\t\t\t\n\t\t\t\tinterpolatorGroup.add(item);\n\t\t\t\tinterpolatorMenu.add(item);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tJMenu enforcerMenu = new JMenu(\"EleConstraintEnforcer\");\n\t\t\tsubMenu.add(enforcerMenu);\n\t\t\t\n\t\t\tButtonGroup enforcerGroup = new ButtonGroup();\n\t\t\t\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tList<Class<? extends EleConstraintEnforcer>> enforcerClasses = asList(\n\t\t\t\t\tNoneEleConstraintEnforcer.class,\n\t\t\t\t\tSimpleEleConstraintEnforcer.class,\n\t\t\t\t\tLPEleConstraintEnforcer.class);\n\t\t\t\n\t\t\tfor (Class<? extends EleConstraintEnforcer> c : enforcerClasses) {\n\t\t\t\t\n\t\t\t\tJRadioButtonMenuItem item = new JRadioButtonMenuItem(\n\t\t\t\t\t\tnew SetEleConstraintEnforcerAction(c,\n\t\t\t\t\t\t\t\tthis, data, renderOptions));\n\t\t\t\t\n\t\t\t\tenforcerGroup.add(item);\n\t\t\t\tenforcerMenu.add(item);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tmenu.add(subMenu);\n\n\t\t} { //\"Help\"\n\n\t\t\tJMenu subMenu = new JMenu(\"Help\");\n\t\t\tsubMenu.add(new HelpControlsAction());\n\t\t\tsubMenu.add(new AboutAction());\n\t\t\tsubMenu.setMnemonic(VK_H);\n\n\t\t\tmenu.add(subMenu);\n\n\t\t}\n\n\t\tthis.setJMenuBar(menu);\n\t\t\n\t}\n\n\t/**\n\t * initializes a debug view and adds a menu item for it\n\t */\n\tprivate void initAndAddDebugView(JMenu menu, int keyEvent,\n\t\t\tboolean enabled, DebugView debugView) {\n\t\t\n\t\tdebugView.setConfiguration(data.getConfig());\n\t\t\n\t\tmenu.add(new JCheckBoxMenuItem(new ToggleDebugViewAction(\n\t\t\t\tdebugView, keyEvent, enabled,\n\t\t\t\tthis, data, renderOptions)));\n\t\t\n\t\tdebugViews.add(debugView);\n\t\t\n\t}\n\n\tpublic MessageManager getMessageManager() {\n\t\treturn messageManager;\n\t}\n\t\n\t/**\n\t * Prepare OpenGL (Profile/Capabilities) and create matching canvas.\n\t * @param config config for OpenGL parameters\n\t */\n\tprivate void createCanvas(Configuration config) {\n\t\t// select OpengGL implementation. TODO: autodetection\n\t\tGLProfile profile;\n\t\tif (\"shader\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\tprofile = GLProfile.get(GLProfile.GL3);\n\t\t} else {\n\t\t\tprofile = GLProfile.get(GLProfile.GL2);\n\t\t}\n\t\tGLCapabilities caps = new GLCapabilities(profile);\n\t\t\n\t\t// set MSAA (Multi Sample Anti-Aliasing)\n\t\tint msaa = config.getInt(\"msaa\", 0);\n\t\tif (msaa > 0) {\n\t\t\tcaps.setSampleBuffers(true);\n\t\t\tcaps.setNumSamples(msaa);\n\t\t}\n\t\t\n\t\tif (\"shader\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\tif (\"shadowVolumes\".equals(config.getString(\"shadowImplementation\")) || \"both\".equals(config.getString(\"shadowImplementation\")))\n\t\t\t\tcaps.setStencilBits(STENCIL_BITS);\n\t\t}\n\t\t\n\t\tglCanvas = new ViewerGLCanvas(data, messageManager, renderOptions, caps);\n\t\tadd(glCanvas, BorderLayout.CENTER);\n\t\t\n\t\tnew FileDrop(glCanvas, new FileDrop.Listener() {\n\t\t\t@Override\n\t\t\tpublic void filesDropped(File[] files) {\n\t\t\t\tif (files.length >= 1) {\n\t\t\t\t\tnew OpenOSMAction(ViewerFrame.this, data, renderOptions)\n\t\t\t\t\t\t.openOSMFile(files[0], true);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\t\n\t\tDefaultNavigation navigation = new DefaultNavigation(this, renderOptions);\n\t\tglCanvas.addMouseListener(navigation);\n\t\tglCanvas.addMouseMotionListener(navigation);\n\t\tglCanvas.addMouseWheelListener(navigation);\n\t\tglCanvas.addKeyListener(navigation);\n\t}\n\n\t/**\n\t * Update with new configuration. May recreate the canvas when the OpenGL-Parameters changed\n\t */\n\tpublic void setConfiguration(Configuration config) {\n\t\tif (!checkConfiguration(config)) {\n\t\t\tSystem.out.println(\"OpenGL configuration changed. Recreating canvas.\");\n\t\t\t\n\t\t\t// reset all DebugViews to free resources bound to the old OpenGL context. The context still needs to exist at this point\n\t\t\tfor(DebugView d : debugViews) {\n\t\t\t\td.reset();\n\t\t\t}\n\t\t\t\n\t\t\t// destroy old OpenGL context\n\t\t\tremove(glCanvas);\n\t\t\tglCanvas.destroy();\n\t\t\t\n\t\t\t// recreate\n\t\t\tcreateCanvas(config);\n\t\t\tpack();\n\t\t}\n\t}\n\t\n\t/**\n\t * Check if the current OpenGL-Parameters match the given configuration.\n\t * @return true if the configuration matches, false otherwise. In the latter case the GLCanvas needs to be recreated\n\t */\n\tprivate boolean checkConfiguration(Configuration config) {\n\t\tif (\"shader\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\tif (!glCanvas.getGLProfile().isGL3())\n\t\t\t\treturn false;\n\t\t} else {\n\t\t\tif (!glCanvas.getGLProfile().isGL2())\n\t\t\t\treturn false;\n\t\t}\n\t\t\n\t\tint msaa = config.getInt(\"msaa\", 0);\n\t\tif (glCanvas.getChosenGLCapabilities().getNumSamples() != msaa)\n\t\t\treturn false;\n\n\t\tif (glCanvas.getChosenGLCapabilities().getSampleBuffers() != msaa > 0)\n\t\t\treturn false;\n\n\t\t\n\t\tif (\"shader\".equals(config.getString(\"joglImplementation\"))\n\t\t\t\t&& (\"shadowVolumes\".equals(config.getString(\"shadowImplementation\")) || \"both\".equals(config.getString(\"shadowImplementation\")))) {\n\t\t\tif (glCanvas.getChosenGLCapabilities().getStencilBits() != STENCIL_BITS)\n\t\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/ViewerGLCanvas.java",
    "content": "package org.osm2world.viewer.view;\n\nimport java.awt.Color;\n\nimport javax.media.opengl.GL;\nimport javax.media.opengl.GLAutoDrawable;\nimport javax.media.opengl.GLCapabilities;\nimport javax.media.opengl.GLEventListener;\nimport javax.media.opengl.awt.GLCanvas;\nimport org.osm2world.core.target.jogl.AbstractJOGLTarget;\nimport org.osm2world.viewer.model.Data;\nimport org.osm2world.viewer.model.MessageManager;\nimport org.osm2world.viewer.model.MessageManager.Message;\nimport org.osm2world.viewer.model.RenderOptions;\nimport org.osm2world.viewer.view.debug.DebugView;\nimport org.osm2world.viewer.view.debug.HelpView;\nimport org.osm2world.viewer.view.debug.WorldObjectView;\n\nimport com.jogamp.opengl.util.FPSAnimator;\n\npublic class ViewerGLCanvas extends GLCanvas {\n\n\tprivate static final long serialVersionUID = 817150566654010861L;\n\n\tpublic ViewerGLCanvas(Data data, MessageManager messageManager, RenderOptions renderOptions, GLCapabilities capabilities) {\n\t\tsuper(capabilities);\n\t\t\n\t\tsetSize(800, 600);\n\t\tsetIgnoreRepaint(true);\n\n\t\taddGLEventListener(new ViewerGLEventListener(data, messageManager, renderOptions));\n\n\t\tFPSAnimator animator = new FPSAnimator( this, 60 );\n        \n\t\tanimator.start();\n\n\t}\n\t\n\n\tpublic class ViewerGLEventListener implements GLEventListener {\n\n\t\tprivate final Data data;\n\t\tprivate final MessageManager messageManager;\n\t\tprivate final RenderOptions renderOptions;\n\t\t\t\t\n\t\tprivate final HelpView helpView = new HelpView();\n\t\t\n\t\tprivate TextRenderer textRenderer;\n\t\t\n\t\tpublic ViewerGLEventListener(Data data, MessageManager messageManager, RenderOptions renderOptions) {\n\t\t\tthis.data = data;\n\t\t\tthis.messageManager = messageManager;\n\t\t\tthis.renderOptions = renderOptions;\n\t\t}\n\n\t\t@Override\n\t\tpublic void display(GLAutoDrawable glDrawable) {\n\t\t\t\n\t        final GL gl = glDrawable.getGL();\n\t        \n\t        AbstractJOGLTarget.clearGL(gl, new Color(0, 0, 0, 0));\n\t        \n\t        helpView.renderTo(gl, null, null);\n\t        \n\t        if (renderOptions.camera != null) {\n\t        \t\n\t        \t/* prepare projection matrix stack */\n\t        \t\n\t        \t//TODO: reactivate to allow\n//\t\t        //calculate height for orthographic projection to match\n//\t\t        //the height of the perspective view volume at lookAt's distance\n//\t\t        double dist = renderOptions.camera.getLookAt().subtract(\n//\t\t        \t\trenderOptions.camera.getPos())\n//\t\t        \t\t.length();\n//\t\t        double tanAngle = Math.tan(renderOptions.projection.getVertAngle());\n//\t\t        double height = tanAngle * dist;\n//\t\t        renderOptions.projection = renderOptions.projection.withVolumeHeight(height);\n\t        \t\t        \t\n\t        \t/* draw debug views */\n\t        \t\n\t        \tDebugView activeWorldObjectView = null;\n\t        \t\n\t        \tfor (DebugView debugView : renderOptions.activeDebugViews) {\n\t        \t\tif (debugView instanceof WorldObjectView) {\n\t        \t\t\t// needs to be rendered last because of transparency\n\t        \t\t\tactiveWorldObjectView = debugView;\n\t        \t\t\tcontinue;\n\t        \t\t}\n\t        \t\tdebugView.renderTo(gl, renderOptions.camera, renderOptions.projection);\n\t        \t}\n\t        \t\n\t        \tif (activeWorldObjectView != null) {\n\t        \t\tactiveWorldObjectView.renderTo(gl, renderOptions.camera, renderOptions.projection);\n\t        \t}\n\t        \t\n\t        \t/* write messages */\n\t        \t\n\t        \tint messageCount = 0;\n\t        \tfor (Message message : messageManager.getLiveMessages()) {\n\t        \t\ttextRenderer.drawText(message.messageString,\n\t        \t\t\t\t10, 10 + messageCount * 20,\n\t        \t\t\t\tViewerGLCanvas.this.getWidth(),\n\t        \t\t\t\tViewerGLCanvas.this.getHeight(),\n\t        \t\t\t\tColor.WHITE);\n\t        \t\tmessageCount++;\n\t        \t}\n\t        \t\n\t        \tgl.glFlush();\n\n\t        }\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void init(GLAutoDrawable glDrawable) {\n\t\t\tif (\"shader\".equals(data.getConfig().getString(\"joglImplementation\"))) {\n\t\t\t\ttextRenderer = new TextRendererShader(glDrawable.getGL().getGL2ES2());\n\t\t\t} else {\n\t\t\t\ttextRenderer = new TextRendererFixedFunction();\n\t\t\t}\n\t\t\thelpView.setConfiguration(data.getConfig());\n\t\t\t//initialization is performed within JOGLTarget\n\t\t}\n\n\t\t@Override\n\t\tpublic void reshape(GLAutoDrawable gLDrawable,\n\t\t\t\tint x, int y, int width, int height) {\n\t\t\t\n\t\t\tfinal GL gl = gLDrawable.getGL();\n\n\t        if (height <= 0) { // avoid a divide by zero error!\n\t            height = 1;\n\t        }\n\t        \t        \n\t        gl.glViewport(0, 0, width, height);\n\t        \n\t        renderOptions.projection =\n\t        \trenderOptions.projection.withAspectRatio((double)width / height);\n\t        \n\t\t}\n\t\n\t\t@Override\n\t\tpublic void dispose(GLAutoDrawable arg0) {\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/ClearingDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.target.jogl.JOGLTarget;\n\n/**\n * shows information from elevation calculation\n */\npublic class ClearingDebugView extends DebugView {\n\n\tprivate static final int LINE_WIDTH = 5;\n\tprivate static final float HALF_NODE_WIDTH = 0.4f;\n\tprivate static final int NODE_COLUMN_WIDTH = 5;\n\t\n\tprivate static final Color LINE_SURFACE_COLOR = Color.LIGHT_GRAY;\n\tprivate static final Color LINE_BELOW_COLOR = Color.YELLOW;\n\tprivate static final Color LINE_ABOVE_COLOR = Color.BLUE;\n\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows information from elevation calculation\";\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\n\t\t//TODO useless for new elevation calculation\n\t\t\n//\t\tfor (MapWaySegment line : map.getMapWaySegments()) {\n//\n//\t\t\tfor (WorldObject rep : line.getRepresentations()) {\n//\n//\t\t\t\tWaySegmentElevationProfile profile = line.getElevationProfile();\n//\t\t\t\tList<VectorXYZ> pointsWithEle = profile.getPointsWithEle();\n//\t\t\t\tint size = pointsWithEle.size();\n//\n//\t\t\t\tVectorXYZ[] linePoints = new VectorXYZ[size];\n//\t\t\t\tVectorXYZ[] upperClearingPoints = new VectorXYZ[size];\n//\t\t\t\tVectorXYZ[] lowerClearingPoints = new VectorXYZ[size];\n//\n//\t\t\t\tVectorXYZ[] clearingPolygonPoints = new VectorXYZ[2*size];\n//\n//\t\t\t\tfor (int i = 0; i < size; i++) {\n//\t\t\t\t\tVectorXYZ p = pointsWithEle.get(i);\n//\n//\t\t\t\t\tlinePoints[i] = p;\n//\n//\t\t\t\t\tfinal VectorXYZ pMin = p.y(p.y-rep.getClearingBelow(p.xz()));\n//\t\t\t\t\tfinal VectorXYZ pMax = p.y(p.y+rep.getClearingAbove(p.xz()));\n//\n//\t\t\t\t\tupperClearingPoints[i] = pMin;\n//\t\t\t\t\tlowerClearingPoints[size-1-i] = pMax;\n//\n//\t\t\t\t\tclearingPolygonPoints[i] = pMin;\n//\t\t\t\t\tclearingPolygonPoints[2*size-1-i] = pMax;\n//\n//\t\t\t\t\t//TODO: this isn't necessarily the maximum clearing precision!\n//\n//\t\t\t\t}\n//\n//\t\t\t\tColor color = getColorForState(rep.getGroundState());\n//\n//\t\t\t\ttarget.drawLineStrip(color, LINE_WIDTH, linePoints);\n//\t\t\t\ttarget.drawLineStrip(color, 1, upperClearingPoints);\n//\t\t\t\ttarget.drawLineStrip(color, 1, lowerClearingPoints);\n//\n//\t\t\t\t/* TODO replace stipple effect\n//\t\t\t\tgl.glEnable(GL2.GL_POLYGON_STIPPLE);\n//\t\t\t\tgl.glPolygonStipple(STIPPLE_PATTERN, 0);\n//\t\t\t\t*/\n//\t\t\t\ttarget.drawConvexPolygon(\n//\t\t\t\t\t\tnew ImmutableMaterial(Lighting.FLAT, color),\n//\t\t\t\t\t\tasList(clearingPolygonPoints), null);\n//\t\t\t\t/*\n//\t\t\t\tgl.glDisable(GL2.GL_POLYGON_STIPPLE);\n//\t\t\t\t*/\n//\n//\t\t\t}\n//\n//\t\t}\n//\n//\t\tfor (MapNode node : map.getMapNodes()) {\n//\n//\t\t\tfor (NodeWorldObject rep : node.getRepresentations()) {\n//\n//\t\t\t\tNodeElevationProfile profile = node.getElevationProfile();\n//\t\t\t\tColor color = getColorForState(rep.getGroundState());\n//\t\t\t\tVectorXYZ p = profile.getPointWithEle();\n//\n//\t\t\t\tdrawBoxAround(target, profile.getPointWithEle(), color, HALF_NODE_WIDTH);\n//\n//\t\t\t\ttarget.drawLineStrip(color, NODE_COLUMN_WIDTH,\n//\t\t\t\t\t\tp.y(p.y-rep.getClearingBelow(p.xz())),\n//\t\t\t\t\t\tp.y(p.y+rep.getClearingAbove(p.xz())));\n//\n//\t\t\t}\n//\n//\t\t}\n\n\t}\n\t\n\tprivate static Color getColorForState(GroundState state) {\n\t\tif (state == GroundState.ABOVE) {\n\t\t\treturn LINE_ABOVE_COLOR;\n\t\t} else if (state == GroundState.BELOW) {\n\t\t\treturn LINE_BELOW_COLOR;\n\t\t} else {\n\t\t\treturn LINE_SURFACE_COLOR;\n\t\t}\n\t}\n\t\t\n\tprivate static final byte STIPPLE_PATTERN[] =\n\t  { (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x11, (byte) 0x11,\n\t    (byte) 0x11, (byte) 0x11, (byte) 0x88, (byte) 0x88, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x11,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x11,\n\t    (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x88, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x11, (byte) 0x11, (byte) 0x11,\n\t    (byte) 0x11, (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x88,\n\t    (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x11, (byte) 0x11,\n\t    (byte) 0x11, (byte) 0x11, (byte) 0x88, (byte) 0x88, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x11,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x11,\n\t    (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x88, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x11, (byte) 0x11, (byte) 0x11,\n\t    (byte) 0x11, (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x88,\n\t    (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x11, (byte) 0x11,\n\t    (byte) 0x11, (byte) 0x11, (byte) 0x88, (byte) 0x88, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x11,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x11,\n\t    (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x88, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x11, (byte) 0x11, (byte) 0x11,\n\t    (byte) 0x11, (byte) 0x88, (byte) 0x88, (byte) 0x88, (byte) 0x88,\n\t    (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x11, (byte) 0x88,\n\t    (byte) 0x88, (byte) 0x88, (byte) 0x88 };\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/DebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static java.util.Arrays.asList;\n\nimport java.awt.Color;\n\nimport javax.media.opengl.GL;\n\nimport org.apache.commons.configuration.Configuration;\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_elevation.creation.TerrainElevationData;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.material.ImmutableMaterial;\nimport org.osm2world.core.target.common.material.Material.Interpolation;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\nimport org.osm2world.core.target.jogl.JOGLRenderingParameters;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.target.jogl.JOGLTargetFixedFunction;\nimport org.osm2world.core.target.jogl.JOGLTargetShader;\n\n/**\n * contains some common methods for debug views\n */\npublic abstract class DebugView {\n\t\n\tprotected Configuration config;\n\t\n\tprotected MapData map;\n\tprotected TerrainElevationData eleData;\n\t\n\tprotected Camera camera;\n\tprotected Projection projection;\n\t\n\t// camera attributes that are used to detect changes\n\tprivate VectorXYZ cameraPos;\n\tprivate VectorXYZ cameraUp;\n\tprivate VectorXYZ cameraLookAt;\n\t\n\tprivate JOGLTarget target = null;\n\tprivate boolean targetNeedsReset;\n\t\n\tpublic final void setConfiguration(Configuration config) {\n\t\t\n\t\tthis.config = config;\n\t\t\n\t\tif (target != null) {\n\t\t\ttarget.setConfiguration(config);\n\t\t\ttargetNeedsReset = true;\n\t\t}\n\t\t\n\t}\n\t\n\tpublic void reset() {\n\t\tif (target != null) {\n\t\t\ttarget.freeResources();\n\t\t\ttarget = null;\n\t\t}\n\t}\n\t\n\tpublic void setConversionResults(Results conversionResults) {\n\t\n\t\tthis.map = conversionResults.getMapData();\n\t\tthis.eleData = conversionResults.getEleData();\n\t\t\n\t\ttargetNeedsReset = true;\n\t}\n\t\n\t/**\n\t * returns true if this DebugView can currently be used for rendering.\n\t * By default, this checks whether all the setters have been used with\n\t * non-null values, but subclasses can overwrite it with their own checks.\n\t */\n\tpublic boolean canBeUsed() {\n\t\treturn map != null\n\t\t\t&& eleData != null;\n\t}\n\t\n\t/**\n\t * returns a description of the debug view\n\t */\n\tpublic String getDescription() {\n\t\treturn \"\";\n\t}\n\t\n\t/**\n\t * renders the content added by {@link #fillTarget(JOGLTarget)}.\n\t * Only has an effect if {@link #canBeUsed()} is true.\n\t * \n\t * @param gl  needs to be the same gl as in previous calls\n\t */\n\tpublic void renderTo(GL gl, Camera camera, Projection projection) {\n\t\t\n\t\tif (canBeUsed() && camera != null && projection != null) {\n\t\t\t\t\t\n\t\t\tif (target == null) {\n\t\t\t\tif (\"shader\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\t\t\ttarget = new JOGLTargetShader(gl.getGL3(), new JOGLRenderingParameters(), null);\n\t\t\t\t} else {\n\t\t\t\t\ttarget = new JOGLTargetFixedFunction(gl.getGL2(), new JOGLRenderingParameters(), null);\n\t\t\t\t}\n\t\t\t\ttarget.setConfiguration(config);\n\t\t\t} else if (targetNeedsReset){\n\t\t\t\ttarget.reset();\n\t\t\t}\n\t\t\ttargetNeedsReset = false;\n\t\t\t\n\t\t\tboolean viewChanged = !camera.getPos().equals(this.cameraPos)\n\t\t\t\t\t|| !camera.getUp().equals(this.cameraUp)\n\t\t\t\t\t|| !camera.getLookAt().equals(this.cameraLookAt)\n\t\t\t\t\t|| !projection.equals(this.projection);\n\t\t\t\n\t\t\tthis.camera = camera;\n\t\t\tthis.cameraPos = camera.getPos();\n\t\t\tthis.cameraUp = camera.getUp();\n\t\t\tthis.cameraLookAt = camera.getLookAt();\n\t\t\tthis.projection = projection;\n\t\t\t\n\t\t\tif (!target.isFinished()) {\n\t\t\t\t\n\t\t\t\tfillTarget(target);\n\t\t\t\t\n\t\t\t\ttarget.finish();\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t\n\t\t\t\tupdateTarget(target, viewChanged);\n\t\t\t\t\n\t\t\t\ttarget.finish();\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\ttarget.render(camera, projection);\n\t\t\t\n\t\t}\n\t}\n\t\t\n\t/**\n\t * lets the subclass add all content and settings for rendering.\n\t * Will only be called if {@link #canBeUsed()} is true.\n\t */\n\tprotected abstract void fillTarget(JOGLTarget target);\n\t\n\t/**\n\t * lets the subclass update the target after the initial\n\t * {@link #fillTarget(JOGLTarget)}.\n\t * \n\t * @param viewChanged  true if camera or projection have changed\n\t */\n\tprotected void updateTarget(JOGLTarget target, boolean viewChanged) {};\n\t\n\tprotected static final void drawBoxAround(JOGLTarget target,\n\t\t\tVectorXZ center, Color color, float halfWidth) {\n\t\tdrawBoxAround(target, center.xyz(0), color, halfWidth);\n\t}\n\n\tprotected static final void drawBoxAround(JOGLTarget target,\n\t\t\tVectorXYZ center, Color color, float halfWidth) {\n\t\tdrawBox(target, color,\n\t\t\tnew VectorXYZ(center.x - halfWidth,\n\t\t\t\tcenter.y,\n\t\t\t\tcenter.z - halfWidth),\n\t\t\tnew VectorXYZ(center.x - halfWidth,\n\t\t\t\tcenter.y,\n\t\t\t\tcenter.z + halfWidth),\n\t\t\tnew VectorXYZ(center.x + halfWidth,\n\t\t\t\tcenter.y,\n\t\t\t\tcenter.z + halfWidth),\n\t\t\tnew VectorXYZ(center.x + halfWidth,\n\t\t\t\tcenter.y,\n\t\t\t\tcenter.z - halfWidth));\n\t}\n\t\n\tprotected static final void drawBox(JOGLTarget target, Color color,\n\t\t\tVectorXYZ v1, VectorXYZ v2, VectorXYZ v3, VectorXYZ v4) {\n\t\ttarget.drawLineLoop(color, 1, asList(v1, v2, v3, v4));\n\t}\n\t\n\tprotected static final void drawArrow(JOGLTarget target, Color color,\n\t\t\tfloat headLength, VectorXYZ... vs) {\n\t\t\n\t\ttarget.drawLineStrip(color, 1, vs);\n\t\t\n\t\t/* draw head */\n\t\t\n\t\tVectorXYZ lastV = VectorXYZ.xyz(vs[vs.length-1]);\n\t\tVectorXYZ slastV = VectorXYZ.xyz(vs[vs.length-2]);\n\t\t\n\t\tVectorXYZ endDir = lastV.subtract(slastV).normalize();\n\t\tVectorXYZ headStart = lastV.subtract(endDir.mult(headLength));\n\t\t\n\t\tVectorXZ endDirXZ = endDir.xz();\n\t\tif (endDirXZ.lengthSquared() < 0.01) { //(almost) vertical vector\n\t\t\tendDirXZ = VectorXZ.X_UNIT;\n\t\t} else {\n\t\t\tendDirXZ = endDirXZ.normalize();\n\t\t}\n\t\tVectorXZ endNormalXZ = endDirXZ.rightNormal();\n\t\t\n\t\t\n\t\tImmutableMaterial colorMaterial =\n\t\t\t\tnew ImmutableMaterial(Interpolation.FLAT, color);\n\t\t\n\t\ttarget.drawTriangleStrip(colorMaterial, asList(\n\t\t\t\tlastV,\n\t\t\t\theadStart.subtract(endDirXZ.mult(headLength/2)),\n\t\t\t\theadStart.add(endDirXZ.mult(headLength/2))),\n\t\t\t\tnull);\n\t\t\n\t\ttarget.drawTriangleStrip(colorMaterial, asList(\n\t\t\t\tlastV,\n\t\t\t\theadStart.subtract(endNormalXZ.mult(headLength/2)),\n\t\t\t\theadStart.add(endNormalXZ.mult(headLength/2))),\n\t\t\t\tnull);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/EleConnectorDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static java.awt.Color.BLUE;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * shows all {@link EleConnector}s\n */\npublic class EleConnectorDebugView extends DebugView {\n\t\n\tprivate static final Color CONNECTOR_COLOR = BLUE;\n\tprivate static final float CONNECTOR_HALF_WIDTH = 0.25f;\n\t\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows all elevation connectors\";\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tprotected void fillTarget(JOGLTarget target) {\n\t\t\n\t\tfor (WorldObject worldObject : map.getWorldObjects()) {\n\t\t\tfor (EleConnector eleConnector : worldObject.getEleConnectors()) {\n\t\t\t\tif (eleConnector.getPosXYZ() == null) {\n\t\t\t\t\tcontinue; //TODO shouldn't happen\n\t\t\t\t}\n\t\t\t\tdrawBoxAround(target, eleConnector.getPosXYZ(),\n\t\t\t\t\t\tCONNECTOR_COLOR, CONNECTOR_HALF_WIDTH);\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/EleConstraintDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static java.awt.Color.*;\nimport static org.osm2world.core.math.GeometryUtil.interpolateBetween;\n\nimport java.awt.Color;\nimport java.util.List;\n\nimport org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;\nimport org.osm2world.core.map_elevation.data.EleConnector;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * shows elevation constraints\n */\npublic class EleConstraintDebugView extends DebugView {\n\t\n\tprivate static final Color SAME_ELE_COLOR = WHITE;\n\tprivate static final Color MIN_VDIST_COLOR = PINK;\n\tprivate static final Color SMOOTHNESS = GREEN;\n\t\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows elevation constraints\";\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tprotected void fillTarget(JOGLTarget target) {\n\t\t\n\t\tConstraintSink sink = new ConstraintSink(target);\n\t\t\n\t\tfor (WorldObject worldObject : map.getWorldObjects()) {\n\t\t\tworldObject.defineEleConstraints(sink);\n\t\t}\n\t\t\n\t}\n\t\n\tprivate static class ConstraintSink implements EleConstraintEnforcer {\n\n\t\tprivate final JOGLTarget target;\n\t\t\t\t\n\t\tprivate ConstraintSink(JOGLTarget target) {\n\t\t\tthis.target = target;\n\t\t}\n\n\t\t@Override\n\t\tpublic void addConnectors(Iterable<EleConnector> connectors) {\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void requireSameEle(EleConnector c1, EleConnector c2) {\n\t\t\tif (!c1.getPosXYZ().equals(c2.getPosXYZ())) {\n\t\t\t\ttarget.drawLineStrip(SAME_ELE_COLOR, 2, c1.getPosXYZ(), c2.getPosXYZ());\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic void requireSameEle(Iterable<EleConnector> cs) {\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\t\tEleConnector upper, EleConnector lower) {\n\t\t\t\n\t\t\tif (upper == null || lower == null) {\n\t\t\t\t//TODO this should not happen\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t\ttarget.drawLineStrip(MIN_VDIST_COLOR, 2,\n\t\t\t\t\tlower.getPosXYZ(),\n\t\t\t\t\tupper.getPosXYZ());\n\t\t\t\n\t\t\tdrawArrow(target, MIN_VDIST_COLOR,\n\t\t\t\t\t(float) (distance / 2),\n\t\t\t\t\tlower.getPosXYZ(),\n\t\t\t\t\tlower.getPosXYZ().addY(distance));\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void requireVerticalDistance(ConstraintType type, double distance,\n\t\t\t\tEleConnector upper, EleConnector base1, EleConnector base2) {\n\n\t\t\tif (upper == null || base1 == null || base2 == null) {\n\t\t\t\t//TODO this should not happen\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t\ttarget.drawLineStrip(MIN_VDIST_COLOR, 2,\n\t\t\t\t\tbase1.getPosXYZ(),\n\t\t\t\t\tbase2.getPosXYZ());\n\t\t\t\n\t\t\tdouble dist1 = base1.pos.distanceTo(upper.pos);\n\t\t\tdouble dist2 = base2.pos.distanceTo(upper.pos);\n\t\t\t\n\t\t\tVectorXYZ base = interpolateBetween(\n\t\t\t\t\tbase1.getPosXYZ(),\n\t\t\t\t\tbase2.getPosXYZ(),\n\t\t\t\t\tdist1 / (dist1 + dist2));\n\t\t\t\n\t\t\ttarget.drawLineStrip(MIN_VDIST_COLOR, 2,\n\t\t\t\t\tbase,\n\t\t\t\t\tupper.getPosXYZ());\n\t\t\t\n\t\t\tdrawArrow(target, MIN_VDIST_COLOR,\n\t\t\t\t\t(float) (distance / 2),\n\t\t\t\t\tbase,\n\t\t\t\t\tbase.addY(distance));\n\t\t\t\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void requireIncline(ConstraintType type, double incline,\n\t\t\t\tList<EleConnector> cs) {\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void requireSmoothness(\n\t\t\t\tEleConnector from, EleConnector via, EleConnector to) {\n\t\t\t\n\t\t\tVectorXYZ v = via.getPosXYZ();\n\t\t\t\n\t\t\tVectorXYZ vToFrom = from.getPosXYZ().subtract(v).normalize();\n\t\t\tVectorXYZ vToTo = to.getPosXYZ().subtract(v).normalize();\n\t\t\t\n\t\t\ttarget.drawLineStrip(SMOOTHNESS, 3,\n\t\t\t\t\tv.add(vToFrom.mult(2)),\n\t\t\t\t\tv,\n\t\t\t\t\tv.add(vToTo.mult(2)));\n\t\t\t\n\t\t}\n\n\t\t@Override\n\t\tpublic void enforceConstraints() {\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/EleDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static java.lang.Math.*;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.target.jogl.JOGLTarget;\n\n/**\n * view that shows points with a color depending on their elevation\n */\npublic class EleDebugView extends DebugView {\n\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows points with a color depending on their elevation\";\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn false;\n\t}\n\t\n\t@Override\n\tprotected void fillTarget(JOGLTarget target) {\n\t\t\n\t\t//TODO port functionality to new elevation calculation\n\t\t\n//\t\t/* collect all points */\n//\n//\t\tList<VectorXYZ> points = new ArrayList<VectorXYZ>();\n//\n//\t\tfor (TerrainPatch patch : terrain.getPatches()) {\n//\t\t\tfor (TriangleXYZ t : patch.getTriangulation()) {\n//\t\t\t\tpoints.addAll(t.getVertices());\n//\t\t\t}\n//\t\t}\n//\n//\t\tfor (MapElement element : map.getMapElements()) {\n//\t\t\tif (element.getElevationProfile() != null) {\n//\t\t\t\tpoints.addAll(element.getElevationProfile().getPointsWithEle());\n//\t\t\t}\n//\t\t}\n//\n//\t\t/* determine minimum and maximum elevations */\n//\n//\t\tdouble minEle = Double.POSITIVE_INFINITY;\n//\t\tdouble maxEle = Double.NEGATIVE_INFINITY;\n//\n//\t\tfor (VectorXYZ point : points) {\n//\t\t\tminEle = min(minEle, point.y);\n//\t\t\tmaxEle = max(maxEle, point.y);\n//\t\t}\n//\n//\t\t/* choose a range for distributing the colors */\n//\n//\t\tdouble eleRange = max (maxEle - minEle, 10);\n//\n//\t\t/* draw the points */\n//\n//\t\tfor (VectorXYZ point : points) {\n//\n//\t\t\tdouble colorGradientValue = (point.y - minEle) / eleRange;\n//\n//\t\t\tColor color = interpolateGradientColor((float)colorGradientValue,\n//\t\t\t\t\tColor.GREEN, Color.YELLOW, Color.RED);\n//\n//\t\t\tdrawBoxAround(target, point, color, 0.4f);\n//\n//\t\t}\n\t\t\n\t}\n\n\t/**\n\t * chooses a color value from a sequence of linear color gradients\n\t */\n\tprivate Color interpolateGradientColor(float value, Color... colors) {\n\n\t\tassert colors.length > 1;\n\t\t\n\t\tint sections = colors.length - 1;\n\t\tfloat sectionLength = 1.0f / sections;\n\t\t\n\t\tint sectionOfValue = min(max(\n\t\t\t\t(int)( value / sectionLength ), 0), sections-1);\n\t\t\t\t\n\t\treturn interpolateGradientColor(\n\t\t\t\t (value - sectionLength * sectionOfValue) / sectionLength,\n\t\t\t\t colors[sectionOfValue], colors[sectionOfValue+1]);\n\t\t\t\t\n\t}\n\t\n\t/**\n\t * chooses a color value from linear color gradient\n\t */\n\tprivate Color interpolateGradientColor(float value, Color c0, Color c1) {\n\t\t\n\t\treturn new Color(\n\t\t\t\tround(c0.getRed() * (1 - value) + c1.getRed() * value),\n\t\t\t\tround(c0.getGreen() * (1 - value) + c1.getGreen() * value),\n\t\t\t\tround(c0.getBlue() * (1 - value) + c1.getBlue() * value));\n\t\t\n\t}\n\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/FaceDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.TargetUtil;\nimport org.osm2world.core.target.common.FaceTarget;\nimport org.osm2world.core.target.common.RenderableToFaceTarget;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.world.data.WorldObject;\n\n/**\n * shows decomposition of {@link WorldObject}s into faces\n * as they would be written to any {@link FaceTarget}\n */\npublic class FaceDebugView extends DebugView {\n\t\n\tprivate static final Color BORDER_COLOR = new Color(0, 0, 1.0f);\n\t\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows decomposition of WorldObjects into faces\";\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\tprivate static class FaceSink\n\t\textends FaceTarget<RenderableToFaceTarget> {\n\n\t\tpublic final List<List<VectorXYZ>> faces =\n\t\t\t\tnew ArrayList<List<VectorXYZ>>();\n\t\t\n\t\t@Override\n\t\tpublic Class<RenderableToFaceTarget> getRenderableType() {\n\t\t\treturn RenderableToFaceTarget.class;\n\t\t}\n\n\t\t@Override\n\t\tpublic void render(RenderableToFaceTarget renderable) {\n\t\t\trenderable.renderTo(this);\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean reconstructFaces() {\n\t\t\treturn true;\n\t\t}\n\t\t\n\t\t@Override\n\t\tpublic void drawFace(Material material, List<VectorXYZ> vs,\n\t\t\t\tList<VectorXYZ> normals, List<List<VectorXZ>> texCoordLists) {\n\t\t\tfaces.add(vs);\n\t\t}\n\t\t\n\t}\n\t\n\t@Override\n\tprotected void fillTarget(JOGLTarget target) {\n\t\t\n\t\tFaceSink faceSink = new FaceSink();\n\t\t\n\t\tTargetUtil.renderWorldObjects(faceSink, map, true);\n\t\t\n\t\tfaceSink.finish();\n\t\t\n\t\tfor (List<VectorXYZ> face : faceSink.faces) {\n\t\t\ttarget.drawLineLoop(BORDER_COLOR, 2, face);\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/HelpView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\n\nimport javax.media.opengl.GL;\n\nimport org.osm2world.core.GlobalValues;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.Projection;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.viewer.view.TextRenderer;\nimport org.osm2world.viewer.view.TextRendererFixedFunction;\nimport org.osm2world.viewer.view.TextRendererShader;\n\n/**\n * view that prints an explanation how to open OSM data.\n * Only visible when no data is available.\n */\npublic class HelpView extends DebugView {\n\t\n\tTextRenderer textRenderer;\n\t\n\t@Override\n\tpublic void reset() {\n\t\tsuper.reset();\n\t\ttextRenderer.destroy();\n\t\ttextRenderer = null;\n\t}\n\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map == null;\n\t}\n\t\n\t@Override\n\tpublic void renderTo(GL gl, Camera camera, Projection projection) {\n\t\t\n\t\tif (!canBeUsed()) { return; }\n\t\tif (textRenderer == null) {\n\t\t\tif (\"shader\".equals(config.getString(\"joglImplementation\"))) {\n\t\t\t\ttextRenderer = new TextRendererShader(gl.getGL2ES2());\n\t\t\t} else {\n\t\t\t\ttextRenderer = new TextRendererFixedFunction();\n\t\t\t}\n\t\t}\n\t\t\n\t\t//TODO: needs real panel measures; currently guesses 800x600\n\t\ttextRenderer.drawText(\"Use \\\"File\\\" > \\\"Open OSM file\\\" \"\n\t\t\t\t+ \"to load a file containing OpenStreetMap data.\",\n\t\t\t\t50, 550, 800, 600, Color.LIGHT_GRAY);\n\t\t\n\t\ttextRenderer.drawText(\"This is OSM2World \" + GlobalValues.VERSION_STRING,\n\t\t\t\t50, 100, 800, 600, Color.LIGHT_GRAY);\n\t\ttextRenderer.drawText(\"Website: \" + GlobalValues.OSM2WORLD_URI,\n\t\t\t\t50, 75, 800, 600, Color.LIGHT_GRAY);\n\t\ttextRenderer.drawText(\"Usage instructions: \" + GlobalValues.WIKI_URI,\n\t\t\t\t50, 50, 800, 600, Color.LIGHT_GRAY);\n\t\t\n\t}\n\t\n\t@Override\n\tprotected void fillTarget(JOGLTarget target) {\n\t\t//do nothing, has its own renderTo implementation\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/InternalCoordsDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static java.awt.Color.*;\nimport static java.lang.Math.*;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\n\n/**\n * shows the internal world coordinate grid\n */\npublic class InternalCoordsDebugView extends DebugView {\n\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows the internal world coordinate grid\";\n\t}\n\t\n\tprivate static final double LINE_DIST = 100;\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\n\t\tAxisAlignedBoundingBoxXZ bound = map.getDataBoundary();\n\t\t\n\t\tfor (int x = (int)floor(bound.minX / LINE_DIST); x < (int)ceil(bound.maxX / LINE_DIST); x++) {\n\t\t\tfor (int z = (int)floor(bound.minZ / LINE_DIST); z < (int)ceil(bound.maxZ / LINE_DIST); z++) {\n\t\t\t\t\n\t\t\t\tColor colorX = (z == 0 && x >= 0) ? RED : WHITE;\n\t\t\t\tint widthX = (z == 0) ? 3 : 1;\n\t\t\t\ttarget.drawLineStrip(colorX, widthX,\n\t\t\t\t\t\tnew VectorXYZ(x * LINE_DIST, 0, z * LINE_DIST),\n\t\t\t\t\t\tnew VectorXYZ((x+1) * LINE_DIST, 0, z * LINE_DIST));\n\n\t\t\t\tColor colorZ = (x == 0 && z >= 0) ? BLUE : WHITE;\n\t\t\t\tint widthZ = (x == 0) ? 3 : 1;\n\t\t\t\ttarget.drawLineStrip(colorZ, widthZ,\n\t\t\t\t\t\tnew VectorXYZ(x * LINE_DIST, 0, z * LINE_DIST),\n\t\t\t\t\t\tnew VectorXYZ(x * LINE_DIST, 0, (z+1) * LINE_DIST));\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t\ttarget.drawLineStrip(GREEN, 3,\n\t\t\t\tVectorXYZ.NULL_VECTOR, new VectorXYZ(0, LINE_DIST, 0));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/InverseDistanceWeightingInterpolatorDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport org.osm2world.core.map_elevation.creation.InverseDistanceWeightingInterpolator;\nimport org.osm2world.viewer.model.RenderOptions;\n\n\npublic class InverseDistanceWeightingInterpolatorDebugView extends TerrainInterpolatorDebugView {\n\t\n\tpublic InverseDistanceWeightingInterpolatorDebugView(RenderOptions renderOptions) {\n\t\tsuper(renderOptions);\n\t}\n\t\n\t@Override\n\tprotected InverseDistanceWeightingInterpolator buildInterpolator() {\n\t\treturn new InverseDistanceWeightingInterpolator(1);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/LatLonDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static java.awt.Color.WHITE;\nimport static java.lang.Math.*;\n\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.map_data.creation.MapProjection;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\n\n/**\n * shows the latitude and longitude grid.\n */\npublic class LatLonDebugView extends DebugView {\n\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows the latitude and longitude grid\";\n\t}\n\t\n\tprivate static final double LINE_DIST = 1.0 / 3600;\n\t\n\tprivate MapProjection mapProjection = null;\n\t\n\t@Override\n\tpublic void setConversionResults(Results conversionResults) {\n\t\tsuper.setConversionResults(conversionResults);\n\t\tmapProjection = conversionResults.getMapProjection();\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null && mapProjection != null;\n\t}\n\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\n\t\tAxisAlignedBoundingBoxXZ bound = map.getDataBoundary();\n\t\t\n\t\tdouble minLon = toDegrees(mapProjection.calcLon(new VectorXZ(bound.minX, bound.minZ)));\n\t\tdouble minLat = toDegrees(mapProjection.calcLat(new VectorXZ(bound.minX, bound.minZ)));\n\t\tdouble maxLon = toDegrees(mapProjection.calcLon(new VectorXZ(bound.maxX, bound.maxZ)));\n\t\tdouble maxLat = toDegrees(mapProjection.calcLat(new VectorXZ(bound.maxX, bound.maxZ)));\n\t\t\n\t\tfor (int x = (int)floor(minLon / LINE_DIST); x < (int)ceil(maxLon / LINE_DIST); x++) {\n\t\t\tfor (int z = (int)floor(minLat / LINE_DIST); z < (int)ceil(maxLat / LINE_DIST); z++) {\n\t\t\t\t\n\t\t\t\tint widthLat = (z % 3600 == 0) ? 6\n\t\t\t\t\t\t: (z % 60 == 0) ? 3 : 1;\n\t\t\t\t\n\t\t\t\tif (widthLat > 1 || camera.getPos().y < 1000) {\n\t\t\t\t\ttarget.drawLineStrip(WHITE, widthLat,\n\t\t\t\t\t\t\tmapProjection.calcPos(z * LINE_DIST, x * LINE_DIST).xyz(0),\n\t\t\t\t\t\t\tmapProjection.calcPos(z * LINE_DIST, (x+1) * LINE_DIST).xyz(0));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tint widthLon = (x % 3600 == 0) ? 6\n\t\t\t\t\t\t: (x % 60 == 0) ? 3 : 1;\n\t\t\t\t\n\t\t\t\tif (widthLon > 1 || camera.getPos().y < 1000) {\n\t\t\t\t\ttarget.drawLineStrip(WHITE, widthLon,\n\t\t\t\t\t\t\tmapProjection.calcPos(z * LINE_DIST, x * LINE_DIST).xyz(0),\n\t\t\t\t\t\t\tmapProjection.calcPos((z+1) * LINE_DIST, x * LINE_DIST).xyz(0));\n\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/LeastSquaresInterpolatorDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport org.osm2world.core.map_elevation.creation.LeastSquaresInterpolator;\nimport org.osm2world.viewer.model.RenderOptions;\n\n\npublic class LeastSquaresInterpolatorDebugView extends TerrainInterpolatorDebugView {\n\n\tpublic LeastSquaresInterpolatorDebugView(RenderOptions renderOptions) {\n\t\tsuper(renderOptions);\n\t}\n\t\n\t@Override\n\tprotected LeastSquaresInterpolator buildInterpolator() {\n\t\treturn new LeastSquaresInterpolator();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/LinearInterpolatorDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport org.osm2world.core.map_elevation.creation.LinearInterpolator;\nimport org.osm2world.viewer.model.RenderOptions;\n\npublic class LinearInterpolatorDebugView extends TerrainInterpolatorDebugView {\n\t\n\tpublic LinearInterpolatorDebugView(RenderOptions renderOptions) {\n\t\tsuper(renderOptions);\n\t}\n\t\n\t@Override\n\tprotected LinearInterpolator buildInterpolator() {\n\t\treturn new LinearInterpolator();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/Map2dTreeDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.map_data.creation.index.Map2dTree;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\n\npublic class Map2dTreeDebugView extends DebugView {\n\t\n\tprivate RenderableMap2dTree map2dTree;\n\t\n\t@Override\n\tpublic void setConversionResults(Results conversionResults) {\n\t\tsuper.setConversionResults(conversionResults);\n\t\tthis.map2dTree = null;\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\n\t\tif (map2dTree == null) {\n\t\t\tmap2dTree = new RenderableMap2dTree();\n\t\t\tfor (MapElement e : map.getMapElements()) {\n\t\t\t\tmap2dTree.insert(e);\n\t\t\t}\n\t\t}\n\t\t\n\t\tmap2dTree.renderTo(target);\n\t\t\n\t}\n\t\n\tprivate class RenderableMap2dTree extends Map2dTree {\n\n\t\tpublic RenderableMap2dTree() {\n\t\t\tsuper(map.getDataBoundary());\n\t\t}\n\n\t\tpublic void renderTo(JOGLTarget target) {\n\t\t\t\n\t\t\trenderNodeTo(this.root, target,\n\t\t\t\t\tmap.getDataBoundary().minX - 10,\n\t\t\t\t\tmap.getDataBoundary().maxX + 10,\n\t\t\t\t\tmap.getDataBoundary().minZ - 10,\n\t\t\t\t\tmap.getDataBoundary().maxZ + 10,\n\t\t\t\t\t1);\n\t\t\t\n\t\t}\n\n\t\tprivate void renderNodeTo(Node node, JOGLTarget target,\n\t\t\t\tdouble minX, double maxX,\n\t\t\t\tdouble minZ, double maxZ,\n\t\t\t\tint depth) {\n\t\t\t\n\t\t\tif (node instanceof InnerNode) {\n\t\t\t\t\n\t\t\t\tInnerNode innerNode = (InnerNode)node;\n\t\t\t\t\n\t\t\t\tColor lineColor = new Color(1.0f,\n\t\t\t\t\t\tMath.min(1.0f, 2.0f / depth), 0.0f);\n\t\t\t\t\n\t\t\t\tif (innerNode.splitAlongX) {\n\t\t\t\t\t\n\t\t\t\t\ttarget.drawLineStrip(lineColor, 1,\n\t\t\t\t\t\t\tnew VectorXYZ(innerNode.splitValue, 0, minZ),\n\t\t\t\t\t\t\tnew VectorXYZ(innerNode.splitValue, 0, maxZ));\n\t\t\t\t\t\n\t\t\t\t\trenderNodeTo(innerNode.lowerChild, target,\n\t\t\t\t\t\t\tminX, innerNode.splitValue,\n\t\t\t\t\t\t\tminZ, maxZ,\n\t\t\t\t\t\t\tdepth + 1);\n\n\t\t\t\t\trenderNodeTo(innerNode.upperChild, target,\n\t\t\t\t\t\t\tinnerNode.splitValue, maxX,\n\t\t\t\t\t\t\tminZ, maxZ,\n\t\t\t\t\t\t\tdepth + 1);\n\t\t\t\t\t\n\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\ttarget.drawLineStrip(lineColor, 1,\n\t\t\t\t\t\t\tnew VectorXYZ(minX, 0, innerNode.splitValue),\n\t\t\t\t\t\t\tnew VectorXYZ(maxX, 0, innerNode.splitValue));\n\t\t\t\t\t\n\t\t\t\t\trenderNodeTo(innerNode.lowerChild, target,\n\t\t\t\t\t\t\tminX, maxX,\n\t\t\t\t\t\t\tminZ, innerNode.splitValue,\n\t\t\t\t\t\t\tdepth + 1);\n\n\t\t\t\t\trenderNodeTo(innerNode.upperChild, target,\n\t\t\t\t\t\t\tminX, maxX,\n\t\t\t\t\t\t\tinnerNode.splitValue, maxZ,\n\t\t\t\t\t\t\tdepth + 1);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/MapDataBoundsDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\nimport java.util.List;\n\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\n\n/**\n * shows the bounding boxes of map data\n */\npublic class MapDataBoundsDebugView extends DebugView {\n\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows the bounding boxes of map data\";\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\tprivate static final Color DATA_BB_COLOR = Color.YELLOW;\n\tprivate static final Color FILE_BB_COLOR = Color.GREEN;\n\t\t\n\t@Override\n\tprotected void fillTarget(JOGLTarget target) {\n\t\t\n\t\tList<VectorXZ> vs = map.getDataBoundary().polygonXZ().getVertexLoop();\n\t\ttarget.drawLineLoop(DATA_BB_COLOR, 1, VectorXZ.listXYZ(vs, 0));\n\n\t\tvs = map.getBoundary().polygonXZ().getVertexLoop();\n\t\ttarget.drawLineLoop(FILE_BB_COLOR, 1, VectorXZ.listXYZ(vs, 0));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/MapDataDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\nimport java.util.Collection;\nimport java.util.Collections;\n\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.map_data.data.overlaps.MapIntersectionWW;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlap;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapAA;\nimport org.osm2world.core.map_data.data.overlaps.MapOverlapWA;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.Vector3D;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.algorithms.TriangulationUtil;\nimport org.osm2world.core.target.common.material.ImmutableMaterial;\nimport org.osm2world.core.target.common.material.Material.Interpolation;\nimport org.osm2world.core.target.jogl.JOGLTarget;\n\n/**\n * shows the plain {@link MapData} as a network of nodes, lines and areas\n */\npublic class MapDataDebugView extends DebugView {\n\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows the map data (without elevation) as a network of nodes, lines and areas\";\n\t}\n\t\n\tprivate static final Color LINE_COLOR = Color.WHITE;\n\tprivate static final Color NODE_COLOR = Color.YELLOW;\n\tprivate static final Color INTERSECTION_COLOR = Color.RED;\n\tprivate static final Color SHARED_SEGMENT_COLOR = Color.ORANGE;\n\tprivate static final Color AREA_COLOR = new Color(0.8f, 0.8f, 1);\n\t\n\tprivate static final float HALF_NODE_WIDTH = 0.4f;\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\n\t\tfor (MapArea area : map.getMapAreas()) {\n\t\t\tVector3D[] vs = new Vector3D[area.getBoundaryNodes().size()];\n\t\t\tfor (int i=0; i < area.getBoundaryNodes().size(); i++) {\n\t\t\t\tvs[i] = area.getBoundaryNodes().get(i).getPos();\n\t\t\t}\n\n\t\t\tCollection<TriangleXZ> triangles =\n\t\t\t\tTriangulationUtil.triangulate(area.getPolygon());\n\t\t\t\n\t\t\tfor (TriangleXZ t : triangles) {\n\t\t\t\ttarget.drawTriangles(\n\t\t\t\t\t\tnew ImmutableMaterial(Interpolation.FLAT, AREA_COLOR),\n\t\t\t\t\t\tCollections.singleton(t.xyz(-0.1)),\n\t\t\t\t\t\tnull);\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tfor (MapWaySegment line : map.getMapWaySegments()) {\n\t\t\tdrawArrow(target, LINE_COLOR, 0.7f,\n\t\t\t\t\tline.getStartNode().getPos().xyz(0),\n\t\t\t\t\tline.getEndNode().getPos().xyz(0));\n\t\t}\n\t\t\n\t\tfor (MapNode node : map.getMapNodes()) {\n\t\t\tdrawBoxAround(target, node.getPos(),\n\t\t\t\t\tNODE_COLOR, HALF_NODE_WIDTH);\n\t\t}\n\t\t\n\t\tfor (MapWaySegment line : map.getMapWaySegments()) {\n\t\t\tfor (MapIntersectionWW intersection : line.getIntersectionsWW()) {\n\t\t\t\tdrawBoxAround(target, intersection.pos,\n\t\t\t\t\t\tINTERSECTION_COLOR, HALF_NODE_WIDTH);\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (MapArea area : map.getMapAreas()) {\n\t\t\tfor (MapOverlap<?, ?> overlap : area.getOverlaps()) {\n\t\t\t\tif (overlap instanceof MapOverlapWA) {\n\t\t\t\t\tfor (VectorXZ pos : ((MapOverlapWA)overlap).getIntersectionPositions()) {\n\t\t\t\t\t\tdrawBoxAround(target, pos,\n\t\t\t\t\t\t\t\tINTERSECTION_COLOR, HALF_NODE_WIDTH);\n\t\t\t\t\t}\n\t\t\t\t\tfor (LineSegmentXZ seg : ((MapOverlapWA)overlap).getSharedSegments()) {\n\t\t\t\t\t\ttarget.drawLineStrip(SHARED_SEGMENT_COLOR, 3, seg.p1.xyz(0), seg.p2.xyz(0));\n\t\t\t\t\t}\n\t\t\t\t\tfor (LineSegmentXZ seg : ((MapOverlapWA)overlap).getOverlappedSegments()) {\n\t\t\t\t\t\ttarget.drawLineStrip(INTERSECTION_COLOR, 3, seg.p1.xyz(0), seg.p2.xyz(0));\n\t\t\t\t\t}\n\t\t\t\t} else if (overlap instanceof MapOverlapAA) {\n\t\t\t\t\tfor (VectorXZ pos : ((MapOverlapAA)overlap).getIntersectionPositions()) {\n\t\t\t\t\t\tdrawBoxAround(target, pos,\n\t\t\t\t\t\t\t\tINTERSECTION_COLOR, HALF_NODE_WIDTH);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/NaturalNeighborInterpolatorDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport org.osm2world.core.map_elevation.creation.NaturalNeighborInterpolator;\nimport org.osm2world.viewer.model.RenderOptions;\n\n\npublic class NaturalNeighborInterpolatorDebugView extends TerrainInterpolatorDebugView {\n\n\tpublic NaturalNeighborInterpolatorDebugView(RenderOptions renderOptions) {\n\t\tsuper(renderOptions);\n\t}\n\t\n\t@Override\n\tprotected NaturalNeighborInterpolator buildInterpolator() {\n\t\treturn new NaturalNeighborInterpolator();\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/NetworkDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.world.data.WorldObject;\nimport org.osm2world.core.world.network.NetworkWaySegmentWorldObject;\n\n/**\n * shows information about {@link NetworkWaySegmentWorldObject}s\n */\npublic class NetworkDebugView extends DebugView {\n\n\tprivate static final Color OFFSET_COLOR = Color.PINK;\n\tprivate static final Color CUT_COLOR = Color.ORANGE;\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\t\t\t\t\n\t\tfor (MapWaySegment line : map.getMapWaySegments()) {\n\t\t\tfor (WorldObject worldObject : line.getRepresentations()) {\n\t\t\t\tif (worldObject instanceof NetworkWaySegmentWorldObject) {\n\t\t\t\t\t\n\t\t\t\t\tNetworkWaySegmentWorldObject representation =\n\t\t\t\t\t\t(NetworkWaySegmentWorldObject) worldObject;\n\n\t\t\t\t\t/* draw lines */\n\n//\t\t\t\t\tutil.drawLineStrip(LINE_COLOR,\n//\t\t\t\t\tline.getStartNode().getPos(), line.getEndNode().getPos());\n\n\t\t\t\t\t/* draw offsets */\n\n\t\t\t\t\tdrawVectorAt(target, OFFSET_COLOR,\n\t\t\t\t\t\t\trepresentation.getStartOffset(), line.getStartNode());\n\n\t\t\t\t\tdrawVectorAt(target, OFFSET_COLOR,\n\t\t\t\t\t\t\trepresentation.getEndOffset(), line.getEndNode());\n\n\t\t\t\t\t/* draw cuts */\n\n\t\t\t\t\tdrawVectorAt(target, CUT_COLOR,\n\t\t\t\t\t\t\trepresentation.getStartCutVector(), line.getStartNode());\n\n\t\t\t\t\tdrawVectorAt(target, CUT_COLOR,\n\t\t\t\t\t\t\trepresentation.getEndCutVector(), line.getEndNode());\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t}\n\t\n\tprivate static void drawVectorAt(JOGLTarget target,\n\t\t\tColor color, VectorXZ vector, MapNode start) {\n\t\t\n\t\tVectorXYZ startV = start.getPos().xyz(0);\n\t\tVectorXYZ endV = startV.add(vector);\n\t\t\n\t\tdrawArrow(target, color, 0.3f * (float)vector.length(), startV, endV);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/OrthoBoundsDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\nimport java.util.List;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.common.rendering.Camera;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil;\nimport org.osm2world.core.target.common.rendering.OrthoTilesUtil.CardinalDirection;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.viewer.control.actions.OrthoBoundsAction;\n\n/**\n * illustrates the construction of the orthogonal perspective\n * as set by {@link OrthoBoundsAction}\n */\npublic class OrthoBoundsDebugView extends DebugView {\n\t\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"illustrates the construction of the orthogonal perspective\";\n\t}\n\t\n\tprivate static final Color LINE_COLOR = Color.YELLOW;\n\tprivate static final Color POINT_COLOR = Color.RED;\n\t\n\tprivate static final float HALF_POINT_WIDTH = 0.4f;\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\t\t\n\t\tCamera orthoCam = OrthoTilesUtil.cameraForBounds(\n\t\t\t\tmap.getDataBoundary(), 30, CardinalDirection.S);\n\t\t\n\t\tList<VectorXYZ> boundVertices = map.getDataBoundary().polygonXZ().xyz(0).getVertices();\n\t\ttarget.drawLineLoop(LINE_COLOR, 1, boundVertices);\n\t\ttarget.drawLineStrip(LINE_COLOR, 1, boundVertices.get(0), boundVertices.get(2));\n\t\ttarget.drawLineStrip(LINE_COLOR, 1, boundVertices.get(1), boundVertices.get(3));\n\t\t\n\t\tdrawBoxAround(target, orthoCam.getPos(), POINT_COLOR, HALF_POINT_WIDTH);\n\t\tdrawBoxAround(target, orthoCam.getLookAt(), POINT_COLOR, HALF_POINT_WIDTH);\n\t\t\n\t\ttarget.drawLineStrip(LINE_COLOR, 1, orthoCam.getPos(), orthoCam.getLookAt());\n\t\t\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/QuadtreeDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static java.lang.Math.min;\nimport static org.osm2world.core.math.VectorXZ.*;\n\nimport java.awt.Color;\nimport java.util.Arrays;\n\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.map_data.creation.index.MapQuadtree;\nimport org.osm2world.core.map_data.creation.index.MapQuadtree.QuadLeaf;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapElement;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.map_data.data.MapWaySegment;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\n\npublic class QuadtreeDebugView extends DebugView {\n\t\n\tprivate static final Color LEAF_BORDER_COLOR = Color.WHITE;\n\tprivate static final Color NODE_ARROW_COLOR = Color.YELLOW;\n\tprivate static final Color LINE_ARROW_COLOR = Color.LIGHT_GRAY;\n\tprivate static final Color AREA_ARROW_COLOR = new Color(0.8f, 0.8f, 1);\n\t\n\tprivate MapQuadtree mapQuadtree;\n\tprivate boolean arrowsEnabled = true;\n\t\n\t@Override\n\tpublic void setConversionResults(Results conversionResults) {\n\t\tsuper.setConversionResults(conversionResults);\n\t\tthis.mapQuadtree = null;\n\t}\n\t\n\tpublic void setArrowsEnabled(boolean arrowsEnabled) {\n\t\tthis.arrowsEnabled = arrowsEnabled;\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\n\t\tif (mapQuadtree == null) {\n\t\t\tmapQuadtree = new MapQuadtree(map.getDataBoundary());\n\t\t\tfor (MapElement e : map.getMapElements()) {\n\t\t\t\tmapQuadtree.insert(e);\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor (QuadLeaf leaf : mapQuadtree.getLeaves()) {\n\t\t\t\n\t\t\t/* draw leaf boundary */\n\t\t\t\n\t\t\tVectorXZ[] vs = new VectorXZ[5];\n\t\t\tvs[0] = new VectorXZ(leaf.minX, leaf.minZ);\n\t\t\tvs[1] = new VectorXZ(leaf.maxX, leaf.minZ);\n\t\t\tvs[2] = new VectorXZ(leaf.maxX, leaf.maxZ);\n\t\t\tvs[3] = new VectorXZ(leaf.minX, leaf.maxZ);\n\t\t\tvs[4] = vs[0];\n\t\t\ttarget.drawLineStrip(LEAF_BORDER_COLOR, 1,\n\t\t\t\t\tlistXYZ(Arrays.asList(vs),0));\n\t\t\t\n\t\t\tif (arrowsEnabled) {\n\t\t\t\t\n\t\t\t\t/* draw arrows from leaf center to elements */\n\t\t\t\t\n\t\t\t\tVectorXZ leafCenter = new VectorXZ(\n\t\t\t\t\t\t(leaf.minX + leaf.maxX)/2,\n\t\t\t\t\t\t(leaf.minZ + leaf.maxZ)/2);\n\t\t\t\t\n\t\t\t\tfor (MapElement e : leaf) {\n\t\t\t\t\t\n\t\t\t\t\tif (e instanceof MapNode) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tVectorXZ nodePos = ((MapNode)e).getPos();\n\t\t\t\t\t\t\n\t\t\t\t\t\tdrawArrow(target, NODE_ARROW_COLOR,\n\t\t\t\t\t\t\t\t(float) min(1,\n\t\t\t\t\t\t\t\t\t\tdistance(leafCenter, nodePos) * 0.3),\n\t\t\t\t\t\t\t\tleafCenter.xyz(0), nodePos.xyz(0));\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (e instanceof MapWaySegment) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tVectorXZ lineStart =\n\t\t\t\t\t\t\t((MapWaySegment)e).getStartNode().getPos();\n\t\t\t\t\t\tVectorXZ lineEnd =\n\t\t\t\t\t\t\t((MapWaySegment)e).getEndNode().getPos();\n\t\t\t\t\t\t\n\t\t\t\t\t\tVectorXZ lineCenter = lineStart.add(lineEnd).mult(0.5f);\n\t\t\t\t\t\t\n\t\t\t\t\t\tfloat headLength = (float)\n\t\t\t\t\t\t\t\tmin(1, distance(leafCenter, lineCenter) * 0.3);\n\t\t\t\t\t\tdrawArrow(target,\n\t\t\t\t\t\t\t\tLINE_ARROW_COLOR, headLength,\n\t\t\t\t\t\t\t\tleafCenter.xyz(0), lineCenter.xyz(0));\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (e instanceof MapArea) {\n\t\t\t\t\t\t\n\t\t\t\t\t\tVectorXZ areaCenter =\n\t\t\t\t\t\t\t((MapArea)e).getOuterPolygon().getCenter();\n\t\t\t\t\t\t\n\t\t\t\t\t\tfloat headLength = (float)\n\t\t\t\t\t\t\t\tmin(1, distance(leafCenter, areaCenter) * 0.3);\n\t\t\t\t\t\tdrawArrow(target,\n\t\t\t\t\t\t\t\tAREA_ARROW_COLOR, headLength,\n\t\t\t\t\t\t\t\tleafCenter.xyz(0), areaCenter.xyz(0));\n\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/RoofDataDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.PolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.world.modules.BuildingModule.Building;\nimport org.osm2world.core.world.modules.BuildingModule.BuildingPart;\nimport org.osm2world.core.world.modules.BuildingModule.BuildingPart.HeightfieldRoof;\n\npublic class RoofDataDebugView extends DebugView {\n\n\tprivate static final Color INNER_POINT_COLOR = Color.YELLOW;\n\tprivate static final Color INNER_SEGMENT_COLOR = Color.GREEN;\n\tprivate static final Color POLYGON_COLOR = Color.BLUE;\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\n\t\tfor (Building building : map.getWorldObjects(Building.class)) {\n\t\t\tfor (BuildingPart part : building.getParts()) {\n\t\t\t\n\t\t\t\tif (!(part.getRoof() instanceof HeightfieldRoof)) return;\n\t\t\t\t\n\t\t\t\tHeightfieldRoof roofData = (HeightfieldRoof)part.getRoof();\n\t\t\t\t\n\t\t\t\tfor (PolygonXZ polygon : roofData.getPolygon().getPolygons()) {\n\t\t\t\t\tfor (VectorXZ v : polygon.getVertices()) {\n\t\t\t\t\t\tdrawBoxAround(target, v, POLYGON_COLOR, 0.3f);\n\t\t\t\t\t}\n\t\t\t\t\tfor (LineSegmentXZ s : polygon.getSegments()) {\n\t\t\t\t\t\ttarget.drawLineStrip(POLYGON_COLOR, 1, s.p1.xyz(0), s.p2.xyz(0));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tfor (VectorXZ v : roofData.getInnerPoints()) {\n\t\t\t\t\tdrawBoxAround(target, v, INNER_POINT_COLOR, 0.5f);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tfor (LineSegmentXZ s : roofData.getInnerSegments()) {\n\t\t\t\t\ttarget.drawLineStrip(INNER_SEGMENT_COLOR, 1, s.p1.xyz(0), s.p2.xyz(0));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\t\t\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/ShadowView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static org.osm2world.core.target.jogl.JOGLRenderingParameters.Winding.CCW;\n\nimport org.osm2world.core.target.TargetUtil;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.jogl.JOGLRenderingParameters;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.target.jogl.JOGLTargetShader;\nimport org.osm2world.viewer.model.RenderOptions;\n\npublic class ShadowView extends DebugView {\n\t\n\tprivate final RenderOptions renderOptions;\n\t\n\tpublic ShadowView(RenderOptions renderOptions) {\n\t\tthis.renderOptions = renderOptions;\n\t}\n\t\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows the world from the perspective of the light\";\n\t};\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tprotected void fillTarget(final JOGLTarget target) {\n\t\tif (target instanceof JOGLTargetShader) {\n\t\t\tsetParameters(target);\n\t\t\ttarget.setXZBoundary(map.getBoundary());\n\t\t\t((JOGLTargetShader)target).setShowShadowPerspective(true);\n\t\t\n\t\t\tboolean underground = config.getBoolean(\"renderUnderground\", true);\n\t\t\n\t\t\tTargetUtil.renderWorldObjects(target, map, underground);\n\t\t}\n\t}\n\t\n\t@Override\n\tprotected void updateTarget(JOGLTarget target, boolean viewChanged) {\n\t\tsetParameters(target);\n\t}\n\t\n\tprivate void setParameters(final JOGLTarget target) {\n\n\t\tboolean drawBoundingBox = config.getBoolean(\"drawBoundingBox\", false);\n\t\tboolean shadowVolumes = \"shadowVolumes\".equals(config.getString(\"shadowImplementation\"))\n\t\t\t\t|| \"both\".equals(config.getString(\"shadowImplementation\"));\n\t\tboolean shadowMaps = \"shadowMap\".equals(config.getString(\"shadowImplementation\"))\n\t\t\t\t|| \"both\".equals(config.getString(\"shadowImplementation\"));\n\t\tint shadowMapWidth = config.getInt(\"shadowMapWidth\", 4096);\n\t\tint shadowMapHeight = config.getInt(\"shadowMapHeight\", 4096);\n\t\tint shadowMapCameraFrustumPadding = config.getInt(\"shadowMapCameraFrustumPadding\", 8);\n\t\ttarget.setRenderingParameters(new JOGLRenderingParameters(\n\t\t\t\trenderOptions.isBackfaceCulling() ? CCW : null,\n    \t\t\trenderOptions.isWireframe(), true, drawBoundingBox, shadowVolumes, shadowMaps, shadowMapWidth, shadowMapHeight, \n    \t\t\tshadowMapCameraFrustumPadding, false, 0, 0, false));\n\t\t\n\t\ttarget.setGlobalLightingParameters(GlobalLightingParameters.DEFAULT);\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/SkyboxView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static java.lang.Math.sqrt;\n\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.material.Materials;\nimport org.osm2world.core.target.jogl.JOGLRenderingParameters;\nimport org.osm2world.core.target.jogl.JOGLTarget;\n\npublic class SkyboxView extends DebugView {\n\t\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows a skybox in the background\";\n\t};\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tprotected void fillTarget(JOGLTarget target) {\n\t\t\n\t\ttarget.setGlobalLightingParameters(GlobalLightingParameters.DEFAULT);\n\t\t\n\t\t// disable backface culling\n\t\ttarget.setRenderingParameters(\n\t\t\t\tnew JOGLRenderingParameters(null, false, true));\n\t\t\n\t\t// draw the skybox close to the limits of the viewing distance\n\t\tdouble skyboxSize = 1.95 * projection.getFarClippingDistance() / sqrt(3);\n\t\t\n\t\ttarget.drawBox(Materials.SKYBOX,\n\t\t\t\tcamera.getPos().add(0, -skyboxSize / 2, 0),\n\t\t\t\tVectorXZ.Z_UNIT, skyboxSize, skyboxSize, skyboxSize);\n\t\t\n\t\ttarget.finish();\n\t\t\n\t}\n\n\t@Override\n\tprotected void updateTarget(JOGLTarget target, boolean viewChanged) {\n\t\tif (viewChanged) {\n\t\t\ttarget.reset();\n\t\t\tfillTarget(target);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/TerrainBoundaryAABBDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.PolygonXZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\n\n/**\n * shows the axis-aligned bounding boxes of the terrain boundaries\n */\npublic class TerrainBoundaryAABBDebugView extends DebugView {\n\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows the axis-aligned bounding boxes of the terrain boundaries\";\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\tprivate static final Color BB_COLOR = Color.RED;\n\t\t\n\t@Override\n\tprotected void fillTarget(JOGLTarget target) {\n\n\t\tfor (TerrainBoundaryWorldObject tb :\n\t\t\tmap.getWorldObjects(TerrainBoundaryWorldObject.class)) {\n\t\t\t\n\t\t\tAxisAlignedBoundingBoxXZ box = tb.getAxisAlignedBoundingBoxXZ();\n\t\t\tif (box != null) {\n\t\t\t\tPolygonXZ polygon = box.polygonXZ();\n\t\t\t\ttarget.drawLineLoop(BB_COLOR, 1, polygon.xyz(0).getVertices());\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/TerrainBoundaryDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.map_elevation.data.GroundState;\nimport org.osm2world.core.math.PolygonXYZ;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.world.data.AreaWorldObject;\nimport org.osm2world.core.world.data.TerrainBoundaryWorldObject;\nimport org.osm2world.core.world.data.WaySegmentWorldObject;\n\n/**\n * draws terrain boundaries defined by world objects\n */\npublic class TerrainBoundaryDebugView extends DebugView {\n\n\tprivate static final Color NODE_BOUNDARY_COLOR = Color.YELLOW;\n\tprivate static final Color WAY_BOUNDARY_COLOR = Color.GREEN;\n\tprivate static final Color AREA_BOUNDARY_COLOR = Color.BLUE;\n\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"draws terrain boundaries defined by world objects\";\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\t\t\t\t\n\t\t/* draw terrain boundaries */\n\t\t\n\t\tfor (TerrainBoundaryWorldObject terrainBoundary :\n\t\t\tmap.getWorldObjects(TerrainBoundaryWorldObject.class)) {\n\n\t\t\tif (terrainBoundary.getGroundState() == GroundState.ON) {\n\n\t\t\t\tColor color = NODE_BOUNDARY_COLOR;\n\n\t\t\t\tif (terrainBoundary instanceof WaySegmentWorldObject) {\n\t\t\t\t\tcolor = WAY_BOUNDARY_COLOR;\n\t\t\t\t} else if (terrainBoundary instanceof AreaWorldObject) {\n\t\t\t\t\tcolor = AREA_BOUNDARY_COLOR;\n\t\t\t\t}\n\n\t\t\t\tPolygonXYZ outlinePolygon = terrainBoundary.getOutlinePolygon();\n\t\t\t\tif (outlinePolygon != null) {\n\t\t\t\t\ttarget.drawLineLoop(color, 1, outlinePolygon.getVertices());\n\t\t\t\t}\n\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/TerrainInterpolatorDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static java.util.Arrays.asList;\n\nimport java.awt.Color;\nimport java.io.IOException;\nimport java.util.Collection;\n\nimport org.osm2world.core.ConversionFacade.Results;\nimport org.osm2world.core.map_data.creation.MapProjection;\nimport org.osm2world.core.map_elevation.creation.TerrainInterpolator;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorGridXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.common.material.ImmutableMaterial;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Interpolation;\nimport org.osm2world.core.target.jogl.JOGLRenderingParameters;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.viewer.model.RenderOptions;\n\npublic abstract class TerrainInterpolatorDebugView extends DebugView {\n\n\tprotected abstract TerrainInterpolator buildInterpolator();\n\t\n\tprivate static final double SAMPLE_DIST = 3;\n\t\n\tprivate static final Material TERRAIN_MAT =\n\t\t\tnew ImmutableMaterial(Interpolation.FLAT, Color.WHITE);\n\n\tprivate final RenderOptions renderOptions;\n\t\n\tprivate MapProjection mapProjection = null;\n\t\n\tprotected TerrainInterpolatorDebugView(RenderOptions renderOptions) {\n\t\tthis.renderOptions = renderOptions;\n\t}\n\t\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows empty terrain approximated by a \" +\n\t\t\t\tbuildInterpolator().getClass().getSimpleName();\n\t}\n\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null && mapProjection != null && eleData != null;\n\t}\n\n\t@Override\n\tpublic void setConversionResults(Results conversionResults) {\n\t\tsuper.setConversionResults(conversionResults);\n\t\tmapProjection = conversionResults.getMapProjection();\n\t}\n\n\t@Override\n\tpublic void fillTarget(JOGLTarget target) {\n\n\t\ttarget.setRenderingParameters(new JOGLRenderingParameters(null,\n    \t\t\trenderOptions.isWireframe(), true));\n\t\t\n\t\ttarget.setGlobalLightingParameters(GlobalLightingParameters.DEFAULT);\n\t\t\n\t\ttry {\n\t\t\t\n\t\t\tCollection<VectorXYZ> sites = eleData.getSites(map);\n\t\t\t\n\t\t\tTerrainInterpolator strategy = buildInterpolator();\n\t\t\tstrategy.setKnownSites(sites);\n\t\t\t\n\t\t\tAxisAlignedBoundingBoxXZ bound = map.getDataBoundary();\n\t\t\t\n\t\t\tVectorGridXZ sampleGrid = new VectorGridXZ(bound, SAMPLE_DIST);\n\t\t\t\n\t\t\tVectorXYZ[][] samples = new VectorXYZ[sampleGrid.sizeX()][sampleGrid.sizeZ()];\n\t\t\t\n\t\t\tlong startTimeMillis = System.currentTimeMillis();\n\t\t\t\n\t\t\tfor (int x = 0; x < sampleGrid.sizeX(); x++) {\n\t\t\t\tfor (int z = 0; z < sampleGrid.sizeZ(); z++) {\n\t\t\t\t\t\n\t\t\t\t\tsamples[x][z] = strategy.interpolateEle(sampleGrid.get(x, z));\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (x % 100 == 0) {\n\t\t\t\t\tlong finishedSamples = x * sampleGrid.sizeZ();\n\t\t\t\t\tSystem.out.println(finishedSamples + \"/\" + sampleGrid.size()\n\t\t\t\t\t\t\t+ \" after \" + ((System.currentTimeMillis() - startTimeMillis) / 1000f));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* draw surface from samples */\n\t\t\t\n\t\t\tfor (int x = 0; x+1 < samples.length; x++) {\n\t\t\t\tfor (int z = 0; z+1 < samples[x].length; z++) {\n\t\t\t\t\t\n\t\t\t\t\ttarget.drawTriangleFan(TERRAIN_MAT,\n\t\t\t\t\t\t\tasList(samples[x][z], samples[x+1][z],\n\t\t\t\t\t\t\t\t\tsamples[x+1][z+1], samples[x][z+1]),\n\t\t\t\t\t\t\tnull);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t} catch (IOException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t\t\t\t\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/WorldObjectNormalsDebugView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static org.osm2world.core.util.FaultTolerantIterationUtil.iterate;\n\nimport java.awt.Color;\n\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.target.TargetUtil;\nimport org.osm2world.core.target.common.Primitive;\nimport org.osm2world.core.target.common.material.Material;\nimport org.osm2world.core.target.common.material.Material.Interpolation;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.core.target.jogl.PrimitiveBuffer;\nimport org.osm2world.core.util.FaultTolerantIterationUtil.Operation;\nimport org.osm2world.core.world.data.WorldObject;\n\npublic class WorldObjectNormalsDebugView extends DebugView {\n\n\tprivate static final Color FLAT_NORMALS_COLOR = Color.YELLOW;\n\tprivate static final Color SMOOTH_NORMALS_COLOR = Color.ORANGE;\n\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"draws world object normals as arrows\";\n\t}\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tprotected void fillTarget(JOGLTarget target) {\n\t\t\n\t\tfinal PrimitiveBuffer primitiveBuffer = new PrimitiveBuffer();\n\n\t\titerate(map.getWorldObjects(), new Operation<WorldObject>() {\n\t\t\t@Override public void perform(WorldObject w) {\n\t\t\t\tTargetUtil.renderObject(primitiveBuffer, w);\n\t\t\t}\n\t\t});\n\t\t\n\t\tfor (Material material : primitiveBuffer .getMaterials()) {\n\t\t\t\n\t\t\tColor color = material.getInterpolation() == Interpolation.FLAT ?\n\t\t\t\t\tFLAT_NORMALS_COLOR : SMOOTH_NORMALS_COLOR;\n\t\t\t\n\t\t\tfor (Primitive primitive : primitiveBuffer.getPrimitives(material)) {\n\t\t\t\t\n\t\t\t\tfor (int i = 0; i < primitive.vertices.size(); i++) {\n\t\t\t\t\tVectorXYZ v = primitive.vertices.get(i);\n\t\t\t\t\tVectorXYZ n = primitive.normals.get(i);\n\t\t\t\t\tif (n != null) {\n\t\t\t\t\t\tdrawArrow(target, color, 0.3f, v, v.add(n));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/src/org/osm2world/viewer/view/debug/WorldObjectView.java",
    "content": "package org.osm2world.viewer.view.debug;\n\nimport static org.osm2world.core.target.jogl.JOGLRenderingParameters.Winding.CCW;\n\nimport org.osm2world.core.target.TargetUtil;\nimport org.osm2world.core.target.common.lighting.GlobalLightingParameters;\nimport org.osm2world.core.target.jogl.JOGLRenderingParameters;\nimport org.osm2world.core.target.jogl.JOGLTarget;\nimport org.osm2world.viewer.model.RenderOptions;\n\npublic class WorldObjectView extends DebugView {\n\t\n\tprivate final RenderOptions renderOptions;\n\t\n\tpublic WorldObjectView(RenderOptions renderOptions) {\n\t\tthis.renderOptions = renderOptions;\n\t}\n\t\n\t@Override\n\tpublic String getDescription() {\n\t\treturn \"shows the world objects\";\n\t};\n\t\n\t@Override\n\tpublic boolean canBeUsed() {\n\t\treturn map != null;\n\t}\n\t\n\t@Override\n\tprotected void fillTarget(final JOGLTarget target) {\n\t\t\n\t\tsetParameters(target);\n\t\ttarget.setXZBoundary(map.getBoundary());\n\t\t\n\t\tboolean underground = config.getBoolean(\"renderUnderground\", true);\n\t\t\n\t\tTargetUtil.renderWorldObjects(target, map, underground);\n\t\t\n\t}\n\t\n\t@Override\n\tprotected void updateTarget(JOGLTarget target, boolean viewChanged) {\n\t\tsetParameters(target);\n\t}\n\t\n\tprivate void setParameters(final JOGLTarget target) {\n\t\t\n\t\tboolean drawBoundingBox = config.getBoolean(\"drawBoundingBox\", false);\n\t\tboolean shadowVolumes = \"shadowVolumes\".equals(config.getString(\"shadowImplementation\"))\n\t\t\t\t|| \"both\".equals(config.getString(\"shadowImplementation\"));\n\t\tboolean shadowMaps = \"shadowMap\".equals(config.getString(\"shadowImplementation\"))\n\t\t\t\t|| \"both\".equals(config.getString(\"shadowImplementation\"));\n\t\tint shadowMapWidth = config.getInt(\"shadowMapWidth\", 4096);\n\t\tint shadowMapHeight = config.getInt(\"shadowMapHeight\", 4096);\n\t\tint shadowMapCameraFrustumPadding = config.getInt(\"shadowMapCameraFrustumPadding\", 8);\n\t\tboolean useSSAO = \"true\".equals(config.getString(\"useSSAO\"));\n\t\tint SSAOkernelSize = config.getInt(\"SSAOkernelSize\", 16);\n\t\tfloat SSAOradius = config.getFloat(\"SSAOradius\", 1);\n\t\tboolean overwriteProjectionClippingPlanes = \"true\".equals(config.getString(\"overwriteProjectionClippingPlanes\"));\n\t\ttarget.setRenderingParameters(new JOGLRenderingParameters(\n\t\t\t\trenderOptions.isBackfaceCulling() ? CCW : null,\n    \t\t\trenderOptions.isWireframe(), true, drawBoundingBox, shadowVolumes, shadowMaps, shadowMapWidth, shadowMapHeight, \n    \t\t\tshadowMapCameraFrustumPadding, useSSAO, SSAOkernelSize, SSAOradius, overwriteProjectionClippingPlanes));\n\t\t\n\t\ttarget.setGlobalLightingParameters(GlobalLightingParameters.DEFAULT);\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/test/files/coastline_big_island.osm",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' generator='JOSM' upload=\"false\">\n  <bounds minlat='51.4542208' minlon='2.1811295' maxlat='51.5006218' maxlon='2.2549438' origin='CGImap 0.0.2' />\n  <node id='-20' action='modify' visible='true' lat='51.47311905341272' lon='2.257041773728734' version='424242'/>\n  <node id='-18' action='modify' visible='true' lat='51.494418204256185' lon='2.2519704657111537' version='424242'/>\n  <node id='-16' action='modify' visible='true' lat='51.51024371322166' lon='2.235524477915715' version='424242'/>\n  <node id='-14' action='modify' visible='true' lat='51.44678093713967' lon='2.2293171731197643' version='424242'/>\n  <node id='-12' action='modify' visible='true' lat='51.44449688974925' lon='2.198112921702403' version='424242'/>\n  <node id='-10' action='modify' visible='true' lat='51.464461014044' lon='2.182006578380878' version='424242'/>\n  <node id='-8' action='modify' visible='true' lat='51.49595526362061' lon='2.1810918474204604' version='424242'/>\n  <node id='-6' action='modify' visible='true' lat='51.51346470616927' lon='2.1993121755572975' version='424242'/>  \n  <way id='-81' action='modify' visible='true' version='424242'>\n    <nd ref='-6' />\n    <nd ref='-8' />\n    <nd ref='-10' />\n    <nd ref='-12' />\n    <nd ref='-14' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-79' action='modify' visible='true' version='424242'>\n    <nd ref='-14' />\n    <nd ref='-20' />\n    <nd ref='-18' />\n    <nd ref='-16' />\n    <nd ref='-6' />\n    <tag k='natural' v='coastline' />\n  </way>\n</osm>\n"
  },
  {
    "path": "OSM2World/test/files/coastline_islands.osm",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' generator='JOSM' upload=\"false\">\n  <bounds minlat='51.4542208' minlon='2.1811295' maxlat='51.5006218' maxlon='2.2549438' origin='CGImap 0.0.2' />\n  <node id='-34' action='modify' visible='true' lat='51.467417707143206' lon='2.2440354980065793' version='424242' />\n  <node id='-32' action='modify' visible='true' lat='51.464355642319205' lon='2.2414608768726607' version='424242' />\n  <node id='-30' action='modify' visible='true' lat='51.46333490838193' lon='2.2360775781381026' version='424242' />\n  <node id='-28' action='modify' visible='true' lat='51.46493890860393' lon='2.224374754802108' version='424242' />\n  <node id='-26' action='modify' visible='true' lat='51.46712609077473' lon='2.224374754802108' version='424242' />\n  <node id='-24' action='modify' visible='true' lat='51.469896371026934' lon='2.2304602229368253' version='424242' />\n  <node id='-22' action='modify' visible='true' lat='51.47091695819401' lon='2.237950029871862' version='424242' />\n  <node id='-20' action='modify' visible='true' lat='51.47368700827931' lon='2.2150124961333115' version='424242' />\n  <node id='-18' action='modify' visible='true' lat='51.479955447942956' lon='2.2154806090667516' version='424242' />\n  <node id='-16' action='modify' visible='true' lat='51.485057030333095' lon='2.212203818532673' version='424242' />\n  <node id='-14' action='modify' visible='true' lat='51.46712609077473' lon='2.204714011597636' version='424242' />\n  <node id='-12' action='modify' visible='true' lat='51.46756351462852' lon='2.19558580939556' version='424242' />\n  <node id='-10' action='modify' visible='true' lat='51.473832795729905' lon='2.192777131794921' version='424242' />\n  <node id='-8' action='modify' visible='true' lat='51.48287070776205' lon='2.1953517529288398' version='424242' />\n  <node id='-6' action='modify' visible='true' lat='51.48724324808336' lon='2.2021393904637168' version='424242' />\n  <way id='-83' action='modify' visible='true' version='424242'>\n    <nd ref='-22' />\n    <nd ref='-24' />\n    <nd ref='-26' />\n    <nd ref='-28' />\n    <nd ref='-30' />\n    <nd ref='-32' />\n    <nd ref='-34' />\n    <nd ref='-22' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-81' action='modify' visible='true' version='424242'>\n    <nd ref='-6' />\n    <nd ref='-8' />\n    <nd ref='-10' />\n    <nd ref='-12' />\n    <nd ref='-14' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-79' action='modify' visible='true' version='424242'>\n    <nd ref='-14' />\n    <nd ref='-20' />\n    <nd ref='-18' />\n    <nd ref='-16' />\n    <nd ref='-6' />\n    <tag k='natural' v='coastline' />\n  </way>\n</osm>\n"
  },
  {
    "path": "OSM2World/test/files/coastline_islands_and_coast.osm",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' generator='JOSM' upload=\"false\">\n  <bounds minlat='51.4542208' minlon='2.1811295' maxlat='51.5006218' maxlon='2.2549438' origin='CGImap 0.0.2' />\n  <node id='-62' action='modify' visible='true' lat='51.50880831095194' lon='2.2009691081301175' version='424242'/>\n  <node id='-60' action='modify' visible='true' lat='51.495258483064205' lon='2.215948722000191' version='424242'/>\n  <node id='-58' action='modify' visible='true' lat='51.49278119726532' lon='2.2309283358702654' version='424242'/>\n  <node id='-56' action='modify' visible='true' lat='51.48870066835069' lon='2.2393543686721817' version='424242'/>\n  <node id='-54' action='modify' visible='true' lat='51.48549428226875' lon='2.2438014415398597' version='424242'/>\n  <node id='-52' action='modify' visible='true' lat='51.481850388023055' lon='2.2470782320739384' version='424242'/>\n  <node id='-50' action='modify' visible='true' lat='51.47878929183597' lon='2.2517593614083364' version='424242'/>\n  <node id='-48' action='modify' visible='true' lat='51.476019551579675' lon='2.258781055409933' version='424242'/>\n  <node id='-46' action='modify' visible='true' lat='51.48724324808336' lon='2.2021393904637168' version='424242'/>\n  <node id='-44' action='modify' visible='true' lat='51.48287070776205' lon='2.1953517529288398' version='424242'/>\n  <node id='-42' action='modify' visible='true' lat='51.473832795729905' lon='2.192777131794921' version='424242'/>\n  <node id='-40' action='modify' visible='true' lat='51.46756351462852' lon='2.19558580939556' version='424242'/>\n  <node id='-38' action='modify' visible='true' lat='51.46712609077473' lon='2.204714011597636' version='424242'/>\n  <node id='-36' action='modify' visible='true' lat='51.485057030333095' lon='2.212203818532673' version='424242'/>\n  <node id='-34' action='modify' visible='true' lat='51.479955447942956' lon='2.2154806090667516' version='424242'/>\n  <node id='-32' action='modify' visible='true' lat='51.47368700827931' lon='2.2150124961333115' version='424242'/>\n  <node id='-30' action='modify' visible='true' lat='51.47091695819401' lon='2.237950029871862' version='424242'/>\n  <node id='-28' action='modify' visible='true' lat='51.469896371026934' lon='2.2304602229368253' version='424242'/>\n  <node id='-26' action='modify' visible='true' lat='51.46712609077473' lon='2.224374754802108' version='424242'/>\n  <node id='-24' action='modify' visible='true' lat='51.46493890860393' lon='2.224374754802108' version='424242'/>\n  <node id='-22' action='modify' visible='true' lat='51.46333490838193' lon='2.2360775781381026' version='424242'/>\n  <node id='-20' action='modify' visible='true' lat='51.464355642319205' lon='2.2414608768726607' version='424242'/>\n  <node id='-18' action='modify' visible='true' lat='51.467417707143206' lon='2.2440354980065793' version='424242'/>\n  <node id='-16' action='modify' visible='true' lat='51.46138597257318' lon='2.2566457924832286' version='424242'/>\n  <node id='-14' action='modify' visible='true' lat='51.45952986023908' lon='2.2506876875504935' version='424242'/>\n  <node id='-12' action='modify' visible='true' lat='51.455611152972075' lon='2.2450605884473545' version='424242'/>\n  <node id='-10' action='modify' visible='true' lat='51.451898383113374' lon='2.2404265068330043' version='424242'/>\n  <node id='-8' action='modify' visible='true' lat='51.44818531129057' lon='2.237281951451839' version='424242'/>\n  <way id='-117' action='modify' visible='true' version='424242'>\n    <nd ref='-62' />\n    <nd ref='-60' />\n    <nd ref='-58' />\n    <nd ref='-56' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-115' action='modify' visible='true' version='424242'>\n    <nd ref='-38' />\n    <nd ref='-32' />\n    <nd ref='-34' />\n    <nd ref='-36' />\n    <nd ref='-46' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-113' action='modify' visible='true' version='424242'>\n    <nd ref='-46' />\n    <nd ref='-44' />\n    <nd ref='-42' />\n    <nd ref='-40' />\n    <nd ref='-38' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-111' action='modify' visible='true' version='424242'>\n    <nd ref='-30' />\n    <nd ref='-28' />\n    <nd ref='-26' />\n    <nd ref='-24' />\n    <nd ref='-22' />\n    <nd ref='-20' />\n    <nd ref='-18' />\n    <nd ref='-30' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-109' action='modify' visible='true' version='424242'>\n    <nd ref='-16' />\n    <nd ref='-14' />\n    <nd ref='-12' />\n    <nd ref='-10' />\n    <nd ref='-8' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-107' action='modify' visible='true' version='424242'>\n    <nd ref='-56' />\n    <nd ref='-54' />\n    <nd ref='-52' />\n    <nd ref='-50' />\n    <nd ref='-48' />\n    <tag k='natural' v='coastline' />\n  </way>\n</osm>\n"
  },
  {
    "path": "OSM2World/test/files/coastline_multiple_coasts.osm",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' generator='JOSM' upload=\"false\">\n  <bounds minlat='51.4542208' minlon='2.1811295' maxlat='51.5006218' maxlon='2.2549438' origin='CGImap 0.0.2' />\n  <node id='-193' action='modify' visible='true' lat='51.44418555419045' lon='2.2337062533929064' version='424242' />\n  <node id='-191' action='modify' visible='true' lat='51.440100677023764' lon='2.2346424792597865' version='424242' />\n  <node id='-189' action='modify' visible='true' lat='51.437766297455354' lon='2.228791067591789' version='424242' />\n  <node id='-187' action='modify' visible='true' lat='51.4357236174633' lon='2.2234077688572316' version='424242' />\n  <node id='-185' action='modify' visible='true' lat='51.43470224321239' lon='2.2140455101884347' version='424242' />\n  <node id='-183' action='modify' visible='true' lat='51.435139977830545' lon='2.209364380854037' version='424242' />\n  <node id='-181' action='modify' visible='true' lat='51.43703677937289' lon='2.2044491950529186' version='424242' />\n  <node id='-179' action='modify' visible='true' lat='51.440100677023764' lon='2.2037470256527594' version='424242' />\n  <node id='-177' action='modify' visible='true' lat='51.440976038603296' lon='2.208194098520438' version='424242' />\n  <node id='-175' action='modify' visible='true' lat='51.443456138635895' lon='2.2147476795885943' version='424242' />\n  <node id='-173' action='modify' visible='true' lat='51.44754071568309' lon='2.2126411713881153' version='424242' />\n  <node id='-171' action='modify' visible='true' lat='51.456875519876114' lon='2.211704945521236' version='424242' />\n  <node id='-169' action='modify' visible='true' lat='51.45818807363956' lon='2.220833147723312' version='424242' />\n  <node id='-168' action='modify' visible='true' lat='51.4481241968649' lon='2.2315997451924274' version='424242' />\n  <node id='-155' action='modify' visible='true' lat='51.46970775922707' lon='2.231365688725708' version='424242' />\n  <node id='-117' action='modify' visible='true' lat='51.448167028884214' lon='2.1925430753282007' version='424242' />\n  <node id='-34' action='modify' visible='true' lat='51.50443783891431' lon='2.204714011597636' version='424242' />\n  <node id='-32' action='modify' visible='true' lat='51.497298499695276' lon='2.204011842197476' version='424242' />\n  <node id='-30' action='modify' visible='true' lat='51.49234401520801' lon='2.2021393904637168' version='424242' />\n  <node id='-28' action='modify' visible='true' lat='51.48214191027787' lon='2.19558580939556' version='424242' />\n  <node id='-26' action='modify' visible='true' lat='51.47077116142503' lon='2.19558580939556' version='424242' />\n  <node id='-24' action='modify' visible='true' lat='51.44495779312415' lon='2.198394486996199' version='424242' />\n  <node id='-22' action='modify' visible='true' lat='51.446124814042804' lon='2.202841559863877' version='424242' />\n  <node id='-20' action='modify' visible='true' lat='51.451230179888924' lon='2.201437221063557' version='424242' />\n  <node id='-18' action='modify' visible='true' lat='51.44597893805905' lon='2.255738321342574' version='424242' />\n  <node id='-16' action='modify' visible='true' lat='51.50531196685879' lon='2.2260131500691474' version='424242' />\n  <node id='-14' action='modify' visible='true' lat='51.49117817589138' lon='2.235375408737943' version='424242' />\n  <node id='-12' action='modify' visible='true' lat='51.479663911711555' lon='2.2388862557387417' version='424242' />\n  <node id='-10' action='modify' visible='true' lat='51.46275162159544' lon='2.2313964488037046' version='424242' />\n  <node id='-8' action='modify' visible='true' lat='51.452397040501495' lon='2.2388862557387417' version='424242' />\n  <node id='-6' action='modify' visible='true' lat='51.45050087686119' lon='2.2456738932736187' version='424242' />\n  <way id='-170' action='modify' visible='true' version='424242'>\n    <nd ref='-168' />\n    <nd ref='-193' />\n    <nd ref='-191' />\n    <nd ref='-189' />\n    <nd ref='-187' />\n    <nd ref='-185' />\n    <nd ref='-183' />\n    <nd ref='-181' />\n    <nd ref='-179' />\n    <nd ref='-177' />\n    <nd ref='-175' />\n    <nd ref='-173' />\n    <nd ref='-171' />\n    <nd ref='-169' />\n    <nd ref='-168' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-131' action='modify' visible='true' version='424242'>\n    <nd ref='-18' />\n    <nd ref='-6' />\n    <nd ref='-8' />\n    <nd ref='-10' />\n    <nd ref='-155' />\n    <nd ref='-12' />\n    <nd ref='-14' />\n    <nd ref='-16' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-83' action='modify' visible='true' version='424242'>\n    <nd ref='-26' />\n    <nd ref='-20' />\n    <nd ref='-22' />\n    <nd ref='-24' />\n    <nd ref='-117' />\n    <tag k='natural' v='coastline' />\n  </way>\n  <way id='-81' action='modify' visible='true' version='424242'>\n    <nd ref='-34' />\n    <nd ref='-32' />\n    <nd ref='-30' />\n    <nd ref='-28' />\n    <nd ref='-26' />\n    <tag k='natural' v='coastline' />\n  </way>\n</osm>\n"
  },
  {
    "path": "OSM2World/test/files/mp_two_holes.osm",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<osm version=\"0.6\" generator=\"JOSM\" upload=\"false\">\n  <node id=\"-26\" visible=\"true\" lat=\"-0.0758398356392741\" lon=\"0.4919413819744184\" version=\"424242\"/>\n  <node id=\"-24\" visible=\"true\" lat=\"-0.07585509521399153\" lon=\"0.49187212692078847\" version=\"424242\"/>\n  <node id=\"-22\" visible=\"true\" lat=\"-0.07577214573084735\" lon=\"0.4918764309071722\" version=\"424242\"/>\n  <node id=\"-21\" visible=\"true\" lat=\"-0.07578310132296985\" lon=\"0.4919362954450558\" version=\"424242\"/>\n  <node id=\"-18\" visible=\"true\" lat=\"-0.07585705156972414\" lon=\"0.4918549109752532\" version=\"424242\"/>\n  <node id=\"-16\" visible=\"true\" lat=\"-0.07584022691042076\" lon=\"0.4918173489122674\" version=\"424242\"/>\n  <node id=\"-15\" visible=\"true\" lat=\"-0.07577762352690895\" lon=\"0.49183534740078144\" version=\"424242\"/>\n  <node id=\"-12\" visible=\"true\" lat=\"-0.07588170165194749\" lon=\"0.49181343619737306\" version=\"424242\"/>\n  <node id=\"-10\" visible=\"true\" lat=\"-0.07588287546538634\" lon=\"0.4919233834859045\" version=\"424242\"/>\n  <node id=\"-8\" visible=\"true\" lat=\"-0.07580031725344245\" lon=\"0.4920043766842175\" version=\"424242\"/>\n  <node id=\"-6\" visible=\"true\" lat=\"-0.07574827819085664\" lon=\"0.49193903434548186\" version=\"424242\"/>\n  <node id=\"-4\" visible=\"true\" lat=\"-0.07574827819085664\" lon=\"0.4918306521429082\" version=\"424242\"/>\n  <node id=\"-3\" action=\"modify\" visible=\"true\" lat=\"-0.07581557682818\" lon=\"0.4917680487045985\" version=\"424242\"/>\n  <way id=\"-23\" action=\"modify\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-21\"/>\n    <nd ref=\"-22\"/>\n    <nd ref=\"-24\"/>\n    <nd ref=\"-26\"/>\n    <nd ref=\"-21\"/>\n  </way>\n  <way id=\"-17\" action=\"modify\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-15\"/>\n    <nd ref=\"-18\"/>\n    <nd ref=\"-16\"/>\n    <nd ref=\"-15\"/>\n  </way>\n  <way id=\"-5\" action=\"modify\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-3\"/>\n    <nd ref=\"-4\"/>\n    <nd ref=\"-6\"/>\n    <nd ref=\"-8\"/>\n    <nd ref=\"-10\"/>\n    <nd ref=\"-12\"/>\n    <nd ref=\"-3\"/>\n  </way>\n  <relation id=\"-34\" visible=\"true\" version=\"424242\">\n    <member type=\"way\" ref=\"-5\" role=\"outer\"/>\n    <member type=\"way\" ref=\"-23\" role=\"inner\"/>\n    <member type=\"way\" ref=\"-17\" role=\"inner\"/>\n    <tag k=\"area\" v=\"yes\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"type\" v=\"multipolygon\"/>\n  </relation>\n</osm>"
  },
  {
    "path": "OSM2World/test/files/mp_two_holes_advanced.osm",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<osm version=\"0.6\" generator=\"JOSM\" upload=\"false\">\n  <node id=\"-91\" action=\"modify\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07581557682818\" lon=\"0.4917680487045985\" version=\"424242\"/>\n  <node id=\"-89\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07574827819085664\" lon=\"0.4918306521429082\" version=\"424242\"/>\n  <node id=\"-87\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07574827819085664\" lon=\"0.49193903434548186\" version=\"424242\"/>\n  <node id=\"-85\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07580031725344245\" lon=\"0.4920043766842175\" version=\"424242\"/>\n  <node id=\"-83\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07588287546538634\" lon=\"0.4919233834859045\" version=\"424242\"/>\n  <node id=\"-81\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07588170165194749\" lon=\"0.49181343619737306\" version=\"424242\"/>\n  <node id=\"-79\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07577762352690895\" lon=\"0.49183534740078144\" version=\"424242\"/>\n  <node id=\"-77\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07584022691042076\" lon=\"0.4918173489122674\" version=\"424242\"/>\n  <node id=\"-75\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07585705156972414\" lon=\"0.4918549109752532\" version=\"424242\"/>\n  <node id=\"-73\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07578310132296985\" lon=\"0.4919362954450558\" version=\"424242\"/>\n  <node id=\"-71\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07577214573084735\" lon=\"0.4918764309071722\" version=\"424242\"/>\n  <node id=\"-69\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.07585509521399153\" lon=\"0.49187212692078847\" version=\"424242\"/>\n  <node id=\"-67\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" lat=\"-0.0758398356392741\" lon=\"0.4919413819744184\" version=\"424242\"/>\n  <way id=\"-103\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-69\"/>\n    <nd ref=\"-67\"/>\n    <nd ref=\"-73\"/>\n  </way>\n  <way id=\"-100\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-83\"/>\n    <nd ref=\"-81\"/>\n    <nd ref=\"-91\"/>\n  </way>\n  <way id=\"-97\" action=\"modify\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-91\"/>\n    <nd ref=\"-89\"/>\n    <nd ref=\"-87\"/>\n    <nd ref=\"-85\"/>\n    <nd ref=\"-83\"/>\n  </way>\n  <way id=\"-95\" action=\"modify\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-79\"/>\n    <nd ref=\"-75\"/>\n    <nd ref=\"-77\"/>\n    <nd ref=\"-79\"/>\n  </way>\n  <way id=\"-93\" action=\"modify\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-73\"/>\n    <nd ref=\"-71\"/>\n    <nd ref=\"-69\"/>\n  </way>\n  <relation id=\"-99\" action=\"modify\" timestamp=\"2011-03-16T20:56:47Z\" visible=\"true\" version=\"424242\">\n    <member type=\"way\" ref=\"-97\" role=\"outer\"/>\n    <member type=\"way\" ref=\"-100\" role=\"outer\"/>\n    <member type=\"way\" ref=\"-93\" role=\"inner\"/>\n    <member type=\"way\" ref=\"-103\" role=\"inner\"/>\n    <member type=\"way\" ref=\"-95\" role=\"inner\"/>\n    <tag k=\"area\" v=\"yes\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"type\" v=\"multipolygon\"/>\n  </relation>\n</osm>"
  },
  {
    "path": "OSM2World/test/files/mp_two_holes_advanced2.osm",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' upload='false' generator='JOSM'>\n  <node id='-30' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.0758398356392741' lon='0.4919413819744184' />\n  <node id='-28' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07585509521399153' lon='0.49187212692078847' />\n  <node id='-26' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07577214573084735' lon='0.4918764309071722' />\n  <node id='-24' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07578310132296985' lon='0.4919362954450558' />\n  <node id='-22' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07585705156972414' lon='0.4918549109752532' />\n  <node id='-20' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07584022691042076' lon='0.4918173489122674' />\n  <node id='-18' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07577762352690895' lon='0.49183534740078144' />\n  <node id='-16' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07588170165194749' lon='0.49181343619737306' />\n  <node id='-14' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07588287546538634' lon='0.4919233834859045' />\n  <node id='-12' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07580031725344245' lon='0.4920043766842175' />\n  <node id='-10' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07574827819085664' lon='0.49193903434548186' />\n  <node id='-8' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07574827819085664' lon='0.4918306521429082' />\n  <node id='-6' action='modify' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242' lat='-0.07581557682818' lon='0.4917680487045985' />\n  <way id='-40' action='modify' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242'>\n    <nd ref='-24' />\n    <nd ref='-26' />\n    <nd ref='-28' />\n  </way>\n  <way id='-38' action='modify' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242'>\n    <nd ref='-18' />\n    <nd ref='-22' />\n    <nd ref='-20' />\n    <nd ref='-18' />\n  </way>\n  <way id='-36' action='modify' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242'>\n    <nd ref='-6' />\n    <nd ref='-8' />\n    <nd ref='-10' />\n    <nd ref='-12' />\n    <nd ref='-14' />\n  </way>\n  <way id='-34' visible='true' version='424242'>\n    <nd ref='-14' />\n    <nd ref='-16' />\n    <nd ref='-6' />\n  </way>\n  <way id='-32' visible='true' version='424242'>\n    <nd ref='-24' />\n    <nd ref='-30' />\n    <nd ref='-28' />\n  </way>\n  <relation id='-42' action='modify' timestamp='2011-03-16T20:56:47Z' visible='true' version='424242'>\n    <member type='way' ref='-36' role='outer' />\n    <member type='way' ref='-34' role='outer' />\n    <member type='way' ref='-40' role='inner' />\n    <member type='way' ref='-32' role='inner' />\n    <member type='way' ref='-38' role='inner' />\n    <tag k='area' v='yes' />\n    <tag k='highway' v='service' />\n    <tag k='type' v='multipolygon' />\n  </relation>\n</osm>\n"
  },
  {
    "path": "OSM2World/test/files/mp_two_holes_outer_tagged.osm",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<osm version=\"0.6\" generator=\"JOSM\" upload=\"false\">\n  <node id=\"-26\" visible=\"true\" lat=\"-0.0758398356392741\" lon=\"0.4919413819744184\" version=\"424242\"/>\n  <node id=\"-24\" visible=\"true\" lat=\"-0.07585509521399153\" lon=\"0.49187212692078847\" version=\"424242\"/>\n  <node id=\"-22\" visible=\"true\" lat=\"-0.07577214573084735\" lon=\"0.4918764309071722\" version=\"424242\"/>\n  <node id=\"-21\" visible=\"true\" lat=\"-0.07578310132296985\" lon=\"0.4919362954450558\" version=\"424242\"/>\n  <node id=\"-18\" visible=\"true\" lat=\"-0.07585705156972414\" lon=\"0.4918549109752532\" version=\"424242\"/>\n  <node id=\"-16\" visible=\"true\" lat=\"-0.07584022691042076\" lon=\"0.4918173489122674\" version=\"424242\"/>\n  <node id=\"-15\" visible=\"true\" lat=\"-0.07577762352690895\" lon=\"0.49183534740078144\" version=\"424242\"/>\n  <node id=\"-12\" visible=\"true\" lat=\"-0.07588170165194749\" lon=\"0.49181343619737306\" version=\"424242\"/>\n  <node id=\"-10\" visible=\"true\" lat=\"-0.07588287546538634\" lon=\"0.4919233834859045\" version=\"424242\"/>\n  <node id=\"-8\" visible=\"true\" lat=\"-0.07580031725344245\" lon=\"0.4920043766842175\" version=\"424242\"/>\n  <node id=\"-6\" visible=\"true\" lat=\"-0.07574827819085664\" lon=\"0.49193903434548186\" version=\"424242\"/>\n  <node id=\"-4\" visible=\"true\" lat=\"-0.07574827819085664\" lon=\"0.4918306521429082\" version=\"424242\"/>\n  <node id=\"-3\" action=\"modify\" visible=\"true\" lat=\"-0.07581557682818\" lon=\"0.4917680487045985\" version=\"424242\"/>\n  <way id=\"-23\" action=\"modify\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-21\"/>\n    <nd ref=\"-22\"/>\n    <nd ref=\"-24\"/>\n    <nd ref=\"-26\"/>\n    <nd ref=\"-21\"/>\n  </way>\n  <way id=\"-17\" action=\"modify\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-15\"/>\n    <nd ref=\"-18\"/>\n    <nd ref=\"-16\"/>\n    <nd ref=\"-15\"/>\n  </way>\n  <way id=\"-5\" action=\"modify\" visible=\"true\" version=\"424242\">\n    <nd ref=\"-3\"/>\n    <nd ref=\"-4\"/>\n    <nd ref=\"-6\"/>\n    <nd ref=\"-8\"/>\n    <nd ref=\"-10\"/>\n    <nd ref=\"-12\"/>\n    <nd ref=\"-3\"/>\n    <tag k=\"area\" v=\"yes\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <relation id=\"-34\" action=\"modify\" visible=\"true\" version=\"424242\">\n    <member type=\"way\" ref=\"-5\" role=\"outer\"/>\n    <member type=\"way\" ref=\"-23\" role=\"inner\"/>\n    <member type=\"way\" ref=\"-17\" role=\"inner\"/>\n    <tag k=\"type\" v=\"multipolygon\"/>\n  </relation>\n</osm>"
  },
  {
    "path": "OSM2World/test/files/mp_two_holes_touching_inners.osm",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' upload='false' generator='JOSM'>\n  <node id='-30' action='modify' visible='true' version='424242' lat='-0.07581557682818' lon='0.4917680487045985' />\n  <node id='-28' visible='true' version='424242' lat='-0.07574827819085664' lon='0.4918306521429082' />\n  <node id='-26' visible='true' version='424242' lat='-0.07574827819085664' lon='0.49193903434548186' />\n  <node id='-24' visible='true' version='424242' lat='-0.07580031725344245' lon='0.4920043766842175' />\n  <node id='-22' visible='true' version='424242' lat='-0.07588287546538634' lon='0.4919233834859045' />\n  <node id='-20' visible='true' version='424242' lat='-0.07588170165194749' lon='0.49181343619737306' />\n  <node id='-18' visible='true' version='424242' lat='-0.07577762352690895' lon='0.49183534740078144' />\n  <node id='-16' visible='true' version='424242' lat='-0.07584022691042076' lon='0.4918173489122674' />\n  <node id='-14' visible='true' version='424242' lat='-0.07585705156972414' lon='0.4918549109752532' />\n  <node id='-12' visible='true' version='424242' lat='-0.07578310132296985' lon='0.4919362954450558' />\n  <node id='-10' visible='true' version='424242' lat='-0.07577214573084735' lon='0.4918764309071722' />\n  <node id='-8' visible='true' version='424242' lat='-0.07585509521399153' lon='0.49187212692078847' />\n  <node id='-6' visible='true' version='424242' lat='-0.0758398356392741' lon='0.4919413819744184' />\n  <way id='-42' visible='true' version='424242'>\n    <nd ref='-10' />\n    <nd ref='-8' />\n    <nd ref='-6' />\n    <nd ref='-10' />\n  </way>\n  <way id='-36' visible='true' version='424242'>\n    <nd ref='-30' />\n    <nd ref='-28' />\n    <nd ref='-26' />\n    <nd ref='-24' />\n    <nd ref='-22' />\n    <nd ref='-20' />\n    <nd ref='-30' />\n  </way>\n  <way id='-34' visible='true' version='424242'>\n    <nd ref='-18' />\n    <nd ref='-14' />\n    <nd ref='-16' />\n    <nd ref='-18' />\n  </way>\n  <way id='-32' visible='true' version='424242'>\n    <nd ref='-6' />\n    <nd ref='-12' />\n    <nd ref='-10' />\n    <nd ref='-6' />\n  </way>\n  <relation id='-38' visible='true' version='424242'>\n    <member type='way' ref='-36' role='outer' />\n    <member type='way' ref='-32' role='inner' />\n    <member type='way' ref='-42' role='inner' />\n    <member type='way' ref='-34' role='inner' />\n    <tag k='area' v='yes' />\n    <tag k='highway' v='service' />\n    <tag k='type' v='multipolygon' />\n  </relation>\n</osm>\n"
  },
  {
    "path": "OSM2World/test/files/sameCoordNodes.osm",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' generator='JOSM' upload='false'>\n  <!-- this test file for OSM2World contains two nodes with the same coords  -->\n  <node id='4' version='1' lat='48.57412203109322' lon='13.465483398374973'/>\n  <node id='5' version='1' lat='48.57412203109322' lon='13.465483398374973'/>\n</osm>\n"
  },
  {
    "path": "OSM2World/test/files/self_intersection.osm",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<osm generator=\"JOSM\" upload=\"false\" version=\"0.6\">\n  <node action=\"modify\" id=\"-170094\" lat=\"48.565430757018504\" lon=\"13.448474189895762\" version=\"424242\" visible=\"true\"/>\n  <node action=\"modify\" id=\"-170092\" lat=\"48.5655316398253\" lon=\"13.448417728647366\" version=\"424242\" visible=\"true\"/>\n  <node action=\"modify\" id=\"-170090\" lat=\"48.56554782458393\" lon=\"13.448504892974023\" version=\"424242\" visible=\"true\"/>\n  <node action=\"modify\" id=\"-170088\" lat=\"48.56508987056163\" lon=\"13.44786610686816\" version=\"424242\" visible=\"true\"/>\n  <node action=\"modify\" id=\"-170086\" lat=\"48.56534350714786\" lon=\"13.447429603029152\" version=\"424242\" visible=\"true\"/>\n  <node action=\"modify\" id=\"-170085\" lat=\"48.56577327734857\" lon=\"13.448015156959528\" version=\"424242\" visible=\"true\"/>\n  <way action=\"modify\" id=\"-170087\" version=\"424242\" visible=\"true\">\n    <nd ref=\"-170085\"/>\n    <nd ref=\"-170086\"/>\n    <nd ref=\"-170088\"/>\n    <nd ref=\"-170090\"/>\n    <nd ref=\"-170092\"/>\n    <nd ref=\"-170094\"/>\n    <nd ref=\"-170085\"/>\n    <tag k=\"building\" v=\"yes\"/>\n  </way>\n</osm>\n"
  },
  {
    "path": "OSM2World/test/files/validFile.osm",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' generator='JOSM' upload='false'>\n  <bounds minlat='48.5701329840823' minlon='13.4553337097168' maxlat='48.5781971466379' maxlon='13.4776496887207' />\n  <node id='4' timestamp='2009-12-29T17:26:47Z' visible='true' version='1' lat='48.57412203109322' lon='13.465483398374973'>\n    <tag k='highway' v='traffic_signals' />\n  </node>\n  <node id='2' action='modify' timestamp='2009-12-29T17:26:47Z' visible='true' version='1' lat='48.575670016109235' lon='13.470591956160817'>\n  </node>\n  <node id='3' timestamp='2009-12-29T17:26:47Z' visible='true' version='1' lat='48.57437491613598' lon='13.46721488748648'>\n    <tag k='addr:housenumber' v='15b' />\n  </node>\n  <node id='1' timestamp='2009-12-29T17:26:47Z' visible='true' version='1' lat='48.57250738140529' lon='13.461750678155019' />\n  <way id='5' timestamp='2009-12-29T17:26:47Z' visible='true' version='1'>\n    <nd ref='1' />\n    <nd ref='4' />\n    <nd ref='2' />\n    <tag k='highway' v='residential' />\n  </way>\n  <relation id='6' timestamp='2009-12-29T17:26:47Z' visible='true' version='1'>\n    <member type='way' ref='5' role='street' />\n    <member type='node' ref='3' role='house' />\n    <tag k='type' v='associatedStreet' />\n  </relation>\n</osm>\n"
  },
  {
    "path": "OSM2World/test/org/openstreetmap/josm/plugins/graphview/util/ValueStringParserTest.java",
    "content": "package org.openstreetmap.josm.plugins.graphview.util;\n\nimport static org.junit.Assert.*;\nimport static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.*;\n\nimport java.awt.Color;\n\nimport org.junit.Test;\n\npublic class ValueStringParserTest {\n\n    /* speed */\n\n    @Test\n    public void testParseSpeedDefault() {\n        assertClose(50, parseSpeed(\"50\"));\n    }\n\n    @Test\n    public void testParseSpeedKmh() {\n        assertClose(30, parseSpeed(\"30 km/h\"));\n        assertClose(100, parseSpeed(\"100km/h\"));\n    }\n\n    @Test\n    public void testParseSpeedMph() {\n        assertClose(40.234f, parseSpeed(\"25mph\"));\n        assertClose(40.234f, parseSpeed(\"25 mph\"));\n    }\n\n    @Test\n    public void testParseSpeedInvalid() {\n        assertNull(parseSpeed(\"lightspeed\"));\n    }\n\n    /* measure */\n\n    @Test\n    public void testParseMeasureDefault() {\n        assertClose(3.5f, parseMeasure(\"3.5\"));\n    }\n\n    @Test\n    public void testParseMeasureM() {\n        assertClose(2, parseMeasure(\"2m\"));\n        assertClose(5.5f, parseMeasure(\"5.5 m\"));\n    }\n\n    @Test\n    public void testParseMeasureKm() {\n        assertClose(1000, parseMeasure(\"1 km\"));\n        assertClose(7200, parseMeasure(\"7.2km\"));\n    }\n\n    @Test\n    public void testParseMeasureMi() {\n        assertClose(1609.344f, parseMeasure(\"1 mi\"));\n    }\n\n    @Test\n    public void testParseMeasureFeetInches() {\n        assertClose(3.6576f, parseMeasure(\"12'0\\\"\"));\n        assertClose(1.9812f, parseMeasure(\"6' 6\\\"\"));\n    }\n\n    @Test\n    public void testParseMeasureInvalid() {\n        assertNull(parseMeasure(\"very long\"));\n        assertNull(parseMeasure(\"6' 16\\\"\"));\n    }\n\n    /* weight */\n\n    @Test\n    public void testParseWeightDefault() {\n        assertClose(3.6f, parseWeight(\"3.6\"));\n    }\n\n    @Test\n    public void testParseWeightT() {\n        assertClose(30, parseWeight(\"30t\"));\n        assertClose(3.5f, parseWeight(\"3.5 t\"));\n    }\n\n    @Test\n    public void testParseWeightInvalid() {\n        assertNull(parseWeight(\"heavy\"));\n    }\n\n    /* angle */\n\n    @Test\n    public void testParseAngleDefault() {\n        assertClose( 47, parseAngle(\"47\"));\n        assertClose(  0, parseAngle(\"360\"));\n    }\n\n    @Test\n    public void testParseAngleLetters() {\n        assertClose(  0.0f, parseAngle(\"N\"));\n        assertClose(225.0f, parseAngle(\"SW\"));\n        assertClose(112.5f, parseAngle(\"ESE\"));\n    }\n\n    @Test\n    public void testParseAngleInvalid() {\n        assertNull(parseAngle(\"forward\"));\n        assertNull(parseAngle(\"-90\"));\n    }\n    \n    @Test\n    public void testParseColorDefault() {\n    \tassertEquals(new Color(255, 0, 0), parseColor(\"#ff0000\"));\n    \tassertEquals(new Color(1, 2, 3), parseColor(\"#010203\"));\n    }\n    \n    @Test\n    public void testParseColorInvalid() {\n    \tassertNull(parseColor(\"ff0000\"));\n    \tassertNull(parseColor(\"#\"));\n    }\n    \n    /* utility methods for testing */\n    \n    private static final void assertClose(float expected, float actual) {\n        if (Math.abs(expected - actual) > 0.001) {\n            throw new AssertionError(\"expected \" + expected + \", was \" + actual);\n        }\n    }\n\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/DelaunayTriangulationTest.java",
    "content": "package org.osm2world;\n\nimport static java.util.Arrays.asList;\nimport static org.junit.Assert.*;\nimport static org.osm2world.core.test.TestUtil.assertAlmostEquals;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Random;\n\nimport org.junit.Test;\nimport org.osm2world.core.map_elevation.creation.DelaunayTriangulation;\nimport org.osm2world.core.map_elevation.creation.DelaunayTriangulation.DelaunayTriangle;\nimport org.osm2world.core.math.AxisAlignedBoundingBoxXZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\n\n\npublic class DelaunayTriangulationTest {\n\t\n\tprivate static final double SIZE = 100;\n\t\n\t@Test\n\tpublic void testInsert() {\n\t\t\n\t\tList<Random> RANDOMS = asList(\n\t\t\t\tnew Random(1), new Random(20), new Random(300));\n\t\t\n\t\tfor (Random random : RANDOMS) {\n\t\t\t\n\t\t\tAxisAlignedBoundingBoxXZ bounds = new AxisAlignedBoundingBoxXZ(\n\t\t\t\t\t-SIZE, -SIZE, +SIZE, +SIZE);\n\t\t\t\n\t\t\tDelaunayTriangulation triangulation =\n\t\t\t\t\tnew DelaunayTriangulation(bounds);\n\t\t\tDelaunayTriangulation triangulation2 =\n\t\t\t\t\tnew DelaunayTriangulation(bounds);\n\t\t\t\n\t\t\tList<VectorXYZ> points = new ArrayList<VectorXYZ>();\n\t\t\t\n\t\t\tfor (int insertCount = 0; insertCount < 100; insertCount++) {\n\t\t\t\t\n\t\t\t\tSystem.out.println(RANDOMS.indexOf(random) + \":\"\n\t\t\t\t\t\t+ insertCount);\n\t\t\t\t\n\t\t\t\tdouble x = (random.nextDouble() * 2 * SIZE) - SIZE;\n\t\t\t\tdouble z = (random.nextDouble() * 2 * SIZE) - SIZE;\n\t\t\t\t\n\t\t\t\tVectorXYZ point = new VectorXYZ(x, 0, z);\n\t\t\t\t\n\t\t\t\tpoints.add(point);\n\t\t\t\t\n\t\t\t\t// check whether undoing works\n\t\t\t\t\n\t\t\t\ttriangulation2.probe(point.xz());\n\t\t\t\t\n\t\t\t\tassertTriangulationsEqual(triangulation, triangulation2);\n\t\t\t\t\n\t\t\t\t// insert for real\n\t\t\t\t\n\t\t\t\ttriangulation.insert(point);\n\t\t\t\ttriangulation2.insert(point);\n\t\t\t\t\n\t\t\t\tassertTriangulationProperties(triangulation, points);\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n\t/**\n\t * asserts that two triangulations are equal\n\t */\n\tprivate static void assertTriangulationsEqual(\n\t\t\tDelaunayTriangulation triangulation1,\n\t\t\tDelaunayTriangulation triangulation2) {\n\t\t\n\t\tfor (DelaunayTriangle t1 : triangulation1.getTriangles()) {\n\t\t\t\n\t\t\tDelaunayTriangle twinTriangle = null;\n\t\t\t\n\t\t\tfor (DelaunayTriangle t2 : triangulation2.getTriangles()) {\n\t\t\t\tif (t1.asTriangleXZ().equals(t2.asTriangleXZ())) {\n\t\t\t\t\ttwinTriangle = t2;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tassertNotNull(\"must contain \" + t1, twinTriangle);\n\t\t\t\n\t\t\tfor (int i = 0; i <= 2; i++) {\n\t\t\t\t\n\t\t\t\tDelaunayTriangle n1 = t1.getNeighbor(i);\n\t\t\t\tDelaunayTriangle n2 = twinTriangle.getNeighbor(i);\n\t\t\t\t\n\t\t\t\tif (n1 == null && n2 == null) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if (n1 != null && n2 != null\n\t\t\t\t\t\t&& n1.asTriangleXZ().equals(n2.asTriangleXZ())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} else {\n\t\t\t\t\tfail(String.format(\"neighbor %d different: %s vs. %s\",\n\t\t\t\t\t\t\ti, n1, n2));\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\t/**\n\t * asserts that a triangulation confirms to a set of required properties\n\t */\n\tprivate static void assertTriangulationProperties(\n\t\t\tDelaunayTriangulation triangulation, List<VectorXYZ> points) {\n\t\t\n\t\tfor (DelaunayTriangle triangle : triangulation.getTriangles()) {\n\t\t\t\n\t\t\t/* check that neighborship relations are symmetric */\n\t\t\t\n\t\t\tfor (int i = 0; i <= 2; i++) {\n\t\t\t\tif (triangle.getNeighbor(i) != null) {\n\t\t\t\t\tint index = triangle.getNeighbor(i).indexOfNeighbor(triangle);\n\t\t\t\t\tassertTrue(0 <= index && index <= 2);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/*\n\t\t\t * check that each triangle's vertices are on the\n\t\t\t * circumcircle, and all other points are outside it\n\t\t\t */\n\t\t\t\n\t\t\tVectorXZ center = triangle.getCircumcircleCenter();\n\t\t\tdouble radius = triangle.p0.distanceToXZ(center);\n\t\t\t\n\t\t\tassertAlmostEquals(radius, triangle.p1.distanceToXZ(center));\n\t\t\tassertAlmostEquals(radius, triangle.p2.distanceToXZ(center));\n\t\t\t\n\t\t\tfor (VectorXYZ otherPoint : points) {\n\t\t\t\tif (triangle.p0 != otherPoint\n\t\t\t\t\t\t&& triangle.p1 != otherPoint\n\t\t\t\t\t\t&& triangle.p2 != otherPoint) {\n\t\t\t\t\t\n\t\t\t\t\tif (otherPoint.distanceToXZ(center) <= radius) {\n\t\t\t\t\t\tSystem.out.println(otherPoint);\n\t\t\t\t\t\tSystem.out.println(triangle);\n\t\t\t\t\t}\n\t\t\t\t\tassertTrue(otherPoint.distanceToXZ(center) > radius);\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/map_data/creation/OSMToMapDataConverterTest.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport static java.util.Arrays.asList;\nimport static org.junit.Assert.*;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.apache.commons.configuration.BaseConfiguration;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.osm2world.core.map_data.data.MapArea;\nimport org.osm2world.core.map_data.data.MapData;\nimport org.osm2world.core.map_data.data.MapNode;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.creation.OsmosisReader;\nimport org.osm2world.core.osm.data.OSMData;\n\npublic class OSMToMapDataConverterTest {\n\n\t/**\n\t * loads {@link MapData} from a file in the test files directory\n\t */\n\tprivate static MapData loadMapData(String filename) throws IOException {\n\t\t\n\t\tFile testFile = new File(\"test\"+File.separator+\"files\"\n\t\t\t\t+File.separator+filename);\n\t\t\n\t\tOSMData osmData = new OsmosisReader(testFile).getData();\n\t\tOriginMapProjection mapProjection = new MetricMapProjection();\n\t\tmapProjection.setOrigin(osmData);\n\t\t\n\t\tOSMToMapDataConverter converter = new OSMToMapDataConverter(mapProjection, new BaseConfiguration());\n\t\treturn converter.createMapData(osmData);\n\t\t\n\t}\n\t\n\t/**\n\t * test code for a group multipolygon test files which\n\t * represent the same case with different multipolygon variants\n\t */\n\tprivate void genericMultipolygonTest(String filename) throws IOException {\n\t\t\n\t\tMapData mapData = loadMapData(filename);\n\t\t\n\t\tassertEquals(13, mapData.getMapNodes().size());\n\t\tassertEquals(0, mapData.getMapWaySegments().size());\n\t\tassertEquals(1, mapData.getMapAreas().size());\n\t\t\n\t\tMapArea area = mapData.getMapAreas().iterator().next();\n\n\t\tassertEquals(2, area.getHoles().size());\n\t\tassertEquals(6, area.getOuterPolygon().size());\n\t\tassertEquals(13, area.getAreaSegments().size());\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testMultipolygon() throws IOException {\n\t\tgenericMultipolygonTest(\"mp_two_holes.osm\");\n\t}\n\t\n\t@Test\n\tpublic void testMultipolygonOuterTagged() throws IOException {\n\t\tgenericMultipolygonTest(\"mp_two_holes_outer_tagged.osm\");\n\t}\n\t\n\t@Test\n\tpublic void testMultipolygonAdvanced() throws IOException {\n\t\tgenericMultipolygonTest(\"mp_two_holes_advanced.osm\");\n\t}\n\t\n\t@Test\n\tpublic void testMultipolygonAdvanced2() throws IOException {\n\t\tgenericMultipolygonTest(\"mp_two_holes_advanced2.osm\");\n\t}\n\t\n\t@Ignore\n\t@Test\n\tpublic void testMultipolygonTouchingInners() throws IOException {\n\t\tgenericMultipolygonTest(\"mp_two_holes_touching_inners.osm\");\n\t}\n\t\n\tprivate void genericCoastlineTest(String filename, List<LatLon> landSites,\n\t\t\tList<LatLon> waterSites) throws IOException {\n\n\t\t/* create map data */\n\t\t\n\t\tFile testFile = new File(\"test\"+File.separator+\"files\"\n\t\t\t\t+File.separator+filename);\n\t\t\n\t\tOSMData osmData = new OsmosisReader(testFile).getData();\n\t\tOriginMapProjection mapProjection = new MetricMapProjection();\n\t\tmapProjection.setOrigin(osmData);\n\t\t\n\t\tOSMToMapDataConverter converter = new OSMToMapDataConverter(mapProjection, new BaseConfiguration());\n\t\tMapData mapData = converter.createMapData(osmData);\n\t\t\n\t\t/* check coastline properties */\n\t\t\n\t\tList<MapArea> waterAreas = new ArrayList<MapArea>();\n\n\t\tfor (MapArea area : mapData.getMapAreas()) {\n\t\t\tif (area.getTags().contains(\"natural\", \"water\")) {\n\t\t\t\twaterAreas.add(area);\n\t\t\t}\n\t\t}\n\t\t\n\t\tassertFalse(waterAreas.isEmpty());\n\t\t\n\t\tfor (LatLon landSite : landSites) {\n\t\t\t\n\t\t\tVectorXZ v = mapProjection.calcPos(landSite);\n\t\t\t\n\t\t\tfor (MapArea waterArea : waterAreas) {\n\t\t\t\tassertFalse(waterArea.getPolygon().contains(v));\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tfor (LatLon waterSite : waterSites) {\n\t\t\t\n\t\t\tVectorXZ v = mapProjection.calcPos(waterSite);\n\t\t\t\n\t\t\tboolean isWater = false;\n\t\t\t\n\t\t\tfor (MapArea waterArea : waterAreas) {\n\t\t\t\tif (waterArea.getPolygon().contains(v)) {\n\t\t\t\t\tisWater = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tassertTrue(isWater);\n\t\t\t\n\t\t}\n\n\t}\n\t\n\t@Test\n\tpublic void\ttestCoastlineBigIsland() throws IOException {\n\n\t\tgenericCoastlineTest(\"coastline_big_island.osm\",\n\t\t\t\tasList(new LatLon(51.4946619, 2.1931507)),\n\t\t\t\tasList(new LatLon(51.4994015, 2.183386),\n\t\t\t\t\t\tnew LatLon(51.4982682, 2.2522352),\n\t\t\t\t\t\tnew LatLon(51.4590992, 2.2500837),\n\t\t\t\t\t\tnew LatLon(51.4569336, 2.1838825)));\n\t\t\t\t\n\t}\n\t\n\t@Test\n\tpublic void\ttestCoastlineIslands() throws IOException {\n\t\t\n\t\tgenericCoastlineTest(\"coastline_islands.osm\",\n\t\t\t\tasList(new LatLon(51.4662933, 2.2364075),\n\t\t\t\t\t\tnew LatLon(51.4780457, 2.2009898)),\n\t\t\t\tasList(new LatLon(51.4815502, 2.2271393),\n\t\t\t\t\t\tnew LatLon(51.4596942, 2.1930457)));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void\ttestCoastlineIslandsAndCoast() throws IOException {\n\t\t\n\t\tgenericCoastlineTest(\"coastline_islands_and_coast.osm\",\n\t\t\t\tasList(new LatLon(51.4957716, 2.2466687),\n\t\t\t\t\t\tnew LatLon(51.456188, 2.2522958),\n\t\t\t\t\t\tnew LatLon(51.4662933, 2.2364075),\n\t\t\t\t\t\tnew LatLon(51.4780457, 2.2009898)),\n\t\t\t\tasList(new LatLon(51.4815502, 2.2271393),\n\t\t\t\t\t\tnew LatLon(51.4596942, 2.1930457)));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void\ttestCoastlineMultipleCoasts() throws IOException {\n\t\t\n\t\tgenericCoastlineTest(\"coastline_multiple_coasts.osm\",\n\t\t\t\tasList(new LatLon(51.4730977, 2.2165471)),\n\t\t\t\tasList(new LatLon(51.4654685, 2.2374005),\n\t\t\t\t\t\tnew LatLon(51.4978323, 2.1844396),\n\t\t\t\t\t\tnew LatLon(51.4555692, 2.2178711)));\n\t\t\n\t}\n\t\n\t/**\n\t * reads two nodes with the same coordinates\n\t */\n\t@Test\n\tpublic void testSameCoordNodes() throws IOException {\n\t\t\n\t\tMapData mapData = loadMapData(\"sameCoordNodes.osm\");\n\t\t\n\t\tassertSame(2, mapData.getMapNodes().size());\n\t\t\n\t\tMapNode[] nodes = mapData.getMapNodes().toArray(new MapNode[2]);\n\t\tassertNotSame(nodes[0].getOsmNode().id, nodes[1].getOsmNode().id);\n\t\t\n\t}\n\t\n\t/**\n\t * reads a self intersecting polygon (can be filtered, but must not crash)\n\t */\n\t@Test\n\tpublic void testSelfIntersection() throws IOException {\n\t\t\n\t\tMapData mapData = loadMapData(\"self_intersection.osm\");\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/map_data/creation/OrthographicAzimuthalMapProjectionTest.java",
    "content": "package org.osm2world.core.map_data.creation;\n\nimport static org.osm2world.core.test.TestUtil.assertAlmostEquals;\n\nimport org.junit.Test;\nimport org.osm2world.core.math.VectorXZ;\n\n\npublic class OrthographicAzimuthalMapProjectionTest {\n\n\t@Test\n\tpublic void testCalcPos() {\n\t\t\n\t\tOrthographicAzimuthalMapProjection proj =\n\t\t\t\tnew OrthographicAzimuthalMapProjection();\n\t\tproj.setOrigin(new LatLon(0, 0));\n\t\t\n\t\tVectorXZ posOrigin = proj.calcPos(0, 0);\n\t\tassertAlmostEquals(0, posOrigin.x);\n\t\tassertAlmostEquals(0, posOrigin.z);\n\t\tassertAlmostEquals(0, proj.calcLat(posOrigin));\n\t\tassertAlmostEquals(0, proj.calcLon(posOrigin));\n\t\t\n\t\tVectorXZ posE = proj.calcPos(0, 0.000009);\n\t\tassertAlmostEquals(1, posE.x);\n\t\tassertAlmostEquals(0, posE.z);\n\t\tassertAlmostEquals(0, proj.calcLat(posE));\n\t\tassertAlmostEquals(0.000009, proj.calcLon(posE));\n\n\t\tVectorXZ posN = proj.calcPos(0.000009, 0);\n\t\tassertAlmostEquals(0, posN.x);\n\t\tassertAlmostEquals(1, posN.z);\n\t\tassertAlmostEquals(0.000009, proj.calcLat(posN));\n\t\tassertAlmostEquals(0, proj.calcLon(posN));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/GeometryUtilTest.java",
    "content": "package org.osm2world.core.math;\n\nimport static org.junit.Assert.*;\nimport static org.osm2world.core.math.GeometryUtil.*;\nimport static org.osm2world.core.math.VectorXZ.*;\nimport static org.osm2world.core.test.TestUtil.*;\n\nimport java.util.List;\n\nimport org.junit.Test;\n\npublic class GeometryUtilTest {\n\n\t@Test\n\tpublic void testIsRightOf() {\n\t\t\n\t\tassertTrue(isRightOf(X_UNIT, NULL_VECTOR, Z_UNIT));\n\t\tassertFalse(isRightOf(X_UNIT, Z_UNIT, NULL_VECTOR));\n\n\t\tassertTrue(isRightOf(NULL_VECTOR, Z_UNIT, X_UNIT));\n\t\tassertFalse(isRightOf(NULL_VECTOR, X_UNIT, Z_UNIT));\n\t\t\n\t\tfor (VectorXZ v1 : anyVectorXZ()) {\n\t\t\tfor (VectorXZ v2 : anyVectorXZ()) {\n\t\t\t\n\t\t\t\tif (!v1.equals(v2) && !v2.equals(NULL_VECTOR)) {\n\t\t\t\t\t\n\t\t\t\t\tVectorXZ l1 = v1;\n\t\t\t\t\tVectorXZ l2 = v1.add(v2);\n\t\t\t\t\tVectorXZ pR = v1.add(v2.rightNormal());\n\t\t\t\t\tVectorXZ pL = v1.subtract(v2.rightNormal());\n\t\t\t\t\t\n\t\t\t\t\tassertTrue(pR + \" should be right of \" + l1 + \"-\" + \"l2\",\n\t\t\t\t\t\t\tisRightOf(pR, l1, l2));\n\t\t\t\t\tassertFalse(pL + \" should not be right of \" + l1 + \"-\" + \"l2\",\n\t\t\t\t\t\t\tisRightOf(pL, l1, l2));\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testIsBetween() {\n\t\t\n\t\tassertTrue(isBetween(NULL_VECTOR, X_UNIT, X_UNIT.invert()));\n\t\tassertTrue(isBetween(NULL_VECTOR, X_UNIT.invert(), X_UNIT));\n\t\tassertTrue(isBetween(Z_UNIT, X_UNIT.invert(), X_UNIT));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testInterpolateElevation() {\n\t\t\n\t\tassertEquals(9.0, GeometryUtil.interpolateElevation(\n\t\t\t\tnew VectorXZ(5, 1),\n\t\t\t\tnew VectorXYZ(3, 7, 1),\n\t\t\t\tnew VectorXYZ(6, 10, 1)).y,\n\t\t\t\t1e-5);\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testEquallyDistributePointsAlong1StartEnd() {\n\t\t\n\t\tList<VectorXZ> result = GeometryUtil.equallyDistributePointsAlong(\n\t\t\t\t1f, true, new VectorXZ(-2, 5), new VectorXZ(+4, 5));\n\t\t\n\t\tassertSame(7, result.size());\n\t\tassertAlmostEquals(-2, 5, result.get(0));\n\t\tassertAlmostEquals(-1, 5, result.get(1));\n\t\tassertAlmostEquals( 0, 5, result.get(2));\n\t\tassertAlmostEquals(+1, 5, result.get(3));\n\t\tassertAlmostEquals(+2, 5, result.get(4));\n\t\tassertAlmostEquals(+3, 5, result.get(5));\n\t\tassertAlmostEquals(+4, 5, result.get(6));\n\t\t\n\t}\n\n\t@Test\n\tpublic void testEquallyDistributePointsAlong1NoStartEnd() {\n\t\t\n\t\tList<VectorXZ> result = GeometryUtil.equallyDistributePointsAlong(\n\t\t\t\t1f, false, new VectorXZ(-2, 5), new VectorXZ(+4, 5));\n\t\t\n\t\tassertSame(6, result.size());\n\t\tassertAlmostEquals(-1.5f, 5, result.get(0));\n\t\tassertAlmostEquals(-0.5f, 5, result.get(1));\n\t\tassertAlmostEquals(+0.5f, 5, result.get(2));\n\t\tassertAlmostEquals(+1.5f, 5, result.get(3));\n\t\tassertAlmostEquals(+2.5f, 5, result.get(4));\n\t\tassertAlmostEquals(+3.5f, 5, result.get(5));\n\t\t\n\t}\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/PolygonXZTest.java",
    "content": "package org.osm2world.core.math;\n\nimport static org.junit.Assert.*;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.junit.Test;\n\npublic class PolygonXZTest {\n\t\n\tprivate static final VectorXZ outlineA0 = new VectorXZ(-1.1f, -1.1f);\n\tprivate static final VectorXZ outlineA1 = new VectorXZ(-1.1f, 1.1f);\n\tprivate static final VectorXZ outlineA2 = new VectorXZ(1.1f, 1.1f);\n\tprivate static final VectorXZ outlineA3 = new VectorXZ(1.1f, -1.1f);\n\tprivate static final VectorXZ outlineA4 = new VectorXZ(0f, 1f);\n\t\n\tprivate static final List<VectorXZ> outlineA = Arrays.asList(outlineA0,\n\t\t\toutlineA1, outlineA2, outlineA3, outlineA0);\n\t\n\tprivate static final List<VectorXZ> outlineB = Arrays.asList(outlineA0,\n\t\t\toutlineA1, outlineA2, outlineA3, outlineA4, outlineA0);\n\t\n\t@Test\n\tpublic void testIsClockwise1() {\n\t\t\n\t\tassertTrue(new SimplePolygonXZ(outlineA).isClockwise());\n\t\t\n\t\tList<VectorXZ> outlineAInv = new ArrayList<VectorXZ>(outlineA);\n\t\tCollections.reverse(outlineAInv);\n\t\tassertFalse(new SimplePolygonXZ(outlineAInv).isClockwise());\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testIsClockwise2() {\n\t\t\n\t\tassertTrue(new SimplePolygonXZ(outlineB).isClockwise());\n\t\t\n\t\tList<VectorXZ> outlineBInv = new ArrayList<VectorXZ>(outlineB);\n\t\tCollections.reverse(outlineBInv);\n\t\tassertFalse(new SimplePolygonXZ(outlineBInv).isClockwise());\n\t}\n\t\n\t@Test\n\tpublic void testIsClockwise3() {\n\t\t\n\t\t// test case created from a former bug\n\t\tassertTrue(new SimplePolygonXZ(Arrays.asList(\n\t\t\t\tnew VectorXZ(114266.61f,12953.262f),\n\t\t\t\tnew VectorXZ(114258.74f,12933.117f),\n\t\t\t\tnew VectorXZ(114257.69f,12939.848f),\n\t\t\t\tnew VectorXZ(114266.61f,12953.262f))).isClockwise());\n\t\t\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testIsEquivalentTo_same() {\n\t\t\n\t\tPolygonXZ polyA = new PolygonXZ(outlineA);\n\t\tassertTrue(polyA.isEquivalentTo(polyA));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testIsEquivalentTo_yes() {\n\t\t\n\t\tassertTrue(\n\t\t\t\tnew PolygonXZ(Arrays.asList(outlineA0, outlineA1, outlineA4, outlineA0))\n\t\t\t\t\t.isEquivalentTo(\n\t\t\t\t\t\tnew PolygonXZ(Arrays.asList(outlineA1, outlineA4, outlineA0, outlineA1))));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testIsEquivalentTo_no() {\n\t\t\n\t\tassertFalse(\n\t\t\t\tnew PolygonXZ(Arrays.asList(outlineA0, outlineA1, outlineA4, outlineA0))\n\t\t\t\t\t.isEquivalentTo(\n\t\t\t\t\t\tnew PolygonXZ(Arrays.asList(outlineA0, outlineA4, outlineA1, outlineA0))));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testIsSelfIntersecting1() {\n\t\tVectorXZ v1 = new VectorXZ(0, 0);\n\t\tVectorXZ v2 = new VectorXZ(1, 0);\n\t\tVectorXZ v3 = new VectorXZ(1, 1);\n\t\tVectorXZ v4 = new VectorXZ(0, 1);\n\t\tVectorXZ v5 = new VectorXZ(0, 0);\n\t\t\n\t\t// Simple rectangle should not be self intersecting\n\t\tassertFalse(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v3, v4, v5)).isSelfIntersecting());\n\t}\n\n\t@Test\n\tpublic void testIsSelfIntersecting2() {\n\t\tVectorXZ v1 = new VectorXZ(1, 5);\n\t\tVectorXZ v2 = new VectorXZ(5, 2);\n\t\tVectorXZ v3 = new VectorXZ(4, 1);\n\t\tVectorXZ v4a = new VectorXZ(3, 4);\n\t\tVectorXZ v4b = new VectorXZ(3, 5);\n\t\tVectorXZ v4c = new VectorXZ(3, 6);\n\t\tVectorXZ v5 = new VectorXZ(1, 5);\n\t\t\n\t\t/*\n\t\t * testing a simple self intersecting polygon with 4 vertices\n\t\t * while the line causing the intersection is above, below or at the\n\t\t * same height as the beginning node\n\t\t */\n\t\t\n\t\tassertTrue(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v3, v4a, v5)).isSelfIntersecting());\n\n\t\tassertTrue(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v3, v4b, v5)).isSelfIntersecting());\n\n\t\tassertTrue(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v3, v4c, v5)).isSelfIntersecting());\n\t}\n\t\n\t@Test\n\tpublic void testIsSelfIntersecting3() {\n\t\tVectorXZ v1 = new VectorXZ(0, 0);\n\t\t\n\t\tVectorXZ v2 = new VectorXZ(10, 0);\n\t\tVectorXZ v3 = new VectorXZ(15, 0);\n\t\tVectorXZ v4 = new VectorXZ(20, 0);\n\t\t\n\t\tVectorXZ v5 = new VectorXZ(20, 20);\n\t\tVectorXZ v6 = new VectorXZ(0, 20);\n\t\tVectorXZ v7 = new VectorXZ(0, 0);\n\t\t\n\t\t// Testing simple non self intersecting polygons\n\t\t\n\t\t// ... with one vertex is used two times\n\t\tassertFalse(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v2, v5, v6, v7)).isSelfIntersecting());\n\t\t\n\t\t// ... with 3 parallel line segments\n\t\tassertFalse(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v3, v4, v5, v6, v7)).isSelfIntersecting());\n\n\t\t// ... with 3 co-linear line segments \n\t\tassertFalse(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v4, v3, v4, v5, v6, v7)).isSelfIntersecting());\n\n\t}\n\n\t@Test\n\tpublic void testIsSelfIntersecting4() {\n\t\tVectorXZ v1 = new VectorXZ(0, 0);\n\t\tVectorXZ v2 = new VectorXZ(0, 1);\n\t\t\n\t\tVectorXZ v3 = new VectorXZ(0, 3);\n\t\tVectorXZ v4 = new VectorXZ(0, 4);\n\t\t\n\t\tVectorXZ v5 = new VectorXZ(2, 2);\n\t\tVectorXZ v6 = new VectorXZ(4, 2);\n\t\t\n\t\t// Testing simple polygons with two vertical line segments\n\t\t\n\t\t// .. without any intersection\n\t\tassertFalse(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v5, v3, v4, v6, v1)).isSelfIntersecting());\n\n\t\t// ... with one point being visited twice\n\t\tassertFalse(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v6, v3, v4, v6, v1)).isSelfIntersecting());\n\n\t\t// .. with a intersection at the right side\n\t\tassertTrue(\n\t\t\t\tnew PolygonXZ(Arrays.asList(v1, v2, v6, v3, v4, v5, v1)).isSelfIntersecting());\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/SimplePolygonXZTest.java",
    "content": "package org.osm2world.core.math;\n\nimport static java.lang.Math.sqrt;\nimport static java.util.Arrays.asList;\nimport static org.junit.Assert.*;\nimport static org.osm2world.core.math.VectorXZ.NULL_VECTOR;\nimport static org.osm2world.core.test.TestUtil.assertAlmostEquals;\n\nimport org.junit.Test;\n\npublic class SimplePolygonXZTest {\n\t\n\tprivate SimplePolygonXZ p1 = new SimplePolygonXZ(asList(\n\t\t\tnew VectorXZ(-1, -1),\n\t\t\tnew VectorXZ(-1,  0),\n\t\t\tnew VectorXZ(-1, +1),\n\t\t\tnew VectorXZ(+1, +1),\n\t\t\tnew VectorXZ(+1, -1),\n\t\t\tnew VectorXZ(-1, -1)));\n\n\tprivate SimplePolygonXZ p2 = new SimplePolygonXZ(asList(\n\t\t\tnew VectorXZ(-0.5, -0.5),\n\t\t\tnew VectorXZ(-0.5, +1.5),\n\t\t\tnew VectorXZ(+1.5, +1.5),\n\t\t\tnew VectorXZ(+1.5, -0.5),\n\t\t\tnew VectorXZ(-0.5, -0.5)));\n\t\t\n\t@Test\n\tpublic void testGetCentroid() {\n\t\t\t\t\n\t\tassertAlmostEquals(NULL_VECTOR, p1.getCentroid());\n\t\tassertAlmostEquals(NULL_VECTOR, p1.reverse().getCentroid());\n\n\t\tassertAlmostEquals(new VectorXZ(0.5, 0.5), p2.getCentroid());\n\t\tassertAlmostEquals(new VectorXZ(0.5, 0.5), p2.reverse().getCentroid());\n\t\t\t\t\n\t}\n\t\n\t@Test\n\tpublic void testGetArea() {\n\t\t\n\t\tassertAlmostEquals(4, p1.getArea());\n\t\tassertAlmostEquals(4, p1.reverse().getArea());\n\t\t\n\t\tassertAlmostEquals(4, p2.getArea());\n\t\tassertAlmostEquals(4, p2.reverse().getArea());\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testGetDiameter() {\n\t\t\n\t\tassertAlmostEquals(sqrt(8), p1.getDiameter());\n\t\tassertAlmostEquals(sqrt(8), p2.getDiameter());\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testGetSimplifiedPolygon() {\n\t\t\n\t\tassertEquals(p2, p2.getSimplifiedPolygon());\n\t\t\n\t\tassertEquals(4, p1.getSimplifiedPolygon().size());\n\t\tassertAlmostEquals(p1.getArea(), p1.getSimplifiedPolygon().getArea());\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testDistanceToSegments() {\n\t\t\n\t\tassertAlmostEquals(1, p1.distanceToSegments(NULL_VECTOR));\n\t\t\n\t\tassertAlmostEquals(0.5, p2.distanceToSegments(NULL_VECTOR));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testShift() {\n\n\t\tSimplePolygonXZ shiftP = p1.shift(VectorXZ.X_UNIT);\n\t\t\n\t\tassertSame(p1.size(), shiftP.size());\n\t\tassertAlmostEquals(new VectorXZ( 0, -1), shiftP.getVertexLoop().get(0));\n\t\tassertAlmostEquals(new VectorXZ( 0,  0), shiftP.getVertexLoop().get(1));\n\t\tassertAlmostEquals(new VectorXZ( 0, +1), shiftP.getVertexLoop().get(2));\n\t\tassertAlmostEquals(new VectorXZ( 2, +1), shiftP.getVertexLoop().get(3));\n\t\tassertAlmostEquals(new VectorXZ( 2, -1), shiftP.getVertexLoop().get(4));\n\t\tassertAlmostEquals(new VectorXZ( 0, -1), shiftP.getVertexLoop().get(5));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/TriangleXYZTest.java",
    "content": "package org.osm2world.core.math;\n\nimport static org.osm2world.core.math.VectorXYZ.*;\nimport static org.osm2world.core.test.TestUtil.assertAlmostEquals;\n\nimport org.junit.Test;\n\n\npublic class TriangleXYZTest {\n\n\t@Test\n\tpublic void testGetYAt() {\n\t\t\n\t\tTriangleXYZ t1 = new TriangleXYZ(X_UNIT, Z_UNIT, Y_UNIT);\n\t\t\n\t\tassertAlmostEquals(1, t1.getYAt(new VectorXZ(0, 0)));\n\t\t\n\t\tassertAlmostEquals(0, t1.getYAt(new VectorXZ(0, 1)));\n\t\tassertAlmostEquals(0, t1.getYAt(new VectorXZ(1, 0)));\n\t\tassertAlmostEquals(0, t1.getYAt(new VectorXZ(0.5, 0.5)));\n\t\tassertAlmostEquals(0, t1.getYAt(new VectorXZ(0.8, 0.2)));\n\n\t\tassertAlmostEquals(0.5, t1.getYAt(new VectorXZ(0, 0.5)));\n\t\tassertAlmostEquals(0.5, t1.getYAt(new VectorXZ(0.5, 0)));\n\t\tassertAlmostEquals(0.5, t1.getYAt(new VectorXZ(0.25, 0.25)));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/TriangleXZTest.java",
    "content": "package org.osm2world.core.math;\n\nimport static org.junit.Assert.*;\nimport static org.osm2world.core.test.TestUtil.assertAlmostEquals;\n\nimport org.junit.Test;\n\npublic class TriangleXZTest {\n\t\n\tprivate final TriangleXZ triangleCW = new TriangleXZ(\n\t\t\tnew VectorXZ( 0, 0),\n\t\t\tnew VectorXZ(-1, 1),\n\t\t\tnew VectorXZ(+1, 0));\n\t\n\tprivate final TriangleXZ triangleCCW = new TriangleXZ(\n\t\t\tnew VectorXZ( 0, 0),\n\t\t\tnew VectorXZ(-1, 1),\n\t\t\tnew VectorXZ(-2, 0));\n\t\n\t@Test\n\tpublic void testIsClockwise() {\n\t\tassertTrue(triangleCW.isClockwise());\n\t\tassertFalse(triangleCW.reverse().isClockwise());\n\t\tassertFalse(triangleCCW.isClockwise());\n\t\tassertTrue(triangleCCW.reverse().isClockwise());\n\t}\n\t\n\t@Test\n\tpublic void testGetArea() {\n\t\tassertAlmostEquals(triangleCW.getArea(), 0.5);\n\t\tassertAlmostEquals(triangleCCW.getArea(), 1);\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/VectorXYZTest.java",
    "content": "package org.osm2world.core.math;\n\nimport static org.osm2world.core.math.VectorXYZ.*;\nimport static org.osm2world.core.test.TestUtil.assertAlmostEquals;\n\nimport org.junit.Test;\n\npublic class VectorXYZTest {\n\t\n\t@Test\n\tpublic void testRotateX() {\n\t\t\n\t\tassertAlmostEquals(1, 0, 0, X_UNIT.rotateX(-0.42));\n\t\tassertAlmostEquals(0, 0, 1, Y_UNIT.rotateX(Math.PI/2));\n\t\tassertAlmostEquals(0,-1, 0, Z_UNIT.rotateX(Math.PI/2));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testRotateY() {\n\t\t\n\t\tassertAlmostEquals(0, 0, 1, X_UNIT.rotateY(-Math.PI/2));\n\t\tassertAlmostEquals(0, 1, 0, Y_UNIT.rotateY(Math.PI*0.42));\n\t\tassertAlmostEquals(1, 0, 0, Z_UNIT.rotateY(Math.PI/2));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testRotateZ() {\n\t\t\n\t\tassertAlmostEquals(0, 1, 0, X_UNIT.rotateZ(Math.PI/2));\n\t\tassertAlmostEquals(0,-1, 0, Y_UNIT.rotateZ(Math.PI));\n\t\tassertAlmostEquals(0, 0, 1, Z_UNIT.rotateZ(0.42));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/VectorXZTest.java",
    "content": "package org.osm2world.core.math;\n\nimport static java.lang.Math.PI;\nimport static org.osm2world.core.math.VectorXZ.*;\nimport static org.osm2world.core.test.TestUtil.assertAlmostEquals;\n\nimport org.junit.Test;\n\npublic class VectorXZTest {\n\t\n\t@Test\n\tpublic void testRightNormal() {\n\t\t\n\t\tVectorXZ xUnitRightNormal = VectorXZ.X_UNIT.rightNormal();\n\t\tassertAlmostEquals(0, -1, xUnitRightNormal);\n\t\t\n\t\tVectorXZ testA = new VectorXZ(0.5f, 0.5f);\n\t\tVectorXZ testARightNormal = testA.rightNormal();\n\t\tassertAlmostEquals(0.707f, -0.707f, testARightNormal);\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testAngle() {\n\t\t\n\t\tassertAlmostEquals(     0, new VectorXZ( 0, +1).angle());\n\t\tassertAlmostEquals(0.5*PI, new VectorXZ(+1,  0).angle());\n\t\tassertAlmostEquals(    PI, new VectorXZ( 0, -1).angle());\n\t\tassertAlmostEquals(1.5*PI, new VectorXZ(-1,  0).angle());\n\n\t\tassertAlmostEquals(0.25*PI, new VectorXZ(1, 1).angle());\n\t\tassertAlmostEquals(0.25*PI, new VectorXZ(5, 5).angle());\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testFromAngle() {\n\t\t\n\t\tassertAlmostEquals( 0, +1, fromAngle(0));\n\t\tassertAlmostEquals(+1,  0, fromAngle(0.5*PI));\n\t\tassertAlmostEquals( 0, -1, fromAngle(PI));\n\t\tassertAlmostEquals(-1,  0, fromAngle(1.5*PI));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testAngleBetween() {\n\t\t\n\t\tassertAlmostEquals(       0, angleBetween(X_UNIT, X_UNIT));\n\t\tassertAlmostEquals(       0, angleBetween(Z_UNIT, Z_UNIT));\n\t\tassertAlmostEquals(      PI, angleBetween(X_UNIT, X_UNIT.invert()));\n\t\tassertAlmostEquals(      PI, angleBetween(Z_UNIT, Z_UNIT.invert()));\n\t\tassertAlmostEquals(0.5 * PI, angleBetween(X_UNIT, Z_UNIT));\n\t\tassertAlmostEquals(0.5 * PI, angleBetween(Z_UNIT, X_UNIT));\n\t\tassertAlmostEquals(0.5 * PI, angleBetween(Z_UNIT, X_UNIT.mult(3)));\n\t\t\n\t\tassertAlmostEquals(0.25 * PI, angleBetween(\n\t\t\t\tX_UNIT, new VectorXZ(1, 1)));\n\t\tassertAlmostEquals(0.75 * PI, angleBetween(\n\t\t\t\tX_UNIT, new VectorXZ(-1, -1)));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/algorithms/CAGUtilTest.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport static java.util.Arrays.asList;\nimport static org.junit.Assert.assertSame;\nimport static org.osm2world.core.test.TestUtil.assertSameCyclicOrder;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.junit.Test;\nimport org.osm2world.core.math.PolygonWithHolesXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.math.algorithms.CAGUtil;\n\npublic class CAGUtilTest {\n\t\n\t@Test\n\tpublic void testOverlap() {\n\t\t\n\t\tList<VectorXZ> outline = asList(\n\t\t\t\tnew VectorXZ( 1,-2),\n\t\t\t\tnew VectorXZ( 1, 2),\n\t\t\t\tnew VectorXZ(-1, 2),\n\t\t\t\tnew VectorXZ(-1,-2),\n\t\t\t\tnew VectorXZ( 1,-2)\n\t\t);\n\t\t\n\t\tList<VectorXZ> subOutline = asList(\n\t\t\t\tnew VectorXZ( 2,-1),\n\t\t\t\tnew VectorXZ( 0,-1),\n\t\t\t\tnew VectorXZ( 0, 1),\n\t\t\t\tnew VectorXZ( 2, 1),\n\t\t\t\tnew VectorXZ( 2,-1)\n\t\t);\n\t\t\n\t\tList<PolygonWithHolesXZ> results = new ArrayList<PolygonWithHolesXZ>(\n\t\t\t\tCAGUtil.subtractPolygons(\n\t\t\t\t\t\tnew SimplePolygonXZ(outline),\n\t\t\t\t\t\tArrays.asList(new SimplePolygonXZ(subOutline))));\n\t\t\n\t\tassertSame(1, results.size());\n\t\t\n\t\tassertSameCyclicOrder(results.get(0).getOuter().getVertices(),\n\t\t\t\tnew VectorXZ( 1,-2),\n\t\t\t\tnew VectorXZ( 1,-1),\n\t\t\t\tnew VectorXZ( 0,-1),\n\t\t\t\tnew VectorXZ( 0, 1),\n\t\t\t\tnew VectorXZ( 1, 1),\n\t\t\t\tnew VectorXZ( 1, 2),\n\t\t\t\tnew VectorXZ(-1, 2),\n\t\t\t\tnew VectorXZ(-1,-2));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testOverlapCommonNode() {\n\t\t\n\t\tList<VectorXZ> outline = asList(\n\t\t\t\tnew VectorXZ( 1,-2),\n\t\t\t\tnew VectorXZ( 1, 2),\n\t\t\t\tnew VectorXZ(-1, 2),\n\t\t\t\tnew VectorXZ(-1,-2),\n\t\t\t\tnew VectorXZ( 1,-2)\n\t\t);\n\t\t\n\t\tList<VectorXZ> subOutline1 = asList(\n\t\t\t\tnew VectorXZ( 2,-1),\n\t\t\t\tnew VectorXZ( 0,-1),\n\t\t\t\tnew VectorXZ( 0, 0),\n\t\t\t\tnew VectorXZ( 2, 0),\n\t\t\t\tnew VectorXZ( 2,-1)\n\t\t);\n\t\t\n\t\tList<VectorXZ> subOutline2 = asList(\n\t\t\t\tnew VectorXZ( 2, 0),\n\t\t\t\tnew VectorXZ( 0, 0),\n\t\t\t\tnew VectorXZ( 0, 1),\n\t\t\t\tnew VectorXZ( 2, 1),\n\t\t\t\tnew VectorXZ( 2, 0)\n\t\t);\n\t\t\n\t\tList<PolygonWithHolesXZ> results = new ArrayList<PolygonWithHolesXZ>(\n\t\t\t\tCAGUtil.subtractPolygons(\n\t\t\t\t\t\tnew SimplePolygonXZ(outline),\n\t\t\t\t\t\tArrays.asList(\n\t\t\t\t\t\t\t\tnew SimplePolygonXZ(subOutline1),\n\t\t\t\t\t\t\t\tnew SimplePolygonXZ(subOutline2))));\n\t\t\n\t\tassertSame(1, results.size());\n\t\t\n\t\tList<VectorXZ> res = results.get(0).getOuter().getVertices();\n\t\t\n\t\tassertSameCyclicOrder(res,\n\t\t\t\tnew VectorXZ( 1,-2),\n\t\t\t\tnew VectorXZ( 1,-1),\n\t\t\t\tnew VectorXZ( 0,-1),\n\t\t\t\tnew VectorXZ( 0, 0),\n\t\t\t\tnew VectorXZ( 0, 1),\n\t\t\t\tnew VectorXZ( 1, 1),\n\t\t\t\tnew VectorXZ( 1, 2),\n\t\t\t\tnew VectorXZ(-1, 2),\n\t\t\t\tnew VectorXZ(-1,-2));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testOverlapConvex() {\n\t\t\n\t\tList<VectorXZ> outline = asList(\n\t\t\t\tnew VectorXZ( 1,-2),\n\t\t\t\tnew VectorXZ( 1, 2),\n\t\t\t\tnew VectorXZ(-1,-1),\n\t\t\t\tnew VectorXZ(-1,-2),\n\t\t\t\tnew VectorXZ( 1,-2)\n\t\t);\n\t\t\n\t\tList<VectorXZ> subOutline = asList(\n\t\t\t\tnew VectorXZ( 0,-3),\n\t\t\t\tnew VectorXZ(-2,-3),\n\t\t\t\tnew VectorXZ(-2, 0),\n\t\t\t\tnew VectorXZ( 0, 0),\n\t\t\t\tnew VectorXZ( 0,-3)\n\t\t);\n\t\t\n\t\tList<PolygonWithHolesXZ> results = new ArrayList<PolygonWithHolesXZ>(\n\t\t\t\tCAGUtil.subtractPolygons(\n\t\t\t\t\t\tnew SimplePolygonXZ(outline),\n\t\t\t\t\t\tArrays.asList(new SimplePolygonXZ(subOutline))));\n\t\t\n\t\tassertSame(1, results.size());\n\t\t\n\t\tList<VectorXZ> res = results.get(0).getOuter().getVertices();\n\t\t\n\t\tassertSameCyclicOrder(res,\n\t\t\t\tnew VectorXZ( 1,-2),\n\t\t\t\tnew VectorXZ( 1, 2),\n\t\t\t\tnew VectorXZ(-1/3.0, 0),\n\t\t\t\tnew VectorXZ( 0, 0),\n\t\t\t\tnew VectorXZ( 0,-2));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/algorithms/EarClippingTriangulationTest.java",
    "content": "package org.osm2world.core.math.algorithms;\nimport static org.junit.Assert.*;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport org.junit.Test;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXZ;\n\n\npublic class EarClippingTriangulationTest {\n\n\tprivate static final VectorXZ outlineA0 = new VectorXZ(-1.1f, -1.1f);\n\tprivate static final VectorXZ outlineA1 = new VectorXZ(-1.1f, 1.1f);\n\tprivate static final VectorXZ outlineA2 = new VectorXZ(1.1f, 1.1f);\n\tprivate static final VectorXZ outlineA3 = new VectorXZ(1.1f, -1.1f);\n\tprivate static final VectorXZ outlineA4 = new VectorXZ(0f, 1f);\n\t\t\n\tprivate static final List<VectorXZ> outlineA = Arrays.asList(\n\t\t\toutlineA0, outlineA1, outlineA2, outlineA3, outlineA0\n\t);\n\t\n\tprivate static final List<VectorXZ> outlineB = Arrays.asList(\n\t\t\toutlineA0, outlineA1, outlineA2, outlineA3, outlineA4, outlineA0\n\t);\n\t\n\tprivate static final SimplePolygonXZ holeA = new SimplePolygonXZ(Arrays.asList(\n\t\t\tnew VectorXZ(0, 0f),\n\t\t\tnew VectorXZ(1f, 0f),\n\t\t\tnew VectorXZ(0f, 1f),\n\t\t\tnew VectorXZ(0, 0f)\n\t));\n\t\n\tprivate static final SimplePolygonXZ holeB = new SimplePolygonXZ(Arrays.asList(\n\t\t\tnew VectorXZ(0.6f, 0.6f),\n\t\t\tnew VectorXZ(0.6f, 0f),\n\t\t\tnew VectorXZ(0f, 0.6f),\n\t\t\tnew VectorXZ(0.6f, 0.6f)\n\t));\n\t\t\n\t@Test\n\tpublic void testFindVisibleOutlineVertex1() {\n\t\t\n\t\tassertNotNull(EarClippingTriangulationUtil.findVisibleOutlineVertex(\n\t\t\t\t\t\toutlineA, new VectorXZ(0.55f, 0.55f),\n\t\t\t\t\t\tCollections.<SimplePolygonXZ>emptyList()));\n\t\t\t\t\n\t}\n\n\t@Test\n\tpublic void testFindVisibleOutlineVertex2() {\n\t\t\n\t\tassertEquals(outlineA2, outlineA.get(\n\t\t\t\tEarClippingTriangulationUtil.findVisibleOutlineVertex(\n\t\t\t\t\t\toutlineA, new VectorXZ(0.55f, 0.55f),\n\t\t\t\t\t\tArrays.asList(holeA))));\n\n\t}\n\n\t@Test\n\tpublic void testFindVisibleOutlineVertex3() {\n\t\t\n\t\tassertNull(\n\t\t\t\tEarClippingTriangulationUtil.findVisibleOutlineVertex(\n\t\t\t\t\t\toutlineA, new VectorXZ(0.55f, 0.55f),\n\t\t\t\t\t\tArrays.asList(holeA, holeB)));\n\t\t\t\t\n\t}\n\t\n\t/** rearrange, no invert */\n\t@Test\n\tpublic void testRearrangeOutline1() {\n\t\t\n\t\tList<VectorXZ> newOutline =\n\t\t\tEarClippingTriangulationUtil.rearrangeOutline(outlineA, 3, false);\n\t\t\n\t\tassertSame(5, newOutline.size());\n\t\tassertEquals(outlineA3, newOutline.get(0));\n\t\tassertEquals(outlineA0, newOutline.get(1));\n\t\tassertEquals(outlineA1, newOutline.get(2));\n\t\tassertEquals(outlineA2, newOutline.get(3));\n\t\tassertEquals(outlineA3, newOutline.get(4));\n\t\t\n\t}\n\t\n\t/** rearrange, invert */\n\t@Test\n\tpublic void testRearrangeOutline2() {\n\t\t\n\t\tList<VectorXZ> newOutline =\n\t\t\tEarClippingTriangulationUtil.rearrangeOutline(outlineA, 3, true);\n\t\t\n\t\tassertSame(5, newOutline.size());\n\t\tassertEquals(outlineA3, newOutline.get(0));\n\t\tassertEquals(outlineA2, newOutline.get(1));\n\t\tassertEquals(outlineA1, newOutline.get(2));\n\t\tassertEquals(outlineA0, newOutline.get(3));\n\t\tassertEquals(outlineA3, newOutline.get(4));\n\t\t\n\t}\n\t\n\t/** no invert, same start */\n\t@Test\n\tpublic void testRearrangeOutline3() {\n\t\t\n\t\tList<VectorXZ> newOutline =\n\t\t\tEarClippingTriangulationUtil.rearrangeOutline(outlineA, 0, false);\n\t\t\n\t\tassertSame(5, newOutline.size());\n\t\tassertEquals(outlineA0, newOutline.get(0));\n\t\tassertEquals(outlineA1, newOutline.get(1));\n\t\tassertEquals(outlineA2, newOutline.get(2));\n\t\tassertEquals(outlineA3, newOutline.get(3));\n\t\tassertEquals(outlineA0, newOutline.get(4));\n\t\t\n\t}\n\t\n\t/** invert, same start */\n\t@Test\n\tpublic void testRearrangeOutline4() {\n\t\t\n\t\tList<VectorXZ> newOutline =\n\t\t\tEarClippingTriangulationUtil.rearrangeOutline(outlineA, 0, true);\n\t\t\n\t\tassertSame(5, newOutline.size());\n\t\tassertEquals(outlineA0, newOutline.get(0));\n\t\tassertEquals(outlineA3, newOutline.get(1));\n\t\tassertEquals(outlineA2, newOutline.get(2));\n\t\tassertEquals(outlineA1, newOutline.get(3));\n\t\tassertEquals(outlineA0, newOutline.get(4));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testInsertVertexInPolygonOutline() {\n\t\t\n\t\tVectorXZ point = new VectorXZ(0.55f, 0.55f);\n\t\t\n\t\tList<VectorXZ> newOutline = new LinkedList<VectorXZ>(outlineA);\n\t\t\n\t\tEarClippingTriangulationUtil.insertVertexInPolygonOutline(newOutline, point);\n\t\t\n\t\tassertSame(7, newOutline.size());\n\t\tassertTrue(newOutline.contains(point));\n\t\t\t\t\n\t\tassertEquals(newOutline.get(newOutline.indexOf(point) - 1),\n\t\t\t\tnewOutline.get(newOutline.indexOf(point) + 1));\n\t\t\t\t\n\t}\n\t\n\t@Test\n\tpublic void testInsertHoleInPolygonOutline() {\n\t\t\n\t\tList<VectorXZ> newOutline = new LinkedList<VectorXZ>(outlineA);\n\t\t\n\t\tEarClippingTriangulationUtil.insertHoleInPolygonOutline(newOutline, holeB, Arrays.asList(holeA));\n\t\t\n\t\tassertSame(outlineA.size() + holeB.getVertexLoop().size() + 1,\n\t\t\t\tnewOutline.size());\n\t\t\n\t\tfor (VectorXZ innerVertex : holeB.getVertexLoop()) {\n\t\t\tassertTrue(newOutline.contains(innerVertex));\n\t\t}\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testTriangulateSimplePolygon() {\n\t\t\n\t\tList<TriangleXZ> triangles = new ArrayList<TriangleXZ>(\n\t\t\tEarClippingTriangulationUtil.triangulateSimplePolygon(new ArrayList<VectorXZ>(outlineA)));\n\t\t\n\t\tassertSame(triangles.size(), 2);\n\t\t\n\t\tCollection<VectorXZ> vsT0 =\n\t\t\tArrays.asList(triangles.get(0).v1,  triangles.get(0).v2,  triangles.get(0).v3);\n\t\tCollection<VectorXZ> vsT1 =\n\t\t\tArrays.asList(triangles.get(1).v1,  triangles.get(1).v2,  triangles.get(1).v3);\n\t\t\n\t\tif (vsT0.contains(outlineA0) && vsT0.contains(outlineA1) && vsT0.contains(outlineA2)) {\n\t\t\tassertTrue(vsT1.contains(outlineA0) && vsT1.contains(outlineA3) && vsT1.contains(outlineA2));\n\t\t} else if (vsT0.contains(outlineA1) && vsT0.contains(outlineA2) && vsT0.contains(outlineA3)) {\n\t\t\tassertTrue(vsT1.contains(outlineA1) && vsT1.contains(outlineA0) && vsT1.contains(outlineA3));\n\t\t} else if (vsT0.contains(outlineA2) && vsT0.contains(outlineA3) && vsT0.contains(outlineA0)) {\n\t\t\tassertTrue(vsT1.contains(outlineA2) && vsT1.contains(outlineA1) && vsT1.contains(outlineA0));\n\t\t} else if (vsT0.contains(outlineA3) && vsT0.contains(outlineA0) && vsT0.contains(outlineA1)) {\n\t\t\tassertTrue(vsT1.contains(outlineA3) && vsT1.contains(outlineA2) && vsT1.contains(outlineA1));\n\t\t} else {\n\t\t\tfail();\n\t\t}\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testIsConvex1() {\n\t\t\n\t\tfor (int i=0; i < outlineA.size(); i++) {\n\t\t\tassertTrue(EarClippingTriangulationUtil.isConvex(i, outlineA));\n\t\t}\n\t\t\n\t}\n\t\n\t@Test\n\tpublic void testIsEarTip1() {\n\t\t\n\t\tfor (int i=0; i < outlineA.size(); i++) {\n\t\t\tassertTrue(EarClippingTriangulationUtil.isEarTip(i, outlineA));\n\t\t}\n\t\t\n\t}\n\n\t@Test\n\tpublic void testIsConvex2() {\n\t\t\n\t\tList<VectorXZ> outlineNoDup = outlineB.subList(0, outlineB.size() - 1);\n\t\t\n\t\tfor (int i=0; i < outlineNoDup.size(); i++) {\n\t\t\tif (i == 4) {\n\t\t\t\tassertFalse(EarClippingTriangulationUtil.isConvex(i, outlineNoDup));\n\t\t\t} else {\n\t\t\t\tassertTrue(\"at \" + i, EarClippingTriangulationUtil.isConvex(i, outlineNoDup));\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t}\n\t\n\t@Test\n\tpublic void testIsEarTip2() {\n\t\t\n\t\tList<VectorXZ> outlineNoDup = outlineB.subList(0, outlineB.size() - 1);\n\t\t\n\t\tassertTrue(EarClippingTriangulationUtil.isEarTip(0, outlineNoDup));\n\t\tassertFalse(EarClippingTriangulationUtil.isEarTip(1, outlineNoDup));\n\t\tassertFalse(EarClippingTriangulationUtil.isEarTip(2, outlineNoDup));\n\t\tassertTrue(EarClippingTriangulationUtil.isEarTip(3, outlineNoDup));\n\t\tassertFalse(EarClippingTriangulationUtil.isEarTip(4, outlineNoDup));\n\t\t\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/algorithms/NormalCalculationUtilTest.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport static java.util.Arrays.asList;\nimport static org.junit.Assert.assertEquals;\nimport static org.osm2world.core.math.VectorXYZ.*;\nimport static org.osm2world.core.test.TestUtil.assertAlmostEquals;\n\nimport java.util.List;\n\nimport org.junit.Test;\nimport org.osm2world.core.math.VectorXYZ;\n\n\npublic class NormalCalculationUtilTest {\n\n\t@Test\n\tpublic final void testCalculateTriangleStripNormals() {\n\t\t\n\t\tList<VectorXYZ> vs = asList(\n\t\t\t\tNULL_VECTOR,\n\t\t\t\tnew VectorXYZ(1, 0, -1),\n\t\t\t\tnew VectorXYZ(1, 0, 0),\n\t\t\t\tnew VectorXYZ(1, 1, 0)\n\t\t\t\t);\n\t\t\n\t\tList<VectorXYZ> normals =\n\t\t\t\tNormalCalculationUtil.calculateTriangleStripNormals(vs, true);\n\t\t\n\t\tassertEquals(4, normals.size());\n\t\tassertAlmostEquals(Y_UNIT, normals.get(2));\n\t\tassertAlmostEquals(X_UNIT.invert(), normals.get(3));\n\t\t\n\t}\n\t\n\t@Test\n\tpublic final void testCalculateTriangleFanNormals() {\n\t\t\n\t\tList<VectorXYZ> vs = asList(\n\t\t\t\tNULL_VECTOR,\n\t\t\t\tnew VectorXYZ(1, 0, -1),\n\t\t\t\tnew VectorXYZ(1, 0, 0),\n\t\t\t\tnew VectorXYZ(1, 1, 0)\n\t\t\t\t);\n\t\t\n\t\tList<VectorXYZ> normals =\n\t\t\t\tNormalCalculationUtil.calculateTriangleFanNormals(vs, true);\n\t\t\n\t\tassertEquals(4, normals.size());\n\t\tassertAlmostEquals(Y_UNIT, normals.get(2));\n\t\tassertAlmostEquals(Z_UNIT.invert(), normals.get(3));\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/algorithms/Poly2TriTriangulationUtilTest.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport static org.osm2world.core.test.TestUtil.assertAlmostEquals;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.junit.Test;\nimport org.osm2world.core.math.LineSegmentXZ;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.util.exception.TriangulationException;\n\n\npublic class Poly2TriTriangulationUtilTest {\n\n\tprivate static final VectorXZ outlineA0 = new VectorXZ(-1.1f, -1.1f);\n\tprivate static final VectorXZ outlineA1 = new VectorXZ(-1.1f, 1.1f);\n\tprivate static final VectorXZ outlineA2 = new VectorXZ(1.1f, 1.1f);\n\tprivate static final VectorXZ outlineA3 = new VectorXZ(1.1f, -1.1f);\n\t\n\tprivate static final List<VectorXZ> outlineA = Arrays.asList(outlineA0,\n\t\t\toutlineA1, outlineA2, outlineA3, outlineA0);\n\n\tprivate static final List<VectorXZ> outlineB = Arrays.asList(\n\t\t\tnew VectorXZ(100, 0),\n\t\t\tnew VectorXZ(0, 100),\n\t\t\tnew VectorXZ(-99, 0),\n\t\t\tnew VectorXZ(0, -99),\n\t\t\tnew VectorXZ(100, 0));\n\t\n\t@Test\n\tpublic void triangulateTest() throws TriangulationException {\n\t\t\n\t\tSimplePolygonXZ polygon = new SimplePolygonXZ(outlineB);\n\t\t\n\t\tList<TriangleXZ> triangles = Poly2TriTriangulationUtil.triangulate(\n\t\t\t\tpolygon,\n\t\t\t\tCollections.<SimplePolygonXZ>emptyList(),\n\t\t\t\tCollections.<LineSegmentXZ>emptyList(),\n\t\t\t\tCollections.<VectorXZ>emptyList());\n\t\t\n\t\tdouble triangleArea = 0;\n\t\t\n\t\tfor (TriangleXZ t : triangles) {\n\t\t\ttriangleArea += t.getArea();\n\t\t}\n\t\t\n\t\tassertAlmostEquals(polygon.getArea(), triangleArea);\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/math/algorithms/TriangulationInputSanitizerTest.java",
    "content": "package org.osm2world.core.math.algorithms;\n\nimport static java.util.Collections.emptyList;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertSame;\nimport static org.junit.Assert.assertTrue;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.junit.Test;\nimport org.osm2world.core.math.SimplePolygonXZ;\nimport org.osm2world.core.math.TriangleXZ;\nimport org.osm2world.core.math.VectorXZ;\n\npublic class TriangulationInputSanitizerTest {\n\n\t@Test\n\tpublic void nearCollinearChainGetsSimplifiedAtOneCentimeter() {\n\n\t\tSimplePolygonXZ polygon = building364Outline();\n\n\t\tTriangulationInputSanitizer.SanitizedPolygonData sanitized =\n\t\t\t\tTriangulationInputSanitizer.sanitize(polygon, emptyList(), 0.01);\n\n\t\tassertEquals(7, sanitized.getRemovedOuterVertices());\n\t\tassertEquals(8, sanitized.getOuterPolygon().size());\n\t}\n\n\t@Test\n\tpublic void smallerToleranceKeepsMostVertices() {\n\n\t\tSimplePolygonXZ polygon = building364Outline();\n\n\t\tTriangulationInputSanitizer.SanitizedPolygonData sanitized =\n\t\t\t\tTriangulationInputSanitizer.sanitize(polygon, emptyList(), 0.001);\n\n\t\tassertEquals(2, sanitized.getRemovedOuterVertices());\n\t\tassertEquals(13, sanitized.getOuterPolygon().size());\n\t}\n\n\t@Test\n\tpublic void disabledSimplificationKeepsOriginalPolygon() {\n\n\t\tSimplePolygonXZ polygon = building364Outline();\n\n\t\tTriangulationInputSanitizer.SanitizedPolygonData sanitized =\n\t\t\t\tTriangulationInputSanitizer.sanitize(polygon, emptyList(), 0.0);\n\n\t\tassertSame(polygon, sanitized.getOuterPolygon());\n\t\tassertEquals(0, sanitized.getRemovedTotalVertices());\n\t}\n\n\t@Test\n\tpublic void invalidSimplificationFallsBackToOriginalRing() {\n\n\t\tSimplePolygonXZ polygon = new SimplePolygonXZ(loop(Arrays.asList(\n\t\t\t\tnew VectorXZ(0, 0),\n\t\t\t\tnew VectorXZ(4, 0),\n\t\t\t\tnew VectorXZ(4, 4),\n\t\t\t\tnew VectorXZ(3, 4),\n\t\t\t\tnew VectorXZ(3, 1),\n\t\t\t\tnew VectorXZ(1, 1),\n\t\t\t\tnew VectorXZ(1, 4),\n\t\t\t\tnew VectorXZ(0, 4)\n\t\t)));\n\n\t\tTriangulationInputSanitizer.SanitizedPolygonData sanitized =\n\t\t\t\tTriangulationInputSanitizer.sanitize(polygon, emptyList(), 3.0);\n\n\t\tassertSame(polygon, sanitized.getOuterPolygon());\n\t\tassertEquals(1, sanitized.getFallbackRingCount());\n\t\tassertEquals(0, sanitized.getRemovedTotalVertices());\n\t}\n\n\t@Test\n\tpublic void building364RegressionAvoidsSliverTrianglesByDefaultTolerance() {\n\n\t\tSimplePolygonXZ polygon = building364Outline();\n\n\t\tList<TriangleXZ> legacyTriangles = TriangulationUtil.triangulate(\n\t\t\t\tpolygon, emptyList(), emptyList(), 0.0);\n\t\tdouble legacyMinArea = minArea(legacyTriangles);\n\t\tassertTrue(\"legacy min area should include slivers\", legacyMinArea < 0.01);\n\n\t\tList<TriangleXZ> fixedTriangles = TriangulationUtil.triangulate(\n\t\t\t\tpolygon, emptyList(), emptyList(), 0.01);\n\t\tdouble fixedMinArea = minArea(fixedTriangles);\n\t\tassertTrue(\n\t\t\t\t\"fixed min area should avoid sliver triangles, got \" + fixedMinArea,\n\t\t\t\tfixedMinArea >= 0.01);\n\t}\n\n\tprivate static SimplePolygonXZ building364Outline() {\n\t\treturn new SimplePolygonXZ(loop(Arrays.asList(\n\t\t\t\tnew VectorXZ(-16.273, -24.972),\n\t\t\t\tnew VectorXZ(-19.639, -6.672),\n\t\t\t\tnew VectorXZ(-16.047, -6.004),\n\t\t\t\tnew VectorXZ(-20.484, 18.119),\n\t\t\t\tnew VectorXZ(-13.134, 19.477),\n\t\t\t\tnew VectorXZ(-12.438, 15.704),\n\t\t\t\tnew VectorXZ(-11.321, 9.626),\n\t\t\t\tnew VectorXZ(-10.183, 3.447),\n\t\t\t\tnew VectorXZ(-9.067, -2.608),\n\t\t\t\tnew VectorXZ(-8.619, -5.057),\n\t\t\t\tnew VectorXZ(-12.344, -5.748),\n\t\t\t\tnew VectorXZ(-11.636, -9.588),\n\t\t\t\tnew VectorXZ(-10.498, -15.766),\n\t\t\t\tnew VectorXZ(-9.382, -21.855),\n\t\t\t\tnew VectorXZ(-9.05, -23.637)\n\t\t)));\n\t}\n\n\tprivate static List<VectorXZ> loop(List<VectorXZ> vertices) {\n\t\tList<VectorXZ> loop = new ArrayList<VectorXZ>(vertices.size() + 1);\n\t\tloop.addAll(vertices);\n\t\tloop.add(vertices.get(0));\n\t\treturn loop;\n\t}\n\n\tprivate static double minArea(List<TriangleXZ> triangles) {\n\t\tdouble minArea = Double.MAX_VALUE;\n\t\tfor (TriangleXZ triangle : triangles) {\n\t\t\tminArea = Math.min(minArea, triangle.getArea());\n\t\t}\n\t\treturn minArea;\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/osm/creation/OsmosisReaderTest.java",
    "content": "package org.osm2world.core.osm.creation;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.List;\n\nimport static org.junit.Assert.*;\nimport org.junit.Test;\nimport org.osm2world.core.osm.creation.OsmosisReader;\nimport org.osm2world.core.osm.data.OSMData;\nimport org.osm2world.core.osm.data.OSMNode;\nimport org.osm2world.core.osm.data.OSMRelation;\n\n\npublic class OsmosisReaderTest {\n\t\n\t@Test\n\tpublic void testValidFile() throws IOException {\n\t\t\n\t\tFile testFile = new File(\"test\"+File.separator+\"files\"\n\t\t\t\t+File.separator+\"validFile.osm\");\n\t\tOSMData osmData = new OsmosisReader(testFile).getData();\n\t\t\n\t\tassertSame(4, osmData.getNodes().size());\n\t\tassertSame(1, osmData.getWays().size());\n\t\tassertSame(1, osmData.getRelations().size());\n\t\t\n\t\tList<OSMNode> wayNodes = osmData.getWays().iterator().next().nodes;\n\t\tassertSame(3, wayNodes.size());\n\t\t\n\t\tassertEquals(\"traffic_signals\", wayNodes.get(1).tags.getValue(\"highway\"));\n\t\t\n\t\tOSMRelation relation = osmData.getRelations().iterator().next();\t\t\n\t\tassertEquals(\"associatedStreet\", relation.tags.getValue(\"type\"));\n\t\t\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/target/common/FaceTargetTest.java",
    "content": "package org.osm2world.core.target.common;\n\nimport static org.junit.Assert.assertSame;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.junit.Test;\nimport org.osm2world.core.math.TriangleXYZ;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.target.common.FaceTarget.Face;\nimport org.osm2world.core.target.common.FaceTarget.IsolatedTriangle;\n\n\npublic class FaceTargetTest {\n\n\t@Test\n\tpublic void testCombineTrianglesToFaces() {\n\t\t\n\t\tVectorXYZ bottomLeft = new VectorXYZ(-1, 0, 0);\n\t\tVectorXYZ bottomCenter = new VectorXYZ(0, 0, 0);\n\t\tVectorXYZ bottomRight = new VectorXYZ(+1, 0, 0);\n\t\tVectorXYZ centerLeft = new VectorXYZ(-1, 1, 0);\n\t\tVectorXYZ center = new VectorXYZ(0, 1, 0);\n\t\tVectorXYZ centerRight = new VectorXYZ(+1, 1, 0);\n\t\tVectorXYZ topCenter = new VectorXYZ(0, 2, 0);\n\t\t\n\t\tList<IsolatedTriangle> isolatedTriangles = new ArrayList<IsolatedTriangle>();\n\t\t\n\t\tisolatedTriangles.add(triangle(centerLeft, bottomLeft, center));\n\t\tisolatedTriangles.add(triangle(bottomLeft, bottomCenter, center));\n\t\tisolatedTriangles.add(triangle(center, bottomCenter, centerRight));\n\t\tisolatedTriangles.add(triangle(bottomCenter, bottomRight, centerRight));\n\t\tisolatedTriangles.add(triangle(centerLeft, center, topCenter));\n\t\tisolatedTriangles.add(triangle(topCenter, center, centerRight));\n\t\t\n\t\tCollection<Face> faces =\n\t\t\t\tFaceTarget.combineTrianglesToFaces(isolatedTriangles);\n\t\t\n\t\tassertSame(1, faces.size());\n\t\tassertSame(6, faces.iterator().next().vs.size());\n\t\t\n\t}\n\t\n\tprivate static final IsolatedTriangle triangle(\n\t\t\tVectorXYZ v1, VectorXYZ v2, VectorXYZ v3) {\n\t\t\n\t\tTriangleXYZ triangleXYZ = new TriangleXYZ(v1, v2, v3);\n\t\t\n\t\treturn new IsolatedTriangle(\n\t\t\t\ttriangleXYZ, triangleXYZ.getNormal(),\n\t\t\t\t0, Collections.<List<VectorXZ>>emptyList());\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/test/TestUtil.java",
    "content": "package org.osm2world.core.test;\n\nimport static org.junit.Assert.fail;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.openstreetmap.josm.plugins.graphview.core.data.EmptyTagGroup;\nimport org.osm2world.core.math.VectorXYZ;\nimport org.osm2world.core.math.VectorXZ;\nimport org.osm2world.core.osm.data.OSMNode;\n\npublic final class TestUtil {\n\t\n\tprivate TestUtil() {}\n\t\n\t/**\n\t * returns a list of nodes where yon don't care about the attributes\n\t */\n\tpublic static final List<OSMNode> createTestNodes(int numberOfNodes) {\n\t\tList<OSMNode> result = new ArrayList<OSMNode>(numberOfNodes);\n\t\tfor (int i = 0; i < numberOfNodes; i++) {\n\t\t\tresult.add(new OSMNode(i, i, EmptyTagGroup.EMPTY_TAG_GROUP, i));\n\t\t}\n\t\treturn result;\n\t}\n\t\n\t/**\n\t * returns VectorXZ objects;\n\t * can be used to test properties that need to be true for all vectors.\n\t * \n\t * Will contain some fixed common vectors\n\t * and a lot of random vectors (that will be the same every time, though,\n\t * so tests can be reproduced).\n\t */\n\tpublic static final Iterable<VectorXZ> anyVectorXZ() {\n\t\treturn Arrays.asList(\n\t\t\t\tVectorXZ.NULL_VECTOR,\n\t\t\t\tVectorXZ.X_UNIT, VectorXZ.Z_UNIT,\n\t\t\t\tVectorXZ.X_UNIT.invert(), VectorXZ.Z_UNIT.invert());\n\t\t\n\t\t//TODO (test): more + random vectors\n\t\t\n\t}\n\t\n\tpublic static final void assertAlmostEquals(\n\t\t\tdouble expected, double actual) {\n\t\tif (Math.abs(expected - actual) > 0.001) {\n\t\t\tthrow new AssertionError(\"expected \" + expected + \", was \" + actual);\n\t\t}\n\t}\n\t\n\tpublic static final void assertAlmostEquals(\n\t\t\tVectorXZ expected, VectorXZ actual) {\n\t\tassertAlmostEquals(expected.x, actual.x);\n\t\tassertAlmostEquals(expected.z, actual.z);\n\t}\n\t\n\tpublic static void assertAlmostEquals(\n\t\t\tdouble expectedX, double expectedZ,\n\t\t\tVectorXZ actual) {\n\t\tassertAlmostEquals(expectedX, actual.x);\n\t\tassertAlmostEquals(expectedZ, actual.z);\n\t}\n\t\t\n\tpublic static final void assertAlmostEquals(\n\t\t\tVectorXYZ expected, VectorXYZ actual) {\n\t\tassertAlmostEquals(expected.x, actual.x);\n\t\tassertAlmostEquals(expected.y, actual.y);\n\t\tassertAlmostEquals(expected.z, actual.z);\n\t}\n\t\n\tpublic static final void assertAlmostEquals(\n\t\t\tdouble expectedX, double expectedY, double expectedZ,\n\t\t\tVectorXYZ actual) {\n\t\tassertAlmostEquals(expectedX, actual.x);\n\t\tassertAlmostEquals(expectedY, actual.y);\n\t\tassertAlmostEquals(expectedZ, actual.z);\n\t}\n\t\n\tpublic static final void assertSameCyclicOrder(\n\t\t\tList<VectorXZ> actual, VectorXZ... expected) {\n\t\t\n\t\tCollections.reverse(actual);\n\t\t\n\t\tif (actual.size() != expected.length) {\n\t\t\tfail(\"expected size\" + expected.length +\n\t\t\t\t\t\", found list of size \" + actual.size());\n\t\t}\n\t\t\n\t\tfor (int offset = 0; offset < actual.size(); offset++) {\n\t\t\t\n\t\t\tboolean matches = true;\n\t\t\t\n\t\t\tfor (int i = 0; i < actual.size(); i++) {\n\t\t\t\tint iWithOffset = (i + offset) % actual.size();\n\t\t\t\tif (VectorXZ.distance(expected[i],\n\t\t\t\t\t\tactual.get(iWithOffset)) > 0.0001) {\n\t\t\t\t\tmatches = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (matches) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t}\n\t\t\n\t\tfail(\"cannot match list to expected sequence. Found \" + actual);\n\t\t\n\t}\n\t\n}"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/util/TriangulationConfigTest.java",
    "content": "package org.osm2world.core.util;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\npublic class TriangulationConfigTest {\n\n\t@Test\n\tpublic void parseCollinearToleranceNullUsesDefault() {\n\t\tassertEquals(\n\t\t\t\tTriangulationConfig.DEFAULT_COLLINEAR_TOLERANCE_M,\n\t\t\t\tTriangulationConfig.parseCollinearToleranceMeters(null),\n\t\t\t\t0.0);\n\t}\n\n\t@Test\n\tpublic void parseCollinearToleranceInvalidDisablesSimplification() {\n\t\tassertEquals(\n\t\t\t\t0.0,\n\t\t\t\tTriangulationConfig.parseCollinearToleranceMeters(\"not-a-number\"),\n\t\t\t\t0.0);\n\t}\n\n\t@Test\n\tpublic void parseCollinearToleranceNonPositiveDisablesSimplification() {\n\t\tassertEquals(\n\t\t\t\t0.0,\n\t\t\t\tTriangulationConfig.parseCollinearToleranceMeters(\"0\"),\n\t\t\t\t0.0);\n\t\tassertEquals(\n\t\t\t\t0.0,\n\t\t\t\tTriangulationConfig.parseCollinearToleranceMeters(\"-0.5\"),\n\t\t\t\t0.0);\n\t}\n\n\t@Test\n\tpublic void parseCollinearTolerancePositiveUsesProvidedValue() {\n\t\tassertEquals(\n\t\t\t\t0.125,\n\t\t\t\tTriangulationConfig.parseCollinearToleranceMeters(\"0.125\"),\n\t\t\t\t0.0);\n\t}\n\n}\n"
  },
  {
    "path": "OSM2World/test/org/osm2world/core/world/modules/PowerModuleTest.java",
    "content": "package org.osm2world.core.world.modules;\n\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.EMPTY_LIST;\nimport static org.junit.Assert.assertEquals;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.junit.Test;\nimport org.openstreetmap.josm.plugins.graphview.core.data.MapBasedTagGroup;\nimport org.openstreetmap.josm.plugins.graphview.core.data.Tag;\nimport org.osm2world.core.ConversionFacade;\nimport org.osm2world.core.osm.data.OSMData;\nimport org.osm2world.core.osm.data.OSMNode;\nimport org.osm2world.core.osm.data.OSMWay;\nimport org.osm2world.core.target.Target;\nimport org.osm2world.core.target.statistics.StatisticsTarget;\nimport org.osm2world.core.target.statistics.StatisticsTarget.Stat;\nimport org.osm2world.core.world.creation.WorldModule;\n\n\npublic class PowerModuleTest {\n\t\n\t@Test\n\tpublic void testRepeatedRendering() throws Exception {\n\t\t\n\t\t/* create fake data */\n\t\t\n\t\tList<OSMNode> nodes = asList(\n\t\t\t\tnew OSMNode(0, 0, new MapBasedTagGroup(new Tag(\"power\",\"tower\")), 101),\n\t\t\t\tnew OSMNode(0, 0.001, new MapBasedTagGroup(new Tag(\"power\",\"tower\")), 102)\n\t\t\t\t);\n\t\t\n\t\tList<OSMWay> ways = asList(\n\t\t\t\tnew OSMWay(new MapBasedTagGroup(new Tag(\"power\",\"line\"), new Tag(\"cables\",\"4\")), 201, nodes)\n\t\t\t\t);\n\t\t\n\t\tOSMData osmData = new OSMData(EMPTY_LIST, nodes, ways, EMPTY_LIST);\n\t\t\n\t\t/* render to multiple targets */\n\t\t\n\t\tConversionFacade cf = new ConversionFacade();\n\t\t\n\t\tStatisticsTarget t1 = new StatisticsTarget();\n\t\tStatisticsTarget t2 = new StatisticsTarget();\n\t\t\n\t\tList<Target<?>> targets = Arrays.<Target<?>>asList(t1, t2);\n\t\tList<WorldModule> modules = Collections.<WorldModule>singletonList(new PowerModule());\n\t\t\n\t\tcf.createRepresentations(osmData, modules, null, targets);\n\t\t\n\t\t/* check whether the results are the same each time */\n\t\t\n\t\tfor (Stat stat : Stat.values()) {\n\t\t\tassertEquals(t1.getGlobalCount(stat), t2.getGlobalCount(stat));\n\t\t}\n\t\t\n\t}\n\t\n}\n"
  },
  {
    "path": "README.md",
    "content": "# Touch Mapper\n\nTactile map maker. Creates files for 3D or embossing printing from OpenStreetMap data. Lives at https://touch-mapper.org\n\nSee [GitHub issues](https://github.com/skarkkai/touch-mapper/issues) (for bug reports) and [discussions](https://github.com/skarkkai/touch-mapper/discussions).\n\nFurther documentation in [doc/](doc).\n\n"
  },
  {
    "path": "TODO",
    "content": "Use base rank only for pruning roads, to hopefully get fewer gaps. not much else we can do.\n\nShow also train tracks in big-roads mode\n\nAlerting of increased error rates over a day, or usage drops over a week\n\nDon't fetch buildings in no-buildings mode\n\nStore timestamps as timestamp type in Athena (there is some planning conversation about this in codex)\n\n\"My maps\" as designed in https://chatgpt.com/g/g-p-6993614b5cdc8191b9fa78956051dceb-touch-mapper-coding/c/69938610-6a8c-838b-9de2-3db7048a9273\n"
  },
  {
    "path": "agpl-3.0.txt",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<http://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "bin/monitor-converter.sh",
    "content": "#!/bin/bash\n\nset -u\n\nscript_dir=\"$(cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd)\"\ndefault_work_dir=\"$script_dir/../runtime/1\"\nwork_dir=\"$default_work_dir\"\nrequest_log=\"\"\ninterval=1\ntop_n=25\ninclude_dmesg=true\nlog_file=\"\"\n\nusage() {\n    cat <<'EOF'\nUsage: ./bin/monitor-converter.sh [options]\n\nStreams converter diagnostics in one timestamped output:\n- vmstat\n- top RSS process snapshots\n- free/swapon snapshots\n- request.log / poller.log / latest-failure.log / prev-request.log tails\n- kernel OOM lines (if readable)\n\nOptions:\n  --work-dir PATH       Worker runtime directory (default: ../runtime/1)\n  --request-log PATH    Explicit request log path (default: <work-dir>/request.log)\n  --interval N          Sampling interval in seconds (default: 1)\n  --top-n N             Number of process rows per snapshot (default: 25)\n  --log-file PATH       Also write combined monitor output to PATH\n  --no-dmesg            Skip kernel OOM stream\n  -h, --help            Show this help\nEOF\n}\n\nwhile [[ $# -gt 0 ]]; do\n    case \"$1\" in\n        --work-dir)\n            work_dir=\"$2\"\n            shift 2\n            ;;\n        --request-log)\n            request_log=\"$2\"\n            shift 2\n            ;;\n        --interval)\n            interval=\"$2\"\n            shift 2\n            ;;\n        --top-n)\n            top_n=\"$2\"\n            shift 2\n            ;;\n        --log-file)\n            log_file=\"$2\"\n            shift 2\n            ;;\n        --no-dmesg)\n            include_dmesg=false\n            shift\n            ;;\n        -h|--help)\n            usage\n            exit 0\n            ;;\n        *)\n            echo \"Unknown option: $1\" >&2\n            usage >&2\n            exit 1\n            ;;\n    esac\ndone\n\nif [[ -z \"$request_log\" ]]; then\n    request_log=\"$work_dir/request.log\"\nfi\n\nif ! [[ \"$interval\" =~ ^[0-9]+([.][0-9]+)?$ ]]; then\n    echo \"--interval must be a positive number, got: $interval\" >&2\n    exit 1\nfi\n\nif ! [[ \"$top_n\" =~ ^[0-9]+$ ]]; then\n    echo \"--top-n must be an integer, got: $top_n\" >&2\n    exit 1\nfi\n\nif [[ -n \"$log_file\" ]]; then\n    log_dir=\"$(dirname \"$log_file\")\"\n    mkdir -p \"$log_dir\"\n    touch \"$log_file\"\n    exec > >(tee -a \"$log_file\") 2>&1\nfi\n\ntimestamp() {\n    date '+%Y-%m-%dT%H:%M:%S%z'\n}\n\nlog_line() {\n    local source=\"$1\"\n    local line=\"$2\"\n    printf '%s [%s] %s\\n' \"$(timestamp)\" \"$source\" \"$line\"\n}\n\nprefix_stream() {\n    local source=\"$1\"\n    while IFS= read -r line; do\n        log_line \"$source\" \"$line\"\n    done\n}\n\ndeclare -a bg_pids=()\n\nstart_stream() {\n    local source=\"$1\"\n    local script=\"$2\"\n    (\n        bash -lc \"$script\" 2>&1 | prefix_stream \"$source\"\n    ) &\n    bg_pids+=(\"$!\")\n}\n\ncleanup() {\n    for pid in \"${bg_pids[@]}\"; do\n        kill \"$pid\" 2>/dev/null || true\n    done\n    wait 2>/dev/null || true\n}\n\ntrap cleanup EXIT INT TERM\n\nlog_line \"monitor\" \"Starting converter monitor\"\nlog_line \"monitor\" \"work_dir=$work_dir request_log=$request_log interval=${interval}s top_n=$top_n include_dmesg=$include_dmesg log_file=${log_file:-<none>}\"\nlog_line \"monitor\" \"Press Ctrl+C to stop\"\n\nrequest_log_q=\"$(printf '%q' \"$request_log\")\"\npoller_log=\"$work_dir/poller.log\"\nlatest_failure_log=\"$work_dir/latest-failure.log\"\nprev_request_log=\"$work_dir/prev-request.log\"\npoller_log_q=\"$(printf '%q' \"$poller_log\")\"\nlatest_failure_log_q=\"$(printf '%q' \"$latest_failure_log\")\"\nprev_request_log_q=\"$(printf '%q' \"$prev_request_log\")\"\ninterval_q=\"$(printf '%q' \"$interval\")\"\ntop_n_q=\"$(printf '%q' \"$top_n\")\"\nswapon_snapshot_cmd=\"echo 'swapon summary unavailable on this host'\"\nif swapon --show --noheadings --output NAME,SIZE,USED,PRIO >/dev/null 2>&1; then\n    swapon_snapshot_cmd=\"swapon --show --noheadings --output NAME,SIZE,USED,PRIO\"\nelif swapon --show --noheadings >/dev/null 2>&1; then\n    swapon_snapshot_cmd=\"swapon --show --noheadings\"\nelif swapon -s >/dev/null 2>&1; then\n    swapon_snapshot_cmd=\"swapon -s\"\nfi\nswapon_snapshot_cmd_q=\"$(printf '%q' \"$swapon_snapshot_cmd\")\"\n\nstart_stream \"vmstat\" \"vmstat -SM ${interval_q}\"\nstart_stream \"ps-top\" \"while true; do ps -eo pid,ppid,comm,rss,vsz,etime,pcpu,pmem,args --sort=-rss | head -n ${top_n_q}; echo '---'; sleep ${interval_q}; done\"\nstart_stream \"mem-swap\" \"while true; do free -m; bash -lc ${swapon_snapshot_cmd_q}; echo '---'; sleep ${interval_q}; done\"\nstart_stream \"request.log\" \"mkdir -p \\\"\\$(dirname ${request_log_q})\\\"; touch ${request_log_q}; tail -n 200 -F ${request_log_q}\"\nstart_stream \"poller.log\" \"mkdir -p \\\"\\$(dirname ${poller_log_q})\\\"; touch ${poller_log_q}; tail -n 200 -F ${poller_log_q}\"\nstart_stream \"latest-failure.log\" \"mkdir -p \\\"\\$(dirname ${latest_failure_log_q})\\\"; touch ${latest_failure_log_q}; tail -n 120 -F ${latest_failure_log_q}\"\nstart_stream \"prev-request.log\" \"mkdir -p \\\"\\$(dirname ${prev_request_log_q})\\\"; touch ${prev_request_log_q}; tail -n 120 -F ${prev_request_log_q}\"\n\nif [[ \"$include_dmesg\" == \"true\" ]]; then\n    if dmesg --color=never -T >/dev/null 2>&1; then\n        start_stream \"kernel-oom\" \"dmesg --color=never -Tw | grep -Ei --line-buffered 'out of memory|oom-killer|killed process'\"\n    elif command -v sudo >/dev/null 2>&1 && sudo -n dmesg --color=never -T >/dev/null 2>&1; then\n        start_stream \"kernel-oom\" \"sudo -n dmesg --color=never -Tw | grep -Ei --line-buffered 'out of memory|oom-killer|killed process'\"\n    else\n        log_line \"monitor\" \"Skipping kernel OOM stream (dmesg not readable without privileges)\"\n    fi\nfi\n\nwait\n"
  },
  {
    "path": "bin/repro-until-failure.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nRepeatedly enqueue a large test map request and watch monitor log output until a\npremature/failed converter execution is detected.\n\nDesigned to work with monitor output generated by:\n  ./bin/monitor-converter.sh --log-file /tmp/touch-mapper-monitor.log\n\"\"\"\n\nimport argparse\nimport copy\nimport json\nimport os\nimport re\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport uuid\nfrom collections import deque\n\n\nLINE_RE = re.compile(r\"^(?P<ts>\\S+)\\s+\\[(?P<src>[^\\]]+)\\]\\s?(?P<msg>.*)$\")\nPROGRESS_STAGE_RE = re.compile(r\"PROGRESS\\s+(?P<stage>[^\\s]+)\")\nKV_FIELD_RE = re.compile(r\"(?P<key>[A-Za-z0-9_-]+)=(?P<value>\\\"(?:\\\\.|[^\\\"\\\\])*\\\"|[^\\s]+)\")\n\nDEFAULT_SQS_ENDPOINT = (\n    \"https://sqs.eu-west-1.amazonaws.com/730535225693/test-requests-touch-mapper\"\n)\n\n# Close to your existing stress case; script overrides requestId on each attempt.\nDEFAULT_REQUEST_TEMPLATE = {\n    \"addrShort\": \"Helsinki Central Railway Station\",\n    \"addrLong\": \"Helsinki Central Railway Station\",\n    \"printingTech\": \"3d\",\n    \"offsetX\": 0,\n    \"offsetY\": 0,\n    \"size\": 17,\n    \"hideLocationMarker\": True,\n    \"lon\": 24.9405947,\n    \"lat\": 60.1715453,\n    \"effectiveArea\": {\n        \"lonMin\": 24.932938538321324,\n        \"lonMax\": 24.948250861678673,\n        \"latMin\": 60.16773073799442,\n        \"latMax\": 60.17535986200558\n    },\n    \"scale\": 5000,\n    \"diameter\": 850,\n    \"multipartMode\": False,\n    \"multipartXpc\": 0,\n    \"multipartYpc\": 0,\n    \"advancedMode\": True,\n    \"requestId\": \"placeholder/unused\"\n}\n\n\ndef parse_args():\n    parser = argparse.ArgumentParser(\n        description=(\n            \"Enqueue repeated Touch Mapper test requests and stop when a failure \"\n            \"is detected from monitor log markers.\"\n        )\n    )\n    parser.add_argument(\n        \"--log-path\",\n        default=\"/tmp/touch-mapper-monitor.log\",\n        help=\"Path to live monitor log file (default: /tmp/touch-mapper-monitor.log)\"\n    )\n    parser.add_argument(\n        \"--sqs-endpoint\",\n        default=DEFAULT_SQS_ENDPOINT,\n        help=\"Unsigned SQS SendMessage endpoint (default: test queue endpoint)\"\n    )\n    parser.add_argument(\n        \"--request-template-json\",\n        help=\"Optional JSON file with request template body\"\n    )\n    parser.add_argument(\n        \"--request-id-prefix\",\n        default=\"Repro\",\n        help=\"Prefix for generated requestId values (default: Repro)\"\n    )\n    parser.add_argument(\n        \"--attempt-timeout-seconds\",\n        type=int,\n        default=1200,\n        help=\"Max seconds to wait for one attempt before marking as failed (default: 1200)\"\n    )\n    parser.add_argument(\n        \"--max-attempts\",\n        type=int,\n        default=0,\n        help=\"Stop after this many attempts even with no failure (0 means unlimited)\"\n    )\n    parser.add_argument(\n        \"--sleep-between-seconds\",\n        type=float,\n        default=1.0,\n        help=\"Delay between attempts (default: 1.0)\"\n    )\n    parser.add_argument(\n        \"--line-buffer-size\",\n        type=int,\n        default=220,\n        help=\"How many recent monitor lines to keep for failure context (default: 220)\"\n    )\n    return parser.parse_args()\n\n\ndef parse_kv_fields(text):\n    values = {}\n    for match in KV_FIELD_RE.finditer(text):\n        key = match.group(\"key\")\n        value = match.group(\"value\")\n        if len(value) >= 2 and value[0] == '\"' and value[-1] == '\"':\n            inner = value[1:-1]\n            inner = inner.replace('\\\\\"', '\"').replace('\\\\\\\\', '\\\\')\n            values[key] = inner\n        else:\n            values[key] = value\n    return values\n\n\ndef build_request_body(template, request_id_prefix):\n    request_body = copy.deepcopy(template)\n    request_token = uuid.uuid4().hex[:12]\n    request_body[\"requestId\"] = \"{prefix}{token}/Helsinki\".format(\n        prefix=request_id_prefix,\n        token=request_token\n    )\n    return request_body\n\n\ndef send_request_via_unsigned_sqs_get(sqs_endpoint, request_body):\n    params = {\n        \"Action\": \"SendMessage\",\n        \"MessageBody\": json.dumps(request_body, separators=(\",\", \":\")),\n        \"Version\": \"2012-11-05\"\n    }\n    url = sqs_endpoint + \"?\" + urllib.parse.urlencode(params)\n    req = urllib.request.Request(url=url, method=\"GET\")\n    with urllib.request.urlopen(req, timeout=35) as response:\n        response_text = response.read().decode(\"utf-8\", errors=\"replace\")\n        status_code = response.getcode()\n    if status_code != 200:\n        raise RuntimeError(\"SQS SendMessage failed with HTTP {}\".format(status_code))\n    if \"SendMessageResponse\" not in response_text and \"SendMessageResult\" not in response_text:\n        raise RuntimeError(\"SQS response did not look like SendMessageResponse\")\n    return response_text\n\n\ndef match_progress_for_request(message, request_id):\n    stage_match = PROGRESS_STAGE_RE.search(message)\n    if not stage_match:\n        return None\n    fields = parse_kv_fields(message)\n    message_request_id = fields.get(\"requestId\")\n    if message_request_id != request_id:\n        return None\n    status = fields.get(\"status\")\n    return (stage_match.group(\"stage\"), status)\n\n\ndef wait_for_attempt_outcome(log_path, request_id, start_offset, timeout_seconds, line_buffer_size):\n    recent_lines = deque(maxlen=line_buffer_size)\n    started = False\n    last_stage = None\n    last_status = None\n    deadline = time.time() + timeout_seconds\n\n    # Wait for log file to appear if it does not yet exist.\n    while not os.path.exists(log_path):\n        if time.time() >= deadline:\n            return {\n                \"ok\": False,\n                \"reason\": \"monitor log file did not appear in time\",\n                \"request_started\": False,\n                \"last_stage\": None,\n                \"last_status\": None,\n                \"recent_lines\": list(recent_lines),\n            }\n        time.sleep(0.3)\n\n    with open(log_path, \"r\", encoding=\"utf-8\", errors=\"replace\") as handle:\n        file_size = os.path.getsize(log_path)\n        if start_offset <= file_size:\n            handle.seek(start_offset)\n        else:\n            handle.seek(0)\n\n        while time.time() < deadline:\n            line = handle.readline()\n            if line == \"\":\n                # Handle truncation / replacement in streamed logs.\n                try:\n                    current_size = os.path.getsize(log_path)\n                    current_pos = handle.tell()\n                    if current_pos > current_size:\n                        handle.seek(0)\n                except OSError:\n                    pass\n                time.sleep(0.25)\n                continue\n\n            clean_line = line.rstrip(\"\\n\")\n            recent_lines.append(clean_line)\n\n            parsed = LINE_RE.match(clean_line)\n            if not parsed:\n                continue\n\n            source = parsed.group(\"src\")\n            message = parsed.group(\"msg\")\n\n            progress = match_progress_for_request(message, request_id)\n            if progress is not None:\n                stage, status = progress\n                started = True\n                last_stage = stage\n                last_status = status\n\n                if stage in (\"failed\", \"signal-received\"):\n                    return {\n                        \"ok\": False,\n                        \"reason\": \"stage {}\".format(stage),\n                        \"request_started\": started,\n                        \"last_stage\": last_stage,\n                        \"last_status\": last_status,\n                        \"recent_lines\": list(recent_lines),\n                    }\n\n                if stage == \"exit\":\n                    success = (status == \"success\")\n                    return {\n                        \"ok\": success,\n                        \"reason\": \"exit status={}\".format(status),\n                        \"request_started\": started,\n                        \"last_stage\": last_stage,\n                        \"last_status\": last_status,\n                        \"recent_lines\": list(recent_lines),\n                    }\n\n            # Poller-level hard exits (timeout/signal) are useful clues.\n            if started and source == \"poller.log\":\n                lower = message.lower()\n                if \"request processing exited with timeout\" in lower:\n                    return {\n                        \"ok\": False,\n                        \"reason\": \"poller timeout\",\n                        \"request_started\": started,\n                        \"last_stage\": last_stage,\n                        \"last_status\": last_status,\n                        \"recent_lines\": list(recent_lines),\n                    }\n                if \"request processing exited with sigkill\" in lower:\n                    return {\n                        \"ok\": False,\n                        \"reason\": \"poller sigkill\",\n                        \"request_started\": started,\n                        \"last_stage\": last_stage,\n                        \"last_status\": last_status,\n                        \"recent_lines\": list(recent_lines),\n                    }\n                if \"request processing exited with sigterm\" in lower:\n                    return {\n                        \"ok\": False,\n                        \"reason\": \"poller sigterm\",\n                        \"request_started\": started,\n                        \"last_stage\": last_stage,\n                        \"last_status\": last_status,\n                        \"recent_lines\": list(recent_lines),\n                    }\n\n            if started and source in (\"request.log\", \"prev-request.log\"):\n                if \"process-request failed:\" in message:\n                    return {\n                        \"ok\": False,\n                        \"reason\": \"process-request failed line\",\n                        \"request_started\": started,\n                        \"last_stage\": last_stage,\n                        \"last_status\": last_status,\n                        \"recent_lines\": list(recent_lines),\n                    }\n\n    timeout_reason = \"timeout waiting for terminal stage\"\n    if not started:\n        timeout_reason = \"no progress lines seen for request\"\n    return {\n        \"ok\": False,\n        \"reason\": timeout_reason,\n        \"request_started\": started,\n        \"last_stage\": last_stage,\n        \"last_status\": last_status,\n        \"recent_lines\": list(recent_lines),\n    }\n\n\ndef load_request_template(path):\n    if not path:\n        return copy.deepcopy(DEFAULT_REQUEST_TEMPLATE)\n    with open(path, \"r\", encoding=\"utf-8\") as handle:\n        return json.load(handle)\n\n\ndef print_recent_lines(lines):\n    print(\"---- recent monitor lines ----\")\n    for line in lines:\n        print(line)\n    print(\"---- end recent monitor lines ----\")\n\n\ndef main():\n    args = parse_args()\n    request_template = load_request_template(args.request_template_json)\n\n    print(\"Watching log: {}\".format(args.log_path))\n    print(\"Queue endpoint: {}\".format(args.sqs_endpoint))\n    if args.max_attempts > 0:\n        print(\"Will run at most {} attempts\".format(args.max_attempts))\n    else:\n        print(\"Will run attempts until first failure\")\n\n    attempt_index = 1\n    while True:\n        if args.max_attempts > 0 and attempt_index > args.max_attempts:\n            print(\"Reached max attempts with no detected failure.\")\n            return 0\n\n        request_body = build_request_body(request_template, args.request_id_prefix)\n        request_id = request_body[\"requestId\"]\n\n        try:\n            start_offset = os.path.getsize(args.log_path) if os.path.exists(args.log_path) else 0\n        except OSError:\n            start_offset = 0\n\n        print(\"\")\n        print(\"Attempt {} requestId={}\".format(attempt_index, request_id))\n        send_request_via_unsigned_sqs_get(args.sqs_endpoint, request_body)\n        print(\"queued request, waiting for outcome...\")\n\n        result = wait_for_attempt_outcome(\n            log_path=args.log_path,\n            request_id=request_id,\n            start_offset=start_offset,\n            timeout_seconds=args.attempt_timeout_seconds,\n            line_buffer_size=args.line_buffer_size,\n        )\n\n        if result[\"ok\"]:\n            print(\n                \"SUCCESS requestId={} terminal={} status={}\".format(\n                    request_id,\n                    result[\"last_stage\"],\n                    result[\"last_status\"],\n                )\n            )\n            attempt_index += 1\n            time.sleep(args.sleep_between_seconds)\n            continue\n\n        print(\n            \"FAILURE DETECTED requestId={} reason={} last_stage={} last_status={}\".format(\n                request_id,\n                result[\"reason\"],\n                result[\"last_stage\"],\n                result[\"last_status\"],\n            )\n        )\n        print_recent_lines(result[\"recent_lines\"])\n        return 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "bin/summarize-monitor-log.py",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport datetime as dt\nimport re\nfrom collections import defaultdict\nfrom typing import Dict, List, Optional, Tuple\n\n\nLINE_RE = re.compile(\n    r\"^(?P<ts>\\d{4}-\\d{2}-\\d{2}(?:[ T])\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?)\\s+\\[(?P<src>[^\\]]+)\\]\\s?(?P<msg>.*)$\"\n)\nMEM_RE = re.compile(\n    r\"(?:\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\s+\\[(?P<comp>[^\\]]+)\\]\\s+(?:>{2,}\\s+)?)?MEMORY\\s+(?P<stage>[^ ]+)\\s+.*VmHWM=(?P<hwm>[0-9]+)kB\"\n)\nPS_RE = re.compile(\n    r\"^\\s*(?P<pid>[0-9]+)\\s+(?P<ppid>[0-9]+)\\s+(?P<comm>\\S+)\\s+(?P<rss>[0-9]+)\\s+(?P<vsz>[0-9]+)\\s+\"\n    r\"(?P<etime>\\S+)\\s+(?P<pcpu>\\S+)\\s+(?P<pmem>\\S+)\\s+(?P<cmd>.+)$\"\n)\nSWAP_RE = re.compile(\n    r\"Swap:\\s+(?P<total>[0-9]+)\\s+(?P<used>[0-9]+)\\s+(?P<free>[0-9]+)\"\n)\nPOLL_START_RE = re.compile(r\"STARTING TO POLL AT\\s+(.+?)\\s*=*$\")\nPOLL_RETURN_RE = re.compile(r\"Poll returned at\\s+(.+)$\")\nPROC_ENTIRE_RE = re.compile(r\"Processing entire request took\\s+([0-9.]+)\")\nSUMMARY_ENTIRE_RE = re.compile(r\"SUMMARY request-entire \\(total\\s+([0-9.]+)s,\")\nOSM2WORLD_IGNORED_RE = re.compile(\n    r\"java\\.lang\\.IllegalStateException: no connector information has been set for this representation\\.\"\n)\nKERNEL_TS_IN_MSG_RE = re.compile(\n    r\"^\\[(?P<kts>[A-Z][a-z]{2}\\s+[A-Z][a-z]{2}\\s+\\d{1,2}\\s+\\d\\d:\\d\\d:\\d\\d\\s+\\d{4})\\]\"\n)\nPROGRESS_RE = re.compile(\n    r\"(?:\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\s+\\[(?P<component>[^\\]]+)\\]\\s+(?:>{2,}\\s+)?)?PROGRESS\\s+(?P<stage>[^\\s]+)\"\n)\nKV_FIELD_RE = re.compile(r\"(?P<key>[A-Za-z0-9_-]+)=(?P<value>\\\"(?:\\\\.|[^\\\"\\\\])*\\\"|[^\\s]+)\")\n\n\ndef parse_ts(raw: str) -> Optional[dt.datetime]:\n    normalized_raw = raw\n    tz_colon_match = re.search(r\"([+-]\\d{2}):(\\d{2})$\", normalized_raw)\n    if tz_colon_match:\n        normalized_raw = normalized_raw[:-6] + tz_colon_match.group(1) + tz_colon_match.group(2)\n\n    formats = [\n        \"%Y-%m-%dT%H:%M:%S%z\",\n        \"%Y-%m-%dT%H:%M:%S.%f%z\",\n        \"%Y-%m-%d %H:%M:%S.%f\",\n        \"%Y-%m-%d %H:%M:%S\",\n    ]\n    for fmt in formats:\n        try:\n            parsed = dt.datetime.strptime(normalized_raw, fmt)\n            if parsed.tzinfo is None:\n                parsed = parsed.replace(tzinfo=dt.timezone.utc)\n            return parsed\n        except ValueError:\n            continue\n    if raw.endswith(\"Z\"):\n        normalized = raw[:-1] + \"+0000\"\n        try:\n            return dt.datetime.strptime(normalized, \"%Y-%m-%dT%H:%M:%S.%f%z\")\n        except ValueError:\n            pass\n        try:\n            return dt.datetime.strptime(normalized, \"%Y-%m-%dT%H:%M:%S%z\")\n        except ValueError:\n            pass\n    return None\n\n\ndef parse_kv_fields(text: str) -> Dict[str, str]:\n    values = {}  # type: Dict[str, str]\n    for match in KV_FIELD_RE.finditer(text):\n        key = match.group(\"key\")\n        value = match.group(\"value\")\n        if len(value) >= 2 and value[0] == '\"' and value[-1] == '\"':\n            inner = value[1:-1]\n            inner = inner.replace('\\\\\"', '\"').replace('\\\\\\\\', '\\\\')\n            values[key] = inner\n        else:\n            values[key] = value\n    return values\n\n\ndef mib(kib: int) -> float:\n    return kib / 1024.0\n\n\ndef format_dt(value: Optional[dt.datetime]) -> str:\n    if value is None:\n        return \"n/a\"\n    return value.isoformat()\n\n\ndef main() -> int:\n    parser = argparse.ArgumentParser(\n        description=\"Summarize Touch Mapper monitor log output.\"\n    )\n    parser.add_argument(\n        \"log_path\",\n        nargs=\"?\",\n        default=\"/tmp/touch-mapper-monitor.log\",\n        help=\"Path to monitor log (default: /tmp/touch-mapper-monitor.log)\",\n    )\n    parser.add_argument(\n        \"--show-failure-lines\",\n        type=int,\n        default=12,\n        help=\"How many recent failure-signal lines to show (default: 12)\",\n    )\n    args = parser.parse_args()\n\n    start_ts = None  # type: Optional[dt.datetime]\n    end_ts = None  # type: Optional[dt.datetime]\n    line_count = 0\n\n    mem_peaks = {}  # type: Dict[str, Tuple[int, int, dt.datetime, str, str]]\n    # comp -> (hwm_kib, line_no, ts, stage, full_line)\n\n    max_swap_used = -1\n    max_swap_total = -1\n    max_swap_line = \"\"\n\n    max_si = -1\n    max_si_line = \"\"\n    max_so = -1\n    max_so_line = \"\"\n\n    max_rss_by_comm = {}  # type: Dict[str, Tuple[int, int, dt.datetime, str]]\n    # comm -> (rss_kib, line_no, ts, full_line)\n\n    ps_counts_by_ts = defaultdict(\n        lambda: {\"process_request\": 0, \"osm2world\": 0, \"blender\": 0}\n    )  # type: Dict[dt.datetime, Dict[str, int]]\n    max_rss_osm2world = (0, \"\", None)  # rss_kib, full_line, ts\n    max_rss_blender = (0, \"\", None)  # rss_kib, full_line, ts\n    max_rss_process_request = (0, \"\", None)  # rss_kib, full_line, ts\n\n    poll_starts = []  # type: List[str]\n    poll_returns = []  # type: List[str]\n    entire_durations = []  # type: List[float]\n    summary_entire_durations = []  # type: List[float]\n    ignored_connector_exception_count = 0\n\n    kernel_oom_lines = []  # type: List[Tuple[Optional[dt.datetime], str, Optional[dt.datetime]]]\n    failure_signal_lines = []  # type: List[Tuple[Optional[dt.datetime], str]]\n    progress_events = []  # type: List[Tuple[int, Optional[dt.datetime], str, str, str, Optional[str], Optional[str], str]]\n\n    failure_patterns = [\n        re.compile(r\"process-request failed\", re.IGNORECASE),\n        re.compile(r\"Can't convert map data to STL\", re.IGNORECASE),\n        re.compile(r\"request processing failed\", re.IGNORECASE),\n        re.compile(r\"subprocess failed with error code\", re.IGNORECASE),\n        re.compile(r\"timeout\", re.IGNORECASE),\n        re.compile(r\"timed out\", re.IGNORECASE),\n        re.compile(r\"\\bTraceback\\b\"),\n        re.compile(r\"\\bKilled process\\b\"),\n        re.compile(r\"\\bOut of memory\\b\"),\n        re.compile(r\"\\bTerminated\\b.*timeout .*process-request\\.py\"),\n    ]\n\n    with open(args.log_path, \"r\", encoding=\"utf-8\", errors=\"replace\") as handle:\n        for raw_line in handle:\n            line_count += 1\n            line = raw_line.rstrip(\"\\n\")\n\n            m_line = LINE_RE.match(line)\n            if not m_line:\n                continue\n\n            ts = parse_ts(m_line.group(\"ts\"))\n            src = m_line.group(\"src\")\n            msg = m_line.group(\"msg\")\n            if ts is not None:\n                if start_ts is None or ts < start_ts:\n                    start_ts = ts\n                if end_ts is None or ts > end_ts:\n                    end_ts = ts\n\n            m_mem = MEM_RE.search(msg)\n            if m_mem and ts is not None:\n                comp = m_mem.group(\"comp\") or src\n                stage = m_mem.group(\"stage\")\n                hwm_kib = int(m_mem.group(\"hwm\"))\n                prev = mem_peaks.get(comp)\n                if prev is None or hwm_kib > prev[0]:\n                    mem_peaks[comp] = (hwm_kib, line_count, ts, stage, line)\n\n            if src == \"mem-swap\":\n                m_swap = SWAP_RE.search(msg)\n                if m_swap:\n                    used = int(m_swap.group(\"used\"))\n                    total = int(m_swap.group(\"total\"))\n                    if used > max_swap_used:\n                        max_swap_used = used\n                        max_swap_total = total\n                        max_swap_line = line\n\n            if src == \"vmstat\":\n                cols = msg.split()\n                if cols and cols[0].isdigit() and len(cols) >= 8:\n                    si = int(cols[6])\n                    so = int(cols[7])\n                    if si > max_si:\n                        max_si = si\n                        max_si_line = line\n                    if so > max_so:\n                        max_so = so\n                        max_so_line = line\n\n            if src == \"ps-top\":\n                m_ps = PS_RE.match(msg)\n                if m_ps and ts is not None:\n                    comm = m_ps.group(\"comm\")\n                    rss_kib = int(m_ps.group(\"rss\"))\n                    cmd = m_ps.group(\"cmd\")\n\n                    prev_comm = max_rss_by_comm.get(comm)\n                    if prev_comm is None or rss_kib > prev_comm[0]:\n                        max_rss_by_comm[comm] = (rss_kib, line_count, ts, line)\n\n                    if \"process-request.py\" in cmd:\n                        ps_counts_by_ts[ts][\"process_request\"] += 1\n                        if rss_kib > max_rss_process_request[0]:\n                            max_rss_process_request = (rss_kib, line, ts)\n                    if \"OSM2World.jar\" in cmd:\n                        ps_counts_by_ts[ts][\"osm2world\"] += 1\n                        if rss_kib > max_rss_osm2world[0]:\n                            max_rss_osm2world = (rss_kib, line, ts)\n                    if \"/blender/blender\" in cmd and \"obj-to-tactile.py\" in cmd:\n                        ps_counts_by_ts[ts][\"blender\"] += 1\n                        if rss_kib > max_rss_blender[0]:\n                            max_rss_blender = (rss_kib, line, ts)\n\n            if src == \"request.log\":\n                m_poll_start = POLL_START_RE.search(msg)\n                if m_poll_start:\n                    poll_starts.append(m_poll_start.group(1))\n                m_poll_return = POLL_RETURN_RE.search(msg)\n                if m_poll_return:\n                    poll_returns.append(m_poll_return.group(1))\n                m_entire = PROC_ENTIRE_RE.search(msg)\n                if m_entire:\n                    entire_durations.append(float(m_entire.group(1)))\n                m_summary_entire = SUMMARY_ENTIRE_RE.search(msg)\n                if m_summary_entire:\n                    summary_entire_durations.append(float(m_summary_entire.group(1)))\n                if OSM2WORLD_IGNORED_RE.search(msg):\n                    ignored_connector_exception_count += 1\n\n            m_progress = PROGRESS_RE.search(msg)\n            if m_progress:\n                fields = parse_kv_fields(msg)\n                status = fields.get(\"status\")\n                request_id = fields.get(\"requestId\")\n                component = m_progress.group(\"component\") or src\n                progress_events.append(\n                    (\n                        line_count,\n                        ts,\n                        src,\n                        component,\n                        m_progress.group(\"stage\"),\n                        status,\n                        request_id,\n                        line,\n                    )\n                )\n\n            if src == \"kernel-oom\":\n                kernel_ts = None  # type: Optional[dt.datetime]\n                m_kernel_ts = KERNEL_TS_IN_MSG_RE.match(msg)\n                if m_kernel_ts:\n                    try:\n                        kernel_ts = dt.datetime.strptime(\n                            m_kernel_ts.group(\"kts\"),\n                            \"%a %b %d %H:%M:%S %Y\"\n                        )\n                    except ValueError:\n                        kernel_ts = None\n                kernel_oom_lines.append((ts, line, kernel_ts))\n\n            if any(pattern.search(msg) for pattern in failure_patterns):\n                failure_signal_lines.append((ts, line))\n\n    recent_window_seconds = 30\n    recent_cutoff = None  # type: Optional[dt.datetime]\n    if end_ts is not None:\n        recent_cutoff = end_ts - dt.timedelta(seconds=recent_window_seconds)\n\n    def is_recent(entry_ts: Optional[dt.datetime]) -> bool:\n        if recent_cutoff is None or entry_ts is None:\n            return False\n        return entry_ts >= recent_cutoff\n\n    max_concurrency = {\n        \"process_request\": (0, None),\n        \"osm2world\": (0, None),\n        \"blender\": (0, None),\n    }  # type: Dict[str, Tuple[int, Optional[dt.datetime]]]\n    for ts, counts in ps_counts_by_ts.items():\n        for key in max_concurrency:\n            if counts[key] > max_concurrency[key][0]:\n                max_concurrency[key] = (counts[key], ts)\n\n    print(\"Monitor Log Summary\")\n    print(\"log_path: {}\".format(args.log_path))\n    print(\"lines: {}\".format(line_count))\n    print(\"time_range: {} .. {}\".format(format_dt(start_ts), format_dt(end_ts)))\n\n    print(\"\\nRequest Flow\")\n    print(\"poll_starts: {}\".format(len(poll_starts)))\n    if poll_starts:\n        print(\"last_poll_start: {}\".format(poll_starts[-1]))\n    print(\"poll_returns: {}\".format(len(poll_returns)))\n    if poll_returns:\n        print(\"last_poll_return: {}\".format(poll_returns[-1]))\n    print(\"processing_entire_count: {}\".format(len(entire_durations)))\n    if entire_durations:\n        print(\"processing_entire_last_seconds: {:.3f}\".format(entire_durations[-1]))\n    print(\"summary_request_entire_count: {}\".format(len(summary_entire_durations)))\n    if summary_entire_durations:\n        print(\"summary_request_entire_last_seconds: {:.3f}\".format(summary_entire_durations[-1]))\n    print(\"osm2world_ignored_connector_exceptions: {}\".format(ignored_connector_exception_count))\n\n    print(\"\\nConverter Stage Markers\")\n    print(\"progress_markers_total: {}\".format(len(progress_events)))\n    request_progress = list(progress_events)\n    print(\"request_progress_markers: {}\".format(len(request_progress)))\n    if request_progress:\n        last_event = request_progress[-1]\n        last_line_no, last_ts, last_src, _last_component, last_stage, last_status, last_request_id, last_line = last_event\n        print(\n            \"last_request_stage: {} (status={}, ts={}, src={}, line={})\".format(\n                last_stage,\n                (last_status if last_status is not None else \"n/a\"),\n                format_dt(last_ts),\n                last_src,\n                last_line_no,\n            )\n        )\n        if last_request_id is not None:\n            print(\"last_request_id: {}\".format(last_request_id))\n        print(\"  {}\".format(last_line))\n\n        saw_complete = any(event[4] == \"complete\" for event in request_progress)\n        saw_failed = any(event[4] == \"failed\" for event in request_progress)\n        saw_exit = any(event[4] == \"exit\" for event in request_progress)\n        saw_signal = any(event[4] == \"signal-received\" for event in request_progress)\n        print(\"request_saw_complete_marker: {}\".format(saw_complete))\n        print(\"request_saw_failed_marker: {}\".format(saw_failed))\n        print(\"request_saw_exit_marker: {}\".format(saw_exit))\n        print(\"request_saw_signal_marker: {}\".format(saw_signal))\n\n        non_exit_request_progress = [event for event in request_progress if event[4] != \"exit\"]\n        if non_exit_request_progress:\n            last_non_exit = non_exit_request_progress[-1]\n            print(\n                \"likely_last_completed_stage_before_stop: {} (ts={}, src={}, line={})\".format(\n                    last_non_exit[4],\n                    format_dt(last_non_exit[1]),\n                    last_non_exit[2],\n                    last_non_exit[0],\n                )\n            )\n            print(\"  {}\".format(last_non_exit[7]))\n    else:\n        print(\"No request stage markers found in this log.\")\n\n    print(\"\\nMemory Peaks (VmHWM from MEMORY lines)\")\n    if not mem_peaks:\n        print(\"no MEMORY lines found\")\n    else:\n        for comp in sorted(mem_peaks):\n            hwm_kib, line_no, ts, stage, full_line = mem_peaks[comp]\n            print(\n                \"{}: {:.1f} MiB at stage={} (line {}, ts={})\".format(\n                    comp,\n                    mib(hwm_kib),\n                    stage,\n                    line_no,\n                    format_dt(ts),\n                )\n            )\n            print(\"  {}\".format(full_line))\n\n    print(\"\\nProcess RSS Peaks (ps-top sampled)\")\n    for label, peak in [\n        (\"osm2world_java\", max_rss_osm2world),\n        (\"blender_obj_to_tactile\", max_rss_blender),\n        (\"process_request\", max_rss_process_request),\n    ]:\n        rss_kib, full_line, ts = peak\n        if rss_kib <= 0:\n            print(\"{}: n/a\".format(label))\n        else:\n            print(\"{}: {:.1f} MiB (ts={})\".format(label, mib(rss_kib), format_dt(ts)))\n            print(\"  {}\".format(full_line))\n\n    print(\"\\nConcurrency Peaks (ps-top sampled)\")\n    for key in [\"process_request\", \"osm2world\", \"blender\"]:\n        count, ts = max_concurrency[key]\n        print(\"{}: {} at {}\".format(key, count, format_dt(ts)))\n\n    print(\"\\nSwap/VMStat\")\n    if max_swap_used >= 0:\n        print(\"max_swap_used: {} MiB / {} MiB\".format(max_swap_used, max_swap_total))\n        print(\"  {}\".format(max_swap_line))\n    else:\n        print(\"max_swap_used: n/a\")\n    print(\"max_vmstat_si: {}\".format(max_si if max_si >= 0 else \"n/a\"))\n    if max_si_line:\n        print(\"  {}\".format(max_si_line))\n    print(\"max_vmstat_so: {}\".format(max_so if max_so >= 0 else \"n/a\"))\n    if max_so_line:\n        print(\"  {}\".format(max_so_line))\n\n    print(\"\\nFailure/OOM Signals\")\n    recent_kernel_oom = [line for ts, line, _kts in kernel_oom_lines if is_recent(ts)]\n    same_day_kernel_oom = 0\n    if end_ts is not None:\n        for _ts, _line, kernel_ts in kernel_oom_lines:\n            if kernel_ts is not None and kernel_ts.date() == end_ts.date():\n                same_day_kernel_oom += 1\n    print(\"kernel_oom_lines_captured_total: {}\".format(len(kernel_oom_lines)))\n    print(\"kernel_oom_lines_same_day_as_capture: {}\".format(same_day_kernel_oom))\n    print(\n        \"kernel_oom_lines_recent_{}s: {}\".format(\n            recent_window_seconds, len(recent_kernel_oom)\n        )\n    )\n    if kernel_oom_lines:\n        print(\"latest_kernel_oom_line: {}\".format(kernel_oom_lines[-1][1]))\n    recent_failure_signals = [line for ts, line in failure_signal_lines if is_recent(ts)]\n    print(\"failure_signal_lines_total: {}\".format(len(failure_signal_lines)))\n    print(\n        \"failure_signal_lines_recent_{}s: {}\".format(\n            recent_window_seconds, len(recent_failure_signals)\n        )\n    )\n    lines_to_show = recent_failure_signals[-args.show_failure_lines:]\n    if not lines_to_show:\n        lines_to_show = [line for _ts, line in failure_signal_lines[-args.show_failure_lines:]]\n    for line in lines_to_show:\n        print(line)\n\n    request_progress_complete = any(event[4] == \"complete\" for event in request_progress)\n    request_progress_last_non_exit_stage = None  # type: Optional[str]\n    non_exit_request_progress = [event for event in request_progress if event[4] != \"exit\"]\n    if non_exit_request_progress:\n        request_progress_last_non_exit_stage = non_exit_request_progress[-1][4]\n\n    print(\"\\nLikely Cause Hints\")\n    recent_failure_signal_lines = [line for ts, line in failure_signal_lines if is_recent(ts)]\n    likely_timeout = any(\"timeout\" in line.lower() for line in recent_failure_signal_lines)\n    likely_oom = (\n        (max_si > 0 or max_so > 0 or (max_swap_total > 0 and max_swap_used / max_swap_total > 0.85))\n        and same_day_kernel_oom > 0\n    )\n    if likely_oom:\n        print(\"- OOM pressure signals present (swap/io + kernel oom lines).\")\n    else:\n        print(\"- OOM not strongly indicated in this capture window.\")\n    if likely_timeout:\n        print(\"- Timeout-related signals present.\")\n    else:\n        print(\"- No strong timeout signal detected in failure patterns.\")\n    if request_progress and not request_progress_complete:\n        print(\n            \"- Converter appears to stop before completion. Last stage marker: {}.\".format(\n                request_progress_last_non_exit_stage or \"n/a\"\n            )\n        )\n    if entire_durations:\n        print(\"- At least one request completed end-to-end during this capture.\")\n    else:\n        print(\"- No full request completion line detected.\")\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "bin/tmpctl",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport os\nimport shutil\nimport sys\nfrom pathlib import Path\n\n\nREPO_ROOT = Path(__file__).resolve().parent.parent\nTMP_ROOT = (REPO_ROOT / \".tmp\").resolve()\n\n\ndef fail(message):\n    print(\"tmpctl: {}\".format(message), file=sys.stderr)\n    raise SystemExit(1)\n\n\ndef resolve_scoped_path(raw_path):\n    path = Path(raw_path)\n    if not path.is_absolute():\n        path = REPO_ROOT / path\n\n    resolved = path.resolve(strict=False)\n    try:\n        resolved.relative_to(TMP_ROOT)\n    except ValueError:\n        fail(\"refusing path outside .tmp: {}\".format(raw_path))\n    return resolved\n\n\ndef ensure_exists(path, raw_path):\n    if not path.exists():\n        fail(\"path does not exist: {}\".format(raw_path))\n\n\ndef cmd_mkdir(args):\n    for raw_path in args.paths:\n        target = resolve_scoped_path(raw_path)\n        target.mkdir(parents=True, exist_ok=True)\n\n\ndef remove_path(path):\n    if not path.exists() and not path.is_symlink():\n        return\n    if path.is_dir() and not path.is_symlink():\n        shutil.rmtree(str(path))\n    else:\n        path.unlink()\n\n\ndef cmd_rm(args):\n    for raw_path in args.paths:\n        target = resolve_scoped_path(raw_path)\n        if not target.exists() and not target.is_symlink():\n            if args.missing_ok:\n                continue\n            fail(\"path does not exist: {}\".format(raw_path))\n        remove_path(target)\n\n\ndef cmd_mv(args):\n    src = resolve_scoped_path(args.src)\n    dst = resolve_scoped_path(args.dst)\n    ensure_exists(src, args.src)\n    dst.parent.mkdir(parents=True, exist_ok=True)\n    shutil.move(str(src), str(dst))\n\n\ndef cmd_cp(args):\n    src = resolve_scoped_path(args.src)\n    dst = resolve_scoped_path(args.dst)\n    ensure_exists(src, args.src)\n    dst.parent.mkdir(parents=True, exist_ok=True)\n\n    if src.is_dir() and not src.is_symlink():\n        if dst.exists() and dst.is_dir():\n            target = dst / src.name\n        else:\n            target = dst\n        shutil.copytree(str(src), str(target), dirs_exist_ok=True)\n        return\n\n    if dst.exists() and dst.is_dir():\n        shutil.copy2(str(src), str(dst / src.name))\n    else:\n        shutil.copy2(str(src), str(dst))\n\n\ndef cmd_write(args):\n    target = resolve_scoped_path(args.path)\n    target.parent.mkdir(parents=True, exist_ok=True)\n\n    if target.exists() and target.is_dir():\n        fail(\"cannot write to directory: {}\".format(args.path))\n\n    if args.stdin:\n        data = sys.stdin.read()\n    elif args.text is not None:\n        data = args.text\n    else:\n        data = \"\"\n\n    mode = \"a\" if args.append else \"w\"\n    with open(target, mode, encoding=\"utf-8\") as handle:\n        handle.write(data)\n\n\ndef build_parser():\n    parser = argparse.ArgumentParser(\n        description=\"Scoped file manipulation utility restricted to .tmp/.\"\n    )\n    subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n    mkdir_parser = subparsers.add_parser(\"mkdir\", help=\"Create directories under .tmp/.\")\n    mkdir_parser.add_argument(\"paths\", nargs=\"+\", help=\"Directory paths under .tmp/.\")\n    mkdir_parser.set_defaults(func=cmd_mkdir)\n\n    rm_parser = subparsers.add_parser(\"rm\", help=\"Remove files/directories under .tmp/.\")\n    rm_parser.add_argument(\"paths\", nargs=\"+\", help=\"Paths under .tmp/.\")\n    rm_parser.add_argument(\"--missing-ok\", action=\"store_true\", help=\"Do not fail when a path is missing.\")\n    rm_parser.set_defaults(func=cmd_rm)\n\n    mv_parser = subparsers.add_parser(\"mv\", help=\"Move/rename a path under .tmp/.\")\n    mv_parser.add_argument(\"src\", help=\"Source path under .tmp/.\")\n    mv_parser.add_argument(\"dst\", help=\"Destination path under .tmp/.\")\n    mv_parser.set_defaults(func=cmd_mv)\n\n    cp_parser = subparsers.add_parser(\"cp\", help=\"Copy a path under .tmp/.\")\n    cp_parser.add_argument(\"src\", help=\"Source path under .tmp/.\")\n    cp_parser.add_argument(\"dst\", help=\"Destination path under .tmp/.\")\n    cp_parser.set_defaults(func=cmd_cp)\n\n    write_parser = subparsers.add_parser(\"write\", help=\"Write text to a file under .tmp/.\")\n    write_parser.add_argument(\"path\", help=\"Target file path under .tmp/.\")\n    write_parser.add_argument(\"--text\", help=\"Text to write. If omitted, empty string is used unless --stdin is set.\")\n    write_parser.add_argument(\"--stdin\", action=\"store_true\", help=\"Read write content from stdin.\")\n    write_parser.add_argument(\"--append\", action=\"store_true\", help=\"Append instead of overwrite.\")\n    write_parser.set_defaults(func=cmd_write)\n\n    return parser\n\n\ndef main():\n    TMP_ROOT.mkdir(parents=True, exist_ok=True)\n    parser = build_parser()\n    args = parser.parse_args()\n    args.func(args)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "converter/.gitignore",
    "content": "/py-lib/*\n/OSM2World\n/blender\n*.sample/\n\n"
  },
  {
    "path": "converter/clip-2d.js",
    "content": "#!/usr/bin/env node\n\"use strict\";\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { performance } = require(\"perf_hooks\");\n\nconst DEFAULT_Q = 1e6;\nconst GROUP_ORDER = [\n  \"roads_car\",\n  \"roads_ped\",\n  \"road_areas_car\",\n  \"road_areas_ped\",\n  \"rails\",\n  \"buildings\",\n  \"waterways\",\n  \"water_areas\",\n  \"other\",\n];\n\nfunction parseArgs(argv) {\n  const args = {\n    inputObj: \"\",\n    outDir: \"\",\n    basename: \"map-clip\",\n    report: \"\",\n    minX: Number.NaN,\n    minY: Number.NaN,\n    maxX: Number.NaN,\n    maxY: Number.NaN,\n    quantization: DEFAULT_Q,\n  };\n\n  for (let i = 0; i < argv.length; i += 1) {\n    const arg = argv[i];\n    const next = argv[i + 1];\n    if (arg === \"--input-obj\") {\n      args.inputObj = next || \"\";\n      i += 1;\n      continue;\n    }\n    if (arg === \"--out-dir\") {\n      args.outDir = next || \"\";\n      i += 1;\n      continue;\n    }\n    if (arg === \"--basename\") {\n      args.basename = next || \"\";\n      i += 1;\n      continue;\n    }\n    if (arg === \"--report\") {\n      args.report = next || \"\";\n      i += 1;\n      continue;\n    }\n    if (arg === \"--min-x\") {\n      args.minX = Number(next);\n      i += 1;\n      continue;\n    }\n    if (arg === \"--min-y\") {\n      args.minY = Number(next);\n      i += 1;\n      continue;\n    }\n    if (arg === \"--max-x\") {\n      args.maxX = Number(next);\n      i += 1;\n      continue;\n    }\n    if (arg === \"--max-y\") {\n      args.maxY = Number(next);\n      i += 1;\n      continue;\n    }\n    if (arg === \"--quantization\") {\n      args.quantization = Number(next);\n      i += 1;\n      continue;\n    }\n    throw new Error(\"Unknown argument: \" + arg);\n  }\n\n  if (!args.inputObj) {\n    throw new Error(\"--input-obj is required\");\n  }\n  if (!args.outDir) {\n    throw new Error(\"--out-dir is required\");\n  }\n  if (!args.report) {\n    throw new Error(\"--report is required\");\n  }\n  [\"minX\", \"minY\", \"maxX\", \"maxY\", \"quantization\"].forEach(function(key) {\n    if (!Number.isFinite(args[key])) {\n      throw new Error(\"Argument \" + key + \" must be numeric\");\n    }\n  });\n  if (!(args.minX < args.maxX) || !(args.minY < args.maxY)) {\n    throw new Error(\"Invalid clipping bounds\");\n  }\n  if (!(args.quantization > 0)) {\n    throw new Error(\"--quantization must be > 0\");\n  }\n\n  return args;\n}\n\nfunction isPedestrianName(name) {\n  return name.endsWith(\"::pedestrian\");\n}\n\nfunction classifyObject(name) {\n  if (name.startsWith(\"BuildingEntrance\")) {\n    return \"skip\";\n  }\n  if (name.startsWith(\"Building\")) {\n    return \"buildings\";\n  }\n  if (name.startsWith(\"RoadArea\")) {\n    return isPedestrianName(name) ? \"road_areas_ped\" : \"road_areas_car\";\n  }\n  if (name.startsWith(\"Road\")) {\n    return isPedestrianName(name) ? \"roads_ped\" : \"roads_car\";\n  }\n  if (name.startsWith(\"Rail\")) {\n    return \"rails\";\n  }\n  if (name.startsWith(\"Waterway\") || name.startsWith(\"River\")) {\n    return \"waterways\";\n  }\n  if (name.startsWith(\"Water\") || name.startsWith(\"AreaFountain\")) {\n    return \"water_areas\";\n  }\n  return \"other\";\n}\n\nfunction signedArea2(ax, ay, bx, by, cx, cy) {\n  return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);\n}\n\nfunction parseObj(inputObjPath) {\n  const raw = fs.readFileSync(inputObjPath, \"utf8\");\n  const lines = raw.split(/\\r?\\n/);\n  const vertices = [];\n  const triangles = [];\n  let currentObjectName = \"null\";\n  let currentObjectOrdinal = 0;\n\n  for (let lineNo = 0; lineNo < lines.length; lineNo += 1) {\n    const line = lines[lineNo].trim();\n    if (!line || line.startsWith(\"#\")) {\n      continue;\n    }\n    if (line.startsWith(\"o \")) {\n      currentObjectName = line.slice(2).trim() || \"unnamed\";\n      currentObjectOrdinal += 1;\n      continue;\n    }\n    if (line.startsWith(\"v \")) {\n      const parts = line.split(/\\s+/);\n      if (parts.length < 4) {\n        continue;\n      }\n      const x = Number(parts[1]);\n      const y = Number(parts[2]);\n      const z = Number(parts[3]);\n      if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {\n        continue;\n      }\n      // Match Blender OBJ import axis mapping (-Z forward, Y up): map plane is (x, -z).\n      vertices.push({\n        x: x,\n        y: -z,\n      });\n      continue;\n    }\n    if (line.startsWith(\"f \")) {\n      const parts = line.slice(2).trim().split(/\\s+/).filter(Boolean);\n      if (parts.length < 3) {\n        continue;\n      }\n      const faceVertexIndices = [];\n      for (let i = 0; i < parts.length; i += 1) {\n        const token = parts[i];\n        const firstField = token.split(\"/\")[0];\n        if (!firstField) {\n          continue;\n        }\n        const rawIndex = Number(firstField);\n        if (!Number.isInteger(rawIndex) || rawIndex === 0) {\n          continue;\n        }\n        let idx = -1;\n        if (rawIndex > 0) {\n          idx = rawIndex - 1;\n        } else {\n          idx = vertices.length + rawIndex;\n        }\n        if (idx < 0 || idx >= vertices.length) {\n          continue;\n        }\n        faceVertexIndices.push(idx);\n      }\n      if (faceVertexIndices.length < 3) {\n        continue;\n      }\n      for (let i = 1; i < faceVertexIndices.length - 1; i += 1) {\n        triangles.push({\n          objectName: currentObjectName,\n          objectOrdinal: currentObjectOrdinal,\n          indices: [faceVertexIndices[0], faceVertexIndices[i], faceVertexIndices[i + 1]],\n        });\n      }\n    }\n  }\n\n  return {\n    vertices,\n    triangles,\n  };\n}\n\nfunction makeInsideFn(plane, bounds, eps) {\n  if (plane === \"x>=\") {\n    return function(v) { return v.x >= bounds.minX - eps; };\n  }\n  if (plane === \"x<=\") {\n    return function(v) { return v.x <= bounds.maxX + eps; };\n  }\n  if (plane === \"y>=\") {\n    return function(v) { return v.y >= bounds.minY - eps; };\n  }\n  if (plane === \"y<=\") {\n    return function(v) { return v.y <= bounds.maxY + eps; };\n  }\n  throw new Error(\"Unknown plane \" + plane);\n}\n\nfunction intersectionWithPlane(a, b, plane, bounds, eps) {\n  if (plane === \"x>=\" || plane === \"x<=\") {\n    const k = plane === \"x>=\" ? bounds.minX : bounds.maxX;\n    const den = b.x - a.x;\n    if (Math.abs(den) < eps) {\n      return null;\n    }\n    let t = (k - a.x) / den;\n    if (t < 0) {\n      t = 0;\n    } else if (t > 1) {\n      t = 1;\n    }\n    let y = a.y + t * (b.y - a.y);\n    if (Math.abs(y - bounds.minY) <= eps) {\n      y = bounds.minY;\n    } else if (Math.abs(y - bounds.maxY) <= eps) {\n      y = bounds.maxY;\n    }\n    return { x: k, y: y };\n  }\n\n  if (plane === \"y>=\" || plane === \"y<=\") {\n    const k = plane === \"y>=\" ? bounds.minY : bounds.maxY;\n    const den = b.y - a.y;\n    if (Math.abs(den) < eps) {\n      return null;\n    }\n    let t = (k - a.y) / den;\n    if (t < 0) {\n      t = 0;\n    } else if (t > 1) {\n      t = 1;\n    }\n    let x = a.x + t * (b.x - a.x);\n    if (Math.abs(x - bounds.minX) <= eps) {\n      x = bounds.minX;\n    } else if (Math.abs(x - bounds.maxX) <= eps) {\n      x = bounds.maxX;\n    }\n    return { x: x, y: k };\n  }\n\n  throw new Error(\"Unknown plane \" + plane);\n}\n\nfunction clipPolygonAgainstPlane(poly, plane, bounds, eps) {\n  if (poly.length === 0) {\n    return poly;\n  }\n  const inside = makeInsideFn(plane, bounds, eps);\n  const out = [];\n  let prev = poly[poly.length - 1];\n  let prevInside = inside(prev);\n\n  for (let i = 0; i < poly.length; i += 1) {\n    const curr = poly[i];\n    const currInside = inside(curr);\n\n    if (currInside) {\n      if (!prevInside) {\n        const p = intersectionWithPlane(prev, curr, plane, bounds, eps);\n        if (p) {\n          out.push(p);\n        }\n      }\n      out.push(curr);\n    } else if (prevInside) {\n      const p = intersectionWithPlane(prev, curr, plane, bounds, eps);\n      if (p) {\n        out.push(p);\n      }\n    }\n\n    prev = curr;\n    prevInside = currInside;\n  }\n\n  return out;\n}\n\nfunction clipTriangleToBounds(tri, bounds, eps) {\n  let poly = tri;\n  poly = clipPolygonAgainstPlane(poly, \"x>=\", bounds, eps);\n  poly = clipPolygonAgainstPlane(poly, \"x<=\", bounds, eps);\n  poly = clipPolygonAgainstPlane(poly, \"y>=\", bounds, eps);\n  poly = clipPolygonAgainstPlane(poly, \"y<=\", bounds, eps);\n  return poly;\n}\n\nfunction ensureBucket(buckets, key, group, sourceName, sourceOrdinal) {\n  if (!buckets[key]) {\n    buckets[key] = {\n      key,\n      group,\n      sourceName,\n      sourceOrdinal,\n      triangles: [],\n      inputTriangles: 0,\n      clippedTriangles: 0,\n      droppedTriangles: 0,\n      droppedDegenerate: 0,\n      droppedCollapsed: 0,\n      verticesBeforeDedupe: 0,\n      verticesAfterDedupe: 0,\n      writtenFaces: 0,\n      path: \"\",\n    };\n  }\n  return buckets[key];\n}\n\nfunction writeBinaryPly(filePath, vertsX, vertsY, faces) {\n  const header = [\n    \"ply\",\n    \"format binary_little_endian 1.0\",\n    \"element vertex \" + vertsX.length,\n    \"property float x\",\n    \"property float y\",\n    \"property float z\",\n    \"element face \" + faces.length,\n    \"property list uchar int vertex_indices\",\n    \"end_header\\n\",\n  ].join(\"\\n\");\n\n  const headerBuf = Buffer.from(header, \"ascii\");\n  const vertexBytes = vertsX.length * 12;\n  const faceBytes = faces.length * 13;\n  const out = Buffer.allocUnsafe(headerBuf.length + vertexBytes + faceBytes);\n\n  let offset = 0;\n  headerBuf.copy(out, offset);\n  offset += headerBuf.length;\n\n  for (let i = 0; i < vertsX.length; i += 1) {\n    out.writeFloatLE(vertsX[i], offset);\n    offset += 4;\n    out.writeFloatLE(vertsY[i], offset);\n    offset += 4;\n    out.writeFloatLE(0.0, offset);\n    offset += 4;\n  }\n\n  for (let i = 0; i < faces.length; i += 1) {\n    const f = faces[i];\n    out.writeUInt8(3, offset);\n    offset += 1;\n    out.writeInt32LE(f[0], offset);\n    offset += 4;\n    out.writeInt32LE(f[1], offset);\n    offset += 4;\n    out.writeInt32LE(f[2], offset);\n    offset += 4;\n  }\n\n  fs.writeFileSync(filePath, out);\n}\n\nfunction dedupeAndWriteBucket(bucket, outPath, quantization) {\n  const keyToIndex = new Map();\n  const vertsX = [];\n  const vertsY = [];\n  const faces = [];\n\n  function getIndex(x, y) {\n    const qx = Math.round(x * quantization);\n    const qy = Math.round(y * quantization);\n    const key = qx + \",\" + qy;\n    const existing = keyToIndex.get(key);\n    if (existing !== undefined) {\n      return existing;\n    }\n    const idx = vertsX.length;\n    keyToIndex.set(key, idx);\n    vertsX.push(x);\n    vertsY.push(y);\n    return idx;\n  }\n\n  for (let i = 0; i < bucket.triangles.length; i += 1) {\n    const tri = bucket.triangles[i];\n    const a = getIndex(tri[0].x, tri[0].y);\n    const b = getIndex(tri[1].x, tri[1].y);\n    const c = getIndex(tri[2].x, tri[2].y);\n    if (a === b || b === c || a === c) {\n      bucket.droppedCollapsed += 1;\n      continue;\n    }\n    faces.push([a, b, c]);\n  }\n\n  bucket.verticesBeforeDedupe = bucket.triangles.length * 3;\n  bucket.verticesAfterDedupe = vertsX.length;\n  bucket.writtenFaces = faces.length;\n\n  writeBinaryPly(outPath, vertsX, vertsY, faces);\n}\n\nfunction prepareTmpDir(outDir) {\n  const tmpDir = path.join(outDir, \"tmp\");\n  // clip-2d outputs are ephemeral intermediates; clear before each run.\n  fs.rmSync(tmpDir, { recursive: true, force: true });\n  fs.mkdirSync(tmpDir, { recursive: true });\n  return tmpDir;\n}\n\nfunction main() {\n  const started = performance.now();\n  const args = parseArgs(process.argv.slice(2));\n  fs.mkdirSync(args.outDir, { recursive: true });\n  const tmpOutDir = prepareTmpDir(args.outDir);\n\n  const bounds = {\n    minX: args.minX,\n    minY: args.minY,\n    maxX: args.maxX,\n    maxY: args.maxY,\n  };\n  const extent = Math.max(1, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY);\n  const eps = 1e-9 * extent;\n  const areaEps = 1e-12 * extent * extent;\n\n  const parseStart = performance.now();\n  const parsed = parseObj(args.inputObj);\n  const parseSeconds = (performance.now() - parseStart) / 1000;\n\n  const clipStart = performance.now();\n  const buckets = {};\n  const objectStats = {\n    seen: 0,\n    skipped: 0,\n  };\n  const seenObjectKeys = new Set();\n  const skippedObjectKeys = new Set();\n\n  let inputTriangles = 0;\n  let clippedTriangles = 0;\n  let droppedTriangles = 0;\n  let droppedDegenerate = 0;\n\n  for (let i = 0; i < parsed.triangles.length; i += 1) {\n    const triRecord = parsed.triangles[i];\n    inputTriangles += 1;\n\n    const sourceName = triRecord.objectName || \"null\";\n    const sourceOrdinal = triRecord.objectOrdinal;\n    const sourceKey = sourceOrdinal + \"::\" + sourceName;\n    if (!seenObjectKeys.has(sourceKey)) {\n      seenObjectKeys.add(sourceKey);\n      objectStats.seen += 1;\n    }\n\n    const group = classifyObject(sourceName);\n    if (group === \"skip\") {\n      droppedTriangles += 1;\n      if (!skippedObjectKeys.has(sourceKey)) {\n        skippedObjectKeys.add(sourceKey);\n        objectStats.skipped += 1;\n      }\n      continue;\n    }\n\n    const v0 = parsed.vertices[triRecord.indices[0]];\n    const v1 = parsed.vertices[triRecord.indices[1]];\n    const v2 = parsed.vertices[triRecord.indices[2]];\n\n    const origArea2 = signedArea2(v0.x, v0.y, v1.x, v1.y, v2.x, v2.y);\n    if (Math.abs(origArea2) <= areaEps) {\n      droppedTriangles += 1;\n      droppedDegenerate += 1;\n      continue;\n    }\n\n    const clipped = clipTriangleToBounds([v0, v1, v2], bounds, eps);\n    if (clipped.length < 3) {\n      droppedTriangles += 1;\n      continue;\n    }\n\n    let bucketKey = group;\n    if (group === \"water_areas\") {\n      bucketKey = group + \"#\" + String(sourceOrdinal).padStart(4, \"0\");\n    }\n    const bucket = ensureBucket(buckets, bucketKey, group, sourceName, sourceOrdinal);\n    bucket.inputTriangles += 1;\n\n    for (let j = 1; j < clipped.length - 1; j += 1) {\n      const a = clipped[0];\n      const b = clipped[j];\n      const c = clipped[j + 1];\n      let area2 = signedArea2(a.x, a.y, b.x, b.y, c.x, c.y);\n      if (Math.abs(area2) <= areaEps) {\n        droppedTriangles += 1;\n        droppedDegenerate += 1;\n        bucket.droppedDegenerate += 1;\n        continue;\n      }\n      if (area2 * origArea2 < 0) {\n        bucket.triangles.push([a, c, b]);\n        area2 = -area2;\n      } else {\n        bucket.triangles.push([a, b, c]);\n      }\n      clippedTriangles += 1;\n      bucket.clippedTriangles += 1;\n    }\n  }\n  const clipSeconds = (performance.now() - clipStart) / 1000;\n\n  const dedupeWriteStart = performance.now();\n  const files = [];\n\n  const sortedBucketKeys = Object.keys(buckets).sort(function(a, b) {\n    const ga = buckets[a].group;\n    const gb = buckets[b].group;\n    const ia = GROUP_ORDER.indexOf(ga);\n    const ib = GROUP_ORDER.indexOf(gb);\n    if (ia !== ib) {\n      return ia - ib;\n    }\n    if (ga === \"water_areas\") {\n      return buckets[a].sourceOrdinal - buckets[b].sourceOrdinal;\n    }\n    return a.localeCompare(b);\n  });\n\n  const waterAreaFiles = [];\n  for (let i = 0; i < sortedBucketKeys.length; i += 1) {\n    const key = sortedBucketKeys[i];\n    const bucket = buckets[key];\n    if (bucket.triangles.length === 0) {\n      continue;\n    }\n\n    let filename = \"\";\n    if (bucket.group === \"water_areas\") {\n      filename = args.basename + \"-water-areas-\" + String(waterAreaFiles.length + 1).padStart(4, \"0\") + \".ply\";\n    } else {\n      filename = args.basename + \"-\" + bucket.group.replace(/_/g, \"-\") + \".ply\";\n    }\n    const outPath = path.join(tmpOutDir, filename);\n    dedupeAndWriteBucket(bucket, outPath, args.quantization);\n    bucket.path = outPath;\n    files.push({\n      group: bucket.group,\n      path: outPath,\n      inputTriangles: bucket.inputTriangles,\n      clippedTriangles: bucket.clippedTriangles,\n      droppedDegenerate: bucket.droppedDegenerate,\n      droppedCollapsed: bucket.droppedCollapsed,\n      verticesBeforeDedupe: bucket.verticesBeforeDedupe,\n      verticesAfterDedupe: bucket.verticesAfterDedupe,\n      writtenFaces: bucket.writtenFaces,\n    });\n    if (bucket.group === \"water_areas\") {\n      waterAreaFiles.push(outPath);\n    }\n  }\n\n  const dedupeWriteSeconds = (performance.now() - dedupeWriteStart) / 1000;\n  const totalSeconds = (performance.now() - started) / 1000;\n\n  const report = {\n    inputObjPath: path.resolve(args.inputObj),\n    outDir: path.resolve(args.outDir),\n    tmpOutDir: path.resolve(tmpOutDir),\n    bounds,\n    quantization: args.quantization,\n    eps,\n    areaEps,\n    timings: {\n      parseSeconds,\n      clipSeconds,\n      dedupeWriteSeconds,\n      totalSeconds,\n    },\n    objects: objectStats,\n    triangles: {\n      input: inputTriangles,\n      clippedOutput: clippedTriangles,\n      dropped: droppedTriangles,\n      droppedDegenerate,\n    },\n    files,\n  };\n\n  fs.writeFileSync(args.report, JSON.stringify(report, null, 2) + \"\\n\", \"utf8\");\n  console.log(JSON.stringify({ reportPath: path.resolve(args.report), files: files.map(function(f) { return f.path; }) }));\n}\n\nmain();\n"
  },
  {
    "path": "converter/map_desc/__init__.py",
    "content": "# Python 3.5\nfrom __future__ import division\n\nimport json\nimport argparse\nimport os\nimport sys\nimport time\nfrom collections import OrderedDict\nfrom typing import Any, Dict, Iterable, List, Optional, TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from typing_extensions import TypedDict  # type: ignore[import-not-found]\nelse:  # pragma: no cover - blender python may not have typing_extensions\n    try:\n        from typing_extensions import TypedDict  # type: ignore[import-not-found]\n    except ImportError:\n        def TypedDict(name, fields, total=True):  # type: ignore[no-redef]\n            return dict\n\nfrom . import map_desc_render\nfrom .areas_raster import analyze_area_visibility, set_debug_osm_id\nfrom .feature_semantics import build_feature_semantics\nfrom .ways_clip import BBox as ClipBBox\nfrom .ways_clip import clip_line_string\nfrom .map_desc_loc_segments import BBox, Point, classify_location\n\n\nBounds = TypedDict(\n    \"Bounds\",\n    {\"minX\": float, \"minY\": float, \"maxX\": float, \"maxY\": float},\n    total=False\n)\n\n\nRuleInputs = TypedDict(\n    \"RuleInputs\",\n    {\n        \"elementTypeField\": str,\n        \"geometryTypeField\": str,\n        \"primaryRepresentationField\": str,\n        \"representationsField\": str,\n        \"tmCategoryField\": str,\n        \"tmRoadTypeField\": str,\n        \"tagsField\": str\n    },\n    total=False\n)\n\ndef _get_field(item: Dict[str, Any], path: Optional[str]) -> Optional[Any]:\n    if not path:\n        return None\n    cur = item\n    for part in path.split(\".\"):\n        if cur is None:\n            return None\n        cur = cur.get(part)\n    return cur\n\n\ndef _input_field(inputs: RuleInputs, name: str) -> Optional[str]:\n    value = inputs.get(name)\n    return value if isinstance(value, str) else None\n\n\ndef _match_tags_any(tags: Optional[Dict[str, Any]], conditions: Optional[List[Dict[str, Any]]]) -> bool:\n    if not conditions:\n        return True\n    for cond in conditions:\n        key = cond.get(\"key\")\n        if not isinstance(key, str):\n            continue\n        if not tags or key not in tags:\n            continue\n        val = tags.get(key)\n        if cond.get(\"anyValue\"):\n            if val is not None and val != \"\":\n                return True\n            continue\n        values = cond.get(\"values\", [])\n        if val in values:\n            return True\n    return False\n\n\ndef _match_tags_all(tags: Optional[Dict[str, Any]], conditions: Optional[List[Dict[str, Any]]]) -> bool:\n    if not conditions:\n        return True\n    for cond in conditions:\n        key = cond.get(\"key\")\n        if not isinstance(key, str):\n            return False\n        if not tags or key not in tags:\n            return False\n        val = tags.get(key)\n        if cond.get(\"anyValue\"):\n            if val is None or val == \"\":\n                return False\n            continue\n        values = cond.get(\"values\", [])\n        if val not in values:\n            return False\n    return True\n\n\ndef _match_any_field(item: Dict[str, Any], field_name: str, values: Optional[List[Any]]) -> bool:\n    if not values:\n        return False\n    val = item.get(field_name)\n    if not val:\n        return False\n    if isinstance(val, list):\n        for entry in val:\n            if entry in values:\n                return True\n        return False\n    return val in values\n\n\ndef _match_rule(item: Dict[str, Any], rule: Dict[str, Any], inputs: RuleInputs) -> bool:\n    if \"elementTypes\" in rule:\n        field_name = _input_field(inputs, \"elementTypeField\")\n        if not field_name or item.get(field_name) not in rule[\"elementTypes\"]:\n            return False\n    if \"geometryTypes\" in rule:\n        field_name = _input_field(inputs, \"geometryTypeField\")\n        if not field_name:\n            return False\n        geom_type = _get_field(item, field_name)\n        if geom_type not in rule[\"geometryTypes\"]:\n            return False\n    if \"primaryRepresentationAny\" in rule:\n        field_name = _input_field(inputs, \"primaryRepresentationField\")\n        if not field_name or not _match_any_field(item, field_name, rule[\"primaryRepresentationAny\"]):\n            return False\n    if \"representationsAny\" in rule:\n        field_name = _input_field(inputs, \"representationsField\")\n        if not field_name or not _match_any_field(item, field_name, rule[\"representationsAny\"]):\n            return False\n    if \"tmCategoryAny\" in rule:\n        field_name = _input_field(inputs, \"tmCategoryField\")\n        if not field_name or not _match_any_field(item, field_name, rule[\"tmCategoryAny\"]):\n            return False\n    if \"tmRoadTypeAny\" in rule:\n        field_name = _input_field(inputs, \"tmRoadTypeField\")\n        if not field_name or not _match_any_field(item, field_name, rule[\"tmRoadTypeAny\"]):\n            return False\n\n    tags_field = _input_field(inputs, \"tagsField\")\n    tags = item.get(tags_field) if tags_field else {}\n    if \"tagsAny\" in rule and not _match_tags_any(tags, rule[\"tagsAny\"]):\n        return False\n    if \"tagsAll\" in rule and not _match_tags_all(tags, rule[\"tagsAll\"]):\n        return False\n\n    if \"anyOf\" in rule:\n        any_matched = False\n        for sub in rule[\"anyOf\"]:\n            if _match_rule(item, sub, inputs):\n                any_matched = True\n                break\n        if not any_matched:\n            return False\n    if \"allOf\" in rule:\n        for sub in rule[\"allOf\"]:\n            if not _match_rule(item, sub, inputs):\n                return False\n\n    return True\n\n\ndef _collect_modifiers(item: Dict[str, Any], spec: Dict[str, Any], options: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:\n    inputs = spec.get(\"inputs\", {})  # type: RuleInputs\n    modifiers = []\n    for rule in spec.get(\"modifierRules\", []):\n        if not _match_rule(item, rule, inputs):\n            continue\n        for mod in rule.get(\"modifiers\", []):\n            entry = {\"name\": mod.get(\"name\")}\n            if mod.get(\"valueFromTag\"):\n                tags_field = _input_field(inputs, \"tagsField\")\n                tags_value = item.get(tags_field) if tags_field else {}\n                tags = tags_value if isinstance(tags_value, dict) else {}\n                entry[\"value\"] = tags.get(mod.get(\"valueFromTag\"))\n            modifiers.append(entry)\n    return modifiers\n\n\ndef classify_item(item: Dict[str, Any], spec: Dict[str, Any],\n                  options_override: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:\n    inputs = spec.get(\"inputs\", {})  # type: RuleInputs\n    options = {}\n    options.update(spec.get(\"options\", {}))\n    options.update(options_override or {})\n\n    for rule in spec.get(\"rules\", []):\n        if not _match_rule(item, rule, inputs):\n            continue\n        actions = rule.get(\"actions\", {})\n        ignore = bool(actions.get(\"ignore\"))\n        opt_name = actions.get(\"ignoreWhenOptionFalse\")\n        if opt_name and not options.get(opt_name):\n            ignore = True\n        return {\n            \"mainClass\": rule.get(\"mainClass\"),\n            \"subClass\": rule.get(\"subClass\"),\n            \"ruleId\": rule.get(\"id\"),\n            \"ignore\": ignore,\n            \"role\": actions.get(\"role\"),\n            \"poiImportance\": actions.get(\"poiImportance\")\n        }\n\n    for fb in spec.get(\"fallbacks\", []):\n        if not _match_rule(item, fb, inputs):\n            continue\n        return {\n            \"mainClass\": fb.get(\"mainClass\"),\n            \"subClass\": fb.get(\"subClass\"),\n            \"ruleId\": fb.get(\"id\"),\n            \"ignore\": False\n        }\n    return None\n\n\ndef _map_bbox_from_meta(map_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:\n    meta = map_data.get(\"meta\")\n    if not meta:\n        return None\n    if meta.get(\"boundary\"):\n        return meta.get(\"boundary\")\n    if meta.get(\"dataBoundary\"):\n        return meta.get(\"dataBoundary\")\n    return None\n\n\ndef _map_bbox_from_items(map_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:\n    min_x = float(\"inf\")\n    min_y = float(\"inf\")\n    max_x = float(\"-inf\")\n    max_y = float(\"-inf\")\n    found = False\n    for key, value in map_data.items():\n        if not isinstance(value, list):\n            continue\n        for item in value:\n            bounds = item.get(\"bounds\")\n            if not bounds:\n                continue\n            try:\n                bminx = bounds.get(\"minX\")\n                bminy = bounds.get(\"minY\")\n                bmaxx = bounds.get(\"maxX\")\n                bmaxy = bounds.get(\"maxY\")\n                if bminx is None or bminy is None or bmaxx is None or bmaxy is None:\n                    continue\n            except Exception:\n                continue\n            min_x = min(min_x, bminx)\n            min_y = min(min_y, bminy)\n            max_x = max(max_x, bmaxx)\n            max_y = max(max_y, bmaxy)\n            found = True\n    if not found:\n        return None\n    return {\"minX\": float(min_x), \"minY\": float(min_y), \"maxX\": float(max_x), \"maxY\": float(max_y)}\n\n\ndef _get_map_bbox(map_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:\n    return _map_bbox_from_meta(map_data) or _map_bbox_from_items(map_data)\n\n\ndef _center_from_bounds(bounds: Optional[Bounds]) -> Optional[Point]:\n    if not bounds:\n        return None\n    min_x = bounds.get(\"minX\")\n    min_y = bounds.get(\"minY\")\n    max_x = bounds.get(\"maxX\")\n    max_y = bounds.get(\"maxY\")\n    if not isinstance(min_x, (int, float)) or not isinstance(min_y, (int, float)):\n        return None\n    if not isinstance(max_x, (int, float)) or not isinstance(max_y, (int, float)):\n        return None\n    return {\n        \"x\": (float(min_x) + float(max_x)) / 2,\n        \"y\": (float(min_y) + float(max_y)) / 2\n    }\n\n\ndef _point_from_coords(coords: Any) -> Optional[Point]:\n    if not isinstance(coords, list) or len(coords) < 2:\n        return None\n    x = coords[0]\n    y = coords[1]\n    if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):\n        return None\n    return {\"x\": float(x), \"y\": float(y)}\n\n\ndef _average_point(coords: Any) -> Optional[Point]:\n    if not isinstance(coords, list) or not coords:\n        return None\n    sum_x = 0.0\n    sum_y = 0.0\n    count = 0\n    for p in coords:\n        if not isinstance(p, list) or len(p) < 2:\n            continue\n        x = p[0]\n        y = p[1]\n        if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):\n            continue\n        sum_x += float(x)\n        sum_y += float(y)\n        count += 1\n    if not count:\n        return None\n    return {\"x\": sum_x / count, \"y\": sum_y / count}\n\n\ndef _polygon_points(geometry: Optional[Dict[str, Any]]) -> Optional[List[Any]]:\n    if not geometry:\n        return None\n    if isinstance(geometry.get(\"outer\"), list):\n        return geometry.get(\"outer\")\n    coords = geometry.get(\"coordinates\")\n    if isinstance(coords, list) and coords and isinstance(coords[0], list):\n        if coords[0] and isinstance(coords[0][0], (int, float)):\n            return coords\n    return None\n\n\ndef _coerce_bbox(bbox: Optional[Dict[str, Any]]) -> Optional[BBox]:\n    if not bbox:\n        return None\n    min_x = bbox.get(\"minX\")\n    min_y = bbox.get(\"minY\")\n    max_x = bbox.get(\"maxX\")\n    max_y = bbox.get(\"maxY\")\n    if not isinstance(min_x, (int, float)) or not isinstance(min_y, (int, float)):\n        return None\n    if not isinstance(max_x, (int, float)) or not isinstance(max_y, (int, float)):\n        return None\n    return {\n        \"minX\": float(min_x),\n        \"minY\": float(min_y),\n        \"maxX\": float(max_x),\n        \"maxY\": float(max_y)\n    }\n\n\ndef _attach_locations(entry: Dict[str, Any], item: Dict[str, Any], bbox: Optional[Dict[str, Any]]) -> None:\n    bbox_typed = _coerce_bbox(bbox)\n    if not bbox_typed or not classify_location:\n        return\n    geom = item.get(\"geometry\") or {}\n    point = None\n\n    if geom.get(\"type\") == \"point\":\n        point = _point_from_coords(geom.get(\"coordinates\"))\n        if point:\n            entry[\"_classification\"][\"location\"] = classify_location(point, bbox_typed)\n        return\n\n    if geom.get(\"type\") == \"line_string\":\n        coords = geom.get(\"coordinates\") or []\n        if coords:\n            start = _point_from_coords(coords[0])\n            end = _point_from_coords(coords[-1])\n            center = _average_point(coords) or _center_from_bounds(item.get(\"bounds\"))\n            if start:\n                entry[\"_classification\"][\"locationStart\"] = classify_location(start, bbox_typed)\n            if end:\n                entry[\"_classification\"][\"locationEnd\"] = classify_location(end, bbox_typed)\n            if center:\n                entry[\"_classification\"][\"locationCenter\"] = classify_location(center, bbox_typed)\n        if entry[\"_classification\"].get(\"mainClass\") == \"D\" and not entry[\"_classification\"].get(\"location\"):\n            if entry[\"_classification\"].get(\"locationCenter\"):\n                entry[\"_classification\"][\"location\"] = entry[\"_classification\"][\"locationCenter\"]\n        return\n\n    if geom.get(\"type\") == \"polygon\":\n        points = _polygon_points(geom)\n        point = _average_point(points) or _center_from_bounds(item.get(\"bounds\"))\n        if point:\n            entry[\"_classification\"][\"locationCenter\"] = classify_location(point, bbox_typed)\n        if entry[\"_classification\"].get(\"mainClass\") == \"D\" and not entry[\"_classification\"].get(\"location\"):\n            if entry[\"_classification\"].get(\"locationCenter\"):\n                entry[\"_classification\"][\"location\"] = entry[\"_classification\"][\"locationCenter\"]\n        return\n\n    point = _center_from_bounds(item.get(\"bounds\"))\n    if point:\n        entry[\"_classification\"][\"locationCenter\"] = classify_location(point, bbox_typed)\n    if entry[\"_classification\"].get(\"mainClass\") == \"D\" and not entry[\"_classification\"].get(\"location\"):\n        if entry[\"_classification\"].get(\"locationCenter\"):\n            entry[\"_classification\"][\"location\"] = entry[\"_classification\"][\"locationCenter\"]\n\n\ndef _coerce_clip_bbox(bbox: Optional[Dict[str, Any]]) -> Optional[ClipBBox]:\n    if not bbox:\n        return None\n    min_x = bbox.get(\"minX\")\n    min_y = bbox.get(\"minY\")\n    max_x = bbox.get(\"maxX\")\n    max_y = bbox.get(\"maxY\")\n    if not isinstance(min_x, (int, float)) or not isinstance(min_y, (int, float)):\n        return None\n    if not isinstance(max_x, (int, float)) or not isinstance(max_y, (int, float)):\n        return None\n    return {\n        \"minX\": float(min_x),\n        \"minY\": float(min_y),\n        \"maxX\": float(max_x),\n        \"maxY\": float(max_y)\n    }\n\n\ndef _is_building(entry: Dict[str, Any], item: Dict[str, Any]) -> bool:\n    cls = entry.get(\"_classification\", {})\n    main_class = cls.get(\"mainClass\")\n    sub_class = cls.get(\"subClass\")\n    if isinstance(main_class, str) and main_class.startswith(\"C\"):\n        return True\n    if isinstance(sub_class, str) and sub_class.startswith(\"C\"):\n        return True\n    tags = item.get(\"tags\") or {}\n    return bool(tags.get(\"building\") or tags.get(\"building:part\"))\n\n\ndef _is_rendered_water_area(entry: Dict[str, Any]) -> bool:\n    cls = entry.get(\"_classification\", {})\n    main_class = cls.get(\"mainClass\")\n    sub_class = cls.get(\"subClass\")\n    if not isinstance(main_class, str) or main_class != \"B\":\n        return False\n    return isinstance(sub_class, str) and sub_class.startswith(\"B1_\")\n\n\ndef _attach_visible_geometry(entry: Dict[str, Any], item: Dict[str, Any],\n                             boundary: Optional[Dict[str, Any]]) -> None:\n    # Code below creates stage \"Raw meta with visibility augmentation\" data.\n    geom = item.get(\"geometry\") or {}\n    if geom.get(\"type\") != \"line_string\":\n        if geom.get(\"type\") != \"polygon\":\n            return\n        if not _is_building(entry, item) and not _is_rendered_water_area(entry):\n            return\n        boundary_box = _coerce_bbox(boundary)\n        if not boundary_box:\n            return\n        # Code below creates stage \"Raw meta with building/water area visibility raster\" data.\n        visible = analyze_area_visibility(geom, boundary_box, item.get(\"osmId\"))\n        if visible is None:\n            return\n        entry[\"visibleGeometry\"] = visible\n        item[\"visibleGeometry\"] = visible\n        return\n    coords = geom.get(\"coordinates\")\n    if not isinstance(coords, list):\n        return\n    boundary_box = _coerce_clip_bbox(boundary)\n    if not boundary_box:\n        visible = [coords]\n        entry[\"visibleGeometry\"] = visible\n        item[\"visibleGeometry\"] = visible\n        return\n    visible = clip_line_string(coords, boundary_box)\n    entry[\"visibleGeometry\"] = visible\n    item[\"visibleGeometry\"] = visible\n\n\ndef group_map_data(map_data: Dict[str, Any], spec: Dict[str, Any],\n                   options_override: Optional[Dict[str, Any]] = None,\n                   profile: Optional[Dict[str, float]] = None) -> OrderedDict:\n    # Code below creates stage \"Grouped + classified meta\" data.\n    grouped = OrderedDict()\n    for main_key in spec.get(\"classes\", OrderedDict()).keys():\n        grouped[main_key] = OrderedDict()\n\n    def add_timing(name: str, elapsed: float) -> None:\n        if profile is None:\n            return\n        profile[name] = profile.get(name, 0.0) + elapsed\n\n    bbox = _get_map_bbox(map_data)\n    boundary = (map_data.get(\"meta\") or {}).get(\"boundary\")\n\n    def add_item(item):\n        classify_start = time.perf_counter()\n        classification = classify_item(item, spec, options_override)\n        add_timing(\"group-map-data.classify-item\", time.perf_counter() - classify_start)\n        if not classification or classification.get(\"ignore\"):\n            return\n\n        semantics = build_feature_semantics(item)\n        if semantics:\n            item[\"semantics\"] = semantics\n\n        modifiers = _collect_modifiers(item, spec, options_override)\n\n        entry = dict(item)\n        entry[\"_classification\"] = {\n            \"mainClass\": classification.get(\"mainClass\"),\n            \"subClass\": classification.get(\"subClass\"),\n            \"ruleId\": classification.get(\"ruleId\"),\n            \"role\": classification.get(\"role\"),\n            \"poiImportance\": classification.get(\"poiImportance\"),\n            \"modifiers\": modifiers\n        }\n\n        _attach_locations(entry, item, bbox)\n\n        attach_visible_geometry_start = time.perf_counter()\n        _attach_visible_geometry(entry, item, boundary)\n        add_timing(\"group-map-data.attach-visible-geometry\", time.perf_counter() - attach_visible_geometry_start)\n\n        main_group = grouped.get(classification.get(\"mainClass\"))\n        if main_group is None:\n            grouped[classification.get(\"mainClass\")] = OrderedDict()\n            main_group = grouped[classification.get(\"mainClass\")]\n        sub_key = classification.get(\"subClass\")\n        if sub_key not in main_group:\n            main_group[sub_key] = []\n        main_group[sub_key].append(entry)\n\n    grouping_start = time.perf_counter()\n    for key, value in map_data.items():\n        if not isinstance(value, list):\n            continue\n        for item in value:\n            if isinstance(item, dict) and item.get(\"elementType\"):\n                add_item(item)\n    add_timing(\"group-map-data.total\", time.perf_counter() - grouping_start)\n\n    return grouped\n\n\ndef _load_json(path: str) -> OrderedDict:\n    with open(path, \"r\") as handle:\n        return json.load(handle, object_pairs_hook=OrderedDict)\n\n\ndef _parse_env_bool(name: str) -> Optional[bool]:\n    raw = os.environ.get(name)\n    if raw is None:\n        return None\n    normalized = raw.strip().lower()\n    if normalized in (\"1\", \"true\", \"yes\", \"on\"):\n        return True\n    if normalized in (\"0\", \"false\", \"no\", \"off\"):\n        return False\n    return None\n\n\ndef _pretty_json_enabled(pretty_json: Optional[bool] = None) -> bool:\n    if pretty_json is not None:\n        return bool(pretty_json)\n    forced = _parse_env_bool(\"TOUCH_MAPPER_PRETTY_JSON\")\n    if forced is not None:\n        return forced\n    return False\n\n\ndef _write_json_fast(path: str, value: Any, pretty_json: Optional[bool] = None) -> None:\n    use_pretty = _pretty_json_enabled(pretty_json)\n    with open(path, \"w\") as handle:\n        if use_pretty:\n            json.dump(\n                value,\n                handle,\n                indent=2,\n                ensure_ascii=False,\n                check_circular=False\n            )\n            handle.write(\"\\n\")\n            return\n        json.dump(\n            value,\n            handle,\n            separators=(\",\", \":\"),\n            ensure_ascii=False,\n            check_circular=False\n        )\n\ndef run_standalone(args: List[str]) -> OrderedDict:\n    parser = argparse.ArgumentParser(description=\"Touch Mapper map description generator\")\n    parser.add_argument(\"input_path\", nargs=\"?\", help=\"Path to map-meta-raw.json\")\n    parser.add_argument(\"--debug-osm-id\", type=int, dest=\"debug_osm_id\",\n                        help=\"Print areas_raster debug output for the given OSM id\")\n    parsed = parser.parse_args(args)\n    if parsed.debug_osm_id is not None:\n        set_debug_osm_id(parsed.debug_osm_id)\n    input_path = parsed.input_path or os.path.join(os.getcwd(), \"map-meta-raw.json\")\n    if not os.path.exists(input_path):\n        input_path = os.path.join(os.getcwd(), \"test/data/map-meta.indented.json\")\n    return run_map_desc(input_path)\n\n\ndef run_map_desc(input_path: str, output_path: Optional[str] = None,\n                 options_override: Optional[Dict[str, Any]] = None,\n                 profile: Optional[Dict[str, float]] = None,\n                 pretty_json: Optional[bool] = None) -> OrderedDict:\n    run_start = time.perf_counter()\n\n    def add_timing(name: str, elapsed: float) -> None:\n        if profile is None:\n            return\n        profile[name] = profile.get(name, 0.0) + elapsed\n\n    base_dir = os.path.dirname(os.path.abspath(__file__))\n    spec_path = os.path.join(base_dir, \"map-description-classifications.json\")\n\n    spec = _load_json(spec_path)\n\n    map_data = _load_json(input_path)\n\n    group_map_data_start = time.perf_counter()\n    grouped = group_map_data(map_data, spec, options_override, profile)\n    add_timing(\"group-map-data\", time.perf_counter() - group_map_data_start)\n\n    if output_path is None:\n        output_path = os.path.join(os.path.dirname(input_path), \"map-meta.json\")\n\n    write_grouped_meta_start = time.perf_counter()\n    _write_json_fast(output_path, grouped, pretty_json=pretty_json)\n    add_timing(\"write-map-meta\", time.perf_counter() - write_grouped_meta_start)\n\n    augmented_path = os.path.join(os.path.dirname(output_path), \"map-meta.augmented.json\")\n    write_augmented_meta_start = time.perf_counter()\n    _write_json_fast(augmented_path, map_data, pretty_json=pretty_json)\n    add_timing(\"write-map-meta-augmented\", time.perf_counter() - write_augmented_meta_start)\n\n    output_path = os.path.join(os.path.dirname(input_path), \"map-content.json\")\n    # Code below creates stage \"Final map content\" data.\n    write_map_content_start = time.perf_counter()\n    map_desc_render.write_map_content(\n        grouped,\n        spec,\n        output_path,\n        map_data,\n        options_override,\n        profile,\n        pretty_json=pretty_json\n    )\n    add_timing(\"write-map-content\", time.perf_counter() - write_map_content_start)\n\n    add_timing(\"total\", time.perf_counter() - run_start)\n    return grouped\n\n\n__all__ = [\n    \"classify_item\",\n    \"group_map_data\",\n    \"run_map_desc\",\n    \"run_standalone\"\n]\n\n\nif __name__ == \"__main__\":\n    run_standalone(sys.argv[1:])\n"
  },
  {
    "path": "converter/map_desc/__main__.py",
    "content": "# Python 3.5\nfrom __future__ import print_function\n\nimport sys\n\nfrom . import run_standalone\n\n\ndef main() -> None:\n    run_standalone(sys.argv[1:])\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "converter/map_desc/areas_raster.py",
    "content": "# Python 3.5\nfrom __future__ import division\n\nimport math\nfrom typing import Any, Dict, List, Optional, Tuple\n\nfrom .map_desc_loc_segments import BBox, Point, classify_location\n\n\nGridCell = Tuple[int, int]\nRing = List[Tuple[float, float]]\n\nGRID_BASE = 60\nGRID_REFINED = 120\n# When True, run the legacy strategy: analyze 60 first and refine to 120 only\n# for multi-component cases. When False, analyze directly at 120.\nTRY_BASE_GRID_FIRST = False\n\nCOMPLEX_FILL_RATIO = 0.4\n\nANGLE_DEGREES = (0.0, 22.5, 45.0, 67.5)\nORIENTATION_LABELS = {\n    0.0: \"east-west\",\n    90.0: \"north-south\",\n    22.5: \"east-northeast to west-southwest\",\n    45.0: \"northeast-southwest\",\n    67.5: \"north-northeast to south-southwest\"\n}\n\nDEBUG_OSM_ID = None\n\n\ndef set_debug_osm_id(osm_id: Optional[int]) -> None:\n    global DEBUG_OSM_ID\n    DEBUG_OSM_ID = osm_id\n\n\ndef _coerce_point(coord: Any) -> Optional[Tuple[float, float]]:\n    if not isinstance(coord, list) or len(coord) < 2:\n        return None\n    x = coord[0]\n    y = coord[1]\n    if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):\n        return None\n    return float(x), float(y)\n\n\ndef _normalize_ring(coords: Any) -> Optional[Ring]:\n    if not isinstance(coords, list) or len(coords) < 3:\n        return None\n    ring = []\n    for coord in coords:\n        point = _coerce_point(coord)\n        if point is None:\n            continue\n        ring.append(point)\n    if len(ring) < 3:\n        return None\n    if ring[0] == ring[-1]:\n        ring = ring[:-1]\n    return ring if len(ring) >= 3 else None\n\n\ndef _extract_polygon_rings(geometry: Dict[str, Any]) -> Tuple[Optional[Ring], List[Ring]]:\n    outer = geometry.get(\"outer\")\n    if outer is None:\n        outer = geometry.get(\"coordinates\")\n    outer_ring = _normalize_ring(outer)\n    holes = []\n    for hole in geometry.get(\"holes\") or []:\n        ring = _normalize_ring(hole)\n        if ring:\n            holes.append(ring)\n    return outer_ring, holes\n\n\ndef _bbox_from_ring(ring: Ring) -> BBox:\n    min_x = ring[0][0]\n    max_x = ring[0][0]\n    min_y = ring[0][1]\n    max_y = ring[0][1]\n    for x, y in ring[1:]:\n        if x < min_x:\n            min_x = x\n        if x > max_x:\n            max_x = x\n        if y < min_y:\n            min_y = y\n        if y > max_y:\n            max_y = y\n    return {\"minX\": min_x, \"minY\": min_y, \"maxX\": max_x, \"maxY\": max_y}\n\n\ndef _bbox_intersection(a: BBox, b: BBox) -> Optional[BBox]:\n    min_x = max(a[\"minX\"], b[\"minX\"])\n    min_y = max(a[\"minY\"], b[\"minY\"])\n    max_x = min(a[\"maxX\"], b[\"maxX\"])\n    max_y = min(a[\"maxY\"], b[\"maxY\"])\n    if min_x > max_x or min_y > max_y:\n        return None\n    return {\"minX\": min_x, \"minY\": min_y, \"maxX\": max_x, \"maxY\": max_y}\n\n\ndef _grid_index_range(clip_min: float, clip_max: float,\n                      base_min: float, step: float, size: int) -> Optional[Tuple[int, int]]:\n    if step <= 0:\n        return None\n    min_idx = int(math.ceil((clip_min - base_min) / step - 0.5))\n    max_idx = int(math.floor((clip_max - base_min) / step - 0.5))\n    if max_idx < 0 or min_idx > size - 1:\n        return None\n    if min_idx < 0:\n        min_idx = 0\n    if max_idx > size - 1:\n        max_idx = size - 1\n    if min_idx > max_idx:\n        return None\n    return min_idx, max_idx\n\n\ndef _point_in_bbox(x: float, y: float, bbox: BBox) -> bool:\n    return bbox[\"minX\"] <= x <= bbox[\"maxX\"] and bbox[\"minY\"] <= y <= bbox[\"maxY\"]\n\n\ndef _point_in_ring(x: float, y: float, ring: Ring) -> bool:\n    inside = False\n    x0, y0 = ring[-1]\n    for x1, y1 in ring:\n        if (y1 > y) != (y0 > y):\n            x_intersect = (x0 - x1) * (y - y1) / (y0 - y1) + x1\n            if x < x_intersect:\n                inside = not inside\n        x0, y0 = x1, y1\n    return inside\n\n\ndef _point_in_polygon(x: float, y: float,\n                      outer: Ring, outer_bbox: BBox,\n                      holes: List[Ring], hole_bboxes: List[BBox]) -> bool:\n    # A point is inside if it is in the outer ring and not inside any hole ring.\n    if not _point_in_bbox(x, y, outer_bbox):\n        return False\n    if not _point_in_ring(x, y, outer):\n        return False\n    for ring, bbox in zip(holes, hole_bboxes):\n        if _point_in_bbox(x, y, bbox) and _point_in_ring(x, y, ring):\n            return False\n    return True\n\n\ndef _make_point(x: float, y: float) -> Point:\n    return {\"x\": x, \"y\": y}\n\n\ndef _segment_key(point: Point, boundary: BBox) -> Tuple[Optional[str], Optional[str]]:\n    classification = classify_location(point, boundary)\n    if not classification:\n        return None, None\n    loc = classification.get(\"loc\") if isinstance(classification, dict) else None\n    loc_kind = loc.get(\"kind\") if isinstance(loc, dict) else None\n    loc_dir = loc.get(\"dir\") if isinstance(loc, dict) else None\n    return loc_kind, loc_dir\n\n\ndef _rasterize_polygon(outer: Ring, holes: List[Ring],\n                       boundary: BBox, clip_bbox: BBox,\n                       grid_size: int,\n                       debug: bool = False,\n                       debug_tag: str = \"\") -> Dict[str, Any]:\n    dx = (boundary[\"maxX\"] - boundary[\"minX\"]) / grid_size\n    dy = (boundary[\"maxY\"] - boundary[\"minY\"]) / grid_size\n    if dx <= 0 or dy <= 0:\n        return {\n            \"gridSize\": grid_size,\n            \"insideCells\": 0,\n            \"consideredCount\": 0,\n            \"trueCells\": [],\n            \"mask\": [],\n            \"segments\": [],\n            \"components\": [],\n            \"edgesTouched\": [],\n            \"componentCount\": 0\n        }\n\n    col_range = _grid_index_range(clip_bbox[\"minX\"], clip_bbox[\"maxX\"],\n                                  boundary[\"minX\"], dx, grid_size)\n    row_range = _grid_index_range(clip_bbox[\"minY\"], clip_bbox[\"maxY\"],\n                                  boundary[\"minY\"], dy, grid_size)\n    if not col_range or not row_range:\n        return {\n            \"gridSize\": grid_size,\n            \"insideCells\": 0,\n            \"consideredCount\": 0,\n            \"trueCells\": [],\n            \"mask\": [],\n            \"segments\": [],\n            \"components\": [],\n            \"edgesTouched\": [],\n            \"componentCount\": 0\n        }\n\n    min_col, max_col = col_range\n    min_row, max_row = row_range\n\n    outer_bbox = _bbox_from_ring(outer)\n    hole_bboxes = [_bbox_from_ring(hole) for hole in holes]\n    if debug:\n        print(\"[areas_raster] {} grid={} dx={:.3f} dy={:.3f}\".format(debug_tag, grid_size, dx, dy))\n        print(\"[areas_raster] {} clip rows {}..{} cols {}..{}\".format(\n            debug_tag, min_row, max_row, min_col, max_col\n        ))\n        print(\"[areas_raster] {} outer_bbox={}\".format(debug_tag, outer_bbox))\n        print(\"[areas_raster] {} holes={}\".format(debug_tag, len(holes)))\n\n    mask = [[False for _ in range(grid_size)] for _ in range(grid_size)]\n    true_cells = []  # type: List[GridCell]\n    segment_counts = {}  # type: Dict[Tuple[Optional[str], Optional[str]], int]\n    considered_count = 0\n    inside_count = 0\n    edge_hits = {\n        \"north\": set(),\n        \"south\": set(),\n        \"east\": set(),\n        \"west\": set()\n    }\n\n    for row in range(min_row, max_row + 1):\n        y = boundary[\"minY\"] + (row + 0.5) * dy\n        for col in range(min_col, max_col + 1):\n            x = boundary[\"minX\"] + (col + 0.5) * dx\n            considered_count += 1\n            # Cell is \"inside\" only if the center point is inside the outer ring\n            # and not inside any hole ring.\n            if not _point_in_polygon(x, y, outer, outer_bbox, holes, hole_bboxes):\n                continue\n            mask[row][col] = True\n            true_cells.append((row, col))\n            inside_count += 1\n            if row == 0:\n                edge_hits[\"south\"].add(col)\n            if row == grid_size - 1:\n                edge_hits[\"north\"].add(col)\n            if col == 0:\n                edge_hits[\"west\"].add(row)\n            if col == grid_size - 1:\n                edge_hits[\"east\"].add(row)\n            seg_key = _segment_key(_make_point(x, y), boundary)\n            segment_counts[seg_key] = segment_counts.get(seg_key, 0) + 1\n\n    segments = []\n    if inside_count:\n        for (loc_kind, loc_dir), count in segment_counts.items():\n            entry = {\"insideCount\": count}  # type: Dict[str, Any]\n            if loc_kind:\n                entry[\"loc\"] = {\"kind\": loc_kind, \"dir\": loc_dir}\n            segments.append(entry)\n        segments.sort(key=lambda entry: (\n            -entry[\"insideCount\"],\n            (entry.get(\"loc\") or {}).get(\"kind\") or \"\",\n            (entry.get(\"loc\") or {}).get(\"dir\") or \"\"\n        ))\n    if debug:\n        top_segments = []\n        for seg in segments[:5]:\n            loc = seg.get(\"loc\") or {}\n            top_segments.append(\n                \"{}:{}={}\".format(loc.get(\"kind\"), loc.get(\"dir\"), seg.get(\"insideCount\"))\n            )\n        print(\"[areas_raster] {} insideCells={} consideredCells={}\".format(\n            debug_tag, inside_count, considered_count\n        ))\n        print(\"[areas_raster] {} segments(top5)={}\".format(debug_tag, top_segments))\n\n    components = []\n    edges_touched = set()\n    if true_cells:\n        visited = set()\n        for cell in true_cells:\n            if cell in visited:\n                continue\n            stack = [cell]\n            visited.add(cell)\n            count = 0\n            sum_x = 0.0\n            sum_y = 0.0\n            edges = set()\n            while stack:\n                row, col = stack.pop()\n                count += 1\n                sum_x += boundary[\"minX\"] + (col + 0.5) * dx\n                sum_y += boundary[\"minY\"] + (row + 0.5) * dy\n                if row == 0:\n                    edges.add(\"south\")\n                if row == grid_size - 1:\n                    edges.add(\"north\")\n                if col == 0:\n                    edges.add(\"west\")\n                if col == grid_size - 1:\n                    edges.add(\"east\")\n                for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):\n                    nr = row + dr\n                    nc = col + dc\n                    if nr < 0 or nr >= grid_size or nc < 0 or nc >= grid_size:\n                        continue\n                    if not mask[nr][nc]:\n                        continue\n                    neighbor = (nr, nc)\n                    if neighbor in visited:\n                        continue\n                    visited.add(neighbor)\n                    stack.append(neighbor)\n            centroid = _make_point(sum_x / count, sum_y / count)\n            location = classify_location(centroid, boundary)\n            edges_touched.update(edges)\n            components.append({\n                \"cellCount\": count,\n                \"centroid\": centroid,\n                \"location\": location,\n                \"touchesEdge\": bool(edges),\n                \"edges\": sorted(edges)\n            })\n\n    if components:\n        components.sort(key=lambda entry: -entry.get(\"cellCount\", 0))\n    edges_percent = []\n    if grid_size > 0:\n        for edge in (\"north\", \"east\", \"south\", \"west\"):\n            hit_count = len(edge_hits.get(edge, []))\n            if hit_count:\n                percent = round(100.0 * hit_count / grid_size, 1)\n                edges_percent.append({edge: percent})\n    if debug:\n        print(\"[areas_raster] {} components={} edgesTouched={}\".format(\n            debug_tag, len(components), sorted(edges_touched)\n        ))\n\n    return {\n        \"gridSize\": grid_size,\n        \"insideCells\": inside_count,\n        \"consideredCount\": considered_count,\n        \"trueCells\": true_cells,\n        \"mask\": mask,\n        \"segments\": segments,\n        \"components\": components,\n        \"edgesTouched\": edges_percent,\n        \"componentCount\": len(components)\n    }\n\n\ndef _shape_from_cells(cells: List[GridCell]) -> Optional[Dict[str, Any]]:\n    if not cells:\n        return None\n    points = []\n    min_x = None\n    max_x = None\n    min_y = None\n    max_y = None\n    for row, col in cells:\n        x = col + 0.5\n        y = row + 0.5\n        points.append((x, y))\n        if min_x is None or x < min_x:\n            min_x = x\n        if max_x is None or x > max_x:\n            max_x = x\n        if min_y is None or y < min_y:\n            min_y = y\n        if max_y is None or y > max_y:\n            max_y = y\n    area_cells = float(len(points))\n\n    best = None\n    for angle in ANGLE_DEGREES:\n        rad = math.radians(angle)\n        ux = math.cos(rad)\n        uy = math.sin(rad)\n        vx = -uy\n        vy = ux\n        min_u = None\n        max_u = None\n        min_v = None\n        max_v = None\n        for x, y in points:\n            proj_u = x * ux + y * uy\n            proj_v = x * vx + y * vy\n            if min_u is None or proj_u < min_u:\n                min_u = proj_u\n            if max_u is None or proj_u > max_u:\n                max_u = proj_u\n            if min_v is None or proj_v < min_v:\n                min_v = proj_v\n            if max_v is None or proj_v > max_v:\n                max_v = proj_v\n        if min_u is None or min_v is None or max_u is None or max_v is None:\n            continue\n        span_u = (max_u - min_u) + 1.0\n        span_v = (max_v - min_v) + 1.0\n        if span_u <= 0 or span_v <= 0:\n            continue\n        rect_area = span_u * span_v\n        fill_ratio = area_cells / rect_area\n        aspect_ratio = max(span_u, span_v) / max(1e-9, min(span_u, span_v))\n        entry = {\n            \"angle\": angle,\n            \"fillRatio\": fill_ratio,\n            \"aspectRatio\": aspect_ratio\n        }\n        if best is None:\n            best = entry\n        else:\n            if fill_ratio > best[\"fillRatio\"] + 1e-6:\n                best = entry\n            elif abs(fill_ratio - best[\"fillRatio\"]) <= 1e-6 and aspect_ratio > best[\"aspectRatio\"]:\n                best = entry\n\n    if not best:\n        return None\n    fill_ratio = best[\"fillRatio\"]\n    aspect_ratio = best[\"aspectRatio\"]\n    angle = best[\"angle\"]\n    orientation_deg = angle\n    orientation_label = ORIENTATION_LABELS.get(angle)\n    if angle == 0.0 and min_x is not None and max_x is not None and min_y is not None and max_y is not None:\n        span_x = max_x - min_x\n        span_y = max_y - min_y\n        if span_y > span_x and aspect_ratio > 1.05:\n            orientation_deg = 90.0\n            orientation_label = ORIENTATION_LABELS.get(orientation_deg)\n    result = {\n        \"orientationDeg\": orientation_deg,\n        \"orientationLabel\": orientation_label,\n        \"fillRatio\": round(fill_ratio, 3),\n        \"aspectRatio\": round(aspect_ratio, 3)\n    }\n    if fill_ratio <= COMPLEX_FILL_RATIO:\n        result[\"type\"] = \"complex\"\n    else:\n        result[\"type\"] = \"regular\"\n    return result\n\n\ndef _dir_label(direction: Optional[str]) -> Optional[str]:\n    if not direction:\n        return None\n    if direction == \"northwest\":\n        return \"north-west\"\n    if direction == \"northeast\":\n        return \"north-east\"\n    if direction == \"southwest\":\n        return \"south-west\"\n    if direction == \"southeast\":\n        return \"south-east\"\n    return direction\n\n\ndef _segment_label_from_loc(loc: Optional[Dict[str, Any]]) -> Optional[str]:\n    if not loc or not isinstance(loc, dict):\n        return None\n    kind = loc.get(\"kind\")\n    direction = loc.get(\"dir\")\n    dir_label = _dir_label(direction)\n    if kind == \"center\":\n        return \"center\"\n    if kind == \"part\":\n        if dir_label:\n            return \"{} part\".format(dir_label)\n        return \"center\"\n    if kind == \"near_edge\":\n        if direction == \"northwest\":\n            return \"near north-west corner\"\n        if direction == \"northeast\":\n            return \"near north-east corner\"\n        if direction == \"southwest\":\n            return \"near south-west corner\"\n        if direction == \"southeast\":\n            return \"near south-east corner\"\n        if dir_label:\n            return \"near {} edge\".format(dir_label)\n        return \"near edge\"\n    return None\n\n\ndef _coverage_breakdown_text(segments: List[Dict[str, Any]],\n                             total_inside: int,\n                             total_cells: int) -> Tuple[Optional[str], List[str]]:\n    if not segments or total_inside <= 0:\n        return None, []\n    ordered = [seg for seg in segments if isinstance(seg, dict) and isinstance(seg.get(\"insideCount\"), (int, float))]\n    if not ordered:\n        return None, []\n    ordered.sort(key=lambda entry: -entry.get(\"insideCount\", 0))\n    phrases = []\n    percents = []\n    for segment in ordered:\n        inside_count = float(segment.get(\"insideCount\", 0))\n        if inside_count <= 0:\n            continue\n        ratio = inside_count / float(total_inside)\n        if ratio >= 0.5:\n            qualifier = \"much of\"\n        elif ratio >= 0.2:\n            qualifier = \"some of\"\n        else:\n            qualifier = \"a little of\"\n        segment_text = _segment_label_from_loc(segment.get(\"loc\"))\n        if not segment_text:\n            continue\n        phrases.append(\"{} {}\".format(qualifier, segment_text))\n        percent = 0.0\n        if total_cells > 0:\n            percent = round(100.0 * inside_count / float(total_cells), 1)\n        percents.append(\"{}={:.1f}%\".format(segment_text, percent))\n    if not phrases:\n        return None, []\n    if len(phrases) == 1:\n        summary = phrases[0]\n    elif len(phrases) == 2:\n        summary = phrases[0] + \" and \" + phrases[1]\n    else:\n        summary = \", \".join(phrases[:-1]) + \", and \" + phrases[-1]\n    return summary, percents\n\n\ndef analyze_area_visibility(geometry: Dict[str, Any],\n                            boundary: Optional[BBox],\n                            osm_id: Optional[Any] = None) -> Optional[Dict[str, Any]]:\n    debug = False\n    debug_tag = \"\"\n    osm_id_num = None\n    if osm_id is not None:\n        try:\n            osm_id_num = int(osm_id)\n        except (TypeError, ValueError):\n            osm_id_num = None\n    if DEBUG_OSM_ID is not None and osm_id_num == DEBUG_OSM_ID:\n        debug = True\n        debug_tag = \"osmId={}\".format(osm_id_num)\n    outer, holes = _extract_polygon_rings(geometry)\n    if not outer or not boundary:\n        return None\n    initial_grid_size = GRID_BASE if TRY_BASE_GRID_FIRST else GRID_REFINED\n    if debug:\n        print(\"[areas_raster] {} start outer_points={} holes={}\".format(\n            debug_tag, len(outer), len(holes)\n        ))\n        print(\"[areas_raster] {} boundary={}\".format(debug_tag, boundary))\n\n    outer_bbox = _bbox_from_ring(outer)\n    clip_bbox = _bbox_intersection(outer_bbox, boundary)\n    if not clip_bbox:\n        return {\n            \"coverage\": {\n                \"coveragePercent\": 0.0,\n                \"insideCells\": 0,\n                \"consideredCells\": 0,\n                \"gridSize\": initial_grid_size,\n                \"segments\": []\n            },\n            \"edgesTouched\": [],\n            \"components\": []\n        }\n    if debug:\n        print(\"[areas_raster] {} outer_bbox={}\".format(debug_tag, outer_bbox))\n        print(\"[areas_raster] {} clip_bbox={}\".format(debug_tag, clip_bbox))\n\n    refined_from = None\n    analysis_60 = None\n    analysis_120 = None\n    if TRY_BASE_GRID_FIRST:\n        analysis_60 = _rasterize_polygon(\n            outer, holes, boundary, clip_bbox, GRID_BASE,\n            debug=debug, debug_tag=debug_tag + \":60\"\n        )\n        analysis = analysis_60\n        if analysis_60.get(\"componentCount\", 0) > 1:\n            analysis_120 = _rasterize_polygon(\n                outer, holes, boundary, clip_bbox, GRID_REFINED,\n                debug=debug, debug_tag=debug_tag + \":120\"\n            )\n            if analysis_120.get(\"insideCells\", 0) > 0:\n                analysis = analysis_120\n                refined_from = GRID_BASE\n    else:\n        analysis_120 = _rasterize_polygon(\n            outer, holes, boundary, clip_bbox, GRID_REFINED,\n            debug=debug, debug_tag=debug_tag + \":120\"\n        )\n        analysis = analysis_120\n\n    inside_count = analysis.get(\"insideCells\", 0)\n    grid_size = analysis.get(\"gridSize\", initial_grid_size)\n    total_cells = grid_size * grid_size if grid_size else 0\n    coverage_percent = 0.0\n    if total_cells > 0:\n        coverage_percent = round(100.0 * inside_count / total_cells, 1)\n        if coverage_percent < 0:\n            coverage_percent = 0.0\n        if coverage_percent > 100:\n            coverage_percent = 100.0\n    if debug:\n        print(\"[areas_raster] {} gridSize={} insideCells={} coveragePercent={}\".format(\n            debug_tag, grid_size, inside_count, coverage_percent\n        ))\n        coverage_summary, coverage_percents = _coverage_breakdown_text(\n            analysis.get(\"segments\", []), inside_count, total_cells\n        )\n        if coverage_summary:\n            print(\"[areas_raster] {} coverageSegments={}\".format(debug_tag, coverage_summary))\n        if coverage_percents:\n            print(\"[areas_raster] {} coverageSegmentsPercent={}\".format(debug_tag, coverage_percents))\n        if refined_from:\n            print(\"[areas_raster] {} refinedFrom={}\".format(debug_tag, refined_from))\n\n    shape = _shape_from_cells(analysis.get(\"trueCells\") or [])\n    if debug:\n        print(\"[areas_raster] {} shape={}\".format(debug_tag, shape))\n\n    coverage = {\n        \"coveragePercent\": coverage_percent,\n        \"insideCells\": inside_count,\n        \"consideredCells\": analysis.get(\"consideredCount\", 0),\n        \"gridSize\": grid_size,\n        \"segments\": analysis.get(\"segments\", [])\n    }\n    result = {\n        \"coverage\": coverage,\n        \"edgesTouched\": analysis.get(\"edgesTouched\", []),\n        \"components\": analysis.get(\"components\", [])\n    }\n    if refined_from:\n        result[\"refinedFrom\"] = refined_from\n    if shape:\n        result[\"shape\"] = shape\n        result[\"shapeGridSize\"] = grid_size\n\n    if debug:\n        from .areas_raster_debug import print_union_grid\n        mask_60 = analysis_60.get(\"mask\") if analysis_60 else None\n        mask_120 = None\n        if analysis_120:\n            mask_120 = analysis_120.get(\"mask\")\n        print_union_grid(mask_60, mask_120, boundary)\n    return result\n\n\n__all__ = [\"analyze_area_visibility\"]\n"
  },
  {
    "path": "converter/map_desc/areas_raster_debug.py",
    "content": "# Python 3.5\nfrom __future__ import division\n\nfrom typing import List, Optional, Tuple\n\nfrom .map_desc_loc_segments import BBox, classify_location\n\n\nGrid = List[List[str]]\nMask = List[List[bool]]\nBorderMap = List[List[bool]]\n\n# fmt: off\n\"\"\" Here a building is marked by * characters. This cell matrix is used for determining where the building is, its size, and rough orientation.\n +------------------------------------------------------------------------------------------------------------------------\n |                                .                                                     ..                               |\n |                                 .                                                   ..                                |\n |      north-west edge             .                                                 ..                                 |\n |                                   .                                               ..\n |                                   .                                               .\n |                                    .                                             .\n |                                     .                                           .\n |              ...........................................................................................\n |              .                        .                                       .                       ******\n |              .                         .                                     .                      **********\n |              .   north-west part        .                                   .                         **********\n |              .                           .                                 .                          ************\n |              .                            .                               ..                           .************\n |              .                             .                             ..                            .  **********\n |              .              ............................................................................  ************\n |              .              .                .                         ..               .              .    **********\n | ..           .              .                 .                       ..                .              .    **********\n |     ..       .              .                  .                     ..     north-east  .              .    **********\n |          ..  .              .                   .                   ..      near center .              .  ..**********\n |              ..             .                   .                   .                   .             ...   **********\n |              .   ..         .                    .                 .                    .         ...  .    **********\n |              .       ...    .                     .               .                     .    ....      .    **********\n |              .            ...              ...............................              ....           .\n |              .              . ..           .                             .           ....              .\n |              .              .     ..       .                             .       ...    .              .\n |              .              .          ..  .                             .  ...         .              .\n |              .              .              .                             ..             .              .\n |              .              .              .                             .              .              .\n |              .              .              .                             .              .              .\n |              .              .              .                             .              .              .\n |              .              .              .         center              .              .              .\n |              .              .              .                             .              .              .\n |              .              .              .                             .              .              .\n |              .              .             ..                             ..             .              .\n |              .              .         ...  .                             .   ..         .              .\n |              .              .    ...       .                             .        ..    .              .\n |              .              ....           .                             .            ...              .\n |              .           ....               ..............................              . ..           .\n |              .      ....    .                     .               .                     .     ...      .\n |              .  ...         .                    .                 .                    .          ..  .\n |             ...             .                   .                   .                   .              ..\n |         ...  .              .                  ..                    .                  .              .   ..\n |    ...       .              .                 ..                      .                 .              .        ..\n |...           .              .                ..                        .                .              .            ..\n |              .              .               ..                          .               .              .\n |              .              .............................................................              .\n |              .                            ..                              .                            .\n |              .                           ..                                .                           .\n |              .                           .                                 .                           .\n |              .                          .                                   .                          .\n |              .                         .                                     .                         .\n |              .                        .                                       .                        .\n |               ..........................................................................................\n |                                     .                                           .\n |                                    .                                             .\n |                                   .                                               .\n |                                  ..                                                .\n |                                 ..                                                  .\n |                                ..                                                    .                                  |\n |                               ..                                                      .                                 |\n +------------------------------------------------------------------------------------------------------------------------\n\"\"\"\n\ndef make_canvas(size: int = 120) -> Grid:\n    return [[\" \" for _ in range(size)] for _ in range(size)]\n\n\ndef add_mask_60(canvas: Grid, mask: Optional[Mask]) -> None:\n    if not mask:\n        return\n    size = len(mask)\n    if size != 60:\n        return\n    for row in range(size):\n        row_data = mask[row]\n        if not row_data:\n            continue\n        for col in range(size):\n            if not row_data[col]:\n                continue\n            for dr in (0, 1):\n                for dc in (0, 1):\n                    rr = row * 2 + dr\n                    cc = col * 2 + dc\n                    if rr >= 120 or cc >= 120:\n                        continue\n                    if canvas[rr][cc] == \"X\":\n                        continue\n                    canvas[rr][cc] = \"*\"\n\n\ndef add_mask_120(canvas: Grid, mask: Optional[Mask]) -> None:\n    if not mask:\n        return\n    size = len(mask)\n    if size != 120:\n        return\n    for row in range(size):\n        row_data = mask[row]\n        if not row_data:\n            continue\n        for col in range(size):\n            if row_data[col]:\n                canvas[row][col] = \"X\"\n\n\ndef _segment_key(boundary: BBox, row: int, col: int, size: int) -> Tuple[Optional[str], Optional[str]]:\n    if not boundary:\n        return None, None\n    dx = (boundary[\"maxX\"] - boundary[\"minX\"]) / size\n    dy = (boundary[\"maxY\"] - boundary[\"minY\"]) / size\n    x = boundary[\"minX\"] + (col + 0.5) * dx\n    y = boundary[\"minY\"] + (row + 0.5) * dy\n    classification = classify_location({\"x\": x, \"y\": y}, boundary)\n    if not classification:\n        return None, None\n    loc = classification.get(\"loc\") if isinstance(classification, dict) else None\n    if not isinstance(loc, dict):\n        return None, None\n    return loc.get(\"kind\"), loc.get(\"dir\")\n\n\ndef _segment_borders(boundary: BBox, size: int = 120) -> BorderMap:\n    borders = [[False for _ in range(size)] for _ in range(size)]\n    if not boundary:\n        return borders\n    for row in range(size):\n        for col in range(size):\n            key = _segment_key(boundary, row, col, size)\n            for dr, dc in ((1, 0), (0, 1)):  # north/east only for thin borders\n                nr = row + dr\n                nc = col + dc\n                if nr < 0 or nr >= size or nc < 0 or nc >= size:\n                    continue\n                if _segment_key(boundary, nr, nc, size) != key:\n                    borders[row][col] = True\n                    break\n    return borders\n\n\ndef _slot(mark: str, border: bool) -> str:\n    first = mark if mark in (\"X\", \"*\") else \" \"\n    second = \".\" if border else \" \"\n    return first + second\n\n\ndef print_canvas(canvas: Grid, borders: Optional[BorderMap] = None) -> None:\n    if not canvas:\n        return\n    width = len(canvas[0])\n    top = \"+\" + (\"-\" * (width * 2)) + \"+\"\n    print(top)\n    border_rows = borders if borders is not None else [[False for _ in range(width)] for _ in range(width)]\n    for row_idx in range(len(canvas) - 1, -1, -1):\n        row = canvas[row_idx]\n        border_row = border_rows[row_idx] if row_idx < len(border_rows) else None\n        slots = []\n        if len(row) < width:\n            row = row + ([\" \"] * (width - len(row)))\n        for col in range(width):\n            border = False\n            if border_row and col < len(border_row):\n                border = border_row[col]\n            slots.append(_slot(row[col], border))\n        print(\"|\" + \"\".join(slots) + \"|\")\n    print(top)\n\n\ndef _union_mask(mask_60: Optional[Mask], mask_120: Optional[Mask]) -> Mask:\n    size = 120\n    filled = [[False for _ in range(size)] for _ in range(size)]\n    if mask_60:\n        if len(mask_60) == 60:\n            for row in range(60):\n                for col in range(60):\n                    if not mask_60[row][col]:\n                        continue\n                    for dr in (0, 1):\n                        for dc in (0, 1):\n                            rr = row * 2 + dr\n                            cc = col * 2 + dc\n                            if rr < size and cc < size:\n                                filled[rr][cc] = True\n    if mask_120 and len(mask_120) == 120:\n        for row in range(120):\n            for col in range(120):\n                if mask_120[row][col]:\n                    filled[row][col] = True\n    return filled\n\n\ndef print_union_grid(mask_60: Optional[Mask], mask_120: Optional[Mask], boundary: Optional[BBox]) -> None:\n    canvas = make_canvas(120)\n    add_mask_60(canvas, mask_60)\n    add_mask_120(canvas, mask_120)\n    borders = _segment_borders(boundary, 120) if boundary else None\n    print_canvas(canvas, borders)\n\n\n__all__ = [\"make_canvas\", \"add_mask_60\", \"add_mask_120\", \"print_canvas\", \"print_union_grid\"]\n"
  },
  {
    "path": "converter/map_desc/feature_semantics.py",
    "content": "# Python 3.5\nfrom __future__ import division\n\nfrom collections import OrderedDict\nfrom typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple\n\nUNKNOWN = object()\n\nPAVED_SURFACES = {\n    \"asphalt\",\n    \"concrete\",\n    \"concrete:lanes\",\n    \"concrete:plates\",\n    \"paved\",\n    \"paving_stones\",\n    \"sett\",\n    \"bricks\",\n    \"cobblestone\"\n}\n\nUNPAVED_SURFACES = {\n    \"unpaved\",\n    \"gravel\",\n    \"fine_gravel\",\n    \"pebblestone\",\n    \"dirt\",\n    \"earth\",\n    \"ground\",\n    \"mud\",\n    \"sand\",\n    \"grass\",\n    \"grass_paver\",\n    \"woodchips\",\n    \"snow\",\n    \"ice\"\n}\n\nSMOOTHNESS_VALUES = {\n    \"excellent\",\n    \"good\",\n    \"intermediate\",\n    \"bad\",\n    \"very_bad\",\n    \"horrible\",\n    \"very_horrible\",\n    \"impassable\"\n}\n\nSIDEWALK_VALUES = {\"both\", \"left\", \"right\", \"no\", \"separate\"}\nONEWAY_VALUES = {\"yes\", \"no\", \"reversible\"}\nCYCLEWAY_VALUES = {\"lane\", \"track\", \"shared_lane\", \"shared\", \"no\"}\nWHEELCHAIR_VALUES = {\"yes\", \"no\", \"limited\"}\nACCESS_VALUES = {\"yes\", \"no\", \"permissive\", \"private\", \"destination\", \"customers\"}\nCROSSING_TYPES = {\"uncontrolled\", \"traffic_signals\", \"marked\", \"island\"}\nKERB_VALUES = {\"flush\", \"lowered\", \"raised\"}\n\n\ndef _normalize_tag_value(value: Any) -> Optional[str]:\n    if value is None:\n        return None\n    text = str(value).strip()\n    if text == \"\":\n        return None\n    return text.lower()\n\n\ndef _record_raw(raw: OrderedDict, key: str, value: Any) -> None:\n    if value is None:\n        return\n    text = str(value)\n    existing = raw.get(key)\n    if existing is None:\n        raw[key] = [text]\n        return\n    if text not in existing:\n        existing.append(text)\n\n\ndef _get_tag_value(tags: Dict[str, Any], key: str, raw: OrderedDict) -> Optional[str]:\n    if key not in tags:\n        return None\n    value = tags.get(key)\n    _record_raw(raw, key, value)\n    if value is None:\n        return None\n    return str(value).strip()\n\n\ndef _parse_yes_no(value: Optional[str]) -> Any:\n    if value is None:\n        return None\n    lowered = value.lower()\n    if lowered in (\"yes\", \"true\", \"1\"):\n        return \"yes\"\n    if lowered in (\"no\", \"false\", \"0\"):\n        return \"no\"\n    return UNKNOWN\n\n\ndef _parse_enum(value: Optional[str], allowed: Iterable[str]) -> Any:\n    if value is None:\n        return None\n    lowered = value.lower()\n    if lowered in allowed:\n        return lowered\n    return UNKNOWN\n\n\ndef _parse_int_strict(value: Optional[str]) -> Any:\n    if value is None:\n        return None\n    text = value.strip()\n    if text == \"\" or \";\" in text or \"|\" in text:\n        return UNKNOWN\n    try:\n        num = int(text)\n    except ValueError:\n        return UNKNOWN\n    if num < 0:\n        return UNKNOWN\n    return num\n\n\ndef _parse_float_meters(value: Optional[str]) -> Any:\n    if value is None:\n        return None\n    text = value.strip().lower()\n    if text == \"\" or \";\" in text or \"|\" in text:\n        return UNKNOWN\n    for suffix in (\" meters\", \" meter\", \" m\"):\n        if text.endswith(suffix):\n            text = text[:-len(suffix)].strip()\n            break\n    try:\n        num = float(text)\n    except ValueError:\n        return UNKNOWN\n    if num < 0:\n        return UNKNOWN\n    return num\n\n\ndef _parse_speed_kmh(value: Optional[str]) -> Any:\n    if value is None:\n        return None\n    text = value.strip().lower()\n    if text == \"\" or \";\" in text or \"|\" in text:\n        return UNKNOWN\n    try:\n        num = float(text)\n    except ValueError:\n        return UNKNOWN\n    if num < 0:\n        return UNKNOWN\n    if num.is_integer():\n        return int(num)\n    return num\n\n\ndef _surface_class(value: Optional[str]) -> Any:\n    if value is None:\n        return None\n    lowered = value.lower()\n    if lowered in PAVED_SURFACES:\n        return \"paved\"\n    if lowered in UNPAVED_SURFACES:\n        return \"unpaved\"\n    return UNKNOWN\n\n\ndef _merge_uniform_or_mixed(values: List[Any], touched: bool) -> Optional[str]:\n    if not touched:\n        return None\n    known = [value for value in values if value not in (None, UNKNOWN)]\n    if not known:\n        return \"unknown\"\n    first = known[0]\n    for value in known[1:]:\n        if value != first:\n            return \"mixed\"\n    return first\n\n\ndef _merge_numeric_with_source(entries: Sequence[Tuple[Any, Optional[str]]],\n                               touched: bool) -> Optional[OrderedDict]:\n    if not touched:\n        return None\n    known = [entry for entry in entries if entry[0] is not None]\n    if not known:\n        return OrderedDict([(\"value\", None), (\"source\", \"unknown\")])\n    value = known[0][0]\n    for entry in known[1:]:\n        if entry[0] != value:\n            return OrderedDict([(\"value\", None), (\"source\", \"unknown\")])\n    source = known[0][1]\n    for entry in known[1:]:\n        if entry[1] != source:\n            source = \"unknown\"\n            break\n    return OrderedDict([(\"value\", value), (\"source\", source or \"unknown\")])\n\n\ndef _merge_lanes(entries: List[Dict[str, Any]], touched: bool) -> Optional[OrderedDict]:\n    if not touched:\n        return None\n    known = [entry for entry in entries if entry.get(\"total\") is not None]\n    if not known:\n        return OrderedDict([\n            (\"total\", None),\n            (\"forward\", None),\n            (\"backward\", None),\n            (\"source\", \"unknown\")\n        ])\n    total = known[0].get(\"total\")\n    for entry in known[1:]:\n        if entry.get(\"total\") != total:\n            return OrderedDict([\n                (\"total\", None),\n                (\"forward\", None),\n                (\"backward\", None),\n                (\"source\", \"unknown\")\n            ])\n    forward_values = [entry.get(\"forward\") for entry in entries if entry.get(\"forward\") is not None]\n    backward_values = [entry.get(\"backward\") for entry in entries if entry.get(\"backward\") is not None]\n    forward = None\n    backward = None\n    if forward_values and all(value == forward_values[0] for value in forward_values):\n        forward = forward_values[0]\n    if backward_values and all(value == backward_values[0] for value in backward_values):\n        backward = backward_values[0]\n    sources = [entry.get(\"source\") for entry in entries if entry.get(\"source\")]\n    source = sources[0] if sources and all(val == sources[0] for val in sources) else \"unknown\"\n    return OrderedDict([\n        (\"total\", total),\n        (\"forward\", forward),\n        (\"backward\", backward),\n        (\"source\", source)\n    ])\n\n\ndef _merge_incline(entries: List[Dict[str, Any]], touched: bool) -> Optional[OrderedDict]:\n    if not touched:\n        return None\n    values = [entry.get(\"value\") for entry in entries]\n    percents = [entry.get(\"percent\") for entry in entries]\n    value = _merge_uniform_or_mixed(values, True)\n    percent_entries = [(percent, \"incline\") for percent in percents]\n    percent_out = _merge_numeric_with_source(percent_entries, True)\n    percent_value = percent_out.get(\"value\") if percent_out else None\n    return OrderedDict([(\"value\", value), (\"percent\", percent_value)])\n\n\ndef _finalize_raw(raw: OrderedDict) -> Optional[OrderedDict]:\n    if not raw:\n        return None\n    out = OrderedDict()\n    for key, values in raw.items():\n        if len(values) == 1:\n            out[key] = values[0]\n        else:\n            out[key] = list(values)\n    return out\n\n\ndef _sorted_tag_sources(sources: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\n    type_rank = {\"relation\": 0, \"way\": 1, \"node\": 2}\n    def sort_key(entry):\n        return (type_rank.get(entry.get(\"osmType\"), 3), entry.get(\"osmId\") or 0)\n    return sorted(sources, key=sort_key)\n\n\ndef _tag_sources(item: Dict[str, Any]) -> List[Dict[str, Any]]:\n    sources = item.get(\"tagSources\")\n    if isinstance(sources, list):\n        cleaned = []\n        for source in sources:\n            if not isinstance(source, dict):\n                continue\n            tags = source.get(\"tags\")\n            if not isinstance(tags, dict):\n                continue\n            cleaned.append({\n                \"osmType\": source.get(\"osmType\"),\n                \"osmId\": source.get(\"osmId\"),\n                \"tags\": tags\n            })\n        if cleaned:\n            return _sorted_tag_sources(cleaned)\n    tags = item.get(\"tags\")\n    if isinstance(tags, dict):\n        return [{\n            \"osmType\": item.get(\"osmType\"),\n            \"osmId\": item.get(\"osmId\"),\n            \"tags\": tags\n        }]\n    return []\n\n\ndef _parse_surface(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, Any, bool]:\n    if \"surface\" in tags:\n        value = _normalize_tag_value(_get_tag_value(tags, \"surface\", raw))\n        return (value if value is not None else UNKNOWN,\n                _surface_class(value) if value is not None else UNKNOWN,\n                True)\n    if \"tracktype\" in tags:\n        highway = _normalize_tag_value(_get_tag_value(tags, \"highway\", raw))\n        track_val = _normalize_tag_value(_get_tag_value(tags, \"tracktype\", raw))\n        if highway == \"track\":\n            if track_val is None:\n                return (UNKNOWN, UNKNOWN, True)\n            return (track_val, _surface_class(track_val), True)\n    if \"material\" in tags:\n        material = _normalize_tag_value(_get_tag_value(tags, \"material\", raw))\n        if material is None:\n            return (UNKNOWN, UNKNOWN, True)\n        return (material, _surface_class(material), True)\n    return (None, None, False)\n\n\ndef _parse_smoothness(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, bool]:\n    if \"smoothness\" not in tags:\n        return (None, False)\n    value = _parse_enum(_get_tag_value(tags, \"smoothness\", raw), SMOOTHNESS_VALUES)\n    return (value, True)\n\n\ndef _parse_lit(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, bool]:\n    if \"lit\" not in tags:\n        return (None, False)\n    return (_parse_yes_no(_get_tag_value(tags, \"lit\", raw)), True)\n\n\ndef _parse_oneway(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, bool]:\n    if \"oneway\" not in tags:\n        return (None, False)\n    value = _normalize_tag_value(_get_tag_value(tags, \"oneway\", raw))\n    if value in (\"yes\", \"true\", \"1\"):\n        return (\"yes\", True)\n    if value in (\"no\", \"false\", \"0\"):\n        return (\"no\", True)\n    if value == \"reversible\":\n        return (\"reversible\", True)\n    return (UNKNOWN, True)\n\n\ndef _parse_lanes(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Dict[str, Any], bool]:\n    lanes = {\"total\": None, \"forward\": None, \"backward\": None, \"source\": None}  # type: Dict[str, Any]\n    forward_raw = _get_tag_value(tags, \"lanes:forward\", raw)\n    backward_raw = _get_tag_value(tags, \"lanes:backward\", raw)\n    total_raw = _get_tag_value(tags, \"lanes\", raw)\n    touched = forward_raw is not None or backward_raw is not None or total_raw is not None\n    if not touched:\n        return (lanes, False)\n    forward = _parse_int_strict(forward_raw)\n    backward = _parse_int_strict(backward_raw)\n    if isinstance(forward, int) and isinstance(backward, int):\n        lanes[\"forward\"] = forward\n        lanes[\"backward\"] = backward\n        lanes[\"total\"] = forward + backward\n        lanes[\"source\"] = \"lanes:forward/backward\"\n        return (lanes, True)\n    total = _parse_int_strict(total_raw)\n    if isinstance(total, int):\n        lanes[\"total\"] = total\n        lanes[\"source\"] = \"lanes\"\n        return (lanes, True)\n    lanes[\"source\"] = \"unknown\"\n    return (lanes, True)\n\n\ndef _parse_width(tags: Dict[str, Any], raw: OrderedDict, lanes_hint: Any) -> Tuple[Tuple[Any, Optional[str]], bool]:\n    width_raw = _get_tag_value(tags, \"width\", raw)\n    if width_raw is not None:\n        value = _parse_float_meters(width_raw)\n        return ((value if value is not UNKNOWN else None, \"width\"), True)\n    est_raw = _get_tag_value(tags, \"est_width\", raw)\n    if est_raw is not None:\n        value = _parse_float_meters(est_raw)\n        return ((value if value is not UNKNOWN else None, \"est_width\"), True)\n    lane_raw = _get_tag_value(tags, \"lane_width\", raw)\n    if lane_raw is not None:\n        lane_width = _parse_float_meters(lane_raw)\n        if lane_width is UNKNOWN:\n            return ((None, \"lane_width*lanes\"), True)\n        if isinstance(lanes_hint, int):\n            return ((lane_width * lanes_hint, \"lane_width*lanes\"), True)\n        return ((None, \"lane_width*lanes\"), True)\n    return ((None, None), False)\n\n\ndef _parse_maxspeed(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Tuple[Any, Optional[str]], bool]:\n    fwd_raw = _get_tag_value(tags, \"maxspeed:forward\", raw)\n    back_raw = _get_tag_value(tags, \"maxspeed:backward\", raw)\n    touched = fwd_raw is not None or back_raw is not None\n    if touched:\n        fwd = _parse_speed_kmh(fwd_raw)\n        back = _parse_speed_kmh(back_raw)\n        if fwd is not UNKNOWN and back is not UNKNOWN and fwd is not None and back is not None and fwd == back:\n            return ((fwd, \"maxspeed:forward/backward\"), True)\n    max_raw = _get_tag_value(tags, \"maxspeed\", raw)\n    if max_raw is not None:\n        value = _parse_speed_kmh(max_raw)\n        return ((value if value is not UNKNOWN else None, \"maxspeed\"), True)\n    if touched:\n        return ((None, \"unknown\"), True)\n    return ((None, None), False)\n\n\ndef _parse_sidewalk(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, bool]:\n    if \"sidewalk\" not in tags:\n        return (None, False)\n    return (_parse_enum(_get_tag_value(tags, \"sidewalk\", raw), SIDEWALK_VALUES), True)\n\n\ndef _parse_cycleway(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, bool]:\n    if \"cycleway\" in tags:\n        return (_parse_enum(_get_tag_value(tags, \"cycleway\", raw), CYCLEWAY_VALUES), True)\n    left_raw = _get_tag_value(tags, \"cycleway:left\", raw)\n    right_raw = _get_tag_value(tags, \"cycleway:right\", raw)\n    if left_raw is None and right_raw is None:\n        return (None, False)\n    left = _parse_enum(left_raw, CYCLEWAY_VALUES)\n    right = _parse_enum(right_raw, CYCLEWAY_VALUES)\n    if left is UNKNOWN or right is UNKNOWN:\n        if left is None and right is None:\n            return (None, False)\n        return (UNKNOWN, True)\n    if left is not None and right is not None and left != right:\n        return (\"mixed\", True)\n    return (left if left is not None else right, True)\n\n\ndef _parse_segregated(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, bool]:\n    if \"segregated\" not in tags:\n        return (None, False)\n    return (_parse_yes_no(_get_tag_value(tags, \"segregated\", raw)), True)\n\n\ndef _parse_crossing(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Dict[str, Any], bool]:\n    touched = False\n    crossing_raw = None\n    if \"highway\" in tags:\n        highway = _normalize_tag_value(_get_tag_value(tags, \"highway\", raw))\n        if highway == \"crossing\":\n            touched = True\n    if \"crossing\" in tags:\n        crossing_raw = _get_tag_value(tags, \"crossing\", raw)\n        touched = True\n    if \"crossing:markings\" in tags or \"tactile_paving\" in tags:\n        touched = True\n    if not touched:\n        return ({}, False)\n    crossing_type = _parse_enum(crossing_raw, CROSSING_TYPES)\n    if crossing_raw is None and touched:\n        crossing_type = UNKNOWN\n    markings = _parse_yes_no(_get_tag_value(tags, \"crossing:markings\", raw))\n    tactile = _parse_yes_no(_get_tag_value(tags, \"tactile_paving\", raw))\n    return (OrderedDict([\n        (\"type\", crossing_type if crossing_type is not None else UNKNOWN),\n        (\"markings\", markings if markings is not None else UNKNOWN),\n        (\"tactile_paving\", tactile if tactile is not None else UNKNOWN)\n    ]), True)\n\n\ndef _parse_kerb(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, bool]:\n    if \"kerb\" not in tags:\n        return (None, False)\n    return (_parse_enum(_get_tag_value(tags, \"kerb\", raw), KERB_VALUES), True)\n\n\ndef _parse_incline(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Dict[str, Any], bool]:\n    if \"incline\" not in tags:\n        return ({}, False)\n    value = _get_tag_value(tags, \"incline\", raw)\n    if value is None:\n        return (OrderedDict([(\"value\", \"unknown\"), (\"percent\", None)]), True)\n    text = value.strip().lower()\n    if text in (\"up\", \"down\"):\n        return (OrderedDict([(\"value\", text), (\"percent\", None)]), True)\n    if text.endswith(\"%\"):\n        num_text = text[:-1].strip()\n        try:\n            num = float(num_text)\n        except ValueError:\n            return (OrderedDict([(\"value\", \"unknown\"), (\"percent\", None)]), True)\n        direction = \"level\" if num == 0 else (\"down\" if num < 0 else \"up\")\n        return (OrderedDict([(\"value\", direction), (\"percent\", abs(num))]), True)\n    if \":\" in text:\n        parts = text.split(\":\", 1)\n        try:\n            num = float(parts[0])\n            denom = float(parts[1])\n        except ValueError:\n            return (OrderedDict([(\"value\", \"unknown\"), (\"percent\", None)]), True)\n        if denom == 0:\n            return (OrderedDict([(\"value\", \"unknown\"), (\"percent\", None)]), True)\n        percent = 100.0 * abs(num) / denom\n        direction = \"level\" if num == 0 else (\"down\" if num < 0 else \"up\")\n        return (OrderedDict([(\"value\", direction), (\"percent\", percent)]), True)\n    return (OrderedDict([(\"value\", \"unknown\"), (\"percent\", None)]), True)\n\n\ndef _parse_steps(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Dict[str, Any], bool]:\n    touched = False\n    if \"highway\" in tags:\n        highway = _normalize_tag_value(_get_tag_value(tags, \"highway\", raw))\n        if highway == \"steps\":\n            touched = True\n    step_raw = _get_tag_value(tags, \"step_count\", raw)\n    if step_raw is not None:\n        touched = True\n    if not touched:\n        return ({}, False)\n    step_count = _parse_int_strict(step_raw)\n    if step_count is UNKNOWN:\n        step_count = None\n    return (OrderedDict([(\"step_count\", step_count)]), True)\n\n\ndef _parse_wheelchair(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, bool]:\n    if \"wheelchair\" not in tags:\n        return (None, False)\n    return (_parse_enum(_get_tag_value(tags, \"wheelchair\", raw), WHEELCHAIR_VALUES), True)\n\n\ndef _parse_access(tags: Dict[str, Any], raw: OrderedDict) -> Tuple[Any, bool]:\n    if \"access\" not in tags:\n        return (None, False)\n    return (_parse_enum(_get_tag_value(tags, \"access\", raw), ACCESS_VALUES), True)\n\n\ndef build_feature_semantics(item: Dict[str, Any]) -> Optional[OrderedDict]:\n    sources = _tag_sources(item)\n    if not sources:\n        return None\n\n    raw_values = OrderedDict()\n\n    surface_values = []\n    surface_classes = []\n    surface_touched = False\n\n    smoothness_values = []\n    smoothness_touched = False\n\n    lit_values = []\n    lit_touched = False\n\n    width_entries = []\n    width_touched = False\n\n    lanes_entries = []\n    lanes_touched = False\n\n    oneway_values = []\n    oneway_touched = False\n\n    maxspeed_entries = []\n    maxspeed_touched = False\n\n    sidewalk_values = []\n    sidewalk_touched = False\n\n    cycleway_values = []\n    cycleway_touched = False\n\n    segregated_values = []\n    segregated_touched = False\n\n    crossing_entries = []\n    crossing_touched = False\n\n    kerb_values = []\n    kerb_touched = False\n\n    incline_entries = []\n    incline_touched = False\n\n    steps_entries = []\n    steps_touched = False\n\n    wheelchair_values = []\n    wheelchair_touched = False\n\n    access_values = []\n    access_touched = False\n\n    for source in sources:\n        tags = source.get(\"tags\") or {}\n        if not isinstance(tags, dict):\n            continue\n\n        surface_value, surface_class, touched = _parse_surface(tags, raw_values)\n        surface_values.append(surface_value)\n        surface_classes.append(surface_class)\n        surface_touched = surface_touched or touched\n\n        smoothness_value, touched = _parse_smoothness(tags, raw_values)\n        smoothness_values.append(smoothness_value)\n        smoothness_touched = smoothness_touched or touched\n\n        lit_value, touched = _parse_lit(tags, raw_values)\n        lit_values.append(lit_value)\n        lit_touched = lit_touched or touched\n\n        lanes_entry, touched = _parse_lanes(tags, raw_values)\n        lanes_entries.append(lanes_entry)\n        lanes_touched = lanes_touched or touched\n\n        width_entry, touched = _parse_width(tags, raw_values, lanes_entry.get(\"total\"))\n        width_entries.append(width_entry)\n        width_touched = width_touched or touched\n\n        oneway_value, touched = _parse_oneway(tags, raw_values)\n        oneway_values.append(oneway_value)\n        oneway_touched = oneway_touched or touched\n\n        maxspeed_entry, touched = _parse_maxspeed(tags, raw_values)\n        maxspeed_entries.append(maxspeed_entry)\n        maxspeed_touched = maxspeed_touched or touched\n\n        sidewalk_value, touched = _parse_sidewalk(tags, raw_values)\n        sidewalk_values.append(sidewalk_value)\n        sidewalk_touched = sidewalk_touched or touched\n\n        cycleway_value, touched = _parse_cycleway(tags, raw_values)\n        cycleway_values.append(cycleway_value)\n        cycleway_touched = cycleway_touched or touched\n\n        segregated_value, touched = _parse_segregated(tags, raw_values)\n        segregated_values.append(segregated_value)\n        segregated_touched = segregated_touched or touched\n\n        crossing_entry, touched = _parse_crossing(tags, raw_values)\n        crossing_entries.append(crossing_entry)\n        crossing_touched = crossing_touched or touched\n\n        kerb_value, touched = _parse_kerb(tags, raw_values)\n        kerb_values.append(kerb_value)\n        kerb_touched = kerb_touched or touched\n\n        incline_entry, touched = _parse_incline(tags, raw_values)\n        incline_entries.append(incline_entry)\n        incline_touched = incline_touched or touched\n\n        steps_entry, touched = _parse_steps(tags, raw_values)\n        steps_entries.append(steps_entry)\n        steps_touched = steps_touched or touched\n\n        wheelchair_value, touched = _parse_wheelchair(tags, raw_values)\n        wheelchair_values.append(wheelchair_value)\n        wheelchair_touched = wheelchair_touched or touched\n\n        access_value, touched = _parse_access(tags, raw_values)\n        access_values.append(access_value)\n        access_touched = access_touched or touched\n\n    output = OrderedDict()\n    raw_out = _finalize_raw(raw_values)\n    if raw_out:\n        output[\"raw\"] = raw_out\n\n    surface_value = _merge_uniform_or_mixed(surface_values, surface_touched)\n    surface_class = _merge_uniform_or_mixed(surface_classes, surface_touched)\n    if surface_value is not None or surface_class is not None:\n        output[\"surface\"] = OrderedDict([(\"class\", surface_class), (\"value\", surface_value)])\n\n    smoothness = _merge_uniform_or_mixed(smoothness_values, smoothness_touched)\n    if smoothness is not None:\n        output[\"smoothness\"] = OrderedDict([(\"value\", smoothness)])\n\n    lit = _merge_uniform_or_mixed(lit_values, lit_touched)\n    if lit is not None:\n        output[\"lit\"] = OrderedDict([(\"value\", lit)])\n\n    width = _merge_numeric_with_source(width_entries, width_touched)\n    if width is not None:\n        output[\"width_m\"] = width\n\n    lanes = _merge_lanes(lanes_entries, lanes_touched)\n    if lanes is not None:\n        output[\"lanes\"] = lanes\n\n    oneway = _merge_uniform_or_mixed(oneway_values, oneway_touched)\n    if oneway is not None:\n        output[\"oneway\"] = OrderedDict([(\"value\", oneway)])\n\n    maxspeed = _merge_numeric_with_source(maxspeed_entries, maxspeed_touched)\n    if maxspeed is not None:\n        output[\"maxspeed_kmh\"] = maxspeed\n\n    sidewalk = _merge_uniform_or_mixed(sidewalk_values, sidewalk_touched)\n    if sidewalk is not None:\n        output[\"sidewalk\"] = OrderedDict([(\"value\", sidewalk)])\n\n    cycleway = _merge_uniform_or_mixed(cycleway_values, cycleway_touched)\n    if cycleway is not None:\n        output[\"cycleway\"] = OrderedDict([(\"value\", cycleway)])\n\n    segregated = _merge_uniform_or_mixed(segregated_values, segregated_touched)\n    if segregated is not None:\n        output[\"segregated\"] = OrderedDict([(\"value\", segregated)])\n\n    if crossing_touched:\n        types = [entry.get(\"type\") for entry in crossing_entries]\n        markings = [entry.get(\"markings\") for entry in crossing_entries]\n        tactile = [entry.get(\"tactile_paving\") for entry in crossing_entries]\n        crossing = OrderedDict([\n            (\"type\", _merge_uniform_or_mixed(types, True)),\n            (\"markings\", _merge_uniform_or_mixed(markings, True)),\n            (\"tactile_paving\", _merge_uniform_or_mixed(tactile, True))\n        ])\n        output[\"crossing\"] = crossing\n\n    kerb = _merge_uniform_or_mixed(kerb_values, kerb_touched)\n    if kerb is not None:\n        output[\"kerb\"] = OrderedDict([(\"value\", kerb)])\n\n    incline = _merge_incline(incline_entries, incline_touched)\n    if incline is not None:\n        output[\"incline\"] = incline\n\n    if steps_touched:\n        step_counts = [entry.get(\"step_count\") for entry in steps_entries]\n        step_value = _merge_uniform_or_mixed(step_counts, True)\n        if step_value == \"mixed\":\n            step_value = None\n        output[\"steps\"] = OrderedDict([(\"step_count\", step_value if step_value != \"unknown\" else None)])\n\n    wheelchair = _merge_uniform_or_mixed(wheelchair_values, wheelchair_touched)\n    if wheelchair is not None:\n        output[\"wheelchair\"] = OrderedDict([(\"value\", wheelchair)])\n\n    access = _merge_uniform_or_mixed(access_values, access_touched)\n    if access is not None:\n        output[\"access\"] = OrderedDict([(\"value\", access)])\n\n    return output if output else None\n"
  },
  {
    "path": "converter/map_desc/map-description-classifications.json",
    "content": "{\n  \"version\": \"1.3\",\n  \"source\": \"TOUCH_MAPPER_MAP_CONTENT_CLASSES.md\",\n  \"description\": \"Rule-based classification for map-meta.json items into Touch Mapper classes using both OSM tags and Touch Mapper fields. Rules are evaluated top-to-bottom with first match winning; rule fields are AND. anyOf/allOf allow nested matching; tagsAny/tagsAll accept objects {key, values} or {key, anyValue:true} where anyValue matches any non-empty tag value. actions may set role, poiImportance, ignore, and ignoreWhenOptionFalse (an option name in the options block). If ignoreWhenOptionFalse is set, ignore is applied only when that option is false; otherwise ignore defaults to false unless explicitly true without gating. modifierRules are applied after classification and add modifiers without changing class/subclass.\",\n  \"inputs\": {\n    \"elementTypeField\": \"elementType\",\n    \"geometryTypeField\": \"geometry.type\",\n    \"tagsField\": \"tags\",\n    \"primaryRepresentationField\": \"primaryRepresentation\",\n    \"representationsField\": \"representations\",\n    \"tmCategoryField\": \"tmCategory\",\n    \"tmRoadTypeField\": \"tmRoadType\"\n  },\n  \"matchPolicy\": {\n    \"firstMatchWins\": true,\n    \"caseSensitive\": true,\n    \"missingTagIsNoMatch\": true\n  },\n  \"options\": {\n    \"emitConnectivityNodes\": true\n  },\n  \"classes\": {\n    \"A\": {\n      \"name\": \"Linear features\",\n      \"subclasses\": {\n        \"A1_road_construction\": \"Roads under construction\",\n        \"A1_major_roads\": \"Major roads\",\n        \"A1_secondary_roads\": \"Secondary roads\",\n        \"A1_local_streets\": \"Local streets\",\n        \"A1_service_roads\": \"Service roads\",\n        \"A1_track_roads\": \"Tracks\",\n        \"A1_vehicle_unspecified\": \"Other vehicular roads\",\n        \"A2_pedestrian_streets\": \"Pedestrian streets\",\n        \"A2_footpaths_trails\": \"Footpaths / trails\",\n        \"A2_cycleways\": \"Cycleways\",\n        \"A2_steps_ramps\": \"Steps / ramps\",\n        \"A2_pedestrian_unspecified\": \"Other pedestrian paths\",\n        \"A3_rail_lines\": \"Rail lines\",\n        \"A3_tram_light_rail\": \"Tram / light rail\",\n        \"A3_subway_metro\": \"Subway / metro\",\n        \"A3_rail_yards_sidings\": \"Rail yards / sidings\",\n        \"A4_rivers\": \"Rivers\",\n        \"A4_streams_canals\": \"Streams / canals\",\n        \"A4_ditches_drains\": \"Ditches / drains\",\n        \"A4_other_waterways\": \"Other waterways\",\n        \"A5_connectivity_nodes\": \"Connectivity nodes\",\n        \"A_other_ways\": \"Other ways\"\n      }\n    },\n    \"B\": {\n      \"name\": \"Areal features\",\n      \"subclasses\": {\n        \"B1_lakes\": \"Lakes\",\n        \"B1_ponds\": \"Ponds\",\n        \"B1_reservoirs\": \"Reservoirs\",\n        \"B1_sea_coast\": \"Sea / coastline\",\n        \"B1_riverbanks\": \"Riverbanks\",\n        \"B1_other_water\": \"Other water bodies\",\n        \"B2_parks_recreation\": \"Parks / recreational areas\",\n        \"B2_forests\": \"Forests\",\n        \"B2_fields_open\": \"Fields / open land\",\n        \"B3_residential\": \"Residential areas\",\n        \"B3_commercial\": \"Commercial areas\",\n        \"B3_industrial\": \"Industrial areas\",\n        \"B_other_areas\": \"Other areas\"\n      }\n    },\n    \"C\": {\n      \"name\": \"Buildings\",\n      \"subclasses\": {\n        \"C1_landmark\": \"Landmark buildings\",\n        \"C2_public\": \"Functional public buildings\",\n        \"C3_other_buildings\": \"Other buildings\"\n      }\n    },\n    \"D\": {\n      \"name\": \"POIs\",\n      \"subclasses\": {\n        \"D1_transport\": \"Transport POIs\",\n        \"D2_civic\": \"Civic & essential services\",\n        \"D3_commercial\": \"Commercial POIs\",\n        \"D4_leisure_cultural\": \"Leisure / cultural POIs\",\n        \"D_other_pois\": \"Other POIs\",\n        \"D_other_nodes\": \"Other nodes\"\n      }\n    },\n    \"E\": {\n      \"name\": \"Boundaries and edges\",\n      \"subclasses\": {\n        \"E1_admin_boundaries\": \"Administrative boundaries\",\n        \"E2_coastlines\": \"Coastlines\",\n        \"E3_fences_walls\": \"Fences / walls\"\n      }\n    }\n  },\n  \"rules\": [\n    {\n      \"id\": \"E1_admin_boundaries\",\n      \"mainClass\": \"E\",\n      \"subClass\": \"E1_admin_boundaries\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"boundary\",\n          \"values\": [\n            \"administrative\"\n          ]\n        }\n      ],\n      \"notes\": \"Administrative boundaries should be surfaced separately from roads/landuse.\"\n    },\n    {\n      \"id\": \"E2_coastlines\",\n      \"mainClass\": \"E\",\n      \"subClass\": \"E2_coastlines\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"natural\",\n          \"values\": [\n            \"coastline\"\n          ]\n        }\n      ],\n      \"notes\": \"Coastlines are boundary edges rather than water areas.\"\n    },\n    {\n      \"id\": \"E3_fences_walls\",\n      \"mainClass\": \"E\",\n      \"subClass\": \"E3_fences_walls\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"barrier\",\n          \"values\": [\n            \"wall\",\n            \"fence\",\n            \"hedge\",\n            \"retaining_wall\"\n          ]\n        }\n      ],\n      \"notes\": \"Barriers are tactually important edges even when not rendered prominently.\"\n    },\n    {\n      \"id\": \"A5_connectivity_crossing\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A5_connectivity_nodes\",\n      \"elementTypes\": [\n        \"node\"\n      ],\n      \"geometryTypes\": [\n        \"point\"\n      ],\n      \"anyOf\": [\n        {\n          \"primaryRepresentationAny\": [\n            \"RoadCrossingAtConnector\"\n          ]\n        },\n        {\n          \"representationsAny\": [\n            \"RoadCrossingAtConnector\"\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"crossing\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"actions\": {\n        \"role\": \"crossing\",\n        \"ignoreWhenOptionFalse\": \"emitConnectivityNodes\"\n      },\n      \"notes\": \"Road crossing nodes provide intersection detail; role identifies crossings for text.\"\n    },\n    {\n      \"id\": \"A5_connectivity_connector\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A5_connectivity_nodes\",\n      \"elementTypes\": [\n        \"node\"\n      ],\n      \"geometryTypes\": [\n        \"point\"\n      ],\n      \"anyOf\": [\n        {\n          \"primaryRepresentationAny\": [\n            \"RoadConnector\"\n          ]\n        },\n        {\n          \"representationsAny\": [\n            \"RoadConnector\"\n          ]\n        }\n      ],\n      \"actions\": {\n        \"role\": \"connector\",\n        \"ignoreWhenOptionFalse\": \"emitConnectivityNodes\"\n      },\n      \"notes\": \"Connector nodes link road segments; role identifies connectors for text.\"\n    },\n    {\n      \"id\": \"A5_connectivity_junction\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A5_connectivity_nodes\",\n      \"elementTypes\": [\n        \"node\"\n      ],\n      \"geometryTypes\": [\n        \"point\"\n      ],\n      \"anyOf\": [\n        {\n          \"primaryRepresentationAny\": [\n            \"RoadJunction\"\n          ]\n        },\n        {\n          \"representationsAny\": [\n            \"RoadJunction\"\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"turning_circle\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"actions\": {\n        \"role\": \"junction\",\n        \"ignoreWhenOptionFalse\": \"emitConnectivityNodes\"\n      },\n      \"notes\": \"Junction nodes mark intersections even when tags are sparse.\"\n    },\n    {\n      \"id\": \"D1_transport_poi\",\n      \"mainClass\": \"D\",\n      \"subClass\": \"D1_transport\",\n      \"elementTypes\": [\n        \"node\",\n        \"way\",\n        \"area\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"public_transport\",\n          \"values\": [\n            \"station\",\n            \"stop_position\",\n            \"platform\",\n            \"stop_area\",\n            \"stop_area_group\"\n          ]\n        },\n        {\n          \"key\": \"railway\",\n          \"values\": [\n            \"station\",\n            \"halt\",\n            \"tram_stop\",\n            \"subway_entrance\"\n          ]\n        },\n        {\n          \"key\": \"amenity\",\n          \"values\": [\n            \"bus_station\",\n            \"ferry_terminal\",\n            \"taxi\"\n          ]\n        }\n      ],\n      \"actions\": {\n        \"poiImportance\": \"high\"\n      },\n      \"notes\": \"Transport POIs are high-salience and should be listed by default.\"\n    },\n    {\n      \"id\": \"D2_civic_poi\",\n      \"mainClass\": \"D\",\n      \"subClass\": \"D2_civic\",\n      \"elementTypes\": [\n        \"node\",\n        \"way\",\n        \"area\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"amenity\",\n          \"values\": [\n            \"school\",\n            \"university\",\n            \"college\",\n            \"kindergarten\",\n            \"hospital\",\n            \"clinic\",\n            \"doctors\",\n            \"pharmacy\",\n            \"police\",\n            \"fire_station\",\n            \"post_office\",\n            \"library\",\n            \"townhall\",\n            \"courthouse\",\n            \"place_of_worship\",\n            \"community_centre\"\n          ]\n        }\n      ],\n      \"actions\": {\n        \"poiImportance\": \"high\"\n      },\n      \"notes\": \"Civic and essential services are high-priority description items.\"\n    },\n    {\n      \"id\": \"D3_commercial_poi\",\n      \"mainClass\": \"D\",\n      \"subClass\": \"D3_commercial\",\n      \"elementTypes\": [\n        \"node\",\n        \"way\",\n        \"area\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"shop\",\n          \"anyValue\": true\n        },\n        {\n          \"key\": \"amenity\",\n          \"values\": [\n            \"restaurant\",\n            \"cafe\",\n            \"bar\",\n            \"pub\",\n            \"fast_food\",\n            \"bank\",\n            \"atm\",\n            \"fuel\",\n            \"marketplace\"\n          ]\n        }\n      ],\n      \"actions\": {\n        \"poiImportance\": \"medium\"\n      },\n      \"notes\": \"Commercial POIs are useful but can be throttled in descriptions.\"\n    },\n    {\n      \"id\": \"D4_leisure_cultural_poi\",\n      \"mainClass\": \"D\",\n      \"subClass\": \"D4_leisure_cultural\",\n      \"elementTypes\": [\n        \"node\",\n        \"way\",\n        \"area\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"tourism\",\n          \"values\": [\n            \"museum\",\n            \"attraction\",\n            \"viewpoint\",\n            \"gallery\",\n            \"zoo\",\n            \"theme_park\",\n            \"aquarium\",\n            \"information\"\n          ]\n        },\n        {\n          \"key\": \"leisure\",\n          \"values\": [\n            \"playground\",\n            \"sports_centre\",\n            \"pitch\",\n            \"stadium\",\n            \"swimming_pool\",\n            \"fitness_centre\",\n            \"golf_course\"\n          ]\n        }\n      ],\n      \"actions\": {\n        \"poiImportance\": \"low\"\n      },\n      \"notes\": \"Leisure and cultural POIs are descriptive but usually lower priority.\"\n    },\n    {\n      \"id\": \"D_other_pois\",\n      \"mainClass\": \"D\",\n      \"subClass\": \"D_other_pois\",\n      \"elementTypes\": [\n        \"node\",\n        \"way\",\n        \"area\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"amenity\",\n          \"anyValue\": true\n        },\n        {\n          \"key\": \"shop\",\n          \"anyValue\": true\n        },\n        {\n          \"key\": \"tourism\",\n          \"anyValue\": true\n        },\n        {\n          \"key\": \"leisure\",\n          \"anyValue\": true\n        },\n        {\n          \"key\": \"public_transport\",\n          \"anyValue\": true\n        }\n      ],\n      \"actions\": {\n        \"poiImportance\": \"low\"\n      },\n      \"notes\": \"Catch-all for POI-tagged elements not matched above.\"\n    },\n    {\n      \"id\": \"B1_lakes\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B1_lakes\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"natural\",\n                  \"values\": [\n                    \"water\"\n                  ]\n                },\n                {\n                  \"key\": \"water\",\n                  \"anyValue\": true\n                },\n                {\n                  \"key\": \"landuse\",\n                  \"values\": [\n                    \"reservoir\"\n                  ]\n                },\n                {\n                  \"key\": \"waterway\",\n                  \"values\": [\n                    \"riverbank\"\n                  ]\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"water\",\n              \"values\": [\n                \"lake\",\n                \"oxbow\",\n                \"lagoon\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Lake-like water bodies should be called out distinctly when tags exist.\"\n    },\n    {\n      \"id\": \"B1_ponds\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B1_ponds\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"natural\",\n                  \"values\": [\n                    \"water\"\n                  ]\n                },\n                {\n                  \"key\": \"water\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"water\",\n              \"values\": [\n                \"pond\",\n                \"basin\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Ponds and basins are smaller water bodies with different salience.\"\n    },\n    {\n      \"id\": \"B1_reservoirs\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B1_reservoirs\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"natural\",\n                  \"values\": [\n                    \"water\"\n                  ]\n                },\n                {\n                  \"key\": \"water\",\n                  \"anyValue\": true\n                },\n                {\n                  \"key\": \"landuse\",\n                  \"values\": [\n                    \"reservoir\"\n                  ]\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"water\",\n              \"values\": [\n                \"reservoir\"\n              ]\n            },\n            {\n              \"key\": \"landuse\",\n              \"values\": [\n                \"reservoir\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Reservoirs are significant water areas often worth naming.\"\n    },\n    {\n      \"id\": \"B1_sea_coast\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B1_sea_coast\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"natural\",\n                  \"values\": [\n                    \"water\"\n                  ]\n                },\n                {\n                  \"key\": \"water\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"water\",\n              \"values\": [\n                \"sea\",\n                \"ocean\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Sea or ocean areas are handled separately from inland waters.\"\n    },\n    {\n      \"id\": \"B1_riverbanks\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B1_riverbanks\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Water\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"natural\",\n                  \"values\": [\n                    \"water\"\n                  ]\n                },\n                {\n                  \"key\": \"water\",\n                  \"anyValue\": true\n                },\n                {\n                  \"key\": \"waterway\",\n                  \"values\": [\n                    \"riverbank\"\n                  ]\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"waterway\",\n              \"values\": [\n                \"riverbank\"\n              ]\n            },\n            {\n              \"key\": \"water\",\n              \"values\": [\n                \"river\",\n                \"canal\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Riverbanks are areal water edges that describe wide rivers or canals.\"\n    },\n    {\n      \"id\": \"B1_other_water\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B1_other_water\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"anyOf\": [\n        {\n          \"tmCategoryAny\": [\n            \"Water\"\n          ]\n        },\n        {\n          \"primaryRepresentationAny\": [\n            \"Water\"\n          ]\n        },\n        {\n          \"representationsAny\": [\n            \"Water\"\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"natural\",\n              \"values\": [\n                \"water\"\n              ]\n            },\n            {\n              \"key\": \"water\",\n              \"anyValue\": true\n            },\n            {\n              \"key\": \"landuse\",\n              \"values\": [\n                \"reservoir\"\n              ]\n            },\n            {\n              \"key\": \"waterway\",\n              \"values\": [\n                \"riverbank\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Fallback for water areas when no more specific water type is tagged.\"\n    },\n    {\n      \"id\": \"A4_rivers\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A4_rivers\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Waterway\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Waterway\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Waterway\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"waterway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"waterway\",\n              \"values\": [\n                \"river\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Rivers are linear waterways with higher prominence.\"\n    },\n    {\n      \"id\": \"A4_streams_canals\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A4_streams_canals\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Waterway\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Waterway\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Waterway\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"waterway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"waterway\",\n              \"values\": [\n                \"stream\",\n                \"canal\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Streams and canals are common linear water features.\"\n    },\n    {\n      \"id\": \"A4_ditches_drains\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A4_ditches_drains\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Waterway\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Waterway\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Waterway\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"waterway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"waterway\",\n              \"values\": [\n                \"ditch\",\n                \"drain\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Small drainage waterways can be called out separately.\"\n    },\n    {\n      \"id\": \"A4_other_waterways\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A4_other_waterways\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"anyOf\": [\n        {\n          \"tmCategoryAny\": [\n            \"Waterway\"\n          ]\n        },\n        {\n          \"primaryRepresentationAny\": [\n            \"Waterway\"\n          ]\n        },\n        {\n          \"representationsAny\": [\n            \"Waterway\"\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"waterway\",\n              \"anyValue\": true\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Fallback for waterways when tags are missing or uncommon.\"\n    },\n    {\n      \"id\": \"A3_subway_metro\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A3_subway_metro\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"railway\",\n          \"values\": [\n            \"subway\",\n            \"metro\"\n          ]\n        }\n      ],\n      \"notes\": \"Underground rail lines are distinct from surface rail.\"\n    },\n    {\n      \"id\": \"A3_tram_light_rail\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A3_tram_light_rail\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"railway\",\n          \"values\": [\n            \"tram\",\n            \"light_rail\"\n          ]\n        }\n      ],\n      \"notes\": \"Tram and light rail lines are a distinct transit class.\"\n    },\n    {\n      \"id\": \"A3_rail_yards_sidings\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A3_rail_yards_sidings\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"railway\",\n          \"values\": [\n            \"yard\",\n            \"siding\"\n          ]\n        }\n      ],\n      \"notes\": \"Rail yards and sidings provide context but lower prominence.\"\n    },\n    {\n      \"id\": \"A3_rail_lines\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A3_rail_lines\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"railway\",\n          \"values\": [\n            \"rail\",\n            \"narrow_gauge\"\n          ]\n        }\n      ],\n      \"notes\": \"Standard rail lines are linear transport features.\"\n    },\n    {\n      \"id\": \"A1_road_construction\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A1_road_construction\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"construction\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Construction roads are noted but not treated as normal roads.\"\n    },\n    {\n      \"id\": \"A1_major_roads\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A1_major_roads\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"motorway\",\n                \"trunk\",\n                \"primary\",\n                \"motorway_link\",\n                \"trunk_link\",\n                \"primary_link\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Highest-order vehicular roads; use highway tags when available.\"\n    },\n    {\n      \"id\": \"A1_secondary_roads\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A1_secondary_roads\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"secondary\",\n                \"tertiary\",\n                \"secondary_link\",\n                \"tertiary_link\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Secondary and tertiary roads support local movement.\"\n    },\n    {\n      \"id\": \"A1_local_streets\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A1_local_streets\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"residential\",\n                \"living_street\",\n                \"unclassified\",\n                \"road\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Local streets are common, lower-speed vehicular roads.\"\n    },\n    {\n      \"id\": \"A1_service_roads\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A1_service_roads\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"service\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Service roads include driveways and parking aisles.\"\n    },\n    {\n      \"id\": \"A1_track_roads\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A1_track_roads\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"track\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Tracks are typically unpaved vehicular routes.\"\n    },\n    {\n      \"id\": \"A2_cycleways\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A2_cycleways\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"cycleway\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Dedicated cycling paths are distinct from pedestrian routes.\"\n    },\n    {\n      \"id\": \"A2_pedestrian_streets\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A2_pedestrian_streets\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"pedestrian\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Pedestrian streets are walkable linear features.\"\n    },\n    {\n      \"id\": \"A2_footpaths_trails\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A2_footpaths_trails\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"footway\",\n                \"path\",\n                \"bridleway\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Footpaths and trails include footways, paths, and bridleways.\"\n    },\n    {\n      \"id\": \"A2_steps_ramps\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A2_steps_ramps\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"primaryRepresentationAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Road\",\n                \"RoadArea\"\n              ]\n            },\n            {\n              \"tmCategoryAny\": [\n                \"Road\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"highway\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"highway\",\n              \"values\": [\n                \"steps\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Steps and ramps are distinct pedestrian routing cues.\"\n    },\n    {\n      \"id\": \"A2_pedestrian_unspecified\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A2_pedestrian_unspecified\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"anyOf\": [\n        {\n          \"tmRoadTypeAny\": [\n            \"pedestrian\"\n          ]\n        }\n      ],\n      \"notes\": \"Fallback for pedestrian paths when highway tags are missing.\"\n    },\n    {\n      \"id\": \"A1_vehicle_unspecified\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A1_vehicle_unspecified\",\n      \"elementTypes\": [\n        \"way\",\n        \"area\"\n      ],\n      \"anyOf\": [\n        {\n          \"primaryRepresentationAny\": [\n            \"Road\",\n            \"RoadArea\"\n          ]\n        },\n        {\n          \"representationsAny\": [\n            \"Road\",\n            \"RoadArea\"\n          ]\n        },\n        {\n          \"tmCategoryAny\": [\n            \"Road\"\n          ]\n        },\n        {\n          \"tmRoadTypeAny\": [\n            \"car\"\n          ]\n        }\n      ],\n      \"notes\": \"Fallback for vehicular roads when highway tags are missing.\"\n    },\n    {\n      \"id\": \"C1_landmark_buildings\",\n      \"mainClass\": \"C\",\n      \"subClass\": \"C1_landmark\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\",\n        \"relation\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Building\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Building\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Building\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"building\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"building\",\n              \"values\": [\n                \"church\",\n                \"cathedral\",\n                \"chapel\",\n                \"mosque\",\n                \"synagogue\",\n                \"temple\",\n                \"shrine\",\n                \"castle\",\n                \"palace\"\n              ]\n            },\n            {\n              \"key\": \"amenity\",\n              \"values\": [\n                \"place_of_worship\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Landmark buildings take precedence for descriptions when tagged.\"\n    },\n    {\n      \"id\": \"C2_public_buildings\",\n      \"mainClass\": \"C\",\n      \"subClass\": \"C2_public\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\",\n        \"relation\"\n      ],\n      \"allOf\": [\n        {\n          \"anyOf\": [\n            {\n              \"tmCategoryAny\": [\n                \"Building\"\n              ]\n            },\n            {\n              \"primaryRepresentationAny\": [\n                \"Building\"\n              ]\n            },\n            {\n              \"representationsAny\": [\n                \"Building\"\n              ]\n            },\n            {\n              \"tagsAny\": [\n                {\n                  \"key\": \"building\",\n                  \"anyValue\": true\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"building\",\n              \"values\": [\n                \"school\",\n                \"university\",\n                \"college\",\n                \"kindergarten\",\n                \"hospital\",\n                \"public\",\n                \"government\",\n                \"civic\",\n                \"train_station\",\n                \"fire_station\",\n                \"police\",\n                \"townhall\",\n                \"library\"\n              ]\n            },\n            {\n              \"key\": \"building:use\",\n              \"values\": [\n                \"school\",\n                \"university\",\n                \"hospital\",\n                \"kindergarten\",\n                \"public\",\n                \"government\",\n                \"utility\"\n              ]\n            },\n            {\n              \"key\": \"amenity\",\n              \"values\": [\n                \"school\",\n                \"university\",\n                \"college\",\n                \"kindergarten\",\n                \"hospital\",\n                \"clinic\",\n                \"doctors\",\n                \"pharmacy\",\n                \"police\",\n                \"fire_station\",\n                \"townhall\",\n                \"courthouse\",\n                \"library\"\n              ]\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Public buildings are identified via building or use tags when present.\"\n    },\n    {\n      \"id\": \"C3_other_buildings\",\n      \"mainClass\": \"C\",\n      \"subClass\": \"C3_other_buildings\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\",\n        \"relation\"\n      ],\n      \"anyOf\": [\n        {\n          \"tmCategoryAny\": [\n            \"Building\"\n          ]\n        },\n        {\n          \"primaryRepresentationAny\": [\n            \"Building\"\n          ]\n        },\n        {\n          \"representationsAny\": [\n            \"Building\"\n          ]\n        },\n        {\n          \"tagsAny\": [\n            {\n              \"key\": \"building\",\n              \"anyValue\": true\n            }\n          ]\n        }\n      ],\n      \"notes\": \"Fallback for buildings when no specific public/landmark tag exists.\"\n    },\n    {\n      \"id\": \"B2_parks_recreation\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B2_parks_recreation\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"leisure\",\n          \"values\": [\n            \"park\",\n            \"garden\",\n            \"nature_reserve\",\n            \"recreation_ground\"\n          ]\n        },\n        {\n          \"key\": \"landuse\",\n          \"values\": [\n            \"recreation_ground\",\n            \"village_green\"\n          ]\n        },\n        {\n          \"key\": \"boundary\",\n          \"values\": [\n            \"protected_area\"\n          ]\n        }\n      ],\n      \"notes\": \"Park and recreation areas provide large-scale context.\"\n    },\n    {\n      \"id\": \"B2_forests\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B2_forests\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"landuse\",\n          \"values\": [\n            \"forest\"\n          ]\n        },\n        {\n          \"key\": \"natural\",\n          \"values\": [\n            \"wood\",\n            \"scrub\"\n          ]\n        }\n      ],\n      \"notes\": \"Forests and woodland are common landscape features.\"\n    },\n    {\n      \"id\": \"B2_fields_open\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B2_fields_open\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"landuse\",\n          \"values\": [\n            \"farmland\",\n            \"meadow\",\n            \"orchard\",\n            \"vineyard\",\n            \"grass\",\n            \"greenfield\"\n          ]\n        }\n      ],\n      \"notes\": \"Open land and fields affect large-area texture.\"\n    },\n    {\n      \"id\": \"B3_residential\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B3_residential\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"landuse\",\n          \"values\": [\n            \"residential\"\n          ]\n        }\n      ],\n      \"notes\": \"Residential landuse helps describe neighborhood character.\"\n    },\n    {\n      \"id\": \"B3_commercial\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B3_commercial\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"landuse\",\n          \"values\": [\n            \"commercial\",\n            \"retail\"\n          ]\n        }\n      ],\n      \"notes\": \"Commercial areas are large-scale contextual zones.\"\n    },\n    {\n      \"id\": \"B3_industrial\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B3_industrial\",\n      \"elementTypes\": [\n        \"area\",\n        \"way\"\n      ],\n      \"geometryTypes\": [\n        \"polygon\"\n      ],\n      \"tagsAny\": [\n        {\n          \"key\": \"landuse\",\n          \"values\": [\n            \"industrial\"\n          ]\n        }\n      ],\n      \"notes\": \"Industrial areas are typically distinct and useful to mention.\"\n    }\n  ],\n  \"modifierRules\": [\n    {\n      \"id\": \"M_bridge\",\n      \"tagsAny\": [\n        {\n          \"key\": \"bridge\",\n          \"values\": [\n            \"yes\"\n          ]\n        }\n      ],\n      \"modifiers\": [\n        {\n          \"name\": \"bridge\"\n        }\n      ],\n      \"notes\": \"Bridge modifier for linear or areal features.\"\n    },\n    {\n      \"id\": \"M_tunnel\",\n      \"tagsAny\": [\n        {\n          \"key\": \"tunnel\",\n          \"values\": [\n            \"yes\"\n          ]\n        }\n      ],\n      \"modifiers\": [\n        {\n          \"name\": \"tunnel\"\n        }\n      ],\n      \"notes\": \"Tunnel modifier for underground features.\"\n    },\n    {\n      \"id\": \"M_layer\",\n      \"tagsAny\": [\n        {\n          \"key\": \"layer\",\n          \"anyValue\": true\n        }\n      ],\n      \"modifiers\": [\n        {\n          \"name\": \"layer\",\n          \"valueFromTag\": \"layer\"\n        }\n      ],\n      \"notes\": \"Capture explicit layer values for height ordering.\"\n    },\n    {\n      \"id\": \"M_ford\",\n      \"tagsAny\": [\n        {\n          \"key\": \"ford\",\n          \"anyValue\": true\n        }\n      ],\n      \"modifiers\": [\n        {\n          \"name\": \"ford\"\n        }\n      ],\n      \"notes\": \"Ford crossings can be referenced in text descriptions.\"\n    }\n  ],\n  \"fallbacks\": [\n    {\n      \"id\": \"A_other_ways\",\n      \"mainClass\": \"A\",\n      \"subClass\": \"A_other_ways\",\n      \"elementTypes\": [\n        \"way\"\n      ],\n      \"notes\": \"Final fallback for unclassified ways.\"\n    },\n    {\n      \"id\": \"B_other_areas\",\n      \"mainClass\": \"B\",\n      \"subClass\": \"B_other_areas\",\n      \"elementTypes\": [\n        \"area\"\n      ],\n      \"notes\": \"Final fallback for unclassified areas.\"\n    },\n    {\n      \"id\": \"D_other_nodes\",\n      \"mainClass\": \"D\",\n      \"subClass\": \"D_other_nodes\",\n      \"elementTypes\": [\n        \"node\"\n      ],\n      \"notes\": \"Final fallback for unclassified nodes.\"\n    }\n  ]\n}\n"
  },
  {
    "path": "converter/map_desc/map_desc_loc_segments.py",
    "content": "# Python 3.5\nfrom __future__ import division\n\nimport math\n\nfrom typing import Any, Dict, Optional, TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from typing_extensions import TypedDict  # type: ignore[import-not-found]\nelse:  # pragma: no cover - blender python may not have typing_extensions\n    try:\n        from typing_extensions import TypedDict  # type: ignore[import-not-found]\n    except ImportError:\n        def TypedDict(name, fields, total=True):  # type: ignore[no-redef]\n            return dict\n\n\nPoint = TypedDict(\"Point\", {\"x\": float, \"y\": float})\nBBox = TypedDict(\"BBox\", {\"minX\": float, \"minY\": float, \"maxX\": float, \"maxY\": float})\nLocationLoc = TypedDict(\"LocationLoc\", {\"kind\": str, \"dir\": Optional[str]})\n\n\n# Distance-from-center bands based on Chebyshev radius r=max(|dx|,|dy|),\n# where dx,dy are normalized to [-1, 1] from map center.\n# Code below creates stage \"Grouped + classified meta\" data.\nCENTER_BAND = 0.30\nPART_BAND = 0.68\n\n\ndef _clamp(value: float, min_value: float, max_value: float) -> float:\n    if value < min_value:\n        return min_value\n    if value > max_value:\n        return max_value\n    return value\n\n\ndef _angle_dir(dx: float, dy: float) -> Optional[str]:\n    if dx == 0 and dy == 0:\n        return None\n    angle = math.degrees(math.atan2(dy, dx))\n    if -25.0 <= angle < 25.0:\n        return \"east\"\n    if 25.0 <= angle < 65.0:\n        return \"northeast\"\n    if 65.0 <= angle < 115.0:\n        return \"north\"\n    if 115.0 <= angle < 155.0:\n        return \"northwest\"\n    if angle >= 155.0 or angle < -155.0:\n        return \"west\"\n    if -155.0 <= angle < -115.0:\n        return \"southwest\"\n    if -115.0 <= angle < -65.0:\n        return \"south\"\n    if -65.0 <= angle < -25.0:\n        return \"southeast\"\n    return None\n\n\ndef _location_loc(kind: str, direction: Optional[str]) -> LocationLoc:\n    return {\"kind\": kind, \"dir\": direction}\n\n\ndef classify_location(point: Optional[Point],\n                      bbox: Optional[BBox]) -> Optional[Dict[str, Any]]:\n    if not point or not bbox:\n        return None\n    min_x = bbox.get(\"minX\")\n    max_x = bbox.get(\"maxX\")\n    min_y = bbox.get(\"minY\")\n    max_y = bbox.get(\"maxY\")\n    point_x = point.get(\"x\")\n    point_y = point.get(\"y\")\n    if min_x is None or max_x is None or min_y is None or max_y is None:\n        return None\n    if point_x is None or point_y is None:\n        return None\n    width = max_x - min_x\n    height = max_y - min_y\n    if width == 0 or height == 0:\n        return None\n\n    nx = (point_x - min_x) / width\n    ny = (point_y - min_y) / height\n    nx = _clamp(nx, 0, 1)\n    ny = _clamp(ny, 0, 1)\n\n    dx = (nx - 0.5) * 2.0\n    dy = (ny - 0.5) * 2.0\n    r = max(abs(dx), abs(dy))\n\n    if r <= CENTER_BAND:\n        return {\"loc\": _location_loc(\"center\", None)}\n\n    direction = _angle_dir(dx, dy)\n    if r <= PART_BAND:\n        return {\"loc\": _location_loc(\"part\", direction)}\n\n    return {\"loc\": _location_loc(\"near_edge\", direction)}\n\n\n__all__ = [\"classify_location\"]\n"
  },
  {
    "path": "converter/map_desc/map_desc_render.py",
    "content": "# Python 3.5\nfrom __future__ import division\n\nimport json\nimport os\nimport re\nimport sys\nimport time\nfrom collections import OrderedDict\nfrom typing import Any, Dict, Iterable, Iterator, List, Optional, Set, Tuple, TYPE_CHECKING\nfrom urllib.parse import quote, unquote, urlsplit, urlunsplit\n\nif TYPE_CHECKING:\n    from typing_extensions import TypedDict  # type: ignore[import-not-found]\nelse:  # pragma: no cover - blender python may not have typing_extensions\n    try:\n        from typing_extensions import TypedDict  # type: ignore[import-not-found]\n    except ImportError:\n        def TypedDict(name, fields, total=True):  # type: ignore[no-redef]\n            return dict\n\nfrom .map_desc_loc_segments import classify_location\n\nMAX_ITEMS_PER_SUBCLASS = 10\nSAMPLE_TS = (0.0, 0.25, 0.5, 0.75, 1.0)\nEDGE_EPS = 1e-6\n\n\nBounds = TypedDict(\n    \"Bounds\",\n    {\"minX\": float, \"minY\": float, \"maxX\": float, \"maxY\": float},\n    total=False\n)\nBoundary = TypedDict(\n    \"Boundary\",\n    {\"minX\": float, \"minY\": float, \"maxX\": float, \"maxY\": float}\n)\n\n# Scoring configuration\n# Change values in this section to tune importance scoring globally.\nScoringLengthConfig = TypedDict(\n    \"ScoringLengthConfig\",\n    {\"minMultiplier\": float, \"maxMultiplier\": float, \"referencePolicy\": str},\n    total=False\n)\nScoringComponentFormatConfig = TypedDict(\n    \"ScoringComponentFormatConfig\",\n    {\"factorDigits\": int, \"sizeKeyDigits\": int},\n    total=False\n)\nScoringBuildingBaseConfig = TypedDict(\n    \"ScoringBuildingBaseConfig\",\n    {\"named\": float, \"unnamed\": float},\n    total=False\n)\nScoringLocationMultiplierConfig = TypedDict(\n    \"ScoringLocationMultiplierConfig\",\n    {\n        \"center\": float,\n        \"part_cardinal\": float,\n        \"part_diagonal\": float,\n        \"near_edge_cardinal\": float,\n        \"near_edge_diagonal\": float,\n    },\n    total=False\n)\nScoringConfig = TypedDict(\n    \"ScoringConfig\",\n    {\n        \"linearSubclassBaseImportance\": Dict[str, float],\n        \"tagFamilyMultipliers\": Tuple[Tuple[str, float], ...],\n        \"tagMultiplierCap\": float,\n        \"buildingBase\": ScoringBuildingBaseConfig,\n        \"poiBase\": float,\n        \"excludedSubclasses\": Set[str],\n        \"length\": ScoringLengthConfig,\n        \"locationMultipliers\": ScoringLocationMultiplierConfig,\n        \"componentFormat\": ScoringComponentFormatConfig,\n        \"includeComponents\": bool,\n    },\n    total=False\n)\n\nSCORING_CONFIG = {  # type: ScoringConfig\n    # Linear (class A) base importance per subclass.\n    \"linearSubclassBaseImportance\": {\n        \"A1_major_roads\": 100.0,\n        \"A3_subway_metro\": 95.0,\n        \"A3_tram_light_rail\": 90.0,\n        \"A1_secondary_roads\": 85.0,\n        \"A4_rivers\": 80.0,\n        \"A3_rail_lines\": 75.0,\n        \"A2_pedestrian_streets\": 70.0,\n        \"A1_local_streets\": 65.0,\n        \"A2_cycleways\": 60.0,\n        \"A4_streams_canals\": 55.0,\n        \"A2_footpaths_trails\": 50.0,\n        \"A2_steps_ramps\": 45.0,\n        \"A1_service_roads\": 40.0,\n        \"A3_rail_yards_sidings\": 35.0,\n        \"A1_track_roads\": 30.0,\n        \"A4_ditches_drains\": 30.0,\n        \"A1_road_construction\": 25.0,\n        \"A1_vehicle_unspecified\": 25.0,\n        \"A2_pedestrian_unspecified\": 25.0,\n        \"A4_other_waterways\": 25.0,\n        \"A5_connectivity_nodes\": 20.0,\n        \"A_other_ways\": 20.0,\n    },\n    # Tag-family factors, multiplied once per family (group-wide), then capped.\n    \"tagFamilyMultipliers\": (\n        (\"wikipedia\", 1.35),\n        (\"wikidata\", 1.25),\n        (\"wikimedia_commons\", 1.15),\n        (\"website\", 1.15),\n        (\"operator\", 1.10),\n        (\"extraNames\", 1.05),\n    ),\n    \"tagMultiplierCap\": 2.5,\n    # Building base scores (change these to tune named/unnamed building emphasis).\n    \"buildingBase\": {\"named\": 30.0, \"unnamed\": 5.0},\n    # POI base score (change this to tune maximum POI score before tag multipliers).\n    \"poiBase\": 30.0,\n    # Subclasses that should not receive importanceScore in this phase.\n    \"excludedSubclasses\": {\"A5_connectivity_nodes\"},\n    # Length multiplier tuning for linear features.\n    \"length\": {\"minMultiplier\": 0.2, \"maxMultiplier\": 1.0, \"referencePolicy\": \"max_map_side\"},\n    # Location multiplier tuning.\n    \"locationMultipliers\": {\n        \"center\": 1.0,\n        \"part_cardinal\": 0.9,\n        \"part_diagonal\": 0.85,\n        \"near_edge_cardinal\": 0.8,\n        \"near_edge_diagonal\": 0.7,\n    },\n    # Formatting for score component debug output.\n    \"componentFormat\": {\"factorDigits\": 3, \"sizeKeyDigits\": 3},\n    # Toggle to emit score components (False keeps only {\"final\": ...}).\n    \"includeComponents\": True,\n}\n\n\ndef _scoring_linear_base_importance() -> Dict[str, float]:\n    return SCORING_CONFIG[\"linearSubclassBaseImportance\"]\n\n\ndef _scoring_tag_family_multipliers() -> Tuple[Tuple[str, float], ...]:\n    return SCORING_CONFIG[\"tagFamilyMultipliers\"]\n\n\ndef _scoring_tag_multiplier_cap() -> float:\n    return float(SCORING_CONFIG[\"tagMultiplierCap\"])\n\n\ndef _scoring_building_base(is_named: bool) -> float:\n    base_by_name = SCORING_CONFIG[\"buildingBase\"]\n    return float(base_by_name[\"named\"] if is_named else base_by_name[\"unnamed\"])\n\n\ndef _scoring_poi_base() -> float:\n    return float(SCORING_CONFIG[\"poiBase\"])\n\n\ndef _scoring_is_excluded_subclass(subclass_key: str) -> bool:\n    return subclass_key in SCORING_CONFIG[\"excludedSubclasses\"]\n\n\ndef _scoring_length_min_multiplier() -> float:\n    return float(SCORING_CONFIG[\"length\"][\"minMultiplier\"])\n\n\ndef _scoring_length_max_multiplier() -> float:\n    return float(SCORING_CONFIG[\"length\"][\"maxMultiplier\"])\n\n\ndef _scoring_length_reference_policy() -> str:\n    return str(SCORING_CONFIG[\"length\"][\"referencePolicy\"])\n\n\ndef _scoring_location_multipliers() -> ScoringLocationMultiplierConfig:\n    return SCORING_CONFIG[\"locationMultipliers\"]\n\n\ndef _scoring_factor_digits() -> int:\n    return int(SCORING_CONFIG[\"componentFormat\"][\"factorDigits\"])\n\n\ndef _scoring_size_key_digits() -> int:\n    return int(SCORING_CONFIG[\"componentFormat\"][\"sizeKeyDigits\"])\n\n\ndef _scoring_include_components() -> bool:\n    return bool(SCORING_CONFIG[\"includeComponents\"])\n\n\ndef _validate_scoring_config() -> None:\n    linear_map = _scoring_linear_base_importance()\n    if not linear_map:\n        raise ValueError(\"SCORING_CONFIG.linearSubclassBaseImportance must not be empty\")\n    for key, value in linear_map.items():\n        if not isinstance(key, str) or not key:\n            raise ValueError(\"Invalid linear subclass key in scoring config\")\n        if not isinstance(value, (int, float)):\n            raise ValueError(\"Linear subclass base importance must be numeric for key: \" + key)\n\n    excluded_subclasses = SCORING_CONFIG[\"excludedSubclasses\"]\n    for excluded_key in excluded_subclasses:\n        if excluded_key not in linear_map:\n            raise ValueError(\"Excluded subclass missing in linear base map: \" + excluded_key)\n\n    seen_families = set()  # type: Set[str]\n    for family, factor in _scoring_tag_family_multipliers():\n        if not isinstance(family, str) or not family:\n            raise ValueError(\"Invalid tag family name in scoring config\")\n        if family in seen_families:\n            raise ValueError(\"Duplicate tag family in scoring config: \" + family)\n        seen_families.add(family)\n        if not isinstance(factor, (int, float)) or float(factor) <= 0:\n            raise ValueError(\"Tag family multiplier must be positive for family: \" + family)\n\n    cap = _scoring_tag_multiplier_cap()\n    if cap < 1.0:\n        raise ValueError(\"SCORING_CONFIG.tagMultiplierCap must be >= 1.0\")\n\n    for building_key in (\"named\", \"unnamed\"):\n        building_base = SCORING_CONFIG[\"buildingBase\"].get(building_key)\n        if not isinstance(building_base, (int, float)):\n            raise ValueError(\"Building base must be numeric for key: \" + building_key)\n\n    poi_base = _scoring_poi_base()\n    if not isinstance(poi_base, float) and not isinstance(poi_base, int):\n        raise ValueError(\"SCORING_CONFIG.poiBase must be numeric\")\n\n    min_multiplier = _scoring_length_min_multiplier()\n    max_multiplier = _scoring_length_max_multiplier()\n    if min_multiplier < 0 or max_multiplier <= 0 or min_multiplier > max_multiplier:\n        raise ValueError(\"Invalid length multiplier bounds in scoring config\")\n\n    policy = _scoring_length_reference_policy()\n    if policy not in (\"max_map_side\",):\n        raise ValueError(\"Unsupported length reference policy: \" + policy)\n\n    location_multipliers = _scoring_location_multipliers()\n    location_keys = (\n        \"center\",\n        \"part_cardinal\",\n        \"part_diagonal\",\n        \"near_edge_cardinal\",\n        \"near_edge_diagonal\",\n    )\n    for key in location_keys:\n        value = location_multipliers.get(key)\n        if not isinstance(value, (int, float)):\n            raise ValueError(\"Location multiplier must be numeric for key: \" + key)\n        if float(value) <= 0:\n            raise ValueError(\"Location multiplier must be positive for key: \" + key)\n    if float(location_multipliers.get(\"center\", 0.0)) != 1.0:\n        raise ValueError(\"Location multiplier for center must be 1.0\")\n\n    if _scoring_factor_digits() < 0 or _scoring_size_key_digits() < 0:\n        raise ValueError(\"Component format digits must be non-negative\")\n\n\n_validate_scoring_config()\n\nWIKIDATA_QID_RE = re.compile(r\"^Q[1-9][0-9]*$\")\nWIKIPEDIA_LANG_RE = re.compile(r\"^[a-z0-9-]+$\", re.IGNORECASE)\n\n\ndef _text_or_none(value: Any) -> Optional[str]:\n    if value is None:\n        return None\n    text = str(value).strip()\n    return text if text else None\n\n\ndef _normalize_netloc(netloc: str) -> Optional[str]:\n    raw = netloc.strip()\n    if not raw:\n        return None\n    if any(ch.isspace() for ch in raw):\n        return None\n\n    user_info = \"\"\n    host_port = raw\n    if \"@\" in raw:\n        user_info, host_port = raw.rsplit(\"@\", 1)\n        if not user_info or not host_port:\n            return None\n\n    host = host_port\n    port = \"\"\n\n    if host_port.startswith(\"[\"):\n        end_idx = host_port.find(\"]\")\n        if end_idx <= 0:\n            return None\n        host = host_port[:end_idx + 1]\n        remainder = host_port[end_idx + 1:]\n        if remainder:\n            if not remainder.startswith(\":\"):\n                return None\n            port = remainder[1:]\n    elif \":\" in host_port:\n        host, port = host_port.rsplit(\":\", 1)\n\n    if not host:\n        return None\n\n    if host.startswith(\"[\") and host.endswith(\"]\"):\n        host_ascii = host.lower()\n    else:\n        try:\n            host_ascii = host.encode(\"idna\").decode(\"ascii\").lower()\n        except Exception:\n            return None\n\n    if port:\n        if not port.isdigit():\n            return None\n        port_num = int(port)\n        if port_num <= 0 or port_num > 65535:\n            return None\n        host_ascii = host_ascii + \":\" + port\n\n    if not user_info:\n        return host_ascii\n\n    user_info_encoded = quote(user_info, safe=\":%!$&'()*+,;=-._~\")\n    return user_info_encoded + \"@\" + host_ascii\n\n\ndef _normalize_absolute_http_url(raw_url: str) -> Optional[str]:\n    parsed = urlsplit(raw_url)\n    scheme = parsed.scheme.lower()\n    if scheme not in (\"http\", \"https\"):\n        return None\n    if not parsed.netloc:\n        return None\n\n    netloc = _normalize_netloc(parsed.netloc)\n    if netloc is None:\n        return None\n\n    path = quote(unquote(parsed.path), safe=\"/:@!$&'()*+,;=-._~\")\n    query = quote(unquote(parsed.query), safe=\":@!$&'()*+,;=-._~/?%\")\n    fragment = quote(unquote(parsed.fragment), safe=\":@!$&'()*+,;=-._~/?%\")\n    return urlunsplit((scheme, netloc, path, query, fragment))\n\n\ndef _wikipedia_url(value: Any) -> Optional[str]:\n    text = _text_or_none(value)\n    if text is None or \":\" not in text:\n        return None\n    lang_raw, title_raw = text.split(\":\", 1)\n    lang = lang_raw.strip().lower()\n    title = title_raw.strip()\n    if not lang or not title:\n        return None\n    if not WIKIPEDIA_LANG_RE.match(lang):\n        return None\n    normalized_title = title.replace(\" \", \"_\")\n    encoded_title = quote(normalized_title, safe=\"_-~.\")\n    return \"https://\" + lang + \".wikipedia.org/wiki/\" + encoded_title\n\n\ndef _wikidata_url(value: Any) -> Optional[str]:\n    text = _text_or_none(value)\n    if text is None:\n        return None\n    if not WIKIDATA_QID_RE.match(text):\n        return None\n    return \"https://www.wikidata.org/wiki/\" + text\n\n\ndef _commons_url(value: Any) -> Optional[str]:\n    text = _text_or_none(value)\n    if text is None:\n        return None\n    if text.startswith(\"Category:\"):\n        category_name = text[len(\"Category:\"):].strip()\n    else:\n        category_name = text\n    if not category_name:\n        return None\n    normalized_name = category_name.replace(\" \", \"_\")\n    encoded_name = quote(normalized_name, safe=\"_-~.\")\n    return \"https://commons.wikimedia.org/wiki/Category:\" + encoded_name\n\n\ndef _website_url(value: Any) -> Optional[str]:\n    text = _text_or_none(value)\n    if text is None:\n        return None\n    if text.startswith(\"http://\") or text.startswith(\"https://\"):\n        normalized = text\n    elif text.startswith(\"//\"):\n        normalized = \"https:\" + text\n    else:\n        normalized = \"https://\" + text\n    return _normalize_absolute_http_url(normalized)\n\n\ndef _search_url(value: Any) -> Optional[str]:\n    text = _text_or_none(value)\n    if text is None:\n        return None\n    return \"https://www.google.com/search?q=\" + quote(text, safe=\"\")\n\n\ndef _first_extra_name_value(extra_names: Any) -> Optional[str]:\n    if not isinstance(extra_names, dict):\n        return None\n    keys = list(extra_names.keys())\n    if isinstance(extra_names, OrderedDict):\n        ordered_keys = keys\n    else:\n        ordered_keys = sorted(keys)\n    for key in ordered_keys:\n        value = _text_or_none(extra_names.get(key))\n        if value is not None:\n            return value\n    return None\n\n\ndef _build_external_link_from_importance_tags(importance_tags: Any) -> Optional[OrderedDict]:\n    if not isinstance(importance_tags, dict):\n        return None\n\n    candidates = [\n        (\"wikipedia\", \"wikipedia\", \"Wikipedia\", _wikipedia_url, importance_tags.get(\"wikipedia\")),\n        (\"wikidata\", \"wikidata\", \"Wikidata\", _wikidata_url, importance_tags.get(\"wikidata\")),\n        (\"wikimedia_commons\", \"commons\", \"Wikimedia Commons\", _commons_url, importance_tags.get(\"wikimedia_commons\")),\n        (\"website\", \"website\", \"Website\", _website_url, importance_tags.get(\"website\")),\n        (\"operator\", \"search\", \"Search\", _search_url, importance_tags.get(\"operator\")),\n        (\"brand\", \"search\", \"Search\", _search_url, importance_tags.get(\"brand\")),\n        (\"extraNames\", \"search\", \"Search\", _search_url, _first_extra_name_value(importance_tags.get(\"extraNames\"))),\n    ]\n\n    for _tag_key, link_type, label, builder, raw_value in candidates:\n        url = builder(raw_value)\n        if url is None:\n            continue\n        external_link = OrderedDict()\n        external_link[\"type\"] = link_type\n        external_link[\"url\"] = url\n        external_link[\"label\"] = label\n        return external_link\n    return None\n\n\ndef _float_or_zero(value: Any) -> float:\n    if not isinstance(value, (int, float)):\n        return 0.0\n    return float(value)\n\n\ndef _boundary_reference_length(boundary: Optional[Boundary]) -> Optional[float]:\n    if not boundary:\n        return None\n    width = abs(boundary[\"maxX\"] - boundary[\"minX\"])\n    height = abs(boundary[\"maxY\"] - boundary[\"minY\"])\n    policy = _scoring_length_reference_policy()\n    if policy == \"max_map_side\":\n        return max(width, height)\n    raise ValueError(\"Unsupported length reference policy: \" + policy)\n\n\ndef _importance_families_for_tag_map(tag_map: Optional[Dict[str, Any]]) -> List[str]:\n    if not isinstance(tag_map, dict):\n        return []\n\n    families = []  # type: List[str]\n    if \"wikipedia\" in tag_map:\n        families.append(\"wikipedia\")\n    if \"wikidata\" in tag_map:\n        families.append(\"wikidata\")\n    if \"wikimedia_commons\" in tag_map:\n        families.append(\"wikimedia_commons\")\n    if \"website\" in tag_map or \"contact:website\" in tag_map or \"url\" in tag_map:\n        families.append(\"website\")\n    if \"operator\" in tag_map or \"brand\" in tag_map or \"brand:wikidata\" in tag_map:\n        families.append(\"operator\")\n    if \"extraNames\" in tag_map:\n        families.append(\"extraNames\")\n    return families\n\n\ndef _group_importance_factor(group: Dict[str, Any]) -> Tuple[float, Optional[OrderedDict]]:\n    children = group.get(\"ways\")\n    if not isinstance(children, list):\n        children = group.get(\"items\")\n    if not isinstance(children, list):\n        return 1.0, None\n\n    active_families = set()  # type: Set[str]\n    for child in children:\n        if not isinstance(child, dict):\n            continue\n        for family in _importance_families_for_tag_map(child.get(\"importanceTags\")):\n            active_families.add(family)\n\n    if not active_families:\n        return 1.0, None\n\n    raw_product = 1.0\n    details = OrderedDict()\n    factor_digits = _scoring_factor_digits()\n    for family, family_factor in _scoring_tag_family_multipliers():\n        if family not in active_families:\n            continue\n        raw_product *= family_factor\n        details[family] = _round_component(family_factor, factor_digits)\n\n    cap = _scoring_tag_multiplier_cap()\n    applied_multiplier = min(raw_product, cap)\n    details[\"rawProduct\"] = _round_component(raw_product, factor_digits)\n    details[\"appliedMultiplier\"] = _round_component(applied_multiplier, factor_digits)\n    if raw_product > cap:\n        details[\"cappedAt\"] = _round_component(cap, factor_digits)\n    return applied_multiplier, details\n\n\ndef _is_diagonal_direction(direction: Optional[str]) -> bool:\n    return direction in (\"northwest\", \"northeast\", \"southwest\", \"southeast\")\n\n\ndef _location_bucket_from_loc(loc: Optional[Dict[str, Any]]) -> Optional[str]:\n    if not isinstance(loc, dict):\n        return None\n    kind = loc.get(\"kind\")\n    direction = loc.get(\"dir\")\n    if kind == \"center\":\n        return \"center\"\n    if kind == \"part\":\n        return \"part_diagonal\" if _is_diagonal_direction(direction) else \"part_cardinal\"\n    if kind == \"near_edge\":\n        return \"near_edge_diagonal\" if _is_diagonal_direction(direction) else \"near_edge_cardinal\"\n    return None\n\n\ndef _location_multiplier_for_bucket(bucket: Optional[str]) -> float:\n    if not bucket:\n        return 1.0\n    value = _scoring_location_multipliers().get(bucket)\n    if not isinstance(value, (int, float)):\n        return 1.0\n    return float(value)\n\n\ndef _location_multiplier_from_loc(loc: Optional[Dict[str, Any]]) -> Tuple[float, Optional[str]]:\n    bucket = _location_bucket_from_loc(loc)\n    return _location_multiplier_for_bucket(bucket), bucket\n\n\ndef _location_component(bucket: Optional[str], multiplier: float) -> OrderedDict:\n    factor_digits = _scoring_factor_digits()\n    details = OrderedDict()\n    if bucket:\n        details[bucket] = _round_component(multiplier, factor_digits)\n    else:\n        details[\"unknown\"] = _round_component(multiplier, factor_digits)\n    details[\"appliedMultiplier\"] = _round_component(multiplier, factor_digits)\n    return details\n\n\ndef _location_loc_from_struct(location: Optional[Any], keys: Tuple[str, ...]) -> Optional[Dict[str, Any]]:\n    if not isinstance(location, dict):\n        return None\n    for key in keys:\n        loc = _extract_loc(location.get(key))\n        if isinstance(loc, dict):\n            return loc\n    return None\n\n\ndef _linear_location_factor(group: Dict[str, Any]) -> Tuple[float, OrderedDict]:\n    ways = group.get(\"ways\")\n    if not isinstance(ways, list):\n        return 1.0, _location_component(None, 1.0)\n\n    weighted_sum = 0.0\n    weight_sum = 0.0\n    fallback_sum = 0.0\n    fallback_count = 0\n    bucket_weights = OrderedDict()  # type: OrderedDict\n\n    for way in ways:\n        if not isinstance(way, dict):\n            continue\n        segments = way.get(\"visibleGeometry\")\n        if not isinstance(segments, list):\n            continue\n        for segment in segments:\n            if not isinstance(segment, dict):\n                continue\n            sample_entries = segment.get(\"locationSamples\")\n            if not isinstance(sample_entries, list) or not sample_entries:\n                continue\n            sample_buckets = []  # type: List[str]\n            sample_multipliers = []  # type: List[float]\n            for sample_entry in sample_entries:\n                if not isinstance(sample_entry, dict):\n                    continue\n                bucket = _location_bucket_from_loc(_extract_loc(sample_entry.get(\"zone\")))\n                if not bucket:\n                    continue\n                sample_buckets.append(bucket)\n                sample_multipliers.append(_location_multiplier_for_bucket(bucket))\n\n            if not sample_multipliers:\n                continue\n\n            segment_multiplier = sum(sample_multipliers) / float(len(sample_multipliers))\n            segment_length = max(0.0, _float_or_zero(segment.get(\"length\")))\n            fallback_sum += segment_multiplier\n            fallback_count += 1\n            if segment_length > 0:\n                weighted_sum += segment_multiplier * segment_length\n                weight_sum += segment_length\n\n            sample_weight = segment_length if segment_length > 0 else 1.0\n            sample_weight = sample_weight / float(len(sample_buckets))\n            for bucket in sample_buckets:\n                bucket_weights[bucket] = _float_or_zero(bucket_weights.get(bucket)) + sample_weight\n\n    if weight_sum > 0:\n        location_factor = weighted_sum / weight_sum\n    elif fallback_count > 0:\n        location_factor = fallback_sum / float(fallback_count)\n    else:\n        location_factor = 1.0\n\n    factor_digits = _scoring_factor_digits()\n    details = OrderedDict()\n    if bucket_weights:\n        dominant_bucket = max(bucket_weights.keys(), key=lambda key: bucket_weights.get(key, 0.0))\n        details[dominant_bucket] = _round_component(\n            _location_multiplier_for_bucket(dominant_bucket), factor_digits\n        )\n    details[\"weighted\"] = _round_component(location_factor, factor_digits)\n    details[\"appliedMultiplier\"] = _round_component(location_factor, factor_digits)\n    return location_factor, details\n\n\ndef _building_location_factor(group: Dict[str, Any]) -> Tuple[float, OrderedDict]:\n    items = group.get(\"items\")\n    best_loc = None  # type: Optional[Dict[str, Any]]\n    best_inside = -1.0\n    first_item = None\n    if isinstance(items, list):\n        for item in items:\n            if not isinstance(item, dict):\n                continue\n            if first_item is None:\n                first_item = item\n            visible_geometry = item.get(\"visibleGeometry\")\n            if not isinstance(visible_geometry, dict):\n                continue\n            coverage = visible_geometry.get(\"coverage\")\n            if not isinstance(coverage, dict):\n                continue\n            segments = coverage.get(\"segments\")\n            if not isinstance(segments, list):\n                continue\n            for segment in segments:\n                if not isinstance(segment, dict):\n                    continue\n                loc = _extract_loc(segment.get(\"loc\"))\n                if not isinstance(loc, dict):\n                    continue\n                inside = _float_or_zero(segment.get(\"insideCount\"))\n                if inside > best_inside:\n                    best_inside = inside\n                    best_loc = loc\n\n    if best_loc is not None:\n        multiplier, bucket = _location_multiplier_from_loc(best_loc)\n        return multiplier, _location_component(bucket, multiplier)\n\n    fallback_loc = _location_loc_from_struct(group.get(\"location\"), (\"center\", \"point\"))\n    if fallback_loc is None and isinstance(first_item, dict):\n        fallback_loc = _location_loc_from_struct(first_item.get(\"location\"), (\"center\", \"point\"))\n    multiplier, bucket = _location_multiplier_from_loc(fallback_loc)\n    return multiplier, _location_component(bucket, multiplier)\n\n\ndef _poi_location_factor(group: Dict[str, Any]) -> Tuple[float, OrderedDict]:\n    loc = _location_loc_from_struct(group.get(\"location\"), (\"point\", \"center\"))\n    if loc is None:\n        items = group.get(\"items\")\n        first_item = items[0] if isinstance(items, list) and items else None\n        if isinstance(first_item, dict):\n            loc = _location_loc_from_struct(first_item.get(\"location\"), (\"point\", \"center\"))\n    multiplier, bucket = _location_multiplier_from_loc(loc)\n    return multiplier, _location_component(bucket, multiplier)\n\n\ndef _linear_length_multiplier(visible_length: float, reference_length: Optional[float]) -> float:\n    min_multiplier = _scoring_length_min_multiplier()\n    max_multiplier = _scoring_length_max_multiplier()\n    length = max(0.0, visible_length)\n    if length <= 0:\n        return min_multiplier\n    if reference_length is None or reference_length <= 0:\n        return max_multiplier\n    if length >= reference_length:\n        return max_multiplier\n    interpolation = length / reference_length\n    return min_multiplier + ((max_multiplier - min_multiplier) * interpolation)\n\n\ndef _round_component(value: float, digits: int) -> float:\n    scale = 10 ** digits\n    return _js_round(value * scale) / float(scale)\n\n\ndef _compact_number_key(value: float, digits: int) -> str:\n    text = _to_fixed(value, digits)\n    if \".\" in text:\n        text = text.rstrip(\"0\").rstrip(\".\")\n    if text == \"-0\":\n        return \"0\"\n    return text\n\n\ndef _build_importance_score(final_score: int, components: OrderedDict) -> OrderedDict:\n    score = OrderedDict()\n    score[\"final\"] = int(final_score)\n    if not _scoring_include_components():\n        return score\n    for key, value in components.items():\n        if value is None:\n            continue\n        if isinstance(value, dict) and not value:\n            continue\n        score[key] = value\n    return score\n\n\ndef _has_meaningful_label(label: Optional[Any]) -> bool:\n    if not isinstance(label, str):\n        return False\n    return bool(label.strip())\n\n\ndef _building_group_size(group: Dict[str, Any]) -> float:\n    items = group.get(\"items\")\n    if not isinstance(items, list) or not items:\n        return 0.0\n    first = items[0]\n    if not isinstance(first, dict):\n        return 0.0\n    visible_geometry = first.get(\"visibleGeometry\")\n    if not isinstance(visible_geometry, dict):\n        return 0.0\n    coverage = visible_geometry.get(\"coverage\")\n    if not isinstance(coverage, dict):\n        return 0.0\n    return max(0.0, _float_or_zero(coverage.get(\"coveragePercent\")))\n\n\ndef _boundary_area(boundary: Optional[Boundary]) -> Optional[float]:\n    if not boundary:\n        return None\n    width = abs(boundary[\"maxX\"] - boundary[\"minX\"])\n    height = abs(boundary[\"maxY\"] - boundary[\"minY\"])\n    area = width * height\n    if area <= 0:\n        return None\n    return area\n\n\ndef _water_area_group_coverage_percent(group: Dict[str, Any],\n                                       boundary: Optional[Boundary]) -> float:\n    items = group.get(\"items\")\n    if isinstance(items, list):\n        for item in items:\n            if not isinstance(item, dict):\n                continue\n            visible_geometry = item.get(\"visibleGeometry\")\n            if not isinstance(visible_geometry, dict):\n                continue\n            coverage = visible_geometry.get(\"coverage\")\n            if not isinstance(coverage, dict):\n                continue\n            value = coverage.get(\"coveragePercent\")\n            if isinstance(value, (int, float)):\n                return max(0.0, float(value))\n\n    boundary_area = _boundary_area(boundary)\n    if boundary_area is None:\n        return 0.0\n    total_area = max(0.0, _float_or_zero(group.get(\"totalArea\")))\n    if total_area <= 0:\n        return 0.0\n    return (total_area * 100.0) / boundary_area\n\n\ndef _apply_linear_importance_scores(main_entry: Dict[str, Any],\n                                    boundary: Optional[Boundary]) -> None:\n    subclasses = main_entry.get(\"subclasses\")\n    if not isinstance(subclasses, list):\n        return\n    reference_length = _boundary_reference_length(boundary)\n    for sub_entry in subclasses:\n        if not isinstance(sub_entry, dict):\n            continue\n        sub_key = sub_entry.get(\"key\")\n        if not isinstance(sub_key, str):\n            continue\n        if _scoring_is_excluded_subclass(sub_key):\n            continue\n        base_score = _scoring_linear_base_importance().get(sub_key)\n        if base_score is None:\n            continue\n        groups = sub_entry.get(\"groups\")\n        if not isinstance(groups, list):\n            continue\n        for group in groups:\n            if not isinstance(group, dict):\n                continue\n            visible_length = max(0.0, _float_or_zero(group.get(\"totalLength\")))\n            length_multiplier = _linear_length_multiplier(visible_length, reference_length)\n            tag_factor, tag_details = _group_importance_factor(group)\n            location_factor, location_details = _linear_location_factor(group)\n            final_score = _js_round(base_score * length_multiplier * tag_factor * location_factor)\n            factor_digits = _scoring_factor_digits()\n\n            components = OrderedDict()\n            components[\"category\"] = OrderedDict([\n                (sub_key, _js_round(base_score))\n            ])\n            components[\"length\"] = OrderedDict([\n                (str(_js_round(visible_length)), _round_component(length_multiplier, factor_digits))\n            ])\n            if tag_details is not None and tag_factor > 1.0:\n                components[\"importanceTags\"] = tag_details\n            components[\"location\"] = location_details\n\n            group[\"importanceScore\"] = _build_importance_score(final_score, components)\n\n\ndef _apply_building_importance_scores(main_entry: Dict[str, Any]) -> None:\n    subclasses = main_entry.get(\"subclasses\")\n    if not isinstance(subclasses, list):\n        return\n\n    building_groups = []  # type: List[Dict[str, Any]]\n    for sub_entry in subclasses:\n        if not isinstance(sub_entry, dict):\n            continue\n        if sub_entry.get(\"kind\") != \"building\":\n            continue\n        groups = sub_entry.get(\"groups\")\n        if not isinstance(groups, list):\n            continue\n        for group in groups:\n            if isinstance(group, dict):\n                building_groups.append(group)\n\n    if not building_groups:\n        return\n\n    named_sizes = []\n    unnamed_sizes = []\n    for group in building_groups:\n        size = _building_group_size(group)\n        if _has_meaningful_label(group.get(\"label\")):\n            named_sizes.append(size)\n        else:\n            unnamed_sizes.append(size)\n\n    max_named = max(named_sizes) if named_sizes else 0.0\n    max_unnamed = max(unnamed_sizes) if unnamed_sizes else 0.0\n\n    for group in building_groups:\n        is_named = _has_meaningful_label(group.get(\"label\"))\n        size = _building_group_size(group)\n        base = _scoring_building_base(is_named)\n        max_size = max_named if is_named else max_unnamed\n        relative_size = (size / max_size) if max_size > 0 else 1.0\n        tag_factor, tag_details = _group_importance_factor(group)\n        location_factor, location_details = _building_location_factor(group)\n        final_score = _js_round(base * relative_size * tag_factor * location_factor)\n        factor_digits = _scoring_factor_digits()\n        size_key_digits = _scoring_size_key_digits()\n\n        components = OrderedDict()\n        components[\"category\"] = OrderedDict([\n            (\"named\" if is_named else \"unnamed\", _js_round(base))\n        ])\n        components[\"size\"] = OrderedDict([\n            (_compact_number_key(size, size_key_digits), _round_component(relative_size, factor_digits))\n        ])\n        if tag_details is not None and tag_factor > 1.0:\n            components[\"importanceTags\"] = tag_details\n        components[\"location\"] = location_details\n\n        group[\"importanceScore\"] = _build_importance_score(final_score, components)\n\n\ndef _apply_poi_importance_scores(main_entry: Dict[str, Any]) -> None:\n    subclasses = main_entry.get(\"subclasses\")\n    if not isinstance(subclasses, list):\n        return\n    for sub_entry in subclasses:\n        if not isinstance(sub_entry, dict):\n            continue\n        if sub_entry.get(\"kind\") != \"poi\":\n            continue\n        sub_key = sub_entry.get(\"key\")\n        groups = sub_entry.get(\"groups\")\n        if not isinstance(groups, list):\n            continue\n        for group in groups:\n            if not isinstance(group, dict):\n                continue\n            tag_factor, tag_details = _group_importance_factor(group)\n            location_factor, location_details = _poi_location_factor(group)\n            poi_base = _scoring_poi_base()\n            final_score = _js_round(poi_base * tag_factor * location_factor)\n\n            components = OrderedDict()\n            if isinstance(sub_key, str):\n                components[\"category\"] = OrderedDict([\n                    (sub_key, _js_round(poi_base))\n                ])\n            if tag_details is not None and tag_factor > 1.0:\n                components[\"importanceTags\"] = tag_details\n            components[\"location\"] = location_details\n\n            group[\"importanceScore\"] = _build_importance_score(final_score, components)\n\n\ndef _apply_water_area_importance_scores(main_entry: Dict[str, Any],\n                                        boundary: Optional[Boundary]) -> None:\n    subclasses = main_entry.get(\"subclasses\")\n    if not isinstance(subclasses, list):\n        return\n\n    factor_digits = _scoring_factor_digits()\n    size_key_digits = _scoring_size_key_digits()\n    for sub_entry in subclasses:\n        if not isinstance(sub_entry, dict):\n            continue\n        if sub_entry.get(\"kind\") != \"area\":\n            continue\n        sub_key = sub_entry.get(\"key\")\n        if not isinstance(sub_key, str) or not sub_key.startswith(\"B1_\"):\n            continue\n        groups = sub_entry.get(\"groups\")\n        if not isinstance(groups, list):\n            continue\n        for group in groups:\n            if not isinstance(group, dict):\n                continue\n            coverage_percent = _water_area_group_coverage_percent(group, boundary)\n            base_score = 10.0 + (1.8 * coverage_percent)\n            final_score = _js_round(base_score)\n            components = OrderedDict()\n            components[\"category\"] = OrderedDict([\n                (sub_key, _js_round(base_score))\n            ])\n            components[\"coveragePercent\"] = OrderedDict([\n                (_compact_number_key(coverage_percent, size_key_digits), _round_component(1.0, factor_digits))\n            ])\n            group[\"importanceScore\"] = _build_importance_score(final_score, components)\n\n\ndef _attach_group_importance_scores(raw: List[Dict[str, Any]],\n                                    boundary: Optional[Boundary]) -> None:\n    for main_entry in raw:\n        if not isinstance(main_entry, dict):\n            continue\n        key = main_entry.get(\"key\")\n        if key == \"A\":\n            _apply_linear_importance_scores(main_entry, boundary)\n        elif key == \"B\":\n            _apply_water_area_importance_scores(main_entry, boundary)\n        elif key == \"C\":\n            _apply_building_importance_scores(main_entry)\n        elif key == \"D\":\n            _apply_poi_importance_scores(main_entry)\n\n\ndef _coerce_boundary(boundary: Optional[Dict[str, Any]]) -> Optional[Boundary]:\n    if not boundary:\n        return None\n    min_x = boundary.get(\"minX\")\n    min_y = boundary.get(\"minY\")\n    max_x = boundary.get(\"maxX\")\n    max_y = boundary.get(\"maxY\")\n    if not isinstance(min_x, (int, float)) or not isinstance(min_y, (int, float)):\n        return None\n    if not isinstance(max_x, (int, float)) or not isinstance(max_y, (int, float)):\n        return None\n    return {\n        \"minX\": float(min_x),\n        \"minY\": float(min_y),\n        \"maxX\": float(max_x),\n        \"maxY\": float(max_y)\n    }\n\n\ndef _js_round(value: float) -> int:\n    # Match JS Math.round behavior for consistent output.\n    if value >= 0:\n        return int((value + 0.5) // 1)\n    return int((value - 0.5) // 1)\n\n\ndef _to_fixed(value: float, digits: int) -> str:\n    # JS-like toFixed using JS rounding rules.\n    scale = 10 ** digits\n    rounded = _js_round(value * scale) / float(scale)\n    if digits == 0:\n        return str(int(_js_round(value)))\n    fmt = \"{0:.\" + str(digits) + \"f}\"\n    return fmt.format(rounded)\n\n\ndef _get_name(tags: Optional[Dict[str, Any]]) -> Optional[str]:\n    # Prefer the most human-friendly name available in tags.\n    if not tags:\n        return None\n    return (\n        tags.get(\"name\") or\n        tags.get(\"name:en\") or\n        tags.get(\"name:fi\") or\n        tags.get(\"name:sv\") or\n        tags.get(\"loc_name\") or\n        tags.get(\"short_name\") or\n        None\n    )\n\n\ndef _tag_text(tags: Optional[Dict[str, Any]], key: str) -> Optional[str]:\n    # Read a string tag value, treating blank strings as missing.\n    if not isinstance(tags, dict):\n        return None\n    if key not in tags:\n        return None\n    value = tags.get(key)\n    if value is None:\n        return None\n    text = str(value).strip()\n    return text if text else None\n\n\ndef _extract_extra_name_tags(tags: Optional[Dict[str, Any]]) -> Optional[OrderedDict]:\n    # Capture localized OSM name:* tags as-is for downstream scoring.\n    if not isinstance(tags, dict):\n        return None\n    extra_names = OrderedDict()\n    for key in sorted(tags.keys()):\n        if not isinstance(key, str) or not key.startswith(\"name:\"):\n            continue\n        value = _tag_text(tags, key)\n        if value is None:\n            continue\n        extra_names[key] = value\n    for key in (\"loc_name\", \"short_name\"):\n        value = _tag_text(tags, key)\n        if value is None:\n            continue\n        extra_names[key] = value\n    return extra_names if extra_names else None\n\n\ndef _extract_importance_tags(item: Dict[str, Any]) -> Optional[OrderedDict]:\n    # Extract a compact whitelist of salience-related OSM tags.\n    tags = item.get(\"tags\")\n    if not isinstance(tags, dict):\n        return None\n\n    importance_tags = OrderedDict()\n\n    for key in (\"wikidata\", \"wikipedia\", \"wikimedia_commons\"):\n        value = _tag_text(tags, key)\n        if value is not None:\n            importance_tags[key] = value\n\n    website = _tag_text(tags, \"website\")\n    if website is None:\n        website = _tag_text(tags, \"contact:website\")\n    if website is None:\n        website = _tag_text(tags, \"url\")\n    if website is not None:\n        importance_tags[\"website\"] = website\n\n    for key in (\"operator\", \"brand\", \"brand:wikidata\", \"historic\", \"heritage\"):\n        value = _tag_text(tags, key)\n        if value is not None:\n            importance_tags[key] = value\n\n    extra_names = _extract_extra_name_tags(tags)\n    if extra_names is not None:\n        importance_tags[\"extraNames\"] = extra_names\n\n    return importance_tags if importance_tags else None\n\n\ndef _loc_from_classification(location: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:\n    if not location or not isinstance(location, dict):\n        return None\n    loc = location.get(\"loc\")\n    if not isinstance(loc, dict):\n        return None\n    kind = loc.get(\"kind\")\n    direction = loc.get(\"dir\")\n    if kind == \"center\":\n        return {\"kind\": \"center\", \"dir\": None}\n    if kind == \"part\":\n        return {\"kind\": \"part\", \"dir\": direction}\n    if kind == \"near_edge\":\n        return {\"kind\": \"near_edge\", \"dir\": direction}\n    return None\n\n\ndef _extract_loc(value: Optional[Any]) -> Optional[Dict[str, Any]]:\n    if not value or not isinstance(value, dict):\n        return None\n    loc = value.get(\"loc\")\n    if isinstance(loc, dict):\n        return loc\n    if \"kind\" in value or \"dir\" in value:\n        return value\n    return None\n\n\ndef _loc_key(loc: Optional[Dict[str, Any]]) -> str:\n    if not loc or not isinstance(loc, dict):\n        return \"\"\n    kind = loc.get(\"kind\") or \"\"\n    direction = loc.get(\"dir\") or \"\"\n    if direction:\n        return str(kind) + \":\" + str(direction)\n    return str(kind)\n\n\ndef _location_struct_from_loc(loc: Optional[Dict[str, Any]], key: str) -> Optional[Dict[str, Any]]:\n    if not loc:\n        return None\n    return {key: {\"loc\": loc}}\n\n\ndef _location_key(location: Optional[Dict[str, Any]]) -> str:\n    # Stable string key for grouping by location locs.\n    if not location:\n        return \"\"\n    parts = []\n    for key in (\"start\", \"end\", \"center\", \"point\"):\n        value = location.get(key)\n        if isinstance(value, dict):\n            loc = _extract_loc(value)\n            if loc:\n                parts.append(key + \"=\" + _loc_key(loc))\n    return \"|\".join(parts)\n\n\ndef _attach_semantics(entry: Dict[str, Any], item: Dict[str, Any]) -> None:\n    semantics = item.get(\"semantics\")\n    if semantics is not None:\n        entry[\"semantics\"] = semantics\n    importance_tags = _extract_importance_tags(item)\n    if importance_tags is not None:\n        entry[\"importanceTags\"] = importance_tags\n        external_link = _build_external_link_from_importance_tags(importance_tags)\n        if external_link is not None:\n            entry[\"externalLink\"] = external_link\n\n\ndef _dir_label(direction: Optional[str]) -> Optional[str]:\n    if not direction:\n        return None\n    if direction == \"northwest\":\n        return \"north-west\"\n    if direction == \"northeast\":\n        return \"north-east\"\n    if direction == \"southwest\":\n        return \"south-west\"\n    if direction == \"southeast\":\n        return \"south-east\"\n    return direction\n\n\ndef _corner_label(direction: Optional[str]) -> Optional[str]:\n    if direction == \"northwest\":\n        return \"north-west corner\"\n    if direction == \"northeast\":\n        return \"north-east corner\"\n    if direction == \"southwest\":\n        return \"south-west corner\"\n    if direction == \"southeast\":\n        return \"south-east corner\"\n    return None\n\n\ndef _loc_phrase(loc: Optional[Dict[str, Any]]) -> Optional[str]:\n    \"\"\"Render location clauses\n\n    Location classes:\n    1) center\n    2) part + direction\n    3) near_edge + direction (diagonal near_edge means corner)\n\n    Clause form:\n    - \"in the center\"\n    - \"in the <dir> part\"\n    - \"near the <dir> edge\"\n    - \"in the <dir> corner\"\n\n    Rules:\n    - corner phrases use \"in the ... corner\"\n    \"\"\"\n    if not loc or not isinstance(loc, dict):\n        return None\n    kind = loc.get(\"kind\")\n    direction = loc.get(\"dir\")\n    dir_label = _dir_label(direction)\n    if kind == \"center\":\n        return \"in the center\"\n    if kind == \"part\":\n        if dir_label:\n            return \"in the \" + dir_label + \" part\"\n        return \"in the center\"\n    if kind == \"near_edge\":\n        if direction in (\"northwest\", \"northeast\", \"southwest\", \"southeast\"):\n            corner = _corner_label(direction)\n            if corner:\n                return \"in the \" + corner\n            return \"in the corner\"\n        if dir_label:\n            return \"near the \" + dir_label + \" edge\"\n        return \"near the edge\"\n    return None\n\n\ndef _location_value_phrase(value: Optional[Any]) -> Optional[str]:\n    if isinstance(value, dict):\n        loc = _extract_loc(value)\n        phrase = _loc_phrase(loc)\n        if phrase:\n            return phrase\n    return None\n\n\ndef _render_location_text(location: Optional[Dict[str, Any]], kind: str) -> Optional[str]:\n    # Render a human-readable location string from structured locs.\n    if not location:\n        return None\n    start_phrase = _location_value_phrase(location.get(\"start\"))\n    end_phrase = _location_value_phrase(location.get(\"end\"))\n    center_phrase = _location_value_phrase(location.get(\"center\")) or _location_value_phrase(location.get(\"point\"))\n    if kind in (\"linear\", \"boundary\"):\n        if not start_phrase and not end_phrase and not center_phrase:\n            return None\n        if start_phrase and end_phrase and start_phrase == end_phrase:\n            text = start_phrase\n        elif start_phrase and end_phrase:\n            text = start_phrase + \" -> \" + end_phrase\n        else:\n            text = start_phrase or end_phrase or \"\"\n        if center_phrase:\n            if text:\n                text += \" (center: \" + center_phrase + \")\"\n            else:\n                text = center_phrase\n        return text if text else None\n    return center_phrase or start_phrase or end_phrase\n\n\ndef _normalize_label(value: Optional[Any]) -> Optional[str]:\n    # Normalize tag values for display.\n    if not value:\n        return None\n    return str(value).replace(\"_\", \" \")\n\n\ndef _format_meters(length_meters: Optional[float]) -> Optional[str]:\n    # Format lengths in meters with coarse rounding like the JS renderer.\n    if length_meters is None:\n        return None\n    length = max(0, length_meters)\n    if length >= 1000:\n        rounded = _js_round(length / 10.0) * 10\n    elif length >= 100:\n        rounded = _js_round(length / 5.0) * 5\n    else:\n        rounded = _js_round(length)\n    return str(int(rounded)) + \" m\"\n\n\ndef _format_area(area_sq_m: Optional[float]) -> Optional[str]:\n    # Format areas using ha for large areas and m^2 otherwise.\n    if area_sq_m is None:\n        return None\n    area = max(0, area_sq_m)\n    if area >= 10000:\n        ha = area / 10000.0\n        digits = 0 if ha >= 10 else 1\n        return \"~\" + _to_fixed(ha, digits) + \" ha\"\n    return \"~\" + str(int(_js_round(area))) + \" m^2\"\n\n\ndef _coord_key(coord: List[float]) -> str:\n    # Stable key for coordinate matching across ways/nodes.\n    return _to_fixed(float(coord[0]), 3) + \",\" + _to_fixed(float(coord[1]), 3)\n\n\ndef _polyline_length(coords: List[List[float]]) -> float:\n    length = 0.0\n    for i in range(1, len(coords)):\n        a = coords[i - 1]\n        b = coords[i]\n        dx = b[0] - a[0]\n        dy = b[1] - a[1]\n        length += (dx * dx + dy * dy) ** 0.5\n    return length\n\n\ndef _sample_point(coords: List[List[float]], t: float) -> Tuple[float, float]:\n    total = _polyline_length(coords)\n    if total <= 0 or len(coords) == 1:\n        return coords[0][0], coords[0][1]\n    target = max(0.0, min(1.0, t)) * total\n    walked = 0.0\n    for i in range(1, len(coords)):\n        a = coords[i - 1]\n        b = coords[i]\n        dx = b[0] - a[0]\n        dy = b[1] - a[1]\n        seg_len = (dx * dx + dy * dy) ** 0.5\n        if seg_len == 0:\n            continue\n        if walked + seg_len >= target:\n            ratio = (target - walked) / seg_len\n            return a[0] + ratio * dx, a[1] + ratio * dy\n        walked += seg_len\n    return coords[-1][0], coords[-1][1]\n\n\ndef _location_zone_for_point(point: Tuple[float, float],\n                             boundary: Optional[Boundary]) -> Optional[Dict[str, Any]]:\n    if not boundary:\n        return None\n    classification = classify_location({\"x\": point[0], \"y\": point[1]}, boundary)\n    if not classification:\n        return None\n    loc = classification.get(\"loc\") if isinstance(classification, dict) else None\n    if isinstance(loc, dict):\n        return loc\n    return _loc_from_classification(classification)\n\n\ndef _sample_location_samples(coords: List[List[float]],\n                             boundary: Optional[Boundary]) -> List[Dict[str, Any]]:\n    samples = []\n    for t in SAMPLE_TS:\n        pt = _sample_point(coords, t)\n        zone = _location_zone_for_point(pt, boundary) or \"unknown\"\n        samples.append({\"t\": t, \"zone\": zone})\n    return samples\n\n\ndef _edge_contact(point: Tuple[float, float], boundary: Boundary, eps: float) -> Optional[Dict[str, Any]]:\n    x, y = point\n    edges = []\n    if abs(x - boundary[\"minX\"]) <= eps:\n        edges.append(\"west\")\n    if abs(x - boundary[\"maxX\"]) <= eps:\n        edges.append(\"east\")\n    if abs(y - boundary[\"minY\"]) <= eps:\n        edges.append(\"south\")\n    if abs(y - boundary[\"maxY\"]) <= eps:\n        edges.append(\"north\")\n    if not edges:\n        return None\n    if len(edges) >= 2:\n        corner = None\n        if \"north\" in edges and \"west\" in edges:\n            corner = \"northwest\"\n        elif \"north\" in edges and \"east\" in edges:\n            corner = \"northeast\"\n        elif \"south\" in edges and \"west\" in edges:\n            corner = \"southwest\"\n        elif \"south\" in edges and \"east\" in edges:\n            corner = \"southeast\"\n        return {\"edge\": \"corner\", \"cornerName\": corner, \"edges\": edges}\n    return {\"edge\": edges[0]}\n\n\ndef _event_sort_key(event: Dict[str, Any]) -> Tuple[float, int]:\n    order = {\n        \"map_edge_crossing\": 0,\n        \"junction\": 1,\n        \"continues_as\": 2,\n        \"terminates\": 3\n    }\n    evt_type = event.get(\"type\") or \"\"\n    return event.get(\"t\", 0.0), order.get(evt_type, 99)\n\n\ndef _build_connectors_by_coord_key(connectors: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:\n    index = {}  # type: Dict[str, List[Dict[str, Any]]]\n    for connector in connectors:\n        point = connector.get(\"point\")\n        if not point:\n            continue\n        key = _coord_key([point[0], point[1]])\n        bucket = index.get(key)\n        if bucket is None:\n            index[key] = [connector]\n            continue\n        bucket.append(connector)\n    return index\n\n\ndef _segment_vertex_first_occurrence_t(\n    coords: List[List[float]]\n) -> \"OrderedDict[str, Dict[str, Any]]\":\n    # Map each unique segment coordinate key to the first path position t in [0, 1].\n    index = OrderedDict()  # type: OrderedDict\n    if not coords:\n        return index\n\n    first = coords[0]\n    first_key = _coord_key(first)\n    index[first_key] = {\"t\": 0.0, \"point\": (first[0], first[1])}\n\n    total = _polyline_length(coords)\n    if total <= 0 or len(coords) == 1:\n        return index\n\n    walked = 0.0\n    for i in range(1, len(coords)):\n        a = coords[i - 1]\n        b = coords[i]\n        dx = b[0] - a[0]\n        dy = b[1] - a[1]\n        seg_len = (dx * dx + dy * dy) ** 0.5\n        walked += seg_len\n        key = _coord_key(b)\n        if key in index:\n            continue\n        index[key] = {\"t\": walked / total, \"point\": (b[0], b[1])}\n    return index\n\n\ndef _segment_events(coords: List[List[float]],\n                    boundary: Optional[Boundary],\n                    connectors_by_coord_key: Optional[Dict[str, List[Dict[str, Any]]]],\n                    connections_index: Dict[str, List[Dict[str, Any]]],\n                    emit_connectivity: bool) -> List[Dict[str, Any]]:\n    events = []\n    if not coords:\n        return events\n    start = (coords[0][0], coords[0][1])\n    end = (coords[-1][0], coords[-1][1])\n\n    if boundary:\n        for t_val, point in ((0.0, start), (1.0, end)):\n            edge_info = _edge_contact(point, boundary, EDGE_EPS)\n            if edge_info:\n                zone = _location_zone_for_point(point, boundary) or \"unknown\"\n                event = {\"t\": t_val, \"type\": \"map_edge_crossing\", \"zone\": zone}\n                event.update(edge_info)\n                events.append(event)\n\n    junction_events = []\n    if emit_connectivity:\n        if connectors_by_coord_key:\n            segment_coord_index = _segment_vertex_first_occurrence_t(coords)\n            for coord_key, hit in segment_coord_index.items():\n                matches = connectors_by_coord_key.get(coord_key)\n                if not matches:\n                    continue\n                t_val = hit.get(\"t\", 0.0)\n                closest = hit.get(\"point\", start)\n                zone = _location_zone_for_point(closest, boundary) or \"unknown\"\n                connections = connections_index.get(coord_key)\n                for connector in matches:\n                    event = {\n                        \"t\": t_val,\n                        \"type\": \"junction\",\n                        \"zone\": zone,\n                        \"connectorType\": connector[\"connectorType\"]\n                    }\n                    if connections:\n                        event[\"connections\"] = connections\n                    events.append(event)\n                    junction_events.append(event)\n\n    for t_val, point in ((0.0, start), (1.0, end)):\n        if boundary and _edge_contact(point, boundary, EDGE_EPS):\n            continue\n        if any(abs(evt.get(\"t\", 0.0) - t_val) <= 1e-6 for evt in junction_events):\n            continue\n        zone = _location_zone_for_point(point, boundary) or \"unknown\"\n        events.append({\"t\": t_val, \"type\": \"terminates\", \"zone\": zone})\n\n    return sorted(events, key=_event_sort_key)\n\n\ndef _build_visible_segments(item: Dict[str, Any],\n                            boundary: Optional[Boundary],\n                            connectors_by_coord_key: Optional[Dict[str, List[Dict[str, Any]]]],\n                            connections_index: Dict[str, List[Dict[str, Any]]],\n                            emit_connectivity: bool,\n                            profile: Optional[Dict[str, float]] = None) -> List[Dict[str, Any]]:\n    start_total = time.perf_counter()\n\n    def add_timing(name: str, elapsed: float) -> None:\n        if profile is None:\n            return\n        profile[name] = profile.get(name, 0.0) + elapsed\n\n    segments = []\n    segment_list = _iter_line_segments(item)\n    if not segment_list:\n        add_timing(\"build-visible-segments.total\", time.perf_counter() - start_total)\n        return segments\n\n    # Order segments by descending length, then stable index.\n    indexed = []\n    for idx, coords in enumerate(segment_list):\n        if not isinstance(coords, list) or not coords:\n            continue\n        indexed.append((idx, coords, _polyline_length(coords)))\n    sorted_indexed = sorted(indexed, key=lambda entry: (-entry[2], entry[0]))\n    for _, coords, length in sorted_indexed:\n        location_samples = _sample_location_samples(coords, boundary)\n        events = _segment_events(\n            coords,\n            boundary,\n            connectors_by_coord_key,\n            connections_index,\n            emit_connectivity\n        )\n        segments.append({\n            \"length\": length,\n            \"locationSamples\": location_samples,\n            \"events\": events\n        })\n    add_timing(\"build-visible-segments.total\", time.perf_counter() - start_total)\n    return segments\n\ndef _iter_grouped_items(grouped: Dict[str, Any]) -> Iterator[Dict[str, Any]]:\n    # Iterate items from already-classified grouped data.\n    if not isinstance(grouped, dict):\n        return\n    for main_group in grouped.values():\n        if not isinstance(main_group, dict):\n            continue\n        for items in main_group.values():\n            if not isinstance(items, list):\n                continue\n            for item in items:\n                yield item\n\n\ndef _connector_type_for_item(item: Dict[str, Any]) -> Optional[str]:\n    cls = item.get(\"_classification\", {})\n    role = cls.get(\"role\")\n    if role == \"junction\":\n        return \"RoadJunction\"\n    if role == \"connector\":\n        return \"RoadConnector\"\n    if role == \"crossing\":\n        return \"RoadCrossingAtConnector\"\n    pr = item.get(\"primaryRepresentation\")\n    if pr in (\"RoadJunction\", \"RoadConnector\", \"RoadCrossingAtConnector\"):\n        return pr\n    return None\n\n\ndef _collect_connectors(grouped: Dict[str, Any]) -> List[Dict[str, Any]]:\n    connectors = []\n    for item in _iter_grouped_items(grouped):\n        if item.get(\"elementType\") != \"node\":\n            continue\n        geom = item.get(\"geometry\") or {}\n        if geom.get(\"type\") != \"point\":\n            continue\n        coords = geom.get(\"coordinates\")\n        if not isinstance(coords, list) or len(coords) < 2:\n            continue\n        connector_type = _connector_type_for_item(item)\n        if not connector_type:\n            continue\n        connectors.append({\n            \"point\": (coords[0], coords[1]),\n            \"connectorType\": connector_type,\n            \"osmType\": item.get(\"osmType\"),\n            \"osmId\": item.get(\"osmId\")\n        })\n    return connectors\n\n\ndef _collect_inferred_named_connectors(\n    connections_index: Dict[str, List[Dict[str, Any]]]\n) -> List[Dict[str, Any]]:\n    # Recover named road junctions from shared line coordinates when explicit node\n    # connector features are missing from grouped metadata.\n    inferred = []\n    for key, features in connections_index.items():\n        if not isinstance(features, list) or len(features) < 2:\n            continue\n        names = []\n        seen_names = set()\n        for feature in features:\n            if feature.get(\"osmType\") != \"way\":\n                continue\n            name = feature.get(\"name\")\n            if not isinstance(name, str):\n                continue\n            norm = name.strip().lower()\n            if not norm or norm in seen_names:\n                continue\n            seen_names.add(norm)\n            names.append(name)\n        if len(names) < 2:\n            continue\n        try:\n            x_str, y_str = key.split(\",\")\n            point = (float(x_str), float(y_str))\n        except (ValueError, AttributeError):\n            continue\n        inferred.append({\n            \"point\": point,\n            \"connectorType\": \"RoadJunction\",\n            \"osmType\": None,\n            \"osmId\": None\n        })\n    return inferred\n\n\ndef _merge_connectors(\n    primary: List[Dict[str, Any]], secondary: List[Dict[str, Any]]\n) -> List[Dict[str, Any]]:\n    merged = list(primary)\n    seen = set()\n    for connector in merged:\n        point = connector.get(\"point\")\n        if not point:\n            continue\n        seen.add(_coord_key([point[0], point[1]]))\n    for connector in secondary:\n        point = connector.get(\"point\")\n        if not point:\n            continue\n        key = _coord_key([point[0], point[1]])\n        if key in seen:\n            continue\n        seen.add(key)\n        merged.append(connector)\n    return merged\n\n\ndef _feature_info(item: Dict[str, Any]) -> Optional[Dict[str, Any]]:\n    osm_id = item.get(\"osmId\")\n    osm_type = item.get(\"osmType\")\n    if osm_id is None or osm_type is None:\n        return None\n    name = _get_name(item.get(\"tags\"))\n    sub_class = item.get(\"_classification\", {}).get(\"subClass\")\n    return {\n        \"osmType\": osm_type,\n        \"osmId\": osm_id,\n        \"name\": name,\n        \"subClass\": sub_class\n    }\n\n\ndef _is_railway_subclass(sub_class: Any) -> bool:\n    return isinstance(sub_class, str) and sub_class.startswith(\"A3_\")\n\n\ndef _build_connections_index(grouped: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]:\n    index = {}\n    for item in _iter_grouped_items(grouped):\n        geom = item.get(\"geometry\") or {}\n        if geom.get(\"type\") != \"line_string\":\n            continue\n        info = _feature_info(item)\n        if not info:\n            continue\n        # Railway intersections are intentionally omitted from connectivity narration.\n        if _is_railway_subclass(info.get(\"subClass\")):\n            continue\n        for coords in _iter_line_segments(item):\n            for coord in coords:\n                key = _coord_key(coord)\n                entry = index.get(key)\n                if not entry:\n                    index[key] = [info]\n                else:\n                    if not any(info.get(\"osmId\") == existing.get(\"osmId\") and\n                               info.get(\"osmType\") == existing.get(\"osmType\")\n                               for existing in entry):\n                        entry.append(info)\n    return index\n\n\ndef _iter_line_segments(item: Dict[str, Any]) -> List[List[List[float]]]:\n    visible = item.get(\"visibleGeometry\")\n    if isinstance(visible, list):\n        return [seg for seg in visible if isinstance(seg, list)]\n    coords = item.get(\"geometry\", {}).get(\"coordinates\")\n    if isinstance(coords, list):\n        return [coords]\n    return []\n\n\ndef _build_road_names_by_coord(map_data: Dict[str, Any]) -> Dict[str, List[str]]:\n    # Build a coordinate->road-names map for connectivity summaries.\n    road_map = {}\n    ways = []\n    if isinstance(map_data, dict) and isinstance(map_data.get(\"ways\"), list):\n        ways = map_data.get(\"ways\") or []\n    else:\n        for item in _iter_grouped_items(map_data):\n            if item.get(\"elementType\") == \"way\":\n                ways.append(item)\n    for way in ways:\n        sub_class = way.get(\"_classification\", {}).get(\"subClass\")\n        if _is_railway_subclass(sub_class):\n            continue\n        name = _get_name(way.get(\"tags\"))\n        if not name:\n            continue\n        for coords in _iter_line_segments(way):\n            for coord in coords:\n                key = _coord_key(coord)\n                entry = road_map.get(key)\n                if not entry:\n                    road_map[key] = [name]\n                else:\n                    if name not in entry:\n                        entry.append(name)\n    return road_map\n\n\ndef _segment_length(coords: List[List[float]]) -> float:\n    length = 0.0\n    for i in range(1, len(coords)):\n        a = coords[i - 1]\n        b = coords[i]\n        dx = b[0] - a[0]\n        dy = b[1] - a[1]\n        length += (dx * dx + dy * dy) ** 0.5\n    return length\n\n\ndef _compute_line_length(item: Dict[str, Any]) -> Optional[float]:\n    # Polyline length in local map units.\n    geom = item.get(\"geometry\") or {}\n    if geom.get(\"type\") != \"line_string\":\n        return None\n    segments = _iter_line_segments(item)\n    if not segments:\n        return None\n    total = 0.0\n    for coords in segments:\n        if len(coords) < 2:\n            continue\n        total += _segment_length(coords)\n    return total\n\n\ndef _ring_area(coords: List[List[float]]) -> float:\n    # Polygon ring area using the shoelace formula.\n    if not coords or len(coords) < 3:\n        return 0\n    total = 0.0\n    for i in range(len(coords)):\n        p1 = coords[i]\n        p2 = coords[(i + 1) % len(coords)]\n        total += p1[0] * p2[1] - p2[0] * p1[1]\n    return abs(total) / 2.0\n\n\ndef _compute_area(geometry: Optional[Dict[str, Any]], bounds: Optional[Bounds]) -> Optional[float]:\n    # Polygon area (with holes) or bbox fallback.\n    if geometry and geometry.get(\"type\") == \"polygon\":\n        area = 0.0\n        outer = geometry.get(\"outer\")\n        if isinstance(outer, list):\n            area += _ring_area(outer)\n        else:\n            coords = geometry.get(\"coordinates\")\n            if isinstance(coords, list) and coords and isinstance(coords[0], list):\n                area += _ring_area(coords)\n        holes = geometry.get(\"holes\")\n        if isinstance(holes, list):\n            for hole in holes:\n                area -= _ring_area(hole)\n        return abs(area)\n    if bounds:\n        min_x = bounds.get(\"minX\")\n        min_y = bounds.get(\"minY\")\n        max_x = bounds.get(\"maxX\")\n        max_y = bounds.get(\"maxY\")\n        if min_x is None or min_y is None or max_x is None or max_y is None:\n            return None\n        width = abs(max_x - min_x)\n        height = abs(max_y - min_y)\n        return width * height\n    return None\n\n\ndef _modifiers_suffix(modifiers: Optional[List[Dict[str, Any]]]) -> str:\n    # Render bracketed modifiers like [bridge, layer=1].\n    if not modifiers:\n        return \"\"\n    labels = []\n    for mod in modifiers:\n        name = mod.get(\"name\")\n        if not name:\n            continue\n        if \"value\" in mod and mod.get(\"value\") is not None:\n            labels.append(name + \"=\" + str(mod.get(\"value\")))\n        else:\n            labels.append(name)\n    if not labels:\n        return \"\"\n    return \" [\" + \", \".join(labels) + \"]\"\n\n\ndef _summarize_linear_base(item: Dict[str, Any],\n                           boundary: Optional[Boundary],\n                           connectors_by_coord_key: Optional[Dict[str, List[Dict[str, Any]]]],\n                           connections_index: Dict[str, List[Dict[str, Any]]],\n                           emit_connectivity: bool,\n                           profile: Optional[Dict[str, float]] = None) -> Dict[str, Any]:\n    # Summary for linear features: name, modifiers, and visible segment metadata.\n    start_total = time.perf_counter()\n\n    def add_timing(name: str, elapsed: float) -> None:\n        if profile is None:\n            return\n        profile[name] = profile.get(name, 0.0) + elapsed\n\n    name = _get_name(item.get(\"tags\"))\n    mod_suffix = _modifiers_suffix(item.get(\"_classification\", {}).get(\"modifiers\"))\n    build_visible_segments_start = time.perf_counter()\n    visible_segments = _build_visible_segments(\n        item, boundary, connectors_by_coord_key, connections_index, emit_connectivity, profile\n    )\n    add_timing(\"summarize-linear-base.build-visible-segments\", time.perf_counter() - build_visible_segments_start)\n\n    length = _compute_line_length(item)\n\n    summary = {\n        \"osmId\": item.get(\"osmId\"),\n        \"osmType\": item.get(\"osmType\"),\n        \"label\": name if name else None,\n        \"displayLabel\": (name if name else \"(unnamed)\") + mod_suffix,\n        \"visibleGeometry\": visible_segments,\n        \"length\": length\n    }\n    _attach_semantics(summary, item)\n    add_timing(\"summarize-linear-base.total\", time.perf_counter() - start_total)\n    return summary\n\n\ndef _connected_road_names(item: Dict[str, Any], road_names_by_coord: Dict[str, List[str]]) -> List[str]:\n    # Resolve road names touching a node by coordinate.\n    coords = item.get(\"geometry\", {}).get(\"coordinates\")\n    if not isinstance(coords, list):\n        return []\n    key = _coord_key(coords)\n    return list(road_names_by_coord.get(key, []))\n\n\ndef _summarize_connectivity_base(item: Dict[str, Any],\n                                 road_names_by_coord: Dict[str, List[str]]) -> Dict[str, Any]:\n    # Summary for junction/connector/crossing nodes.\n    role = item.get(\"_classification\", {}).get(\"role\") or \"node\"\n    names = _connected_road_names(item, road_names_by_coord)\n    if names:\n        label = role + \": \" + \" x \".join(names)\n    else:\n        label = role + \": (unnamed)\"\n    summary = {\n        \"osmId\": item.get(\"osmId\"),\n        \"osmType\": item.get(\"osmType\"),\n        \"label\": label\n    }\n    _attach_semantics(summary, item)\n    return summary\n\n\ndef _summarize_building_base(item: Dict[str, Any]) -> Dict[str, Any]:\n    # Summary for buildings with type/name/address and location.\n    tags = item.get(\"tags\") or {}\n    amenity = _normalize_label(tags.get(\"amenity\"))\n    building_use = _normalize_label(tags.get(\"building:use\"))\n    building = _normalize_label(tags.get(\"building\"))\n    subtype = item.get(\"_classification\", {}).get(\"subClass\")\n    if amenity:\n        type_label = amenity\n    elif building_use:\n        type_label = building_use\n    elif building and building != \"yes\":\n        type_label = building + \" building\"\n    elif subtype == \"C1_landmark\":\n        type_label = \"landmark building\"\n    elif subtype == \"C2_public\":\n        type_label = \"public building\"\n    else:\n        type_label = \"building\"\n\n    name = _get_name(tags)\n    street = tags.get(\"addr:street\")\n    number = tags.get(\"addr:housenumber\")\n    address = street + \" \" + number if (street and number) else None\n\n    parts = [type_label]\n    if name:\n        parts.append(name)\n    if address:\n        parts.append(address)\n    summary = {\n        \"osmId\": item.get(\"osmId\"),\n        \"osmType\": item.get(\"osmType\"),\n        \"label\": name if name else None,\n        \"displayLabel\": \", \".join(parts),\n        \"location\": _location_struct_from_loc(\n            _loc_from_classification(item.get(\"_classification\", {}).get(\"locationCenter\")),\n            \"center\"\n        )\n    }\n    if \"visibleGeometry\" in item:\n        summary[\"visibleGeometry\"] = item.get(\"visibleGeometry\")\n    _attach_semantics(summary, item)\n    return summary\n\n\ndef _summarize_poi_base(item: Dict[str, Any]) -> Dict[str, Any]:\n    # Summary for POIs with category/qualifier and location.\n    tags = item.get(\"tags\") or {}\n    subtype = item.get(\"_classification\", {}).get(\"subClass\")\n    category = \"poi\"\n    if subtype == \"D1_transport\":\n        category = \"transport\"\n    elif subtype == \"D2_civic\":\n        category = \"civic\"\n    elif subtype == \"D3_commercial\":\n        category = \"commercial\"\n    elif subtype == \"D4_leisure_cultural\":\n        category = \"leisure\"\n\n    qualifier = (\n        _normalize_label(tags.get(\"public_transport\")) or\n        _normalize_label(tags.get(\"railway\")) or\n        _normalize_label(tags.get(\"amenity\")) or\n        _normalize_label(tags.get(\"shop\")) or\n        _normalize_label(tags.get(\"tourism\")) or\n        _normalize_label(tags.get(\"leisure\"))\n    )\n    name = _get_name(tags)\n    label = qualifier or category\n    if name:\n        summary = {\n            \"osmId\": item.get(\"osmId\"),\n            \"osmType\": item.get(\"osmType\"),\n            \"label\": name,\n            \"displayLabel\": label + \": \" + name,\n            \"location\": _location_struct_from_loc(\n                _loc_from_classification(item.get(\"_classification\", {}).get(\"location\")),\n                \"point\"\n            )\n        }\n        _attach_semantics(summary, item)\n        return summary\n    summary = {\n        \"osmId\": item.get(\"osmId\"),\n        \"osmType\": item.get(\"osmType\"),\n        \"label\": None,\n        \"displayLabel\": label,\n        \"location\": _location_struct_from_loc(\n            _loc_from_classification(item.get(\"_classification\", {}).get(\"location\")),\n            \"point\"\n        )\n    }\n    _attach_semantics(summary, item)\n    return summary\n\n\ndef _area_type_label(sub_class: Optional[str]) -> str:\n    # Human-facing labels for area subclasses.\n    mapping = {\n        \"B1_lakes\": \"lake\",\n        \"B1_ponds\": \"pond\",\n        \"B1_reservoirs\": \"reservoir\",\n        \"B1_sea_coast\": \"sea\",\n        \"B1_riverbanks\": \"riverbank\",\n        \"B1_other_water\": \"water\",\n        \"B2_parks_recreation\": \"park\",\n        \"B2_forests\": \"forest\",\n        \"B2_fields_open\": \"open land\",\n        \"B3_residential\": \"residential area\",\n        \"B3_commercial\": \"commercial area\",\n        \"B3_industrial\": \"industrial area\",\n        \"B_other_areas\": \"area\",\n        \"E1_admin_boundaries\": \"admin boundary\",\n        \"E2_coastlines\": \"coastline\",\n        \"E3_fences_walls\": \"fence/wall\"\n    }\n    if sub_class is None:\n        return \"area\"\n    return mapping.get(sub_class, \"area\")\n\n\ndef _summarize_area_base(item: Dict[str, Any]) -> Dict[str, Any]:\n    # Summary for area features with type/name/size and location.\n    subtype = item.get(\"_classification\", {}).get(\"subClass\")\n    label = _area_type_label(subtype)\n    name = _get_name(item.get(\"tags\"))\n    base = label + \": \" + name if name else label + \" (unnamed)\"\n    summary = {\n        \"osmId\": item.get(\"osmId\"),\n        \"osmType\": item.get(\"osmType\"),\n        \"label\": name if name else None,\n        \"displayLabel\": base,\n        \"location\": _location_struct_from_loc(\n            _loc_from_classification(item.get(\"_classification\", {}).get(\"locationCenter\")),\n            \"center\"\n        ),\n        \"area\": _compute_area(item.get(\"geometry\"), item.get(\"bounds\")),\n    }\n    if \"visibleGeometry\" in item:\n        summary[\"visibleGeometry\"] = item.get(\"visibleGeometry\")\n    _attach_semantics(summary, item)\n    return summary\n\n\ndef _summarize_boundary_base(item: Dict[str, Any],\n                             boundary: Optional[Boundary],\n                             connectors_by_coord_key: Optional[Dict[str, List[Dict[str, Any]]]],\n                             connections_index: Dict[str, List[Dict[str, Any]]],\n                             emit_connectivity: bool) -> Dict[str, Any]:\n    # Summary for boundary/edge features with length and visible segment metadata.\n    subtype = item.get(\"_classification\", {}).get(\"subClass\")\n    label = _area_type_label(subtype)\n    name = _get_name(item.get(\"tags\"))\n    summary = label + \": \" + name if name else label\n    summary = {\n        \"osmId\": item.get(\"osmId\"),\n        \"osmType\": item.get(\"osmType\"),\n        \"label\": name if name else None,\n        \"displayLabel\": summary,\n        \"visibleSegments\": _build_visible_segments(\n            item, boundary, connectors_by_coord_key, connections_index, emit_connectivity\n        ),\n        \"length\": _compute_line_length(item)\n    }\n    _attach_semantics(summary, item)\n    return summary\n\n\ndef _sort_groups(groups: List[Dict[str, Any]], kind: str) -> List[Dict[str, Any]]:\n    # Sort grouped summaries by salience: named first, then size/length.\n    def sort_key(entry):\n        if kind in (\"linear\", \"boundary\"):\n            metric = entry.get(\"totalLength\", 0)\n        elif kind == \"area\":\n            metric = entry.get(\"totalArea\", 0)\n        elif kind == \"connectivity\":\n            metric = _group_count(entry)\n        else:\n            metric = 0\n        return (entry.get(\"label\") is None, -metric, entry.get(\"displayLabel\") or \"\")\n    return sorted(groups, key=sort_key)\n\n\ndef _group_count(group: Dict[str, Any]) -> int:\n    count = group.get(\"count\")\n    if isinstance(count, int):\n        return count\n    ways = group.get(\"ways\")\n    if isinstance(ways, list):\n        return len(ways)\n    items = group.get(\"items\")\n    if isinstance(items, list):\n        return len(items)\n    return 0\n\n\ndef _build_way_groups(items: List[Dict[str, Any]],\n                      boundary: Optional[Boundary],\n                      connectors_by_coord_key: Optional[Dict[str, List[Dict[str, Any]]]],\n                      connections_index: Dict[str, List[Dict[str, Any]]],\n                      emit_connectivity: bool,\n                      profile: Optional[Dict[str, float]] = None) -> List[Dict[str, Any]]:\n    # Ways are grouped by displayLabel to keep one top-level entry per logical way.\n    start_total = time.perf_counter()\n\n    def add_timing(name: str, elapsed: float) -> None:\n        if profile is None:\n            return\n        profile[name] = profile.get(name, 0.0) + elapsed\n\n    groups = OrderedDict()\n    for item in items:\n        summarize_start = time.perf_counter()\n        base = _summarize_linear_base(\n            item, boundary, connectors_by_coord_key, connections_index, emit_connectivity, profile\n        )\n        add_timing(\"build-way-groups.summarize-linear-base\", time.perf_counter() - summarize_start)\n\n        display_label = base.get(\"displayLabel\") or \"\"\n        is_unnamed = (base.get(\"label\") is None) or display_label.startswith(\"(unnamed)\")\n        if is_unnamed:\n            key = display_label + \"||\" + str(base.get(\"osmType\") or \"\") + \":\" + str(base.get(\"osmId\"))\n        else:\n            key = display_label\n        group = groups.get(key)\n        if not group:\n            groups[key] = {\n                \"label\": base.get(\"label\"),\n                \"displayLabel\": display_label,\n                \"totalLength\": 0.0,\n                \"totalArea\": 0.0,\n                \"ways\": [],\n                \"visibleGeometry\": []\n            }\n            group = groups[key]\n        if base.get(\"length\"):\n            group[\"totalLength\"] += base.get(\"length\")\n        group[\"ways\"].append(base)\n\n    for group in groups.values():\n        ways = group.get(\"ways\") or []\n        sorted_ways = sorted(\n            ways,\n            key=lambda entry: (\n                -(entry.get(\"length\") or 0.0),\n                entry.get(\"osmId\") is None,\n                entry.get(\"osmId\") or 0\n            )\n        )\n        group[\"ways\"] = sorted_ways\n        buckets = []\n        for way in sorted_ways:\n            visible = way.get(\"visibleGeometry\")\n            buckets.append({\n                \"osmId\": way.get(\"osmId\"),\n                \"segments\": visible if isinstance(visible, list) else []\n            })\n        group[\"visibleGeometry\"] = buckets\n\n    add_timing(\"build-way-groups.total\", time.perf_counter() - start_total)\n\n    return list(groups.values())\n\n\ndef _build_groups(items: List[Dict[str, Any]], kind: str,\n                  road_names_by_coord: Dict[str, List[str]],\n                  boundary: Optional[Boundary],\n                  connectors_by_coord_key: Optional[Dict[str, List[Dict[str, Any]]]],\n                  connections_index: Dict[str, List[Dict[str, Any]]],\n                  emit_connectivity: bool) -> List[Dict[str, Any]]:\n    # Collapse repeated items into grouped summaries for compact output.\n    groups = OrderedDict()\n    for item in items:\n        if kind == \"connectivity\":\n            base = _summarize_connectivity_base(item, road_names_by_coord)\n        elif kind == \"building\":\n            base = _summarize_building_base(item)\n        elif kind == \"poi\":\n            base = _summarize_poi_base(item)\n        elif kind == \"area\":\n            base = _summarize_area_base(item)\n        elif kind == \"boundary\":\n            base = _summarize_boundary_base(\n                item, boundary, connectors_by_coord_key, connections_index, emit_connectivity\n            )\n        else:\n            base = _summarize_linear_base(\n                item, boundary, connectors_by_coord_key, connections_index, emit_connectivity\n            )\n\n        key = (base.get(\"displayLabel\") or \"\") + \"||\" + _location_key(base.get(\"location\"))\n        group = groups.get(key)\n        if not group:\n            groups[key] = {\n                \"label\": base.get(\"label\"),\n                \"displayLabel\": base.get(\"displayLabel\"),\n                \"location\": base.get(\"location\"),\n                \"totalLength\": 0.0,\n                \"totalArea\": 0.0,\n                \"items\": []\n            }\n            group = groups[key]\n        if base.get(\"length\"):\n            group[\"totalLength\"] += base.get(\"length\")\n        if base.get(\"area\"):\n            group[\"totalArea\"] += base.get(\"area\")\n        group[\"items\"].append(base)\n\n    return list(groups.values())\n\n\ndef _render_group_line(group: Dict[str, Any], kind: str) -> str:\n    # Render a single summary line, with totals for grouped items.\n    display_label = group.get(\"displayLabel\") or \"(unnamed)\"\n    if _group_count(group) == 1:\n        if kind == \"linear\":\n            length = _format_meters(group.get(\"totalLength\"))\n            location_text = _render_location_text(group.get(\"location\"), kind)\n            if length and location_text:\n                return display_label + \" — \" + length + \" — \" + location_text\n            if length:\n                return display_label + \" — \" + length\n            return display_label + \" — \" + location_text if location_text else display_label\n        if kind == \"boundary\":\n            b_len = _format_meters(group.get(\"totalLength\"))\n            location_text = _render_location_text(group.get(\"location\"), kind)\n            if b_len and location_text:\n                return display_label + \" — \" + b_len + \" — \" + location_text\n            if b_len:\n                return display_label + \" — \" + b_len\n            return display_label + \" — \" + location_text if location_text else display_label\n        if kind == \"area\":\n            area = _format_area(group.get(\"totalArea\"))\n            location_text = _render_location_text(group.get(\"location\"), kind)\n            if area and location_text:\n                return display_label + \", \" + area + \" — \" + location_text\n            if area:\n                return display_label + \", \" + area\n            return display_label + \" — \" + location_text if location_text else display_label\n        location_text = _render_location_text(group.get(\"location\"), kind)\n        return display_label + \" — \" + location_text if location_text else display_label\n\n    prefix = str(_group_count(group)) + \" x \" + display_label\n    if kind in (\"linear\", \"boundary\"):\n        total_len = _format_meters(group.get(\"totalLength\"))\n        location_text = _render_location_text(group.get(\"location\"), kind)\n        if total_len and location_text:\n            return prefix + \" — total \" + total_len + \" — \" + location_text\n        if total_len:\n            return prefix + \" — total \" + total_len\n        return prefix + \" — \" + location_text if location_text else prefix\n    if kind == \"area\":\n        total_area = _format_area(group.get(\"totalArea\"))\n        location_text = _render_location_text(group.get(\"location\"), kind)\n        if total_area and location_text:\n            return prefix + \" — total \" + total_area + \" — \" + location_text\n        if total_area:\n            return prefix + \" — total \" + total_area\n        return prefix + \" — \" + location_text if location_text else prefix\n    location_text = _render_location_text(group.get(\"location\"), kind)\n    return prefix + \" — \" + location_text if location_text else prefix\n\n\ndef _resolve_options(spec: Dict[str, Any], options_override: Optional[Dict[str, Any]]) -> Dict[str, Any]:\n    options = {}\n    options.update(spec.get(\"options\", {}))\n    options.update(options_override or {})\n    return options\n\n\ndef build_intermediate(grouped: Dict[str, Any], spec: Dict[str, Any],\n                       map_data: Optional[Dict[str, Any]] = None,\n                       options_override: Optional[Dict[str, Any]] = None,\n                       profile: Optional[Dict[str, float]] = None) -> Dict[str, Any]:\n    # Code below creates stage \"Render-ready intermediate\" data.\n    # Build a structured intermediate representation for later rendering.\n    start_total = time.perf_counter()\n\n    def add_timing(name: str, elapsed: float) -> None:\n        if profile is None:\n            return\n        profile[name] = profile.get(name, 0.0) + elapsed\n\n    road_names_by_coord = _build_road_names_by_coord(map_data or grouped)\n\n    options = _resolve_options(spec, options_override)\n    emit_connectivity = bool(options.get(\"emitConnectivityNodes\", True))\n    boundary = _coerce_boundary((map_data or {}).get(\"meta\", {}).get(\"boundary\"))\n\n    connections_index = _build_connections_index(grouped) if emit_connectivity else {}\n\n    connectors = _collect_connectors(grouped) if emit_connectivity else []\n\n    if emit_connectivity:\n        connectors = _merge_connectors(\n            connectors,\n            _collect_inferred_named_connectors(connections_index)\n        )\n\n    connectors_by_coord_key = _build_connectors_by_coord_key(connectors) if emit_connectivity else {}\n\n    classes = spec.get(\"classes\") or OrderedDict()\n    main_keys = sorted(classes.keys())\n    raw = []  # type: List[Dict[str, Any]]\n\n    for main_key in main_keys:\n        main_name = classes.get(main_key, {}).get(\"name\", main_key)\n        sub_groups = grouped.get(main_key) or OrderedDict()\n        subclasses = classes.get(main_key, {}).get(\"subclasses\") or {}\n        sub_order = list(subclasses.keys())\n        sub_keys = [k for k in sub_order if sub_groups.get(k)]\n        for key in sub_groups.keys():\n            if key not in sub_keys and sub_groups.get(key):\n                sub_keys.append(key)\n\n        main_entry = {  # type: Dict[str, Any]\n            \"key\": main_key,\n            \"name\": main_name,\n            \"subclasses\": []\n        }\n\n        if not sub_keys:\n            main_entry[\"subclasses\"].append({\n                \"key\": None,\n                \"name\": None,\n                \"kind\": None,\n                \"groups\": [],\n                \"empty\": True\n            })\n            raw.append(main_entry)\n            continue\n\n        for sub_key in sub_keys:\n            items = sub_groups.get(sub_key) or []\n            if not items:\n                continue\n            sub_name = subclasses.get(sub_key, sub_key)\n\n            kind = \"linear\"\n            if main_key == \"A\" and sub_key == \"A5_connectivity_nodes\":\n                kind = \"connectivity\"\n            elif main_key == \"C\":\n                kind = \"building\"\n            elif main_key == \"D\":\n                kind = \"poi\"\n            elif main_key == \"B\":\n                kind = \"area\"\n            elif main_key == \"E\":\n                geom_type = items[0].get(\"geometry\", {}).get(\"type\")\n                kind = \"area\" if geom_type == \"polygon\" else \"linear\"\n\n            if main_key == \"A\" and kind == \"linear\":\n                build_way_groups_start = time.perf_counter()\n                grouped_items = _build_way_groups(\n                    items,\n                    boundary,\n                    connectors_by_coord_key,\n                    connections_index,\n                    emit_connectivity,\n                    profile\n                )\n                add_timing(\"build-intermediate.subclass-build-way-groups\", time.perf_counter() - build_way_groups_start)\n            else:\n                grouped_items = _build_groups(\n                    items,\n                    kind,\n                    road_names_by_coord,\n                    boundary,\n                    connectors_by_coord_key,\n                    connections_index,\n                    emit_connectivity\n                )\n\n            sort_kind = \"boundary\" if (kind == \"linear\" and main_key == \"E\") else kind\n            sorted_groups = _sort_groups(grouped_items, sort_kind)\n\n            main_entry[\"subclasses\"].append({\n                \"key\": sub_key,\n                \"name\": sub_name,\n                \"kind\": sort_kind,\n                \"groups\": sorted_groups\n            })\n\n        raw.append(main_entry)\n\n    _attach_group_importance_scores(raw, boundary)\n\n    add_timing(\"build-intermediate.total\", time.perf_counter() - start_total)\n    return {\"raw\": raw}\n\n\ndef render_from_intermediate(intermediate: Dict[str, Any]) -> str:\n    # Render human-readable output from the intermediate representation.\n    lines = []  # type: List[str]\n    for main_entry in intermediate.get(\"raw\", []):\n        main_key = main_entry.get(\"key\")\n        main_name = main_entry.get(\"name\") or main_key\n        lines.append(str(main_key) + \" — \" + str(main_name))\n\n        subclasses = main_entry.get(\"subclasses\") or []\n        if subclasses and subclasses[0].get(\"empty\"):\n            lines.append(\"  (no items)\")\n            lines.append(\"\")\n            continue\n\n        for sub_entry in subclasses:\n            sub_key = sub_entry.get(\"key\")\n            sub_name = sub_entry.get(\"name\") or sub_key\n            groups = sub_entry.get(\"groups\") or []\n            count = sum(_group_count(group) for group in groups)\n            lines.append(\"  \" + str(sub_key) + \" — \" + str(sub_name) + \" (\" + str(count) + \")\")\n\n            display = groups[:MAX_ITEMS_PER_SUBCLASS]\n            for group in display:\n                lines.append(\"    - \" + _render_group_line(group, sub_entry.get(\"kind\", \"\")))\n            if len(groups) > MAX_ITEMS_PER_SUBCLASS:\n                lines.append(\"    - ... (+\" + str(len(groups) - MAX_ITEMS_PER_SUBCLASS) + \" more)\")\n\n        lines.append(\"\")\n\n    return \"\\n\".join(lines).strip()\n\n\ndef write_map_content(grouped: Dict[str, Any], spec: Dict[str, Any],\n                      output_path: str, map_data: Optional[Dict[str, Any]] = None,\n                      options_override: Optional[Dict[str, Any]] = None,\n                      profile: Optional[Dict[str, float]] = None,\n                      pretty_json: Optional[bool] = None) -> Dict[str, Any]:\n    # Code below creates stage \"Final map content\" data.\n    # Persist structured grouped output for map content consumers.\n    start_total = time.perf_counter()\n\n    def add_timing(name: str, elapsed: float) -> None:\n        if profile is None:\n            return\n        profile[name] = profile.get(name, 0.0) + elapsed\n\n    build_intermediate_start = time.perf_counter()\n    intermediate = build_intermediate(grouped, spec, map_data, options_override, profile)\n    add_timing(\"write-map-content.build-intermediate\", time.perf_counter() - build_intermediate_start)\n\n    content = OrderedDict()  # type: OrderedDict\n    for main_entry in intermediate.get(\"raw\", []):\n        key = main_entry.get(\"key\") or \"unknown\"\n        content[key] = main_entry\n    if map_data:\n        meta = map_data.get(\"meta\") or {}\n        boundary = meta.get(\"boundary\")\n        if boundary:\n            content[\"boundary\"] = boundary\n\n    json_dump_start = time.perf_counter()\n    _write_json_fast(output_path, content, pretty_json=pretty_json)\n    add_timing(\"write-map-content.json-dump\", time.perf_counter() - json_dump_start)\n    add_timing(\"write-map-content.total\", time.perf_counter() - start_total)\n    return content\n\n\ndef _load_json(path: str) -> OrderedDict:\n    # Read JSON with stable key ordering for deterministic output.\n    with open(path, \"r\") as handle:\n        return json.load(handle, object_pairs_hook=OrderedDict)\n\n\ndef _parse_env_bool(name: str) -> Optional[bool]:\n    raw = os.environ.get(name)\n    if raw is None:\n        return None\n    normalized = raw.strip().lower()\n    if normalized in (\"1\", \"true\", \"yes\", \"on\"):\n        return True\n    if normalized in (\"0\", \"false\", \"no\", \"off\"):\n        return False\n    return None\n\n\ndef _pretty_json_enabled(pretty_json: Optional[bool] = None) -> bool:\n    if pretty_json is not None:\n        return bool(pretty_json)\n    forced = _parse_env_bool(\"TOUCH_MAPPER_PRETTY_JSON\")\n    if forced is not None:\n        return forced\n    return False\n\n\ndef _write_json_fast(path: str, value: Any, pretty_json: Optional[bool] = None) -> None:\n    use_pretty = _pretty_json_enabled(pretty_json)\n    with open(path, \"w\") as handle:\n        if use_pretty:\n            json.dump(\n                value,\n                handle,\n                indent=2,\n                ensure_ascii=False,\n                check_circular=False\n            )\n            handle.write(\"\\n\")\n            return\n        json.dump(\n            value,\n            handle,\n            separators=(\",\", \":\"),\n            ensure_ascii=False,\n            check_circular=False\n        )\n\n\ndef run_standalone(args: List[str]) -> str:\n    # CLI entry: load grouped JSON and write map-content.json.\n    base_dir = os.path.dirname(os.path.abspath(__file__))\n    spec_path = os.path.join(base_dir, \"map-description-classifications.json\")\n    spec = _load_json(spec_path)\n    input_path = args[0] if args else os.path.join(os.getcwd(), \"map-meta.json\")\n    grouped = _load_json(input_path)\n    output_path = os.path.join(os.path.dirname(input_path), \"map-content.json\")\n    write_map_content(grouped, spec, output_path, grouped)\n    return output_path\n\n\n__all__ = [\"build_intermediate\", \"render_from_intermediate\", \"write_map_content\", \"run_standalone\"]\n\n\nif __name__ == \"__main__\":\n    run_standalone(sys.argv[1:])\n"
  },
  {
    "path": "converter/map_desc/ways_clip.py",
    "content": "# Python 3.5\nfrom __future__ import division\n\nfrom typing import List, Optional, Tuple, TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from typing_extensions import TypedDict  # type: ignore[import-not-found]\nelse:  # pragma: no cover - blender python may not have typing_extensions\n    try:\n        from typing_extensions import TypedDict  # type: ignore[import-not-found]\n    except ImportError:\n        def TypedDict(name, fields, total=True):  # type: ignore[no-redef]\n            return dict\n\n\nBBox = TypedDict(\"BBox\", {\"minX\": float, \"minY\": float, \"maxX\": float, \"maxY\": float})\n\n\nPoint = Tuple[float, float]\nCoord = List[float]\nSegment = List[Coord]\n\n\ndef _coerce_point(coord: object) -> Optional[Point]:\n    if not isinstance(coord, list) or len(coord) < 2:\n        return None\n    x = coord[0]\n    y = coord[1]\n    if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):\n        return None\n    return float(x), float(y)\n\n\ndef _points_close(a: Coord, b: Coord, eps: float = 1e-9) -> bool:\n    return abs(a[0] - b[0]) <= eps and abs(a[1] - b[1]) <= eps\n\n\ndef _clip_segment(p0: Point, p1: Point, bbox: BBox) -> Optional[Tuple[Point, Point]]:\n    # Clip a line segment to the bbox using Liang–Barsky; return the visible segment or None.\n    min_x = bbox[\"minX\"]\n    min_y = bbox[\"minY\"]\n    max_x = bbox[\"maxX\"]\n    max_y = bbox[\"maxY\"]\n\n    dx = p1[0] - p0[0]\n    dy = p1[1] - p0[1]\n\n    p = (-dx, dx, -dy, dy)\n    q = (p0[0] - min_x, max_x - p0[0], p0[1] - min_y, max_y - p0[1])\n\n    u1 = 0.0\n    u2 = 1.0\n    for pi, qi in zip(p, q):\n        if pi == 0:\n            if qi < 0:\n                return None\n            continue\n        t = qi / pi\n        if pi < 0:\n            if t > u2:\n                return None\n            if t > u1:\n                u1 = t\n        else:\n            if t < u1:\n                return None\n            if t < u2:\n                u2 = t\n\n    if u1 > u2:\n        return None\n    c0 = (p0[0] + u1 * dx, p0[1] + u1 * dy)\n    c1 = (p0[0] + u2 * dx, p0[1] + u2 * dy)\n    if c0 == c1:\n        return None\n    return c0, c1\n\n\ndef clip_line_string(coords: List[Coord], bbox: BBox) -> List[Segment]:\n    # Clip a polyline against the bbox and return visible segments as lists of [x, y] points.\n    segments = []  # type: List[Segment]\n    current = []  # type: Segment\n    prev = None  # type: Optional[Point]\n\n    for coord in coords:\n        point = _coerce_point(coord)\n        if point is None:\n            if len(current) >= 2:\n                segments.append(current)\n            current = []\n            prev = None\n            continue\n\n        if prev is None:\n            prev = point\n            continue\n\n        clipped = _clip_segment(prev, point, bbox)\n        if clipped is None:\n            if len(current) >= 2:\n                segments.append(current)\n            current = []\n        else:\n            c0, c1 = clipped\n            c0_list = [c0[0], c0[1]]\n            c1_list = [c1[0], c1[1]]\n            if not current:\n                current = [c0_list, c1_list]\n            else:\n                if _points_close(current[-1], c0_list):\n                    if not _points_close(current[-1], c1_list):\n                        current.append(c1_list)\n                else:\n                    if len(current) >= 2:\n                        segments.append(current)\n                    current = [c0_list, c1_list]\n\n        prev = point\n\n    if len(current) >= 2:\n        segments.append(current)\n\n    return segments\n"
  },
  {
    "path": "converter/obj-to-tactile.py",
    "content": "# pyright: reportMissingImports=false\nfrom __future__ import print_function\nimport sys\nimport argparse\nimport os\nimport bpy\nimport mathutils\nimport bmesh\nimport json\nimport math\nimport time\n\nscript_dir = os.path.dirname(__file__)\nif script_dir not in sys.path:\n    sys.path.insert(0, script_dir)\nimport tactile_constants as tc\n\nperf_clock = getattr(time, 'perf_counter', time.time)\n\nsys.path.insert(1, \"%s/blender/2.78/python/lib/python3.5/svgwrite\" % (script_dir,))\n# These modules imported at the site of use\n\n\ndef do_cmdline():\n    parser = argparse.ArgumentParser(description='''Read OSM map meshes, modify to tactile map, and export as .stl''')\n    parser.add_argument('--min-x', metavar='FLOAT', type=float, help='minimum X bound')\n    parser.add_argument('--min-y', metavar='FLOAT', type=float, help='minimum Y bound')\n    parser.add_argument('--max-x', metavar='FLOAT', type=float, help='maximum X bound')\n    parser.add_argument('--max-y', metavar='FLOAT', type=float, help='maximum Y bound')\n    parser.add_argument('--no-stl-export', action='store_true', help='do not export to .stl file')\n    parser.add_argument('--scale', metavar='N', type=int, help=\"scale to export STL in, 4000 would mean one Blender unit (meter) = 0.25mm (STL file unit is normally mm)\")\n    parser.add_argument('--marker1', metavar='MARKER', help=\"first marker's position relative to top left corner\")\n    parser.add_argument('--diameter', metavar='METERS', type=int, help=\"larger of map area x and y diameter in meters\")\n    parser.add_argument('--size', metavar='METERS', type=float, help=\"print size in cm\")\n    parser.add_argument('--no-borders', action='store_true', help=\"don't draw borders around the edges\")\n    parser.add_argument('--export-wireframe-png', action='store_true', help=\"export orthographic top-view wireframe PNG\")\n    parser.add_argument('--base-path', help='base output path (without extension), defaults to first input path')\n    parser.add_argument('mesh_paths', metavar='PATHS', nargs='+', help='.obj/.ply files to use as input')\n    args = parser.parse_args(sys.argv[sys.argv.index(\"--\") + 1:])\n    return args\n\ndef print_verts(ob):\n    for v in ob.data.vertices:\n        print(ob.name, ob.matrix_world * mathutils.Vector(v.co))\n\ndef get_minimum_coordinate(ob):\n    min_x, min_y, min_z, _max_x, _max_y, _max_z = get_object_world_bounds(ob)\n    return (min_x, min_y, min_z)\n\n\ndef get_object_world_bounds(ob):\n    bbox_corners = [ob.matrix_world * mathutils.Vector(corner) for corner in ob.bound_box]\n    min_x = 1000000\n    min_y = 1000000\n    min_z = 1000000\n    max_x = -1000000\n    max_y = -1000000\n    max_z = -1000000\n    for corner in bbox_corners:\n        min_x = min(min_x, corner[0])\n        min_y = min(min_y, corner[1])\n        min_z = min(min_z, corner[2])\n        max_x = max(max_x, corner[0])\n        max_y = max(max_y, corner[1])\n        max_z = max(max_z, corner[2])\n    return (min_x, min_y, min_z, max_x, max_y, max_z)\n\ndef move_everything(move_by):\n    vector = mathutils.Vector(move_by)\n    for ob in bpy.context.scene.objects:\n        if ob.type == 'MESH':\n            ob.location += vector\n\ndef all_mesh_objects():\n    out = []\n    for ob in bpy.context.scene.objects:\n        if ob.type != 'MESH':\n            continue\n        if ob.name == 'map':\n            # Happens when there is nothing on the map\n            continue\n        out.append(ob)\n    return out\n\ndef rgb(r, g, b):\n    return 'rgb(%d, %d, %d)' % (round(r*2.55), round(g*2.55), round(b*2.55))\n\ndef add_polygons(dwg, g, ob):\n    mesh = ob.data\n    verts = mesh.vertices\n    for polygon in mesh.polygons:\n        points = []\n        for vert_index in polygon.vertices:\n            world_co = ob.matrix_world * verts[vert_index].co\n            points.append(('%.1f' % world_co[0], '%.1f' % world_co[1]))\n        g.add(dwg.polygon(points=points))\n\ndef add_svg_object(dwg, main_g, ob, color):\n    g = dwg.g(stroke=color, fill=color)\n    g['stroke-width'] = 0.3 # removes gaps between objects\n    main_g.add(g)\n\n    if ob.name.startswith('Road'):\n        g['stroke-width'] = 0.8 # Make roads a bit thicker so embosser draws them\n    add_polygons(dwg, g, ob)\n\ndef add_road_overlay_object(dwg, main_g, ob):\n    g = dwg.g(opacity=0.0, fill='red', stroke='blue')\n    g['stroke-width'] = 5.0\n    main_g.add(g)\n\n    add_polygons(dwg, g, ob)\n\ndef export_svg(base_path, args):\n    t = perf_clock()\n    min_x, min_y, max_x, max_y = (args.min_x, args.min_y, args.max_x, args.max_y)\n    one_cm_units = (max_y - min_y) / args.size\n\n    import svgwrite\n    dwg = svgwrite.Drawing(base_path + '.svg', profile = 'basic')\n    dwg['width']  = \"%.2f\" % (args.size) + 'cm'\n    dwg['height'] = \"%.2f\" % (args.size + 1) + 'cm'\n    dwg['viewBox'] = \"%f %f %f %f\" % (min_x, min_y - one_cm_units, max_x - min_x, max_y - min_y + one_cm_units)\n    dwg['shape-rendering'] = 'geometricPrecision'\n    dwg['stroke-linejoin'] = 'round' # greatly reduces protruding edges caused by non-zero stroke-width\n\n    # Group objects into different layers\n    objs = all_mesh_objects()\n    buildings = []\n    roads_car = []\n    roads_ped = []\n    rails = []\n    rivers = []\n    water_areas = []\n    for ob in objs:\n        try:\n            if ob.name.startswith('Road'):\n                if is_pedestrian(ob.name):\n                    roads_ped.append(ob)\n                else:\n                    roads_car.append(ob)\n            elif ob.name.startswith('Rail'):\n                rails.append(ob)\n            elif ob.name.startswith('Waterway') or ob.name.startswith('River'):\n                rivers.append(ob)\n            elif ob.name.startswith('Water') or ob.name.startswith('AreaFountain'):\n                water_areas.append(ob)\n            elif ob.name.startswith('Building'):\n                buildings.append(ob)\n            else:\n                print(\"UNHANDLED TYPE IN SVG CREATION: \" + ob.name)\n        except Exception as e:\n            print(\"SVG export failed {}: {}\".format(ob.name, str(e)))\n\n    # White background\n    dwg.add(dwg.rect(insert=(min_x - 5, min_y - 5 - one_cm_units), size=(max_x - min_x + 10, max_y - min_y + 10 + one_cm_units), fill=rgb(100, 100, 100)))\n\n    # A group for main content\n    clip_path = dwg.defs.add(dwg.clipPath(id='main_clip'))\n    clip_path.add(dwg.rect(insert=(min_x, min_y), size=(max_x - min_x, max_y - min_y)))\n    main_g = dwg.add(dwg.g(clip_path='url(#main_clip)'))\n\n    for ob in rails:\n        add_svg_object(dwg, main_g, ob, rgb(0, 50, 0))\n    for ob in rivers:\n        add_svg_object(dwg, main_g, ob, rgb(20, 20, 100))\n    for ob in water_areas:\n        add_svg_object(dwg, main_g, ob, rgb(20, 20, 100))\n    for ob in roads_car:\n        add_svg_object(dwg, main_g, ob, rgb(70, 0, 0))\n    for ob in roads_ped:\n        add_svg_object(dwg, main_g, ob, rgb(0, 0, 0))\n    for ob in buildings:\n        add_svg_object(dwg, main_g, ob, rgb(80, 20, 100))\n\n    # Add overlays\n    for ob in objs:\n        try:\n            if ob.name.startswith('Road') or ob.name.startswith('Rail') or ob.name.startswith('Waterway') or ob.name.startswith('River'):\n                add_road_overlay_object(dwg, main_g, ob)\n        except Exception as e:\n            print(\"SVG export failed2 {}: {}\".format(ob.name, str(e)))\n\n    # Add north marker to top-right corner\n    g = dwg.g(fill='black')\n    g['stroke-width'] = 0\n    g.set_desc('North-east corner')\n    g.add(dwg.polygon(points=[\n        ('%.2f' % (max_x),                      '%.2f' % (min_y - one_cm_units*0.3)),\n        ('%.2f' % (max_x - one_cm_units*0.7),   '%.2f' % (min_y - one_cm_units*0.3)),\n        ('%.2f' % (max_x - one_cm_units*0.7/2), '%.2f' % (min_y - one_cm_units)),\n    ]))\n    dwg.add(g)\n\n    dwg.save()\n    print(\"creating SVG took \" + (str(perf_clock() - t)))\n\ndef _export_stl(stl_path, scale):\n    print(\"creating {stl}...\".format(stl=stl_path))\n    bpy.ops.export_mesh.stl(filepath=stl_path, check_existing=False, \\\n                            axis_forward='Y', axis_up='Z', global_scale=(1000 / scale))\n\ndef export_stl(base_path, scale):\n    bpy.ops.object.select_all(action='SELECT')\n    _export_stl(base_path + '.stl', scale)\n\ndef export_stl_separate(base_path, scale):\n    bpy.ops.object.select_all(action='DESELECT')\n    for ob in bpy.context.scene.objects:\n        ob.select = ob.name.endswith('Roads') or ob.name.endswith('RoadAreas') or ob.name.endswith('Rails')\n    _export_stl(base_path + '-ways.stl', scale)\n    bpy.ops.object.select_all(action='INVERT')\n    _export_stl(base_path + '-rest.stl', scale)\n\ndef export_blend_file(base_path):\n    blend_path = base_path + '.blend'\n    bpy.ops.object.select_all(action='SELECT') # it's handy to have everything selected initially\n    bpy.ops.wm.save_as_mainfile(filepath=blend_path, check_existing=False, compress=True)\n\n\ndef export_wireframe_png(base_path, output_name, min_x, min_y, max_x, max_y):\n    t = time.time()\n    wireframe_path = base_path + '-' + output_name + '.png'\n    width = max_x - min_x\n    height = max_y - min_y\n    ortho_scale = max(width, height) * tc.WIREFRAME_CAMERA_PADDING\n    if ortho_scale < tc.WIREFRAME_CAMERA_MIN_SCALE:\n        ortho_scale = tc.WIREFRAME_CAMERA_MIN_SCALE\n    center_x = (min_x + max_x) / 2\n    center_y = (min_y + max_y) / 2\n    source_meshes = all_mesh_objects()\n    max_z = 0.0\n    for source in source_meshes:\n        (_obj_min_x, _obj_min_y, _obj_min_z, _obj_max_x, _obj_max_y, obj_max_z) = get_object_world_bounds(source)\n        max_z = max(max_z, obj_max_z)\n    camera_height = max_z + max(ortho_scale, 10.0)\n\n    scene = bpy.context.scene\n    scene.render.engine = 'BLENDER_RENDER'\n    scene.render.alpha_mode = 'TRANSPARENT'\n    scene.render.image_settings.file_format = 'PNG'\n    scene.render.image_settings.color_mode = 'RGBA'\n    scene.render.resolution_x = tc.WIREFRAME_RENDER_RESOLUTION\n    scene.render.resolution_y = tc.WIREFRAME_RENDER_RESOLUTION\n    scene.render.resolution_percentage = 100\n    scene.render.filepath = wireframe_path\n    if scene.world is not None:\n        scene.world.horizon_color = (0.0, 0.0, 0.0)\n\n    # Best effort: mirror viewport intent on Blender versions that expose overlay controls.\n    for screen in bpy.data.screens:\n        for area in screen.areas:\n            for space in area.spaces:\n                overlay = getattr(space, 'overlay', None)\n                if overlay is not None and hasattr(overlay, 'show_wireframes'):\n                    overlay.show_wireframes = True\n                shading = getattr(space, 'shading', None)\n                if shading is not None and hasattr(shading, 'type'):\n                    shading.type = 'SOLID'\n\n    bpy.ops.object.camera_add()\n    camera = bpy.context.active_object\n    camera.location = (center_x, center_y, camera_height)\n    camera.rotation_euler = (0.0, 0.0, 0.0)\n    camera.data.type = 'ORTHO'\n    camera.data.ortho_scale = ortho_scale\n    camera.data.clip_start = 0.01\n    camera.data.clip_end = max(1000.0, camera_height + ortho_scale * 4)\n    scene.camera = camera\n\n    solid_material = bpy.data.materials.new('SolidRenderMaterial')\n    solid_material.type = 'SURFACE'\n    solid_material.use_shadeless = True\n    solid_material.diffuse_color = (0.72, 0.72, 0.72)\n    solid_material.specular_intensity = 0.0\n\n    wire_material = bpy.data.materials.new('WireframeRenderMaterial')\n    wire_material.type = 'WIRE'\n    wire_material.use_shadeless = True\n    wire_material.diffuse_color = (0.0, 0.0, 0.0)\n    wire_overlay_objects = []\n    solid_overlay_objects = []\n    hidden_originals = []\n    for ob in source_meshes:\n        hidden_originals.append((ob, ob.hide_render))\n        ob.hide_render = True\n\n        solid_ob = ob.copy()\n        solid_ob.data = ob.data.copy()\n        scene.objects.link(solid_ob)\n        solid_ob.hide_render = False\n        mesh = solid_ob.data\n        if len(mesh.materials) == 0:\n            mesh.materials.append(solid_material)\n        else:\n            for material_index in range(len(mesh.materials)):\n                mesh.materials[material_index] = solid_material\n        solid_overlay_objects.append(solid_ob)\n\n        wire_ob = solid_ob.copy()\n        wire_ob.data = solid_ob.data.copy()\n        scene.objects.link(wire_ob)\n        wire_ob.hide_render = False\n        wire_mesh = wire_ob.data\n        if len(wire_mesh.materials) == 0:\n            wire_mesh.materials.append(wire_material)\n        else:\n            for material_index in range(len(wire_mesh.materials)):\n                wire_mesh.materials[material_index] = wire_material\n        wire_ob.location[2] += 0.0001\n        wire_overlay_objects.append(wire_ob)\n\n    bpy.ops.render.render(write_still=True)\n\n    bpy.ops.object.select_all(action='DESELECT')\n    for ob in solid_overlay_objects:\n        ob.select = True\n    for ob in wire_overlay_objects:\n        ob.select = True\n    camera.select = True\n    bpy.context.scene.objects.active = camera\n    bpy.ops.object.delete()\n    for (ob, old_hide_render) in hidden_originals:\n        ob.hide_render = old_hide_render\n    print(\"creating wireframe PNG took %.2f\" % (time.time() - t))\n\ndef create_cube(min_x, min_y, max_x, max_y, min_z, max_z):\n    x1 = min(min_x, max_x)\n    x2 = max(min_x, max_x)\n    y1 = min(min_y, max_y)\n    y2 = max(min_y, max_y)\n    z1 = min(min_z, max_z)\n    z2 = max(min_z, max_z)\n\n    bpy.ops.mesh.primitive_cube_add()\n    cube = bpy.context.active_object\n    bpy.ops.object.mode_set(mode = 'EDIT')\n    bpy.ops.mesh.select_all(action='SELECT')\n    bpy.ops.mesh.normals_make_consistent()\n    bpy.ops.object.mode_set(mode = 'OBJECT')\n    cube.location = [ (x1 + x2) / 2, (y1 + y2) / 2, (z1 + z2) / 2 ]\n    cube.scale = [ (x2 - x1) / 2, (y2 - y1) / 2, (z2 - z1) / 2 ]\n    bpy.context.scene.update() # flush changes to location and scale\n    return cube\n\ndef add_borders(min_x, min_y, max_x, max_y, width, bottom, height, corner_height):\n    borders = []\n    borders.append(create_cube(min_x, min_y, min_x + width, max_y, bottom, height))\n    borders.append(create_cube(max_x, min_y, max_x - width, max_y, bottom, height))\n    borders.append(create_cube(min_x + width*0.99, min_y, max_x - width*0.99, min_y + width, bottom, height))\n    borders.append(create_cube(min_x + width*0.99, max_y, max_x - width*0.99, max_y - width, bottom, height))\n    join_objects(borders, 'Borders')\n\n    # Marker for north-east corner\n    create_cube(max_x - width*0.99, max_y - width*0.99, max_x - width*2.7, max_y - width*2.7, 0, height).name = 'CornerInside'\n    create_cube(max_x, max_y, max_x - width*3, max_y - width*3, height*0.99, corner_height).name = 'CornerTop'\n\ndef create_bounds(min_x, min_y, max_x, max_y, scale, no_borders):\n    mm_to_units = scale / 1000\n    if not no_borders:\n        add_borders(min_x, min_y, max_x, max_y, tc.BORDER_WIDTH_MM * mm_to_units, \\\n                    0, tc.BORDER_HEIGHT_MM * mm_to_units, (tc.BUILDING_HEIGHT_MM + 1) * mm_to_units)\n    base_height = tc.BASE_HEIGHT_MM * mm_to_units\n    overlap = tc.BASE_OVERLAP_MM * mm_to_units # move cube this much up so that it overlaps enough with objects they merge into one object\n    base_cube = create_cube(min_x, min_y, max_x, max_y, -base_height + overlap, overlap)\n    base_cube.name = 'Base'\n    return base_cube\n\ndef add_marker1(args, scale):\n    min_x, min_y, max_x, max_y = (args.min_x, args.min_y, args.max_x, args.max_y)\n    if args.marker1 == 'center':\n        marker_x, marker_y = (0.5, 0.5)\n    else:\n        coords = json.loads(args.marker1)\n        marker_x = float(coords['x'])\n        marker_y = float(coords['y'])\n        \n    mm_to_units = scale / 1000\n    radius = tc.MARKER_RADIUS_MM * mm_to_units\n    height = tc.MARKER_HEIGHT_MM * mm_to_units\n    # If the cone has sharp top, three.js won't render it remotely properly, and it'll 3D print poorly too\n    bpy.ops.mesh.primitive_cone_add(vertices = 16, radius1 = radius, radius2 = radius / 8, depth = height, \\\n        location = [ min_x + (max_x - min_x) * marker_x, min_y + (max_y - min_y) * marker_y, height / 2 ])\n    bpy.context.active_object.name = 'SelectedAddress'\n\ndef remove_everything():\n    bpy.ops.object.select_all(action='SELECT')\n    bpy.ops.object.delete()\n\ndef mesh_name_for_path(mesh_path):\n    basename = os.path.basename(mesh_path).lower()\n    if 'road-areas-ped' in basename:\n        return 'RoadAreaGroup::pedestrian'\n    if 'road-areas-car' in basename:\n        return 'RoadAreaGroup'\n    if 'roads-ped' in basename:\n        return 'RoadGroup::pedestrian'\n    if 'roads-car' in basename:\n        return 'RoadGroup'\n    if 'rails' in basename:\n        return 'RailGroup'\n    if 'buildings' in basename:\n        return 'BuildingGroup'\n    if 'waterways' in basename:\n        return 'WaterwayGroup'\n    if 'water-areas' in basename:\n        return 'WaterGroup'\n    if 'other' in basename:\n        return 'OtherGroup'\n    return 'MeshGroup'\n\n\ndef imported_meshes_since(old_names):\n    out = []\n    for ob in bpy.context.scene.objects:\n        if ob.type != 'MESH':\n            continue\n        if ob.name in old_names:\n            continue\n        out.append(ob)\n    return out\n\n\ndef import_mesh_file(mesh_path):\n    t = perf_clock()\n    old_names = set((ob.name for ob in bpy.context.scene.objects if ob.type == 'MESH'))\n    extension = os.path.splitext(mesh_path)[1].lower()\n    if extension == '.obj':\n        bpy.ops.import_scene.obj(filepath=mesh_path, axis_forward='-Z', axis_up='Y')\n    elif extension == '.ply':\n        if not hasattr(bpy.ops.import_mesh, 'ply'):\n            bpy.ops.wm.addon_enable(module='io_mesh_ply')\n        bpy.ops.import_mesh.ply(filepath=mesh_path)\n    else:\n        raise Exception(\"unsupported mesh extension: \" + extension)\n\n    imported = imported_meshes_since(old_names)\n    target_name = mesh_name_for_path(mesh_path)\n    for i, ob in enumerate(imported):\n        if i == 0:\n            ob.name = target_name\n        else:\n            ob.name = target_name + ('_%03d' % i)\n\n# Extrude floor to a flat-roofed building\ndef extrude_building(ob, height):\n    bpy.ops.object.mode_set(mode = 'EDIT')\n    bpy.ops.mesh.select_all(action='SELECT')\n    bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={ \"value\": (0.0, 0.0, height) })\n    bpy.ops.mesh.select_all(action='SELECT')\n    bpy.ops.mesh.normals_make_consistent()\n    bpy.ops.object.mode_set(mode = 'OBJECT')\n\ndef join_selected(name):\n    combined = bpy.context.selected_objects[0]\n    bpy.context.scene.objects.active = combined\n    combined.name = name\n    bpy.ops.object.join()\n    return combined\n\ndef join_objects(objects, name):\n    if len(objects) == 0:\n        return None\n    if len(objects) == 1:\n        bpy.ops.object.select_all(action='DESELECT')\n        objects[0].select = True\n        bpy.context.scene.objects.active = objects[0]\n        objects[0].name = name\n        return objects[0]\n    bpy.ops.object.select_all(action='DESELECT')\n    for ob in objects:\n        ob.select = True\n    return join_selected(name)\n\ndef raise_ob(objs, height):\n    bpy.ops.object.select_all(action='DESELECT')\n    bpy.context.scene.objects.active = objs\n    bpy.ops.object.mode_set(mode = 'EDIT')\n    bpy.ops.mesh.select_all(action='SELECT')\n    bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={ \"value\": (0.0, 0.0, height) })\n    bpy.ops.object.mode_set(mode = 'OBJECT')\n\ndef water_remesh_and_extrude(object, extrude_height):\n    # Extrude just enough that remeshing works\n    bpy.context.scene.objects.active = object\n    bpy.ops.object.mode_set(mode = 'EDIT')\n    bpy.ops.mesh.select_all(action='SELECT')\n    bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={ \"value\": (0.0, 0.0, extrude_height) })\n    bpy.ops.mesh.normals_make_consistent()\n    bpy.ops.object.mode_set(mode = 'OBJECT')\n\n    # Remesh\n    max_dimension = max(object.dimensions[0], object.dimensions[1])\n    depth = min(max(math.log2(max_dimension) - 1, 2), 8) # Max vertex distance == 2m => max dimension 128 == remesh depth 6 (or so)\n    modifier = object.modifiers.new('Modifier', 'REMESH')\n    modifier.octree_depth = math.ceil(depth)\n    modifier.use_remove_disconnected = False\n    bpy.ops.object.modifier_apply(apply_as='DATA', modifier=modifier.name)\n\ndef water_wave_pattern(object, depth, scale):\n    extrude_height = 1.0\n    water_remesh_and_extrude(object, extrude_height)\n\n    # Start creating wave pattern\n    bpy.ops.object.mode_set(mode = 'EDIT')\n    bpy.ops.mesh.select_all(action='SELECT')\n    bm = bmesh.from_edit_mesh(object.data)\n    bm.verts.ensure_lookup_table()\n\n    # Record x,y positions of edge verts (verts of non-horizontal edges)\n    edge_verts = {}\n    for edge in bm.edges:\n        verts = edge.verts\n        if abs(verts[0].co.z - verts[1].co.z) > extrude_height / 2:\n            edge_verts[str(verts[0].co.x) + ',' + str(verts[0].co.y)] = True\n\n    # Set top verts' z positions. Bottom verts are at 0.\n    density = math.pi * 2 / tc.WATER_WAVE_DISTANCE_MM / (scale/1000) \n    for v in bm.verts:\n        if v.co.z > extrude_height / 2:\n            min_height = -10000\n            if str(v.co.x) + ',' + str(v.co.y) in edge_verts:\n                min_height = depth / 4\n            v.co.z = max(min_height, (math.sin(v.co.x * density) + math.sin(v.co.y * density)) * depth / 4 + depth / 2)\n        else:\n            v.co.z = 0\n    bmesh.update_edit_mesh(object.data, tessface=False, destructive=False)\n\n    bpy.ops.object.mode_set(mode = 'OBJECT')\n\ndef is_pedestrian(road_name):\n    return road_name.endswith('::pedestrian')\n\n## Disable stdout buffering\n#class Unbuffered(object):\n#   def __init__(self, stream):\n#       self.stream = stream\n#   def write(self, data):\n#       self.stream.write(data)\n#       self.stream.flush()\n#   def __getattr__(self, attr):\n#       return getattr(self.stream, attr)\n#\n#sys.stdout = Unbuffered(sys.stdout)\n\n\n# Join edges that seem to form two ends of the same logical road or railway\ndef join_matching_edges(ob, min_x, min_y, max_x, max_y):\n    lt = 0.2  # length difference + -\n    dt = 0.15  # max distance \n    at = 0.5  # max sin(angle)  (30°)\n    \n    bpy.context.scene.objects.active = ob\n    bpy.ops.object.mode_set(mode = 'EDIT')\n    bpy.ops.mesh.select_all(action='DESELECT')\n    from math import sin\n    bm = bmesh.from_edit_mesh( bpy.context.object.data )\n    bm.edges.ensure_lookup_table()\n    \n    center   = lambda e : ( e.verts[0].co + e.verts[1].co ) / 2\n    length   = lambda e : ( e.verts[0].co - e.verts[1].co ).length\n    dist     = lambda v1, v2: (  v2 -  v1 ).length\n    sinAngle = lambda e1, e2: abs(sin((e1.verts[1].co - e1.verts[0].co).angle(e2.verts[1].co - e2.verts[0].co)))\n\n    def point_between_edge_neighbor_verts(e):\n        # Return middle of the verts adjacent to the edge\n        verts = []\n        for v in e.verts:\n            for linked_e in v.link_edges:\n                verts.extend((vv for vv in linked_e.verts if vv != e.verts[0] and vv != e.verts[1]))\n        if len(verts) != 2:\n            #print(\"edge has non-2 adjacent verts: \" + str(len(verts)))\n            return None\n        return ((verts[0].co[0] + verts[1].co[0]) / 2, \\\n                (verts[0].co[1] + verts[1].co[1]) / 2, \\\n                (verts[0].co[2] + verts[1].co[2]) / 2)\n    \n    class CEdge:\n        def __init__(self, e, into_edge):\n            self.e = e\n            self.center = center(e)\n            self.length = length(e)\n            self.into_edge = into_edge\n            self.welded = False\n    \n    # Lengthen an edge that is supposedly at the end of a road, in an attempt to make roads'\n    # widths consistent, instead of being the more narrow the greater the angle of their end edge.\n    radians_90degrees = math.pi / 2\n    def lengthen_edges(ce1, ce2):\n        for ce in (ce1, ce2):\n            verts = ce.e.verts\n            edge_v = mathutils.Vector(verts[0].co) - mathutils.Vector(verts[1].co)\n            angle = ce.into_edge.angle(edge_v)\n            if abs(angle - radians_90degrees) > radians_90degrees / 9:\n                multiplier = 1 / math.sin(angle)\n                if multiplier > 3:\n                    continue\n                else:\n                    verts[0].co = ce.center + (verts[0].co - ce.center) * multiplier\n                    verts[1].co = ce.center + (verts[1].co - ce.center) * multiplier\n    \n    def filter_edges(edges):\n        out = []\n        for e in edges:\n            if len(e.link_faces) != 1:\n                continue\n            # Because roads are clipped at the edges, funny coincidences can happen, so ignore those edges\n            c = center(e)\n            if abs(c[0] - min_x) < 0.1 or abs(c[0] - max_x) < 0.1 or abs(c[2] - min_y) < 0.1 or abs(c[2] - max_y) < 0.1:\n                continue\n            point_between_edges = point_between_edge_neighbor_verts(e)\n            if not point_between_edges:\n                continue\n            vector_into_edge_face = center(e) - mathutils.Vector(point_between_edges)\n            if vector_into_edge_face.length == 0:\n                continue\n            out.append(CEdge(e, vector_into_edge_face / vector_into_edge_face.length))\n        return out\n    candidate_edges = filter_edges(bm.edges)\n\n    # Index edges into search tree\n    edge_index_to_ce = {} # enable finding CEdge by edge\n    kd = mathutils.kdtree.KDTree(len(candidate_edges))\n    for i, ce in enumerate(candidate_edges):\n        kd.insert(ce.center, i)\n        edge_index_to_ce[ce.e.index] = ce\n    kd.balance()\n\n    def mark_all_t_junction_edges_welded(cedge):\n        face_edges = cedge.e.link_faces[0].edges\n        if len(face_edges) == 6:\n            # Faces with 6 edges are probably T junctions. If we allow multiple roads\n            # to connect to them, we often get a road that intersects itself (because X junctions are disabled in OSM2World)\n            for fe in face_edges:\n                fe_ce = edge_index_to_ce.get(fe.index, None)\n                if fe_ce:\n                    fe_ce.welded = True\n\n    to_weld = {}\n    for i, ce in enumerate(candidate_edges[:-1]):\n        if ce.welded:\n            continue\n        ce.welded = True\n        lmin = ce.length - lt\n        lmax = ce.length + lt\n        matches = []\n        for (_co, oe_index, _dist) in kd.find_range(ce.center, dt):\n            oe = candidate_edges[oe_index]\n            if not oe.welded and lmin < oe.length < lmax and sinAngle(ce.e, oe.e) < at:\n                turn_angle = ce.into_edge.angle(-oe.into_edge)\n                if turn_angle > math.pi * 0.6: # pi * 0.5 is 90%\n                    #print(\"not merging edges (%s, %s) pointing to opposite directions, angle is %f\" % (ce.e, oe.e, turn_angle))\n                    continue\n                matches.append(oe)\n                oe.welded = True\n        \n        if len(matches) == 1:\n            # Join nothing where >2 ways meet, else all roads in the scene may become joined and intersect itself\n            ev1, ev2 = ce.e.verts[:]\n            oev1, oev2 = matches[0].e.verts[:]\n            if dist(ev1.co, oev1.co) < dist(ev1.co, oev2.co) :\n                if ev1 != oev1: to_weld[ev1] = oev1\n                if ev2 != oev2: to_weld[ev2] = oev2\n            else :\n                if ev1 != oev2: to_weld[ev1] = oev2\n                if ev2 != oev1: to_weld[ev2] = oev1\n                # TODO: move welded verts to locations between the originals?\n            lengthen_edges(ce, matches[0])\n            mark_all_t_junction_edges_welded(ce)\n            mark_all_t_junction_edges_welded(matches[0])\n            \n    print(\"%s: melding %d out of %d edges\" % (ob.name, len(to_weld) / 2, len(bm.edges)))\n    bmesh.ops.weld_verts(bm, targetmap = to_weld)\n    bmesh.update_edit_mesh(bpy.context.object.data ,True)\n    bpy.ops.object.mode_set(mode = 'OBJECT')\n\n# Decimating gets rid of useless and harmful lane edges, as well as changing\n# tris to n-gons (important to find edge's \"direction\")\ndef decimate(ob):\n    # Decimating gets rid of useless lanes\n    bpy.context.scene.objects.active = ob\n    modifier = ob.modifiers.new('Modifier', 'DECIMATE')\n    modifier.decimate_type = 'DISSOLVE'\n    bpy.ops.object.modifier_apply(apply_as='DATA', modifier=modifier.name)\n\n# Fatten slightly to cause overlap and avoid faces too close to each other\ndef fatten(ob):\n    bpy.context.scene.objects.active = ob\n    bpy.ops.object.mode_set(mode = 'EDIT')\n    bpy.ops.mesh.select_all(action='SELECT')\n    bpy.ops.transform.shrink_fatten(value=-0.05) # less than this and programs start to \"remove double vertices\"\n    bpy.ops.object.mode_set(mode = 'OBJECT')\n\ndef do_ways(ways, height, min_x, min_y, max_x, max_y):\n    if ways == None:\n        return\n    t = perf_clock()\n    decimate(ways)\n    join_matching_edges(ways, min_x, min_y, max_x, max_y)\n    raise_ob(ways, height)\n    fatten(ways)\n    print(\"processing %s took %.2f\" % (ways.name, perf_clock() - t))\n\ndef do_road_areas(roads, height):\n    if roads == None:\n        return\n    t = perf_clock()\n    decimate(roads)\n    raise_ob(roads, height)\n    fatten(roads)\n    #print(\"processing %s took %.2f\" % (roads.name, perf_clock() - t))\n\ndef process_objects(min_x, min_y, max_x, max_y, scale, no_borders):\n    t = perf_clock()\n    mm_to_units = scale / 1000\n    if not no_borders:\n        space = (tc.BORDER_WIDTH_MM - tc.BORDER_HORIZONTAL_OVERLAP_MM) * mm_to_units \n        min_x = min_x + space\n        min_y = min_y + space\n        max_x = max_x - space\n        max_y = max_y - space\n\n    # First find out everything that we can join together into combined objects and do join,\n    # because CPU usage is dominated by each Blender operation iterating through every object in the scene.\n    roads_car = []\n    roads_ped = []\n    road_areas_car = []\n    road_areas_ped = []\n    buildings = []\n    rails = []\n    waterways = []\n    water_areas = []\n    deleteables = []\n    for ob in all_mesh_objects():\n        if ob.name.startswith('BuildingEntrance'):\n            deleteables.append(ob)\n        elif ob.name.startswith('Building'):\n            buildings.append(ob)\n        elif ob.name.startswith('Road'):\n            if is_pedestrian(ob.name):\n                if ob.name.startswith('RoadArea'):\n                    road_areas_ped.append(ob)\n                else:\n                    roads_ped.append(ob)\n            else:\n                if ob.name.startswith('RoadArea'):\n                    road_areas_car.append(ob)\n                else:\n                    roads_car.append(ob)\n        elif ob.name.startswith('Rail'):\n            rails.append(ob)\n        elif ob.name.startswith('Waterway') or ob.name.startswith('River'):\n            waterways.append(ob)\n        elif ob.name.startswith('Water') or ob.name.startswith('AreaFountain'):\n            water_areas.append(ob)\n        else:\n            print(\"UNHANDLED OBJECT TYPE: \" + ob.name)\n    print(\"initial steps took %.2f\" % (perf_clock() - t))\n\n    # Delete\n    t = perf_clock()\n    if len(deleteables) > 0:\n        bpy.ops.object.select_all(action='DESELECT')\n        for ob in deleteables:\n            ob.select = True\n        bpy.ops.object.delete()\n        #print(\"deleting %d objects took %.2f\" % (len(deleteables), perf_clock() - t))\n\n    # Pre-join stuff for performance\n    joined_roads_car = join_objects(roads_car, 'CarRoads')\n    joined_roads_ped = join_objects(roads_ped, 'PedestrianRoads')\n    joined_road_areas_car = join_objects(road_areas_car, 'CarRoadAreas')\n    joined_road_areas_ped = join_objects(road_areas_ped, 'PedestrianRoadAreas')\n    joined_rails = join_objects(rails, 'Rails')\n    joined_buildings = join_objects(buildings, 'Buildings')\n    \n    # Buildings\n    if joined_buildings:\n        t = perf_clock()\n        extrude_building(joined_buildings, tc.BUILDING_HEIGHT_MM * mm_to_units)\n        fatten(joined_buildings)\n        print(\"processing %d buildings took %.2f\" % (len(buildings), perf_clock() - t))\n\n    # Waters\n    t = perf_clock()\n    if len(waterways) > 0:\n        joined_waterways = join_objects(waterways, 'JoinedWaterways')\n        raise_ob(joined_waterways, tc.WATERWAY_DEPTH_MM * mm_to_units)\n    if len(water_areas):\n        for water in water_areas:\n            water_wave_pattern(water, tc.WATER_AREA_DEPTH_MM * mm_to_units, scale)\n        join_objects(water_areas, 'WaterAreas')\n    print(\"processing waters took %.2f\" % (perf_clock() - t))\n\n    # Rails\n    if joined_rails != None:\n        do_ways(joined_rails, tc.ROAD_HEIGHT_CAR_MM * mm_to_units * 0.99, min_x, min_y, max_x, max_y) # 0.99 to avoid faces in the same coordinates with roads\n\n    # Roads\n    do_road_areas(joined_road_areas_car, tc.ROAD_HEIGHT_CAR_MM * mm_to_units)\n    do_road_areas(joined_road_areas_ped, tc.ROAD_HEIGHT_PEDESTRIAN_MM * mm_to_units)\n    do_ways(joined_roads_car, tc.ROAD_HEIGHT_CAR_MM * mm_to_units, min_x, min_y, max_x, max_y)\n    do_ways(joined_roads_ped, tc.ROAD_HEIGHT_PEDESTRIAN_MM * mm_to_units, min_x, min_y, max_x, max_y)\n\ndef make_tactile_map(args):\n    t = perf_clock()\n    min_x, min_y, max_x, max_y = (args.min_x, args.min_y, args.max_x, args.max_y)\n\n    process_objects(min_x, min_y, max_x, max_y, args.scale, args.no_borders)\n    print(\"process_objects() took \" + (str(perf_clock() - t)))\n\n    # Create the support cube and borders\n    base_cube = create_bounds(min_x, min_y, max_x, max_y, args.scale, args.no_borders)\n\n    # Add marker(s)\n    if args.marker1 != None:\n        add_marker1(args, args.scale)\n\n    return base_cube\n\ndef main():\n    args = do_cmdline()\n    remove_everything()\n\n    for mesh_path in args.mesh_paths:\n        import_mesh_file(mesh_path)\n\n    if args.base_path:\n        base_path = args.base_path\n    else:\n        base_path = os.path.splitext(args.mesh_paths[0])[0]\n    if args.export_wireframe_png:\n        export_wireframe_png(base_path, 'wireframe-flat', args.min_x, args.min_y, args.max_x, args.max_y)\n    export_svg(base_path, args)\n    base_cube = make_tactile_map(args)\n    move_everything([-c for c in get_minimum_coordinate(base_cube)])\n    if not args.no_stl_export:\n        export_stl(base_path, args.scale)\n        export_stl_separate(base_path, args.scale)\n        export_blend_file(base_path)\n    if args.export_wireframe_png:\n        final_min_x, final_min_y, _final_min_z, final_max_x, final_max_y, _final_max_z = get_object_world_bounds(base_cube)\n        export_wireframe_png(base_path, 'wireframe', final_min_x, final_min_y, final_max_x, final_max_y)\n    bpy.ops.object.select_all(action='SELECT') # it's handy to have everything selected when getting into UI\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "converter/osm-to-tactile.py",
    "content": "#!/usr/bin/python3 -u\n\nimport argparse\nimport os\nimport sys\nimport subprocess\nimport json\n\nscript_dir = os.path.dirname(os.path.realpath(__file__))\nif script_dir not in sys.path:\n    sys.path.insert(0, script_dir)\nfrom tactile_constants import BORDER_WIDTH_MM, BORDER_HORIZONTAL_OVERLAP_MM\nfrom telemetry import TelemetryLogger\n\n\ndef parse_env_bool(name):\n    raw = os.environ.get(name)\n    if raw is None:\n        return None\n    normalized = raw.strip().lower()\n    if normalized in (\"1\", \"true\", \"yes\", \"on\"):\n        return True\n    if normalized in (\"0\", \"false\", \"no\", \"off\"):\n        return False\n    return None\n\n\ndef pretty_json_enabled():\n    forced = parse_env_bool('TOUCH_MAPPER_PRETTY_JSON')\n    if forced is not None:\n        return forced\n    return False\n\n\ndef write_json_file(path, value, pretty_json):\n    with open(path, 'w') as handle:\n        if pretty_json:\n            json.dump(value, handle, indent=2, ensure_ascii=False)\n            handle.write(\"\\n\")\n            return\n        json.dump(value, handle, separators=(',', ':'), ensure_ascii=False)\n\n\ndef compute_clip_bounds(boundary, scale, no_borders):\n    clip_min_x = boundary['minX']\n    clip_min_y = boundary['minY']\n    clip_max_x = boundary['maxX']\n    clip_max_y = boundary['maxY']\n    if not no_borders:\n        mm_to_units = scale / 1000.0\n        space = (BORDER_WIDTH_MM - BORDER_HORIZONTAL_OVERLAP_MM) * mm_to_units\n        clip_min_x += space\n        clip_min_y += space\n        clip_max_x -= space\n        clip_max_y -= space\n    if clip_min_x >= clip_max_x or clip_min_y >= clip_max_y:\n        raise Exception(\"invalid clip bounds after border inset\")\n    return {\n        'minX': clip_min_x,\n        'minY': clip_min_y,\n        'maxX': clip_max_x,\n        'maxY': clip_max_y,\n    }\n\ndef do_cmdline():\n    parser = argparse.ArgumentParser(description='''Convert .osm file into a tactile map. Writes one or more .stl files in the input file's directory.''')\n    parser.add_argument('input', metavar='OSM_FILE', help='input file path')\n    parser.add_argument('--foreground', action='store_true', help=\"open Blender UI, and don't perform STL export\")\n    parser.add_argument('--scale', metavar='N', type=int, default=3100, help=\"scale to print in, default 1 : 3100\")\n    parser.add_argument('--marker1', metavar='MARKER', help=\"first marker's position relative to top left corner\")\n    parser.add_argument('--diameter', metavar='METERS', type=int, required=True, help=\"larger of map area x and y diameter in meters\")\n    parser.add_argument('--size', metavar='CM', type=float, required=True, help=\"print size in cm\")\n    parser.add_argument('--no-borders', action='store_true', help=\"don't draw borders around the edges\")\n    parser.add_argument('--exclude-buildings', action='store_true', help=\"don't include buildings\")\n    args = parser.parse_args()\n    return args\n\ndef _parse_int_env(name, fallback):\n    raw = os.environ.get(name)\n    if raw is None:\n        return fallback\n    try:\n        return int(raw)\n    except Exception:\n        return fallback\n\n\ndef run_osm2world(input_path, output_path, scale, exclude_buildings, telemetry):\n    # Code below creates stage \"OSM2World raw meta\" data.\n    osm2world_path = os.path.join(script_dir, 'OSM2World', 'build', 'OSM2World.jar')\n    #print(osm2world_path + \" \" + input_path + \" \" + output_path)\n    cmd = [\n        'java', '-Xmx1G',\n        '-jar', osm2world_path,\n        '-i', input_path,\n        '-o', output_path]\n    output_basename = os.path.splitext(os.path.basename(output_path))[0]\n    osm2world_log_path = os.path.join(\n        os.path.dirname(output_path),\n        output_basename + '-osm2world.log'\n    )\n    run_result = telemetry.run_subprocess(\n        cmd,\n        env={\n            'TOUCH_MAPPER_SCALE': str(scale),\n            'TOUCH_MAPPER_EXTRUDER_WIDTH': '0.5',\n            'TOUCH_MAPPER_EXCLUDE_BUILDINGS': ('true' if exclude_buildings else 'false')\n        },\n        output_log_path=osm2world_log_path,\n        depth_offset=0\n    )\n\n    meta_path = os.path.join(os.path.dirname(output_path), 'map-meta-raw.json')\n    if not os.path.exists(meta_path):\n        raise Exception(\"Couldn't find map-meta-raw.json from OSM2World output\")\n    with open(meta_path, 'r') as f:\n        meta = json.load(f)\n    write_json_file(meta_path, meta, pretty_json_enabled())\n\n    return meta, run_result.get('maxRssKiB')\n\ndef run_clip_2d(obj_path, clip_bounds, telemetry):\n    out_dir = os.path.dirname(obj_path)\n    clip_report_path = os.path.join(out_dir, 'map-clip-report.json')\n    clip_cmd = [\n        'node',\n        os.path.join(script_dir, 'clip-2d.js'),\n        '--input-obj', obj_path,\n        '--out-dir', out_dir,\n        '--basename', 'map-clip',\n        '--report', clip_report_path,\n        '--min-x', str(clip_bounds['minX']),\n        '--min-y', str(clip_bounds['minY']),\n        '--max-x', str(clip_bounds['maxX']),\n        '--max-y', str(clip_bounds['maxY']),\n    ]\n    run_result = telemetry.run_subprocess(\n        clip_cmd,\n        depth_offset=0\n    )\n\n    if not os.path.exists(clip_report_path):\n        raise Exception(\"clip-2d did not produce report\")\n    with open(clip_report_path, 'r') as f:\n        report = json.load(f)\n    file_entries = report.get('files', [])\n    mesh_paths = []\n    for entry in file_entries:\n        mesh_path = entry.get('path')\n        if not mesh_path:\n            continue\n        if not os.path.exists(mesh_path):\n            raise Exception(\"clip-2d output missing: \" + mesh_path)\n        mesh_paths.append(mesh_path)\n    if not mesh_paths:\n        raise Exception(\"clip-2d produced no meshes\")\n\n    telemetry.log(\"clip-2d outputs: {} files report={}\".format(len(mesh_paths), clip_report_path))\n    return mesh_paths, report, run_result.get('maxRssKiB')\n\n\ndef run_blender(mesh_paths, boundary, args, output_base_path, telemetry):\n    blender_dir = os.path.join(script_dir, 'blender')\n    blender_env = {\n        'LD_LIBRARY_PATH': os.path.join(blender_dir, 'lib') + \":\" + os.environ.get('LD_LIBRARY_PATH', '')\n    }\n    blender_path = os.path.join(blender_dir, 'blender')\n    obj_to_tactile_path = os.path.join(script_dir, 'obj-to-tactile.py')\n    blender_args = [\n        #'--debug',\n        '-noaudio',\n        '--factory-startup',\n    ]\n    script_args = [\n        '--scale', str(args.scale),\n        '--min-x', str(boundary['minX']),\n        '--min-y', str(boundary['minY']),\n        '--max-x', str(boundary['maxX']),\n        '--max-y', str(boundary['maxY']),\n        '--diameter', str(args.diameter),\n        '--size', str(args.size),\n        '--base-path', output_base_path,\n    ]\n    if args.foreground:\n        script_args.append('--no-stl-export')\n    else:\n        blender_args.append('--background')\n    if args.no_borders:\n        script_args.append('--no-borders')\n    if args.marker1:\n        script_args.extend(('--marker1', args.marker1))\n    cmd = [blender_path] + blender_args + ['--python', obj_to_tactile_path, '--'] + script_args + mesh_paths\n    run_result = telemetry.run_subprocess(\n        cmd,\n        env=blender_env,\n        depth_offset=0\n    )\n\n    return run_result.get('maxRssKiB')\n\ndef print_size(scale, boundary, telemetry):\n    sizeX = boundary['maxX'] - boundary['minX']\n    sizeY = boundary['maxY'] - boundary['minY']\n    telemetry.log(\n        \"Map is {:.0f} x {:.0f} meters. Selected scale {:.0f} will result in a {:.0f} x {:.0f} mm print.\".format(\n            sizeX,\n            sizeY,\n            scale,\n            sizeX / scale * 1000,\n            sizeY / scale * 1000\n        )\n    )\n\ndef main():\n    log_depth_base = _parse_int_env('TOUCH_MAPPER_LOG_DEPTH_BASE', 0)\n    telemetry = TelemetryLogger(component='osm-to-tactile', base_depth=log_depth_base)\n    # Handle command line\n    args = do_cmdline()\n    osm_path = args.input\n    input_basename, input_extension = os.path.splitext(osm_path)\n    if input_extension.lower() != '.osm':\n        raise Exception(\"input file must have extension .osm\")\n    input_dir = os.path.dirname(osm_path)\n\n    # Run OSM2World\n    obj_path = input_basename + '.obj'\n    osm2world_stage = telemetry.start_stage('run-osm2world', component='run-osm2world')\n    meta, osm2world_rss_kib = run_osm2world(osm_path, obj_path, args.scale, args.exclude_buildings, telemetry)\n    telemetry.end_stage(osm2world_stage, own_max_rss_kib=osm2world_rss_kib)\n    boundary = meta.get('meta', {}).get('boundary')\n    if boundary is None:\n        raise Exception(\"map-meta-raw.json missing meta.boundary\")\n\n    print_size(args.scale, boundary, telemetry)\n\n    # Run clip-2d\n    clip_bounds = compute_clip_bounds(boundary, args.scale, args.no_borders)\n    clip_stage = telemetry.start_stage('run-clip-2d', component='run-clip-2d')\n    mesh_paths, clip_report, clip_rss_kib = run_clip_2d(obj_path, clip_bounds, telemetry)\n    clip_timings = clip_report.get('timings', {})\n    for key in ('parseSeconds', 'clipSeconds', 'dedupeWriteSeconds'):\n        value = clip_timings.get(key)\n        if isinstance(value, (int, float)):\n            telemetry.attach_external_child(\n                clip_stage,\n                {\n                    'name': 'clip-2d.' + key,\n                    'component': 'clip-2d',\n                    'totalSec': float(value),\n                    'selfSec': float(value),\n                    'childSec': 0.0,\n                    'maxRssKiB': None,\n                    'children': [],\n                }\n            )\n    telemetry.end_stage(clip_stage, own_max_rss_kib=clip_rss_kib)\n\n    # Run Blender\n    blender_stage = telemetry.start_stage('run-blender', component='run-blender')\n    meta_path = input_basename + '-meta.json'\n    blender_rss_kib = run_blender(mesh_paths, boundary, args, input_basename, telemetry)\n    telemetry.end_stage(blender_stage, own_max_rss_kib=blender_rss_kib)\n\n    write_meta_stage = telemetry.start_stage('write-map-meta', component='write-map-meta')\n    write_json_file(meta_path, meta, pretty_json_enabled())\n    telemetry.end_stage(write_meta_stage, own_max_rss_kib=None)\n\n    timings_path = os.path.join(os.path.dirname(osm_path), 'osm-to-tactile-timings.json')\n    telemetry.write_json(timings_path, extra={'metaPath': meta_path})\n    telemetry.log('timings-json: ' + timings_path)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "converter/poller.sh",
    "content": "#!/bin/bash\n#\n# Run me at boot or periodically from cron, I will exit if I'm already running.\n#\n\nVERBOSE=true\n\ndirname=\"$( dirname \"${BASH_SOURCE[0]}\" )\"\n\nif [[ $# -ne 2 ]]; then\n    echo \"Usage: $0 TM-ENVIRONMENT WORKER-NAME\" >&2\n    exit 1\nfi\n\nenvironment=\"$1\"\nworker_name=\"$2\"\nwork_dir=\"$(cd $dirname/../runtime; pwd)/$worker_name\"\n\nif [[ ! -d \"$work_dir\" ]]; then\n  if ! mkdir \"$work_dir\"; then\n    echo \"can't create $work_dir\" >&2\n    exit 1\n  fi\nfi\n\ncd $dirname\n(\n  flock --exclusive --nonblock 200 || exit 1\n  echo $$ >&200\n  echo \"Starting at $(date --utc --rfc-3339=seconds) as worker $worker_name with environment=$environment\"\n\n  # Keep the loop as simple as possible to minimize chance of this process ever exiting\n  while true; do\n      cd .  # \"dist\" may have just been replaced due to version update\n      PYTHONUNBUFFERED=true TM_ENVIRONMENT=$environment timeout --kill-after=1s 10m ./process-request.py --poll-time 300 --work-dir \"$work_dir\" &> \"$work_dir/request.log\"\n      exit_code=$?\n\n      last_progress_line=\"$(grep -E 'PROGRESS ' \"$work_dir/request.log\" | tail -n 1 || true)\"\n      if [[ -n \"$last_progress_line\" ]]; then\n        echo \"last progress marker before poller loop iteration end: $last_progress_line\"\n      else\n        echo \"last progress marker before poller loop iteration end: <none found>\"\n      fi\n\n      if [[ $exit_code -eq 124 ]]; then\n        echo \"request processing exited with timeout (10m hard limit): exit_code=$exit_code\" >&2\n      elif [[ $exit_code -eq 137 ]]; then\n        echo \"request processing exited with SIGKILL (likely timeout --kill-after or external kill): exit_code=$exit_code\" >&2\n      elif [[ $exit_code -eq 143 ]]; then\n        echo \"request processing exited with SIGTERM: exit_code=$exit_code\" >&2\n      fi\n\n      cp \"$work_dir/request.log\" \"$work_dir/prev-request.log\"\n      if [[ $exit_code -ne 0 ]]; then\n        $VERBOSE && echo \"request processing failed\" >&2\n        cp \"$work_dir/request.log\" \"$work_dir/latest-failure.log\"\n      fi\n  done\n\n) 200>\"$work_dir/lockfile\" &>\"$work_dir/poller.log\"\n"
  },
  {
    "path": "converter/process-request.py",
    "content": "#!/usr/bin/python3 -u\n\nimport sys,os\nscript_dir = os.path.dirname(__file__)\nsys.path.insert(1, \"%s/py-lib/boto3\" % (script_dir,))\nsys.path.insert(1, script_dir)\n\nimport re\nimport boto3  # type: ignore[import-not-found]\nimport json\nimport argparse\nimport urllib.request\nimport random\nimport subprocess\nimport functools\nimport time\nimport datetime\nimport gzip\nimport copy\nimport io\nimport math\nimport signal\nimport atexit\nimport xml.etree.ElementTree as ET\nfrom typing import Any, Dict, Optional\n\nimport stats_pipeline\n\nSTORE_AGE = 8640000\n# Use wall-clock timing for stage durations.\n# Python 3.5's time.clock() returns CPU time on Unix, which under-reports\n# network-bound stages like OSM fetch. perf_counter() is monotonic wall-clock.\ntime_clock = getattr(time, 'perf_counter', time.time)\nINSTRUMENTATION_ENV_VAR = 'TOUCH_MAPPER_INSTRUMENTATION'\nTOP_RAM_STAGE_TO_FIELD = {\n    'run-osm2world': 'rss_osm2world_kib',\n    'run-blender': 'rss_blender_kib',\n    'run-clip-2d': 'rss_clip_2d_kib',\n}\nMAX_RSS_KIB_RE = re.compile(r'^\\s*Maximum resident set size \\(kbytes\\):\\s*([0-9]+)\\s*$')\nMAX_OSM_BYTES_GENERAL = 25 * 1024 * 1024\nMAX_OSM_BYTES_ONLY_BIG_ROADS_BEFORE_PRUNE = 70 * 1024 * 1024\nSTATUS_PROGRESS_SEEN = 20\nSTATUS_PROGRESS_CONVERTING = 60\nSTATUS_PROGRESS_UPLOADING_PRIMARY = 80\nSTATUS_PROGRESS_DONE = 100\nNO_GEOMETRY_ERROR_DESCRIPTION = 'Map would contain no geometry in selected area.'\n\n\ndef parse_env_bool(name):\n    raw = os.environ.get(name)\n    if raw is None:\n        return None\n    normalized = raw.strip().lower()\n    if normalized in ('1', 'true', 'yes', 'on'):\n        return True\n    if normalized in ('0', 'false', 'no', 'off'):\n        return False\n    return None\n\n\nINSTRUMENTATION_ENABLED = (parse_env_bool(INSTRUMENTATION_ENV_VAR) is True)\nINFO_JSON_META_DENYLIST = {'nodes', 'ways', 'areas'}\nSTATS_ENABLED = True\nSTATS_QUICKTIME_MODE = False\nVALID_CONTENT_MODES = set(['normal', 'no-buildings', 'only-big-roads'])\nTARGET_ROAD_DENSITY_UI_MIN = 1.0\nTARGET_ROAD_DENSITY_UI_MAX = 100.0\nTARGET_ROAD_DENSITY_UI_DEFAULT = 10.0\nTARGET_ROAD_DENSITY_DIVISOR = 10.0\nVERSION_TAG_PACKAGE_RE = re.compile(r'^package-(.+)$')\nCODE_VERSION_WARNING_EMITTED = False\nprogress_state = {\n    'status': 'starting',\n    'stage': 'bootstrap',\n    'request_id': None,\n    'termination_signal': None,\n}\n\n\ndef compact_log_text(value, max_length=240):\n    text = re.sub(r'\\s+', ' ', str(value)).strip()\n    if len(text) > max_length:\n        return text[:max_length] + '...'\n    return text\n\n\ndef log_progress(stage, status=None, request_id=None, detail=None):\n    if not INSTRUMENTATION_ENABLED:\n        return\n    if request_id is not None:\n        progress_state['request_id'] = request_id\n    if status is not None:\n        progress_state['status'] = status\n    progress_state['stage'] = stage\n    parts = [\n        'PROGRESS:process-request:{stage}'.format(stage=stage),\n        'ts={ts}'.format(ts=datetime.datetime.utcnow().isoformat() + 'Z'),\n        'status={status}'.format(status=progress_state['status']),\n    ]\n    if progress_state['request_id'] is not None:\n        parts.append('requestId={request_id}'.format(request_id=progress_state['request_id']))\n    if detail:\n        parts.append('detail={detail}'.format(detail=compact_log_text(detail)))\n    print(\" \".join(parts))\n\n\ndef log_exit_progress():\n    if not INSTRUMENTATION_ENABLED:\n        return\n    parts = [\n        'PROGRESS:process-request:exit',\n        'ts={ts}'.format(ts=datetime.datetime.utcnow().isoformat() + 'Z'),\n        'status={status}'.format(status=progress_state['status']),\n        'last_stage={stage}'.format(stage=progress_state['stage']),\n    ]\n    if progress_state['request_id'] is not None:\n        parts.append('requestId={request_id}'.format(request_id=progress_state['request_id']))\n    if progress_state['termination_signal'] is not None:\n        parts.append('signal={signal}'.format(signal=progress_state['termination_signal']))\n    print(\" \".join(parts))\n\n\ndef handle_termination_signal(signum, frame):\n    if progress_state.get('termination_signal') is None:\n        progress_state['termination_signal'] = signum\n        log_progress(\n            'signal-received',\n            status='terminated',\n            detail='signal={}'.format(signum)\n        )\n    raise SystemExit(128 + signum)\n\n\nif INSTRUMENTATION_ENABLED:\n    atexit.register(log_exit_progress)\n    signal.signal(signal.SIGTERM, handle_termination_signal)\n    signal.signal(signal.SIGINT, handle_termination_signal)\n\n\ndef now_iso_utc():\n    return datetime.datetime.utcnow().replace(microsecond=0).isoformat() + 'Z'\n\n\ndef empty_code_version_fields():\n    return {  # type: Dict[str, Any]\n        'code_branch': None,\n        'code_deployed': None,\n        'code_commit': None,\n    }\n\n\ndef warn_code_version_once(message):\n    global CODE_VERSION_WARNING_EMITTED\n    if CODE_VERSION_WARNING_EMITTED:\n        return\n    CODE_VERSION_WARNING_EMITTED = True\n    print('code version metadata unavailable: ' + str(message))\n\n\ndef read_code_version_fields(base_dir):\n    fields = empty_code_version_fields()\n    version_path = os.path.join(base_dir, 'VERSION.txt')\n    try:\n        with open(version_path, 'r', encoding='utf8') as f:\n            version_line = None\n            for raw_line in f:\n                stripped = raw_line.strip()\n                if stripped:\n                    version_line = stripped\n                    break\n    except Exception as e:\n        warn_code_version_once('failed to read {} ({})'.format(version_path, e))\n        return fields\n\n    if not version_line:\n        warn_code_version_once('empty VERSION.txt at {}'.format(version_path))\n        return fields\n\n    parts = version_line.split()\n    if len(parts) != 4:\n        warn_code_version_once(\n            'unexpected VERSION.txt format at {} (expected 4 fields, got {}): {}'.format(\n                version_path,\n                len(parts),\n                compact_log_text(version_line)\n            )\n        )\n        return fields\n\n    _, branch, tag, commit = parts\n    code_deployed = None\n    tag_match = VERSION_TAG_PACKAGE_RE.match(tag)\n    if tag_match:\n        code_deployed = tag_match.group(1)\n\n    return {\n        'code_branch': branch,\n        'code_deployed': code_deployed,\n        'code_commit': commit,\n    }\n\n\ndef stats_root_dir_from_work_dir(work_dir):\n    if work_dir:\n        runtime_dir = os.path.dirname(os.path.abspath(work_dir))\n        environment_dir = os.path.dirname(runtime_dir)\n        return os.path.join(environment_dir, 'stats')\n    return os.path.join(os.path.dirname(script_dir), 'stats')\n\n\ndef duration_since(start_time):\n    if start_time is None:\n        return None\n    return time_clock() - start_time\n\n\ndef raise_if_exception(value):\n    if isinstance(value, BaseException):\n        raise value\n    raise Exception(str(value))\n\n\ndef read_process_rss_kib():\n    try:\n        with open('/proc/self/status', 'r') as f:\n            for line in f:\n                if line.startswith('VmRSS:'):\n                    parts = line.split()\n                    if len(parts) >= 2:\n                        return int(parts[1])\n    except Exception:\n        return None\n    return None\n\n\ndef track_process_rss_kib(ctx):\n    rss_kib = read_process_rss_kib()\n    if rss_kib is None:\n        return\n    ctx['rss_process_request_last_kib'] = rss_kib\n    peak = ctx.get('rss_process_request_peak_kib')\n    if peak is None or rss_kib > peak:\n        ctx['rss_process_request_peak_kib'] = rss_kib\n\n\ndef interpreted_request_bool(request_body, key, default=False):\n    if not isinstance(request_body, dict):\n        return bool(default)\n    return bool(request_body.get(key, default))\n\n\ndef interpreted_request_text(request_body, key, max_length=2048):\n    if not isinstance(request_body, dict):\n        return None\n    raw_value = request_body.get(key)\n    if not isinstance(raw_value, str):\n        return None\n    text = raw_value.strip()\n    if text == '':\n        return None\n    if len(text) > max_length:\n        return text[:max_length]\n    return text\n\n\ndef do_cmdline():\n    parser = argparse.ArgumentParser(description='''Create STL and put into S3 based on a SQS request''')\n    parser.add_argument('--poll-time', metavar='SECONDS', type=int, help=\"poll for a request at most this long\")\n    parser.add_argument('--work-dir', metavar='PATH', help=\"write all files into this directory\")\n    args = parser.parse_args()\n    return args\n\n\nclass RequestProcessingError(Exception):\n    def __init__(self, code, description):\n        self.code = code\n        self.description = description\n        super(RequestProcessingError, self).__init__(description)\n\n\ndef raise_too_large_osm(actual_bytes, threshold_bytes, phase_text):\n    raise RequestProcessingError(\n        code='too_large',\n        description='OSM data is {} > {} bytes {}'.format(actual_bytes, threshold_bytes, phase_text)\n    )\n\n\ndef ensure_osm_size_limit(actual_bytes, threshold_bytes, phase_text):\n    if actual_bytes > threshold_bytes:\n        raise_too_large_osm(actual_bytes, threshold_bytes, phase_text)\n\n\ndef map_info_object_name_from_request_id(request_id):\n    return 'map/info/' + re.sub(r'\\/.+', '.json', request_id) # deadbeef/foo.stl => info/deadbeef.json\n\n\ndef write_info_json(bucket, key, payload, cache_control=None):\n    kwargs = {\n        'Key': key,\n        'Body': json.dumps(payload).encode('utf8'),\n        'ACL': 'public-read',\n        'ContentType': 'application/json',\n    }\n    if cache_control is not None:\n        kwargs['CacheControl'] = cache_control\n    bucket.put_object(**kwargs)\n\n\ndef write_status_info_json(ctx, progress, error_code=None, error_description=None):\n    if ctx.get('s3') is None or ctx.get('map_bucket_name') is None or ctx.get('info_object_name') is None:\n        return\n    status_payload = {\n        'requestId': ctx.get('request_id'),\n        'status': {\n            'progress': int(progress),\n        }\n    }\n    if error_code is not None:\n        status_payload['status']['errorCode'] = str(error_code)\n    if error_description is not None:\n        status_payload['status']['errorDescription'] = str(error_description)\n    ctx['status_progress'] = int(progress)\n    if error_code is not None:\n        ctx['error_code'] = str(error_code)\n    if error_description is not None:\n        ctx['error_description'] = str(error_description)\n    try:\n        bucket = ctx['s3'].Bucket(ctx['map_bucket_name'])\n        write_info_json(bucket, ctx['info_object_name'], status_payload, cache_control='no-cache')\n    except Exception as e:\n        print(\"status info write failed: \" + str(e))\n\ndef normalize_content_mode(value):\n    if isinstance(value, str):\n        normalized = value.strip().lower()\n        if normalized in VALID_CONTENT_MODES:\n            return normalized\n    return 'normal'\n\ndef ensure_request_content_mode(request_body):\n    mode = normalize_content_mode((request_body or {}).get('contentMode'))\n    request_body['contentMode'] = mode\n    return mode\n\ndef normalize_target_road_density_ui(value):\n    try:\n        density = float(value)\n    except Exception:\n        return TARGET_ROAD_DENSITY_UI_DEFAULT\n    if not math.isfinite(density):\n        return TARGET_ROAD_DENSITY_UI_DEFAULT\n    if density < TARGET_ROAD_DENSITY_UI_MIN:\n        return TARGET_ROAD_DENSITY_UI_MIN\n    if density > TARGET_ROAD_DENSITY_UI_MAX:\n        return TARGET_ROAD_DENSITY_UI_MAX\n    return density\n\ndef ensure_request_target_road_density(request_body):\n    density = normalize_target_road_density_ui((request_body or {}).get('targetRoadDensity'))\n    request_body['targetRoadDensity'] = density\n    return density\n\ndef add_or_replace_bounds(osm_data, request_body):\n    if isinstance(osm_data, bytes):\n        osm_text = osm_data.decode('utf8')\n    else:\n        osm_text = str(osm_data)\n    eff_area = request_body['effectiveArea']\n    bounds_line = '  <bounds minlat=\"{}\" minlon=\"{}\" maxlat=\"{}\" maxlon=\"{}\"/>'.format(\n        eff_area['latMin'],\n        eff_area['lonMin'],\n        eff_area['latMax'],\n        eff_area['lonMax']\n    )\n    if re.search(r'<bounds [^>]+/>\\s*', osm_text):\n        return re.sub(r'<bounds [^>]+/>\\s*', bounds_line + '\\n', osm_text, count=1)\n    if re.search(r'<meta [^>]+/>\\s*', osm_text):\n        return re.sub(r'(<meta [^>]+/>\\s*)', r'\\1' + bounds_line + '\\n', osm_text, count=1)\n    return re.sub(r'(<osm[^>]*>\\s*)', r'\\1' + bounds_line + '\\n', osm_text, count=1)\n\ndef write_osm_with_bounds(osm_data, request_body, osm_path):\n    with open(osm_path, 'wb') as f:\n        f.write(add_or_replace_bounds(osm_data, request_body).encode('utf8'))\n\ndef element_tags(elem):\n    tags = {}\n    for child in list(elem):\n        if child.tag != 'tag':\n            continue\n        key = child.get('k')\n        if key is None:\n            continue\n        tags[key] = child.get('v')\n    return tags\n\ndef has_building_tags(tags):\n    def is_active_building_value(value):\n        if value is None:\n            return False\n        normalized = str(value).strip().lower()\n        return normalized != '' and normalized != 'no'\n    return (\n        is_active_building_value(tags.get('building')) or\n        is_active_building_value(tags.get('building:part'))\n    )\n\ndef parse_osm_tree(osm_path):\n    try:\n        tree = ET.parse(osm_path)\n        return tree\n    except Exception as e:\n        raise Exception(\"Can't parse OSM XML at {}: {}\".format(osm_path, e))\n\ndef set_bounds_on_tree(root, request_body):\n    eff_area = request_body['effectiveArea']\n    bounds_attrs = {\n        'minlat': str(eff_area['latMin']),\n        'minlon': str(eff_area['lonMin']),\n        'maxlat': str(eff_area['latMax']),\n        'maxlon': str(eff_area['lonMax']),\n    }\n    for child in list(root):\n        if child.tag == 'bounds':\n            child.attrib = bounds_attrs\n            return\n    bounds_elem = ET.Element('bounds', bounds_attrs)\n    insert_index = 0\n    for i, child in enumerate(list(root)):\n        if child.tag in ('note', 'meta'):\n            insert_index = i + 1\n            continue\n        if child.tag in ('node', 'way', 'relation'):\n            break\n        insert_index = i + 1\n    root.insert(insert_index, bounds_elem)\n\ndef filter_osm_file_for_no_buildings(osm_path, request_body):\n    tree = parse_osm_tree(osm_path)\n    root = tree.getroot()\n    ways = {}\n    relations = {}\n    for child in list(root):\n        elem_id = child.get('id')\n        if elem_id is None:\n            continue\n        if child.tag == 'way':\n            ways[elem_id] = child\n        elif child.tag == 'relation':\n            relations[elem_id] = child\n\n    building_way_ids = set()\n    building_relation_ids = set()\n    for way_id, way_elem in ways.items():\n        if has_building_tags(element_tags(way_elem)):\n            building_way_ids.add(way_id)\n    for rel_id, rel_elem in relations.items():\n        if has_building_tags(element_tags(rel_elem)):\n            building_relation_ids.add(rel_id)\n\n    pending_building_relations = list(building_relation_ids)\n    seen_building_relations = set()\n    while pending_building_relations:\n        rel_id = pending_building_relations.pop()\n        if rel_id in seen_building_relations:\n            continue\n        seen_building_relations.add(rel_id)\n        rel_elem = relations.get(rel_id)\n        if rel_elem is None:\n            continue\n        for member in rel_elem.findall('member'):\n            member_type = member.get('type')\n            member_ref = member.get('ref')\n            if member_ref is None:\n                continue\n            if member_type == 'way':\n                building_way_ids.add(member_ref)\n            elif member_type == 'relation' and member_ref not in seen_building_relations:\n                building_relation_ids.add(member_ref)\n                pending_building_relations.append(member_ref)\n\n    keep_way_ids = set(ways.keys()) - building_way_ids\n    keep_relation_ids = set(relations.keys()) - building_relation_ids\n\n    keep_node_ids = set()\n    for way_id in keep_way_ids:\n        way_elem = ways.get(way_id)\n        if way_elem is None:\n            continue\n        for nd in way_elem.findall('nd'):\n            ref = nd.get('ref')\n            if ref is not None:\n                keep_node_ids.add(ref)\n\n    for rel_id in keep_relation_ids:\n        rel_elem = relations.get(rel_id)\n        if rel_elem is None:\n            continue\n        for member in rel_elem.findall('member'):\n            member_type = member.get('type')\n            member_ref = member.get('ref')\n            if member_ref is None:\n                continue\n            if member_type == 'node':\n                keep_node_ids.add(member_ref)\n\n    new_root = ET.Element(root.tag, root.attrib)\n    for child in list(root):\n        elem_id = child.get('id')\n        if child.tag in ('note', 'meta'):\n            new_root.append(copy.deepcopy(child))\n            continue\n        if child.tag == 'node':\n            tags = element_tags(child)\n            if has_building_tags(tags):\n                continue\n            if len(tags) == 0 and elem_id not in keep_node_ids:\n                continue\n            new_root.append(copy.deepcopy(child))\n            continue\n        if child.tag == 'way':\n            if elem_id in keep_way_ids:\n                new_root.append(copy.deepcopy(child))\n            continue\n        if child.tag == 'relation' and elem_id in keep_relation_ids:\n            rel_copy = copy.deepcopy(child)\n            for member in list(rel_copy):\n                if member.tag != 'member':\n                    continue\n                member_type = member.get('type')\n                member_ref = member.get('ref')\n                if member_ref is None:\n                    continue\n                if member_type == 'way' and member_ref in building_way_ids:\n                    rel_copy.remove(member)\n                elif member_type == 'relation' and member_ref in building_relation_ids:\n                    rel_copy.remove(member)\n            new_root.append(rel_copy)\n\n    set_bounds_on_tree(new_root, request_body)\n    filtered_tree = ET.ElementTree(new_root)\n    with open(osm_path, 'wb') as f:\n        filtered_tree.write(f, encoding='UTF-8', xml_declaration=True)\n\ndef run_subprocess_with_max_rss_kib(cmd):\n    timed_cmd = list(cmd)\n    use_time = os.path.exists('/usr/bin/time')\n    if use_time:\n        timed_cmd = ['/usr/bin/time', '-v'] + timed_cmd\n\n    process = subprocess.Popen(\n        timed_cmd,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE\n    )\n    stdout_data, stderr_data = process.communicate()\n    stdout_text = stdout_data.decode('utf8', errors='replace')\n    stderr_text = stderr_data.decode('utf8', errors='replace')\n\n    max_rss_kib = None\n    if use_time:\n        for line in stderr_text.splitlines():\n            match = MAX_RSS_KIB_RE.match(line)\n            if match is not None:\n                try:\n                    max_rss_kib = int(match.group(1))\n                except Exception:\n                    max_rss_kib = None\n\n    if process.returncode != 0:\n        raise Exception(\n            \"command failed ({}) for {} stderr={}\".format(\n                process.returncode,\n                \" \".join(cmd),\n                compact_log_text(stderr_text, max_length=1000)\n            )\n        )\n\n    if stdout_text.strip() != '':\n        print(stdout_text.strip())\n\n    return max_rss_kib\n\ndef prune_osm_file_for_only_big_roads_with_node(osm_path, request_body):\n    eff_area = request_body['effectiveArea']\n    target_road_density_for_pruner = ensure_request_target_road_density(request_body) / TARGET_ROAD_DENSITY_DIVISOR\n    cmd = [\n        'node',\n        os.path.join(script_dir, 'prune-only-big-roads.js'),\n        '--osm', osm_path,\n        '--lon-min', str(eff_area['lonMin']),\n        '--lat-min', str(eff_area['latMin']),\n        '--lon-max', str(eff_area['lonMax']),\n        '--lat-max', str(eff_area['latMax']),\n        '--print-size-cm', str(request_body['size']),\n        '--map-scale', str(request_body['scale']),\n        '--target-road-density', str(target_road_density_for_pruner),\n    ]\n    print(\"running: \" + \" \".join(cmd))\n    return run_subprocess_with_max_rss_kib(cmd)\n\ndef get_osm(request_body, work_dir):\n    # TODO: verify the requested region isn't too large\n    content_mode = ensure_request_content_mode(request_body)\n    ensure_request_target_road_density(request_body)\n    osm_path = '{}/map.osm'.format(work_dir)\n    eff_area = request_body['effectiveArea']\n    bbox = \"{},{},{},{}\".format( eff_area['lonMin'], eff_area['latMin'], eff_area['lonMax'], eff_area['latMax'] )\n    overpass_map_attempts = [\n        { 'url': \"http://www.overpass-api.de/api/xapi?map?bbox=\" + bbox,\n          'provider': 'overpass',\n          'method': lambda url: get_osm_overpass_api(url=url, timeout=20, request_body=request_body, osm_path=osm_path),\n        },\n        { 'url': \"http://overpass.osm.rambler.ru/cgi/xapi?map?bbox=\" + bbox,\n          'provider': 'overpass',\n          'method': lambda url: get_osm_overpass_api(url=url, timeout=60, request_body=request_body, osm_path=osm_path),\n        },\n        { 'url': \"http://www.overpass-api.de/api/xapi?map?bbox=\" + bbox,\n          'provider': 'overpass',\n          'method': lambda url: get_osm_overpass_api(url=url, timeout=60, request_body=request_body, osm_path=osm_path),\n        },\n    ]\n    # All content modes share the same fetch strategy:\n    # randomized Overpass endpoint order first, then OSM main API fallback.\n    attempts = list(overpass_map_attempts)\n    random.shuffle(attempts)\n    attempts.append(\n        { 'url': \"http://api.openstreetmap.org/api/0.6/map?bbox=\" + bbox,\n          'provider': 'main_api',\n          'method': lambda url: get_osm_main_api(url=url, timeout=120, osm_path=osm_path),\n        }\n    )\n    for i, attempt in enumerate(attempts):\n        try:\n            fetch_start_time = time_clock()\n            attempt['method'](attempt['url'])\n            fetch_attempt_seconds = duration_since(fetch_start_time)\n            fetched_osm_bytes = os.path.getsize(osm_path)\n            prune_rss_kib = None\n            prune_only_big_roads_seconds = None\n            if content_mode == 'only-big-roads':\n                ensure_osm_size_limit(\n                    actual_bytes=fetched_osm_bytes,\n                    threshold_bytes=MAX_OSM_BYTES_ONLY_BIG_ROADS_BEFORE_PRUNE,\n                    phase_text='before pruning'\n                )\n                prune_start_time = time_clock()\n                prune_rss_kib = prune_osm_file_for_only_big_roads_with_node(osm_path, request_body)\n                prune_only_big_roads_seconds = duration_since(prune_start_time)\n                pruned_osm_bytes = os.path.getsize(osm_path)\n                ensure_osm_size_limit(\n                    actual_bytes=pruned_osm_bytes,\n                    threshold_bytes=MAX_OSM_BYTES_GENERAL,\n                    phase_text='after pruning'\n                )\n            elif content_mode == 'no-buildings':\n                ensure_osm_size_limit(\n                    actual_bytes=fetched_osm_bytes,\n                    threshold_bytes=MAX_OSM_BYTES_GENERAL,\n                    phase_text='before pruning'\n                )\n                filter_osm_file_for_no_buildings(osm_path, request_body)\n                pruned_osm_bytes = os.path.getsize(osm_path)\n            else:\n                ensure_osm_size_limit(\n                    actual_bytes=fetched_osm_bytes,\n                    threshold_bytes=MAX_OSM_BYTES_GENERAL,\n                    phase_text='before pruning'\n                )\n                pruned_osm_bytes = fetched_osm_bytes\n            return (\n                osm_path,\n                fetched_osm_bytes,\n                pruned_osm_bytes,\n                prune_rss_kib,\n                fetch_attempt_seconds,\n                prune_only_big_roads_seconds,\n                attempt.get('provider'),\n                attempt['url']\n            )\n        except Exception as e:\n            if isinstance(e, RequestProcessingError):\n                raise\n            msg = \"Can't read map data from \" + attempt['url'] + \": \" + str(e)\n            if i == len(attempts) - 1:\n                raise Exception(msg)\n            else:\n                print(msg)\n\ndef get_osm_overpass_api(url, timeout, request_body, osm_path):\n    print(\"getting \" + url)\n    osm_data = urllib.request.urlopen(url, timeout=timeout).read()\n    write_osm_with_bounds(osm_data, request_body, osm_path)\n\ndef get_osm_main_api(url, timeout, osm_path):\n    print(\"getting \" + url)\n    osm_data = urllib.request.urlopen(url, timeout=timeout).read()\n    with open(osm_path, 'wb') as f:\n        f.write(osm_data)\n\ndef read_osm_to_tactile_rss_kib(output_dir):\n    fields = {}  # type: Dict[str, Optional[int]]\n    fields['rss_osm2world_kib'] = None\n    fields['rss_blender_kib'] = None\n    fields['rss_clip_2d_kib'] = None\n    timings_path = os.path.join(output_dir, 'osm-to-tactile-timings.json')\n    try:\n        with open(timings_path, 'r') as f:\n            payload = json.load(f)\n    except Exception as e:\n        print(\"warning: can't read {}: {}\".format(timings_path, e))\n        return fields\n\n    stages = payload.get('stages')\n    if not isinstance(stages, list):\n        print(\"warning: invalid stages payload in {}\".format(timings_path))\n        return fields\n\n    for stage in stages:\n        if not isinstance(stage, dict):\n            continue\n        stage_name = stage.get('name')\n        if not isinstance(stage_name, str):\n            continue\n        field_name = TOP_RAM_STAGE_TO_FIELD.get(stage_name)\n        if field_name is None:\n            continue\n        rss_value = stage.get('maxRssKiB')\n        if isinstance(rss_value, int):\n            fields[field_name] = rss_value\n    return fields\n\ndef has_empty_clip_report(output_dir):\n    clip_report_path = os.path.join(output_dir, 'map-clip-report.json')\n    if not os.path.exists(clip_report_path):\n        return False\n    try:\n        with open(clip_report_path, 'r') as f:\n            report = json.load(f)\n    except Exception:\n        return False\n    file_entries = report.get('files')\n    if not isinstance(file_entries, list):\n        return False\n    for entry in file_entries:\n        if isinstance(entry, dict) and entry.get('path'):\n            return False\n    return True\n\ndef run_osm_to_tactile(osm_path, request_body):\n    output_dir = os.path.dirname(osm_path)\n    clip_report_path = os.path.join(output_dir, 'map-clip-report.json')\n    try:\n        if os.path.exists(clip_report_path):\n            os.remove(clip_report_path)\n        stl_path = output_dir + '/map.stl'\n        if os.path.exists(stl_path):\n            os.rename(stl_path, stl_path + \".old\")\n        args = ['--scale', str(request_body['scale']), '--diameter', str(request_body['diameter']), '--size', str(request_body['size']), ]\n        if request_body.get('noBorders', False):\n            args.append('--no-borders')\n        if not request_body.get('hideLocationMarker', False) and not request_body.get('multipartMode', False) and 'marker1' in request_body:\n            eff_area = request_body['effectiveArea']\n            marker1x = (request_body['marker1']['lon'] - eff_area['lonMin']) / (eff_area['lonMax'] - eff_area['lonMin'])\n            marker1y = (request_body['marker1']['lat'] - eff_area['latMin']) / (eff_area['latMax'] - eff_area['latMin'])\n            if 0.04 < marker1x < 0.96 and 0.04 < marker1y < 0.96:\n                args.extend([ '--marker1', json.dumps({ 'x': marker1x, 'y': marker1y }) ])\n        cmd = ['./osm-to-tactile.py'] + args + [osm_path]\n        print(\"running: \" + \" \".join(cmd))\n        subprocess.check_call(cmd)\n        artifact_paths = {\n            'stl_path': os.path.join(output_dir, 'map.stl'),\n            'stl_ways_path': os.path.join(output_dir, 'map-ways.stl'),\n            'stl_rest_path': os.path.join(output_dir, 'map-rest.stl'),\n            'svg_path': os.path.join(output_dir, 'map.svg'),\n            'blend_path': os.path.join(output_dir, 'map.blend'),\n            'meta_raw_path': os.path.join(output_dir, 'map-meta-raw.json'),\n        }\n        with open(artifact_paths['meta_raw_path'], 'r') as f:\n            meta = json.load(f)\n\n        rss_kib = read_osm_to_tactile_rss_kib(os.path.dirname(osm_path))\n        return artifact_paths, meta, rss_kib\n    except Exception as e:\n        if has_empty_clip_report(output_dir):\n            raise RequestProcessingError(code='unknown', description=NO_GEOMETRY_ERROR_DESCRIPTION)\n        raise Exception(\"Can't convert map data to STL: \" + str(e)) # let's not reveal too much, error msg likely contains paths\n\n# Receive a message from SQS and delete it. Poll up to \"poll_time\" seconds. Return parsed request, or None if no msg received.\ndef receive_sqs_msg(queue_name, poll_time):\n    end = time_clock() + poll_time\n    sqs = boto3.resource('sqs')\n    queue = sqs.get_queue_by_name(QueueName = queue_name)\n    while end - time_clock() > 20:\n        messages = queue.receive_messages(\n            WaitTimeSeconds = 20\n        )\n        if len(messages) > 0:\n            message = messages[0]\n            print(message.body)\n\n            # Delete message immediately so we won't start looping on it if processing fails\n            response = queue.delete_messages(Entries=[{\n                'Id': 'dummy',\n                'ReceiptHandle': message.receipt_handle,\n            }]) # ignore errors\n\n            # Parse\n            request = json.loads(message.body)\n            # TODO: validate request -- its contents are untrusted\n            return request\n    return None\n\ndef svg_to_pdf(svg_path, pdf_path):\n    try:\n        # Run conversion in a short-lived subprocess so Cairo/Pango allocations\n        # do not remain in process-request.py RSS after PDF generation.\n        cmd = [\n            sys.executable,\n            '-c',\n            'import cairosvg,sys; cairosvg.svg2pdf(url=sys.argv[1], write_to=sys.argv[2])',\n            svg_path,\n            pdf_path\n        ]\n        return run_subprocess_with_max_rss_kib(cmd)\n    except Exception as e:\n        raise Exception(\"Can't convert SVG to PDF: \" + str(e))\n\n\ndef gzip_file_to_bytes(path, compresslevel=5, rss_tracker=None):\n    out = io.BytesIO()\n    with open(path, 'rb') as src:\n        with gzip.GzipFile(fileobj=out, mode='wb', compresslevel=compresslevel) as gz:\n            while True:\n                chunk = src.read(1024 * 1024)\n                if not chunk:\n                    break\n                gz.write(chunk)\n                if rss_tracker is not None:\n                    rss_tracker()\n    if rss_tracker is not None:\n        rss_tracker()\n    return out.getvalue()\n\n\ndef upload_primary_assets(bucket, json_object_name, info, name_base,\n                          map_object_name, map_content, stl_path, common_args,\n                          rss_tracker=None,\n                          progress_logger=None):\n    # Put the augmented request to S3\n    if progress_logger is not None:\n        progress_logger('upload-primary-info-json-start', detail='key={}'.format(json_object_name))\n    bucket.put_object(\n        Key=json_object_name,\n        Body=json.dumps(info).encode('utf8'),\n        ACL='public-read',\n        ContentType='application/json'\n    )\n    if progress_logger is not None:\n        progress_logger('upload-primary-info-json-done', detail='key={}'.format(json_object_name))\n\n    # Map content description is also read on the map display page, so make sure it's uploaded before STL\n    map_content_key = name_base + '.map-content.json'\n    if progress_logger is not None:\n        progress_logger('upload-primary-map-content-start', detail='key={}'.format(map_content_key))\n    bucket.put_object(\n        Key=map_content_key,\n        Body=gzip.compress(map_content, compresslevel=5),\n        **common_args,\n        ContentType='application/json'\n    )\n    if rss_tracker is not None:\n        rss_tracker()\n    if progress_logger is not None:\n        progress_logger('upload-primary-map-content-done', detail='key={}'.format(map_content_key))\n\n    # Put full STL file to S3. Completion of this upload makes UI consider the STL creation complete.\n    if progress_logger is not None:\n        progress_logger('upload-primary-stl-start', detail='key={}'.format(map_object_name))\n    bucket.put_object(\n        Key=map_object_name,\n        Body=gzip_file_to_bytes(stl_path, compresslevel=5, rss_tracker=rss_tracker),\n        **common_args,\n        ContentType='application/sla'\n    )\n    if rss_tracker is not None:\n        rss_tracker()\n    if progress_logger is not None:\n        progress_logger('upload-primary-stl-done', detail='key={}'.format(map_object_name))\n\ndef attach_request_metadata_to_map_content(map_content, request_body):\n    try:\n        map_content_json = json.loads(map_content.decode('utf8'))\n    except Exception as e:\n        raise Exception(\"Can't parse map-content.json: \" + str(e))\n    map_content_json['metadata'] = {\n        'requestBody': copy.deepcopy(request_body)\n    }\n    return json.dumps(map_content_json, ensure_ascii=False, separators=(',', ':')).encode('utf8')\n\n\ndef build_info_payload(request_body, meta, status_payload=None):\n    # info.json is only map-page bootstrap metadata; omit large geometry arrays.\n    info = copy.copy(request_body)\n    for key, value in meta.items():\n        if key in INFO_JSON_META_DENYLIST:\n            continue\n        info[key] = value\n    if status_payload is not None:\n        info['status'] = copy.deepcopy(status_payload)\n    return info\n\n\ndef upload_secondary_assets(bucket, name_base, svg_path, pdf_path, stl_ways_path, stl_rest_path, blend_path, common_args, rss_tracker=None, progress_logger=None):\n    def upload_blob_from_path(key, path, content_type):\n        try:\n            if progress_logger is not None:\n                progress_logger('upload-secondary-item-start', detail='key={}'.format(key))\n            bucket.put_object(\n                Key=key,\n                Body=gzip_file_to_bytes(path, compresslevel=5, rss_tracker=rss_tracker),\n                **common_args,\n                ContentType=content_type\n            )\n            if rss_tracker is not None:\n                rss_tracker()\n            if progress_logger is not None:\n                progress_logger('upload-secondary-item-done', detail='key={}'.format(key))\n        except Exception as e:\n            print(\"upload failed for {}: {}\".format(key, e))\n            if progress_logger is not None:\n                progress_logger('upload-secondary-item-failed', detail='key={} error={}'.format(key, e))\n\n    uploads = [\n        (name_base + '.svg', svg_path, 'image/svg+xml'),\n        (name_base + '.pdf', pdf_path, 'application/pdf'),\n        (name_base + '-ways.stl', stl_ways_path, 'application/sla'),\n        (name_base + '-rest.stl', stl_rest_path, 'application/sla'),\n        (name_base + '.blend', blend_path, 'application/binary')\n    ]\n\n    # Upload sequentially to avoid keeping multiple large gzip blobs in memory at once.\n    for key, path, content_type in uploads:\n        upload_blob_from_path(key, path, content_type)\n\ndef run_map_desc(raw_meta_path, profile=None):\n    import map_desc\n    map_desc.run_map_desc(raw_meta_path, profile=profile)\n\n\ndef init_main_context():\n    return {\n        's3': None,\n        'stats_s3': None,\n        'map_bucket_name': None,\n        'stats_bucket_name': None,\n        'queue_name': None,\n        'map_object_name': None,\n        'info_object_name': None,\n        'map_content_key': None,\n        'name_base': None,\n        'request_body': None,\n        'request_id': None,\n        'map_id': None,\n        'args': None,\n        'stats_root_dir': None,\n        'environment': None,\n        'worker_name': 'unknown',\n        'current_stage': 'bootstrap',\n        'failure_stage': None,\n        'failure_class': None,\n        'failure_message': None,\n        'failure_exception': None,\n        'error_code': None,\n        'error_description': None,\n        'status': 'starting',\n        'status_progress': None,\n        'processing_start_time': None,\n        'code_version_fields': empty_code_version_fields(),\n        'progress_logger': None,\n        'main_start_time': None,\n        'timing_get_osm_seconds': None,\n        'timing_prune_only_big_roads_seconds': None,\n        'timing_map_desc_seconds': None,\n        'timing_upload_primary_seconds': None,\n        'timing_svg_to_pdf_seconds': None,\n        'osm_fetch_provider': None,\n        'osm_fetch_endpoint': None,\n        'stl_bytes': None,\n        'stl_gzip_bytes': None,\n        'map_content_gzip_bytes': None,\n        'osm_fetched_bytes': None,\n        'osm_pruned_bytes': None,\n        'rss_osm2world_kib': None,\n        'rss_blender_kib': None,\n        'rss_clip_2d_kib': None,\n        'rss_prune_only_big_roads_kib': None,\n        'rss_svg_to_pdf_kib': None,\n        'rss_process_request_last_kib': None,\n        'rss_process_request_peak_kib': None,\n    }\n\n\ndef bootstrap_runtime(ctx):\n    ctx['main_start_time'] = time_clock()\n    ctx['progress_logger'] = (log_progress if INSTRUMENTATION_ENABLED else None)\n    log_progress('main-start', status='running')\n    ctx['status'] = 'running'\n    ctx['environment'] = os.environ['TM_ENVIRONMENT']\n    ctx['queue_name'] = ctx['environment'] + \"-requests-touch-mapper\"\n    ctx['map_bucket_name'] = ctx['environment'] + \".maps.touch-mapper\"\n    ctx['stats_bucket_name'] = ctx['environment'] + \".stats.touch-mapper\"\n    ctx['args'] = do_cmdline()\n    ctx['stats_root_dir'] = stats_root_dir_from_work_dir(ctx['args'].work_dir)\n    ctx['code_version_fields'] = read_code_version_fields(script_dir)\n    if ctx['args'].work_dir:\n        ctx['worker_name'] = os.path.basename(os.path.normpath(ctx['args'].work_dir))\n\n\ndef init_stats_services(ctx):\n    if not STATS_ENABLED:\n        return\n    try:\n        ctx['stats_s3'] = boto3.resource('s3')\n        if not STATS_QUICKTIME_MODE:\n            try:\n                stats_pipeline.run_daily_upload_if_due(\n                    stats_root_dir=ctx['stats_root_dir'],\n                    s3_resource=ctx['stats_s3'],\n                    stats_bucket_name=ctx['stats_bucket_name']\n                )\n            except Exception as e:\n                print(\"stats daily upload failed: \" + str(e))\n    except Exception as e:\n        print(\"stats init failed: \" + str(e))\n        ctx['stats_s3'] = None\n\n\ndef handle_main_exception(ctx, e):\n    ctx['failure_exception'] = e\n    ctx['failure_stage'] = ctx['current_stage']\n    ctx['failure_class'] = e.__class__.__name__\n    ctx['failure_message'] = str(e)\n    if isinstance(e, RequestProcessingError):\n        ctx['error_code'] = e.code\n        ctx['error_description'] = e.description\n    else:\n        ctx['error_code'] = 'unknown'\n        ctx['error_description'] = str(e)\n    ctx['status'] = 'failed'\n    try:\n        print(\"process-request failed: \" + str(e))\n        log_progress('failed', status='failed', detail=str(e))\n        write_status_info_json(\n            ctx,\n            progress=(ctx.get('status_progress') if ctx.get('status_progress') is not None else STATUS_PROGRESS_SEEN),\n            error_code=ctx.get('error_code'),\n            error_description=ctx.get('error_description')\n        )\n    except Exception:\n        pass\n\n\ndef build_stats_record(ctx):\n    request_body = ctx['request_body']\n    total_elapsed = duration_since(ctx['processing_start_time'])\n    return {\n        'schema_version': 1,\n        'timestamp': now_iso_utc(),\n        'day': datetime.datetime.utcnow().strftime('%d'),\n        'code_branch': ctx['code_version_fields'].get('code_branch'),\n        'code_deployed': ctx['code_version_fields'].get('code_deployed'),\n        'code_commit': ctx['code_version_fields'].get('code_commit'),\n        'request_id': ctx['request_id'],\n        'map_id': ctx['map_id'],\n        'status': ctx['status'],\n        'failure_stage': ctx['failure_stage'],\n        'failure_class': ctx['failure_class'],\n        'failure_message': ctx['failure_message'],\n        'error_code': ctx['error_code'],\n        'error_description': ctx['error_description'],\n        'termination_signal': progress_state.get('termination_signal'),\n        'browser_fingerprint': request_body.get('browserFingerprint'),\n        'browser_ip': request_body.get('browserIp'),\n        'browser_referrer': interpreted_request_text(request_body, 'browserReferrer'),\n        'browser_ip_country': None,\n        'browser_ip_country_code': None,\n        'browser_ip_region': None,\n        'browser_ip_city': None,\n        'browser_ip_latitude': None,\n        'browser_ip_longitude': None,\n        'addr_long': request_body.get('addrLong'),\n        'printing_tech': request_body.get('printingTech'),\n        'offset_x': request_body.get('offsetX'),\n        'offset_y': request_body.get('offsetY'),\n        'size_cm': request_body.get('size'),\n        'content_mode': request_body.get('contentMode'),\n        'hide_location_marker': interpreted_request_bool(request_body, 'hideLocationMarker', False),\n        'lon': request_body.get('lon'),\n        'lat': request_body.get('lat'),\n        'scale': request_body.get('scale'),\n        'multipart_mode': interpreted_request_bool(request_body, 'multipartMode', False),\n        'no_borders': interpreted_request_bool(request_body, 'noBorders', False),\n        'multipart_xpc': request_body.get('multipartXpc'),\n        'multipart_ypc': request_body.get('multipartYpc'),\n        'advanced_mode': interpreted_request_bool(request_body, 'advancedMode', False),\n        'osm_fetch_provider': ctx['osm_fetch_provider'],\n        'osm_fetch_endpoint': ctx['osm_fetch_endpoint'],\n        'timing_get_osm_seconds': ctx['timing_get_osm_seconds'],\n        'timing_prune_only_big_roads_seconds': ctx['timing_prune_only_big_roads_seconds'],\n        'timing_map_desc_seconds': ctx['timing_map_desc_seconds'],\n        'timing_upload_primary_seconds': ctx['timing_upload_primary_seconds'],\n        'timing_svg_to_pdf_seconds': ctx['timing_svg_to_pdf_seconds'],\n        'timing_total_seconds': total_elapsed,\n        'timing_failed_after_seconds': (total_elapsed if ctx['status'] == 'failed' else None),\n        'stl_bytes': ctx['stl_bytes'],\n        'stl_gzip_bytes': ctx['stl_gzip_bytes'],\n        'map_content_gzip_bytes': ctx['map_content_gzip_bytes'],\n        'osm_fetched_bytes': ctx['osm_fetched_bytes'],\n        'osm_pruned_bytes': ctx['osm_pruned_bytes'],\n        'rss_osm2world_kib': ctx['rss_osm2world_kib'],\n        'rss_blender_kib': ctx['rss_blender_kib'],\n        'rss_clip_2d_kib': ctx['rss_clip_2d_kib'],\n        'rss_prune_only_big_roads_kib': ctx['rss_prune_only_big_roads_kib'],\n        'rss_svg_to_pdf_kib': ctx['rss_svg_to_pdf_kib'],\n        'rss_process_request_peak_kib': ctx['rss_process_request_peak_kib'],\n    }\n\n\ndef write_final_stats_if_possible(ctx):\n    if not STATS_ENABLED:\n        return\n    if ctx['request_body'] is None:\n        return\n    try:\n        track_process_rss_kib(ctx)\n        if ctx['stats_root_dir'] is None:\n            args = ctx.get('args')\n            work_dir = args.work_dir if args else None\n            ctx['stats_root_dir'] = stats_root_dir_from_work_dir(work_dir)\n        if ctx['map_id'] is None:\n            ctx['map_id'] = stats_pipeline.map_id_from_request_id(ctx['request_id'])\n\n        stats_pipeline.write_attempt_record(\n            stats_root_dir=ctx['stats_root_dir'],\n            record=build_stats_record(ctx),\n            quicktime_mode=STATS_QUICKTIME_MODE,\n            s3_resource=ctx['stats_s3'],\n            stats_bucket_name=ctx['stats_bucket_name']\n        )\n    except Exception as stats_error:\n        print(\"stats write failed: \" + str(stats_error))\n\n\ndef rethrow_failure_if_needed(ctx):\n    failure_exception = ctx['failure_exception']\n    if failure_exception is None:\n        return\n    if isinstance(failure_exception, SystemExit):\n        raise_if_exception(failure_exception)\n    if isinstance(failure_exception, KeyboardInterrupt):\n        raise_if_exception(failure_exception)\n    sys.exit(1)\n\n\ndef main():\n    # TODO: if output S3 object already exists, exit immediately\n    ctx = init_main_context()\n    try:\n        bootstrap_runtime(ctx)\n        track_process_rss_kib(ctx)\n        init_stats_services(ctx)\n        track_process_rss_kib(ctx)\n\n        # Receive SQS msg\n        ctx['current_stage'] = 'poll'\n        log_progress('poll-start')\n        print(\"\\n\\n============= STARTING TO POLL AT %s ===========\" % (datetime.datetime.now().isoformat()))\n        ctx['request_body'] = receive_sqs_msg(ctx['queue_name'], ctx['args'].poll_time)\n        if ctx['request_body'] == None:\n            log_progress('poll-empty', status='idle')\n            ctx['status'] = 'idle'\n            return\n        ctx['request_body']['contentMode'] = normalize_content_mode(ctx['request_body'].get('contentMode'))\n        ctx['request_body']['targetRoadDensity'] = normalize_target_road_density_ui(\n            ctx['request_body'].get('targetRoadDensity')\n        )\n        ctx['request_id'] = ctx['request_body'].get('requestId')\n        ctx['map_id'] = stats_pipeline.map_id_from_request_id(ctx['request_id'])\n        ctx['processing_start_time'] = time_clock()\n        log_progress('poll-returned', request_id=ctx['request_id'])\n        print(\"Poll returned at %s\" % (datetime.datetime.now().isoformat()))\n        track_process_rss_kib(ctx)\n\n        # Get OSM data\n        ctx['current_stage'] = 'get-osm'\n        log_progress('get-osm-start')\n        ctx['s3'] = ctx['stats_s3'] if ctx['stats_s3'] is not None else boto3.resource('s3')\n        ctx['map_object_name'] = 'map/data/' + ctx['request_body']['requestId'] + '.stl'\n        ctx['info_object_name'] = map_info_object_name_from_request_id(ctx['request_body']['requestId'])\n        ctx['name_base'] = ctx['map_object_name'][:-4]\n        bucket = ctx['s3'].Bucket(ctx['map_bucket_name'])\n        write_status_info_json(ctx, STATUS_PROGRESS_SEEN)\n        osm_result = get_osm(ctx['request_body'], ctx['args'].work_dir)\n        if osm_result is None:\n            raise Exception(\"OSM path not available\")\n        (\n            osm_path,\n            fetched_osm_bytes,\n            pruned_osm_bytes,\n            prune_rss_kib,\n            fetch_attempt_seconds,\n            prune_only_big_roads_seconds,\n            osm_fetch_provider,\n            osm_fetch_endpoint\n        ) = osm_result\n        ctx['osm_fetched_bytes'] = fetched_osm_bytes\n        ctx['osm_pruned_bytes'] = pruned_osm_bytes\n        ctx['rss_prune_only_big_roads_kib'] = prune_rss_kib\n        ctx['timing_get_osm_seconds'] = fetch_attempt_seconds\n        ctx['timing_prune_only_big_roads_seconds'] = prune_only_big_roads_seconds\n        ctx['osm_fetch_provider'] = osm_fetch_provider\n        ctx['osm_fetch_endpoint'] = osm_fetch_endpoint\n        log_progress('get-osm-done')\n        track_process_rss_kib(ctx)\n\n        # Convert OSM => STL\n        ctx['current_stage'] = 'osm-to-tactile'\n        log_progress('osm-to-tactile-start')\n        write_status_info_json(ctx, STATUS_PROGRESS_CONVERTING)\n        artifacts, meta, rss_kib = run_osm_to_tactile(osm_path, ctx['request_body'])\n        ctx['rss_osm2world_kib'] = rss_kib.get('rss_osm2world_kib')\n        ctx['rss_blender_kib'] = rss_kib.get('rss_blender_kib')\n        ctx['rss_clip_2d_kib'] = rss_kib.get('rss_clip_2d_kib')\n        ctx['stl_bytes'] = os.path.getsize(artifacts['stl_path'])\n        log_progress('osm-to-tactile-done')\n        track_process_rss_kib(ctx)\n        raw_meta_path = artifacts['meta_raw_path']\n\n        # Enrich map-meta.json\n        ctx['current_stage'] = 'map-desc'\n        map_desc_start_time = time_clock()\n        log_progress('map-desc-start')\n        run_map_desc(raw_meta_path, profile={})\n        ctx['timing_map_desc_seconds'] = duration_since(map_desc_start_time)\n        log_progress('map-desc-done')\n        track_process_rss_kib(ctx)\n\n        ctx['current_stage'] = 'map-content-read'\n        map_content_path = os.path.join(os.path.dirname(osm_path), 'map-content.json')\n        log_progress('map-content-read-start', detail='path={}'.format(map_content_path))\n        with open(map_content_path, 'rb') as f:\n            map_content = f.read()\n        map_content = attach_request_metadata_to_map_content(map_content, ctx['request_body'])\n        ctx['map_content_gzip_bytes'] = len(gzip.compress(map_content, compresslevel=5))\n        log_progress('map-content-read-done')\n        track_process_rss_kib(ctx)\n\n        common_args = {\n            'ACL': 'public-read', 'ContentEncoding': 'gzip',\n            'CacheControl': 'max-age=8640000', 'StorageClass': 'GLACIER_IR',\n        }\n\n        # Put the augmented request to S3\n        ctx['current_stage'] = 'prepare-upload'\n        json_object_name = ctx['info_object_name']\n        ctx['map_content_key'] = ctx['name_base'] + '.map-content.json'\n        info = build_info_payload(\n            ctx['request_body'],\n            meta,\n            status_payload={ 'progress': STATUS_PROGRESS_UPLOADING_PRIMARY }\n        )\n        ctx['stl_gzip_bytes'] = len(gzip_file_to_bytes(\n            artifacts['stl_path'],\n            compresslevel=5,\n            rss_tracker=functools.partial(track_process_rss_kib, ctx)\n        ))\n        track_process_rss_kib(ctx)\n\n        # Upload primary assets\n        ctx['current_stage'] = 'upload-primary'\n        upload_primary_start_time = time_clock()\n        log_progress('upload-primary-start')\n        write_status_info_json(ctx, STATUS_PROGRESS_UPLOADING_PRIMARY)\n        try:\n            upload_primary_assets(\n                bucket,\n                json_object_name,\n                info,\n                ctx['name_base'],\n                ctx['map_object_name'],\n                map_content,\n                artifacts['stl_path'],\n                common_args,\n                rss_tracker=functools.partial(track_process_rss_kib, ctx),\n                progress_logger=ctx['progress_logger']\n            )\n        finally:\n            ctx['timing_upload_primary_seconds'] = duration_since(upload_primary_start_time)\n        log_progress('upload-primary-done')\n        map_content = None\n        track_process_rss_kib(ctx)\n\n        # Mark map as ready for client polling\n        info['status'] = { 'progress': STATUS_PROGRESS_DONE }\n        write_info_json(bucket, json_object_name, info)\n        ctx['status_progress'] = STATUS_PROGRESS_DONE\n\n        # Create PDF from SVG and put it to S3\n        ctx['current_stage'] = 'svg-to-pdf'\n        svg_to_pdf_start_time = time_clock()\n        log_progress('svg-to-pdf-start')\n        pdf_path = os.path.join(os.path.dirname(osm_path), 'map.pdf')\n        ctx['rss_svg_to_pdf_kib'] = svg_to_pdf(artifacts['svg_path'], pdf_path)\n        ctx['timing_svg_to_pdf_seconds'] = duration_since(svg_to_pdf_start_time)\n        log_progress('svg-to-pdf-done')\n        track_process_rss_kib(ctx)\n\n        # Upload secondary assets\n        ctx['current_stage'] = 'upload-secondary'\n        log_progress('upload-secondary-start')\n        upload_secondary_assets(\n            bucket,\n            ctx['name_base'],\n            artifacts['svg_path'],\n            pdf_path,\n            artifacts['stl_ways_path'],\n            artifacts['stl_rest_path'],\n            artifacts['blend_path'],\n            common_args,\n            rss_tracker=functools.partial(track_process_rss_kib, ctx),\n            progress_logger=ctx['progress_logger']\n        )\n        log_progress('upload-secondary-done')\n        track_process_rss_kib(ctx)\n\n        print(\"Processing entire request took \" + str(time_clock() - ctx['main_start_time']))\n        log_progress('complete', status='success')\n        ctx['status'] = 'success'\n    except BaseException as e:\n        track_process_rss_kib(ctx)\n        handle_main_exception(ctx, e)\n    finally:\n        write_final_stats_if_possible(ctx)\n        rethrow_failure_if_needed(ctx)\n\n# never output anything\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "converter/prune-only-big-roads.js",
    "content": "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('fs');\n\nconst FLAG_ROAD = 1;\nconst FLAG_WATER_AREA = 2;\nconst FLAG_LINEAR_WATERWAY = 4;\nconst FLAG_RAIL_TRACK = 8;\n\nconst MEMBER_TYPE_NODE = 0;\nconst MEMBER_TYPE_WAY = 1;\nconst MEMBER_TYPE_RELATION = 2;\nconst ROAD_NAME_HASH_UNNAMED = -1;\nconst ROAD_SCORE_MIN = -100;\nconst ROAD_SCORE_MAX = 17;\nconst ROAD_SCORE_BUCKET_COUNT = ROAD_SCORE_MAX - ROAD_SCORE_MIN + 1;\n\nconst ROAD_BASE_RANK = {\n  corridor: 0,\n  steps: 1,\n  path: 2,\n  footway: 3,\n  cycleway: 3,\n  bridleway: 3,\n  track: 4,\n  service: 5,\n  living_street: 6,\n  residential: 7,\n  unclassified: 8,\n  tertiary: 9,\n  tertiary_link: 9,\n  secondary: 10,\n  secondary_link: 10,\n  primary: 11,\n  primary_link: 11,\n  trunk: 12,\n  trunk_link: 12,\n  motorway: 13,\n  motorway_link: 13,\n};\n\nconst NON_TRACK_RAILWAY_VALUES = {\n  platform: true,\n  platform_edge: true,\n  station: true,\n  halt: true,\n  tram_stop: true,\n  subway_entrance: true,\n  crossing: true,\n  level_crossing: true,\n  signal: true,\n  switch: true,\n  buffer_stop: true,\n};\n\nfunction usage() {\n  return [\n    'Usage:',\n    '  prune-only-big-roads.js --osm <path> --lon-min <n> --lat-min <n> --lon-max <n> --lat-max <n> --print-size-cm <n> --map-scale <n> [--target-road-density <n>] [--output <path>]',\n  ].join('\\n');\n}\n\nfunction parseArgs(argv) {\n  const out = {\n    osm: null,\n    output: null,\n    lonMin: null,\n    latMin: null,\n    lonMax: null,\n    latMax: null,\n    printSizeCm: null,\n    mapScale: null,\n    targetRoadDensity: 1.2,\n  };\n\n  function takeValue(i) {\n    if (i + 1 >= argv.length) {\n      throw new Error('Missing value for ' + argv[i]);\n    }\n    return argv[i + 1];\n  }\n\n  for (let i = 2; i < argv.length; i += 1) {\n    const arg = argv[i];\n    if (arg === '--osm') {\n      out.osm = takeValue(i);\n      i += 1;\n    } else if (arg === '--output') {\n      out.output = takeValue(i);\n      i += 1;\n    } else if (arg === '--lon-min') {\n      out.lonMin = Number(takeValue(i));\n      i += 1;\n    } else if (arg === '--lat-min') {\n      out.latMin = Number(takeValue(i));\n      i += 1;\n    } else if (arg === '--lon-max') {\n      out.lonMax = Number(takeValue(i));\n      i += 1;\n    } else if (arg === '--lat-max') {\n      out.latMax = Number(takeValue(i));\n      i += 1;\n    } else if (arg === '--print-size-cm') {\n      out.printSizeCm = Number(takeValue(i));\n      i += 1;\n    } else if (arg === '--map-scale') {\n      out.mapScale = Number(takeValue(i));\n      i += 1;\n    } else if (arg === '--target-road-density') {\n      out.targetRoadDensity = Number(takeValue(i));\n      i += 1;\n    } else if (arg === '--help' || arg === '-h') {\n      throw new Error(usage());\n    } else {\n      throw new Error('Unknown argument: ' + arg + '\\n' + usage());\n    }\n  }\n\n  if (!out.osm) {\n    throw new Error('--osm is required\\n' + usage());\n  }\n  if (!Number.isFinite(out.lonMin) || !Number.isFinite(out.latMin) || !Number.isFinite(out.lonMax) || !Number.isFinite(out.latMax)) {\n    throw new Error('All bounds args are required and must be numeric\\n' + usage());\n  }\n  if (!Number.isFinite(out.printSizeCm) || out.printSizeCm <= 0) {\n    throw new Error('--print-size-cm must be a positive number');\n  }\n  if (!Number.isFinite(out.mapScale) || out.mapScale <= 0) {\n    throw new Error('--map-scale must be a positive number');\n  }\n  if (!Number.isFinite(out.targetRoadDensity) || out.targetRoadDensity < 0) {\n    throw new Error('--target-road-density must be a non-negative number');\n  }\n  if (!out.output) {\n    out.output = out.osm;\n  }\n  return out;\n}\n\nfunction decodeXmlEntities(value) {\n  if (value === null || value === undefined) {\n    return '';\n  }\n  return String(value)\n    .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))\n    .replace(/&#([0-9]+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)))\n    .replace(/&quot;/g, '\"')\n    .replace(/&apos;/g, \"'\")\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>')\n    .replace(/&amp;/g, '&');\n}\n\nfunction escapeXmlAttr(value) {\n  return String(value)\n    .replace(/&/g, '&amp;')\n    .replace(/\"/g, '&quot;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/'/g, '&apos;');\n}\n\nfunction parseTagToken(raw) {\n  let text = String(raw || '').trim();\n  if (!text) {\n    return null;\n  }\n  if (text.startsWith('?')) {\n    return null;\n  }\n  if (text.startsWith('!--')) {\n    return null;\n  }\n  if (text.startsWith('!')) {\n    return null;\n  }\n\n  let closing = false;\n  if (text[0] === '/') {\n    closing = true;\n    text = text.slice(1).trim();\n  }\n\n  let selfClosing = false;\n  if (!closing && text.endsWith('/')) {\n    selfClosing = true;\n    text = text.slice(0, -1).trim();\n  }\n\n  if (!text) {\n    return null;\n  }\n\n  const spaceIdx = text.search(/\\s/);\n  const name = spaceIdx === -1 ? text : text.slice(0, spaceIdx);\n  const attrsText = spaceIdx === -1 ? '' : text.slice(spaceIdx + 1);\n  const attrs = {};\n\n  if (!closing && attrsText) {\n    const attrRe = /([^\\s=\\/]+)\\s*=\\s*(\"([^\"]*)\"|'([^']*)')/g;\n    let m;\n    while ((m = attrRe.exec(attrsText)) !== null) {\n      const key = m[1];\n      const value = decodeXmlEntities(m[3] !== undefined ? m[3] : m[4]);\n      attrs[key] = value;\n    }\n  }\n\n  return {\n    name,\n    attrs,\n    closing,\n    selfClosing,\n  };\n}\n\nfunction findXmlTokenEnd(buffer, startIndex) {\n  if (startIndex < 0 || startIndex >= buffer.length || buffer[startIndex] !== '<') {\n    return {\n      complete: false,\n      kind: 'tag',\n      endIndex: -1,\n    };\n  }\n\n  if (buffer.startsWith('<!--', startIndex)) {\n    const endIdx = buffer.indexOf('-->', startIndex + 4);\n    if (endIdx === -1) {\n      return { complete: false, kind: 'comment', endIndex: -1 };\n    }\n    return { complete: true, kind: 'comment', endIndex: endIdx + 2 };\n  }\n\n  if (buffer.startsWith('<![CDATA[', startIndex)) {\n    const endIdx = buffer.indexOf(']]>', startIndex + 9);\n    if (endIdx === -1) {\n      return { complete: false, kind: 'cdata', endIndex: -1 };\n    }\n    return { complete: true, kind: 'cdata', endIndex: endIdx + 2 };\n  }\n\n  if (buffer.startsWith('<?', startIndex)) {\n    const endIdx = buffer.indexOf('?>', startIndex + 2);\n    if (endIdx === -1) {\n      return { complete: false, kind: 'processing-instruction', endIndex: -1 };\n    }\n    return { complete: true, kind: 'processing-instruction', endIndex: endIdx + 1 };\n  }\n\n  if (buffer.startsWith('<!', startIndex)) {\n    let quote = null;\n    let bracketDepth = 0;\n    for (let i = startIndex + 2; i < buffer.length; i += 1) {\n      const ch = buffer[i];\n      if (quote !== null) {\n        if (ch === quote) {\n          quote = null;\n        }\n        continue;\n      }\n\n      if (ch === '\\\"' || ch === '\\'') {\n        quote = ch;\n        continue;\n      }\n      if (ch === '[') {\n        bracketDepth += 1;\n        continue;\n      }\n      if (ch === ']') {\n        if (bracketDepth > 0) {\n          bracketDepth -= 1;\n        }\n        continue;\n      }\n      if (ch === '>' && bracketDepth === 0) {\n        return { complete: true, kind: 'declaration', endIndex: i };\n      }\n    }\n    return { complete: false, kind: 'declaration', endIndex: -1 };\n  }\n\n  let quote = null;\n  for (let i = startIndex + 1; i < buffer.length; i += 1) {\n    const ch = buffer[i];\n    if (quote !== null) {\n      if (ch === quote) {\n        quote = null;\n      }\n      continue;\n    }\n    if (ch === '\\\"' || ch === '\\'') {\n      quote = ch;\n      continue;\n    }\n    if (ch === '>') {\n      return { complete: true, kind: 'tag', endIndex: i };\n    }\n  }\n  return { complete: false, kind: 'tag', endIndex: -1 };\n}\n\nfunction malformedXmlError(kind) {\n  if (kind === 'comment') {\n    return new Error('Malformed XML: unterminated comment');\n  }\n  if (kind === 'cdata') {\n    return new Error('Malformed XML: unterminated CDATA');\n  }\n  if (kind === 'processing-instruction') {\n    return new Error('Malformed XML: unterminated processing instruction');\n  }\n  if (kind === 'declaration') {\n    return new Error('Malformed XML: unterminated declaration/doctype');\n  }\n  return new Error('Malformed XML: unterminated tag');\n}\n\nfunction streamParseXml(xmlSource, onToken) {\n  return new Promise((resolve, reject) => {\n    let buffer = '';\n\n    function consume(finalChunk) {\n      while (true) {\n        const lt = buffer.indexOf('<');\n        if (lt === -1) {\n          buffer = finalChunk ? '' : buffer;\n          return;\n        }\n        if (lt > 0) {\n          buffer = buffer.slice(lt);\n        }\n\n        const tokenEnd = findXmlTokenEnd(buffer, 0);\n        if (!tokenEnd.complete) {\n          if (finalChunk) {\n            throw malformedXmlError(tokenEnd.kind);\n          }\n          return;\n        }\n        const endIndex = tokenEnd.endIndex;\n\n        if (tokenEnd.kind !== 'tag') {\n          buffer = buffer.slice(endIndex + 1);\n          continue;\n        }\n\n        const tokenRaw = buffer.slice(1, endIndex);\n        buffer = buffer.slice(endIndex + 1);\n\n        const token = parseTagToken(tokenRaw);\n        if (token !== null) {\n          onToken(token);\n        }\n      }\n    }\n\n    function onChunk(chunk) {\n      buffer += String(chunk);\n      try {\n        consume(false);\n      } catch (err) {\n        throw err;\n      }\n    }\n\n    function onEnd() {\n      consume(true);\n      resolve();\n    }\n\n    if (Array.isArray(xmlSource)) {\n      try {\n        for (let i = 0; i < xmlSource.length; i += 1) {\n          onChunk(xmlSource[i]);\n        }\n        onEnd();\n      } catch (err) {\n        reject(err);\n      }\n      return;\n    }\n\n    const stream = fs.createReadStream(xmlSource, { encoding: 'utf8' });\n    stream.on('data', (chunk) => {\n      try {\n        onChunk(chunk);\n      } catch (err) {\n        stream.destroy(err);\n      }\n    });\n\n    stream.on('end', () => {\n      try {\n        onEnd();\n      } catch (err) {\n        reject(err);\n      }\n    });\n\n    stream.on('error', reject);\n  });\n}\n\nfunction tokenToSignature(token) {\n  const attrs = token.attrs || {};\n  const keys = Object.keys(attrs).sort();\n  const attrText = keys.map((key) => key + '=' + String(attrs[key])).join(',');\n  return (\n    (token.closing ? '/' : '') +\n    token.name +\n    (token.selfClosing ? '/' : '') +\n    (attrText ? '{' + attrText + '}' : '')\n  );\n}\n\nfunction assertEqualArray(label, got, expected) {\n  if (got.length !== expected.length) {\n    throw new Error(\n      'streamParseXml self-test ' + label + ': token count mismatch got=' + got.length + ' expected=' + expected.length +\n      ' got=' + JSON.stringify(got) + ' expected=' + JSON.stringify(expected)\n    );\n  }\n  for (let i = 0; i < got.length; i += 1) {\n    if (got[i] !== expected[i]) {\n      throw new Error(\n        'streamParseXml self-test ' + label + ': mismatch at index ' + i +\n        ' got=' + got[i] + ' expected=' + expected[i]\n      );\n    }\n  }\n}\n\nasync function runStreamParseXmlSelfTest() {\n  const cases = [\n    {\n      label: 'normal-osm-tags',\n      chunks: [\n        '<osm version=\\\"0.6\\\">',\n        '<node id=\\\"1\\\" lat=\\\"60.1\\\" lon=\\\"24.9\\\"/>',\n        '<way id=\\\"2\\\"><nd ref=\\\"1\\\"/><tag k=\\\"highway\\\" v=\\\"residential\\\"/></way>',\n        '</osm>',\n      ],\n      expected: [\n        'osm{version=0.6}',\n        'node/{id=1,lat=60.1,lon=24.9}',\n        'way{id=2}',\n        'nd/{ref=1}',\n        'tag/{k=highway,v=residential}',\n        '/way',\n        '/osm',\n      ],\n    },\n    {\n      label: 'comment-with-angle-brackets',\n      chunks: [\n        '<osm>',\n        '<!-- a comment with > and < and <<>> -->',\n        '<node id=\\\"1\\\"/>',\n        '</osm>',\n      ],\n      expected: [\n        'osm',\n        'node/{id=1}',\n        '/osm',\n      ],\n    },\n    {\n      label: 'quoted-attribute-with-split-chunks',\n      chunks: [\n        '<osm><node id=\\\"1\\\" note=\\\"left ',\n        '&gt; right\\\" name=\\\"a',\n        '>b\\\" /></osm>',\n      ],\n      expected: [\n        'osm',\n        'node/{id=1,name=a>b,note=left > right}',\n        '/osm',\n      ],\n    },\n    {\n      label: 'cdata-with-tag-text',\n      chunks: [\n        '<osm>',\n        '<![CDATA[<tag k=\\\"x\\\" v=\\\"y\\\">not-a-tag</tag>]]>',\n        '<way id=\\\"9\\\"/>',\n        '</osm>',\n      ],\n      expected: [\n        'osm',\n        'way/{id=9}',\n        '/osm',\n      ],\n    },\n  ];\n\n  for (let i = 0; i < cases.length; i += 1) {\n    const testCase = cases[i];\n    const got = [];\n    await streamParseXml(testCase.chunks, (token) => {\n      got.push(tokenToSignature(token));\n    });\n    assertEqualArray(testCase.label, got, testCase.expected);\n  }\n\n  console.log('streamParseXml self-test passed: ' + cases.length + ' cases');\n}\n\nfunction parseOsmId(raw) {\n  if (raw === null || raw === undefined || raw === '') {\n    return null;\n  }\n  const n = Number(raw);\n  if (!Number.isFinite(n)) {\n    return null;\n  }\n  return n;\n}\n\nfunction createState() {\n  return {\n    rootAttrs: null,\n    nodeIdToIx: new Map(),\n    nodeIdByIx: [],\n    nodeLat: null,\n    nodeLon: null,\n    nodeHasCoord: null,\n    nodeTags: null,\n\n    wayIdToIx: new Map(),\n    wayIds: [],\n    wayFlags: [],\n    wayRank: [],\n    wayRoadNameHash: [],\n    wayRefStart: [],\n    wayRefLen: [],\n    wayTags: [],\n    wayNodeRefIxFlat: [],\n\n    relationIdToIx: new Map(),\n    relationIds: [],\n    relationIsWaterTag: [],\n    relationTags: [],\n    relationMembers: [],\n\n    keepWay: null,\n    keptRoadWay: null,\n    keptRailWay: null,\n    keepNode: null,\n    keepRelation: null,\n  };\n}\n\nfunction finalizeWayIntoState(state, wayData) {\n  const wayTagsMap = wayTagPairsToMap(wayData.tags);\n  let flags = 0;\n  let rank = 0;\n  const highway = wayTagsMap.highway;\n  const linearWaterway = hasLinearWaterwayTags(wayTagsMap);\n  const railTrack = hasRailTrackWayTags(wayTagsMap);\n\n  if (linearWaterway) {\n    flags |= FLAG_LINEAR_WATERWAY;\n  }\n  if (highway !== null && highway !== undefined && highway !== '' && !linearWaterway) {\n    flags |= FLAG_ROAD;\n    rank = baseRoadRank(highway);\n  }\n  if (hasWaterAreaTags(wayTagsMap)) {\n    flags |= FLAG_WATER_AREA;\n  }\n  if (railTrack) {\n    flags |= FLAG_RAIL_TRACK;\n  }\n\n  const wayIx = state.wayIds.length;\n  state.wayIdToIx.set(wayData.id, wayIx);\n  state.wayIds.push(wayData.id);\n  state.wayFlags.push(flags);\n  state.wayRank.push(rank);\n  state.wayRoadNameHash.push(roadNameHashFromWayTags(wayTagsMap, (flags & FLAG_ROAD) !== 0));\n  state.wayRefStart.push(wayData.refStart);\n  state.wayRefLen.push(wayData.refLen);\n  state.wayTags.push(wayData.tags);\n}\n\nfunction ensureNodeIx(state, nodeId) {\n  let ix = state.nodeIdToIx.get(nodeId);\n  if (ix !== undefined) {\n    return ix;\n  }\n  ix = state.nodeIdByIx.length;\n  state.nodeIdToIx.set(nodeId, ix);\n  state.nodeIdByIx.push(nodeId);\n  return ix;\n}\n\nfunction hasWaterAreaTags(tags) {\n  return (\n    tags.natural === 'water' ||\n    (Object.prototype.hasOwnProperty.call(tags, 'water') && tags.water !== null && tags.water !== '') ||\n    tags.landuse === 'reservoir' ||\n    tags.waterway === 'riverbank'\n  );\n}\n\nfunction hasLinearWaterwayTags(tags) {\n  const raw = tags.waterway;\n  if (raw === null || raw === undefined) {\n    return false;\n  }\n  const normalized = String(raw).trim().toLowerCase();\n  return normalized !== '' && normalized !== 'riverbank';\n}\n\nfunction hasRailTrackWayTags(tags) {\n  const raw = tags.railway;\n  if (raw === null || raw === undefined) {\n    return false;\n  }\n  const normalized = String(raw).trim().toLowerCase();\n  if (normalized === '') {\n    return false;\n  }\n  return !Object.prototype.hasOwnProperty.call(NON_TRACK_RAILWAY_VALUES, normalized);\n}\n\nfunction clampRoadScore(score) {\n  const n = Math.round(Number(score));\n  if (!Number.isFinite(n)) {\n    return ROAD_SCORE_MIN;\n  }\n  if (n < ROAD_SCORE_MIN) {\n    return ROAD_SCORE_MIN;\n  }\n  if (n > ROAD_SCORE_MAX) {\n    return ROAD_SCORE_MAX;\n  }\n  return n;\n}\n\nfunction scoreToBucketIx(score) {\n  return clampRoadScore(score) - ROAD_SCORE_MIN;\n}\n\nfunction bucketIxToScore(bucketIx) {\n  return ROAD_SCORE_MIN + bucketIx;\n}\n\nfunction baseRoadRank(highwayValue) {\n  if (highwayValue === null || highwayValue === undefined) {\n    return ROAD_SCORE_MIN;\n  }\n  const normalized = String(highwayValue).trim().toLowerCase();\n  if (!Object.prototype.hasOwnProperty.call(ROAD_BASE_RANK, normalized)) {\n    return ROAD_SCORE_MIN;\n  }\n  return ROAD_BASE_RANK[normalized];\n}\n\nfunction normalizeRoadNameForGrouping(raw) {\n  if (raw === null || raw === undefined) {\n    return null;\n  }\n  const normalized = String(raw).trim().toLowerCase().replace(/\\s+/g, ' ');\n  if (!normalized) {\n    return null;\n  }\n  return normalized;\n}\n\nfunction hashRoadName32(normalizedRoadName) {\n  // 32-bit hash keeps memory small; rare collisions are acceptable.\n  return hashUpdate(2166136261 >>> 0, normalizedRoadName) >>> 0;\n}\n\nfunction roadNameHashFromWayTags(wayTagsMap, isRoadWay) {\n  if (!isRoadWay) {\n    return ROAD_NAME_HASH_UNNAMED;\n  }\n  const normalizedRoadName = normalizeRoadNameForGrouping(wayTagsMap.name);\n  if (normalizedRoadName === null) {\n    return ROAD_NAME_HASH_UNNAMED;\n  }\n  return hashRoadName32(normalizedRoadName);\n}\n\nfunction computeHaversineM(lat1, lon1, lat2, lon2) {\n  const earthRadiusM = 6371000.0;\n  const phi1 = (lat1 * Math.PI) / 180.0;\n  const phi2 = (lat2 * Math.PI) / 180.0;\n  const deltaPhi = ((lat2 - lat1) * Math.PI) / 180.0;\n  const deltaLambda = ((lon2 - lon1) * Math.PI) / 180.0;\n  const a =\n    Math.sin(deltaPhi / 2.0) * Math.sin(deltaPhi / 2.0) +\n    Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2.0) * Math.sin(deltaLambda / 2.0);\n  const c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(Math.max(0.0, 1.0 - a)));\n  return earthRadiusM * c;\n}\n\nfunction computeTargetRoadLengthMeters(printSizeCm, mapScale, targetRoadDensity) {\n  const printAreaCm2 = printSizeCm * printSizeCm;\n  const targetPrintedRoadLengthCm = targetRoadDensity * printAreaCm2;\n  return targetPrintedRoadLengthCm * mapScale / 100.0;\n}\n\nfunction deriveRemovedRankBuckets(lengthByRank, targetRoadLengthM) {\n  const removed = new Set();\n  if (!(targetRoadLengthM > 0)) {\n    return removed;\n  }\n  let remainingM = 0.0;\n  for (let bucketIx = 0; bucketIx < lengthByRank.length; bucketIx += 1) {\n    remainingM += Number(lengthByRank[bucketIx] || 0.0);\n  }\n\n  for (let bucketIx = 0; bucketIx < lengthByRank.length; bucketIx += 1) {\n    if (remainingM <= targetRoadLengthM) {\n      break;\n    }\n    const bucketM = Number(lengthByRank[bucketIx] || 0.0);\n    if (remainingM - bucketM >= targetRoadLengthM) {\n      removed.add(bucketIxToScore(bucketIx));\n      remainingM -= bucketM;\n    } else {\n      break;\n    }\n  }\n  return removed;\n}\n\nfunction wayTagPairsToMap(tagPairs) {\n  const tags = Object.create(null);\n  for (let i = 0; i < tagPairs.length; i += 1) {\n    const pair = tagPairs[i];\n    tags[pair[0]] = pair[1];\n  }\n  return tags;\n}\n\nfunction memberTypeFromString(typeValue) {\n  if (typeValue === 'node') {\n    return MEMBER_TYPE_NODE;\n  }\n  if (typeValue === 'way') {\n    return MEMBER_TYPE_WAY;\n  }\n  if (typeValue === 'relation') {\n    return MEMBER_TYPE_RELATION;\n  }\n  return null;\n}\n\nfunction memberTypeToString(typeCode) {\n  if (typeCode === MEMBER_TYPE_NODE) {\n    return 'node';\n  }\n  if (typeCode === MEMBER_TYPE_WAY) {\n    return 'way';\n  }\n  if (typeCode === MEMBER_TYPE_RELATION) {\n    return 'relation';\n  }\n  return '';\n}\n\n// Step 1: First pass over OSM XML.\n// Build compact indexes for ways/relations and intern all referenced node IDs,\n// while collecting tags and member/ref lists needed later for ranking and output.\nasync function firstPassCollectIndexes(state, osmPath) {\n  let currentWay = null;\n  let currentRelation = null;\n\n  await streamParseXml(osmPath, (token) => {\n    if (token.name === 'osm' && !token.closing && state.rootAttrs === null) {\n      state.rootAttrs = token.attrs;\n    }\n\n    if (currentWay !== null) {\n      if (!token.closing && token.name === 'nd') {\n        const nodeId = parseOsmId(token.attrs.ref);\n        if (nodeId !== null) {\n          const nodeIx = ensureNodeIx(state, nodeId);\n          state.wayNodeRefIxFlat.push(nodeIx);\n          currentWay.refLen += 1;\n        }\n      } else if (!token.closing && token.name === 'tag') {\n        const key = token.attrs.k;\n        const value = token.attrs.v;\n        if (key !== undefined && value !== undefined) {\n          currentWay.tags.push([key, value]);\n        }\n      }\n\n      if ((token.closing && token.name === 'way') || (token.selfClosing && token.name === 'way')) {\n        finalizeWayIntoState(state, currentWay);\n        currentWay = null;\n      }\n      return;\n    }\n\n    if (currentRelation !== null) {\n      if (!token.closing && token.name === 'member') {\n        const typeCode = memberTypeFromString(token.attrs.type);\n        const refId = parseOsmId(token.attrs.ref);\n        if (typeCode !== null && refId !== null) {\n          if (typeCode === MEMBER_TYPE_NODE) {\n            ensureNodeIx(state, refId);\n          }\n          currentRelation.members.push({\n            type: typeCode,\n            ref: refId,\n            role: token.attrs.role || '',\n          });\n        }\n      } else if (!token.closing && token.name === 'tag') {\n        const key = token.attrs.k;\n        const value = token.attrs.v;\n        if (key !== undefined && value !== undefined) {\n          currentRelation.tags.push([key, value]);\n        }\n      }\n\n      if ((token.closing && token.name === 'relation') || (token.selfClosing && token.name === 'relation')) {\n        const relationTagsMap = wayTagPairsToMap(currentRelation.tags);\n        const relIx = state.relationIds.length;\n        state.relationIdToIx.set(currentRelation.id, relIx);\n        state.relationIds.push(currentRelation.id);\n        state.relationIsWaterTag.push(hasWaterAreaTags(relationTagsMap));\n        state.relationTags.push(currentRelation.tags);\n        state.relationMembers.push(currentRelation.members);\n        currentRelation = null;\n      }\n      return;\n    }\n\n    if (!token.closing && token.name === 'way') {\n      const wayId = parseOsmId(token.attrs.id);\n      if (wayId === null) {\n        return;\n      }\n      currentWay = {\n        id: wayId,\n        tags: [],\n        refStart: state.wayNodeRefIxFlat.length,\n        refLen: 0,\n      };\n      if (token.selfClosing) {\n        finalizeWayIntoState(state, currentWay);\n        currentWay = null;\n      }\n      return;\n    }\n\n    if (!token.closing && token.name === 'relation') {\n      const relationId = parseOsmId(token.attrs.id);\n      if (relationId === null) {\n        return;\n      }\n      currentRelation = {\n        id: relationId,\n        tags: [],\n        members: [],\n      };\n      if (token.selfClosing) {\n        const relationTagsMap = wayTagPairsToMap(currentRelation.tags);\n        const relIx = state.relationIds.length;\n        state.relationIdToIx.set(currentRelation.id, relIx);\n        state.relationIds.push(currentRelation.id);\n        state.relationIsWaterTag.push(hasWaterAreaTags(relationTagsMap));\n        state.relationTags.push(currentRelation.tags);\n        state.relationMembers.push(currentRelation.members);\n        currentRelation = null;\n      }\n    }\n  });\n}\n\n// Step 2: Second pass over OSM XML.\n// Load coordinates (and optional node tags) only for interned node IDs from step 1.\nasync function secondPassLoadNodeCoordinates(state, osmPath) {\n  const nodeCount = state.nodeIdByIx.length;\n  state.nodeLat = new Float64Array(nodeCount);\n  state.nodeLon = new Float64Array(nodeCount);\n  state.nodeHasCoord = new Uint8Array(nodeCount);\n  state.nodeTags = new Array(nodeCount);\n\n  let currentNode = null;\n\n  await streamParseXml(osmPath, (token) => {\n    if (currentNode !== null) {\n      if (!token.closing && token.name === 'tag' && currentNode.ix !== null) {\n        const key = token.attrs.k;\n        const value = token.attrs.v;\n        if (key !== undefined && value !== undefined) {\n          currentNode.tags.push([key, value]);\n        }\n      }\n      if ((token.closing && token.name === 'node') || (token.selfClosing && token.name === 'node')) {\n        if (currentNode.ix !== null && currentNode.tags.length > 0) {\n          state.nodeTags[currentNode.ix] = currentNode.tags;\n        }\n        currentNode = null;\n      }\n      return;\n    }\n\n    if (!token.closing && token.name === 'node') {\n      const nodeId = parseOsmId(token.attrs.id);\n      let ix = null;\n      if (nodeId !== null) {\n        const gotIx = state.nodeIdToIx.get(nodeId);\n        if (gotIx !== undefined) {\n          ix = gotIx;\n          const lat = Number(token.attrs.lat);\n          const lon = Number(token.attrs.lon);\n          if (Number.isFinite(lat) && Number.isFinite(lon)) {\n            state.nodeLat[ix] = lat;\n            state.nodeLon[ix] = lon;\n            state.nodeHasCoord[ix] = 1;\n          }\n        }\n      }\n\n      if (!token.selfClosing) {\n        currentNode = {\n          ix,\n          tags: [],\n        };\n      }\n    }\n  });\n}\n\nfunction clipSegmentToLonLatBounds(aLon, aLat, bLon, bLat, bounds) {\n  const lonMin = Number(bounds.lonMin);\n  const lonMax = Number(bounds.lonMax);\n  const latMin = Number(bounds.latMin);\n  const latMax = Number(bounds.latMax);\n  if (\n    !Number.isFinite(lonMin) || !Number.isFinite(lonMax) ||\n    !Number.isFinite(latMin) || !Number.isFinite(latMax)\n  ) {\n    return null;\n  }\n\n  const dx = bLon - aLon;\n  const dy = bLat - aLat;\n  let t0 = 0.0;\n  let t1 = 1.0;\n  const eps = 1e-12;\n\n  function clipEdge(p, q) {\n    if (Math.abs(p) <= eps) {\n      return q >= 0.0;\n    }\n    const r = q / p;\n    if (p < 0.0) {\n      if (r > t1) {\n        return false;\n      }\n      if (r > t0) {\n        t0 = r;\n      }\n    } else {\n      if (r < t0) {\n        return false;\n      }\n      if (r < t1) {\n        t1 = r;\n      }\n    }\n    return true;\n  }\n\n  if (!clipEdge(-dx, aLon - lonMin)) {\n    return null;\n  }\n  if (!clipEdge(dx, lonMax - aLon)) {\n    return null;\n  }\n  if (!clipEdge(-dy, aLat - latMin)) {\n    return null;\n  }\n  if (!clipEdge(dy, latMax - aLat)) {\n    return null;\n  }\n  if (t1 < t0) {\n    return null;\n  }\n\n  return {\n    lon0: aLon + t0 * dx,\n    lat0: aLat + t0 * dy,\n    lon1: aLon + t1 * dx,\n    lat1: aLat + t1 * dy,\n  };\n}\n\nfunction segmentLengthWithinBoundsMeters(aLon, aLat, bLon, bLat, bounds) {\n  const clipped = clipSegmentToLonLatBounds(aLon, aLat, bLon, bLat, bounds);\n  if (clipped === null) {\n    return 0.0;\n  }\n  return computeHaversineM(clipped.lat0, clipped.lon0, clipped.lat1, clipped.lon1);\n}\n\nfunction computeWayLengthMetersWithinBounds(state, wayIx, bounds) {\n  const start = state.wayRefStart[wayIx];\n  const len = state.wayRefLen[wayIx];\n  let total = 0.0;\n  let prevIx = null;\n\n  for (let i = start; i < start + len; i += 1) {\n    const nodeIx = state.wayNodeRefIxFlat[i];\n    if (state.nodeHasCoord[nodeIx] !== 1) {\n      prevIx = null;\n      continue;\n    }\n    if (prevIx !== null) {\n      total += segmentLengthWithinBoundsMeters(\n        state.nodeLon[prevIx],\n        state.nodeLat[prevIx],\n        state.nodeLon[nodeIx],\n        state.nodeLat[nodeIx],\n        bounds\n      );\n    }\n    prevIx = nodeIx;\n  }\n\n  return total;\n}\n\n// Step 3: Build road-meter totals by rank and decide all-or-none bucket pruning.\n// This uses base-rank ways and printout-area road-density target to compute removed buckets.\nfunction thirdStepComputePruningDecision(state, bounds, printSizeCm, mapScale, targetRoadDensity) {\n  const wayCount = state.wayIds.length;\n  const lengthByRank = new Float64Array(ROAD_SCORE_BUCKET_COUNT);\n  const roadGroupLength = [];\n  const roadGroupMaxRank = [];\n  const roadGroupIxByNameHash = new Map();\n  const wayRoadGroupIx = new Int32Array(wayCount);\n  const roadWayLengthWithinBounds = new Float64Array(wayCount);\n  wayRoadGroupIx.fill(-1);\n\n  for (let wayIx = 0; wayIx < wayCount; wayIx += 1) {\n    if ((state.wayFlags[wayIx] & FLAG_ROAD) === 0) {\n      continue;\n    }\n    const rank = state.wayRank[wayIx];\n    const nameHash = state.wayRoadNameHash[wayIx];\n    const wayLengthMeters = computeWayLengthMetersWithinBounds(state, wayIx, bounds);\n    roadWayLengthWithinBounds[wayIx] = wayLengthMeters;\n\n    let roadGroupIx;\n    if (nameHash === ROAD_NAME_HASH_UNNAMED) {\n      roadGroupIx = roadGroupLength.length;\n      roadGroupLength.push(0.0);\n      roadGroupMaxRank.push(rank);\n    } else {\n      roadGroupIx = roadGroupIxByNameHash.get(nameHash);\n      if (roadGroupIx === undefined) {\n        roadGroupIx = roadGroupLength.length;\n        roadGroupIxByNameHash.set(nameHash, roadGroupIx);\n        roadGroupLength.push(0.0);\n        roadGroupMaxRank.push(rank);\n      }\n    }\n\n    roadGroupLength[roadGroupIx] += wayLengthMeters;\n    if (rank > roadGroupMaxRank[roadGroupIx]) {\n      roadGroupMaxRank[roadGroupIx] = rank;\n    }\n    wayRoadGroupIx[wayIx] = roadGroupIx;\n  }\n\n  for (let roadGroupIx = 0; roadGroupIx < roadGroupLength.length; roadGroupIx += 1) {\n    const groupRank = roadGroupMaxRank[roadGroupIx];\n    lengthByRank[scoreToBucketIx(groupRank)] += roadGroupLength[roadGroupIx];\n  }\n\n  const targetRoadLengthM = computeTargetRoadLengthMeters(printSizeCm, mapScale, targetRoadDensity);\n  const removedRanks = deriveRemovedRankBuckets(lengthByRank, targetRoadLengthM);\n\n  state.keepWay = new Uint8Array(wayCount);\n  state.keptRoadWay = new Uint8Array(wayCount);\n  state.keptRailWay = new Uint8Array(wayCount);\n\n  for (let wayIx = 0; wayIx < wayCount; wayIx += 1) {\n    if ((state.wayFlags[wayIx] & FLAG_ROAD) !== 0) {\n      const roadGroupIx = wayRoadGroupIx[wayIx];\n      const roadGroupRank = roadGroupIx >= 0 ? roadGroupMaxRank[roadGroupIx] : state.wayRank[wayIx];\n      if (!removedRanks.has(roadGroupRank) && roadWayLengthWithinBounds[wayIx] > 0.0) {\n        state.keepWay[wayIx] = 1;\n        state.keptRoadWay[wayIx] = 1;\n      }\n    }\n    if ((state.wayFlags[wayIx] & FLAG_WATER_AREA) !== 0) {\n      state.keepWay[wayIx] = 1;\n    }\n    if ((state.wayFlags[wayIx] & FLAG_RAIL_TRACK) !== 0) {\n      state.keepWay[wayIx] = 1;\n      state.keptRailWay[wayIx] = 1;\n    }\n  }\n}\n\n// Step 4: Apply relation-driven keep logic.\n// Keep road relations that reference kept roads, keep water relations recursively,\n// and include relation-member ways/nodes needed by those kept relations.\nfunction fourthStepApplyRelationKeepLogic(state) {\n  const keepRelation = new Set();\n  const waterRelationClosure = new Set();\n  const keepNodeFromRelations = new Uint8Array(state.nodeIdByIx.length);\n\n  for (let relIx = 0; relIx < state.relationIds.length; relIx += 1) {\n    const relId = state.relationIds[relIx];\n    if (state.relationIsWaterTag[relIx]) {\n      keepRelation.add(relId);\n      waterRelationClosure.add(relId);\n    }\n\n    const members = state.relationMembers[relIx];\n    for (let j = 0; j < members.length; j += 1) {\n      const member = members[j];\n      if (member.type !== MEMBER_TYPE_WAY) {\n        continue;\n      }\n      const wayIx = state.wayIdToIx.get(member.ref);\n      if (wayIx === undefined) {\n        continue;\n      }\n      if (state.keptRoadWay[wayIx] === 1 || (state.keptRailWay && state.keptRailWay[wayIx] === 1)) {\n        keepRelation.add(relId);\n        break;\n      }\n    }\n  }\n\n  const queue = Array.from(waterRelationClosure);\n  for (let qi = 0; qi < queue.length; qi += 1) {\n    const relId = queue[qi];\n    const relIx = state.relationIdToIx.get(relId);\n    if (relIx === undefined) {\n      continue;\n    }\n    const members = state.relationMembers[relIx];\n\n    for (let j = 0; j < members.length; j += 1) {\n      const member = members[j];\n      if (member.type === MEMBER_TYPE_NODE) {\n        const nodeIx = state.nodeIdToIx.get(member.ref);\n        if (nodeIx !== undefined) {\n          keepNodeFromRelations[nodeIx] = 1;\n        }\n      } else if (member.type === MEMBER_TYPE_WAY) {\n        const wayIx = state.wayIdToIx.get(member.ref);\n        if (wayIx !== undefined) {\n          const isLinearWaterway = (state.wayFlags[wayIx] & FLAG_LINEAR_WATERWAY) !== 0;\n          if (!isLinearWaterway) {\n            state.keepWay[wayIx] = 1;\n          }\n        }\n      } else if (member.type === MEMBER_TYPE_RELATION) {\n        if (!waterRelationClosure.has(member.ref)) {\n          waterRelationClosure.add(member.ref);\n          keepRelation.add(member.ref);\n          queue.push(member.ref);\n        }\n      }\n    }\n  }\n\n  for (const relId of keepRelation) {\n    const relIx = state.relationIdToIx.get(relId);\n    if (relIx === undefined) {\n      continue;\n    }\n    const members = state.relationMembers[relIx];\n    for (let j = 0; j < members.length; j += 1) {\n      const member = members[j];\n      if (member.type === MEMBER_TYPE_NODE) {\n        const nodeIx = state.nodeIdToIx.get(member.ref);\n        if (nodeIx !== undefined) {\n          keepNodeFromRelations[nodeIx] = 1;\n        }\n      }\n    }\n  }\n\n  state.keepRelation = keepRelation;\n  return keepNodeFromRelations;\n}\n\n// Step 5: Mark nodes required by all kept ways and kept relation-node members.\n// This produces the final node keep-mask for output writing.\nfunction fifthStepMarkKeptNodes(state, keepNodeFromRelations) {\n  const keepNode = new Uint8Array(state.nodeIdByIx.length);\n  for (let i = 0; i < keepNodeFromRelations.length; i += 1) {\n    if (keepNodeFromRelations[i] === 1) {\n      keepNode[i] = 1;\n    }\n  }\n\n  for (let wayIx = 0; wayIx < state.wayIds.length; wayIx += 1) {\n    if (state.keepWay[wayIx] !== 1) {\n      continue;\n    }\n    const start = state.wayRefStart[wayIx];\n    const len = state.wayRefLen[wayIx];\n    for (let j = start; j < start + len; j += 1) {\n      const nodeIx = state.wayNodeRefIxFlat[j];\n      keepNode[nodeIx] = 1;\n    }\n  }\n\n  state.keepNode = keepNode;\n}\n\nfunction formatBoundsXml(bounds) {\n  return (\n    '  <bounds minlat=\"' + escapeXmlAttr(bounds.latMin) +\n    '\" minlon=\"' + escapeXmlAttr(bounds.lonMin) +\n    '\" maxlat=\"' + escapeXmlAttr(bounds.latMax) +\n    '\" maxlon=\"' + escapeXmlAttr(bounds.lonMax) +\n    '\"/>'\n  );\n}\n\nfunction formatTagXml(indent, key, value) {\n  return indent + '<tag k=\"' + escapeXmlAttr(key) + '\" v=\"' + escapeXmlAttr(value) + '\"/>';\n}\n\nfunction formatNodeXml(state, nodeIx) {\n  const id = state.nodeIdByIx[nodeIx];\n  const lat = state.nodeLat[nodeIx];\n  const lon = state.nodeLon[nodeIx];\n  const tags = state.nodeTags[nodeIx] || [];\n\n  if (tags.length === 0) {\n    return '  <node id=\"' + id + '\" lat=\"' + lat + '\" lon=\"' + lon + '\"/>';\n  }\n\n  const lines = [];\n  lines.push('  <node id=\"' + id + '\" lat=\"' + lat + '\" lon=\"' + lon + '\">');\n  for (let i = 0; i < tags.length; i += 1) {\n    lines.push(formatTagXml('    ', tags[i][0], tags[i][1]));\n  }\n  lines.push('  </node>');\n  return lines.join('\\n');\n}\n\nfunction formatWayXml(state, wayIx) {\n  const wayId = state.wayIds[wayIx];\n  const tags = state.wayTags[wayIx];\n  const start = state.wayRefStart[wayIx];\n  const len = state.wayRefLen[wayIx];\n\n  const lines = [];\n  lines.push('  <way id=\"' + wayId + '\">');\n  for (let i = start; i < start + len; i += 1) {\n    const nodeIx = state.wayNodeRefIxFlat[i];\n    const nodeId = state.nodeIdByIx[nodeIx];\n    lines.push('    <nd ref=\"' + nodeId + '\"/>');\n  }\n  for (let i = 0; i < tags.length; i += 1) {\n    lines.push(formatTagXml('    ', tags[i][0], tags[i][1]));\n  }\n  lines.push('  </way>');\n  return lines.join('\\n');\n}\n\nfunction isRelationMemberKept(state, member) {\n  if (member.type === MEMBER_TYPE_NODE) {\n    const nodeIx = state.nodeIdToIx.get(member.ref);\n    if (nodeIx === undefined) {\n      return false;\n    }\n    return state.keepNode[nodeIx] === 1 && state.nodeHasCoord[nodeIx] === 1;\n  }\n  if (member.type === MEMBER_TYPE_WAY) {\n    const wayIx = state.wayIdToIx.get(member.ref);\n    if (wayIx === undefined) {\n      return false;\n    }\n    return state.keepWay[wayIx] === 1;\n  }\n  if (member.type === MEMBER_TYPE_RELATION) {\n    return state.keepRelation.has(member.ref);\n  }\n  return false;\n}\n\nfunction formatRelationXml(state, relIx) {\n  const relId = state.relationIds[relIx];\n  const tags = state.relationTags[relIx];\n  const members = state.relationMembers[relIx];\n\n  const lines = [];\n  lines.push('  <relation id=\"' + relId + '\">');\n  for (let i = 0; i < members.length; i += 1) {\n    const m = members[i];\n    if (!isRelationMemberKept(state, m)) {\n      continue;\n    }\n    const typeString = memberTypeToString(m.type);\n    if (!typeString) {\n      continue;\n    }\n    lines.push(\n      '    <member type=\"' + typeString +\n      '\" ref=\"' + m.ref +\n      '\" role=\"' + escapeXmlAttr(m.role || '') +\n      '\"/>'\n    );\n  }\n  for (let i = 0; i < tags.length; i += 1) {\n    lines.push(formatTagXml('    ', tags[i][0], tags[i][1]));\n  }\n  lines.push('  </relation>');\n  return lines.join('\\n');\n}\n\nfunction writeText(stream, text) {\n  return new Promise((resolve, reject) => {\n    stream.write(text, (err) => {\n      if (err) {\n        reject(err);\n      } else {\n        resolve();\n      }\n    });\n  });\n}\n\nfunction endStream(stream) {\n  return new Promise((resolve, reject) => {\n    stream.end(() => resolve());\n    stream.on('error', reject);\n  });\n}\n\n// Step 6: Write pruned output OSM XML from compact indexed structures.\n// Emit kept nodes/ways/relations and refreshed bounds without reloading full XML trees.\nasync function sixthStepWritePrunedOsm(state, bounds, outputPath) {\n  const stream = fs.createWriteStream(outputPath, { encoding: 'utf8' });\n\n  let rootAttrs = state.rootAttrs;\n  if (!rootAttrs) {\n    rootAttrs = { version: '0.6', generator: 'touch-mapper-prune-only-big-roads' };\n  }\n  if (!Object.prototype.hasOwnProperty.call(rootAttrs, 'version')) {\n    rootAttrs.version = '0.6';\n  }\n  if (!Object.prototype.hasOwnProperty.call(rootAttrs, 'generator')) {\n    rootAttrs.generator = 'touch-mapper-prune-only-big-roads';\n  }\n\n  const rootAttrParts = [];\n  const rootKeys = Object.keys(rootAttrs);\n  for (let i = 0; i < rootKeys.length; i += 1) {\n    const key = rootKeys[i];\n    rootAttrParts.push(' ' + key + '=\"' + escapeXmlAttr(rootAttrs[key]) + '\"');\n  }\n\n  await writeText(stream, '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n');\n  await writeText(stream, '<osm' + rootAttrParts.join('') + '>\\n');\n  await writeText(stream, formatBoundsXml(bounds) + '\\n');\n\n  for (let nodeIx = 0; nodeIx < state.nodeIdByIx.length; nodeIx += 1) {\n    if (state.keepNode[nodeIx] !== 1) {\n      continue;\n    }\n    if (state.nodeHasCoord[nodeIx] !== 1) {\n      continue;\n    }\n    await writeText(stream, formatNodeXml(state, nodeIx) + '\\n');\n  }\n\n  for (let wayIx = 0; wayIx < state.wayIds.length; wayIx += 1) {\n    if (state.keepWay[wayIx] !== 1) {\n      continue;\n    }\n    await writeText(stream, formatWayXml(state, wayIx) + '\\n');\n  }\n\n  for (let relIx = 0; relIx < state.relationIds.length; relIx += 1) {\n    const relId = state.relationIds[relIx];\n    if (!state.keepRelation.has(relId)) {\n      continue;\n    }\n    await writeText(stream, formatRelationXml(state, relIx) + '\\n');\n  }\n\n  await writeText(stream, '</osm>\\n');\n  await endStream(stream);\n}\n\nasync function run() {\n  const args = parseArgs(process.argv);\n  const bounds = {\n    lonMin: args.lonMin,\n    latMin: args.latMin,\n    lonMax: args.lonMax,\n    latMax: args.latMax,\n  };\n\n  const state = createState();\n\n  await firstPassCollectIndexes(state, args.osm);\n  await secondPassLoadNodeCoordinates(state, args.osm);\n  thirdStepComputePruningDecision(state, bounds, args.printSizeCm, args.mapScale, args.targetRoadDensity);\n  const keepNodeFromRelations = fourthStepApplyRelationKeepLogic(state);\n  fifthStepMarkKeptNodes(state, keepNodeFromRelations);\n\n  const outputPath = args.output;\n  const inplace = outputPath === args.osm;\n  const tempPath = inplace ? (outputPath + '.pruned.tmp') : outputPath;\n\n  await sixthStepWritePrunedOsm(state, bounds, tempPath);\n\n  if (inplace) {\n    fs.renameSync(tempPath, outputPath);\n  }\n}\n\nfunction hashUpdate(current, text) {\n  let hash = current >>> 0;\n  const s = String(text);\n  for (let i = 0; i < s.length; i += 1) {\n    hash ^= s.charCodeAt(i);\n    hash = Math.imul(hash, 16777619) >>> 0;\n  }\n  return hash >>> 0;\n}\n\nfunction attrsToCanonicalString(attrs) {\n  const keys = Object.keys(attrs || {}).sort();\n  const parts = [];\n  for (let i = 0; i < keys.length; i += 1) {\n    const key = keys[i];\n    parts.push(key + '=' + String(attrs[key]));\n  }\n  return parts.join('|');\n}\n\nasync function collectSummaryWithCustomParser(osmPath) {\n  const summary = {\n    nodes: 0,\n    ways: 0,\n    relations: 0,\n    tags: 0,\n    ndRefs: 0,\n    relationMembers: 0,\n    relationMemberNode: 0,\n    relationMemberWay: 0,\n    relationMemberRelation: 0,\n    selfClosingNodes: 0,\n    selfClosingWays: 0,\n    selfClosingRelations: 0,\n    hash: 2166136261 >>> 0,\n  };\n\n  let currentNode = null;\n  let currentWay = null;\n  let currentRelation = null;\n\n  function addHash(text) {\n    summary.hash = hashUpdate(summary.hash, text);\n  }\n\n  await streamParseXml(osmPath, (token) => {\n    if (currentNode !== null) {\n      if (!token.closing && token.name === 'tag') {\n        summary.tags += 1;\n        addHash('N:TAG:' + attrsToCanonicalString(token.attrs));\n      }\n      if ((token.closing && token.name === 'node') || (token.selfClosing && token.name === 'node')) {\n        addHash('N:CLOSE');\n        currentNode = null;\n      }\n      return;\n    }\n\n    if (currentWay !== null) {\n      if (!token.closing && token.name === 'nd') {\n        summary.ndRefs += 1;\n        addHash('W:ND:' + attrsToCanonicalString(token.attrs));\n      } else if (!token.closing && token.name === 'tag') {\n        summary.tags += 1;\n        addHash('W:TAG:' + attrsToCanonicalString(token.attrs));\n      }\n      if ((token.closing && token.name === 'way') || (token.selfClosing && token.name === 'way')) {\n        addHash('W:CLOSE');\n        currentWay = null;\n      }\n      return;\n    }\n\n    if (currentRelation !== null) {\n      if (!token.closing && token.name === 'member') {\n        summary.relationMembers += 1;\n        const memberType = token.attrs.type || '';\n        if (memberType === 'node') {\n          summary.relationMemberNode += 1;\n        } else if (memberType === 'way') {\n          summary.relationMemberWay += 1;\n        } else if (memberType === 'relation') {\n          summary.relationMemberRelation += 1;\n        }\n        addHash('R:MEM:' + attrsToCanonicalString(token.attrs));\n      } else if (!token.closing && token.name === 'tag') {\n        summary.tags += 1;\n        addHash('R:TAG:' + attrsToCanonicalString(token.attrs));\n      }\n      if ((token.closing && token.name === 'relation') || (token.selfClosing && token.name === 'relation')) {\n        addHash('R:CLOSE');\n        currentRelation = null;\n      }\n      return;\n    }\n\n    if (!token.closing && token.name === 'node') {\n      summary.nodes += 1;\n      if (token.selfClosing) {\n        summary.selfClosingNodes += 1;\n      }\n      addHash('N:OPEN:' + attrsToCanonicalString(token.attrs));\n      if (!token.selfClosing) {\n        currentNode = {};\n      }\n      return;\n    }\n\n    if (!token.closing && token.name === 'way') {\n      summary.ways += 1;\n      if (token.selfClosing) {\n        summary.selfClosingWays += 1;\n      }\n      addHash('W:OPEN:' + attrsToCanonicalString(token.attrs));\n      if (!token.selfClosing) {\n        currentWay = {};\n      }\n      return;\n    }\n\n    if (!token.closing && token.name === 'relation') {\n      summary.relations += 1;\n      if (token.selfClosing) {\n        summary.selfClosingRelations += 1;\n      }\n      addHash('R:OPEN:' + attrsToCanonicalString(token.attrs));\n      if (!token.selfClosing) {\n        currentRelation = {};\n      }\n      return;\n    }\n  });\n\n  summary.hashHex = (summary.hash >>> 0).toString(16).padStart(8, '0');\n  return summary;\n}\n\nfunction assertTrue(label, condition) {\n  if (!condition) {\n    throw new Error(label);\n  }\n}\n\nfunction computeRoadGroupingKeep(roadEntries, targetRoadLengthM) {\n  const lengthByRank = new Float64Array(ROAD_SCORE_BUCKET_COUNT);\n  const roadGroupLength = [];\n  const roadGroupMaxRank = [];\n  const roadGroupIxByNameHash = new Map();\n  const entryRoadGroupIx = new Int32Array(roadEntries.length);\n  entryRoadGroupIx.fill(-1);\n\n  for (let i = 0; i < roadEntries.length; i += 1) {\n    const entry = roadEntries[i];\n    let roadGroupIx;\n    if (entry.nameHash === ROAD_NAME_HASH_UNNAMED) {\n      roadGroupIx = roadGroupLength.length;\n      roadGroupLength.push(0.0);\n      roadGroupMaxRank.push(entry.rank);\n    } else {\n      roadGroupIx = roadGroupIxByNameHash.get(entry.nameHash);\n      if (roadGroupIx === undefined) {\n        roadGroupIx = roadGroupLength.length;\n        roadGroupIxByNameHash.set(entry.nameHash, roadGroupIx);\n        roadGroupLength.push(0.0);\n        roadGroupMaxRank.push(entry.rank);\n      }\n    }\n    roadGroupLength[roadGroupIx] += entry.lengthMeters;\n    if (entry.rank > roadGroupMaxRank[roadGroupIx]) {\n      roadGroupMaxRank[roadGroupIx] = entry.rank;\n    }\n    entryRoadGroupIx[i] = roadGroupIx;\n  }\n\n  for (let roadGroupIx = 0; roadGroupIx < roadGroupLength.length; roadGroupIx += 1) {\n    const rank = roadGroupMaxRank[roadGroupIx];\n    lengthByRank[scoreToBucketIx(rank)] += roadGroupLength[roadGroupIx];\n  }\n\n  const removedRanks = deriveRemovedRankBuckets(lengthByRank, targetRoadLengthM);\n  const kept = new Array(roadEntries.length);\n  for (let i = 0; i < roadEntries.length; i += 1) {\n    const roadGroupIx = entryRoadGroupIx[i];\n    kept[i] = !removedRanks.has(roadGroupMaxRank[roadGroupIx]);\n  }\n  return {\n    kept,\n    entryRoadGroupIx,\n  };\n}\n\nfunction runRoadGroupingSelfTest() {\n  const mainStreetHash = hashRoadName32(normalizeRoadNameForGrouping('Main Street'));\n  const otherStreetHash = hashRoadName32(normalizeRoadNameForGrouping('Other Street'));\n  const targetRoadLengthM = 100.0;\n\n  const sameNameMixedRanks = computeRoadGroupingKeep(\n    [\n      { rank: 4, nameHash: mainStreetHash, lengthMeters: 100.0 },\n      { rank: 8, nameHash: mainStreetHash, lengthMeters: 100.0 },\n      { rank: 4, nameHash: otherStreetHash, lengthMeters: 120.0 },\n    ],\n    targetRoadLengthM\n  );\n  assertTrue('same-name ways should share one group', sameNameMixedRanks.entryRoadGroupIx[0] === sameNameMixedRanks.entryRoadGroupIx[1]);\n  assertTrue('same-name mixed-rank segment A should be kept', sameNameMixedRanks.kept[0] === true);\n  assertTrue('same-name mixed-rank segment B should be kept', sameNameMixedRanks.kept[1] === true);\n  assertTrue('other lower-rank road should be pruned', sameNameMixedRanks.kept[2] === false);\n\n  const unnamedFallback = computeRoadGroupingKeep(\n    [\n      { rank: 4, nameHash: ROAD_NAME_HASH_UNNAMED, lengthMeters: 100.0 },\n      { rank: 8, nameHash: ROAD_NAME_HASH_UNNAMED, lengthMeters: 100.0 },\n    ],\n    targetRoadLengthM\n  );\n  assertTrue('unnamed ways should not share a group', unnamedFallback.entryRoadGroupIx[0] !== unnamedFallback.entryRoadGroupIx[1]);\n  assertTrue('unnamed lower-rank road should be pruned independently', unnamedFallback.kept[0] === false);\n  assertTrue('unnamed higher-rank road should be kept independently', unnamedFallback.kept[1] === true);\n\n  const normalizedA = normalizeRoadNameForGrouping(' Main  Street ');\n  const normalizedB = normalizeRoadNameForGrouping('main street');\n  assertTrue('name normalization should collapse case and whitespace', normalizedA === normalizedB);\n  assertTrue('normalized names should produce identical hash', hashRoadName32(normalizedA) === hashRoadName32(normalizedB));\n\n  const groupCheck = computeRoadGroupingKeep(\n    [\n      { rank: 5, nameHash: hashRoadName32(normalizeRoadNameForGrouping('Alpha Way')), lengthMeters: 20.0 },\n      { rank: 5, nameHash: hashRoadName32(normalizeRoadNameForGrouping('Beta Way')), lengthMeters: 20.0 },\n    ],\n    10.0\n  );\n  assertTrue('distinct names should map to distinct groups', groupCheck.entryRoadGroupIx[0] !== groupCheck.entryRoadGroupIx[1]);\n\n  // Ranking behavior tests for base-highway-only scoring.\n  const genericFootwayRank = baseRoadRank('footway');\n  const genericServiceRank = baseRoadRank('service');\n  const residentialRank = baseRoadRank('residential');\n  assertTrue('generic footway should rank below generic service', genericFootwayRank < genericServiceRank);\n  assertTrue('generic service should rank below residential', genericServiceRank < residentialRank);\n\n  const primaryRank = baseRoadRank('primary');\n  const primaryLinkRank = baseRoadRank('primary_link');\n  assertTrue('primary_link should rank at least primary', primaryLinkRank >= primaryRank);\n\n  function rankFromWayTags(tags) {\n    const state = createState();\n    const tagPairs = Object.keys(tags).map((key) => [key, tags[key]]);\n    finalizeWayIntoState(state, {\n      id: 1,\n      tags: tagPairs,\n      refStart: 0,\n      refLen: 0,\n    });\n    return state.wayRank[0];\n  }\n  const namedFootwayRank = rankFromWayTags({ highway: 'footway', name: 'Main Footway' });\n  const platformFootwayRank = rankFromWayTags({ highway: 'footway', public_transport: 'platform', railway: 'platform' });\n  const serviceDrivewayRank = rankFromWayTags({ highway: 'service', service: 'driveway', access: 'private' });\n  const trackSurfaceRank = rankFromWayTags({ highway: 'track', tracktype: 'grade1', surface: 'paved', name: 'Track Road' });\n  assertTrue('name tags should not affect rank', namedFootwayRank === baseRoadRank('footway'));\n  assertTrue('platform-related tags should not affect rank', platformFootwayRank === baseRoadRank('footway'));\n  assertTrue('service/access tags should not affect rank', serviceDrivewayRank === baseRoadRank('service'));\n  assertTrue('surface/tracktype tags should not affect rank', trackSurfaceRank === baseRoadRank('track'));\n\n  const unknownRank = baseRoadRank('mystery_road');\n  assertTrue('unknown highway should be assigned very low score', unknownRank === ROAD_SCORE_MIN);\n\n  assertTrue('rail track should be detected', hasRailTrackWayTags({ railway: 'rail' }) === true);\n  assertTrue('tram/light rail should be detected', hasRailTrackWayTags({ railway: 'light_rail' }) === true);\n  assertTrue('railway platform should not be treated as track', hasRailTrackWayTags({ railway: 'platform' }) === false);\n  assertTrue('railway station should not be treated as track', hasRailTrackWayTags({ railway: 'station' }) === false);\n\n  console.log('road-grouping self-test passed');\n}\n\nfunction assertAlmostEqual(label, actual, expected, tolerance) {\n  if (Math.abs(actual - expected) > tolerance) {\n    throw new Error(label + ': got=' + actual + ' expected=' + expected + ' tolerance=' + tolerance);\n  }\n}\n\nfunction runBboxLengthSelfTest() {\n  const bounds = {\n    lonMin: 24.0,\n    lonMax: 25.0,\n    latMin: 60.0,\n    latMax: 61.0,\n  };\n\n  const insideSegmentLength = segmentLengthWithinBoundsMeters(24.1, 60.1, 24.2, 60.2, bounds);\n  const expectedInsideLength = computeHaversineM(60.1, 24.1, 60.2, 24.2);\n  assertAlmostEqual('inside segment length', insideSegmentLength, expectedInsideLength, 1e-6);\n\n  const outsideSegmentLength = segmentLengthWithinBoundsMeters(23.0, 59.0, 23.1, 59.1, bounds);\n  assertAlmostEqual('outside segment length', outsideSegmentLength, 0.0, 1e-12);\n\n  const crossingSegmentLength = segmentLengthWithinBoundsMeters(23.5, 60.5, 24.5, 60.5, bounds);\n  const expectedCrossingLength = computeHaversineM(60.5, 24.0, 60.5, 24.5);\n  assertAlmostEqual('crossing segment clipped length', crossingSegmentLength, expectedCrossingLength, 1e-6);\n\n  const edgeTouchSegmentLength = segmentLengthWithinBoundsMeters(24.0, 60.2, 24.8, 60.2, bounds);\n  const expectedEdgeTouchLength = computeHaversineM(60.2, 24.0, 60.2, 24.8);\n  assertAlmostEqual('edge-touch segment length', edgeTouchSegmentLength, expectedEdgeTouchLength, 1e-6);\n\n  const multiSegmentLength =\n    segmentLengthWithinBoundsMeters(23.9, 60.5, 24.2, 60.5, bounds) +\n    segmentLengthWithinBoundsMeters(24.2, 60.5, 24.3, 60.5, bounds);\n  const expectedMultiSegmentLength =\n    computeHaversineM(60.5, 24.0, 60.5, 24.2) +\n    computeHaversineM(60.5, 24.2, 60.5, 24.3);\n  assertAlmostEqual('multi-segment clipped length', multiSegmentLength, expectedMultiSegmentLength, 1e-6);\n\n  const mainStreetHash = hashRoadName32(normalizeRoadNameForGrouping('Main Street'));\n  const targetRoadLengthM = 100.0;\n  const groupedWithOutsideSegment = computeRoadGroupingKeep(\n    [\n      { rank: 8, nameHash: mainStreetHash, lengthMeters: 100.0 },\n      { rank: 4, nameHash: mainStreetHash, lengthMeters: 0.0 },\n      { rank: 4, nameHash: hashRoadName32(normalizeRoadNameForGrouping('Other Street')), lengthMeters: 120.0 },\n    ],\n    targetRoadLengthM\n  );\n  assertTrue('same-name mixed-rank group should stay kept with outside zero-length segment', groupedWithOutsideSegment.kept[0] === true);\n  assertTrue('same-name outside zero-length segment follows group keep decision', groupedWithOutsideSegment.kept[1] === true);\n\n  const relationFilterState = {\n    nodeIdToIx: new Map([[1, 0], [2, 1]]),\n    keepNode: new Uint8Array([1, 0]),\n    nodeHasCoord: new Uint8Array([1, 1]),\n    wayIdToIx: new Map([[10, 0], [11, 1]]),\n    keepWay: new Uint8Array([1, 0]),\n    keepRelation: new Set([100]),\n  };\n  assertTrue('kept node relation member should be kept', isRelationMemberKept(relationFilterState, { type: MEMBER_TYPE_NODE, ref: 1 }) === true);\n  assertTrue('removed node relation member should be dropped', isRelationMemberKept(relationFilterState, { type: MEMBER_TYPE_NODE, ref: 2 }) === false);\n  assertTrue('kept way relation member should be kept', isRelationMemberKept(relationFilterState, { type: MEMBER_TYPE_WAY, ref: 10 }) === true);\n  assertTrue('removed way relation member should be dropped', isRelationMemberKept(relationFilterState, { type: MEMBER_TYPE_WAY, ref: 11 }) === false);\n  assertTrue('kept relation member should be kept', isRelationMemberKept(relationFilterState, { type: MEMBER_TYPE_RELATION, ref: 100 }) === true);\n  assertTrue('removed relation member should be dropped', isRelationMemberKept(relationFilterState, { type: MEMBER_TYPE_RELATION, ref: 101 }) === false);\n\n  const railRelationState = {\n    relationIds: [200],\n    relationIdToIx: new Map([[200, 0]]),\n    relationIsWaterTag: [false],\n    relationMembers: [[{ type: MEMBER_TYPE_WAY, ref: 20, role: '' }]],\n    wayIdToIx: new Map([[20, 0]]),\n    keptRoadWay: new Uint8Array([0]),\n    keptRailWay: new Uint8Array([1]),\n    wayFlags: [FLAG_RAIL_TRACK],\n    keepWay: new Uint8Array([1]),\n    nodeIdByIx: [],\n    nodeIdToIx: new Map(),\n  };\n  fourthStepApplyRelationKeepLogic(railRelationState);\n  assertTrue('relation with kept rail way should be kept', railRelationState.keepRelation.has(200) === true);\n\n  console.log('bbox-length self-test passed');\n}\n\nmodule.exports = {\n  parseTagToken,\n  streamParseXml,\n  collectSummaryWithCustomParser,\n  runStreamParseXmlSelfTest,\n  runRoadGroupingSelfTest,\n  runBboxLengthSelfTest,\n  normalizeRoadNameForGrouping,\n  hashRoadName32,\n  hasRailTrackWayTags,\n  segmentLengthWithinBoundsMeters,\n  clipSegmentToLonLatBounds,\n};\n\nif (require.main === module) {\n  if (process.argv.indexOf('--self-test-stream-parse-xml') !== -1) {\n    runStreamParseXmlSelfTest().catch((err) => {\n      console.error(String(err && err.stack ? err.stack : err));\n      process.exit(1);\n    });\n  } else if (process.argv.indexOf('--self-test-grouping') !== -1) {\n    try {\n      runRoadGroupingSelfTest();\n    } catch (err) {\n      console.error(String(err && err.stack ? err.stack : err));\n      process.exit(1);\n    }\n  } else if (process.argv.indexOf('--self-test-bbox-length') !== -1) {\n    try {\n      runBboxLengthSelfTest();\n    } catch (err) {\n      console.error(String(err && err.stack ? err.stack : err));\n      process.exit(1);\n    }\n  } else {\n    run().catch((err) => {\n      console.error(String(err && err.stack ? err.stack : err));\n      process.exit(1);\n    });\n  }\n}\n"
  },
  {
    "path": "converter/run-stats-quicktime-smoke.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nQuicktime smoke runner for stats telemetry rollover logic.\n\nThis script validates that:\n1) every 3 writes advances one virtual day and uploads month data to S3\n2) every 3 virtual days advances one virtual month\n3) completed month directory is removed after successful final month upload\n\nIt uses a fake in-memory S3 resource, so no AWS network calls are made.\n\"\"\"\n\nimport argparse\nimport datetime\nimport gzip\nimport io\nimport json\nimport os\nimport shutil\nimport sys\n\nimport stats_pipeline\n\n\nclass FakeBucket(object):\n    def __init__(self, bucket_name, uploads):\n        self._bucket_name = bucket_name\n        self._uploads = uploads\n\n    def put_object(self, **kwargs):\n        key = kwargs.get('Key')\n        body = kwargs.get('Body', b'')\n        self._uploads.append({\n            'bucket': self._bucket_name,\n            'key': key,\n            'body': body,\n            'content_type': kwargs.get('ContentType'),\n            'content_encoding': kwargs.get('ContentEncoding')\n        })\n        return {}\n\n\nclass FakeS3Resource(object):\n    def __init__(self):\n        self.uploads = []\n        self._buckets = {}\n\n    def Bucket(self, bucket_name):\n        if bucket_name not in self._buckets:\n            self._buckets[bucket_name] = FakeBucket(bucket_name, self.uploads)\n        return self._buckets[bucket_name]\n\n\ndef parse_args():\n    parser = argparse.ArgumentParser(description='Quicktime telemetry smoke runner')\n    parser.add_argument(\n        '--root',\n        default='.tmp/telemetry-quicktime-smoke',\n        help='Temporary root for local stats files (default: .tmp/telemetry-quicktime-smoke)'\n    )\n    parser.add_argument(\n        '--attempts',\n        type=int,\n        default=9,\n        help='How many synthetic stats writes to run (default: 9)'\n    )\n    parser.add_argument(\n        '--bucket',\n        default='test.stats.touch-mapper',\n        help='Fake bucket name for upload calls (default: test.stats.touch-mapper)'\n    )\n    parser.add_argument(\n        '--keep-files',\n        action='store_true',\n        help='Keep temporary files after run'\n    )\n    return parser.parse_args()\n\n\ndef add_months(year, month, offset):\n    zero_based = (int(year) * 12 + (int(month) - 1)) + int(offset)\n    next_year = zero_based // 12\n    next_month = (zero_based % 12) + 1\n    return next_year, next_month\n\n\ndef read_quicktime_state(stats_root):\n    state_path = os.path.join(stats_root, '.maintenance', 'quicktime-state.json')\n    with open(state_path, 'r', encoding='utf8') as handle:\n        return json.load(handle)\n\n\ndef uploaded_line_count(upload_entry):\n    body = upload_entry['body']\n    with gzip.GzipFile(fileobj=io.BytesIO(body), mode='rb') as gz:\n        lines = gz.read().decode('utf8').splitlines()\n    return len([line for line in lines if line.strip() != ''])\n\n\ndef assert_true(condition, message):\n    if not condition:\n        raise Exception(message)\n\n\ndef main():\n    args = parse_args()\n    stats_root = os.path.abspath(args.root)\n    if args.attempts <= 0:\n        raise Exception('--attempts must be > 0')\n\n    if os.path.isdir(stats_root):\n        shutil.rmtree(stats_root)\n    os.makedirs(stats_root)\n\n    fake_s3 = FakeS3Resource()\n    seed_now = datetime.datetime(2026, 2, 13, 12, 0, 0)\n    initial_year = seed_now.year\n    initial_month = seed_now.month\n\n    print('Running quicktime smoke: attempts={} root={}'.format(args.attempts, stats_root))\n    for attempt in range(1, args.attempts + 1):\n        map_id = 'B{:04d}'.format(attempt)\n        request_id = map_id + '/QuicktimeSmoke'\n        record = {\n            'schema_version': 1,\n            'timestamp': '2026-02-13T12:00:00Z',\n            'event_date': '2026-02-13',\n            'day': '13',\n            'request_id': request_id,\n            'map_id': map_id,\n            'status': 'success'\n        }\n        stats_pipeline.write_attempt_record(\n            stats_root_dir=stats_root,\n            record=record,\n            quicktime_mode=True,\n            s3_resource=fake_s3,\n            stats_bucket_name=args.bucket,\n            now_utc=seed_now\n        )\n\n        state = read_quicktime_state(stats_root)\n        uploads = len(fake_s3.uploads)\n        completed_days = attempt // 3\n        completed_months = completed_days // 3\n        expected_uploads = completed_days\n        expected_day = (completed_days % 3) + 1\n        expected_days_elapsed = completed_days % 3\n        expected_writes_in_day = attempt % 3\n        expected_year, expected_month = add_months(initial_year, initial_month, completed_months)\n\n        assert_true(uploads == expected_uploads, 'upload count mismatch at attempt {}'.format(attempt))\n        assert_true(int(state['virtual_year']) == expected_year, 'virtual year mismatch at attempt {}'.format(attempt))\n        assert_true(int(state['virtual_month']) == expected_month, 'virtual month mismatch at attempt {}'.format(attempt))\n        assert_true(int(state['virtual_day']) == expected_day, 'virtual day mismatch at attempt {}'.format(attempt))\n        assert_true(\n            int(state['days_elapsed_in_current_month']) == expected_days_elapsed,\n            'days_elapsed mismatch at attempt {}'.format(attempt)\n        )\n        assert_true(\n            int(state['writes_in_current_day']) == expected_writes_in_day,\n            'writes_in_current_day mismatch at attempt {}'.format(attempt)\n        )\n\n        if attempt % 3 == 0:\n            upload = fake_s3.uploads[-1]\n            month_for_upload_year, month_for_upload_month = add_months(\n                initial_year,\n                initial_month,\n                (completed_days - 1) // 3\n            )\n            expected_key = stats_pipeline.month_object_key(month_for_upload_year, month_for_upload_month)\n            assert_true(upload['key'] == expected_key, 'upload key mismatch at attempt {}'.format(attempt))\n\n            attempts_in_upload_month = attempt - (((completed_days - 1) // 3) * 9)\n            expected_lines = attempts_in_upload_month\n            actual_lines = uploaded_line_count(upload)\n            assert_true(\n                actual_lines == expected_lines,\n                'uploaded line count mismatch at attempt {} (expected {}, got {})'.format(\n                    attempt, expected_lines, actual_lines\n                )\n            )\n\n        print(\n            'attempt={} uploads={} virtual_date={}-{:02d}-{:02d} writes_in_day={} days_elapsed={}'.format(\n                attempt,\n                uploads,\n                int(state['virtual_year']),\n                int(state['virtual_month']),\n                int(state['virtual_day']),\n                int(state['writes_in_current_day']),\n                int(state['days_elapsed_in_current_month'])\n            )\n        )\n\n    if args.attempts >= 9:\n        completed_month_path = os.path.join(\n            stats_root,\n            '{:04d}'.format(initial_year),\n            '{:02d}'.format(initial_month)\n        )\n        assert_true(\n            not os.path.isdir(completed_month_path),\n            'expected completed month directory to be removed: {}'.format(completed_month_path)\n        )\n\n    print('Quicktime smoke passed: uploads={}'.format(len(fake_s3.uploads)))\n    if not args.keep_files:\n        shutil.rmtree(stats_root)\n        print('Removed temporary directory {}'.format(stats_root))\n    return 0\n\n\nif __name__ == '__main__':\n    try:\n        sys.exit(main())\n    except Exception as e:\n        print('Quicktime smoke failed: {}'.format(e))\n        sys.exit(1)\n"
  },
  {
    "path": "converter/stats_pipeline.py",
    "content": "#!/usr/bin/python3\n\nimport contextlib\nimport datetime\nimport fcntl\nimport gzip\nimport ipaddress\nimport io\nimport json\nimport os\nimport re\nimport shutil\nimport time\nimport urllib.request\n\n\nQUICKTIME_WRITES_PER_DAY = 3\nQUICKTIME_DAYS_PER_MONTH = 3\nIP_GEO_LOOKUP_TIMEOUT_SECONDS = 1.5\n\n\ndef map_id_from_request_id(request_id):\n    if request_id is None:\n        return 'unknown'\n    text = str(request_id)\n    if text == '':\n        return 'unknown'\n    return text.split('/', 1)[0]\n\n\ndef month_object_key(year, month):\n    return 'stats-json/{year}/{month}/stats-{year}-{month}.jsonl.gz'.format(\n        year='{0:04d}'.format(int(year)),\n        month='{0:02d}'.format(int(month))\n    )\n\n\ndef write_attempt_record(stats_root_dir, record, quicktime_mode=False, s3_resource=None,\n                         stats_bucket_name=None, now_utc=None):\n    if quicktime_mode:\n        return _write_attempt_record_quicktime(\n            stats_root_dir=stats_root_dir,\n            record=record,\n            s3_resource=s3_resource,\n            stats_bucket_name=stats_bucket_name,\n            now_utc=now_utc\n        )\n    return _write_attempt_record_real_date(stats_root_dir=stats_root_dir, record=record, now_utc=now_utc)\n\n\ndef run_daily_upload_if_due(stats_root_dir, s3_resource, stats_bucket_name, now_utc=None):\n    _ensure_dir(stats_root_dir)\n    if now_utc is None:\n        now_utc = datetime.datetime.utcnow()\n    today = now_utc.date()\n    marker_value = today.isoformat()\n\n    maintenance_dir = _maintenance_dir(stats_root_dir)\n    _ensure_dir(maintenance_dir)\n    lock_path = os.path.join(maintenance_dir, 'upload.lock')\n    marker_path = os.path.join(maintenance_dir, 'last-successful-run-utc.txt')\n\n    with _exclusive_lock(lock_path):\n        previous_marker = _read_small_text(marker_path)\n        if previous_marker == marker_value:\n            return False\n\n        flush_day = today - datetime.timedelta(days=1)\n        upload_ok = False\n        try:\n            upload_ok = upload_month_from_local_data(\n                stats_root_dir=stats_root_dir,\n                s3_resource=s3_resource,\n                stats_bucket_name=stats_bucket_name,\n                year=flush_day.year,\n                month=flush_day.month,\n                max_day=flush_day.day\n            )\n        except Exception as e:\n            print('stats daily upload failed: ' + str(e))\n            upload_ok = False\n\n        if not upload_ok:\n            return False\n\n        if _is_last_day_of_month(flush_day):\n            month_dir = _month_dir(stats_root_dir, flush_day.year, flush_day.month)\n            if os.path.isdir(month_dir):\n                shutil.rmtree(month_dir)\n                print('stats cleanup: removed local month directory {}'.format(month_dir))\n\n        _write_small_text_atomic(marker_path, marker_value)\n        return True\n\n\ndef upload_month_from_local_data(stats_root_dir, s3_resource, stats_bucket_name, year, month, max_day=None):\n    month_dir = _month_dir(stats_root_dir, year, month)\n    if not os.path.isdir(month_dir):\n        return True\n\n    payload, line_count = _build_month_gzip_payload(month_dir, max_day=max_day)\n    if line_count == 0:\n        return True\n\n    key = month_object_key(year=year, month=month)\n    s3_resource.Bucket(stats_bucket_name).put_object(\n        Key=key,\n        Body=payload,\n        ContentType='application/x-ndjson',\n        ContentEncoding='gzip'\n    )\n    print(\n        'stats upload complete: bucket={} key={} lines={}'.format(\n            stats_bucket_name, key, line_count\n        )\n    )\n    return True\n\n\ndef _write_attempt_record_real_date(stats_root_dir, record, now_utc=None):\n    if now_utc is None:\n        now_utc = datetime.datetime.utcnow()\n    target_date = now_utc.date()\n    record_to_store = dict(record)\n    _enrich_record_with_ip_geo(stats_root_dir, record_to_store)\n    record_to_store['event_date'] = target_date.isoformat()\n    record_to_store['day'] = '{:02d}'.format(target_date.day)\n    file_path = _stats_file_path(\n        stats_root_dir=stats_root_dir,\n        map_id=record_to_store.get('map_id'),\n        year=target_date.year,\n        month=target_date.month,\n        day=target_date.day\n    )\n    _write_json_atomic(file_path, record_to_store)\n    return file_path\n\n\ndef _write_attempt_record_quicktime(stats_root_dir, record, s3_resource, stats_bucket_name, now_utc=None):\n    if s3_resource is None:\n        raise Exception('quicktime mode requires s3_resource')\n    if not stats_bucket_name:\n        raise Exception('quicktime mode requires stats_bucket_name')\n    if now_utc is None:\n        now_utc = datetime.datetime.utcnow()\n\n    maintenance_dir = _maintenance_dir(stats_root_dir)\n    _ensure_dir(maintenance_dir)\n    lock_path = os.path.join(maintenance_dir, 'quicktime.lock')\n    state_path = os.path.join(maintenance_dir, 'quicktime-state.json')\n\n    with _exclusive_lock(lock_path):\n        state = _load_quicktime_state(state_path, now_utc)\n        year = int(state['virtual_year'])\n        month = int(state['virtual_month'])\n        day = int(state['virtual_day'])\n\n        record_to_store = dict(record)\n        _enrich_record_with_ip_geo(stats_root_dir, record_to_store)\n        record_to_store['event_date'] = '{:04d}-{:02d}-{:02d}'.format(year, month, day)\n        record_to_store['day'] = '{:02d}'.format(day)\n\n        file_path = _stats_file_path(\n            stats_root_dir=stats_root_dir,\n            map_id=record_to_store.get('map_id'),\n            year=year,\n            month=month,\n            day=day\n        )\n        _write_json_atomic(file_path, record_to_store)\n\n        state['writes_in_current_day'] = int(state['writes_in_current_day']) + 1\n        if int(state['writes_in_current_day']) >= QUICKTIME_WRITES_PER_DAY:\n            upload_ok = False\n            try:\n                upload_ok = upload_month_from_local_data(\n                    stats_root_dir=stats_root_dir,\n                    s3_resource=s3_resource,\n                    stats_bucket_name=stats_bucket_name,\n                    year=year,\n                    month=month,\n                    max_day=day\n                )\n            except Exception as e:\n                print('stats quicktime upload failed: ' + str(e))\n                upload_ok = False\n\n            if upload_ok:\n                state['writes_in_current_day'] = 0\n                state['days_elapsed_in_current_month'] = int(state['days_elapsed_in_current_month']) + 1\n\n                if int(state['days_elapsed_in_current_month']) >= QUICKTIME_DAYS_PER_MONTH:\n                    month_dir = _month_dir(stats_root_dir, year, month)\n                    if os.path.isdir(month_dir):\n                        shutil.rmtree(month_dir)\n                        print('stats quicktime cleanup: removed local month directory {}'.format(month_dir))\n                    _advance_quicktime_month(state)\n                else:\n                    state['virtual_day'] = int(state['virtual_day']) + 1\n            else:\n                state['writes_in_current_day'] = QUICKTIME_WRITES_PER_DAY - 1\n\n        _write_json_atomic(state_path, state)\n        return file_path\n\n\ndef _build_month_gzip_payload(month_dir, max_day=None):\n    line_count = 0\n    output = io.BytesIO()\n    with gzip.GzipFile(fileobj=output, mode='wb', compresslevel=5) as gz:\n        for path in _iter_stats_file_paths(month_dir, max_day=max_day):\n            try:\n                with open(path, 'r', encoding='utf8') as handle:\n                    value = json.load(handle)\n            except Exception as e:\n                print('stats monthly upload skipping unreadable file {}: {}'.format(path, e))\n                continue\n            line = json.dumps(value, separators=(',', ':'), ensure_ascii=False) + '\\n'\n            gz.write(line.encode('utf8'))\n            line_count += 1\n    if line_count == 0:\n        return b'', 0\n    return output.getvalue(), line_count\n\n\ndef _iter_stats_file_paths(month_dir, max_day=None):\n    day_dirs = []\n    for name in os.listdir(month_dir):\n        path = os.path.join(month_dir, name)\n        if not os.path.isdir(path):\n            continue\n        if not re.match(r'^[0-9]{1,2}$', name):\n            continue\n        day = int(name)\n        if day < 1 or day > 31:\n            continue\n        if max_day is not None and day > int(max_day):\n            continue\n        day_dirs.append((day, name))\n\n    for _day_int, day_name in sorted(day_dirs):\n        day_dir = os.path.join(month_dir, day_name)\n        for name in sorted(os.listdir(day_dir)):\n            if not name.endswith('.json'):\n                continue\n            path = os.path.join(day_dir, name)\n            if os.path.isfile(path):\n                yield path\n\n\ndef _load_quicktime_state(state_path, now_utc):\n    default_state = {\n        'virtual_year': int(now_utc.year),\n        'virtual_month': int(now_utc.month),\n        'virtual_day': 1,\n        'writes_in_current_day': 0,\n        'days_elapsed_in_current_month': 0\n    }\n\n    if not os.path.isfile(state_path):\n        return default_state\n\n    try:\n        with open(state_path, 'r', encoding='utf8') as handle:\n            value = json.load(handle)\n    except Exception:\n        return default_state\n\n    state = dict(default_state)\n    for key in state.keys():\n        raw = value.get(key)\n        if isinstance(raw, bool):\n            continue\n        if isinstance(raw, int):\n            state[key] = raw\n        elif isinstance(raw, float) and int(raw) == raw:\n            state[key] = int(raw)\n\n    if state['virtual_month'] < 1 or state['virtual_month'] > 12:\n        state['virtual_month'] = default_state['virtual_month']\n    if state['virtual_day'] < 1:\n        state['virtual_day'] = 1\n    if state['writes_in_current_day'] < 0:\n        state['writes_in_current_day'] = 0\n    if state['days_elapsed_in_current_month'] < 0:\n        state['days_elapsed_in_current_month'] = 0\n    return state\n\n\ndef _advance_quicktime_month(state):\n    next_month = int(state['virtual_month']) + 1\n    next_year = int(state['virtual_year'])\n    if next_month > 12:\n        next_month = 1\n        next_year += 1\n\n    state['virtual_year'] = next_year\n    state['virtual_month'] = next_month\n    state['virtual_day'] = 1\n    state['writes_in_current_day'] = 0\n    state['days_elapsed_in_current_month'] = 0\n\n\ndef _enrich_record_with_ip_geo(stats_root_dir, record):\n    ip_value = _normalize_ip(record.get('browser_ip'))\n    if ip_value is None:\n        return\n\n    maintenance_dir = _maintenance_dir(stats_root_dir)\n    _ensure_dir(maintenance_dir)\n    cache_path = os.path.join(maintenance_dir, 'ip-geo-cache.json')\n    lock_path = os.path.join(maintenance_dir, 'ip-geo-cache.lock')\n\n    with _exclusive_lock(lock_path):\n        cache = _read_json_object(cache_path)\n        cached = cache.get(ip_value)\n        if isinstance(cached, dict):\n            _apply_geo_fields(record, cached)\n            return\n\n        lookup = _lookup_ip_geo(ip_value)\n        if lookup is None:\n            return\n\n        cache[ip_value] = lookup\n        _write_json_atomic(cache_path, cache)\n        _apply_geo_fields(record, lookup)\n\n\ndef _normalize_ip(value):\n    if value is None:\n        return None\n    text = str(value).strip()\n    if text == '':\n        return None\n    try:\n        ipaddress.ip_address(text)\n        return text\n    except Exception:\n        return None\n\n\ndef _lookup_ip_geo(ip_value):\n    url = 'https://ipapi.co/{}/json/'.format(ip_value)\n    request = urllib.request.Request(url=url, headers={'User-Agent': 'TouchMapperStats/1.0'})\n    try:\n        with urllib.request.urlopen(request, timeout=IP_GEO_LOOKUP_TIMEOUT_SECONDS) as response:\n            payload = json.loads(response.read().decode('utf8'))\n    except Exception as e:\n        print('stats ip geolocation lookup failed for {}: {}'.format(ip_value, e))\n        return None\n\n    if not isinstance(payload, dict):\n        return None\n    if payload.get('error') is True:\n        return None\n\n    return {\n        'country': payload.get('country_name') or payload.get('country'),\n        'country_code': payload.get('country_code') or payload.get('country'),\n        'region': payload.get('region') or payload.get('region_code'),\n        'city': payload.get('city'),\n        'latitude': _as_float_or_none(payload.get('latitude') or payload.get('lat')),\n        'longitude': _as_float_or_none(payload.get('longitude') or payload.get('lon'))\n    }\n\n\ndef _apply_geo_fields(record, geo_value):\n    record['browser_ip_country'] = geo_value.get('country')\n    record['browser_ip_country_code'] = geo_value.get('country_code')\n    record['browser_ip_region'] = geo_value.get('region')\n    record['browser_ip_city'] = geo_value.get('city')\n    record['browser_ip_latitude'] = geo_value.get('latitude')\n    record['browser_ip_longitude'] = geo_value.get('longitude')\n\n\ndef _as_float_or_none(value):\n    if value is None:\n        return None\n    if isinstance(value, bool):\n        return None\n    try:\n        return float(value)\n    except Exception:\n        return None\n\n\ndef _month_dir(stats_root_dir, year, month):\n    return os.path.join(\n        stats_root_dir,\n        '{:04d}'.format(int(year)),\n        '{:02d}'.format(int(month))\n    )\n\n\ndef _stats_file_path(stats_root_dir, map_id, year, month, day):\n    safe_map_id = _safe_map_id_for_filename(map_id)\n    day_dir = os.path.join(\n        stats_root_dir,\n        '{:04d}'.format(int(year)),\n        '{:02d}'.format(int(month)),\n        '{:02d}'.format(int(day))\n    )\n    _ensure_dir(day_dir)\n    return os.path.join(day_dir, safe_map_id + '.json')\n\n\ndef _safe_map_id_for_filename(map_id):\n    raw = map_id\n    if raw is None:\n        raw = 'unknown'\n    text = str(raw).strip()\n    if text == '':\n        text = 'unknown'\n    return re.sub(r'[^A-Za-z0-9._-]', '_', text)\n\n\ndef _maintenance_dir(stats_root_dir):\n    return os.path.join(stats_root_dir, '.maintenance')\n\n\ndef _read_small_text(path):\n    if not os.path.isfile(path):\n        return None\n    try:\n        with open(path, 'r', encoding='utf8') as handle:\n            return handle.read().strip()\n    except Exception:\n        return None\n\n\ndef _read_json_object(path):\n    if not os.path.isfile(path):\n        return {}\n    try:\n        with open(path, 'r', encoding='utf8') as handle:\n            value = json.load(handle)\n    except Exception:\n        return {}\n    return value if isinstance(value, dict) else {}\n\n\ndef _write_small_text_atomic(path, value):\n    _ensure_dir(os.path.dirname(path))\n    tmp_path = path + '.tmp-{}-{}'.format(os.getpid(), int(time.time() * 1000))\n    with open(tmp_path, 'w', encoding='utf8') as handle:\n        handle.write(value)\n        handle.write('\\n')\n    os.replace(tmp_path, path)\n\n\ndef _write_json_atomic(path, value):\n    _ensure_dir(os.path.dirname(path))\n    tmp_path = path + '.tmp-{}-{}'.format(os.getpid(), int(time.time() * 1000))\n    with open(tmp_path, 'w', encoding='utf8') as handle:\n        json.dump(value, handle, separators=(',', ':'), ensure_ascii=False)\n        handle.write('\\n')\n    os.replace(tmp_path, path)\n\n\ndef _ensure_dir(path):\n    if not path:\n        return\n    if os.path.isdir(path):\n        return\n    try:\n        os.makedirs(path)\n    except OSError:\n        if not os.path.isdir(path):\n            raise\n\n\n@contextlib.contextmanager\ndef _exclusive_lock(path):\n    _ensure_dir(os.path.dirname(path))\n    handle = open(path, 'a+')\n    try:\n        fcntl.flock(handle.fileno(), fcntl.LOCK_EX)\n        yield\n    finally:\n        fcntl.flock(handle.fileno(), fcntl.LOCK_UN)\n        handle.close()\n\n\ndef _is_last_day_of_month(target_day):\n    return (target_day + datetime.timedelta(days=1)).month != target_day.month\n"
  },
  {
    "path": "converter/tactile_constants.py",
    "content": "\"\"\"Shared tactile map geometry/rendering constants.\"\"\"\n\n# Feature heights and base geometry.\nROAD_HEIGHT_CAR_MM = 0.82  # 3 x 0.25-0.3mm layers\nROAD_HEIGHT_PEDESTRIAN_MM = 1.5\nBUILDING_HEIGHT_MM = 2.9\nBASE_HEIGHT_MM = 0.6\nBASE_OVERLAP_MM = 0.01\n\n# Water geometry.\nWATER_AREA_DEPTH_MM = 1.5\nWATER_WAVE_DISTANCE_MM = 10.3\nWATERWAY_DEPTH_MM = 0.55  # 2 x 0.25-0.3mm layers\n\n# Border geometry.\nBORDER_WIDTH_MM = 1.2\nBORDER_HORIZONTAL_OVERLAP_MM = 0.05\nBORDER_HEIGHT_MM = (ROAD_HEIGHT_PEDESTRIAN_MM + BUILDING_HEIGHT_MM) / 2\n\n# Marker geometry.\nMARKER_HEIGHT_MM = BUILDING_HEIGHT_MM + 2\nMARKER_RADIUS_MM = MARKER_HEIGHT_MM * 0.5\n\n# Wireframe render defaults.\nWIREFRAME_RENDER_RESOLUTION = 1024\nWIREFRAME_CAMERA_PADDING = 1.02\nWIREFRAME_CAMERA_MIN_SCALE = 1.0\n"
  },
  {
    "path": "converter/telemetry.py",
    "content": "#!/usr/bin/python3\n\nimport datetime\nimport json\nimport os\nimport re\nimport shlex\nimport subprocess\nimport time\nfrom typing import Any, Dict, List, Optional\n\n\n_MAX_RSS_RE = re.compile(r\"Maximum resident set size \\(kbytes\\):\\s*([0-9]+)\")\n_WHITESPACE_RE = re.compile(r\"\\s\")\n\n\ndef utc_ts_fixed() -> str:\n    # Fixed-width UTC timestamp without timezone suffix.\n    return datetime.datetime.utcnow().strftime(\"%Y-%m-%d %H:%M:%S.%f\")[:-3]\n\n\ndef _format_max_rss_mib(max_rss_kib: Optional[int]) -> str:\n    if max_rss_kib is None:\n        return \"n/a\"\n    return \"{:.1f} MiB\".format(float(max_rss_kib) / 1024.0)\n\n\ndef _max_opt(a: Optional[int], b: Optional[int]) -> Optional[int]:\n    if a is None:\n        return b\n    if b is None:\n        return a\n    return a if a >= b else b\n\n\ndef _format_command(cmd: List[str], env: Optional[Dict[str, str]]) -> str:\n    rendered_cmd = \" \".join(shlex.quote(part) for part in cmd)\n    if not env:\n        return rendered_cmd\n    rendered_env = \",\".join(\n        \"{}={}\".format(key, shlex.quote(env[key])) for key in sorted(env.keys())\n    )\n    return \"{} env={}\".format(rendered_cmd, rendered_env)\n\n\nclass TelemetryLogger(object):\n    def __init__(self, component: str, base_depth: int = 0):\n        self.component = component\n        self.base_depth = base_depth\n        self.started_at_utc = utc_ts_fixed()\n        self.ended_at_utc = None  # type: Optional[str]\n        self._stack = []  # type: List[Dict[str, Any]]\n        self._roots = []  # type: List[Dict[str, Any]]\n        self._warned_time_unavailable = False\n\n    def _marker(self, depth: int) -> str:\n        if depth <= 0:\n            return \"\"\n        return \">>\" * depth\n\n    def _line(self, component: str, depth: int, message: str) -> None:\n        marker = self._marker(depth)\n        infix = (\" \" + marker + \" \") if marker else \" \"\n        print(\"{} [{}]{}{}\".format(utc_ts_fixed(), component, infix, message))\n\n    def _default_component(self) -> str:\n        if self._stack:\n            return str(self._stack[-1].get(\"component\", self.component))\n        return self.component\n\n    def _escape_field_value(self, value: str) -> str:\n        escaped = value.replace(\"\\\\\", \"\\\\\\\\\").replace('\"', '\\\\\"')\n        if _WHITESPACE_RE.search(value):\n            return '\"' + escaped + '\"'\n        return escaped\n\n    def _format_fields(self, fields: Optional[Dict[str, Any]]) -> str:\n        if not fields:\n            return \"\"\n        parts = []  # type: List[str]\n        for key in sorted(fields.keys()):\n            value = fields[key]\n            if value is None:\n                continue\n            value_text = self._escape_field_value(str(value))\n            parts.append(\"{}={}\".format(key, value_text))\n        return \" \".join(parts)\n\n    def log(\n        self,\n        message: str,\n        depth: Optional[int] = None,\n        component: Optional[str] = None,\n        fields: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        rendered = message\n        rendered_fields = self._format_fields(fields)\n        if rendered_fields:\n            rendered = \"{} {}\".format(rendered, rendered_fields)\n        if depth is None:\n            depth = self.current_inline_depth()\n        self._line(component or self._default_component(), depth, rendered)\n\n    def current_inline_depth(self) -> int:\n        # Depth to use for lines emitted inside the currently active stage.\n        if self._stack:\n            return self.base_depth + len(self._stack)\n        return self.base_depth\n\n    def start_stage(self, name: str, component: Optional[str] = None) -> Dict[str, Any]:\n        stage_component = component or self.component\n        depth = self.base_depth + len(self._stack)\n        stage = {\n            \"name\": name,\n            \"component\": stage_component,\n            \"depth\": depth,\n            \"startedAtUtc\": utc_ts_fixed(),\n            \"startPerf\": time.perf_counter(),\n            \"childSec\": 0.0,\n            \"childMaxRssKiB\": None,  # type: Optional[int]\n            \"children\": [],\n        }  # type: Dict[str, Any]\n        self._line(stage_component, depth, \"START \" + name)\n        self._stack.append(stage)\n        return stage\n\n    def attach_external_child(self, stage: Dict[str, Any], child: Dict[str, Any]) -> None:\n        total_sec = float(child.get(\"totalSec\", 0.0))\n        stage[\"childSec\"] += total_sec\n        stage[\"children\"].append(child)\n        child_rss = child.get(\"maxRssKiB\")\n        if isinstance(child_rss, int):\n            stage[\"childMaxRssKiB\"] = _max_opt(stage[\"childMaxRssKiB\"], child_rss)\n\n    def end_stage(self, stage: Dict[str, Any], own_max_rss_kib: Optional[int] = None) -> Dict[str, Any]:\n        if not self._stack or self._stack[-1] is not stage:\n            raise RuntimeError(\"Stage stack mismatch for '{}'\".format(stage.get(\"name\")))\n        self._stack.pop()\n\n        total_sec = time.perf_counter() - float(stage[\"startPerf\"])\n        child_sec = float(stage[\"childSec\"])\n        self_sec = total_sec - child_sec\n        if self_sec < 0:\n            self_sec = 0.0\n\n        max_rss_kib = _max_opt(own_max_rss_kib, stage.get(\"childMaxRssKiB\"))\n        node = {\n            \"name\": stage[\"name\"],\n            \"component\": stage[\"component\"],\n            \"startedAtUtc\": stage[\"startedAtUtc\"],\n            \"endedAtUtc\": utc_ts_fixed(),\n            \"totalSec\": total_sec,\n            \"selfSec\": self_sec,\n            \"childSec\": child_sec,\n            \"maxRssKiB\": max_rss_kib,\n            \"children\": stage[\"children\"],\n        }  # type: Dict[str, Any]\n\n        self._line(\n            stage[\"component\"],\n            int(stage[\"depth\"]),\n            \"DONE {} (total {:.2f}s, self {:.2f}s, child {:.2f}s, maxRSS {})\".format(\n                stage[\"name\"],\n                total_sec,\n                self_sec,\n                child_sec,\n                _format_max_rss_mib(max_rss_kib),\n            ),\n        )\n\n        if self._stack:\n            parent = self._stack[-1]\n            parent[\"childSec\"] += total_sec\n            parent[\"childMaxRssKiB\"] = _max_opt(parent[\"childMaxRssKiB\"], max_rss_kib)\n            parent[\"children\"].append(node)\n        else:\n            self._roots.append(node)\n        return node\n\n    def run_subprocess(\n        self,\n        cmd: List[str],\n        env: Optional[Dict[str, str]] = None,\n        cwd: Optional[str] = None,\n        output_log_path: Optional[str] = None,\n        depth_offset: int = 0,\n        component: Optional[str] = None,\n        check: bool = True,\n    ) -> Dict[str, Any]:\n        stage_component = component or self._default_component()\n        if self._stack:\n            base_depth = int(self._stack[-1][\"depth\"])\n        else:\n            base_depth = self.base_depth\n        depth = base_depth + depth_offset\n\n        self._line(stage_component, depth, \"running: \" + _format_command(cmd, env))\n\n        run_env = os.environ.copy()\n        if env:\n            run_env.update(env)\n\n        timed_cmd = cmd\n        using_time = False\n        if os.path.exists(\"/usr/bin/time\"):\n            timed_cmd = [\"/usr/bin/time\", \"-v\"] + cmd\n            using_time = True\n        else:\n            if not self._warned_time_unavailable:\n                self._warned_time_unavailable = True\n                self._line(\n                    stage_component,\n                    depth,\n                    \"note: /usr/bin/time not available, subprocess maxRSS unavailable\",\n                )\n\n        started = time.perf_counter()\n        process = subprocess.Popen(\n            timed_cmd,\n            cwd=cwd,\n            env=run_env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n        )\n        stdout_data, stderr_data = process.communicate()\n        elapsed_sec = time.perf_counter() - started\n\n        stdout_text = stdout_data.decode(\"utf-8\", errors=\"replace\")\n        stderr_text = stderr_data.decode(\"utf-8\", errors=\"replace\")\n        combined_output = stdout_text + stderr_text\n\n        max_rss_kib = None  # type: Optional[int]\n        if using_time:\n            match = _MAX_RSS_RE.search(stderr_text)\n            if match:\n                try:\n                    max_rss_kib = int(match.group(1))\n                except Exception:\n                    max_rss_kib = None\n\n        if output_log_path:\n            with open(output_log_path, \"w\", encoding=\"utf-8\") as handle:\n                handle.write(combined_output)\n\n        result = {\n            \"returncode\": process.returncode,\n            \"elapsedSec\": elapsed_sec,\n            \"maxRssKiB\": max_rss_kib,\n            \"output\": combined_output,\n        }\n        if check and process.returncode != 0:\n            raise subprocess.CalledProcessError(process.returncode, cmd, output=combined_output)\n        return result\n\n    def finalize(self) -> None:\n        self.ended_at_utc = utc_ts_fixed()\n\n    def summary_payload(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:\n        payload = {\n            \"version\": 1,\n            \"component\": self.component,\n            \"startedAtUtc\": self.started_at_utc,\n            \"endedAtUtc\": (self.ended_at_utc or utc_ts_fixed()),\n            \"stages\": self._roots,\n        }  # type: Dict[str, Any]\n        if extra:\n            payload.update(extra)\n        return payload\n\n    def write_json(self, output_path: str, extra: Optional[Dict[str, Any]] = None) -> None:\n        self.finalize()\n        payload = self.summary_payload(extra=extra)\n        with open(output_path, \"w\", encoding=\"utf-8\") as handle:\n            json.dump(payload, handle, indent=2, ensure_ascii=False)\n            handle.write(\"\\n\")\n"
  },
  {
    "path": "converter/upload-pending-stats.py",
    "content": "#!/usr/bin/python3\n\nimport argparse\nimport datetime\nimport os\nimport shutil\nimport sys\n\nscript_dir = os.path.dirname(__file__)\nsys.path.insert(1, '%s/py-lib/boto3' % (script_dir,))\nsys.path.insert(1, script_dir)\n\nimport stats_pipeline\n\n\ndef infer_environment_from_script_dir(current_script_dir):\n    probe = os.path.abspath(current_script_dir)\n    while True:\n        name = os.path.basename(probe)\n        if name in ('test', 'prod'):\n            return name\n        parent = os.path.dirname(probe)\n        if parent == probe:\n            break\n        probe = parent\n    return None\n\n\ndef parse_args():\n    parser = argparse.ArgumentParser(\n        description='Upload pending local stats JSON files to S3 for one Touch Mapper environment.'\n    )\n    parser.add_argument(\n        'environment',\n        nargs='?',\n        choices=['test', 'prod'],\n        help='Environment whose local stats should be uploaded. Defaults from script location when under .../test/... or .../prod/....'\n    )\n    parser.add_argument(\n        '--tm-root',\n        default='~/touch-mapper',\n        help='Touch Mapper root directory on EC2 (default: ~/touch-mapper).'\n    )\n    parser.add_argument(\n        '--keep-local-months',\n        action='store_true',\n        help='Keep local month directories even if they are already complete.'\n    )\n    args = parser.parse_args()\n\n    if args.environment is None:\n        inferred_environment = infer_environment_from_script_dir(script_dir)\n        if inferred_environment is not None:\n            args.environment = inferred_environment\n        else:\n            parser.error('environment is required unless script path has test or prod as an ancestor directory')\n\n    return args\n\n\ndef iter_month_dirs(stats_root_dir):\n    for year_name in sorted(os.listdir(stats_root_dir)):\n        year_path = os.path.join(stats_root_dir, year_name)\n        if not os.path.isdir(year_path):\n            continue\n        if not year_name.isdigit() or len(year_name) != 4:\n            continue\n\n        for month_name in sorted(os.listdir(year_path)):\n            month_path = os.path.join(year_path, month_name)\n            if not os.path.isdir(month_path):\n                continue\n            if not month_name.isdigit():\n                continue\n            month = int(month_name)\n            if month < 1 or month > 12:\n                continue\n\n            yield int(year_name), month, month_path\n\n\ndef is_month_before_today(year, month, today):\n    return (year < today.year) or (year == today.year and month < today.month)\n\n\ndef main():\n    args = parse_args()\n\n    tm_root = os.path.abspath(os.path.expanduser(args.tm_root))\n    env_dir = os.path.join(tm_root, args.environment)\n    stats_root_dir = os.path.join(env_dir, 'stats')\n    stats_bucket_name = args.environment + '.stats.touch-mapper'\n\n    if not os.path.isdir(env_dir):\n        print('Environment directory does not exist: {}'.format(env_dir), file=sys.stderr)\n        return 2\n    if not os.path.isdir(stats_root_dir):\n        print('Stats directory does not exist: {}'.format(stats_root_dir), file=sys.stderr)\n        return 2\n\n    try:\n        import boto3  # type: ignore[import-not-found]\n    except ImportError:\n        print('boto3 is required. Install with: sudo -H pip3 install --upgrade boto3', file=sys.stderr)\n        return 2\n\n    s3_resource = boto3.resource('s3')\n    today = datetime.datetime.utcnow().date()\n\n    uploaded_months = 0\n    skipped_months = 0\n    removed_months = 0\n\n    print('Uploading pending stats for environment={} stats_root={} bucket={}'.format(\n        args.environment,\n        stats_root_dir,\n        stats_bucket_name\n    ))\n\n    for year, month, month_path in iter_month_dirs(stats_root_dir):\n        ok = stats_pipeline.upload_month_from_local_data(\n            stats_root_dir=stats_root_dir,\n            s3_resource=s3_resource,\n            stats_bucket_name=stats_bucket_name,\n            year=year,\n            month=month,\n            max_day=None\n        )\n\n        if not ok:\n            skipped_months += 1\n            print('skip: failed to upload {:04d}-{:02d}'.format(year, month))\n            continue\n\n        uploaded_months += 1\n\n        if (not args.keep_local_months) and is_month_before_today(year, month, today):\n            shutil.rmtree(month_path)\n            removed_months += 1\n            print('cleanup: removed local month directory {}'.format(month_path))\n\n    print('done: uploaded_months={} skipped_months={} removed_months={}'.format(\n        uploaded_months,\n        skipped_months,\n        removed_months\n    ))\n    return 0\n\n\nif __name__ == '__main__':\n    sys.exit(main())\n"
  },
  {
    "path": "doc/application-stats-telemetry.md",
    "content": "# Application Stats Telemetry\n\nThis document describes the map-attempt telemetry pipeline and the quicktime simulation mode.\n\n## Overview\n\n`converter/process-request.py` records one telemetry JSON object per map attempt (success or failure) to local disk:\n\n`stats/<year>/<month>/<day>/<map-id>.json`\n\n- `<map-id>` is derived from `requestId` by taking the prefix before `/`.\n- If the same map id is written again on the same day, the existing file is overwritten.\n- Telemetry write runs in the final step so telemetry failures never affect user-visible processing.\n\n## Deploy code version metadata\n\nConverter telemetry reads deployed `dist/VERSION.txt` (same directory as `process-request.py`) once at process startup.\nExpected line format:\n\n`<timestamp> <branch> <tag> <commit>`\n\nExample:\n\n`2026-02-15T08:25:31 touch-mapper.larger-sizes package-20260215-082120 4714eb471656fc2737543657ef5be6e4056bdb11`\n\nStored telemetry fields:\n\n- `code_branch`: branch value from `VERSION.txt` (example: `touch-mapper.larger-sizes`)\n- `code_deployed`: value extracted from `package-*` tag (example: `20260215-082120`)\n- `code_commit`: commit hash value from `VERSION.txt`\n\nIf `VERSION.txt` is missing/malformed, telemetry processing still succeeds and these fields are stored as `null`.\n\n## Timing telemetry fields\n\nConverter telemetry includes per-stage timing fields (seconds), including:\n\n- `timing_get_osm_seconds` (successful OSM fetch attempt only; excludes prior timed-out attempts and excludes only-big-roads pruning)\n- `timing_prune_only_big_roads_seconds` (runtime of `prune-only-big-roads.js` when `content_mode=only-big-roads`, otherwise null)\n- `timing_map_desc_seconds`\n- `timing_upload_primary_seconds` (total for primary uploads: info JSON, map-content JSON, main STL)\n- `timing_svg_to_pdf_seconds`\n- `timing_total_seconds`\n\nOSM fetch source fields:\n\n- `osm_fetch_provider`: provider category for the successful fetch attempt (`overpass` or `main_api`)\n- `osm_fetch_endpoint`: endpoint URL for the successful fetch attempt\n\nFetch policy notes:\n\n- All content modes (`normal`, `no-buildings`, `only-big-roads`) use randomized Overpass `map?bbox` endpoint attempts first, then OSM main API fallback.\n\n## Status polling and structured errors\n\nMap creation progress is now published to `map/info/<id>.json` via top-level `status`:\n\n- `status.progress` (`20`, `60`, `80`, `100`)\n- `status.errorCode` (`unknown` or `too_large`) on failures\n- `status.errorDescription` diagnostic text on failures\n\nFor `too_large`, converter includes size/threshold details in `errorDescription`, for example:\n\n- `OSM data is 73400321 > 73400320 bytes before pruning`\n- `OSM data is 26220000 > 26214400 bytes after pruning`\n\n## RAM telemetry fields\n\nRAM telemetry combines:\n- Subprocess stage RSS from `/usr/bin/time -v` via `converter/telemetry.py` (`maxRssKiB`)\n- In-process peak RSS sampled from `/proc/self/status` inside `process-request.py` (VmRSS)\n\n`process-request.py` stores these fixed fields:\n\n- `rss_osm2world_kib`\n- `rss_blender_kib`\n- `rss_clip_2d_kib`\n- `rss_prune_only_big_roads_kib` (from `prune-only-big-roads.js` subprocess in `content_mode=only-big-roads`)\n- `rss_svg_to_pdf_kib` (from the CairoSVG subprocess used for SVG -> PDF conversion)\n- `rss_process_request_peak_kib` (peak VmRSS observed in `process-request.py` itself)\n\nThese represent top memory consumers for the converter pipeline and are written as KiB integers.\nIf timings JSON is missing/malformed or RSS is unavailable, fields are stored as `null` and request processing continues.\n\n## OSM size telemetry fields\n\nConverter telemetry also stores two OSM file size fields (bytes):\n\n- `osm_fetched_bytes`: file size immediately after OSM fetch and before content filtering.\n- `osm_pruned_bytes`: file size after content-mode pruning/filtering (`no-buildings`, `only-big-roads`), or same as fetched size when no pruning is applied.\n\nStructured error telemetry fields:\n\n- `error_code`: converter error code (`unknown`, `too_large`, ...)\n- `error_description`: diagnostic text linked to `error_code`\n\n## Browser IP and geolocation\n\n- Browser code also computes a stable hashed fingerprint (`browserFingerprint`) from browser/device properties.\n- Converter telemetry stores it as `browser_fingerprint` for approximate unique-visitor counting.\n- Browser code (`web/src/scripts/map-creation.js`) performs a best-effort public IP lookup before enqueueing the SQS request and includes `browserIp` in the message body.\n- Converter telemetry stores that value as `browser_ip`.\n- Browser code also includes `browserReferrer` (`document.referrer`) and converter telemetry stores it as `browser_referrer`.\n- During per-request stats write, converter performs a best-effort IP geolocation lookup and stores:\n  - `browser_ip_country`\n  - `browser_ip_country_code`\n  - `browser_ip_region`\n  - `browser_ip_city`\n  - `browser_ip_latitude`\n  - `browser_ip_longitude`\n- Geolocation results are cached in a local file to avoid repeated lookups:\n  - `stats/.maintenance/ip-geo-cache.json`\n\n## S3 monthly object format\n\nLocal stats are compacted into one gzipped NDJSON object per month:\n\n`s3://<environment>.stats.touch-mapper/stats-json/<YYYY>/<MM>/stats-<YYYY>-<MM>.jsonl.gz`\n\n- Each line is one JSON object.\n- Content encoding is gzip.\n- Storage class is left as default `STANDARD` (normal redundancy).\n\n## Normal mode behavior\n\n- At `process-request.py` startup, daily maintenance runs once per UTC day using a lock file.\n- The job rebuilds month-to-date NDJSON for the previous UTC day and rewrites the monthly S3 object.\n- When the previous day is the final day of a month, the local month directory is deleted after successful upload.\n\nMaintenance files under `stats/.maintenance/`:\n\n- `upload.lock`\n- `last-successful-run-utc.txt`\n\n## Quicktime mode behavior\n\nQuicktime mode is controlled by a constant in `converter/process-request.py`:\n\n- `STATS_QUICKTIME_MODE = True`\n\nWhen enabled:\n\n- Every 3 telemetry writes => one virtual day passes and monthly S3 upload runs.\n- Every 3 virtual day rollovers => one virtual month passes and the completed local month directory is deleted.\n\nQuicktime state files under `stats/.maintenance/`:\n\n- `quicktime.lock`\n- `quicktime-state.json`\n\nQuicktime exists for end-to-end testing of day/month rollover logic without waiting for real calendar boundaries.\n\nQuicktime smoke runner (local, no AWS calls):\n\n```bash\npython3 converter/run-stats-quicktime-smoke.py --root .tmp/telemetry-quicktime-smoke --attempts 9\n```\n\n## Athena table\n\nCloudFormation provisions:\n\n- Private stats bucket (`<environment>.stats.touch-mapper`)\n- Glue database\n- Glue external table `application_stats_json`\n\nTable characteristics:\n\n- JSON SerDe (`org.openx.data.jsonserde.JsonSerDe`)\n- Partitioned by `year`, `month`\n- Partition projection enabled\n- Table location root: `s3://<environment>.stats.touch-mapper/stats-json/`\n\nQuery example:\n\n```sql\nSELECT status, count(*)\nFROM application_stats_json\nWHERE year = '2026' AND month = '02'\nGROUP BY status;\n```\n\nQuery example grouped by deployed code commit:\n\n```sql\nSELECT code_commit, count(*) AS attempts\nFROM application_stats_json\nWHERE year = '2026' AND month = '02'\nGROUP BY code_commit\nORDER BY attempts DESC;\n```\n\nQuery example for monthly RAM percentiles:\n\n```sql\nSELECT\n  format('%s-%02d', year, CAST(month AS INTEGER)) AS year_month,\n  approx_percentile(rss_osm2world_kib, 0.5) AS p50_osm2world_kib,\n  approx_percentile(rss_osm2world_kib, 0.95) AS p95_osm2world_kib\nFROM application_stats_json\nWHERE rss_osm2world_kib IS NOT NULL\nGROUP BY 1\nORDER BY 1;\n```\n\n## Manual pending-stats upload (EC2)\n\nTo force-upload all pending local stats JSON files for one environment:\n\nThe script is deployed on EC2 at `~/touch-mapper/<environment>/dist/upload-pending-stats.py` for both `test` and `prod`.\n\n```bash\n# On EC2 host\ncd ~/touch-mapper/<environment>/dist\n./upload-pending-stats.py <environment>\n```\n\nExamples:\n\n```bash\ncd ~/touch-mapper/test/dist\n./upload-pending-stats.py\n# or explicitly:\n./upload-pending-stats.py test\n\ncd ~/touch-mapper/prod/dist\n./upload-pending-stats.py\n# or explicitly:\n./upload-pending-stats.py prod\n```\n\nBehavior:\n- Uploads each month directory under `~/touch-mapper/<environment>/stats/YYYY/MM/` into:\n  - `s3://<environment>.stats.touch-mapper/stats-json/YYYY/MM/stats-YYYY-MM.jsonl.gz`\n- Keeps current month local files.\n- Removes local past-month directories after successful upload.\n- Use `--keep-local-months` to disable cleanup.\n"
  },
  {
    "path": "doc/converter-pipeline-stages.md",
    "content": "# Converter Pipeline Stages\n\nThis is the authoritative source for converter stage names and metadata lifecycle definitions.\n\nThis document describes converter data flow and stage names used by code comments and artifacts.\n\n## OSM2World notes\n- `OSM2World/` is a modified upstream dependency and rarely changed.\n- To build safely, use:\n  - `ant clean jar`\n- OSM2World outputs:\n  - `map.obj`: geometry (height is applied later in Blender).\n  - `map-meta-raw.json`: semantic metadata before Touch Mapper enrichment.\n  - `clip-2d` then clips `map.obj` into grouped `.ply` meshes for Blender input.\n\n## Processing pipeline\n1. OSM data is fetched from OSM servers for the requested area.\n2. OSM2World reads OSM data and outputs `map.obj` and `map-meta-raw.json`.\n3. `clip-2d` clips OBJ triangles to map bounds and writes grouped `.ply` files plus `map-clip-report.json`.\n4. Blender (`obj-to-tactile.py`) reads grouped `.ply` files and writes tactile outputs (`map.stl`, split STLs, SVG, blend, wireframes).\n5. `converter.map_desc` enriches metadata and writes `map-meta.augmented.json`, `map-meta.json`, and `map-content.json`.\n6. `converter/process-request.py` uploads artifacts to S3. Uploaded `.map-content.json` includes `metadata.requestBody` (full request params including real `requestId`).\n7. Browser UI fetches `.map-content.json` from S3/CloudFront and presents map descriptions.\n\n### OSM fetch mode notes\n- All content modes (`normal`, `no-buildings`, `only-big-roads`) use the same network fetch strategy:\n  - randomized Overpass `xapi?map?bbox=` endpoint attempts first\n  - OSM main API `api/0.6/map?bbox=` fallback last\n- Mode-specific behavior is applied after fetch:\n  - `normal`: no local OSM content pruning.\n  - `no-buildings`: local OSM filtering removes building features.\n  - `only-big-roads`: local OSM pruning keeps major-road-focused content for tactile density/continuity.\n\n## Metadata stage names (must stay in sync)\nAny time you change these stages, keep this document and in-code stage comments synchronized.\n\nEach referenced code file contains comments in format:\n- `Code below creates stage \"<stage name>\" data`\n\n### Stage: OSM2World raw meta\n- Created at: `osm-to-tactile.py`\n- Stored as: `map-meta-raw.json`\n- Diff from previous: baseline semantic output from OSM2World; no Touch Mapper enrichment.\n\n### Stage: Raw meta with visibility augmentation\n- Created at: `converter/map_desc/__init__.py`\n- Stored as: `map-meta.augmented.json`\n- Diff from previous: adds `visibleGeometry` to line strings (clipped to boundary when possible).\n\n### Stage: Raw meta with building/water area visibility raster\n- Created at: `converter/map_desc/__init__.py`\n- Stored as: `map-meta.augmented.json`\n- Diff from previous: adds `visibleGeometry` raster summaries for building and rendered water-area polygons (coverage, segments, components, shape).\n\n### Stage: Grouped + classified meta\n- Created at: `converter/map_desc/__init__.py`\n- Stored as: `map-meta.json`\n- Diff from previous: reorganized into TM classes/subclasses with `_classification` and location annotations.\n\n### Stage: Render-ready intermediate\n- Created at: `converter/map_desc/map_desc_render.py`\n- Stored as: in-memory (not written to disk)\n- Diff from previous: items are grouped/sorted with display labels, counts, lengths/areas, and connectivity.\n\n### Stage: Final map content\n- Created at: `converter/map_desc/map_desc_render.py`\n- Stored as: `map-content.json`\n- Diff from previous: serializes structured grouped data for all classes.\n"
  },
  {
    "path": "doc/creating-new-server.doc",
    "content": "Overview\n--------\n\n- Tarball test instance's \"dist\" to S3 (make test-cp-to-s3.sh)\n- Create a fresh Ubuntu instance, configure it a little, and install ec2-init.sh from S3 \n\nCreating a new server\n---------------------\n\n- Create a Ubuntu nano instance in EC2\n  - tag Name=tm-YYYYMMDD-HHMM\n  - Use ~/.ssh/amazon-ec2-second_keypair.pubkey\n  - Update the public IP to \"tm-ec2\" in ~/.ssh/config\n\n- Configure the instance:\nscp install/setup-ec2.sh ubuntu@tm-ec2:/tmp/\nssh tm-ec2\nbash /tmp/setup-ec2.sh\nIn another terminal: cat ~/.aws/{config,credentials}\n\n- Move test instance from old server\n  - Stop test instance on the old server\n    pkill -f \"poller.sh test\"\n    pkill -f ./process-request.py\n\n  - Install to test instance\n    make test-install-ec2\n\n  - Reboot server and verify that https://test.touch-mapper.org works\n\n  - Copy test to prod\n    make prod-install-ec2\n\n  - Reboot server\n  - Stop old server\n  - Verify that https://touch-mapper.org works\n\n- UPDATE CLOUDWATCH ALERTS\n\n"
  },
  {
    "path": "doc/deployed-map-inspection.md",
    "content": "# Deployed map inspection by map ID\n\nThis guide is for inspecting a deployed map (test env) from a URL like:\n`https://test.touch-mapper.org/en/map?map=<MAP_ID>`\n\nUse this when you need to validate deployed artifacts (`.map-content.json`, `.stl`, `.svg`, `.pdf`, `.blend`) or compare deployed output with local test fixtures.\n\n## Key idea\n\n`MAP_ID` is persistent. Actual artifact URLs are derived from `info.requestId`, not directly from the page HTML download links.\n\n## URL rules (from deployed app-common.js)\n\n1. v2 ID (starts with `B`):\n- info URL: `/map/info/<MAP_ID>.json`\n- data prefix: `/map/data/<REQUEST_ID_ENCODED>`\n\n2. v1 ID (legacy lowercase):\n- info URL: `/map/<MAP_ID>/info.json`\n- data prefix: `/map/<REQUEST_ID_ENCODED>`\n\n3. `REQUEST_ID_ENCODED`:\n- Start from `info.requestId` (fallback to `MAP_ID` if missing).\n- If it has two segments (`<id>/<name>`), URL-encode only the second segment and keep the slash.\n\n## Minimal inspection workflow\n\n```bash\nMAP_ID='B739ab809b79de80'\nHOST='https://test.touch-mapper.org'\n\n# Build info URL by ID version\nif [[ \"$MAP_ID\" == B* ]]; then\n  INFO_URL=\"$HOST/map/info/$MAP_ID.json\"\nelse\n  INFO_URL=\"$HOST/map/$MAP_ID/info.json\"\nfi\n\n# Read key metadata\ncurl -sSfL \"$INFO_URL\" | jq '{requestId, printingTech, addrShort, addrLong, size, scale}'\n\nREQUEST_ID=\"$(curl -sSfL \"$INFO_URL\" | jq -r '.requestId // empty')\"\nif [[ -z \"$REQUEST_ID\" ]]; then REQUEST_ID=\"$MAP_ID\"; fi\n\nREQUEST_ID_ENCODED=\"$(printf '%s' \"$REQUEST_ID\" | jq -sRr '\n  split(\"/\") as $p |\n  if ($p|length) > 1 then $p[0] + \"/\" + ($p[1]|@uri) else $p[0] end\n')\"\n\nif [[ \"$MAP_ID\" == B* ]]; then\n  BASE=\"$HOST/map/data/$REQUEST_ID_ENCODED\"\nelse\n  BASE=\"$HOST/map/$REQUEST_ID_ENCODED\"\nfi\n\necho \"INFO_URL=$INFO_URL\"\necho \"BASE=$BASE\"\n```\n\n## Validate deployed artifacts\n\n```bash\nfor SUFFIX in '.map-content.json' '.stl' '-ways.stl' '-rest.stl' '.svg' '.pdf' '.blend'; do\n  URL=\"${BASE}${SUFFIX}\"\n  echo \"== $URL ==\"\n  curl -sSfL -I \"$URL\" | sed -n '1p;/content-type/p;/content-encoding/p;/last-modified/p'\ndone\n```\n\n## Read map-content.json safely\n\nCloudFront/S3 may return gzip-encoded JSON even when extension is `.json`.\n\n```bash\ncurl -sSfL \"${BASE}.map-content.json\" | gzip -dc | jq -r 'keys'\ncurl -sSfL \"${BASE}.map-content.json\" | gzip -dc | jq -r '\n  to_entries[]\n  | select(.value.subclasses? != null)\n  | \"\\(.key) subclasses=\\(.value.subclasses|length)\"\n'\n```\n\n## Compare deployed map-content with local fixture\n\n```bash\ncurl -sSfL \"${BASE}.map-content.json\" | gzip -dc | jq -S 'del(.metadata)' > /tmp/live-map-content.json\njq -S 'del(.metadata)' test/data/map-content.json > /tmp/local-map-content.json\ndiff -u /tmp/local-map-content.json /tmp/live-map-content.json | sed -n '1,200p'\n```\n\n## Notes\n\n- Do not use `view-source:` URLs with curl. Fetch the normal page URL.\n- Keep to `curl -sSfL` command style in this environment.\n- Uploaded `.map-content.json` may include `metadata.requestBody`; remove `metadata` when comparing against local fixtures that do not include upload-time metadata.\n- If deployed JS changes URL builders, update this file (and keep AGENTS pointer intact).\n"
  },
  {
    "path": "doc/development-conventions.md",
    "content": "# Development Conventions\n\nThis is the authoritative source for coding, web/i18n, and Python development conventions.\n\nThis document collects day-to-day coding conventions for Touch Mapper.\n\n## Repo layout (high level)\n- `web/`: static web UI built with Metalsmith + ECT + Less.\n- `converter/`: Python-based OSM -> tactile map conversion pipeline.\n- `OSM2World/`: vendored upstream with local modifications; rarely changed.\n- `install/`: AWS packaging/deploy scripts for server-side code.\n- `translation/`: helper scripts for spreadsheet-based translation collection (rarely used).\n\n## Web source vs generated files\n- `web/src/*.ect` are generated from `web/pre-src/*.pre` via `web/pre2src.py`.\n  - Edit `web/pre-src/*.pre`, not `web/src/*.ect`.\n- `web/build.js` runs the Metalsmith pipeline; output goes to `web/build/`.\n- `web/src/styles/*.less` are the source; compiled to CSS during the build.\n\n## Web build conventions\n- Build pipeline (`web/build.js`) does:\n  - Converts `{{ key }}` in ECT files to `@t('key')` for i18n.\n  - Runs `metalsmith-i18next` over `web/locales/*/tm.json`. Available locales are locale folders under `web/locales`.\n  - Compiles Less and concatenates JS bundles into `scripts/app-common.js` and `scripts/vendor-common.js`.\n- JavaScript should be as modern as possible while still working on latest Edge, Chrome, Firefox, and Safari (no legacy browser support).\n  - OK: `async/await`, `fetch`, optional chaining, nullish coalescing, `URL`, `URLSearchParams`, `AbortController`, `class`.\n  - Prefer: `const` by default, `let` when reassignment is needed (avoid `var`).\n  - Prefer: `===`/`!==`, template literals, and `for...of` over index loops when iterating arrays.\n  - Avoid: implicit globals, `with`, or `eval`.\n  - Avoid: manual `XMLHttpRequest` or legacy polyfills aimed at IE/old Edge.\n- `web/src/index.html` is a small redirect page; main page template is `web/pre-src/index.pre`.\n- `web/src/scripts/environment.js.*` are environment stubs; `web/create-env-js.sh` generates a real `environment.js` (requires AWS CLI + config).\n\n## Translations and i18n\n- Translation keys live in `web/locales/<lang>/tm.json`; English is source of truth and runtime fallback.\n- Keys are lowercase snake_case; prefixes group by feature (examples: `location2_`, `location3_`, `map_`, `multipart_`, `meta_`, `printing_`).\n- In templates, use `{{ key }}` in `web/pre-src/*.pre`; `web/build.js` converts this to `@t('key')`.\n- When adding/changing strings, update all locale files in the same change and translate non-English strings directly; if unknown, use English as placeholder.\n- Translation spreadsheets in `translation/` exist for human translators but are not the primary edit path.\n- `window.TM.translations` is a small, template-injected subset of strings for JS and not a full locale dictionary.\n- There is no full client-side locale dictionary; JS should receive text from templates (rendered HTML / `data-*` attributes), not runtime key lookup.\n\n## Coding guidelines\n- In `converter/`, do not preserve backward compatibility by default. If data production changes, matching consumer changes are expected.\n- For UI-only changes, confirm whether backward compatibility is required.\n- Prioritize accessibility in UI and content; Touch Mapper is used by visually impaired users.\n- Above non-trivial functions always write a comment that describes the function's purpose (not how it works)\n\n## Python conventions\n- Write modern Python; avoid Python 2 compatibility patterns.\n- Blender runtime for some scripts is old (`blender/2.78/python/bin/python3.5m` or similar); verify runtime version at start.\n- Prefer: f-strings, `pathlib`, context managers (`with`), and `enumerate`/`zip` over manual index loops.\n- Prefer: explicit exceptions (no bare `except`) and small pure functions.\n- Logging: `print` for CLI/one-off tooling; `logging` for long-running/importable code.\n- Avoid: mutable default args, implicit `None` returns for data-producing functions, and excessive global state.\n- Use type hints broadly and use functional `TypedDict` form for maximal type checking.\n- Type checking target: Pylance/Pyright \"standard\" (see `pyrightconfig.json`).\n  - Run `pyright` for changed Python files and fix reported issues.\n\n## AWS and deployment notes\n- Root `Makefile` has targets for AWS install and packaging (dev/test/prod).\n- `install/parameters.sh` derives environment names; `TOUCH_MAPPER_DEV_ENV` controls dev suffix.\n- `install/run-dev-converter.sh` runs OSM -> STL converter locally.\n\n## Setup references\n- See `doc/development-setup.md` for full local setup and web workflow.\n- `init.sh` installs system dependencies and builds `OSM2World`.\n"
  },
  {
    "path": "doc/development-setup.md",
    "content": "# Development Setup\n\nThis is the authoritative source for local setup and run workflows.\n\nThis document contains local setup and deployment-oriented developer workflows.\n\n## Install dependencies\n\n```bash\n./init.sh\n```\n\n## Setup AWS CLI\n\n```bash\naws configure\n```\n\n## Create AWS resources\n\n```bash\nmake dev-aws-install\n```\n\nAPI Gateway for email-sending Lambda is currently not created automatically.\nThe same API Gateway endpoints can be reused across Touch Mapper instances.\n\n## Install static website to S3\n\n```bash\nmake dev-web-s3-install\n```\n\nThe last output line includes the CloudFront URL (for example `https://something.cloudfront.net`) for accessing the web UI.\n\n## Run OSM -> STL converter service\n\nRun in a separate terminal tab:\n\n```bash\ninstall/run-dev-converter.sh\n```\n\n## Local web development\n\nIn `web/`, run these in separate terminal tabs:\n\n```bash\nmake watch\nmake serve\n```\n\nThen open `http://localhost:9000`.\n"
  },
  {
    "path": "doc/map-content-verification.md",
    "content": "# Map-Content Verification\n\nThis is the authoritative source for verification commands and content/locale review workflow.\n\nUse this workflow when changing converter map-description logic or related UI description-model code.\n\n## Core suite\n- Use tests under `test/map-content/`.\n- Canonical workflow and CLI details live in `doc/map-description-introspection.md`.\n- Fast default command:\n  - `node test/map-content/run-tests.js --category average --offline --jobs 1`\n- Prefer `simple`/`average` for routine checks.\n- Use `complex` mainly for performance profiling.\n\n## Blender visual regression artifacts\nRun with `--with-blender` to generate geometry regression snapshots in `test/map-content/out/<category>/pipeline/`:\n- `map-wireframe-flat.png`: before geometry modifications (flat source geometry).\n- `map-wireframe.png`: after geometry modifications.\n\n## Content and language checks\n- Current UI grouping for linear features is roads + non-road linear groups (`paths`, `railways`, `waterways`, `otherLinear`) plus buildings.\n- If map content UI strings changed, inspect `simulated.txt` in each locale output for natural language quality.\n- For railway-related changes, verify rail-rich fixtures produce railway entries in both `normal` and `only-big-roads` modes.\n- For railway connectivity changes, verify railway junction/intersection narration is absent in simulated text output.\n- For an explicit mode check on one rail-rich OSM source, run:\n  - `node test/map-content/check-railway-modes.js --osm test/map-content/out/complex/source/map.osm --locale en`\n\nRequired for POI/type-label/i18n changes:\n- Run `inspect-map-description` against `test/map-content/out/complex/pipeline/map-content.json` for `en`, `de`, `fi`, and `nl`.\n- Render simulation text from resulting `mapDescriptionModel` and review POI lines in each locale.\n- Treat leftover English suffixes in non-English outputs as validation failures unless the borrowed word is intentionally identical in that locale.\n\n## Browser level testing\n\nPlaywright UI validation can be run with `bash test/e2e/run-touch-mapper-settings-regression.sh`.\nThe settings regression also verifies big-roads density visibility (shown only for `only-big-roads`) and value persistence across area -> map -> area navigation.\n"
  },
  {
    "path": "doc/map-description-introspection.md",
    "content": "# Map Description Introspection (Dev Only)\n\nThis workflow generates `map-content.json` from `map.osm` and then builds\nstructured map description models from UI code without rendering the page.\n\n## Overview\n\n1. `test/map-content/generate-map-content-from-osm.py`\n   - runs OSM2World on a `.osm` file\n   - runs `clip-2d` (when `--with-blender`) to produce grouped Blender `.ply` inputs\n   - runs `python3 -m converter.map_desc` on the generated `map-meta-raw.json`\n   - emits generated file paths as JSON\n2. `test/map-content/inspect-map-description.js`\n   - calls the Python generator above\n   - loads `map-desc-ways.js`, `map-desc-areas.js`, and `map-description.js`\n   - runs their `buildModel(...)` functions\n   - writes one machine-readable JSON artifact\n3. `test/map-content/run-tests.js`\n   - runs category tests (`--category`) or the full suite (`--all`) in parallel\n   - writes test artifacts to `test/map-content/out/<category>/`\n   - prints and stores per-stage timing data\n4. `test/map-content/tests.json`\n   - defines test categories (`simple`, `average`, `complex`) using server-style `requestBody` payload fields\n   - includes map bbox (`effectiveArea`) used for OSM fetching/caching\n\n## End-to-end example\n\n```bash\nnode test/map-content/inspect-map-description.js \\\n  --osm test/data/map.osm \\\n  --locale en \\\n  --out /tmp/tm-map-description-model.json\n```\n\nThe command prints output JSON with:\n\n- `outPath`: written artifact path\n- `mapContentPath`: generated `map-content.json` path\n\nThe artifact includes:\n\n- source metadata (`inputOsmPath`, locale, generator output paths)\n- `waysModel` from `window.TM.mapDescWays.buildModel(...)` (all linear features)\n- `areasModel` from `window.TM.mapDescAreas.buildModel(...)`\n- `mapDescriptionModel` from `window.TM.mapDescription.buildModel(...)` (`roads`, `paths`, `railways`, `waterways`, `waterAreas`, `otherLinear`, `buildings`)\n\n## Python generator usage\n\n```bash\npython3 test/map-content/generate-map-content-from-osm.py \\\n  --osm test/data/map.osm \\\n  --out-dir /tmp/tm-map-desc-work\n```\n\nOptional flags:\n\n- `--scale <int>`: OSM2World `TOUCH_MAPPER_SCALE` value (default `1400`)\n- `--content-mode <normal|no-buildings|only-big-roads>`: set content mode for conversion (`no-buildings` maps to `TOUCH_MAPPER_EXCLUDE_BUILDINGS=true`)\n- `TOUCH_MAPPER_TRIANGULATION_COLLINEAR_TOLERANCE_M=<float>` (env): floor-level triangulation simplification tolerance in meters (default `0.01`; set `0` to disable)\n- `--with-blender`: also run Blender tactile export and write `map.stl`, `map-ways.stl`, `map-rest.stl`, `map.svg`, `map.blend`, pre-modification wireframe-overlay render `map-wireframe-flat.png`, and post-modification wireframe-overlay render `map-wireframe.png` into `--out-dir`\n  - also writes `map-clip-report.json` from the `clip-2d` stage\n- `--diameter <int>` and `--size <float>`: required when `--with-blender` is used\n- `--no-borders`: pass through to Blender export when `--with-blender` is used\n- `--marker1 <json>`: pass marker position JSON through to Blender export when `--with-blender` is used\n\n## Notes\n\n- This tooling is development-only; production browser/runtime behavior is not\n  changed.\n- Locale dictionaries are loaded from `web/locales/<lang>/tm.json` with English\n  fallback from `web/locales/en/tm.json`.\n\n## Smoke tests\n\n```bash\nbash test/map-content/test-map-content-suite.sh\n```\n\nThis script covers:\n\n- all three category tests (`simple`, `average`, `complex`)\n- required artifact presence in `test/map-content/out/<category>/`\n- per-locale structured and text-simulated outputs\n\n## Category Test Runner\n\nRun all tests:\n\n```bash\nnode test/map-content/run-tests.js --all\n```\n\nRun one category:\n\n```bash\nnode test/map-content/run-tests.js --category average\n```\n\nOptional flags:\n\n- `--jobs <N>`: max tests to run in parallel\n- `--offline`: use only cached OSM input from `test/map-content/cache/`\n- `--keep-existing-out`: do not clean `test/map-content/out/<category>/` before a run\n- `--with-blender`: run Blender tactile export during generation and keep the generated `map*.stl`, `.svg`, `.blend`, `map-wireframe-flat.png`, and `map-wireframe.png` files in `test/map-content/out/<category>/pipeline/`\n\n### Makefile shortcuts\n\nFrom `test/map-content/`:\n\n- `make average` / `make complex`: regular map-content runs\n- `make average-bl` / `make complex-bl`: same runs with Blender outputs in each category's `pipeline/` directory\n- `make simple-bl` and `make all-bl` are also available\n\n## Test Definition Shape\n\n`test/map-content/tests.json` uses this structure:\n\n```json\n{\n  \"tests\": [\n    {\n      \"category\": \"simple\",\n      \"requestBody\": {\n        \"lat\": 0,\n        \"lon\": 0,\n        \"scale\": 2100,\n        \"diameter\": 357,\n        \"size\": 17,\n        \"effectiveArea\": {\n          \"lonMin\": 0,\n          \"lonMax\": 0,\n          \"latMin\": 0,\n          \"latMax\": 0\n        }\n      }\n    }\n  ]\n}\n```\n\n## Recommended Usage For Development\n\n- Use `simple` or `average` for routine correctness checks while iterating.\n- Use `complex` primarily for performance profiling and hotspot analysis.\n- Typical quick validation command:\n  - `node test/map-content/run-tests.js --category average --offline --jobs 1`\n"
  },
  {
    "path": "doc/map-description-model-schema.md",
    "content": "# Map Description Model Schema\n\nThis document defines the development-time JSON schema used to inspect map\ndescription outputs without rendering UI.\n\nThe schema is produced by:\n\n- `window.TM.mapDescWays.buildModel(...)`\n- `window.TM.mapDescAreas.buildModel(...)`\n- `window.TM.mapDescription.buildModel(...)`\n- `test/map-content/inspect-map-description.js`\n\n## Top-level structure\n\n```json\n{\n  \"roads\": \"SectionModel\",\n  \"paths\": \"SectionModel\",\n  \"railways\": \"SectionModel\",\n  \"waterways\": \"SectionModel\",\n  \"otherLinear\": \"SectionModel\",\n  \"buildings\": \"SectionModel\",\n  \"ui\": {\n    \"sectionHeightNotes\": {\n      \"roads\": \"string\",\n      \"paths\": \"string\",\n      \"railways\": \"string\",\n      \"waterways\": \"string\",\n      \"otherLinear\": \"string\",\n      \"buildings\": \"string\",\n      \"poiFamiliar\": \"string\",\n      \"poiDaily\": \"string\",\n      \"poiTransport\": \"string\"\n    },\n    \"buildingsToggle\": {\n      \"hiddenCount\": 0,\n      \"collapsedLabel\": \"string\",\n      \"expandedLabel\": \"string\"\n    }\n  }\n}\n```\n\n`roads` and `buildings` are always present in UI. `paths`, `railways`,\n`waterways`, and `otherLinear` are hidden in UI when empty.\n\n`ui.buildingsToggle` is `null` when the section does not need a show-more\ntoggle.\n\n`ui.sectionHeightNotes` contains one localized note string per map-content\nsection heading (for example raised mm, waved surface, or varying raised\namounts).\n\n## `SectionModel`\n\n```json\n{\n  \"items\": [\"ItemModel\"],\n  \"emptyMessage\": \"string or null\",\n  \"count\": 0\n}\n```\n\n- `items` contains render-ready items for that section.\n- `emptyMessage` is used when `count` is `0`.\n- `count` is the number of non-empty section items.\n\n## `ItemModel`\n\n```json\n{\n  \"type\": \"way | building | summary | message\",\n  \"attrs\": {\n    \"dataOsmId\": \"optional string\",\n    \"dataUnnamedSurface\": \"optional string\",\n    \"initiallyHidden\": \"optional boolean\"\n  },\n  \"lines\": [\"LineModel\"]\n}\n```\n\nNotes:\n\n- `type=\"summary\"` is currently used for unnamed-way aggregate rows.\n- `type=\"message\"` is used for section-level fallback text items.\n- `attrs.initiallyHidden=true` marks building items hidden behind the\n  show-more toggle in initial state.\n\n## `LineModel`\n\n```json\n{\n  \"className\": \"optional string\",\n  \"parts\": [\n    {\n      \"text\": \"string\",\n      \"className\": \"optional string\",\n      \"wrap\": true\n    }\n  ]\n}\n```\n\n- `className` maps to the corresponding line-level CSS class in rendered UI.\n- `parts` preserves text segmentation and style classes for inline spans.\n- `wrap=false` means the text fragment is rendered as raw inline text (no span).\n"
  },
  {
    "path": "doc/touch-mapper-en.txt",
    "content": "Touch Mapper – maps for visually impaired people\n\nTactile maps help blind and visually impaired people to perceive their environment. Traditionally making tactile maps has been slow, laborious, and expensive. The new Touch Mapper service makes it startlingly easy to create and 3D-print a tactile map of your environment.\n\nTouch Mapper can generate a tactile map of any address. A 3D model of the selected area is created (based on street address input) and a 3D print of this model can be made directly by the user, or ordered from the service.\n\nIn the tactile map, roads, buildings, and railways are elevated, pedestrian roads more so than other roads. The selected address is marked by a cone, water areas have a wavy surface, and the map can be properly oriented because the north-east corner is indicated.\n\nTouch Mapper is made by Finnish programmer Sofia Pahaoja.\n\nYou can find Touch Mapper at www.touch-mapper.org\n"
  },
  {
    "path": "doc/touch-mapper-fi.txt",
    "content": "Touch Mapper – Karttapalveluita näkövammaisille\n\nKohokartat auttavat sokeita ja heikkonäköisiä hahmottamaan ympäristöään. Perinteisesti kohokarttojen valmistaminen on ollut hidasta, työlästä ja kallista. Uusi, 3D-tulostusta hyödyntävä Touch Mapper -palvelu tekee siitä hätkähdyttävän helppoa.\n\nTouch Mapperin kautta on mahdollista hankkia yksilöllinen kohokartta haluamastaan alueesta. Kartasta luodaan 3D-malli, jonka voi joko tulostaa itse tai tilata valmiiksi tulostettuna. Kartta-aineistona Touch Mapper käyttää OpenStreetMapin avointa karttadataa.\n\nTulostettavassa kohokartassa talot ja kadut ovat koholla, kävelytiet erottuvat autoteistä. Haettu osoite on merkitty tunnisteella, vesialueet aaltoilevat ja kartan saa asetettua oikein päin pohjois-itä-nurkasta löytyvän merkin avulla.\n\nTouch Mapper on espoolaisen Sofia Pahaojan kehittämä palvelu.\n\nTouch Mapperin löydät osoitteesta www.touch-mapper.org\n"
  },
  {
    "path": "doc/ui-visual-baseline.md",
    "content": "# UI visual baseline for main views\n\nThis document captures the reference appearance for Touch Mapper's three main user-facing pages.\n\nUse this as a default guide for UI and styling changes unless a task explicitly requests a redesign.\n\n## Global visual language\n\n- Brand color is a bright Touch Mapper green used for primary actions, key panels, and header branding.\n- The default page background is white, with dark text and blue links.\n- Layout is centered, single-column, and intentionally simple.\n- Main interaction flow is vertical and linear: heading, actions, map or form, supporting content.\n- Panels and action bars use soft rounded corners and subtle shadows.\n- Form controls are simple rectangular inputs/selects with clear labels and predictable placement.\n- Visual style should feel practical and trustworthy, not decorative or trend-driven.\n\n## View 1: Address search (`start`)\n\n- Hero section uses a large photo background at the top.\n- Header items sit on top of the hero area, with logo at the top-left and language selector plus Help at the top-right.\n- A short, high-contrast tagline is centered near the top.\n- Intro text appears below the hero image and explains the value proposition in plain language.\n- A short three-step \"It's simple\" sequence uses numbered green circles.\n- The main search form is a white card-like row with left label (\"Street address\"), center text input, and right orange Search button.\n- Footer includes OpenStreetMap attribution and an open-source repository link.\n\n## View 2: Settings (`area`)\n\n- Breadcrumb appears at the top (Address search > Settings > Map).\n- Main heading is \"Map parameters\".\n- Form uses a clear two-column rhythm: left labels and right controls.\n- Controls include radios, checkboxes, dropdowns, and concise helper text.\n- Primary call-to-action is a wide bright-green button (\"Create tactile map\").\n- Large draggable map preview sits below form controls.\n- Scale indicator is shown under the preview map.\n- A smaller green back button appears below the map section.\n- Footer keeps map data attribution and repository link visible.\n\n## View 3: Map result (`map`)\n\n- Breadcrumb remains visible at the top for orientation.\n- Page title is location-specific (\"Map for ...\").\n- A prominent full-width bright-green download bar is the primary action.\n- Additional download links are inside the primary download block behind a \"Show more downloads\" toggle.\n- Email-share input and send button appear in a dedicated green panel.\n- OpenStreetMap attribution appears before the 3D map preview.\n- 3D preview is displayed in a large light-gray rounded container with soft shadow and clearer neutral contrast between layers for readability.\n- 3D preview controls hint text below the preview uses black text for stronger readability.\n- Map content description follows below as structured text sections.\n\n## Accessibility and UX guardrails\n\n- Keep strong contrast for all text and controls, including inside green action areas.\n- Preserve clear heading hierarchy and readable body text spacing.\n- Keep labels explicit and near their controls.\n- Maintain keyboard accessibility and visible focus states for links, inputs, and buttons.\n- Preserve meaningful link/button text so controls are understandable out of context.\n- Avoid relying on color alone to communicate state or importance.\n\n## Change policy\n\n- Treat this baseline as the \"do not drift\" reference for routine UI work.\n- If a task intentionally changes this look, update this file in the same change.\n- Document which view changed and what visual rule was intentionally revised.\n"
  },
  {
    "path": "doc/way-area-extrusion-tiers.md",
    "content": "# Way/Area Extrusion Tiers (Low / High / Building)\n\nThis is the authoritative source for tactile extrusion tier constants and mappings.\n\nThis document records how way/area features are rendered into tactile height tiers in the Blender conversion step.\n\nIt is derived from:\n\n- `converter/obj-to-tactile.py` (actual `bpy.ops.mesh.extrude_region_move` usage and heights)\n- `OSM2World/src/org/osm2world/core/target/obj/ObjTarget.java` (how `::pedestrian` suffix is assigned)\n- `OSM2World/src/org/osm2world/core/map_data/object_info/TouchMapperCategory.java` (category naming)\n- `converter/map_desc/map-description-classifications.json` (user-facing way/area subtype names)\n\n## Tier Definitions\n\n- `low`: raised/depressed with the lower relief values\n- `high`: raised with the higher non-building relief values\n- `building`: dedicated building extrusion height\n\nCurrent constants in `converter/obj-to-tactile.py`:\n\n- `ROAD_HEIGHT_CAR_MM = 0.82`\n- `ROAD_HEIGHT_PEDESTRIAN_MM = 1.5`\n- `BUILDING_HEIGHT_MM = 2.9`\n- `WATERWAY_DEPTH_MM = 0.55`\n- `WATER_AREA_DEPTH_MM = 1.5` (wave pattern height target)\n\n## Source-of-Truth Mapping by OBJ Category\n\nThese are the categories/prefixes actually handled in `process_objects(...)`.\n\n| OBJ category/prefix | Way/Area kind | Tier | How it is extruded |\n|---|---|---|---|\n| `Road*` with no `::pedestrian` suffix | way/area | `low` | `raise_ob(..., ROAD_HEIGHT_CAR_MM * mm_to_units)` |\n| `Road*::pedestrian` | way/area | `high` | `raise_ob(..., ROAD_HEIGHT_PEDESTRIAN_MM * mm_to_units)` |\n| `Rail*` | way | `low` | `do_ways(..., ROAD_HEIGHT_CAR_MM * mm_to_units * 0.99)` |\n| `Waterway*` or `River*` | way | `low` (depressed) | `raise_ob(..., WATERWAY_DEPTH_MM * mm_to_units)` |\n| `Water*` or `AreaFountain*` | area | `high` | `water_wave_pattern(..., WATER_AREA_DEPTH_MM * mm_to_units, ...)` after remesh prep extrusion |\n| `Building*` | area | `building` | `extrude_building(..., BUILDING_HEIGHT_MM * mm_to_units)` |\n\nNotes:\n\n- `BuildingEntrance*` is deleted before extrusion and is not rendered as its own tactile tier.\n- Unknown/unhandled object prefixes are not sent through `raise_ob(...)` / `extrude_building(...)`; they remain un-extruded by this stage.\n\n## Map Page Height Note Mapping\n\nMap page section notes are shown directly below each section heading. Current\nmapping is:\n\n| Map page section | Note text rule |\n|---|---|\n| `roads` | `Raised __mm__ mm` (from road car extrusion height) |\n| `paths` | `Raised __mm__ mm` (from pedestrian road extrusion height) |\n| `railways` | `Raised __mm__ mm` (from rail extrusion height) |\n| `waterways` | `Waved surface` |\n| `waterAreas` | `Waved surface` |\n| `otherLinear` | `Raised by varying amounts` when mixed/unknown |\n| `buildings` | `Raised __mm__ mm` (from building extrusion height) |\n\nIf a section resolves to more than one non-water profile in current map data,\nthe fallback note is `Raised by varying amounts` instead of a single mm value.\n\n## How \"Pedestrian\" vs \"Car\" Is Decided\n\nThe suffix `::pedestrian` is assigned in OSM2World (`ObjTarget.java`) when a way/node is path-like. Path-like detection includes, among others:\n\n- `highway=path|footway|cycleway|service|bridleway|living_street|pedestrian|track|steps`\n- `footway=*`, `foot=yes|designated`\n- `tourism=attraction`, `man_made=pier|breakwater`\n\n`obj-to-tactile.py` then uses that suffix (`road_name.endswith(\"::pedestrian\")`) to choose the `high` tier for `Road*` and `RoadArea*`.\n\n## Mapping to Current Map-Description Way/Area Subtypes\n\nThis section maps the user-facing subtype names to tactile tiers as currently rendered.\n\n### Linear subclasses (`A*`)\n\n| Subclass | Tier |\n|---|---|\n| `A1_road_construction` | `low` |\n| `A1_major_roads` | `low` |\n| `A1_secondary_roads` | `low` |\n| `A1_local_streets` | `low` |\n| `A1_service_roads` | `low` |\n| `A1_track_roads` | `low` |\n| `A1_vehicle_unspecified` | `low` |\n| `A2_pedestrian_streets` | `high` |\n| `A2_footpaths_trails` | `high` |\n| `A2_cycleways` | `high` |\n| `A2_steps_ramps` | `high` |\n| `A2_pedestrian_unspecified` | `high` |\n| `A3_rail_lines` | `low` |\n| `A3_tram_light_rail` | `low` |\n| `A3_subway_metro` | `low` |\n| `A3_rail_yards_sidings` | `low` |\n| `A4_rivers` | `low` |\n| `A4_streams_canals` | `low` |\n| `A4_ditches_drains` | `low` |\n| `A4_other_waterways` | `low` |\n| `A_other_ways` | usually unhandled by extrusion unless represented as one of the handled prefixes above |\n\n`A5_connectivity_nodes` is node-only, not a way/area extrusion tier.\n\n### Areal subclasses (`B*`)\n\n| Subclass | Tier |\n|---|---|\n| `B1_lakes` | `high` (water area wave pattern) |\n| `B1_ponds` | `high` (water area wave pattern) |\n| `B1_reservoirs` | `high` (water area wave pattern) |\n| `B1_sea_coast` | `high` (water area wave pattern when represented as `Water*`/`AreaFountain*`) |\n| `B1_riverbanks` | `high` (water area wave pattern) |\n| `B1_other_water` | `high` (water area wave pattern) |\n| `B2_parks_recreation` | not explicitly extruded in `obj-to-tactile.py` (unhandled area family) |\n| `B2_forests` | not explicitly extruded in `obj-to-tactile.py` (unhandled area family) |\n| `B2_fields_open` | not explicitly extruded in `obj-to-tactile.py` (unhandled area family) |\n| `B3_residential` | not explicitly extruded in `obj-to-tactile.py` (unhandled area family) |\n| `B3_commercial` | not explicitly extruded in `obj-to-tactile.py` (unhandled area family) |\n| `B3_industrial` | not explicitly extruded in `obj-to-tactile.py` (unhandled area family) |\n| `B_other_areas` | not explicitly extruded in `obj-to-tactile.py` (unhandled area family) |\n\n### Building subclasses (`C*`)\n\n| Subclass | Tier |\n|---|---|\n| `C1_landmark` | `building` |\n| `C2_public` | `building` |\n| `C3_other_buildings` | `building` |\n\n## Relevant Code Pointers\n\n- Height constants: `converter/obj-to-tactile.py:19`\n- Building extrusion: `converter/obj-to-tactile.py:299`\n- Generic raise extrusion: `converter/obj-to-tactile.py:359`\n- Water area remesh prep extrusion: `converter/obj-to-tactile.py:367`\n- Feature bucketing + tier application: `converter/obj-to-tactile.py:598`\n- Pedestrian suffix logic: `OSM2World/src/org/osm2world/core/target/obj/ObjTarget.java:96`\n- Category naming: `OSM2World/src/org/osm2world/core/map_data/object_info/TouchMapperCategory.java:24`\n"
  },
  {
    "path": "init.sh",
    "content": "#!/bin/bash\n\nset -e\n\nBLENDER_DL_URL=https://download.blender.org/release/Blender2.78/blender-2.78c-linux-glibc219-x86_64.tar.bz2\nBLENDER_VERSION=2.78\n\n# Download Blender. Best to keep using the old version.\nblender_basename=$(basename $BLENDER_DL_URL)\nif [[ ! -f $blender_basename ]]; then\n    curl -O $BLENDER_DL_URL\n    tar xf $blender_basename\n    ln -sfn ${blender_basename%.tar.bz2} blender\nfi\n\n# Create symlinks\nln -s ../OSM2World ../blender converter || true\n\n# Install Python modules for AWS\npip install --target=converter/py-lib/boto3 boto3==1.2.2\n\n# Install Python modules for Blender scripts\ncurl -o /tmp/get-pip.py https://bootstrap.pypa.io/pip/3.5/get-pip.py\nblender/$BLENDER_VERSION/python/bin/python* /tmp/get-pip.py\nblender/$BLENDER_VERSION/python/bin/pip install --target=blender/$BLENDER_VERSION/python/lib/python3.5/svgwrite svgwrite==1.1.9\n\nsudo pip install xlwt xlrd # for translation file conversions\nsudo pip install awscli\nsudo apt install python3-cairosvg # for SVG to PDF conversion\n\n# Install \"jq\" and \"rename\" for install/web-s3.sh\nsudo apt install jq rename\n\n# Compile OSM2World\nmake osm2world\n\necho \"Finished\"\n\n"
  },
  {
    "path": "install/.gitignore",
    "content": "/dist\n/dist.old\n/lambda-email-sending.zip\n\n"
  },
  {
    "path": "install/cloudformation-update.sh",
    "content": "#!/bin/bash\n\nset -e\n\ncd \"$( dirname \"${BASH_SOURCE[0]}\" )\"\n\nif [[ $# != 1 ]]; then\n    echo \"Usage: $0 ENVIRONMENT\"\n    exit 1\nfi\nenvironment=$1\n\neval $( ./parameters.sh $environment )\n\nif aws cloudformation describe-stacks --stack-name $stack_name >&/dev/null; then\n    mode=update\nelse\n    mode=create\nfi\n\necho \"mode: $mode\"\necho \"stack_name: $stack_name\"\necho \"env_name: $env_name\"\necho \"is_dev_env: $is_dev_env\"\necho \"domain: $domain\"\n\ncmd=\"aws cloudformation $mode-stack --stack-name $stack_name --template-body file://cloudformation.json --capabilities CAPABILITY_IAM --parameters ParameterKey=Environment,ParameterValue=$env_name ParameterKey=IsDevEnv,ParameterValue=$is_dev_env ParameterKey=Domain,ParameterValue=$domain\"\necho \"Running: $cmd\"\n$cmd\n\n"
  },
  {
    "path": "install/cloudformation.json",
    "content": "{\n  \"AWSTemplateFormatVersion\": \"2010-09-09\",\n  \"Parameters\": {\n    \"Environment\": {\n      \"Description\": \"Environment Name: test, prod, or dev-USERNAME\",\n      \"Type\": \"String\",\n      \"AllowedPattern\": \"dev-.+|test|prod\"\n    },\n    \"IsDevEnv\": {\n      \"Description\": \"True if this is a development environment, so eg. has no Route53 domain name. Environment name should start with 'dev-'.\",\n      \"Type\": \"String\",\n      \"AllowedPattern\": \"true|false\"\n    },\n    \"Domain\": {\n      \"Description\": \"CloudFront domain for non-dev. Also defines S3 bucket names.\",\n      \"Type\": \"String\",\n      \"AllowedPattern\": \".*touch-mapper.org\"\n    }\n  },\n  \"Conditions\": {\n    \"IsDevEnv\": {\n      \"Fn::Equals\": [\n        {\n          \"Ref\": \"IsDevEnv\"\n        },\n        \"true\"\n      ]\n    }\n  },\n  \"Resources\": {\n    \"Logs\": {\n      \"Type\": \"AWS::S3::Bucket\",\n      \"Properties\": {\n        \"AccessControl\": \"LogDeliveryWrite\",\n        \"VersioningConfiguration\": {\n          \"Status\": \"Suspended\"\n        },\n        \"BucketName\": {\n          \"Fn::Join\": [\n            \".\",\n            [\n              {\n                \"Ref\": \"Environment\"\n              },\n              \"logs.touch-mapper\"\n            ]\n          ]\n        }\n      }\n    },\n    \"WebBucket\": {\n      \"Type\": \"AWS::S3::Bucket\",\n      \"Properties\": {\n        \"WebsiteConfiguration\": {\n          \"IndexDocument\": \"index.html\",\n          \"ErrorDocument\": \"error.html\"\n        },\n        \"AccessControl\": \"PublicRead\",\n        \"LoggingConfiguration\": {\n          \"DestinationBucketName\": {\n            \"Ref\": \"Logs\"\n          },\n          \"LogFilePrefix\": \"web/\"\n        },\n        \"VersioningConfiguration\": {\n          \"Status\": \"Suspended\"\n        },\n        \"BucketName\": {\n          \"Ref\": \"Domain\"\n        }\n      }\n    },\n    \"WebBucketRedirect\": {\n      \"Type\": \"AWS::S3::Bucket\",\n      \"Properties\": {\n        \"WebsiteConfiguration\": {\n          \"RedirectAllRequestsTo\": {\n            \"HostName\": {\n              \"Ref\": \"Domain\"\n            }\n          }\n        },\n        \"AccessControl\": \"PublicRead\",\n        \"LoggingConfiguration\": {\n          \"DestinationBucketName\": {\n            \"Ref\": \"Logs\"\n          },\n          \"LogFilePrefix\": \"webRedirect/\"\n        },\n        \"VersioningConfiguration\": {\n          \"Status\": \"Suspended\"\n        },\n        \"BucketName\": {\n          \"Fn::Join\": [\n            \".\",\n            [\n              \"www\",\n              {\n                \"Ref\": \"Domain\"\n              }\n            ]\n          ]\n        }\n      }\n    },\n    \"MapsBucket\": {\n      \"Type\": \"AWS::S3::Bucket\",\n      \"Properties\": {\n        \"WebsiteConfiguration\": {\n          \"IndexDocument\": \"index.html\"\n        },\n        \"AccessControl\": \"PublicRead\",\n        \"LoggingConfiguration\": {\n          \"DestinationBucketName\": {\n            \"Ref\": \"Logs\"\n          },\n          \"LogFilePrefix\": \"maps/\"\n        },\n        \"CorsConfiguration\": {\n          \"CorsRules\": [\n            {\n              \"AllowedMethods\": [\n                \"GET\",\n                \"HEAD\"\n              ],\n              \"AllowedOrigins\": [\n                \"*\"\n              ]\n            }\n          ]\n        },\n        \"LifecycleConfiguration\": {\n          \"Rules\": [\n            {\n              \"ExpirationInDays\": \"180\",\n              \"Id\": \"Delete after 180 days\",\n              \"Prefix\": \"map/data/\",\n              \"Status\": \"Enabled\"\n            }\n          ]\n        },\n        \"VersioningConfiguration\": {\n          \"Status\": \"Suspended\"\n        },\n        \"BucketName\": {\n          \"Fn::Join\": [\n            \".\",\n            [\n              {\n                \"Ref\": \"Environment\"\n              },\n              \"maps.touch-mapper\"\n            ]\n          ]\n        }\n      }\n    },\n    \"StatsBucket\": {\n      \"Type\": \"AWS::S3::Bucket\",\n      \"Properties\": {\n        \"PublicAccessBlockConfiguration\": {\n          \"BlockPublicAcls\": true,\n          \"BlockPublicPolicy\": true,\n          \"IgnorePublicAcls\": true,\n          \"RestrictPublicBuckets\": true\n        },\n        \"BucketEncryption\": {\n          \"ServerSideEncryptionConfiguration\": [\n            {\n              \"ServerSideEncryptionByDefault\": {\n                \"SSEAlgorithm\": \"AES256\"\n              }\n            }\n          ]\n        },\n        \"VersioningConfiguration\": {\n          \"Status\": \"Suspended\"\n        },\n        \"BucketName\": {\n          \"Fn::Join\": [\n            \".\",\n            [\n              {\n                \"Ref\": \"Environment\"\n              },\n              \"stats.touch-mapper\"\n            ]\n          ]\n        }\n      }\n    },\n    \"RequestsQueue\": {\n      \"Type\": \"AWS::SQS::Queue\",\n      \"Properties\": {\n        \"DelaySeconds\": \"0\",\n        \"MaximumMessageSize\": \"65536\",\n        \"MessageRetentionPeriod\": \"300\",\n        \"ReceiveMessageWaitTimeSeconds\": \"20\",\n        \"VisibilityTimeout\": \"600\",\n        \"QueueName\": {\n          \"Fn::Join\": [\n            \"-\",\n            [\n              {\n                \"Ref\": \"Environment\"\n              },\n              \"requests-touch-mapper\"\n            ]\n          ]\n        }\n      }\n    },\n    \"RequestsSqsPolicy\": {\n      \"Type\": \"AWS::SQS::QueuePolicy\",\n      \"Properties\": {\n        \"Queues\": [\n          {\n            \"Ref\": \"RequestsQueue\"\n          }\n        ],\n        \"PolicyDocument\": {\n          \"Version\": \"2012-10-17\",\n          \"Statement\": [\n            {\n              \"Effect\": \"Allow\",\n              \"Principal\": \"*\",\n              \"Action\": [\n                \"SQS:GetQueueAttributes\",\n                \"SQS:SendMessage\"\n              ],\n              \"Resource\": {\n                \"Fn::GetAtt\": [\n                  \"RequestsQueue\",\n                  \"Arn\"\n                ]\n              }\n            }\n          ]\n        }\n      }\n    },\n    \"StatsAthenaDatabase\": {\n      \"Type\": \"AWS::Glue::Database\",\n      \"Properties\": {\n        \"CatalogId\": {\n          \"Ref\": \"AWS::AccountId\"\n        },\n        \"DatabaseInput\": {\n          \"Name\": {\n            \"Fn::Join\": [\n              \"_\",\n              [\n                \"touch_mapper_stats\",\n                {\n                  \"Fn::Join\": [\n                    \"_\",\n                    {\n                      \"Fn::Split\": [\n                        \"-\",\n                        {\n                          \"Ref\": \"Environment\"\n                        }\n                      ]\n                    }\n                  ]\n                }\n              ]\n            ]\n          }\n        }\n      }\n    },\n    \"ApplicationStatsJsonTable\": {\n      \"Type\": \"AWS::Glue::Table\",\n      \"Properties\": {\n        \"CatalogId\": {\n          \"Ref\": \"AWS::AccountId\"\n        },\n        \"DatabaseName\": {\n          \"Ref\": \"StatsAthenaDatabase\"\n        },\n        \"TableInput\": {\n          \"Name\": \"application_stats_json\",\n          \"TableType\": \"EXTERNAL_TABLE\",\n          \"Parameters\": {\n            \"EXTERNAL\": \"TRUE\",\n            \"classification\": \"json\",\n            \"projection.enabled\": \"true\",\n            \"projection.year.type\": \"integer\",\n            \"projection.year.range\": \"2024,2100\",\n            \"projection.month.type\": \"integer\",\n            \"projection.month.range\": \"1,12\",\n            \"projection.month.digits\": \"2\",\n            \"storage.location.template\": {\n              \"Fn::Join\": [\n                \"\",\n                [\n                  \"s3://\",\n                  {\n                    \"Ref\": \"StatsBucket\"\n                  },\n                  \"/stats-json/${year}/${month}/\"\n                ]\n              ]\n            }\n          },\n          \"PartitionKeys\": [\n            {\n              \"Name\": \"year\",\n              \"Type\": \"string\",\n              \"Comment\": \"Partition key for UTC year derived from S3 object path.\"\n            },\n            {\n              \"Name\": \"month\",\n              \"Type\": \"string\",\n              \"Comment\": \"Partition key for UTC month derived from S3 object path.\"\n            }\n          ],\n          \"StorageDescriptor\": {\n            \"Columns\": [\n              {\n                \"Name\": \"schema_version\",\n                \"Type\": \"int\",\n                \"Comment\": \"Version number of this telemetry record schema for backward-compatible query handling.\"\n              },\n              {\n                \"Name\": \"timestamp\",\n                \"Type\": \"string\",\n                \"Comment\": \"UTC time when the telemetry record was written in ISO 8601 format.\"\n              },\n              {\n                \"Name\": \"day\",\n                \"Type\": \"string\",\n                \"Comment\": \"UTC day-of-month component as two digits.\"\n              },\n              {\n                \"Name\": \"code_branch\",\n                \"Type\": \"string\",\n                \"Comment\": \"Git branch name parsed from deployed dist/VERSION.txt.\"\n              },\n              {\n                \"Name\": \"code_deployed\",\n                \"Type\": \"string\",\n                \"Comment\": \"Deployment tag payload parsed from package-* in deployed dist/VERSION.txt.\"\n              },\n              {\n                \"Name\": \"code_commit\",\n                \"Type\": \"string\",\n                \"Comment\": \"Git commit hash parsed from deployed dist/VERSION.txt.\"\n              },\n              {\n                \"Name\": \"request_id\",\n                \"Type\": \"string\",\n                \"Comment\": \"Full map request identifier received from the browser payload.\"\n              },\n              {\n                \"Name\": \"map_id\",\n                \"Type\": \"string\",\n                \"Comment\": \"Map identifier prefix derived from request_id before the slash.\"\n              },\n              {\n                \"Name\": \"status\",\n                \"Type\": \"string\",\n                \"Comment\": \"Terminal processing status such as success or failed.\"\n              },\n              {\n                \"Name\": \"failure_stage\",\n                \"Type\": \"string\",\n                \"Comment\": \"Pipeline stage active when processing failed, if any.\"\n              },\n              {\n                \"Name\": \"failure_class\",\n                \"Type\": \"string\",\n                \"Comment\": \"Exception class name captured on processing failure.\"\n              },\n              {\n                \"Name\": \"failure_message\",\n                \"Type\": \"string\",\n                \"Comment\": \"Exception message captured on processing failure.\"\n              },\n              {\n                \"Name\": \"error_code\",\n                \"Type\": \"string\",\n                \"Comment\": \"Structured converter error code for UI and analytics, such as unknown or too_large.\"\n              },\n              {\n                \"Name\": \"error_description\",\n                \"Type\": \"string\",\n                \"Comment\": \"Diagnostic error text associated with error_code.\"\n              },\n              {\n                \"Name\": \"termination_signal\",\n                \"Type\": \"int\",\n                \"Comment\": \"Termination signal number if the process received SIGTERM or SIGINT.\"\n              },\n              {\n                \"Name\": \"browser_fingerprint\",\n                \"Type\": \"string\",\n                \"Comment\": \"Hashed browser fingerprint used for approximate unique visitor estimation.\"\n              },\n              {\n                \"Name\": \"browser_ip\",\n                \"Type\": \"string\",\n                \"Comment\": \"Public client IP address reported by browser-side lookup when available.\"\n              },\n              {\n                \"Name\": \"browser_ip_country\",\n                \"Type\": \"string\",\n                \"Comment\": \"Country name resolved from browser_ip geolocation lookup.\"\n              },\n              {\n                \"Name\": \"browser_ip_country_code\",\n                \"Type\": \"string\",\n                \"Comment\": \"Country code resolved from browser_ip geolocation lookup.\"\n              },\n              {\n                \"Name\": \"browser_ip_region\",\n                \"Type\": \"string\",\n                \"Comment\": \"Region or state resolved from browser_ip geolocation lookup.\"\n              },\n              {\n                \"Name\": \"browser_ip_city\",\n                \"Type\": \"string\",\n                \"Comment\": \"City resolved from browser_ip geolocation lookup.\"\n              },\n              {\n                \"Name\": \"browser_ip_latitude\",\n                \"Type\": \"double\",\n                \"Comment\": \"Latitude resolved from browser_ip geolocation lookup.\"\n              },\n              {\n                \"Name\": \"browser_ip_longitude\",\n                \"Type\": \"double\",\n                \"Comment\": \"Longitude resolved from browser_ip geolocation lookup.\"\n              },\n              {\n                \"Name\": \"addr_long\",\n                \"Type\": \"string\",\n                \"Comment\": \"Full address label selected by the user.\"\n              },\n              {\n                \"Name\": \"printing_tech\",\n                \"Type\": \"string\",\n                \"Comment\": \"Selected printing technology mode such as 3d or 2d.\"\n              },\n              {\n                \"Name\": \"offset_x\",\n                \"Type\": \"int\",\n                \"Comment\": \"Map area X-offset in meters from the selected center.\"\n              },\n              {\n                \"Name\": \"offset_y\",\n                \"Type\": \"int\",\n                \"Comment\": \"Map area Y-offset in meters from the selected center.\"\n              },\n              {\n                \"Name\": \"size_cm\",\n                \"Type\": \"double\",\n                \"Comment\": \"Requested physical map size in centimeters.\"\n              },\n              {\n                \"Name\": \"content_mode\",\n                \"Type\": \"string\",\n                \"Comment\": \"Selected content mode: normal, no-buildings, or only-big-roads.\"\n              },\n              {\n                \"Name\": \"hide_location_marker\",\n                \"Type\": \"boolean\",\n                \"Comment\": \"Whether the location marker was omitted from the map.\"\n              },\n              {\n                \"Name\": \"lon\",\n                \"Type\": \"double\",\n                \"Comment\": \"Requested map center longitude sent by browser.\"\n              },\n              {\n                \"Name\": \"lat\",\n                \"Type\": \"double\",\n                \"Comment\": \"Requested map center latitude sent by browser.\"\n              },\n              {\n                \"Name\": \"scale\",\n                \"Type\": \"int\",\n                \"Comment\": \"Requested map scale denominator used for conversion.\"\n              },\n              {\n                \"Name\": \"multipart_mode\",\n                \"Type\": \"boolean\",\n                \"Comment\": \"Whether multipart map mode was enabled by the user.\"\n              },\n              {\n                \"Name\": \"no_borders\",\n                \"Type\": \"boolean\",\n                \"Comment\": \"Whether map border geometry generation was disabled.\"\n              },\n              {\n                \"Name\": \"multipart_xpc\",\n                \"Type\": \"int\",\n                \"Comment\": \"Multipart horizontal offset as percent of map diameter.\"\n              },\n              {\n                \"Name\": \"multipart_ypc\",\n                \"Type\": \"int\",\n                \"Comment\": \"Multipart vertical offset as percent of map diameter.\"\n              },\n              {\n                \"Name\": \"advanced_mode\",\n                \"Type\": \"boolean\",\n                \"Comment\": \"Whether advanced UI options were enabled for this request.\"\n              },\n              {\n                \"Name\": \"osm_fetch_provider\",\n                \"Type\": \"string\",\n                \"Comment\": \"Provider category of the successful OSM fetch attempt: overpass or main_api.\"\n              },\n              {\n                \"Name\": \"osm_fetch_endpoint\",\n                \"Type\": \"string\",\n                \"Comment\": \"URL endpoint used by the successful OSM fetch attempt.\"\n              },\n              {\n                \"Name\": \"timing_get_osm_seconds\",\n                \"Type\": \"double\",\n                \"Comment\": \"Elapsed seconds for the successful OSM fetch attempt (excludes prior timed-out attempts and excludes only-big-roads pruning).\"\n              },\n              {\n                \"Name\": \"timing_prune_only_big_roads_seconds\",\n                \"Type\": \"double\",\n                \"Comment\": \"Elapsed seconds spent running prune-only-big-roads.js in content_mode=only-big-roads; null otherwise.\"\n              },\n              {\n                \"Name\": \"timing_map_desc_seconds\",\n                \"Type\": \"double\",\n                \"Comment\": \"Elapsed seconds spent generating map description metadata.\"\n              },\n              {\n                \"Name\": \"timing_upload_primary_seconds\",\n                \"Type\": \"double\",\n                \"Comment\": \"Elapsed seconds spent uploading primary map artifacts (info.json, map-content.json, and main STL).\"\n              },\n              {\n                \"Name\": \"timing_svg_to_pdf_seconds\",\n                \"Type\": \"double\",\n                \"Comment\": \"Elapsed seconds spent converting SVG to PDF.\"\n              },\n              {\n                \"Name\": \"timing_total_seconds\",\n                \"Type\": \"double\",\n                \"Comment\": \"Total elapsed seconds from request pickup to terminal state.\"\n              },\n              {\n                \"Name\": \"timing_failed_after_seconds\",\n                \"Type\": \"double\",\n                \"Comment\": \"Elapsed seconds until failure for failed attempts, otherwise null.\"\n              },\n              {\n                \"Name\": \"stl_bytes\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"Uncompressed byte size of the generated STL artifact.\"\n              },\n              {\n                \"Name\": \"stl_gzip_bytes\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"Gzipped byte size of the generated STL artifact.\"\n              },\n              {\n                \"Name\": \"map_content_gzip_bytes\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"Gzipped byte size of map-content JSON after request metadata attachment.\"\n              },\n              {\n                \"Name\": \"osm_fetched_bytes\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"OSM XML byte size on disk immediately after fetch and before content-mode pruning.\"\n              },\n              {\n                \"Name\": \"osm_pruned_bytes\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"OSM XML byte size on disk after content-mode pruning/filtering.\"\n              },\n              {\n                \"Name\": \"rss_osm2world_kib\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"Maximum RSS in KiB for run-osm2world stage from osm-to-tactile-timings.json (/usr/bin/time -v).\"\n              },\n              {\n                \"Name\": \"rss_blender_kib\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"Maximum RSS in KiB for run-blender stage from osm-to-tactile-timings.json (/usr/bin/time -v).\"\n              },\n              {\n                \"Name\": \"rss_clip_2d_kib\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"Maximum RSS in KiB for run-clip-2d stage from osm-to-tactile-timings.json (/usr/bin/time -v).\"\n              },\n              {\n                \"Name\": \"rss_prune_only_big_roads_kib\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"Maximum RSS in KiB for prune-only-big-roads.js subprocess in content_mode=only-big-roads (/usr/bin/time -v).\"\n              },\n              {\n                \"Name\": \"rss_svg_to_pdf_kib\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"Maximum RSS in KiB for SVG-to-PDF subprocess execution in process-request.py (/usr/bin/time -v).\"\n              },\n              {\n                \"Name\": \"rss_process_request_peak_kib\",\n                \"Type\": \"bigint\",\n                \"Comment\": \"Peak VmRSS in KiB observed within process-request.py while handling the request.\"\n              }\n            ],\n            \"Location\": {\n              \"Fn::Join\": [\n                \"\",\n                [\n                  \"s3://\",\n                  {\n                    \"Ref\": \"StatsBucket\"\n                  },\n                  \"/stats-json/\"\n                ]\n              ]\n            },\n            \"InputFormat\": \"org.apache.hadoop.mapred.TextInputFormat\",\n            \"OutputFormat\": \"org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat\",\n            \"SerdeInfo\": {\n              \"SerializationLibrary\": \"org.openx.data.jsonserde.JsonSerDe\",\n              \"Parameters\": {\n                \"ignore.malformed.json\": \"true\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"WebBucketS3Policy\": {\n      \"Type\": \"AWS::S3::BucketPolicy\",\n      \"Properties\": {\n        \"Bucket\": {\n          \"Ref\": \"WebBucket\"\n        },\n        \"PolicyDocument\": {\n          \"Version\": \"2012-10-17\",\n          \"Statement\": [\n            {\n              \"Effect\": \"Allow\",\n              \"Principal\": \"*\",\n              \"Action\": \"s3:GetObject\",\n              \"Resource\": {\n                \"Fn::Join\": [\n                  \"\",\n                  [\n                    \"arn:aws:s3:::\",\n                    {\n                      \"Ref\": \"WebBucket\"\n                    },\n                    \"/*\"\n                  ]\n                ]\n              }\n            }\n          ]\n        }\n      }\n    },\n    \"CloudFront\": {\n      \"Type\": \"AWS::CloudFront::Distribution\",\n      \"Properties\": {\n        \"DistributionConfig\": {\n          \"Aliases\": [\n            {\n              \"Ref\": \"Domain\"\n            },\n            {\n              \"Fn::Join\": [\n                \"\",\n                [\n                  \"*.\",\n                  {\n                    \"Ref\": \"Domain\"\n                  }\n                ]\n              ]\n            }\n          ],\n          \"Enabled\": true,\n          \"PriceClass\": \"PriceClass_All\",\n          \"Origins\": [\n            {\n              \"Id\": \"web\",\n              \"DomainName\": {\n                \"Fn::Select\": [\n                  \"2\",\n                  {\n                    \"Fn::Split\": [\n                      \"/\",\n                      {\n                        \"Fn::GetAtt\": [\n                          \"WebBucket\",\n                          \"WebsiteURL\"\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              },\n              \"CustomOriginConfig\": {\n                \"OriginProtocolPolicy\": \"http-only\"\n              }\n            },\n            {\n              \"Id\": \"maps\",\n              \"DomainName\": {\n                \"Fn::GetAtt\": [\n                  \"MapsBucket\",\n                  \"DomainName\"\n                ]\n              },\n              \"S3OriginConfig\": {}\n            }\n          ],\n          \"DefaultRootObject\": \"index.html\",\n          \"DefaultCacheBehavior\": {\n            \"TargetOriginId\": \"web\",\n            \"ViewerProtocolPolicy\": \"redirect-to-https\",\n            \"DefaultTTL\": 3600,\n            \"MinTTL\": 3600,\n            \"MaxTTL\": 3600,\n            \"Compress\": true,\n            \"AllowedMethods\": [\n              \"HEAD\",\n              \"GET\",\n              \"OPTIONS\"\n            ],\n            \"CachedMethods\": [\n              \"HEAD\",\n              \"GET\"\n            ],\n            \"ForwardedValues\": {\n              \"QueryString\": false,\n              \"Headers\": [\n                \"Origin\"\n              ],\n              \"Cookies\": {\n                \"Forward\": \"none\"\n              }\n            }\n          },\n          \"CacheBehaviors\": [\n            {\n              \"TargetOriginId\": \"maps\",\n              \"PathPattern\": \"/map/*\",\n              \"ViewerProtocolPolicy\": \"allow-all\",\n              \"MinTTL\": 0,\n              \"AllowedMethods\": [\n                \"HEAD\",\n                \"GET\",\n                \"OPTIONS\"\n              ],\n              \"CachedMethods\": [\n                \"HEAD\",\n                \"GET\"\n              ],\n              \"ForwardedValues\": {\n                \"QueryString\": false,\n                \"Cookies\": {\n                  \"Forward\": \"none\"\n                }\n              }\n            }\n          ],\n          \"HttpVersion\": \"http2\",\n          \"ViewerCertificate\": {\n            \"Fn::If\": [\n              \"IsDevEnv\",\n              {\n                \"CloudFrontDefaultCertificate\": true\n              },\n              {\n                \"AcmCertificateArn\": \"arn:aws:acm:us-east-1:730535225693:certificate/9dbd7fe0-68f5-4973-8d3a-b95623e476db\",\n                \"SslSupportMethod\": \"sni-only\"\n              }\n            ]\n          },\n          \"Logging\": {\n            \"Bucket\": {\n              \"Fn::GetAtt\": [\n                \"Logs\",\n                \"DomainName\"\n              ]\n            },\n            \"Prefix\": \"webCloudFront/\"\n          }\n        }\n      }\n    },\n    \"EmailSendingRole\": {\n      \"Type\": \"AWS::IAM::Role\",\n      \"Properties\": {\n        \"AssumeRolePolicyDocument\": {\n          \"Version\": \"2012-10-17\",\n          \"Statement\": [\n            {\n              \"Effect\": \"Allow\",\n              \"Principal\": {\n                \"Service\": \"lambda.amazonaws.com\"\n              },\n              \"Action\": \"sts:AssumeRole\"\n            }\n          ]\n        },\n        \"Policies\": [\n          {\n            \"PolicyName\": \"EmailSendingPolicy\",\n            \"PolicyDocument\": {\n              \"Version\": \"2012-10-17\",\n              \"Statement\": [\n                {\n                  \"Effect\": \"Allow\",\n                  \"Action\": [\n                    \"ses:SendEmail\"\n                  ],\n                  \"Resource\": [\n                    \"*\"\n                  ]\n                }\n              ]\n            }\n          }\n        ]\n      }\n    },\n    \"EmailSendingLambda\": {\n      \"Type\": \"AWS::Lambda::Function\",\n      \"Properties\": {\n        \"Handler\": \"lambda-email-sending.lambda_handler\",\n        \"Role\": {\n          \"Fn::GetAtt\": [\n            \"EmailSendingRole\",\n            \"Arn\"\n          ]\n        },\n        \"Code\": {\n          \"S3Bucket\": {\n            \"Fn::Join\": [\n              \".\",\n              [\n                {\n                  \"Ref\": \"Environment\"\n                },\n                \"internal.touch-mapper\"\n              ]\n            ]\n          },\n          \"S3Key\": \"lambda-email-sending.zip\"\n        },\n        \"Runtime\": \"python2.7\",\n        \"Timeout\": \"10\"\n      }\n    }\n  },\n  \"Description\": \"Create Touch Mapper resources\",\n  \"Outputs\": {\n    \"cloudFrontDomainName\": {\n      \"Value\": {\n        \"Fn::GetAtt\": [\n          \"CloudFront\",\n          \"DomainName\"\n        ]\n      }\n    },\n    \"WebBucketUrl\": {\n      \"Value\": {\n        \"Fn::GetAtt\": [\n          \"WebBucket\",\n          \"WebsiteURL\"\n        ]\n      }\n    },\n    \"mapsBucket\": {\n      \"Value\": {\n        \"Fn::GetAtt\": [\n          \"MapsBucket\",\n          \"DomainName\"\n        ]\n      }\n    },\n    \"statsBucket\": {\n      \"Value\": {\n        \"Ref\": \"StatsBucket\"\n      }\n    },\n    \"statsAthenaDatabase\": {\n      \"Value\": {\n        \"Ref\": \"StatsAthenaDatabase\"\n      }\n    },\n    \"statsAthenaTable\": {\n      \"Value\": \"application_stats_json\"\n    },\n    \"requestsQueue\": {\n      \"Value\": {\n        \"Ref\": \"RequestsQueue\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "install/ec2-init.sh",
    "content": "#!/bin/bash\n### BEGIN INIT INFO\n# Provides:          touch-mapper\n# Required-Start:    $all\n# Required-Stop:\n# Default-Start:\n# Default-Stop:\n# Short-Description: Init and start Touch Mapper converter poller\n### END INIT INFO\n#\n\nexec sudo -H -u ubuntu ~ubuntu/touch-mapper/test/dist/ec2-restart-pollers.sh\n\n"
  },
  {
    "path": "install/ec2-restart-pollers.sh",
    "content": "#!/bin/bash\n\nset -e\n\nexport LC_ALL=en_US.UTF-8 # Else Python will fail with unicode\n\nsudo killall -9 poller.sh process-request.py || true\n\ncd ~/touch-mapper\n\nfor execmode in *; do\n    test -d \"$execmode/stats\" || mkdir \"$execmode/stats\"\n    for worker in $(seq 1 2 3); do\n        work_dir=\"$(cd $execmode/runtime; pwd)/$worker\"\n        test -d $work_dir || mkdir $work_dir\n        nohup $execmode/dist/poller.sh $execmode $worker &>>/tmp/touch-mapper-start-$execmode &\n    done\ndone\n\necho \"restarting complete\"\n"
  },
  {
    "path": "install/lambda-email-sending.py",
    "content": "\"\"\"\nSend email about a Touch Mapper map to given email address.\n\nSample POST body:\n{ \"mapUrl\": \"https://touch-mapper.org/map/fe99742222faf8d1/Augsburg-rest.stl\", \"meta\": { \"address\": \"foo\", \"size\": 17, \"permaUrl\": \"bar\" }, \"to\": \"skarkkai@gmail.com\", \"emailType\": \"order\" }\n\"\"\"\n\nfrom __future__ import print_function\nimport json, boto3, urllib, re\n\nses = boto3.client('ses')\n\nPLAYFUL_PIXELS_URL = \"https://www.playfulpixels.com/en/tactile-map\"\nMAP_URL_REGEXP = re.compile('^https?://.*touch-mapper.org/map/[^ ]+\\.stl')\nMAIL_FROM = 'info@touch-mapper.org'\n\ndef lambda_handler(event, context):\n    print(event['body'])\n    req = json.loads(event['body']);\n    #if not MAP_URL_REGEXP.match(req['mapUrl']):\n    #    raise Exception(\"invalid map URL:\" + req['mapUrl'])\n\n    if req['emailType'] == 'order':\n        subject = 'Touch Mapper order link for ' + req['meta']['address']\n        metaJson = json.dumps(req['meta'], separators=(',', ':'))\n        body = 'Order your Touch Mapper tactile map at:\\n\\n' \\\n            + PLAYFUL_PIXELS_URL + '?touchMapFileUrl=' + urllib.quote_plus(req['mapUrl']) + '&mapMeta=' + urllib.quote_plus(metaJson)\n    else:\n        subject = 'Touch Mapper STL file for ' + req['meta']['address']\n        body = 'Download your Touch Mapper tactile map STL file at:\\n\\n' \\\n            + req['mapUrl']\n\n    body = body + '\\n\\n' \\\n            + 'Address: ' + req['meta']['address'] + '\\n' \\\n            + 'Size: ' + str(req['meta']['size']) + ' cm\\n\\n' \\\n            + ('View map or create more: ' + req['meta']['permaUrl'] + '\\n\\n' if 'permaUrl' in req['meta'] else '') \\\n            + 'Sincerely,\\n' \\\n            + 'Touch Mapper'\n\n    ret = ses.send_email(Source=MAIL_FROM,\n                         Destination={'ToAddresses': [req['to']]},\n                         ReplyToAddresses=[MAIL_FROM],\n                         Message={'Subject': {'Data': subject, 'Charset': 'UTF-8'},\n                                  'Body': {'Text': {'Data': body, 'Charset': 'UTF-8'}}})\n    return {\n        \"statusCode\": 200,\n        \"headers\": {\n            \"content-type\": \"application/json; charset=utf-8\",\n            \"access-control-allow-origin\": \"*\",\n        },\n        \"body\": \"{}\"\n    }\n"
  },
  {
    "path": "install/lambda-update.sh",
    "content": "#!/bin/bash\n#\n# To update existing lambda's code:\n# zip lambda-email-sending.zip lambda-email-sending.py && aws lambda update-function-code --zip-file fileb://lambda-email-sending.zip --function-name TouchMapperDevSkarkkai-EmailSendingLambda-1J5HH0GRNN6YR \n#\n\nset -e\n\ncd \"$( dirname \"${BASH_SOURCE[0]}\" )\"\n\nif [[ $# != 1 ]]; then\n    echo \"Usage: $0 ENVIRONMENT\"\n    exit 1\nfi\nenvironment=$1\n\neval $( ./parameters.sh $environment )\n\nrm -f lambda-email-sending.zip\nzip lambda-email-sending.zip lambda-email-sending.py\n\nbucket=$env_name.internal.touch-mapper\nregion=$(aws configure get region)\n\ncmd=\"aws s3api create-bucket --bucket $bucket --region $region --create-bucket-configuration LocationConstraint=$region\"\n$cmd 2>/dev/null || true\n\ncmd=\"aws s3 cp lambda-email-sending.zip s3://$bucket\"\n$cmd\n\n"
  },
  {
    "path": "install/package.sh",
    "content": "#!/bin/bash\n\nset -e\n\n# Tag release\n#branch=$( git rev-parse --abbrev-ref HEAD )\n#if [[ $branch != release/* ]]; then\n#    echo \"Current branch must be release/*, not $branch, maybe create one\"\n#    exit 1\n#fi\ntime=$( date +%Y%m%d-%H%M%S )\ngit tag package-$time\n\ncd \"$( dirname \"${BASH_SOURCE[0]}\" )\"\nrm -rf dist.old\nmv dist dist.old || true\nmkdir dist\ncp -alH ../blender dist/\nmkdir dist/OSM2World\ncp -alH ../OSM2World/build dist/OSM2World/\ncp -plH ../converter/*{py,sh,js} dist/\ncp -aH ../converter/map_desc dist/\ncp -aH ../converter/py-lib dist/\ncp -aH ../install/{ec2-restart-pollers.sh,ec2-init.sh} dist/\necho \"$( date +'%Y-%m-%dT%H:%M:%S') $( git rev-parse --abbrev-ref HEAD ) $( git describe --tags ) $( git rev-parse --verify HEAD )\" >dist/VERSION.txt\n#GZIP=-4 tar czf dist.tgz dist\n"
  },
  {
    "path": "install/parameters.sh",
    "content": "# Inputs:\n#   $USER\n#   $TOUCH_MAPPER_DEV_ENV (optional)\n# Outputs:\n#   $env_name\n#   $stack_name (cloudformation stack)\n#   $is_dev_env (true | false)\n#   $domain (CloudFront and S3)\n\nif [[ $# != 1 ]]; then\n    echo \"Usage: $0 ENVIRONMENT\"\n    exit 1\nfi\nenvironment=$1\n\nif [[ $environment == dev ]]; then\n    dev_env=${TOUCH_MAPPER_DEV_ENV:-$USER}\n    if [[ ! $dev_env ]]; then\n        echo \"Environment variables TOUCH_MAPPER_DEV_ENV and USER are both unset, aborting\"\n    fi\n    echo env_name=dev-$dev_env\n    echo stack_name=TouchMapperDev${dev_env^}\n    echo is_dev_env=true\n    echo domain=dev-$dev_env.touch-mapper.org\nelse\n    echo env_name=$environment\n    echo stack_name=TouchMapper${environment^}\n    echo is_dev_env=false\n    if [[ $environment == prod ]]; then\n        echo domain=touch-mapper.org\n    else\n        echo domain=$environment.touch-mapper.org\n    fi\nfi\n\n\n"
  },
  {
    "path": "install/run-dev-converter.sh",
    "content": "#!/bin/bash\n\ncd \"$( dirname \"${BASH_SOURCE[0]}\" )\"\nmkdir -p ../runtime\nmkdir -p ../stats\neval $( ./parameters.sh dev | egrep '^env_name=' )\ncd ../converter\necho \"Starting poller.sh for $env_name, see runtime/1/*.log\"\necho \"Stop by running: killall -9 poller.sh\"\n./poller.sh $env_name 1\n"
  },
  {
    "path": "install/setup-ec2.sh",
    "content": "#!/bin/bash\n\nsudo bash -c \"echo LC_ALL=en_US.UTF-8 >>/etc/default/locale\"\n. /etc/default/locale \nexport LC_ALL\n\nsudo apt-get update\nsudo apt-get upgrade -y\nsudo apt-get -y install awscli openjdk-8-jre-headless libglu1-mesa libxi6 python3-cairosvg python3-pip\nsudo -H pip3 install --upgrade boto3\naws configure\nsudo cp -r /home/ubuntu/.aws /root/\n\nsudo fallocate -l 200M /swap\nsudo mkswap /swap\necho \"/swap  none  swap  sw 0  0\" | sudo tee -a /etc/fstab\n\ndir=~ubuntu/touch-mapper\nfor execmode in test prod; do\n    mkdir -p $dir/$execmode/runtime\n    mkdir -p $dir/$execmode/stats\ndone\n\n# Make a symlink whose target will be installed a bit later\nsudo ln -s $dir/test/dist/ec2-init.sh /etc/init.d/touch-mapper\n"
  },
  {
    "path": "install/web-s3.sh",
    "content": "#!/bin/bash\n\nset -e\n\nif [[ $# != 1 ]]; then\n    echo \"Usage: $0 ENVIRONMENT\"\n    exit 1\nfi\nenvironment=$1\n\ncd \"$( dirname \"${BASH_SOURCE[0]}\" )\"\ncd ../web\neval $( ../install/parameters.sh $environment )\nurl=s3://$domain\necho \"env_name: $env_name\"\necho \"S3 web bucket: $url\"\n\n# Make sure \"web/build\" is up to date\nENVIRONMENT=$env_name make build\n\n# build => dist (keep mtimes for unchanged files to avoid unnecessary S3 uploads)\ntmp_dist=dist.next\nrm -rf \"$tmp_dist\"\ncp -a build \"$tmp_dist\"\n./create-env-js.sh $env_name >\"$tmp_dist/scripts/environment.js\"\nfor lang in $(cd \"$tmp_dist\"; find ?? -type d); do\n    (\n        cd \"$tmp_dist/$lang\"\n        rename 's/\\.html//' *.html\n        mv index index.html\n    )\ndone\nrm -f \"$tmp_dist/.gitignore\"\n\nmkdir -p dist\nrsync -a --delete --checksum \"$tmp_dist\"/ dist/\nrm -rf \"$tmp_dist\"\n\ntime=$( date +%Y%m%d-%H%M%S )\ngit tag web-install-$time\n\n# Sync dist to S3\nmapfile -t langs < <( cd dist && find ?? -maxdepth 0 -type d )\nsync_excludes=()\nfor lang in \"${langs[@]}\"; do\n    sync_excludes+=( --exclude \"$lang/*\" )\ndone\nsync_excludes+=(\n    --exclude \"scripts/three-r182*.js\"\n    --exclude \"scripts/three-addons/*\"\n    --exclude \"scripts/vendor-common.js\"\n    --exclude \"scripts/aws-sdk*.js\"\n)\n\n# Run all S3 sync tasks in parallel. Keep output quiet unless any task fails.\nsync_log_dir=$(mktemp -d)\nsync_pids=()\nsync_labels=()\n\nrun_sync_task() {\n    local label=$1\n    shift\n    local log_file=\"$sync_log_dir/$label.log\"\n    (\n        \"$@\"\n    ) >\"$log_file\" 2>&1 &\n    sync_pids+=( \"$!\" )\n    sync_labels+=( \"$label\" )\n}\n\necho \"Syncing assets to S3...\"\n\n# Sync non-locale assets once; locale HTML paths are synced separately with explicit content-type.\nrun_sync_task \"assets\" \\\n    aws s3 sync --delete --cache-control must-revalidate \"${sync_excludes[@]}\" dist/ $url\n# Three.js vendor assets are effectively immutable and large. Use size-only matching to avoid\n# timestamp-driven re-uploads while still allowing true updates when file sizes change.\nrun_sync_task \"three-core\" \\\n    aws s3 sync --delete --size-only --cache-control must-revalidate \\\n    --exclude \"*\" --include \"three-r182*.js\" dist/scripts/ $url/scripts/\nif [[ -d dist/scripts/three-addons ]]; then\n    run_sync_task \"three-addons\" \\\n        aws s3 sync --delete --size-only --cache-control must-revalidate dist/scripts/three-addons/ $url/scripts/three-addons/\nelse\n    run_sync_task \"three-addons-rm\" \\\n        aws s3 rm --quiet --recursive $url/scripts/three-addons\nfi\nrun_sync_task \"vendor\" \\\n    aws s3 sync --delete --size-only --cache-control must-revalidate \\\n    --exclude \"*\" --include \"vendor-common.js\" --include \"aws-sdk*.js\" dist/scripts/ $url/scripts/\nfor lang in \"${langs[@]}\"; do\n    run_sync_task \"lang-$lang\" \\\n        aws s3 sync --delete --cache-control must-revalidate --content-type text/html dist/$lang/ $url/$lang\ndone\n\nsync_failed=0\nfor i in \"${!sync_pids[@]}\"; do\n    pid=\"${sync_pids[$i]}\"\n    label=\"${sync_labels[$i]}\"\n    if ! wait \"$pid\"; then\n        sync_failed=1\n        echo \"S3 sync task failed: $label\"\n        cat \"$sync_log_dir/$label.log\"\n    fi\ndone\nrm -rf \"$sync_log_dir\"\n\necho \"Syncing complete\"\n\nif [[ $sync_failed -ne 0 ]]; then\n    echo \"Aborting before CloudFront invalidation because one or more S3 sync tasks failed.\"\n    exit 1\nfi\n\n\n# Invalidate CloudFront\ndistribution_id=$( aws cloudfront list-distributions | jq --raw-output \".DistributionList.Items[] | select(.Origins.Items[].DomainName == \\\"$env_name.maps.touch-mapper.s3.amazonaws.com\\\") | .Id\" )\necho \"Invalidating $env_name environment CloudFront distribution '$distribution_id'\"\naws cloudfront create-invalidation --distribution-id $distribution_id --paths '/*'\n\necho \"Web resources installed to https://$( cat dist/scripts/environment.js  | egrep '^window.TM_DOMAIN' | cut -d \"'\" -f 2 )\"\n"
  },
  {
    "path": "ol-custom.json",
    "content": "{\n  \".comment\": \"see http://openlayers.org/en/v3.15.1/doc/tutorials/custom-builds.html\",\n  \"exports\": [\n    \"ol.Map\",\n    \"ol.Map#on\",\n    \"ol.Map#getView\",\n    \"ol.Map#getInteractions\",\n    \"ol.Map#updateSize\",\n    \"ol.Overlay\",\n    \"ol.Overlay#setPosition\",\n    \"ol.Overlay#getElement\",\n    \"ol.Overlay#setElement\",\n    \"ol.View\",\n    \"ol.View#getCenter\",\n    \"ol.View#setCenter\",\n    \"ol.View#getProjection\",\n    \"ol.View#setResolution\",\n    \"ol.Collection\",\n    \"ol.interaction.Interaction#setActive\",\n    \"ol.interaction.DragPan\",\n    \"ol.interaction.KeyboardPan\",\n    \"ol.control.ScaleLine\",\n    \"ol.source.OSM\",\n    \"ol.proj.Projection#getPointResolution\",\n    \"ol.proj.fromLonLat\",\n    \"ol.proj.toLonLat\",\n    \"ol.layer.Tile\"\n  ],\n  \"compile\": {\n    \"externs\": [\n      \"externs/bingmaps.js\",\n      \"externs/closure-compiler.js\",\n      \"externs/geojson.js\",\n      \"externs/oli.js\",\n      \"externs/olx.js\",\n      \"externs/proj4js.js\",\n      \"externs/tilejson.js\",\n      \"externs/topojson.js\"\n    ],\n    \"define\": [\n      \"goog.array.ASSUME_NATIVE_FUNCTIONS=true\",\n      \"goog.dom.ASSUME_STANDARDS_MODE=true\",\n      \"goog.json.USE_NATIVE_JSON=true\",\n      \"goog.DEBUG=false\",\n      \"ol.ENABLE_DOM=false\",\n      \"ol.ENABLE_WEBGL=false\",\n      \"ol.ENABLE_PROJ4JS=false\",\n      \"ol.ENABLE_IMAGE=false\",\n      \"ol.ENABLE_VECTOR=false\"\n    ],\n    \"jscomp_off\": [\n      \"unknownDefines\"\n    ],\n    \"extra_annotation_name\": [\n      \"api\", \"observable\"\n    ],\n    \"compilation_level\": \"ADVANCED_OPTIMIZATIONS\",\n    \"use_types_for_optimization\": true,\n    \"manage_closure_dependencies\": true\n  }\n}\n"
  },
  {
    "path": "pyrightconfig.json",
    "content": "{\n  \"typeCheckingMode\": \"standard\",\n  \"include\": [\n    \"converter\"\n  ]\n}\n"
  },
  {
    "path": "test/data/.gitignore",
    "content": "map-meta.json\n"
  },
  {
    "path": "test/data/map-content.json",
    "content": "{\n  \"A\": {\n    \"key\": \"A\",\n    \"name\": \"Linear features\",\n    \"subclasses\": [\n      {\n        \"key\": \"A1_secondary_roads\",\n        \"name\": \"Secondary roads\",\n        \"kind\": \"linear\",\n        \"groups\": [\n          {\n            \"label\": \"L\\u00e4hderannantie\",\n            \"displayLabel\": \"L\\u00e4hderannantie\",\n            \"totalLength\": 16.966932720523957,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 140383778,\n                \"osmType\": \"way\",\n                \"label\": \"L\\u00e4hderannantie\",\n                \"displayLabel\": \"L\\u00e4hderannantie\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 16.966932720523957,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.37866475183628595,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365013,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383778,\n                            \"name\": \"L\\u00e4hderannantie\",\n                            \"subClass\": \"A1_secondary_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 16.966932720523957,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"asphalt\",\n                    \"lit\": \"yes\",\n                    \"lanes\": \"1\",\n                    \"oneway\": \"yes\",\n                    \"maxspeed\": \"30\",\n                    \"highway\": \"secondary\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"asphalt\"\n                  },\n                  \"lit\": {\n                    \"value\": \"yes\"\n                  },\n                  \"lanes\": {\n                    \"total\": 1,\n                    \"forward\": null,\n                    \"backward\": null,\n                    \"source\": \"lanes\"\n                  },\n                  \"oneway\": {\n                    \"value\": \"yes\"\n                  },\n                  \"maxspeed_kmh\": {\n                    \"value\": 30,\n                    \"source\": \"maxspeed\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 140383778,\n                \"segments\": [\n                  {\n                    \"length\": 16.966932720523957,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.37866475183628595,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365013,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383778,\n                            \"name\": \"L\\u00e4hderannantie\",\n                            \"subClass\": \"A1_secondary_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"key\": \"A1_local_streets\",\n        \"name\": \"Local streets\",\n        \"kind\": \"linear\",\n        \"groups\": [\n          {\n            \"label\": \"Nuum\\u00e4entie\",\n            \"displayLabel\": \"Nuum\\u00e4entie\",\n            \"totalLength\": 319.05520432850614,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 140383769,\n                \"osmType\": \"way\",\n                \"label\": \"Nuum\\u00e4entie\",\n                \"displayLabel\": \"Nuum\\u00e4entie\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 302.9625695395819,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.048110729964643974,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212465,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.14895370977812578,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212467,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.25795914988785434,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.47608276443637876,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958718,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.4895103418067408,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958712,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.5458257341712616,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.808684537800262,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.873421678994662,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958724,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.9818690110057007,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958725,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958726,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 302.9625695395819,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"asphalt\",\n                    \"maxspeed\": \"30\",\n                    \"highway\": \"residential\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"asphalt\"\n                  },\n                  \"maxspeed_kmh\": {\n                    \"value\": 30,\n                    \"source\": \"maxspeed\"\n                  }\n                }\n              },\n              {\n                \"osmId\": 25365013,\n                \"osmType\": \"way\",\n                \"label\": \"Nuum\\u00e4entie\",\n                \"displayLabel\": \"Nuum\\u00e4entie\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 16.0926347889242,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.2247981935058489,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365013,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383778,\n                            \"name\": \"L\\u00e4hderannantie\",\n                            \"subClass\": \"A1_secondary_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 16.0926347889242,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"asphalt\",\n                    \"lit\": \"yes\",\n                    \"maxspeed\": \"30\",\n                    \"highway\": \"residential\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"asphalt\"\n                  },\n                  \"lit\": {\n                    \"value\": \"yes\"\n                  },\n                  \"maxspeed_kmh\": {\n                    \"value\": 30,\n                    \"source\": \"maxspeed\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 140383769,\n                \"segments\": [\n                  {\n                    \"length\": 302.9625695395819,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.048110729964643974,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212465,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.14895370977812578,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212467,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.25795914988785434,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.47608276443637876,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958718,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.4895103418067408,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958712,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.5458257341712616,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.808684537800262,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.873421678994662,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958724,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.9818690110057007,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958725,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958726,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              },\n              {\n                \"osmId\": 25365013,\n                \"segments\": [\n                  {\n                    \"length\": 16.0926347889242,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.2247981935058489,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365013,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383778,\n                            \"name\": \"L\\u00e4hderannantie\",\n                            \"subClass\": \"A1_secondary_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": \"Nuuniitynkuja\",\n            \"displayLabel\": \"Nuuniitynkuja\",\n            \"totalLength\": 36.41522743963166,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 25365011,\n                \"osmType\": \"way\",\n                \"label\": \"Nuuniitynkuja\",\n                \"displayLabel\": \"Nuuniitynkuja\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 36.41522743963166,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.5246664558870006,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 36.41522743963166,\n                \"semantics\": {\n                  \"raw\": {\n                    \"maxspeed\": \"30\",\n                    \"highway\": \"residential\"\n                  },\n                  \"maxspeed_kmh\": {\n                    \"value\": 30,\n                    \"source\": \"maxspeed\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 25365011,\n                \"segments\": [\n                  {\n                    \"length\": 36.41522743963166,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.5246664558870006,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"key\": \"A1_service_roads\",\n        \"name\": \"Service roads\",\n        \"kind\": \"linear\",\n        \"groups\": [\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 131.22360364789728,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958718,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 131.22360364789728,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958718,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.15379087256084842,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958718,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982747,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 131.22360364789728,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958718,\n                \"segments\": [\n                  {\n                    \"length\": 131.22360364789728,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958718,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.15379087256084842,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958718,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982747,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 130.45481817559906,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958719,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 130.45481817559906,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.15639577879170863,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958722,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.1731011384606544,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958720,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8820942109946125,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958720,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 130.45481817559906,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958719,\n                \"segments\": [\n                  {\n                    \"length\": 130.45481817559906,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.15639577879170863,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958722,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.1731011384606544,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958720,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8820942109946125,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958720,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 70.47328960088342,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958714,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 70.47328960088342,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.05136730256931453,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.10020786723960141,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466273,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.1751207657142702,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466274,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.21982820848082832,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466275,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.25978723731641934,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466276,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.30581747075327603,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466277,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.3699043449729242,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466278,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.4935215580566515,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 70.47328960088342,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958714,\n                \"segments\": [\n                  {\n                    \"length\": 70.47328960088342,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.05136730256931453,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.10020786723960141,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466273,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.1751207657142702,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466274,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.21982820848082832,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466275,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.25978723731641934,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466276,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.30581747075327603,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466277,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.3699043449729242,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466278,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.4935215580566515,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 58.064255068449356,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 520982744,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 58.064255068449356,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.803969409845976,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982745,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8834530525227473,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982746,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadConnector\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 58.064255068449356,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 520982744,\n                \"segments\": [\n                  {\n                    \"length\": 58.064255068449356,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.803969409845976,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982745,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8834530525227473,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982746,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadConnector\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 55.35991572506152,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457957434,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 55.35991572506152,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.6642623722210731,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469206,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.829510485240354,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469205,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.965322332081043,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469204,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 55.35991572506152,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457957434,\n                \"segments\": [\n                  {\n                    \"length\": 55.35991572506152,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365011,\n                            \"name\": \"Nuuniitynkuja\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.6642623722210731,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469206,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.829510485240354,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469205,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.965322332081043,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469204,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 54.730551322115055,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 520982745,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 54.730551322115055,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982745,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 54.730551322115055,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 520982745,\n                \"segments\": [\n                  {\n                    \"length\": 54.730551322115055,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982745,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 51.94729401707427,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 520982743,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 51.94729401707427,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982743,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 51.94729401707427,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 520982743,\n                \"segments\": [\n                  {\n                    \"length\": 51.94729401707427,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982743,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 50.188664805397096,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 520982747,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 50.188664805397096,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958718,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982747,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 50.188664805397096,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 520982747,\n                \"segments\": [\n                  {\n                    \"length\": 50.188664805397096,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958718,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982747,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 48.1461601722197,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958724,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 48.1461601722197,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958724,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 48.1461601722197,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958724,\n                \"segments\": [\n                  {\n                    \"length\": 48.1461601722197,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958724,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 40.79980512004592,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958713,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 40.79980512004592,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958712,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958713,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.18463936953528057,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958713,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 40.79980512004592,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958713,\n                \"segments\": [\n                  {\n                    \"length\": 40.79980512004592,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958712,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958713,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.18463936953528057,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958713,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 40.17609794794471,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958710,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 40.17609794794471,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.1529874983990162,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982743,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadConnector\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 40.17609794794471,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958710,\n                \"segments\": [\n                  {\n                    \"length\": 40.17609794794471,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.1529874983990162,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982743,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadConnector\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958710,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 38.8347682016924,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 655212465,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 38.8347682016924,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212465,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 38.8347682016924,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 655212465,\n                \"segments\": [\n                  {\n                    \"length\": 38.8347682016924,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212465,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 36.07313272787226,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958712,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 36.07313272787226,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958712,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.9075390809898531,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958712,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958713,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 36.07313272787226,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958712,\n                \"segments\": [\n                  {\n                    \"length\": 36.07313272787226,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958712,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.9075390809898531,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958712,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958713,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 31.26883261332281,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958726,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 31.26883261332281,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958726,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 31.26883261332281,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958726,\n                \"segments\": [\n                  {\n                    \"length\": 31.26883261332281,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958726,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 28.34439283118525,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958725,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 28.34439283118525,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958725,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 28.34439283118525,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958725,\n                \"segments\": [\n                  {\n                    \"length\": 28.34439283118525,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958725,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 27.64035445629487,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796466321,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 27.64035445629487,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 27.64035445629487,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796466321,\n                \"segments\": [\n                  {\n                    \"length\": 27.64035445629487,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 26.07194970608792,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958728,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 26.07194970608792,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958726,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.34251998308393483,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958729,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 26.07194970608792,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958728,\n                \"segments\": [\n                  {\n                    \"length\": 26.07194970608792,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958726,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.34251998308393483,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958729,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 25.424056796664054,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 520982746,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 25.424056796664054,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadConnector\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1355154126,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982746,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982746,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 25.424056796664054,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 520982746,\n                \"segments\": [\n                  {\n                    \"length\": 25.424056796664054,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadConnector\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1355154126,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982746,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982744,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982746,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 16.63364706030776,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 655212467,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 16.63364706030776,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212467,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 16.63364706030776,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"asphalt\",\n                    \"highway\": \"service\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"asphalt\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 655212467,\n                \"segments\": [\n                  {\n                    \"length\": 16.63364706030776,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 140383769,\n                            \"name\": \"Nuum\\u00e4entie\",\n                            \"subClass\": \"A1_local_streets\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212467,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 15.366409762856124,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958729,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 15.366409762856124,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958729,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 15.366409762856124,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958729,\n                \"segments\": [\n                  {\n                    \"length\": 15.366409762856124,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958728,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958729,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 14.587132206160328,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958722,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 14.587132206160328,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958722,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 14.587132206160328,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958722,\n                \"segments\": [\n                  {\n                    \"length\": 14.587132206160328,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958722,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 5.353118161673352,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457959169,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 5.353118161673352,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 5.353118161673352,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"service\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457959169,\n                \"segments\": [\n                  {\n                    \"length\": 5.353118161673352,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"key\": \"A2_footpaths_trails\",\n        \"name\": \"Footpaths / trails\",\n        \"kind\": \"linear\",\n        \"groups\": [\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 257.1883554134748,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 24724848,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 257.1883554134748,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.2684792051976044,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1327887801,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.33325813747575733,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365012,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.6415530759158944,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1282562992,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 257.1883554134748,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"asphalt\",\n                    \"highway\": \"footway\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"asphalt\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 24724848,\n                \"segments\": [\n                  {\n                    \"length\": 257.1883554134748,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.2684792051976044,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1327887801,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.33325813747575733,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365012,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.6415530759158944,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1282562992,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 178.61656885894612,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 1282562992,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 178.61656885894612,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.6567774186500721,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1282562992,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1355154126,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1282562992,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 178.61656885894612,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"unpaved\",\n                    \"width\": \"0.5\",\n                    \"highway\": \"path\"\n                  },\n                  \"surface\": {\n                    \"class\": \"unpaved\",\n                    \"value\": \"unpaved\"\n                  },\n                  \"width_m\": {\n                    \"value\": 0.5,\n                    \"source\": \"width\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 1282562992,\n                \"segments\": [\n                  {\n                    \"length\": 178.61656885894612,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.6567774186500721,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1282562992,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1355154126,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1282562992,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 82.6864855488189,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 525693383,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 82.6864855488189,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8623200721309893,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1327887801,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 82.6864855488189,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"asphalt\",\n                    \"highway\": \"footway\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"asphalt\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 525693383,\n                \"segments\": [\n                  {\n                    \"length\": 82.6864855488189,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8623200721309893,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1327887801,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 45.275098800307426,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958716,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 45.275098800307426,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958713,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8868646517006763,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466272,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 45.275098800307426,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958716,\n                \"segments\": [\n                  {\n                    \"length\": 45.275098800307426,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958713,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8868646517006763,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466272,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 40.97598631784877,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958720,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 40.97598631784877,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958720,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958720,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 40.97598631784877,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958720,\n                \"segments\": [\n                  {\n                    \"length\": 40.97598631784877,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958720,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958720,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958719,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 38.79053506404736,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796466272,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 38.79053506404736,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466272,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 38.79053506404736,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796466272,\n                \"segments\": [\n                  {\n                    \"length\": 38.79053506404736,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958716,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466272,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 30.453397213983944,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 25365012,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 30.453397213983944,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365012,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8478507257395548,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365012,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212919,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 30.453397213983944,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"paved\",\n                    \"highway\": \"footway\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"paved\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 25365012,\n                \"segments\": [\n                  {\n                    \"length\": 30.453397213983944,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365012,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8478507257395548,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365012,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212919,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 16.783060929921234,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796466273,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 16.783060929921234,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466273,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 16.783060929921234,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796466273,\n                \"segments\": [\n                  {\n                    \"length\": 16.783060929921234,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466273,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 16.362217215259026,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 655212919,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 16.362217215259026,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365012,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212919,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 16.362217215259026,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 655212919,\n                \"segments\": [\n                  {\n                    \"length\": 16.362217215259026,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 25365012,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 655212919,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 15.551830209080276,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796466274,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 15.551830209080276,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466274,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 15.551830209080276,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796466274,\n                \"segments\": [\n                  {\n                    \"length\": 15.551830209080276,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466274,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 14.220536479313235,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796466275,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 14.220536479313235,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466275,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 14.220536479313235,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796466275,\n                \"segments\": [\n                  {\n                    \"length\": 14.220536479313235,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466275,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 10.442586996351242,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796466278,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 10.442586996351242,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466278,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 10.442586996351242,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796466278,\n                \"segments\": [\n                  {\n                    \"length\": 10.442586996351242,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466278,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 9.806908791259684,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796466277,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 9.806908791259684,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466277,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 9.806908791259684,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796466277,\n                \"segments\": [\n                  {\n                    \"length\": 9.806908791259684,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466277,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 9.332144233776074,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796466276,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 9.332144233776074,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466276,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 9.332144233776074,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796466276,\n                \"segments\": [\n                  {\n                    \"length\": 9.332144233776074,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796466276,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457958714,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 9.229429126441136,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796469204,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 9.229429126441136,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469204,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 9.229429126441136,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796469204,\n                \"segments\": [\n                  {\n                    \"length\": 9.229429126441136,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469204,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 8.226874406547488,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796469206,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 8.226874406547488,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469206,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 8.226874406547488,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796469206,\n                \"segments\": [\n                  {\n                    \"length\": 8.226874406547488,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469206,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 7.20314296486216,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 1327887801,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 7.20314296486216,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1327887801,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1327887801,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 7.20314296486216,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"paved\",\n                    \"highway\": \"footway\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"paved\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 1327887801,\n                \"segments\": [\n                  {\n                    \"length\": 7.20314296486216,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1327887801,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1327887801,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 3.7973983199027197,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 796469205,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 3.7973983199027197,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469205,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 3.7973983199027197,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"footway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 796469205,\n                \"segments\": [\n                  {\n                    \"length\": 3.7973983199027197,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 796469205,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 457957434,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 2.8370824802955603,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 1355154126,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 2.8370824802955603,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadConnector\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1355154126,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982746,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1282562992,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1355154126,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 2.8370824802955603,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"path\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 1355154126,\n                \"segments\": [\n                  {\n                    \"length\": 2.8370824802955603,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadConnector\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1355154126,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 520982746,\n                            \"name\": null,\n                            \"subClass\": \"A1_service_roads\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1282562992,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 1355154126,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"key\": \"A2_cycleways\",\n        \"name\": \"Cycleways\",\n        \"kind\": \"linear\",\n        \"groups\": [\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 64.43008251585589,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 457958723,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 64.43008251585589,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 64.43008251585589,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"cycleway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 457958723,\n                \"segments\": [\n                  {\n                    \"length\": 64.43008251585589,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 36.100407943031364,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 24724510,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 36.100407943031364,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.337245396964462,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 426104083,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.5208736657337482,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8215979103308385,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 36.100407943031364,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"asphalt\",\n                    \"lit\": \"yes\",\n                    \"segregated\": \"no\",\n                    \"highway\": \"cycleway\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"asphalt\"\n                  },\n                  \"lit\": {\n                    \"value\": \"yes\"\n                  },\n                  \"segregated\": {\n                    \"value\": \"no\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 24724510,\n                \"segments\": [\n                  {\n                    \"length\": 36.100407943031364,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.337245396964462,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 426104083,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.5208736657337482,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 525693383,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 0.8215979103308385,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724848,\n                            \"name\": null,\n                            \"subClass\": \"A2_footpaths_trails\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed) [bridge, layer=1]\",\n            \"totalLength\": 5.412099054355507,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 140542965,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed) [bridge, layer=1]\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 5.412099054355507,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 5.412099054355507,\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"cycleway\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 140542965,\n                \"segments\": [\n                  {\n                    \"length\": 5.412099054355507,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 3.785341988246539,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 426104083,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 3.785341988246539,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 426104083,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 3.785341988246539,\n                \"semantics\": {\n                  \"raw\": {\n                    \"surface\": \"asphalt\",\n                    \"lit\": \"yes\",\n                    \"cycleway\": \"crossing\",\n                    \"segregated\": \"no\",\n                    \"highway\": \"cycleway\"\n                  },\n                  \"surface\": {\n                    \"class\": \"paved\",\n                    \"value\": \"asphalt\"\n                  },\n                  \"lit\": {\n                    \"value\": \"yes\"\n                  },\n                  \"cycleway\": {\n                    \"value\": \"unknown\"\n                  },\n                  \"segregated\": {\n                    \"value\": \"no\"\n                  }\n                }\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 426104083,\n                \"segments\": [\n                  {\n                    \"length\": 3.785341988246539,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"junction\",\n                        \"zone\": \"unknown\",\n                        \"connectorType\": \"RoadJunction\",\n                        \"connections\": [\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 24724510,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          },\n                          {\n                            \"osmType\": \"way\",\n                            \"osmId\": 426104083,\n                            \"name\": null,\n                            \"subClass\": \"A2_cycleways\"\n                          }\n                        ]\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"key\": \"A4_streams_canals\",\n        \"name\": \"Streams / canals\",\n        \"kind\": \"linear\",\n        \"groups\": [\n          {\n            \"label\": null,\n            \"displayLabel\": \"(unnamed)\",\n            \"totalLength\": 237.4579757744891,\n            \"totalArea\": 0.0,\n            \"ways\": [\n              {\n                \"osmId\": 44507498,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"(unnamed)\",\n                \"visibleGeometry\": [\n                  {\n                    \"length\": 159.34649938670591,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  },\n                  {\n                    \"length\": 59.56975504340956,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  },\n                  {\n                    \"length\": 18.541721344373634,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ],\n                \"length\": 237.4579757744891\n              }\n            ],\n            \"visibleGeometry\": [\n              {\n                \"osmId\": 44507498,\n                \"segments\": [\n                  {\n                    \"length\": 159.34649938670591,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  },\n                  {\n                    \"length\": 59.56975504340956,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  },\n                  {\n                    \"length\": 18.541721344373634,\n                    \"locationSamples\": [\n                      {\n                        \"t\": 0.0,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.25,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.5,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 0.75,\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"zone\": \"unknown\"\n                      }\n                    ],\n                    \"events\": [\n                      {\n                        \"t\": 0.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      },\n                      {\n                        \"t\": 1.0,\n                        \"type\": \"terminates\",\n                        \"zone\": \"unknown\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"key\": \"A5_connectivity_nodes\",\n        \"name\": \"Connectivity nodes\",\n        \"kind\": \"connectivity\",\n        \"groups\": [\n          {\n            \"label\": \"connector: (unnamed)\",\n            \"displayLabel\": null,\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 268724899,\n                \"osmType\": \"node\",\n                \"label\": \"connector: (unnamed)\"\n              },\n              {\n                \"osmId\": 268725041,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 276457277,\n                \"osmType\": \"node\",\n                \"label\": \"crossing: (unnamed)\",\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"crossing\",\n                    \"crossing\": \"uncontrolled\"\n                  },\n                  \"crossing\": {\n                    \"type\": \"uncontrolled\",\n                    \"markings\": \"unknown\",\n                    \"tactile_paving\": \"unknown\"\n                  }\n                }\n              },\n              {\n                \"osmId\": 276457280,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuum\\u00e4entie\",\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"turning_circle\"\n                  }\n                }\n              },\n              {\n                \"osmId\": 1538138535,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 1538139031,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 1539655844,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuum\\u00e4entie\"\n              },\n              {\n                \"osmId\": 1539656211,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuum\\u00e4entie\"\n              },\n              {\n                \"osmId\": 2921555993,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4254168966,\n                \"osmType\": \"node\",\n                \"label\": \"crossing: (unnamed)\",\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"crossing\",\n                    \"crossing:markings\": \"yes\"\n                  },\n                  \"crossing\": {\n                    \"type\": \"unknown\",\n                    \"markings\": \"yes\",\n                    \"tactile_paving\": \"unknown\"\n                  }\n                }\n              },\n              {\n                \"osmId\": 4254168969,\n                \"osmType\": \"node\",\n                \"label\": \"crossing: (unnamed)\",\n                \"semantics\": {\n                  \"raw\": {\n                    \"highway\": \"crossing\",\n                    \"crossing:markings\": \"yes\"\n                  },\n                  \"crossing\": {\n                    \"type\": \"unknown\",\n                    \"markings\": \"yes\",\n                    \"tactile_paving\": \"unknown\"\n                  }\n                }\n              },\n              {\n                \"osmId\": 4540138274,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuuniitynkuja\"\n              },\n              {\n                \"osmId\": 4540138286,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152725,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuum\\u00e4entie\"\n              },\n              {\n                \"osmId\": 4540152727,\n                \"osmType\": \"node\",\n                \"label\": \"connector: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152732,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuum\\u00e4entie\"\n              },\n              {\n                \"osmId\": 4540152736,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152737,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152741,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152742,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152743,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152754,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuum\\u00e4entie\"\n              },\n              {\n                \"osmId\": 4540152768,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152780,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152782,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 4540152890,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuum\\u00e4entie\"\n              },\n              {\n                \"osmId\": 4540152893,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuum\\u00e4entie\"\n              },\n              {\n                \"osmId\": 4540152903,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 5076856678,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 5076867288,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 5076867294,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 5076867299,\n                \"osmType\": \"node\",\n                \"label\": \"connector: (unnamed)\"\n              },\n              {\n                \"osmId\": 5076867300,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 5076867301,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 5113592090,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 6139123141,\n                \"osmType\": \"node\",\n                \"label\": \"junction: Nuum\\u00e4entie\"\n              },\n              {\n                \"osmId\": 6139126877,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 7448817945,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 7448817965,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 7448817966,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 7448817969,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 7448817972,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 7448840427,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 7448840431,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 11901878576,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 11901878578,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 12285606746,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 12285606749,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              },\n              {\n                \"osmId\": 12741249986,\n                \"osmType\": \"node\",\n                \"label\": \"junction: (unnamed)\"\n              }\n            ]\n          }\n        ]\n      }\n    ]\n  },\n  \"B\": {\n    \"key\": \"B\",\n    \"name\": \"Areal features\",\n    \"subclasses\": [\n      {\n        \"key\": \"B1_lakes\",\n        \"name\": \"Lakes\",\n        \"kind\": \"area\",\n        \"groups\": [\n          {\n            \"label\": \"Lippaj\\u00e4rvi\",\n            \"displayLabel\": \"lake: Lippaj\\u00e4rvi\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 585814.0000454999,\n            \"items\": [\n              {\n                \"osmId\": 171940210,\n                \"osmType\": \"way\",\n                \"label\": \"Lippaj\\u00e4rvi\",\n                \"displayLabel\": \"lake: Lippaj\\u00e4rvi\",\n                \"location\": null,\n                \"area\": 585814.0000454999\n              }\n            ]\n          }\n        ]\n      }\n    ]\n  },\n  \"C\": {\n    \"key\": \"C\",\n    \"name\": \"Buildings\",\n    \"subclasses\": [\n      {\n        \"key\": \"C2_public\",\n        \"name\": \"Functional public buildings\",\n        \"kind\": \"building\",\n        \"groups\": [\n          {\n            \"label\": null,\n            \"displayLabel\": \"kindergarten, Nuum\\u00e4entie 2\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 322065714,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"kindergarten, Nuum\\u00e4entie 2\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 1.9,\n                    \"insideCells\": 68,\n                    \"consideredCells\": 135,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 42,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 21,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"east\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 5,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [\n                    {\n                      \"east\": 13.3\n                    }\n                  ],\n                  \"components\": [\n                    {\n                      \"cellCount\": 68,\n                      \"centroid\": {\n                        \"x\": 104.38793529411768,\n                        \"y\": 60.20817696078431\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      \"touchesEdge\": true,\n                      \"edges\": [\n                        \"east\"\n                      ]\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 22.5,\n                    \"orientationLabel\": \"east-northeast to west-southwest\",\n                    \"fillRatio\": 0.62,\n                    \"aspectRatio\": 2.244,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"utility, Nuum\\u00e4entie 5\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"north\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 322066438,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"utility, Nuum\\u00e4entie 5\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"north\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.0,\n                    \"insideCells\": 1,\n                    \"consideredCells\": 1,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"north\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 1,\n                      \"centroid\": {\n                        \"x\": -17.805300000000003,\n                        \"y\": 77.28505\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 1.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"key\": \"C3_other_buildings\",\n        \"name\": \"Other buildings\",\n        \"kind\": \"building\",\n        \"groups\": [\n          {\n            \"label\": null,\n            \"displayLabel\": \"building\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 322066671,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"building\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.0,\n                    \"insideCells\": 0,\n                    \"consideredCells\": 0,\n                    \"gridSize\": 60,\n                    \"segments\": []\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": []\n                }\n              },\n              {\n                \"osmId\": 1376096432,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"building\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 3,\n                    \"consideredCells\": 6,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 3,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [\n                    {\n                      \"east\": 3.3\n                    }\n                  ],\n                  \"components\": [\n                    {\n                      \"cellCount\": 3,\n                      \"centroid\": {\n                        \"x\": 115.40472222222222,\n                        \"y\": 75.96397222222221\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      \"touchesEdge\": true,\n                      \"edges\": [\n                        \"east\"\n                      ]\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.75,\n                    \"aspectRatio\": 1.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"building\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"northeast\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 1376096430,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"building\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"northeast\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 4,\n                    \"consideredCells\": 4,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 4,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 4,\n                      \"centroid\": {\n                        \"x\": 83.0914,\n                        \"y\": 41.615949999999984\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 4.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 1376096431,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"building\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"northeast\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 12,\n                    \"consideredCells\": 48,\n                    \"gridSize\": 120,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 12,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 12,\n                      \"centroid\": {\n                        \"x\": 76.33198055555555,\n                        \"y\": 63.24859861111113\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"refinedFrom\": 60,\n                  \"shape\": {\n                    \"orientationDeg\": 22.5,\n                    \"orientationLabel\": \"east-northeast to west-southwest\",\n                    \"fillRatio\": 0.603,\n                    \"aspectRatio\": 2.096,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"building, Nuum\\u00e4entie 13\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 322065439,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"building, Nuum\\u00e4entie 13\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.0,\n                    \"insideCells\": 1,\n                    \"consideredCells\": 1,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"southeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 1,\n                      \"centroid\": {\n                        \"x\": 61.32936666666666,\n                        \"y\": -93.13398333333333\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"southeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 1.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"building, Nuum\\u00e4entie 5\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 322066437,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"building, Nuum\\u00e4entie 5\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 3,\n                    \"consideredCells\": 4,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"north\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 3,\n                      \"centroid\": {\n                        \"x\": -20.443122222222225,\n                        \"y\": 60.11103888888888\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.75,\n                    \"aspectRatio\": 1.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 322066439,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"building, Nuum\\u00e4entie 5\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.2,\n                    \"insideCells\": 8,\n                    \"consideredCells\": 16,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 7,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"north\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 8,\n                      \"centroid\": {\n                        \"x\": -31.653866666666666,\n                        \"y\": 45.579183333333326\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 8.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"garages building\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 288601751,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"garages building\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.2,\n                    \"insideCells\": 9,\n                    \"consideredCells\": 16,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 9,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"north\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 9,\n                      \"centroid\": {\n                        \"x\": 11.650381481481475,\n                        \"y\": 93.57834259259259\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.562,\n                    \"aspectRatio\": 4.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330061,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.2,\n                    \"insideCells\": 8,\n                    \"consideredCells\": 12,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 7,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"center\",\n                          \"dir\": null\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 8,\n                      \"centroid\": {\n                        \"x\": -7.105427357601002e-15,\n                        \"y\": -39.63033333333334\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 0.667,\n                    \"aspectRatio\": 3.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 190330096,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 3,\n                    \"consideredCells\": 6,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 3,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"east\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 3,\n                      \"centroid\": {\n                        \"x\": 94.30214444444444,\n                        \"y\": -23.11686111111111\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"east\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.75,\n                    \"aspectRatio\": 1.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 190330146,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 2,\n                    \"consideredCells\": 2,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"southeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 2,\n                      \"centroid\": {\n                        \"x\": 17.80529999999999,\n                        \"y\": -35.667100000000005\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"southeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 2.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"center\",\n                  \"dir\": null\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330100,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"center\",\n                      \"dir\": null\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 2,\n                    \"consideredCells\": 2,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"center\",\n                          \"dir\": null\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 2,\n                      \"centroid\": {\n                        \"x\": 13.84856666666667,\n                        \"y\": -19.814166666666665\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"center\",\n                          \"dir\": null\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 2.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 10a\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190327151,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 10a\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.3,\n                    \"insideCells\": 10,\n                    \"consideredCells\": 15,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 10,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"east\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 10,\n                      \"centroid\": {\n                        \"x\": 102.47939333333333,\n                        \"y\": 19.02552\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"east\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.667,\n                    \"aspectRatio\": 1.667,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 10b\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"east\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190327196,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 10b\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"east\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.2,\n                    \"insideCells\": 6,\n                    \"consideredCells\": 12,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 6,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"east\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 6,\n                      \"centroid\": {\n                        \"x\": 85.72922222222222,\n                        \"y\": -13.869316666666663\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"east\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 67.5,\n                    \"orientationLabel\": \"north-northeast to south-southwest\",\n                    \"fillRatio\": 0.674,\n                    \"aspectRatio\": 1.466,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 11a\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"south\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330237,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 11a\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"south\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.3,\n                    \"insideCells\": 11,\n                    \"consideredCells\": 12,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 7,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 4,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 11,\n                      \"centroid\": {\n                        \"x\": 28.23668787878787,\n                        \"y\": -65.03105606060605\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.917,\n                    \"aspectRatio\": 1.333,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 11b\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330239,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 11b\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.5,\n                    \"insideCells\": 17,\n                    \"consideredCells\": 24,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 9,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 8,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"south\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 17,\n                      \"centroid\": {\n                        \"x\": 32.00299019607843,\n                        \"y\": -91.73519509803921\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.708,\n                    \"aspectRatio\": 1.5,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 12\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330004,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 12\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.2,\n                    \"insideCells\": 6,\n                    \"consideredCells\": 9,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 4,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"east\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"southeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 6,\n                      \"centroid\": {\n                        \"x\": 98.25887777777778,\n                        \"y\": -42.933027777777774\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"east\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.667,\n                    \"aspectRatio\": 1.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 190330161,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 12\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.2,\n                    \"insideCells\": 6,\n                    \"consideredCells\": 6,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 6,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"southeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 6,\n                      \"centroid\": {\n                        \"x\": 104.85343333333333,\n                        \"y\": -55.48326666666666\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"southeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 1.5,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 13\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"southeast\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330064,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 13\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"southeast\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.2,\n                    \"insideCells\": 6,\n                    \"consideredCells\": 12,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 6,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 6,\n                      \"centroid\": {\n                        \"x\": 55.39426666666666,\n                        \"y\": -83.2259\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.75,\n                    \"aspectRatio\": 2.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 190330204,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 13\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"southeast\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.2,\n                    \"insideCells\": 6,\n                    \"consideredCells\": 6,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 4,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southeast\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"southeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 6,\n                      \"centroid\": {\n                        \"x\": 51.43753333333333,\n                        \"y\": -61.428116666666654\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 1.5,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 14\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"southeast\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330316,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 14\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"southeast\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.3,\n                    \"insideCells\": 11,\n                    \"consideredCells\": 15,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 11,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 11,\n                      \"centroid\": {\n                        \"x\": 81.11303333333332,\n                        \"y\": -75.83987424242423\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.733,\n                    \"aspectRatio\": 1.667,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 4\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"center\",\n                  \"dir\": null\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330040,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 4\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"center\",\n                      \"dir\": null\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.3,\n                    \"insideCells\": 12,\n                    \"consideredCells\": 36,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 10,\n                        \"loc\": {\n                          \"kind\": \"center\",\n                          \"dir\": null\n                        }\n                      },\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"east\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 12,\n                      \"centroid\": {\n                        \"x\": 25.059311111111104,\n                        \"y\": 7.928466666666665\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"center\",\n                          \"dir\": null\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 45.0,\n                    \"orientationLabel\": \"northeast-southwest\",\n                    \"fillRatio\": 0.616,\n                    \"aspectRatio\": 3.343,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 4\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"north\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 288603510,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 4\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"north\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.6,\n                    \"insideCells\": 20,\n                    \"consideredCells\": 64,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 9,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 8,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northeast\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 20,\n                      \"centroid\": {\n                        \"x\": 27.10362333333332,\n                        \"y\": 59.45049999999999\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 45.0,\n                    \"orientationLabel\": \"northeast-southwest\",\n                    \"fillRatio\": 0.551,\n                    \"aspectRatio\": 2.478,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 4\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 288603511,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 4\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.5,\n                    \"insideCells\": 17,\n                    \"consideredCells\": 32,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 10,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 7,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"east\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 17,\n                      \"centroid\": {\n                        \"x\": 51.3211588235294,\n                        \"y\": 26.695542156862743\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 22.5,\n                    \"orientationLabel\": \"east-northeast to west-southwest\",\n                    \"fillRatio\": 0.598,\n                    \"aspectRatio\": 2.383,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 5\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 288603132,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 5\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 5,\n                    \"consideredCells\": 18,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 5,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northwest\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [\n                    {\n                      \"north\": 3.3\n                    }\n                  ],\n                  \"components\": [\n                    {\n                      \"cellCount\": 5,\n                      \"centroid\": {\n                        \"x\": -72.40822,\n                        \"y\": 114.53944333333331\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      \"touchesEdge\": true,\n                      \"edges\": [\n                        \"north\"\n                      ]\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.833,\n                    \"aspectRatio\": 1.5,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 288603135,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 5\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.5,\n                    \"insideCells\": 17,\n                    \"consideredCells\": 54,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 11,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 6,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northwest\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 17,\n                      \"centroid\": {\n                        \"x\": -83.6732725490196,\n                        \"y\": 88.24222450980392\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 67.5,\n                    \"orientationLabel\": \"north-northeast to south-southwest\",\n                    \"fillRatio\": 0.516,\n                    \"aspectRatio\": 2.986,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 5\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"northwest\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 288603349,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 5\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"northwest\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.5,\n                    \"insideCells\": 19,\n                    \"consideredCells\": 27,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 9,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 4,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 4,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"north\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 19,\n                      \"centroid\": {\n                        \"x\": -41.545700000000004,\n                        \"y\": 85.21151666666665\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 0.704,\n                    \"aspectRatio\": 3.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 288603350,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 5\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"northwest\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.4,\n                    \"insideCells\": 16,\n                    \"consideredCells\": 40,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 15,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northwest\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 16,\n                      \"centroid\": {\n                        \"x\": -71.2212,\n                        \"y\": 53.50564999999999\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 67.5,\n                    \"orientationLabel\": \"north-northeast to south-southwest\",\n                    \"fillRatio\": 0.548,\n                    \"aspectRatio\": 2.321,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 6\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330163,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 6\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.5,\n                    \"insideCells\": 18,\n                    \"consideredCells\": 32,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 9,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"southeast\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 8,\n                        \"loc\": {\n                          \"kind\": \"center\",\n                          \"dir\": null\n                        }\n                      },\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"east\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 18,\n                      \"centroid\": {\n                        \"x\": 31.65386666666666,\n                        \"y\": -23.777400000000007\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"southeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 0.75,\n                    \"aspectRatio\": 1.5,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 7\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"center\",\n                  \"dir\": null\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330273,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 7\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"center\",\n                      \"dir\": null\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.5,\n                    \"insideCells\": 19,\n                    \"consideredCells\": 33,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 19,\n                        \"loc\": {\n                          \"kind\": \"center\",\n                          \"dir\": null\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 19,\n                      \"centroid\": {\n                        \"x\": -14.681563157894738,\n                        \"y\": -1.1452517543859648\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"center\",\n                          \"dir\": null\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 0.576,\n                    \"aspectRatio\": 3.667,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 8\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330288,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 8\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.3,\n                    \"insideCells\": 11,\n                    \"consideredCells\": 16,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 9,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"east\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"east\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 11,\n                      \"centroid\": {\n                        \"x\": 54.85471212121211,\n                        \"y\": -18.19284393939394\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"east\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.917,\n                    \"aspectRatio\": 1.333,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 9\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330010,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 9\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.4,\n                    \"insideCells\": 15,\n                    \"consideredCells\": 20,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 10,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"southwest\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 5,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"south\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 15,\n                      \"centroid\": {\n                        \"x\": -21.76203333333333,\n                        \"y\": -43.197243333333326\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"southwest\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.75,\n                    \"aspectRatio\": 1.25,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 9\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"southwest\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330024,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 9\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"southwest\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.8,\n                    \"insideCells\": 28,\n                    \"consideredCells\": 90,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 27,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southwest\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"southwest\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 28,\n                      \"centroid\": {\n                        \"x\": -55.25295476190477,\n                        \"y\": -63.97590952380953\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southwest\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 45.0,\n                    \"orientationLabel\": \"northeast-southwest\",\n                    \"fillRatio\": 0.63,\n                    \"aspectRatio\": 3.032,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuum\\u00e4entie 9\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"south\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330059,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 9\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"south\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.4,\n                    \"insideCells\": 16,\n                    \"consideredCells\": 24,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 12,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 4,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"south\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 16,\n                      \"centroid\": {\n                        \"x\": 2.4729583333333256,\n                        \"y\": -83.2259\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 0.667,\n                    \"aspectRatio\": 2.667,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 190330178,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuum\\u00e4entie 9\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"south\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.3,\n                    \"insideCells\": 11,\n                    \"consideredCells\": 20,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 8,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 3,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"south\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 11,\n                      \"centroid\": {\n                        \"x\": -20.323221212121215,\n                        \"y\": -64.6707621212121\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"south\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 0.733,\n                    \"aspectRatio\": 1.667,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuuniitynkuja 1\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190326831,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuuniitynkuja 1\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 1.2,\n                    \"insideCells\": 43,\n                    \"consideredCells\": 60,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 35,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 8,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"north\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [\n                    {\n                      \"north\": 20.0\n                    }\n                  ],\n                  \"components\": [\n                    {\n                      \"cellCount\": 43,\n                      \"centroid\": {\n                        \"x\": 65.28609999999999,\n                        \"y\": 111.2951220930233\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      \"touchesEdge\": true,\n                      \"edges\": [\n                        \"north\"\n                      ]\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.717,\n                    \"aspectRatio\": 2.4,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Nuuniitynkuja 2\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 288600948,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuuniitynkuja 2\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 4,\n                    \"consideredCells\": 4,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 4,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"north\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [\n                    {\n                      \"north\": 3.3\n                    }\n                  ],\n                  \"components\": [\n                    {\n                      \"cellCount\": 4,\n                      \"centroid\": {\n                        \"x\": 11.870199999999997,\n                        \"y\": 114.93576666666665\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      \"touchesEdge\": true,\n                      \"edges\": [\n                        \"north\"\n                      ]\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 1.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              },\n              {\n                \"osmId\": 288601098,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Nuuniitynkuja 2\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.4,\n                    \"insideCells\": 15,\n                    \"consideredCells\": 30,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 15,\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"north\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [\n                    {\n                      \"north\": 5.0\n                    }\n                  ],\n                  \"components\": [\n                    {\n                      \"cellCount\": 15,\n                      \"centroid\": {\n                        \"x\": -18.069082222222224,\n                        \"y\": 108.46248555555557\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"edge\",\n                          \"dir\": \"north\"\n                        }\n                      },\n                      \"touchesEdge\": true,\n                      \"edges\": [\n                        \"north\"\n                      ]\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 0.625,\n                    \"aspectRatio\": 1.5,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential building, Tuokkostie 9\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330226,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential building, Tuokkostie 9\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 2,\n                    \"consideredCells\": 3,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"southwest\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [\n                    {\n                      \"south\": 3.3\n                    }\n                  ],\n                  \"components\": [\n                    {\n                      \"cellCount\": 2,\n                      \"centroid\": {\n                        \"x\": -79.13466666666667,\n                        \"y\": -116.91338333333333\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"corner\",\n                          \"dir\": \"southwest\"\n                        }\n                      },\n                      \"touchesEdge\": true,\n                      \"edges\": [\n                        \"south\"\n                      ]\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 1.0,\n                    \"aspectRatio\": 2.0,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"residential, Nuum\\u00e4entie 7\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"west\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 322066644,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"residential, Nuum\\u00e4entie 7\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"west\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.4,\n                    \"insideCells\": 14,\n                    \"consideredCells\": 35,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 10,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"west\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 4,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"west\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 14,\n                      \"centroid\": {\n                        \"x\": -64.43822857142858,\n                        \"y\": 21.233607142857142\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"west\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 67.5,\n                    \"orientationLabel\": \"north-northeast to south-southwest\",\n                    \"fillRatio\": 0.575,\n                    \"aspectRatio\": 2.43,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"shed building, Nuum\\u00e4entie 7\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 322066645,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"shed building, Nuum\\u00e4entie 7\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 5,\n                    \"consideredCells\": 6,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"west\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 1,\n                        \"loc\": {\n                          \"kind\": \"center\",\n                          \"dir\": null\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 5,\n                      \"centroid\": {\n                        \"x\": -34.42358,\n                        \"y\": 16.25125666666667\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northwest\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 0.0,\n                    \"orientationLabel\": \"east-west\",\n                    \"fillRatio\": 0.833,\n                    \"aspectRatio\": 1.5,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"storage, Nuum\\u00e4entie 4\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 322066294,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"storage, Nuum\\u00e4entie 4\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.1,\n                    \"insideCells\": 11,\n                    \"consideredCells\": 28,\n                    \"gridSize\": 120,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 9,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"north\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 11,\n                      \"centroid\": {\n                        \"x\": 19.693740909090902,\n                        \"y\": 37.20234924242424\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"northeast\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"refinedFrom\": 60,\n                  \"shape\": {\n                    \"orientationDeg\": 22.5,\n                    \"orientationLabel\": \"east-northeast to west-southwest\",\n                    \"fillRatio\": 0.603,\n                    \"aspectRatio\": 2.096,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"terrace building, Nuum\\u00e4entie 7\",\n            \"location\": {\n              \"center\": {\n                \"loc\": {\n                  \"kind\": \"part\",\n                  \"dir\": \"west\"\n                }\n              }\n            },\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330049,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"terrace building, Nuum\\u00e4entie 7\",\n                \"location\": {\n                  \"center\": {\n                    \"loc\": {\n                      \"kind\": \"part\",\n                      \"dir\": \"west\"\n                    }\n                  }\n                },\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.4,\n                    \"insideCells\": 15,\n                    \"consideredCells\": 28,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 13,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"west\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 2,\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"southwest\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 15,\n                      \"centroid\": {\n                        \"x\": -68.18770444444446,\n                        \"y\": -21.267352222222218\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"part\",\n                          \"dir\": \"west\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 22.5,\n                    \"orientationLabel\": \"east-northeast to west-southwest\",\n                    \"fillRatio\": 0.668,\n                    \"aspectRatio\": 2.379,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          },\n          {\n            \"label\": null,\n            \"displayLabel\": \"terrace building, Nuum\\u00e4entie 7\",\n            \"location\": null,\n            \"totalLength\": 0.0,\n            \"totalArea\": 0.0,\n            \"items\": [\n              {\n                \"osmId\": 190330155,\n                \"osmType\": \"way\",\n                \"label\": null,\n                \"displayLabel\": \"terrace building, Nuum\\u00e4entie 7\",\n                \"location\": null,\n                \"visibleGeometry\": {\n                  \"coverage\": {\n                    \"coveragePercent\": 0.4,\n                    \"insideCells\": 13,\n                    \"consideredCells\": 24,\n                    \"gridSize\": 60,\n                    \"segments\": [\n                      {\n                        \"insideCount\": 8,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"west\"\n                        }\n                      },\n                      {\n                        \"insideCount\": 5,\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"southwest\"\n                        }\n                      }\n                    ]\n                  },\n                  \"edgesTouched\": [],\n                  \"components\": [\n                    {\n                      \"cellCount\": 13,\n                      \"centroid\": {\n                        \"x\": -41.545700000000004,\n                        \"y\": -14.783908974358978\n                      },\n                      \"location\": {\n                        \"loc\": {\n                          \"kind\": \"offset_center\",\n                          \"dir\": \"west\"\n                        }\n                      },\n                      \"touchesEdge\": false,\n                      \"edges\": []\n                    }\n                  ],\n                  \"shape\": {\n                    \"orientationDeg\": 90.0,\n                    \"orientationLabel\": \"north-south\",\n                    \"fillRatio\": 0.619,\n                    \"aspectRatio\": 2.333,\n                    \"type\": \"regular\"\n                  },\n                  \"shapeGridSize\": 60\n                }\n              }\n            ]\n          }\n        ]\n      }\n    ]\n  },\n  \"D\": {\n    \"key\": \"D\",\n    \"name\": \"POIs\",\n    \"subclasses\": [\n      {\n        \"key\": null,\n        \"name\": null,\n        \"kind\": null,\n        \"groups\": [],\n        \"empty\": true\n      }\n    ]\n  },\n  \"E\": {\n    \"key\": \"E\",\n    \"name\": \"Boundaries and edges\",\n    \"subclasses\": [\n      {\n        \"key\": null,\n        \"name\": null,\n        \"kind\": null,\n        \"groups\": [],\n        \"empty\": true\n      }\n    ]\n  }\n}"
  },
  {
    "path": "test/data/map-meta.indented.json",
    "content": "{\n   \"areas\" : [\n      {\n         \"bounds\" : {\n            \"maxX\" : -102.729,\n            \"maxY\" : 235.033,\n            \"minX\" : -1587.309,\n            \"minY\" : -1063.551\n         },\n         \"center\" : [\n            -845.019,\n            -414.259\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -102.729,\n                  142.9\n               ],\n               [\n                  -126.36,\n                  91.836\n               ],\n               [\n                  -127.162,\n                  89.298\n               ],\n               [\n                  -129.577,\n                  81.673\n               ],\n               [\n                  -128.648,\n                  59.197\n               ],\n               [\n                  -120.154,\n                  38.903\n               ],\n               [\n                  -117.43,\n                  29.875\n               ],\n               [\n                  -116.38,\n                  18.631\n               ],\n               [\n                  -119.292,\n                  4.216\n               ],\n               [\n                  -127.156,\n                  -18.75\n               ],\n               [\n                  -125.813,\n                  -28.134\n               ],\n               [\n                  -124.94,\n                  -34.223\n               ],\n               [\n                  -125.172,\n                  -38.687\n               ],\n               [\n                  -129.184,\n                  -46.802\n               ],\n               [\n                  -133.6,\n                  -55.073\n               ],\n               [\n                  -135.855,\n                  -62.409\n               ],\n               [\n                  -136.269,\n                  -74.052\n               ],\n               [\n                  -133.368,\n                  -84.55\n               ],\n               [\n                  -132.086,\n                  -96.36\n               ],\n               [\n                  -136.247,\n                  -122.553\n               ],\n               [\n                  -148.024,\n                  -128.831\n               ],\n               [\n                  -154.009,\n                  -131.08\n               ],\n               [\n                  -161.464,\n                  -139.128\n               ],\n               [\n                  -172.258,\n                  -142.467\n               ],\n               [\n                  -180.89,\n                  -144.226\n               ],\n               [\n                  -182.36,\n                  -148.156\n               ],\n               [\n                  -189.522,\n                  -150.805\n               ],\n               [\n                  -204.3,\n                  -155.202\n               ],\n               [\n                  -215.226,\n                  -158.452\n               ],\n               [\n                  -222.847,\n                  -160.935\n               ],\n               [\n                  -233.281,\n                  -164.341\n               ],\n               [\n                  -248.584,\n                  -172.389\n               ],\n               [\n                  -267.324,\n                  -184.656\n               ],\n               [\n                  -279.886,\n                  -188.774\n               ],\n               [\n                  -286.164,\n                  -192.603\n               ],\n               [\n                  -291.071,\n                  -197.902\n               ],\n               [\n                  -292.641,\n                  -197.112\n               ],\n               [\n                  -299.057,\n                  -205.727\n               ],\n               [\n                  -306.96,\n                  -216.346\n               ],\n               [\n                  -309.806,\n                  -215.267\n               ],\n               [\n                  -319.217,\n                  -222.291\n               ],\n               [\n                  -329.137,\n                  -231.941\n               ],\n               [\n                  -336.394,\n                  -242.928\n               ],\n               [\n                  -337.67,\n                  -246.468\n               ],\n               [\n                  -339.14,\n                  -260.493\n               ],\n               [\n                  -347.286,\n                  -267.561\n               ],\n               [\n                  -361.511,\n                  -283.456\n               ],\n               [\n                  -376.035,\n                  -303.96\n               ],\n               [\n                  -386.53,\n                  -316.215\n               ],\n               [\n                  -405.563,\n                  -342.618\n               ],\n               [\n                  -418.014,\n                  -361.306\n               ],\n               [\n                  -442.916,\n                  -379.505\n               ],\n               [\n                  -482.945,\n                  -405.407\n               ],\n               [\n                  -500.215,\n                  -411.684\n               ],\n               [\n                  -513.362,\n                  -421.502\n               ],\n               [\n                  -533.18,\n                  -438.965\n               ],\n               [\n                  -548.483,\n                  -457.798\n               ],\n               [\n                  -555.242,\n                  -473.86\n               ],\n               [\n                  -562.305,\n                  -495.642\n               ],\n               [\n                  -560.934,\n                  -505.448\n               ],\n               [\n                  -560.343,\n                  -522.132\n               ],\n               [\n                  -562.697,\n                  -541.165\n               ],\n               [\n                  -568.588,\n                  -556.08\n               ],\n               [\n                  -652.054,\n                  -656.206\n               ],\n               [\n                  -700.643,\n                  -704.81\n               ],\n               [\n                  -712.613,\n                  -714.827\n               ],\n               [\n                  -729.093,\n                  -735.618\n               ],\n               [\n                  -736.554,\n                  -750.531\n               ],\n               [\n                  -735.963,\n                  -767.805\n               ],\n               [\n                  -726.988,\n                  -850.196\n               ],\n               [\n                  -726.015,\n                  -859.155\n               ],\n               [\n                  -732.918,\n                  -877.552\n               ],\n               [\n                  -731.873,\n                  -891.029\n               ],\n               [\n                  -731.945,\n                  -895.681\n               ],\n               [\n                  -733.824,\n                  -900.345\n               ],\n               [\n                  -738.842,\n                  -908.035\n               ],\n               [\n                  -744.755,\n                  -910.116\n               ],\n               [\n                  -747.916,\n                  -915.413\n               ],\n               [\n                  -752.006,\n                  -916.627\n               ],\n               [\n                  -755.935,\n                  -920.934\n               ],\n               [\n                  -760.384,\n                  -932.775\n               ],\n               [\n                  -761.628,\n                  -936.247\n               ],\n               [\n                  -761.34,\n                  -940.076\n               ],\n               [\n                  -759.914,\n                  -946.686\n               ],\n               [\n                  -763.363,\n                  -950.893\n               ],\n               [\n                  -769.503,\n                  -952.362\n               ],\n               [\n                  -777.046,\n                  -953.175\n               ],\n               [\n                  -778.522,\n                  -953.597\n               ],\n               [\n                  -782.496,\n                  -954.799\n               ],\n               [\n                  -788.028,\n                  -955.723\n               ],\n               [\n                  -789.033,\n                  -955.89\n               ],\n               [\n                  -800.81,\n                  -957.86\n               ],\n               [\n                  -815.665,\n                  -957.927\n               ],\n               [\n                  -831.891,\n                  -957.993\n               ],\n               [\n                  -851.422,\n                  -954.087\n               ],\n               [\n                  -855.478,\n                  -953.275\n               ],\n               [\n                  -865.608,\n                  -947.732\n               ],\n               [\n                  -879.347,\n                  -943.425\n               ],\n               [\n                  -896.413,\n                  -940.51\n               ],\n               [\n                  -916.391,\n                  -935.379\n               ],\n               [\n                  -923.885,\n                  -938.851\n               ],\n               [\n                  -945.112,\n                  -935.657\n               ],\n               [\n                  -965.096,\n                  -929.414\n               ],\n               [\n                  -985.488,\n                  -921.501\n               ],\n               [\n                  -997.005,\n                  -913.177\n               ],\n               [\n                  -1004.079,\n                  -911.797\n               ],\n               [\n                  -1015.596,\n                  -913.177\n               ],\n               [\n                  -1024.477,\n                  -912.898\n               ],\n               [\n                  -1035.442,\n                  -917.762\n               ],\n               [\n                  -1047.788,\n                  -937.594\n               ],\n               [\n                  -1114.498,\n                  -954.443\n               ],\n               [\n                  -1140.859,\n                  -957.214\n               ],\n               [\n                  -1162.783,\n                  -963.736\n               ],\n               [\n                  -1195.251,\n                  -979.417\n               ],\n               [\n                  -1205.519,\n                  -980.385\n               ],\n               [\n                  -1223.414,\n                  -982.878\n               ],\n               [\n                  -1253.942,\n                  -995.097\n               ],\n               [\n                  -1287.797,\n                  -1013.404\n               ],\n               [\n                  -1318.436,\n                  -1024.533\n               ],\n               [\n                  -1344.051,\n                  -1020.738\n               ],\n               [\n                  -1374.015,\n                  -1027.872\n               ],\n               [\n                  -1401.681,\n                  -1034.939\n               ],\n               [\n                  -1437.592,\n                  -1046.902\n               ],\n               [\n                  -1470.751,\n                  -1053.379\n               ],\n               [\n                  -1485.401,\n                  -1052.99\n               ],\n               [\n                  -1494.885,\n                  -1051.198\n               ],\n               [\n                  -1495.719,\n                  -1059.11\n               ],\n               [\n                  -1496.41,\n                  -1063.128\n               ],\n               [\n                  -1497.935,\n                  -1063.551\n               ],\n               [\n                  -1498.217,\n                  -1058.832\n               ],\n               [\n                  -1499.881,\n                  -1054.392\n               ],\n               [\n                  -1500.157,\n                  -1050.363\n               ],\n               [\n                  -1505.567,\n                  -1047.87\n               ],\n               [\n                  -1511.398,\n                  -1047.036\n               ],\n               [\n                  -1516.112,\n                  -1044.12\n               ],\n               [\n                  -1524.717,\n                  -1047.726\n               ],\n               [\n                  -1529.574,\n                  -1050.786\n               ],\n               [\n                  -1539.981,\n                  -1050.786\n               ],\n               [\n                  -1544.28,\n                  -1049.395\n               ],\n               [\n                  -1548.304,\n                  -1051.198\n               ],\n               [\n                  -1548.027,\n                  -1053.969\n               ],\n               [\n                  -1554.184,\n                  -1051.087\n               ],\n               [\n                  -1557.875,\n                  -1044.676\n               ],\n               [\n                  -1562.181,\n                  -1043.007\n               ],\n               [\n                  -1564.397,\n                  -1046.201\n               ],\n               [\n                  -1564.258,\n                  -1052.722\n               ],\n               [\n                  -1565.507,\n                  -1055.082\n               ],\n               [\n                  -1580.418,\n                  -1048.226\n               ],\n               [\n                  -1584.148,\n                  -1041.939\n               ],\n               [\n                  -1584.845,\n                  -1031.121\n               ],\n               [\n                  -1580.539,\n                  -1022.374\n               ],\n               [\n                  -1575.825,\n                  -1018.913\n               ],\n               [\n                  -1578.18,\n                  -1011.557\n               ],\n               [\n                  -1582.347,\n                  -1009.476\n               ],\n               [\n                  -1579.014,\n                  -1002.264\n               ],\n               [\n                  -1579.429,\n                  -986.161\n               ],\n               [\n                  -1587.309,\n                  -977.447\n               ],\n               [\n                  -1586.326,\n                  -973.129\n               ],\n               [\n                  -1581.424,\n                  -973.719\n               ],\n               [\n                  -1577.693,\n                  -965.283\n               ],\n               [\n                  -1578.677,\n                  -954.098\n               ],\n               [\n                  -1582.993,\n                  -947.031\n               ],\n               [\n                  -1574.162,\n                  -932.897\n               ],\n               [\n                  -1573.963,\n                  -915.046\n               ],\n               [\n                  -1571.216,\n                  -910.528\n               ],\n               [\n                  -1569.846,\n                  -901.502\n               ],\n               [\n                  -1573.178,\n                  -892.087\n               ],\n               [\n                  -1571.216,\n                  -890.907\n               ],\n               [\n                  -1566.9,\n                  -891.297\n               ],\n               [\n                  -1563.761,\n                  -887.969\n               ],\n               [\n                  -1562.584,\n                  -879.722\n               ],\n               [\n                  -1552.576,\n                  -873.445\n               ],\n               [\n                  -1545.85,\n                  -855.883\n               ],\n               [\n                  -1535.311,\n                  -849.117\n               ],\n               [\n                  -1532.957,\n                  -842.25\n               ],\n               [\n                  -1520.787,\n                  -832.044\n               ],\n               [\n                  -1508.231,\n                  -823.207\n               ],\n               [\n                  -1502.738,\n                  -826.357\n               ],\n               [\n                  -1484.429,\n                  -816.229\n               ],\n               [\n                  -1484.097,\n                  -809.474\n               ],\n               [\n                  -1458.979,\n                  -796.919\n               ],\n               [\n                  -1452.154,\n                  -791.8\n               ],\n               [\n                  -1449.109,\n                  -786.213\n               ],\n               [\n                  -1443.323,\n                  -782.974\n               ],\n               [\n                  -1435.221,\n                  -778.945\n               ],\n               [\n                  -1419.675,\n                  -769.329\n               ],\n               [\n                  -1410.551,\n                  -764.332\n               ],\n               [\n                  -1403.295,\n                  -758.344\n               ],\n               [\n                  -1397.895,\n                  -751.377\n               ],\n               [\n                  -1388.113,\n                  -743.664\n               ],\n               [\n                  -1385.339,\n                  -741.472\n               ],\n               [\n                  -1345.101,\n                  -704.833\n               ],\n               [\n                  -1337.248,\n                  -698.411\n               ],\n               [\n                  -1317.833,\n                  -687.637\n               ],\n               [\n                  -1301.337,\n                  -678.299\n               ],\n               [\n                  -1296.827,\n                  -675.35\n               ],\n               [\n                  -1283.68,\n                  -672.211\n               ],\n               [\n                  -1279.463,\n                  -672.512\n               ],\n               [\n                  -1277.888,\n                  -675.16\n               ],\n               [\n                  -1274.064,\n                  -677.609\n               ],\n               [\n                  -1271.809,\n                  -675.65\n               ],\n               [\n                  -1269.256,\n                  -672.411\n               ],\n               [\n                  -1258.562,\n                  -676.73\n               ],\n               [\n                  -1246.791,\n                  -676.529\n               ],\n               [\n                  -1240.12,\n                  -685.355\n               ],\n               [\n                  -1238.551,\n                  -689.284\n               ],\n               [\n                  -1234.721,\n                  -691.143\n               ],\n               [\n                  -1231.09,\n                  -690.564\n               ],\n               [\n                  -1228.935,\n                  -696.841\n               ],\n               [\n                  -1225.696,\n                  -697.921\n               ],\n               [\n                  -1221.673,\n                  -702.139\n               ],\n               [\n                  -1217.456,\n                  -707.437\n               ],\n               [\n                  -1210.294,\n                  -711.844\n               ],\n               [\n                  -1202.441,\n                  -719.402\n               ],\n               [\n                  -1189.122,\n                  -732.29\n               ],\n               [\n                  -1170.774,\n                  -740.47\n               ],\n               [\n                  -1165.469,\n                  -771.188\n               ],\n               [\n                  -1147.745,\n                  -797.042\n               ],\n               [\n                  -1144.313,\n                  -803.92\n               ],\n               [\n                  -1135.675,\n                  -809.117\n               ],\n               [\n                  -1115.073,\n                  -812.545\n               ],\n               [\n                  -1092.602,\n                  -814.337\n               ],\n               [\n                  -1076.509,\n                  -815.172\n               ],\n               [\n                  -1071.513,\n                  -810.731\n               ],\n               [\n                  -1053.099,\n                  -813.391\n               ],\n               [\n                  -1049.861,\n                  -815.94\n               ],\n               [\n                  -1042.698,\n                  -818.399\n               ],\n               [\n                  -1030.435,\n                  -822.028\n               ],\n               [\n                  -1023.273,\n                  -827.915\n               ],\n               [\n                  -1018.072,\n                  -828.605\n               ],\n               [\n                  -1011.894,\n                  -827.525\n               ],\n               [\n                  -1009.048,\n                  -829.195\n               ],\n               [\n                  -1002.372,\n                  -833.313\n               ],\n               [\n                  -995.801,\n                  -835.861\n               ],\n               [\n                  -988.246,\n                  -838.711\n               ],\n               [\n                  -978.138,\n                  -837.531\n               ],\n               [\n                  -973.336,\n                  -833.903\n               ],\n               [\n                  -971.175,\n                  -828.794\n               ],\n               [\n                  -966.66,\n                  -826.246\n               ],\n               [\n                  -961.901,\n                  -825.834\n               ],\n               [\n                  -958.911,\n                  -821.204\n               ],\n               [\n                  -960.824,\n                  -812.579\n               ],\n               [\n                  -939.182,\n                  -784.733\n               ],\n               [\n                  -926.781,\n                  -772.323\n               ],\n               [\n                  -917.811,\n                  -767.994\n               ],\n               [\n                  -911.561,\n                  -764.477\n               ],\n               [\n                  -908.858,\n                  -760.37\n               ],\n               [\n                  -908.096,\n                  -753.881\n               ],\n               [\n                  -907.847,\n                  -743.598\n               ],\n               [\n                  -912.401,\n                  -729.83\n               ],\n               [\n                  -909.56,\n                  -721.75\n               ],\n               [\n                  -902.652,\n                  -715.217\n               ],\n               [\n                  -898.695,\n                  -707.415\n               ],\n               [\n                  -898.573,\n                  -696.552\n               ],\n               [\n                  -901.265,\n                  -678.221\n               ],\n               [\n                  -900.624,\n                  -670.709\n               ],\n               [\n                  -894.528,\n                  -664.409\n               ],\n               [\n                  -893.113,\n                  -660.146\n               ],\n               [\n                  -893.163,\n                  -654.236\n               ],\n               [\n                  -892.251,\n                  -650.552\n               ],\n               [\n                  -883.901,\n                  -633.423\n               ],\n               [\n                  -867.338,\n                  -622.327\n               ],\n               [\n                  -846.37,\n                  -588.658\n               ],\n               [\n                  -824.911,\n                  -531.549\n               ],\n               [\n                  -823.342,\n                  -512.516\n               ],\n               [\n                  -809.603,\n                  -490.923\n               ],\n               [\n                  -799.793,\n                  -451.287\n               ],\n               [\n                  -798.976,\n                  -437.541\n               ],\n               [\n                  -797.439,\n                  -411.651\n               ],\n               [\n                  -800.197,\n                  -392.439\n               ],\n               [\n                  -802.253,\n                  -378.103\n               ],\n               [\n                  -811.128,\n                  -361.228\n               ],\n               [\n                  -818.014,\n                  -344.065\n               ],\n               [\n                  -823.64,\n                  -330.307\n               ],\n               [\n                  -831.283,\n                  -315.859\n               ],\n               [\n                  -836.141,\n                  -309.759\n               ],\n               [\n                  -841.806,\n                  -301.222\n               ],\n               [\n                  -844.718,\n                  -295.734\n               ],\n               [\n                  -845.978,\n                  -286.083\n               ],\n               [\n                  -846.542,\n                  -269.966\n               ],\n               [\n                  -846.663,\n                  -260.415\n               ],\n               [\n                  -844.265,\n                  -254.182\n               ],\n               [\n                  -845.53,\n                  -240.535\n               ],\n               [\n                  -842.192,\n                  -232.175\n               ],\n               [\n                  -826.586,\n                  -216.959\n               ],\n               [\n                  -803.568,\n                  -194.507\n               ],\n               [\n                  -785.204,\n                  -148.389\n               ],\n               [\n                  -781.904,\n                  -83.225\n               ],\n               [\n                  -778.29,\n                  -75.845\n               ],\n               [\n                  -774.991,\n                  -69.7\n               ],\n               [\n                  -771.669,\n                  -64.891\n               ],\n               [\n                  -765.872,\n                  -58.579\n               ],\n               [\n                  -759.727,\n                  -54.171\n               ],\n               [\n                  -753.465,\n                  -46.345\n               ],\n               [\n                  -753.542,\n                  -37.807\n               ],\n               [\n                  -754.471,\n                  -29.536\n               ],\n               [\n                  -755.035,\n                  -19.952\n               ],\n               [\n                  -755.267,\n                  23.841\n               ],\n               [\n                  -755.046,\n                  42.276\n               ],\n               [\n                  -753.299,\n                  57.126\n               ],\n               [\n                  -752.443,\n                  66.154\n               ],\n               [\n                  -748.314,\n                  78.255\n               ],\n               [\n                  -743.971,\n                  87.751\n               ],\n               [\n                  -735.371,\n                  97.592\n               ],\n               [\n                  -732.1,\n                  104.082\n               ],\n               [\n                  -729.95,\n                  110.271\n               ],\n               [\n                  -728.811,\n                  111.229\n               ],\n               [\n                  -727.049,\n                  110.494\n               ],\n               [\n                  -724.473,\n                  107.744\n               ],\n               [\n                  -722.252,\n                  106.709\n               ],\n               [\n                  -718.416,\n                  106.843\n               ],\n               [\n                  -716.476,\n                  108.924\n               ],\n               [\n                  -714.537,\n                  111.908\n               ],\n               [\n                  -709.701,\n                  114.758\n               ],\n               [\n                  -688.148,\n                  117.63\n               ],\n               [\n                  -681.068,\n                  117.964\n               ],\n               [\n                  -666.291,\n                  116.249\n               ],\n               [\n                  -655.486,\n                  116.539\n               ],\n               [\n                  -648.236,\n                  119.188\n               ],\n               [\n                  -642.328,\n                  122.606\n               ],\n               [\n                  -631.999,\n                  131.668\n               ],\n               [\n                  -613.513,\n                  151.416\n               ],\n               [\n                  -597.867,\n                  166.056\n               ],\n               [\n                  -584.676,\n                  182.365\n               ],\n               [\n                  -580.105,\n                  188.788\n               ],\n               [\n                  -575.319,\n                  191.493\n               ],\n               [\n                  -571.329,\n                  192.339\n               ],\n               [\n                  -566.919,\n                  193.286\n               ],\n               [\n                  -564.908,\n                  193.709\n               ],\n               [\n                  -556.668,\n                  200.867\n               ],\n               [\n                  -554.943,\n                  203.16\n               ],\n               [\n                  -553.556,\n                  203.572\n               ],\n               [\n                  -547.847,\n                  205.287\n               ],\n               [\n                  -538.358,\n                  206.344\n               ],\n               [\n                  -510.953,\n                  200.867\n               ],\n               [\n                  -501.198,\n                  200.266\n               ],\n               [\n                  -491.45,\n                  200.344\n               ],\n               [\n                  -476.44,\n                  196.971\n               ],\n               [\n                  -464.812,\n                  195.957\n               ],\n               [\n                  -459.65,\n                  195.345\n               ],\n               [\n                  -455.644,\n                  190.825\n               ],\n               [\n                  -452.394,\n                  186.985\n               ],\n               [\n                  -441.667,\n                  184.502\n               ],\n               [\n                  -431.764,\n                  181.597\n               ],\n               [\n                  -423.784,\n                  176.598\n               ],\n               [\n                  -415.107,\n                  170.019\n               ],\n               [\n                  -398.975,\n                  160.612\n               ],\n               [\n                  -378.483,\n                  149.735\n               ],\n               [\n                  -362.119,\n                  141.486\n               ],\n               [\n                  -350.674,\n                  136.054\n               ],\n               [\n                  -347.005,\n                  133.415\n               ],\n               [\n                  -336.93,\n                  129.942\n               ],\n               [\n                  -327.071,\n                  130.098\n               ],\n               [\n                  -316.311,\n                  136.377\n               ],\n               [\n                  -306.297,\n                  147.932\n               ],\n               [\n                  -286.097,\n                  157.272\n               ],\n               [\n                  -273,\n                  164.185\n               ],\n               [\n                  -261.969,\n                  178.891\n               ],\n               [\n                  -245.262,\n                  196.047\n               ],\n               [\n                  -228.948,\n                  205.465\n               ],\n               [\n                  -203.267,\n                  217.243\n               ],\n               [\n                  -193.955,\n                  222.542\n               ],\n               [\n                  -174.358,\n                  231.025\n               ],\n               [\n                  -159.63,\n                  235.033\n               ],\n               [\n                  -146.637,\n                  231.047\n               ],\n               [\n                  -133.141,\n                  223.143\n               ],\n               [\n                  -119.403,\n                  215.195\n               ],\n               [\n                  -114.025,\n                  204.585\n               ],\n               [\n                  -113.644,\n                  198.028\n               ],\n               [\n                  -105.984,\n                  179.225\n               ],\n               [\n                  -104.006,\n                  169.685\n               ],\n               [\n                  -105.785,\n                  158.419\n               ],\n               [\n                  -102.729,\n                  142.9\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 171940210,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Water\",\n         \"representations\" : [\n            \"Water\"\n         ],\n         \"tags\" : {\n            \"loc_name\" : \"Lippis\",\n            \"name\" : \"Lippajärvi\",\n            \"name:sv\" : \"Klappträsk\",\n            \"natural\" : \"water\",\n            \"water\" : \"lake\",\n            \"wikidata\" : \"Q24356918\",\n            \"wikipedia\" : \"fi:Lippajärvi (järvi)\"\n         },\n         \"tmCategory\" : \"Water\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 91.493,\n            \"maxY\" : 125.701,\n            \"minX\" : 43.263,\n            \"minY\" : 100.898\n         },\n         \"center\" : [\n            67.378,\n            113.2995\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  43.661,\n                  100.898\n               ],\n               [\n                  43.263,\n                  109.191\n               ],\n               [\n                  45.706,\n                  109.303\n               ],\n               [\n                  45.319,\n                  117.44\n               ],\n               [\n                  50.896,\n                  117.708\n               ],\n               [\n                  50.829,\n                  119.099\n               ],\n               [\n                  56.671,\n                  119.378\n               ],\n               [\n                  56.604,\n                  120.747\n               ],\n               [\n                  62.772,\n                  121.047\n               ],\n               [\n                  62.882,\n                  118.743\n               ],\n               [\n                  69.028,\n                  119.044\n               ],\n               [\n                  68.956,\n                  120.546\n               ],\n               [\n                  74.593,\n                  120.825\n               ],\n               [\n                  74.543,\n                  121.949\n               ],\n               [\n                  80.633,\n                  122.239\n               ],\n               [\n                  80.556,\n                  123.764\n               ],\n               [\n                  86.326,\n                  124.042\n               ],\n               [\n                  86.259,\n                  125.456\n               ],\n               [\n                  91.15,\n                  125.701\n               ],\n               [\n                  91.493,\n                  118.721\n               ],\n               [\n                  90.465,\n                  118.665\n               ],\n               [\n                  90.924,\n                  109.537\n               ],\n               [\n                  85.121,\n                  109.236\n               ],\n               [\n                  85.198,\n                  107.7\n               ],\n               [\n                  79.401,\n                  107.41\n               ],\n               [\n                  79.473,\n                  105.974\n               ],\n               [\n                  73.604,\n                  105.674\n               ],\n               [\n                  73.67,\n                  104.36\n               ],\n               [\n                  67.818,\n                  104.059\n               ],\n               [\n                  67.889,\n                  102.612\n               ],\n               [\n                  61.307,\n                  102.289\n               ],\n               [\n                  61.191,\n                  104.56\n               ],\n               [\n                  55.748,\n                  104.282\n               ],\n               [\n                  55.814,\n                  102.957\n               ],\n               [\n                  50.006,\n                  102.668\n               ],\n               [\n                  50.078,\n                  101.232\n               ],\n               [\n                  43.661,\n                  100.898\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190326831,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:city\" : \"Espoo\",\n            \"addr:housenumber\" : \"1\",\n            \"addr:postcode\" : \"02720\",\n            \"addr:street\" : \"Nuuniitynkuja\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 109.095,\n            \"maxY\" : 22.628,\n            \"minX\" : 90.587,\n            \"minY\" : 11.808\n         },\n         \"center\" : [\n            99.841,\n            17.218\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  90.752,\n                  22.628\n               ],\n               [\n                  109.095,\n                  21.915\n               ],\n               [\n                  108.708,\n                  11.808\n               ],\n               [\n                  99.777,\n                  12.153\n               ],\n               [\n                  99.998,\n                  17.874\n               ],\n               [\n                  90.587,\n                  18.231\n               ],\n               [\n                  90.752,\n                  22.628\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190327151,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"10a\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 90.874,\n            \"maxY\" : -8.018,\n            \"minX\" : 78.533,\n            \"minY\" : -21.833\n         },\n         \"center\" : [\n            84.7035,\n            -14.9255\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  90.393,\n                  -8.018\n               ],\n               [\n                  90.874,\n                  -17.737\n               ],\n               [\n                  87.857,\n                  -17.892\n               ],\n               [\n                  88.033,\n                  -21.366\n               ],\n               [\n                  78.859,\n                  -21.833\n               ],\n               [\n                  78.533,\n                  -15.31\n               ],\n               [\n                  82.756,\n                  -15.098\n               ],\n               [\n                  82.424,\n                  -8.419\n               ],\n               [\n                  90.393,\n                  -8.018\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190327196,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"10b\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 103.06,\n            \"maxY\" : -36.639,\n            \"minX\" : 90.775,\n            \"minY\" : -48.149\n         },\n         \"center\" : [\n            96.9175,\n            -42.394\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  90.775,\n                  -47.837\n               ],\n               [\n                  90.907,\n                  -43.551\n               ],\n               [\n                  93.565,\n                  -43.64\n               ],\n               [\n                  93.715,\n                  -38.787\n               ],\n               [\n                  97.544,\n                  -38.898\n               ],\n               [\n                  97.616,\n                  -36.639\n               ],\n               [\n                  103.06,\n                  -36.806\n               ],\n               [\n                  102.916,\n                  -41.637\n               ],\n               [\n                  101.347,\n                  -41.592\n               ],\n               [\n                  101.153,\n                  -48.149\n               ],\n               [\n                  90.775,\n                  -47.837\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330004,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"12\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -11.946,\n            \"maxY\" : -34.724,\n            \"minX\" : -32.736,\n            \"minY\" : -51.099\n         },\n         \"center\" : [\n            -22.341,\n            -42.9115\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -24.988,\n                  -36.961\n               ],\n               [\n                  -14.239,\n                  -34.724\n               ],\n               [\n                  -11.946,\n                  -45.744\n               ],\n               [\n                  -19.014,\n                  -47.214\n               ],\n               [\n                  -18.76,\n                  -48.449\n               ],\n               [\n                  -31.465,\n                  -51.099\n               ],\n               [\n                  -32.736,\n                  -44.987\n               ],\n               [\n                  -31.642,\n                  -44.765\n               ],\n               [\n                  -32.637,\n                  -39.989\n               ],\n               [\n                  -24.707,\n                  -38.331\n               ],\n               [\n                  -24.988,\n                  -36.961\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330010,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"9\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -36.776,\n            \"maxY\" : -43.217,\n            \"minX\" : -72.781,\n            \"minY\" : -84.282\n         },\n         \"center\" : [\n            -54.7785,\n            -63.7495\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -72.781,\n                  -48.483\n               ],\n               [\n                  -64.193,\n                  -43.217\n               ],\n               [\n                  -52.742,\n                  -61.952\n               ],\n               [\n                  -50.698,\n                  -60.694\n               ],\n               [\n                  -45.005,\n                  -70.012\n               ],\n               [\n                  -43.099,\n                  -68.843\n               ],\n               [\n                  -36.776,\n                  -79.195\n               ],\n               [\n                  -45.077,\n                  -84.282\n               ],\n               [\n                  -50.836,\n                  -74.854\n               ],\n               [\n                  -52.787,\n                  -76.045\n               ],\n               [\n                  -58.705,\n                  -66.349\n               ],\n               [\n                  -61.015,\n                  -67.763\n               ],\n               [\n                  -72.781,\n                  -48.483\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330024,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"9\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 36.35,\n            \"maxY\" : 20.513,\n            \"minX\" : 13.443,\n            \"minY\" : -2.987\n         },\n         \"center\" : [\n            24.8965,\n            8.763\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  13.443,\n                  19.845\n               ],\n               [\n                  18.682,\n                  20.513\n               ],\n               [\n                  19.069,\n                  17.44\n               ],\n               [\n                  21.544,\n                  17.752\n               ],\n               [\n                  21.672,\n                  16.739\n               ],\n               [\n                  21.837,\n                  15.425\n               ],\n               [\n                  23.661,\n                  15.648\n               ],\n               [\n                  24.053,\n                  12.565\n               ],\n               [\n                  26.308,\n                  12.843\n               ],\n               [\n                  26.507,\n                  11.24\n               ],\n               [\n                  26.684,\n                  9.848\n               ],\n               [\n                  28.646,\n                  10.093\n               ],\n               [\n                  29.033,\n                  7.021\n               ],\n               [\n                  31.144,\n                  7.288\n               ],\n               [\n                  31.326,\n                  5.885\n               ],\n               [\n                  31.509,\n                  4.427\n               ],\n               [\n                  33.432,\n                  4.672\n               ],\n               [\n                  33.83,\n                  1.566\n               ],\n               [\n                  35.869,\n                  1.822\n               ],\n               [\n                  36.112,\n                  -0.115\n               ],\n               [\n                  36.35,\n                  -1.94\n               ],\n               [\n                  28.204,\n                  -2.987\n               ],\n               [\n                  27.48,\n                  2.69\n               ],\n               [\n                  23.727,\n                  2.212\n               ],\n               [\n                  23.009,\n                  7.833\n               ],\n               [\n                  19.058,\n                  7.332\n               ],\n               [\n                  18.284,\n                  13.422\n               ],\n               [\n                  14.321,\n                  12.921\n               ],\n               [\n                  13.443,\n                  19.845\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330040,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"4\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"C\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -60.159,\n            \"maxY\" : -6.994,\n            \"minX\" : -76.114,\n            \"minY\" : -35.37\n         },\n         \"center\" : [\n            -68.1365,\n            -21.182\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -76.114,\n                  -9.443\n               ],\n               [\n                  -68.653,\n                  -6.994\n               ],\n               [\n                  -67.664,\n                  -10.011\n               ],\n               [\n                  -65.519,\n                  -16.557\n               ],\n               [\n                  -63.408,\n                  -23.013\n               ],\n               [\n                  -61.353,\n                  -29.28\n               ],\n               [\n                  -60.159,\n                  -32.92\n               ],\n               [\n                  -67.62,\n                  -35.37\n               ],\n               [\n                  -76.114,\n                  -9.443\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330049,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"7\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"B\",\n            \"building\" : \"terrace\",\n            \"building:levels\" : \"2\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 9.812,\n            \"maxY\" : -67.941,\n            \"minX\" : -3.695,\n            \"minY\" : -100.412\n         },\n         \"center\" : [\n            3.0585,\n            -84.1765\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -3.695,\n                  -69.377\n               ],\n               [\n                  4.114,\n                  -67.941\n               ],\n               [\n                  9.812,\n                  -98.976\n               ],\n               [\n                  2.003,\n                  -100.412\n               ],\n               [\n                  -3.695,\n                  -69.377\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330059,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"9\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 4.296,\n            \"maxY\" : -26.219,\n            \"minX\" : -4.612,\n            \"minY\" : -50.13\n         },\n         \"center\" : [\n            -0.158,\n            -38.1745\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -4.612,\n                  -27.054\n               ],\n               [\n                  0.66,\n                  -26.219\n               ],\n               [\n                  4.296,\n                  -49.295\n               ],\n               [\n                  -0.976,\n                  -50.13\n               ],\n               [\n                  -4.612,\n                  -27.054\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330061,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 61.628,\n            \"maxY\" : -77.236,\n            \"minX\" : 48.762,\n            \"minY\" : -88.59\n         },\n         \"center\" : [\n            55.195,\n            -82.913\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  48.762,\n                  -85.017\n               ],\n               [\n                  51.503,\n                  -77.236\n               ],\n               [\n                  61.628,\n                  -80.821\n               ],\n               [\n                  58.881,\n                  -88.59\n               ],\n               [\n                  48.762,\n                  -85.017\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330064,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"13\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 101.065,\n            \"maxY\" : -21.31,\n            \"minX\" : 92.836,\n            \"minY\" : -25.985\n         },\n         \"center\" : [\n            96.9505,\n            -23.6475\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  100.816,\n                  -21.31\n               ],\n               [\n                  101.065,\n                  -25.496\n               ],\n               [\n                  92.952,\n                  -25.985\n               ],\n               [\n                  92.836,\n                  -21.8\n               ],\n               [\n                  100.816,\n                  -21.31\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330096,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 15.394,\n            \"maxY\" : -16.212,\n            \"minX\" : 11.26,\n            \"minY\" : -23.225\n         },\n         \"center\" : [\n            13.327,\n            -19.7185\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  11.752,\n                  -23.225\n               ],\n               [\n                  11.26,\n                  -16.479\n               ],\n               [\n                  14.902,\n                  -16.212\n               ],\n               [\n                  15.394,\n                  -22.957\n               ],\n               [\n                  11.752,\n                  -23.225\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330100,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 18.953,\n            \"maxY\" : -30.772,\n            \"minX\" : 13.852,\n            \"minY\" : -38.442\n         },\n         \"center\" : [\n            16.4025,\n            -34.607\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  15.128,\n                  -38.442\n               ],\n               [\n                  13.852,\n                  -31.473\n               ],\n               [\n                  17.676,\n                  -30.772\n               ],\n               [\n                  18.953,\n                  -37.741\n               ],\n               [\n                  15.128,\n                  -38.442\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330146,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -35.013,\n            \"maxY\" : -1.684,\n            \"minX\" : -47.768,\n            \"minY\" : -29.792\n         },\n         \"center\" : [\n            -41.3905,\n            -15.738\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -47.768,\n                  -3.154\n               ],\n               [\n                  -40.07,\n                  -1.684\n               ],\n               [\n                  -39.385,\n                  -5.313\n               ],\n               [\n                  -38.169,\n                  -11.714\n               ],\n               [\n                  -36.848,\n                  -18.649\n               ],\n               [\n                  -35.599,\n                  -25.24\n               ],\n               [\n                  -35.013,\n                  -28.323\n               ],\n               [\n                  -42.712,\n                  -29.792\n               ],\n               [\n                  -47.768,\n                  -3.154\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330155,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"7\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"C\",\n            \"building\" : \"terrace\",\n            \"building:levels\" : \"2\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 110.371,\n            \"maxY\" : -51.577,\n            \"minX\" : 99.031,\n            \"minY\" : -60.516\n         },\n         \"center\" : [\n            104.701,\n            -56.0465\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  99.031,\n                  -59.949\n               ],\n               [\n                  99.462,\n                  -51.577\n               ],\n               [\n                  110.371,\n                  -52.134\n               ],\n               [\n                  109.935,\n                  -60.516\n               ],\n               [\n                  99.031,\n                  -59.949\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330161,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"12\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 40.108,\n            \"maxY\" : -9.889,\n            \"minX\" : 23.573,\n            \"minY\" : -37.729\n         },\n         \"center\" : [\n            31.8405,\n            -23.809\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  28.602,\n                  -37.729\n               ],\n               [\n                  23.573,\n                  -12.16\n               ],\n               [\n                  35.084,\n                  -9.889\n               ],\n               [\n                  40.108,\n                  -35.459\n               ],\n               [\n                  28.602,\n                  -37.729\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330163,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"6\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -13.305,\n            \"maxY\" : -55.629,\n            \"minX\" : -26.177,\n            \"minY\" : -74.698\n         },\n         \"center\" : [\n            -19.741,\n            -65.1635\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -26.177,\n                  -57.41\n               ],\n               [\n                  -16.472,\n                  -55.629\n               ],\n               [\n                  -13.305,\n                  -72.906\n               ],\n               [\n                  -23.01,\n                  -74.698\n               ],\n               [\n                  -26.177,\n                  -57.41\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330178,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"9\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 56.472,\n            \"maxY\" : -56.342,\n            \"minX\" : 45.971,\n            \"minY\" : -66.672\n         },\n         \"center\" : [\n            51.2215,\n            -61.507\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  45.971,\n                  -65.058\n               ],\n               [\n                  47.541,\n                  -56.342\n               ],\n               [\n                  56.472,\n                  -57.967\n               ],\n               [\n                  54.902,\n                  -66.672\n               ],\n               [\n                  45.971,\n                  -65.058\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330204,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"13\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -73.925,\n            \"maxY\" : -116.019,\n            \"minX\" : -85.973,\n            \"minY\" : -134.041\n         },\n         \"center\" : [\n            -79.949,\n            -125.03\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -76.821,\n                  -116.019\n               ],\n               [\n                  -73.925,\n                  -132.249\n               ],\n               [\n                  -83.945,\n                  -134.041\n               ],\n               [\n                  -85.57,\n                  -124.935\n               ],\n               [\n                  -84.707,\n                  -124.779\n               ],\n               [\n                  -85.973,\n                  -117.655\n               ],\n               [\n                  -76.821,\n                  -116.019\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330226,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"9\",\n            \"addr:street\" : \"Tuokkostie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 35.908,\n            \"maxY\" : -58.59,\n            \"minX\" : 20.804,\n            \"minY\" : -72.95\n         },\n         \"center\" : [\n            28.356,\n            -65.77\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  20.804,\n                  -60.962\n               ],\n               [\n                  33.576,\n                  -58.59\n               ],\n               [\n                  35.908,\n                  -71.192\n               ],\n               [\n                  26.419,\n                  -72.95\n               ],\n               [\n                  25.999,\n                  -70.668\n               ],\n               [\n                  22.716,\n                  -71.281\n               ],\n               [\n                  20.804,\n                  -60.962\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330237,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"11a\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 42.197,\n            \"maxY\" : -82.869,\n            \"minX\" : 21.053,\n            \"minY\" : -100.913\n         },\n         \"center\" : [\n            31.625,\n            -91.891\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  21.053,\n                  -86.286\n               ],\n               [\n                  39.102,\n                  -82.869\n               ],\n               [\n                  42.197,\n                  -99.199\n               ],\n               [\n                  33.128,\n                  -100.913\n               ],\n               [\n                  32.465,\n                  -97.407\n               ],\n               [\n                  23.484,\n                  -99.11\n               ],\n               [\n                  21.053,\n                  -86.286\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330239,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"11b\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -8.619,\n            \"maxY\" : 19.477,\n            \"minX\" : -20.484,\n            \"minY\" : -24.972\n         },\n         \"center\" : [\n            -14.5515,\n            -2.7475\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -16.273,\n                  -24.972\n               ],\n               [\n                  -19.639,\n                  -6.672\n               ],\n               [\n                  -16.047,\n                  -6.004\n               ],\n               [\n                  -20.484,\n                  18.119\n               ],\n               [\n                  -13.134,\n                  19.477\n               ],\n               [\n                  -12.438,\n                  15.704\n               ],\n               [\n                  -11.321,\n                  9.626\n               ],\n               [\n                  -10.183,\n                  3.447\n               ],\n               [\n                  -9.067,\n                  -2.608\n               ],\n               [\n                  -8.619,\n                  -5.057\n               ],\n               [\n                  -12.344,\n                  -5.748\n               ],\n               [\n                  -11.636,\n                  -9.588\n               ],\n               [\n                  -10.498,\n                  -15.766\n               ],\n               [\n                  -9.382,\n                  -21.855\n               ],\n               [\n                  -9.05,\n                  -23.637\n               ],\n               [\n                  -16.273,\n                  -24.972\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330273,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"7\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"D\",\n            \"building\" : \"residential\",\n            \"building:levels\" : \"2\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 64.026,\n            \"maxY\" : -11.959,\n            \"minX\" : 46.259,\n            \"minY\" : -25.93\n         },\n         \"center\" : [\n            55.1425,\n            -18.9445\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  46.994,\n                  -11.959\n               ],\n               [\n                  64.026,\n                  -14.442\n               ],\n               [\n                  63.026,\n                  -21.332\n               ],\n               [\n                  61.805,\n                  -21.154\n               ],\n               [\n                  61.114,\n                  -25.93\n               ],\n               [\n                  46.469,\n                  -23.792\n               ],\n               [\n                  47.425,\n                  -17.225\n               ],\n               [\n                  46.259,\n                  -17.058\n               ],\n               [\n                  46.994,\n                  -11.959\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330288,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"8\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 89.476,\n            \"maxY\" : -70.613,\n            \"minX\" : 72.438,\n            \"minY\" : -82.924\n         },\n         \"center\" : [\n            80.957,\n            -76.7685\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  72.858,\n                  -70.613\n               ],\n               [\n                  89.476,\n                  -71.381\n               ],\n               [\n                  89.05,\n                  -80.564\n               ],\n               [\n                  84.894,\n                  -80.375\n               ],\n               [\n                  84.778,\n                  -82.924\n               ],\n               [\n                  79.882,\n                  -82.702\n               ],\n               [\n                  80.003,\n                  -80.075\n               ],\n               [\n                  72.438,\n                  -79.718\n               ],\n               [\n                  72.858,\n                  -70.613\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 190330316,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"14\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"residential\",\n            \"source\" : \"NLS.fi\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 16.012,\n            \"maxY\" : 138.837,\n            \"minX\" : 6.203,\n            \"minY\" : 112.687\n         },\n         \"center\" : [\n            11.1075,\n            125.762\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  6.203,\n                  138.815\n               ],\n               [\n                  15.935,\n                  138.837\n               ],\n               [\n                  15.941,\n                  136.532\n               ],\n               [\n                  15.974,\n                  126.157\n               ],\n               [\n                  16.007,\n                  116.249\n               ],\n               [\n                  16.012,\n                  112.709\n               ],\n               [\n                  6.28,\n                  112.687\n               ],\n               [\n                  6.203,\n                  138.815\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 288600948,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:city\" : \"Espoo\",\n            \"addr:housenumber\" : \"2\",\n            \"addr:postcode\" : \"02720\",\n            \"addr:street\" : \"Nuuniitynkuja\",\n            \"addr:unit\" : \"A\",\n            \"building\" : \"residential\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -10.012,\n            \"maxY\" : 147.787,\n            \"minX\" : -33.4,\n            \"minY\" : 95.12\n         },\n         \"center\" : [\n            -21.706,\n            121.4535\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -33.4,\n                  147.52\n               ],\n               [\n                  -23.695,\n                  147.787\n               ],\n               [\n                  -23.585,\n                  143.924\n               ],\n               [\n                  -23.33,\n                  134.774\n               ],\n               [\n                  -23.231,\n                  131.167\n               ],\n               [\n                  -17.478,\n                  131.334\n               ],\n               [\n                  -17.351,\n                  126.836\n               ],\n               [\n                  -17.08,\n                  117.162\n               ],\n               [\n                  -16.975,\n                  113.399\n               ],\n               [\n                  -10.515,\n                  113.578\n               ],\n               [\n                  -10.41,\n                  109.759\n               ],\n               [\n                  -10.139,\n                  100.018\n               ],\n               [\n                  -10.012,\n                  95.399\n               ],\n               [\n                  -19.954,\n                  95.12\n               ],\n               [\n                  -20.446,\n                  112.854\n               ],\n               [\n                  -26.895,\n                  112.676\n               ],\n               [\n                  -27.37,\n                  129.975\n               ],\n               [\n                  -32.913,\n                  129.82\n               ],\n               [\n                  -33.4,\n                  147.52\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 288601098,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:city\" : \"Espoo\",\n            \"addr:housenumber\" : \"2\",\n            \"addr:postcode\" : \"02720\",\n            \"addr:street\" : \"Nuuniitynkuja\",\n            \"addr:unit\" : \"C\",\n            \"building\" : \"residential\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 26.192,\n            \"maxY\" : 97.224,\n            \"minX\" : -2.717,\n            \"minY\" : 89.966\n         },\n         \"center\" : [\n            11.7375,\n            93.595\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -2.717,\n                  96.49\n               ],\n               [\n                  10.624,\n                  97.224\n               ],\n               [\n                  10.923,\n                  96.044\n               ],\n               [\n                  25.894,\n                  96.779\n               ],\n               [\n                  26.192,\n                  90.556\n               ],\n               [\n                  11.514,\n                  89.966\n               ],\n               [\n                  11.221,\n                  91.447\n               ],\n               [\n                  -2.418,\n                  90.556\n               ],\n               [\n                  -2.717,\n                  96.49\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 288601751,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"building\" : \"garages\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -55.124,\n            \"maxY\" : 137.401,\n            \"minX\" : -77.6,\n            \"minY\" : 108.368\n         },\n         \"center\" : [\n            -66.362,\n            122.8845\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -61.424,\n                  137.401\n               ],\n               [\n                  -77.6,\n                  112.453\n               ],\n               [\n                  -71.306,\n                  108.368\n               ],\n               [\n                  -69.023,\n                  111.885\n               ],\n               [\n                  -64.95,\n                  118.164\n               ],\n               [\n                  -61.038,\n                  124.198\n               ],\n               [\n                  -56.992,\n                  130.432\n               ],\n               [\n                  -55.124,\n                  133.315\n               ],\n               [\n                  -61.424,\n                  137.401\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 288603132,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:city\" : \"Espoo\",\n            \"addr:housenumber\" : \"5\",\n            \"addr:postcode\" : \"02720\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"C\",\n            \"building\" : \"residential\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -70.654,\n            \"maxY\" : 106.486,\n            \"minX\" : -96.562,\n            \"minY\" : 70.763\n         },\n         \"center\" : [\n            -83.608,\n            88.6245\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -76.76,\n                  106.486\n               ],\n               [\n                  -96.562,\n                  74.548\n               ],\n               [\n                  -90.455,\n                  70.763\n               ],\n               [\n                  -88.112,\n                  74.537\n               ],\n               [\n                  -84.337,\n                  80.626\n               ],\n               [\n                  -80.623,\n                  86.615\n               ],\n               [\n                  -76.722,\n                  92.916\n               ],\n               [\n                  -72.859,\n                  99.139\n               ],\n               [\n                  -70.654,\n                  102.701\n               ],\n               [\n                  -76.76,\n                  106.486\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 288603135,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:city\" : \"Espoo\",\n            \"addr:housenumber\" : \"5\",\n            \"addr:postcode\" : \"02720\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"B\",\n            \"building\" : \"residential\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -34.787,\n            \"maxY\" : 104.516,\n            \"minX\" : -49.084,\n            \"minY\" : 66.722\n         },\n         \"center\" : [\n            -41.9355,\n            85.619\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -49.084,\n                  104.071\n               ],\n               [\n                  -41.645,\n                  104.516\n               ],\n               [\n                  -41.424,\n                  100.775\n               ],\n               [\n                  -41.054,\n                  94.508\n               ],\n               [\n                  -40.684,\n                  88.252\n               ],\n               [\n                  -40.545,\n                  85.958\n               ],\n               [\n                  -35.914,\n                  86.237\n               ],\n               [\n                  -35.688,\n                  82.363\n               ],\n               [\n                  -35.323,\n                  76.251\n               ],\n               [\n                  -34.98,\n                  70.451\n               ],\n               [\n                  -34.787,\n                  67.212\n               ],\n               [\n                  -42.911,\n                  66.722\n               ],\n               [\n                  -44.005,\n                  85.101\n               ],\n               [\n                  -47.945,\n                  84.867\n               ],\n               [\n                  -49.084,\n                  104.071\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 288603349,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:city\" : \"Espoo\",\n            \"addr:housenumber\" : \"5\",\n            \"addr:postcode\" : \"02720\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"D\",\n            \"building\" : \"residential\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -57.009,\n            \"maxY\" : 64.718,\n            \"minX\" : -86.299,\n            \"minY\" : 42.432\n         },\n         \"center\" : [\n            -71.654,\n            53.575\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -81.966,\n                  64.718\n               ],\n               [\n                  -79.247,\n                  63.071\n               ],\n               [\n                  -72.82,\n                  59.163\n               ],\n               [\n                  -66.851,\n                  55.545\n               ],\n               [\n                  -60.474,\n                  51.671\n               ],\n               [\n                  -57.009,\n                  49.567\n               ],\n               [\n                  -61.341,\n                  42.432\n               ],\n               [\n                  -86.299,\n                  57.583\n               ],\n               [\n                  -81.966,\n                  64.718\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 288603350,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:city\" : \"Espoo\",\n            \"addr:housenumber\" : \"5\",\n            \"addr:postcode\" : \"02720\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"A\",\n            \"building\" : \"residential\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 44.971,\n            \"maxY\" : 74.748,\n            \"minX\" : 11.099,\n            \"minY\" : 43.233\n         },\n         \"center\" : [\n            28.035,\n            58.9905\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  11.099,\n                  69.193\n               ],\n               [\n                  17.626,\n                  74.748\n               ],\n               [\n                  19.24,\n                  72.845\n               ],\n               [\n                  22.893,\n                  68.548\n               ],\n               [\n                  27.076,\n                  63.627\n               ],\n               [\n                  29.508,\n                  60.777\n               ],\n               [\n                  32.531,\n                  63.349\n               ],\n               [\n                  34.543,\n                  60.978\n               ],\n               [\n                  38.306,\n                  56.558\n               ],\n               [\n                  42.401,\n                  51.738\n               ],\n               [\n                  44.971,\n                  48.721\n               ],\n               [\n                  38.527,\n                  43.233\n               ],\n               [\n                  26.662,\n                  57.193\n               ],\n               [\n                  23.556,\n                  54.555\n               ],\n               [\n                  11.099,\n                  69.193\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 288603510,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:city\" : \"Espoo\",\n            \"addr:housenumber\" : \"4\",\n            \"addr:postcode\" : \"02720\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"A\",\n            \"building\" : \"residential\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 58.903,\n            \"maxY\" : 43.022,\n            \"minX\" : 43.208,\n            \"minY\" : 10.861\n         },\n         \"center\" : [\n            51.0555,\n            26.9415\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  50.951,\n                  43.022\n               ],\n               [\n                  51.52,\n                  40.873\n               ],\n               [\n                  53.15,\n                  34.706\n               ],\n               [\n                  54.703,\n                  28.828\n               ],\n               [\n                  56.367,\n                  22.517\n               ],\n               [\n                  57.909,\n                  16.694\n               ],\n               [\n                  58.903,\n                  12.91\n               ],\n               [\n                  51.161,\n                  10.861\n               ],\n               [\n                  43.208,\n                  40.973\n               ],\n               [\n                  50.951,\n                  43.022\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 288603511,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:city\" : \"Espoo\",\n            \"addr:housenumber\" : \"4\",\n            \"addr:postcode\" : \"02720\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"B\",\n            \"building\" : \"residential\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 62.352,\n            \"maxY\" : -92.431,\n            \"minX\" : 59.589,\n            \"minY\" : -95.57\n         },\n         \"center\" : [\n            60.9705,\n            -94.0005\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  60.13,\n                  -95.57\n               ],\n               [\n                  59.589,\n                  -92.876\n               ],\n               [\n                  61.816,\n                  -92.431\n               ],\n               [\n                  62.352,\n                  -95.114\n               ],\n               [\n                  60.13,\n                  -95.57\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 322065439,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"13\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"yes\",\n            \"building:fi:id\" : \"04906100540002003\",\n            \"building:levels\" : \"1\",\n            \"start_date\" : \"2006-11-07\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 117.34,\n            \"maxY\" : 90.634,\n            \"minX\" : 83.64,\n            \"minY\" : 32.068\n         },\n         \"center\" : [\n            100.49,\n            61.351\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  98.799,\n                  42.966\n               ],\n               [\n                  98.053,\n                  59.063\n               ],\n               [\n                  83.64,\n                  81.784\n               ],\n               [\n                  92.532,\n                  88.686\n               ],\n               [\n                  95.461,\n                  90.634\n               ],\n               [\n                  117.086,\n                  64.941\n               ],\n               [\n                  117.34,\n                  32.068\n               ],\n               [\n                  98.937,\n                  37.01\n               ],\n               [\n                  98.799,\n                  42.966\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 322065714,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"2\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"kindergarten\",\n            \"building:fi:id\" : \"04906100540009001\",\n            \"building:levels\" : \"2\",\n            \"building:use\" : \"kindergarten\",\n            \"start_date\" : \"2016-12-20\",\n            \"website\" : \"https://www.espoo.fi/fi-FI/Kasvatus_ja_opetus/Varhaiskasvatus/Varhaiskasvatuksen_toimipaikat/Kunnalliset_paivakodit/Leppavaara/Nuumaen_paivakoti\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 22.771,\n            \"maxY\" : 43.256,\n            \"minX\" : 16.504,\n            \"minY\" : 30.71\n         },\n         \"center\" : [\n            19.6375,\n            36.983\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  18.56,\n                  30.71\n               ],\n               [\n                  16.504,\n                  42.476\n               ],\n               [\n                  20.566,\n                  43.256\n               ],\n               [\n                  22.771,\n                  31.467\n               ],\n               [\n                  18.56,\n                  30.71\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 322066294,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"4\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"warehouse\",\n            \"building:fi:id\" : \"04906100540008004\",\n            \"building:levels\" : \"1\",\n            \"building:use\" : \"storage\",\n            \"start_date\" : \"1979\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -17.008,\n            \"maxY\" : 62.625,\n            \"minX\" : -25.32,\n            \"minY\" : 56.959\n         },\n         \"center\" : [\n            -21.164,\n            59.792\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -24.944,\n                  56.959\n               ],\n               [\n                  -25.32,\n                  62.024\n               ],\n               [\n                  -17.434,\n                  62.625\n               ],\n               [\n                  -17.008,\n                  57.527\n               ],\n               [\n                  -24.944,\n                  56.959\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 322066437,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"5\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"yes\",\n            \"building:fi:id\" : \"04906100530001005\",\n            \"building:levels\" : \"2\",\n            \"start_date\" : \"1978\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -14.477,\n            \"maxY\" : 79.825,\n            \"minX\" : -18.545,\n            \"minY\" : 77.22\n         },\n         \"center\" : [\n            -16.511,\n            78.5225\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -18.428,\n                  77.22\n               ],\n               [\n                  -18.545,\n                  79.557\n               ],\n               [\n                  -14.643,\n                  79.825\n               ],\n               [\n                  -14.477,\n                  77.509\n               ],\n               [\n                  -18.428,\n                  77.22\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 322066438,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"5\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"yes\",\n            \"building:fi:id\" : \"04906199070001001\",\n            \"building:levels\" : \"1\",\n            \"building:use\" : \"utility\",\n            \"power\" : \"substation\",\n            \"ref\" : \"717\",\n            \"start_date\" : \"1986\",\n            \"substation\" : \"minor_distribution\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -17.395,\n            \"maxY\" : 49.066,\n            \"minX\" : -49.283,\n            \"minY\" : 41.575\n         },\n         \"center\" : [\n            -33.339,\n            45.3205\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -48.879,\n                  41.575\n               ],\n               [\n                  -49.283,\n                  46.551\n               ],\n               [\n                  -17.76,\n                  49.066\n               ],\n               [\n                  -17.395,\n                  44.09\n               ],\n               [\n                  -48.879,\n                  41.575\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 322066439,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"5\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"yes\",\n            \"building:fi:id\" : \"04906100530001006\",\n            \"building:levels\" : \"1\",\n            \"start_date\" : \"1979\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -50.731,\n            \"maxY\" : 32.38,\n            \"minX\" : -77.727,\n            \"minY\" : 10.839\n         },\n         \"center\" : [\n            -64.229,\n            21.6095\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -54.936,\n                  10.839\n               ],\n               [\n                  -77.727,\n                  25.979\n               ],\n               [\n                  -73.489,\n                  32.38\n               ],\n               [\n                  -72.079,\n                  31.433\n               ],\n               [\n                  -70.819,\n                  30.598\n               ],\n               [\n                  -69.421,\n                  29.663\n               ],\n               [\n                  -66.431,\n                  27.671\n               ],\n               [\n                  -65.116,\n                  26.791\n               ],\n               [\n                  -63.773,\n                  25.901\n               ],\n               [\n                  -60.783,\n                  23.919\n               ],\n               [\n                  -59.672,\n                  23.184\n               ],\n               [\n                  -58.125,\n                  22.149\n               ],\n               [\n                  -55.13,\n                  20.157\n               ],\n               [\n                  -53.737,\n                  19.233\n               ],\n               [\n                  -52.472,\n                  18.387\n               ],\n               [\n                  -50.731,\n                  17.218\n               ],\n               [\n                  -54.936,\n                  10.839\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 322066644,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"7\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"addr:unit\" : \"A\",\n            \"building\" : \"terrace\",\n            \"building:fi:id\" : \"04906100550002001\",\n            \"building:levels\" : \"2\",\n            \"building:use\" : \"residential\",\n            \"start_date\" : \"1978\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -28.675,\n            \"maxY\" : 19.377,\n            \"minX\" : -41.396,\n            \"minY\" : 13.232\n         },\n         \"center\" : [\n            -35.0355,\n            16.3045\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -40.993,\n                  13.232\n               ],\n               [\n                  -41.396,\n                  18.42\n               ],\n               [\n                  -29.078,\n                  19.377\n               ],\n               [\n                  -28.675,\n                  14.201\n               ],\n               [\n                  -40.993,\n                  13.232\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 322066645,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"addr:housenumber\" : \"7\",\n            \"addr:street\" : \"Nuumäentie\",\n            \"building\" : \"shed\",\n            \"building:fi:id\" : \"04906100550002005\",\n            \"building:levels\" : \"1\",\n            \"start_date\" : \"1978\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -26.282,\n            \"maxY\" : -118.746,\n            \"minX\" : -28.735,\n            \"minY\" : -121.451\n         },\n         \"center\" : [\n            -27.5085,\n            -120.0985\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  -28.409,\n                  -121.451\n               ],\n               [\n                  -28.735,\n                  -119.024\n               ],\n               [\n                  -26.608,\n                  -118.746\n               ],\n               [\n                  -26.282,\n                  -121.162\n               ],\n               [\n                  -28.409,\n                  -121.451\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 322066671,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"building\" : \"yes\",\n            \"building:fi:id\" : \"04906100570002003\",\n            \"building:levels\" : \"1\",\n            \"start_date\" : \"1988-07-04\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 89.918,\n            \"maxY\" : 44.079,\n            \"minX\" : 75.146,\n            \"minY\" : 40.996\n         },\n         \"center\" : [\n            82.532,\n            42.5375\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  89.863,\n                  44.079\n               ],\n               [\n                  89.918,\n                  41.307\n               ],\n               [\n                  75.206,\n                  40.996\n               ],\n               [\n                  75.146,\n                  43.768\n               ],\n               [\n                  89.863,\n                  44.079\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 1376096430,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"building\" : \"yes\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 84.706,\n            \"maxY\" : 69.327,\n            \"minX\" : 68.542,\n            \"minY\" : 57.449\n         },\n         \"center\" : [\n            76.624,\n            63.388\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  68.542,\n                  59.92\n               ],\n               [\n                  70.144,\n                  57.449\n               ],\n               [\n                  84.706,\n                  66.844\n               ],\n               [\n                  83.109,\n                  69.327\n               ],\n               [\n                  68.542,\n                  59.92\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 1376096431,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"building\" : \"yes\"\n         },\n         \"tmCategory\" : \"Building\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 118.899,\n            \"maxY\" : 81.439,\n            \"minX\" : 110.769,\n            \"minY\" : 71.242\n         },\n         \"center\" : [\n            114.834,\n            76.3405\n         ],\n         \"elementType\" : \"area\",\n         \"geometry\" : {\n            \"holes\" : [],\n            \"outer\" : [\n               [\n                  110.769,\n                  79.446\n               ],\n               [\n                  114.395,\n                  71.242\n               ],\n               [\n                  118.899,\n                  73.234\n               ],\n               [\n                  115.273,\n                  81.439\n               ],\n               [\n                  110.769,\n                  79.446\n               ]\n            ],\n            \"type\" : \"polygon\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"hasHoles\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 1376096432,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Building\",\n         \"representations\" : [\n            \"Building\"\n         ],\n         \"tags\" : {\n            \"building\" : \"yes\"\n         },\n         \"tmCategory\" : \"Building\"\n      }\n   ],\n   \"meta\" : {\n      \"boundary\" : {\n         \"maxX\" : 118.702,\n         \"maxY\" : 118.899,\n         \"minX\" : -118.702,\n         \"minY\" : -118.895\n      },\n      \"dataBoundary\" : {\n         \"maxX\" : 130.189,\n         \"maxY\" : 581.041,\n         \"minX\" : -1673.804,\n         \"minY\" : -1102.813\n      }\n   },\n   \"nodes\" : [\n      {\n         \"bounds\" : {\n            \"maxX\" : 125.558,\n            \"maxY\" : 25.099,\n            \"minX\" : 125.558,\n            \"minY\" : 25.099\n         },\n         \"center\" : [\n            125.558,\n            25.099\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               125.558,\n               25.099\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 268724899,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadConnector\",\n         \"representations\" : [\n            \"RoadConnector\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 115.124,\n            \"maxY\" : 98.315,\n            \"minX\" : 115.124,\n            \"minY\" : 98.315\n         },\n         \"center\" : [\n            115.124,\n            98.315\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               115.124,\n               98.315\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 268725041,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 18.306,\n            \"maxY\" : 194.544,\n            \"minX\" : 18.306,\n            \"minY\" : 194.544\n         },\n         \"center\" : [\n            18.306,\n            194.544\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               18.306,\n               194.544\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 276457277,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadCrossingAtConnector\",\n         \"representations\" : [\n            \"RoadCrossingAtConnector\"\n         ],\n         \"tags\" : {\n            \"crossing\" : \"uncontrolled\",\n            \"highway\" : \"crossing\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 71.697,\n            \"maxY\" : -46.835,\n            \"minX\" : 71.697,\n            \"minY\" : -46.835\n         },\n         \"center\" : [\n            71.697,\n            -46.835\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               71.697,\n               -46.835\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 276457280,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"turning_circle\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 106.597,\n            \"maxY\" : 112.776,\n            \"minX\" : 106.597,\n            \"minY\" : 112.776\n         },\n         \"center\" : [\n            106.597,\n            112.776\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               106.597,\n               112.776\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 1538138535,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 109.962,\n            \"maxY\" : 102.456,\n            \"minX\" : 109.962,\n            \"minY\" : 102.456\n         },\n         \"center\" : [\n            109.962,\n            102.456\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               109.962,\n               102.456\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 1538139031,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 14.321,\n            \"maxY\" : -45.166,\n            \"minX\" : 14.321,\n            \"minY\" : -45.166\n         },\n         \"center\" : [\n            14.321,\n            -45.166\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               14.321,\n               -45.166\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 1539655844,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 68.475,\n            \"maxY\" : 80.37,\n            \"minX\" : 68.475,\n            \"minY\" : 80.37\n         },\n         \"center\" : [\n            68.475,\n            80.37\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               68.475,\n               80.37\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 1539656211,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -6.613,\n            \"maxY\" : 109.704,\n            \"minX\" : -6.613,\n            \"minY\" : 109.704\n         },\n         \"center\" : [\n            -6.613,\n            109.704\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -6.613,\n               109.704\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 2921555993,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 126.42,\n            \"maxY\" : 102.212,\n            \"minX\" : 126.42,\n            \"minY\" : 102.212\n         },\n         \"center\" : [\n            126.42,\n            102.212\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               126.42,\n               102.212\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4254168966,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadCrossingAtConnector\",\n         \"representations\" : [\n            \"RoadCrossingAtConnector\"\n         ],\n         \"tags\" : {\n            \"crossing:island\" : \"yes\",\n            \"crossing:markings\" : \"yes\",\n            \"crossing:signals\" : \"no\",\n            \"highway\" : \"crossing\",\n            \"traffic_calming\" : \"table\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 111.808,\n            \"maxY\" : 125.712,\n            \"minX\" : 111.808,\n            \"minY\" : 125.712\n         },\n         \"center\" : [\n            111.808,\n            125.712\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               111.808,\n               125.712\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4254168969,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadCrossingAtConnector\",\n         \"representations\" : [\n            \"RoadCrossingAtConnector\"\n         ],\n         \"tags\" : {\n            \"crossing:island\" : \"yes\",\n            \"crossing:markings\" : \"yes\",\n            \"crossing:signals\" : \"no\",\n            \"highway\" : \"crossing\",\n            \"traffic_calming\" : \"table\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 34.123,\n            \"maxY\" : 101.644,\n            \"minX\" : 34.123,\n            \"minY\" : 101.644\n         },\n         \"center\" : [\n            34.123,\n            101.644\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               34.123,\n               101.644\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540138274,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 20.809,\n            \"maxY\" : 181.697,\n            \"minX\" : 20.809,\n            \"minY\" : 181.697\n         },\n         \"center\" : [\n            20.809,\n            181.697\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               20.809,\n               181.697\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540138286,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -3.645,\n            \"maxY\" : 31.712,\n            \"minX\" : -3.645,\n            \"minY\" : 31.712\n         },\n         \"center\" : [\n            -3.645,\n            31.712\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -3.645,\n               31.712\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152725,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -43.568,\n            \"maxY\" : 27.214,\n            \"minX\" : -43.568,\n            \"minY\" : 27.214\n         },\n         \"center\" : [\n            -43.568,\n            27.214\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -43.568,\n               27.214\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152727,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadConnector\",\n         \"representations\" : [\n            \"RoadConnector\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -6.817,\n            \"maxY\" : 48.476,\n            \"minX\" : -6.817,\n            \"minY\" : 48.476\n         },\n         \"center\" : [\n            -6.817,\n            48.476\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -6.817,\n               48.476\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152732,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 11.475,\n            \"maxY\" : 25.288,\n            \"minX\" : 11.475,\n            \"minY\" : 25.288\n         },\n         \"center\" : [\n            11.475,\n            25.288\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               11.475,\n               25.288\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152736,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 18.969,\n            \"maxY\" : 24.52,\n            \"minX\" : 18.969,\n            \"minY\" : 24.52\n         },\n         \"center\" : [\n            18.969,\n            24.52\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               18.969,\n               24.52\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152737,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 35.786,\n            \"maxY\" : 75.539,\n            \"minX\" : 35.786,\n            \"minY\" : 75.539\n         },\n         \"center\" : [\n            35.786,\n            75.539\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               35.786,\n               75.539\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152741,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 38.151,\n            \"maxY\" : 70.819,\n            \"minX\" : 38.151,\n            \"minY\" : 70.819\n         },\n         \"center\" : [\n            38.151,\n            70.819\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               38.151,\n               70.819\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152742,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 53.189,\n            \"maxY\" : 54.165,\n            \"minX\" : 53.189,\n            \"minY\" : 54.165\n         },\n         \"center\" : [\n            53.189,\n            54.165\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               53.189,\n               54.165\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152743,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -7.574,\n            \"maxY\" : 52.473,\n            \"minX\" : -7.574,\n            \"minY\" : 52.473\n         },\n         \"center\" : [\n            -7.574,\n            52.473\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -7.574,\n               52.473\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152754,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -3.822,\n            \"maxY\" : -52.323,\n            \"minX\" : -3.822,\n            \"minY\" : -52.323\n         },\n         \"center\" : [\n            -3.822,\n            -52.323\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -3.822,\n               -52.323\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152768,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -43.132,\n            \"maxY\" : -60.872,\n            \"minX\" : -43.132,\n            \"minY\" : -60.872\n         },\n         \"center\" : [\n            -43.132,\n            -60.872\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -43.132,\n               -60.872\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152780,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -4.33,\n            \"maxY\" : -50.208,\n            \"minX\" : -4.33,\n            \"minY\" : -50.208\n         },\n         \"center\" : [\n            -4.33,\n            -50.208\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -4.33,\n               -50.208\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152782,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 33.355,\n            \"maxY\" : -47.425,\n            \"minX\" : 33.355,\n            \"minY\" : -47.425\n         },\n         \"center\" : [\n            33.355,\n            -47.425\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               33.355,\n               -47.425\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152890,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 66.204,\n            \"maxY\" : -46.846,\n            \"minX\" : 66.204,\n            \"minY\" : -46.846\n         },\n         \"center\" : [\n            66.204,\n            -46.846\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               66.204,\n               -46.846\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152893,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 73.432,\n            \"maxY\" : -38.075,\n            \"minX\" : 73.432,\n            \"minY\" : -38.075\n         },\n         \"center\" : [\n            73.432,\n            -38.075\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               73.432,\n               -38.075\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 4540152903,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 31.592,\n            \"maxY\" : 88.608,\n            \"minX\" : 31.592,\n            \"minY\" : 88.608\n         },\n         \"center\" : [\n            31.592,\n            88.608\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               31.592,\n               88.608\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 5076856678,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -9.746,\n            \"maxY\" : 30.966,\n            \"minX\" : -9.746,\n            \"minY\" : 30.966\n         },\n         \"center\" : [\n            -9.746,\n            30.966\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -9.746,\n               30.966\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 5076867288,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -46.923,\n            \"maxY\" : 21.337,\n            \"minX\" : -46.923,\n            \"minY\" : 21.337\n         },\n         \"center\" : [\n            -46.923,\n            21.337\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -46.923,\n               21.337\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 5076867294,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -68.493,\n            \"maxY\" : 34.795,\n            \"minX\" : -68.493,\n            \"minY\" : 34.795\n         },\n         \"center\" : [\n            -68.493,\n            34.795\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -68.493,\n               34.795\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 5076867299,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadConnector\",\n         \"representations\" : [\n            \"RoadConnector\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -45.105,\n            \"maxY\" : 17.095,\n            \"minX\" : -45.105,\n            \"minY\" : 17.095\n         },\n         \"center\" : [\n            -45.105,\n            17.095\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -45.105,\n               17.095\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 5076867300,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -27.647,\n            \"maxY\" : 51.204,\n            \"minX\" : -27.647,\n            \"minY\" : 51.204\n         },\n         \"center\" : [\n            -27.647,\n            51.204\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -27.647,\n               51.204\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 5076867301,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 35.67,\n            \"maxY\" : 78.979,\n            \"minX\" : 35.67,\n            \"minY\" : 78.979\n         },\n         \"center\" : [\n            35.67,\n            78.979\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               35.67,\n               78.979\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 5113592090,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 93.665,\n            \"maxY\" : 97.113,\n            \"minX\" : 93.665,\n            \"minY\" : 97.113\n         },\n         \"center\" : [\n            93.665,\n            97.113\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               93.665,\n               97.113\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 6139123141,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 30.508,\n            \"maxY\" : 114.301,\n            \"minX\" : 30.508,\n            \"minY\" : 114.301\n         },\n         \"center\" : [\n            30.508,\n            114.301\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               30.508,\n               114.301\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 6139126877,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 49.315,\n            \"maxY\" : 50.814,\n            \"minX\" : 49.315,\n            \"minY\" : 50.814\n         },\n         \"center\" : [\n            49.315,\n            50.814\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               49.315,\n               50.814\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 7448817945,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 40.263,\n            \"maxY\" : 68.481,\n            \"minX\" : 40.263,\n            \"minY\" : 68.481\n         },\n         \"center\" : [\n            40.263,\n            68.481\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               40.263,\n               68.481\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 7448817965,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 42.147,\n            \"maxY\" : 66.388,\n            \"minX\" : 42.147,\n            \"minY\" : 66.388\n         },\n         \"center\" : [\n            42.147,\n            66.388\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               42.147,\n               66.388\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 7448817966,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 44.325,\n            \"maxY\" : 63.984,\n            \"minX\" : 44.325,\n            \"minY\" : 63.984\n         },\n         \"center\" : [\n            44.325,\n            63.984\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               44.325,\n               63.984\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 7448817969,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 47.353,\n            \"maxY\" : 60.633,\n            \"minX\" : 47.353,\n            \"minY\" : 60.633\n         },\n         \"center\" : [\n            47.353,\n            60.633\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               47.353,\n               60.633\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 7448817972,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -7.851,\n            \"maxY\" : 117.073,\n            \"minX\" : -7.851,\n            \"minY\" : 117.073\n         },\n         \"center\" : [\n            -7.851,\n            117.073\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -7.851,\n               117.073\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 7448840427,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -2.413,\n            \"maxY\" : 101.577,\n            \"minX\" : -2.413,\n            \"minY\" : 101.577\n         },\n         \"center\" : [\n            -2.413,\n            101.577\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -2.413,\n               101.577\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 7448840431,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -69.642,\n            \"maxY\" : 37.389,\n            \"minX\" : -69.642,\n            \"minY\" : 37.389\n         },\n         \"center\" : [\n            -69.642,\n            37.389\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -69.642,\n               37.389\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 11901878576,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -8.613,\n            \"maxY\" : 40.606,\n            \"minX\" : -8.613,\n            \"minY\" : 40.606\n         },\n         \"center\" : [\n            -8.613,\n            40.606\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               -8.613,\n               40.606\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 11901878578,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 47.049,\n            \"maxY\" : 85.124,\n            \"minX\" : 47.049,\n            \"minY\" : 85.124\n         },\n         \"center\" : [\n            47.049,\n            85.124\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               47.049,\n               85.124\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 12285606746,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 47.005,\n            \"maxY\" : 77.921,\n            \"minX\" : 47.005,\n            \"minY\" : 77.921\n         },\n         \"center\" : [\n            47.005,\n            77.921\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               47.005,\n               77.921\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 12285606749,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 123.171,\n            \"maxY\" : 72.143,\n            \"minX\" : 123.171,\n            \"minY\" : 72.143\n         },\n         \"center\" : [\n            123.171,\n            72.143\n         ],\n         \"elementType\" : \"node\",\n         \"geometry\" : {\n            \"coordinates\" : [\n               123.171,\n               72.143\n            ],\n            \"type\" : \"point\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"layer\" : 0,\n         \"osmId\" : 12741249986,\n         \"osmType\" : \"node\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"RoadJunction\",\n         \"representations\" : [\n            \"RoadJunction\"\n         ],\n         \"tags\" : {},\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      }\n   ],\n   \"ways\" : [\n      {\n         \"bounds\" : {\n            \"maxX\" : 127.747,\n            \"maxY\" : 378.246,\n            \"minX\" : 18.698,\n            \"minY\" : 25.099\n         },\n         \"center\" : [\n            73.2225,\n            201.6725\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  125.558,\n                  25.099\n               ],\n               [\n                  127.747,\n                  53.241\n               ],\n               [\n                  127.161,\n                  62.091\n               ],\n               [\n                  123.768,\n                  70.195\n               ],\n               [\n                  123.171,\n                  72.143\n               ],\n               [\n                  115.124,\n                  98.315\n               ],\n               [\n                  112.167,\n                  100.931\n               ],\n               [\n                  109.962,\n                  102.456\n               ],\n               [\n                  108.293,\n                  107.867\n               ],\n               [\n                  106.597,\n                  112.776\n               ],\n               [\n                  107.851,\n                  115.459\n               ],\n               [\n                  108.249,\n                  117.563\n               ],\n               [\n                  108.094,\n                  120.847\n               ],\n               [\n                  107.033,\n                  124.109\n               ],\n               [\n                  90.012,\n                  176.264\n               ],\n               [\n                  84.369,\n                  193.597\n               ],\n               [\n                  79.501,\n                  208.07\n               ],\n               [\n                  58.097,\n                  275.1\n               ],\n               [\n                  57.35,\n                  277.549\n               ],\n               [\n                  42.921,\n                  325.042\n               ],\n               [\n                  34.421,\n                  349.779\n               ],\n               [\n                  27.353,\n                  364.542\n               ],\n               [\n                  18.698,\n                  378.246\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 24724510,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"bicycle\" : \"designated\",\n            \"foot\" : \"designated\",\n            \"highway\" : \"cycleway\",\n            \"lit\" : \"yes\",\n            \"segregated\" : \"no\",\n            \"source\" : \"survey\",\n            \"surface\" : \"asphalt\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 106.597,\n            \"maxY\" : 112.776,\n            \"minX\" : -11.675,\n            \"minY\" : -50.208\n         },\n         \"center\" : [\n            47.461,\n            31.284\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  106.597,\n                  112.776\n               ],\n               [\n                  84.297,\n                  94.619\n               ],\n               [\n                  73.819,\n                  86.882\n               ],\n               [\n                  69.315,\n                  84.923\n               ],\n               [\n                  61.656,\n                  84.044\n               ],\n               [\n                  47.049,\n                  85.124\n               ],\n               [\n                  42.103,\n                  85.658\n               ],\n               [\n                  39.588,\n                  88.352\n               ],\n               [\n                  35.322,\n                  88.474\n               ],\n               [\n                  31.592,\n                  88.608\n               ],\n               [\n                  26.148,\n                  87.15\n               ],\n               [\n                  11.072,\n                  87.561\n               ],\n               [\n                  3.998,\n                  85.658\n               ],\n               [\n                  -2.827,\n                  80.86\n               ],\n               [\n                  -8.011,\n                  74.292\n               ],\n               [\n                  -11.299,\n                  65.698\n               ],\n               [\n                  -11.675,\n                  61.267\n               ],\n               [\n                  -10.465,\n                  52.195\n               ],\n               [\n                  -8.613,\n                  40.606\n               ],\n               [\n                  -7.121,\n                  31.289\n               ],\n               [\n                  -0.683,\n                  -4.267\n               ],\n               [\n                  7.236,\n                  -50.208\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 24724848,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"footway\" : \"sidewalk\",\n            \"highway\" : \"footway\",\n            \"source\" : \"survey\",\n            \"surface\" : \"asphalt\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 35.549,\n            \"maxY\" : 254.126,\n            \"minX\" : 2.931,\n            \"minY\" : 82.597\n         },\n         \"center\" : [\n            19.24,\n            168.3615\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  35.549,\n                  82.597\n               ],\n               [\n                  35.322,\n                  88.474\n               ],\n               [\n                  34.123,\n                  101.644\n               ],\n               [\n                  32.879,\n                  118.242\n               ],\n               [\n                  32.15,\n                  122.951\n               ],\n               [\n                  31.741,\n                  125.623\n               ],\n               [\n                  27.977,\n                  144.915\n               ],\n               [\n                  25.722,\n                  156.493\n               ],\n               [\n                  20.809,\n                  181.697\n               ],\n               [\n                  20.45,\n                  183.545\n               ],\n               [\n                  18.306,\n                  194.544\n               ],\n               [\n                  12.73,\n                  222.564\n               ],\n               [\n                  8.922,\n                  241.69\n               ],\n               [\n                  4.805,\n                  250.24\n               ],\n               [\n                  2.931,\n                  254.126\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 25365011,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"residential\",\n            \"maxspeed\" : \"30\",\n            \"name\" : \"Nuuniitynkuja\",\n            \"name:fi\" : \"Nuuniitynkuja\",\n            \"name:sv\" : \"Noängsgränden\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 32.293,\n            \"maxY\" : 254.315,\n            \"minX\" : -3.524,\n            \"minY\" : 88.608\n         },\n         \"center\" : [\n            14.3845,\n            171.4615\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  31.592,\n                  88.608\n               ],\n               [\n                  32.293,\n                  94.33\n               ],\n               [\n                  31.437,\n                  101.599\n               ],\n               [\n                  30.508,\n                  114.301\n               ],\n               [\n                  29.016,\n                  126.291\n               ],\n               [\n                  28.546,\n                  128.773\n               ],\n               [\n                  26.966,\n                  136.811\n               ],\n               [\n                  25.396,\n                  144.782\n               ],\n               [\n                  17.588,\n                  182.754\n               ],\n               [\n                  15.62,\n                  193.709\n               ],\n               [\n                  6.358,\n                  239.92\n               ],\n               [\n                  4.54,\n                  244.273\n               ],\n               [\n                  2.804,\n                  245.709\n               ],\n               [\n                  -0.318,\n                  247.813\n               ],\n               [\n                  -2.706,\n                  249.717\n               ],\n               [\n                  -3.524,\n                  254.315\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 25365012,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"footway\" : \"sidewalk\",\n            \"highway\" : \"footway\",\n            \"surface\" : \"paved\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 121.573,\n            \"maxY\" : 116.628,\n            \"minX\" : 105.254,\n            \"minY\" : 105.896\n         },\n         \"center\" : [\n            113.4135,\n            111.262\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  121.573,\n                  116.628\n               ],\n               [\n                  115.682,\n                  112.743\n               ],\n               [\n                  108.293,\n                  107.867\n               ],\n               [\n                  105.254,\n                  105.896\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 25365013,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"residential\",\n            \"lit\" : \"yes\",\n            \"maxspeed\" : \"30\",\n            \"name\" : \"Nuumäentie\",\n            \"name:fi\" : \"Nuumäentie\",\n            \"name:sv\" : \"Nobacksvägen\",\n            \"surface\" : \"asphalt\",\n            \"traffic_calming\" : \"table\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 110.913,\n            \"maxY\" : -60.828,\n            \"minX\" : -136.269,\n            \"minY\" : -120.817\n         },\n         \"center\" : [\n            -12.678,\n            -90.8225\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  110.913,\n                  -120.817\n               ],\n               [\n                  98.606,\n                  -117.544\n               ],\n               [\n                  89.299,\n                  -111.9\n               ],\n               [\n                  85.453,\n                  -110.03\n               ],\n               [\n                  81.158,\n                  -110.264\n               ],\n               [\n                  72.697,\n                  -112.412\n               ],\n               [\n                  69.022,\n                  -111.021\n               ],\n               [\n                  65.353,\n                  -107.837\n               ],\n               [\n                  62.363,\n                  -107.236\n               ],\n               [\n                  59.589,\n                  -109.306\n               ],\n               [\n                  57.71,\n                  -112.256\n               ],\n               [\n                  55.488,\n                  -118.568\n               ],\n               [\n                  52.2,\n                  -120.549\n               ],\n               [\n                  47.668,\n                  -120.694\n               ],\n               [\n                  34.642,\n                  -115.585\n               ],\n               [\n                  25.325,\n                  -119.058\n               ],\n               [\n                  17.798,\n                  -120.36\n               ],\n               [\n                  11.752,\n                  -118.902\n               ],\n               [\n                  3.788,\n                  -113.982\n               ],\n               [\n                  -2.291,\n                  -107.347\n               ],\n               [\n                  -10.222,\n                  -94.602\n               ],\n               [\n                  -16.483,\n                  -88.679\n               ],\n               [\n                  -21.335,\n                  -86.297\n               ],\n               [\n                  -25.922,\n                  -84.861\n               ],\n               [\n                  -30.609,\n                  -84.828\n               ],\n               [\n                  -46.763,\n                  -91.518\n               ],\n               [\n                  -51.438,\n                  -91.986\n               ],\n               [\n                  -57.02,\n                  -88.746\n               ],\n               [\n                  -62.607,\n                  -82.936\n               ],\n               [\n                  -74.417,\n                  -68.253\n               ],\n               [\n                  -78.457,\n                  -65.258\n               ],\n               [\n                  -85.255,\n                  -61.952\n               ],\n               [\n                  -92.179,\n                  -60.828\n               ],\n               [\n                  -100.414,\n                  -60.828\n               ],\n               [\n                  -106.465,\n                  -61.485\n               ],\n               [\n                  -116.186,\n                  -64.668\n               ],\n               [\n                  -136.269,\n                  -74.052\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 44507498,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Waterway\",\n         \"representations\" : [\n            \"Waterway\"\n         ],\n         \"tags\" : {\n            \"waterway\" : \"stream\"\n         },\n         \"tmCategory\" : \"Waterway\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 105.254,\n            \"maxY\" : 105.896,\n            \"minX\" : -8.398,\n            \"minY\" : -47.726\n         },\n         \"center\" : [\n            48.428,\n            29.085\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  105.254,\n                  105.896\n               ],\n               [\n                  101.59,\n                  103.692\n               ],\n               [\n                  93.665,\n                  97.113\n               ],\n               [\n                  80.622,\n                  86.292\n               ],\n               [\n                  74.344,\n                  82.363\n               ],\n               [\n                  68.475,\n                  80.37\n               ],\n               [\n                  61.7,\n                  80.326\n               ],\n               [\n                  47.033,\n                  81.595\n               ],\n               [\n                  35.549,\n                  82.597\n               ],\n               [\n                  17.576,\n                  83.944\n               ],\n               [\n                  11.26,\n                  83.654\n               ],\n               [\n                  5.33,\n                  82.263\n               ],\n               [\n                  0.312,\n                  79.713\n               ],\n               [\n                  -3.192,\n                  75.828\n               ],\n               [\n                  -6.69,\n                  71.097\n               ],\n               [\n                  -8.398,\n                  64.273\n               ],\n               [\n                  -8.287,\n                  56.247\n               ],\n               [\n                  -7.574,\n                  52.473\n               ],\n               [\n                  -6.817,\n                  48.476\n               ],\n               [\n                  -3.645,\n                  31.712\n               ],\n               [\n                  9.182,\n                  -36.06\n               ],\n               [\n                  10.497,\n                  -39.945\n               ],\n               [\n                  11.326,\n                  -41.926\n               ],\n               [\n                  14.321,\n                  -45.166\n               ],\n               [\n                  17.731,\n                  -47.058\n               ],\n               [\n                  20.29,\n                  -47.726\n               ],\n               [\n                  33.355,\n                  -47.425\n               ],\n               [\n                  57.306,\n                  -46.869\n               ],\n               [\n                  66.204,\n                  -46.846\n               ],\n               [\n                  71.697,\n                  -46.835\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 140383769,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"residential\",\n            \"maxspeed\" : \"30\",\n            \"name\" : \"Nuumäentie\",\n            \"name:fi\" : \"Nuumäentie\",\n            \"name:sv\" : \"Nobacksvägen\",\n            \"surface\" : \"asphalt\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 120.474,\n            \"maxY\" : 129.051,\n            \"minX\" : 110.808,\n            \"minY\" : 96.69\n         },\n         \"center\" : [\n            115.641,\n            112.8705\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  110.808,\n                  129.051\n               ],\n               [\n                  111.808,\n                  125.712\n               ],\n               [\n                  115.682,\n                  112.743\n               ],\n               [\n                  119.54,\n                  99.84\n               ],\n               [\n                  120.474,\n                  96.69\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 140383778,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"bicycle\" : \"use_sidepath\",\n            \"highway\" : \"secondary\",\n            \"lanes\" : \"1\",\n            \"lit\" : \"yes\",\n            \"maxspeed\" : \"30\",\n            \"name\" : \"Lähderannantie\",\n            \"name:fi\" : \"Lähderannantie\",\n            \"name:sv\" : \"Källstrandsvägen\",\n            \"oneway\" : \"yes\",\n            \"surface\" : \"asphalt\",\n            \"traffic_calming\" : \"table\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"car\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 20.71,\n            \"maxY\" : -113.57,\n            \"minX\" : 18.919,\n            \"minY\" : -123.432\n         },\n         \"center\" : [\n            19.8145,\n            -118.501\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  18.919,\n                  -113.57\n               ],\n               [\n                  20.71,\n                  -123.432\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ABOVE\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 1,\n         \"osmId\" : 140542965,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ABOVE\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"bridge\" : \"yes\",\n            \"foot\" : \"yes\",\n            \"highway\" : \"cycleway\",\n            \"layer\" : \"1\",\n            \"source\" : \"survey\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 130.189,\n            \"maxY\" : 103.503,\n            \"minX\" : 115.124,\n            \"minY\" : 98.315\n         },\n         \"center\" : [\n            122.6565,\n            100.909\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  115.124,\n                  98.315\n               ],\n               [\n                  119.54,\n                  99.84\n               ],\n               [\n                  126.42,\n                  102.212\n               ],\n               [\n                  130.189,\n                  103.503\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 426104083,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"bicycle\" : \"designated\",\n            \"cycleway\" : \"crossing\",\n            \"foot\" : \"designated\",\n            \"highway\" : \"cycleway\",\n            \"lit\" : \"yes\",\n            \"segregated\" : \"no\",\n            \"surface\" : \"asphalt\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 34.123,\n            \"maxY\" : 193.03,\n            \"minX\" : -20.44,\n            \"minY\" : 101.198\n         },\n         \"center\" : [\n            6.8415,\n            147.114\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  34.123,\n                  101.644\n               ],\n               [\n                  31.437,\n                  101.599\n               ],\n               [\n                  25.85,\n                  101.699\n               ],\n               [\n                  -2.219,\n                  101.198\n               ],\n               [\n                  -2.413,\n                  101.577\n               ],\n               [\n                  -6.613,\n                  109.704\n               ],\n               [\n                  -7.005,\n                  114.457\n               ],\n               [\n                  -7.851,\n                  117.073\n               ],\n               [\n                  -11.023,\n                  126.847\n               ],\n               [\n                  -11.239,\n                  127.515\n               ],\n               [\n                  -15.593,\n                  134.829\n               ],\n               [\n                  -16.389,\n                  136.165\n               ],\n               [\n                  -17.456,\n                  140.362\n               ],\n               [\n                  -18.406,\n                  144.102\n               ],\n               [\n                  -19.335,\n                  147.765\n               ],\n               [\n                  -20.44,\n                  168.371\n               ],\n               [\n                  -20.39,\n                  169.128\n               ],\n               [\n                  -19.816,\n                  178.29\n               ],\n               [\n                  -19.362,\n                  185.404\n               ],\n               [\n                  -18.965,\n                  191.738\n               ],\n               [\n                  -13.261,\n                  193.03\n               ],\n               [\n                  -2.219,\n                  189.712\n               ],\n               [\n                  5.943,\n                  186.807\n               ],\n               [\n                  13.791,\n                  184.012\n               ],\n               [\n                  17.588,\n                  182.754\n               ],\n               [\n                  20.809,\n                  181.697\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457957434,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -3.645,\n            \"maxY\" : 31.712,\n            \"minX\" : -43.568,\n            \"minY\" : 27.214\n         },\n         \"center\" : [\n            -23.6065,\n            29.463\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -3.645,\n                  31.712\n               ],\n               [\n                  -7.121,\n                  31.289\n               ],\n               [\n                  -9.746,\n                  30.966\n               ],\n               [\n                  -10.78,\n                  30.832\n               ],\n               [\n                  -43.568,\n                  27.214\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958710,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"service\" : \"parking_aisle\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 12.122,\n            \"maxY\" : 48.476,\n            \"minX\" : -6.817,\n            \"minY\" : 22.016\n         },\n         \"center\" : [\n            2.6525,\n            35.246\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -6.817,\n                  48.476\n               ],\n               [\n                  5.728,\n                  41.864\n               ],\n               [\n                  8.508,\n                  40.428\n               ],\n               [\n                  11.475,\n                  25.288\n               ],\n               [\n                  12.122,\n                  22.016\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958712,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 41.384,\n            \"maxY\" : 25.288,\n            \"minX\" : 11.475,\n            \"minY\" : 0.219\n         },\n         \"center\" : [\n            26.4295,\n            12.7535\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  11.475,\n                  25.288\n               ],\n               [\n                  18.969,\n                  24.52\n               ],\n               [\n                  22.362,\n                  22.461\n               ],\n               [\n                  27.706,\n                  17.118\n               ],\n               [\n                  41.384,\n                  0.219\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958713,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 67.486,\n            \"maxY\" : 82.597,\n            \"minX\" : 35.549,\n            \"minY\" : 21.715\n         },\n         \"center\" : [\n            51.5175,\n            52.156\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  35.549,\n                  82.597\n               ],\n               [\n                  35.67,\n                  78.979\n               ],\n               [\n                  35.786,\n                  75.539\n               ],\n               [\n                  38.151,\n                  70.819\n               ],\n               [\n                  40.263,\n                  68.481\n               ],\n               [\n                  42.147,\n                  66.388\n               ],\n               [\n                  44.325,\n                  63.984\n               ],\n               [\n                  47.353,\n                  60.633\n               ],\n               [\n                  53.189,\n                  54.165\n               ],\n               [\n                  58.412,\n                  46.706\n               ],\n               [\n                  67.486,\n                  21.715\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958714,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 53.189,\n            \"maxY\" : 54.165,\n            \"minX\" : 18.969,\n            \"minY\" : 24.52\n         },\n         \"center\" : [\n            36.079,\n            39.3425\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  18.969,\n                  24.52\n               ],\n               [\n                  49.315,\n                  50.814\n               ],\n               [\n                  53.189,\n                  54.165\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958716,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -7.574,\n            \"maxY\" : 129.808,\n            \"minX\" : -80.612,\n            \"minY\" : 50.803\n         },\n         \"center\" : [\n            -44.093,\n            90.3055\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -7.574,\n                  52.473\n               ],\n               [\n                  -10.465,\n                  52.195\n               ],\n               [\n                  -24.602,\n                  50.803\n               ],\n               [\n                  -27.647,\n                  51.204\n               ],\n               [\n                  -30.708,\n                  51.605\n               ],\n               [\n                  -36.975,\n                  53.464\n               ],\n               [\n                  -51.565,\n                  54.265\n               ],\n               [\n                  -56.136,\n                  55.456\n               ],\n               [\n                  -75.959,\n                  68.492\n               ],\n               [\n                  -80.612,\n                  72.088\n               ],\n               [\n                  -80.347,\n                  76.607\n               ],\n               [\n                  -75.826,\n                  83.932\n               ],\n               [\n                  -67.045,\n                  97.102\n               ],\n               [\n                  -66.243,\n                  103.347\n               ],\n               [\n                  -64.381,\n                  109.737\n               ],\n               [\n                  -61.325,\n                  115.325\n               ],\n               [\n                  -52.444,\n                  129.808\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958718,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 14.321,\n            \"maxY\" : -29.024,\n            \"minX\" : -54.45,\n            \"minY\" : -73.24\n         },\n         \"center\" : [\n            -20.0645,\n            -51.132\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  14.321,\n                  -45.166\n               ],\n               [\n                  11.271,\n                  -49.062\n               ],\n               [\n                  7.236,\n                  -50.208\n               ],\n               [\n                  5.247,\n                  -50.665\n               ],\n               [\n                  -3.822,\n                  -52.323\n               ],\n               [\n                  -4.076,\n                  -51.544\n               ],\n               [\n                  -4.33,\n                  -50.208\n               ],\n               [\n                  -8.287,\n                  -29.024\n               ],\n               [\n                  -41.258,\n                  -34.869\n               ],\n               [\n                  -45.608,\n                  -36.36\n               ],\n               [\n                  -51.001,\n                  -39.21\n               ],\n               [\n                  -54.152,\n                  -42.961\n               ],\n               [\n                  -54.45,\n                  -45.956\n               ],\n               [\n                  -51.455,\n                  -51.5\n               ],\n               [\n                  -46.508,\n                  -57.5\n               ],\n               [\n                  -43.132,\n                  -60.872\n               ],\n               [\n                  -42.01,\n                  -61.997\n               ],\n               [\n                  -37.661,\n                  -66.338\n               ],\n               [\n                  -34.367,\n                  -73.24\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958719,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -4.33,\n            \"maxY\" : -50.208,\n            \"minX\" : -43.132,\n            \"minY\" : -60.872\n         },\n         \"center\" : [\n            -23.731,\n            -55.54\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -43.132,\n                  -60.872\n               ],\n               [\n                  -37.815,\n                  -56.453\n               ],\n               [\n                  -4.33,\n                  -50.208\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958720,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -1.197,\n            \"maxY\" : -52.323,\n            \"minX\" : -3.822,\n            \"minY\" : -66.672\n         },\n         \"center\" : [\n            -2.5095,\n            -59.4975\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -3.822,\n                  -52.323\n               ],\n               [\n                  -1.197,\n                  -66.672\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958722,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"service\" : \"parking_aisle\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 18.919,\n            \"maxY\" : -50.208,\n            \"minX\" : 7.236,\n            \"minY\" : -113.57\n         },\n         \"center\" : [\n            13.0775,\n            -81.889\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  7.236,\n                  -50.208\n               ],\n               [\n                  18.919,\n                  -113.57\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958723,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"foot\" : \"yes\",\n            \"highway\" : \"cycleway\",\n            \"source\" : \"survey\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 43.722,\n            \"maxY\" : -47.425,\n            \"minX\" : 33.355,\n            \"minY\" : -94.156\n         },\n         \"center\" : [\n            38.5385,\n            -70.7905\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  33.355,\n                  -47.425\n               ],\n               [\n                  36.715,\n                  -54.004\n               ],\n               [\n                  43.722,\n                  -94.156\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958724,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"service\" : \"driveway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 66.204,\n            \"maxY\" : -46.846,\n            \"minX\" : 58.03,\n            \"minY\" : -73.273\n         },\n         \"center\" : [\n            62.117,\n            -60.0595\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  66.204,\n                  -46.846\n               ],\n               [\n                  65.331,\n                  -55.908\n               ],\n               [\n                  64.015,\n                  -62.765\n               ],\n               [\n                  62.407,\n                  -67.585\n               ],\n               [\n                  58.03,\n                  -73.273\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958725,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"service\" : \"driveway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 71.697,\n            \"maxY\" : -46.835,\n            \"minX\" : 70.874,\n            \"minY\" : -78.093\n         },\n         \"center\" : [\n            71.2855,\n            -62.464\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  71.697,\n                  -46.835\n               ],\n               [\n                  70.874,\n                  -78.093\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958726,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"service\" : \"driveway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 73.432,\n            \"maxY\" : -27.31,\n            \"minX\" : 60.092,\n            \"minY\" : -46.835\n         },\n         \"center\" : [\n            66.762,\n            -37.0725\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  71.697,\n                  -46.835\n               ],\n               [\n                  73.432,\n                  -38.075\n               ],\n               [\n                  60.092,\n                  -27.31\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958728,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"service\" : \"driveway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 79.782,\n            \"maxY\" : -24.082,\n            \"minX\" : 73.432,\n            \"minY\" : -38.075\n         },\n         \"center\" : [\n            76.607,\n            -31.0785\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  73.432,\n                  -38.075\n               ],\n               [\n                  79.782,\n                  -24.082\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457958729,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"service\" : \"driveway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -62.231,\n            \"maxY\" : -113.692,\n            \"minX\" : -66.796,\n            \"minY\" : -132.56\n         },\n         \"center\" : [\n            -64.5135,\n            -123.126\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -62.231,\n                  -132.56\n               ],\n               [\n                  -66.796,\n                  -113.692\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 457959169,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"service\" : \"parking_aisle\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -4.889,\n            \"maxY\" : 30.966,\n            \"minX\" : -10.846,\n            \"minY\" : -19.885\n         },\n         \"center\" : [\n            -7.8675,\n            5.5405\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -9.746,\n                  30.966\n               ],\n               [\n                  -10.846,\n                  24.821\n               ],\n               [\n                  -5.353,\n                  -4.078\n               ],\n               [\n                  -6.834,\n                  -7.206\n               ],\n               [\n                  -4.889,\n                  -19.885\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 520982743,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -28.923,\n            \"maxY\" : 27.214,\n            \"minX\" : -46.923,\n            \"minY\" : -25.896\n         },\n         \"center\" : [\n            -37.923,\n            0.658999999999999\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -28.923,\n                  -25.896\n               ],\n               [\n                  -29.526,\n                  -21.9\n               ],\n               [\n                  -36.517,\n                  0.72\n               ],\n               [\n                  -43.745,\n                  10.115\n               ],\n               [\n                  -45.105,\n                  17.095\n               ],\n               [\n                  -46.923,\n                  21.337\n               ],\n               [\n                  -43.568,\n                  27.214\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 520982744,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -45.105,\n            \"maxY\" : 17.095,\n            \"minX\" : -60.866,\n            \"minY\" : -30.861\n         },\n         \"center\" : [\n            -52.9855,\n            -6.883\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -52.604,\n                  -30.861\n               ],\n               [\n                  -60.866,\n                  -5.269\n               ],\n               [\n                  -60.463,\n                  -2.13\n               ],\n               [\n                  -54.787,\n                  6.409\n               ],\n               [\n                  -45.105,\n                  17.095\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 520982745,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -46.923,\n            \"maxY\" : 34.795,\n            \"minX\" : -68.493,\n            \"minY\" : 21.337\n         },\n         \"center\" : [\n            -57.708,\n            28.066\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -68.493,\n                  34.795\n               ],\n               [\n                  -46.923,\n                  21.337\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 520982746,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -27.647,\n            \"maxY\" : 100.052,\n            \"minX\" : -37.788,\n            \"minY\" : 51.204\n         },\n         \"center\" : [\n            -32.7175,\n            75.628\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -27.647,\n                  51.204\n               ],\n               [\n                  -28.547,\n                  67.568\n               ],\n               [\n                  -34.61,\n                  91.102\n               ],\n               [\n                  -37.788,\n                  100.052\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 520982747,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 109.962,\n            \"maxY\" : 102.456,\n            \"minX\" : 35.67,\n            \"minY\" : 76.14\n         },\n         \"center\" : [\n            72.816,\n            89.298\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  109.962,\n                  102.456\n               ],\n               [\n                  105.293,\n                  101.477\n               ],\n               [\n                  96.229,\n                  94.263\n               ],\n               [\n                  79.373,\n                  81.372\n               ],\n               [\n                  74.643,\n                  77.988\n               ],\n               [\n                  68.834,\n                  76.641\n               ],\n               [\n                  63.955,\n                  76.14\n               ],\n               [\n                  47.005,\n                  77.921\n               ],\n               [\n                  35.67,\n                  78.979\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 525693383,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"footway\" : \"sidewalk\",\n            \"highway\" : \"footway\",\n            \"surface\" : \"asphalt\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 123.171,\n            \"maxY\" : 97.113,\n            \"minX\" : 93.665,\n            \"minY\" : 69.282\n         },\n         \"center\" : [\n            108.418,\n            83.1975\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  93.665,\n                  97.113\n               ],\n               [\n                  96.229,\n                  94.263\n               ],\n               [\n                  115.417,\n                  69.282\n               ],\n               [\n                  123.171,\n                  72.143\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 655212465,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 74.914,\n            \"maxY\" : 80.37,\n            \"minX\" : 68.475,\n            \"minY\" : 65.431\n         },\n         \"center\" : [\n            71.6945,\n            72.9005\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  68.475,\n                  80.37\n               ],\n               [\n                  68.834,\n                  76.641\n               ],\n               [\n                  70.316,\n                  72.021\n               ],\n               [\n                  74.914,\n                  65.431\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 655212467,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"surface\" : \"asphalt\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 30.508,\n            \"maxY\" : 116.249,\n            \"minX\" : 16.007,\n            \"minY\" : 114.301\n         },\n         \"center\" : [\n            23.2575,\n            115.275\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  30.508,\n                  114.301\n               ],\n               [\n                  18.864,\n                  114.301\n               ],\n               [\n                  18.798,\n                  116.227\n               ],\n               [\n                  16.007,\n                  116.249\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 655212919,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 62.49,\n            \"maxY\" : 50.814,\n            \"minX\" : 49.315,\n            \"minY\" : 14.88\n         },\n         \"center\" : [\n            55.9025,\n            32.847\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  49.315,\n                  50.814\n               ],\n               [\n                  52.111,\n                  47.608\n               ],\n               [\n                  53.974,\n                  45.193\n               ],\n               [\n                  62.49,\n                  14.88\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796466272,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 35.786,\n            \"maxY\" : 75.539,\n            \"minX\" : 19.24,\n            \"minY\" : 72.845\n         },\n         \"center\" : [\n            27.513,\n            74.192\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  19.24,\n                  72.845\n               ],\n               [\n                  22.843,\n                  73.769\n               ],\n               [\n                  35.786,\n                  75.539\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796466273,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 38.151,\n            \"maxY\" : 70.819,\n            \"minX\" : 22.893,\n            \"minY\" : 68.548\n         },\n         \"center\" : [\n            30.522,\n            69.6835\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  22.893,\n                  68.548\n               ],\n               [\n                  25.827,\n                  69.36\n               ],\n               [\n                  34.731,\n                  69.705\n               ],\n               [\n                  38.151,\n                  70.819\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796466274,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 40.263,\n            \"maxY\" : 68.481,\n            \"minX\" : 27.076,\n            \"minY\" : 63.627\n         },\n         \"center\" : [\n            33.6695,\n            66.054\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  27.076,\n                  63.627\n               ],\n               [\n                  30.58,\n                  64.473\n               ],\n               [\n                  37.803,\n                  66.633\n               ],\n               [\n                  40.263,\n                  68.481\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796466275,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 42.147,\n            \"maxY\" : 66.388,\n            \"minX\" : 34.543,\n            \"minY\" : 60.978\n         },\n         \"center\" : [\n            38.345,\n            63.683\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  34.543,\n                  60.978\n               ],\n               [\n                  42.147,\n                  66.388\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796466276,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 44.325,\n            \"maxY\" : 63.984,\n            \"minX\" : 38.306,\n            \"minY\" : 56.558\n         },\n         \"center\" : [\n            41.3155,\n            60.271\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  38.306,\n                  56.558\n               ],\n               [\n                  41.307,\n                  58.807\n               ],\n               [\n                  42.169,\n                  61.145\n               ],\n               [\n                  44.325,\n                  63.984\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796466277,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 47.353,\n            \"maxY\" : 60.633,\n            \"minX\" : 42.401,\n            \"minY\" : 51.738\n         },\n         \"center\" : [\n            44.877,\n            56.1855\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  42.401,\n                  51.738\n               ],\n               [\n                  44.59,\n                  53.664\n               ],\n               [\n                  45.756,\n                  57.516\n               ],\n               [\n                  47.353,\n                  60.633\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796466278,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 125.558,\n            \"maxY\" : 25.778,\n            \"minX\" : 91.067,\n            \"minY\" : 25.099\n         },\n         \"center\" : [\n            108.3125,\n            25.4385\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  125.558,\n                  25.099\n               ],\n               [\n                  91.067,\n                  25.778\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796466321,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"service\",\n            \"service\" : \"driveway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -7.851,\n            \"maxY\" : 117.162,\n            \"minX\" : -17.08,\n            \"minY\" : 117.073\n         },\n         \"center\" : [\n            -12.4655,\n            117.1175\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -17.08,\n                  117.162\n               ],\n               [\n                  -7.851,\n                  117.073\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796469204,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -6.613,\n            \"maxY\" : 109.759,\n            \"minX\" : -10.41,\n            \"minY\" : 109.704\n         },\n         \"center\" : [\n            -8.5115,\n            109.7315\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -10.41,\n                  109.759\n               ],\n               [\n                  -6.613,\n                  109.704\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796469205,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -2.413,\n            \"maxY\" : 101.577,\n            \"minX\" : -10.139,\n            \"minY\" : 100.018\n         },\n         \"center\" : [\n            -6.276,\n            100.7975\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -10.139,\n                  100.018\n               ],\n               [\n                  -4.419,\n                  100.074\n               ],\n               [\n                  -2.413,\n                  101.577\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 796469206,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"footway\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -8.613,\n            \"maxY\" : 282.291,\n            \"minX\" : -113.511,\n            \"minY\" : 34.717\n         },\n         \"center\" : [\n            -61.062,\n            158.504\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -22.209,\n                  282.291\n               ],\n               [\n                  -25.419,\n                  274.198\n               ],\n               [\n                  -33.029,\n                  250.919\n               ],\n               [\n                  -70.261,\n                  165.989\n               ],\n               [\n                  -80.424,\n                  133.894\n               ],\n               [\n                  -101.657,\n                  100.575\n               ],\n               [\n                  -110.676,\n                  89.22\n               ],\n               [\n                  -113.511,\n                  81.394\n               ],\n               [\n                  -110.14,\n                  69.616\n               ],\n               [\n                  -94.185,\n                  38.691\n               ],\n               [\n                  -85.669,\n                  34.717\n               ],\n               [\n                  -76.849,\n                  36.91\n               ],\n               [\n                  -69.642,\n                  37.389\n               ],\n               [\n                  -51.554,\n                  36.131\n               ],\n               [\n                  -8.613,\n                  40.606\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 1282562992,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"path\",\n            \"mtb:scale\" : \"1\",\n            \"surface\" : \"unpaved\",\n            \"width\" : \"0.5\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : 47.049,\n            \"maxY\" : 85.124,\n            \"minX\" : 47.005,\n            \"minY\" : 77.921\n         },\n         \"center\" : [\n            47.027,\n            81.5225\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  47.049,\n                  85.124\n               ],\n               [\n                  47.033,\n                  81.595\n               ],\n               [\n                  47.005,\n                  77.921\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 1327887801,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"footway\" : \"crossing\",\n            \"highway\" : \"footway\",\n            \"surface\" : \"paved\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      },\n      {\n         \"bounds\" : {\n            \"maxX\" : -68.493,\n            \"maxY\" : 37.389,\n            \"minX\" : -69.642,\n            \"minY\" : 34.795\n         },\n         \"center\" : [\n            -69.0675,\n            36.092\n         ],\n         \"elementType\" : \"way\",\n         \"geometry\" : {\n            \"closed\" : false,\n            \"coordinates\" : [\n               [\n                  -68.493,\n                  34.795\n               ],\n               [\n                  -69.642,\n                  37.389\n               ]\n            ],\n            \"type\" : \"line_string\"\n         },\n         \"groundStates\" : [\n            \"ON\"\n         ],\n         \"isClosed\" : false,\n         \"layer\" : 0,\n         \"osmId\" : 1355154126,\n         \"osmType\" : \"way\",\n         \"primaryGroundState\" : \"ON\",\n         \"primaryRepresentation\" : \"Road\",\n         \"representations\" : [\n            \"Road\"\n         ],\n         \"tags\" : {\n            \"highway\" : \"path\"\n         },\n         \"tmCategory\" : \"Road\",\n         \"tmRoadType\" : \"pedestrian\"\n      }\n   ]\n}\n"
  },
  {
    "path": "test/data/map.obj",
    "content": "# This file was created by OSM2World 0.2.0 - http://osm2world.org\n\n# Projection information:\n# Coordinate origin (0,0,0): lat 60.23452933118341, lon 24.741759758497647, ele 0\n# North direction: (0.0, 0.0, -1.0)\n# 1 coordinate unit corresponds to roughly 1 m in reality\n\nmtllib map.obj.mtl\n\ng RoadJunction\no RoadJunction0\nusemtl ASPHALT_0\nv  94.79047988716746 0.0 -96.80731187127854\nv  94.8805752025865 0.0 -97.18299731663896\nv  93.95749107604308 0.0 -98.29493555996723\nf 1 2 3\nv  92.44940341220476 0.0 -97.04337505317471\nf 1 3 4\nv  93.37214687596531 0.0 -95.93115409412945\nf 1 4 5\nv  93.85040846495534 0.0 -95.96157744090947\nf 1 5 6\no RoadJunction1::pedestrian\nv  110.97140725784757 0.0 -102.43876750360106\nv  110.24953826511036 0.0 -103.4237405216404\nv  109.17929268171274 0.0 -103.09362781073843\nf 7 8 9\nv  109.02495622411656 0.0 -102.8316979281359\nf 7 9 10\nv  109.25480047792338 0.0 -101.73553570032783\nf 7 10 11\nv  110.33432661790872 0.0 -101.51761156191897\nf 7 11 12\no RoadJunction2::pedestrian\no RoadJunction3::pedestrian\nv  36.628105383947094 0.0 -79.45200532650587\nv  36.2714878306463 0.0 -79.90962484598164\nv  35.00767829955404 0.0 -79.86735813695397\nf 13 14 15\nv  35.06876972092947 0.0 -78.04820874203571\nf 13 15 16\nv  36.33256750731405 0.0 -78.09082517901845\nf 13 16 17\nv  36.52401789559469 0.0 -78.3368525094222\nf 13 17 18\no RoadJunction4\nv  69.19251130105131 0.0 -79.51482297363674\nv  69.57848571598208 0.0 -79.98161619858757\nv  69.11379853937834 0.0 -81.35003016722149\nf 19 20 21\nv  67.55032674637447 0.0 -81.08659059204376\nf 19 21 22\nv  67.55971209735334 0.0 -79.64145984472731\nf 19 22 23\nv  67.93381477528219 0.0 -79.39364516383489\nf 19 23 24\no RoadJunction5::pedestrian\nv  32.441569892976226 0.0 -89.13784051262849\nv  32.24877271241451 0.0 -89.36374438542524\nv  31.13708408599999 0.0 -89.4999369277385\nf 25 26 27\nv  30.645382907120492 0.0 -88.93421473195184\nf 25 27 28\nv  30.93512758993694 0.0 -87.85234226744379\nf 25 28 29\nv  32.40135990702557 0.0 -88.01856254549477\nf 25 29 30\no RoadJunction6::pedestrian\nv  107.44631849682293 0.0 -112.03259025383608\nv  107.5192753949997 0.0 -113.42669812933126\nv  106.50463063255633 0.0 -113.90093015769652\nf 31 32 33\nv  105.48346630117582 0.0 -112.59149346537308\nf 31 33 34\nv  106.1906270068775 0.0 -111.7229753325787\nf 31 34 35\nv  106.38771635318807 0.0 -111.6668560440979\nf 31 35 36\ng RoadCrossingAtConnector\no RoadCrossingAtConnector7::pedestrian\nv  127.65657717506116 0.0 -103.22909237180704\nv  124.81942922994108 0.0 -102.25410953221399\nv  128.02057077005892 0.0 -102.16989046778602\nf 37 38 39\nv  125.18342282493884 0.0 -101.19490762819296\nf 38 40 39\no RoadCrossingAtConnector8\nv  112.84358010046327 0.0 -124.4560843551745\nv  111.98390583773295 0.0 -127.33027296450395\nv  111.63209416226707 0.0 -124.09372703549606\nf 41 42 43\nv  110.77241989953674 0.0 -126.96791564482551\nf 42 44 43\ng RoadJunction\no RoadJunction9\nv  66.73649310097011 0.0 47.91197434453654\nv  67.21544495224126 0.0 47.566556592778625\nv  67.21255095231717 0.0 46.121398267052044\nf 45 46 47\nv  65.19213564005547 0.0 46.126032492479595\nf 45 47 48\nv  65.19587115332784 0.0 47.57118888802749\nf 45 48 49\nv  65.47780424868765 0.0 47.79071685523588\nf 45 49 50\no RoadJunction10\nv  34.50025957758958 0.0 48.27736768949207\nv  34.651424732692554 0.0 48.117679998003375\nv  34.61788570614038 0.0 46.67290801068139\nf 51 52 53\nv  32.058696724693824 0.0 46.7320927294353\nf 51 53 54\nv  32.09198245996769 0.0 48.176870574461894\nf 51 54 55\nv  33.374110272430165 0.0 48.85251006148411\nf 51 55 56\ng RoadConnector\no RoadConnector11::pedestrian\nv  127.03340093105876 0.0 -27.193467019387324\nv  123.28154627600085 0.0 -23.887592267697098\nv  124.13114771699111 0.0 -22.951014242629807\nf 57 58 59\nv  127.7859050759493 0.0 -26.363926470285772\nf 60 57 59\ng RoadJunction\no RoadJunction12::pedestrian\nv  45.42432746543081 0.0 -63.710450487767936\nv  44.16242678156336 0.0 -65.10512450715345\nv  43.225315621077506 0.0 -64.25611115127234\nf 61 62 63\nv  43.31052098339328 0.0 -63.57408258388433\nf 61 63 64\nv  44.2024708041097 0.0 -62.89671597118105\nf 61 64 65\nv  44.48610643781065 0.0 -62.86266377585094\nf 61 65 66\no RoadJunction13::pedestrian\nv  48.52581716630131 0.0 -60.27698020204448\nv  47.11814476997842 0.0 -61.83595187893732\nv  46.17992374235826 0.0 -60.98816516702033\nf 67 68 69\nv  46.37582186364845 0.0 -59.95386610803083\nf 67 69 70\nv  47.37260713397426 0.0 -59.443161559840156\nf 67 70 71\nv  47.5869786745357 0.0 -59.42987732048231\nf 67 71 72\no RoadJunction14::pedestrian\nv  43.21982783429783 0.0 -66.14553216060906\nv  42.01479900469546 0.0 -67.47991259358228\nv  41.07495856523471 0.0 -66.63392148610781\nf 73 74 75\nv  41.089022367785766 0.0 -66.3225531040091\nf 73 75 76\nv  41.73830509145679 0.0 -65.40995683214322\nf 73 76 77\nv  42.282716673811976 0.0 -65.29651880472795\nf 73 77 78\no RoadJunction15::pedestrian\nv  41.32835119904268 0.0 -68.24251092544601\nv  40.13557971365028 0.0 -69.56525748169419\nv  39.19723048411276 0.0 -68.7176126686988\nf 79 80 81\nv  39.215067415962984 0.0 -68.39418256756753\nf 79 81 82\nv  39.887766122132014 0.0 -67.49870701714772\nf 79 82 83\nv  40.388510759581926 0.0 -67.39651981797154\nf 79 83 84\no RoadJunction16::pedestrian\nv  73.85015365541702 0.0 39.21802131366962\nv  74.43751703948797 0.0 37.38922621343407\nv  73.28602008783145 0.0 36.86667882238004\nf 85 86 87\nv  73.01971263815834 0.0 36.9298496455455\nf 85 87 88\nv  72.22559958409381 0.0 37.91391542735887\nf 85 88 89\nv  72.60973282423151 0.0 38.9723443568081\nf 85 89 90\no RoadJunction17::pedestrian\nv  50.26286357581707 0.0 -50.57914980157699\nv  50.28640804127241 0.0 -50.913831815626864\nv  49.553693640198944 0.0 -51.76090301519672\nf 91 92 93\nv  48.34367652513587 0.0 -50.713348714673224\nf 91 93 94\nv  49.07710527688534 0.0 -49.86689595356463\nf 91 94 95\nv  49.418771885706114 0.0 -49.84300495892252\nf 91 95 96\no RoadJunction18::pedestrian\nv  -1.4264359322282991 0.0 -101.03723935590799\nv  -2.278288047771035 0.0 -102.69346894388315\nv  -3.4016577301736914 0.0 -102.1129161364399\nf 97 98 99\nv  -3.493052522001269 0.0 -101.46751706194597\nf 97 99 100\nv  -2.8214815436785625 0.0 -100.57119545015932\nf 97 100 101\nv  -2.5520570329888654 0.0 -100.4610639111915\nf 97 101 102\no RoadJunction19::pedestrian\nv  -6.932482579312694 0.0 -116.2875218323461\nv  -7.5675649262426266 0.0 -118.24786797680893\nv  -8.770327312194263 0.0 -117.85753011224595\nf 103 104 105\nv  -8.875551969333046 0.0 -117.64290632333756\nf 103 105 106\nv  -8.88635220400182 0.0 -116.52295839347106\nf 103 106 107\nv  -8.135647037975145 0.0 -115.89842506933829\nf 103 107 108\no RoadJunction20::pedestrian\nv  115.88850310126362 0.0 -97.73402238973348\nv  116.04407038586427 0.0 -98.04028112455043\nv  115.67848063028455 0.0 -99.09893316431436\nf 109 110 111\nv  114.91085822715579 0.0 -99.2512534700291\nf 109 111 112\nv  114.16874487167448 0.0 -98.41240439045791\nf 109 112 113\nv  114.81796253011596 0.0 -97.40486756870241\nf 109 113 114\no RoadJunction21\nv  15.45855091247789 0.0 44.97080618420656\nv  14.24069137829584 0.0 44.014621161613924\nv  13.179472849098863 0.0 44.99559322795496\nf 115 116 117\nv  13.268367628544391 0.0 45.484928370871636\nf 115 117 118\nv  14.264061735558473 0.0 46.26441169386778\nf 115 118 119\nv  14.757410903362135 0.0 46.23448875877565\nf 115 119 120\no RoadJunction22::pedestrian\nv  -7.9303844327257496 0.0 -39.88486481161539\nv  -8.18941618206438 0.0 -41.504096165115264\nv  -9.29538302413283 0.0 -41.327355239106915\nf 121 122 123\nv  -9.486628005956016 0.0 -41.0779894912188\nf 121 123 124\nv  -9.370538399172181 0.0 -39.96402216051385\nf 121 124 125\nv  -9.03629423268292 0.0 -39.70776731010888\nf 121 125 126\no RoadJunction23::pedestrian\nusemtl EARTH_0\nv  -68.51577317519197 0.0 -37.872024935837594\nv  -70.76673790171846 0.0 -37.8754818619394\nv  -70.69246302963906 0.0 -36.75794740875295\nf 127 128 129\nv  -69.71257603561715 0.0 -36.16559533564947\nf 127 129 130\nv  -68.68853812605785 0.0 -36.61918807199628\nf 127 130 131\nv  -68.59348020286275 0.0 -36.75472388932787\nf 127 131 132\no RoadJunction24::pedestrian\nusemtl ASPHALT_0\nv  123.96134447595063 0.0 -71.47527582178826\nv  123.45058752961215 0.0 -73.1391580106186\nv  122.38004695846449 0.0 -72.81000318958753\nf 133 134 135\nv  122.13592505168815 0.0 -72.43501039182546\nf 133 135 136\nv  122.57364929609714 0.0 -71.24867212158874\nf 133 136 137\nv  122.89050419534573 0.0 -71.14709735796838\nf 133 137 138\no RoadJunction25\nv  -5.142153625144269 0.0 -48.307953092158314\nv  -6.397331281938981 0.0 -50.14321348324194\nv  -7.817251062532834 0.0 -49.87429197363234\nf 139 140 141\nv  -7.23695369536905 0.0 -46.808858276759715\nf 139 141 142\nv  -5.816987967694701 0.0 -47.07753706989876\nf 139 142 143\nv  -5.731751572200942 0.0 -47.18930423465288\nf 139 143 144\no RoadJunction26\nv  -2.760216371800264 0.0 -30.92277212911428\nv  -3.1097783017782348 0.0 -32.76995105494805\nv  -4.529744029452583 0.0 -32.50127226180901\nf 145 146 147\nv  -4.645764424622315 0.0 -32.154346617010525\nf 145 147 148\nv  -4.51046794325866 0.0 -31.042548581029276\nf 145 148 149\nv  -4.180168617367208 0.0 -30.654022092906295\nf 145 149 150\ng RoadConnector\no RoadConnector27::pedestrian\nv  -42.93254524271714 0.0 -28.301145321958646\nv  -44.85782391653445 0.0 -27.12348915717802\nv  -44.16379602531517 0.0 -26.06645259594592\nf 151 152 153\nv  -42.31783481543323 0.0 -27.36491292491741\nf 154 151 153\ng RoadJunction\no RoadJunction28\nv  -6.6760951840888 0.0 -51.61510161652178\nv  -7.051476143739381 0.0 -53.59958400239378\nv  -8.471517241866707 0.0 -53.33130384792511\nf 155 156 157\nv  -8.63988154814258 0.0 -53.005678825485\nf 155 157 158\nv  -8.51884337135498 0.0 -51.74696886471897\nf 155 158 159\nv  -8.096014964682652 0.0 -51.34618010691218\nf 155 159 160\no RoadJunction29::pedestrian\nv  36.7141256599652 0.0 -75.09805444624408\nv  36.39060045644655 0.0 -76.36984806681342\nv  35.12680267006196 0.0 -76.32723162983068\nf 161 162 163\nv  34.90759379451539 0.0 -75.98408708305955\nf 161 163 164\nv  35.059345249987516 0.0 -74.87441528183037\nf 161 164 165\nv  35.58358751046713 0.0 -74.53158776752312\nf 161 165 166\no RoadJunction30::pedestrian\nv  39.23687945637438 0.0 -70.5601254748339\nv  38.30413566931833 0.0 -71.92475751425813\nv  37.17359751982026 0.0 -71.35829083553718\nf 167 168 169\nv  37.10279662646417 0.0 -71.06652707436025\nf 167 169 170\nv  37.44967704100944 0.0 -70.00159797405074\nf 167 170 171\nv  38.29853022683686 0.0 -69.71248066183851\nf 167 171 172\no RoadJunction31::pedestrian\nv  54.21740350635939 0.0 -53.79861707996824\nv  53.06220577048045 0.0 -55.24933087171021\nv  52.123367278714845 0.0 -54.40222799014804\nf 173 174 175\nv  52.14952373914482 0.0 -54.00628935013931\nf 173 175 176\nv  52.882238140218284 0.0 -53.15921815056946\nf 173 176 177\nv  53.18158246139859 0.0 -53.07330627183977\nf 173 177 178\no RoadJunction32::pedestrian\nv  12.603882444697328 0.0 -25.807879434355772\nv  11.889681340119733 0.0 -26.459618362082292\nv  10.64876910969431 0.0 -26.216435628418342\nf 179 180 181\nv  11.0623127812664 0.0 -24.115677749206377\nf 179 181 182\nv  12.30280943737607 0.0 -24.36097155620606\nf 179 182 183\nv  12.474967522386793 0.0 -24.549951793997497\nf 179 183 184\no RoadJunction33::pedestrian\nv  20.1690088497931 0.0 -24.531356289079636\nv  20.106590977086213 0.0 -24.764717643444072\nv  19.373162225336742 0.0 -25.611170404552666\nf 185 186 187\nv  18.018771962825454 0.0 -25.252950804795805\nf 185 187 188\nv  17.88985704051492 0.0 -23.99502316443753\nf 185 188 189\nv  19.512993761144777 0.0 -23.450317340180288\nf 185 189 190\no RoadJunction34::pedestrian\nv  -42.055845303812646 0.0 61.05568801228494\nv  -42.08959720622956 0.0 60.73381033757817\nv  -42.80547177823531 0.0 59.87246046218679\nf 191 192 193\nv  -43.314889084189794 0.0 59.79570924055462\nf 191 193 194\nv  -44.208506824684484 0.0 60.69038702581859\nf 191 194 195\nv  -42.95118621474262 0.0 61.94864134745243\nf 191 195 196\no RoadJunction35::pedestrian\nv  -3.550109804657843 0.0 50.92495292893175\nv  -3.8645652448705987 0.0 49.25635893961416\nv  -5.10758209830076 0.0 49.488544442305965\nf 197 198 199\nv  -5.268262564360567 0.0 49.813331365964366\nf 197 199 200\nv  -5.062921614010039 0.0 50.91434685395274\nf 197 200 201\nv  -4.792374065287724 0.0 51.16113191261439\nf 197 201 202\no RoadJunction36::pedestrian\nv  -3.1181817416035615 0.0 53.05835014076277\nv  -2.872153296923923 0.0 52.79208577014247\nv  -3.0995637132623126 0.0 51.54818645423605\nf 203 204 205\nv  -3.4843855737059513 0.0 51.3188751480753\nf 203 205 206\nv  -4.686608665198667 0.0 51.710870867252645\nf 203 206 207\nv  -4.219897955681228 0.0 53.25989763967934\nf 203 207 208\no RoadJunction37\nv  21.774611610291995 0.0 -180.52000514945357\nv  21.262783499222497 0.0 -183.15020445773362\nv  19.84414307995893 0.0 -182.874613597065\nf 209 210 211\nv  19.732939609510428 0.0 -182.7155500513685\nf 209 211 212\nv  19.338664059279804 0.0 -181.51407271239702\nf 209 212 213\nv  20.35614814517994 0.0 -180.24350495090985\nf 209 213 214\no RoadJunction38\nv  34.93236323849894 0.0 -100.72359039294142\nv  34.76956786573833 0.0 -102.68523613632729\nv  33.328448588818326 0.0 -102.57722599170214\nf 215 216 217\nv  33.1225478575584 0.0 -102.25958567093708\nf 215 217 218\nv  33.14373000239728 0.0 -100.99524698122151\nf 215 218 219\nv  33.493154024945625 0.0 -100.59256443644557\nf 215 219 220\no RoadJunction39::pedestrian\nv  -5.642706695158454 0.0 -109.20361902657778\nv  -6.056035029711004 0.0 -110.64295699467773\nv  -7.316272342773201 0.0 -110.53901989669323\nf 221 222 223\nv  -7.49479580919516 0.0 -110.27683166650117\nf 221 223 224\nv  -7.511017441763809 0.0 -109.15694914171657\nf 221 224 225\nv  -6.7660763775611095 0.0 -108.62306621913454\nf 221 225 226\no RoadJunction40::pedestrian\nv  -9.301603233531825 0.0 -29.860395314887132\nv  -8.675169471665725 0.0 -30.533539670425597\nv  -8.811951208937618 0.0 -31.645155956303974\nf 227 228 229\nv  -10.819594775277206 0.0 -31.391551660278463\nf 227 229 230\nv  -10.675653389980344 0.0 -30.28083977671911\nf 227 230 231\nv  -10.546333762305157 0.0 -30.083211194732645\nf 227 231 232\no RoadJunction41::pedestrian\nv  -45.9794574020809 0.0 -20.740445241852896\nv  -45.91780305163257 0.0 -21.822519627356797\nv  -47.01597487690473 0.0 -22.44943241854076\nf 233 234 235\nv  -47.368856092440225 0.0 -22.360407327699885\nf 233 235 236\nv  -48.03821653378573 0.0 -21.287580405436362\nf 233 236 237\nv  -47.141731104734234 0.0 -20.242327940715747\nf 233 237 238\ng RoadConnector\no RoadConnector42::pedestrian\nv  -69.22585927560964 0.0 -34.784918310204446\nv  -68.6604285230379 0.0 -34.02344164400079\nv  -67.70557783432204 0.0 -34.852453807862425\nf 239 240 241\nv  -68.38013436703041 0.0 -35.519186237932345\nf 242 239 241\ng RoadJunction\no RoadJunction43::pedestrian\nv  -26.582924394457333 0.0 -51.7015874930784\nv  -27.070062651311336 0.0 -52.22722700475853\nv  -28.33267059875008 0.0 -52.15778511080261\nf 243 244 245\nv  -28.54648675496643 0.0 -51.959495721509384\nf 243 245 246\nv  -28.710738653054335 0.0 -50.70569257924734\nf 243 246 247\nv  -26.748024681886104 0.0 -50.447895784298844\nf 243 247 248\no RoadJunction44::pedestrian\nv  -44.21284272022672 0.0 -15.822127048994929\nv  -45.08322854015296 0.0 -18.649244564021036\nv  -46.245502242806296 0.0 -18.151127262883886\nf 249 250 251\nv  -46.526981972887086 0.0 -16.46721227310741\nf 249 251 252\nv  -45.5898965864145 0.0 -15.618170469642356\nf 249 252 253\nv  -45.454018562985866 0.0 -15.580293360434407\nf 249 253 254\no RoadJunction45::pedestrian\nv  47.88897491546373 0.0 -78.39120029849519\nv  47.571309087200106 0.0 -78.74670817461659\nv  46.45134160665022 0.0 -78.75524358274272\nf 255 256 257\nv  46.23063587493463 0.0 -78.55571267017903\nf 255 257 258\nv  46.12654838658222 0.0 -77.44055985309537\nf 255 258 259\nv  47.771936625768525 0.0 -77.2773322410367\nf 255 259 260\no RoadJunction46::pedestrian\nv  47.91803265350113 0.0 -85.62127479226564\nv  46.283907292110875 0.0 -85.76985843666314\nv  46.163684005561514 0.0 -84.6563296440168\nf 261 262 263\nv  46.485242686246 0.0 -84.2965474859166\nf 261 263 264\nv  47.60523117988375 0.0 -84.29146961232685\nf 261 264 265\nv  47.83544846525771 0.0 -84.50432364627343\nf 261 265 266\no RoadJunction47::pedestrian\nv  72.3032452863761 0.0 47.83130159149047\nv  72.50761005498912 0.0 45.99651234522491\nv  71.26718922380361 0.0 45.75083538836339\nf 267 268 269\nv  70.71555498693535 0.0 46.11438333088223\nf 267 269 270\nv  70.71844898685943 0.0 47.55954165660881\nf 267 270 271\nv  71.03916724363765 0.0 47.79801935241012\nf 267 271 272\ng RoadCrossingAtConnector\no RoadCrossingAtConnector48\nv  17.307165182252017 0.0 -195.87608548797272\nv  17.886915572162717 0.0 -192.93263691777508\nv  18.725084427837285 0.0 -196.15536308222494\nf 273 274 275\nv  19.304834817747984 0.0 -193.2119145120273\nf 274 276 275\ng Road\no Road49::pedestrian\nv  79.20625152417173 0.0 23.820726304472988\nf 87 86 277\nv  80.35774847582826 0.0 24.343273695527014\nf 86 278 277\no Road50::pedestrian\nf 269 268 90\nf 268 85 90\no Road51::pedestrian\nv  59.69494347296773 0.0 27.802032890906684\nf 89 88 279\nv  60.48905652703227 0.0 26.817967109093313\nf 88 280 279\no Road52::pedestrian\nv  7.7867166647246595 0.0 50.106456128373814\nv  6.68528333527534 0.0 50.30954387162618\nv  19.469852680391796 0.0 113.46919659144919\nf 281 282 283\nv  18.368147319608205 0.0 113.6708034085508\nf 282 284 283\no Road53::pedestrian\nf 239 242 130\nf 242 131 130\no Road54::pedestrian\nv  84.64020989851981 0.0 -94.17649918844334\nf 35 34 285\nv  83.95379010148018 0.0 -95.06150081155666\nf 34 286 285\no Road55::pedestrian\nv  74.0987832486107 0.0 -86.39690069422115\nf 285 286 287\nv  73.5392167513893 0.0 -87.36709930577886\nf 286 288 287\no Road56::pedestrian\nv  69.46019114422506 0.0 -84.38214924950695\nf 287 288 289\nv  69.16980885577493 0.0 -85.46385075049305\nf 288 290 289\no Road57::pedestrian\nv  61.66732918650087 0.0 -83.48411460797452\nf 289 290 291\nv  61.644670813499125 0.0 -84.60388539202548\nf 290 292 291\no Road58::pedestrian\nf 291 292 266\nf 292 261 266\no Road59::pedestrian\nv  41.85255976804186 0.0 -85.157121079614\nf 263 262 293\nv  42.353440231958146 0.0 -86.158878920386\nf 262 294 293\no Road60::pedestrian\nv  39.35752422987424 0.0 -87.84162668363714\nf 293 294 295\nv  39.818475770125765 0.0 -88.86237331636286\nf 294 296 295\no Road61::pedestrian\nv  35.30599154257305 0.0 -87.9142288575131\nf 295 296 297\nv  35.33800845742696 0.0 -89.03377114248691\nf 296 298 297\no Road62::pedestrian\nv  35.301895007024676 0.0 -87.91436101643315\nv  35.34210499297533 0.0 -89.03363898356686\nf 299 300 30\nf 300 25 30\no Road63::pedestrian\nv  26.213487872865958 0.0 -86.59384234143745\nf 29 28 301\nv  26.08251212713404 0.0 -87.70615765856256\nf 28 302 301\no Road64::pedestrian\nv  11.137797965870782 0.0 -87.00487894271342\nf 301 302 303\nv  11.006202034129217 0.0 -88.1171210572866\nf 302 304 303\no Road65::pedestrian\nv  4.235393357957542 0.0 -85.15080733811703\nf 303 304 305\nv  3.7606066420424584 0.0 -86.16519266188297\nf 304 306 305\no Road66::pedestrian\nv  -2.442145838835608 0.0 -80.45319872504534\nf 305 306 307\nv  -3.211854161164392 0.0 -81.26680127495466\nf 306 308 307\no Road67::pedestrian\nv  -7.524130760864099 0.0 -74.0153082099998\nf 307 308 309\nv  -8.4978692391359 0.0 -74.5686917900002\nf 308 310 309\no Road68::pedestrian\nv  -10.753119316975171 0.0 -65.57304287707123\nf 309 310 311\nv  -11.844880683024828 0.0 -65.82295712292876\nf 310 312 311\no Road69::pedestrian\nv  -11.115160873417313 0.0 -61.2804221837799\nf 311 312 313\nv  -12.234839126582688 0.0 -61.25357781622011\nf 312 314 313\no Road70::pedestrian\nv  -9.909915580057646 0.0 -52.26903573061401\nf 313 314 315\nv  -11.020084419942354 0.0 -52.12096426938599\nf 314 316 315\no Road71::pedestrian\nv  -9.912016578965774 0.0 -52.283370463004175\nv  -11.017983421034225 0.0 -52.106629536995825\nf 317 318 122\nf 318 123 122\no Road72::pedestrian\nv  -6.568045100021416 0.0 -31.377548750753252\nf 121 126 319\nv  -7.673954899978585 0.0 -31.20045124924675\nf 126 320 319\no Road73::pedestrian\nv  -6.569960092226228 0.0 -31.388774860114964\nv  -7.672039907773773 0.0 -31.18922513988504\nv  -0.13154444160817047 0.0 4.169548628593316\nf 321 322 323\nv  -1.2344555583918297 0.0 4.364451371406685\nf 322 324 323\no Road74::pedestrian\nv  7.787861411942643 0.0 50.11287384860639\nf 323 324 325\nv  6.6841385880573565 0.0 50.303126151393606\nf 324 326 325\no Road75::pedestrian\nv  -0.6461418929611666 0.0 66.57122625054171\nf 203 208 327\nv  -1.7478581070388335 0.0 66.77277374945828\nf 208 328 327\no Road76::pedestrian\nv  -38.05297027709511 0.0 55.94607776741441\nf 193 192 329\nv  -37.57702972290488 0.0 56.9599222325856\nf 192 330 329\no Road77::pedestrian\nf 329 330 200\nf 330 201 200\no Road78::pedestrian\nv  71.50603902136922 0.0 78.10964111954019\nf 267 272 331\nv  70.24196097863077 0.0 78.07635888045982\nf 272 332 331\no Road79::pedestrian\nv  65.95682007693561 0.0 55.99799712604979\nf 45 50 333\nv  64.70517992306439 0.0 55.81800287395021\nf 50 334 333\no Road80::pedestrian\nv  64.62668397015885 0.0 62.92497803612877\nf 333 334 335\nv  63.40331602984114 0.0 62.60502196387123\nf 334 336 335\no Road81::pedestrian\nv  62.9651781183002 0.0 67.88196370088575\nf 335 336 337\nv  61.84882188169979 0.0 67.28803629911424\nf 336 338 337\no Road82::pedestrian\nv  58.531074124135756 0.0 73.6585839383513\nf 337 338 339\nv  57.528925875864246 0.0 72.8874160616487\nf 338 340 339\no Road83::pedestrian\nv  37.31466710385384 0.0 53.80362630924079\nf 51 56 341\nv  36.115332896146164 0.0 54.204373690759205\nf 56 342 341\no Road84::pedestrian\nv  44.34484500570177 0.0 94.04730616270791\nf 341 342 343\nv  43.09915499429823 0.0 94.2646938372921\nf 342 344 343\no Road85::pedestrian\nv  19.10088807009096 0.0 -73.38744619422323\nv  19.379111929909037 0.0 -72.30255380577677\nv  22.73532781491468 0.0 -74.31855136541478\nf 345 346 347\nv  22.95067218508532 0.0 -73.21944863458523\nf 346 348 347\no Road86::pedestrian\nf 347 348 164\nf 348 165 164\no Road87::pedestrian\nv  52.54391752424905 0.0 -47.963221648929725\nf 91 96 349\nv  51.67808247575094 0.0 -47.25277835107027\nf 96 350 349\no Road88::pedestrian\nv  54.474420618312436 0.0 -45.444354744212404\nf 349 350 351\nv  53.47357938168756 0.0 -44.94164525578759\nf 350 352 351\no Road89::pedestrian\nv  63.029128687712024 0.0 -15.03146042637006\nf 351 352 353\nv  61.95087131228798 0.0 -14.728539573629941\nf 352 354 353\no Road90::pedestrian\nv  26.944570928622493 0.0 -64.17135870698202\nv  27.207429071377508 0.0 -63.082641293017986\nv  30.434010620415304 0.0 -65.01363583281055\nf 355 356 357\nv  30.725989379584693 0.0 -63.93236416718944\nf 356 358 357\no Road91::pedestrian\nv  37.55066733568578 0.0 -67.13292822403858\nf 357 358 359\nv  38.055332664314214 0.0 -66.13307177596141\nf 358 360 359\no Road92::pedestrian\nf 359 360 82\nf 360 83 82\no Road93::pedestrian\nv  22.743631825059847 0.0 -69.08771210009165\nv  23.042368174940155 0.0 -68.00828789990835\nv  25.740899990133645 0.0 -69.91334147772537\nf 361 362 363\nv  25.91310000986636 0.0 -68.80665852227463\nf 362 364 363\no Road94::pedestrian\nv  34.63250143594497 0.0 -70.25626947634473\nf 363 364 365\nv  34.82949856405504 0.0 -69.15373052365527\nf 364 366 365\no Road95::pedestrian\nf 365 366 170\nf 366 171 170\no Road96::pedestrian\nv  37.97016726638514 0.0 -57.00612540399208\nv  38.641832733614855 0.0 -56.10987459600792\nv  40.85797636865778 0.0 -59.14163081323532\nf 367 368 369\nv  41.756023631342224 0.0 -58.47236918676469\nf 368 370 369\no Road97::pedestrian\nv  41.67792101647383 0.0 -61.41414946518468\nf 369 370 371\nv  42.66007898352616 0.0 -60.87585053481533\nf 370 372 371\no Road98::pedestrian\nf 371 372 64\nf 372 65 64\no Road99::pedestrian\nv  34.21835863816449 0.0 -61.43429813593294\nv  34.86764136183551 0.0 -60.52170186406706\nf 373 374 76\nf 374 77 76\no Road100::pedestrian\nv  42.03108301603308 0.0 -52.1584300508326\nv  42.77091698396693 0.0 -51.3175699491674\nv  44.11901286846908 0.0 -53.96693749289674\nf 375 376 377\nv  45.06098713153093 0.0 -53.36106250710326\nf 376 378 377\no Road101::pedestrian\nv  45.236721157306725 0.0 -57.72564132751302\nf 377 378 379\nv  46.275278842693275 0.0 -57.30635867248698\nf 378 380 379\no Road102::pedestrian\nf 379 380 70\nf 380 71 70\no Road Nuumäentie\nv  105.63687879080021 0.0 -105.28319777091751\nv  104.8711212091998 0.0 -106.50880222908249\nv  102.00819755022289 0.0 -103.10273473765996\nf 381 382 383\nv  101.17180244977712 0.0 -104.28126526234003\nf 382 384 383\no Road Nuumäentie\nf 383 384 2\nf 384 3 2\no Road Nuumäentie\nv  81.0452940821743 0.0 -85.70638505790124\nf 5 4 385\nv  80.1987059178257 0.0 -86.87761494209876\nf 4 386 385\no Road Nuumäentie\nv  74.6539192975594 0.0 -81.71025750144993\nf 385 386 387\nv  74.03408070244059 0.0 -83.01574249855007\nf 386 388 387\no Road Nuumäentie\nf 387 388 20\nf 388 21 20\no Road Nuumäentie\nv  61.67117254640675 0.0 -79.60399465491422\nf 23 22 389\nv  61.728827453593254 0.0 -81.04800534508577\nf 22 390 389\no Road Nuumäentie\nv  46.97071447081892 0.0 -80.87510886012694\nf 389 390 391\nv  47.09528552918108 0.0 -82.31489113987305\nf 390 392 391\no Road Nuumäentie\nv  46.97019214140328 0.0 -80.8751542433885\nv  47.09580785859672 0.0 -82.3148457566115\nv  35.48619214140328 0.0 -81.87715424338849\nf 393 394 395\nv  35.61180785859672 0.0 -83.3168457566115\nf 394 396 395\no Road Nuumäentie\nv  35.49499710383927 0.0 -81.87644019844335\nv  35.60300289616073 0.0 -83.31755980155664\nv  17.565550767574223 0.0 -83.22149494537767\nf 397 398 399\nv  17.586449232425778 0.0 -84.66650505462233\nf 398 400 399\no Road Nuumäentie\nv  11.359502744776025 0.0 -82.93830317578241\nf 399 400 401\nv  11.160497255223975 0.0 -84.36969682421758\nf 400 402 401\no Road Nuumäentie\nv  5.577963764054597 0.0 -81.58429777359363\nf 401 402 403\nv  5.082036235945403 0.0 -82.94170222640638\nf 402 404 403\no Road Nuumäentie\nv  0.7513290809135016 0.0 -79.13931655150527\nf 403 404 405\nv  -0.12732908091350165 0.0 -80.28668344849471\nf 404 406 405\no Road Nuumäentie\nv  -2.632545394650105 0.0 -75.37068907189321\nf 405 406 407\nv  -3.7514546053498954 0.0 -76.2853109281068\nf 406 408 407\no Road Nuumäentie\nv  -6.0365402413818945 0.0 -70.78859590748522\nf 407 408 409\nv  -7.343459758618106 0.0 -71.40540409251477\nf 408 410 409\no Road Nuumäentie\nv  -7.680251460514863 0.0 -64.18957473715922\nf 409 410 411\nv  -9.115748539485136 0.0 -64.35642526284077\nf 410 412 411\no Road Nuumäentie\nv  -7.568049241183707 0.0 -56.31933634501808\nf 411 412 413\nv  -9.005950758816295 0.0 -56.17466365498192\nf 412 414 413\no Road Nuumäentie\nf 413 414 156\nf 414 157 156\no Road Nuumäentie\nf 155 160 140\nf 160 141 140\no Road Nuumäentie\nf 143 142 146\nf 142 147 146\no Road Nuumäentie\nv  9.880901033879672 0.0 35.876533149438316\nf 145 150 415\nv  8.483098966120329 0.0 36.24346685056169\nf 150 416 415\no Road Nuumäentie\nv  11.172915626086752 0.0 39.68953805210002\nf 415 416 417\nv  9.821084373913248 0.0 40.20046194789998\nf 416 418 417\no Road Nuumäentie\nv  11.933862137195339 0.0 41.53532575900449\nf 417 418 419\nv  10.718137862804662 0.0 42.31667424099551\nf 418 420 419\no Road Nuumäentie\nf 419 420 116\nf 420 117 116\no Road Nuumäentie\nv  17.99965462583257 0.0 46.387218789446315\nf 115 120 421\nv  17.462345374167434 0.0 47.728781210553684\nf 120 422 421\no Road Nuumäentie\nv  20.37374162648741 0.0 47.00828830272432\nf 421 422 423\nv  20.20625837351259 0.0 48.44371169727568\nf 422 424 423\no Road Nuumäentie\nf 423 424 54\nf 424 55 54\no Road Nuumäentie\nv  57.29668086951207 0.0 46.14647948526007\nf 53 52 425\nv  57.31531913048792 0.0 47.591520514739926\nf 52 426 425\no Road Nuumäentie\nf 425 426 48\nf 426 49 48\no Road Nuumäentie\nf 47 46 270\nf 46 271 270\no Road103::pedestrian\nv  -62.775295852325094 0.0 132.69168913323426\nv  -61.68670414767491 0.0 132.42831086676574\nv  -67.3402958523251 0.0 113.82368913323425\nf 427 428 429\nv  -66.25170414767491 0.0 113.56031086676573\nf 428 430 429\no Road104::pedestrian\nv  127.18700927057706 0.0 -53.244222674457134\nf 57 60 431\nv  128.30699072942295 0.0 -53.237777325542865\nf 60 432 431\no Road105::pedestrian\nv  126.61591487411492 0.0 -61.962616947344515\nf 431 432 433\nv  127.70608512588508 0.0 -62.21938305265549\nf 432 434 433\no Road106::pedestrian\nv  123.24136686603053 0.0 -70.00458585956976\nf 433 434 435\nv  124.29463313396947 0.0 -70.38541414043023\nf 434 436 435\no Road107::pedestrian\nf 435 436 138\nf 436 133 138\no Road108::pedestrian\nf 135 134 114\nf 134 109 114\no Road109::pedestrian\nv  111.8215880632714 0.0 -100.49021592969382\nf 113 112 437\nv  112.5124119367286 0.0 -101.37178407030618\nf 112 438 437\no Road110::pedestrian\nf 437 438 12\nf 438 7 12\no Road111::pedestrian\nv  107.7578772083012 0.0 -107.70194364454902\nf 9 8 439\nv  108.82812279169882 0.0 -108.03205635545099\nf 8 440 439\no Road112::pedestrian\nv  107.76369892818258 0.0 -107.68413289513092\nv  108.82230107181744 0.0 -108.0498671048691\nf 441 442 36\nf 442 31 36\no Road113::pedestrian\nv  107.31805081785022 0.0 -115.63094525848702\nf 33 32 443\nv  108.38394918214978 0.0 -115.28705474151299\nf 32 444 443\no Road114::pedestrian\nv  107.69036734609476 0.0 -117.60210959806862\nf 443 444 445\nv  108.80763265390523 0.0 -117.52389040193138\nf 444 446 445\no Road115::pedestrian\nv  107.54312947717094 0.0 -120.74629414243286\nf 445 446 447\nv  108.64487052282905 0.0 -120.94770585756713\nf 446 448 447\no Road116::pedestrian\nv  106.50054741090398 0.0 -123.93552307636106\nf 447 448 449\nv  107.56545258909603 0.0 -124.28247692363892\nf 448 450 449\no Road117::pedestrian\nv  89.47957121065153 0.0 -176.09045004481933\nf 449 450 451\nv  90.54442878934847 0.0 -176.43754995518069\nf 450 452 451\no Road118::pedestrian\nv  83.83735821688123 0.0 -193.42105393692222\nf 451 452 453\nv  84.90064178311877 0.0 -193.7729460630778\nf 452 454 453\no Road119::pedestrian\nv  78.96886275323166 0.0 -207.89555816650787\nf 453 454 455\nv  80.03313724676835 0.0 -208.24444183349212\nf 454 456 455\no Road120::pedestrian\nv  57.562438966738036 0.0 -274.93313327357384\nf 455 456 457\nv  58.63156103326197 0.0 -275.2668667264262\nf 456 458 457\no Road121::pedestrian\nv  56.81427305628742 0.0 -277.38591522311793\nf 457 458 459\nv  57.885726943712584 0.0 -277.712084776882\nf 458 460 459\no Road122::pedestrian\nv  42.38820177172382 0.0 -324.869587556667\nf 459 460 461\nv  43.45379822827618 0.0 -325.21441244333295\nf 460 462 461\no Road123::pedestrian\nv  33.9027841902848 0.0 -349.56674454722787\nf 461 462 463\nv  34.9392158097152 0.0 -349.9912554527721\nf 462 464 463\no Road124::pedestrian\nv  26.86287836461524 0.0 -364.27111112020225\nf 463 464 465\nv  27.84312163538476 0.0 -364.8128888797977\nf 464 466 465\no Road125::pedestrian\nv  18.22452374931675 0.0 -377.9469678232878\nf 465 466 467\nv  19.171476250683252 0.0 -378.54503217671214\nf 466 468 467\no Road126::pedestrian\nv  31.447591072419435 0.0 -100.96683065514222\nf 219 218 469\nv  31.426408927580567 0.0 -102.23116934485779\nf 218 470 469\no Road127::pedestrian\nv  31.42568521915507 0.0 -100.9668431941936\nv  31.44831478084493 0.0 -102.2311568058064\nv  25.849984255173965 0.0 -101.06674194260174\nf 471 472 473\nv  25.850015744826038 0.0 -102.33125805739826\nf 472 474 473\no Road128::pedestrian\nv  -2.5440254734302306 0.0 -100.65568211073727\nf 473 474 475\nv  -1.8939745265697694 0.0 -101.74031788926271\nf 474 476 475\no Road129::pedestrian\no Road130::pedestrian\nf 99 98 226\nf 98 221 226\no Road131::pedestrian\nv  -7.624962918421957 0.0 -114.33291844949935\nf 223 222 477\nv  -6.385037081578043 0.0 -114.58108155050064\nf 222 478 477\no Road132::pedestrian\nf 477 478 108\nf 478 103 108\no Road133::pedestrian\nv  -11.624485425324893 0.0 -126.65215253526246\nf 105 104 479\nv  -10.421514574675106 0.0 -127.04184746473753\nf 104 480 479\no Road134::pedestrian\nv  -11.8150512248284 0.0 -127.25439447863367\nf 479 480 481\nv  -10.662948775171602 0.0 -127.77560552136633\nf 480 482 481\no Road135::pedestrian\nv  -16.136219768281155 0.0 -134.50548410434496\nf 481 482 483\nv  -15.049780231718843 0.0 -135.15251589565506\nf 482 484 483\no Road136::pedestrian\nv  -16.97302271028727 0.0 -135.922785786417\nf 483 484 485\nv  -15.804977289712728 0.0 -136.407214213583\nf 484 486 485\no Road137::pedestrian\nv  -18.068781779665972 0.0 -140.20627992453663\nf 485 486 487\nv  -16.843218220334027 0.0 -140.51772007546336\nf 486 488 487\no Road138::pedestrian\nv  -19.018826515345147 0.0 -143.94645607217626\nf 487 488 489\nv  -17.79317348465485 0.0 -144.25754392782375\nf 488 490 489\no Road139::pedestrian\nv  -19.960065868996647 0.0 -147.66990577931134\nf 489 490 491\nv  -18.709934131003354 0.0 -147.86009422068864\nf 490 492 491\no Road140::pedestrian\nv  -21.07224594371688 0.0 -168.37491382759856\nf 491 492 493\nv  -19.807754056283123 0.0 -168.36708617240146\nf 492 494 493\no Road141::pedestrian\nv  -21.02095304530092 0.0 -169.16860179821646\nf 493 494 495\nv  -19.75904695469908 0.0 -169.08739820178351\nf 494 496 495\no Road142::pedestrian\nv  -20.446997784442885 0.0 -178.32990046893315\nf 495 496 497\nv  -19.185002215557112 0.0 -178.25009953106684\nf 496 498 497\no Road143::pedestrian\nv  -19.992997237173068 0.0 -185.4439091226773\nf 497 498 499\nv  -18.73100276282693 0.0 -185.3640908773227\nf 498 500 499\no Road144::pedestrian\nv  -19.446403286002212 0.0 -192.14787940619064\nf 499 500 501\nv  -18.483596713997787 0.0 -191.32812059380936\nf 500 502 501\no Road145::pedestrian\nv  -13.2391420252122 0.0 -193.66188011547374\nf 501 502 503\nv  -13.282857974787799 0.0 -192.39811988452627\nf 502 504 503\no Road146::pedestrian\nv  -2.0219614986415864 0.0 -190.31277123797278\nf 503 504 505\nv  -2.4160385013584134 0.0 -189.1112287620272\nf 504 506 505\no Road147::pedestrian\nv  6.155063250528487 0.0 -187.40263363669976\nf 505 506 507\nv  5.730936749471512 0.0 -186.21136636330021\nf 506 508 507\no Road148::pedestrian\nv  13.996497179492726 0.0 -184.60993073228715\nf 507 508 509\nv  13.585502820507275 0.0 -183.41406926771285\nf 508 510 509\no Road149::pedestrian\nv  17.786846569686265 0.0 -183.35417521867944\nf 509 510 511\nv  17.389153430313737 0.0 -182.15382478132054\nf 510 512 511\no Road150::pedestrian\nv  17.785137775115313 0.0 -183.35473866948573\nv  17.39086222488469 0.0 -182.15326133051425\nf 513 514 212\nf 514 213 212\no Road151::pedestrian\nv  36.18090476554613 0.0 -82.61813335451383\nv  34.91709523445387 0.0 -82.57586664548616\nf 515 516 14\nf 516 15 14\no Road152::pedestrian\nf 17 16 162\nf 16 163 162\no Road153::pedestrian\nf 161 166 168\nf 166 169 168\no Road154::pedestrian\nf 167 172 80\nf 172 81 80\no Road155::pedestrian\nf 79 84 74\nf 84 75 74\no Road156::pedestrian\nf 73 78 62\nf 78 63 62\no Road157::pedestrian\nf 61 66 68\nf 66 69 68\no Road158::pedestrian\nf 67 72 174\nf 72 175 174\no Road159::pedestrian\nv  58.97293110940581 0.0 -46.99773025536217\nf 173 178 517\nv  57.851068890594185 0.0 -46.414269744637835\nf 178 518 517\no Road160::pedestrian\nv  68.08029604319832 0.0 -21.930783373853846\nf 517 518 519\nv  66.89170395680169 0.0 -21.499216626146154\nf 518 520 519\no Road161::pedestrian\nf 179 184 188\nf 184 189 188\no Road162::pedestrian\nv  22.75232381953446 0.0 -22.95839075915918\nf 185 190 521\nv  21.971676180465536 0.0 -21.963609240840817\nf 190 522 521\no Road163::pedestrian\nv  28.175888963699077 0.0 -17.54103027455103\nf 521 522 523\nv  27.236111036300922 0.0 -16.694969725448967\nf 522 524 523\no Road164::pedestrian\nv  41.87544980670051 0.0 -0.6167780020148832\nf 523 524 525\nv  40.89255019329949 0.0 0.17877800201488317\nf 524 526 525\no Road165::pedestrian\nv  6.020484965238245 0.0 -42.42453795277617\nf 139 144 527\nv  5.435515034761755 0.0 -41.303462047223825\nf 144 528 527\no Road166::pedestrian\nv  9.013708676449065 0.0 -40.80748515907351\nf 527 528 529\nv  8.002291323550933 0.0 -40.04851484092649\nf 528 530 529\no Road167::pedestrian\nf 529 530 180\nf 530 181 180\no Road168::pedestrian\nv  12.742248328054835 0.0 -22.13864690349984\nf 183 182 531\nv  11.501751671945165 0.0 -21.893353096500157\nf 182 532 531\no Road169::pedestrian\nv  11.623636867908601 0.0 49.58678327982561\nf 119 118 533\nv  10.9183631320914 0.0 48.537216720174385\nf 118 534 533\no Road170::pedestrian\nv  7.408738848257765 0.0 50.816203536404956\nf 533 534 535\nv  7.063261151742235 0.0 49.59979646359504\nf 534 536 535\no Road171::pedestrian\nv  7.37758088869986 0.0 50.82420216110289\nv  7.094419111300139 0.0 49.59179783889711\nv  5.37467539404157 0.0 51.284232787527635\nf 537 538 539\nv  5.11932460595843 0.0 50.04576721247236\nf 538 540 539\no Road172::pedestrian\nf 539 540 204\nf 540 205 204\no Road173::pedestrian\no Road174::pedestrian\nv  -4.6883620900735306 0.0 51.701362390785384\nv  -3.4636379099264683 0.0 51.38663760921461\nf 541 542 202\nf 542 197 202\no Road175::pedestrian\nv  -8.646780767010988 0.0 29.543911580061362\nf 199 198 543\nv  -7.927219232989013 0.0 28.50408841993864\nf 198 544 543\no Road176::pedestrian\nv  -41.09984279342885 0.0 35.481157291390474\nf 543 544 545\nv  -41.41615720657116 0.0 34.256842708609526\nf 544 546 545\no Road177::pedestrian\nv  -45.35702978547348 0.0 36.94031388300939\nf 545 546 547\nv  -45.858970214526515 0.0 35.77968611699061\nf 546 548 547\no Road178::pedestrian\nv  -50.6038661382401 0.0 39.701970473947966\nf 547 548 549\nv  -51.398133861759895 0.0 38.718029526052035\nf 548 550 549\no Road179::pedestrian\nv  -53.569388354102635 0.0 43.20658892779944\nf 549 550 551\nv  -54.73461164589737 0.0 42.71541107220056\nf 550 552 551\no Road180::pedestrian\nv  -53.83010334144183 0.0 45.831587845845675\nf 551 552 553\nv  -55.06989665855818 0.0 46.08041215415433\nf 552 554 553\no Road181::pedestrian\nv  -50.93047837286338 0.0 51.146974060359774\nf 553 554 555\nv  -51.97952162713662 0.0 51.853025939640226\nf 554 556 555\no Road182::pedestrian\nv  -46.04013785271503 0.0 57.07472918918552\nf 555 556 557\nv  -46.97586214728498 0.0 57.92527081081448\nf 556 558 557\no Road183::pedestrian\nf 557 558 194\nf 558 195 194\no Road184::pedestrian\nv  -41.56283339028462 0.0 61.55001870895018\nf 191 196 559\nv  -42.45716660971538 0.0 62.44398129104982\nf 196 560 559\no Road185::pedestrian\nv  -37.144881079644534 0.0 65.97279935454051\nf 559 560 561\nv  -38.17711892035547 0.0 66.70320064545948\nf 560 562 561\no Road186::pedestrian\nv  -33.7963947098081 0.0 72.96767693047056\nf 561 562 563\nv  -34.9376052901919 0.0 73.51232306952943\nf 562 564 563\no Road187::pedestrian\nv  -10.4044809116062 0.0 -51.56564501961699\nf 159 158 565\nv  -10.5255190883938 0.0 -52.82435498038301\nf 158 566 565\no Road188::pedestrian\nv  -10.403044314043926 0.0 -51.56578481870617\nv  -10.526955685956073 0.0 -52.82421518129383\nv  -24.61236515075068 0.0 -50.1708269105357\nf 567 568 569\nv  -24.59163484924932 0.0 -51.43517308946429\nf 568 570 569\no Road189::pedestrian\nf 569 570 248\nf 570 243 248\no Road190::pedestrian\nv  -30.839375676622033 0.0 -50.98654168856339\nf 247 246 571\nv  -30.576624323377963 0.0 -52.2234583114366\nf 246 572 571\no Road191::pedestrian\nv  -37.082967114103035 0.0 -52.84102860927232\nf 571 572 573\nv  -36.86703288589697 0.0 -54.086971390727676\nf 572 574 573\no Road192::pedestrian\nv  -51.66252499053262 0.0 -53.64030877418157\nf 573 574 575\nv  -51.46747500946738 0.0 -54.88969122581843\nf 574 576 575\no Road193::pedestrian\nv  -56.39282855217562 0.0 -54.878254947074154\nf 575 576 577\nv  -55.87917144782438 0.0 -56.03374505292585\nf 576 578 577\no Road194::pedestrian\nv  -76.32627904242476 0.0 -67.97735803087149\nf 577 578 579\nv  -75.59172095757525 0.0 -69.00664196912852\nf 578 580 579\no Road195::pedestrian\nv  -81.18745418114092 0.0 -71.8260787584018\nf 579 580 581\nv  -80.03654581885907 0.0 -72.34992124159818\nf 580 582 581\no Road196::pedestrian\nv  -80.94993049122091 0.0 -76.7973288578987\nf 581 582 583\nv  -79.74406950877908 0.0 -76.4166711421013\nf 582 584 583\no Road197::pedestrian\nv  -76.35812343792927 0.0 -84.27345995109125\nf 583 584 585\nv  -75.29387656207072 0.0 -83.59054004890875\nf 584 586 585\no Road198::pedestrian\nv  -67.6371970783758 0.0 -97.32347882913743\nf 585 586 587\nv  -66.4528029216242 0.0 -96.88052117086258\nf 586 588 587\no Road199::pedestrian\nv  -66.86193764343452 0.0 -103.47609858609854\nf 587 588 589\nv  -65.62406235656547 0.0 -103.21790141390144\nf 588 590 589\no Road200::pedestrian\nv  -64.96530035458368 0.0 -109.97854367519417\nf 589 590 591\nv  -63.796699645416325 0.0 -109.49545632480582\nf 590 592 591\no Road201::pedestrian\nv  -61.872025696775125 0.0 -115.6420380709956\nf 591 592 593\nv  -60.77797430322488 0.0 -115.00796192900441\nf 592 594 593\no Road202::pedestrian\nv  -52.982992326825055 0.0 -130.13851100286772\nf 593 594 595\nv  -51.90500767317495 0.0 -129.47748899713227\nf 594 596 595\no Road203::pedestrian\nv  119.35720512221015 0.0 -100.36932601988197\nf 111 110 597\nv  119.72279487778987 0.0 -99.31067398011804\nf 110 598 597\no Road204::pedestrian\nv  119.35747367457024 0.0 -100.3694186842145\nv  119.72252632542977 0.0 -99.31058131578551\nf 599 600 38\nf 600 40 38\no Road205::pedestrian\nv  130.00753291244126 0.0 -104.03278269016951\nf 37 39 601\nv  130.37046708755872 0.0 -102.97321730983049\nf 39 602 601\no Road206::pedestrian\nf 187 186 94\nf 186 95 94\no Road207::pedestrian\nf 93 92 176\nf 92 177 176\no Road208::pedestrian\nv  29.65800001899898 0.0 -113.74099999761582\nv  29.65800001899898 0.0 -114.86100000238419\nv  18.474859993520223 0.0 -113.8982965631793\nf 603 604 605\nv  19.25314000647978 0.0 -114.7037034368207\nf 604 606 605\no Road209::pedestrian\nv  18.40727590192613 0.0 -115.82583334902482\nf 605 606 607\nv  19.188724098073866 0.0 -116.62816665097519\nf 606 608 607\no Road210::pedestrian\nv  16.002585948646352 0.0 -115.68901739418001\nf 607 608 609\nv  16.01141405135365 0.0 -116.80898260581998\nf 608 610 609\no Road Lähderannantie\nv  111.41367819345479 0.0 -129.23239508638954\nv  110.20232180654523 0.0 -128.86960491361043\nf 611 612 42\nf 612 44 42\no Road Lähderannantie\nv  116.28780766740599 0.0 -112.92396221015736\nf 41 43 613\nv  115.07619233259402 0.0 -112.56203778984263\nf 43 614 613\no Road Lähderannantie\nv  116.28775981584853 0.0 -112.92412232578033\nv  115.07624018415147 0.0 -112.56187767421966\nv  120.14575981584854 0.0 -100.02112232578034\nf 615 616 617\nv  118.93424018415148 0.0 -99.65887767421967\nf 616 618 617\no Road Lähderannantie\nv  120.14617287668194 0.0 -100.01973506883205\nv  118.93382712331807 0.0 -99.66026493116796\nv  121.08017287668194 0.0 -96.86973506883204\nf 619 620 621\nv  119.86782712331807 0.0 -96.51026493116795\nf 620 622 621\no Road211::pedestrian\nv  -7.053351759318173 0.0 -30.733100982009375\nf 149 148 623\nv  -7.188648240681828 0.0 -31.844899017990627\nf 148 624 623\no Road212::pedestrian\nv  -7.052609131364054 0.0 -30.733191857060813\nv  -7.189390868635947 0.0 -31.84480814293919\nf 625 626 228\nf 626 229 228\no Road213::pedestrian\nv  -10.713301381792874 0.0 -30.275986243875614\nf 231 230 627\nv  -10.846698618207125 0.0 -31.388013756124387\nf 230 628 627\no Road214::pedestrian\nf 627 628 154\nf 628 151 154\no Road Nuuniitynkuja\nv  34.82695779570625 0.0 -82.56911101235754\nv  36.27104220429375 0.0 -82.62488898764245\nv  34.59995779570625 0.0 -88.44611101235755\nf 629 630 631\nv  36.04404220429375 0.0 -88.50188898764246\nf 630 632 631\no Road Nuuniitynkuja\nv  34.60239539322335 0.0 -88.40848702175208\nv  36.04160460677666 0.0 -88.53951297824793\nf 633 634 220\nf 634 215 220\no Road Nuuniitynkuja\nv  32.16112607196562 0.0 -118.15966067852116\nf 217 216 635\nv  33.59687392803438 0.0 -118.32433932147885\nf 216 636 635\no Road Nuuniitynkuja\nv  31.435831773142827 0.0 -122.84106145279847\nf 635 636 637\nv  32.86416822685717 0.0 -123.06093854720152\nf 636 638 637\no Road Nuuniitynkuja\nv  31.029117127199306 0.0 -125.49912298104115\nf 637 638 639\nv  32.45288287280069 0.0 -125.74687701895886\nf 638 640 639\no Road Nuuniitynkuja\nv  27.267769196581607 0.0 -144.77674513427874\nf 639 640 641\nv  28.686230803418393 0.0 -145.05325486572124\nf 640 642 641\no Road Nuuniitynkuja\nv  25.012757385023363 0.0 -156.35480573993758\nf 641 642 643\nv  26.43124261497664 0.0 -156.63119426006241\nf 642 644 643\no Road Nuuniitynkuja\nf 643 644 214\nf 644 209 214\no Road Nuuniitynkuja\nv  19.740723821955925 0.0 -183.40697810437365\nf 211 210 645\nv  21.159276178044074 0.0 -183.68302189562633\nf 210 646 645\no Road Nuuniitynkuja\nf 645 646 274\nf 646 276 274\no Road Nuuniitynkuja\nv  12.021322337167913 0.0 -222.42293721037797\nf 273 275 647\nv  13.438677662832088 0.0 -222.70506278962202\nf 275 648 647\no Road Nuuniitynkuja\nv  8.236704003763961 0.0 -241.4608883722812\nf 647 648 649\nv  9.60729599623604 0.0 -241.9191116277188\nf 648 650 649\no Road Nuuniitynkuja\nv  4.154055642631791 0.0 -249.92632152763764\nf 649 650 651\nv  5.4559443573682085 0.0 -250.55367847236238\nf 650 652 651\no Road Nuuniitynkuja\nv  2.280147757875525 0.0 -253.81213044216642\nf 651 652 653\nv  3.581852242124475 0.0 -254.4398695578336\nf 652 654 653\no Road215::pedestrian\nv  105.53125245087213 0.0 -100.97021032733123\nf 11 10 655\nv  105.05474754912788 0.0 -101.98378967266878\nf 10 656 655\no Road216::pedestrian\nv  96.57773161406196 0.0 -93.82483762824265\nf 655 656 657\nv  95.88026838593804 0.0 -94.70116237175736\nf 656 658 657\no Road217::pedestrian\nv  96.56919067252427 0.0 -93.81817384407191\nv  95.88880932747573 0.0 -94.7078261559281\nv  79.7060573526698 0.0 -80.92180803816275\nf 659 660 661\nv  79.03994264733021 0.0 -81.82219196183725\nf 660 662 661\no Road218::pedestrian\nv  74.8736097446507 0.0 -77.47768720539023\nf 661 662 663\nv  74.41239025534931 0.0 -78.49831279460977\nf 662 664 663\no Road219::pedestrian\nv  68.96049737889557 0.0 -76.09547418410965\nf 663 664 665\nv  68.70750262110444 0.0 -77.18652581589036\nf 664 666 665\no Road220::pedestrian\nv  68.89120279885802 0.0 -76.083929230283\nv  68.77679720114199 0.0 -77.198070769717\nv  63.954338285397235 0.0 -75.58000038856792\nf 667 668 669\nv  63.95566171460276 0.0 -76.69999961143208\nf 668 670 669\no Road221::pedestrian\nf 669 670 260\nf 670 255 260\no Road222::pedestrian\nf 259 258 18\nf 258 13 18\no Road223::pedestrian\nv  20.631394621943972 0.0 119.86513269150728\nf 283 284 671\nv  19.529419220996363 0.0 120.06525821808376\nf 284 672 671\nv  21.260987700473805 0.0 123.33193723671177\nf 671 672 673\nv  20.159012299526196 0.0 123.53206276328824\nf 672 674 673\no Road Nuumäentie\nv  121.97080961474789 0.0 -116.0247834130039\nv  121.1751903852521 0.0 -117.2312165869961\nv  116.0798096147479 0.0 -112.1397834130039\nf 675 676 677\nv  115.2841903852521 0.0 -113.34621658699609\nf 676 678 677\no Road Nuumäentie\nv  116.0799858260065 0.0 -112.13989965784207\nv  115.2840141739935 0.0 -113.34610034215792\nv  108.69098582600651 0.0 -107.26389965784207\nf 679 680 681\nv  107.8950141739935 0.0 -108.47010034215793\nf 680 682 681\no Road Nuumäentie\nv  108.68618778726247 0.0 -107.26076068721936\nv  107.89981221273754 0.0 -108.47323931278065\nf 683 684 381\nf 684 382 381\no Road224::pedestrian\nv  31.73300153188638 0.0 -94.33131087020226\nf 27 26 685\nv  32.85299846811362 0.0 -94.32868912979774\nf 26 686 685\no Road225::pedestrian\nv  30.8808429742963 0.0 -101.53350675278548\nf 685 686 687\nv  31.993157025703702 0.0 -101.66449324721452\nf 686 688 687\no Road226::pedestrian\nv  30.87849178579695 0.0 -101.55815177680722\nv  31.995508214203053 0.0 -101.63984822319279\nv  30.011493551493306 0.0 -113.41241611709232\nf 689 690 691\nv  31.12850997989941 0.0 -113.49411256347788\nf 690 692 691\no Road227::pedestrian\nv  29.847324017421563 0.0 -115.07534308702024\nv  30.958752082507544 0.0 -115.21364589545296\nv  28.462754586401037 0.0 -126.20428486290155\nf 693 694 695\nv  29.56924541359896 0.0 -126.37771513709845\nf 694 696 695\no Road228::pedestrian\nv  27.99614326300055 0.0 -128.66689829668377\nf 695 696 697\nv  29.095856736999448 0.0 -128.87910170331622\nf 696 698 697\no Road229::pedestrian\nv  26.41653566398833 0.0 -136.70288457038174\nf 697 698 699\nv  27.51546433601167 0.0 -136.91911542961827\nf 698 700 699\no Road230::pedestrian\nv  24.847011535690193 0.0 -144.67149358061613\nf 699 700 701\nv  25.94498846430981 0.0 -144.8925064193839\nf 700 702 701\no Road231::pedestrian\nv  17.03947621013607 0.0 -182.6412096873681\nf 701 702 703\nv  18.136523789863933 0.0 -182.86679031263188\nf 702 704 703\no Road232::pedestrian\nv  17.03682315784791 0.0 -182.6549843883747\nv  18.139176842152093 0.0 -182.8530156116253\nv  15.069843962104983 0.0 -193.60446131511281\nf 705 706 707\nv  16.170156037895016 0.0 -193.8135386848872\nf 706 708 707\no Road233::pedestrian\nv  5.822470833391997 0.0 -239.7562669416934\nf 707 708 709\nv  6.893529166608002 0.0 -240.08373305830656\nf 708 710 709\no Road234::pedestrian\nv  4.0900447000975895 0.0 -243.93962299005477\nf 709 710 711\nv  4.989955299902411 0.0 -244.60637700994522\nf 710 712 711\no Road235::pedestrian\nv  2.468647479700759 0.0 -245.26051511753545\nf 711 712 713\nv  3.1393525202992407 0.0 -246.15748488246456\nf 712 714 713\no Road236::pedestrian\nv  -0.6493034227078365 0.0 -247.36151573142314\nf 713 714 715\nv  0.01330342270783641 0.0 -248.26448426857684\nf 714 716 715\no Road237::pedestrian\nv  -3.1872138050085663 0.0 -249.4305854812698\nf 715 716 717\nv  -2.2247861949914336 0.0 -250.00341451873024\nf 716 718 717\no Road238::pedestrian\nv  -4.0753430633454295 0.0 -254.21691417446357\nf 717 718 719\nv  -2.97265693665457 0.0 -254.41308582553643\nf 718 720 719\no Road239::pedestrian\nv  47.592994246818876 0.0 -81.59246106320512\nf 265 264 721\nv  46.47300575318113 0.0 -81.59753893679488\nf 264 722 721\no Road240::pedestrian\nv  47.592983740274946 0.0 -81.59073229593693\nv  46.47301625972506 0.0 -81.59926770406307\nf 723 724 256\nf 724 257 256\no Road241::pedestrian\nv  -10.401889183715676 0.0 -110.3189412623923\nv  -10.418110816284324 0.0 -109.1990587376077\nf 725 726 224\nf 726 225 224\no Road242::pedestrian\nv  -17.07459988266561 0.0 -117.72197396493326\nv  -17.085400117334387 0.0 -116.60202603506676\nf 727 728 106\nf 728 107 106\no Road243::pedestrian\nv  -10.144482254780007 0.0 -100.577973166815\nv  -10.133517745219992 0.0 -99.458026833185\nv  -4.598558957664121 0.0 -100.60443244941544\nf 729 730 731\nv  -4.239441042335878 0.0 -99.54356755058456\nf 730 732 731\no Road244::pedestrian\nf 731 732 100\nf 732 101 100\no Road245::pedestrian\nv  96.69903571110606 0.0 -94.68586721518454\nf 1 6 733\nv  95.75896428889394 0.0 -93.84013278481547\nf 6 734 733\no Road246::pedestrian\nv  96.73041587508355 0.0 -94.64813941840211\nv  95.72758412491645 0.0 -93.8778605815979\nv  115.59243730098177 0.0 -69.88943065844353\nf 735 736 737\nv  115.24156269901823 0.0 -68.67456934155646\nf 736 738 737\no Road247::pedestrian\nf 737 738 136\nf 738 137 136\no Road248::pedestrian\nv  69.46334826288457 0.0 -76.70158890490093\nf 19 24 739\nv  68.20465173711544 0.0 -76.58041109509908\nf 24 740 739\no Road249::pedestrian\nv  69.43604151583972 0.0 -76.83412240832781\nv  68.23195848416029 0.0 -76.4478775916722\nv  70.88259165154955 0.0 -72.30157824539204\nf 741 742 743\nv  69.74940834845046 0.0 -71.74042175460796\nf 742 744 743\no Road250::pedestrian\nv  75.43251985343538 0.0 -65.79278365494626\nf 743 744 745\nv  74.39548014656462 0.0 -65.06921634505373\nf 744 746 745\no Road251::pedestrian\nusemtl EARTH_0\nv  -21.68845195380451 0.0 -282.08453030664924\nv  -22.72954804619549 0.0 -282.49746969335075\nv  -24.892335577054038 0.0 -274.0076724185167\nf 747 748 749\nv  -25.945664422945963 0.0 -274.3883275814833\nf 748 750 749\no Road252::pedestrian\nv  -32.505801367301885 0.0 -250.71934230940656\nf 749 750 751\nv  -33.55219863269812 0.0 -251.11865769059347\nf 750 752 751\no Road253::pedestrian\nv  -69.73687974436987 0.0 -165.79177434165786\nf 751 752 753\nv  -70.78512025563012 0.0 -166.18622565834215\nf 752 754 753\no Road254::pedestrian\nv  -79.91662983866159 0.0 -133.6569862407924\nf 753 754 755\nv  -80.93137016133842 0.0 -134.1310137592076\nf 754 756 755\no Road255::pedestrian\nv  -101.20100212210819 0.0 -100.24993702451916\nf 755 756 757\nv  -102.1129978778918 0.0 -100.90006297548085\nf 756 758 757\no Road256::pedestrian\nv  -110.18709780814135 0.0 -88.94691640571779\nf 757 758 759\nv  -111.16490219185866 0.0 -89.49308359428221\nf 758 760 759\no Road257::pedestrian\nv  -112.95133121139736 0.0 -81.37474254081283\nf 759 760 761\nv  -114.07066878860263 0.0 -81.41325745918718\nf 760 762 761\no Road258::pedestrian\nv  -109.61943696631221 0.0 -69.82243190312559\nf 761 762 763\nv  -110.6605630336878 0.0 -69.40956809687441\nf 762 764 763\no Road259::pedestrian\nv  -93.7969563763961 0.0 -39.09476001393233\nf 763 764 765\nv  -94.57304362360391 0.0 -38.287239986067675\nf 764 766 765\no Road260::pedestrian\nv  -85.61506708724315 0.0 -35.274396845695996\nf 765 766 767\nv  -85.72293291275685 0.0 -34.159603154304\nf 766 768 767\no Road261::pedestrian\nv  -76.935470366676 0.0 -37.46328372319923\nf 767 768 769\nv  -76.76252963332401 0.0 -36.35671627680076\nf 768 770 769\no Road262::pedestrian\nf 769 770 128\nf 770 129 128\no Road263::pedestrian\nv  -51.56363177047729 0.0 -36.690917165005466\nf 127 132 771\nv  -51.54436822952271 0.0 -35.571082834994534\nf 132 772 771\no Road264::pedestrian\nf 771 772 124\nf 772 125 124\ng Waterway\no Waterway265\nusemtl WATER_0\nv  110.40411511892556 0.0 122.73048800225575\nv  111.42188486843614 0.0 118.90351204526623\nv  97.8303399757163 0.0 119.36574413233338\nf 773 774 775\nv  99.38166000501997 0.0 115.72225591291013\nf 774 776 775\no Waterway266\nv  88.35174230350596 0.0 113.63870728226337\nf 775 776 777\nv  90.24625767296865 0.0 110.1612927609179\nf 776 778 777\no Waterway267\nv  85.06144504476718 0.0 111.97089795558539\nf 777 778 779\nv  85.84455494550843 0.0 108.08910209261734\nf 778 780 779\no Waterway268\nv  81.45686932260494 0.0 112.22131379317894\nf 779 780 781\nv  80.85913068481756 0.0 108.30668625543146\nf 780 782 781\no Waterway269\nv  72.5849718015813 0.0 114.38882822716643\nf 781 782 783\nv  72.80902819563644 0.0 110.43517182192865\nf 782 784 783\no Waterway270\nv  68.00691398524536 0.0 112.72100015870953\nf 783 784 785\nv  70.03708598954472 0.0 109.32099988351044\nf 784 786 785\no Waterway271\nv  64.48010517875787 0.0 109.61420420549007\nf 785 786 787\nv  66.22589479956355 0.0 106.05979583864729\nf 786 788 787\no Waterway272\nv  62.79771644263123 0.0 109.16768883919458\nf 787 788 789\nv  61.928283568165064 0.0 105.30431120877944\nf 788 790 789\no Waterway273\nv  61.03985577334863 0.0 110.65337436590721\nf 789 790 791\nv  58.13814426268377 0.0 107.9586256675552\nf 790 792 791\no Waterway274\nv  59.49045555486308 0.0 113.12224371519802\nf 791 792 793\nv  55.92954448935501 0.0 111.38975630631538\nf 792 794 793\no Waterway275\nv  57.02321121780313 0.0 119.81841060204012\nf 793 794 795\nv  53.95278882032426 0.0 117.31758942901416\nf 794 796 795\no Waterway276\nv  52.76071407648653 0.0 122.44794704544195\nf 795 796 797\nv  51.639285937438956 0.0 118.65005300171892\nf 796 798 797\no Waterway277\nv  47.331268710725844 0.0 122.64515663025465\nf 797 798 799\nv  48.00473128091134 0.0 118.74284341820287\nf 798 800 799\no Waterway278\nv  34.62519715512902 0.0 117.5649287414872\nf 799 800 801\nv  34.65880284445368 0.0 113.60507130768485\nf 800 802 801\no Waterway279\nv  25.84175265868183 0.0 120.96937825840627\nf 801 802 803\nv  24.80824735415186 0.0 117.14662178906335\nf 802 804 803\no Waterway280\nv  20.417890216652882 0.0 121.91622215436566\nf 803 804 805\nv  19.74292363466894 0.0 118.01416880367967\nf 804 806 805\nv  17.733314226167312 0.0 122.33894312895205\nf 805 806 807\nv  17.862685772226193 0.0 118.38105692019553\nf 806 808 807\no Waterway281\nv  10.990058086072588 0.0 120.72952419320882\nf 807 808 809\nv  12.51394189500438 0.0 117.07447585217824\nf 808 810 809\no Waterway282\nv  2.5257544585887177 0.0 115.50749544415912\nf 809 810 811\nv  5.050245510063075 0.0 112.45650459372698\nf 810 812 811\no Waterway283\nv  -3.8682411701442843 0.0 108.54395883311196\nf 811 812 813\nv  -0.7137588690269244 0.0 106.15004119661482\nf 812 814 813\no Waterway284\nv  -11.755490248368027 0.0 95.8545205847103\nf 813 814 815\nv  -8.688509789716615 0.0 93.3494794463964\nf 814 816 815\no Waterway285\nv  -17.612428639061932 0.0 90.30528137363422\nf 815 816 817\nv  -15.353571388987735 0.0 87.05271866675493\nf 816 818 817\no Waterway286\nv  -22.06920217856858 0.0 88.13584401675291\nf 817 818 819\nv  -20.60079783966553 0.0 84.45815602891525\nf 818 820 819\no Waterway287\nv  -26.22809746121728 0.0 86.81719643727291\nf 819 820 821\nv  -25.61590254638473 0.0 82.90480361130976\nf 820 822 821\no Waterway288\nv  -30.229621744326455 0.0 86.77131477020193\nf 821 822 823\nv  -30.98837824625159 0.0 82.88468527806083\nf 822 824 823\no Waterway289\nv  -46.28042425507535 0.0 93.43829185473805\nf 823 824 825\nv  -47.24557573293974 0.0 89.59770819295292\nf 824 826 825\no Waterway290\nv  -51.856690109426374 0.0 93.92122576182507\nf 825 826 827\nv  -51.01930990097191 0.0 90.05077428623679\nf 826 828 827\no Waterway291\nv  -58.24245712920677 0.0 90.30356178824931\nf 827 828 829\nv  -55.797542901153285 0.0 87.18843825043315\nf 828 830 829\no Waterway292\nv  -64.09347756863832 0.0 84.24396964601554\nf 829 830 831\nv  -61.120522468278764 0.0 81.62803038646825\nf 830 832 831\no Waterway293\nv  -75.7891894090688 0.0 69.68040897524232\nf 831 832 833\nv  -73.0448106250099 0.0 66.82559106020777\nf 832 834 833\no Waterway294\nv  -79.48395618453621 0.0 66.95085591590238\nf 833 834 835\nv  -77.43004384096852 0.0 63.565144126140154\nf 834 836 835\no Waterway295\nv  -85.85295405571689 0.0 63.83955161630472\nf 835 836 837\nv  -84.65704595913346 0.0 60.06444843057312\nf 836 838 837\no Waterway296\nv  -92.33814891859548 0.0 62.80159362044793\nf 837 838 839\nv  -92.01985108535703 0.0 58.854406428566804\nf 838 840 839\no Waterway297\nv  -100.30698029300919 0.0 62.805105697548434\nf 839 840 841\nv  -100.52101970433294 0.0 58.85089435155353\nf 840 842 841\no Waterway298\nv  -106.0478095820022 0.0 63.42054961499148\nf 841 842 843\nv  -106.88219040763678 0.0 59.54945043307841\nf 842 844 843\no Waterway299\nv  -115.45751301371797 0.0 66.50911565844751\nf 843 844 845\nv  -116.91448696818986 0.0 62.826884387277104\nf 844 846 845\no Waterway300\nv  -135.43081158074818 0.0 75.84583397525942\nf 845 846 847\nv  -137.1071883984352 0.0 72.25816606929094\nf 846 848 847\ng Road\no Road301::pedestrian\nusemtl ASPHALT_0\nv  -53.205680665264765 0.0 31.055243734620877\nv  -52.00231933473523 0.0 30.666756265379124\nv  -61.495567171412134 0.0 5.327270301810092\nf 849 850 851\nv  -60.236432828587866 0.0 5.210729698189908\nf 850 852 851\no Road302::pedestrian\nv  -61.05535664784869 0.0 1.908948302104729\nf 851 852 853\nv  -59.87064335215131 0.0 2.351051697895271\nf 852 854 853\no Road303::pedestrian\nv  -55.28593670076156 0.0 -6.797345748046873\nf 853 854 855\nv  -54.28806329923844 0.0 -6.020654251953126\nf 854 856 855\no Road304::pedestrian\nf 855 856 252\nf 856 253 252\no Road305::pedestrian\nv  -29.54818011017659 0.0 25.990340241850973\nv  -28.297819889823405 0.0 25.80165975814903\nv  -30.142355174072037 0.0 22.040913273993418\nf 857 858 859\nv  -28.909644825927963 0.0 21.75908672600658\nf 858 860 859\no Road306::pedestrian\nv  -37.07846256463602 0.0 -0.4292938942759109\nf 859 860 861\nv  -35.955537435363986 0.0 -1.010706105724089\nf 860 862 861\no Road307::pedestrian\nv  -44.32124608858306 0.0 -9.854825641567743\nf 861 862 863\nv  -43.168753911416935 0.0 -10.375174358432258\nf 862 864 863\no Road308::pedestrian\nf 863 864 254\nf 864 249 254\no Road309::pedestrian\nf 251 250 238\nf 250 233 238\no Road310::pedestrian\nf 235 234 152\nf 234 153 152\no Road311::pedestrian\nv  -29.171819720775837 0.0 -67.47130166535807\nf 245 244 865\nv  -27.922180279224165 0.0 -67.66469833464193\nf 244 866 865\no Road312::pedestrian\nv  -35.214637953807134 0.0 -90.9171671127509\nf 865 866 867\nv  -34.005362046192865 0.0 -91.2868328872491\nf 866 868 867\no Road313::pedestrian\nv  -38.383811500227914 0.0 -99.84043698908108\nf 867 868 869\nv  -37.19218849977208 0.0 -100.26356301091893\nf 868 870 869\no Road314::pedestrian\nf 241 240 236\nf 240 237 236\no Road315::pedestrian\nv  -10.213750997279226 0.0 -24.824383777696955\nf 227 232 871\nv  -11.478249002720775 0.0 -24.817616222303048\nf 232 872 871\no Road316::pedestrian\nv  -4.725848381506901 0.0 4.158194132044697\nf 871 872 873\nv  -5.980151618493099 0.0 3.9978058679553032\nf 872 874 873\no Road317::pedestrian\nv  -6.208375954409683 0.0 7.2973499040619165\nf 873 874 875\nv  -7.459624045590316 0.0 7.114650095938084\nf 874 876 875\no Road318::pedestrian\nv  -4.2640524914421 0.0 19.78913101158253\nf 875 876 877\nv  -5.513947508557901 0.0 19.980868988417473\nf 876 878 877\no Road319::pedestrian\nv  91.05455559256032 0.0 -25.14586442267786\nf 59 58 879\nv  91.07944440743967 0.0 -26.410135577322137\nf 58 880 879\ng Building\no Building320\nusemtl BUILDING_DEFAULT_0\nv  98.053 0.0 -59.063\nv  83.64 0.0 -81.784\nv  92.532 0.0 -88.686\nf 881 882 883\nv  95.461 0.0 -90.634\nf 881 883 884\nv  117.086 0.0 -64.941\nf 881 884 885\nv  98.799 0.0 -42.966\nf 886 881 885\nv  117.34 0.0 -32.068\nf 886 885 887\nv  98.937 0.0 -37.01\nf 886 887 888\no Building321\nv  45.971 0.0 65.058\nv  47.541 0.0 56.342\nv  56.472 0.0 57.967\nf 889 890 891\nv  54.902 0.0 66.672\nf 889 891 892\no Building322\nv  -26.177 0.0 57.41\nv  -16.472 0.0 55.629\nv  -13.305 0.0 72.906\nf 893 894 895\nv  -23.01 0.0 74.698\nf 893 895 896\no Building323\nv  20.804 0.0 60.962\nv  33.576 0.0 58.59\nv  35.908 0.0 71.192\nf 897 898 899\nv  26.419 0.0 72.95\nv  25.999 0.0 70.668\nf 899 900 901\nf 897 899 901\nv  22.716 0.0 71.281\nf 897 901 902\no Building324\nv  21.053 0.0 86.286\nv  39.102 0.0 82.869\nv  42.197 0.0 99.199\nf 903 904 905\nv  33.128 0.0 100.913\nv  32.465 0.0 97.407\nf 905 906 907\nf 903 905 907\nv  23.484 0.0 99.11\nf 903 907 908\no Building325\nv  -76.821 0.0 116.019\nv  -73.925 0.0 132.249\nv  -83.945 0.0 134.041\nf 909 910 911\nv  -85.57 0.0 124.935\nv  -84.707 0.0 124.779\nf 911 912 913\nf 909 911 913\nv  -85.973 0.0 117.655\nf 909 913 914\no Building326\nv  60.13 0.0 95.57\nv  59.589 0.0 92.876\nv  61.816 0.0 92.431\nf 915 916 917\nv  62.352 0.0 95.114\nf 915 917 918\no Building 717\nv  -18.428 0.0 -77.22\nv  -18.545 0.0 -79.557\nv  -14.643 0.0 -79.825\nf 919 920 921\nv  -14.477 0.0 -77.509\nf 919 921 922\no Building327\nv  -48.879 0.0 -41.575\nv  -49.283 0.0 -46.551\nv  -17.76 0.0 -49.066\nf 923 924 925\nv  -17.395 0.0 -44.09\nf 923 925 926\no Building328\nv  109.095 0.0 -21.915\nv  108.708 0.0 -11.808\nv  99.777 0.0 -12.153\nf 927 928 929\nv  99.998 0.0 -17.874\nf 927 929 930\nv  90.752 0.0 -22.628\nf 931 927 930\nv  90.587 0.0 -18.231\nf 931 930 932\no Building329\nv  -24.944 0.0 -56.959\nv  -25.32 0.0 -62.024\nv  -17.434 0.0 -62.625\nf 933 934 935\nv  -17.008 0.0 -57.527\nf 933 935 936\no Building330\nv  -33.4 0.0 -147.52\nv  -23.695 0.0 -147.787\nv  -23.231 0.0 -131.167\nf 937 938 939\nv  -17.478 0.0 -131.334\nv  -16.975 0.0 -113.399\nf 939 940 941\nf 937 939 941\nv  -10.515 0.0 -113.578\nv  -10.012 0.0 -95.399\nf 941 942 943\nf 937 941 943\nv  -19.954 0.0 -95.12\nv  -20.446 0.0 -112.854\nf 943 944 945\nf 937 943 945\nv  -26.895 0.0 -112.676\nv  -27.37 0.0 -129.975\nf 945 946 947\nf 937 945 947\nv  -32.913 0.0 -129.82\nf 937 947 948\no Building331\nv  50.951 0.0 -43.022\nv  58.903 0.0 -12.91\nv  51.161 0.0 -10.861\nf 949 950 951\nv  43.208 0.0 -40.973\nf 949 951 952\no Building332\nv  18.56 0.0 -30.71\nv  16.504 0.0 -42.476\nv  20.566 0.0 -43.256\nf 953 954 955\nv  22.771 0.0 -31.467\nf 953 955 956\no Building333\nv  11.099 0.0 -69.193\nv  17.626 0.0 -74.748\nv  29.508 0.0 -60.777\nf 957 958 959\nv  32.531 0.0 -63.349\nv  44.971 0.0 -48.721\nf 959 960 961\nf 957 959 961\nv  38.527 0.0 -43.233\nv  26.662 0.0 -57.193\nf 961 962 963\nf 957 961 963\nv  23.556 0.0 -54.555\nf 957 963 964\no Building334\nv  6.203 0.0 -138.815\nv  15.935 0.0 -138.837\nv  16.012 0.0 -112.709\nf 965 966 967\nv  6.28 0.0 -112.687\nf 965 967 968\ng Water\no Water Lippajärvi\nusemtl WATER_0\nv  -1586.326 0.0 973.129\nv  -1587.309 0.0 977.447\nv  -1581.424 0.0 973.719\nf 969 970 971\nv  -1579.429 0.0 986.161\nf 971 970 972\nv  -1577.693 0.0 965.283\nf 971 972 973\nv  -1516.112 0.0 1044.12\nf 974 973 972\nv  -1579.014 0.0 1002.264\nf 975 974 972\nv  -1574.162 0.0 932.897\nf 976 973 974\nv  -1575.825 0.0 1018.913\nf 975 977 974\nv  -1505.567 0.0 1047.87\nf 976 974 978\nv  -1578.677 0.0 954.098\nf 979 973 976\nv  -1524.717 0.0 1047.726\nf 980 974 977\nv  -1578.18 0.0 1011.557\nf 977 975 981\nv  -1511.398 0.0 1047.036\nf 974 982 978\nv  -1552.576 0.0 873.445\nf 983 976 978\nv  -1582.993 0.0 947.031\nf 984 979 976\nv  -1544.28 0.0 1049.395\nf 980 977 985\nv  -1582.347 0.0 1009.476\nf 975 986 981\nv  -1535.311 0.0 849.117\nf 983 978 987\nv  -1571.216 0.0 910.528\nf 983 988 976\nv  -1557.875 0.0 1044.676\nf 977 989 985\nv  -1539.981 0.0 1050.786\nf 985 990 980\nv  -1437.592 0.0 1046.902\nf 991 987 978\nv  -1545.85 0.0 855.883\nf 987 992 983\nv  -1573.963 0.0 915.046\nf 988 993 976\nv  -1563.761 0.0 887.969\nf 988 983 994\nv  -1548.304 0.0 1051.198\nf 985 989 995\nv  -1562.181 0.0 1043.007\nf 977 996 989\nv  -1529.574 0.0 1050.786\nf 990 997 980\nv  -1470.751 0.0 1053.379\nf 998 991 978\nv  -1401.681 0.0 1034.939\nf 999 987 991\nv  -1562.584 0.0 879.722\nf 983 1000 994\nv  -1569.846 0.0 901.502\nf 1001 988 994\nv  -1554.184 0.0 1051.087\nf 989 1002 995\nv  -1580.539 0.0 1022.374\nf 977 1003 996\nv  -1494.885 0.0 1051.198\nf 998 978 1004\nv  -1502.738 0.0 826.357\nf 1005 987 999\nv  -1566.9 0.0 891.297\nf 994 1006 1001\nv  -1548.027 0.0 1053.969\nf 995 1002 1007\nv  -1584.845 0.0 1031.121\nf 1003 1008 996\nv  -1500.157 0.0 1050.363\nf 978 1009 1004\nv  -1485.401 0.0 1052.99\nf 1004 1010 998\nv  -1374.015 0.0 1027.872\nf 1011 1005 999\nv  -1520.787 0.0 832.044\nf 987 1005 1012\nv  -1573.178 0.0 892.087\nf 1013 1001 1006\nv  -1584.148 0.0 1041.939\nf 1008 1014 996\nv  -1499.881 0.0 1054.392\nf 1009 1015 1004\nv  -1484.429 0.0 816.229\nf 1011 1016 1005\nv  -1508.231 0.0 823.207\nf 1017 1012 1005\nv  -1532.957 0.0 842.25\nf 987 1012 1018\nv  -1571.216 0.0 890.907\nf 1019 1013 1006\nv  -1564.397 0.0 1046.201\nf 996 1014 1020\nv  -1495.719 0.0 1059.11\nf 1004 1015 1021\nv  -1458.979 0.0 796.919\nf 1011 1022 1016\nv  -1580.418 0.0 1048.226\nf 1020 1014 1023\nv  -1498.217 0.0 1058.832\nf 1015 1024 1021\nv  -1484.097 0.0 809.474\nf 1022 1025 1016\nv  -1344.051 0.0 1020.738\nf 1011 1026 1022\nv  -1564.258 0.0 1052.722\nf 1020 1023 1027\nv  -1496.41 0.0 1063.128\nf 1024 1028 1021\nv  -1452.154 0.0 791.8\nf 1029 1022 1026\nv  -1565.507 0.0 1055.082\nf 1027 1023 1030\nv  -1497.935 0.0 1063.551\nf 1024 1031 1028\nv  -1435.221 0.0 778.945\nf 1026 1032 1029\nv  -1443.323 0.0 782.974\nf 1029 1032 1033\nv  -1419.675 0.0 769.329\nf 1034 1032 1026\nv  -1449.109 0.0 786.213\nf 1033 1035 1029\nv  -1410.551 0.0 764.332\nf 1036 1034 1026\nv  -1253.942 0.0 995.097\nf 1037 1036 1026\nv  -1287.797 0.0 1013.404\nf 1037 1026 1038\nv  -1403.295 0.0 758.344\nf 1039 1036 1037\nv  -1318.436 0.0 1024.533\nf 1026 1040 1038\nv  -1385.339 0.0 741.472\nf 1037 1041 1039\nv  -1397.895 0.0 751.377\nf 1039 1041 1042\nv  -1223.414 0.0 982.878\nf 1037 1043 1041\nv  -1165.469 0.0 771.188\nf 1044 1041 1043\nv  -1205.519 0.0 980.385\nf 1043 1045 1044\nv  -1202.441 0.0 719.402\nf 1046 1041 1044\nv  -1144.313 0.0 803.92\nf 1044 1045 1047\nv  -1189.122 0.0 732.29\nf 1048 1046 1044\nv  -1345.101 0.0 704.833\nf 1041 1046 1049\nv  -1162.783 0.0 963.736\nf 1045 1050 1047\nv  -1147.745 0.0 797.042\nf 1051 1044 1047\nv  -1170.774 0.0 740.47\nf 1052 1048 1044\nv  -1217.456 0.0 707.437\nf 1053 1049 1046\nv  -1135.675 0.0 809.117\nf 1054 1047 1050\nv  -1195.251 0.0 979.417\nf 1055 1050 1045\nv  -1210.294 0.0 711.844\nf 1056 1053 1046\nv  -1317.833 0.0 687.637\nf 1053 1057 1049\nv  -1140.859 0.0 957.214\nf 1054 1050 1058\nv  -1337.248 0.0 698.411\nf 1049 1057 1059\nv  -1228.935 0.0 696.841\nf 1057 1053 1060\nv  -1115.073 0.0 812.545\nf 1054 1058 1061\nv  -1221.673 0.0 702.139\nf 1053 1062 1060\nv  -1274.064 0.0 677.609\nf 1060 1063 1057\nv  -1114.498 0.0 954.443\nf 1061 1058 1064\nv  -1225.696 0.0 697.921\nf 1062 1065 1060\nv  -1301.337 0.0 678.299\nf 1057 1063 1066\nv  -1238.551 0.0 689.284\nf 1063 1060 1067\nv  -1092.602 0.0 814.337\nf 1061 1064 1068\nv  -1296.827 0.0 675.35\nf 1069 1066 1063\nv  -1234.721 0.0 691.143\nf 1070 1067 1060\nv  -1258.562 0.0 676.73\nf 1067 1071 1063\nv  -1035.442 0.0 917.762\nf 1068 1064 1072\nv  -1277.888 0.0 675.16\nf 1073 1069 1063\nv  -1231.09 0.0 690.564\nf 1074 1070 1060\nv  -1271.809 0.0 675.65\nf 1075 1063 1071\nv  -1240.12 0.0 685.355\nf 1076 1071 1067\nv  -1047.788 0.0 937.594\nf 1077 1072 1064\nv  -1076.509 0.0 815.172\nf 1078 1068 1072\nv  -1283.68 0.0 672.211\nf 1073 1079 1069\nv  -1269.256 0.0 672.411\nf 1080 1075 1071\nv  -1246.791 0.0 676.529\nf 1081 1071 1076\nv  -1049.861 0.0 815.94\nf 1082 1078 1072\nv  -1279.463 0.0 672.512\nf 1083 1079 1073\nv  -1042.698 0.0 818.399\nf 1084 1082 1072\nv  -1053.099 0.0 813.391\nf 1085 1078 1082\nv  -1023.273 0.0 827.915\nf 1072 1086 1084\nv  -1071.513 0.0 810.731\nf 1087 1078 1085\nv  -1030.435 0.0 822.028\nf 1088 1084 1086\nv  -1024.477 0.0 912.898\nf 1086 1072 1089\nv  -1018.072 0.0 828.605\nf 1086 1089 1090\nv  -1002.372 0.0 833.313\nf 1091 1090 1089\nv  -1015.596 0.0 913.177\nf 1089 1092 1091\nv  -1009.048 0.0 829.195\nf 1093 1090 1091\nv  -1004.079 0.0 911.797\nf 1091 1092 1094\nv  -1011.894 0.0 827.525\nf 1095 1090 1093\nv  -995.801 0.0 835.861\nf 1091 1094 1096\nv  -988.246 0.0 838.711\nf 1096 1094 1097\nv  -997.005 0.0 913.177\nf 1094 1098 1097\nv  -985.488 0.0 921.501\nf 1097 1098 1099\nv  -965.096 0.0 929.414\nf 1097 1099 1100\nv  -978.138 0.0 837.531\nf 1101 1097 1100\nv  -945.112 0.0 935.657\nf 1101 1100 1102\nv  -973.336 0.0 833.903\nf 1102 1103 1101\nv  -961.901 0.0 825.834\nf 1102 1104 1103\nv  -966.66 0.0 826.246\nf 1103 1104 1105\nv  -916.391 0.0 935.379\nf 1102 1106 1104\nv  -971.175 0.0 828.794\nf 1105 1107 1103\nv  -958.911 0.0 821.204\nf 1108 1104 1106\nv  -923.885 0.0 938.851\nf 1102 1109 1106\nv  -896.413 0.0 940.51\nf 1106 1110 1108\nv  -917.811 0.0 767.994\nf 1111 1108 1110\nv  -879.347 0.0 943.425\nf 1112 1111 1110\nv  -939.182 0.0 784.733\nf 1111 1113 1108\nv  -911.561 0.0 764.477\nf 1112 1114 1111\nv  -960.824 0.0 812.579\nf 1115 1108 1113\nv  -926.781 0.0 772.323\nf 1111 1116 1113\nv  -865.608 0.0 947.732\nf 1114 1112 1117\nv  -760.384 0.0 932.775\nf 1114 1117 1118\nv  -800.81 0.0 957.86\nf 1117 1119 1118\nv  -755.935 0.0 920.934\nf 1120 1114 1118\nv  -761.628 0.0 936.247\nf 1119 1121 1118\nv  -831.891 0.0 957.993\nf 1117 1122 1119\nv  -752.006 0.0 916.627\nf 1123 1114 1120\nv  -782.496 0.0 954.799\nf 1124 1121 1119\nv  -855.478 0.0 953.275\nf 1117 1125 1122\nv  -908.858 0.0 760.37\nf 1126 1114 1123\nv  -778.522 0.0 953.597\nf 1127 1121 1124\nv  -747.916 0.0 915.413\nf 1123 1128 1126\nv  -777.046 0.0 953.175\nf 1127 1129 1121\nv  -908.096 0.0 753.881\nf 1128 1130 1126\nv  -769.503 0.0 952.362\nf 1131 1121 1129\nv  -744.755 0.0 910.116\nf 1132 1130 1128\nv  -761.34 0.0 940.076\nf 1131 1133 1121\nv  -735.963 0.0 767.805\nf 1132 1134 1130\nv  -763.363 0.0 950.893\nf 1133 1131 1135\nv  -907.847 0.0 743.598\nf 1134 1136 1130\nv  -732.918 0.0 877.552\nf 1134 1132 1137\nv  -759.914 0.0 946.686\nf 1133 1135 1138\nv  -736.554 0.0 750.531\nf 1139 1136 1134\nv  -738.842 0.0 908.035\nf 1140 1137 1132\nv  -726.015 0.0 859.155\nf 1134 1137 1141\nv  -898.695 0.0 707.415\nf 1139 1142 1136\nv  -731.873 0.0 891.029\nf 1143 1137 1140\nv  -902.652 0.0 715.217\nf 1142 1144 1136\nv  -729.093 0.0 735.618\nf 1139 1145 1142\nv  -733.824 0.0 900.345\nf 1146 1143 1140\nv  -909.56 0.0 721.75\nf 1147 1136 1144\nv  -898.573 0.0 696.552\nf 1145 1148 1142\nv  -731.945 0.0 895.681\nf 1143 1146 1149\nv  -912.401 0.0 729.83\nf 1147 1150 1136\nv  -894.528 0.0 664.409\nf 1145 1151 1148\nv  -901.265 0.0 678.221\nf 1151 1152 1148\nv  -893.113 0.0 660.146\nf 1153 1151 1145\nv  -900.624 0.0 670.709\nf 1151 1154 1152\nv  -867.338 0.0 622.327\nf 1145 1155 1153\nv  -892.251 0.0 650.552\nf 1156 1153 1155\nv  -712.613 0.0 714.827\nf 1155 1145 1157\nv  -883.901 0.0 633.423\nf 1155 1158 1156\nv  -893.163 0.0 654.236\nf 1159 1153 1156\nv  -846.37 0.0 588.658\nf 1160 1155 1157\nv  -700.643 0.0 704.81\nf 1161 1160 1157\nv  -652.054 0.0 656.206\nf 1162 1160 1161\nv  -824.911 0.0 531.549\nf 1160 1162 1163\nv  -809.603 0.0 490.923\nf 1162 1164 1163\nv  -823.342 0.0 512.516\nf 1163 1164 1165\nv  -568.588 0.0 556.08\nf 1162 1166 1164\nv  -562.305 0.0 495.642\nf 1166 1167 1164\nv  -799.793 0.0 451.287\nf 1164 1167 1168\nv  -560.934 0.0 505.448\nf 1166 1169 1167\nv  -555.242 0.0 473.86\nf 1170 1168 1167\nv  -560.343 0.0 522.132\nf 1171 1169 1166\nv  -797.439 0.0 411.651\nf 1170 1172 1168\nv  -562.697 0.0 541.165\nf 1171 1166 1173\nv  -548.483 0.0 457.798\nf 1174 1172 1170\nv  -802.253 0.0 378.103\nf 1174 1175 1172\nv  -533.18 0.0 438.965\nf 1176 1175 1174\nv  -513.362 0.0 421.502\nf 1177 1175 1176\nv  -803.568 0.0 194.507\nf 1175 1177 1178\nv  -500.215 0.0 411.684\nf 1177 1179 1178\nv  -811.128 0.0 361.228\nf 1178 1180 1175\nv  -785.204 0.0 148.389\nf 1179 1181 1178\nv  -818.014 0.0 344.065\nf 1182 1180 1178\nv  -442.916 0.0 379.505\nf 1179 1183 1181\nv  -823.64 0.0 330.307\nf 1184 1182 1178\nv  -418.014 0.0 361.306\nf 1183 1185 1181\nv  -482.945 0.0 405.407\nf 1186 1183 1179\nv  -831.283 0.0 315.859\nf 1187 1184 1178\nv  -753.465 0.0 46.345\nf 1185 1188 1181\nv  -842.192 0.0 232.175\nf 1187 1178 1189\nv  -759.727 0.0 54.171\nf 1181 1188 1190\nv  -405.563 0.0 342.618\nf 1191 1188 1185\nv  -836.141 0.0 309.759\nf 1189 1192 1187\nv  -771.669 0.0 64.891\nf 1193 1181 1190\nv  -386.53 0.0 316.215\nf 1194 1188 1191\nv  -844.265 0.0 254.182\nf 1192 1189 1195\nv  -765.872 0.0 58.579\nf 1193 1190 1196\nv  -778.29 0.0 75.845\nf 1193 1197 1181\nv  -361.511 0.0 283.456\nf 1194 1198 1188\nv  -845.53 0.0 240.535\nf 1199 1195 1189\nv  -846.542 0.0 269.966\nf 1192 1195 1200\nv  -781.904 0.0 83.225\nf 1197 1201 1181\nv  -774.991 0.0 69.7\nf 1197 1193 1202\nv  -753.542 0.0 37.807\nf 1203 1188 1198\nv  -376.035 0.0 303.96\nf 1204 1198 1194\nv  -846.663 0.0 260.415\nf 1205 1200 1195\nv  -845.978 0.0 286.083\nf 1206 1192 1200\nv  -755.035 0.0 19.952\nf 1203 1198 1207\nv  -841.806 0.0 301.222\nf 1206 1208 1192\nv  -755.267 0.0 -23.841\nf 1207 1198 1209\nv  -754.471 0.0 29.536\nf 1203 1207 1210\nv  -844.718 0.0 295.734\nf 1208 1206 1211\nv  -755.046 0.0 -42.276\nf 1198 1212 1209\nv  -336.394 0.0 242.928\nf 1198 1213 1212\nv  -655.486 0.0 -116.539\nf 1213 1214 1212\nv  -337.67 0.0 246.468\nf 1215 1213 1198\nv  -666.291 0.0 -116.249\nf 1214 1216 1212\nv  -329.137 0.0 231.941\nf 1217 1214 1213\nv  -347.286 0.0 267.561\nf 1218 1215 1198\nv  -688.148 0.0 -117.63\nf 1216 1219 1212\nv  -648.236 0.0 -119.188\nf 1217 1220 1214\nv  -339.14 0.0 260.493\nf 1215 1218 1221\nv  -709.701 0.0 -114.758\nf 1219 1222 1212\nv  -681.068 0.0 -117.964\nf 1223 1219 1216\nv  -642.328 0.0 -122.606\nf 1217 1224 1220\nv  -718.416 0.0 -106.843\nf 1225 1212 1222\nv  -319.217 0.0 222.291\nf 1226 1224 1217\nv  -716.476 0.0 -108.924\nf 1225 1222 1227\nv  -743.971 0.0 -87.751\nf 1228 1212 1225\nv  -347.005 0.0 -133.415\nf 1226 1229 1224\nv  -714.537 0.0 -111.908\nf 1222 1230 1227\nv  -735.371 0.0 -97.592\nf 1228 1225 1231\nv  -753.299 0.0 -57.126\nf 1212 1228 1232\nv  -362.119 0.0 -141.486\nf 1224 1229 1233\nv  -292.641 0.0 197.112\nf 1229 1226 1234\nv  -722.252 0.0 -106.709\nf 1231 1225 1235\nv  -748.314 0.0 -78.255\nf 1228 1236 1232\nv  -350.674 0.0 -136.054\nf 1229 1237 1233\nv  -378.483 0.0 -149.735\nf 1233 1238 1224\nv  -309.806 0.0 215.267\nf 1234 1226 1239\nv  -336.93 0.0 -129.942\nf 1229 1234 1240\nv  -732.1 0.0 -104.082\nf 1231 1235 1241\nv  -752.443 0.0 -66.154\nf 1236 1242 1232\nv  -398.975 0.0 -160.612\nf 1224 1238 1243\nv  -306.96 0.0 216.346\nf 1239 1244 1234\nv  -286.164 0.0 192.603\nf 1245 1240 1234\nv  -724.473 0.0 -107.744\nf 1246 1241 1235\nv  -631.999 0.0 -131.668\nf 1224 1243 1247\nv  -291.071 0.0 197.902\nf 1234 1248 1245\nv  -279.886 0.0 188.774\nf 1245 1249 1240\nv  -729.95 0.0 -110.271\nf 1250 1241 1246\nv  -415.107 0.0 -170.019\nf 1247 1243 1251\nv  -327.071 0.0 -130.098\nf 1240 1249 1252\nv  -727.049 0.0 -110.494\nf 1253 1250 1246\nv  -613.513 0.0 -151.416\nf 1247 1251 1254\nv  -248.584 0.0 172.389\nf 1249 1255 1252\nv  -728.811 0.0 -111.229\nf 1256 1250 1253\nv  -441.667 0.0 -184.502\nf 1257 1254 1251\nv  -233.281 0.0 164.341\nf 1258 1252 1255\nv  -267.324 0.0 184.656\nf 1259 1255 1249\nv  -423.784 0.0 -176.598\nf 1251 1260 1257\nv  -597.867 0.0 -166.056\nf 1254 1257 1261\nv  -215.226 0.0 158.452\nf 1262 1252 1258\nv  -431.764 0.0 -181.597\nf 1257 1260 1263\nv  -452.394 0.0 -186.985\nf 1257 1264 1261\nv  -180.89 0.0 144.226\nf 1262 1265 1252\nv  -501.198 0.0 -200.266\nf 1264 1266 1261\nv  -316.311 0.0 -136.377\nf 1265 1267 1252\nv  -189.522 0.0 150.805\nf 1262 1268 1265\nv  -510.953 0.0 -200.867\nf 1261 1266 1269\nv  -476.44 0.0 -196.971\nf 1264 1270 1266\nv  -161.464 0.0 139.128\nf 1265 1271 1267\nv  -182.36 0.0 148.156\nf 1265 1268 1272\nv  -564.908 0.0 -193.709\nf 1269 1273 1261\nv  -491.45 0.0 -200.344\nf 1274 1266 1270\nv  -464.812 0.0 -195.957\nf 1275 1270 1264\nv  -136.269 0.0 74.052\nf 1271 1276 1267\nv  -172.258 0.0 142.467\nf 1277 1271 1265\nv  -584.676 0.0 -182.365\nf 1278 1261 1273\nv  -538.358 0.0 -206.344\nf 1273 1269 1279\nv  -455.644 0.0 -190.825\nf 1264 1280 1275\nv  -135.855 0.0 62.409\nf 1281 1267 1276\nv  -154.009 0.0 131.08\nf 1271 1282 1276\nv  -575.319 0.0 -191.493\nf 1283 1278 1273\nv  -556.668 0.0 -200.867\nf 1273 1279 1284\nv  -459.65 0.0 -195.345\nf 1285 1275 1280\nv  -133.6 0.0 55.073\nf 1281 1286 1267\nv  -132.086 0.0 96.36\nf 1287 1276 1282\nv  -580.105 0.0 -188.788\nf 1278 1283 1288\nv  -547.847 0.0 -205.287\nf 1284 1279 1289\nv  -286.097 0.0 -157.272\nf 1286 1290 1267\nv  -148.024 0.0 128.831\nf 1291 1287 1282\nv  -133.368 0.0 84.55\nf 1292 1276 1287\nv  -554.943 0.0 -203.16\nf 1289 1293 1284\nv  -306.297 0.0 -147.932\nf 1290 1294 1267\nv  -128.648 0.0 -59.197\nf 1290 1286 1295\nv  -136.247 0.0 122.553\nf 1296 1287 1291\nv  -127.156 0.0 18.75\nf 1297 1295 1286\nv  -273.0 0.0 -164.185\nf 1290 1295 1298\nv  -129.184 0.0 46.802\nf 1297 1286 1299\nv  -119.292 0.0 -4.216\nf 1300 1295 1297\nv  -129.577 0.0 -81.673\nf 1301 1298 1295\nv  -124.94 0.0 34.223\nf 1302 1297 1299\nv  -120.154 0.0 -38.903\nf 1303 1295 1300\nv  -126.36 0.0 -91.836\nf 1298 1301 1304\nv  -125.172 0.0 38.687\nf 1302 1299 1305\nv  -117.43 0.0 -29.875\nf 1303 1300 1306\nv  -261.969 0.0 -178.891\nf 1307 1298 1304\nv  -116.38 0.0 -18.631\nf 1306 1300 1308\nv  -245.262 0.0 -196.047\nf 1304 1309 1307\nv  -105.785 0.0 -158.419\nf 1304 1310 1309\nv  -228.948 0.0 -205.465\nf 1309 1310 1311\nv  -102.729 0.0 -142.9\nf 1310 1304 1312\nv  -113.644 0.0 -198.028\nf 1310 1313 1311\nv  -203.267 0.0 -217.243\nf 1311 1313 1314\nv  -105.984 0.0 -179.225\nf 1315 1313 1310\nv  -193.955 0.0 -222.542\nf 1313 1316 1314\nv  -104.006 0.0 -169.685\nf 1315 1310 1317\nv  -174.358 0.0 -231.025\nf 1316 1313 1318\nv  -133.141 0.0 -223.143\nf 1319 1318 1313\nv  -119.403 0.0 -215.195\nf 1320 1319 1313\nv  -146.637 0.0 -231.047\nf 1321 1318 1319\nv  -114.025 0.0 -204.585\nf 1322 1320 1313\nv  -159.63 0.0 -235.033\nf 1323 1318 1321\ng Building\no Building335\nusemtl BUILDING_DEFAULT_0\nv  99.031 0.0 59.949\nv  99.462 0.0 51.577\nv  110.371 0.0 52.134\nf 1324 1325 1326\nv  109.935 0.0 60.516\nf 1324 1326 1327\no Building336\nv  28.602 0.0 37.729\nv  23.573 0.0 12.16\nv  35.084 0.0 9.889\nf 1328 1329 1330\nv  40.108 0.0 35.459\nf 1328 1330 1331\no Building337\nv  -47.768 0.0 3.154\nv  -40.07 0.0 1.684\nv  -35.013 0.0 28.323\nf 1332 1333 1334\nv  -42.712 0.0 29.792\nf 1332 1334 1335\no Building338\nv  43.661 0.0 -100.898\nv  43.263 0.0 -109.191\nv  45.706 0.0 -109.303\nf 1336 1337 1338\nv  45.319 0.0 -117.44\nv  50.896 0.0 -117.708\nf 1338 1339 1340\nf 1336 1338 1340\nv  50.829 0.0 -119.099\nv  56.671 0.0 -119.378\nf 1340 1341 1342\nf 1336 1340 1342\nv  56.604 0.0 -120.747\nv  62.772 0.0 -121.047\nf 1342 1343 1344\nf 1336 1342 1344\nv  62.882 0.0 -118.743\nf 1336 1344 1345\nv  69.028 0.0 -119.044\nf 1336 1345 1346\nv  68.956 0.0 -120.546\nv  74.593 0.0 -120.825\nf 1346 1347 1348\nf 1336 1346 1348\nv  74.543 0.0 -121.949\nv  80.633 0.0 -122.239\nf 1348 1349 1350\nf 1336 1348 1350\nv  80.556 0.0 -123.764\nv  86.326 0.0 -124.042\nf 1350 1351 1352\nf 1336 1350 1352\nv  86.259 0.0 -125.456\nv  91.15 0.0 -125.701\nf 1352 1353 1354\nf 1336 1352 1354\nv  91.493 0.0 -118.721\nv  90.465 0.0 -118.665\nf 1354 1355 1356\nf 1336 1354 1356\nv  90.924 0.0 -109.537\nv  85.121 0.0 -109.236\nf 1356 1357 1358\nv  85.198 0.0 -107.7\nv  79.401 0.0 -107.41\nf 1358 1359 1360\nf 1356 1358 1360\nv  79.473 0.0 -105.974\nv  73.604 0.0 -105.674\nf 1360 1361 1362\nf 1356 1360 1362\nv  73.67 0.0 -104.36\nv  67.818 0.0 -104.059\nf 1362 1363 1364\nf 1356 1362 1364\nv  67.889 0.0 -102.612\nv  61.307 0.0 -102.289\nf 1364 1365 1366\nf 1356 1364 1366\nv  61.191 0.0 -104.56\nf 1356 1366 1367\nv  55.748 0.0 -104.282\nf 1356 1367 1368\nf 1336 1356 1368\nv  55.814 0.0 -102.957\nv  50.006 0.0 -102.668\nf 1368 1369 1370\nf 1336 1368 1370\nv  50.078 0.0 -101.232\nf 1336 1370 1371\no Building339\nv  15.128 0.0 38.442\nv  13.852 0.0 31.473\nv  17.676 0.0 30.772\nf 1372 1373 1374\nv  18.953 0.0 37.741\nf 1372 1374 1375\no Building340\nv  48.762 0.0 85.017\nv  51.503 0.0 77.236\nv  61.628 0.0 80.821\nf 1376 1377 1378\nv  58.881 0.0 88.59\nf 1376 1378 1379\no Building341\nv  -3.695 0.0 69.377\nv  4.114 0.0 67.941\nv  9.812 0.0 98.976\nf 1380 1381 1382\nv  2.003 0.0 100.412\nf 1380 1382 1383\no Building342\nv  72.858 0.0 70.613\nv  89.476 0.0 71.381\nv  89.05 0.0 80.564\nf 1384 1385 1386\nv  84.894 0.0 80.375\nf 1384 1386 1387\nv  84.778 0.0 82.924\nf 1384 1387 1388\nv  79.882 0.0 82.702\nv  80.003 0.0 80.075\nf 1388 1389 1390\nf 1384 1388 1390\nv  72.438 0.0 79.718\nf 1384 1390 1391\no Building343\nv  -4.612 0.0 27.054\nv  0.66 0.0 26.219\nv  4.296 0.0 49.295\nf 1392 1393 1394\nv  -0.976 0.0 50.13\nf 1392 1394 1395\no Building344\nv  -76.114 0.0 9.443\nv  -68.653 0.0 6.994\nv  -60.159 0.0 32.92\nf 1396 1397 1398\nv  -67.62 0.0 35.37\nf 1396 1398 1399\no Building345\nusemtl CONCRETE_0\nv  -2.717 0.0 -96.49\nv  10.624 0.0 -97.224\nv  10.923 0.0 -96.044\nf 1400 1401 1402\nv  25.894 0.0 -96.779\nv  26.192 0.0 -90.556\nf 1402 1403 1404\nf 1400 1402 1404\nv  11.514 0.0 -89.966\nv  11.221 0.0 -91.447\nf 1404 1405 1406\nf 1400 1404 1406\nv  -2.418 0.0 -90.556\nf 1400 1406 1407\no Building346\nusemtl BUILDING_DEFAULT_0\nv  100.816 0.0 21.31\nv  101.065 0.0 25.496\nv  92.952 0.0 25.985\nf 1408 1409 1410\nv  92.836 0.0 21.8\nf 1408 1410 1411\no Building347\nv  84.706 0.0 -66.844\nv  70.144 0.0 -57.449\nv  68.542 0.0 -59.92\nf 1412 1413 1414\nv  83.109 0.0 -69.327\nf 1415 1412 1414\no Building348\nv  89.863 0.0 -44.079\nv  89.918 0.0 -41.307\nv  75.206 0.0 -40.996\nf 1416 1417 1418\nv  75.146 0.0 -43.768\nf 1416 1418 1419\no Building349\nv  11.752 0.0 23.225\nv  11.26 0.0 16.479\nv  14.902 0.0 16.212\nf 1420 1421 1422\nv  15.394 0.0 22.957\nf 1420 1422 1423\no Building350\nv  118.899 0.0 -73.234\nv  114.395 0.0 -71.242\nv  110.769 0.0 -79.446\nf 1424 1425 1426\nv  115.273 0.0 -81.439\nf 1427 1424 1426\no Building351\nv  -24.988 0.0 36.961\nv  -14.239 0.0 34.724\nv  -11.946 0.0 45.744\nf 1428 1429 1430\nv  -19.014 0.0 47.214\nf 1428 1430 1431\nv  -18.76 0.0 48.449\nf 1428 1431 1432\nv  -31.465 0.0 51.099\nv  -32.736 0.0 44.987\nf 1432 1433 1434\nv  -31.642 0.0 44.765\nf 1432 1434 1435\nv  -32.637 0.0 39.989\nf 1432 1435 1436\nv  -24.707 0.0 38.331\nf 1432 1436 1437\nf 1428 1432 1437\no Building352\nv  90.393 0.0 8.018\nv  90.874 0.0 17.737\nv  87.857 0.0 17.892\nf 1438 1439 1440\nv  88.033 0.0 21.366\nv  78.859 0.0 21.833\nf 1440 1441 1442\nf 1438 1440 1442\nv  78.533 0.0 15.31\nv  82.756 0.0 15.098\nf 1442 1443 1444\nf 1438 1442 1444\nv  82.424 0.0 8.419\nf 1438 1444 1445\no Building353\nv  90.775 0.0 47.837\nv  90.907 0.0 43.551\nv  93.565 0.0 43.64\nf 1446 1447 1448\nv  93.715 0.0 38.787\nv  97.544 0.0 38.898\nf 1448 1449 1450\nf 1446 1448 1450\nv  97.616 0.0 36.639\nv  103.06 0.0 36.806\nf 1450 1451 1452\nf 1446 1450 1452\nv  102.916 0.0 41.637\nv  101.347 0.0 41.592\nf 1452 1453 1454\nf 1446 1452 1454\nv  101.153 0.0 48.149\nf 1446 1454 1455\no Building354\nv  -90.455 0.0 -70.763\nv  -96.562 0.0 -74.548\nv  -76.76 0.0 -106.486\nf 1456 1457 1458\nv  -70.654 0.0 -102.701\nf 1459 1456 1458\no Building355\nv  -71.306 0.0 -108.368\nv  -77.6 0.0 -112.453\nv  -61.424 0.0 -137.401\nf 1460 1461 1462\nv  -55.124 0.0 -133.315\nf 1463 1460 1462\no Building356\nv  -28.409 0.0 121.451\nv  -28.735 0.0 119.024\nv  -26.608 0.0 118.746\nf 1464 1465 1466\nv  -26.282 0.0 121.162\nf 1464 1466 1467\no Building357\nv  13.443 0.0 -19.845\nv  18.682 0.0 -20.513\nv  19.069 0.0 -17.44\nf 1468 1469 1470\nv  21.544 0.0 -17.752\nv  21.837 0.0 -15.425\nf 1470 1471 1472\nf 1468 1470 1472\nv  23.661 0.0 -15.648\nv  24.053 0.0 -12.565\nf 1472 1473 1474\nf 1468 1472 1474\nv  26.308 0.0 -12.843\nv  26.684 0.0 -9.848\nf 1474 1475 1476\nf 1468 1474 1476\nv  28.646 0.0 -10.093\nv  29.033 0.0 -7.021\nf 1476 1477 1478\nf 1468 1476 1478\nv  31.144 0.0 -7.288\nv  31.509 0.0 -4.427\nf 1478 1479 1480\nf 1468 1478 1480\nv  33.432 0.0 -4.672\nv  33.83 0.0 -1.566\nf 1480 1481 1482\nf 1468 1480 1482\nv  35.869 0.0 -1.822\nv  36.35 0.0 1.94\nf 1482 1483 1484\nf 1468 1482 1484\nv  28.204 0.0 2.987\nv  27.48 0.0 -2.69\nf 1484 1485 1486\nf 1468 1484 1486\nv  23.727 0.0 -2.212\nv  23.009 0.0 -7.833\nf 1486 1487 1488\nf 1468 1486 1488\nv  19.058 0.0 -7.332\nv  18.284 0.0 -13.422\nf 1488 1489 1490\nf 1468 1488 1490\nv  14.321 0.0 -12.921\nf 1468 1490 1491\no Building358\nv  -81.966 0.0 -64.718\nv  -57.009 0.0 -49.567\nv  -61.341 0.0 -42.432\nf 1492 1493 1494\nv  -86.299 0.0 -57.583\nf 1492 1494 1495\no Building359\nv  -49.084 0.0 -104.071\nv  -41.645 0.0 -104.516\nv  -40.545 0.0 -85.958\nf 1496 1497 1498\nv  -35.914 0.0 -86.237\nv  -34.787 0.0 -67.212\nf 1498 1499 1500\nf 1496 1498 1500\nv  -42.911 0.0 -66.722\nv  -44.005 0.0 -85.101\nf 1500 1501 1502\nf 1496 1500 1502\nv  -47.945 0.0 -84.867\nf 1496 1502 1503\no Building360\nv  -54.936 0.0 -10.839\nv  -77.727 0.0 -25.979\nv  -73.489 0.0 -32.38\nf 1504 1505 1506\nv  -50.731 0.0 -17.218\nf 1504 1506 1507\no Building361\nv  -40.993 0.0 -13.232\nv  -41.396 0.0 -18.42\nv  -29.078 0.0 -19.377\nf 1508 1509 1510\nv  -28.675 0.0 -14.201\nf 1508 1510 1511\no Building362\nv  46.994 0.0 11.959\nv  64.026 0.0 14.442\nv  63.026 0.0 21.332\nf 1512 1513 1514\nv  61.805 0.0 21.154\nf 1512 1514 1515\nv  61.114 0.0 25.93\nf 1512 1515 1516\nv  46.469 0.0 23.792\nv  47.425 0.0 17.225\nf 1516 1517 1518\nf 1512 1516 1518\nv  46.259 0.0 17.058\nf 1512 1518 1519\no Building363\nv  -72.781 0.0 48.483\nv  -64.193 0.0 43.217\nv  -52.742 0.0 61.952\nf 1520 1521 1522\nv  -50.698 0.0 60.694\nv  -45.005 0.0 70.012\nf 1522 1523 1524\nf 1520 1522 1524\nv  -43.099 0.0 68.843\nv  -36.776 0.0 79.195\nf 1524 1525 1526\nf 1520 1524 1526\nv  -45.077 0.0 84.282\nv  -50.836 0.0 74.854\nf 1526 1527 1528\nf 1520 1526 1528\nv  -52.787 0.0 76.045\nv  -58.705 0.0 66.349\nf 1528 1529 1530\nf 1520 1528 1530\nv  -61.015 0.0 67.763\nf 1520 1530 1531\no Building364\nv  -16.273 0.0 24.972\nv  -19.639 0.0 6.672\nv  -16.047 0.0 6.004\nf 1532 1533 1534\nv  -20.484 0.0 -18.119\nv  -13.134 0.0 -19.477\nf 1534 1535 1536\nf 1532 1534 1536\nv  -8.619 0.0 5.057\nv  -12.344 0.0 5.748\nf 1536 1537 1538\nf 1532 1536 1538\nv  -9.05 0.0 23.637\nf 1532 1538 1539\n"
  },
  {
    "path": "test/data/map.osm",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<osm version=\"0.6\" generator=\"Overpass API 0.7.62.10 2d4cfc48\">\n<note>The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.</note>\n<meta osm_base=\"2026-01-26T14:28:00Z\"/>\n  <bounds minlat=\"60.23346126413721\" minlon=\"24.739611882081924\" maxlat=\"60.235597398229636\" maxlon=\"24.7439076349134\"/>\n  <node id=\"25505399\" lat=\"60.2272290\" lon=\"24.7243739\" version=\"9\" timestamp=\"2012-04-04T10:29:31Z\" changeset=\"11174649\" uid=\"190690\" user=\"posiki\"/>\n  <node id=\"25505400\" lat=\"60.2276612\" lon=\"24.7252653\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"25505403\" lat=\"60.2286848\" lon=\"24.7256147\" version=\"11\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"25505405\" lat=\"60.2292409\" lon=\"24.7264449\" version=\"11\" timestamp=\"2025-08-17T16:54:09Z\" changeset=\"170577090\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"25505406\" lat=\"60.2301190\" lon=\"24.7271102\" version=\"10\" timestamp=\"2012-07-17T19:10:22Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505407\" lat=\"60.2308312\" lon=\"24.7273303\" version=\"10\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505408\" lat=\"60.2314384\" lon=\"24.7269580\" version=\"11\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"25505409\" lat=\"60.2321041\" lon=\"24.7264418\" version=\"12\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"25505411\" lat=\"60.2331963\" lon=\"24.7275517\" version=\"11\" timestamp=\"2024-08-13T06:11:46Z\" changeset=\"155178405\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"25505412\" lat=\"60.2337817\" lon=\"24.7276114\" version=\"11\" timestamp=\"2024-08-13T06:11:46Z\" changeset=\"155178405\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"25505413\" lat=\"60.2342640\" lon=\"24.7281078\" version=\"9\" timestamp=\"2009-05-06T12:11:44Z\" changeset=\"1095845\" uid=\"117400\" user=\"scellus\"/>\n  <node id=\"25505416\" lat=\"60.2353176\" lon=\"24.7282978\" version=\"12\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"25505419\" lat=\"60.2356000\" lon=\"24.7300301\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"25505422\" lat=\"60.2361675\" lon=\"24.7311802\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"25505426\" lat=\"60.2359424\" lon=\"24.7335489\" version=\"10\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505431\" lat=\"60.2356208\" lon=\"24.7345381\" version=\"10\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505432\" lat=\"60.2354214\" lon=\"24.7355524\" version=\"10\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505463\" lat=\"60.2340346\" lon=\"24.7393423\" version=\"11\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"25505464\" lat=\"60.2334284\" lon=\"24.7392944\" version=\"10\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505465\" lat=\"60.2331746\" lon=\"24.7383304\" version=\"10\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505467\" lat=\"60.2329807\" lon=\"24.7372617\" version=\"10\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505469\" lat=\"60.2321892\" lon=\"24.7356231\" version=\"10\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505490\" lat=\"60.2277865\" lon=\"24.7284320\" version=\"10\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505494\" lat=\"60.2268105\" lon=\"24.7286227\" version=\"9\" timestamp=\"2008-10-15T08:52:07Z\" changeset=\"344481\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"25505498\" lat=\"60.2259730\" lon=\"24.7278358\" version=\"11\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"25505512\" lat=\"60.2258708\" lon=\"24.7207195\" version=\"9\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505516\" lat=\"60.2254245\" lon=\"24.7184574\" version=\"11\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505519\" lat=\"60.2252310\" lon=\"24.7163967\" version=\"11\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505522\" lat=\"60.2250886\" lon=\"24.7140825\" version=\"12\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505527\" lat=\"60.2250500\" lon=\"24.7134323\" version=\"13\" timestamp=\"2012-07-17T19:10:23Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505539\" lat=\"60.2271052\" lon=\"24.7145681\" version=\"9\" timestamp=\"2012-07-17T19:10:24Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25505542\" lat=\"60.2277163\" lon=\"24.7163675\" version=\"9\" timestamp=\"2012-07-17T19:10:24Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"25885728\" lat=\"60.2355770\" lon=\"24.7439596\" version=\"4\" timestamp=\"2011-12-09T14:06:52Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"247103490\" lat=\"60.2264300\" lon=\"24.7133538\" version=\"10\" timestamp=\"2012-07-17T19:10:22Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"249664736\" lat=\"60.2251116\" lon=\"24.7131625\" version=\"11\" timestamp=\"2008-10-15T08:52:08Z\" changeset=\"344481\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"249664744\" lat=\"60.2251485\" lon=\"24.7143261\" version=\"9\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"249664746\" lat=\"60.2251223\" lon=\"24.7144114\" version=\"10\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"249664856\" lat=\"60.2250562\" lon=\"24.7146198\" version=\"12\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"262795580\" lat=\"60.2263526\" lon=\"24.7282836\" version=\"9\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"262795581\" lat=\"60.2259224\" lon=\"24.7267069\" version=\"9\" timestamp=\"2012-07-17T19:10:24Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"268688014\" lat=\"60.2308259\" lon=\"24.7361118\" version=\"3\" timestamp=\"2011-12-10T17:06:30Z\" changeset=\"10082848\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268724899\" lat=\"60.2347548\" lon=\"24.7440317\" version=\"2\" timestamp=\"2011-12-09T14:07:20Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268724954\" lat=\"60.2351599\" lon=\"24.7439993\" version=\"10\" timestamp=\"2025-07-18T16:55:41Z\" changeset=\"169117123\" uid=\"486606\" user=\"Barro\">\n    <tag k=\"bench\" v=\"yes\"/>\n    <tag k=\"bin\" v=\"yes\"/>\n    <tag k=\"bus\" v=\"yes\"/>\n    <tag k=\"highway\" v=\"bus_stop\"/>\n    <tag k=\"lit\" v=\"yes\"/>\n    <tag k=\"name\" v=\"Nuumäki\"/>\n    <tag k=\"name:fi\" v=\"Nuumäki\"/>\n    <tag k=\"name:sv\" v=\"Nobacken\"/>\n    <tag k=\"network\" v=\"HSL\"/>\n    <tag k=\"network:wikidata\" v=\"Q473211\"/>\n    <tag k=\"operator\" v=\"HSL\"/>\n    <tag k=\"public_transport\" v=\"platform\"/>\n    <tag k=\"ref\" v=\"E1402\"/>\n    <tag k=\"route_ref\" v=\"15;217;218;219;235N;236;236V;239;239T\"/>\n    <tag k=\"shelter\" v=\"yes\"/>\n    <tag k=\"source\" v=\"survey\"/>\n    <tag k=\"waste_bin\" v=\"yes\"/>\n    <tag k=\"zone:HSL\" v=\"B\"/>\n  </node>\n  <node id=\"268725040\" lat=\"60.2350871\" lon=\"24.7440607\" version=\"2\" timestamp=\"2011-12-09T14:07:12Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268725041\" lat=\"60.2354125\" lon=\"24.7438429\" version=\"2\" timestamp=\"2011-12-09T14:06:36Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268725042\" lat=\"60.2356149\" lon=\"24.7437157\" version=\"2\" timestamp=\"2011-12-09T14:07:16Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268725043\" lat=\"60.2361127\" lon=\"24.7433885\" version=\"2\" timestamp=\"2011-12-09T14:06:39Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268725045\" lat=\"60.2370005\" lon=\"24.7428110\" version=\"2\" timestamp=\"2011-12-09T14:07:16Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268725046\" lat=\"60.2374491\" lon=\"24.7425364\" version=\"2\" timestamp=\"2011-12-09T14:06:54Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268725047\" lat=\"60.2376713\" lon=\"24.7423826\" version=\"2\" timestamp=\"2011-12-09T14:07:23Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268728008\" lat=\"60.2354983\" lon=\"24.7437193\" version=\"5\" timestamp=\"2025-12-07T06:27:39Z\" changeset=\"175610623\" uid=\"20996589\" user=\"Lumikeiju\">\n    <tag k=\"bicycle\" v=\"yes\"/>\n    <tag k=\"crossing:island\" v=\"no\"/>\n    <tag k=\"highway\" v=\"crossing\"/>\n  </node>\n  <node id=\"268728009\" lat=\"60.2353045\" lon=\"24.7432186\" version=\"2\" timestamp=\"2011-12-10T16:03:13Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268728010\" lat=\"60.2352509\" lon=\"24.7428762\" version=\"3\" timestamp=\"2011-12-10T16:03:13Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268728011\" lat=\"60.2352713\" lon=\"24.7424030\" version=\"3\" timestamp=\"2011-12-10T16:03:13Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268728012\" lat=\"60.2352834\" lon=\"24.7420778\" version=\"3\" timestamp=\"2011-12-10T16:03:13Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268728013\" lat=\"60.2351680\" lon=\"24.7416387\" version=\"3\" timestamp=\"2011-12-10T16:03:14Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268728015\" lat=\"60.2342054\" lon=\"24.7419259\" version=\"3\" timestamp=\"2011-12-10T16:03:14Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"268728055\" lat=\"60.2341527\" lon=\"24.7419647\" version=\"3\" timestamp=\"2011-12-10T16:03:14Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"276457277\" lat=\"60.2362769\" lon=\"24.7420910\" version=\"4\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"crossing\" v=\"uncontrolled\"/>\n    <tag k=\"highway\" v=\"crossing\"/>\n  </node>\n  <node id=\"276457278\" lat=\"60.2368121\" lon=\"24.7418128\" version=\"3\" timestamp=\"2011-12-10T16:03:20Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\">\n    <tag k=\"highway\" v=\"turning_circle\"/>\n  </node>\n  <node id=\"276457279\" lat=\"60.2363984\" lon=\"24.7431983\" version=\"3\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"276457280\" lat=\"60.2341086\" lon=\"24.7430571\" version=\"3\" timestamp=\"2016-12-05T19:53:52Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"highway\" v=\"turning_circle\"/>\n  </node>\n  <node id=\"278302281\" lat=\"60.2350346\" lon=\"24.7416098\" version=\"2\" timestamp=\"2011-12-10T16:03:23Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"278302283\" lat=\"60.2352808\" lon=\"24.7419635\" version=\"2\" timestamp=\"2011-12-10T16:03:23Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"300502030\" lat=\"60.2312835\" lon=\"24.7341959\" version=\"13\" timestamp=\"2008-10-15T08:52:07Z\" changeset=\"344481\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"300502032\" lat=\"60.2312153\" lon=\"24.7342131\" version=\"2\" timestamp=\"2008-10-11T08:59:46Z\" changeset=\"243733\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"300502033\" lat=\"60.2311760\" lon=\"24.7342152\" version=\"3\" timestamp=\"2011-12-10T17:06:33Z\" changeset=\"10082848\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"300502034\" lat=\"60.2310448\" lon=\"24.7340929\" version=\"2\" timestamp=\"2008-10-11T08:59:46Z\" changeset=\"243733\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"300502035\" lat=\"60.2309767\" lon=\"24.7339384\" version=\"2\" timestamp=\"2008-10-11T08:59:46Z\" changeset=\"243733\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"300502036\" lat=\"60.2308829\" lon=\"24.7336723\" version=\"2\" timestamp=\"2008-10-11T08:59:46Z\" changeset=\"243733\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"300502037\" lat=\"60.2308147\" lon=\"24.7334406\" version=\"2\" timestamp=\"2008-10-11T08:59:46Z\" changeset=\"243733\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"300502038\" lat=\"60.2308147\" lon=\"24.7332089\" version=\"2\" timestamp=\"2008-10-11T08:59:47Z\" changeset=\"243733\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"300502047\" lat=\"60.2308873\" lon=\"24.7330210\" version=\"11\" timestamp=\"2012-07-17T19:10:27Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300502048\" lat=\"60.2311200\" lon=\"24.7337453\" version=\"12\" timestamp=\"2012-07-17T19:10:27Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300504710\" lat=\"60.2355860\" lon=\"24.7293079\" version=\"11\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300504726\" lat=\"60.2355346\" lon=\"24.7288304\" version=\"11\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505146\" lat=\"60.2358576\" lon=\"24.7388213\" version=\"9\" timestamp=\"2012-07-17T19:10:28Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505147\" lat=\"60.2356856\" lon=\"24.7390472\" version=\"9\" timestamp=\"2012-07-17T19:10:28Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505148\" lat=\"60.2356133\" lon=\"24.7394539\" version=\"9\" timestamp=\"2012-07-17T19:10:28Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505149\" lat=\"60.2359200\" lon=\"24.7400007\" version=\"9\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505151\" lat=\"60.2345672\" lon=\"24.7396012\" version=\"10\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505152\" lat=\"60.2338641\" lon=\"24.7392940\" version=\"11\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505153\" lat=\"60.2336637\" lon=\"24.7393697\" version=\"10\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505154\" lat=\"60.2333720\" lon=\"24.7390813\" version=\"9\" timestamp=\"2012-07-17T19:10:28Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505155\" lat=\"60.2332795\" lon=\"24.7388381\" version=\"9\" timestamp=\"2012-07-17T19:10:28Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505156\" lat=\"60.2325858\" lon=\"24.7362054\" version=\"9\" timestamp=\"2012-07-17T19:10:28Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505157\" lat=\"60.2317987\" lon=\"24.7349555\" version=\"9\" timestamp=\"2012-07-17T19:10:28Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505158\" lat=\"60.2276313\" lon=\"24.7284427\" version=\"9\" timestamp=\"2012-07-17T19:10:28Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505159\" lat=\"60.2281971\" lon=\"24.7174205\" version=\"10\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"300505161\" lat=\"60.2356307\" lon=\"24.7301370\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505162\" lat=\"60.2358895\" lon=\"24.7306584\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505163\" lat=\"60.2360210\" lon=\"24.7309415\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505164\" lat=\"60.2362252\" lon=\"24.7312629\" version=\"9\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505165\" lat=\"60.2362495\" lon=\"24.7313495\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505166\" lat=\"60.2363337\" lon=\"24.7316870\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505167\" lat=\"60.2363734\" lon=\"24.7318466\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505168\" lat=\"60.2363337\" lon=\"24.7325142\" version=\"9\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505169\" lat=\"60.2363290\" lon=\"24.7328671\" version=\"9\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505170\" lat=\"60.2362987\" lon=\"24.7331387\" version=\"9\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505171\" lat=\"60.2362435\" lon=\"24.7335150\" version=\"9\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505172\" lat=\"60.2361606\" lon=\"24.7339471\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505173\" lat=\"60.2360566\" lon=\"24.7342485\" version=\"10\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"300505174\" lat=\"60.2354937\" lon=\"24.7349549\" version=\"9\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505175\" lat=\"60.2357622\" lon=\"24.7372821\" version=\"9\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505176\" lat=\"60.2356906\" lon=\"24.7377869\" version=\"9\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505177\" lat=\"60.2359174\" lon=\"24.7381585\" version=\"9\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"300505178\" lat=\"60.2359424\" lon=\"24.7384597\" version=\"9\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747030\" lat=\"60.2253245\" lon=\"24.7179030\" version=\"8\" timestamp=\"2008-10-15T08:52:08Z\" changeset=\"344481\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"303747031\" lat=\"60.2253586\" lon=\"24.7174395\" version=\"8\" timestamp=\"2008-10-15T08:52:08Z\" changeset=\"344481\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"303747032\" lat=\"60.2250653\" lon=\"24.7151469\" version=\"12\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747033\" lat=\"60.2250688\" lon=\"24.7148818\" version=\"9\" timestamp=\"2008-10-15T08:52:08Z\" changeset=\"344481\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"303747034\" lat=\"60.2250600\" lon=\"24.7137486\" version=\"10\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747035\" lat=\"60.2250859\" lon=\"24.7136372\" version=\"9\" timestamp=\"2008-10-15T08:52:08Z\" changeset=\"344481\" uid=\"3855\" user=\"Liimes\"/>\n  <node id=\"303747036\" lat=\"60.2253439\" lon=\"24.7131603\" version=\"10\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747037\" lat=\"60.2256693\" lon=\"24.7131804\" version=\"10\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747038\" lat=\"60.2257476\" lon=\"24.7130378\" version=\"10\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747039\" lat=\"60.2257811\" lon=\"24.7131443\" version=\"10\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747040\" lat=\"60.2259574\" lon=\"24.7131940\" version=\"10\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747041\" lat=\"60.2260209\" lon=\"24.7131159\" version=\"11\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747119\" lat=\"60.2251585\" lon=\"24.7134925\" version=\"8\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"303747123\" lat=\"60.2255246\" lon=\"24.7131879\" version=\"5\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"303747125\" lat=\"60.2257864\" lon=\"24.7130556\" version=\"5\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"303747126\" lat=\"60.2261479\" lon=\"24.7132757\" version=\"5\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"303747127\" lat=\"60.2263083\" lon=\"24.7132793\" version=\"5\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"303747173\" lat=\"60.2265146\" lon=\"24.7132935\" version=\"7\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747174\" lat=\"60.2265217\" lon=\"24.7134071\" version=\"7\" timestamp=\"2012-07-17T19:10:29Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747175\" lat=\"60.2268399\" lon=\"24.7137880\" version=\"8\" timestamp=\"2012-07-17T19:10:30Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747205\" lat=\"60.2270541\" lon=\"24.7142415\" version=\"5\" timestamp=\"2012-07-17T19:10:30Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"303747239\" lat=\"60.2275312\" lon=\"24.7157898\" version=\"5\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"303747244\" lat=\"60.2265252\" lon=\"24.7133290\" version=\"4\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"303747245\" lat=\"60.2269007\" lon=\"24.7139787\" version=\"4\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"303747502\" lat=\"60.2257299\" lon=\"24.7201320\" version=\"4\" timestamp=\"2012-07-17T19:10:30Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"382107334\" lat=\"60.2282715\" lon=\"24.7255003\" version=\"2\" timestamp=\"2012-07-17T19:10:30Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"382114101\" lat=\"60.2268713\" lon=\"24.7138201\" version=\"3\" timestamp=\"2017-08-08T09:13:52Z\" changeset=\"50938268\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"382367812\" lat=\"60.2317466\" lon=\"24.7266300\" version=\"1\" timestamp=\"2009-04-23T18:03:17Z\" changeset=\"915502\" uid=\"117400\" user=\"scellus\"/>\n  <node id=\"382367894\" lat=\"60.2327820\" lon=\"24.7272194\" version=\"5\" timestamp=\"2024-08-13T06:11:46Z\" changeset=\"155178405\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"382372454\" lat=\"60.2351236\" lon=\"24.7281445\" version=\"3\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"382380015\" lat=\"60.2340427\" lon=\"24.7280127\" version=\"4\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"382380018\" lat=\"60.2341130\" lon=\"24.7281260\" version=\"4\" timestamp=\"2024-08-13T06:11:46Z\" changeset=\"155178405\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"382381757\" lat=\"60.2325803\" lon=\"24.7268029\" version=\"5\" timestamp=\"2024-08-13T06:11:46Z\" changeset=\"155178405\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"384296553\" lat=\"60.2355285\" lon=\"24.7285721\" version=\"3\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"384899935\" lat=\"60.2359721\" lon=\"24.7345404\" version=\"4\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"384899936\" lat=\"60.2357515\" lon=\"24.7354144\" version=\"4\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"385345247\" lat=\"60.2370651\" lon=\"24.7413579\" version=\"1\" timestamp=\"2009-04-26T17:25:59Z\" changeset=\"976665\" uid=\"117400\" user=\"scellus\"/>\n  <node id=\"391308169\" lat=\"60.2318726\" lon=\"24.7264748\" version=\"3\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"391308171\" lat=\"60.2323685\" lon=\"24.7264601\" version=\"4\" timestamp=\"2024-08-13T06:11:46Z\" changeset=\"155178405\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"391311575\" lat=\"60.2343501\" lon=\"24.7280976\" version=\"3\" timestamp=\"2025-08-17T16:54:09Z\" changeset=\"170577090\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"391311576\" lat=\"60.2347435\" lon=\"24.7280934\" version=\"3\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"391429351\" lat=\"60.2363829\" lon=\"24.7320183\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"392060899\" lat=\"60.2339464\" lon=\"24.7277966\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"516392299\" lat=\"60.2273697\" lon=\"24.7153599\" version=\"3\" timestamp=\"2012-07-17T19:10:34Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"567690632\" lat=\"60.2261679\" lon=\"24.7299801\" version=\"3\" timestamp=\"2025-07-09T05:25:34Z\" changeset=\"168677046\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"607993948\" lat=\"60.2337030\" lon=\"24.7408290\" version=\"2\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"607993949\" lat=\"60.2335241\" lon=\"24.7433756\" version=\"3\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"607993950\" lat=\"60.2335660\" lon=\"24.7428882\" version=\"3\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"607993951\" lat=\"60.2334481\" lon=\"24.7420818\" version=\"3\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"607993952\" lat=\"60.2334440\" lon=\"24.7437667\" version=\"3\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1121431155\" lat=\"60.2304166\" lon=\"24.7318351\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121431763\" lat=\"60.2341089\" lon=\"24.7394222\" version=\"3\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121432576\" lat=\"60.2279205\" lon=\"24.7285670\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121435310\" lat=\"60.2364508\" lon=\"24.7315848\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1121435959\" lat=\"60.2281973\" lon=\"24.7290818\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121437454\" lat=\"60.2274792\" lon=\"24.7247655\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121438880\" lat=\"60.2323152\" lon=\"24.7356497\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121439620\" lat=\"60.2349091\" lon=\"24.7280974\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121448500\" lat=\"60.2296676\" lon=\"24.7315779\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121449366\" lat=\"60.2272601\" lon=\"24.7212100\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121453490\" lat=\"60.2364457\" lon=\"24.7347305\" version=\"1\" timestamp=\"2011-01-26T10:33:46Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121458905\" lat=\"60.2311326\" lon=\"24.7272432\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121462519\" lat=\"60.2261231\" lon=\"24.7246582\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121465142\" lat=\"60.2330530\" lon=\"24.7375386\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121468108\" lat=\"60.2267671\" lon=\"24.7130089\" version=\"1\" timestamp=\"2011-01-26T10:40:43Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121470828\" lat=\"60.2318664\" lon=\"24.7279508\" version=\"1\" timestamp=\"2011-01-26T10:42:14Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121472529\" lat=\"60.2300766\" lon=\"24.7315850\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121480639\" lat=\"60.2308309\" lon=\"24.7327085\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121493664\" lat=\"60.2256902\" lon=\"24.7114727\" version=\"1\" timestamp=\"2011-01-26T10:52:49Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121495066\" lat=\"60.2283753\" lon=\"24.7195398\" version=\"1\" timestamp=\"2011-01-26T10:53:19Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121495785\" lat=\"60.2259294\" lon=\"24.7211162\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121498693\" lat=\"60.2249018\" lon=\"24.7145605\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121500386\" lat=\"60.2324436\" lon=\"24.7265205\" version=\"1\" timestamp=\"2011-01-26T10:55:44Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121501717\" lat=\"60.2355199\" lon=\"24.7285515\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121501727\" lat=\"60.2272132\" lon=\"24.7219894\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121503209\" lat=\"60.2311867\" lon=\"24.7280655\" version=\"1\" timestamp=\"2011-01-26T10:57:28Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121505436\" lat=\"60.2259236\" lon=\"24.7272693\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121509151\" lat=\"60.2305986\" lon=\"24.7273025\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121510927\" lat=\"60.2327991\" lon=\"24.7365817\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121510959\" lat=\"60.2259862\" lon=\"24.7279469\" version=\"3\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121516543\" lat=\"60.2382067\" lon=\"24.7391978\" version=\"1\" timestamp=\"2011-01-26T11:07:34Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121517169\" lat=\"60.2261057\" lon=\"24.7228003\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121522698\" lat=\"60.2273686\" lon=\"24.7209916\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121523874\" lat=\"60.2363146\" lon=\"24.7332271\" version=\"1\" timestamp=\"2011-01-26T11:12:56Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121524098\" lat=\"60.2256988\" lon=\"24.7196224\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121547271\" lat=\"60.2273068\" lon=\"24.7210537\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121563189\" lat=\"60.2350611\" lon=\"24.7394319\" version=\"1\" timestamp=\"2011-01-26T11:31:46Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121565228\" lat=\"60.2247942\" lon=\"24.7140508\" version=\"1\" timestamp=\"2011-01-26T11:32:29Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121567336\" lat=\"60.2270947\" lon=\"24.7234498\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121570469\" lat=\"60.2355762\" lon=\"24.7298989\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121571574\" lat=\"60.2285202\" lon=\"24.7182799\" version=\"1\" timestamp=\"2011-01-26T11:35:32Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121576944\" lat=\"60.2307427\" lon=\"24.7324706\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121577546\" lat=\"60.2319829\" lon=\"24.7352183\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121581788\" lat=\"60.2272217\" lon=\"24.7227042\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121582079\" lat=\"60.2270797\" lon=\"24.7235013\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121588440\" lat=\"60.2365129\" lon=\"24.7320142\" version=\"1\" timestamp=\"2011-01-26T11:44:36Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121592625\" lat=\"60.2246211\" lon=\"24.7121271\" version=\"1\" timestamp=\"2011-01-26T11:47:10Z\" changeset=\"7091971\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121596206\" lat=\"60.2261490\" lon=\"24.7280008\" version=\"3\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121598355\" lat=\"60.2342424\" lon=\"24.7282574\" version=\"1\" timestamp=\"2011-01-26T11:50:14Z\" changeset=\"7092613\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121598961\" lat=\"60.2352630\" lon=\"24.7394151\" version=\"1\" timestamp=\"2011-01-26T11:50:34Z\" changeset=\"7092613\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121599535\" lat=\"60.2338711\" lon=\"24.7282502\" version=\"1\" timestamp=\"2011-01-26T11:50:49Z\" changeset=\"7092613\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121601021\" lat=\"60.2285603\" lon=\"24.7255735\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121601448\" lat=\"60.2263276\" lon=\"24.7232221\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121603242\" lat=\"60.2259543\" lon=\"24.7215932\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121603657\" lat=\"60.2261256\" lon=\"24.7251779\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121605214\" lat=\"60.2251235\" lon=\"24.7157469\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121609346\" lat=\"60.2378472\" lon=\"24.7375684\" version=\"1\" timestamp=\"2011-01-26T11:56:13Z\" changeset=\"7092613\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121609721\" lat=\"60.2359801\" lon=\"24.7400887\" version=\"2\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1121609776\" lat=\"60.2318233\" lon=\"24.7265275\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121610451\" lat=\"60.2338480\" lon=\"24.7276768\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121611363\" lat=\"60.2374599\" lon=\"24.7409167\" version=\"1\" timestamp=\"2011-01-26T11:57:12Z\" changeset=\"7092613\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121611406\" lat=\"60.2264085\" lon=\"24.7122205\" version=\"1\" timestamp=\"2011-01-26T11:57:13Z\" changeset=\"7092613\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121612457\" lat=\"60.2271062\" lon=\"24.7242683\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121615390\" lat=\"60.2257961\" lon=\"24.7120815\" version=\"1\" timestamp=\"2011-01-26T11:58:58Z\" changeset=\"7092613\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121621418\" lat=\"60.2248973\" lon=\"24.7142088\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121621842\" lat=\"60.2259575\" lon=\"24.7263535\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121624294\" lat=\"60.2285511\" lon=\"24.7186132\" version=\"1\" timestamp=\"2011-01-26T12:02:34Z\" changeset=\"7092613\" uid=\"400335\" user=\"SykeImport\"/>\n  <node id=\"1121625893\" lat=\"60.2295336\" lon=\"24.7314713\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121632921\" lat=\"60.2346967\" lon=\"24.7396539\" version=\"3\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121633197\" lat=\"60.2281739\" lon=\"24.7254981\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1121635891\" lat=\"60.2276296\" lon=\"24.7251522\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1157979400\" lat=\"60.2363543\" lon=\"24.7317182\" version=\"3\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1538138535\" lat=\"60.2355424\" lon=\"24.7436886\" version=\"1\" timestamp=\"2011-12-09T14:05:58Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1538138567\" lat=\"60.2354608\" lon=\"24.7435980\" version=\"1\" timestamp=\"2011-12-09T14:05:59Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1538138691\" lat=\"60.2355421\" lon=\"24.7438530\" version=\"1\" timestamp=\"2011-12-09T14:06:01Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1538138712\" lat=\"60.2354806\" lon=\"24.7436643\" version=\"2\" timestamp=\"2012-10-01T07:40:23Z\" changeset=\"13318708\" uid=\"38239\" user=\"Daeron\">\n    <tag k=\"highway\" v=\"give_way\"/>\n  </node>\n  <node id=\"1538138724\" lat=\"60.2355854\" lon=\"24.7437185\" version=\"1\" timestamp=\"2011-12-09T14:06:01Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1538138817\" lat=\"60.2353979\" lon=\"24.7439397\" version=\"1\" timestamp=\"2011-12-09T14:06:03Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1538138929\" lat=\"60.2354360\" lon=\"24.7437894\" version=\"1\" timestamp=\"2011-12-09T14:06:05Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1538138940\" lat=\"60.2350076\" lon=\"24.7440713\" version=\"1\" timestamp=\"2011-12-09T14:06:05Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1538139031\" lat=\"60.2354497\" lon=\"24.7437495\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"1538139047\" lat=\"60.2356886\" lon=\"24.7437648\" version=\"1\" timestamp=\"2011-12-09T14:06:07Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1538139090\" lat=\"60.2355665\" lon=\"24.7437113\" version=\"1\" timestamp=\"2011-12-09T14:06:07Z\" changeset=\"10073267\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539655683\" lat=\"60.2334205\" lon=\"24.7421345\" version=\"2\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1539655691\" lat=\"60.2335091\" lon=\"24.7421021\" version=\"2\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1539655694\" lat=\"60.2339770\" lon=\"24.7398333\" version=\"2\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1539655816\" lat=\"60.2340783\" lon=\"24.7418907\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"1539655835\" lat=\"60.2341006\" lon=\"24.7421269\" version=\"1\" timestamp=\"2011-12-10T16:01:17Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539655838\" lat=\"60.2341066\" lon=\"24.7420806\" version=\"1\" timestamp=\"2011-12-10T16:01:17Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539655841\" lat=\"60.2341083\" lon=\"24.7427967\" version=\"1\" timestamp=\"2011-12-10T16:01:17Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539655844\" lat=\"60.2341236\" lon=\"24.7420189\" version=\"1\" timestamp=\"2011-12-10T16:01:18Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539655881\" lat=\"60.2341705\" lon=\"24.7419497\" version=\"1\" timestamp=\"2011-12-10T16:01:19Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656192\" lat=\"60.2351067\" lon=\"24.7416078\" version=\"1\" timestamp=\"2011-12-10T16:01:28Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656208\" lat=\"60.2352105\" lon=\"24.7417020\" version=\"1\" timestamp=\"2011-12-10T16:01:28Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656209\" lat=\"60.2352454\" lon=\"24.7417654\" version=\"1\" timestamp=\"2011-12-10T16:01:28Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656211\" lat=\"60.2352513\" lon=\"24.7429988\" version=\"1\" timestamp=\"2011-12-10T16:01:28Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656213\" lat=\"60.2352683\" lon=\"24.7418562\" version=\"1\" timestamp=\"2011-12-10T16:01:28Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656217\" lat=\"60.2352692\" lon=\"24.7431050\" version=\"1\" timestamp=\"2011-12-10T16:01:28Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656229\" lat=\"60.2355915\" lon=\"24.7423547\" version=\"1\" timestamp=\"2011-12-10T16:01:28Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656240\" lat=\"60.2356578\" lon=\"24.7423341\" version=\"1\" timestamp=\"2011-12-10T16:01:29Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656268\" lat=\"60.2362684\" lon=\"24.7432864\" version=\"1\" timestamp=\"2011-12-10T16:01:29Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1539656319\" lat=\"60.2367004\" lon=\"24.7419212\" version=\"1\" timestamp=\"2011-12-10T16:01:30Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\"/>\n  <node id=\"1700764927\" lat=\"60.2271099\" lon=\"24.7243544\" version=\"1\" timestamp=\"2012-04-04T10:29:28Z\" changeset=\"11174649\" uid=\"190690\" user=\"posiki\"/>\n  <node id=\"1700764931\" lat=\"60.2271515\" lon=\"24.7244085\" version=\"1\" timestamp=\"2012-04-04T10:29:28Z\" changeset=\"11174649\" uid=\"190690\" user=\"posiki\"/>\n  <node id=\"1700764934\" lat=\"60.2275907\" lon=\"24.7249899\" version=\"1\" timestamp=\"2012-04-04T10:29:28Z\" changeset=\"11174649\" uid=\"190690\" user=\"posiki\"/>\n  <node id=\"1828984129\" lat=\"60.2249739\" lon=\"24.7146550\" version=\"1\" timestamp=\"2012-07-17T19:08:59Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984132\" lat=\"60.2249777\" lon=\"24.7146826\" version=\"1\" timestamp=\"2012-07-17T19:08:59Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984134\" lat=\"60.2250163\" lon=\"24.7146499\" version=\"1\" timestamp=\"2012-07-17T19:08:59Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984136\" lat=\"60.2250138\" lon=\"24.7146951\" version=\"1\" timestamp=\"2012-07-17T19:08:59Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984140\" lat=\"60.2250712\" lon=\"24.7134549\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984143\" lat=\"60.2250849\" lon=\"24.7137436\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984145\" lat=\"60.2250886\" lon=\"24.7138942\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984147\" lat=\"60.2250849\" lon=\"24.7147102\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984149\" lat=\"60.2251011\" lon=\"24.7138164\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984151\" lat=\"60.2250924\" lon=\"24.7146148\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984153\" lat=\"60.2251161\" lon=\"24.7141704\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984157\" lat=\"60.2251148\" lon=\"24.7145169\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984161\" lat=\"60.2251298\" lon=\"24.7134524\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984163\" lat=\"60.2251435\" lon=\"24.7135704\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984166\" lat=\"60.2251681\" lon=\"24.7130950\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984168\" lat=\"60.2252653\" lon=\"24.7130824\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984169\" lat=\"60.2252945\" lon=\"24.7168973\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984170\" lat=\"60.2253750\" lon=\"24.7132456\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984171\" lat=\"60.2254411\" lon=\"24.7132030\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984172\" lat=\"60.2254598\" lon=\"24.7131276\" version=\"1\" timestamp=\"2012-07-17T19:09:00Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984174\" lat=\"60.2255890\" lon=\"24.7190700\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984176\" lat=\"60.2257212\" lon=\"24.7199462\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984178\" lat=\"60.2258569\" lon=\"24.7132118\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984182\" lat=\"60.2259511\" lon=\"24.7276007\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984183\" lat=\"60.2259648\" lon=\"24.7262801\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984184\" lat=\"60.2260146\" lon=\"24.7260968\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984185\" lat=\"60.2260533\" lon=\"24.7258482\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984186\" lat=\"60.2260795\" lon=\"24.7255394\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984188\" lat=\"60.2260944\" lon=\"24.7250423\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984192\" lat=\"60.2261792\" lon=\"24.7242966\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984193\" lat=\"60.2262503\" lon=\"24.7239276\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984194\" lat=\"60.2262839\" lon=\"24.7230237\" version=\"1\" timestamp=\"2012-07-17T19:09:01Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984197\" lat=\"60.2263251\" lon=\"24.7233828\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984198\" lat=\"60.2263375\" lon=\"24.7235912\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984199\" lat=\"60.2263251\" lon=\"24.7237192\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984200\" lat=\"60.2263489\" lon=\"24.7133290\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984202\" lat=\"60.2265516\" lon=\"24.7134639\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984203\" lat=\"60.2266257\" lon=\"24.7134852\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984204\" lat=\"60.2266821\" lon=\"24.7136663\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984206\" lat=\"60.2269624\" lon=\"24.7140213\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984207\" lat=\"60.2269942\" lon=\"24.7238777\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984208\" lat=\"60.2270048\" lon=\"24.7240606\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984209\" lat=\"60.2270198\" lon=\"24.7237410\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984210\" lat=\"60.2270374\" lon=\"24.7241475\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984211\" lat=\"60.2270427\" lon=\"24.7236221\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984212\" lat=\"60.2270912\" lon=\"24.7232439\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984213\" lat=\"60.2270850\" lon=\"24.7233380\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984215\" lat=\"60.2270833\" lon=\"24.7241866\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984218\" lat=\"60.2271335\" lon=\"24.7144687\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984219\" lat=\"60.2271441\" lon=\"24.7231143\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984220\" lat=\"60.2271767\" lon=\"24.7228924\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984221\" lat=\"60.2272057\" lon=\"24.7222806\" version=\"1\" timestamp=\"2012-07-17T19:09:02Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984222\" lat=\"60.2271988\" lon=\"24.7227628\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984225\" lat=\"60.2272293\" lon=\"24.7215828\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984227\" lat=\"60.2272456\" lon=\"24.7223710\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984231\" lat=\"60.2274157\" lon=\"24.7154834\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984232\" lat=\"60.2274659\" lon=\"24.7155385\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984233\" lat=\"60.2274950\" lon=\"24.7156432\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984234\" lat=\"60.2276176\" lon=\"24.7160711\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984235\" lat=\"60.2276625\" lon=\"24.7162362\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984236\" lat=\"60.2277789\" lon=\"24.7164652\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984237\" lat=\"60.2278679\" lon=\"24.7166924\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984239\" lat=\"60.2280451\" lon=\"24.7253015\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984241\" lat=\"60.2281073\" lon=\"24.7288652\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984242\" lat=\"60.2281341\" lon=\"24.7198598\" version=\"3\" timestamp=\"2025-07-30T09:48:50Z\" changeset=\"169710901\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1828984244\" lat=\"60.2281737\" lon=\"24.7197302\" version=\"3\" timestamp=\"2025-07-30T09:48:50Z\" changeset=\"169710901\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1828984247\" lat=\"60.2282548\" lon=\"24.7175626\" version=\"1\" timestamp=\"2012-07-17T19:09:03Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984254\" lat=\"60.2282213\" lon=\"24.7196539\" version=\"3\" timestamp=\"2025-07-30T09:48:50Z\" changeset=\"169710901\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1828984257\" lat=\"60.2282592\" lon=\"24.7195811\" version=\"3\" timestamp=\"2025-07-30T09:48:50Z\" changeset=\"169710901\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1828984264\" lat=\"60.2284514\" lon=\"24.7191994\" version=\"3\" timestamp=\"2025-07-30T09:48:50Z\" changeset=\"169710901\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1828984265\" lat=\"60.2285037\" lon=\"24.7254632\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984266\" lat=\"60.2286340\" lon=\"24.7299610\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1828984267\" lat=\"60.2288387\" lon=\"24.7257658\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984268\" lat=\"60.2289384\" lon=\"24.7260655\" version=\"1\" timestamp=\"2012-07-17T19:09:04Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984272\" lat=\"60.2297540\" lon=\"24.7268332\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984273\" lat=\"60.2298386\" lon=\"24.7316205\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984274\" lat=\"60.2299250\" lon=\"24.7268616\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984275\" lat=\"60.2299885\" lon=\"24.7316098\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984278\" lat=\"60.2302723\" lon=\"24.7317128\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984280\" lat=\"60.2304751\" lon=\"24.7272877\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984281\" lat=\"60.2305858\" lon=\"24.7321120\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984285\" lat=\"60.2314514\" lon=\"24.7344212\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984286\" lat=\"60.2316886\" lon=\"24.7347656\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984288\" lat=\"60.2321257\" lon=\"24.7354757\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984289\" lat=\"60.2322459\" lon=\"24.7264830\" version=\"3\" timestamp=\"2024-08-13T06:11:46Z\" changeset=\"155178405\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1828984291\" lat=\"60.2323470\" lon=\"24.7356728\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984292\" lat=\"60.2324457\" lon=\"24.7358041\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984293\" lat=\"60.2325324\" lon=\"24.7359836\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"1828984294\" lat=\"60.2325955\" lon=\"24.7361539\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984296\" lat=\"60.2327515\" lon=\"24.7364929\" version=\"1\" timestamp=\"2012-07-17T19:09:05Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984297\" lat=\"60.2327586\" lon=\"24.7364645\" version=\"1\" timestamp=\"2012-07-17T19:09:06Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984299\" lat=\"60.2328335\" lon=\"24.7366953\" version=\"1\" timestamp=\"2012-07-17T19:09:06Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984300\" lat=\"60.2328705\" lon=\"24.7369226\" version=\"1\" timestamp=\"2012-07-17T19:09:06Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984302\" lat=\"60.2331059\" lon=\"24.7378653\" version=\"1\" timestamp=\"2012-07-17T19:09:06Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984304\" lat=\"60.2331984\" lon=\"24.7384600\" version=\"1\" timestamp=\"2012-07-17T19:09:06Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984305\" lat=\"60.2332337\" lon=\"24.7384866\" version=\"1\" timestamp=\"2012-07-17T19:09:06Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984306\" lat=\"60.2332495\" lon=\"24.7386428\" version=\"1\" timestamp=\"2012-07-17T19:09:06Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984307\" lat=\"60.2333518\" lon=\"24.7389730\" version=\"1\" timestamp=\"2012-07-17T19:09:06Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984308\" lat=\"60.2337698\" lon=\"24.7393465\" version=\"2\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984310\" lat=\"60.2339687\" lon=\"24.7393015\" version=\"1\" timestamp=\"2012-07-17T19:09:06Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984317\" lat=\"60.2341818\" lon=\"24.7394948\" version=\"2\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984323\" lat=\"60.2342219\" lon=\"24.7394990\" version=\"2\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984327\" lat=\"60.2343609\" lon=\"24.7394589\" version=\"2\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984349\" lat=\"60.2347977\" lon=\"24.7396349\" version=\"2\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984378\" lat=\"60.2352668\" lon=\"24.7358788\" version=\"1\" timestamp=\"2012-07-17T19:09:09Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984381\" lat=\"60.2353341\" lon=\"24.7360595\" version=\"1\" timestamp=\"2012-07-17T19:09:09Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984383\" lat=\"60.2353491\" lon=\"24.7357683\" version=\"1\" timestamp=\"2012-07-17T19:09:09Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984386\" lat=\"60.2354039\" lon=\"24.7361449\" version=\"1\" timestamp=\"2012-07-17T19:09:09Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984396\" lat=\"60.2355179\" lon=\"24.7362125\" version=\"1\" timestamp=\"2012-07-17T19:09:09Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984397\" lat=\"60.2355510\" lon=\"24.7346636\" version=\"1\" timestamp=\"2012-07-17T19:09:09Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984401\" lat=\"60.2356924\" lon=\"24.7368804\" version=\"1\" timestamp=\"2012-07-17T19:09:10Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984403\" lat=\"60.2357278\" lon=\"24.7354808\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984405\" lat=\"60.2357266\" lon=\"24.7341869\" version=\"2\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984409\" lat=\"60.2358744\" lon=\"24.7349112\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984411\" lat=\"60.2360047\" lon=\"24.7334485\" version=\"1\" timestamp=\"2012-07-17T19:09:10Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"1828984412\" lat=\"60.2360741\" lon=\"24.7329934\" version=\"2\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984413\" lat=\"60.2360000\" lon=\"24.7330244\" version=\"2\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"1828984415\" lat=\"60.2361443\" lon=\"24.7330217\" version=\"1\" timestamp=\"2012-07-17T19:09:10Z\" changeset=\"12270775\" uid=\"38239\" user=\"Daeron\"/>\n  <node id=\"2009370349\" lat=\"60.2343332\" lon=\"24.7431867\" version=\"1\" timestamp=\"2012-11-11T20:09:42Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009370351\" lat=\"60.2343374\" lon=\"24.7433527\" version=\"1\" timestamp=\"2012-11-11T20:09:42Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009370359\" lat=\"60.2343686\" lon=\"24.7433495\" version=\"1\" timestamp=\"2012-11-11T20:09:42Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009370363\" lat=\"60.2343700\" lon=\"24.7434041\" version=\"1\" timestamp=\"2012-11-11T20:09:42Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009370370\" lat=\"60.2343918\" lon=\"24.7431808\" version=\"1\" timestamp=\"2012-11-11T20:09:43Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009370371\" lat=\"60.2343937\" lon=\"24.7432572\" version=\"1\" timestamp=\"2012-11-11T20:09:43Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009370408\" lat=\"60.2344537\" lon=\"24.7432512\" version=\"1\" timestamp=\"2012-11-11T20:09:43Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009370415\" lat=\"60.2344573\" lon=\"24.7433954\" version=\"1\" timestamp=\"2012-11-11T20:09:43Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009370579\" lat=\"60.2346354\" lon=\"24.7437268\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009370585\" lat=\"60.2346385\" lon=\"24.7435652\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009370632\" lat=\"60.2346899\" lon=\"24.7435692\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009370651\" lat=\"60.2346931\" lon=\"24.7433989\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009370663\" lat=\"60.2347262\" lon=\"24.7437338\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009370678\" lat=\"60.2347326\" lon=\"24.7434019\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009371467\" lat=\"60.2354357\" lon=\"24.7425498\" version=\"1\" timestamp=\"2012-11-11T20:09:56Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371475\" lat=\"60.2354387\" lon=\"24.7426659\" version=\"1\" timestamp=\"2012-11-11T20:09:56Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371481\" lat=\"60.2354482\" lon=\"24.7428691\" version=\"1\" timestamp=\"2012-11-11T20:09:56Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371482\" lat=\"60.2354511\" lon=\"24.7429882\" version=\"1\" timestamp=\"2012-11-11T20:09:56Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371483\" lat=\"60.2354516\" lon=\"24.7426646\" version=\"1\" timestamp=\"2012-11-11T20:09:56Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371488\" lat=\"60.2354542\" lon=\"24.7427697\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371494\" lat=\"60.2354641\" lon=\"24.7429869\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371499\" lat=\"60.2354661\" lon=\"24.7427685\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371501\" lat=\"60.2354668\" lon=\"24.7430928\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371504\" lat=\"60.2354686\" lon=\"24.7428670\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371513\" lat=\"60.2354786\" lon=\"24.7430916\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371514\" lat=\"60.2354813\" lon=\"24.7431978\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371523\" lat=\"60.2354942\" lon=\"24.7431965\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371526\" lat=\"60.2354968\" lon=\"24.7433014\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371538\" lat=\"60.2355102\" lon=\"24.7425426\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371540\" lat=\"60.2355106\" lon=\"24.7433000\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371541\" lat=\"60.2355112\" lon=\"24.7425868\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371545\" lat=\"60.2355133\" lon=\"24.7434050\" version=\"1\" timestamp=\"2012-11-11T20:09:57Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371610\" lat=\"60.2355843\" lon=\"24.7425798\" version=\"1\" timestamp=\"2012-11-11T20:09:58Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371612\" lat=\"60.2355867\" lon=\"24.7426807\" version=\"1\" timestamp=\"2012-11-11T20:09:58Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371619\" lat=\"60.2355953\" lon=\"24.7433967\" version=\"1\" timestamp=\"2012-11-11T20:09:58Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371622\" lat=\"60.2355958\" lon=\"24.7434153\" version=\"1\" timestamp=\"2012-11-11T20:09:58Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371623\" lat=\"60.2355960\" lon=\"24.7428976\" version=\"1\" timestamp=\"2012-11-11T20:09:58Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371626\" lat=\"60.2355987\" lon=\"24.7430088\" version=\"1\" timestamp=\"2012-11-11T20:09:58Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371628\" lat=\"60.2355992\" lon=\"24.7426795\" version=\"1\" timestamp=\"2012-11-11T20:09:58Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371635\" lat=\"60.2356017\" lon=\"24.7427852\" version=\"1\" timestamp=\"2012-11-11T20:09:58Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371652\" lat=\"60.2356122\" lon=\"24.7430075\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371654\" lat=\"60.2356140\" lon=\"24.7427840\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371656\" lat=\"60.2356147\" lon=\"24.7431095\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371660\" lat=\"60.2356167\" lon=\"24.7428956\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371673\" lat=\"60.2356248\" lon=\"24.7431086\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371675\" lat=\"60.2356274\" lon=\"24.7432188\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371688\" lat=\"60.2356411\" lon=\"24.7432174\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371689\" lat=\"60.2356436\" lon=\"24.7433218\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371699\" lat=\"60.2356563\" lon=\"24.7433206\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009371703\" lat=\"60.2356585\" lon=\"24.7434091\" version=\"1\" timestamp=\"2012-11-11T20:09:59Z\" changeset=\"13838651\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453717\" lat=\"60.2333252\" lon=\"24.7402408\" version=\"1\" timestamp=\"2012-11-11T20:33:11Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453734\" lat=\"60.2333413\" lon=\"24.7404221\" version=\"1\" timestamp=\"2012-11-11T20:33:12Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453788\" lat=\"60.2334070\" lon=\"24.7402114\" version=\"1\" timestamp=\"2012-11-11T20:33:12Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453790\" lat=\"60.2334084\" lon=\"24.7402270\" version=\"1\" timestamp=\"2012-11-11T20:33:12Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453805\" lat=\"60.2334724\" lon=\"24.7402041\" version=\"1\" timestamp=\"2012-11-11T20:33:12Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453809\" lat=\"60.2334871\" lon=\"24.7403697\" version=\"1\" timestamp=\"2012-11-11T20:33:12Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453821\" lat=\"60.2336228\" lon=\"24.7423592\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453823\" lat=\"60.2336273\" lon=\"24.7417960\" version=\"1\" timestamp=\"2012-11-11T20:33:12Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453825\" lat=\"60.2336382\" lon=\"24.7425233\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453827\" lat=\"60.2336402\" lon=\"24.7419373\" version=\"1\" timestamp=\"2012-11-11T20:33:12Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453830\" lat=\"60.2336390\" lon=\"24.7421847\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453836\" lat=\"60.2336543\" lon=\"24.7423472\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453871\" lat=\"60.2337335\" lon=\"24.7428252\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453884\" lat=\"60.2337656\" lon=\"24.7426421\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453887\" lat=\"60.2337542\" lon=\"24.7421407\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453912\" lat=\"60.2337722\" lon=\"24.7409441\" version=\"1\" timestamp=\"2012-11-11T20:33:13Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453924\" lat=\"60.2337849\" lon=\"24.7424673\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453927\" lat=\"60.2337844\" lon=\"24.7432938\" version=\"1\" timestamp=\"2012-11-11T20:33:13Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453930\" lat=\"60.2337864\" lon=\"24.7432052\" version=\"1\" timestamp=\"2012-11-11T20:33:13Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453937\" lat=\"60.2338056\" lon=\"24.7433711\" version=\"1\" timestamp=\"2012-11-11T20:33:13Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453939\" lat=\"60.2338073\" lon=\"24.7432959\" version=\"1\" timestamp=\"2012-11-11T20:33:13Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453943\" lat=\"60.2338033\" lon=\"24.7428749\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453946\" lat=\"60.2338100\" lon=\"24.7432074\" version=\"1\" timestamp=\"2012-11-11T20:33:13Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453952\" lat=\"60.2338132\" lon=\"24.7430705\" version=\"1\" timestamp=\"2012-11-11T20:33:13Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453958\" lat=\"60.2338179\" lon=\"24.7410943\" version=\"1\" timestamp=\"2012-11-11T20:33:13Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453963\" lat=\"60.2338355\" lon=\"24.7426917\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009453975\" lat=\"60.2338462\" lon=\"24.7408046\" version=\"1\" timestamp=\"2012-11-11T20:33:13Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453986\" lat=\"60.2338569\" lon=\"24.7408399\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009453993\" lat=\"60.2338583\" lon=\"24.7413434\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454012\" lat=\"60.2338740\" lon=\"24.7422378\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454014\" lat=\"60.2338744\" lon=\"24.7415190\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454018\" lat=\"60.2338881\" lon=\"24.7433788\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454026\" lat=\"60.2338890\" lon=\"24.7421708\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454029\" lat=\"60.2338898\" lon=\"24.7424095\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454036\" lat=\"60.2338945\" lon=\"24.7422302\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454037\" lat=\"60.2338950\" lon=\"24.7430781\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454039\" lat=\"60.2339004\" lon=\"24.7409454\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454042\" lat=\"60.2339061\" lon=\"24.7416929\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454044\" lat=\"60.2339109\" lon=\"24.7409799\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454046\" lat=\"60.2339190\" lon=\"24.7418342\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454049\" lat=\"60.2339206\" lon=\"24.7406557\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454059\" lat=\"60.2339304\" lon=\"24.7427532\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454066\" lat=\"60.2339333\" lon=\"24.7406975\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454079\" lat=\"60.2339449\" lon=\"24.7425916\" version=\"1\" timestamp=\"2012-11-11T20:33:14Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454100\" lat=\"60.2339728\" lon=\"24.7408054\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454107\" lat=\"60.2339817\" lon=\"24.7421362\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454110\" lat=\"60.2339841\" lon=\"24.7408424\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454114\" lat=\"60.2339857\" lon=\"24.7437490\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454124\" lat=\"60.2339908\" lon=\"24.7435517\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454138\" lat=\"60.2340030\" lon=\"24.7423673\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454145\" lat=\"60.2340086\" lon=\"24.7427816\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454162\" lat=\"60.2340136\" lon=\"24.7412861\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454168\" lat=\"60.2340232\" lon=\"24.7426200\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454178\" lat=\"60.2340296\" lon=\"24.7414617\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454205\" lat=\"60.2340610\" lon=\"24.7437569\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454209\" lat=\"60.2340660\" lon=\"24.7435595\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454222\" lat=\"60.2340703\" lon=\"24.7411904\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454229\" lat=\"60.2340790\" lon=\"24.7417421\" version=\"1\" timestamp=\"2012-11-11T20:33:15Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454233\" lat=\"60.2340865\" lon=\"24.7418375\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454250\" lat=\"60.2340938\" lon=\"24.7404428\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454252\" lat=\"60.2340941\" lon=\"24.7414203\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454254\" lat=\"60.2340968\" lon=\"24.7435901\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454261\" lat=\"60.2340996\" lon=\"24.7434023\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454264\" lat=\"60.2341052\" lon=\"24.7414157\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454266\" lat=\"60.2341184\" lon=\"24.7415436\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454274\" lat=\"60.2341252\" lon=\"24.7411674\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454275\" lat=\"60.2341272\" lon=\"24.7411872\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454278\" lat=\"60.2341373\" lon=\"24.7434528\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454290\" lat=\"60.2341381\" lon=\"24.7434047\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454293\" lat=\"60.2341411\" lon=\"24.7405982\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454308\" lat=\"60.2341553\" lon=\"24.7436220\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454311\" lat=\"60.2341557\" lon=\"24.7435936\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454337\" lat=\"60.2341701\" lon=\"24.7411692\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454340\" lat=\"60.2341799\" lon=\"24.7435248\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454344\" lat=\"60.2341809\" lon=\"24.7434555\" version=\"1\" timestamp=\"2012-11-11T20:33:16Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454355\" lat=\"60.2341840\" lon=\"24.7420335\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454358\" lat=\"60.2341850\" lon=\"24.7413127\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454368\" lat=\"60.2341903\" lon=\"24.7421027\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454371\" lat=\"60.2341904\" lon=\"24.7422773\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454375\" lat=\"60.2341973\" lon=\"24.7413076\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454378\" lat=\"60.2341987\" lon=\"24.7436246\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454382\" lat=\"60.2342002\" lon=\"24.7435261\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454397\" lat=\"60.2342108\" lon=\"24.7424855\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454401\" lat=\"60.2342116\" lon=\"24.7405362\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454407\" lat=\"60.2342174\" lon=\"24.7415021\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454421\" lat=\"60.2342336\" lon=\"24.7406712\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454425\" lat=\"60.2342466\" lon=\"24.7420104\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454433\" lat=\"60.2342529\" lon=\"24.7420796\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454435\" lat=\"60.2342617\" lon=\"24.7409869\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454440\" lat=\"60.2342749\" lon=\"24.7411262\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454447\" lat=\"60.2342863\" lon=\"24.7416763\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454449\" lat=\"60.2342938\" lon=\"24.7417717\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454452\" lat=\"60.2342959\" lon=\"24.7434417\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454454\" lat=\"60.2342964\" lon=\"24.7428656\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454456\" lat=\"60.2343003\" lon=\"24.7435885\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454459\" lat=\"60.2343050\" lon=\"24.7414653\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454461\" lat=\"60.2343156\" lon=\"24.7426006\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454463\" lat=\"60.2343170\" lon=\"24.7415960\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454466\" lat=\"60.2343207\" lon=\"24.7419724\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454469\" lat=\"60.2343231\" lon=\"24.7420383\" version=\"1\" timestamp=\"2012-11-11T20:33:17Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454479\" lat=\"60.2343335\" lon=\"24.7434396\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454486\" lat=\"60.2343377\" lon=\"24.7429002\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454488\" lat=\"60.2343379\" lon=\"24.7435840\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454491\" lat=\"60.2343393\" lon=\"24.7428781\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454508\" lat=\"60.2343746\" lon=\"24.7426179\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454511\" lat=\"60.2343761\" lon=\"24.7425968\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454518\" lat=\"60.2343813\" lon=\"24.7419635\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454521\" lat=\"60.2343837\" lon=\"24.7420294\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454529\" lat=\"60.2343996\" lon=\"24.7429183\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454533\" lat=\"60.2344201\" lon=\"24.7421863\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454536\" lat=\"60.2344219\" lon=\"24.7426101\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454541\" lat=\"60.2344405\" lon=\"24.7423946\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454543\" lat=\"60.2344445\" lon=\"24.7403825\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454551\" lat=\"60.2344665\" lon=\"24.7405175\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454553\" lat=\"60.2344694\" lon=\"24.7414044\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454555\" lat=\"60.2344754\" lon=\"24.7414694\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454557\" lat=\"60.2344777\" lon=\"24.7415364\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454566\" lat=\"60.2344839\" lon=\"24.7416038\" version=\"1\" timestamp=\"2012-11-11T20:33:18Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454569\" lat=\"60.2345010\" lon=\"24.7408954\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454571\" lat=\"60.2345025\" lon=\"24.7422701\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454573\" lat=\"60.2345142\" lon=\"24.7410347\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454576\" lat=\"60.2345119\" lon=\"24.7424175\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454578\" lat=\"60.2345434\" lon=\"24.7423719\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454580\" lat=\"60.2345457\" lon=\"24.7424088\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454583\" lat=\"60.2345492\" lon=\"24.7421891\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454585\" lat=\"60.2345535\" lon=\"24.7422570\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454588\" lat=\"60.2345691\" lon=\"24.7423299\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454590\" lat=\"60.2345713\" lon=\"24.7423647\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454591\" lat=\"60.2345924\" lon=\"24.7422851\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454593\" lat=\"60.2345948\" lon=\"24.7423233\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454594\" lat=\"60.2345952\" lon=\"24.7421046\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454595\" lat=\"60.2345997\" lon=\"24.7421761\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454597\" lat=\"60.2346178\" lon=\"24.7422426\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454598\" lat=\"60.2346200\" lon=\"24.7422781\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454599\" lat=\"60.2346422\" lon=\"24.7421950\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454602\" lat=\"60.2346447\" lon=\"24.7422358\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454604\" lat=\"60.2346454\" lon=\"24.7420189\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454606\" lat=\"60.2346499\" lon=\"24.7420906\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454608\" lat=\"60.2346679\" lon=\"24.7421549\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454610\" lat=\"60.2346699\" lon=\"24.7421879\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454613\" lat=\"60.2346860\" lon=\"24.7421048\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454614\" lat=\"60.2346888\" lon=\"24.7421496\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454615\" lat=\"60.2346921\" lon=\"24.7413891\" version=\"1\" timestamp=\"2012-11-11T20:33:19Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454617\" lat=\"60.2347076\" lon=\"24.7420030\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2009454618\" lat=\"60.2347043\" lon=\"24.7415221\" version=\"1\" timestamp=\"2012-11-11T20:33:19Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\"/>\n  <node id=\"2009454619\" lat=\"60.2347136\" lon=\"24.7420978\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2651860082\" lat=\"60.2387624\" lon=\"24.7379391\" version=\"2\" timestamp=\"2025-02-12T12:17:15Z\" changeset=\"162427801\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"2651860091\" lat=\"60.2396066\" lon=\"24.7371889\" version=\"2\" timestamp=\"2016-06-11T09:03:08Z\" changeset=\"39948402\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"2898580206\" lat=\"60.2380708\" lon=\"24.7385818\" version=\"1\" timestamp=\"2014-06-04T06:51:30Z\" changeset=\"22728162\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921549165\" lat=\"60.2357763\" lon=\"24.7418720\" version=\"2\" timestamp=\"2018-12-15T15:35:35Z\" changeset=\"65502642\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921549166\" lat=\"60.2357765\" lon=\"24.7420481\" version=\"2\" timestamp=\"2018-12-15T15:35:35Z\" changeset=\"65502642\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921549167\" lat=\"60.2355418\" lon=\"24.7420495\" version=\"2\" timestamp=\"2018-12-15T15:35:35Z\" changeset=\"65502642\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921549168\" lat=\"60.2355416\" lon=\"24.7418734\" version=\"2\" timestamp=\"2018-12-15T15:35:35Z\" changeset=\"65502642\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551963\" lat=\"60.2358545\" lon=\"24.7411554\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551964\" lat=\"60.2358569\" lon=\"24.7413310\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551965\" lat=\"60.2357076\" lon=\"24.7413394\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551966\" lat=\"60.2357091\" lon=\"24.7414435\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551967\" lat=\"60.2355480\" lon=\"24.7414526\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551968\" lat=\"60.2355496\" lon=\"24.7415695\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551969\" lat=\"60.2353863\" lon=\"24.7415786\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551970\" lat=\"60.2353838\" lon=\"24.7413987\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551971\" lat=\"60.2355431\" lon=\"24.7413898\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551972\" lat=\"60.2355415\" lon=\"24.7412731\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551973\" lat=\"60.2356969\" lon=\"24.7412645\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921551974\" lat=\"60.2356955\" lon=\"24.7411642\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921555985\" lat=\"60.2353961\" lon=\"24.7417106\" version=\"1\" timestamp=\"2014-06-18T08:22:16Z\" changeset=\"22999688\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921555986\" lat=\"60.2354027\" lon=\"24.7419520\" version=\"1\" timestamp=\"2014-06-18T08:22:16Z\" changeset=\"22999688\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921555987\" lat=\"60.2353921\" lon=\"24.7419574\" version=\"1\" timestamp=\"2014-06-18T08:22:16Z\" changeset=\"22999688\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921555988\" lat=\"60.2353987\" lon=\"24.7422283\" version=\"1\" timestamp=\"2014-06-18T08:22:16Z\" changeset=\"22999688\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921555989\" lat=\"60.2353428\" lon=\"24.7422337\" version=\"1\" timestamp=\"2014-06-18T08:22:16Z\" changeset=\"22999688\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921555990\" lat=\"60.2353375\" lon=\"24.7419681\" version=\"1\" timestamp=\"2014-06-18T08:22:16Z\" changeset=\"22999688\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921555991\" lat=\"60.2353508\" lon=\"24.7419628\" version=\"1\" timestamp=\"2014-06-18T08:22:16Z\" changeset=\"22999688\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921555992\" lat=\"60.2353428\" lon=\"24.7417160\" version=\"1\" timestamp=\"2014-06-18T08:22:16Z\" changeset=\"22999688\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921555993\" lat=\"60.2355148\" lon=\"24.7416401\" version=\"2\" timestamp=\"2016-12-05T19:45:04Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921555994\" lat=\"60.2354100\" lon=\"24.7416450\" version=\"2\" timestamp=\"2016-12-05T19:45:04Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921555995\" lat=\"60.2354167\" lon=\"24.7422287\" version=\"2\" timestamp=\"2016-12-05T19:45:04Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921555996\" lat=\"60.2355215\" lon=\"24.7422238\" version=\"2\" timestamp=\"2016-12-05T19:45:04Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566081\" lat=\"60.2351509\" lon=\"24.7419606\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566082\" lat=\"60.2352008\" lon=\"24.7420787\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566083\" lat=\"60.2350753\" lon=\"24.7422937\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566084\" lat=\"60.2350984\" lon=\"24.7423484\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566085\" lat=\"60.2349670\" lon=\"24.7425735\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566086\" lat=\"60.2349177\" lon=\"24.7424569\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566087\" lat=\"60.2350431\" lon=\"24.7422422\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566088\" lat=\"60.2350194\" lon=\"24.7421860\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566089\" lat=\"60.2349158\" lon=\"24.7426817\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566090\" lat=\"60.2346453\" lon=\"24.7428256\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566091\" lat=\"60.2346269\" lon=\"24.7426855\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566092\" lat=\"60.2348974\" lon=\"24.7425416\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566821\" lat=\"60.2357636\" lon=\"24.7406483\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566822\" lat=\"60.2355395\" lon=\"24.7403556\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566823\" lat=\"60.2355028\" lon=\"24.7404695\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566824\" lat=\"60.2357269\" lon=\"24.7407623\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566825\" lat=\"60.2354859\" lon=\"24.7403708\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566826\" lat=\"60.2351990\" lon=\"24.7400125\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566827\" lat=\"60.2351650\" lon=\"24.7401230\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921566828\" lat=\"60.2354519\" lon=\"24.7404813\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921568772\" lat=\"60.2354642\" lon=\"24.7408716\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921568773\" lat=\"60.2354682\" lon=\"24.7410062\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921568774\" lat=\"60.2353015\" lon=\"24.7410261\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921568775\" lat=\"60.2353040\" lon=\"24.7411099\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921568776\" lat=\"60.2351331\" lon=\"24.7411303\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921568777\" lat=\"60.2351287\" lon=\"24.7409833\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921568778\" lat=\"60.2352938\" lon=\"24.7409635\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921568779\" lat=\"60.2352917\" lon=\"24.7408922\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"2921568780\" lat=\"60.2351107\" lon=\"24.7402766\" version=\"1\" timestamp=\"2014-06-18T08:36:44Z\" changeset=\"22999939\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921568781\" lat=\"60.2349746\" lon=\"24.7407282\" version=\"1\" timestamp=\"2014-06-18T08:36:44Z\" changeset=\"22999939\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921568782\" lat=\"60.2349105\" lon=\"24.7406498\" version=\"1\" timestamp=\"2014-06-18T08:36:44Z\" changeset=\"22999939\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"2921568783\" lat=\"60.2350466\" lon=\"24.7401982\" version=\"1\" timestamp=\"2014-06-18T08:36:44Z\" changeset=\"22999939\" uid=\"2101719\" user=\"deodorantdealer\"/>\n  <node id=\"3288219619\" lat=\"60.2272490\" lon=\"24.7292027\" version=\"1\" timestamp=\"2015-01-14T19:26:50Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288219621\" lat=\"60.2272509\" lon=\"24.7292490\" version=\"1\" timestamp=\"2015-01-14T19:26:50Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288219763\" lat=\"60.2273007\" lon=\"24.7291954\" version=\"1\" timestamp=\"2015-01-14T19:26:51Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288219764\" lat=\"60.2273023\" lon=\"24.7292418\" version=\"1\" timestamp=\"2015-01-14T19:26:51Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220146\" lat=\"60.2276353\" lon=\"24.7288788\" version=\"1\" timestamp=\"2015-01-14T19:26:55Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220369\" lat=\"60.2276691\" lon=\"24.7288694\" version=\"1\" timestamp=\"2015-01-14T19:26:55Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220374\" lat=\"60.2276990\" lon=\"24.7288722\" version=\"1\" timestamp=\"2015-01-14T19:26:55Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220395\" lat=\"60.2277302\" lon=\"24.7288885\" version=\"1\" timestamp=\"2015-01-14T19:26:55Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220399\" lat=\"60.2277556\" lon=\"24.7289096\" version=\"1\" timestamp=\"2015-01-14T19:26:55Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220410\" lat=\"60.2277681\" lon=\"24.7288642\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220411\" lat=\"60.2277695\" lon=\"24.7296192\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220430\" lat=\"60.2277939\" lon=\"24.7295259\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220431\" lat=\"60.2278006\" lon=\"24.7289075\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220436\" lat=\"60.2278164\" lon=\"24.7295636\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220437\" lat=\"60.2278170\" lon=\"24.7295796\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220438\" lat=\"60.2278207\" lon=\"24.7294614\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220441\" lat=\"60.2278259\" lon=\"24.7294701\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220444\" lat=\"60.2278299\" lon=\"24.7289686\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220447\" lat=\"60.2278372\" lon=\"24.7294361\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220460\" lat=\"60.2278520\" lon=\"24.7290394\" version=\"1\" timestamp=\"2015-01-14T19:26:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220565\" lat=\"60.2278568\" lon=\"24.7293634\" version=\"1\" timestamp=\"2015-01-14T19:26:57Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220567\" lat=\"60.2278611\" lon=\"24.7293349\" version=\"1\" timestamp=\"2015-01-14T19:26:57Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220569\" lat=\"60.2278666\" lon=\"24.7291209\" version=\"1\" timestamp=\"2015-01-14T19:26:57Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220570\" lat=\"60.2278687\" lon=\"24.7292845\" version=\"1\" timestamp=\"2015-01-14T19:26:57Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220573\" lat=\"60.2278720\" lon=\"24.7292009\" version=\"1\" timestamp=\"2015-01-14T19:26:57Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220574\" lat=\"60.2278728\" lon=\"24.7291173\" version=\"1\" timestamp=\"2015-01-14T19:26:57Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288220579\" lat=\"60.2278782\" lon=\"24.7292005\" version=\"1\" timestamp=\"2015-01-14T19:26:57Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288222596\" lat=\"60.2334383\" lon=\"24.7412457\" version=\"1\" timestamp=\"2015-01-14T19:27:28Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288222599\" lat=\"60.2334409\" lon=\"24.7412842\" version=\"1\" timestamp=\"2015-01-14T19:27:29Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288222611\" lat=\"60.2334601\" lon=\"24.7412398\" version=\"1\" timestamp=\"2015-01-14T19:27:29Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288222614\" lat=\"60.2334626\" lon=\"24.7412783\" version=\"1\" timestamp=\"2015-01-14T19:27:29Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288222719\" lat=\"60.2336708\" lon=\"24.7428478\" version=\"1\" timestamp=\"2015-01-14T19:27:31Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288222724\" lat=\"60.2336749\" lon=\"24.7428880\" version=\"1\" timestamp=\"2015-01-14T19:27:31Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288222733\" lat=\"60.2336950\" lon=\"24.7428380\" version=\"1\" timestamp=\"2015-01-14T19:27:31Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288222737\" lat=\"60.2336990\" lon=\"24.7428783\" version=\"1\" timestamp=\"2015-01-14T19:27:31Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223342\" lat=\"60.2346267\" lon=\"24.7407657\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223357\" lat=\"60.2346482\" lon=\"24.7410180\" version=\"1\" timestamp=\"2015-01-14T19:27:41Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223363\" lat=\"60.2346569\" lon=\"24.7412409\" version=\"1\" timestamp=\"2015-01-14T19:27:42Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223379\" lat=\"60.2346840\" lon=\"24.7408418\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223390\" lat=\"60.2346945\" lon=\"24.7408103\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223393\" lat=\"60.2346948\" lon=\"24.7410107\" version=\"1\" timestamp=\"2015-01-14T19:27:42Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223404\" lat=\"60.2347034\" lon=\"24.7412336\" version=\"1\" timestamp=\"2015-01-14T19:27:42Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223409\" lat=\"60.2347104\" lon=\"24.7407622\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223426\" lat=\"60.2347283\" lon=\"24.7407080\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223436\" lat=\"60.2347442\" lon=\"24.7406599\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223453\" lat=\"60.2347620\" lon=\"24.7406058\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223454\" lat=\"60.2347627\" lon=\"24.7403533\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223465\" lat=\"60.2347779\" lon=\"24.7405577\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223478\" lat=\"60.2347958\" lon=\"24.7405036\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223484\" lat=\"60.2348052\" lon=\"24.7420956\" version=\"1\" timestamp=\"2015-01-14T19:27:43Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223487\" lat=\"60.2348117\" lon=\"24.7404555\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223489\" lat=\"60.2348120\" lon=\"24.7421718\" version=\"1\" timestamp=\"2015-01-14T19:27:43Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223492\" lat=\"60.2348202\" lon=\"24.7404300\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223532\" lat=\"60.2349028\" lon=\"24.7408753\" version=\"1\" timestamp=\"2015-01-14T19:27:45Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223542\" lat=\"60.2349109\" lon=\"24.7420584\" version=\"1\" timestamp=\"2015-01-14T19:27:45Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223547\" lat=\"60.2349179\" lon=\"24.7421319\" version=\"1\" timestamp=\"2015-01-14T19:27:45Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223555\" lat=\"60.2349254\" lon=\"24.7414450\" version=\"1\" timestamp=\"2015-01-14T19:27:45Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223572\" lat=\"60.2349475\" lon=\"24.7408680\" version=\"1\" timestamp=\"2015-01-14T19:27:45Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223586\" lat=\"60.2349701\" lon=\"24.7414384\" version=\"1\" timestamp=\"2015-01-14T19:27:46Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223628\" lat=\"60.2350410\" lon=\"24.7413084\" version=\"1\" timestamp=\"2015-01-14T19:27:46Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223635\" lat=\"60.2350461\" lon=\"24.7414520\" version=\"1\" timestamp=\"2015-01-14T19:27:46Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223666\" lat=\"60.2350865\" lon=\"24.7413016\" version=\"1\" timestamp=\"2015-01-14T19:27:47Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223669\" lat=\"60.2350919\" lon=\"24.7414443\" version=\"1\" timestamp=\"2015-01-14T19:27:47Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223749\" lat=\"60.2352230\" lon=\"24.7414263\" version=\"1\" timestamp=\"2015-01-14T19:27:48Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223751\" lat=\"60.2352256\" lon=\"24.7414978\" version=\"1\" timestamp=\"2015-01-14T19:27:48Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223762\" lat=\"60.2352440\" lon=\"24.7414242\" version=\"1\" timestamp=\"2015-01-14T19:27:48Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223764\" lat=\"60.2352464\" lon=\"24.7414948\" version=\"1\" timestamp=\"2015-01-14T19:27:48Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\"/>\n  <node id=\"3288223799\" lat=\"60.2349153\" lon=\"24.7435475\" version=\"3\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223832\" lat=\"60.2351127\" lon=\"24.7438784\" version=\"3\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223833\" lat=\"60.2353260\" lon=\"24.7434341\" version=\"3\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"3288223852\" lat=\"60.2353435\" lon=\"24.7434871\" version=\"3\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4143402186\" lat=\"60.2283516\" lon=\"24.7179139\" version=\"4\" timestamp=\"2025-08-17T16:54:09Z\" changeset=\"170577090\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"4195526816\" lat=\"60.2378039\" lon=\"24.7422547\" version=\"3\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"4195526817\" lat=\"60.2379270\" lon=\"24.7420981\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"4237124746\" lat=\"60.2388446\" lon=\"24.7382553\" version=\"1\" timestamp=\"2016-06-11T09:05:13Z\" changeset=\"39948402\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4237124747\" lat=\"60.2387381\" lon=\"24.7401272\" version=\"1\" timestamp=\"2016-06-11T09:05:13Z\" changeset=\"39948402\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4237124748\" lat=\"60.2381682\" lon=\"24.7416070\" version=\"2\" timestamp=\"2016-12-11T14:20:39Z\" changeset=\"44323882\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4237124749\" lat=\"60.2389405\" lon=\"24.7398341\" version=\"1\" timestamp=\"2016-06-11T09:05:13Z\" changeset=\"39948402\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4237124751\" lat=\"60.2381949\" lon=\"24.7414541\" version=\"1\" timestamp=\"2016-06-11T09:05:13Z\" changeset=\"39948402\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4237124752\" lat=\"60.2388180\" lon=\"24.7400540\" version=\"1\" timestamp=\"2016-06-11T09:05:13Z\" changeset=\"39948402\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4237124753\" lat=\"60.2389398\" lon=\"24.7399659\" version=\"3\" timestamp=\"2016-12-11T14:20:39Z\" changeset=\"44323882\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4237124754\" lat=\"60.2388180\" lon=\"24.7391831\" version=\"1\" timestamp=\"2016-06-11T09:05:13Z\" changeset=\"39948402\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4237124756\" lat=\"60.2386502\" lon=\"24.7403150\" version=\"1\" timestamp=\"2016-06-11T09:05:13Z\" changeset=\"39948402\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250468337\" lat=\"60.2392805\" lon=\"24.7379421\" version=\"1\" timestamp=\"2016-06-18T13:32:54Z\" changeset=\"40115982\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4250512206\" lat=\"60.2356064\" lon=\"24.7411143\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512207\" lat=\"60.2371914\" lon=\"24.7413511\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512208\" lat=\"60.2352421\" lon=\"24.7416301\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512209\" lat=\"60.2351435\" lon=\"24.7415228\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512211\" lat=\"60.2362727\" lon=\"24.7407449\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512212\" lat=\"60.2380941\" lon=\"24.7371186\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512213\" lat=\"60.2368766\" lon=\"24.7417848\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512214\" lat=\"60.2367888\" lon=\"24.7416346\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512215\" lat=\"60.2353300\" lon=\"24.7413511\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512216\" lat=\"60.2358173\" lon=\"24.7408093\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512217\" lat=\"60.2353827\" lon=\"24.7411357\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512218\" lat=\"60.2351915\" lon=\"24.7413189\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512220\" lat=\"60.2368607\" lon=\"24.7417318\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512222\" lat=\"60.2368340\" lon=\"24.7416400\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250512223\" lat=\"60.2367547\" lon=\"24.7411205\" version=\"1\" timestamp=\"2016-06-18T14:14:59Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250515016\" lat=\"60.2353543\" lon=\"24.7394733\" version=\"2\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"4250515024\" lat=\"60.2358459\" lon=\"24.7409815\" version=\"1\" timestamp=\"2016-06-18T14:17:25Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250515043\" lat=\"60.2374090\" lon=\"24.7423519\" version=\"1\" timestamp=\"2016-06-18T14:17:26Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250525482\" lat=\"60.2259758\" lon=\"24.7299035\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250525483\" lat=\"60.2271705\" lon=\"24.7292492\" version=\"3\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250525484\" lat=\"60.2257081\" lon=\"24.7305463\" version=\"4\" timestamp=\"2025-07-09T05:25:34Z\" changeset=\"168677046\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250525485\" lat=\"60.2317688\" lon=\"24.7363694\" version=\"2\" timestamp=\"2023-12-01T18:58:40Z\" changeset=\"144671875\" uid=\"20402022\" user=\"HarriHe\"/>\n  <node id=\"4250525486\" lat=\"60.2341444\" lon=\"24.7400092\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250525487\" lat=\"60.2337263\" lon=\"24.7412752\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250525488\" lat=\"60.2304797\" lon=\"24.7331696\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526489\" lat=\"60.2350684\" lon=\"24.7400199\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526491\" lat=\"60.2351196\" lon=\"24.7398920\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526492\" lat=\"60.2256534\" lon=\"24.7301896\" version=\"2\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526493\" lat=\"60.2351829\" lon=\"24.7398322\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526494\" lat=\"60.2326504\" lon=\"24.7405510\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526495\" lat=\"60.2342510\" lon=\"24.7400092\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526496\" lat=\"60.2345945\" lon=\"24.7402774\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526497\" lat=\"60.2322752\" lon=\"24.7384919\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526498\" lat=\"60.2352442\" lon=\"24.7398483\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526499\" lat=\"60.2322542\" lon=\"24.7384200\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"4250526500\" lat=\"60.2291403\" lon=\"24.7322906\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526501\" lat=\"60.2284424\" lon=\"24.7309978\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526502\" lat=\"60.2300642\" lon=\"24.7326492\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526503\" lat=\"60.2327893\" lon=\"24.7398491\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526504\" lat=\"60.2323441\" lon=\"24.7385930\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526505\" lat=\"60.2264830\" lon=\"24.7300492\" version=\"2\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526506\" lat=\"60.2343579\" lon=\"24.7400368\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526507\" lat=\"60.2300961\" lon=\"24.7324937\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526508\" lat=\"60.2277841\" lon=\"24.7297310\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526510\" lat=\"60.2327702\" lon=\"24.7394191\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526511\" lat=\"60.2327733\" lon=\"24.7396077\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526512\" lat=\"60.2318673\" lon=\"24.7360825\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526513\" lat=\"60.2304637\" lon=\"24.7329228\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526514\" lat=\"60.2274334\" lon=\"24.7291465\" version=\"2\" timestamp=\"2024-01-31T09:19:43Z\" changeset=\"146895228\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526515\" lat=\"60.2325678\" lon=\"24.7367691\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526516\" lat=\"60.2327813\" lon=\"24.7402407\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526518\" lat=\"60.2326424\" lon=\"24.7380019\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"4250526519\" lat=\"60.2325808\" lon=\"24.7381509\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"4250526521\" lat=\"60.2340539\" lon=\"24.7399878\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526522\" lat=\"60.2287350\" lon=\"24.7311418\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526523\" lat=\"60.2353220\" lon=\"24.7399135\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526524\" lat=\"60.2317372\" lon=\"24.7360028\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526526\" lat=\"60.2295155\" lon=\"24.7323435\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526527\" lat=\"60.2297769\" lon=\"24.7323979\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526529\" lat=\"60.2354706\" lon=\"24.7400414\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526530\" lat=\"60.2275680\" lon=\"24.7288444\" version=\"2\" timestamp=\"2024-01-31T09:19:43Z\" changeset=\"146895228\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526531\" lat=\"60.2339634\" lon=\"24.7399609\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526532\" lat=\"60.2326903\" lon=\"24.7371124\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526533\" lat=\"60.2335403\" lon=\"24.7418554\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526535\" lat=\"60.2309964\" lon=\"24.7359644\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526537\" lat=\"60.2336012\" lon=\"24.7417044\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526538\" lat=\"60.2357481\" lon=\"24.7404016\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526539\" lat=\"60.2326170\" lon=\"24.7384921\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526540\" lat=\"60.2326687\" lon=\"24.7378370\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"4250526541\" lat=\"60.2305626\" lon=\"24.7338302\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526542\" lat=\"60.2325119\" lon=\"24.7366940\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526543\" lat=\"60.2258746\" lon=\"24.7282298\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526545\" lat=\"60.2334760\" lon=\"24.7413557\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526546\" lat=\"60.2268256\" lon=\"24.7295880\" version=\"2\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526548\" lat=\"60.2357875\" lon=\"24.7405403\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526550\" lat=\"60.2313080\" lon=\"24.7358786\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526551\" lat=\"60.2336735\" lon=\"24.7414960\" version=\"1\" timestamp=\"2016-06-18T14:20:53Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526552\" lat=\"60.2323924\" lon=\"24.7386421\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526553\" lat=\"60.2324883\" lon=\"24.7385884\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526554\" lat=\"60.2327788\" lon=\"24.7387013\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526555\" lat=\"60.2336149\" lon=\"24.7409434\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526556\" lat=\"60.2302430\" lon=\"24.7341001\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"4250526557\" lat=\"60.2307300\" lon=\"24.7360020\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526558\" lat=\"60.2303924\" lon=\"24.7342200\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"4250526560\" lat=\"60.2259590\" lon=\"24.7288638\" version=\"2\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526561\" lat=\"60.2334364\" lon=\"24.7419037\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526562\" lat=\"60.2335798\" lon=\"24.7398966\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526563\" lat=\"60.2344671\" lon=\"24.7401227\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526564\" lat=\"60.2348049\" lon=\"24.7402399\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526565\" lat=\"60.2266294\" lon=\"24.7298181\" version=\"2\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526568\" lat=\"60.2332127\" lon=\"24.7396721\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526569\" lat=\"60.2329700\" lon=\"24.7398751\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526571\" lat=\"60.2317088\" lon=\"24.7363480\" version=\"2\" timestamp=\"2023-12-01T18:58:40Z\" changeset=\"144671875\" uid=\"20402022\" user=\"HarriHe\"/>\n  <node id=\"4250526572\" lat=\"60.2279812\" lon=\"24.7299938\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526573\" lat=\"60.2282080\" lon=\"24.7304023\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526574\" lat=\"60.2326534\" lon=\"24.7374673\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526575\" lat=\"60.2267677\" lon=\"24.7289864\" version=\"3\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4250526577\" lat=\"60.2336704\" lon=\"24.7411518\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526578\" lat=\"60.2297259\" lon=\"24.7325795\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526579\" lat=\"60.2346828\" lon=\"24.7403319\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526581\" lat=\"60.2320968\" lon=\"24.7362388\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250526582\" lat=\"60.2285756\" lon=\"24.7311748\" version=\"1\" timestamp=\"2016-06-18T14:20:54Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551247\" lat=\"60.2330836\" lon=\"24.7377274\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551248\" lat=\"60.2378598\" lon=\"24.7373224\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551250\" lat=\"60.2376681\" lon=\"24.7367002\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551254\" lat=\"60.2377426\" lon=\"24.7370059\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551256\" lat=\"60.2326812\" lon=\"24.7363484\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551257\" lat=\"60.2331351\" lon=\"24.7380630\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551259\" lat=\"60.2376095\" lon=\"24.7364910\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551267\" lat=\"60.2364088\" lon=\"24.7343075\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551273\" lat=\"60.2378385\" lon=\"24.7371991\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551276\" lat=\"60.2377293\" lon=\"24.7368933\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551277\" lat=\"60.2377773\" lon=\"24.7371186\" version=\"1\" timestamp=\"2016-06-18T14:27:24Z\" changeset=\"40116567\" uid=\"494482\" user=\"Muokkaaja\"/>\n  <node id=\"4250551278\" lat=\"60.2365889\" lon=\"24.7350205\" version=\"2\" timestamp=\"2025-02-12T12:17:15Z\" changeset=\"162427801\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"4254168965\" lat=\"60.2354262\" lon=\"24.7439228\" version=\"4\" timestamp=\"2025-07-18T16:55:36Z\" changeset=\"169117116\" uid=\"486606\" user=\"Barro\">\n    <tag k=\"crossing:island\" v=\"yes\"/>\n    <tag k=\"crossing:markings\" v=\"yes\"/>\n    <tag k=\"crossing:signals\" v=\"no\"/>\n    <tag k=\"highway\" v=\"crossing\"/>\n    <tag k=\"traffic_calming\" v=\"table\"/>\n  </node>\n  <node id=\"4254168966\" lat=\"60.2354475\" lon=\"24.7440473\" version=\"4\" timestamp=\"2025-07-18T16:55:37Z\" changeset=\"169117116\" uid=\"486606\" user=\"Barro\">\n    <tag k=\"crossing:island\" v=\"yes\"/>\n    <tag k=\"crossing:markings\" v=\"yes\"/>\n    <tag k=\"crossing:signals\" v=\"no\"/>\n    <tag k=\"highway\" v=\"crossing\"/>\n    <tag k=\"traffic_calming\" v=\"table\"/>\n  </node>\n  <node id=\"4254168967\" lat=\"60.2354591\" lon=\"24.7441155\" version=\"1\" timestamp=\"2016-06-20T10:57:05Z\" changeset=\"40154543\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4254168968\" lat=\"60.2356442\" lon=\"24.7436965\" version=\"1\" timestamp=\"2016-06-20T10:57:05Z\" changeset=\"40154543\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4254168969\" lat=\"60.2356586\" lon=\"24.7437829\" version=\"4\" timestamp=\"2025-07-18T16:55:36Z\" changeset=\"169117116\" uid=\"486606\" user=\"Barro\">\n    <tag k=\"crossing:island\" v=\"yes\"/>\n    <tag k=\"crossing:markings\" v=\"yes\"/>\n    <tag k=\"crossing:signals\" v=\"no\"/>\n    <tag k=\"highway\" v=\"crossing\"/>\n    <tag k=\"traffic_calming\" v=\"table\"/>\n  </node>\n  <node id=\"4255037761\" lat=\"60.2370225\" lon=\"24.7427975\" version=\"1\" timestamp=\"2016-06-20T20:14:04Z\" changeset=\"40166189\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4300534693\" lat=\"60.2353000\" lon=\"24.7423000\" version=\"3\" timestamp=\"2025-06-14T12:07:30Z\" changeset=\"167610054\" uid=\"21499192\" user=\"Aku Kuhmonen\">\n    <tag k=\"emergency\" v=\"fire_hydrant\"/>\n    <tag k=\"fire_hydrant:diameter\" v=\"150\"/>\n    <tag k=\"fire_hydrant:position\" v=\"lane\"/>\n    <tag k=\"fire_hydrant:type\" v=\"underground\"/>\n    <tag k=\"name\" v=\"Maapaloposti\"/>\n    <tag k=\"survey:date\" v=\"2025-06-14\"/>\n  </node>\n  <node id=\"4540138243\" lat=\"60.2374334\" lon=\"24.7424640\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138244\" lat=\"60.2353282\" lon=\"24.7418871\" version=\"2\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138245\" lat=\"60.2353016\" lon=\"24.7427400\" version=\"2\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138246\" lat=\"60.2353065\" lon=\"24.7429877\" version=\"2\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138247\" lat=\"60.2354017\" lon=\"24.7432972\" version=\"2\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138248\" lat=\"60.2355203\" lon=\"24.7436211\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138249\" lat=\"60.2356303\" lon=\"24.7436475\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138250\" lat=\"60.2366806\" lon=\"24.7429510\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138251\" lat=\"60.2373642\" lon=\"24.7425290\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138252\" lat=\"60.2367772\" lon=\"24.7418467\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138266\" lat=\"60.2361781\" lon=\"24.7421298\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138274\" lat=\"60.2354424\" lon=\"24.7423772\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138275\" lat=\"60.2354429\" lon=\"24.7422275\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138276\" lat=\"60.2354384\" lon=\"24.7417196\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138277\" lat=\"60.2355575\" lon=\"24.7416330\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138278\" lat=\"60.2356748\" lon=\"24.7415564\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138279\" lat=\"60.2357525\" lon=\"24.7414632\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138280\" lat=\"60.2358567\" lon=\"24.7414099\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138281\" lat=\"60.2360418\" lon=\"24.7413899\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138282\" lat=\"60.2362517\" lon=\"24.7414166\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138283\" lat=\"60.2362633\" lon=\"24.7415198\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138284\" lat=\"60.2362335\" lon=\"24.7417196\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138285\" lat=\"60.2361823\" lon=\"24.7420093\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138286\" lat=\"60.2361615\" lon=\"24.7421363\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540138287\" lat=\"60.2359351\" lon=\"24.7422252\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540139290\" lat=\"60.2356338\" lon=\"24.7423415\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540139296\" lat=\"60.2358311\" lon=\"24.7422660\" version=\"1\" timestamp=\"2016-12-05T19:45:02Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152720\" lat=\"60.2348826\" lon=\"24.7415478\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152721\" lat=\"60.2347389\" lon=\"24.7415797\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152722\" lat=\"60.2347119\" lon=\"24.7410858\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152723\" lat=\"60.2347758\" lon=\"24.7409366\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152724\" lat=\"60.2348482\" lon=\"24.7409205\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152725\" lat=\"60.2348142\" lon=\"24.7416938\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152726\" lat=\"60.2348063\" lon=\"24.7415647\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152727\" lat=\"60.2347738\" lon=\"24.7409714\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152728\" lat=\"60.2348976\" lon=\"24.7417787\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152729\" lat=\"60.2347041\" lon=\"24.7418510\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152730\" lat=\"60.2347281\" lon=\"24.7421110\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152731\" lat=\"60.2349215\" lon=\"24.7420387\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152732\" lat=\"60.2349648\" lon=\"24.7416364\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152733\" lat=\"60.2349054\" lon=\"24.7418634\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152734\" lat=\"60.2348925\" lon=\"24.7419137\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152735\" lat=\"60.2347271\" lon=\"24.7419791\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152736\" lat=\"60.2347565\" lon=\"24.7419674\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152737\" lat=\"60.2347496\" lon=\"24.7421030\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152738\" lat=\"60.2347311\" lon=\"24.7421644\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152739\" lat=\"60.2346831\" lon=\"24.7422611\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152740\" lat=\"60.2345313\" lon=\"24.7425086\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152741\" lat=\"60.2352079\" lon=\"24.7424073\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152742\" lat=\"60.2351655\" lon=\"24.7424501\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152743\" lat=\"60.2350159\" lon=\"24.7427222\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152744\" lat=\"60.2349489\" lon=\"24.7428167\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152745\" lat=\"60.2347244\" lon=\"24.7429809\" version=\"1\" timestamp=\"2016-12-05T19:53:49Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152746\" lat=\"60.2349463\" lon=\"24.7428367\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4540152747\" lat=\"60.2347109\" lon=\"24.7430520\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4540152748\" lat=\"60.2347604\" lon=\"24.7431752\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152749\" lat=\"60.2349695\" lon=\"24.7428934\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4540152750\" lat=\"60.2350840\" lon=\"24.7408182\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152751\" lat=\"60.2349813\" lon=\"24.7408311\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152752\" lat=\"60.2349958\" lon=\"24.7412994\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152753\" lat=\"60.2350985\" lon=\"24.7412865\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152754\" lat=\"60.2350007\" lon=\"24.7416227\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152755\" lat=\"60.2349857\" lon=\"24.7413146\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152756\" lat=\"60.2349929\" lon=\"24.7412041\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152757\" lat=\"60.2350096\" lon=\"24.7410907\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152758\" lat=\"60.2350168\" lon=\"24.7408267\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152759\" lat=\"60.2350275\" lon=\"24.7407440\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152760\" lat=\"60.2351446\" lon=\"24.7403853\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152761\" lat=\"60.2351769\" lon=\"24.7403011\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152762\" lat=\"60.2352175\" lon=\"24.7403059\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152763\" lat=\"60.2352833\" lon=\"24.7403877\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152764\" lat=\"60.2354016\" lon=\"24.7405466\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152765\" lat=\"60.2354577\" lon=\"24.7405611\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152766\" lat=\"60.2355151\" lon=\"24.7405948\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152767\" lat=\"60.2355653\" lon=\"24.7406501\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152768\" lat=\"60.2340593\" lon=\"24.7416906\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"4540152769\" lat=\"60.2342686\" lon=\"24.7416098\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152770\" lat=\"60.2342161\" lon=\"24.7410132\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152771\" lat=\"60.2342027\" lon=\"24.7409345\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152772\" lat=\"60.2341771\" lon=\"24.7408369\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152773\" lat=\"60.2341434\" lon=\"24.7407799\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152774\" lat=\"60.2341165\" lon=\"24.7407745\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152775\" lat=\"60.2340667\" lon=\"24.7408287\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152776\" lat=\"60.2340128\" lon=\"24.7409182\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152777\" lat=\"60.2339724\" lon=\"24.7409996\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152778\" lat=\"60.2339334\" lon=\"24.7410783\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152779\" lat=\"60.2338714\" lon=\"24.7411379\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152780\" lat=\"60.2339825\" lon=\"24.7409793\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152781\" lat=\"60.2340222\" lon=\"24.7410755\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152782\" lat=\"60.2340783\" lon=\"24.7416814\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152783\" lat=\"60.2339077\" lon=\"24.7416045\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152784\" lat=\"60.2339354\" lon=\"24.7419088\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152785\" lat=\"60.2340815\" lon=\"24.7418513\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152786\" lat=\"60.2340538\" lon=\"24.7415487\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152787\" lat=\"60.2340663\" lon=\"24.7416860\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152788\" lat=\"60.2340742\" lon=\"24.7418547\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"4540152889\" lat=\"60.2339304\" lon=\"24.7417381\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152890\" lat=\"60.2341033\" lon=\"24.7423633\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152891\" lat=\"60.2340442\" lon=\"24.7424241\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152892\" lat=\"60.2336835\" lon=\"24.7425509\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152893\" lat=\"60.2341085\" lon=\"24.7429577\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152894\" lat=\"60.2340271\" lon=\"24.7429419\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152895\" lat=\"60.2339655\" lon=\"24.7429181\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152896\" lat=\"60.2339222\" lon=\"24.7428890\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152897\" lat=\"60.2338711\" lon=\"24.7428098\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152898\" lat=\"60.2338278\" lon=\"24.7430422\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152899\" lat=\"60.2341108\" lon=\"24.7431163\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152900\" lat=\"60.2339740\" lon=\"24.7431066\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152901\" lat=\"60.2339720\" lon=\"24.7432185\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152902\" lat=\"60.2341088\" lon=\"24.7432282\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152903\" lat=\"60.2341873\" lon=\"24.7430885\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152904\" lat=\"60.2342840\" lon=\"24.7428471\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152905\" lat=\"60.2343130\" lon=\"24.7432034\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152906\" lat=\"60.2352294\" lon=\"24.7424753\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152907\" lat=\"60.2352524\" lon=\"24.7420309\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152908\" lat=\"60.2352088\" lon=\"24.7417932\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152909\" lat=\"60.2351550\" lon=\"24.7417002\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152910\" lat=\"60.2336440\" lon=\"24.7432866\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152911\" lat=\"60.2338082\" lon=\"24.7436586\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152912\" lat=\"60.2339621\" lon=\"24.7438808\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540152913\" lat=\"60.2347655\" lon=\"24.7440071\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4540152914\" lat=\"60.2347771\" lon=\"24.7432579\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4540156189\" lat=\"60.2333385\" lon=\"24.7406337\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156191\" lat=\"60.2335080\" lon=\"24.7405511\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156192\" lat=\"60.2335068\" lon=\"24.7404179\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156193\" lat=\"60.2333438\" lon=\"24.7404827\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156194\" lat=\"60.2333616\" lon=\"24.7406645\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156195\" lat=\"60.2335246\" lon=\"24.7405998\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156205\" lat=\"60.2324838\" lon=\"24.7410280\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156206\" lat=\"60.2324268\" lon=\"24.7414324\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156207\" lat=\"60.2323969\" lon=\"24.7424161\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156208\" lat=\"60.2326156\" lon=\"24.7423739\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156209\" lat=\"60.2330171\" lon=\"24.7422230\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156210\" lat=\"60.2333946\" lon=\"24.7420842\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156211\" lat=\"60.2334383\" lon=\"24.7421765\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156212\" lat=\"60.2334840\" lon=\"24.7428652\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156213\" lat=\"60.2334027\" lon=\"24.7431959\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156214\" lat=\"60.2330862\" lon=\"24.7433800\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156215\" lat=\"60.2330981\" lon=\"24.7437481\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156216\" lat=\"60.2328154\" lon=\"24.7436800\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156217\" lat=\"60.2327291\" lon=\"24.7436356\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156218\" lat=\"60.2322281\" lon=\"24.7435811\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156219\" lat=\"60.2318858\" lon=\"24.7434180\" version=\"3\" timestamp=\"2024-11-01T10:03:23Z\" changeset=\"158611597\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4540156222\" lat=\"60.2320944\" lon=\"24.7427083\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156223\" lat=\"60.2325328\" lon=\"24.7425106\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156224\" lat=\"60.2329627\" lon=\"24.7423436\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540156225\" lat=\"60.2332623\" lon=\"24.7422277\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4540196326\" lat=\"60.2308742\" lon=\"24.7360700\" version=\"1\" timestamp=\"2016-12-05T20:30:57Z\" changeset=\"44191945\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4548426298\" lat=\"60.2355219\" lon=\"24.7286040\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"4548426299\" lat=\"60.2365185\" lon=\"24.7317035\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"4549290847\" lat=\"60.2381902\" lon=\"24.7417931\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"4549290866\" lat=\"60.2392135\" lon=\"24.7397081\" version=\"1\" timestamp=\"2016-12-11T14:20:39Z\" changeset=\"44323882\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4549290868\" lat=\"60.2389771\" lon=\"24.7399390\" version=\"1\" timestamp=\"2016-12-11T14:20:39Z\" changeset=\"44323882\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4549290869\" lat=\"60.2392068\" lon=\"24.7396517\" version=\"1\" timestamp=\"2016-12-11T14:20:39Z\" changeset=\"44323882\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4549302054\" lat=\"60.2397126\" lon=\"24.7377725\" version=\"1\" timestamp=\"2016-12-11T14:25:45Z\" changeset=\"44323962\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4549302056\" lat=\"60.2397485\" lon=\"24.7377589\" version=\"1\" timestamp=\"2016-12-11T14:25:45Z\" changeset=\"44323962\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4549302057\" lat=\"60.2396531\" lon=\"24.7371532\" version=\"1\" timestamp=\"2016-12-11T14:25:45Z\" changeset=\"44323962\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"4684825109\" lat=\"60.2259428\" lon=\"24.7275006\" version=\"1\" timestamp=\"2017-02-14T08:18:07Z\" changeset=\"46071671\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5011574085\" lat=\"60.2334067\" lon=\"24.7421857\" version=\"1\" timestamp=\"2017-08-03T11:34:13Z\" changeset=\"50805854\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5011574086\" lat=\"60.2352358\" lon=\"24.7431771\" version=\"3\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"5011574087\" lat=\"60.2354237\" lon=\"24.7436617\" version=\"1\" timestamp=\"2017-08-03T11:34:13Z\" changeset=\"50805854\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5011574088\" lat=\"60.2353824\" lon=\"24.7437912\" version=\"1\" timestamp=\"2017-08-03T11:34:13Z\" changeset=\"50805854\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5011574089\" lat=\"60.2350489\" lon=\"24.7440315\" version=\"1\" timestamp=\"2017-08-03T11:34:13Z\" changeset=\"50805854\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021869004\" lat=\"60.2280662\" lon=\"24.7200019\" version=\"2\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021869013\" lat=\"60.2284875\" lon=\"24.7186082\" version=\"2\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021869014\" lat=\"60.2284593\" lon=\"24.7187467\" version=\"2\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892192\" lat=\"60.2272569\" lon=\"24.7149054\" version=\"1\" timestamp=\"2017-08-08T09:26:18Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892196\" lat=\"60.2278482\" lon=\"24.7166422\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"5021892198\" lat=\"60.2284884\" lon=\"24.7187929\" version=\"1\" timestamp=\"2017-08-08T09:26:18Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892200\" lat=\"60.2279504\" lon=\"24.7202429\" version=\"1\" timestamp=\"2017-08-08T09:26:18Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892201\" lat=\"60.2284496\" lon=\"24.7189864\" version=\"1\" timestamp=\"2017-08-08T09:26:18Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892203\" lat=\"60.2283253\" lon=\"24.7194835\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892204\" lat=\"60.2283721\" lon=\"24.7193201\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892205\" lat=\"60.2283368\" lon=\"24.7193485\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892206\" lat=\"60.2283201\" lon=\"24.7194178\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892207\" lat=\"60.2282689\" lon=\"24.7195225\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892323\" lat=\"60.2284095\" lon=\"24.7178833\" version=\"2\" timestamp=\"2025-07-30T09:48:50Z\" changeset=\"169710901\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"5021892326\" lat=\"60.2284355\" lon=\"24.7182124\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892327\" lat=\"60.2284620\" lon=\"24.7182940\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892328\" lat=\"60.2284902\" lon=\"24.7185319\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892329\" lat=\"60.2284417\" lon=\"24.7187059\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5021892330\" lat=\"60.2284637\" lon=\"24.7186367\" version=\"1\" timestamp=\"2017-08-08T09:26:19Z\" changeset=\"50938597\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"5076856656\" lat=\"60.2344910\" lon=\"24.7417474\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856657\" lat=\"60.2348104\" lon=\"24.7416309\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856658\" lat=\"60.2349982\" lon=\"24.7415704\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856659\" lat=\"60.2350797\" lon=\"24.7415485\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856660\" lat=\"60.2351195\" lon=\"24.7415553\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856661\" lat=\"60.2351967\" lon=\"24.7416148\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856662\" lat=\"60.2352557\" lon=\"24.7417086\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856664\" lat=\"60.2352936\" lon=\"24.7417266\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856665\" lat=\"60.2352988\" lon=\"24.7418321\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856666\" lat=\"60.2353159\" lon=\"24.7419601\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856667\" lat=\"60.2353122\" lon=\"24.7422329\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"5076856668\" lat=\"60.2353241\" lon=\"24.7423989\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\">\n    <tag k=\"crossing\" v=\"uncontrolled\"/>\n    <tag k=\"highway\" v=\"crossing\"/>\n  </node>\n  <node id=\"5076856669\" lat=\"60.2352843\" lon=\"24.7428754\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"5076856670\" lat=\"60.2352922\" lon=\"24.7430140\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856671\" lat=\"60.2353098\" lon=\"24.7430955\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856672\" lat=\"60.2353793\" lon=\"24.7432851\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856673\" lat=\"60.2362694\" lon=\"24.7420424\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856674\" lat=\"60.2361710\" lon=\"24.7420780\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856675\" lat=\"60.2358299\" lon=\"24.7422193\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856676\" lat=\"60.2356861\" lon=\"24.7422763\" version=\"1\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076856677\" lat=\"60.2354420\" lon=\"24.7423286\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"5076856678\" lat=\"60.2353253\" lon=\"24.7423314\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"5076867288\" lat=\"60.2348075\" lon=\"24.7415834\" version=\"1\" timestamp=\"2017-09-03T10:57:21Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867289\" lat=\"60.2344927\" lon=\"24.7416629\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867290\" lat=\"60.2344646\" lon=\"24.7416361\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867291\" lat=\"60.2343507\" lon=\"24.7416713\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867292\" lat=\"60.2343326\" lon=\"24.7412255\" version=\"1\" timestamp=\"2017-09-03T10:57:21Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867293\" lat=\"60.2345358\" lon=\"24.7410990\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867294\" lat=\"60.2347210\" lon=\"24.7409107\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867295\" lat=\"60.2342521\" lon=\"24.7408079\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867296\" lat=\"60.2345102\" lon=\"24.7406657\" version=\"1\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867297\" lat=\"60.2345869\" lon=\"24.7407684\" version=\"1\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867298\" lat=\"60.2346202\" lon=\"24.7409682\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867299\" lat=\"60.2348419\" lon=\"24.7405204\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867300\" lat=\"60.2346829\" lon=\"24.7409436\" version=\"1\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867301\" lat=\"60.2349893\" lon=\"24.7412595\" version=\"1\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867302\" lat=\"60.2351363\" lon=\"24.7412432\" version=\"1\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867303\" lat=\"60.2353477\" lon=\"24.7411335\" version=\"1\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5076867304\" lat=\"60.2354281\" lon=\"24.7410760\" version=\"1\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5113592086\" lat=\"60.2354409\" lon=\"24.7436650\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"5113592087\" lat=\"60.2352603\" lon=\"24.7431960\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"5113592088\" lat=\"60.2352299\" lon=\"24.7431104\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"5113592089\" lat=\"60.2352178\" lon=\"24.7430053\" version=\"3\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"5113592090\" lat=\"60.2352388\" lon=\"24.7424052\" version=\"1\" timestamp=\"2017-09-18T18:53:37Z\" changeset=\"52154065\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5300314764\" lat=\"60.2348618\" lon=\"24.7435500\" version=\"2\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5300314765\" lat=\"60.2350599\" lon=\"24.7435340\" version=\"2\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5300314767\" lat=\"60.2352640\" lon=\"24.7432732\" version=\"2\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5300314769\" lat=\"60.2348174\" lon=\"24.7438830\" version=\"2\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"5574151461\" lat=\"60.2365286\" lon=\"24.7419901\" version=\"1\" timestamp=\"2018-04-23T17:28:49Z\" changeset=\"58346372\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"6139123137\" lat=\"60.2353623\" lon=\"24.7436410\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123138\" lat=\"60.2352955\" lon=\"24.7436987\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123139\" lat=\"60.2353154\" lon=\"24.7437924\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123140\" lat=\"60.2353823\" lon=\"24.7437348\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123141\" lat=\"60.2354017\" lon=\"24.7434546\" version=\"1\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"6139123142\" lat=\"60.2353761\" lon=\"24.7435010\" version=\"2\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"6139123143\" lat=\"60.2351517\" lon=\"24.7438482\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123144\" lat=\"60.2351840\" lon=\"24.7425745\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123145\" lat=\"60.2351031\" lon=\"24.7427130\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123146\" lat=\"60.2351147\" lon=\"24.7429167\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123149\" lat=\"60.2350641\" lon=\"24.7429813\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123150\" lat=\"60.2351551\" lon=\"24.7432658\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123152\" lat=\"60.2352009\" lon=\"24.7429400\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123153\" lat=\"60.2352168\" lon=\"24.7426721\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"6139123154\" lat=\"60.2351763\" lon=\"24.7430321\" version=\"1\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"6139123155\" lat=\"60.2351171\" lon=\"24.7431153\" version=\"1\" timestamp=\"2018-12-15T15:31:10Z\" changeset=\"65502560\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"6139126749\" lat=\"60.2357902\" lon=\"24.7414439\" version=\"1\" timestamp=\"2018-12-15T15:36:47Z\" changeset=\"65502665\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"6139126753\" lat=\"60.2362074\" lon=\"24.7418673\" version=\"1\" timestamp=\"2018-12-15T15:36:47Z\" changeset=\"65502665\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"6139126877\" lat=\"60.2355561\" lon=\"24.7423118\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"6139126878\" lat=\"60.2355736\" lon=\"24.7420494\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"6139126879\" lat=\"60.2356638\" lon=\"24.7422848\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"6139126880\" lat=\"60.2356626\" lon=\"24.7420488\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"6139126881\" lat=\"60.2357583\" lon=\"24.7422477\" version=\"1\" timestamp=\"2018-12-15T15:35:35Z\" changeset=\"65502642\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"6139126882\" lat=\"60.2357558\" lon=\"24.7420482\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448755159\" lat=\"60.2347609\" lon=\"24.7434076\" version=\"1\" timestamp=\"2020-04-26T13:21:40Z\" changeset=\"84142141\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817945\" lat=\"60.2349858\" lon=\"24.7426521\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817946\" lat=\"60.2349570\" lon=\"24.7427027\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817947\" lat=\"60.2349353\" lon=\"24.7427364\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817948\" lat=\"60.2346630\" lon=\"24.7428905\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817949\" lat=\"60.2348965\" lon=\"24.7426920\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"B7\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817950\" lat=\"60.2348411\" lon=\"24.7427215\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"B8\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817951\" lat=\"60.2347883\" lon=\"24.7427496\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"B9\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817952\" lat=\"60.2347316\" lon=\"24.7427797\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"B10\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817953\" lat=\"60.2346793\" lon=\"24.7428076\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"B11\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817954\" lat=\"60.2351837\" lon=\"24.7421079\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"A1\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817955\" lat=\"60.2351451\" lon=\"24.7421740\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"A2\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817956\" lat=\"60.2351009\" lon=\"24.7422497\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"A3\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817957\" lat=\"60.2350771\" lon=\"24.7423848\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"A4\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817958\" lat=\"60.2350374\" lon=\"24.7424529\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"A5\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817959\" lat=\"60.2349941\" lon=\"24.7425270\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"A6\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817960\" lat=\"60.2351920\" lon=\"24.7421731\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817961\" lat=\"60.2351524\" lon=\"24.7422271\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817962\" lat=\"60.2351555\" lon=\"24.7423882\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817963\" lat=\"60.2351085\" lon=\"24.7423131\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817964\" lat=\"60.2351279\" lon=\"24.7424438\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817965\" lat=\"60.2351445\" lon=\"24.7424883\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817966\" lat=\"60.2351257\" lon=\"24.7425224\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817967\" lat=\"60.2350576\" lon=\"24.7425072\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817968\" lat=\"60.2350786\" lon=\"24.7425228\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817969\" lat=\"60.2351041\" lon=\"24.7425618\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817970\" lat=\"60.2350114\" lon=\"24.7425666\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817971\" lat=\"60.2350460\" lon=\"24.7425877\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817972\" lat=\"60.2350740\" lon=\"24.7426166\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448817973\" lat=\"60.2346797\" lon=\"24.7421519\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"C12\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817974\" lat=\"60.2346303\" lon=\"24.7422394\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"C13\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817975\" lat=\"60.2345822\" lon=\"24.7423266\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"C14\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448817976\" lat=\"60.2345283\" lon=\"24.7424132\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"C15\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448840411\" lat=\"60.2360486\" lon=\"24.7413908\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840412\" lat=\"60.2361309\" lon=\"24.7414012\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840413\" lat=\"60.2361948\" lon=\"24.7414094\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840420\" lat=\"60.2357400\" lon=\"24.7413376\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448840421\" lat=\"60.2358222\" lon=\"24.7413330\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448840422\" lat=\"60.2358238\" lon=\"24.7414267\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840423\" lat=\"60.2357405\" lon=\"24.7414776\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840424\" lat=\"60.2356687\" lon=\"24.7414458\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448840425\" lat=\"60.2356688\" lon=\"24.7415603\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840426\" lat=\"60.2355818\" lon=\"24.7414507\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448840427\" lat=\"60.2355810\" lon=\"24.7416177\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840428\" lat=\"60.2355153\" lon=\"24.7415714\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448840429\" lat=\"60.2354278\" lon=\"24.7415763\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7448840430\" lat=\"60.2354283\" lon=\"24.7416798\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840431\" lat=\"60.2354418\" lon=\"24.7417161\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840432\" lat=\"60.2355734\" lon=\"24.7420999\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7448840433\" lat=\"60.2355561\" lon=\"24.7421011\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7529995939\" lat=\"60.2349935\" lon=\"24.7406655\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"1\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995940\" lat=\"60.2350283\" lon=\"24.7405501\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"2\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995941\" lat=\"60.2350608\" lon=\"24.7404421\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"3\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995942\" lat=\"60.2350959\" lon=\"24.7403258\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"4\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995943\" lat=\"60.2351989\" lon=\"24.7401654\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"5\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995944\" lat=\"60.2352536\" lon=\"24.7402337\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"6\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995945\" lat=\"60.2353074\" lon=\"24.7403009\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"7\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995946\" lat=\"60.2353640\" lon=\"24.7403715\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"8\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995947\" lat=\"60.2354199\" lon=\"24.7404414\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"9\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995948\" lat=\"60.2355344\" lon=\"24.7405108\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"10\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995949\" lat=\"60.2356954\" lon=\"24.7408108\" version=\"1\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7529995950\" lat=\"60.2355908\" lon=\"24.7405845\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"11\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995951\" lat=\"60.2356450\" lon=\"24.7406553\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"12\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995952\" lat=\"60.2357010\" lon=\"24.7407285\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"13\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995953\" lat=\"60.2351622\" lon=\"24.7411268\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"14\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995954\" lat=\"60.2352143\" lon=\"24.7411206\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"15\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995955\" lat=\"60.2352692\" lon=\"24.7411140\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"16\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995956\" lat=\"60.2353221\" lon=\"24.7410236\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"17\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995957\" lat=\"60.2353783\" lon=\"24.7410169\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"18\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7529995958\" lat=\"60.2354346\" lon=\"24.7410102\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"19\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028797\" lat=\"60.2344820\" lon=\"24.7406584\" version=\"1\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7530028798\" lat=\"60.2342967\" lon=\"24.7412364\" version=\"1\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7530028799\" lat=\"60.2347523\" lon=\"24.7415635\" version=\"1\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"7530028800\" lat=\"60.2347021\" lon=\"24.7407874\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"1\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028801\" lat=\"60.2347376\" lon=\"24.7406800\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"2\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028802\" lat=\"60.2347700\" lon=\"24.7405815\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"3\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028803\" lat=\"60.2348042\" lon=\"24.7404783\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"4\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028804\" lat=\"60.2344394\" lon=\"24.7405354\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"5\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028805\" lat=\"60.2343806\" lon=\"24.7405742\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"6\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028806\" lat=\"60.2343226\" lon=\"24.7406124\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"7\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028807\" lat=\"60.2342663\" lon=\"24.7406496\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"8\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028808\" lat=\"60.2344816\" lon=\"24.7410471\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"9\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028809\" lat=\"60.2344241\" lon=\"24.7410691\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"10\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028810\" lat=\"60.2343618\" lon=\"24.7410930\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"11\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028811\" lat=\"60.2343026\" lon=\"24.7411156\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"12\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028812\" lat=\"60.2346704\" lon=\"24.7415347\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"13\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028813\" lat=\"60.2346158\" lon=\"24.7415549\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"14\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028814\" lat=\"60.2345603\" lon=\"24.7415755\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"15\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028815\" lat=\"60.2345059\" lon=\"24.7415957\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"16\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028816\" lat=\"60.2344432\" lon=\"24.7415492\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"17\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028817\" lat=\"60.2343877\" lon=\"24.7415698\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"18\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7530028818\" lat=\"60.2343330\" lon=\"24.7415900\" version=\"2\" timestamp=\"2020-08-12T10:11:00Z\" changeset=\"89296693\" uid=\"1484179\" user=\"oiva\">\n    <tag k=\"addr:door\" v=\"19\"/>\n    <tag k=\"entrance\" v=\"home\"/>\n  </node>\n  <node id=\"7556210592\" lat=\"60.2348788\" lon=\"24.7395856\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210595\" lat=\"60.2342766\" lon=\"24.7394832\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210597\" lat=\"60.2362904\" lon=\"24.7373218\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210598\" lat=\"60.2365284\" lon=\"24.7382502\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210599\" lat=\"60.2359421\" lon=\"24.7365829\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210600\" lat=\"60.2366046\" lon=\"24.7386048\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210601\" lat=\"60.2366406\" lon=\"24.7388713\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210603\" lat=\"60.2364624\" lon=\"24.7395992\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210604\" lat=\"60.2363671\" lon=\"24.7396965\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210605\" lat=\"60.2358130\" lon=\"24.7399009\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210606\" lat=\"60.2359200\" lon=\"24.7400007\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210607\" lat=\"60.2359524\" lon=\"24.7398456\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210628\" lat=\"60.2368495\" lon=\"24.7409054\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210630\" lat=\"60.2363479\" lon=\"24.7404471\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210631\" lat=\"60.2363479\" lon=\"24.7404471\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210633\" lat=\"60.2362805\" lon=\"24.7403678\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210634\" lat=\"60.2362805\" lon=\"24.7403678\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210636\" lat=\"60.2360634\" lon=\"24.7401983\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210637\" lat=\"60.2360634\" lon=\"24.7401983\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210639\" lat=\"60.2359801\" lon=\"24.7400887\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210647\" lat=\"60.2358582\" lon=\"24.7362174\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210649\" lat=\"60.2356980\" lon=\"24.7358415\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210650\" lat=\"60.2356966\" lon=\"24.7356631\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210651\" lat=\"60.2357544\" lon=\"24.7360362\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210655\" lat=\"60.2361393\" lon=\"24.7398420\" version=\"2\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210664\" lat=\"60.2368495\" lon=\"24.7409054\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210665\" lat=\"60.2358130\" lon=\"24.7399009\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210666\" lat=\"60.2355202\" lon=\"24.7396280\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210667\" lat=\"60.2353315\" lon=\"24.7394588\" version=\"1\" timestamp=\"2020-05-25T12:37:43Z\" changeset=\"85717622\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"7556210682\" lat=\"60.2363580\" lon=\"24.7317433\" version=\"2\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"7577475547\" lat=\"60.2391909\" lon=\"24.7397301\" version=\"1\" timestamp=\"2020-06-01T10:40:08Z\" changeset=\"86042095\" uid=\"1484179\" user=\"oiva\"/>\n  <node id=\"8207527227\" lat=\"60.2341897\" lon=\"24.7281246\" version=\"2\" timestamp=\"2024-08-13T06:11:46Z\" changeset=\"155178405\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"8503461537\" lat=\"60.2325058\" lon=\"24.7424327\" version=\"1\" timestamp=\"2021-03-10T11:39:55Z\" changeset=\"100776121\" uid=\"8105430\" user=\"Larmax\"/>\n  <node id=\"8568942717\" lat=\"60.2334734\" lon=\"24.7435440\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942718\" lat=\"60.2335195\" lon=\"24.7430752\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942719\" lat=\"60.2335388\" lon=\"24.7432283\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942720\" lat=\"60.2335409\" lon=\"24.7433060\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942721\" lat=\"60.2335606\" lon=\"24.7429423\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942722\" lat=\"60.2335320\" lon=\"24.7430087\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942723\" lat=\"60.2334910\" lon=\"24.7423866\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942724\" lat=\"60.2334451\" lon=\"24.7426223\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942725\" lat=\"60.2334464\" lon=\"24.7427043\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942726\" lat=\"60.2334642\" lon=\"24.7427638\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942727\" lat=\"60.2335209\" lon=\"24.7428040\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942728\" lat=\"60.2335474\" lon=\"24.7428380\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942729\" lat=\"60.2334598\" lon=\"24.7422180\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942730\" lat=\"60.2337072\" lon=\"24.7409136\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942731\" lat=\"60.2337673\" lon=\"24.7412059\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942732\" lat=\"60.2337670\" lon=\"24.7412907\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942733\" lat=\"60.2337541\" lon=\"24.7413737\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942734\" lat=\"60.2337327\" lon=\"24.7414615\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942735\" lat=\"60.2336795\" lon=\"24.7415748\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942736\" lat=\"60.2335650\" lon=\"24.7417183\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942737\" lat=\"60.2335054\" lon=\"24.7418283\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942738\" lat=\"60.2334612\" lon=\"24.7419724\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942739\" lat=\"60.2339728\" lon=\"24.7402171\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942740\" lat=\"60.2339431\" lon=\"24.7403401\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942741\" lat=\"60.2339162\" lon=\"24.7404132\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942742\" lat=\"60.2337843\" lon=\"24.7406269\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942743\" lat=\"60.2337321\" lon=\"24.7407280\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942744\" lat=\"60.2339829\" lon=\"24.7400918\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942745\" lat=\"60.2339829\" lon=\"24.7399428\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8568942746\" lat=\"60.2339484\" lon=\"24.7396574\" version=\"1\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"8915699144\" lat=\"60.2320372\" lon=\"24.7434899\" version=\"1\" timestamp=\"2021-07-13T07:14:46Z\" changeset=\"107884960\" uid=\"10590172\" user=\"marsusram\"/>\n  <node id=\"9312377379\" lat=\"60.2260025\" lon=\"24.7303094\" version=\"1\" timestamp=\"2021-12-05T13:06:04Z\" changeset=\"114581446\" uid=\"10590172\" user=\"marsusram\"/>\n  <node id=\"9312377388\" lat=\"60.2263129\" lon=\"24.7303484\" version=\"3\" timestamp=\"2025-07-09T05:25:34Z\" changeset=\"168677046\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"11409449788\" lat=\"60.2365319\" lon=\"24.7318435\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449789\" lat=\"60.2362694\" lon=\"24.7315379\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449790\" lat=\"60.2363283\" lon=\"24.7326907\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449791\" lat=\"60.2362896\" lon=\"24.7333491\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449792\" lat=\"60.2362841\" lon=\"24.7334425\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449793\" lat=\"60.2362090\" lon=\"24.7335738\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449794\" lat=\"60.2361867\" lon=\"24.7337679\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449795\" lat=\"60.2361157\" lon=\"24.7340915\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449796\" lat=\"60.2358003\" lon=\"24.7352073\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449797\" lat=\"60.2360042\" lon=\"24.7368199\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449798\" lat=\"60.2361363\" lon=\"24.7370195\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449799\" lat=\"60.2363750\" lon=\"24.7376170\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449800\" lat=\"60.2364808\" lon=\"24.7380817\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449801\" lat=\"60.2366048\" lon=\"24.7391064\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449802\" lat=\"60.2365338\" lon=\"24.7393506\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449803\" lat=\"60.2363082\" lon=\"24.7397034\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449804\" lat=\"60.2360536\" lon=\"24.7398778\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449805\" lat=\"60.2354879\" lon=\"24.7286908\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449806\" lat=\"60.2354891\" lon=\"24.7287602\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449807\" lat=\"60.2355078\" lon=\"24.7287953\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449808\" lat=\"60.2355602\" lon=\"24.7289179\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449810\" lat=\"60.2355890\" lon=\"24.7294360\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449811\" lat=\"60.2355736\" lon=\"24.7297034\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449813\" lat=\"60.2357121\" lon=\"24.7303239\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449814\" lat=\"60.2354060\" lon=\"24.7284534\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449815\" lat=\"60.2354643\" lon=\"24.7285126\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449818\" lat=\"60.2354972\" lon=\"24.7286506\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449819\" lat=\"60.2352323\" lon=\"24.7282192\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449820\" lat=\"60.2350425\" lon=\"24.7281290\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449825\" lat=\"60.2339032\" lon=\"24.7277365\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449826\" lat=\"60.2340031\" lon=\"24.7279015\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449833\" lat=\"60.2321899\" lon=\"24.7264396\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449834\" lat=\"60.2319593\" lon=\"24.7264520\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449837\" lat=\"60.2316918\" lon=\"24.7267179\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449844\" lat=\"60.2312842\" lon=\"24.7270826\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449845\" lat=\"60.2315620\" lon=\"24.7268562\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449847\" lat=\"60.2285986\" lon=\"24.7255991\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449848\" lat=\"60.2286517\" lon=\"24.7255982\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449849\" lat=\"60.2284362\" lon=\"24.7254516\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449851\" lat=\"60.2281038\" lon=\"24.7254265\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449852\" lat=\"60.2279725\" lon=\"24.7252501\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449853\" lat=\"60.2278488\" lon=\"24.7253325\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449854\" lat=\"60.2277564\" lon=\"24.7253280\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11409449855\" lat=\"60.2276981\" lon=\"24.7253142\" version=\"1\" timestamp=\"2023-12-11T11:04:36Z\" changeset=\"144995017\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"11572255107\" lat=\"60.2275341\" lon=\"24.7289946\" version=\"1\" timestamp=\"2024-01-31T09:19:43Z\" changeset=\"146895228\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"11572255108\" lat=\"60.2274970\" lon=\"24.7291165\" version=\"1\" timestamp=\"2024-01-31T09:19:43Z\" changeset=\"146895228\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"11901877265\" lat=\"60.2369924\" lon=\"24.7412998\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901877266\" lat=\"60.2367833\" lon=\"24.7411621\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901877267\" lat=\"60.2360204\" lon=\"24.7404884\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901877268\" lat=\"60.2357321\" lon=\"24.7403045\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878569\" lat=\"60.2354328\" lon=\"24.7399203\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878570\" lat=\"60.2353308\" lon=\"24.7397571\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878571\" lat=\"60.2352605\" lon=\"24.7397058\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878572\" lat=\"60.2351547\" lon=\"24.7397668\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878573\" lat=\"60.2348769\" lon=\"24.7400555\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878574\" lat=\"60.2348412\" lon=\"24.7402096\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878575\" lat=\"60.2348609\" lon=\"24.7403692\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878576\" lat=\"60.2348652\" lon=\"24.7404996\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878577\" lat=\"60.2348539\" lon=\"24.7408269\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"11901878578\" lat=\"60.2348941\" lon=\"24.7416039\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\"/>\n  <node id=\"12179685472\" lat=\"60.2270816\" lon=\"24.7289007\" version=\"1\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12179685482\" lat=\"60.2259413\" lon=\"24.7274824\" version=\"1\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12179728216\" lat=\"60.2259619\" lon=\"24.7276726\" version=\"1\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12179728223\" lat=\"60.2259657\" lon=\"24.7276993\" version=\"1\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12179728241\" lat=\"60.2268910\" lon=\"24.7286051\" version=\"1\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12179728243\" lat=\"60.2259230\" lon=\"24.7270005\" version=\"1\" timestamp=\"2024-09-16T07:32:22Z\" changeset=\"156674915\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12285606718\" lat=\"60.2379097\" lon=\"24.7420817\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606719\" lat=\"60.2375956\" lon=\"24.7423853\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606720\" lat=\"60.2377722\" lon=\"24.7422638\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606721\" lat=\"60.2380249\" lon=\"24.7419428\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606744\" lat=\"60.2340886\" lon=\"24.7419637\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606745\" lat=\"60.2353230\" lon=\"24.7424761\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606746\" lat=\"60.2352940\" lon=\"24.7426111\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606747\" lat=\"60.2352988\" lon=\"24.7425216\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606748\" lat=\"60.2352623\" lon=\"24.7426108\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\">\n    <tag k=\"crossing\" v=\"uncontrolled\"/>\n    <tag k=\"crossing:markings\" v=\"yes\"/>\n    <tag k=\"highway\" v=\"crossing\"/>\n  </node>\n  <node id=\"12285606749\" lat=\"60.2352293\" lon=\"24.7426103\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606750\" lat=\"60.2353767\" lon=\"24.7423441\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606751\" lat=\"60.2366845\" lon=\"24.7418748\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606752\" lat=\"60.2367236\" lon=\"24.7418419\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606753\" lat=\"60.2367365\" lon=\"24.7418105\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606754\" lat=\"60.2367554\" lon=\"24.7417540\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606755\" lat=\"60.2367725\" lon=\"24.7417108\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12285606756\" lat=\"60.2368138\" lon=\"24.7416960\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\"/>\n  <node id=\"12541752053\" lat=\"60.2271962\" lon=\"24.7148994\" version=\"1\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12541752060\" lat=\"60.2284877\" lon=\"24.7192367\" version=\"1\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12541752073\" lat=\"60.2362656\" lon=\"24.7315015\" version=\"1\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12541752074\" lat=\"60.2362571\" lon=\"24.7314217\" version=\"1\" timestamp=\"2025-01-29T12:36:51Z\" changeset=\"161895696\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12582011413\" lat=\"60.2363444\" lon=\"24.7330197\" version=\"1\" timestamp=\"2025-02-12T12:17:15Z\" changeset=\"162427801\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582011415\" lat=\"60.2386760\" lon=\"24.7374736\" version=\"1\" timestamp=\"2025-02-12T12:17:15Z\" changeset=\"162427801\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582011416\" lat=\"60.2386659\" lon=\"24.7379201\" version=\"1\" timestamp=\"2025-02-12T12:17:15Z\" changeset=\"162427801\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582011417\" lat=\"60.2379161\" lon=\"24.7374428\" version=\"1\" timestamp=\"2025-02-12T12:17:15Z\" changeset=\"162427801\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582078946\" lat=\"60.2363600\" lon=\"24.7329108\" version=\"1\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582078960\" lat=\"60.2358563\" lon=\"24.7335699\" version=\"1\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582078961\" lat=\"60.2358233\" lon=\"24.7336467\" version=\"1\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582078962\" lat=\"60.2358116\" lon=\"24.7338932\" version=\"1\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582078963\" lat=\"60.2357710\" lon=\"24.7339740\" version=\"1\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582078964\" lat=\"60.2356570\" lon=\"24.7342644\" version=\"1\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12582078965\" lat=\"60.2356283\" lon=\"24.7343867\" version=\"1\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\"/>\n  <node id=\"12741231905\" lat=\"60.2276009\" lon=\"24.7206709\" version=\"1\" timestamp=\"2025-04-09T11:16:39Z\" changeset=\"164719335\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741231906\" lat=\"60.2278769\" lon=\"24.7205749\" version=\"1\" timestamp=\"2025-04-09T11:16:39Z\" changeset=\"164719335\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249969\" lat=\"60.2349253\" lon=\"24.7433858\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249970\" lat=\"60.2349004\" lon=\"24.7433868\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249971\" lat=\"60.2348976\" lon=\"24.7431206\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249972\" lat=\"60.2349225\" lon=\"24.7431195\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249973\" lat=\"60.2350676\" lon=\"24.7430000\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249974\" lat=\"60.2350454\" lon=\"24.7430290\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249975\" lat=\"60.2351298\" lon=\"24.7432925\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249976\" lat=\"60.2351521\" lon=\"24.7432636\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249977\" lat=\"60.2352430\" lon=\"24.7437641\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249978\" lat=\"60.2351693\" lon=\"24.7438297\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249979\" lat=\"60.2351872\" lon=\"24.7439112\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249980\" lat=\"60.2352609\" lon=\"24.7438456\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249985\" lat=\"60.2352112\" lon=\"24.7430586\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12741249986\" lat=\"60.2351774\" lon=\"24.7439885\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12988984752\" lat=\"60.2257357\" lon=\"24.7305291\" version=\"1\" timestamp=\"2025-07-09T05:25:34Z\" changeset=\"168677046\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12988984753\" lat=\"60.2259602\" lon=\"24.7303611\" version=\"1\" timestamp=\"2025-07-09T05:25:34Z\" changeset=\"168677046\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12988984763\" lat=\"60.2271969\" lon=\"24.7292491\" version=\"1\" timestamp=\"2025-07-09T05:25:34Z\" changeset=\"168677046\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12989039536\" lat=\"60.2259926\" lon=\"24.7302514\" version=\"1\" timestamp=\"2025-07-09T05:25:34Z\" changeset=\"168677046\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12989039537\" lat=\"60.2260087\" lon=\"24.7303256\" version=\"1\" timestamp=\"2025-07-09T05:25:34Z\" changeset=\"168677046\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"12989039538\" lat=\"60.2260100\" lon=\"24.7302978\" version=\"1\" timestamp=\"2025-07-09T05:25:34Z\" changeset=\"168677046\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036927189\" lat=\"60.2268504\" lon=\"24.7136802\" version=\"1\" timestamp=\"2025-07-30T09:17:10Z\" changeset=\"169709512\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945672\" lat=\"60.2260240\" lon=\"24.7280093\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945673\" lat=\"60.2260834\" lon=\"24.7279835\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945674\" lat=\"60.2261178\" lon=\"24.7279783\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945675\" lat=\"60.2262554\" lon=\"24.7280813\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945676\" lat=\"60.2262941\" lon=\"24.7281524\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945677\" lat=\"60.2263050\" lon=\"24.7282264\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945681\" lat=\"60.2266452\" lon=\"24.7284978\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945682\" lat=\"60.2265241\" lon=\"24.7285167\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945683\" lat=\"60.2264404\" lon=\"24.7284814\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945684\" lat=\"60.2264823\" lon=\"24.7285154\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036945685\" lat=\"60.2263713\" lon=\"24.7283906\" version=\"1\" timestamp=\"2025-07-30T09:11:42Z\" changeset=\"169709299\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13036990665\" lat=\"60.2310038\" lon=\"24.7272804\" version=\"1\" timestamp=\"2025-07-30T09:48:50Z\" changeset=\"169710901\" uid=\"20916114\" user=\"liljaadeliina\"/>\n  <node id=\"13076214961\" lat=\"60.2288387\" lon=\"24.7257658\" version=\"1\" timestamp=\"2025-08-17T16:54:09Z\" changeset=\"170577090\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"13076214964\" lat=\"60.2289384\" lon=\"24.7260655\" version=\"1\" timestamp=\"2025-08-17T16:54:09Z\" changeset=\"170577090\" uid=\"742733\" user=\"perusluku\"/>\n  <node id=\"13104452417\" lat=\"60.2352133\" lon=\"24.7429170\" version=\"1\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\"/>\n  <way id=\"24724510\" version=\"19\" timestamp=\"2025-07-18T16:55:34Z\" changeset=\"169117100\" uid=\"486606\" user=\"Barro\">\n    <nd ref=\"268724899\"/>\n    <nd ref=\"1538138940\"/>\n    <nd ref=\"268725040\"/>\n    <nd ref=\"268724954\"/>\n    <nd ref=\"12741249986\"/>\n    <nd ref=\"268725041\"/>\n    <nd ref=\"1538138929\"/>\n    <nd ref=\"1538139031\"/>\n    <nd ref=\"268728008\"/>\n    <nd ref=\"1538138535\"/>\n    <nd ref=\"1538139090\"/>\n    <nd ref=\"1538138724\"/>\n    <nd ref=\"268725042\"/>\n    <nd ref=\"4254168968\"/>\n    <nd ref=\"268725043\"/>\n    <nd ref=\"1539656268\"/>\n    <nd ref=\"276457279\"/>\n    <nd ref=\"268725045\"/>\n    <nd ref=\"4255037761\"/>\n    <nd ref=\"268725046\"/>\n    <nd ref=\"268725047\"/>\n    <nd ref=\"4195526816\"/>\n    <nd ref=\"4195526817\"/>\n    <tag k=\"bicycle\" v=\"designated\"/>\n    <tag k=\"foot\" v=\"designated\"/>\n    <tag k=\"highway\" v=\"cycleway\"/>\n    <tag k=\"lit\" v=\"yes\"/>\n    <tag k=\"segregated\" v=\"no\"/>\n    <tag k=\"source\" v=\"survey\"/>\n    <tag k=\"surface\" v=\"asphalt\"/>\n  </way>\n  <way id=\"24724848\" version=\"10\" timestamp=\"2025-07-18T16:55:40Z\" changeset=\"169117097\" uid=\"486606\" user=\"Barro\">\n    <nd ref=\"1538138535\"/>\n    <nd ref=\"5076856672\"/>\n    <nd ref=\"5076856671\"/>\n    <nd ref=\"5076856670\"/>\n    <nd ref=\"5076856669\"/>\n    <nd ref=\"12285606746\"/>\n    <nd ref=\"12285606747\"/>\n    <nd ref=\"12285606745\"/>\n    <nd ref=\"5076856668\"/>\n    <nd ref=\"5076856678\"/>\n    <nd ref=\"5076856667\"/>\n    <nd ref=\"5076856666\"/>\n    <nd ref=\"5076856665\"/>\n    <nd ref=\"5076856662\"/>\n    <nd ref=\"5076856661\"/>\n    <nd ref=\"5076856660\"/>\n    <nd ref=\"5076856659\"/>\n    <nd ref=\"5076856658\"/>\n    <nd ref=\"11901878578\"/>\n    <nd ref=\"5076856657\"/>\n    <nd ref=\"5076856656\"/>\n    <nd ref=\"1539655816\"/>\n    <tag k=\"footway\" v=\"sidewalk\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n    <tag k=\"source\" v=\"survey\"/>\n    <tag k=\"surface\" v=\"asphalt\"/>\n  </way>\n  <way id=\"25365011\" version=\"6\" timestamp=\"2018-04-23T17:28:50Z\" changeset=\"58346372\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"268728011\"/>\n    <nd ref=\"5076856668\"/>\n    <nd ref=\"4540138274\"/>\n    <nd ref=\"1539656229\"/>\n    <nd ref=\"4540139290\"/>\n    <nd ref=\"1539656240\"/>\n    <nd ref=\"4540139296\"/>\n    <nd ref=\"4540138287\"/>\n    <nd ref=\"4540138286\"/>\n    <nd ref=\"4540138266\"/>\n    <nd ref=\"276457277\"/>\n    <nd ref=\"5574151461\"/>\n    <nd ref=\"1539656319\"/>\n    <nd ref=\"4540138252\"/>\n    <nd ref=\"276457278\"/>\n    <tag k=\"highway\" v=\"residential\"/>\n    <tag k=\"maxspeed\" v=\"30\"/>\n    <tag k=\"name\" v=\"Nuuniitynkuja\"/>\n    <tag k=\"name:fi\" v=\"Nuuniitynkuja\"/>\n    <tag k=\"name:sv\" v=\"Noängsgränden\"/>\n  </way>\n  <way id=\"25365012\" version=\"4\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\">\n    <nd ref=\"5076856678\"/>\n    <nd ref=\"12285606750\"/>\n    <nd ref=\"5076856677\"/>\n    <nd ref=\"6139126877\"/>\n    <nd ref=\"6139126879\"/>\n    <nd ref=\"5076856676\"/>\n    <nd ref=\"6139126881\"/>\n    <nd ref=\"5076856675\"/>\n    <nd ref=\"5076856674\"/>\n    <nd ref=\"5076856673\"/>\n    <nd ref=\"12285606751\"/>\n    <nd ref=\"12285606752\"/>\n    <nd ref=\"12285606753\"/>\n    <nd ref=\"12285606754\"/>\n    <nd ref=\"12285606755\"/>\n    <nd ref=\"12285606756\"/>\n    <tag k=\"footway\" v=\"sidewalk\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n    <tag k=\"surface\" v=\"paved\"/>\n  </way>\n  <way id=\"25365013\" version=\"8\" timestamp=\"2025-07-18T16:55:38Z\" changeset=\"169117100\" uid=\"486606\" user=\"Barro\">\n    <nd ref=\"25885728\"/>\n    <nd ref=\"1538138691\"/>\n    <nd ref=\"268728008\"/>\n    <nd ref=\"1538138712\"/>\n    <tag k=\"highway\" v=\"residential\"/>\n    <tag k=\"lit\" v=\"yes\"/>\n    <tag k=\"maxspeed\" v=\"30\"/>\n    <tag k=\"name\" v=\"Nuumäentie\"/>\n    <tag k=\"name:fi\" v=\"Nuumäentie\"/>\n    <tag k=\"name:sv\" v=\"Nobacksvägen\"/>\n    <tag k=\"surface\" v=\"asphalt\"/>\n    <tag k=\"traffic_calming\" v=\"table\"/>\n  </way>\n  <way id=\"33645972\" version=\"7\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\">\n    <nd ref=\"25505426\"/>\n    <nd ref=\"1828984411\"/>\n    <nd ref=\"1828984413\"/>\n    <nd ref=\"1828984412\"/>\n    <nd ref=\"1828984415\"/>\n    <nd ref=\"300505170\"/>\n    <nd ref=\"11409449791\"/>\n    <nd ref=\"11409449792\"/>\n    <nd ref=\"300505171\"/>\n    <nd ref=\"11409449793\"/>\n    <nd ref=\"11409449794\"/>\n    <nd ref=\"300505172\"/>\n    <nd ref=\"11409449795\"/>\n    <nd ref=\"300505173\"/>\n    <nd ref=\"384899935\"/>\n    <nd ref=\"1828984409\"/>\n    <nd ref=\"11409449796\"/>\n    <nd ref=\"384899936\"/>\n    <nd ref=\"1828984403\"/>\n    <nd ref=\"7556210650\"/>\n    <nd ref=\"7556210649\"/>\n    <nd ref=\"7556210651\"/>\n    <nd ref=\"7556210647\"/>\n    <nd ref=\"7556210599\"/>\n    <nd ref=\"11409449797\"/>\n    <nd ref=\"11409449798\"/>\n    <nd ref=\"7556210597\"/>\n    <nd ref=\"11409449799\"/>\n    <nd ref=\"11409449800\"/>\n    <nd ref=\"7556210598\"/>\n    <nd ref=\"7556210600\"/>\n    <nd ref=\"7556210601\"/>\n    <nd ref=\"11409449801\"/>\n    <nd ref=\"11409449802\"/>\n    <nd ref=\"7556210603\"/>\n    <nd ref=\"7556210604\"/>\n    <nd ref=\"11409449803\"/>\n    <nd ref=\"7556210655\"/>\n    <nd ref=\"11409449804\"/>\n    <nd ref=\"7556210607\"/>\n    <nd ref=\"7556210665\"/>\n    <nd ref=\"7556210666\"/>\n    <nd ref=\"300505148\"/>\n    <nd ref=\"300505147\"/>\n    <nd ref=\"300505146\"/>\n    <nd ref=\"300505178\"/>\n    <nd ref=\"300505177\"/>\n    <nd ref=\"300505176\"/>\n    <nd ref=\"300505175\"/>\n    <nd ref=\"1828984401\"/>\n    <nd ref=\"1828984396\"/>\n    <nd ref=\"1828984386\"/>\n    <nd ref=\"1828984381\"/>\n    <nd ref=\"1828984378\"/>\n    <nd ref=\"1828984383\"/>\n    <nd ref=\"25505432\"/>\n    <nd ref=\"300505174\"/>\n    <nd ref=\"1828984397\"/>\n    <nd ref=\"25505431\"/>\n    <nd ref=\"12582078965\"/>\n    <nd ref=\"12582078964\"/>\n    <nd ref=\"1828984405\"/>\n    <nd ref=\"12582078963\"/>\n    <nd ref=\"12582078962\"/>\n    <nd ref=\"12582078961\"/>\n    <nd ref=\"12582078960\"/>\n    <nd ref=\"25505426\"/>\n    <tag k=\"natural\" v=\"wetland\"/>\n    <tag k=\"wetland\" v=\"reedbed\"/>\n  </way>\n  <way id=\"44507498\" version=\"4\" timestamp=\"2021-03-28T12:07:18Z\" changeset=\"101870513\" uid=\"5159390\" user=\"tvenhola\">\n    <nd ref=\"607993952\"/>\n    <nd ref=\"8568942717\"/>\n    <nd ref=\"607993949\"/>\n    <nd ref=\"8568942720\"/>\n    <nd ref=\"8568942719\"/>\n    <nd ref=\"8568942718\"/>\n    <nd ref=\"8568942722\"/>\n    <nd ref=\"8568942721\"/>\n    <nd ref=\"607993950\"/>\n    <nd ref=\"8568942728\"/>\n    <nd ref=\"8568942727\"/>\n    <nd ref=\"8568942726\"/>\n    <nd ref=\"8568942725\"/>\n    <nd ref=\"8568942724\"/>\n    <nd ref=\"8568942723\"/>\n    <nd ref=\"8568942729\"/>\n    <nd ref=\"607993951\"/>\n    <nd ref=\"8568942738\"/>\n    <nd ref=\"8568942737\"/>\n    <nd ref=\"8568942736\"/>\n    <nd ref=\"8568942735\"/>\n    <nd ref=\"8568942734\"/>\n    <nd ref=\"8568942733\"/>\n    <nd ref=\"8568942732\"/>\n    <nd ref=\"8568942731\"/>\n    <nd ref=\"8568942730\"/>\n    <nd ref=\"607993948\"/>\n    <nd ref=\"8568942743\"/>\n    <nd ref=\"8568942742\"/>\n    <nd ref=\"8568942741\"/>\n    <nd ref=\"8568942740\"/>\n    <nd ref=\"8568942739\"/>\n    <nd ref=\"8568942744\"/>\n    <nd ref=\"8568942745\"/>\n    <nd ref=\"1539655694\"/>\n    <nd ref=\"8568942746\"/>\n    <nd ref=\"300505152\"/>\n    <tag k=\"waterway\" v=\"stream\"/>\n  </way>\n  <way id=\"96810463\" version=\"22\" timestamp=\"2025-08-17T16:54:09Z\" changeset=\"170577090\" uid=\"742733\" user=\"perusluku\">\n    <nd ref=\"1121509151\"/>\n    <nd ref=\"25505407\"/>\n    <nd ref=\"13036990665\"/>\n    <nd ref=\"1121458905\"/>\n    <nd ref=\"1121503209\"/>\n    <nd ref=\"1121470828\"/>\n    <nd ref=\"1121609776\"/>\n    <nd ref=\"391308169\"/>\n    <nd ref=\"11409449834\"/>\n    <nd ref=\"25505409\"/>\n    <nd ref=\"11409449833\"/>\n    <nd ref=\"1828984289\"/>\n    <nd ref=\"391308171\"/>\n    <nd ref=\"1121500386\"/>\n    <nd ref=\"382381757\"/>\n    <nd ref=\"382367894\"/>\n    <nd ref=\"25505411\"/>\n    <nd ref=\"25505412\"/>\n    <nd ref=\"1121610451\"/>\n    <nd ref=\"1121599535\"/>\n    <nd ref=\"1121598355\"/>\n    <nd ref=\"25505413\"/>\n    <nd ref=\"391311575\"/>\n    <nd ref=\"391311576\"/>\n    <nd ref=\"1121439620\"/>\n    <nd ref=\"11409449820\"/>\n    <nd ref=\"382372454\"/>\n    <nd ref=\"11409449819\"/>\n    <nd ref=\"25505416\"/>\n    <nd ref=\"11409449814\"/>\n    <nd ref=\"11409449815\"/>\n    <nd ref=\"1121501717\"/>\n    <nd ref=\"384296553\"/>\n    <nd ref=\"4548426298\"/>\n    <nd ref=\"11409449818\"/>\n    <nd ref=\"11409449805\"/>\n    <nd ref=\"11409449806\"/>\n    <nd ref=\"11409449807\"/>\n    <nd ref=\"300504726\"/>\n    <nd ref=\"11409449808\"/>\n    <nd ref=\"300504710\"/>\n    <nd ref=\"11409449810\"/>\n    <nd ref=\"11409449811\"/>\n    <nd ref=\"1121570469\"/>\n    <nd ref=\"25505419\"/>\n    <nd ref=\"300505161\"/>\n    <nd ref=\"11409449813\"/>\n    <nd ref=\"300505162\"/>\n    <nd ref=\"300505163\"/>\n    <nd ref=\"25505422\"/>\n    <nd ref=\"300505164\"/>\n    <nd ref=\"300505165\"/>\n    <nd ref=\"1121435310\"/>\n    <nd ref=\"4548426299\"/>\n    <nd ref=\"11409449788\"/>\n    <nd ref=\"1121588440\"/>\n    <nd ref=\"12582078946\"/>\n    <nd ref=\"12582011413\"/>\n    <nd ref=\"1121523874\"/>\n    <nd ref=\"4250551267\"/>\n    <nd ref=\"1121453490\"/>\n    <nd ref=\"4250551278\"/>\n    <nd ref=\"1121609346\"/>\n    <nd ref=\"2898580206\"/>\n    <nd ref=\"1121516543\"/>\n    <nd ref=\"1121611363\"/>\n    <nd ref=\"7556210664\"/>\n    <nd ref=\"7556210630\"/>\n    <nd ref=\"7556210633\"/>\n    <nd ref=\"7556210636\"/>\n    <nd ref=\"1121609721\"/>\n    <nd ref=\"7556210606\"/>\n    <nd ref=\"7556210665\"/>\n    <nd ref=\"7556210666\"/>\n    <nd ref=\"4250515016\"/>\n    <nd ref=\"7556210667\"/>\n    <nd ref=\"1121598961\"/>\n    <nd ref=\"1121563189\"/>\n    <nd ref=\"7556210592\"/>\n    <nd ref=\"1828984349\"/>\n    <nd ref=\"1121632921\"/>\n    <nd ref=\"300505151\"/>\n    <nd ref=\"1828984327\"/>\n    <nd ref=\"7556210595\"/>\n    <nd ref=\"1828984323\"/>\n    <nd ref=\"1828984317\"/>\n    <nd ref=\"1121431763\"/>\n    <nd ref=\"25505463\"/>\n    <nd ref=\"1828984310\"/>\n    <nd ref=\"300505152\"/>\n    <nd ref=\"1828984308\"/>\n    <nd ref=\"300505153\"/>\n    <nd ref=\"25505464\"/>\n    <nd ref=\"300505154\"/>\n    <nd ref=\"1828984307\"/>\n    <nd ref=\"300505155\"/>\n    <nd ref=\"1828984306\"/>\n    <nd ref=\"1828984305\"/>\n    <nd ref=\"1828984304\"/>\n    <nd ref=\"25505465\"/>\n    <nd ref=\"4250551257\"/>\n    <nd ref=\"1828984302\"/>\n    <nd ref=\"4250551247\"/>\n    <nd ref=\"1121465142\"/>\n    <nd ref=\"25505467\"/>\n    <nd ref=\"1828984300\"/>\n    <nd ref=\"1828984299\"/>\n    <nd ref=\"1121510927\"/>\n    <nd ref=\"1828984296\"/>\n    <nd ref=\"1828984297\"/>\n    <nd ref=\"4250551256\"/>\n    <nd ref=\"300505156\"/>\n    <nd ref=\"1828984294\"/>\n    <nd ref=\"1828984293\"/>\n    <nd ref=\"1828984292\"/>\n    <nd ref=\"1828984291\"/>\n    <nd ref=\"1121438880\"/>\n    <nd ref=\"25505469\"/>\n    <nd ref=\"1828984288\"/>\n    <nd ref=\"1121577546\"/>\n    <nd ref=\"300505157\"/>\n    <nd ref=\"1828984286\"/>\n    <nd ref=\"1828984285\"/>\n    <nd ref=\"300502030\"/>\n    <nd ref=\"300502048\"/>\n    <nd ref=\"300502047\"/>\n    <nd ref=\"1121480639\"/>\n    <nd ref=\"1121576944\"/>\n    <nd ref=\"1828984281\"/>\n    <nd ref=\"1121431155\"/>\n    <nd ref=\"1828984278\"/>\n    <nd ref=\"1121472529\"/>\n    <nd ref=\"1828984275\"/>\n    <nd ref=\"1828984273\"/>\n    <nd ref=\"1121448500\"/>\n    <nd ref=\"1121625893\"/>\n    <nd ref=\"1828984266\"/>\n    <nd ref=\"1121435959\"/>\n    <nd ref=\"1828984241\"/>\n    <nd ref=\"1121432576\"/>\n    <nd ref=\"25505490\"/>\n    <nd ref=\"300505158\"/>\n    <nd ref=\"12179728241\"/>\n    <nd ref=\"25505494\"/>\n    <nd ref=\"13036945681\"/>\n    <nd ref=\"13036945682\"/>\n    <nd ref=\"13036945684\"/>\n    <nd ref=\"13036945683\"/>\n    <nd ref=\"13036945685\"/>\n    <nd ref=\"262795580\"/>\n    <nd ref=\"13036945677\"/>\n    <nd ref=\"13036945676\"/>\n    <nd ref=\"13036945675\"/>\n    <nd ref=\"1121596206\"/>\n    <nd ref=\"13036945674\"/>\n    <nd ref=\"13036945673\"/>\n    <nd ref=\"13036945672\"/>\n    <nd ref=\"1121510959\"/>\n    <nd ref=\"25505498\"/>\n    <nd ref=\"12179728223\"/>\n    <nd ref=\"12179728216\"/>\n    <nd ref=\"1828984182\"/>\n    <nd ref=\"4684825109\"/>\n    <nd ref=\"12179685482\"/>\n    <nd ref=\"1121505436\"/>\n    <nd ref=\"12179728243\"/>\n    <nd ref=\"262795581\"/>\n    <nd ref=\"1121621842\"/>\n    <nd ref=\"1828984183\"/>\n    <nd ref=\"1828984184\"/>\n    <nd ref=\"1828984185\"/>\n    <nd ref=\"1828984186\"/>\n    <nd ref=\"1121603657\"/>\n    <nd ref=\"1828984188\"/>\n    <nd ref=\"1121462519\"/>\n    <nd ref=\"1828984192\"/>\n    <nd ref=\"1828984193\"/>\n    <nd ref=\"1828984199\"/>\n    <nd ref=\"1828984198\"/>\n    <nd ref=\"1828984197\"/>\n    <nd ref=\"1121601448\"/>\n    <nd ref=\"1828984194\"/>\n    <nd ref=\"1121517169\"/>\n    <nd ref=\"1121603242\"/>\n    <nd ref=\"1121495785\"/>\n    <nd ref=\"25505512\"/>\n    <nd ref=\"303747502\"/>\n    <nd ref=\"1828984176\"/>\n    <nd ref=\"1121524098\"/>\n    <nd ref=\"1828984174\"/>\n    <nd ref=\"25505516\"/>\n    <nd ref=\"303747030\"/>\n    <nd ref=\"303747031\"/>\n    <nd ref=\"1828984169\"/>\n    <nd ref=\"25505519\"/>\n    <nd ref=\"1121605214\"/>\n    <nd ref=\"303747032\"/>\n    <nd ref=\"303747033\"/>\n    <nd ref=\"1828984147\"/>\n    <nd ref=\"1828984136\"/>\n    <nd ref=\"1828984132\"/>\n    <nd ref=\"1121498693\"/>\n    <nd ref=\"1121621418\"/>\n    <nd ref=\"1121565228\"/>\n    <nd ref=\"1121592625\"/>\n    <nd ref=\"1121493664\"/>\n    <nd ref=\"1121615390\"/>\n    <nd ref=\"1121611406\"/>\n    <nd ref=\"1121468108\"/>\n    <nd ref=\"13036927189\"/>\n    <nd ref=\"382114101\"/>\n    <nd ref=\"1828984206\"/>\n    <nd ref=\"303747205\"/>\n    <nd ref=\"1828984218\"/>\n    <nd ref=\"25505539\"/>\n    <nd ref=\"12541752053\"/>\n    <nd ref=\"5021892192\"/>\n    <nd ref=\"516392299\"/>\n    <nd ref=\"1828984231\"/>\n    <nd ref=\"1828984232\"/>\n    <nd ref=\"303747239\"/>\n    <nd ref=\"1828984234\"/>\n    <nd ref=\"1828984235\"/>\n    <nd ref=\"25505542\"/>\n    <nd ref=\"1828984236\"/>\n    <nd ref=\"5021892196\"/>\n    <nd ref=\"1828984237\"/>\n    <nd ref=\"300505159\"/>\n    <nd ref=\"1828984247\"/>\n    <nd ref=\"4143402186\"/>\n    <nd ref=\"5021892323\"/>\n    <nd ref=\"1121571574\"/>\n    <nd ref=\"1121624294\"/>\n    <nd ref=\"12541752060\"/>\n    <nd ref=\"1121495066\"/>\n    <nd ref=\"5021869004\"/>\n    <nd ref=\"5021892200\"/>\n    <nd ref=\"12741231906\"/>\n    <nd ref=\"12741231905\"/>\n    <nd ref=\"1121522698\"/>\n    <nd ref=\"1121547271\"/>\n    <nd ref=\"1121449366\"/>\n    <nd ref=\"1828984225\"/>\n    <nd ref=\"1121501727\"/>\n    <nd ref=\"1828984221\"/>\n    <nd ref=\"1828984227\"/>\n    <nd ref=\"1121581788\"/>\n    <nd ref=\"1828984222\"/>\n    <nd ref=\"1828984220\"/>\n    <nd ref=\"1828984219\"/>\n    <nd ref=\"1828984212\"/>\n    <nd ref=\"1828984213\"/>\n    <nd ref=\"1121567336\"/>\n    <nd ref=\"1121582079\"/>\n    <nd ref=\"1828984211\"/>\n    <nd ref=\"1828984209\"/>\n    <nd ref=\"1828984207\"/>\n    <nd ref=\"1828984208\"/>\n    <nd ref=\"1828984210\"/>\n    <nd ref=\"1828984215\"/>\n    <nd ref=\"1121612457\"/>\n    <nd ref=\"1700764927\"/>\n    <nd ref=\"1700764931\"/>\n    <nd ref=\"25505399\"/>\n    <nd ref=\"1121437454\"/>\n    <nd ref=\"1700764934\"/>\n    <nd ref=\"1121635891\"/>\n    <nd ref=\"25505400\"/>\n    <nd ref=\"11409449855\"/>\n    <nd ref=\"11409449854\"/>\n    <nd ref=\"11409449853\"/>\n    <nd ref=\"11409449852\"/>\n    <nd ref=\"1828984239\"/>\n    <nd ref=\"11409449851\"/>\n    <nd ref=\"1121633197\"/>\n    <nd ref=\"382107334\"/>\n    <nd ref=\"11409449849\"/>\n    <nd ref=\"1828984265\"/>\n    <nd ref=\"1121601021\"/>\n    <nd ref=\"11409449847\"/>\n    <nd ref=\"11409449848\"/>\n    <nd ref=\"25505403\"/>\n    <nd ref=\"1828984267\"/>\n    <nd ref=\"1828984268\"/>\n    <nd ref=\"25505405\"/>\n  </way>\n  <way id=\"140383769\" version=\"8\" timestamp=\"2025-07-18T16:55:40Z\" changeset=\"169117097\" uid=\"486606\" user=\"Barro\">\n    <nd ref=\"1538138712\"/>\n    <nd ref=\"1538138567\"/>\n    <nd ref=\"6139123141\"/>\n    <nd ref=\"268728009\"/>\n    <nd ref=\"1539656217\"/>\n    <nd ref=\"1539656211\"/>\n    <nd ref=\"268728010\"/>\n    <nd ref=\"12285606748\"/>\n    <nd ref=\"268728011\"/>\n    <nd ref=\"268728012\"/>\n    <nd ref=\"278302283\"/>\n    <nd ref=\"1539656213\"/>\n    <nd ref=\"1539656209\"/>\n    <nd ref=\"1539656208\"/>\n    <nd ref=\"268728013\"/>\n    <nd ref=\"1539656192\"/>\n    <nd ref=\"278302281\"/>\n    <nd ref=\"4540152754\"/>\n    <nd ref=\"4540152732\"/>\n    <nd ref=\"4540152725\"/>\n    <nd ref=\"268728015\"/>\n    <nd ref=\"1539655881\"/>\n    <nd ref=\"268728055\"/>\n    <nd ref=\"1539655844\"/>\n    <nd ref=\"1539655838\"/>\n    <nd ref=\"1539655835\"/>\n    <nd ref=\"4540152890\"/>\n    <nd ref=\"1539655841\"/>\n    <nd ref=\"4540152893\"/>\n    <nd ref=\"276457280\"/>\n    <tag k=\"highway\" v=\"residential\"/>\n    <tag k=\"maxspeed\" v=\"30\"/>\n    <tag k=\"name\" v=\"Nuumäentie\"/>\n    <tag k=\"name:fi\" v=\"Nuumäentie\"/>\n    <tag k=\"name:sv\" v=\"Nobacksvägen\"/>\n    <tag k=\"surface\" v=\"asphalt\"/>\n  </way>\n  <way id=\"140383778\" version=\"7\" timestamp=\"2025-11-02T16:36:02Z\" changeset=\"174111731\" uid=\"10617839\" user=\"rssto\">\n    <nd ref=\"1538139047\"/>\n    <nd ref=\"4254168969\"/>\n    <nd ref=\"1538138691\"/>\n    <nd ref=\"4254168965\"/>\n    <nd ref=\"1538138817\"/>\n    <tag k=\"bicycle\" v=\"use_sidepath\"/>\n    <tag k=\"highway\" v=\"secondary\"/>\n    <tag k=\"lanes\" v=\"1\"/>\n    <tag k=\"lit\" v=\"yes\"/>\n    <tag k=\"maxspeed\" v=\"30\"/>\n    <tag k=\"name\" v=\"Lähderannantie\"/>\n    <tag k=\"name:fi\" v=\"Lähderannantie\"/>\n    <tag k=\"name:sv\" v=\"Källstrandsvägen\"/>\n    <tag k=\"oneway\" v=\"yes\"/>\n    <tag k=\"surface\" v=\"asphalt\"/>\n    <tag k=\"traffic_calming\" v=\"table\"/>\n  </way>\n  <way id=\"140542965\" version=\"1\" timestamp=\"2011-12-10T16:01:50Z\" changeset=\"10082287\" uid=\"139957\" user=\"ij_\">\n    <nd ref=\"1539655691\"/>\n    <nd ref=\"1539655683\"/>\n    <tag k=\"bridge\" v=\"yes\"/>\n    <tag k=\"foot\" v=\"yes\"/>\n    <tag k=\"highway\" v=\"cycleway\"/>\n    <tag k=\"layer\" v=\"1\"/>\n    <tag k=\"source\" v=\"survey\"/>\n  </way>\n  <way id=\"171940210\" version=\"20\" timestamp=\"2025-08-17T16:54:09Z\" changeset=\"170577090\" uid=\"742733\" user=\"perusluku\">\n    <nd ref=\"7556210605\"/>\n    <nd ref=\"4250515016\"/>\n    <nd ref=\"7556210667\"/>\n    <nd ref=\"1121598961\"/>\n    <nd ref=\"1121563189\"/>\n    <nd ref=\"7556210592\"/>\n    <nd ref=\"1828984349\"/>\n    <nd ref=\"1121632921\"/>\n    <nd ref=\"300505151\"/>\n    <nd ref=\"1828984327\"/>\n    <nd ref=\"7556210595\"/>\n    <nd ref=\"1828984323\"/>\n    <nd ref=\"1828984317\"/>\n    <nd ref=\"1121431763\"/>\n    <nd ref=\"25505463\"/>\n    <nd ref=\"1828984310\"/>\n    <nd ref=\"300505152\"/>\n    <nd ref=\"1828984308\"/>\n    <nd ref=\"300505153\"/>\n    <nd ref=\"25505464\"/>\n    <nd ref=\"300505154\"/>\n    <nd ref=\"1828984307\"/>\n    <nd ref=\"300505155\"/>\n    <nd ref=\"1828984306\"/>\n    <nd ref=\"1828984305\"/>\n    <nd ref=\"1828984304\"/>\n    <nd ref=\"25505465\"/>\n    <nd ref=\"4250551257\"/>\n    <nd ref=\"1828984302\"/>\n    <nd ref=\"4250551247\"/>\n    <nd ref=\"1121465142\"/>\n    <nd ref=\"25505467\"/>\n    <nd ref=\"1828984300\"/>\n    <nd ref=\"1828984299\"/>\n    <nd ref=\"1121510927\"/>\n    <nd ref=\"1828984296\"/>\n    <nd ref=\"1828984297\"/>\n    <nd ref=\"4250551256\"/>\n    <nd ref=\"300505156\"/>\n    <nd ref=\"1828984294\"/>\n    <nd ref=\"1828984293\"/>\n    <nd ref=\"1828984292\"/>\n    <nd ref=\"1828984291\"/>\n    <nd ref=\"1121438880\"/>\n    <nd ref=\"25505469\"/>\n    <nd ref=\"1828984288\"/>\n    <nd ref=\"1121577546\"/>\n    <nd ref=\"300505157\"/>\n    <nd ref=\"1828984286\"/>\n    <nd ref=\"1828984285\"/>\n    <nd ref=\"300502030\"/>\n    <nd ref=\"300502048\"/>\n    <nd ref=\"300502047\"/>\n    <nd ref=\"1121480639\"/>\n    <nd ref=\"1121576944\"/>\n    <nd ref=\"1828984281\"/>\n    <nd ref=\"1121431155\"/>\n    <nd ref=\"1828984278\"/>\n    <nd ref=\"1121472529\"/>\n    <nd ref=\"1828984275\"/>\n    <nd ref=\"1828984273\"/>\n    <nd ref=\"1121448500\"/>\n    <nd ref=\"1121625893\"/>\n    <nd ref=\"1828984266\"/>\n    <nd ref=\"1121435959\"/>\n    <nd ref=\"1828984241\"/>\n    <nd ref=\"1121432576\"/>\n    <nd ref=\"25505490\"/>\n    <nd ref=\"300505158\"/>\n    <nd ref=\"12179728241\"/>\n    <nd ref=\"25505494\"/>\n    <nd ref=\"13036945681\"/>\n    <nd ref=\"13036945682\"/>\n    <nd ref=\"13036945684\"/>\n    <nd ref=\"13036945683\"/>\n    <nd ref=\"13036945685\"/>\n    <nd ref=\"262795580\"/>\n    <nd ref=\"13036945677\"/>\n    <nd ref=\"13036945676\"/>\n    <nd ref=\"13036945675\"/>\n    <nd ref=\"1121596206\"/>\n    <nd ref=\"13036945674\"/>\n    <nd ref=\"13036945673\"/>\n    <nd ref=\"13036945672\"/>\n    <nd ref=\"1121510959\"/>\n    <nd ref=\"25505498\"/>\n    <nd ref=\"12179728223\"/>\n    <nd ref=\"12179728216\"/>\n    <nd ref=\"1828984182\"/>\n    <nd ref=\"4684825109\"/>\n    <nd ref=\"12179685482\"/>\n    <nd ref=\"1121505436\"/>\n    <nd ref=\"12179728243\"/>\n    <nd ref=\"262795581\"/>\n    <nd ref=\"1121621842\"/>\n    <nd ref=\"1828984183\"/>\n    <nd ref=\"1828984184\"/>\n    <nd ref=\"1828984185\"/>\n    <nd ref=\"1828984186\"/>\n    <nd ref=\"1121603657\"/>\n    <nd ref=\"1828984188\"/>\n    <nd ref=\"1121462519\"/>\n    <nd ref=\"1828984192\"/>\n    <nd ref=\"1828984193\"/>\n    <nd ref=\"1828984199\"/>\n    <nd ref=\"1828984198\"/>\n    <nd ref=\"1828984197\"/>\n    <nd ref=\"1121601448\"/>\n    <nd ref=\"1828984194\"/>\n    <nd ref=\"1121517169\"/>\n    <nd ref=\"1121603242\"/>\n    <nd ref=\"1121495785\"/>\n    <nd ref=\"25505512\"/>\n    <nd ref=\"303747502\"/>\n    <nd ref=\"1828984176\"/>\n    <nd ref=\"1121524098\"/>\n    <nd ref=\"1828984174\"/>\n    <nd ref=\"25505516\"/>\n    <nd ref=\"303747030\"/>\n    <nd ref=\"303747031\"/>\n    <nd ref=\"1828984169\"/>\n    <nd ref=\"25505519\"/>\n    <nd ref=\"1121605214\"/>\n    <nd ref=\"303747032\"/>\n    <nd ref=\"303747033\"/>\n    <nd ref=\"1828984147\"/>\n    <nd ref=\"1828984136\"/>\n    <nd ref=\"1828984132\"/>\n    <nd ref=\"1828984129\"/>\n    <nd ref=\"1828984134\"/>\n    <nd ref=\"249664856\"/>\n    <nd ref=\"1828984151\"/>\n    <nd ref=\"1828984157\"/>\n    <nd ref=\"249664746\"/>\n    <nd ref=\"249664744\"/>\n    <nd ref=\"1828984153\"/>\n    <nd ref=\"25505522\"/>\n    <nd ref=\"1828984145\"/>\n    <nd ref=\"1828984149\"/>\n    <nd ref=\"1828984143\"/>\n    <nd ref=\"303747034\"/>\n    <nd ref=\"303747035\"/>\n    <nd ref=\"1828984163\"/>\n    <nd ref=\"303747119\"/>\n    <nd ref=\"1828984161\"/>\n    <nd ref=\"1828984140\"/>\n    <nd ref=\"25505527\"/>\n    <nd ref=\"249664736\"/>\n    <nd ref=\"1828984166\"/>\n    <nd ref=\"1828984168\"/>\n    <nd ref=\"303747036\"/>\n    <nd ref=\"1828984170\"/>\n    <nd ref=\"1828984171\"/>\n    <nd ref=\"1828984172\"/>\n    <nd ref=\"303747123\"/>\n    <nd ref=\"303747037\"/>\n    <nd ref=\"303747038\"/>\n    <nd ref=\"303747125\"/>\n    <nd ref=\"303747039\"/>\n    <nd ref=\"1828984178\"/>\n    <nd ref=\"303747040\"/>\n    <nd ref=\"303747041\"/>\n    <nd ref=\"303747126\"/>\n    <nd ref=\"303747127\"/>\n    <nd ref=\"1828984200\"/>\n    <nd ref=\"247103490\"/>\n    <nd ref=\"303747173\"/>\n    <nd ref=\"303747244\"/>\n    <nd ref=\"303747174\"/>\n    <nd ref=\"1828984202\"/>\n    <nd ref=\"1828984203\"/>\n    <nd ref=\"1828984204\"/>\n    <nd ref=\"303747175\"/>\n    <nd ref=\"303747245\"/>\n    <nd ref=\"1828984206\"/>\n    <nd ref=\"303747205\"/>\n    <nd ref=\"1828984218\"/>\n    <nd ref=\"25505539\"/>\n    <nd ref=\"12541752053\"/>\n    <nd ref=\"5021892192\"/>\n    <nd ref=\"516392299\"/>\n    <nd ref=\"1828984231\"/>\n    <nd ref=\"1828984232\"/>\n    <nd ref=\"1828984233\"/>\n    <nd ref=\"303747239\"/>\n    <nd ref=\"1828984234\"/>\n    <nd ref=\"1828984235\"/>\n    <nd ref=\"25505542\"/>\n    <nd ref=\"1828984236\"/>\n    <nd ref=\"5021892196\"/>\n    <nd ref=\"1828984237\"/>\n    <nd ref=\"300505159\"/>\n    <nd ref=\"1828984247\"/>\n    <nd ref=\"4143402186\"/>\n    <nd ref=\"5021892326\"/>\n    <nd ref=\"5021892327\"/>\n    <nd ref=\"5021892328\"/>\n    <nd ref=\"5021869013\"/>\n    <nd ref=\"5021892330\"/>\n    <nd ref=\"5021892329\"/>\n    <nd ref=\"5021869014\"/>\n    <nd ref=\"5021892198\"/>\n    <nd ref=\"5021892201\"/>\n    <nd ref=\"1828984264\"/>\n    <nd ref=\"5021892204\"/>\n    <nd ref=\"5021892205\"/>\n    <nd ref=\"5021892206\"/>\n    <nd ref=\"5021892203\"/>\n    <nd ref=\"5021892207\"/>\n    <nd ref=\"1828984257\"/>\n    <nd ref=\"1828984254\"/>\n    <nd ref=\"1828984244\"/>\n    <nd ref=\"1828984242\"/>\n    <nd ref=\"5021869004\"/>\n    <nd ref=\"5021892200\"/>\n    <nd ref=\"12741231906\"/>\n    <nd ref=\"12741231905\"/>\n    <nd ref=\"1121522698\"/>\n    <nd ref=\"1121547271\"/>\n    <nd ref=\"1121449366\"/>\n    <nd ref=\"1828984225\"/>\n    <nd ref=\"1121501727\"/>\n    <nd ref=\"1828984221\"/>\n    <nd ref=\"1828984227\"/>\n    <nd ref=\"1121581788\"/>\n    <nd ref=\"1828984222\"/>\n    <nd ref=\"1828984220\"/>\n    <nd ref=\"1828984219\"/>\n    <nd ref=\"1828984212\"/>\n    <nd ref=\"1828984213\"/>\n    <nd ref=\"1121567336\"/>\n    <nd ref=\"1121582079\"/>\n    <nd ref=\"1828984211\"/>\n    <nd ref=\"1828984209\"/>\n    <nd ref=\"1828984207\"/>\n    <nd ref=\"1828984208\"/>\n    <nd ref=\"1828984210\"/>\n    <nd ref=\"1828984215\"/>\n    <nd ref=\"1121612457\"/>\n    <nd ref=\"1700764927\"/>\n    <nd ref=\"1700764931\"/>\n    <nd ref=\"25505399\"/>\n    <nd ref=\"1121437454\"/>\n    <nd ref=\"1700764934\"/>\n    <nd ref=\"1121635891\"/>\n    <nd ref=\"25505400\"/>\n    <nd ref=\"11409449855\"/>\n    <nd ref=\"11409449854\"/>\n    <nd ref=\"11409449853\"/>\n    <nd ref=\"11409449852\"/>\n    <nd ref=\"1828984239\"/>\n    <nd ref=\"11409449851\"/>\n    <nd ref=\"1121633197\"/>\n    <nd ref=\"382107334\"/>\n    <nd ref=\"11409449849\"/>\n    <nd ref=\"1828984265\"/>\n    <nd ref=\"1121601021\"/>\n    <nd ref=\"11409449847\"/>\n    <nd ref=\"11409449848\"/>\n    <nd ref=\"25505403\"/>\n    <nd ref=\"13076214961\"/>\n    <nd ref=\"13076214964\"/>\n    <nd ref=\"25505405\"/>\n    <nd ref=\"1828984272\"/>\n    <nd ref=\"1828984274\"/>\n    <nd ref=\"25505406\"/>\n    <nd ref=\"1828984280\"/>\n    <nd ref=\"1121509151\"/>\n    <nd ref=\"25505407\"/>\n    <nd ref=\"13036990665\"/>\n    <nd ref=\"1121458905\"/>\n    <nd ref=\"11409449844\"/>\n    <nd ref=\"25505408\"/>\n    <nd ref=\"11409449845\"/>\n    <nd ref=\"11409449837\"/>\n    <nd ref=\"382367812\"/>\n    <nd ref=\"1121609776\"/>\n    <nd ref=\"391308169\"/>\n    <nd ref=\"11409449834\"/>\n    <nd ref=\"25505409\"/>\n    <nd ref=\"11409449833\"/>\n    <nd ref=\"1828984289\"/>\n    <nd ref=\"391308171\"/>\n    <nd ref=\"1121500386\"/>\n    <nd ref=\"382381757\"/>\n    <nd ref=\"382367894\"/>\n    <nd ref=\"25505411\"/>\n    <nd ref=\"25505412\"/>\n    <nd ref=\"1121610451\"/>\n    <nd ref=\"11409449825\"/>\n    <nd ref=\"392060899\"/>\n    <nd ref=\"11409449826\"/>\n    <nd ref=\"382380015\"/>\n    <nd ref=\"382380018\"/>\n    <nd ref=\"8207527227\"/>\n    <nd ref=\"25505413\"/>\n    <nd ref=\"391311575\"/>\n    <nd ref=\"391311576\"/>\n    <nd ref=\"1121439620\"/>\n    <nd ref=\"11409449820\"/>\n    <nd ref=\"382372454\"/>\n    <nd ref=\"11409449819\"/>\n    <nd ref=\"25505416\"/>\n    <nd ref=\"11409449814\"/>\n    <nd ref=\"11409449815\"/>\n    <nd ref=\"1121501717\"/>\n    <nd ref=\"384296553\"/>\n    <nd ref=\"4548426298\"/>\n    <nd ref=\"11409449818\"/>\n    <nd ref=\"11409449805\"/>\n    <nd ref=\"11409449806\"/>\n    <nd ref=\"11409449807\"/>\n    <nd ref=\"300504726\"/>\n    <nd ref=\"11409449808\"/>\n    <nd ref=\"300504710\"/>\n    <nd ref=\"11409449810\"/>\n    <nd ref=\"11409449811\"/>\n    <nd ref=\"1121570469\"/>\n    <nd ref=\"25505419\"/>\n    <nd ref=\"300505161\"/>\n    <nd ref=\"11409449813\"/>\n    <nd ref=\"300505162\"/>\n    <nd ref=\"300505163\"/>\n    <nd ref=\"25505422\"/>\n    <nd ref=\"300505164\"/>\n    <nd ref=\"300505165\"/>\n    <nd ref=\"12541752074\"/>\n    <nd ref=\"12541752073\"/>\n    <nd ref=\"11409449789\"/>\n    <nd ref=\"300505166\"/>\n    <nd ref=\"1157979400\"/>\n    <nd ref=\"7556210682\"/>\n    <nd ref=\"300505167\"/>\n    <nd ref=\"391429351\"/>\n    <nd ref=\"300505168\"/>\n    <nd ref=\"11409449790\"/>\n    <nd ref=\"300505169\"/>\n    <nd ref=\"300505170\"/>\n    <nd ref=\"11409449791\"/>\n    <nd ref=\"11409449792\"/>\n    <nd ref=\"300505171\"/>\n    <nd ref=\"11409449793\"/>\n    <nd ref=\"11409449794\"/>\n    <nd ref=\"300505172\"/>\n    <nd ref=\"11409449795\"/>\n    <nd ref=\"300505173\"/>\n    <nd ref=\"384899935\"/>\n    <nd ref=\"1828984409\"/>\n    <nd ref=\"11409449796\"/>\n    <nd ref=\"384899936\"/>\n    <nd ref=\"1828984403\"/>\n    <nd ref=\"7556210650\"/>\n    <nd ref=\"7556210649\"/>\n    <nd ref=\"7556210651\"/>\n    <nd ref=\"7556210647\"/>\n    <nd ref=\"7556210599\"/>\n    <nd ref=\"11409449797\"/>\n    <nd ref=\"11409449798\"/>\n    <nd ref=\"7556210597\"/>\n    <nd ref=\"11409449799\"/>\n    <nd ref=\"11409449800\"/>\n    <nd ref=\"7556210598\"/>\n    <nd ref=\"7556210600\"/>\n    <nd ref=\"7556210601\"/>\n    <nd ref=\"11409449801\"/>\n    <nd ref=\"11409449802\"/>\n    <nd ref=\"7556210603\"/>\n    <nd ref=\"7556210604\"/>\n    <nd ref=\"11409449803\"/>\n    <nd ref=\"7556210655\"/>\n    <nd ref=\"11409449804\"/>\n    <nd ref=\"7556210607\"/>\n    <nd ref=\"7556210605\"/>\n    <tag k=\"loc_name\" v=\"Lippis\"/>\n    <tag k=\"name\" v=\"Lippajärvi\"/>\n    <tag k=\"name:sv\" v=\"Klappträsk\"/>\n    <tag k=\"natural\" v=\"water\"/>\n    <tag k=\"water\" v=\"lake\"/>\n    <tag k=\"wikidata\" v=\"Q24356918\"/>\n    <tag k=\"wikipedia\" v=\"fi:Lippajärvi (järvi)\"/>\n  </way>\n  <way id=\"190326831\" version=\"2\" timestamp=\"2014-06-18T08:17:00Z\" changeset=\"22999613\" uid=\"2101719\" user=\"deodorantdealer\">\n    <nd ref=\"2009371467\"/>\n    <nd ref=\"2009371538\"/>\n    <nd ref=\"2009371541\"/>\n    <nd ref=\"2009371610\"/>\n    <nd ref=\"2009371612\"/>\n    <nd ref=\"2009371628\"/>\n    <nd ref=\"2009371635\"/>\n    <nd ref=\"2009371654\"/>\n    <nd ref=\"2009371660\"/>\n    <nd ref=\"2009371623\"/>\n    <nd ref=\"2009371626\"/>\n    <nd ref=\"2009371652\"/>\n    <nd ref=\"2009371656\"/>\n    <nd ref=\"2009371673\"/>\n    <nd ref=\"2009371675\"/>\n    <nd ref=\"2009371688\"/>\n    <nd ref=\"2009371689\"/>\n    <nd ref=\"2009371699\"/>\n    <nd ref=\"2009371703\"/>\n    <nd ref=\"2009371622\"/>\n    <nd ref=\"2009371619\"/>\n    <nd ref=\"2009371545\"/>\n    <nd ref=\"2009371540\"/>\n    <nd ref=\"2009371526\"/>\n    <nd ref=\"2009371523\"/>\n    <nd ref=\"2009371514\"/>\n    <nd ref=\"2009371513\"/>\n    <nd ref=\"2009371501\"/>\n    <nd ref=\"2009371494\"/>\n    <nd ref=\"2009371482\"/>\n    <nd ref=\"2009371481\"/>\n    <nd ref=\"2009371504\"/>\n    <nd ref=\"2009371499\"/>\n    <nd ref=\"2009371488\"/>\n    <nd ref=\"2009371483\"/>\n    <nd ref=\"2009371475\"/>\n    <nd ref=\"2009371467\"/>\n    <tag k=\"addr:city\" v=\"Espoo\"/>\n    <tag k=\"addr:housenumber\" v=\"1\"/>\n    <tag k=\"addr:postcode\" v=\"02720\"/>\n    <tag k=\"addr:street\" v=\"Nuuniitynkuja\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190327151\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009370678\"/>\n    <nd ref=\"2009370663\"/>\n    <nd ref=\"2009370579\"/>\n    <nd ref=\"2009370585\"/>\n    <nd ref=\"2009370632\"/>\n    <nd ref=\"2009370651\"/>\n    <nd ref=\"2009370678\"/>\n    <tag k=\"addr:housenumber\" v=\"10a\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190327196\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009370415\"/>\n    <nd ref=\"2009370363\"/>\n    <nd ref=\"2009370359\"/>\n    <nd ref=\"2009370351\"/>\n    <nd ref=\"2009370349\"/>\n    <nd ref=\"2009370370\"/>\n    <nd ref=\"2009370371\"/>\n    <nd ref=\"2009370408\"/>\n    <nd ref=\"2009370415\"/>\n    <tag k=\"addr:housenumber\" v=\"10b\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330004\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454261\"/>\n    <nd ref=\"2009454290\"/>\n    <nd ref=\"2009454278\"/>\n    <nd ref=\"2009454344\"/>\n    <nd ref=\"2009454340\"/>\n    <nd ref=\"2009454382\"/>\n    <nd ref=\"2009454378\"/>\n    <nd ref=\"2009454308\"/>\n    <nd ref=\"2009454311\"/>\n    <nd ref=\"2009454254\"/>\n    <nd ref=\"2009454261\"/>\n    <tag k=\"addr:housenumber\" v=\"12\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330010\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454375\"/>\n    <nd ref=\"2009454407\"/>\n    <nd ref=\"2009454266\"/>\n    <nd ref=\"2009454264\"/>\n    <nd ref=\"2009454252\"/>\n    <nd ref=\"2009454222\"/>\n    <nd ref=\"2009454274\"/>\n    <nd ref=\"2009454275\"/>\n    <nd ref=\"2009454337\"/>\n    <nd ref=\"2009454358\"/>\n    <nd ref=\"2009454375\"/>\n    <tag k=\"addr:housenumber\" v=\"9\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330024\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454250\"/>\n    <nd ref=\"2009454293\"/>\n    <nd ref=\"2009454100\"/>\n    <nd ref=\"2009454110\"/>\n    <nd ref=\"2009454039\"/>\n    <nd ref=\"2009454044\"/>\n    <nd ref=\"2009453958\"/>\n    <nd ref=\"2009453912\"/>\n    <nd ref=\"2009453986\"/>\n    <nd ref=\"2009453975\"/>\n    <nd ref=\"2009454066\"/>\n    <nd ref=\"2009454049\"/>\n    <nd ref=\"2009454250\"/>\n    <tag k=\"addr:housenumber\" v=\"9\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330040\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454617\"/>\n    <nd ref=\"2009454619\"/>\n    <nd ref=\"2009454613\"/>\n    <nd ref=\"2009454614\"/>\n    <nd ref=\"7448817973\"/>\n    <nd ref=\"2009454608\"/>\n    <nd ref=\"2009454610\"/>\n    <nd ref=\"2009454599\"/>\n    <nd ref=\"2009454602\"/>\n    <nd ref=\"7448817974\"/>\n    <nd ref=\"2009454597\"/>\n    <nd ref=\"2009454598\"/>\n    <nd ref=\"2009454591\"/>\n    <nd ref=\"2009454593\"/>\n    <nd ref=\"7448817975\"/>\n    <nd ref=\"2009454588\"/>\n    <nd ref=\"2009454590\"/>\n    <nd ref=\"2009454578\"/>\n    <nd ref=\"2009454580\"/>\n    <nd ref=\"7448817976\"/>\n    <nd ref=\"2009454576\"/>\n    <nd ref=\"2009454571\"/>\n    <nd ref=\"2009454585\"/>\n    <nd ref=\"2009454583\"/>\n    <nd ref=\"2009454595\"/>\n    <nd ref=\"2009454594\"/>\n    <nd ref=\"2009454606\"/>\n    <nd ref=\"2009454604\"/>\n    <nd ref=\"2009454617\"/>\n    <tag k=\"addr:housenumber\" v=\"4\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"C\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330049\" version=\"3\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454543\"/>\n    <nd ref=\"2009454551\"/>\n    <nd ref=\"7530028804\"/>\n    <nd ref=\"7530028805\"/>\n    <nd ref=\"7530028806\"/>\n    <nd ref=\"7530028807\"/>\n    <nd ref=\"2009454421\"/>\n    <nd ref=\"2009454401\"/>\n    <nd ref=\"2009454543\"/>\n    <tag k=\"addr:housenumber\" v=\"7\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"B\"/>\n    <tag k=\"building\" v=\"terrace\"/>\n    <tag k=\"building:levels\" v=\"2\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330059\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454042\"/>\n    <nd ref=\"2009454046\"/>\n    <nd ref=\"2009453827\"/>\n    <nd ref=\"2009453823\"/>\n    <nd ref=\"2009454042\"/>\n    <tag k=\"addr:housenumber\" v=\"9\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330061\" version=\"1\" timestamp=\"2012-11-11T20:33:21Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\">\n    <nd ref=\"2009454447\"/>\n    <nd ref=\"2009454449\"/>\n    <nd ref=\"2009454233\"/>\n    <nd ref=\"2009454229\"/>\n    <nd ref=\"2009454447\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330064\" version=\"3\" timestamp=\"2020-05-19T12:16:15Z\" changeset=\"85441342\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009453884\"/>\n    <nd ref=\"2009453963\"/>\n    <nd ref=\"2009453943\"/>\n    <nd ref=\"2009453871\"/>\n    <nd ref=\"2009453884\"/>\n    <tag k=\"addr:housenumber\" v=\"13\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330096\" version=\"1\" timestamp=\"2012-11-11T20:33:23Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\">\n    <nd ref=\"2009454488\"/>\n    <nd ref=\"2009454456\"/>\n    <nd ref=\"2009454452\"/>\n    <nd ref=\"2009454479\"/>\n    <nd ref=\"2009454488\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330100\" version=\"1\" timestamp=\"2012-11-11T20:33:23Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\">\n    <nd ref=\"2009454466\"/>\n    <nd ref=\"2009454518\"/>\n    <nd ref=\"2009454521\"/>\n    <nd ref=\"2009454469\"/>\n    <nd ref=\"2009454466\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330146\" version=\"1\" timestamp=\"2012-11-11T20:33:25Z\" changeset=\"13838974\" uid=\"52884\" user=\"abrax5\">\n    <nd ref=\"2009454355\"/>\n    <nd ref=\"2009454425\"/>\n    <nd ref=\"2009454433\"/>\n    <nd ref=\"2009454368\"/>\n    <nd ref=\"2009454355\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330155\" version=\"3\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454569\"/>\n    <nd ref=\"2009454573\"/>\n    <nd ref=\"7530028808\"/>\n    <nd ref=\"7530028809\"/>\n    <nd ref=\"7530028810\"/>\n    <nd ref=\"7530028811\"/>\n    <nd ref=\"2009454440\"/>\n    <nd ref=\"2009454435\"/>\n    <nd ref=\"2009454569\"/>\n    <tag k=\"addr:housenumber\" v=\"7\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"C\"/>\n    <tag k=\"building\" v=\"terrace\"/>\n    <tag k=\"building:levels\" v=\"2\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330161\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454124\"/>\n    <nd ref=\"2009454209\"/>\n    <nd ref=\"2009454205\"/>\n    <nd ref=\"2009454114\"/>\n    <nd ref=\"2009454124\"/>\n    <tag k=\"addr:housenumber\" v=\"12\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330163\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454371\"/>\n    <nd ref=\"2009454533\"/>\n    <nd ref=\"2009454541\"/>\n    <nd ref=\"2009454397\"/>\n    <nd ref=\"2009454371\"/>\n    <tag k=\"addr:housenumber\" v=\"6\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330178\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454162\"/>\n    <nd ref=\"2009454178\"/>\n    <nd ref=\"2009454014\"/>\n    <nd ref=\"2009453993\"/>\n    <nd ref=\"2009454162\"/>\n    <tag k=\"addr:housenumber\" v=\"9\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330204\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454079\"/>\n    <nd ref=\"2009454168\"/>\n    <nd ref=\"2009454145\"/>\n    <nd ref=\"2009454059\"/>\n    <nd ref=\"2009454079\"/>\n    <tag k=\"addr:housenumber\" v=\"13\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330226\" version=\"2\" timestamp=\"2021-03-10T11:39:55Z\" changeset=\"100776121\" uid=\"8105430\" user=\"Larmax\">\n    <nd ref=\"2009453809\"/>\n    <nd ref=\"2009453734\"/>\n    <nd ref=\"2009453717\"/>\n    <nd ref=\"2009453788\"/>\n    <nd ref=\"2009453790\"/>\n    <nd ref=\"2009453805\"/>\n    <nd ref=\"2009453809\"/>\n    <tag k=\"addr:housenumber\" v=\"9\"/>\n    <tag k=\"addr:street\" v=\"Tuokkostie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330237\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454107\"/>\n    <nd ref=\"2009454138\"/>\n    <nd ref=\"2009454029\"/>\n    <nd ref=\"2009454012\"/>\n    <nd ref=\"2009454036\"/>\n    <nd ref=\"2009454026\"/>\n    <nd ref=\"2009454107\"/>\n    <tag k=\"addr:housenumber\" v=\"11a\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330239\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009453887\"/>\n    <nd ref=\"2009453924\"/>\n    <nd ref=\"2009453825\"/>\n    <nd ref=\"2009453821\"/>\n    <nd ref=\"2009453836\"/>\n    <nd ref=\"2009453830\"/>\n    <nd ref=\"2009453887\"/>\n    <tag k=\"addr:housenumber\" v=\"11b\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330273\" version=\"3\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454459\"/>\n    <nd ref=\"2009454553\"/>\n    <nd ref=\"2009454555\"/>\n    <nd ref=\"2009454615\"/>\n    <nd ref=\"2009454618\"/>\n    <nd ref=\"7530028812\"/>\n    <nd ref=\"7530028813\"/>\n    <nd ref=\"7530028814\"/>\n    <nd ref=\"7530028815\"/>\n    <nd ref=\"2009454566\"/>\n    <nd ref=\"2009454557\"/>\n    <nd ref=\"7530028816\"/>\n    <nd ref=\"7530028817\"/>\n    <nd ref=\"7530028818\"/>\n    <nd ref=\"2009454463\"/>\n    <nd ref=\"2009454459\"/>\n    <tag k=\"addr:housenumber\" v=\"7\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"D\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"building:levels\" v=\"2\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330288\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454536\"/>\n    <nd ref=\"2009454529\"/>\n    <nd ref=\"2009454486\"/>\n    <nd ref=\"2009454491\"/>\n    <nd ref=\"2009454454\"/>\n    <nd ref=\"2009454461\"/>\n    <nd ref=\"2009454508\"/>\n    <nd ref=\"2009454511\"/>\n    <nd ref=\"2009454536\"/>\n    <tag k=\"addr:housenumber\" v=\"8\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"190330316\" version=\"2\" timestamp=\"2020-05-19T07:56:02Z\" changeset=\"85424648\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2009454037\"/>\n    <nd ref=\"2009454018\"/>\n    <nd ref=\"2009453937\"/>\n    <nd ref=\"2009453939\"/>\n    <nd ref=\"2009453927\"/>\n    <nd ref=\"2009453930\"/>\n    <nd ref=\"2009453946\"/>\n    <nd ref=\"2009453952\"/>\n    <nd ref=\"2009454037\"/>\n    <tag k=\"addr:housenumber\" v=\"14\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"residential\"/>\n    <tag k=\"source\" v=\"NLS.fi\"/>\n  </way>\n  <way id=\"288600948\" version=\"3\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2921549165\"/>\n    <nd ref=\"2921549166\"/>\n    <nd ref=\"6139126882\"/>\n    <nd ref=\"6139126880\"/>\n    <nd ref=\"6139126878\"/>\n    <nd ref=\"2921549167\"/>\n    <nd ref=\"2921549168\"/>\n    <nd ref=\"2921549165\"/>\n    <tag k=\"addr:city\" v=\"Espoo\"/>\n    <tag k=\"addr:housenumber\" v=\"2\"/>\n    <tag k=\"addr:postcode\" v=\"02720\"/>\n    <tag k=\"addr:street\" v=\"Nuuniitynkuja\"/>\n    <tag k=\"addr:unit\" v=\"A\"/>\n    <tag k=\"building\" v=\"residential\"/>\n  </way>\n  <way id=\"288601098\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2921551963\"/>\n    <nd ref=\"2921551964\"/>\n    <nd ref=\"7448840421\"/>\n    <nd ref=\"7448840420\"/>\n    <nd ref=\"2921551965\"/>\n    <nd ref=\"2921551966\"/>\n    <nd ref=\"7448840424\"/>\n    <nd ref=\"7448840426\"/>\n    <nd ref=\"2921551967\"/>\n    <nd ref=\"2921551968\"/>\n    <nd ref=\"7448840428\"/>\n    <nd ref=\"7448840429\"/>\n    <nd ref=\"2921551969\"/>\n    <nd ref=\"2921551970\"/>\n    <nd ref=\"2921551971\"/>\n    <nd ref=\"2921551972\"/>\n    <nd ref=\"2921551973\"/>\n    <nd ref=\"2921551974\"/>\n    <nd ref=\"2921551963\"/>\n    <tag k=\"addr:city\" v=\"Espoo\"/>\n    <tag k=\"addr:housenumber\" v=\"2\"/>\n    <tag k=\"addr:postcode\" v=\"02720\"/>\n    <tag k=\"addr:street\" v=\"Nuuniitynkuja\"/>\n    <tag k=\"addr:unit\" v=\"C\"/>\n    <tag k=\"building\" v=\"residential\"/>\n  </way>\n  <way id=\"288601751\" version=\"1\" timestamp=\"2014-06-18T08:22:17Z\" changeset=\"22999688\" uid=\"2101719\" user=\"deodorantdealer\">\n    <nd ref=\"2921555985\"/>\n    <nd ref=\"2921555986\"/>\n    <nd ref=\"2921555987\"/>\n    <nd ref=\"2921555988\"/>\n    <nd ref=\"2921555989\"/>\n    <nd ref=\"2921555990\"/>\n    <nd ref=\"2921555991\"/>\n    <nd ref=\"2921555992\"/>\n    <nd ref=\"2921555985\"/>\n    <tag k=\"building\" v=\"garages\"/>\n  </way>\n  <way id=\"288601752\" version=\"2\" timestamp=\"2016-12-05T19:45:05Z\" changeset=\"44190919\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2921555993\"/>\n    <nd ref=\"2921555994\"/>\n    <nd ref=\"2921555995\"/>\n    <nd ref=\"4540138275\"/>\n    <nd ref=\"2921555996\"/>\n    <nd ref=\"2921555993\"/>\n    <tag k=\"access\" v=\"private\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"288603132\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2921566821\"/>\n    <nd ref=\"2921566822\"/>\n    <nd ref=\"2921566823\"/>\n    <nd ref=\"7529995948\"/>\n    <nd ref=\"7529995950\"/>\n    <nd ref=\"7529995951\"/>\n    <nd ref=\"7529995952\"/>\n    <nd ref=\"2921566824\"/>\n    <nd ref=\"2921566821\"/>\n    <tag k=\"addr:city\" v=\"Espoo\"/>\n    <tag k=\"addr:housenumber\" v=\"5\"/>\n    <tag k=\"addr:postcode\" v=\"02720\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"C\"/>\n    <tag k=\"building\" v=\"residential\"/>\n  </way>\n  <way id=\"288603135\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2921566825\"/>\n    <nd ref=\"2921566826\"/>\n    <nd ref=\"2921566827\"/>\n    <nd ref=\"7529995943\"/>\n    <nd ref=\"7529995944\"/>\n    <nd ref=\"7529995945\"/>\n    <nd ref=\"7529995946\"/>\n    <nd ref=\"7529995947\"/>\n    <nd ref=\"2921566828\"/>\n    <nd ref=\"2921566825\"/>\n    <tag k=\"addr:city\" v=\"Espoo\"/>\n    <tag k=\"addr:housenumber\" v=\"5\"/>\n    <tag k=\"addr:postcode\" v=\"02720\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"B\"/>\n    <tag k=\"building\" v=\"residential\"/>\n  </way>\n  <way id=\"288603349\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2921568772\"/>\n    <nd ref=\"2921568773\"/>\n    <nd ref=\"7529995958\"/>\n    <nd ref=\"7529995957\"/>\n    <nd ref=\"7529995956\"/>\n    <nd ref=\"2921568774\"/>\n    <nd ref=\"2921568775\"/>\n    <nd ref=\"7529995955\"/>\n    <nd ref=\"7529995954\"/>\n    <nd ref=\"7529995953\"/>\n    <nd ref=\"2921568776\"/>\n    <nd ref=\"2921568777\"/>\n    <nd ref=\"2921568778\"/>\n    <nd ref=\"2921568779\"/>\n    <nd ref=\"2921568772\"/>\n    <tag k=\"addr:city\" v=\"Espoo\"/>\n    <tag k=\"addr:housenumber\" v=\"5\"/>\n    <tag k=\"addr:postcode\" v=\"02720\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"D\"/>\n    <tag k=\"building\" v=\"residential\"/>\n  </way>\n  <way id=\"288603350\" version=\"2\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2921568780\"/>\n    <nd ref=\"7529995942\"/>\n    <nd ref=\"7529995941\"/>\n    <nd ref=\"7529995940\"/>\n    <nd ref=\"7529995939\"/>\n    <nd ref=\"2921568781\"/>\n    <nd ref=\"2921568782\"/>\n    <nd ref=\"2921568783\"/>\n    <nd ref=\"2921568780\"/>\n    <tag k=\"addr:city\" v=\"Espoo\"/>\n    <tag k=\"addr:housenumber\" v=\"5\"/>\n    <tag k=\"addr:postcode\" v=\"02720\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"A\"/>\n    <tag k=\"building\" v=\"residential\"/>\n  </way>\n  <way id=\"288603510\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2921566081\"/>\n    <nd ref=\"2921566082\"/>\n    <nd ref=\"7448817954\"/>\n    <nd ref=\"7448817955\"/>\n    <nd ref=\"7448817956\"/>\n    <nd ref=\"2921566083\"/>\n    <nd ref=\"2921566084\"/>\n    <nd ref=\"7448817957\"/>\n    <nd ref=\"7448817958\"/>\n    <nd ref=\"7448817959\"/>\n    <nd ref=\"2921566085\"/>\n    <nd ref=\"2921566086\"/>\n    <nd ref=\"2921566087\"/>\n    <nd ref=\"2921566088\"/>\n    <nd ref=\"2921566081\"/>\n    <tag k=\"addr:city\" v=\"Espoo\"/>\n    <tag k=\"addr:housenumber\" v=\"4\"/>\n    <tag k=\"addr:postcode\" v=\"02720\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"A\"/>\n    <tag k=\"building\" v=\"residential\"/>\n  </way>\n  <way id=\"288603511\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"2921566089\"/>\n    <nd ref=\"7448817949\"/>\n    <nd ref=\"7448817950\"/>\n    <nd ref=\"7448817951\"/>\n    <nd ref=\"7448817952\"/>\n    <nd ref=\"7448817953\"/>\n    <nd ref=\"2921566090\"/>\n    <nd ref=\"2921566091\"/>\n    <nd ref=\"2921566092\"/>\n    <nd ref=\"2921566089\"/>\n    <tag k=\"addr:city\" v=\"Espoo\"/>\n    <tag k=\"addr:housenumber\" v=\"4\"/>\n    <tag k=\"addr:postcode\" v=\"02720\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"B\"/>\n    <tag k=\"building\" v=\"residential\"/>\n  </way>\n  <way id=\"322065439\" version=\"1\" timestamp=\"2015-01-14T19:29:41Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\">\n    <nd ref=\"3288222719\"/>\n    <nd ref=\"3288222733\"/>\n    <nd ref=\"3288222737\"/>\n    <nd ref=\"3288222724\"/>\n    <nd ref=\"3288222719\"/>\n    <tag k=\"addr:housenumber\" v=\"13\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"yes\"/>\n    <tag k=\"building:fi:id\" v=\"04906100540002003\"/>\n    <tag k=\"building:levels\" v=\"1\"/>\n    <tag k=\"start_date\" v=\"2006-11-07\"/>\n  </way>\n  <way id=\"322065714\" version=\"6\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\">\n    <nd ref=\"3288223799\"/>\n    <nd ref=\"5300314765\"/>\n    <nd ref=\"5300314767\"/>\n    <nd ref=\"3288223833\"/>\n    <nd ref=\"3288223852\"/>\n    <nd ref=\"3288223832\"/>\n    <nd ref=\"5300314769\"/>\n    <nd ref=\"5300314764\"/>\n    <nd ref=\"3288223799\"/>\n    <tag k=\"addr:housenumber\" v=\"2\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"kindergarten\"/>\n    <tag k=\"building:fi:id\" v=\"04906100540009001\"/>\n    <tag k=\"building:levels\" v=\"2\"/>\n    <tag k=\"building:use\" v=\"kindergarten\"/>\n    <tag k=\"start_date\" v=\"2016-12-20\"/>\n    <tag k=\"website\" v=\"https://www.espoo.fi/fi-FI/Kasvatus_ja_opetus/Varhaiskasvatus/Varhaiskasvatuksen_toimipaikat/Kunnalliset_paivakodit/Leppavaara/Nuumaen_paivakoti\"/>\n  </way>\n  <way id=\"322066294\" version=\"1\" timestamp=\"2015-01-14T19:30:41Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\">\n    <nd ref=\"3288223484\"/>\n    <nd ref=\"3288223542\"/>\n    <nd ref=\"3288223547\"/>\n    <nd ref=\"3288223489\"/>\n    <nd ref=\"3288223484\"/>\n    <tag k=\"addr:housenumber\" v=\"4\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"warehouse\"/>\n    <tag k=\"building:fi:id\" v=\"04906100540008004\"/>\n    <tag k=\"building:levels\" v=\"1\"/>\n    <tag k=\"building:use\" v=\"storage\"/>\n    <tag k=\"start_date\" v=\"1979\"/>\n  </way>\n  <way id=\"322066437\" version=\"1\" timestamp=\"2015-01-14T19:30:56Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\">\n    <nd ref=\"3288223628\"/>\n    <nd ref=\"3288223666\"/>\n    <nd ref=\"3288223669\"/>\n    <nd ref=\"3288223635\"/>\n    <nd ref=\"3288223628\"/>\n    <tag k=\"addr:housenumber\" v=\"5\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"yes\"/>\n    <tag k=\"building:fi:id\" v=\"04906100530001005\"/>\n    <tag k=\"building:levels\" v=\"2\"/>\n    <tag k=\"start_date\" v=\"1978\"/>\n  </way>\n  <way id=\"322066438\" version=\"2\" timestamp=\"2020-04-26T13:06:45Z\" changeset=\"84141602\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"3288223749\"/>\n    <nd ref=\"3288223762\"/>\n    <nd ref=\"3288223764\"/>\n    <nd ref=\"3288223751\"/>\n    <nd ref=\"3288223749\"/>\n    <tag k=\"addr:housenumber\" v=\"5\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"yes\"/>\n    <tag k=\"building:fi:id\" v=\"04906199070001001\"/>\n    <tag k=\"building:levels\" v=\"1\"/>\n    <tag k=\"building:use\" v=\"utility\"/>\n    <tag k=\"power\" v=\"substation\"/>\n    <tag k=\"ref\" v=\"717\"/>\n    <tag k=\"start_date\" v=\"1986\"/>\n    <tag k=\"substation\" v=\"minor_distribution\"/>\n  </way>\n  <way id=\"322066439\" version=\"1\" timestamp=\"2015-01-14T19:30:57Z\" changeset=\"28145903\" uid=\"2488523\" user=\"Hannesj_import\">\n    <nd ref=\"3288223532\"/>\n    <nd ref=\"3288223572\"/>\n    <nd ref=\"3288223586\"/>\n    <nd ref=\"3288223555\"/>\n    <nd ref=\"3288223532\"/>\n    <tag k=\"addr:housenumber\" v=\"5\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"yes\"/>\n    <tag k=\"building:fi:id\" v=\"04906100530001006\"/>\n    <tag k=\"building:levels\" v=\"1\"/>\n    <tag k=\"start_date\" v=\"1979\"/>\n  </way>\n  <way id=\"322066644\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"3288223342\"/>\n    <nd ref=\"3288223454\"/>\n    <nd ref=\"3288223492\"/>\n    <nd ref=\"3288223487\"/>\n    <nd ref=\"7530028803\"/>\n    <nd ref=\"3288223478\"/>\n    <nd ref=\"3288223465\"/>\n    <nd ref=\"7530028802\"/>\n    <nd ref=\"3288223453\"/>\n    <nd ref=\"3288223436\"/>\n    <nd ref=\"7530028801\"/>\n    <nd ref=\"3288223426\"/>\n    <nd ref=\"3288223409\"/>\n    <nd ref=\"7530028800\"/>\n    <nd ref=\"3288223390\"/>\n    <nd ref=\"3288223379\"/>\n    <nd ref=\"3288223342\"/>\n    <tag k=\"addr:housenumber\" v=\"7\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"addr:unit\" v=\"A\"/>\n    <tag k=\"building\" v=\"terrace\"/>\n    <tag k=\"building:fi:id\" v=\"04906100550002001\"/>\n    <tag k=\"building:levels\" v=\"2\"/>\n    <tag k=\"building:use\" v=\"residential\"/>\n    <tag k=\"start_date\" v=\"1978\"/>\n  </way>\n  <way id=\"322066645\" version=\"2\" timestamp=\"2018-06-28T03:28:20Z\" changeset=\"60233533\" uid=\"435380\" user=\"Samuli Pahaoja\">\n    <nd ref=\"3288223357\"/>\n    <nd ref=\"3288223393\"/>\n    <nd ref=\"3288223404\"/>\n    <nd ref=\"3288223363\"/>\n    <nd ref=\"3288223357\"/>\n    <tag k=\"addr:housenumber\" v=\"7\"/>\n    <tag k=\"addr:street\" v=\"Nuumäentie\"/>\n    <tag k=\"building\" v=\"shed\"/>\n    <tag k=\"building:fi:id\" v=\"04906100550002005\"/>\n    <tag k=\"building:levels\" v=\"1\"/>\n    <tag k=\"start_date\" v=\"1978\"/>\n  </way>\n  <way id=\"322066671\" version=\"2\" timestamp=\"2021-03-10T11:39:55Z\" changeset=\"100776121\" uid=\"8105430\" user=\"Larmax\">\n    <nd ref=\"3288222596\"/>\n    <nd ref=\"3288222611\"/>\n    <nd ref=\"3288222614\"/>\n    <nd ref=\"3288222599\"/>\n    <nd ref=\"3288222596\"/>\n    <tag k=\"building\" v=\"yes\"/>\n    <tag k=\"building:fi:id\" v=\"04906100570002003\"/>\n    <tag k=\"building:levels\" v=\"1\"/>\n    <tag k=\"start_date\" v=\"1988-07-04\"/>\n  </way>\n  <way id=\"425747971\" version=\"12\" timestamp=\"2025-02-12T12:29:41Z\" changeset=\"162428248\" uid=\"5159390\" user=\"tvenhola\">\n    <nd ref=\"1121516543\"/>\n    <nd ref=\"2898580206\"/>\n    <nd ref=\"1121609346\"/>\n    <nd ref=\"12582011417\"/>\n    <nd ref=\"4250551248\"/>\n    <nd ref=\"4250551273\"/>\n    <nd ref=\"4250551277\"/>\n    <nd ref=\"4250551254\"/>\n    <nd ref=\"4250551276\"/>\n    <nd ref=\"4250551250\"/>\n    <nd ref=\"4250551259\"/>\n    <nd ref=\"4250512212\"/>\n    <nd ref=\"12582011415\"/>\n    <nd ref=\"12582011416\"/>\n    <nd ref=\"2651860082\"/>\n    <nd ref=\"2651860091\"/>\n    <nd ref=\"4549302057\"/>\n    <nd ref=\"4549302056\"/>\n    <nd ref=\"4549302054\"/>\n    <nd ref=\"4250468337\"/>\n    <nd ref=\"4237124746\"/>\n    <nd ref=\"4237124754\"/>\n    <nd ref=\"4237124749\"/>\n    <nd ref=\"4549290869\"/>\n    <nd ref=\"4549290866\"/>\n    <nd ref=\"7577475547\"/>\n    <nd ref=\"4549290868\"/>\n    <nd ref=\"4237124753\"/>\n    <nd ref=\"4237124752\"/>\n    <nd ref=\"4237124747\"/>\n    <nd ref=\"4237124756\"/>\n    <nd ref=\"4237124751\"/>\n    <nd ref=\"4237124748\"/>\n    <nd ref=\"4549290847\"/>\n    <nd ref=\"12285606721\"/>\n    <nd ref=\"12285606718\"/>\n    <nd ref=\"12285606720\"/>\n    <nd ref=\"12285606719\"/>\n    <nd ref=\"4540138243\"/>\n    <nd ref=\"4250515043\"/>\n    <nd ref=\"4250512207\"/>\n    <nd ref=\"4250512213\"/>\n    <nd ref=\"4250512220\"/>\n    <nd ref=\"4250512222\"/>\n    <nd ref=\"4250512214\"/>\n    <nd ref=\"4250512223\"/>\n    <nd ref=\"4250512211\"/>\n    <nd ref=\"4250515024\"/>\n    <nd ref=\"4250512206\"/>\n    <nd ref=\"4250512215\"/>\n    <nd ref=\"4250512208\"/>\n    <nd ref=\"4250512209\"/>\n    <nd ref=\"4250512218\"/>\n    <nd ref=\"4250512217\"/>\n    <nd ref=\"4250512216\"/>\n    <nd ref=\"7556210639\"/>\n    <nd ref=\"7556210637\"/>\n    <nd ref=\"7556210634\"/>\n    <nd ref=\"7556210631\"/>\n    <nd ref=\"7556210628\"/>\n    <nd ref=\"1121611363\"/>\n    <nd ref=\"1121516543\"/>\n    <tag k=\"leisure\" v=\"park\"/>\n    <tag k=\"name\" v=\"Nuuniitty\"/>\n    <tag k=\"name:fi\" v=\"Nuuniitty\"/>\n    <tag k=\"name:sv\" v=\"Noängen\"/>\n  </way>\n  <way id=\"425748844\" version=\"13\" timestamp=\"2025-10-13T12:03:32Z\" changeset=\"173224130\" uid=\"20916114\" user=\"liljaadeliina\">\n    <nd ref=\"1121609721\"/>\n    <nd ref=\"4250512216\"/>\n    <nd ref=\"4250526548\"/>\n    <nd ref=\"4250526538\"/>\n    <nd ref=\"4250526529\"/>\n    <nd ref=\"4250526523\"/>\n    <nd ref=\"4250526498\"/>\n    <nd ref=\"4250526493\"/>\n    <nd ref=\"4250526491\"/>\n    <nd ref=\"4250526489\"/>\n    <nd ref=\"11901878574\"/>\n    <nd ref=\"4250526564\"/>\n    <nd ref=\"4250526579\"/>\n    <nd ref=\"4250526496\"/>\n    <nd ref=\"4250526563\"/>\n    <nd ref=\"4250526506\"/>\n    <nd ref=\"4250526495\"/>\n    <nd ref=\"4250525486\"/>\n    <nd ref=\"4250526521\"/>\n    <nd ref=\"4250526531\"/>\n    <nd ref=\"4250526555\"/>\n    <nd ref=\"4250526577\"/>\n    <nd ref=\"4250525487\"/>\n    <nd ref=\"4250526551\"/>\n    <nd ref=\"4250526537\"/>\n    <nd ref=\"4250526533\"/>\n    <nd ref=\"4250526561\"/>\n    <nd ref=\"4250526545\"/>\n    <nd ref=\"4250526562\"/>\n    <nd ref=\"4250526568\"/>\n    <nd ref=\"4250526569\"/>\n    <nd ref=\"4250526516\"/>\n    <nd ref=\"4250526494\"/>\n    <nd ref=\"4250526503\"/>\n    <nd ref=\"4250526511\"/>\n    <nd ref=\"4250526510\"/>\n    <nd ref=\"4250526554\"/>\n    <nd ref=\"4250526539\"/>\n    <nd ref=\"4250526553\"/>\n    <nd ref=\"4250526552\"/>\n    <nd ref=\"4250526504\"/>\n    <nd ref=\"4250526497\"/>\n    <nd ref=\"4250526499\"/>\n    <nd ref=\"4250526519\"/>\n    <nd ref=\"4250526518\"/>\n    <nd ref=\"4250526540\"/>\n    <nd ref=\"4250526574\"/>\n    <nd ref=\"4250526532\"/>\n    <nd ref=\"4250526515\"/>\n    <nd ref=\"4250526542\"/>\n    <nd ref=\"4250526581\"/>\n    <nd ref=\"4250526512\"/>\n    <nd ref=\"4250525485\"/>\n    <nd ref=\"4250526571\"/>\n    <nd ref=\"4250526524\"/>\n    <nd ref=\"4250526550\"/>\n    <nd ref=\"4250526535\"/>\n    <nd ref=\"4540196326\"/>\n    <nd ref=\"268688014\"/>\n    <nd ref=\"4250526557\"/>\n    <nd ref=\"4250526556\"/>\n    <nd ref=\"4250526558\"/>\n    <nd ref=\"4250526541\"/>\n    <nd ref=\"4250525488\"/>\n    <nd ref=\"4250526513\"/>\n    <nd ref=\"4250526507\"/>\n    <nd ref=\"4250526502\"/>\n    <nd ref=\"4250526527\"/>\n    <nd ref=\"4250526578\"/>\n    <nd ref=\"4250526526\"/>\n    <nd ref=\"4250526500\"/>\n    <nd ref=\"4250526522\"/>\n    <nd ref=\"4250526582\"/>\n    <nd ref=\"4250526501\"/>\n    <nd ref=\"4250526573\"/>\n    <nd ref=\"4250526572\"/>\n    <nd ref=\"4250526508\"/>\n    <nd ref=\"3288220411\"/>\n    <nd ref=\"3288220437\"/>\n    <nd ref=\"3288220436\"/>\n    <nd ref=\"3288220430\"/>\n    <nd ref=\"3288220438\"/>\n    <nd ref=\"3288220441\"/>\n    <nd ref=\"3288220447\"/>\n    <nd ref=\"3288220565\"/>\n    <nd ref=\"3288220567\"/>\n    <nd ref=\"3288220570\"/>\n    <nd ref=\"3288220573\"/>\n    <nd ref=\"3288220579\"/>\n    <nd ref=\"3288220574\"/>\n    <nd ref=\"3288220569\"/>\n    <nd ref=\"3288220460\"/>\n    <nd ref=\"3288220444\"/>\n    <nd ref=\"3288220431\"/>\n    <nd ref=\"3288220410\"/>\n    <nd ref=\"3288220399\"/>\n    <nd ref=\"3288220395\"/>\n    <nd ref=\"3288220374\"/>\n    <nd ref=\"3288220369\"/>\n    <nd ref=\"3288220146\"/>\n    <nd ref=\"4250526530\"/>\n    <nd ref=\"11572255107\"/>\n    <nd ref=\"11572255108\"/>\n    <nd ref=\"4250526514\"/>\n    <nd ref=\"3288219764\"/>\n    <nd ref=\"3288219763\"/>\n    <nd ref=\"3288219619\"/>\n    <nd ref=\"3288219621\"/>\n    <nd ref=\"12988984763\"/>\n    <nd ref=\"4250525483\"/>\n    <nd ref=\"12179685472\"/>\n    <nd ref=\"4250526575\"/>\n    <nd ref=\"4250526546\"/>\n    <nd ref=\"4250526565\"/>\n    <nd ref=\"4250526505\"/>\n    <nd ref=\"9312377388\"/>\n    <nd ref=\"567690632\"/>\n    <nd ref=\"12989039536\"/>\n    <nd ref=\"12989039538\"/>\n    <nd ref=\"9312377379\"/>\n    <nd ref=\"12989039537\"/>\n    <nd ref=\"12988984753\"/>\n    <nd ref=\"12988984752\"/>\n    <nd ref=\"4250525484\"/>\n    <nd ref=\"4250526492\"/>\n    <nd ref=\"4250525482\"/>\n    <nd ref=\"4250526560\"/>\n    <nd ref=\"4250526543\"/>\n    <nd ref=\"1121510959\"/>\n    <nd ref=\"13036945672\"/>\n    <nd ref=\"13036945673\"/>\n    <nd ref=\"13036945674\"/>\n    <nd ref=\"1121596206\"/>\n    <nd ref=\"13036945675\"/>\n    <nd ref=\"13036945676\"/>\n    <nd ref=\"13036945677\"/>\n    <nd ref=\"262795580\"/>\n    <nd ref=\"13036945685\"/>\n    <nd ref=\"13036945683\"/>\n    <nd ref=\"13036945684\"/>\n    <nd ref=\"13036945682\"/>\n    <nd ref=\"13036945681\"/>\n    <nd ref=\"25505494\"/>\n    <nd ref=\"12179728241\"/>\n    <nd ref=\"300505158\"/>\n    <nd ref=\"25505490\"/>\n    <nd ref=\"1121432576\"/>\n    <nd ref=\"1828984241\"/>\n    <nd ref=\"1121435959\"/>\n    <nd ref=\"1828984266\"/>\n    <nd ref=\"1121625893\"/>\n    <nd ref=\"1121448500\"/>\n    <nd ref=\"1828984273\"/>\n    <nd ref=\"1828984275\"/>\n    <nd ref=\"1121472529\"/>\n    <nd ref=\"1828984278\"/>\n    <nd ref=\"1121431155\"/>\n    <nd ref=\"1828984281\"/>\n    <nd ref=\"1121576944\"/>\n    <nd ref=\"1121480639\"/>\n    <nd ref=\"300502047\"/>\n    <nd ref=\"300502038\"/>\n    <nd ref=\"300502037\"/>\n    <nd ref=\"300502036\"/>\n    <nd ref=\"300502035\"/>\n    <nd ref=\"300502034\"/>\n    <nd ref=\"300502033\"/>\n    <nd ref=\"300502032\"/>\n    <nd ref=\"300502030\"/>\n    <nd ref=\"1828984285\"/>\n    <nd ref=\"1828984286\"/>\n    <nd ref=\"300505157\"/>\n    <nd ref=\"1121577546\"/>\n    <nd ref=\"1828984288\"/>\n    <nd ref=\"25505469\"/>\n    <nd ref=\"1121438880\"/>\n    <nd ref=\"1828984291\"/>\n    <nd ref=\"1828984292\"/>\n    <nd ref=\"1828984293\"/>\n    <nd ref=\"1828984294\"/>\n    <nd ref=\"300505156\"/>\n    <nd ref=\"4250551256\"/>\n    <nd ref=\"1828984297\"/>\n    <nd ref=\"1828984296\"/>\n    <nd ref=\"1121510927\"/>\n    <nd ref=\"1828984299\"/>\n    <nd ref=\"1828984300\"/>\n    <nd ref=\"25505467\"/>\n    <nd ref=\"1121465142\"/>\n    <nd ref=\"4250551247\"/>\n    <nd ref=\"1828984302\"/>\n    <nd ref=\"4250551257\"/>\n    <nd ref=\"25505465\"/>\n    <nd ref=\"1828984304\"/>\n    <nd ref=\"1828984305\"/>\n    <nd ref=\"1828984306\"/>\n    <nd ref=\"300505155\"/>\n    <nd ref=\"1828984307\"/>\n    <nd ref=\"300505154\"/>\n    <nd ref=\"25505464\"/>\n    <nd ref=\"300505153\"/>\n    <nd ref=\"1828984308\"/>\n    <nd ref=\"300505152\"/>\n    <nd ref=\"1828984310\"/>\n    <nd ref=\"25505463\"/>\n    <nd ref=\"1121431763\"/>\n    <nd ref=\"1828984317\"/>\n    <nd ref=\"1828984323\"/>\n    <nd ref=\"7556210595\"/>\n    <nd ref=\"1828984327\"/>\n    <nd ref=\"300505151\"/>\n    <nd ref=\"1121632921\"/>\n    <nd ref=\"1828984349\"/>\n    <nd ref=\"7556210592\"/>\n    <nd ref=\"1121563189\"/>\n    <nd ref=\"1121598961\"/>\n    <nd ref=\"7556210667\"/>\n    <nd ref=\"4250515016\"/>\n    <nd ref=\"300505149\"/>\n    <nd ref=\"1121609721\"/>\n    <tag k=\"landuse\" v=\"forest\"/>\n  </way>\n  <way id=\"426104083\" version=\"5\" timestamp=\"2025-07-18T16:55:38Z\" changeset=\"169117097\" uid=\"486606\" user=\"Barro\">\n    <nd ref=\"268725041\"/>\n    <nd ref=\"4254168965\"/>\n    <nd ref=\"4254168966\"/>\n    <nd ref=\"4254168967\"/>\n    <tag k=\"bicycle\" v=\"designated\"/>\n    <tag k=\"cycleway\" v=\"crossing\"/>\n    <tag k=\"foot\" v=\"designated\"/>\n    <tag k=\"highway\" v=\"cycleway\"/>\n    <tag k=\"lit\" v=\"yes\"/>\n    <tag k=\"segregated\" v=\"no\"/>\n    <tag k=\"surface\" v=\"asphalt\"/>\n  </way>\n  <way id=\"457957427\" version=\"2\" timestamp=\"2017-09-03T10:51:50Z\" changeset=\"51689801\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540138243\"/>\n    <nd ref=\"4250515043\"/>\n    <nd ref=\"4250512207\"/>\n    <nd ref=\"4250512213\"/>\n    <nd ref=\"4250512220\"/>\n    <nd ref=\"4250512222\"/>\n    <nd ref=\"4250512214\"/>\n    <nd ref=\"4250512223\"/>\n    <nd ref=\"4250512211\"/>\n    <nd ref=\"4250515024\"/>\n    <nd ref=\"4250512206\"/>\n    <nd ref=\"4250512215\"/>\n    <nd ref=\"4250512208\"/>\n    <nd ref=\"5076856664\"/>\n    <nd ref=\"4540138244\"/>\n    <nd ref=\"4540138245\"/>\n    <nd ref=\"4540138246\"/>\n    <nd ref=\"4540138247\"/>\n    <nd ref=\"4540138248\"/>\n    <nd ref=\"4540138249\"/>\n    <nd ref=\"4540138250\"/>\n    <nd ref=\"4540138251\"/>\n    <nd ref=\"4540138243\"/>\n    <tag k=\"landuse\" v=\"residential\"/>\n  </way>\n  <way id=\"457957434\" version=\"4\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540138274\"/>\n    <nd ref=\"5076856677\"/>\n    <nd ref=\"4540138275\"/>\n    <nd ref=\"4540138276\"/>\n    <nd ref=\"7448840431\"/>\n    <nd ref=\"2921555993\"/>\n    <nd ref=\"4540138277\"/>\n    <nd ref=\"7448840427\"/>\n    <nd ref=\"7448840425\"/>\n    <nd ref=\"4540138278\"/>\n    <nd ref=\"7448840423\"/>\n    <nd ref=\"4540138279\"/>\n    <nd ref=\"6139126749\"/>\n    <nd ref=\"7448840422\"/>\n    <nd ref=\"4540138280\"/>\n    <nd ref=\"4540138281\"/>\n    <nd ref=\"7448840411\"/>\n    <nd ref=\"7448840412\"/>\n    <nd ref=\"7448840413\"/>\n    <nd ref=\"4540138282\"/>\n    <nd ref=\"4540138283\"/>\n    <nd ref=\"4540138284\"/>\n    <nd ref=\"6139126753\"/>\n    <nd ref=\"4540138285\"/>\n    <nd ref=\"5076856674\"/>\n    <nd ref=\"4540138286\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"457958709\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152720\"/>\n    <nd ref=\"4540152726\"/>\n    <nd ref=\"4540152721\"/>\n    <nd ref=\"4540152722\"/>\n    <nd ref=\"4540152723\"/>\n    <nd ref=\"4540152724\"/>\n    <nd ref=\"4540152720\"/>\n    <tag k=\"access\" v=\"private\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"457958710\" version=\"3\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152725\"/>\n    <nd ref=\"5076856657\"/>\n    <nd ref=\"5076867288\"/>\n    <nd ref=\"4540152726\"/>\n    <nd ref=\"4540152727\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"service\" v=\"parking_aisle\"/>\n  </way>\n  <way id=\"457958711\" version=\"1\" timestamp=\"2016-12-05T19:53:50Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152728\"/>\n    <nd ref=\"4540152729\"/>\n    <nd ref=\"4540152730\"/>\n    <nd ref=\"4540152737\"/>\n    <nd ref=\"4540152731\"/>\n    <nd ref=\"4540152733\"/>\n    <nd ref=\"4540152728\"/>\n    <tag k=\"access\" v=\"private\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"457958712\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152732\"/>\n    <nd ref=\"4540152733\"/>\n    <nd ref=\"4540152734\"/>\n    <nd ref=\"4540152736\"/>\n    <nd ref=\"4540152735\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"457958713\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152736\"/>\n    <nd ref=\"4540152737\"/>\n    <nd ref=\"4540152738\"/>\n    <nd ref=\"4540152739\"/>\n    <nd ref=\"4540152740\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"457958714\" version=\"3\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"268728011\"/>\n    <nd ref=\"5113592090\"/>\n    <nd ref=\"4540152741\"/>\n    <nd ref=\"4540152742\"/>\n    <nd ref=\"7448817965\"/>\n    <nd ref=\"7448817966\"/>\n    <nd ref=\"7448817969\"/>\n    <nd ref=\"7448817972\"/>\n    <nd ref=\"4540152743\"/>\n    <nd ref=\"4540152744\"/>\n    <nd ref=\"4540152745\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"457958715\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152746\"/>\n    <nd ref=\"4540152747\"/>\n    <nd ref=\"4540152748\"/>\n    <nd ref=\"4540152749\"/>\n    <nd ref=\"4540152746\"/>\n    <tag k=\"access\" v=\"private\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"457958716\" version=\"2\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152737\"/>\n    <nd ref=\"7448817945\"/>\n    <nd ref=\"4540152743\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"457958717\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152750\"/>\n    <nd ref=\"4540152758\"/>\n    <nd ref=\"4540152751\"/>\n    <nd ref=\"4540152756\"/>\n    <nd ref=\"4540152752\"/>\n    <nd ref=\"4540152753\"/>\n    <nd ref=\"4540152750\"/>\n    <tag k=\"access\" v=\"private\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"457958718\" version=\"4\" timestamp=\"2020-05-17T20:42:45Z\" changeset=\"85348252\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152754\"/>\n    <nd ref=\"5076856658\"/>\n    <nd ref=\"4540152755\"/>\n    <nd ref=\"5076867301\"/>\n    <nd ref=\"4540152756\"/>\n    <nd ref=\"4540152757\"/>\n    <nd ref=\"4540152758\"/>\n    <nd ref=\"4540152759\"/>\n    <nd ref=\"4540152760\"/>\n    <nd ref=\"4540152761\"/>\n    <nd ref=\"4540152762\"/>\n    <nd ref=\"4540152763\"/>\n    <nd ref=\"4540152764\"/>\n    <nd ref=\"4540152765\"/>\n    <nd ref=\"4540152766\"/>\n    <nd ref=\"4540152767\"/>\n    <nd ref=\"7529995949\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"457958719\" version=\"2\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\">\n    <nd ref=\"1539655844\"/>\n    <nd ref=\"12285606744\"/>\n    <nd ref=\"1539655816\"/>\n    <nd ref=\"4540152788\"/>\n    <nd ref=\"4540152768\"/>\n    <nd ref=\"4540152787\"/>\n    <nd ref=\"4540152782\"/>\n    <nd ref=\"4540152769\"/>\n    <nd ref=\"4540152770\"/>\n    <nd ref=\"4540152771\"/>\n    <nd ref=\"4540152772\"/>\n    <nd ref=\"4540152773\"/>\n    <nd ref=\"4540152774\"/>\n    <nd ref=\"4540152775\"/>\n    <nd ref=\"4540152776\"/>\n    <nd ref=\"4540152780\"/>\n    <nd ref=\"4540152777\"/>\n    <nd ref=\"4540152778\"/>\n    <nd ref=\"4540152779\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"457958720\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152780\"/>\n    <nd ref=\"4540152781\"/>\n    <nd ref=\"4540152782\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"457958721\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152783\"/>\n    <nd ref=\"4540152784\"/>\n    <nd ref=\"4540152788\"/>\n    <nd ref=\"4540152785\"/>\n    <nd ref=\"4540152787\"/>\n    <nd ref=\"4540152786\"/>\n    <nd ref=\"4540152783\"/>\n    <tag k=\"access\" v=\"private\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"457958722\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152768\"/>\n    <nd ref=\"4540152889\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"service\" v=\"parking_aisle\"/>\n  </way>\n  <way id=\"457958723\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"1539655816\"/>\n    <nd ref=\"1539655691\"/>\n    <tag k=\"foot\" v=\"yes\"/>\n    <tag k=\"highway\" v=\"cycleway\"/>\n    <tag k=\"source\" v=\"survey\"/>\n  </way>\n  <way id=\"457958724\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152890\"/>\n    <nd ref=\"4540152891\"/>\n    <nd ref=\"4540152892\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"service\" v=\"driveway\"/>\n  </way>\n  <way id=\"457958725\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152893\"/>\n    <nd ref=\"4540152894\"/>\n    <nd ref=\"4540152895\"/>\n    <nd ref=\"4540152896\"/>\n    <nd ref=\"4540152897\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"service\" v=\"driveway\"/>\n  </way>\n  <way id=\"457958726\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"276457280\"/>\n    <nd ref=\"4540152898\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"service\" v=\"driveway\"/>\n  </way>\n  <way id=\"457958727\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152899\"/>\n    <nd ref=\"4540152900\"/>\n    <nd ref=\"4540152901\"/>\n    <nd ref=\"4540152902\"/>\n    <nd ref=\"4540152899\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"457958728\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"276457280\"/>\n    <nd ref=\"4540152903\"/>\n    <nd ref=\"4540152904\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"service\" v=\"driveway\"/>\n  </way>\n  <way id=\"457958729\" version=\"1\" timestamp=\"2016-12-05T19:53:51Z\" changeset=\"44191108\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540152903\"/>\n    <nd ref=\"4540152905\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"service\" v=\"driveway\"/>\n  </way>\n  <way id=\"457958730\" version=\"3\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\">\n    <nd ref=\"4540152906\"/>\n    <nd ref=\"4540152907\"/>\n    <nd ref=\"4540152908\"/>\n    <nd ref=\"4540152909\"/>\n    <nd ref=\"4250512209\"/>\n    <nd ref=\"4250512218\"/>\n    <nd ref=\"4250512217\"/>\n    <nd ref=\"4250512216\"/>\n    <nd ref=\"4250526548\"/>\n    <nd ref=\"4250526538\"/>\n    <nd ref=\"4250526529\"/>\n    <nd ref=\"4250526523\"/>\n    <nd ref=\"4250526498\"/>\n    <nd ref=\"4250526493\"/>\n    <nd ref=\"4250526491\"/>\n    <nd ref=\"4250526489\"/>\n    <nd ref=\"11901878574\"/>\n    <nd ref=\"4250526564\"/>\n    <nd ref=\"4250526579\"/>\n    <nd ref=\"4250526496\"/>\n    <nd ref=\"4250526563\"/>\n    <nd ref=\"4250526506\"/>\n    <nd ref=\"4250526495\"/>\n    <nd ref=\"4250525486\"/>\n    <nd ref=\"4250526521\"/>\n    <nd ref=\"4250526531\"/>\n    <nd ref=\"4250526555\"/>\n    <nd ref=\"4250526577\"/>\n    <nd ref=\"4250525487\"/>\n    <nd ref=\"4250526551\"/>\n    <nd ref=\"4250526537\"/>\n    <nd ref=\"4250526533\"/>\n    <nd ref=\"4540152910\"/>\n    <nd ref=\"4540152911\"/>\n    <nd ref=\"4540152912\"/>\n    <nd ref=\"4540152913\"/>\n    <nd ref=\"4540152914\"/>\n    <nd ref=\"4540152906\"/>\n    <tag k=\"landuse\" v=\"residential\"/>\n  </way>\n  <way id=\"457959169\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540156189\"/>\n    <nd ref=\"4540156191\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"service\" v=\"parking_aisle\"/>\n  </way>\n  <way id=\"457959170\" version=\"1\" timestamp=\"2016-12-05T19:57:50Z\" changeset=\"44191184\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"4540156192\"/>\n    <nd ref=\"4540156193\"/>\n    <nd ref=\"4540156194\"/>\n    <nd ref=\"4540156195\"/>\n    <nd ref=\"4540156192\"/>\n    <tag k=\"access\" v=\"private\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"457959176\" version=\"2\" timestamp=\"2021-03-10T11:39:55Z\" changeset=\"100776121\" uid=\"8105430\" user=\"Larmax\">\n    <nd ref=\"4250526561\"/>\n    <nd ref=\"4250526545\"/>\n    <nd ref=\"4250526562\"/>\n    <nd ref=\"4250526568\"/>\n    <nd ref=\"4250526569\"/>\n    <nd ref=\"4250526516\"/>\n    <nd ref=\"4250526494\"/>\n    <nd ref=\"4540156205\"/>\n    <nd ref=\"4540156206\"/>\n    <nd ref=\"4540156207\"/>\n    <nd ref=\"8503461537\"/>\n    <nd ref=\"4540156208\"/>\n    <nd ref=\"4540156209\"/>\n    <nd ref=\"4540156210\"/>\n    <nd ref=\"4250526561\"/>\n    <tag k=\"landuse\" v=\"residential\"/>\n  </way>\n  <way id=\"457959177\" version=\"3\" timestamp=\"2021-07-13T07:14:46Z\" changeset=\"107884960\" uid=\"10590172\" user=\"marsusram\">\n    <nd ref=\"4540156211\"/>\n    <nd ref=\"4540156212\"/>\n    <nd ref=\"4540156213\"/>\n    <nd ref=\"4540156214\"/>\n    <nd ref=\"4540156215\"/>\n    <nd ref=\"4540156216\"/>\n    <nd ref=\"4540156217\"/>\n    <nd ref=\"4540156218\"/>\n    <nd ref=\"8915699144\"/>\n    <nd ref=\"4540156219\"/>\n    <nd ref=\"4540156222\"/>\n    <nd ref=\"4540156223\"/>\n    <nd ref=\"4540156224\"/>\n    <nd ref=\"4540156225\"/>\n    <nd ref=\"5011574085\"/>\n    <nd ref=\"4540156211\"/>\n    <tag k=\"landuse\" v=\"residential\"/>\n  </way>\n  <way id=\"512565825\" version=\"1\" timestamp=\"2017-08-03T11:34:13Z\" changeset=\"50805854\" uid=\"5159390\" user=\"tvenhola\">\n    <nd ref=\"4250526561\"/>\n    <nd ref=\"4540156210\"/>\n    <nd ref=\"5011574085\"/>\n    <nd ref=\"4540156211\"/>\n    <nd ref=\"4540156212\"/>\n    <nd ref=\"4540156213\"/>\n    <nd ref=\"4540156214\"/>\n    <nd ref=\"4540156215\"/>\n    <nd ref=\"4540152912\"/>\n    <nd ref=\"4540152911\"/>\n    <nd ref=\"4540152910\"/>\n    <nd ref=\"4250526533\"/>\n    <nd ref=\"4250526561\"/>\n    <tag k=\"landuse\" v=\"forest\"/>\n  </way>\n  <way id=\"512565826\" version=\"4\" timestamp=\"2025-11-19T10:13:54Z\" changeset=\"174851696\" uid=\"20916114\" user=\"liljaadeliina\">\n    <nd ref=\"4540152906\"/>\n    <nd ref=\"6139123153\"/>\n    <nd ref=\"6139123152\"/>\n    <nd ref=\"12741249985\"/>\n    <nd ref=\"5011574086\"/>\n    <nd ref=\"5011574087\"/>\n    <nd ref=\"5011574088\"/>\n    <nd ref=\"5011574089\"/>\n    <nd ref=\"4540152913\"/>\n    <nd ref=\"4540152914\"/>\n    <nd ref=\"4540152906\"/>\n    <tag k=\"amenity\" v=\"kindergarten\"/>\n    <tag k=\"education\" v=\"kindergarten\"/>\n    <tag k=\"name\" v=\"Nuumäen päiväkotiv\"/>\n    <tag k=\"name:fi\" v=\"Nuumäen päiväkoti\"/>\n  </way>\n  <way id=\"520982743\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"5076867288\"/>\n    <nd ref=\"7530028799\"/>\n    <nd ref=\"5076867289\"/>\n    <nd ref=\"5076867290\"/>\n    <nd ref=\"5076867291\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"520982744\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7530028798\"/>\n    <nd ref=\"5076867292\"/>\n    <nd ref=\"5076867293\"/>\n    <nd ref=\"5076867298\"/>\n    <nd ref=\"5076867300\"/>\n    <nd ref=\"5076867294\"/>\n    <nd ref=\"4540152727\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"520982745\" version=\"2\" timestamp=\"2020-05-17T20:47:58Z\" changeset=\"85348382\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"5076867295\"/>\n    <nd ref=\"7530028797\"/>\n    <nd ref=\"5076867296\"/>\n    <nd ref=\"5076867297\"/>\n    <nd ref=\"5076867300\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"520982746\" version=\"1\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"5076867299\"/>\n    <nd ref=\"5076867294\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"520982747\" version=\"1\" timestamp=\"2017-09-03T10:57:22Z\" changeset=\"51689910\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"5076867301\"/>\n    <nd ref=\"5076867302\"/>\n    <nd ref=\"5076867303\"/>\n    <nd ref=\"5076867304\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"525693383\" version=\"5\" timestamp=\"2025-08-28T20:30:58Z\" changeset=\"171154193\" uid=\"742733\" user=\"perusluku\">\n    <nd ref=\"1538139031\"/>\n    <nd ref=\"5113592086\"/>\n    <nd ref=\"6139123142\"/>\n    <nd ref=\"5113592087\"/>\n    <nd ref=\"5113592088\"/>\n    <nd ref=\"5113592089\"/>\n    <nd ref=\"13104452417\"/>\n    <nd ref=\"12285606749\"/>\n    <nd ref=\"5113592090\"/>\n    <tag k=\"footway\" v=\"sidewalk\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n    <tag k=\"surface\" v=\"asphalt\"/>\n  </way>\n  <way id=\"655212464\" version=\"2\" timestamp=\"2025-11-02T16:47:48Z\" changeset=\"174112060\" uid=\"10617839\" user=\"rssto\">\n    <nd ref=\"6139123137\"/>\n    <nd ref=\"6139123138\"/>\n    <nd ref=\"6139123139\"/>\n    <nd ref=\"6139123140\"/>\n    <nd ref=\"6139123137\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"655212465\" version=\"2\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\">\n    <nd ref=\"6139123141\"/>\n    <nd ref=\"6139123142\"/>\n    <nd ref=\"6139123143\"/>\n    <nd ref=\"12741249986\"/>\n    <tag k=\"highway\" v=\"service\"/>\n  </way>\n  <way id=\"655212466\" version=\"4\" timestamp=\"2025-11-02T16:48:33Z\" changeset=\"174112474\" uid=\"10617839\" user=\"rssto\">\n    <nd ref=\"6139123144\"/>\n    <nd ref=\"6139123145\"/>\n    <nd ref=\"6139123146\"/>\n    <nd ref=\"6139123149\"/>\n    <nd ref=\"6139123150\"/>\n    <nd ref=\"5011574086\"/>\n    <nd ref=\"12741249985\"/>\n    <nd ref=\"6139123152\"/>\n    <nd ref=\"6139123153\"/>\n    <nd ref=\"6139123144\"/>\n    <tag k=\"access\" v=\"customers\"/>\n    <tag k=\"amenity\" v=\"parking\"/>\n    <tag k=\"fee\" v=\"no\"/>\n    <tag k=\"parking\" v=\"surface\"/>\n  </way>\n  <way id=\"655212467\" version=\"3\" timestamp=\"2025-11-02T16:48:37Z\" changeset=\"174112038\" uid=\"10617839\" user=\"rssto\">\n    <nd ref=\"1539656211\"/>\n    <nd ref=\"5113592089\"/>\n    <nd ref=\"6139123154\"/>\n    <nd ref=\"6139123155\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"surface\" v=\"asphalt\"/>\n  </way>\n  <way id=\"655212919\" version=\"2\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"6139126877\"/>\n    <nd ref=\"7448840433\"/>\n    <nd ref=\"7448840432\"/>\n    <nd ref=\"6139126878\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796466272\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448817945\"/>\n    <nd ref=\"7448817946\"/>\n    <nd ref=\"7448817947\"/>\n    <nd ref=\"7448817948\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796466273\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448817954\"/>\n    <nd ref=\"7448817960\"/>\n    <nd ref=\"4540152741\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796466274\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448817955\"/>\n    <nd ref=\"7448817961\"/>\n    <nd ref=\"7448817962\"/>\n    <nd ref=\"4540152742\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796466275\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448817956\"/>\n    <nd ref=\"7448817963\"/>\n    <nd ref=\"7448817964\"/>\n    <nd ref=\"7448817965\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796466276\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448817957\"/>\n    <nd ref=\"7448817966\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796466277\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448817958\"/>\n    <nd ref=\"7448817967\"/>\n    <nd ref=\"7448817968\"/>\n    <nd ref=\"7448817969\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796466278\" version=\"1\" timestamp=\"2020-04-26T13:21:04Z\" changeset=\"84142123\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448817959\"/>\n    <nd ref=\"7448817970\"/>\n    <nd ref=\"7448817971\"/>\n    <nd ref=\"7448817972\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796466321\" version=\"1\" timestamp=\"2020-04-26T13:21:40Z\" changeset=\"84142141\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"268724899\"/>\n    <nd ref=\"7448755159\"/>\n    <tag k=\"highway\" v=\"service\"/>\n    <tag k=\"service\" v=\"driveway\"/>\n  </way>\n  <way id=\"796469204\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448840426\"/>\n    <nd ref=\"7448840427\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796469205\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448840428\"/>\n    <nd ref=\"2921555993\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"796469206\" version=\"1\" timestamp=\"2020-04-26T13:27:12Z\" changeset=\"84142369\" uid=\"1484179\" user=\"oiva\">\n    <nd ref=\"7448840429\"/>\n    <nd ref=\"7448840430\"/>\n    <nd ref=\"7448840431\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n  </way>\n  <way id=\"1282562992\" version=\"1\" timestamp=\"2024-05-13T18:49:48Z\" changeset=\"151281332\" uid=\"336756\" user=\"nolla\">\n    <nd ref=\"385345247\"/>\n    <nd ref=\"11901877265\"/>\n    <nd ref=\"11901877266\"/>\n    <nd ref=\"11901877267\"/>\n    <nd ref=\"11901877268\"/>\n    <nd ref=\"11901878569\"/>\n    <nd ref=\"11901878570\"/>\n    <nd ref=\"11901878571\"/>\n    <nd ref=\"11901878572\"/>\n    <nd ref=\"11901878573\"/>\n    <nd ref=\"11901878574\"/>\n    <nd ref=\"11901878575\"/>\n    <nd ref=\"11901878576\"/>\n    <nd ref=\"11901878577\"/>\n    <nd ref=\"11901878578\"/>\n    <tag k=\"highway\" v=\"path\"/>\n    <tag k=\"mtb:scale\" v=\"1\"/>\n    <tag k=\"surface\" v=\"unpaved\"/>\n    <tag k=\"width\" v=\"0.5\"/>\n  </way>\n  <way id=\"1327887801\" version=\"1\" timestamp=\"2024-10-24T15:03:51Z\" changeset=\"158301288\" uid=\"5258545\" user=\"j numminen\">\n    <nd ref=\"12285606746\"/>\n    <nd ref=\"12285606748\"/>\n    <nd ref=\"12285606749\"/>\n    <tag k=\"footway\" v=\"crossing\"/>\n    <tag k=\"highway\" v=\"footway\"/>\n    <tag k=\"surface\" v=\"paved\"/>\n  </way>\n  <way id=\"1355154126\" version=\"1\" timestamp=\"2025-01-29T12:06:50Z\" changeset=\"161894546\" uid=\"20916114\" user=\"liljaadeliina\">\n    <nd ref=\"5076867299\"/>\n    <nd ref=\"11901878576\"/>\n    <tag k=\"highway\" v=\"path\"/>\n  </way>\n  <way id=\"1376096430\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\">\n    <nd ref=\"12741249969\"/>\n    <nd ref=\"12741249970\"/>\n    <nd ref=\"12741249971\"/>\n    <nd ref=\"12741249972\"/>\n    <nd ref=\"12741249969\"/>\n    <tag k=\"building\" v=\"yes\"/>\n  </way>\n  <way id=\"1376096431\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\">\n    <nd ref=\"12741249973\"/>\n    <nd ref=\"12741249974\"/>\n    <nd ref=\"12741249975\"/>\n    <nd ref=\"12741249976\"/>\n    <nd ref=\"12741249973\"/>\n    <tag k=\"building\" v=\"yes\"/>\n  </way>\n  <way id=\"1376096432\" version=\"1\" timestamp=\"2025-04-09T11:30:04Z\" changeset=\"164719828\" uid=\"20916114\" user=\"liljaadeliina\">\n    <nd ref=\"12741249977\"/>\n    <nd ref=\"12741249978\"/>\n    <nd ref=\"12741249979\"/>\n    <nd ref=\"12741249980\"/>\n    <nd ref=\"12741249977\"/>\n    <tag k=\"building\" v=\"yes\"/>\n  </way>\n  <relation id=\"1393843\" version=\"4\" timestamp=\"2025-07-30T09:48:50Z\" changeset=\"169710901\" uid=\"20916114\" user=\"liljaadeliina\">\n    <member type=\"way\" ref=\"96810463\" role=\"outer\"/>\n    <member type=\"way\" ref=\"1298610242\" role=\"outer\"/>\n    <tag k=\"boundary\" v=\"protected_area\"/>\n    <tag k=\"leisure\" v=\"nature_reserve\"/>\n    <tag k=\"name\" v=\"Lippajärven luonnonsuojelualue\"/>\n    <tag k=\"opening_hours\" v=\"24/7\"/>\n    <tag k=\"operator\" v=\"Espoon kaupunki\"/>\n    <tag k=\"operator:type\" v=\"government\"/>\n    <tag k=\"operator:wikidata\" v=\"Q47034\"/>\n    <tag k=\"protect_id\" v=\"19\"/>\n    <tag k=\"protection_title\" v=\"Yksityiset suojelualueet\"/>\n    <tag k=\"type\" v=\"multipolygon\"/>\n  </relation>\n  <relation id=\"1865236\" version=\"46\" timestamp=\"2025-05-26T06:17:49Z\" changeset=\"166760957\" uid=\"5258545\" user=\"j numminen\">\n    <member type=\"way\" ref=\"4603649\" role=\"forward\"/>\n    <member type=\"way\" ref=\"81410195\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1187420093\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1187420092\" role=\"forward\"/>\n    <member type=\"way\" ref=\"23123825\" role=\"forward\"/>\n    <member type=\"way\" ref=\"171940215\" role=\"forward\"/>\n    <member type=\"way\" ref=\"42772687\" role=\"forward\"/>\n    <member type=\"way\" ref=\"171940224\" role=\"forward\"/>\n    <member type=\"way\" ref=\"4603651\" role=\"forward\"/>\n    <member type=\"way\" ref=\"24598080\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1287292087\" role=\"forward\"/>\n    <member type=\"way\" ref=\"171940206\" role=\"forward\"/>\n    <member type=\"way\" ref=\"33704322\" role=\"forward\"/>\n    <member type=\"way\" ref=\"24598088\" role=\"forward\"/>\n    <member type=\"way\" ref=\"130713063\" role=\"forward\"/>\n    <member type=\"way\" ref=\"138483059\" role=\"forward\"/>\n    <member type=\"way\" ref=\"130340906\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140383815\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1194663436\" role=\"forward\"/>\n    <member type=\"way\" ref=\"970098668\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1194663439\" role=\"forward\"/>\n    <member type=\"way\" ref=\"424274558\" role=\"forward\"/>\n    <member type=\"way\" ref=\"920265971\" role=\"backward\"/>\n    <member type=\"way\" ref=\"426618722\" role=\"backward\"/>\n    <member type=\"way\" ref=\"140383770\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140383802\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140383819\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140383835\" role=\"backward\"/>\n    <member type=\"way\" ref=\"140383787\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140383778\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140383767\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140542860\" role=\"backward\"/>\n    <member type=\"way\" ref=\"140542868\" role=\"forward\"/>\n    <member type=\"way\" ref=\"587513738\" role=\"forward\"/>\n    <member type=\"way\" ref=\"587513737\" role=\"forward\"/>\n    <member type=\"way\" ref=\"675202186\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140543003\" role=\"forward\"/>\n    <member type=\"way\" ref=\"675202187\" role=\"forward\"/>\n    <member type=\"way\" ref=\"587513736\" role=\"forward\"/>\n    <member type=\"way\" ref=\"22981225\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140542871\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140542843\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140542864\" role=\"backward\"/>\n    <member type=\"way\" ref=\"140542847\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140542858\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140542841\" role=\"forward\"/>\n    <member type=\"way\" ref=\"426284036\" role=\"backward\"/>\n    <member type=\"way\" ref=\"426200288\" role=\"backward\"/>\n    <member type=\"way\" ref=\"140234586\" role=\"backward\"/>\n    <member type=\"way\" ref=\"140234576\" role=\"forward\"/>\n    <member type=\"way\" ref=\"23284897\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140234583\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1291880040\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1277653824\" role=\"forward\"/>\n    <member type=\"way\" ref=\"23284898\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1375258638\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1389718425\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1291880039\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140238863\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140238874\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140236956\" role=\"backward\"/>\n    <member type=\"way\" ref=\"317955957\" role=\"backward\"/>\n    <member type=\"way\" ref=\"317955963\" role=\"backward\"/>\n    <member type=\"way\" ref=\"317955961\" role=\"backward\"/>\n    <member type=\"way\" ref=\"317955960\" role=\"backward\"/>\n    <member type=\"way\" ref=\"317955964\" role=\"backward\"/>\n    <member type=\"way\" ref=\"140236951\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140236946\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140236949\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140236965\" role=\"forward\"/>\n    <member type=\"way\" ref=\"140244131\" role=\"backward\"/>\n    <member type=\"way\" ref=\"140244135\" role=\"forward\"/>\n    <member type=\"way\" ref=\"675643796\" role=\"forward\"/>\n    <member type=\"way\" ref=\"4297979\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1190383233\" role=\"forward\"/>\n    <member type=\"way\" ref=\"35780798\" role=\"forward\"/>\n    <member type=\"way\" ref=\"35778369\" role=\"forward\"/>\n    <member type=\"way\" ref=\"5367923\" role=\"forward\"/>\n    <member type=\"way\" ref=\"245443271\" role=\"forward\"/>\n    <member type=\"way\" ref=\"42833597\" role=\"forward\"/>\n    <member type=\"way\" ref=\"784595993\" role=\"forward\"/>\n    <member type=\"way\" ref=\"784595985\" role=\"forward\"/>\n    <member type=\"way\" ref=\"35778355\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1190763596\" role=\"forward\"/>\n    <member type=\"way\" ref=\"35778631\" role=\"forward\"/>\n    <member type=\"way\" ref=\"35780807\" role=\"forward\"/>\n    <member type=\"way\" ref=\"674486885\" role=\"forward\"/>\n    <member type=\"way\" ref=\"675789780\" role=\"forward\"/>\n    <member type=\"way\" ref=\"5368856\" role=\"forward\"/>\n    <member type=\"way\" ref=\"673453969\" role=\"forward\"/>\n    <member type=\"way\" ref=\"1263181142\" role=\"forward\"/>\n    <member type=\"way\" ref=\"586652862\" role=\"forward\"/>\n    <member type=\"way\" ref=\"35779492\" role=\"forward\"/>\n    <member type=\"way\" ref=\"35779494\" role=\"backward\"/>\n    <member type=\"way\" ref=\"35779495\" role=\"forward\"/>\n    <member type=\"way\" ref=\"156515002\" role=\"forward\"/>\n    <tag k=\"network\" v=\"Espoo\"/>\n    <tag k=\"operator\" v=\"HSL\"/>\n    <tag k=\"ref\" v=\"20\"/>\n    <tag k=\"type\" v=\"was:route\"/>\n    <tag k=\"was:route\" v=\"bus\"/>\n  </relation>\n  <relation id=\"1866146\" version=\"44\" timestamp=\"2025-08-04T18:01:33Z\" changeset=\"169969863\" uid=\"5258545\" user=\"j numminen\">\n    <member type=\"node\" ref=\"249558270\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4810166310\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921214\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4810166314\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921213\" role=\"stop\"/>\n    <member type=\"node\" ref=\"711445602\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921212\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4810166311\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238785\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477952585\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340234784\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477957886\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6323310490\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477969901\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6323310489\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477974878\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921210\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681212\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776547\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681203\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1736975678\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397899\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776546\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397870\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776545\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268725204\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480843\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607989339\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1538138970\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268724954\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480842\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268728776\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480841\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607995446\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480840\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607996663\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6326926808\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607998796\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1536455200\" role=\"stop\"/>\n    <member type=\"node\" ref=\"59871347\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238295\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5217833121\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238297\" role=\"stop\"/>\n    <member type=\"node\" ref=\"498462819\" role=\"platform\"/>\n    <member type=\"node\" ref=\"282280287\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518655407\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311236983\" role=\"stop\"/>\n    <member type=\"node\" ref=\"38458038\" role=\"platform\"/>\n    <member type=\"node\" ref=\"418340755\" role=\"stop\"/>\n    <member type=\"node\" ref=\"418340775\" role=\"platform\"/>\n    <member type=\"way\" ref=\"138587885\" role=\"\"/>\n    <member type=\"way\" ref=\"4336514\" role=\"\"/>\n    <member type=\"way\" ref=\"4336496\" role=\"\"/>\n    <member type=\"way\" ref=\"81421217\" role=\"\"/>\n    <member type=\"way\" ref=\"332021864\" role=\"\"/>\n    <member type=\"way\" ref=\"85692740\" role=\"\"/>\n    <member type=\"way\" ref=\"4336445\" role=\"\"/>\n    <member type=\"way\" ref=\"81421206\" role=\"\"/>\n    <member type=\"way\" ref=\"85692744\" role=\"\"/>\n    <member type=\"way\" ref=\"85692741\" role=\"\"/>\n    <member type=\"way\" ref=\"81514433\" role=\"\"/>\n    <member type=\"way\" ref=\"969489029\" role=\"\"/>\n    <member type=\"way\" ref=\"1420025268\" role=\"\"/>\n    <member type=\"way\" ref=\"81514437\" role=\"\"/>\n    <member type=\"way\" ref=\"81514438\" role=\"\"/>\n    <member type=\"way\" ref=\"868160510\" role=\"\"/>\n    <member type=\"way\" ref=\"81514431\" role=\"\"/>\n    <member type=\"way\" ref=\"969489026\" role=\"\"/>\n    <member type=\"way\" ref=\"37084600\" role=\"\"/>\n    <member type=\"way\" ref=\"42772684\" role=\"\"/>\n    <member type=\"way\" ref=\"42772685\" role=\"\"/>\n    <member type=\"way\" ref=\"4336451\" role=\"\"/>\n    <member type=\"way\" ref=\"37084631\" role=\"\"/>\n    <member type=\"way\" ref=\"142173485\" role=\"\"/>\n    <member type=\"way\" ref=\"81410202\" role=\"\"/>\n    <member type=\"way\" ref=\"81410208\" role=\"\"/>\n    <member type=\"way\" ref=\"143657813\" role=\"\"/>\n    <member type=\"way\" ref=\"4603649\" role=\"\"/>\n    <member type=\"way\" ref=\"81410195\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420093\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420092\" role=\"\"/>\n    <member type=\"way\" ref=\"23123825\" role=\"\"/>\n    <member type=\"way\" ref=\"171940215\" role=\"\"/>\n    <member type=\"way\" ref=\"42772687\" role=\"\"/>\n    <member type=\"way\" ref=\"171940224\" role=\"\"/>\n    <member type=\"way\" ref=\"4603651\" role=\"\"/>\n    <member type=\"way\" ref=\"24598080\" role=\"\"/>\n    <member type=\"way\" ref=\"1287292087\" role=\"\"/>\n    <member type=\"way\" ref=\"171940206\" role=\"\"/>\n    <member type=\"way\" ref=\"33704322\" role=\"\"/>\n    <member type=\"way\" ref=\"24598088\" role=\"\"/>\n    <member type=\"way\" ref=\"130713063\" role=\"\"/>\n    <member type=\"way\" ref=\"138483059\" role=\"\"/>\n    <member type=\"way\" ref=\"130340906\" role=\"\"/>\n    <member type=\"way\" ref=\"140383815\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663436\" role=\"\"/>\n    <member type=\"way\" ref=\"970098668\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663439\" role=\"\"/>\n    <member type=\"way\" ref=\"424274558\" role=\"\"/>\n    <member type=\"way\" ref=\"920265971\" role=\"\"/>\n    <member type=\"way\" ref=\"426618722\" role=\"\"/>\n    <member type=\"way\" ref=\"140383770\" role=\"\"/>\n    <member type=\"way\" ref=\"140383802\" role=\"\"/>\n    <member type=\"way\" ref=\"140383819\" role=\"\"/>\n    <member type=\"way\" ref=\"140383835\" role=\"\"/>\n    <member type=\"way\" ref=\"140383787\" role=\"\"/>\n    <member type=\"way\" ref=\"140383778\" role=\"\"/>\n    <member type=\"way\" ref=\"140383767\" role=\"\"/>\n    <member type=\"way\" ref=\"140542860\" role=\"\"/>\n    <member type=\"way\" ref=\"140542868\" role=\"\"/>\n    <member type=\"way\" ref=\"587513738\" role=\"\"/>\n    <member type=\"way\" ref=\"140542929\" role=\"\"/>\n    <member type=\"way\" ref=\"140542922\" role=\"\"/>\n    <member type=\"way\" ref=\"140542898\" role=\"\"/>\n    <member type=\"way\" ref=\"140542943\" role=\"\"/>\n    <member type=\"way\" ref=\"140542926\" role=\"\"/>\n    <member type=\"way\" ref=\"140542915\" role=\"\"/>\n    <member type=\"way\" ref=\"140542902\" role=\"\"/>\n    <member type=\"way\" ref=\"140542940\" role=\"\"/>\n    <member type=\"way\" ref=\"140542896\" role=\"\"/>\n    <member type=\"way\" ref=\"140554200\" role=\"\"/>\n    <member type=\"way\" ref=\"140554202\" role=\"\"/>\n    <member type=\"way\" ref=\"140554194\" role=\"\"/>\n    <member type=\"way\" ref=\"140554196\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880042\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880041\" role=\"\"/>\n    <member type=\"way\" ref=\"15092066\" role=\"\"/>\n    <member type=\"way\" ref=\"15092074\" role=\"\"/>\n    <member type=\"way\" ref=\"116239838\" role=\"\"/>\n    <member type=\"way\" ref=\"116239840\" role=\"\"/>\n    <member type=\"way\" ref=\"116240538\" role=\"\"/>\n    <member type=\"way\" ref=\"138498004\" role=\"\"/>\n    <member type=\"way\" ref=\"57014731\" role=\"\"/>\n    <member type=\"way\" ref=\"140235641\" role=\"\"/>\n    <member type=\"way\" ref=\"225963264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494569\" role=\"\"/>\n    <member type=\"way\" ref=\"830404282\" role=\"\"/>\n    <member type=\"way\" ref=\"140233264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494557\" role=\"\"/>\n    <member type=\"way\" ref=\"140233256\" role=\"\"/>\n    <member type=\"way\" ref=\"830404278\" role=\"\"/>\n    <member type=\"way\" ref=\"140233230\" role=\"\"/>\n    <member type=\"way\" ref=\"830404279\" role=\"\"/>\n    <member type=\"way\" ref=\"140236948\" role=\"\"/>\n    <member type=\"way\" ref=\"140236955\" role=\"\"/>\n    <member type=\"way\" ref=\"151584548\" role=\"\"/>\n    <member type=\"way\" ref=\"140236953\" role=\"\"/>\n    <member type=\"way\" ref=\"140236962\" role=\"\"/>\n    <member type=\"way\" ref=\"140236949\" role=\"\"/>\n    <member type=\"way\" ref=\"140236965\" role=\"\"/>\n    <member type=\"way\" ref=\"140244131\" role=\"\"/>\n    <member type=\"way\" ref=\"140244135\" role=\"\"/>\n    <member type=\"way\" ref=\"675643796\" role=\"\"/>\n    <member type=\"way\" ref=\"4297979\" role=\"\"/>\n    <member type=\"way\" ref=\"1190383233\" role=\"\"/>\n    <member type=\"way\" ref=\"35780798\" role=\"\"/>\n    <member type=\"way\" ref=\"35778369\" role=\"\"/>\n    <member type=\"way\" ref=\"5367923\" role=\"\"/>\n    <member type=\"way\" ref=\"245443271\" role=\"\"/>\n    <member type=\"way\" ref=\"42833597\" role=\"\"/>\n    <member type=\"way\" ref=\"784595993\" role=\"\"/>\n    <member type=\"way\" ref=\"784595985\" role=\"\"/>\n    <member type=\"way\" ref=\"35778355\" role=\"\"/>\n    <member type=\"way\" ref=\"1190763596\" role=\"\"/>\n    <member type=\"way\" ref=\"35778631\" role=\"\"/>\n    <member type=\"way\" ref=\"35780807\" role=\"\"/>\n    <member type=\"way\" ref=\"674486885\" role=\"\"/>\n    <member type=\"way\" ref=\"675789780\" role=\"\"/>\n    <member type=\"way\" ref=\"5368856\" role=\"\"/>\n    <member type=\"way\" ref=\"673453969\" role=\"\"/>\n    <member type=\"way\" ref=\"1263181142\" role=\"\"/>\n    <member type=\"way\" ref=\"586652862\" role=\"\"/>\n    <tag k=\"from\" v=\"Högnäs\"/>\n    <tag k=\"name\" v=\"237 Högnäs–Järvenperä–Viherlaakso–Leppävaara\"/>\n    <tag k=\"network\" v=\"HSL\"/>\n    <tag k=\"network:wikidata\" v=\"Q473211\"/>\n    <tag k=\"public_transport:version\" v=\"2\"/>\n    <tag k=\"ref\" v=\"237\"/>\n    <tag k=\"route\" v=\"bus\"/>\n    <tag k=\"to\" v=\"Leppävaara\"/>\n    <tag k=\"type\" v=\"route\"/>\n  </relation>\n  <relation id=\"8290886\" version=\"43\" timestamp=\"2025-11-10T18:25:47Z\" changeset=\"174477166\" uid=\"5258545\" user=\"j numminen\">\n    <member type=\"node\" ref=\"6320480844\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1537060552\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6312763421\" role=\"stop\"/>\n    <member type=\"node\" ref=\"751460618\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6312763420\" role=\"stop\"/>\n    <member type=\"node\" ref=\"751460955\" role=\"platform\"/>\n    <member type=\"node\" ref=\"7124505641\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4268309535\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6312763419\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5046826053\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6312763418\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5239362582\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6312763417\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4534806230\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6312763416\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397856\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6312763415\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397860\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480843\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607989339\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1538138970\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268724954\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480842\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268728776\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480841\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607995446\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480840\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607996663\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480839\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1127774774\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480838\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5611810567\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480837\" role=\"stop\"/>\n    <member type=\"node\" ref=\"445790427\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480836\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1683691358\" role=\"platform\"/>\n    <member type=\"node\" ref=\"26432825\" role=\"stop\"/>\n    <member type=\"node\" ref=\"11876183846\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480835\" role=\"stop\"/>\n    <member type=\"node\" ref=\"269701728\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480834\" role=\"stop\"/>\n    <member type=\"node\" ref=\"416008458\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480833\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1687529637\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6306725245\" role=\"stop\"/>\n    <member type=\"node\" ref=\"249683098\" role=\"platform\"/>\n    <member type=\"node\" ref=\"3865492262\" role=\"stop\"/>\n    <member type=\"node\" ref=\"31282255\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6306725243\" role=\"stop\"/>\n    <member type=\"node\" ref=\"269697212\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6306725241\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267531878\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6306725238\" role=\"stop\"/>\n    <member type=\"node\" ref=\"599733050\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480832\" role=\"stop\"/>\n    <member type=\"node\" ref=\"727776838\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480831\" role=\"stop\"/>\n    <member type=\"node\" ref=\"727801306\" role=\"platform\"/>\n    <member type=\"node\" ref=\"5618361251\" role=\"stop\"/>\n    <member type=\"node\" ref=\"2470238678\" role=\"platform\"/>\n    <member type=\"node\" ref=\"5618361255\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1770442653\" role=\"platform\"/>\n    <member type=\"node\" ref=\"5618361258\" role=\"stop\"/>\n    <member type=\"node\" ref=\"676484028\" role=\"platform\"/>\n    <member type=\"node\" ref=\"5299598226\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5269882728\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6318972252\" role=\"stop\"/>\n    <member type=\"node\" ref=\"6241407971\" role=\"platform\"/>\n    <member type=\"way\" ref=\"218243927\" role=\"\"/>\n    <member type=\"way\" ref=\"587513743\" role=\"\"/>\n    <member type=\"way\" ref=\"140378411\" role=\"\"/>\n    <member type=\"way\" ref=\"587513739\" role=\"\"/>\n    <member type=\"way\" ref=\"618084338\" role=\"\"/>\n    <member type=\"way\" ref=\"42750272\" role=\"\"/>\n    <member type=\"way\" ref=\"675391835\" role=\"\"/>\n    <member type=\"way\" ref=\"675391824\" role=\"\"/>\n    <member type=\"way\" ref=\"218242417\" role=\"\"/>\n    <member type=\"way\" ref=\"674291456\" role=\"\"/>\n    <member type=\"way\" ref=\"138960298\" role=\"\"/>\n    <member type=\"way\" ref=\"140382663\" role=\"\"/>\n    <member type=\"way\" ref=\"140382696\" role=\"\"/>\n    <member type=\"way\" ref=\"618084336\" role=\"\"/>\n    <member type=\"way\" ref=\"675391838\" role=\"\"/>\n    <member type=\"way\" ref=\"4350739\" role=\"\"/>\n    <member type=\"way\" ref=\"7265564\" role=\"\"/>\n    <member type=\"way\" ref=\"140383822\" role=\"\"/>\n    <member type=\"way\" ref=\"140383768\" role=\"\"/>\n    <member type=\"way\" ref=\"47805401\" role=\"\"/>\n    <member type=\"way\" ref=\"138483060\" role=\"\"/>\n    <member type=\"way\" ref=\"140383775\" role=\"\"/>\n    <member type=\"way\" ref=\"140383815\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663436\" role=\"\"/>\n    <member type=\"way\" ref=\"970098668\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663439\" role=\"\"/>\n    <member type=\"way\" ref=\"424274558\" role=\"\"/>\n    <member type=\"way\" ref=\"920265971\" role=\"\"/>\n    <member type=\"way\" ref=\"426618722\" role=\"\"/>\n    <member type=\"way\" ref=\"140383770\" role=\"\"/>\n    <member type=\"way\" ref=\"140383802\" role=\"\"/>\n    <member type=\"way\" ref=\"140383819\" role=\"\"/>\n    <member type=\"way\" ref=\"140383835\" role=\"\"/>\n    <member type=\"way\" ref=\"140383787\" role=\"\"/>\n    <member type=\"way\" ref=\"140383778\" role=\"\"/>\n    <member type=\"way\" ref=\"140383767\" role=\"\"/>\n    <member type=\"way\" ref=\"140542860\" role=\"\"/>\n    <member type=\"way\" ref=\"140542868\" role=\"\"/>\n    <member type=\"way\" ref=\"587513738\" role=\"\"/>\n    <member type=\"way\" ref=\"140542929\" role=\"\"/>\n    <member type=\"way\" ref=\"140542922\" role=\"\"/>\n    <member type=\"way\" ref=\"140542898\" role=\"\"/>\n    <member type=\"way\" ref=\"140542943\" role=\"\"/>\n    <member type=\"way\" ref=\"140542926\" role=\"\"/>\n    <member type=\"way\" ref=\"140542915\" role=\"\"/>\n    <member type=\"way\" ref=\"140542902\" role=\"\"/>\n    <member type=\"way\" ref=\"140542940\" role=\"\"/>\n    <member type=\"way\" ref=\"140542896\" role=\"\"/>\n    <member type=\"way\" ref=\"140554200\" role=\"\"/>\n    <member type=\"way\" ref=\"140554202\" role=\"\"/>\n    <member type=\"way\" ref=\"140554194\" role=\"\"/>\n    <member type=\"way\" ref=\"140554196\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880042\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880041\" role=\"\"/>\n    <member type=\"way\" ref=\"15092066\" role=\"\"/>\n    <member type=\"way\" ref=\"15092074\" role=\"\"/>\n    <member type=\"way\" ref=\"116239838\" role=\"\"/>\n    <member type=\"way\" ref=\"116239840\" role=\"\"/>\n    <member type=\"way\" ref=\"116240538\" role=\"\"/>\n    <member type=\"way\" ref=\"138498012\" role=\"\"/>\n    <member type=\"way\" ref=\"140235642\" role=\"\"/>\n    <member type=\"way\" ref=\"140235630\" role=\"\"/>\n    <member type=\"way\" ref=\"140235633\" role=\"\"/>\n    <member type=\"way\" ref=\"140235627\" role=\"\"/>\n    <member type=\"way\" ref=\"140235640\" role=\"\"/>\n    <member type=\"way\" ref=\"140235626\" role=\"\"/>\n    <member type=\"way\" ref=\"26391022\" role=\"\"/>\n    <member type=\"way\" ref=\"4341669\" role=\"\"/>\n    <member type=\"way\" ref=\"1340274110\" role=\"\"/>\n    <member type=\"way\" ref=\"1340274111\" role=\"\"/>\n    <member type=\"way\" ref=\"1317451536\" role=\"\"/>\n    <member type=\"way\" ref=\"968132560\" role=\"\"/>\n    <member type=\"way\" ref=\"42750269\" role=\"\"/>\n    <member type=\"way\" ref=\"23115416\" role=\"\"/>\n    <member type=\"way\" ref=\"1340274109\" role=\"\"/>\n    <member type=\"way\" ref=\"1443558432\" role=\"\"/>\n    <member type=\"way\" ref=\"28399044\" role=\"\"/>\n    <member type=\"way\" ref=\"23115417\" role=\"\"/>\n    <member type=\"way\" ref=\"1311180673\" role=\"\"/>\n    <member type=\"way\" ref=\"1311180672\" role=\"\"/>\n    <member type=\"way\" ref=\"142855109\" role=\"\"/>\n    <member type=\"way\" ref=\"142855107\" role=\"\"/>\n    <member type=\"way\" ref=\"1067805347\" role=\"\"/>\n    <member type=\"way\" ref=\"142855101\" role=\"\"/>\n    <member type=\"way\" ref=\"142855100\" role=\"\"/>\n    <member type=\"way\" ref=\"1249549840\" role=\"\"/>\n    <member type=\"way\" ref=\"710479917\" role=\"\"/>\n    <member type=\"way\" ref=\"229705809\" role=\"\"/>\n    <member type=\"way\" ref=\"229705818\" role=\"\"/>\n    <member type=\"way\" ref=\"229705817\" role=\"\"/>\n    <member type=\"way\" ref=\"229705811\" role=\"\"/>\n    <member type=\"way\" ref=\"229705812\" role=\"\"/>\n    <member type=\"way\" ref=\"618823669\" role=\"\"/>\n    <member type=\"way\" ref=\"153547916\" role=\"\"/>\n    <member type=\"way\" ref=\"801373036\" role=\"\"/>\n    <member type=\"way\" ref=\"153547915\" role=\"\"/>\n    <member type=\"way\" ref=\"35780794\" role=\"\"/>\n    <member type=\"way\" ref=\"114551819\" role=\"\"/>\n    <member type=\"way\" ref=\"114551818\" role=\"\"/>\n    <member type=\"way\" ref=\"23125618\" role=\"\"/>\n    <member type=\"way\" ref=\"577599932\" role=\"\"/>\n    <member type=\"way\" ref=\"4297939\" role=\"\"/>\n    <member type=\"way\" ref=\"35376116\" role=\"\"/>\n    <member type=\"way\" ref=\"896103013\" role=\"\"/>\n    <member type=\"way\" ref=\"4763416\" role=\"\"/>\n    <member type=\"way\" ref=\"896103012\" role=\"\"/>\n    <member type=\"way\" ref=\"24610584\" role=\"\"/>\n    <member type=\"way\" ref=\"284916354\" role=\"\"/>\n    <member type=\"way\" ref=\"284916351\" role=\"\"/>\n    <member type=\"way\" ref=\"284916349\" role=\"\"/>\n    <member type=\"way\" ref=\"95918707\" role=\"\"/>\n    <member type=\"way\" ref=\"239450251\" role=\"\"/>\n    <member type=\"way\" ref=\"34360569\" role=\"\"/>\n    <member type=\"way\" ref=\"34360570\" role=\"\"/>\n    <member type=\"way\" ref=\"845129788\" role=\"\"/>\n    <member type=\"way\" ref=\"42772688\" role=\"\"/>\n    <member type=\"way\" ref=\"34360566\" role=\"\"/>\n    <member type=\"way\" ref=\"45347982\" role=\"\"/>\n    <member type=\"way\" ref=\"144331669\" role=\"\"/>\n    <member type=\"way\" ref=\"284916576\" role=\"\"/>\n    <member type=\"way\" ref=\"239454857\" role=\"\"/>\n    <member type=\"way\" ref=\"239454858\" role=\"\"/>\n    <member type=\"way\" ref=\"49012350\" role=\"\"/>\n    <member type=\"way\" ref=\"750401324\" role=\"\"/>\n    <member type=\"way\" ref=\"4706328\" role=\"\"/>\n    <member type=\"way\" ref=\"284917915\" role=\"\"/>\n    <member type=\"way\" ref=\"1016803823\" role=\"\"/>\n    <member type=\"way\" ref=\"4549312\" role=\"\"/>\n    <member type=\"way\" ref=\"750401331\" role=\"\"/>\n    <member type=\"way\" ref=\"284917913\" role=\"\"/>\n    <member type=\"way\" ref=\"284917962\" role=\"\"/>\n    <member type=\"way\" ref=\"284917960\" role=\"\"/>\n    <member type=\"way\" ref=\"284918276\" role=\"\"/>\n    <member type=\"way\" ref=\"284918275\" role=\"\"/>\n    <member type=\"way\" ref=\"284919289\" role=\"\"/>\n    <member type=\"way\" ref=\"284919294\" role=\"\"/>\n    <member type=\"way\" ref=\"284919292\" role=\"\"/>\n    <member type=\"way\" ref=\"284919296\" role=\"\"/>\n    <member type=\"way\" ref=\"284919295\" role=\"\"/>\n    <member type=\"way\" ref=\"239491903\" role=\"\"/>\n    <member type=\"way\" ref=\"239491906\" role=\"\"/>\n    <member type=\"way\" ref=\"239491902\" role=\"\"/>\n    <member type=\"way\" ref=\"35780810\" role=\"\"/>\n    <member type=\"way\" ref=\"239491895\" role=\"\"/>\n    <member type=\"way\" ref=\"239491900\" role=\"\"/>\n    <member type=\"way\" ref=\"378450538\" role=\"\"/>\n    <member type=\"way\" ref=\"239491896\" role=\"\"/>\n    <member type=\"way\" ref=\"31923637\" role=\"\"/>\n    <member type=\"way\" ref=\"378450534\" role=\"\"/>\n    <member type=\"way\" ref=\"146667062\" role=\"\"/>\n    <member type=\"way\" ref=\"582512741\" role=\"\"/>\n    <member type=\"way\" ref=\"146667071\" role=\"\"/>\n    <member type=\"way\" ref=\"146668049\" role=\"\"/>\n    <member type=\"way\" ref=\"582512742\" role=\"\"/>\n    <member type=\"way\" ref=\"34684240\" role=\"\"/>\n    <member type=\"way\" ref=\"29026057\" role=\"\"/>\n    <member type=\"way\" ref=\"605098515\" role=\"\"/>\n    <member type=\"way\" ref=\"605098516\" role=\"\"/>\n    <member type=\"way\" ref=\"49205801\" role=\"\"/>\n    <member type=\"way\" ref=\"29026055\" role=\"\"/>\n    <member type=\"way\" ref=\"34684233\" role=\"\"/>\n    <member type=\"way\" ref=\"22945396\" role=\"\"/>\n    <member type=\"way\" ref=\"29025775\" role=\"\"/>\n    <member type=\"way\" ref=\"1133761197\" role=\"\"/>\n    <member type=\"way\" ref=\"710519872\" role=\"\"/>\n    <member type=\"way\" ref=\"586989711\" role=\"\"/>\n    <member type=\"way\" ref=\"586989713\" role=\"\"/>\n    <member type=\"way\" ref=\"548583082\" role=\"\"/>\n    <member type=\"way\" ref=\"1133761196\" role=\"\"/>\n    <member type=\"way\" ref=\"548583081\" role=\"\"/>\n    <member type=\"way\" ref=\"48335842\" role=\"\"/>\n    <member type=\"way\" ref=\"4579306\" role=\"\"/>\n    <member type=\"way\" ref=\"34684236\" role=\"\"/>\n    <member type=\"way\" ref=\"48335856\" role=\"\"/>\n    <member type=\"way\" ref=\"674176854\" role=\"\"/>\n    <member type=\"way\" ref=\"674681845\" role=\"\"/>\n    <member type=\"way\" ref=\"669641883\" role=\"\"/>\n    <tag k=\"colour\" v=\"#007AC9\"/>\n    <tag k=\"from\" v=\"Jupperi\"/>\n    <tag k=\"interval\" v=\"15\"/>\n    <tag k=\"interval:peak\" v=\"10\"/>\n    <tag k=\"interval:saturday\" v=\"15\"/>\n    <tag k=\"interval:sunday\" v=\"30\"/>\n    <tag k=\"name\" v=\"548 Jupperi–Viherlaakso–Kauniainen–Mankkaa–Tapiola(M)\"/>\n    <tag k=\"network\" v=\"HSL\"/>\n    <tag k=\"network:wikidata\" v=\"Q473211\"/>\n    <tag k=\"public_transport:version\" v=\"2\"/>\n    <tag k=\"ref\" v=\"548\"/>\n    <tag k=\"route\" v=\"bus\"/>\n    <tag k=\"to\" v=\"Tapiola\"/>\n    <tag k=\"type\" v=\"route\"/>\n    <tag k=\"via\" v=\"Kauniainen\"/>\n  </relation>\n  <relation id=\"9379269\" version=\"24\" timestamp=\"2025-08-08T14:18:22Z\" changeset=\"170158278\" uid=\"5258545\" user=\"j numminen\">\n    <member type=\"node\" ref=\"607976820\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1518478314\" role=\"platform\"/>\n    <member type=\"node\" ref=\"607977376\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1539976308\" role=\"platform\"/>\n    <member type=\"node\" ref=\"477952585\" role=\"platform\"/>\n    <member type=\"node\" ref=\"477957886\" role=\"platform\"/>\n    <member type=\"node\" ref=\"477969901\" role=\"platform\"/>\n    <member type=\"node\" ref=\"477974878\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1519681212\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1519681203\" role=\"platform\"/>\n    <member type=\"node\" ref=\"267397899\" role=\"platform\"/>\n    <member type=\"node\" ref=\"267397870\" role=\"platform\"/>\n    <member type=\"node\" ref=\"268725204\" role=\"platform\"/>\n    <member type=\"node\" ref=\"607989339\" role=\"platform\"/>\n    <member type=\"node\" ref=\"268724954\" role=\"platform\"/>\n    <member type=\"node\" ref=\"268728776\" role=\"platform\"/>\n    <member type=\"node\" ref=\"607995446\" role=\"platform\"/>\n    <member type=\"node\" ref=\"607996663\" role=\"platform\"/>\n    <member type=\"node\" ref=\"607998796\" role=\"platform\"/>\n    <member type=\"node\" ref=\"59871347\" role=\"platform\"/>\n    <member type=\"node\" ref=\"5217833121\" role=\"platform\"/>\n    <member type=\"node\" ref=\"498462819\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1518655407\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311236983\" role=\"stop\"/>\n    <member type=\"node\" ref=\"38458038\" role=\"platform\"/>\n    <member type=\"node\" ref=\"418340755\" role=\"stop\"/>\n    <member type=\"node\" ref=\"418340775\" role=\"platform\"/>\n    <member type=\"way\" ref=\"138483065\" role=\"\"/>\n    <member type=\"way\" ref=\"934048495\" role=\"\"/>\n    <member type=\"way\" ref=\"138483061\" role=\"\"/>\n    <member type=\"way\" ref=\"969489027\" role=\"\"/>\n    <member type=\"way\" ref=\"969489026\" role=\"\"/>\n    <member type=\"way\" ref=\"37084600\" role=\"\"/>\n    <member type=\"way\" ref=\"42772684\" role=\"\"/>\n    <member type=\"way\" ref=\"42772685\" role=\"\"/>\n    <member type=\"way\" ref=\"4336451\" role=\"\"/>\n    <member type=\"way\" ref=\"37084631\" role=\"\"/>\n    <member type=\"way\" ref=\"142173485\" role=\"\"/>\n    <member type=\"way\" ref=\"81410202\" role=\"\"/>\n    <member type=\"way\" ref=\"81410208\" role=\"\"/>\n    <member type=\"way\" ref=\"143657813\" role=\"\"/>\n    <member type=\"way\" ref=\"4603649\" role=\"\"/>\n    <member type=\"way\" ref=\"81410195\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420093\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420092\" role=\"\"/>\n    <member type=\"way\" ref=\"23123825\" role=\"\"/>\n    <member type=\"way\" ref=\"171940215\" role=\"\"/>\n    <member type=\"way\" ref=\"42772687\" role=\"\"/>\n    <member type=\"way\" ref=\"171940224\" role=\"\"/>\n    <member type=\"way\" ref=\"4603651\" role=\"\"/>\n    <member type=\"way\" ref=\"24598080\" role=\"\"/>\n    <member type=\"way\" ref=\"1287292087\" role=\"\"/>\n    <member type=\"way\" ref=\"171940206\" role=\"\"/>\n    <member type=\"way\" ref=\"33704322\" role=\"\"/>\n    <member type=\"way\" ref=\"24598088\" role=\"\"/>\n    <member type=\"way\" ref=\"130713063\" role=\"\"/>\n    <member type=\"way\" ref=\"138483059\" role=\"\"/>\n    <member type=\"way\" ref=\"130340906\" role=\"\"/>\n    <member type=\"way\" ref=\"140383815\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663436\" role=\"\"/>\n    <member type=\"way\" ref=\"970098668\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663439\" role=\"\"/>\n    <member type=\"way\" ref=\"424274558\" role=\"\"/>\n    <member type=\"way\" ref=\"920265971\" role=\"\"/>\n    <member type=\"way\" ref=\"426618722\" role=\"\"/>\n    <member type=\"way\" ref=\"140383770\" role=\"\"/>\n    <member type=\"way\" ref=\"140383802\" role=\"\"/>\n    <member type=\"way\" ref=\"140383819\" role=\"\"/>\n    <member type=\"way\" ref=\"140383835\" role=\"\"/>\n    <member type=\"way\" ref=\"140383787\" role=\"\"/>\n    <member type=\"way\" ref=\"140383778\" role=\"\"/>\n    <member type=\"way\" ref=\"140383767\" role=\"\"/>\n    <member type=\"way\" ref=\"140542860\" role=\"\"/>\n    <member type=\"way\" ref=\"140542868\" role=\"\"/>\n    <member type=\"way\" ref=\"587513738\" role=\"\"/>\n    <member type=\"way\" ref=\"140542929\" role=\"\"/>\n    <member type=\"way\" ref=\"140542922\" role=\"\"/>\n    <member type=\"way\" ref=\"140542898\" role=\"\"/>\n    <member type=\"way\" ref=\"140542943\" role=\"\"/>\n    <member type=\"way\" ref=\"140542926\" role=\"\"/>\n    <member type=\"way\" ref=\"140542915\" role=\"\"/>\n    <member type=\"way\" ref=\"140542902\" role=\"\"/>\n    <member type=\"way\" ref=\"140542940\" role=\"\"/>\n    <member type=\"way\" ref=\"140542896\" role=\"\"/>\n    <member type=\"way\" ref=\"140554200\" role=\"\"/>\n    <member type=\"way\" ref=\"140554202\" role=\"\"/>\n    <member type=\"way\" ref=\"140554194\" role=\"\"/>\n    <member type=\"way\" ref=\"140554196\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880042\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880041\" role=\"\"/>\n    <member type=\"way\" ref=\"15092066\" role=\"\"/>\n    <member type=\"way\" ref=\"15092074\" role=\"\"/>\n    <member type=\"way\" ref=\"116239838\" role=\"\"/>\n    <member type=\"way\" ref=\"116239840\" role=\"\"/>\n    <member type=\"way\" ref=\"116240538\" role=\"\"/>\n    <member type=\"way\" ref=\"138498004\" role=\"\"/>\n    <member type=\"way\" ref=\"57014731\" role=\"\"/>\n    <member type=\"way\" ref=\"140235641\" role=\"\"/>\n    <member type=\"way\" ref=\"225963264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494569\" role=\"\"/>\n    <member type=\"way\" ref=\"830404282\" role=\"\"/>\n    <member type=\"way\" ref=\"140233264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494557\" role=\"\"/>\n    <member type=\"way\" ref=\"140233256\" role=\"\"/>\n    <member type=\"way\" ref=\"830404278\" role=\"\"/>\n    <member type=\"way\" ref=\"140233230\" role=\"\"/>\n    <member type=\"way\" ref=\"830404279\" role=\"\"/>\n    <member type=\"way\" ref=\"140236948\" role=\"\"/>\n    <member type=\"way\" ref=\"140236955\" role=\"\"/>\n    <member type=\"way\" ref=\"151584548\" role=\"\"/>\n    <member type=\"way\" ref=\"140236953\" role=\"\"/>\n    <member type=\"way\" ref=\"140236962\" role=\"\"/>\n    <member type=\"way\" ref=\"140236949\" role=\"\"/>\n    <member type=\"way\" ref=\"140236965\" role=\"\"/>\n    <member type=\"way\" ref=\"140244131\" role=\"\"/>\n    <member type=\"way\" ref=\"140244135\" role=\"\"/>\n    <member type=\"way\" ref=\"675643796\" role=\"\"/>\n    <member type=\"way\" ref=\"4297979\" role=\"\"/>\n    <member type=\"way\" ref=\"1190383233\" role=\"\"/>\n    <member type=\"way\" ref=\"35780798\" role=\"\"/>\n    <member type=\"way\" ref=\"35778369\" role=\"\"/>\n    <member type=\"way\" ref=\"5367923\" role=\"\"/>\n    <member type=\"way\" ref=\"245443271\" role=\"\"/>\n    <member type=\"way\" ref=\"42833597\" role=\"\"/>\n    <member type=\"way\" ref=\"784595993\" role=\"\"/>\n    <member type=\"way\" ref=\"784595985\" role=\"\"/>\n    <member type=\"way\" ref=\"35778355\" role=\"\"/>\n    <member type=\"way\" ref=\"1190763596\" role=\"\"/>\n    <member type=\"way\" ref=\"35778631\" role=\"\"/>\n    <member type=\"way\" ref=\"35780807\" role=\"\"/>\n    <member type=\"way\" ref=\"674486885\" role=\"\"/>\n    <member type=\"way\" ref=\"675789780\" role=\"\"/>\n    <member type=\"way\" ref=\"5368856\" role=\"\"/>\n    <member type=\"way\" ref=\"673453969\" role=\"\"/>\n    <member type=\"way\" ref=\"1263181142\" role=\"\"/>\n    <member type=\"way\" ref=\"586652862\" role=\"\"/>\n    <tag k=\"colour\" v=\"#007AC9\"/>\n    <tag k=\"from\" v=\"Vanhakartano\"/>\n    <tag k=\"name\" v=\"236 Vanhakartano–Viherlaakso–Leppävaara\"/>\n    <tag k=\"network\" v=\"HSL\"/>\n    <tag k=\"network:wikidata\" v=\"Q473211\"/>\n    <tag k=\"public_transport:version\" v=\"2\"/>\n    <tag k=\"ref\" v=\"236\"/>\n    <tag k=\"route\" v=\"bus\"/>\n    <tag k=\"to\" v=\"Leppävaara\"/>\n    <tag k=\"type\" v=\"route\"/>\n  </relation>\n  <relation id=\"9399637\" version=\"31\" timestamp=\"2026-01-15T17:38:00Z\" changeset=\"177268542\" uid=\"5258545\" user=\"j numminen\">\n    <member type=\"node\" ref=\"719270277\" role=\"stop\"/>\n    <member type=\"node\" ref=\"33588480\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238805\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4144754027\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238804\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4144741240\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238803\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5731754569\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238802\" role=\"stop\"/>\n    <member type=\"node\" ref=\"923141718\" role=\"platform\"/>\n    <member type=\"node\" ref=\"2886121555\" role=\"stop\"/>\n    <member type=\"node\" ref=\"2162345664\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238801\" role=\"stop\"/>\n    <member type=\"node\" ref=\"602761125\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238800\" role=\"stop\"/>\n    <member type=\"node\" ref=\"412197994\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238799\" role=\"stop\"/>\n    <member type=\"node\" ref=\"602594097\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238798\" role=\"stop\"/>\n    <member type=\"node\" ref=\"936807872\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1518419859\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4558376856\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238797\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4558370114\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238796\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1278044159\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238795\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1278044435\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238794\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518655443\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238793\" role=\"stop\"/>\n    <member type=\"node\" ref=\"401270873\" role=\"platform\"/>\n    <member type=\"node\" ref=\"10246360325\" role=\"stop\"/>\n    <member type=\"node\" ref=\"10246360326\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1277719901\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5899096200\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238791\" role=\"stop\"/>\n    <member type=\"node\" ref=\"401270252\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6325416636\" role=\"stop\"/>\n    <member type=\"node\" ref=\"401274510\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6325416635\" role=\"stop\"/>\n    <member type=\"node\" ref=\"401274684\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6325416634\" role=\"stop\"/>\n    <member type=\"node\" ref=\"401274693\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238790\" role=\"stop\"/>\n    <member type=\"node\" ref=\"431057003\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238789\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607975645\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238788\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607976552\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238787\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518478314\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1539976495\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607977376\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238786\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1539976308\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238785\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477952585\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340234784\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477957886\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6323310490\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477969901\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6323310489\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477974878\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921210\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681212\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776547\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681203\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1736975678\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397899\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776546\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397870\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776545\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268725204\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480843\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607989339\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1538138970\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268724954\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480842\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268728776\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480841\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607995446\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480840\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607996663\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6326926808\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607998796\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1536455200\" role=\"stop\"/>\n    <member type=\"node\" ref=\"59871347\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238295\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5217833121\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238297\" role=\"stop\"/>\n    <member type=\"node\" ref=\"498462819\" role=\"platform\"/>\n    <member type=\"node\" ref=\"282280287\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518655407\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311236983\" role=\"stop\"/>\n    <member type=\"node\" ref=\"38458038\" role=\"platform\"/>\n    <member type=\"node\" ref=\"418340755\" role=\"stop\"/>\n    <member type=\"node\" ref=\"418340775\" role=\"platform\"/>\n    <member type=\"way\" ref=\"57965704\" role=\"\"/>\n    <member type=\"way\" ref=\"206255285\" role=\"\"/>\n    <member type=\"way\" ref=\"75263895\" role=\"\"/>\n    <member type=\"way\" ref=\"75263894\" role=\"\"/>\n    <member type=\"way\" ref=\"25787931\" role=\"\"/>\n    <member type=\"way\" ref=\"284915227\" role=\"\"/>\n    <member type=\"way\" ref=\"413141062\" role=\"\"/>\n    <member type=\"way\" ref=\"6932866\" role=\"\"/>\n    <member type=\"way\" ref=\"413141058\" role=\"\"/>\n    <member type=\"way\" ref=\"413141205\" role=\"\"/>\n    <member type=\"way\" ref=\"1422990690\" role=\"\"/>\n    <member type=\"way\" ref=\"1413429629\" role=\"\"/>\n    <member type=\"way\" ref=\"138497990\" role=\"\"/>\n    <member type=\"way\" ref=\"284914937\" role=\"\"/>\n    <member type=\"way\" ref=\"284914939\" role=\"\"/>\n    <member type=\"way\" ref=\"284913598\" role=\"\"/>\n    <member type=\"way\" ref=\"284913605\" role=\"\"/>\n    <member type=\"way\" ref=\"751180288\" role=\"\"/>\n    <member type=\"way\" ref=\"218074602\" role=\"\"/>\n    <member type=\"way\" ref=\"4492083\" role=\"\"/>\n    <member type=\"way\" ref=\"677107249\" role=\"\"/>\n    <member type=\"way\" ref=\"683604516\" role=\"\"/>\n    <member type=\"way\" ref=\"206228627\" role=\"\"/>\n    <member type=\"way\" ref=\"206245571\" role=\"\"/>\n    <member type=\"way\" ref=\"206245557\" role=\"\"/>\n    <member type=\"way\" ref=\"206245556\" role=\"\"/>\n    <member type=\"way\" ref=\"206245566\" role=\"\"/>\n    <member type=\"way\" ref=\"206245560\" role=\"\"/>\n    <member type=\"way\" ref=\"206249569\" role=\"\"/>\n    <member type=\"way\" ref=\"206245568\" role=\"\"/>\n    <member type=\"way\" ref=\"206249565\" role=\"\"/>\n    <member type=\"way\" ref=\"909257041\" role=\"\"/>\n    <member type=\"way\" ref=\"909257042\" role=\"\"/>\n    <member type=\"way\" ref=\"206246955\" role=\"\"/>\n    <member type=\"way\" ref=\"123432935\" role=\"\"/>\n    <member type=\"way\" ref=\"123770950\" role=\"\"/>\n    <member type=\"way\" ref=\"138498008\" role=\"\"/>\n    <member type=\"way\" ref=\"140582667\" role=\"\"/>\n    <member type=\"way\" ref=\"140582663\" role=\"\"/>\n    <member type=\"way\" ref=\"140582670\" role=\"\"/>\n    <member type=\"way\" ref=\"1422990691\" role=\"\"/>\n    <member type=\"way\" ref=\"140582675\" role=\"\"/>\n    <member type=\"way\" ref=\"140582664\" role=\"\"/>\n    <member type=\"way\" ref=\"47304106\" role=\"\"/>\n    <member type=\"way\" ref=\"140582165\" role=\"\"/>\n    <member type=\"way\" ref=\"143657051\" role=\"\"/>\n    <member type=\"way\" ref=\"24440221\" role=\"\"/>\n    <member type=\"way\" ref=\"34565374\" role=\"\"/>\n    <member type=\"way\" ref=\"140582139\" role=\"\"/>\n    <member type=\"way\" ref=\"140582145\" role=\"\"/>\n    <member type=\"way\" ref=\"140582158\" role=\"\"/>\n    <member type=\"way\" ref=\"140582144\" role=\"\"/>\n    <member type=\"way\" ref=\"140582131\" role=\"\"/>\n    <member type=\"way\" ref=\"140580329\" role=\"\"/>\n    <member type=\"way\" ref=\"140580327\" role=\"\"/>\n    <member type=\"way\" ref=\"47304108\" role=\"\"/>\n    <member type=\"way\" ref=\"140580328\" role=\"\"/>\n    <member type=\"way\" ref=\"122821335\" role=\"\"/>\n    <member type=\"way\" ref=\"1234282620\" role=\"\"/>\n    <member type=\"way\" ref=\"4614564\" role=\"\"/>\n    <member type=\"way\" ref=\"17375278\" role=\"\"/>\n    <member type=\"way\" ref=\"17375280\" role=\"\"/>\n    <member type=\"way\" ref=\"4614565\" role=\"\"/>\n    <member type=\"way\" ref=\"122821339\" role=\"\"/>\n    <member type=\"way\" ref=\"140580331\" role=\"\"/>\n    <member type=\"way\" ref=\"140580330\" role=\"\"/>\n    <member type=\"way\" ref=\"934048496\" role=\"\"/>\n    <member type=\"way\" ref=\"6881756\" role=\"\"/>\n    <member type=\"way\" ref=\"138483063\" role=\"\"/>\n    <member type=\"way\" ref=\"934048495\" role=\"\"/>\n    <member type=\"way\" ref=\"138483061\" role=\"\"/>\n    <member type=\"way\" ref=\"969489027\" role=\"\"/>\n    <member type=\"way\" ref=\"969489026\" role=\"\"/>\n    <member type=\"way\" ref=\"37084600\" role=\"\"/>\n    <member type=\"way\" ref=\"42772684\" role=\"\"/>\n    <member type=\"way\" ref=\"42772685\" role=\"\"/>\n    <member type=\"way\" ref=\"4336451\" role=\"\"/>\n    <member type=\"way\" ref=\"37084631\" role=\"\"/>\n    <member type=\"way\" ref=\"142173485\" role=\"\"/>\n    <member type=\"way\" ref=\"81410202\" role=\"\"/>\n    <member type=\"way\" ref=\"81410208\" role=\"\"/>\n    <member type=\"way\" ref=\"143657813\" role=\"\"/>\n    <member type=\"way\" ref=\"4603649\" role=\"\"/>\n    <member type=\"way\" ref=\"81410195\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420093\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420092\" role=\"\"/>\n    <member type=\"way\" ref=\"23123825\" role=\"\"/>\n    <member type=\"way\" ref=\"171940215\" role=\"\"/>\n    <member type=\"way\" ref=\"42772687\" role=\"\"/>\n    <member type=\"way\" ref=\"171940224\" role=\"\"/>\n    <member type=\"way\" ref=\"4603651\" role=\"\"/>\n    <member type=\"way\" ref=\"24598080\" role=\"\"/>\n    <member type=\"way\" ref=\"1287292087\" role=\"\"/>\n    <member type=\"way\" ref=\"171940206\" role=\"\"/>\n    <member type=\"way\" ref=\"33704322\" role=\"\"/>\n    <member type=\"way\" ref=\"24598088\" role=\"\"/>\n    <member type=\"way\" ref=\"130713063\" role=\"\"/>\n    <member type=\"way\" ref=\"138483059\" role=\"\"/>\n    <member type=\"way\" ref=\"130340906\" role=\"\"/>\n    <member type=\"way\" ref=\"140383815\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663436\" role=\"\"/>\n    <member type=\"way\" ref=\"970098668\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663439\" role=\"\"/>\n    <member type=\"way\" ref=\"424274558\" role=\"\"/>\n    <member type=\"way\" ref=\"920265971\" role=\"\"/>\n    <member type=\"way\" ref=\"426618722\" role=\"\"/>\n    <member type=\"way\" ref=\"140383770\" role=\"\"/>\n    <member type=\"way\" ref=\"140383802\" role=\"\"/>\n    <member type=\"way\" ref=\"140383819\" role=\"\"/>\n    <member type=\"way\" ref=\"140383835\" role=\"\"/>\n    <member type=\"way\" ref=\"140383787\" role=\"\"/>\n    <member type=\"way\" ref=\"140383778\" role=\"\"/>\n    <member type=\"way\" ref=\"140383767\" role=\"\"/>\n    <member type=\"way\" ref=\"140542860\" role=\"\"/>\n    <member type=\"way\" ref=\"140542868\" role=\"\"/>\n    <member type=\"way\" ref=\"587513738\" role=\"\"/>\n    <member type=\"way\" ref=\"140542929\" role=\"\"/>\n    <member type=\"way\" ref=\"140542922\" role=\"\"/>\n    <member type=\"way\" ref=\"140542898\" role=\"\"/>\n    <member type=\"way\" ref=\"140542943\" role=\"\"/>\n    <member type=\"way\" ref=\"140542926\" role=\"\"/>\n    <member type=\"way\" ref=\"140542915\" role=\"\"/>\n    <member type=\"way\" ref=\"140542902\" role=\"\"/>\n    <member type=\"way\" ref=\"140542940\" role=\"\"/>\n    <member type=\"way\" ref=\"140542896\" role=\"\"/>\n    <member type=\"way\" ref=\"140554200\" role=\"\"/>\n    <member type=\"way\" ref=\"140554202\" role=\"\"/>\n    <member type=\"way\" ref=\"140554194\" role=\"\"/>\n    <member type=\"way\" ref=\"140554196\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880042\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880041\" role=\"\"/>\n    <member type=\"way\" ref=\"15092066\" role=\"\"/>\n    <member type=\"way\" ref=\"15092074\" role=\"\"/>\n    <member type=\"way\" ref=\"116239838\" role=\"\"/>\n    <member type=\"way\" ref=\"116239840\" role=\"\"/>\n    <member type=\"way\" ref=\"116240538\" role=\"\"/>\n    <member type=\"way\" ref=\"138498004\" role=\"\"/>\n    <member type=\"way\" ref=\"57014731\" role=\"\"/>\n    <member type=\"way\" ref=\"140235641\" role=\"\"/>\n    <member type=\"way\" ref=\"225963264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494569\" role=\"\"/>\n    <member type=\"way\" ref=\"830404282\" role=\"\"/>\n    <member type=\"way\" ref=\"140233264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494557\" role=\"\"/>\n    <member type=\"way\" ref=\"140233256\" role=\"\"/>\n    <member type=\"way\" ref=\"830404278\" role=\"\"/>\n    <member type=\"way\" ref=\"140233230\" role=\"\"/>\n    <member type=\"way\" ref=\"830404279\" role=\"\"/>\n    <member type=\"way\" ref=\"140236948\" role=\"\"/>\n    <member type=\"way\" ref=\"140236955\" role=\"\"/>\n    <member type=\"way\" ref=\"151584548\" role=\"\"/>\n    <member type=\"way\" ref=\"140236953\" role=\"\"/>\n    <member type=\"way\" ref=\"140236962\" role=\"\"/>\n    <member type=\"way\" ref=\"140236949\" role=\"\"/>\n    <member type=\"way\" ref=\"140236965\" role=\"\"/>\n    <member type=\"way\" ref=\"140244131\" role=\"\"/>\n    <member type=\"way\" ref=\"140244135\" role=\"\"/>\n    <member type=\"way\" ref=\"675643796\" role=\"\"/>\n    <member type=\"way\" ref=\"4297979\" role=\"\"/>\n    <member type=\"way\" ref=\"1190383233\" role=\"\"/>\n    <member type=\"way\" ref=\"35780798\" role=\"\"/>\n    <member type=\"way\" ref=\"35778369\" role=\"\"/>\n    <member type=\"way\" ref=\"5367923\" role=\"\"/>\n    <member type=\"way\" ref=\"245443271\" role=\"\"/>\n    <member type=\"way\" ref=\"42833597\" role=\"\"/>\n    <member type=\"way\" ref=\"784595993\" role=\"\"/>\n    <member type=\"way\" ref=\"784595985\" role=\"\"/>\n    <member type=\"way\" ref=\"35778355\" role=\"\"/>\n    <member type=\"way\" ref=\"1190763596\" role=\"\"/>\n    <member type=\"way\" ref=\"35778631\" role=\"\"/>\n    <member type=\"way\" ref=\"35780807\" role=\"\"/>\n    <member type=\"way\" ref=\"674486885\" role=\"\"/>\n    <member type=\"way\" ref=\"675789780\" role=\"\"/>\n    <member type=\"way\" ref=\"5368856\" role=\"\"/>\n    <member type=\"way\" ref=\"673453969\" role=\"\"/>\n    <member type=\"way\" ref=\"1263181142\" role=\"\"/>\n    <member type=\"way\" ref=\"586652862\" role=\"\"/>\n    <tag k=\"colour\" v=\"#007AC9\"/>\n    <tag k=\"from\" v=\"Serena\"/>\n    <tag k=\"name\" v=\"236 Serena–Kalajärvi–Viherlaakso–Leppävaara\"/>\n    <tag k=\"network\" v=\"HSL\"/>\n    <tag k=\"network:wikidata\" v=\"Q473211\"/>\n    <tag k=\"network:wikipedia\" v=\"fi:Helsingin seudun liikenne\"/>\n    <tag k=\"public_transport:version\" v=\"2\"/>\n    <tag k=\"ref\" v=\"236\"/>\n    <tag k=\"to\" v=\"Leppävaara\"/>\n    <tag k=\"type\" v=\"was:route\"/>\n    <tag k=\"was:route\" v=\"bus\"/>\n  </relation>\n  <relation id=\"9399882\" version=\"28\" timestamp=\"2025-08-18T08:24:09Z\" changeset=\"170602369\" uid=\"5258545\" user=\"j numminen\">\n    <member type=\"node\" ref=\"719270277\" role=\"stop\"/>\n    <member type=\"node\" ref=\"33588480\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238805\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4144754027\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238804\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4144741240\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238803\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5731754569\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238802\" role=\"stop\"/>\n    <member type=\"node\" ref=\"923141718\" role=\"platform\"/>\n    <member type=\"node\" ref=\"2886121555\" role=\"stop\"/>\n    <member type=\"node\" ref=\"2162345664\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238801\" role=\"stop\"/>\n    <member type=\"node\" ref=\"602761125\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238800\" role=\"stop\"/>\n    <member type=\"node\" ref=\"412197994\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238799\" role=\"stop\"/>\n    <member type=\"node\" ref=\"602594097\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238798\" role=\"stop\"/>\n    <member type=\"node\" ref=\"936807872\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1518419859\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4558376856\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238797\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4558370114\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238796\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1278044159\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238795\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1278044435\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238794\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518655443\" role=\"platform\"/>\n    <member type=\"node\" ref=\"265630340\" role=\"stop\"/>\n    <member type=\"node\" ref=\"401273023\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214407\" role=\"stop\"/>\n    <member type=\"node\" ref=\"265634345\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214406\" role=\"stop\"/>\n    <member type=\"node\" ref=\"265634306\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214405\" role=\"stop\"/>\n    <member type=\"node\" ref=\"478821359\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214404\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4062077314\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214403\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4062091875\" role=\"platform\"/>\n    <member type=\"node\" ref=\"151017794\" role=\"stop\"/>\n    <member type=\"node\" ref=\"6149857870\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214402\" role=\"stop\"/>\n    <member type=\"node\" ref=\"608026614\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214401\" role=\"stop\"/>\n    <member type=\"node\" ref=\"478087389\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214400\" role=\"stop\"/>\n    <member type=\"node\" ref=\"478084826\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1000767074\" role=\"stop\"/>\n    <member type=\"node\" ref=\"265634447\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214399\" role=\"stop\"/>\n    <member type=\"node\" ref=\"608018529\" role=\"platform\"/>\n    <member type=\"node\" ref=\"11456133192\" role=\"platform\"/>\n    <member type=\"node\" ref=\"11455507207\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214396\" role=\"stop\"/>\n    <member type=\"node\" ref=\"6149861551\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214395\" role=\"stop\"/>\n    <member type=\"node\" ref=\"608014343\" role=\"platform\"/>\n    <member type=\"node\" ref=\"11282838437\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6325416633\" role=\"stop\"/>\n    <member type=\"node\" ref=\"401275680\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6325416632\" role=\"stop\"/>\n    <member type=\"node\" ref=\"401275697\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6325416631\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5674603284\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6325416630\" role=\"stop\"/>\n    <member type=\"node\" ref=\"289636004\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238785\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477952585\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340234784\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477957886\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6323310490\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477969901\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6323310489\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477974878\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921210\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681212\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776547\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681203\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1736975678\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397899\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776546\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397870\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776545\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268725204\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480843\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607989339\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1538138970\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268724954\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480842\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268728776\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480841\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607995446\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480840\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607996663\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6326926808\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607998796\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1536455200\" role=\"stop\"/>\n    <member type=\"node\" ref=\"59871347\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238295\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5217833121\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238297\" role=\"stop\"/>\n    <member type=\"node\" ref=\"498462819\" role=\"platform\"/>\n    <member type=\"node\" ref=\"282280287\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518655407\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311236983\" role=\"stop\"/>\n    <member type=\"node\" ref=\"38458038\" role=\"platform\"/>\n    <member type=\"node\" ref=\"418340755\" role=\"stop\"/>\n    <member type=\"node\" ref=\"418340775\" role=\"platform\"/>\n    <member type=\"way\" ref=\"57965704\" role=\"\"/>\n    <member type=\"way\" ref=\"206255285\" role=\"\"/>\n    <member type=\"way\" ref=\"75263895\" role=\"\"/>\n    <member type=\"way\" ref=\"75263894\" role=\"\"/>\n    <member type=\"way\" ref=\"25787931\" role=\"\"/>\n    <member type=\"way\" ref=\"284915227\" role=\"\"/>\n    <member type=\"way\" ref=\"413141062\" role=\"\"/>\n    <member type=\"way\" ref=\"6932866\" role=\"\"/>\n    <member type=\"way\" ref=\"413141058\" role=\"\"/>\n    <member type=\"way\" ref=\"413141205\" role=\"\"/>\n    <member type=\"way\" ref=\"1422990690\" role=\"\"/>\n    <member type=\"way\" ref=\"1413429629\" role=\"\"/>\n    <member type=\"way\" ref=\"138497990\" role=\"\"/>\n    <member type=\"way\" ref=\"284914937\" role=\"\"/>\n    <member type=\"way\" ref=\"284914939\" role=\"\"/>\n    <member type=\"way\" ref=\"284913598\" role=\"\"/>\n    <member type=\"way\" ref=\"284913605\" role=\"\"/>\n    <member type=\"way\" ref=\"751180288\" role=\"\"/>\n    <member type=\"way\" ref=\"218074602\" role=\"\"/>\n    <member type=\"way\" ref=\"4492083\" role=\"\"/>\n    <member type=\"way\" ref=\"677107249\" role=\"\"/>\n    <member type=\"way\" ref=\"683604516\" role=\"\"/>\n    <member type=\"way\" ref=\"206228627\" role=\"\"/>\n    <member type=\"way\" ref=\"206245571\" role=\"\"/>\n    <member type=\"way\" ref=\"206245557\" role=\"\"/>\n    <member type=\"way\" ref=\"206245556\" role=\"\"/>\n    <member type=\"way\" ref=\"206245566\" role=\"\"/>\n    <member type=\"way\" ref=\"206245560\" role=\"\"/>\n    <member type=\"way\" ref=\"206249569\" role=\"\"/>\n    <member type=\"way\" ref=\"206245568\" role=\"\"/>\n    <member type=\"way\" ref=\"206249565\" role=\"\"/>\n    <member type=\"way\" ref=\"909257041\" role=\"\"/>\n    <member type=\"way\" ref=\"909257042\" role=\"\"/>\n    <member type=\"way\" ref=\"206246955\" role=\"\"/>\n    <member type=\"way\" ref=\"123432935\" role=\"\"/>\n    <member type=\"way\" ref=\"123770950\" role=\"\"/>\n    <member type=\"way\" ref=\"138498008\" role=\"\"/>\n    <member type=\"way\" ref=\"140582667\" role=\"\"/>\n    <member type=\"way\" ref=\"140582663\" role=\"\"/>\n    <member type=\"way\" ref=\"140582670\" role=\"\"/>\n    <member type=\"way\" ref=\"1422990691\" role=\"\"/>\n    <member type=\"way\" ref=\"140582675\" role=\"\"/>\n    <member type=\"way\" ref=\"140582664\" role=\"\"/>\n    <member type=\"way\" ref=\"47304106\" role=\"\"/>\n    <member type=\"way\" ref=\"24440340\" role=\"\"/>\n    <member type=\"way\" ref=\"689282312\" role=\"\"/>\n    <member type=\"way\" ref=\"140582172\" role=\"\"/>\n    <member type=\"way\" ref=\"24440383\" role=\"\"/>\n    <member type=\"way\" ref=\"24440390\" role=\"\"/>\n    <member type=\"way\" ref=\"39894766\" role=\"\"/>\n    <member type=\"way\" ref=\"533416384\" role=\"\"/>\n    <member type=\"way\" ref=\"403908671\" role=\"\"/>\n    <member type=\"way\" ref=\"403906696\" role=\"\"/>\n    <member type=\"way\" ref=\"403908668\" role=\"\"/>\n    <member type=\"way\" ref=\"688007833\" role=\"\"/>\n    <member type=\"way\" ref=\"403908670\" role=\"\"/>\n    <member type=\"way\" ref=\"403907423\" role=\"\"/>\n    <member type=\"way\" ref=\"403907722\" role=\"\"/>\n    <member type=\"way\" ref=\"403907715\" role=\"\"/>\n    <member type=\"way\" ref=\"403907717\" role=\"\"/>\n    <member type=\"way\" ref=\"403907719\" role=\"\"/>\n    <member type=\"way\" ref=\"403907730\" role=\"\"/>\n    <member type=\"way\" ref=\"403907720\" role=\"\"/>\n    <member type=\"way\" ref=\"15244402\" role=\"\"/>\n    <member type=\"way\" ref=\"42833596\" role=\"\"/>\n    <member type=\"way\" ref=\"1131217134\" role=\"\"/>\n    <member type=\"way\" ref=\"1234282612\" role=\"\"/>\n    <member type=\"way\" ref=\"975471055\" role=\"\"/>\n    <member type=\"way\" ref=\"160715573\" role=\"\"/>\n    <member type=\"way\" ref=\"975471303\" role=\"\"/>\n    <member type=\"way\" ref=\"1017238924\" role=\"\"/>\n    <member type=\"way\" ref=\"143768251\" role=\"\"/>\n    <member type=\"way\" ref=\"687778723\" role=\"\"/>\n    <member type=\"way\" ref=\"143768249\" role=\"\"/>\n    <member type=\"way\" ref=\"81514436\" role=\"\"/>\n    <member type=\"way\" ref=\"81514429\" role=\"\"/>\n    <member type=\"way\" ref=\"81514428\" role=\"\"/>\n    <member type=\"way\" ref=\"1420025268\" role=\"\"/>\n    <member type=\"way\" ref=\"81514437\" role=\"\"/>\n    <member type=\"way\" ref=\"81514438\" role=\"\"/>\n    <member type=\"way\" ref=\"868160510\" role=\"\"/>\n    <member type=\"way\" ref=\"81514431\" role=\"\"/>\n    <member type=\"way\" ref=\"969489026\" role=\"\"/>\n    <member type=\"way\" ref=\"37084600\" role=\"\"/>\n    <member type=\"way\" ref=\"42772684\" role=\"\"/>\n    <member type=\"way\" ref=\"42772685\" role=\"\"/>\n    <member type=\"way\" ref=\"4336451\" role=\"\"/>\n    <member type=\"way\" ref=\"37084631\" role=\"\"/>\n    <member type=\"way\" ref=\"142173485\" role=\"\"/>\n    <member type=\"way\" ref=\"81410202\" role=\"\"/>\n    <member type=\"way\" ref=\"81410208\" role=\"\"/>\n    <member type=\"way\" ref=\"143657813\" role=\"\"/>\n    <member type=\"way\" ref=\"4603649\" role=\"\"/>\n    <member type=\"way\" ref=\"81410195\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420093\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420092\" role=\"\"/>\n    <member type=\"way\" ref=\"23123825\" role=\"\"/>\n    <member type=\"way\" ref=\"171940215\" role=\"\"/>\n    <member type=\"way\" ref=\"42772687\" role=\"\"/>\n    <member type=\"way\" ref=\"171940224\" role=\"\"/>\n    <member type=\"way\" ref=\"4603651\" role=\"\"/>\n    <member type=\"way\" ref=\"24598080\" role=\"\"/>\n    <member type=\"way\" ref=\"1287292087\" role=\"\"/>\n    <member type=\"way\" ref=\"171940206\" role=\"\"/>\n    <member type=\"way\" ref=\"33704322\" role=\"\"/>\n    <member type=\"way\" ref=\"24598088\" role=\"\"/>\n    <member type=\"way\" ref=\"130713063\" role=\"\"/>\n    <member type=\"way\" ref=\"138483059\" role=\"\"/>\n    <member type=\"way\" ref=\"130340906\" role=\"\"/>\n    <member type=\"way\" ref=\"140383815\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663436\" role=\"\"/>\n    <member type=\"way\" ref=\"970098668\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663439\" role=\"\"/>\n    <member type=\"way\" ref=\"424274558\" role=\"\"/>\n    <member type=\"way\" ref=\"920265971\" role=\"\"/>\n    <member type=\"way\" ref=\"426618722\" role=\"\"/>\n    <member type=\"way\" ref=\"140383770\" role=\"\"/>\n    <member type=\"way\" ref=\"140383802\" role=\"\"/>\n    <member type=\"way\" ref=\"140383819\" role=\"\"/>\n    <member type=\"way\" ref=\"140383835\" role=\"\"/>\n    <member type=\"way\" ref=\"140383787\" role=\"\"/>\n    <member type=\"way\" ref=\"140383778\" role=\"\"/>\n    <member type=\"way\" ref=\"140383767\" role=\"\"/>\n    <member type=\"way\" ref=\"140542860\" role=\"\"/>\n    <member type=\"way\" ref=\"140542868\" role=\"\"/>\n    <member type=\"way\" ref=\"587513738\" role=\"\"/>\n    <member type=\"way\" ref=\"140542929\" role=\"\"/>\n    <member type=\"way\" ref=\"140542922\" role=\"\"/>\n    <member type=\"way\" ref=\"140542898\" role=\"\"/>\n    <member type=\"way\" ref=\"140542943\" role=\"\"/>\n    <member type=\"way\" ref=\"140542926\" role=\"\"/>\n    <member type=\"way\" ref=\"140542915\" role=\"\"/>\n    <member type=\"way\" ref=\"140542902\" role=\"\"/>\n    <member type=\"way\" ref=\"140542940\" role=\"\"/>\n    <member type=\"way\" ref=\"140542896\" role=\"\"/>\n    <member type=\"way\" ref=\"140554200\" role=\"\"/>\n    <member type=\"way\" ref=\"140554202\" role=\"\"/>\n    <member type=\"way\" ref=\"140554194\" role=\"\"/>\n    <member type=\"way\" ref=\"140554196\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880042\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880041\" role=\"\"/>\n    <member type=\"way\" ref=\"15092066\" role=\"\"/>\n    <member type=\"way\" ref=\"15092074\" role=\"\"/>\n    <member type=\"way\" ref=\"116239838\" role=\"\"/>\n    <member type=\"way\" ref=\"116239840\" role=\"\"/>\n    <member type=\"way\" ref=\"116240538\" role=\"\"/>\n    <member type=\"way\" ref=\"138498004\" role=\"\"/>\n    <member type=\"way\" ref=\"57014731\" role=\"\"/>\n    <member type=\"way\" ref=\"140235641\" role=\"\"/>\n    <member type=\"way\" ref=\"225963264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494569\" role=\"\"/>\n    <member type=\"way\" ref=\"830404282\" role=\"\"/>\n    <member type=\"way\" ref=\"140233264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494557\" role=\"\"/>\n    <member type=\"way\" ref=\"140233256\" role=\"\"/>\n    <member type=\"way\" ref=\"830404278\" role=\"\"/>\n    <member type=\"way\" ref=\"140233230\" role=\"\"/>\n    <member type=\"way\" ref=\"830404279\" role=\"\"/>\n    <member type=\"way\" ref=\"140236948\" role=\"\"/>\n    <member type=\"way\" ref=\"140236955\" role=\"\"/>\n    <member type=\"way\" ref=\"151584548\" role=\"\"/>\n    <member type=\"way\" ref=\"140236953\" role=\"\"/>\n    <member type=\"way\" ref=\"140236962\" role=\"\"/>\n    <member type=\"way\" ref=\"140236949\" role=\"\"/>\n    <member type=\"way\" ref=\"140236965\" role=\"\"/>\n    <member type=\"way\" ref=\"140244131\" role=\"\"/>\n    <member type=\"way\" ref=\"140244135\" role=\"\"/>\n    <member type=\"way\" ref=\"675643796\" role=\"\"/>\n    <member type=\"way\" ref=\"4297979\" role=\"\"/>\n    <member type=\"way\" ref=\"1190383233\" role=\"\"/>\n    <member type=\"way\" ref=\"35780798\" role=\"\"/>\n    <member type=\"way\" ref=\"35778369\" role=\"\"/>\n    <member type=\"way\" ref=\"5367923\" role=\"\"/>\n    <member type=\"way\" ref=\"245443271\" role=\"\"/>\n    <member type=\"way\" ref=\"42833597\" role=\"\"/>\n    <member type=\"way\" ref=\"784595993\" role=\"\"/>\n    <member type=\"way\" ref=\"784595985\" role=\"\"/>\n    <member type=\"way\" ref=\"35778355\" role=\"\"/>\n    <member type=\"way\" ref=\"1190763596\" role=\"\"/>\n    <member type=\"way\" ref=\"35778631\" role=\"\"/>\n    <member type=\"way\" ref=\"35780807\" role=\"\"/>\n    <member type=\"way\" ref=\"674486885\" role=\"\"/>\n    <member type=\"way\" ref=\"675789780\" role=\"\"/>\n    <member type=\"way\" ref=\"5368856\" role=\"\"/>\n    <member type=\"way\" ref=\"673453969\" role=\"\"/>\n    <member type=\"way\" ref=\"1263181142\" role=\"\"/>\n    <member type=\"way\" ref=\"586652862\" role=\"\"/>\n    <tag k=\"colour\" v=\"#007AC9\"/>\n    <tag k=\"from\" v=\"Serena\"/>\n    <tag k=\"name\" v=\"239V Serena–Kalajärvi–Perusmäki–Viherlaakso–Leppävaara\"/>\n    <tag k=\"network\" v=\"HSL\"/>\n    <tag k=\"network:wikidata\" v=\"Q473211\"/>\n    <tag k=\"network:wikipedia\" v=\"fi:Helsingin seudun liikenne\"/>\n    <tag k=\"public_transport:version\" v=\"2\"/>\n    <tag k=\"ref\" v=\"239V\"/>\n    <tag k=\"route\" v=\"bus\"/>\n    <tag k=\"to\" v=\"Leppävaara\"/>\n    <tag k=\"type\" v=\"route\"/>\n  </relation>\n  <relation id=\"9401193\" version=\"32\" timestamp=\"2026-01-15T17:38:00Z\" changeset=\"177268542\" uid=\"5258545\" user=\"j numminen\">\n    <member type=\"node\" ref=\"719270277\" role=\"stop\"/>\n    <member type=\"node\" ref=\"33588480\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238805\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4144754027\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238804\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4144741240\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238803\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5731754569\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238802\" role=\"stop\"/>\n    <member type=\"node\" ref=\"923141718\" role=\"platform\"/>\n    <member type=\"node\" ref=\"2886121555\" role=\"stop\"/>\n    <member type=\"node\" ref=\"2162345664\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238801\" role=\"stop\"/>\n    <member type=\"node\" ref=\"602761125\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238800\" role=\"stop\"/>\n    <member type=\"node\" ref=\"412197994\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238799\" role=\"stop\"/>\n    <member type=\"node\" ref=\"602594097\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238798\" role=\"stop\"/>\n    <member type=\"node\" ref=\"936807872\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1518419859\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4558376856\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238797\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4558370114\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238796\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1278044159\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238795\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1278044435\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238794\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518655443\" role=\"platform\"/>\n    <member type=\"node\" ref=\"265630340\" role=\"stop\"/>\n    <member type=\"node\" ref=\"401273023\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214407\" role=\"stop\"/>\n    <member type=\"node\" ref=\"265634345\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214406\" role=\"stop\"/>\n    <member type=\"node\" ref=\"265634306\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214405\" role=\"stop\"/>\n    <member type=\"node\" ref=\"478821359\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214404\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4062077314\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214403\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4062091875\" role=\"platform\"/>\n    <member type=\"node\" ref=\"151017794\" role=\"stop\"/>\n    <member type=\"node\" ref=\"6149857870\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214402\" role=\"stop\"/>\n    <member type=\"node\" ref=\"608026614\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214401\" role=\"stop\"/>\n    <member type=\"node\" ref=\"478087389\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214400\" role=\"stop\"/>\n    <member type=\"node\" ref=\"478084826\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1000767074\" role=\"stop\"/>\n    <member type=\"node\" ref=\"265634447\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214399\" role=\"stop\"/>\n    <member type=\"node\" ref=\"608018529\" role=\"platform\"/>\n    <member type=\"node\" ref=\"11456133192\" role=\"platform\"/>\n    <member type=\"node\" ref=\"11455507207\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214396\" role=\"stop\"/>\n    <member type=\"node\" ref=\"6149861551\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6341214395\" role=\"stop\"/>\n    <member type=\"node\" ref=\"608014343\" role=\"platform\"/>\n    <member type=\"node\" ref=\"11282838437\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238790\" role=\"stop\"/>\n    <member type=\"node\" ref=\"431057003\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238789\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607975645\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238788\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607976552\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238787\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518478314\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1539976495\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607977376\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238786\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1539976308\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340238785\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477952585\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6340234784\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477957886\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6323310490\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477969901\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6323310489\" role=\"stop\"/>\n    <member type=\"node\" ref=\"477974878\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921210\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681212\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776547\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681203\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1736975678\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397899\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776546\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397870\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776545\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268725204\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480843\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607989339\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1538138970\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268724954\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480842\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268728776\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480841\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607995446\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480840\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607996663\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6326926808\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607998796\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1536455200\" role=\"stop\"/>\n    <member type=\"node\" ref=\"59871347\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238295\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5217833121\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238297\" role=\"stop\"/>\n    <member type=\"node\" ref=\"498462819\" role=\"platform\"/>\n    <member type=\"node\" ref=\"282280287\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518655407\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311236983\" role=\"stop\"/>\n    <member type=\"node\" ref=\"38458038\" role=\"platform\"/>\n    <member type=\"node\" ref=\"418340755\" role=\"stop\"/>\n    <member type=\"node\" ref=\"418340775\" role=\"platform\"/>\n    <member type=\"way\" ref=\"57965704\" role=\"\"/>\n    <member type=\"way\" ref=\"206255285\" role=\"\"/>\n    <member type=\"way\" ref=\"75263895\" role=\"\"/>\n    <member type=\"way\" ref=\"75263894\" role=\"\"/>\n    <member type=\"way\" ref=\"25787931\" role=\"\"/>\n    <member type=\"way\" ref=\"284915227\" role=\"\"/>\n    <member type=\"way\" ref=\"413141062\" role=\"\"/>\n    <member type=\"way\" ref=\"6932866\" role=\"\"/>\n    <member type=\"way\" ref=\"413141058\" role=\"\"/>\n    <member type=\"way\" ref=\"413141205\" role=\"\"/>\n    <member type=\"way\" ref=\"1422990690\" role=\"\"/>\n    <member type=\"way\" ref=\"1413429629\" role=\"\"/>\n    <member type=\"way\" ref=\"138497990\" role=\"\"/>\n    <member type=\"way\" ref=\"284914937\" role=\"\"/>\n    <member type=\"way\" ref=\"284914939\" role=\"\"/>\n    <member type=\"way\" ref=\"284913598\" role=\"\"/>\n    <member type=\"way\" ref=\"284913605\" role=\"\"/>\n    <member type=\"way\" ref=\"751180288\" role=\"\"/>\n    <member type=\"way\" ref=\"218074602\" role=\"\"/>\n    <member type=\"way\" ref=\"4492083\" role=\"\"/>\n    <member type=\"way\" ref=\"677107249\" role=\"\"/>\n    <member type=\"way\" ref=\"683604516\" role=\"\"/>\n    <member type=\"way\" ref=\"206228627\" role=\"\"/>\n    <member type=\"way\" ref=\"206245571\" role=\"\"/>\n    <member type=\"way\" ref=\"206245557\" role=\"\"/>\n    <member type=\"way\" ref=\"206245556\" role=\"\"/>\n    <member type=\"way\" ref=\"206245566\" role=\"\"/>\n    <member type=\"way\" ref=\"206245560\" role=\"\"/>\n    <member type=\"way\" ref=\"206249569\" role=\"\"/>\n    <member type=\"way\" ref=\"206245568\" role=\"\"/>\n    <member type=\"way\" ref=\"206249565\" role=\"\"/>\n    <member type=\"way\" ref=\"909257041\" role=\"\"/>\n    <member type=\"way\" ref=\"909257042\" role=\"\"/>\n    <member type=\"way\" ref=\"206246955\" role=\"\"/>\n    <member type=\"way\" ref=\"123432935\" role=\"\"/>\n    <member type=\"way\" ref=\"123770950\" role=\"\"/>\n    <member type=\"way\" ref=\"138498008\" role=\"\"/>\n    <member type=\"way\" ref=\"140582667\" role=\"\"/>\n    <member type=\"way\" ref=\"140582663\" role=\"\"/>\n    <member type=\"way\" ref=\"140582670\" role=\"\"/>\n    <member type=\"way\" ref=\"1422990691\" role=\"\"/>\n    <member type=\"way\" ref=\"140582675\" role=\"\"/>\n    <member type=\"way\" ref=\"140582664\" role=\"\"/>\n    <member type=\"way\" ref=\"47304106\" role=\"\"/>\n    <member type=\"way\" ref=\"24440340\" role=\"\"/>\n    <member type=\"way\" ref=\"689282312\" role=\"\"/>\n    <member type=\"way\" ref=\"140582172\" role=\"\"/>\n    <member type=\"way\" ref=\"24440383\" role=\"\"/>\n    <member type=\"way\" ref=\"24440390\" role=\"\"/>\n    <member type=\"way\" ref=\"39894766\" role=\"\"/>\n    <member type=\"way\" ref=\"533416384\" role=\"\"/>\n    <member type=\"way\" ref=\"403908671\" role=\"\"/>\n    <member type=\"way\" ref=\"403906696\" role=\"\"/>\n    <member type=\"way\" ref=\"403908668\" role=\"\"/>\n    <member type=\"way\" ref=\"688007833\" role=\"\"/>\n    <member type=\"way\" ref=\"403908670\" role=\"\"/>\n    <member type=\"way\" ref=\"403907423\" role=\"\"/>\n    <member type=\"way\" ref=\"403907722\" role=\"\"/>\n    <member type=\"way\" ref=\"403907715\" role=\"\"/>\n    <member type=\"way\" ref=\"403907717\" role=\"\"/>\n    <member type=\"way\" ref=\"403907719\" role=\"\"/>\n    <member type=\"way\" ref=\"403907730\" role=\"\"/>\n    <member type=\"way\" ref=\"403907720\" role=\"\"/>\n    <member type=\"way\" ref=\"15244402\" role=\"\"/>\n    <member type=\"way\" ref=\"42833596\" role=\"\"/>\n    <member type=\"way\" ref=\"1131217134\" role=\"\"/>\n    <member type=\"way\" ref=\"1234282612\" role=\"\"/>\n    <member type=\"way\" ref=\"975471055\" role=\"\"/>\n    <member type=\"way\" ref=\"160715573\" role=\"\"/>\n    <member type=\"way\" ref=\"975471303\" role=\"\"/>\n    <member type=\"way\" ref=\"1017238924\" role=\"\"/>\n    <member type=\"way\" ref=\"4614564\" role=\"\"/>\n    <member type=\"way\" ref=\"17375278\" role=\"\"/>\n    <member type=\"way\" ref=\"17375280\" role=\"\"/>\n    <member type=\"way\" ref=\"4614565\" role=\"\"/>\n    <member type=\"way\" ref=\"122821339\" role=\"\"/>\n    <member type=\"way\" ref=\"140580331\" role=\"\"/>\n    <member type=\"way\" ref=\"140580330\" role=\"\"/>\n    <member type=\"way\" ref=\"934048496\" role=\"\"/>\n    <member type=\"way\" ref=\"6881756\" role=\"\"/>\n    <member type=\"way\" ref=\"138483063\" role=\"\"/>\n    <member type=\"way\" ref=\"934048495\" role=\"\"/>\n    <member type=\"way\" ref=\"138483061\" role=\"\"/>\n    <member type=\"way\" ref=\"969489027\" role=\"\"/>\n    <member type=\"way\" ref=\"969489026\" role=\"\"/>\n    <member type=\"way\" ref=\"37084600\" role=\"\"/>\n    <member type=\"way\" ref=\"42772684\" role=\"\"/>\n    <member type=\"way\" ref=\"42772685\" role=\"\"/>\n    <member type=\"way\" ref=\"4336451\" role=\"\"/>\n    <member type=\"way\" ref=\"37084631\" role=\"\"/>\n    <member type=\"way\" ref=\"142173485\" role=\"\"/>\n    <member type=\"way\" ref=\"81410202\" role=\"\"/>\n    <member type=\"way\" ref=\"81410208\" role=\"\"/>\n    <member type=\"way\" ref=\"143657813\" role=\"\"/>\n    <member type=\"way\" ref=\"4603649\" role=\"\"/>\n    <member type=\"way\" ref=\"81410195\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420093\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420092\" role=\"\"/>\n    <member type=\"way\" ref=\"23123825\" role=\"\"/>\n    <member type=\"way\" ref=\"171940215\" role=\"\"/>\n    <member type=\"way\" ref=\"42772687\" role=\"\"/>\n    <member type=\"way\" ref=\"171940224\" role=\"\"/>\n    <member type=\"way\" ref=\"4603651\" role=\"\"/>\n    <member type=\"way\" ref=\"24598080\" role=\"\"/>\n    <member type=\"way\" ref=\"1287292087\" role=\"\"/>\n    <member type=\"way\" ref=\"171940206\" role=\"\"/>\n    <member type=\"way\" ref=\"33704322\" role=\"\"/>\n    <member type=\"way\" ref=\"24598088\" role=\"\"/>\n    <member type=\"way\" ref=\"130713063\" role=\"\"/>\n    <member type=\"way\" ref=\"138483059\" role=\"\"/>\n    <member type=\"way\" ref=\"130340906\" role=\"\"/>\n    <member type=\"way\" ref=\"140383815\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663436\" role=\"\"/>\n    <member type=\"way\" ref=\"970098668\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663439\" role=\"\"/>\n    <member type=\"way\" ref=\"424274558\" role=\"\"/>\n    <member type=\"way\" ref=\"920265971\" role=\"\"/>\n    <member type=\"way\" ref=\"426618722\" role=\"\"/>\n    <member type=\"way\" ref=\"140383770\" role=\"\"/>\n    <member type=\"way\" ref=\"140383802\" role=\"\"/>\n    <member type=\"way\" ref=\"140383819\" role=\"\"/>\n    <member type=\"way\" ref=\"140383835\" role=\"\"/>\n    <member type=\"way\" ref=\"140383787\" role=\"\"/>\n    <member type=\"way\" ref=\"140383778\" role=\"\"/>\n    <member type=\"way\" ref=\"140383767\" role=\"\"/>\n    <member type=\"way\" ref=\"140542860\" role=\"\"/>\n    <member type=\"way\" ref=\"140542868\" role=\"\"/>\n    <member type=\"way\" ref=\"587513738\" role=\"\"/>\n    <member type=\"way\" ref=\"140542929\" role=\"\"/>\n    <member type=\"way\" ref=\"140542922\" role=\"\"/>\n    <member type=\"way\" ref=\"140542898\" role=\"\"/>\n    <member type=\"way\" ref=\"140542943\" role=\"\"/>\n    <member type=\"way\" ref=\"140542926\" role=\"\"/>\n    <member type=\"way\" ref=\"140542915\" role=\"\"/>\n    <member type=\"way\" ref=\"140542902\" role=\"\"/>\n    <member type=\"way\" ref=\"140542940\" role=\"\"/>\n    <member type=\"way\" ref=\"140542896\" role=\"\"/>\n    <member type=\"way\" ref=\"140554200\" role=\"\"/>\n    <member type=\"way\" ref=\"140554202\" role=\"\"/>\n    <member type=\"way\" ref=\"140554194\" role=\"\"/>\n    <member type=\"way\" ref=\"140554196\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880042\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880041\" role=\"\"/>\n    <member type=\"way\" ref=\"15092066\" role=\"\"/>\n    <member type=\"way\" ref=\"15092074\" role=\"\"/>\n    <member type=\"way\" ref=\"116239838\" role=\"\"/>\n    <member type=\"way\" ref=\"116239840\" role=\"\"/>\n    <member type=\"way\" ref=\"116240538\" role=\"\"/>\n    <member type=\"way\" ref=\"138498004\" role=\"\"/>\n    <member type=\"way\" ref=\"57014731\" role=\"\"/>\n    <member type=\"way\" ref=\"140235641\" role=\"\"/>\n    <member type=\"way\" ref=\"225963264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494569\" role=\"\"/>\n    <member type=\"way\" ref=\"830404282\" role=\"\"/>\n    <member type=\"way\" ref=\"140233264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494557\" role=\"\"/>\n    <member type=\"way\" ref=\"140233256\" role=\"\"/>\n    <member type=\"way\" ref=\"830404278\" role=\"\"/>\n    <member type=\"way\" ref=\"140233230\" role=\"\"/>\n    <member type=\"way\" ref=\"830404279\" role=\"\"/>\n    <member type=\"way\" ref=\"140236948\" role=\"\"/>\n    <member type=\"way\" ref=\"140236955\" role=\"\"/>\n    <member type=\"way\" ref=\"151584548\" role=\"\"/>\n    <member type=\"way\" ref=\"140236953\" role=\"\"/>\n    <member type=\"way\" ref=\"140236962\" role=\"\"/>\n    <member type=\"way\" ref=\"140236949\" role=\"\"/>\n    <member type=\"way\" ref=\"140236965\" role=\"\"/>\n    <member type=\"way\" ref=\"140244131\" role=\"\"/>\n    <member type=\"way\" ref=\"140244135\" role=\"\"/>\n    <member type=\"way\" ref=\"675643796\" role=\"\"/>\n    <member type=\"way\" ref=\"4297979\" role=\"\"/>\n    <member type=\"way\" ref=\"1190383233\" role=\"\"/>\n    <member type=\"way\" ref=\"35780798\" role=\"\"/>\n    <member type=\"way\" ref=\"35778369\" role=\"\"/>\n    <member type=\"way\" ref=\"5367923\" role=\"\"/>\n    <member type=\"way\" ref=\"245443271\" role=\"\"/>\n    <member type=\"way\" ref=\"42833597\" role=\"\"/>\n    <member type=\"way\" ref=\"784595993\" role=\"\"/>\n    <member type=\"way\" ref=\"784595985\" role=\"\"/>\n    <member type=\"way\" ref=\"35778355\" role=\"\"/>\n    <member type=\"way\" ref=\"1190763596\" role=\"\"/>\n    <member type=\"way\" ref=\"35778631\" role=\"\"/>\n    <member type=\"way\" ref=\"35780807\" role=\"\"/>\n    <member type=\"way\" ref=\"674486885\" role=\"\"/>\n    <member type=\"way\" ref=\"675789780\" role=\"\"/>\n    <member type=\"way\" ref=\"5368856\" role=\"\"/>\n    <member type=\"way\" ref=\"673453969\" role=\"\"/>\n    <member type=\"way\" ref=\"1263181142\" role=\"\"/>\n    <member type=\"way\" ref=\"586652862\" role=\"\"/>\n    <tag k=\"colour\" v=\"#007AC9\"/>\n    <tag k=\"from\" v=\"Serena\"/>\n    <tag k=\"name\" v=\"239 Serena–Kalajärvi–Perusmäki–Viherlaakso–Leppävaara\"/>\n    <tag k=\"network\" v=\"HSL\"/>\n    <tag k=\"network:wikidata\" v=\"Q473211\"/>\n    <tag k=\"network:wikipedia\" v=\"fi:Helsingin seudun liikenne\"/>\n    <tag k=\"public_transport:version\" v=\"2\"/>\n    <tag k=\"ref\" v=\"239\"/>\n    <tag k=\"route\" v=\"bus\"/>\n    <tag k=\"to\" v=\"Leppävaara\"/>\n    <tag k=\"type\" v=\"route\"/>\n  </relation>\n  <relation id=\"10904594\" version=\"15\" timestamp=\"2025-08-09T16:45:31Z\" changeset=\"170211882\" uid=\"5258545\" user=\"j numminen\">\n    <member type=\"node\" ref=\"249558270\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4810166310\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921214\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4810166314\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921213\" role=\"stop\"/>\n    <member type=\"node\" ref=\"711445602\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921212\" role=\"stop\"/>\n    <member type=\"node\" ref=\"4810166311\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921211\" role=\"stop\"/>\n    <member type=\"node\" ref=\"948718417\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320921210\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681212\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776547\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1519681203\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1736975678\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397899\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776546\" role=\"stop\"/>\n    <member type=\"node\" ref=\"267397870\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6314776545\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268725204\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480843\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607989339\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1538138970\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268724954\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480842\" role=\"stop\"/>\n    <member type=\"node\" ref=\"268728776\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480841\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607995446\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6320480840\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607996663\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6326926808\" role=\"stop\"/>\n    <member type=\"node\" ref=\"607998796\" role=\"platform\"/>\n    <member type=\"node\" ref=\"1536455200\" role=\"stop\"/>\n    <member type=\"node\" ref=\"59871347\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238295\" role=\"stop\"/>\n    <member type=\"node\" ref=\"5217833121\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311238297\" role=\"stop\"/>\n    <member type=\"node\" ref=\"498462819\" role=\"platform\"/>\n    <member type=\"node\" ref=\"282280287\" role=\"stop\"/>\n    <member type=\"node\" ref=\"1518655407\" role=\"platform\"/>\n    <member type=\"node\" ref=\"6311236983\" role=\"stop\"/>\n    <member type=\"node\" ref=\"38458038\" role=\"platform\"/>\n    <member type=\"node\" ref=\"418340755\" role=\"stop\"/>\n    <member type=\"node\" ref=\"418340775\" role=\"platform\"/>\n    <member type=\"way\" ref=\"138587885\" role=\"\"/>\n    <member type=\"way\" ref=\"4336514\" role=\"\"/>\n    <member type=\"way\" ref=\"4336496\" role=\"\"/>\n    <member type=\"way\" ref=\"81421217\" role=\"\"/>\n    <member type=\"way\" ref=\"332021864\" role=\"\"/>\n    <member type=\"way\" ref=\"85692740\" role=\"\"/>\n    <member type=\"way\" ref=\"4603648\" role=\"\"/>\n    <member type=\"way\" ref=\"17369042\" role=\"\"/>\n    <member type=\"way\" ref=\"32469880\" role=\"\"/>\n    <member type=\"way\" ref=\"85692739\" role=\"\"/>\n    <member type=\"way\" ref=\"81421247\" role=\"\"/>\n    <member type=\"way\" ref=\"42774319\" role=\"\"/>\n    <member type=\"way\" ref=\"4603649\" role=\"\"/>\n    <member type=\"way\" ref=\"81410195\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420093\" role=\"\"/>\n    <member type=\"way\" ref=\"1187420092\" role=\"\"/>\n    <member type=\"way\" ref=\"23123825\" role=\"\"/>\n    <member type=\"way\" ref=\"171940215\" role=\"\"/>\n    <member type=\"way\" ref=\"42772687\" role=\"\"/>\n    <member type=\"way\" ref=\"171940224\" role=\"\"/>\n    <member type=\"way\" ref=\"4603651\" role=\"\"/>\n    <member type=\"way\" ref=\"24598080\" role=\"\"/>\n    <member type=\"way\" ref=\"1287292087\" role=\"\"/>\n    <member type=\"way\" ref=\"171940206\" role=\"\"/>\n    <member type=\"way\" ref=\"33704322\" role=\"\"/>\n    <member type=\"way\" ref=\"24598088\" role=\"\"/>\n    <member type=\"way\" ref=\"130713063\" role=\"\"/>\n    <member type=\"way\" ref=\"138483059\" role=\"\"/>\n    <member type=\"way\" ref=\"130340906\" role=\"\"/>\n    <member type=\"way\" ref=\"140383815\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663436\" role=\"\"/>\n    <member type=\"way\" ref=\"970098668\" role=\"\"/>\n    <member type=\"way\" ref=\"1194663439\" role=\"\"/>\n    <member type=\"way\" ref=\"424274558\" role=\"\"/>\n    <member type=\"way\" ref=\"920265971\" role=\"\"/>\n    <member type=\"way\" ref=\"426618722\" role=\"\"/>\n    <member type=\"way\" ref=\"140383770\" role=\"\"/>\n    <member type=\"way\" ref=\"140383802\" role=\"\"/>\n    <member type=\"way\" ref=\"140383819\" role=\"\"/>\n    <member type=\"way\" ref=\"140383835\" role=\"\"/>\n    <member type=\"way\" ref=\"140383787\" role=\"\"/>\n    <member type=\"way\" ref=\"140383778\" role=\"\"/>\n    <member type=\"way\" ref=\"140383767\" role=\"\"/>\n    <member type=\"way\" ref=\"140542860\" role=\"\"/>\n    <member type=\"way\" ref=\"140542868\" role=\"\"/>\n    <member type=\"way\" ref=\"587513738\" role=\"\"/>\n    <member type=\"way\" ref=\"140542929\" role=\"\"/>\n    <member type=\"way\" ref=\"140542922\" role=\"\"/>\n    <member type=\"way\" ref=\"140542898\" role=\"\"/>\n    <member type=\"way\" ref=\"140542943\" role=\"\"/>\n    <member type=\"way\" ref=\"140542926\" role=\"\"/>\n    <member type=\"way\" ref=\"140542915\" role=\"\"/>\n    <member type=\"way\" ref=\"140542902\" role=\"\"/>\n    <member type=\"way\" ref=\"140542940\" role=\"\"/>\n    <member type=\"way\" ref=\"140542896\" role=\"\"/>\n    <member type=\"way\" ref=\"140554200\" role=\"\"/>\n    <member type=\"way\" ref=\"140554202\" role=\"\"/>\n    <member type=\"way\" ref=\"140554194\" role=\"\"/>\n    <member type=\"way\" ref=\"140554196\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880042\" role=\"\"/>\n    <member type=\"way\" ref=\"1291880041\" role=\"\"/>\n    <member type=\"way\" ref=\"15092066\" role=\"\"/>\n    <member type=\"way\" ref=\"15092074\" role=\"\"/>\n    <member type=\"way\" ref=\"116239838\" role=\"\"/>\n    <member type=\"way\" ref=\"116239840\" role=\"\"/>\n    <member type=\"way\" ref=\"116240538\" role=\"\"/>\n    <member type=\"way\" ref=\"138498004\" role=\"\"/>\n    <member type=\"way\" ref=\"57014731\" role=\"\"/>\n    <member type=\"way\" ref=\"140235641\" role=\"\"/>\n    <member type=\"way\" ref=\"225963264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494569\" role=\"\"/>\n    <member type=\"way\" ref=\"830404282\" role=\"\"/>\n    <member type=\"way\" ref=\"140233264\" role=\"\"/>\n    <member type=\"way\" ref=\"143494557\" role=\"\"/>\n    <member type=\"way\" ref=\"140233256\" role=\"\"/>\n    <member type=\"way\" ref=\"830404278\" role=\"\"/>\n    <member type=\"way\" ref=\"140233230\" role=\"\"/>\n    <member type=\"way\" ref=\"830404279\" role=\"\"/>\n    <member type=\"way\" ref=\"140236948\" role=\"\"/>\n    <member type=\"way\" ref=\"140236955\" role=\"\"/>\n    <member type=\"way\" ref=\"151584548\" role=\"\"/>\n    <member type=\"way\" ref=\"140236953\" role=\"\"/>\n    <member type=\"way\" ref=\"140236962\" role=\"\"/>\n    <member type=\"way\" ref=\"140236949\" role=\"\"/>\n    <member type=\"way\" ref=\"140236965\" role=\"\"/>\n    <member type=\"way\" ref=\"140244131\" role=\"\"/>\n    <member type=\"way\" ref=\"140244135\" role=\"\"/>\n    <member type=\"way\" ref=\"675643796\" role=\"\"/>\n    <member type=\"way\" ref=\"4297979\" role=\"\"/>\n    <member type=\"way\" ref=\"1190383233\" role=\"\"/>\n    <member type=\"way\" ref=\"35780798\" role=\"\"/>\n    <member type=\"way\" ref=\"35778369\" role=\"\"/>\n    <member type=\"way\" ref=\"5367923\" role=\"\"/>\n    <member type=\"way\" ref=\"245443271\" role=\"\"/>\n    <member type=\"way\" ref=\"42833597\" role=\"\"/>\n    <member type=\"way\" ref=\"784595993\" role=\"\"/>\n    <member type=\"way\" ref=\"784595985\" role=\"\"/>\n    <member type=\"way\" ref=\"35778355\" role=\"\"/>\n    <member type=\"way\" ref=\"1190763596\" role=\"\"/>\n    <member type=\"way\" ref=\"35778631\" role=\"\"/>\n    <member type=\"way\" ref=\"35780807\" role=\"\"/>\n    <member type=\"way\" ref=\"674486885\" role=\"\"/>\n    <member type=\"way\" ref=\"675789780\" role=\"\"/>\n    <member type=\"way\" ref=\"5368856\" role=\"\"/>\n    <member type=\"way\" ref=\"673453969\" role=\"\"/>\n    <member type=\"way\" ref=\"1263181142\" role=\"\"/>\n    <member type=\"way\" ref=\"586652862\" role=\"\"/>\n    <tag k=\"from\" v=\"Högnäs\"/>\n    <tag k=\"name\" v=\"237V Högnäs–Viherlaakso–Leppävaara\"/>\n    <tag k=\"network\" v=\"HSL\"/>\n    <tag k=\"network:wikidata\" v=\"Q473211\"/>\n    <tag k=\"public_transport:version\" v=\"2\"/>\n    <tag k=\"ref\" v=\"237V\"/>\n    <tag k=\"route\" v=\"bus\"/>\n    <tag k=\"to\" v=\"Leppävaara\"/>\n    <tag k=\"type\" v=\"route\"/>\n  </relation>\n\n</osm>\n"
  },
  {
    "path": "test/e2e/README.md",
    "content": "# UI Regression: Area Settings Persistence\n\nThis end-to-end regression test automates the Touch Mapper UI flow:\n\n1. Open address search\n2. Search for a real address\n3. Change map settings\n4. Create a map\n5. Return to the settings page\n6. Verify settings persisted\n\nThe test includes an explicit assertion for `mapSizePreset` persistence.\n\n## Run\n\nFrom repo root:\n\n```bash\nbash test/e2e/run-touch-mapper-settings-regression.sh\n```\n\nOptions are passed through to the Node runner:\n\n```bash\nbash test/e2e/run-touch-mapper-settings-regression.sh \\\n  --base-url https://test.touch-mapper.org \\\n  --address \"Helsinki Central Railway Station\"\n```\n\nUse `--headed` to run with a visible browser.\n\n## Output\n\nArtifacts are written under `.tmp/e2e/settings-regression-<timestamp>/`:\n\n- `01-area-initial.png`\n- `02-area-configured.png`\n- `03-map-created.png`\n- `04-area-returned.png`\n- `report.json` (or `report-error.json` on crash)\n"
  },
  {
    "path": "test/e2e/run-touch-mapper-settings-regression.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nrepo_root=\"$(cd \"$(dirname \"$0\")/../..\" && pwd)\"\nruntime_dir=\"$repo_root/.tmp/e2e-playwright-runtime\"\nscript_path=\"$repo_root/test/e2e/touch-mapper-settings-regression.js\"\n\nmkdir -p \"$runtime_dir\"\n\nif [[ ! -f \"$runtime_dir/package.json\" ]]; then\n  cat > \"$runtime_dir/package.json\" <<'JSON'\n{\n  \"name\": \"touch-mapper-e2e-runtime\",\n  \"private\": true,\n  \"version\": \"1.0.0\"\n}\nJSON\nfi\n\nif [[ ! -d \"$runtime_dir/node_modules/playwright\" ]]; then\n  echo \"Installing Playwright into .tmp runtime...\"\n  (cd \"$runtime_dir\" && npm install --no-audit --no-fund playwright@1.58.2)\nfi\n\nif [[ ! -d \"$HOME/.cache/ms-playwright/chromium-1208\" ]]; then\n  echo \"Installing Playwright Chromium browser...\"\n  (cd \"$runtime_dir\" && ./node_modules/.bin/playwright install chromium)\nfi\n\nNODE_PATH=\"$runtime_dir/node_modules\" node \"$script_path\" \"$@\"\n"
  },
  {
    "path": "test/e2e/touch-mapper-settings-regression.js",
    "content": "#!/usr/bin/env node\n\"use strict\";\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { chromium } = require(\"playwright\");\n\nfunction parseArgs(argv) {\n  const opts = {\n    baseUrl: \"https://test.touch-mapper.org\",\n    address: \"Helsinki Central Railway Station\",\n    headless: true,\n    timeoutMs: 420000\n  };\n\n  for (let i = 0; i < argv.length; i += 1) {\n    const arg = argv[i];\n    if (arg === \"--base-url\") {\n      opts.baseUrl = argv[i + 1];\n      i += 1;\n    } else if (arg === \"--address\") {\n      opts.address = argv[i + 1];\n      i += 1;\n    } else if (arg === \"--headed\") {\n      opts.headless = false;\n    } else if (arg === \"--timeout-ms\") {\n      opts.timeoutMs = Number(argv[i + 1]);\n      i += 1;\n    } else if (arg === \"--help\") {\n      printHelp();\n      process.exit(0);\n    } else {\n      throw new Error(\"Unknown argument: \" + arg);\n    }\n  }\n\n  if (!opts.baseUrl) {\n    throw new Error(\"--base-url cannot be empty\");\n  }\n  if (!opts.address) {\n    throw new Error(\"--address cannot be empty\");\n  }\n  if (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs <= 0) {\n    throw new Error(\"--timeout-ms must be a positive number\");\n  }\n\n  return opts;\n}\n\nfunction printHelp() {\n  console.log(\n    [\n      \"Usage: node test/e2e/touch-mapper-settings-regression.js [options]\",\n      \"\",\n      \"Options:\",\n      \"  --base-url <url>     Base URL to test (default: https://test.touch-mapper.org)\",\n      \"  --address <text>      Address query to submit\",\n      \"  --timeout-ms <n>      Max wait for map generation (default: 420000)\",\n      \"  --headed              Run with visible browser\",\n      \"  --help                Show this help\"\n    ].join(\"\\n\")\n  );\n}\n\nfunction nowIso() {\n  return new Date().toISOString();\n}\n\nfunction normalizeNumericString(value) {\n  if (value === null || value === undefined) {\n    return \"\";\n  }\n  const parsed = Number(String(value));\n  return Number.isFinite(parsed) ? String(parsed) : String(value);\n}\n\nfunction expectEqual(label, actual, expected, failures, normalizeNumeric) {\n  const lhs = normalizeNumeric ? normalizeNumericString(actual) : String(actual);\n  const rhs = normalizeNumeric ? normalizeNumericString(expected) : String(expected);\n  if (lhs !== rhs) {\n    failures.push({ label, expected: String(expected), actual: String(actual) });\n  }\n}\n\nasync function readAreaSettings(page) {\n  return page.evaluate(() => {\n    const q = (selector) => document.querySelector(selector);\n    const getValue = (selector) => {\n      const el = q(selector);\n      return el ? el.value : null;\n    };\n    const isChecked = (selector) => {\n      const el = q(selector);\n      return el ? Boolean(el.checked) : null;\n    };\n    const getText = (selector) => {\n      const el = q(selector);\n      return el ? (el.textContent || \"\").trim() : null;\n    };\n\n    let printingTech = null;\n    if (isChecked(\"#printing-tech-2d\")) printingTech = \"2d\";\n    if (isChecked(\"#printing-tech-3d\")) printingTech = \"3d\";\n\n    return {\n      url: location.href,\n      address: getText(\".first-address\"),\n      printingTech,\n      mapSizePreset: getValue(\"#map-size-preset\"),\n      mapScalePreset: getValue(\"#map-scale-preset\"),\n      contentMode: getValue(\"#content-mode\"),\n      targetRoadDensityUi: getValue(\"#target-road-density-ui\"),\n      targetRoadDensityVisible: (function() {\n        const el = q(\".target-road-density-row\");\n        if (!el) {\n          return null;\n        }\n        return window.getComputedStyle(el).display !== \"none\";\n      })(),\n      hideLocationMarker: isChecked(\"#hide-location-marker\"),\n      advanced: isChecked(\"#advanced-input\"),\n      lon: getValue(\"#lon-input\"),\n      lat: getValue(\"#lat-input\"),\n      xOffset: getValue(\"#x-offset-input\"),\n      yOffset: getValue(\"#y-offset-input\"),\n      mapSizeInput: getValue(\"#map-size-input\"),\n      scaleInput: getValue(\"#scale-input\"),\n      multipart: isChecked(\"#multipart-map-input\"),\n      multipartAdjustmentX: getText(\".multipart-adjustment-x\"),\n      multipartAdjustmentY: getText(\".multipart-adjustment-y\")\n    };\n  });\n}\n\nasync function setSelectValueByDom(page, selector, value) {\n  await page.$eval(\n    selector,\n    (el, nextValue) => {\n      el.value = nextValue;\n      el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n    },\n    value\n  );\n}\n\n(async () => {\n  const options = parseArgs(process.argv.slice(2));\n  const runId = \"settings-regression-\" + Date.now();\n  const outDir = path.join(\".tmp\", \"e2e\", runId);\n  fs.mkdirSync(outDir, { recursive: true });\n\n  const logs = {\n    startedAt: nowIso(),\n    console: [],\n    pageErrors: [],\n    requestFailures: []\n  };\n\n  const expectedSettings = {\n    printingTech: \"2d\",\n    mapSizePreset: \"20\",\n    mapScalePreset: \"5600\",\n    contentMode: \"only-big-roads\",\n    targetRoadDensityUi: \"37\",\n    hideLocationMarker: true,\n    advanced: true,\n    xOffset: \"120\",\n    yOffset: \"-80\",\n    mapSizeInput: \"28.0\",\n    scaleInput: \"5600\",\n    multipart: true,\n    multipartAdjustmentX: \"10\",\n    multipartAdjustmentY: \"10\"\n  };\n\n  const browser = await chromium.launch({ headless: options.headless });\n  const context = await browser.newContext({ viewport: { width: 1440, height: 2200 } });\n  const page = await context.newPage();\n\n  page.on(\"console\", (msg) => {\n    if (msg.type() === \"warning\" || msg.type() === \"error\") {\n      logs.console.push({ type: msg.type(), text: msg.text() });\n    }\n  });\n  page.on(\"pageerror\", (err) => {\n    logs.pageErrors.push(String(err));\n  });\n  page.on(\"requestfailed\", (req) => {\n    logs.requestFailures.push({\n      method: req.method(),\n      url: req.url(),\n      failure: req.failure() ? req.failure().errorText : \"unknown\"\n    });\n  });\n\n  let report;\n  try {\n    await page.goto(options.baseUrl + \"/\", { waitUntil: \"domcontentloaded\", timeout: 120000 });\n    await page.waitForURL(/\\/en\\/?$/, { timeout: 120000 });\n    await page.waitForSelector(\"#address-input\", { timeout: 120000 });\n\n    await page.fill(\"#address-input\", options.address);\n    await Promise.all([\n      page.waitForURL(/\\/en\\/area/, { timeout: 180000 }),\n      page.click(\"#address-search-submit\")\n    ]);\n\n    await page.waitForSelector(\"#submit-button\", { timeout: 120000 });\n    await page.waitForFunction(() => {\n      const first = document.querySelector(\".first-address\");\n      return Boolean(first && (first.textContent || \"\").trim());\n    }, { timeout: 120000 });\n\n    await page.screenshot({ path: path.join(outDir, \"01-area-initial.png\"), fullPage: true });\n\n    await setSelectValueByDom(page, \"#content-mode\", \"normal\");\n    const targetRoadDensityHiddenInNormal = await page.$eval(\n      \".target-road-density-row\",\n      (el) => window.getComputedStyle(el).display === \"none\"\n    );\n\n    await setSelectValueByDom(page, \"#map-size-preset\", expectedSettings.mapSizePreset);\n    await setSelectValueByDom(page, \"#map-scale-preset\", expectedSettings.mapScalePreset);\n    await setSelectValueByDom(page, \"#content-mode\", expectedSettings.contentMode);\n    await page.waitForFunction(() => {\n      const row = document.querySelector(\".target-road-density-row\");\n      return Boolean(row && window.getComputedStyle(row).display !== \"none\");\n    }, { timeout: 120000 });\n    await page.$eval(\n      \"#target-road-density-ui\",\n      (el, value) => {\n        el.value = value;\n        el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n      },\n      expectedSettings.targetRoadDensityUi\n    );\n\n    await page.check(\"#printing-tech-2d\");\n    await page.check(\"#hide-location-marker\");\n\n    await page.check(\"#advanced-input\");\n    await page.waitForSelector(\"#advanced-controls\", { state: \"visible\", timeout: 120000 });\n\n    await page.fill(\"#x-offset-input\", expectedSettings.xOffset);\n    await page.fill(\"#y-offset-input\", expectedSettings.yOffset);\n    await page.fill(\"#map-size-input\", expectedSettings.mapSizeInput);\n    await page.fill(\"#scale-input\", expectedSettings.scaleInput);\n    await page.check(\"#multipart-map-input\");\n\n    await page.click(\".area-movement-buttons .right-10\");\n    await page.click(\".area-movement-buttons .up-10\");\n\n    const beforeSubmit = await readAreaSettings(page);\n    await page.screenshot({ path: path.join(outDir, \"02-area-configured.png\"), fullPage: true });\n\n    await Promise.all([\n      page.waitForURL(/\\/en\\/map\\?map=/, { timeout: options.timeoutMs }),\n      page.click(\"#submit-button\")\n    ]);\n\n    await page.waitForLoadState(\"domcontentloaded\", { timeout: 180000 });\n    await page.waitForSelector(\".map-content\", { timeout: 180000 });\n    await page.waitForTimeout(6000);\n\n    const mapChecks = await page.evaluate(() => {\n      const q = (selector) => document.querySelector(selector);\n      const cssDisplay = (selector) => {\n        const el = q(selector);\n        return el ? window.getComputedStyle(el).display : null;\n      };\n      return {\n        url: location.href,\n        title: document.title,\n        noDataDisplay: cssDisplay(\".no-data-available-msg\"),\n        has3dPreviewContainer: Boolean(q(\".preview-3d\")),\n        downloadMapHref: q(\"#download-map\") ? q(\"#download-map\").getAttribute(\"href\") : null,\n        mapContentHref: q(\"#download-map-content\") ? q(\"#download-map-content\").getAttribute(\"href\") : null,\n        summaryItems: Array.from(document.querySelectorAll(\".map-content-summary li\")).length,\n        backLinkText: q(\"a.back-to-previous-page\") ? (q(\"a.back-to-previous-page\").textContent || \"\").trim() : \"\"\n      };\n    });\n\n    await page.screenshot({ path: path.join(outDir, \"03-map-created.png\"), fullPage: true });\n\n    await page.click(\"a.back-to-previous-page\");\n    await page.waitForURL(/\\/en\\/area/, { timeout: 120000 });\n    await page.waitForSelector(\"#submit-button\", { timeout: 120000 });\n\n    const afterReturn = await readAreaSettings(page);\n    await page.screenshot({ path: path.join(outDir, \"04-area-returned.png\"), fullPage: true });\n\n    const failures = [];\n\n    if (mapChecks.noDataDisplay !== \"none\") {\n      failures.push({\n        label: \"map-page.no-data-msg\",\n        expected: \"none\",\n        actual: String(mapChecks.noDataDisplay)\n      });\n    }\n    if (!mapChecks.downloadMapHref) {\n      failures.push({ label: \"map-page.download-map-href\", expected: \"non-empty\", actual: \"empty\" });\n    }\n    if (!mapChecks.mapContentHref) {\n      failures.push({ label: \"map-page.map-content-href\", expected: \"non-empty\", actual: \"empty\" });\n    }\n    if (mapChecks.summaryItems < 1) {\n      failures.push({ label: \"map-page.summary-items\", expected: \">= 1\", actual: String(mapChecks.summaryItems) });\n    }\n    if (!targetRoadDensityHiddenInNormal) {\n      failures.push({ label: \"settings.targetRoadDensityHiddenInNormal\", expected: \"true\", actual: \"false\" });\n    }\n\n    expectEqual(\"settings.printingTech\", afterReturn.printingTech, expectedSettings.printingTech, failures);\n    expectEqual(\"settings.mapSizePreset\", afterReturn.mapSizePreset, expectedSettings.mapSizePreset, failures);\n    expectEqual(\"settings.mapScalePreset\", afterReturn.mapScalePreset, expectedSettings.mapScalePreset, failures);\n    expectEqual(\"settings.contentMode\", afterReturn.contentMode, expectedSettings.contentMode, failures);\n    expectEqual(\"settings.targetRoadDensityUi\", afterReturn.targetRoadDensityUi, expectedSettings.targetRoadDensityUi, failures);\n    expectEqual(\"settings.targetRoadDensityVisible\", afterReturn.targetRoadDensityVisible, true, failures);\n    expectEqual(\"settings.hideLocationMarker\", afterReturn.hideLocationMarker, expectedSettings.hideLocationMarker, failures);\n    expectEqual(\"settings.advanced\", afterReturn.advanced, expectedSettings.advanced, failures);\n    expectEqual(\"settings.xOffset\", afterReturn.xOffset, expectedSettings.xOffset, failures);\n    expectEqual(\"settings.yOffset\", afterReturn.yOffset, expectedSettings.yOffset, failures);\n    expectEqual(\"settings.mapSizeInput\", afterReturn.mapSizeInput, expectedSettings.mapSizeInput, failures, true);\n    expectEqual(\"settings.scaleInput\", afterReturn.scaleInput, expectedSettings.scaleInput, failures);\n    expectEqual(\"settings.multipart\", afterReturn.multipart, expectedSettings.multipart, failures);\n    expectEqual(\"settings.multipartAdjustmentX\", afterReturn.multipartAdjustmentX, expectedSettings.multipartAdjustmentX, failures);\n    expectEqual(\"settings.multipartAdjustmentY\", afterReturn.multipartAdjustmentY, expectedSettings.multipartAdjustmentY, failures);\n\n    logs.finishedAt = nowIso();\n\n    report = {\n      success: failures.length === 0,\n      outDir,\n      options,\n      expectedSettings,\n      beforeSubmit,\n      afterReturn,\n      mapChecks,\n      failures,\n      logCounts: {\n        consoleWarningsOrErrors: logs.console.length,\n        pageErrors: logs.pageErrors.length,\n        requestFailures: logs.requestFailures.length\n      },\n      logs\n    };\n\n    fs.writeFileSync(path.join(outDir, \"report.json\"), JSON.stringify(report, null, 2));\n\n    if (failures.length > 0) {\n      console.error(\"Regression test FAILED. See \" + path.join(outDir, \"report.json\"));\n      failures.forEach((f) => {\n        console.error(\" - \" + f.label + \" expected=\" + f.expected + \" actual=\" + f.actual);\n      });\n      process.exitCode = 1;\n    } else {\n      console.log(\"Regression test PASSED. See \" + path.join(outDir, \"report.json\"));\n    }\n  } catch (error) {\n    logs.finishedAt = nowIso();\n    report = {\n      success: false,\n      outDir,\n      options,\n      error: String(error),\n      stack: error && error.stack ? String(error.stack) : null,\n      logs\n    };\n    fs.writeFileSync(path.join(outDir, \"report-error.json\"), JSON.stringify(report, null, 2));\n    console.error(\"Regression test crashed. See \" + path.join(outDir, \"report-error.json\"));\n    console.error(String(error));\n    process.exitCode = 1;\n  } finally {\n    await browser.close();\n  }\n})();\n"
  },
  {
    "path": "test/map-content/.gitignore",
    "content": "out/\n"
  },
  {
    "path": "test/map-content/Makefile",
    "content": ".PHONY: simple average complex all simple-bl average-bl complex-bl all-bl\n\nsimple:\n\tnode run-tests.js --category simple\n\naverage:\n\tnode run-tests.js --category average\n\ncomplex:\n\tnode run-tests.js --category complex\n\nall:\n\tnode run-tests.js --all\n\nsimple-bl:\n\tnode run-tests.js --category simple --with-blender\n\naverage-bl:\n\tnode run-tests.js --category average --with-blender\n\ncomplex-bl:\n\tnode run-tests.js --category complex --with-blender\n\nall-bl:\n\tnode run-tests.js --all --with-blender\n"
  },
  {
    "path": "test/map-content/cache/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "test/map-content/check-railway-modes.js",
    "content": "#!/usr/bin/env node\n\"use strict\";\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst inspect = require(\"./inspect-map-description.js\");\n\nfunction parseArgs(argv) {\n  const args = {\n    osm: null,\n    locale: \"en\"\n  };\n  for (let i = 2; i < argv.length; i += 1) {\n    const arg = argv[i];\n    if (arg === \"--osm\") {\n      args.osm = argv[i + 1] || null;\n      i += 1;\n      continue;\n    }\n    if (arg === \"--locale\") {\n      args.locale = argv[i + 1] || \"en\";\n      i += 1;\n      continue;\n    }\n    throw new Error(\"Unknown argument: \" + arg + \"\\nUsage: check-railway-modes.js --osm <path> [--locale en]\");\n  }\n  if (!args.osm) {\n    throw new Error(\"--osm is required\\nUsage: check-railway-modes.js --osm <path> [--locale en]\");\n  }\n  return args;\n}\n\nconst NON_TRACK_RAILWAY_VALUES = {\n  platform: true,\n  platform_edge: true,\n  station: true,\n  halt: true,\n  tram_stop: true,\n  subway_entrance: true,\n  crossing: true,\n  level_crossing: true,\n  signal: true,\n  switch: true,\n  buffer_stop: true\n};\nconst TRACK_RAILWAY_VALUES = {\n  rail: true,\n  narrow_gauge: true,\n  tram: true,\n  light_rail: true,\n  subway: true,\n  metro: true,\n  yard: true,\n  siding: true\n};\n\nfunction sectionCount(model, key) {\n  if (!model || typeof model !== \"object\") {\n    return 0;\n  }\n  const section = model[key];\n  if (!section || typeof section !== \"object\") {\n    return 0;\n  }\n  const count = Number(section.count);\n  return Number.isFinite(count) ? count : 0;\n}\n\nfunction lineText(line) {\n  if (!line || !Array.isArray(line.parts)) {\n    return \"\";\n  }\n  return line.parts.map(function(part) {\n    return part && part.text ? String(part.text) : \"\";\n  }).join(\"\");\n}\n\nfunction collectConnectionSentences(model) {\n  const hits = [];\n  [\"roads\", \"paths\", \"railways\", \"waterways\", \"otherLinear\"].forEach(function(sectionKey) {\n    const section = model && model[sectionKey];\n    const items = section && Array.isArray(section.items) ? section.items : [];\n    items.forEach(function(item) {\n      const lines = item && Array.isArray(item.lines) ? item.lines : [];\n      lines.forEach(function(line) {\n        const text = lineText(line);\n        if (/^connects to\\b/i.test(text.trim())) {\n          hits.push({ section: sectionKey, text: text.trim() });\n        }\n      });\n    });\n  });\n  return hits;\n}\n\nfunction findRailwayConnectionSentences(connectionHits) {\n  return connectionHits.filter(function(hit) {\n    const text = (hit && hit.text ? String(hit.text) : \"\").toLowerCase();\n    return /\\brail(?:way|ways|line|lines)?\\b/.test(text) ||\n      /\\btram\\b/.test(text) ||\n      /\\blight rail\\b/.test(text) ||\n      /\\bsubway\\b/.test(text) ||\n      /\\bmetro\\b/.test(text);\n  });\n}\n\nfunction countTrackRailwayWaysInOsm(osmText) {\n  const wayBlocks = String(osmText).match(/<way\\b[\\s\\S]*?<\\/way>/g) || [];\n  let count = 0;\n  wayBlocks.forEach(function(block) {\n    const match = block.match(/<tag\\s+k=\"railway\"\\s+v=\"([^\"]+)\"\\s*\\/?>/i);\n    if (!match) {\n      return;\n    }\n    const normalized = String(match[1]).trim().toLowerCase();\n    if (!normalized) {\n      return;\n    }\n    if (Object.prototype.hasOwnProperty.call(NON_TRACK_RAILWAY_VALUES, normalized)) {\n      return;\n    }\n    count += 1;\n  });\n  return count;\n}\n\nfunction readWayRailwayValueById(osmText) {\n  const map = {};\n  const wayBlocks = String(osmText).match(/<way\\b[\\s\\S]*?<\\/way>/g) || [];\n  wayBlocks.forEach(function(block) {\n    const idMatch = block.match(/\\sid=\"([^\"]+)\"/i);\n    if (!idMatch || !idMatch[1]) {\n      return;\n    }\n    const railMatch = block.match(/<tag\\s+k=\"railway\"\\s+v=\"([^\"]+)\"\\s*\\/?>/i);\n    if (!railMatch || !railMatch[1]) {\n      return;\n    }\n    map[String(idMatch[1])] = String(railMatch[1]).trim().toLowerCase();\n  });\n  return map;\n}\n\nfunction railwayWayIdsFromMapContent(mapContentPath) {\n  const raw = fs.readFileSync(mapContentPath, \"utf8\");\n  let parsed = null;\n  try {\n    parsed = JSON.parse(raw);\n  } catch (error) {\n    throw new Error(\"Failed to parse map-content.json at \" + mapContentPath + \": \" + error.message);\n  }\n  const classA = parsed && parsed.A;\n  const subclasses = classA && Array.isArray(classA.subclasses) ? classA.subclasses : [];\n  const ids = {};\n  subclasses.forEach(function(subclass) {\n    if (!subclass || typeof subclass.key !== \"string\" || subclass.key.indexOf(\"A3_\") !== 0) {\n      return;\n    }\n    const groups = Array.isArray(subclass.groups) ? subclass.groups : [];\n    groups.forEach(function(group) {\n      const ways = group && Array.isArray(group.ways) ? group.ways : [];\n      ways.forEach(function(way) {\n        if (!way || way.osmId === undefined || way.osmId === null) {\n          return;\n        }\n        ids[String(way.osmId)] = true;\n      });\n    });\n  });\n  return Object.keys(ids);\n}\n\nfunction assertRailwaySectionBackedByRailwayTags(label, mapContentPath, wayRailwayValueById) {\n  const railwayWayIds = railwayWayIdsFromMapContent(mapContentPath);\n  const invalid = [];\n  railwayWayIds.forEach(function(id) {\n    const railwayValue = wayRailwayValueById[id];\n    if (!railwayValue || !TRACK_RAILWAY_VALUES[railwayValue]) {\n      invalid.push({ osmId: id, railway: railwayValue || null });\n    }\n  });\n  if (invalid.length) {\n    throw new Error(\n      label + \" map-content has non-railway way ids inside A3_* subclasses: \" +\n      JSON.stringify(invalid.slice(0, 8))\n    );\n  }\n}\n\nfunction runScenario(repoRoot, osmPath, locale, contentMode) {\n  return inspect.inspectMapDescription({\n    repoRoot: repoRoot,\n    osmPath: osmPath,\n    locale: locale,\n    contentMode: contentMode\n  });\n}\n\nfunction main() {\n  const args = parseArgs(process.argv);\n  const repoRoot = path.resolve(__dirname, \"..\", \"..\");\n  const osmPath = path.resolve(args.osm);\n  if (!fs.existsSync(osmPath)) {\n    throw new Error(\"OSM file not found: \" + osmPath);\n  }\n\n  const normal = runScenario(repoRoot, osmPath, args.locale, \"normal\");\n  const onlyBigRoads = runScenario(repoRoot, osmPath, args.locale, \"only-big-roads\");\n  const osmText = fs.readFileSync(osmPath, \"utf8\");\n  const wayRailwayValueById = readWayRailwayValueById(osmText);\n  assertRailwaySectionBackedByRailwayTags(\"normal\", normal.mapContentPath, wayRailwayValueById);\n  assertRailwaySectionBackedByRailwayTags(\"only-big-roads\", onlyBigRoads.mapContentPath, wayRailwayValueById);\n\n  const normalRailCount = sectionCount(normal.mapDescriptionModel, \"railways\");\n  const onlyBigRoadsRailCount = sectionCount(onlyBigRoads.mapDescriptionModel, \"railways\");\n  const trackRailwayWays = countTrackRailwayWaysInOsm(osmText);\n  if (trackRailwayWays > 0) {\n    if (normalRailCount <= 0) {\n      throw new Error(\"Expected railways in normal mode for track-rich OSM, got count=\" + normalRailCount);\n    }\n    if (onlyBigRoadsRailCount <= 0) {\n      throw new Error(\"Expected railways in only-big-roads mode for track-rich OSM, got count=\" + onlyBigRoadsRailCount);\n    }\n  }\n\n  const normalConnections = collectConnectionSentences(normal.mapDescriptionModel);\n  const onlyBigRoadsConnections = collectConnectionSentences(onlyBigRoads.mapDescriptionModel);\n  const railwaySectionConnections =\n    normalConnections.filter(function(hit) { return hit.section === \"railways\"; })\n      .concat(onlyBigRoadsConnections.filter(function(hit) { return hit.section === \"railways\"; }));\n  if (railwaySectionConnections.length) {\n    throw new Error(\"Expected no railway-section connection narration, found: \" + JSON.stringify(railwaySectionConnections.slice(0, 6)));\n  }\n\n  const railMentioningConnections = findRailwayConnectionSentences(normalConnections.concat(onlyBigRoadsConnections));\n  if (railMentioningConnections.length) {\n    throw new Error(\"Expected no rail-derived connection narration, found: \" + JSON.stringify(railMentioningConnections.slice(0, 6)));\n  }\n\n  process.stdout.write(\n    \"railway-mode-check OK: trackWays=\" + trackRailwayWays +\n    \", normal.railways=\" + normalRailCount +\n    \", only-big-roads.railways=\" + onlyBigRoadsRailCount + \"\\n\"\n  );\n}\n\nmain();\n"
  },
  {
    "path": "test/map-content/generate-map-content-from-osm.py",
    "content": "#!/usr/bin/env python3\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport os\nimport subprocess\nimport sys\nimport time\nfrom pathlib import Path\nfrom typing import Dict, List, TypedDict\n\nREPO_ROOT = Path(__file__).resolve().parents[2]\nif str(REPO_ROOT) not in sys.path:\n    sys.path.insert(0, str(REPO_ROOT))\n\nfrom converter.tactile_constants import BORDER_WIDTH_MM, BORDER_HORIZONTAL_OVERLAP_MM\n\nclass ClipOutputs(TypedDict):\n    reportPath: str\n    meshPaths: List[str]\n\n\ndef _stage_start(log_prefix: str, name: str) -> float:\n    now = time.time()\n    print(f\"{log_prefix}START {name}\", file=sys.stderr)\n    return now\n\n\ndef _stage_done(log_prefix: str, name: str, start: float) -> float:\n    duration = time.time() - start\n    print(f\"{log_prefix}DONE {name} ({duration:.2f}s)\", file=sys.stderr)\n    return duration\n\n\ndef run_cmd(cmd: List[str], cwd: Path, env: Dict[str, str] | None = None) -> None:\n    env_vars = os.environ.copy()\n    if env:\n        env_vars.update(env)\n    completed = subprocess.run(\n        cmd,\n        cwd=str(cwd),\n        env=env_vars,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.STDOUT,\n        text=True,\n        check=False,\n    )\n    if completed.returncode != 0:\n        if completed.stdout:\n            print(completed.stdout, file=sys.stderr, end=\"\")\n        raise subprocess.CalledProcessError(completed.returncode, cmd, output=completed.stdout)\n\n\ndef _parse_env_bool(name: str) -> bool | None:\n    raw = os.environ.get(name)\n    if raw is None:\n        return None\n    normalized = raw.strip().lower()\n    if normalized in (\"1\", \"true\", \"yes\", \"on\"):\n        return True\n    if normalized in (\"0\", \"false\", \"no\", \"off\"):\n        return False\n    return None\n\n\ndef pretty_json_enabled(pretty_arg: bool | None) -> bool:\n    if pretty_arg is not None:\n        return pretty_arg\n    forced = _parse_env_bool(\"TOUCH_MAPPER_PRETTY_JSON\")\n    if forced is not None:\n        return forced\n    # This tool is development-only under test/map-content; default to pretty output.\n    return True\n\n\ndef rewrite_json(path: Path, pretty_json: bool) -> None:\n    with path.open(\"r\", encoding=\"utf-8\") as handle:\n        payload = json.load(handle)\n    with path.open(\"w\", encoding=\"utf-8\") as handle:\n        if pretty_json:\n            json.dump(payload, handle, indent=2, ensure_ascii=False)\n            handle.write(\"\\n\")\n            return\n        json.dump(payload, handle, separators=(\",\", \":\"), ensure_ascii=False)\n\n\ndef compute_clip_bounds(boundary: Dict[str, float], scale: int, no_borders: bool) -> Dict[str, float]:\n    clip_min_x = boundary[\"minX\"]\n    clip_min_y = boundary[\"minY\"]\n    clip_max_x = boundary[\"maxX\"]\n    clip_max_y = boundary[\"maxY\"]\n    if not no_borders:\n        mm_to_units = float(scale) / 1000.0\n        space = (BORDER_WIDTH_MM - BORDER_HORIZONTAL_OVERLAP_MM) * mm_to_units\n        clip_min_x += space\n        clip_min_y += space\n        clip_max_x -= space\n        clip_max_y -= space\n    if clip_min_x >= clip_max_x or clip_min_y >= clip_max_y:\n        raise ValueError(\"Invalid clip bounds after border inset\")\n    return {\n        \"minX\": clip_min_x,\n        \"minY\": clip_min_y,\n        \"maxX\": clip_max_x,\n        \"maxY\": clip_max_y,\n    }\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Generate map-content.json from a map.osm file using OSM2World + converter.map_desc.\"\n    )\n    parser.add_argument(\"--osm\", required=True, help=\"Path to input .osm file\")\n    parser.add_argument(\"--out-dir\", required=True, help=\"Directory for generated files\")\n    parser.add_argument(\"--scale\", type=int, default=1400, help=\"TOUCH_MAPPER_SCALE for OSM2World\")\n    parser.add_argument(\n        \"--content-mode\",\n        choices=[\"normal\", \"no-buildings\", \"only-big-roads\"],\n        default=\"normal\",\n        help=\"Content mode for conversion. no-buildings maps to TOUCH_MAPPER_EXCLUDE_BUILDINGS=true.\",\n    )\n    parser.add_argument(\n        \"--with-blender\",\n        action=\"store_true\",\n        help=\"Also run Blender tactile export to produce STL/SVG/BLEND outputs.\",\n    )\n    parser.add_argument(\n        \"--diameter\",\n        type=int,\n        help=\"Larger of map area x and y diameter in meters (required with --with-blender).\",\n    )\n    parser.add_argument(\n        \"--size\",\n        type=float,\n        help=\"Output print size in cm (required with --with-blender).\",\n    )\n    parser.add_argument(\n        \"--no-borders\",\n        action=\"store_true\",\n        help=\"Pass --no-borders to obj-to-tactile.py when running with --with-blender.\",\n    )\n    parser.add_argument(\n        \"--marker1\",\n        help=\"Marker JSON passed through to obj-to-tactile.py when running with --with-blender.\",\n    )\n    parser.add_argument(\n        \"--pretty-json\",\n        dest=\"pretty_json\",\n        action=\"store_true\",\n        default=None,\n        help=\"Write generated JSON with indentation.\",\n    )\n    parser.add_argument(\n        \"--compact-json\",\n        dest=\"pretty_json\",\n        action=\"store_false\",\n        help=\"Write generated JSON without indentation.\",\n    )\n    parser.add_argument(\"--log-prefix\", default=\"\", help=\"Optional log prefix for stage timing output\")\n    return parser.parse_args()\n\n\ndef ensure_paths(repo_root: Path, osm_path: Path) -> Path:\n    if not osm_path.exists():\n        raise FileNotFoundError(f\"Input OSM file not found: {osm_path}\")\n    if osm_path.suffix.lower() != \".osm\":\n        raise ValueError(f\"Input file must end with .osm: {osm_path}\")\n    jar_path = repo_root / \"OSM2World\" / \"build\" / \"OSM2World.jar\"\n    if not jar_path.exists():\n        raise FileNotFoundError(f\"OSM2World jar not found: {jar_path}\")\n    return jar_path\n\n\ndef read_boundary(raw_meta_path: Path) -> Dict[str, float]:\n    with raw_meta_path.open(\"r\", encoding=\"utf-8\") as handle:\n        payload = json.load(handle)\n    boundary = payload.get(\"meta\", {}).get(\"boundary\")\n    if not isinstance(boundary, dict):\n        raise ValueError(f\"map-meta-raw.json missing meta.boundary object: {raw_meta_path}\")\n\n    result: Dict[str, float] = {}\n    for key in (\"minX\", \"minY\", \"maxX\", \"maxY\"):\n        value = boundary.get(key)\n        if not isinstance(value, (int, float)):\n            raise ValueError(f\"meta.boundary.{key} missing or non-numeric in {raw_meta_path}\")\n        result[key] = float(value)\n    return result\n\n\ndef run_clip_2d(\n    repo_root: Path,\n    obj_path: Path,\n    boundary: Dict[str, float],\n    args: argparse.Namespace,\n) -> ClipOutputs:\n    clip_bounds = compute_clip_bounds(boundary, int(args.scale), bool(args.no_borders))\n    clip_script_path = repo_root / \"converter\" / \"clip-2d.js\"\n    clip_report_path = obj_path.parent / \"map-clip-report.json\"\n    if not clip_script_path.exists():\n        raise FileNotFoundError(f\"clip-2d.js not found: {clip_script_path}\")\n    clip_cmd = [\n        \"node\",\n        str(clip_script_path),\n        \"--input-obj\",\n        str(obj_path),\n        \"--out-dir\",\n        str(obj_path.parent),\n        \"--basename\",\n        \"map-clip\",\n        \"--report\",\n        str(clip_report_path),\n        \"--min-x\",\n        str(clip_bounds[\"minX\"]),\n        \"--min-y\",\n        str(clip_bounds[\"minY\"]),\n        \"--max-x\",\n        str(clip_bounds[\"maxX\"]),\n        \"--max-y\",\n        str(clip_bounds[\"maxY\"]),\n    ]\n    run_cmd(clip_cmd, cwd=repo_root)\n    if not clip_report_path.exists():\n        raise FileNotFoundError(f\"clip-2d did not produce report: {clip_report_path}\")\n    with clip_report_path.open(\"r\", encoding=\"utf-8\") as handle:\n        report = json.load(handle)\n    file_entries = report.get(\"files\", [])\n    mesh_paths: List[str] = []\n    for entry in file_entries:\n        if not isinstance(entry, dict):\n            continue\n        file_path = entry.get(\"path\")\n        if not isinstance(file_path, str) or not file_path:\n            continue\n        if not Path(file_path).exists():\n            raise FileNotFoundError(f\"clip-2d output missing: {file_path}\")\n        mesh_paths.append(file_path)\n    if not mesh_paths:\n        raise ValueError(f\"clip-2d produced no mesh outputs: {clip_report_path}\")\n    return {\n        \"reportPath\": str(clip_report_path),\n        \"meshPaths\": mesh_paths,\n    }\n\n\ndef run_blender_export(\n    repo_root: Path,\n    mesh_paths: List[str],\n    raw_meta_path: Path,\n    output_base_path: Path,\n    args: argparse.Namespace,\n) -> None:\n    if args.diameter is None or args.size is None:\n        raise ValueError(\"--diameter and --size are required when --with-blender is set\")\n\n    blender_dir = repo_root / \"converter\" / \"blender\"\n    blender_path = blender_dir / \"blender\"\n    obj_to_tactile_path = repo_root / \"converter\" / \"obj-to-tactile.py\"\n    if not blender_path.exists():\n        raise FileNotFoundError(f\"Blender binary not found: {blender_path}\")\n    if not obj_to_tactile_path.exists():\n        raise FileNotFoundError(f\"obj-to-tactile.py not found: {obj_to_tactile_path}\")\n\n    boundary = read_boundary(raw_meta_path)\n    blender_cmd = [\n        str(blender_path),\n        \"-noaudio\",\n        \"--factory-startup\",\n        \"--background\",\n        \"--python\",\n        str(obj_to_tactile_path),\n        \"--\",\n        \"--scale\",\n        str(args.scale),\n        \"--min-x\",\n        str(boundary[\"minX\"]),\n        \"--min-y\",\n        str(boundary[\"minY\"]),\n        \"--max-x\",\n        str(boundary[\"maxX\"]),\n        \"--max-y\",\n        str(boundary[\"maxY\"]),\n        \"--diameter\",\n        str(args.diameter),\n        \"--size\",\n        str(args.size),\n        \"--base-path\",\n        str(output_base_path),\n    ]\n    if args.no_borders:\n        blender_cmd.append(\"--no-borders\")\n    if args.marker1:\n        blender_cmd.extend([\"--marker1\", args.marker1])\n    blender_cmd.append(\"--export-wireframe-png\")\n    blender_cmd.extend(mesh_paths)\n\n    inherited_ld_library_path = os.environ.get(\"LD_LIBRARY_PATH\", \"\")\n    combined_ld_library_path = str(blender_dir / \"lib\")\n    if inherited_ld_library_path:\n        combined_ld_library_path += \":\" + inherited_ld_library_path\n    run_cmd(\n        blender_cmd,\n        cwd=repo_root,\n        env={\n            \"LD_LIBRARY_PATH\": combined_ld_library_path,\n        },\n    )\n\n\ndef main() -> int:\n    args = parse_args()\n    pretty_json = pretty_json_enabled(args.pretty_json)\n    repo_root = Path(__file__).resolve().parents[2]\n    if str(repo_root) not in sys.path:\n        sys.path.insert(0, str(repo_root))\n    from converter.map_desc import run_map_desc\n\n    osm_path = Path(args.osm).resolve()\n    out_dir = Path(args.out_dir).resolve()\n    out_dir.mkdir(parents=True, exist_ok=True)\n\n    # Preserve caller-provided indentation exactly (do not normalize spaces).\n    log_prefix = args.log_prefix if args.log_prefix else \"\"\n    jar_path = ensure_paths(repo_root, osm_path)\n    obj_path = out_dir / \"map.obj\"\n    raw_meta_path = out_dir / \"map-meta-raw.json\"\n\n    osm2world_cmd = [\n        \"java\",\n        \"-Xmx1G\",\n        \"-jar\",\n        str(jar_path),\n        \"-i\",\n        str(osm_path),\n        \"-o\",\n        str(obj_path),\n    ]\n    timings: Dict[str, float] = {}\n    clip_report_path: str | None = None\n\n    osm2world_start = _stage_start(log_prefix, \"run-osm2world\")\n    run_cmd(\n        osm2world_cmd,\n        cwd=repo_root,\n        env={\n            \"TOUCH_MAPPER_SCALE\": str(args.scale),\n            \"TOUCH_MAPPER_EXTRUDER_WIDTH\": \"0.5\",\n            \"TOUCH_MAPPER_EXCLUDE_BUILDINGS\": \"true\" if args.content_mode == \"no-buildings\" else \"false\",\n        },\n    )\n    timings[\"run-osm2world\"] = _stage_done(log_prefix, \"run-osm2world\", osm2world_start)\n\n    if not raw_meta_path.exists():\n        raise FileNotFoundError(f\"OSM2World did not produce expected file: {raw_meta_path}\")\n    rewrite_json(raw_meta_path, pretty_json)\n\n    map_desc_start = _stage_start(log_prefix, \"run-map-desc\")\n    map_desc_profile: Dict[str, float] = {}\n    run_map_desc(str(raw_meta_path), profile=map_desc_profile, pretty_json=pretty_json)\n    for key, value in sorted(map_desc_profile.items()):\n        timings[\"run-map-desc.\" + key] = value\n    timings[\"run-map-desc\"] = _stage_done(log_prefix, \"run-map-desc\", map_desc_start)\n\n    clip_outputs: ClipOutputs | None = None\n    if args.with_blender:\n        clip_start = _stage_start(log_prefix, \"run-clip-2d\")\n        clip_outputs = run_clip_2d(repo_root, obj_path, read_boundary(raw_meta_path), args)\n        timings[\"run-clip-2d\"] = _stage_done(log_prefix, \"run-clip-2d\", clip_start)\n        clip_report_path = str(clip_outputs[\"reportPath\"])\n\n        blender_start = _stage_start(log_prefix, \"run-blender\")\n        mesh_paths = clip_outputs[\"meshPaths\"] if clip_outputs else []\n        run_blender_export(repo_root, mesh_paths, raw_meta_path, out_dir / \"map\", args)\n        timings[\"run-blender\"] = _stage_done(log_prefix, \"run-blender\", blender_start)\n\n    map_meta_path = out_dir / \"map-meta.json\"\n    map_meta_augmented_path = out_dir / \"map-meta.augmented.json\"\n    map_content_path = out_dir / \"map-content.json\"\n    for generated in [map_meta_path, map_meta_augmented_path, map_content_path]:\n        if not generated.exists():\n            raise FileNotFoundError(f\"converter.map_desc did not produce expected file: {generated}\")\n\n    blender_output_paths = {}\n    if args.with_blender:\n        if clip_report_path is None:\n            raise RuntimeError(\"clip report path missing after clip-2d stage\")\n        blender_output_paths = {\n            \"mapClipReportPath\": clip_report_path,\n            \"mapStlPath\": str(out_dir / \"map.stl\"),\n            \"mapWaysStlPath\": str(out_dir / \"map-ways.stl\"),\n            \"mapRestStlPath\": str(out_dir / \"map-rest.stl\"),\n            \"mapSvgPath\": str(out_dir / \"map.svg\"),\n            \"mapBlendPath\": str(out_dir / \"map.blend\"),\n            \"mapWireframeFlatPath\": str(out_dir / \"map-wireframe-flat.png\"),\n            \"mapWireframePath\": str(out_dir / \"map-wireframe.png\"),\n        }\n        for name, file_path in blender_output_paths.items():\n            if not Path(file_path).exists():\n                raise FileNotFoundError(f\"Blender did not produce expected file ({name}): {file_path}\")\n\n    result = {\n        \"repoRoot\": str(repo_root),\n        \"inputOsmPath\": str(osm_path),\n        \"contentMode\": args.content_mode,\n        \"outputDir\": str(out_dir),\n        \"mapObjPath\": str(obj_path),\n        \"mapMetaRawPath\": str(raw_meta_path),\n        \"mapMetaPath\": str(map_meta_path),\n        \"mapMetaAugmentedPath\": str(map_meta_augmented_path),\n        \"mapContentPath\": str(map_content_path),\n        \"timings\": timings,\n    }\n    result.update(blender_output_paths)\n    print(json.dumps(result, indent=2))\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "test/map-content/inspect-map-description.js",
    "content": "#!/usr/bin/env node\n\n\"use strict\";\n\nconst fs = require(\"fs\");\nconst os = require(\"os\");\nconst path = require(\"path\");\nconst vm = require(\"vm\");\nconst { spawnSync } = require(\"child_process\");\n\nfunction parseArgs(argv) {\n  const args = {\n    locale: \"en\",\n    out: null,\n    osm: null,\n    mapContent: null,\n    contentMode: \"normal\",\n    workDir: null\n  };\n  for (let i = 0; i < argv.length; i += 1) {\n    const arg = argv[i];\n    if (arg === \"--osm\") {\n      args.osm = argv[i + 1] || null;\n      i += 1;\n      continue;\n    }\n    if (arg === \"--map-content\") {\n      args.mapContent = argv[i + 1] || null;\n      i += 1;\n      continue;\n    }\n    if (arg === \"--locale\") {\n      args.locale = argv[i + 1] || \"en\";\n      i += 1;\n      continue;\n    }\n    if (arg === \"--out\") {\n      args.out = argv[i + 1] || null;\n      i += 1;\n      continue;\n    }\n    if (arg === \"--content-mode\") {\n      args.contentMode = argv[i + 1] || \"normal\";\n      i += 1;\n      continue;\n    }\n    if (arg === \"--work-dir\") {\n      args.workDir = argv[i + 1] || null;\n      i += 1;\n      continue;\n    }\n    throw new Error(\"Unknown argument: \" + arg);\n  }\n  if (!args.osm && !args.mapContent) {\n    throw new Error(\"Missing required input: --osm <path-to-map.osm> or --map-content <path>\");\n  }\n  if (args.osm && args.mapContent) {\n    throw new Error(\"Use exactly one input: either --osm or --map-content\");\n  }\n  if (!args.out) {\n    throw new Error(\"Missing required argument: --out <path-to-output.json>\");\n  }\n  if ([\"normal\", \"no-buildings\", \"only-big-roads\"].indexOf(args.contentMode) === -1) {\n    throw new Error(\"Invalid --content-mode value: \" + args.contentMode);\n  }\n  return args;\n}\n\nfunction readJson(jsonPath) {\n  const raw = fs.readFileSync(jsonPath, \"utf8\");\n  try {\n    return JSON.parse(raw);\n  } catch (error) {\n    throw new Error(\"Failed to parse JSON file: \" + jsonPath + \" (\" + error.message + \")\");\n  }\n}\n\nfunction loadScriptIntoContext(context, scriptPath) {\n  const source = fs.readFileSync(scriptPath, \"utf8\");\n  vm.runInContext(source, context, { filename: scriptPath });\n}\n\nfunction makeTranslator(localeDict, enDict) {\n  return function t(key, fallback) {\n    if (localeDict[key] !== undefined && localeDict[key] !== null && localeDict[key] !== \"\") {\n      return localeDict[key];\n    }\n    if (enDict[key] !== undefined && enDict[key] !== null && enDict[key] !== \"\") {\n      return enDict[key];\n    }\n    return fallback !== undefined ? fallback : key;\n  };\n}\n\nfunction ensureFileExists(filePath, label) {\n  if (!fs.existsSync(filePath)) {\n    throw new Error(label + \" not found: \" + filePath);\n  }\n}\n\nfunction runPythonGenerator(repoRoot, osmPath, workDir, options) {\n  const generatorPath = path.join(repoRoot, \"test\", \"map-content\", \"generate-map-content-from-osm.py\");\n  ensureFileExists(generatorPath, \"Python generator\");\n  const outputDir = workDir || fs.mkdtempSync(path.join(os.tmpdir(), \"tm-map-desc-\"));\n  fs.mkdirSync(outputDir, { recursive: true });\n\n  const cmdArgs = [\n    generatorPath,\n    \"--osm\",\n    osmPath,\n    \"--out-dir\",\n    outputDir\n  ];\n  if (options && Number.isFinite(Number(options.scale))) {\n    cmdArgs.push(\"--scale\", String(Number(options.scale)));\n  }\n  if (options && typeof options.contentMode === \"string\" && options.contentMode.length > 0) {\n    cmdArgs.push(\"--content-mode\", String(options.contentMode));\n  }\n  if (options && options.logPrefix) {\n    cmdArgs.push(\"--log-prefix\", String(options.logPrefix));\n  }\n\n  const result = spawnSync(\"python3\", cmdArgs, {\n    cwd: repoRoot,\n    encoding: \"utf8\"\n  });\n  if (result.status !== 0) {\n    const stderr = (result.stderr || \"\").trim();\n    const stdout = (result.stdout || \"\").trim();\n    throw new Error(\n      \"Failed to generate map-content.json.\\nstdout:\\n\" + stdout + \"\\nstderr:\\n\" + stderr\n    );\n  }\n  return JSON.parse(result.stdout);\n}\n\nfunction resolveLocaleDict(repoRoot, locale) {\n  const enPath = path.join(repoRoot, \"web\", \"locales\", \"en\", \"tm.json\");\n  ensureFileExists(enPath, \"English locale dictionary\");\n  const enDict = readJson(enPath);\n\n  const localePath = path.join(repoRoot, \"web\", \"locales\", locale, \"tm.json\");\n  if (!fs.existsSync(localePath)) {\n    throw new Error(\"Locale dictionary not found for locale '\" + locale + \"': \" + localePath);\n  }\n  const localeDict = readJson(localePath);\n  return { localeDict, enDict };\n}\n\nfunction buildModels(repoRoot, mapContent, locale) {\n  const localeData = resolveLocaleDict(repoRoot, locale);\n  const translator = makeTranslator(localeData.localeDict, localeData.enDict);\n  const helpers = { t: translator };\n\n  const sandbox = {\n    window: {\n      TM: {\n        translations: localeData.localeDict\n      }\n    },\n    console: console,\n    isNaN: isNaN,\n    isFinite: isFinite,\n    setTimeout: setTimeout,\n    clearTimeout: clearTimeout\n  };\n  sandbox.global = sandbox;\n  sandbox.globalThis = sandbox;\n  const context = vm.createContext(sandbox);\n\n  loadScriptIntoContext(context, path.join(repoRoot, \"web\", \"src\", \"scripts\", \"map-desc-ways.js\"));\n  loadScriptIntoContext(context, path.join(repoRoot, \"web\", \"src\", \"scripts\", \"map-desc-areas.js\"));\n  loadScriptIntoContext(context, path.join(repoRoot, \"web\", \"src\", \"scripts\", \"map-desc-pois.js\"));\n  loadScriptIntoContext(context, path.join(repoRoot, \"web\", \"src\", \"scripts\", \"map-description.js\"));\n\n  const tmApi = sandbox.window && sandbox.window.TM ? sandbox.window.TM : {};\n  if (!tmApi.mapDescWays || typeof tmApi.mapDescWays.buildModel !== \"function\") {\n    throw new Error(\"window.TM.mapDescWays.buildModel is not available\");\n  }\n  if (!tmApi.mapDescAreas || typeof tmApi.mapDescAreas.buildModel !== \"function\") {\n    throw new Error(\"window.TM.mapDescAreas.buildModel is not available\");\n  }\n  if (!tmApi.mapDescription || typeof tmApi.mapDescription.buildModel !== \"function\") {\n    throw new Error(\"window.TM.mapDescription.buildModel is not available\");\n  }\n\n  return {\n    waysModel: tmApi.mapDescWays.buildModel(mapContent, helpers),\n    areasModel: tmApi.mapDescAreas.buildModel(mapContent, helpers),\n    mapDescriptionModel: tmApi.mapDescription.buildModel(mapContent, helpers, {\n      maxVisibleBuildings: 10\n    })\n  };\n}\n\nfunction inspectMapDescription(options) {\n  if (!options || typeof options !== \"object\") {\n    throw new Error(\"inspectMapDescription options object is required\");\n  }\n\n  const repoRoot = options.repoRoot ? path.resolve(options.repoRoot) : path.resolve(__dirname, \"..\", \"..\");\n  const locale = options.locale || \"en\";\n  let generation = null;\n  let mapContentPath = null;\n  let inputOsmPath = null;\n\n  if (options.mapContentPath) {\n    mapContentPath = path.resolve(options.mapContentPath);\n    ensureFileExists(mapContentPath, \"Input map-content.json\");\n  } else if (options.osmPath) {\n    inputOsmPath = path.resolve(options.osmPath);\n    ensureFileExists(inputOsmPath, \"Input OSM file\");\n    generation = runPythonGenerator(\n      repoRoot,\n      inputOsmPath,\n      options.workDir ? path.resolve(options.workDir) : null,\n      {\n        scale: options.scale,\n        contentMode: options.contentMode || \"normal\",\n        logPrefix: options.logPrefix || \"\"\n      }\n    );\n    mapContentPath = generation.mapContentPath;\n    ensureFileExists(mapContentPath, \"Generated map-content.json\");\n  } else {\n    throw new Error(\"inspectMapDescription requires either mapContentPath or osmPath\");\n  }\n\n  const mapContent = readJson(mapContentPath);\n  const models = buildModels(repoRoot, mapContent, locale);\n\n  return {\n    source: {\n      inputOsmPath: inputOsmPath,\n      locale: locale,\n      generation: generation,\n      mapContentPath: mapContentPath\n    },\n    mapContentPath: mapContentPath,\n    waysModel: models.waysModel,\n    areasModel: models.areasModel,\n    mapDescriptionModel: models.mapDescriptionModel\n  };\n}\n\nfunction main() {\n  const args = parseArgs(process.argv.slice(2));\n  const repoRoot = path.resolve(__dirname, \"..\", \"..\");\n  const artifact = inspectMapDescription({\n    repoRoot: repoRoot,\n    locale: args.locale,\n    osmPath: args.osm ? path.resolve(args.osm) : null,\n    mapContentPath: args.mapContent ? path.resolve(args.mapContent) : null,\n    contentMode: args.contentMode,\n    workDir: args.workDir ? path.resolve(args.workDir) : null\n  });\n\n  const outPath = path.resolve(args.out);\n  fs.mkdirSync(path.dirname(outPath), { recursive: true });\n  fs.writeFileSync(outPath, JSON.stringify(artifact, null, 2) + \"\\n\", \"utf8\");\n  process.stdout.write(JSON.stringify({ outPath: outPath, mapContentPath: artifact.mapContentPath }, null, 2) + \"\\n\");\n}\n\nif (require.main === module) {\n  try {\n    main();\n  } catch (error) {\n    process.stderr.write((error && error.message ? error.message : String(error)) + \"\\n\");\n    process.exit(1);\n  }\n}\n\nmodule.exports = {\n  inspectMapDescription: inspectMapDescription,\n  runPythonGenerator: runPythonGenerator,\n  resolveLocaleDict: resolveLocaleDict\n};\n"
  },
  {
    "path": "test/map-content/render-text-simulation.js",
    "content": "\"use strict\";\n\nfunction lineText(line) {\n  if (!line || !Array.isArray(line.parts)) {\n    return \"\";\n  }\n  return line.parts.map(function(part) {\n    return part && part.text !== undefined && part.text !== null ? String(part.text) : \"\";\n  }).join(\"\").replace(/\\s+/g, \" \").trim();\n}\n\nfunction pushSection(lines, label, section) {\n  const count = section && Number.isFinite(Number(section.count)) ? Number(section.count) : 0;\n  lines.push(\"- \" + label + \" (\" + count + \")\");\n  const items = section && Array.isArray(section.items) ? section.items : [];\n  const itemPrefix = label === \"Buildings\"\n    ? \"Building\"\n    : (label.indexOf(\"POI\") === 0 ? \"POI\" : \"Feature\");\n\n  items.forEach(function(item, index) {\n    lines.push(\"  - \" + itemPrefix + \" \" + (index + 1));\n    const modelLines = item && Array.isArray(item.lines) ? item.lines : [];\n    modelLines.forEach(function(modelLine) {\n      const text = lineText(modelLine);\n      if (text) {\n        lines.push(\"    - \" + text);\n      }\n    });\n  });\n}\n\nfunction renderSimulationText(mapDescriptionModel) {\n  const lines = [\"Map content\"];\n  const model = mapDescriptionModel && typeof mapDescriptionModel === \"object\" ? mapDescriptionModel : {};\n  pushSection(lines, \"Roads\", model.roads || model.ways);\n  if (model.paths && Number(model.paths.count) > 0) {\n    pushSection(lines, \"Paths\", model.paths);\n  }\n  if (model.railways && Number(model.railways.count) > 0) {\n    pushSection(lines, \"Railways\", model.railways);\n  }\n  if (model.waterways && Number(model.waterways.count) > 0) {\n    pushSection(lines, \"Waterways\", model.waterways);\n  }\n  if (model.waterAreas && Number(model.waterAreas.count) > 0) {\n    pushSection(lines, \"Water areas\", model.waterAreas);\n  }\n  if (model.otherLinear && Number(model.otherLinear.count) > 0) {\n    pushSection(lines, \"Other linear features\", model.otherLinear);\n  }\n  pushSection(lines, \"Buildings\", model.buildings);\n  if (model.poiFamiliar) {\n    pushSection(lines, \"POI familiar places\", model.poiFamiliar);\n  }\n  if (model.poiDaily && Number(model.poiDaily.count) > 0) {\n    pushSection(lines, \"POI daily essentials\", model.poiDaily);\n  }\n  if (model.poiTransport && Number(model.poiTransport.count) > 0) {\n    pushSection(lines, \"POI transport points\", model.poiTransport);\n  }\n  return lines.join(\"\\n\") + \"\\n\";\n}\n\nmodule.exports = {\n  renderSimulationText: renderSimulationText\n};\n"
  },
  {
    "path": "test/map-content/run-tests.js",
    "content": "#!/usr/bin/env node\n\n\"use strict\";\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { spawn } = require(\"child_process\");\nconst { spawnSync } = require(\"child_process\");\nconst inspect = require(\"./inspect-map-description.js\");\nconst { renderSimulationText } = require(\"./render-text-simulation.js\");\n\nfunction parseArgs(argv) {\n  const args = {\n    all: false,\n    categories: [],\n    jobs: null,\n    offline: false,\n    keepExistingOut: false,\n    withBlender: false\n  };\n  for (let i = 0; i < argv.length; i += 1) {\n    const arg = argv[i];\n    if (arg === \"--all\") {\n      args.all = true;\n      continue;\n    }\n    if (arg === \"--category\") {\n      args.categories.push(argv[i + 1] || \"\");\n      i += 1;\n      continue;\n    }\n    if (arg === \"--jobs\") {\n      args.jobs = Number(argv[i + 1]);\n      i += 1;\n      continue;\n    }\n    if (arg === \"--offline\") {\n      args.offline = true;\n      continue;\n    }\n    if (arg === \"--keep-existing-out\") {\n      args.keepExistingOut = true;\n      continue;\n    }\n    if (arg === \"--with-blender\") {\n      args.withBlender = true;\n      continue;\n    }\n    throw new Error(\"Unknown argument: \" + arg);\n  }\n  if (!args.all && args.categories.length === 0) {\n    throw new Error(\"Select categories with --all or one/more --category <simple|average|complex>\");\n  }\n  if (args.jobs !== null && (!Number.isFinite(args.jobs) || args.jobs < 1)) {\n    throw new Error(\"--jobs must be a positive integer\");\n  }\n  return args;\n}\n\nfunction buildMarkerArg(requestBody) {\n  if (!requestBody || typeof requestBody !== \"object\") {\n    return null;\n  }\n  if (requestBody.hideLocationMarker || requestBody.multipartMode || !requestBody.marker1) {\n    return null;\n  }\n  const area = requestBody.effectiveArea;\n  if (!area || typeof area !== \"object\") {\n    return null;\n  }\n  const lonMin = Number(area.lonMin);\n  const lonMax = Number(area.lonMax);\n  const latMin = Number(area.latMin);\n  const latMax = Number(area.latMax);\n  const markerLon = Number(requestBody.marker1.lon);\n  const markerLat = Number(requestBody.marker1.lat);\n  if (!Number.isFinite(lonMin) || !Number.isFinite(lonMax) || !Number.isFinite(latMin) || !Number.isFinite(latMax)) {\n    return null;\n  }\n  if (!Number.isFinite(markerLon) || !Number.isFinite(markerLat)) {\n    return null;\n  }\n  const width = lonMax - lonMin;\n  const height = latMax - latMin;\n  if (width === 0 || height === 0) {\n    return null;\n  }\n  const marker1x = (markerLon - lonMin) / width;\n  const marker1y = (markerLat - latMin) / height;\n  if (!(marker1x > 0.04 && marker1x < 0.96 && marker1y > 0.04 && marker1y < 0.96)) {\n    return null;\n  }\n  return JSON.stringify({ x: marker1x, y: marker1y });\n}\n\nfunction readJson(filePath) {\n  return JSON.parse(fs.readFileSync(filePath, \"utf8\"));\n}\n\nfunction writeJson(filePath, value) {\n  fs.mkdirSync(path.dirname(filePath), { recursive: true });\n  fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + \"\\n\", \"utf8\");\n}\n\nfunction prettyPrintJsonFiles(rootDir) {\n  listFilesRecursive(rootDir).forEach(function(fileInfo) {\n    if (!fileInfo.path.toLowerCase().endsWith(\".json\")) {\n      return;\n    }\n    const absPath = path.join(rootDir, fileInfo.path);\n    let parsed = null;\n    try {\n      parsed = JSON.parse(fs.readFileSync(absPath, \"utf8\"));\n    } catch (error) {\n      throw new Error(\"Could not parse JSON file for pretty-printing: \" + absPath + \" (\" + error.message + \")\");\n    }\n    fs.writeFileSync(absPath, JSON.stringify(parsed, null, 2) + \"\\n\", \"utf8\");\n  });\n}\n\nfunction stageStart(testCategory, stageName) {\n  const start = Date.now();\n  process.stdout.write(\"[\" + testCategory + \"] START \" + stageName + \"\\n\");\n  return start;\n}\n\nfunction childTimingTotalSeconds(timings, stageName) {\n  const prefix = stageName + \".\";\n  let total = 0;\n  Object.keys(timings).forEach(function(key) {\n    if (!key.startsWith(prefix)) {\n      return;\n    }\n    const suffix = key.slice(prefix.length);\n    if (!suffix || suffix === \"total\" || suffix.indexOf(\".\") !== -1) {\n      return;\n    }\n    const value = timings[key];\n    if (!Number.isFinite(value)) {\n      return;\n    }\n    total += value;\n  });\n  return total;\n}\n\nfunction stageDone(testCategory, stageName, startMs) {\n  const elapsed = (Date.now() - startMs) / 1000;\n  process.stdout.write(\"[\" + testCategory + \"] DONE \" + stageName + \" (\" + elapsed.toFixed(2) + \"s)\\n\");\n  return elapsed;\n}\n\nfunction stageDoneWithTimings(testCategory, stageName, startMs, timings) {\n  const elapsed = (Date.now() - startMs) / 1000;\n  const childTotal = childTimingTotalSeconds(timings, stageName);\n  const selfTime = Math.max(0, elapsed - childTotal);\n  if (childTotal > 0.0005) {\n    process.stdout.write(\n      \"[\" + testCategory + \"] DONE \" + stageName +\n      \" (total \" + elapsed.toFixed(2) + \"s, self \" + selfTime.toFixed(2) + \"s, child \" + childTotal.toFixed(2) + \"s)\\n\"\n    );\n  } else {\n    process.stdout.write(\"[\" + testCategory + \"] DONE \" + stageName + \" (\" + elapsed.toFixed(2) + \"s)\\n\");\n  }\n  return elapsed;\n}\n\nfunction stageFail(testCategory, stageName, startMs, error, timings) {\n  const elapsed = (Date.now() - startMs) / 1000;\n  const childTotal = childTimingTotalSeconds(timings, stageName);\n  const selfTime = Math.max(0, elapsed - childTotal);\n  if (childTotal > 0.0005) {\n    process.stdout.write(\n      \"[\" + testCategory + \"] FAIL \" + stageName +\n      \" (total \" + elapsed.toFixed(2) + \"s, self \" + selfTime.toFixed(2) + \"s, child \" + childTotal.toFixed(2) + \"s): \" +\n      error.message + \"\\n\"\n    );\n  } else {\n    process.stdout.write(\"[\" + testCategory + \"] FAIL \" + stageName + \" (\" + elapsed.toFixed(2) + \"s): \" + error.message + \"\\n\");\n  }\n  return elapsed;\n}\n\nasync function runStage(testCategory, stageName, timings, fn) {\n  const start = stageStart(testCategory, stageName);\n  try {\n    const result = await fn();\n    timings[stageName] = stageDoneWithTimings(testCategory, stageName, start, timings);\n    return result;\n  } catch (error) {\n    timings[stageName] = stageFail(testCategory, stageName, start, error, timings);\n    throw error;\n  }\n}\n\nfunction discoverLocales(repoRoot) {\n  const localesDir = path.join(repoRoot, \"web\", \"locales\");\n  return fs.readdirSync(localesDir).filter(function(name) {\n    const localeDir = path.join(localesDir, name);\n    return fs.statSync(localeDir).isDirectory() && fs.existsSync(path.join(localeDir, \"tm.json\"));\n  }).sort();\n}\n\nfunction listFilesRecursive(rootDir) {\n  const files = [];\n  function walk(currentDir) {\n    fs.readdirSync(currentDir).forEach(function(entry) {\n      const abs = path.join(currentDir, entry);\n      const rel = path.relative(rootDir, abs);\n      const stat = fs.statSync(abs);\n      if (stat.isDirectory()) {\n        walk(abs);\n      } else {\n        files.push({\n          path: rel,\n          size: stat.size\n        });\n      }\n    });\n  }\n  if (fs.existsSync(rootDir)) {\n    walk(rootDir);\n  }\n  files.sort(function(a, b) { return a.path.localeCompare(b.path); });\n  return files;\n}\n\nfunction fetchWithCurl(url) {\n  const result = spawnSync(\"curl\", [\"-sSfL\", url], {\n    encoding: null\n  });\n  if (result.status !== 0) {\n    const errorText = result.stderr ? String(result.stderr) : \"curl failed\";\n    throw new Error(errorText.trim() || \"curl failed\");\n  }\n  return result.stdout;\n}\n\nfunction overpassMapUrls(bbox) {\n  // Public instances from OSM wiki (current list), using map endpoint for bbox export.\n  return [\n    \"https://overpass.private.coffee/api/map?bbox=\" + encodeURIComponent(bbox),\n    \"https://overpass-api.de/api/map?bbox=\" + encodeURIComponent(bbox),\n    \"https://maps.mail.ru/osm/tools/overpass/api/map?bbox=\" + encodeURIComponent(bbox)\n  ];\n}\n\nfunction parseBoundsFromOsmText(osmText) {\n  const tagMatch = osmText.match(/<bounds\\b[^>]*>/i);\n  if (!tagMatch) {\n    return null;\n  }\n  const tag = tagMatch[0];\n  function readAttr(name) {\n    const doubleQuoted = tag.match(new RegExp(name + \"\\\\s*=\\\\s*\\\"([^\\\"]+)\\\"\", \"i\"));\n    if (doubleQuoted) {\n      return Number(doubleQuoted[1]);\n    }\n    const singleQuoted = tag.match(new RegExp(name + \"\\\\s*=\\\\s*'([^']+)'\", \"i\"));\n    if (singleQuoted) {\n      return Number(singleQuoted[1]);\n    }\n    return NaN;\n  }\n  const parsed = {\n    lonMin: readAttr(\"minlon\"),\n    lonMax: readAttr(\"maxlon\"),\n    latMin: readAttr(\"minlat\"),\n    latMax: readAttr(\"maxlat\")\n  };\n  if (!Number.isFinite(parsed.lonMin) || !Number.isFinite(parsed.lonMax) ||\n      !Number.isFinite(parsed.latMin) || !Number.isFinite(parsed.latMax)) {\n    return null;\n  }\n  return parsed;\n}\n\nfunction inspectOsmPayloadForCache(payload, requestBody, sourceLabel) {\n  const text = Buffer.isBuffer(payload) ? payload.toString(\"utf8\") : String(payload);\n  const head = text.slice(0, 8192).toLowerCase();\n  if (head.indexOf(\"<osm\") === -1) {\n    return {\n      stale: true,\n      reason: sourceLabel + \" is not OSM XML (missing <osm> root element)\"\n    };\n  }\n\n  const area = requestBody && requestBody.effectiveArea ? requestBody.effectiveArea : null;\n  if (!area) {\n    return { stale: false, reason: \"\" };\n  }\n\n  const expected = {\n    lonMin: Number(area.lonMin),\n    lonMax: Number(area.lonMax),\n    latMin: Number(area.latMin),\n    latMax: Number(area.latMax)\n  };\n  if (!Number.isFinite(expected.lonMin) || !Number.isFinite(expected.lonMax) ||\n      !Number.isFinite(expected.latMin) || !Number.isFinite(expected.latMax)) {\n    return { stale: false, reason: \"\" };\n  }\n\n  const found = parseBoundsFromOsmText(text);\n  if (!found) {\n    return {\n      stale: true,\n      reason: sourceLabel + \" is missing a parseable <bounds> tag\"\n    };\n  }\n\n  const requestSpan = Math.max(\n    Math.abs(expected.lonMax - expected.lonMin),\n    Math.abs(expected.latMax - expected.latMin)\n  );\n  const tolerance = Math.max(1e-5, requestSpan * 0.01);\n  const diffs = {\n    lonMin: Math.abs(found.lonMin - expected.lonMin),\n    lonMax: Math.abs(found.lonMax - expected.lonMax),\n    latMin: Math.abs(found.latMin - expected.latMin),\n    latMax: Math.abs(found.latMax - expected.latMax)\n  };\n  const worstDiff = Math.max(diffs.lonMin, diffs.lonMax, diffs.latMin, diffs.latMax);\n  if (worstDiff > tolerance) {\n    return {\n      stale: true,\n      reason:\n        sourceLabel +\n        \" bounds do not match requestBody.effectiveArea (worst diff \" +\n        worstDiff.toExponential(3) +\n        \", tolerance \" +\n        tolerance.toExponential(3) +\n        \")\"\n    };\n  }\n\n  return { stale: false, reason: \"\" };\n}\n\nasync function fetchOsmToCache(cacheOsmPath, requestBody, offline) {\n  if (fs.existsSync(cacheOsmPath)) {\n    const cachedPayload = fs.readFileSync(cacheOsmPath);\n    const cachedInspection = inspectOsmPayloadForCache(cachedPayload, requestBody, \"cached OSM\");\n    if (!cachedInspection.stale) {\n      return;\n    }\n    if (offline) {\n      throw new Error(\"Offline mode enabled and cached OSM is stale: \" + cachedInspection.reason);\n    }\n    process.stdout.write(\"[cache] stale map.osm detected, refetching: \" + cachedInspection.reason + \"\\n\");\n  }\n  if (offline) {\n    throw new Error(\"Offline mode enabled and cached OSM not found: \" + cacheOsmPath);\n  }\n  const area = requestBody && requestBody.effectiveArea ? requestBody.effectiveArea : null;\n  if (!area) {\n    throw new Error(\"requestBody.effectiveArea is required to fetch OSM\");\n  }\n  const bbox = [area.lonMin, area.latMin, area.lonMax, area.latMax].join(\",\");\n  const urls = overpassMapUrls(bbox).concat([\n    \"https://api.openstreetmap.org/api/0.6/map?bbox=\" + encodeURIComponent(bbox)\n  ]);\n  let response = null;\n  let lastError = null;\n  for (let i = 0; i < urls.length; i += 1) {\n    try {\n      const fetchedPayload = fetchWithCurl(urls[i]);\n      const fetchedInspection = inspectOsmPayloadForCache(\n        fetchedPayload,\n        requestBody,\n        \"response from \" + urls[i]\n      );\n      if (fetchedInspection.stale) {\n        throw new Error(fetchedInspection.reason);\n      }\n      response = fetchedPayload;\n      break;\n    } catch (error) {\n      lastError = error;\n    }\n  }\n  if (!response) {\n    throw new Error(\"Could not fetch OSM from any endpoint: \" + (lastError ? lastError.message : \"unknown error\"));\n  }\n  fs.mkdirSync(path.dirname(cacheOsmPath), { recursive: true });\n  fs.writeFileSync(cacheOsmPath, response);\n}\n\nfunction runGenerator(repoRoot, testCategory, sourceOsmPath, pipelineDir, requestBody) {\n  const generatorPath = path.join(repoRoot, \"test\", \"map-content\", \"generate-map-content-from-osm.py\");\n  const args = [\n    generatorPath,\n    \"--osm\",\n    sourceOsmPath,\n    \"--out-dir\",\n    pipelineDir,\n    \"--scale\",\n    String(Number(requestBody.scale || 1400)),\n    \"--log-prefix\",\n    \"[\" + testCategory + \"]  \"\n  ];\n  const contentMode = typeof requestBody.contentMode === \"string\" ? requestBody.contentMode : \"normal\";\n  args.push(\"--content-mode\", contentMode);\n  if (requestBody.noBorders) {\n    args.push(\"--no-borders\");\n  }\n  if (Number.isFinite(Number(requestBody.diameter))) {\n    args.push(\"--diameter\", String(Number(requestBody.diameter)));\n  }\n  if (Number.isFinite(Number(requestBody.size))) {\n    args.push(\"--size\", String(Number(requestBody.size)));\n  }\n  const marker1 = buildMarkerArg(requestBody);\n  if (marker1) {\n    args.push(\"--marker1\", marker1);\n  }\n  if (requestBody.withBlender) {\n    args.push(\"--with-blender\");\n  }\n\n  return new Promise(function(resolve, reject) {\n    const child = spawn(\"python3\", args, {\n      cwd: repoRoot,\n      env: Object.assign({}, process.env, {\n        TOUCH_MAPPER_PRETTY_JSON: \"1\"\n      }),\n      stdio: [\"ignore\", \"pipe\", \"pipe\"]\n    });\n\n    let stdout = \"\";\n    let stderr = \"\";\n    child.stdout.on(\"data\", function(chunk) { stdout += String(chunk); });\n    child.stderr.on(\"data\", function(chunk) {\n      const text = String(chunk);\n      stderr += text;\n      process.stderr.write(text);\n    });\n\n    child.on(\"error\", reject);\n    child.on(\"close\", function(code) {\n      if (code !== 0) {\n        reject(new Error(\"Generator failed with code \" + code + \"\\n\" + stderr));\n        return;\n      }\n      try {\n        resolve(JSON.parse(stdout));\n      } catch (error) {\n        reject(new Error(\"Could not parse generator JSON output: \" + error.message + \"\\nstdout:\\n\" + stdout));\n      }\n    });\n  });\n}\n\nfunction ensureRequestBody(testDef) {\n  if (!testDef.requestBody || typeof testDef.requestBody !== \"object\") {\n    throw new Error(\"Category '\" + testDef.category + \"' missing requestBody object\");\n  }\n  const area = testDef.requestBody.effectiveArea;\n  if (!area || !Number.isFinite(Number(area.lonMin)) || !Number.isFinite(Number(area.lonMax)) ||\n      !Number.isFinite(Number(area.latMin)) || !Number.isFinite(Number(area.latMax))) {\n    throw new Error(\"Category '\" + testDef.category + \"' has invalid requestBody.effectiveArea values\");\n  }\n}\n\nasync function runSingleTest(repoRoot, testDef, args, locales) {\n  const testCategory = testDef.category;\n  const timings = {};\n  const totalStart = Date.now();\n\n  const cacheDir = path.join(repoRoot, \"test\", \"map-content\", \"cache\", testCategory);\n  const outDir = path.join(repoRoot, \"test\", \"map-content\", \"out\", testCategory);\n  const sourceDir = path.join(outDir, \"source\");\n  const pipelineDir = path.join(outDir, \"pipeline\");\n  const descriptionsDir = path.join(outDir, \"descriptions\");\n  const cacheMapInfoPath = path.join(cacheDir, \"map-info.json\");\n  const cacheOsmPath = path.join(cacheDir, \"map.osm\");\n\n  try {\n    await runStage(testCategory, \"resolve-test-config\", timings, async function() {\n      ensureRequestBody(testDef);\n      if (!args.keepExistingOut) {\n        fs.rmSync(outDir, { recursive: true, force: true });\n      }\n      fs.mkdirSync(sourceDir, { recursive: true });\n      fs.mkdirSync(pipelineDir, { recursive: true });\n      fs.mkdirSync(descriptionsDir, { recursive: true });\n      fs.mkdirSync(cacheDir, { recursive: true });\n    });\n\n    const mapInfo = await runStage(testCategory, \"fetch-map-info\", timings, async function() {\n      const info = {\n        category: testDef.category,\n        requestBody: testDef.requestBody\n      };\n      writeJson(cacheMapInfoPath, info);\n      writeJson(path.join(sourceDir, \"map-info.json\"), info);\n      return info;\n    });\n\n    const sourceOsmPath = await runStage(testCategory, \"fetch-map-osm\", timings, async function() {\n      await fetchOsmToCache(cacheOsmPath, mapInfo.requestBody, args.offline);\n      const sourcePath = path.join(sourceDir, \"map.osm\");\n      fs.copyFileSync(cacheOsmPath, sourcePath);\n      return sourcePath;\n    });\n\n    const generation = await runStage(testCategory, \"generate-map-content\", timings, async function() {\n      const requestBody = Object.assign({}, mapInfo.requestBody, {\n        withBlender: args.withBlender\n      });\n      const generated = await runGenerator(repoRoot, testCategory, sourceOsmPath, pipelineDir, requestBody);\n      if (generated && generated.timings && typeof generated.timings === \"object\") {\n        Object.keys(generated.timings).forEach(function(key) {\n          const value = generated.timings[key];\n          if (Number.isFinite(value)) {\n            timings[\"generate-map-content.\" + key] = value;\n          }\n        });\n      }\n      return generated;\n    });\n\n    const structuredByLocale = await runStage(testCategory, \"render-structured-models\", timings, async function() {\n      const byLocale = {};\n      locales.forEach(function(locale) {\n        const artifact = inspect.inspectMapDescription({\n          repoRoot: repoRoot,\n          locale: locale,\n          mapContentPath: generation.mapContentPath\n        });\n        artifact.source.testCategory = testCategory;\n        artifact.source.requestBody = testDef.requestBody;\n        byLocale[locale] = artifact;\n\n        const localeDir = path.join(descriptionsDir, locale);\n        fs.mkdirSync(localeDir, { recursive: true });\n        writeJson(path.join(localeDir, \"structured.json\"), artifact);\n      });\n      return byLocale;\n    });\n\n    await runStage(testCategory, \"render-text-simulations\", timings, async function() {\n      locales.forEach(function(locale) {\n        const artifact = structuredByLocale[locale];\n        const localeDir = path.join(descriptionsDir, locale);\n        const simulationText = renderSimulationText(artifact.mapDescriptionModel);\n        fs.writeFileSync(path.join(localeDir, \"simulated.txt\"), simulationText, \"utf8\");\n      });\n    });\n\n    await runStage(testCategory, \"write-manifest-and-timings\", timings, async function() {\n      writeJson(path.join(outDir, \"manifest.json\"), {\n        test: {\n          category: testDef.category\n        },\n        locales: locales,\n        files: listFilesRecursive(outDir),\n        generatedAt: new Date().toISOString()\n      });\n    });\n\n    const totalSeconds = (Date.now() - totalStart) / 1000;\n    const timingsWithTotal = Object.assign({}, timings, { total: totalSeconds });\n    writeJson(path.join(outDir, \"timings.json\"), timingsWithTotal);\n    await runStage(testCategory, \"pretty-print-json-outputs\", timings, async function() {\n      prettyPrintJsonFiles(outDir);\n    });\n\n    return {\n      category: testCategory,\n      status: \"ok\",\n      durationSec: (Date.now() - totalStart) / 1000,\n      error: null\n    };\n  } catch (error) {\n    return {\n      category: testCategory,\n      status: \"failed\",\n      durationSec: (Date.now() - totalStart) / 1000,\n      error: error.message\n    };\n  }\n}\n\nasync function runWithConcurrency(items, maxParallel, workerFn) {\n  const results = new Array(items.length);\n  let cursor = 0;\n\n  async function worker() {\n    while (true) {\n      const index = cursor;\n      cursor += 1;\n      if (index >= items.length) {\n        return;\n      }\n      results[index] = await workerFn(items[index]);\n    }\n  }\n\n  const workerCount = Math.min(maxParallel, items.length);\n  const workers = [];\n  for (let i = 0; i < workerCount; i += 1) {\n    workers.push(worker());\n  }\n  await Promise.all(workers);\n  return results;\n}\n\nfunction printSummary(results) {\n  process.stdout.write(\"\\nSummary\\n\");\n  results.forEach(function(result) {\n    const line = [\n      result.status.toUpperCase(),\n      result.category,\n      result.durationSec.toFixed(2) + \"s\"\n    ];\n    if (result.error) {\n      line.push(result.error);\n    }\n    process.stdout.write(line.join(\"\\t\") + \"\\n\");\n  });\n}\n\nasync function main() {\n  const args = parseArgs(process.argv.slice(2));\n  const repoRoot = path.resolve(__dirname, \"..\", \"..\");\n  const testsPath = path.join(repoRoot, \"test\", \"map-content\", \"tests.json\");\n  const testsFile = readJson(testsPath);\n  const allTests = Array.isArray(testsFile.tests) ? testsFile.tests : [];\n  const byCategory = {};\n  allTests.forEach(function(testDef) {\n    byCategory[testDef.category] = testDef;\n  });\n\n  const selectedTests = args.all\n    ? allTests.slice()\n    : args.categories.map(function(category) {\n        if (!byCategory[category]) {\n          throw new Error(\"Unknown category: \" + category);\n        }\n        return byCategory[category];\n      });\n\n  if (selectedTests.length === 0) {\n    throw new Error(\"No tests selected\");\n  }\n\n  const locales = discoverLocales(repoRoot);\n  const jobs = args.jobs === null ? selectedTests.length : Math.min(args.jobs, selectedTests.length);\n\n  const results = await runWithConcurrency(selectedTests, jobs, function(testDef) {\n    return runSingleTest(repoRoot, testDef, args, locales);\n  });\n\n  printSummary(results);\n  const failed = results.filter(function(result) { return result.status !== \"ok\"; });\n  if (failed.length) {\n    process.exitCode = 1;\n  }\n}\n\nmain().catch(function(error) {\n  process.stderr.write((error && error.message ? error.message : String(error)) + \"\\n\");\n  process.exit(1);\n});\n"
  },
  {
    "path": "test/map-content/test-map-content-suite.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nrepo_root=\"$(cd \"$(dirname \"$0\")/../..\" && pwd)\"\n\nnode \"$repo_root/test/map-content/run-tests.js\" --all --jobs 2\n\nfor category in simple average complex; do\n  test_root=\"$repo_root/test/map-content/out/$category\"\n  [[ -f \"$test_root/source/map-info.json\" ]]\n  [[ -f \"$test_root/source/map.osm\" ]]\n\n  [[ -f \"$test_root/pipeline/map.obj\" ]]\n  [[ -f \"$test_root/pipeline/map-meta-raw.json\" ]]\n  [[ -f \"$test_root/pipeline/map-meta.augmented.json\" ]]\n  [[ -f \"$test_root/pipeline/map-meta.json\" ]]\n  [[ -f \"$test_root/pipeline/map-content.json\" ]]\n\n  for locale in de en fi nl; do\n    [[ -f \"$test_root/descriptions/$locale/structured.json\" ]]\n    [[ -f \"$test_root/descriptions/$locale/simulated.txt\" ]]\n  done\n\n  [[ -f \"$test_root/manifest.json\" ]]\n  [[ -f \"$test_root/timings.json\" ]]\n\ndone\n\necho \"Map-content suite smoke test passed.\"\n"
  },
  {
    "path": "test/map-content/tests.json",
    "content": "{\n  \"tests\": [\n    {\n      \"category\": \"simple\",\n      \"settingsUrl\": \"https://test.touch-mapper.org/en/area?origin=BlindSquare&addrName=Japan&lat=36.5748441&lon=139.2394179&requestId=simple%2FJapan-m30X-p150Y&printingTech=3d&scale=9999&diameter=1700&size=17&offsetX=0&offsetY=0&advancedMode=true&hideLocationMarker=true&multipartMode=true&multipartXpc=-30&multipartYpc=150&noBorders=true&effectiveArea.latMax=36.60548003920552&effectiveArea.latMin=36.590162069602755&effectiveArea.lonMin=139.22422469849255&effectiveArea.lonMax=139.24321620037688\",\n      \"requestBody\": {\n        \"requestId\": \"simple/Japan-m30X-p150Y\",\n        \"addrShort\": \"Japan\",\n        \"addrLong\": \"Japan, Japan\",\n        \"printingTech\": \"3d\",\n        \"lat\": 36.5748441,\n        \"lon\": 139.2394179,\n        \"scale\": 9999,\n        \"diameter\": 1700,\n        \"size\": 17,\n        \"offsetX\": 0,\n        \"offsetY\": 0,\n        \"advancedMode\": true,\n        \"hideLocationMarker\": true,\n        \"multipartMode\": true,\n        \"multipartXpc\": -30,\n        \"multipartYpc\": 150,\n        \"noBorders\": true,\n        \"effectiveArea\": {\n          \"latMax\": 36.60548003920552,\n          \"latMin\": 36.590162069602755,\n          \"lonMin\": 139.22422469849255,\n          \"lonMax\": 139.24321620037688\n        }\n      }\n    },\n    {\n      \"category\": \"average\",\n      \"settingsUrl\": \"https://test.touch-mapper.org/fi/area?addrName=Otaniemi&lat=60.1879383&lon=24.8318201&requestId=B1eb8c821ee8d40d%2FOtaniemi-m30X-p10Y&printingTech=3d&scale=1000&diameter=170&size=17&offsetX=0&offsetY=0&advancedMode=true&hideLocationMarker=true&multipartMode=true&multipartXpc=-30&multipartYpc=10&noBorders=true&effectiveArea.latMax=60.18885379260042&effectiveArea.latMin=60.18732797159972&effectiveArea.lonMax=24.832432898239837&effectiveArea.lonMin=24.829368907040653\",\n      \"requestBody\": {\n        \"requestId\": \"B1eb8c821ee8d40d/Otaniemi-m30X-p10Y\",\n        \"addrShort\": \"Otaniemi\",\n        \"addrLong\": \"Otaniemi, Espoo, Uusimaa, Finland\",\n        \"printingTech\": \"3d\",\n        \"lat\": 60.1879383,\n        \"lon\": 24.8318201,\n        \"scale\": 1000,\n        \"diameter\": 170,\n        \"size\": 17,\n        \"offsetX\": 0,\n        \"offsetY\": 0,\n        \"advancedMode\": true,\n        \"hideLocationMarker\": true,\n        \"multipartMode\": true,\n        \"multipartXpc\": -30,\n        \"multipartYpc\": 10,\n        \"noBorders\": true,\n        \"effectiveArea\": {\n          \"latMax\": 60.18885379260042,\n          \"latMin\": 60.18732797159972,\n          \"lonMax\": 24.832432898239837,\n          \"lonMin\": 24.829368907040653\n        }\n      }\n    },\n    {\n      \"category\": \"complex\",\n      \"settingsUrl\": \"https://test.touch-mapper.org/en/area?origin=BlindSquare&addrName=Helsinki%20Central%20Railway%20Station&lat=60.1715453&lon=24.9405947&requestId=complex%2FHelsinki%20Central%20Railway%20Station&printingTech=3d&scale=5000&diameter=850&size=17&offsetX=0&offsetY=0&advancedMode=true&hideLocationMarker=true&multipartMode=false&multipartXpc=0&multipartYpc=0&effectiveArea.lonMax=24.948250861678673&effectiveArea.latMax=60.17535986200558&effectiveArea.latMin=60.16773073799442&effectiveArea.lonMin=24.932938538321324\",\n      \"requestBody\": {\n        \"requestId\": \"complex/Helsinki Central Railway Station\",\n        \"addrShort\": \"Helsinki Central Railway Station\",\n        \"addrLong\": \"Helsinki Central Railway Station, Helsinki, Uusimaa, Finland\",\n        \"printingTech\": \"3d\",\n        \"lat\": 60.1715453,\n        \"lon\": 24.9405947,\n        \"scale\": 5000,\n        \"diameter\": 850,\n        \"size\": 17,\n        \"offsetX\": 0,\n        \"offsetY\": 0,\n        \"advancedMode\": true,\n        \"hideLocationMarker\": true,\n        \"multipartMode\": false,\n        \"multipartXpc\": 0,\n        \"multipartYpc\": 0,\n        \"effectiveArea\": {\n          \"lonMax\": 24.948250861678673,\n          \"latMax\": 60.17535986200558,\n          \"latMin\": 60.16773073799442,\n          \"lonMin\": 24.932938538321324\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "test/osm-parser-compat/README.md",
    "content": "# OSM Parser Compatibility Check\n\nThis test compares the custom streaming parser in `converter/prune-only-big-roads.js`\nagainst the `sax` library parser on the same OSM XML input.\n\nIt computes a canonical summary (counts + stable hash) from both parsers and fails\nif any field differs.\n\n## Run\n\nFrom repo root:\n\n```bash\nnode test/osm-parser-compat/compare-osm-parsers.js\n```\n\nOptional explicit OSM path:\n\n```bash\nnode test/osm-parser-compat/compare-osm-parsers.js /path/to/map.osm\n```\n\n## Dependency notes\n\nPreferred setup:\n\n```bash\ncd test/osm-parser-compat\nnpm install\n```\n\nThe script also supports fallback loading of `sax` from:\n\n1. `.tmp/osm-parser-verify/node_modules/sax`\n2. local npm cache extraction via npm's internal `cacache` module\n\nThis keeps the verifier runnable even when external npm registry DNS is flaky.\n"
  },
  {
    "path": "test/osm-parser-compat/compare-osm-parsers.js",
    "content": "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst repoRoot = path.resolve(__dirname, '..', '..');\nconst pruneModulePath = path.join(repoRoot, 'converter', 'prune-only-big-roads.js');\nconst pruneParser = require(pruneModulePath);\n\nfunction hashUpdate(current, text) {\n  let hash = current >>> 0;\n  const s = String(text);\n  for (let i = 0; i < s.length; i += 1) {\n    hash ^= s.charCodeAt(i);\n    hash = Math.imul(hash, 16777619) >>> 0;\n  }\n  return hash >>> 0;\n}\n\nfunction attrsToCanonicalString(attrs) {\n  const keys = Object.keys(attrs || {}).sort();\n  const parts = [];\n  for (let i = 0; i < keys.length; i += 1) {\n    const key = keys[i];\n    parts.push(key + '=' + String(attrs[key]));\n  }\n  return parts.join('|');\n}\n\nfunction normalizeSaxAttrs(attrs) {\n  const out = {};\n  const keys = Object.keys(attrs || {});\n  for (let i = 0; i < keys.length; i += 1) {\n    const key = keys[i];\n    const value = attrs[key];\n    if (value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'value')) {\n      out[key] = String(value.value);\n    } else {\n      out[key] = String(value);\n    }\n  }\n  return out;\n}\n\nfunction ensureSaxFromCache(vendorDir) {\n  const cacheDir = path.join(process.env.HOME || '', '.npm', '_cacache');\n  const possibleKeys = [\n    'make-fetch-happen:request-cache:https://registry.npmjs.org/sax/-/sax-1.2.4.tgz',\n    'make-fetch-happen:request-cache:https://registry.npmjs.org/sax/-/sax-1.4.1.tgz',\n  ];\n  const cacachePath = '/usr/lib/node_modules/npm/node_modules/cacache';\n\n  let cacache = null;\n  try {\n    cacache = require(cacachePath);\n  } catch (_) {\n    return null;\n  }\n\n  if (!fs.existsSync(vendorDir)) {\n    fs.mkdirSync(vendorDir, { recursive: true });\n  }\n  const vendorSaxDir = path.join(vendorDir, 'sax');\n\n  for (let i = 0; i < possibleKeys.length; i += 1) {\n    const key = possibleKeys[i];\n    try {\n      const entry = cacache.get.sync(cacheDir, key);\n      if (!entry || !entry.data) {\n        continue;\n      }\n\n      const tgzPath = path.join(vendorDir, 'sax.tgz');\n      fs.writeFileSync(tgzPath, entry.data);\n\n      if (fs.existsSync(vendorSaxDir)) {\n        fs.rmSync(vendorSaxDir, { recursive: true, force: true });\n      }\n      fs.mkdirSync(vendorSaxDir, { recursive: true });\n\n      const { spawnSync } = require('child_process');\n      const untar = spawnSync('tar', ['-xzf', tgzPath, '-C', vendorSaxDir, '--strip-components=1'], {\n        stdio: 'pipe',\n      });\n      if (untar.status !== 0) {\n        continue;\n      }\n\n      return vendorSaxDir;\n    } catch (_) {\n      continue;\n    }\n  }\n\n  return null;\n}\n\nfunction loadSaxModule() {\n  try {\n    return require('sax');\n  } catch (_) {}\n\n  const fallbackLocations = [\n    path.join(repoRoot, '.tmp', 'osm-parser-verify', 'node_modules', 'sax'),\n    path.join(__dirname, 'node_modules', 'sax'),\n  ];\n  for (let i = 0; i < fallbackLocations.length; i += 1) {\n    const p = fallbackLocations[i];\n    try {\n      return require(p);\n    } catch (_) {}\n  }\n\n  const vendorDir = path.join(__dirname, '.vendor');\n  const cachedSaxDir = ensureSaxFromCache(vendorDir);\n  if (cachedSaxDir) {\n    try {\n      return require(cachedSaxDir);\n    } catch (_) {}\n  }\n\n  throw new Error(\n    [\n      'Could not load sax parser.',\n      'Tried: local node_modules, .tmp/osm-parser-verify/node_modules/sax, and npm cache extraction.',\n      'Install with: cd test/osm-parser-compat && npm install',\n    ].join(' ')\n  );\n}\n\nfunction collectSummaryWithSaxParser(osmPath) {\n  const sax = loadSaxModule();\n\n  return new Promise((resolve, reject) => {\n    const summary = {\n      nodes: 0,\n      ways: 0,\n      relations: 0,\n      tags: 0,\n      ndRefs: 0,\n      relationMembers: 0,\n      relationMemberNode: 0,\n      relationMemberWay: 0,\n      relationMemberRelation: 0,\n      selfClosingNodes: 0,\n      selfClosingWays: 0,\n      selfClosingRelations: 0,\n      hash: 2166136261 >>> 0,\n    };\n\n    function addHash(text) {\n      summary.hash = hashUpdate(summary.hash, text);\n    }\n\n    let currentNode = false;\n    let currentWay = false;\n    let currentRelation = false;\n\n    const parser = sax.createStream(true, {\n      trim: false,\n      normalize: false,\n      xmlns: false,\n      lowercase: true,\n      position: false,\n      strictEntities: false,\n    });\n\n    parser.on('opentag', (tag) => {\n      const name = String(tag.name || '').toLowerCase();\n      const attrs = normalizeSaxAttrs(tag.attributes || {});\n      const selfClosing = !!tag.isSelfClosing;\n\n      if (currentNode) {\n        if (name === 'tag') {\n          summary.tags += 1;\n          addHash('N:TAG:' + attrsToCanonicalString(attrs));\n        }\n        return;\n      }\n\n      if (currentWay) {\n        if (name === 'nd') {\n          summary.ndRefs += 1;\n          addHash('W:ND:' + attrsToCanonicalString(attrs));\n        } else if (name === 'tag') {\n          summary.tags += 1;\n          addHash('W:TAG:' + attrsToCanonicalString(attrs));\n        }\n        return;\n      }\n\n      if (currentRelation) {\n        if (name === 'member') {\n          summary.relationMembers += 1;\n          const memberType = attrs.type || '';\n          if (memberType === 'node') {\n            summary.relationMemberNode += 1;\n          } else if (memberType === 'way') {\n            summary.relationMemberWay += 1;\n          } else if (memberType === 'relation') {\n            summary.relationMemberRelation += 1;\n          }\n          addHash('R:MEM:' + attrsToCanonicalString(attrs));\n        } else if (name === 'tag') {\n          summary.tags += 1;\n          addHash('R:TAG:' + attrsToCanonicalString(attrs));\n        }\n        return;\n      }\n\n      if (name === 'node') {\n        summary.nodes += 1;\n        if (selfClosing) {\n          summary.selfClosingNodes += 1;\n        }\n        addHash('N:OPEN:' + attrsToCanonicalString(attrs));\n        if (!selfClosing) {\n          currentNode = true;\n        }\n        return;\n      }\n\n      if (name === 'way') {\n        summary.ways += 1;\n        if (selfClosing) {\n          summary.selfClosingWays += 1;\n        }\n        addHash('W:OPEN:' + attrsToCanonicalString(attrs));\n        if (!selfClosing) {\n          currentWay = true;\n        }\n        return;\n      }\n\n      if (name === 'relation') {\n        summary.relations += 1;\n        if (selfClosing) {\n          summary.selfClosingRelations += 1;\n        }\n        addHash('R:OPEN:' + attrsToCanonicalString(attrs));\n        if (!selfClosing) {\n          currentRelation = true;\n        }\n      }\n    });\n\n    parser.on('closetag', (rawName) => {\n      const name = String(rawName || '').toLowerCase();\n      if (name === 'node' && currentNode) {\n        addHash('N:CLOSE');\n        currentNode = false;\n      } else if (name === 'way' && currentWay) {\n        addHash('W:CLOSE');\n        currentWay = false;\n      } else if (name === 'relation' && currentRelation) {\n        addHash('R:CLOSE');\n        currentRelation = false;\n      }\n    });\n\n    parser.on('error', (err) => reject(err));\n    parser.on('end', () => {\n      summary.hashHex = (summary.hash >>> 0).toString(16).padStart(8, '0');\n      resolve(summary);\n    });\n\n    fs.createReadStream(osmPath, { encoding: 'utf8' })\n      .on('error', reject)\n      .pipe(parser);\n  });\n}\n\nfunction diffSummaries(left, right) {\n  const diffs = [];\n  const keys = Object.keys(left);\n  for (let i = 0; i < keys.length; i += 1) {\n    const key = keys[i];\n    if (!Object.prototype.hasOwnProperty.call(right, key)) {\n      diffs.push(key + ': missing from sax summary');\n      continue;\n    }\n    if (left[key] !== right[key]) {\n      diffs.push(key + ': custom=' + JSON.stringify(left[key]) + ' sax=' + JSON.stringify(right[key]));\n    }\n  }\n  return diffs;\n}\n\nasync function compareOne(label, osmPath) {\n  const custom = await pruneParser.collectSummaryWithCustomParser(osmPath);\n  const sax = await collectSummaryWithSaxParser(osmPath);\n  const diffs = diffSummaries(custom, sax);\n\n  console.log('--- ' + label + ' ---');\n  console.log('file:', osmPath);\n  console.log('custom:', JSON.stringify(custom));\n  console.log('sax:   ', JSON.stringify(sax));\n\n  if (diffs.length > 0) {\n    console.log('DIFFS:');\n    for (let i = 0; i < diffs.length; i += 1) {\n      console.log('  - ' + diffs[i]);\n    }\n    return false;\n  }\n\n  console.log('MATCH: summaries are identical');\n  return true;\n}\n\nfunction writeFixture(pathname, text) {\n  fs.mkdirSync(path.dirname(pathname), { recursive: true });\n  fs.writeFileSync(pathname, text, 'utf8');\n}\n\nfunction ensureEdgeFixtures() {\n  const fixturesDir = path.join(__dirname, 'fixtures');\n\n  writeFixture(\n    path.join(fixturesDir, 'edge-self-closing.osm'),\n    '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n    '<osm version=\"0.6\" generator=\"fixture\">\\n' +\n    '  <node id=\"1\" lat=\"60\" lon=\"24\"/>\\n' +\n    '  <way id=\"10\"/>\\n' +\n    '  <relation id=\"20\"/>\\n' +\n    '</osm>\\n'\n  );\n\n  writeFixture(\n    path.join(fixturesDir, 'edge-nested.osm'),\n    '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n    '<osm version=\"0.6\" generator=\"fixture\">\\n' +\n    '  <node id=\"1\" lat=\"60\" lon=\"24\"><tag k=\"name\" v=\"A &amp; B\"/></node>\\n' +\n    '  <node id=\"2\" lat=\"60.1\" lon=\"24.1\"/>\\n' +\n    '  <way id=\"10\"><nd ref=\"1\"/><nd ref=\"2\"/><tag k=\"highway\" v=\"residential\"/></way>\\n' +\n    '  <relation id=\"20\"><member type=\"way\" ref=\"10\" role=\"outer\"/><tag k=\"type\" v=\"multipolygon\"/></relation>\\n' +\n    '</osm>\\n'\n  );\n\n  writeFixture(\n    path.join(fixturesDir, 'edge-entities.osm'),\n    '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n    '<osm version=\"0.6\" generator=\"fixture\">\\n' +\n    '  <!-- comment -->\\n' +\n    '  <node id=\"3\" lat=\"61\" lon=\"25\">\\n' +\n    '    <tag k=\"note\" v=\"Tom &apos;N&apos; Jerry &quot;Q&quot;\"/>\\n' +\n    '  </node>\\n' +\n    '  <relation id=\"30\">\\n' +\n    '    <member type=\"node\" ref=\"3\" role=\"\"/>\\n' +\n    '    <member type=\"relation\" ref=\"20\" role=\"sub\"/>\\n' +\n    '  </relation>\\n' +\n    '</osm>\\n'\n  );\n\n  return [\n    { label: 'edge-self-closing', path: path.join(fixturesDir, 'edge-self-closing.osm') },\n    { label: 'edge-nested', path: path.join(fixturesDir, 'edge-nested.osm') },\n    { label: 'edge-entities', path: path.join(fixturesDir, 'edge-entities.osm') },\n  ];\n}\n\nasync function main() {\n  const explicitOsm = process.argv[2];\n  const complexOsm = path.join(repoRoot, 'test', 'map-content', 'cache', 'complex', 'map.osm');\n  const fixtures = ensureEdgeFixtures();\n\n  const targets = [];\n  targets.push({ label: 'complex', path: explicitOsm || complexOsm });\n  for (let i = 0; i < fixtures.length; i += 1) {\n    targets.push(fixtures[i]);\n  }\n\n  let ok = true;\n  for (let i = 0; i < targets.length; i += 1) {\n    const target = targets[i];\n    const oneOk = await compareOne(target.label, target.path);\n    if (!oneOk) {\n      ok = false;\n    }\n  }\n\n  if (!ok) {\n    process.exit(1);\n  }\n}\n\nmain().catch((err) => {\n  console.error(err && err.stack ? err.stack : String(err));\n  process.exit(1);\n});\n"
  },
  {
    "path": "test/osm-parser-compat/fixtures/edge-entities.osm",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<osm version=\"0.6\" generator=\"fixture\">\n  <!-- comment -->\n  <node id=\"3\" lat=\"61\" lon=\"25\">\n    <tag k=\"note\" v=\"Tom &apos;N&apos; Jerry &quot;Q&quot;\"/>\n  </node>\n  <relation id=\"30\">\n    <member type=\"node\" ref=\"3\" role=\"\"/>\n    <member type=\"relation\" ref=\"20\" role=\"sub\"/>\n  </relation>\n</osm>\n"
  },
  {
    "path": "test/osm-parser-compat/fixtures/edge-nested.osm",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<osm version=\"0.6\" generator=\"fixture\">\n  <node id=\"1\" lat=\"60\" lon=\"24\"><tag k=\"name\" v=\"A &amp; B\"/></node>\n  <node id=\"2\" lat=\"60.1\" lon=\"24.1\"/>\n  <way id=\"10\"><nd ref=\"1\"/><nd ref=\"2\"/><tag k=\"highway\" v=\"residential\"/></way>\n  <relation id=\"20\"><member type=\"way\" ref=\"10\" role=\"outer\"/><tag k=\"type\" v=\"multipolygon\"/></relation>\n</osm>\n"
  },
  {
    "path": "test/osm-parser-compat/fixtures/edge-self-closing.osm",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<osm version=\"0.6\" generator=\"fixture\">\n  <node id=\"1\" lat=\"60\" lon=\"24\"/>\n  <way id=\"10\"/>\n  <relation id=\"20\"/>\n</osm>\n"
  },
  {
    "path": "test/osm-parser-compat/package.json",
    "content": "{\n  \"name\": \"touch-mapper-osm-parser-compat\",\n  \"private\": true,\n  \"version\": \"1.0.0\",\n  \"description\": \"Compatibility checks between custom OSM parser and sax\",\n  \"license\": \"UNLICENSED\",\n  \"dependencies\": {\n    \"sax\": \"1.2.4\"\n  },\n  \"scripts\": {\n    \"compare\": \"node compare-osm-parsers.js\"\n  }\n}\n"
  },
  {
    "path": "test/run-osm2world-regression.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# This regression test runs OSM2World and verifies its outputs (map.obj and map-meta.json) haven't changed.\n\nrepo_root=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\ndata_dir=\"$repo_root/test/data\"\nwork_dir=\"$(mktemp -d \"${TMPDIR:-/tmp}/tm-osm2world-test.XXXXXX\")\"\nlog_path=\"$work_dir/osm-to-tactile.stdout.log\"\ntest_ok=0\n\ncleanup() {\n  if [[ \"$test_ok\" -eq 1 ]]; then\n    rm -rf \"$work_dir\"\n  else\n    echo \"Test failed; keeping temp dir: $work_dir\" >&2\n    echo \"osm-to-tactile stdout: $log_path\" >&2\n  fi\n}\ntrap cleanup EXIT\n\nif ! command -v json_pp >/dev/null 2>&1; then\n  echo \"json_pp is required for this test (used to pretty-print JSON output).\" >&2\n  exit 1\nfi\n\nosm2world_jar=\"$repo_root/converter/OSM2World/build/OSM2World.jar\"\nif [[ ! -f \"$osm2world_jar\" ]]; then\n  echo \"Missing OSM2World jar: $osm2world_jar\" >&2\n  exit 1\nfi\n\ncp \"$data_dir/map.osm\" \"$work_dir/map.osm\"\n\npython3 \"$repo_root/converter/osm-to-tactile.py\" \\\n  --scale 1400 \\\n  --diameter 238 \\\n  --size 17 \\\n  \"$work_dir/map.osm\" >\"$log_path\"\n\njson_pp < \"$work_dir/map-meta.json\" > \"$work_dir/map-meta.indented.json\"\n\nif ! diff -u \"$data_dir/map.obj\" \"$work_dir/map.obj\"; then\n  echo \"map.obj differs from expected output.\" >&2\n  exit 1\nfi\n\nif ! diff -u \"$data_dir/map-meta.indented.json\" \"$work_dir/map-meta.indented.json\"; then\n  echo \"map-meta.json differs from expected output.\" >&2\n  exit 1\nfi\n\necho \"OSM2World regression test passed.\"\ntest_ok=1\n"
  },
  {
    "path": "translation/spreadsheet-to-translations.py",
    "content": "#!/usr/bin/python2\n\nfrom __future__ import print_function\nimport sys\nimport argparse\nimport json\nimport xlrd\nfrom collections import OrderedDict\nfrom distutils.command.config import LANG_EXT\n\ndef do_cmdline():\n    parser = argparse.ArgumentParser(description='''Convert translation spreadsheet into locale JSON''')\n    parser.add_argument('--locales_dir', metavar='DIR', required=True)\n    parser.add_argument('spreadsheet', metavar='SPREADSHEET')\n    args = parser.parse_args()\n    return args\n\ndef read_excel(filepath):\n    book = xlrd.open_workbook(filename=filepath)\n    sheet = book.sheet_by_index(0)\n    header = [sheet.cell(0, col_index).value for col_index in xrange(sheet.ncols)]\n    if 3 < len(header) > 4 or header[0] != 'code' or header[1] != 'en':\n        raise Exception(\"invalid header: \" + header)\n    lang = header[2]\n\n    tr = {}\n    for row_index in xrange(1, sheet.nrows):\n        tr[sheet.cell(row_index, 0).value.strip()] = sheet.cell(row_index, 2).value.strip()\n\n    return lang, tr\n\ndef main():\n    args = do_cmdline()\n    with open(args.locales_dir + '/en/tm.json') as f:\n        tr_en = json.load(f, object_pairs_hook=OrderedDict)\n    lang, tr_new = read_excel(args.spreadsheet)\n    with open(args.locales_dir + '/' + lang + '/tm.json') as f:\n        tr_old = json.load(f)\n    out = OrderedDict()\n    for code, value_en in tr_en.iteritems():\n        val = tr_new.get(code, tr_old.get(code))\n        if val:\n            out[code] = val\n    out_path = args.locales_dir + '/' + lang + '/tm.json'\n    print(\"creating \" + out_path)\n    with open(out_path, 'w') as f:\n        f.write(json.dumps(out, indent=2, ensure_ascii=False, separators=(',', ': ')).encode('utf8'))\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "translation/translations-to-spreadsheet.py",
    "content": "#!/usr/bin/python2\n\nfrom __future__ import print_function\nimport sys\nimport argparse\nimport json\nimport pprint\nimport xlwt\nimport xlrd\nfrom collections import OrderedDict\n\ndef do_cmdline():\n    parser = argparse.ArgumentParser(description='''Convert one locale JSON to a spreadsheet file''')\n    parser.add_argument('--language', metavar='LANG', required=True)\n    parser.add_argument('locales_dir', metavar='LOCALES_DIR')\n    args = parser.parse_args()\n    return args\n\ndef read_old_excel(filepath, lang):\n    tr = {}\n    book = xlrd.open_workbook(filename=filepath)\n    if not book:\n        return tr\n    sheet = book.sheet_by_index(0)\n    if sheet.cell(0, 2).value != lang:\n        raise Exception(\"existing file %s has wrong language %s != %s\" % (lang, sheet.cell(0, 2), lang))\n    return sheet\n\ndef write_excel(excel_path, translations, lang):\n    style_bold = xlwt.easyxf('font: bold on')\n\n    book = xlwt.Workbook(encoding=\"utf-8\")\n    sheet = book.add_sheet(\"Touch Mapper \" + lang)\n    sheet.col(0).width = 7000\n    sheet.col(1).width = 15000\n    sheet.col(2).width = 15000\n    sheet.col(3).width = 15000\n    sheet.write(0, 0, \"code\", style_bold)\n    sheet.write(0, 1, \"en\", style_bold)\n    sheet.write(0, 2, lang, style_bold)\n    sheet.write(0, 3, lang + ' previous', style_bold)\n\n    style_wrap = xlwt.XFStyle()\n    style_wrap.alignment.wrap = 1\n\n    style_bold = xlwt.XFStyle()\n    style_bold.alignment.wrap = 1\n    font = xlwt.Font()\n    font.bold = True\n    style_bold.font = font\n\n    tr_en = translations['en']\n    tr_en_old = translations['en.old']\n    tr_x = translations[lang]\n    tr_x_old = translations[lang + '.old']\n    line = 1\n    for code, value in tr_en.iteritems():\n        sheet.write(line, 0, code)\n        sheet.write(line, 1, value, style_wrap)\n        if tr_en.get(code, '') == tr_en_old.get(code, ''):\n            sheet.write(line, 2, tr_x.get(code, ''), style_wrap)\n        else:\n            if tr_x_old.get(code, '') == '':\n                sheet.write(line, 2, 'NEW', style_bold)\n            else:\n                sheet.write(line, 2, 'ENGLISH TEXT CHANGED', style_bold)\n                sheet.write(line, 3, tr_x_old.get(code, ''))\n        line = line + 1\n    book.save(excel_path)\n\ndef main():\n    args = do_cmdline()\n    translations = {}\n\n    # Read new strings\n    with open(args.locales_dir + '/en/tm.json') as f:\n        translations['en'] = json.load(f, object_pairs_hook=OrderedDict)\n    with open(args.locales_dir + '/' + args.language + '/tm.json') as f:\n        translations[args.language] = json.load(f)\n    excel_path = 'translations-' + args.language + '.xls'\n\n    # Read old strings and translations\n    old_excel = read_old_excel(excel_path, args.language)\n    translations[args.language + '.old'] = {}\n    translations['en.old'] = {}\n    for row_index in xrange(1, old_excel.nrows):\n        code = old_excel.cell(row_index, 0).value.strip()\n        translations[args.language + '.old'][code] = old_excel.cell(row_index, 2).value.strip()\n        translations['en.old'][code]               = old_excel.cell(row_index, 1).value.strip()\n\n    write_excel(excel_path, translations, args.language)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "web/.eslintrc",
    "content": "{\n    \"extends\": \"eslint:recommended\",\n\n    \"rules\": {\n        \"no-alert\": 2,\n        \"no-array-constructor\": 2,\n        \"no-caller\": 2,\n        \"no-catch-shadow\": 2,\n        \"no-eval\": 2,\n        \"no-extend-native\": 2,\n        \"no-extra-bind\": 2,\n        \"no-implied-eval\": 2,\n        \"no-iterator\": 2,\n        \"no-label-var\": 2,\n        \"no-labels\": 2,\n        \"no-lone-blocks\": 2,\n        \"no-loop-func\": 2,\n        \"no-multi-spaces\": 2,\n        \"no-multi-str\": 2,\n        \"no-native-reassign\": 2,\n        \"no-new\": 2,\n        \"no-new-func\": 2,\n        \"no-new-object\": 2,\n        \"no-new-wrappers\": 2,\n        \"no-octal-escape\": 2,\n        \"no-process-exit\": 2,\n        \"no-proto\": 2,\n        \"no-return-assign\": 2,\n        \"no-script-url\": 2,\n        \"no-sequences\": 2,\n        \"no-shadow\": 2,\n        \"no-shadow-restricted-names\": 2,\n        \"no-spaced-func\": 2,\n        \"no-trailing-spaces\": 2,\n        \"no-undef-init\": 2,\n        \"no-underscore-dangle\": 2,\n        \"no-unused-expressions\": 2,\n        \"no-use-before-define\": 0,\n        \"no-with\": 2,\n        \"camelcase\": 0,\n        \"comma-spacing\": 2,\n        \"consistent-return\": 2,\n        \"curly\": 0,\n        \"dot-notation\": [2, { \"allowKeywords\": true }],\n        \"eol-last\": 2,\n        \"no-extra-parens\": [2, \"functions\"],\n        \"eqeqeq\": 0,\n        \"key-spacing\": [2, { \"beforeColon\": false, \"afterColon\": true }],\n        \"new-cap\": 0,\n        \"new-parens\": 2,\n        \"quotes\": [2, \"double\"],\n        \"semi\": 2,\n        \"semi-spacing\": [2, {\"before\": false, \"after\": true}],\n        \"space-infix-ops\": 2,\n        \"space-unary-ops\": [2, { \"words\": true, \"nonwords\": false }],\n        \"yoda\": [2, \"never\"]\n    },\n\n    \"parserOptions\": {\n        \"ecmaVersion\": 6\n    },\n\n    \"env\": {\n        \"browser\": true\n    }\n}\n"
  },
  {
    "path": "web/.gitignore",
    "content": "/build\n/dist\n/node_modules\n"
  },
  {
    "path": "web/Makefile",
    "content": "\nENVIRONMENT ?= dev\n\nbuild: node_modules\n\t./pre2src.py\n\trm -rf build\n\tnode build.js\n\t./create-env-js.sh $(ENVIRONMENT) >build/scripts/environment.js\n\nnode_modules: package.json\n\tnpm install\n\nserve:\n\tnode_modules/.bin/http-server -c-1 -p 9000 -a 127.0.0.1 build\n\nwatch:\n\tmake build\n\twhile true; do inotifywait -q -e close_write,moved_to,create build.js pre-src pre-src/* src/*.html src/styles/* src/scripts/* locales locales/*; echo doing; make build; done\n\n.PHONY: build\n"
  },
  {
    "path": "web/build.js",
    "content": "var fs = require('fs');\nvar path = require('path');\nvar Metalsmith = require('metalsmith');\nvar i18next = require('metalsmith-i18next');\nvar inPlace = require('metalsmith-in-place');\nvar concat = require('metalsmith-concat');\nvar ignore = require('metalsmith-ignore');\nvar less = require('metalsmith-less')\nvar copy = require('metalsmith-copy');\nvar replace = require('metalsmith-text-replace');\nvar eslint = require(\"metalsmith-eslint\");\n\nfunction allowPluginErrors(plugin, pluginName) {\n    return function(files, metalsmith, done) {\n        plugin(files, metalsmith, function(err) {\n            if (err) {\n                console.warn(\"Warning: \" + pluginName + \" failed: \" + err.message);\n            }\n            done();\n        });\n    };\n}\n\nvar metalsmith = Metalsmith(__dirname)\n    .use(ignore([\n        '**/*~',\n        '**/*.js.prod',\n        '**/*.js.test',\n        '**/three-20160121.min.js',\n        '**/three-STLLoader-20160122.js',\n    ]))\n    .use(allowPluginErrors(eslint({\n        src: [\"**/*.js\", \"!**/vendor-common/**/*.js\", \"!**/vendor-other/**/*.js\"],\n        formatter: \"unix\",\n        eslintConfig: JSON.parse(fs.readFileSync(path.join(process.cwd(), \".eslintrc\"), \"utf8\"))\n    }), \"eslint\"))\n    .use(copy({\n        pattern: '**/environment.js.dev',\n        transform: function (file) {\n            return file.replace(/\\.[^/.]+$/, \"\");\n        },\n        move: true,\n    }))\n    .use(replace({\n        '**/*.ect': {\n            find: /{{ ([a-z0-9_]+) }}/gi,\n            replace: function(match, str) { return \"<%= @t('\" + str + \"') %>\"; }\n        }\n    }))\n    .use(i18next({\n        pattern: '*.ect',\n        locales: ['de', 'en', 'es', 'fi', 'nl'],\n        namespaces: ['tm'],\n        fallbackLng: 'en',\n    }))\n    .use(inPlace({\n        engine: 'ect',\n        pattern:  '**/*.ect',\n        rename: true,\n    }))\n    .use(less({\n        pattern: ['**/styles/common.less', '**/styles/index.less', '**/styles/area.less', '**/styles/map.less', '**/styles/help.less'],\n        useDynamicSourceMap: true,\n    }))\n    .use(concat({\n        files: ['**/util.js', '**/map-calc.js', '**/model-preview.js', '**/language.js',\n                '**/email.js', '**/multipart-mode.js', '**/backbone-helpers.js', '**/map-creation.js',\n                '**/osm-preview.js', '**/map-desc-ways.js', '**/map-desc-areas.js', '**/map-desc-pois.js', '**/map-description.js' ],\n        output: 'scripts/app-common.js'\n    }))\n    .use(concat({\n        files: ['**/jquery-2.1.4.min.js', '**/underscore-1.13.1.min.js', '**/backbone-1.2.3.min.js', ],\n        output: 'scripts/vendor-common.js'\n    }))\n    .use(copy({ pattern: '**/aws-sdk*.js', directory: 'scripts', move: true, }))\n    .use(copy({ pattern: '**/ol-*.js',     directory: 'scripts', move: true, }))\n    .use(copy({ pattern: '**/three-r182*.js', directory: 'scripts', move: true, }))\n    .use(copy({\n        pattern: '**/three-addons/**/*.js',\n        directory: 'scripts',\n        transform: function (file) {\n            return file.replace(/^.*three-addons\\//, 'scripts/three-addons/');\n        },\n        move: true,\n    }))\n//  .use(permalinks({\n//      pattern: ':title'\n//  })\n    .build(function(err){\n        if (err) throw err;\n    });\n"
  },
  {
    "path": "web/create-env-js.sh",
    "content": "#!/bin/bash\n\nif [[ $# != 1 ]]; then\n    echo \"Usage: $0 ENVIRONMENT\"\n    exit 1\nfi\nenvironment=$1\n\ncd \"$( dirname \"${BASH_SOURCE[0]}\" )\"\n\neval $( ../install/parameters.sh $environment )\nif [[ $is_dev_env == true ]]; then\n    tm_domain=$( aws cloudfront list-distributions | jq --raw-output \".DistributionList.Items[] | select(.Origins.Items[].DomainName == \\\"$env_name.maps.touch-mapper.s3.amazonaws.com\\\") | .DomainName\" )\n    if [[ ! $tm_domain ]]; then\n        echo \"$0: no CloudFront distribution for '$env_name' found\" >&2\n        exit 1\n    fi\nelse\n    tm_domain=$domain\nfi\n\nmap_request_sqs_queue=$( aws sqs get-queue-url --queue-name \"${environment}-requests-touch-mapper\" --query QueueUrl --output text 2>/dev/null )\nif [[ -z \"$map_request_sqs_queue\" || \"$map_request_sqs_queue\" == \"None\" ]]; then\n    echo \"$0: failed to resolve SQS queue URL for ${environment}-requests-touch-mapper\" >&2\n    exit 1\nfi\n\necho \"window.TM_ENVIRONMENT = '$env_name';\"\necho \"window.TM_DOMAIN = '$tm_domain';\"\necho \"window.TM_REGION = '$(aws configure get region)';\"\necho \"window.TM_MAP_REQUEST_SQS_QUEUE = '$map_request_sqs_queue';\"\n"
  },
  {
    "path": "web/eslint.config.js",
    "content": "export default [\n  {\n    files: [\"**/*.js\"],\n    languageOptions: {\n      ecmaVersion: \"latest\",\n      sourceType: \"module\"\n    },\n  },\n];\n"
  },
  {
    "path": "web/locales/de/tm.json",
    "content": "{\n    \"location2_top_left\": \"oberes linkes Viertel\",\n    \"location2_top_row\": \"obere Hälfte\",\n    \"location2_top_right\": \"oberes rechtes Viertel\",\n    \"location2_right_column\": \"rechte Hälfte\",\n    \"location2_bottom_right\": \"unteres rechtes Viertel\",\n    \"location2_bottom_row\": \"untere Hälfte\",\n    \"location2_bottom_left\": \"unteres linkes Viertel\",\n    \"location2_left_column\": \"linke Hälfte\",\n    \"location3_middle_center\": \"Mitte\",\n    \"location3_top_center\": \"oben Mitte\",\n    \"location3_bottom_center\": \"unten Mitte\",\n    \"location3_middle_left\": \"links Mitte\",\n    \"location3_middle_right\": \"rechts Mitte\",\n    \"location3_top_left\": \"obere linke Ecke\",\n    \"location3_top_row\": \"obere Kante\",\n    \"location3_mostly_top_row\": \"größtenteils obere Kante\",\n    \"location3_top_left_to_mc\": \"von der oberen linken Ecke zur Mitte\",\n    \"location3_top_left_and_center\": \"obere Kante links\",\n    \"location3_top_right_and_center\": \"obere Kante rechts\",\n    \"location3_top_right\": \"obere rechte Ecke\",\n    \"location3_right_column\": \"rechte Kante\",\n    \"location3_mostly_right_column\": \"größtenteils rechte Kante\",\n    \"location3_top_right_to_mc\": \"von der Mitte zur oberen rechten Ecke\",\n    \"location3_top_right_and_middle\": \"rechte Kante oben\",\n    \"location3_bottom_right_and_middle\": \"von der Mitte zur unteren rechten Ecke\",\n    \"location3_bottom_right\": \"untere rechte Ecke\",\n    \"location3_bottom_row\": \"untere Kante\",\n    \"location3_mostly_bottom_row\": \"größtenteils untere Kante\",\n    \"location3_bottom_right_to_mc\": \"von der Mitte zur unteren rechten Ecke\",\n    \"location3_bottom_right_and_center\": \"untere Kante rechts\",\n    \"location3_bottom_left_and_center\": \"untere Kante links\",\n    \"location3_bottom_left\": \"untere linke Ecke\",\n    \"location3_left_column\": \"linke Kante\",\n    \"location3_mostly_left_column\": \"größtenteils linke Kante\",\n    \"location3_bottom_left_to_mc\": \"von der Mitte zur unteren linken Ecke\",\n    \"location3_bottom_left_and_middle\": \"linke Kante unten\",\n    \"location3_top_left_and_middle\": \"linke Kante oben\",\n    \"location3_middle_row\": \"in der Mitte von links nach rechts\",\n    \"location3_center_column\": \"in der Mitte von oben nach unten\",\n    \"location3_top_left_diagonal\": \"diagonal von oben links nach unten rechts\",\n    \"location3_top_right_diagonal\": \"diagonal von unten links nach oben rechts\",\n    \"location3_top_left_to_middle_right\": \"von der oberen linken Ecke zur Mitte rechts\",\n    \"location3_top_right_to_middle_left\": \"von der linken Mitte zur oberen rechten Ecke\",\n    \"location3_top_right_to_bottom_center\": \"von der unteren Mitte zur oberen rechten Ecke\",\n    \"location3_bottom_right_to_top_center\": \"von der oberen Mitte zur unteren rechten Ecke\",\n    \"location3_bottom_right_to_middle_left\": \"von der linken Mitte zur unteren rechten Ecke\",\n    \"location3_bottom_left_to_middle_right\": \"von der unteren linken Ecke zur Mitte rechts\",\n    \"location3_bottom_left_to_top_center\": \"von der unteren linken Ecke zur oberen Mitte\",\n    \"location3_top_left_to_bottom_center\": \"von der oberen linken Ecke zur unteren Mitte\",\n    \"location3_near_top_left\": \"nahe der oberen linken Ecke\",\n    \"location3_top_center_to_middle_left\": \"von der linken Mitte zur oberen Mitte\",\n    \"location3_near_top_right\": \"nahe der oberen rechten Ecke\",\n    \"location3_top_center_to_middle_right\": \"von der oberen Mitte zur rechten Mitte\",\n    \"location3_near_bottom_right\": \"nahe der unteren rechten Ecke\",\n    \"location3_middle_right_to_bottom_center\": \"von der rechten Mitte zur unteren Mitte\",\n    \"location3_near_bottom_left\": \"nahe der unteren linken Ecke\",\n    \"location3_middle_left_to_bottom_center\": \"von der linken Mitte zur unteren Mitte\",\n    \"location_general\": \"viele Bereiche\",\n    \"skip_to_main_content\": \"Zum Hauptinhalt springen\",\n    \"meta_description\": \"Taktile Karten einfach von beliebigen Adressen erstellen. Zum Bestellen oder Selberdrucken\",\n    \"meta_keywords\": \"taktile Karte,blind,Sehbehinderung,Hilfsmittel für Blinde,Hilfsmittel für Sehbehinderte,Technologie für Blinde,Technologie für Sehbehinderte,3D-Druck\",\n    \"meta_og_site_name\": \"Touch Mapper - Taktile Karten für sehbehinderte Menschen\",\n    \"title\": \"Taktile Karten einfach erstellen\",\n    \"blurb\": \"Taktile Karten einfach von beliebigen Adressen erstellen\",\n    \"language\": \"Sprache\",\n    \"help\": \"Hilfe\",\n    \"open_help_aria\": \"Hilfe in einem neuen Tab öffnen\",\n    \"map_content_external_link_aria\": \"Externer Link, öffnet in einem neuen Tab\",\n    \"breadcrumb_nav_title\": \"Brotkrumennavigation\",\n    \"breadcrumb_home\": \"Adresssuche\",\n    \"breadcrumb_area\": \"Einstellungen\",\n    \"breadcrumb_map\": \"Karte\",\n    \"intro\": \"Taktile Karten sind eine große Hilfe für blinde und sehbehinderte Menschen, um sich zu orientieren und Routen zu planen. Mit Touch Mapper können Sie ganz einfach individuelle Außenkarten für jede gewünschte Adresse erstellen. Sie können die Karte entweder kostenlos selbst mit einem 3D-Drucker drucken oder einen 3D-Druck bestellen, beginnend ab 29 Euro/USD. Prägegeräte und Quellpapierdrucker werden ebenfalls unterstützt.\",\n    \"it_is_simple\": \"Es ist ganz einfach\",\n    \"simple__enter_address\": \"Geben Sie eine Adresse ein und klicken Sie auf „Suchen“\",\n    \"simple__click_create_tactile_map\": \"Klicken Sie auf „Taktile Karte erstellen“\",\n    \"simple__intro_order_or_print\": \"Bestellen Sie die Karte oder drucken Sie sie selber aus\",\n    \"address\": \"Adresse\",\n    \"street_address\": \"Adresse\",\n    \"search\": \"Suchen\",\n    \"searching\": \"Suche …\",\n    \"nothing_found\": \"Nichts gefunden\",\n    \"map_parameters\": \"Kartenparameter\",\n    \"show_all_addresses\": \"Mehr anzeigen\",\n    \"show_all_addresses_aria\": \"Weitere Adressen anzeigen\",\n    \"map_printing\": \"Karte drucken\",\n    \"map_printing__order\": \"3D-Druck bestellen\",\n    \"map_printing__self\": \"Karte selbst ausdrucken\",\n    \"printing_tech\": \"Drucktechnik\",\n    \"printing_tech__3d\": \"3D-Druck\",\n    \"printing_tech__2d\": \"Präge- oder Quellpapier\",\n    \"map_size\": \"Kartengröße\",\n    \"can_print_any_size\": \"Sie können in jeder Größe drucken, aber die Maßstabsauswahl unten geht von 27,9 cm / 11 Zoll aus.\",\n    \"size_order_17\": \"17 cm / 6,7 Zoll Kantenlänge (passt in fast alle 3D-Drucker)\",\n    \"size_order_20\": \"20 cm / 7,9 Zoll Kantenlänge (passt in die meisten 3D-Drucker)\",\n    \"size_self_8\": \"8 cm / 3,1 Zoll Kantenlänge (gut für Testdrucke)\",\n    \"size_self_17\": \"17 cm / 6,7 Zoll Kantenlänge (passt in fast alle 3D-Drucker)\",\n    \"size_self_20\": \"20 cm / 7,9 Zoll Kantenlänge (passt in die meisten 3D-Drucker)\",\n    \"map_scale\": \"Kartenmaßstab\",\n    \"map_scale__1000\": \"einzelne Gebäude oder Sehenswürdigkeiten\",\n    \"map_scale__1800\": \"dicht bebaute Städte\",\n    \"map_scale__2400\": \"Standard\",\n    \"map_scale__3200\": \"durchschnittlich dicht bebaute Vororte\",\n    \"map_scale__5600\": \"wenig bebaute Gebiete\",\n    \"map_scale__9999\": \"Gebiete mit nur großen Straßen\",\n    \"map_scale_coverage\": \"Bei diesem Maßstab deckt eine Karte mit __size__ Breite ein Gebiet von __area_metric__ / __area_imperial__ ab.\",\n    \"map_content\": \"Inhalt\",\n    \"content_mode_normal\": \"Normal\",\n    \"content_mode_no_buildings\": \"Keine Gebäude\",\n    \"content_mode_only_big_roads\": \"Nur große Straßen, Bahnstrecken und Wasserflächen\",\n    \"target_road_density_ui\": \"Straßenanteil\",\n    \"target_road_density_ui_hint\": \"Höhere Zahl bedeutet mehr Straßen, Standard ist 10\",\n    \"location_marker\": \"Standortmarkierung\",\n    \"hide_location_marker\": \"Standortmarkierung ausblenden\",\n    \"advanced_settings\": \"Erweitert\",\n    \"show_advanced_settings\": \"Erweiterte Optionen anzeigen\",\n    \"coordinates\": \"Koordinaten\",\n    \"longitude_x\": \"Länge (X)\",\n    \"latitude_y\": \"Breite (Y)\",\n    \"area_adjustment\": \"Gebiet anpassen\",\n    \"x_offset\": \"Verschieben, X\",\n    \"positive_is_right\": \"positiver Wert ist rechts\",\n    \"y_offset\": \"Verschieben, Y\",\n    \"positive_is_up\": \"positiver Wert ist oben\",\n    \"custom_map_size\": \"Benutzerdefinierte Kartengröße\",\n    \"unit_cm\": \"cm\",\n    \"custom_scale\": \"Benutzerdefinierter Maßstab\",\n    \"multipart_map\": \"Mehrteilige Karte\",\n    \"multipart_map_label\": \"Teil einer mehrteiligen Karte (blendet Standortmarkierung aus)\",\n    \"adjust_area_for_multipart\": \"Gebiet auf Kartenteil anpassen\",\n    \"move_map_area_left\": \"Gebiet auf Karte um __amount__ Prozent nach links verschieben\",\n    \"move_map_area_up\": \"Gebiet auf Karte um __amount__ Prozent nach oben verschieben\",\n    \"move_map_area_down\": \"Gebiet auf Karte um __amount__ Prozent nach unten verschieben\",\n    \"move_map_area_right\": \"Gebiet auf Karte um __amount__ Prozent nach rechts verschieben\",\n    \"multipart_current_adjustment\": \"Aktuell\",\n    \"multipart_current_adjustment_aria\": \"Aktuelle Anpassung für Gebiet auf Kartenteil\",\n    \"create_tactile_map\": \"Taktile Karte erstellen\",\n    \"progress__connecting\": \"Verbinden...\",\n    \"progress__reading_osm\": \"20% erstellt...\",\n    \"progress__converting\": \"60% erstellt...\",\n    \"progress__uploading\": \"80% hochgeladen...\",\n    \"map_for_address\": \"Karte von\",\n    \"content__heading\": \"Inhalt\",\n    \"content__road\": \"Straßen\",\n    \"content__bus_stop\": \"Bushaltestellen\",\n    \"content__shop\": \"Geschäfte\",\n    \"content__restaurant\": \"Restaurants\",\n    \"content__no_roads\": \"Keine Straßen\",\n    \"content__unnamed_roads\": \"Unbenannte Straßen\",\n    \"warning_no_buildings\": \"Achtung: Auf der Karte befinden sich keine Gebäude. Die Daten von OpenStreetMap für dieses Gebiet könnten unvollständig sein. __startTag__Weitere Informationen.__endTag__\",\n    \"download\": \"STL-Datei zum Ausdrucken herunterladen\",\n    \"other_downloads\": \"Weitere Downloads\",\n    \"show_more_downloads\": \"Mehr Downloads anzeigen\",\n    \"download_map_descriptions\": \"Kartenbeschreibungen\",\n    \"download_stl_ways\": \"STL: Nur Straßen\",\n    \"download_stl_rest\": \"STL: Alles außer Straßen\",\n    \"download_pdf_embossers\": \"PDF\",\n    \"download_svg_embossers\": \"SVG\",\n    \"download_blender\": \"Originale Blender-Datei\",\n    \"order_from_1\": \"Bei __companyName__ bestellen.\",\n    \"order_from_2\": \"29€ für eine 20 cm / 6,9 Zoll Karte, 99€ für 5. Fragen Sie nach anderen Optionen.\",\n    \"open_or_download_svg\": \"SVG-Datei öffnen (für Prägegeräte) oder mit der rechten Maustaste herunterladen\",\n    \"open_or_download_pdf\": \"PDF-Datei öffnen (für Quellpapiergeräte) oder mit der rechten Maustaste herunterladen\",\n    \"emboss_ordering_later\": \"Bestelloption für Präge- oder Quellpapierkarten folgt später.\",\n    \"osm_attribution\": \"Kartendaten © __startTag__OpenStreetMap-Mitwirkende__endTag__. Wenn Sie ein Produkt mit Touch Mapper erstellen, müssen Sie OpenStreetMap als Datenquelle angeben.\",\n    \"email_map_to\": \"Kartenlink per E-Mail senden an\",\n    \"send_submit\": \"Senden\",\n    \"send_email\": \"E-Mail senden\",\n    \"email_sent\": \"E-Mail gesendet\",\n    \"error_sending_email\": \"Fehler beim Senden der E-Mail\",\n    \"conversion_error_unknown\": \"Kartenerstellung fehlgeschlagen.\",\n    \"conversion_error_too_large\": \"Die Karte ist zu dicht, um verarbeitet zu werden.\",\n    \"drag_map\": \"Karte ziehen, um Gebiet anzupassen\",\n    \"map_diameter\": \"Kartendurchmesser\",\n    \"label_3d_preview\": \"3D-Vorschau der taktilen Karte\",\n    \"map_preview_controls_hint\": \"Sie können die Karte zoomen, drehen und verschieben\",\n    \"tooltip_selected_address\": \"Gewählte Adresse\",\n    \"meters\": \"Meter\",\n    \"yards\": \"Yards\",\n    \"data_no_longer_available\": \"Die zwischengespeicherten Dateien dieser Karte sind abgelaufen. Gehen Sie zurück zu den Kartenparametern und klicken Sie auf „Taktile Karte erstellen“, um eine ähnliche Karte zu erstellen.\",\n    \"back_to_index\": \"Zurück zur Adresssuche\",\n    \"back_to_area\": \"Zurück zu den Kartenparametern\",\n    \"help_has_more_info\": \"Mehr Informationen zu Touch Mapper auf der __startTag__Hilfeseite__endTag__\",\n    \"close_tab_to_exit_help\": \"Diesen Tab schließen, um Hilfe zu verlassen\",\n    \"what_is_the_map_like\": \"Wie sieht die Karte aus?\",\n    \"what_is_the_map_like__map_content\": \"Die Karte enthält Folgendes:\",\n    \"what_is_the_map_like__roads\": \"Straßen aller Größen. Straßen, die von Fußgängern genutzt werden können, sind höher, weil sie für blinde Menschen wichtiger sind.\",\n    \"what_is_the_map_like__buildings\": \"Gebäude\",\n    \"map_content_unnamed_roads_one\": \"__count__ unbenannte Straße\",\n    \"map_content_unnamed_roads_many\": \"__count__ unbenannte Straßen\",\n    \"map_content_heading\": \"Karteninhalt\",\n    \"map_content_subheading_ways\": \"Wege\",\n    \"map_content_subheading_roads\": \"Straßen\",\n    \"map_content_subheading_paths\": \"Pfade\",\n    \"map_content_subheading_railways\": \"Bahnstrecken\",\n    \"map_content_subheading_waterways\": \"Wasserwege\",\n    \"map_content_subheading_water_areas\": \"Wasserflächen\",\n    \"map_content_subheading_other_linear\": \"Andere lineare Merkmale\",\n    \"map_content_subheading_buildings\": \"Gebäude\",\n    \"map_content_subheading_poi_familiar\": \"Bekannte Orte\",\n    \"map_content_subheading_poi_daily\": \"Alltägliche Ziele\",\n    \"map_content_subheading_poi_transport\": \"Verkehrspunkte\",\n    \"map_content_subheading_poi_accessibility\": \"Hinweise zur Barrierefreiheit\",\n    \"map_content_way_unnamed\": \"Unbenannter Weg\",\n    \"map_content_way_unnamed_waterway\": \"Unbenannter Wasserweg\",\n    \"map_content_no_ways\": \"Keine Wege für diese Karte aufgelistet.\",\n    \"map_content_no_roads\": \"Keine Straßen für diese Karte aufgelistet.\",\n    \"map_content_no_paths\": \"Keine Pfade für diese Karte aufgelistet.\",\n    \"map_content_no_railways\": \"Keine Bahnstrecken für diese Karte aufgelistet.\",\n    \"map_content_no_waterways\": \"Keine Wasserwege für diese Karte aufgelistet.\",\n    \"map_content_no_water_areas\": \"Keine Wasserflächen für diese Karte aufgelistet.\",\n    \"map_content_no_other_linear\": \"Keine anderen linearen Merkmale für diese Karte aufgelistet.\",\n    \"map_content_unnamed_features_one\": \"__count__ unbenannter __type__\",\n    \"map_content_unnamed_features_many\": \"__count__ unbenannte __type__\",\n    \"map_content_way_route_from_to\": \"Von __start__ bis __end__\",\n    \"map_content_way_route_near\": \"__location__\",\n    \"map_content_connects_to_way\": \"Verbindet mit Weg __way__\",\n    \"map_content_connects_to_ways\": \"Verbindet mit Wegen __ways__\",\n    \"map_content_connects_to_type\": \"Verbindet mit __type__\",\n    \"map_content_connects_to_type_many\": \"Verbindet mit __count__ __type__\",\n    \"map_content_connects_to_type_many_fallback\": \"Verbindet mit __count__ Wegen vom Typ __type__\",\n    \"map_content_way_junction_one\": \"1 Kreuzung\",\n    \"map_content_way_junction_many\": \"__count__ Kreuzungen\",\n    \"map_content_way_length_m\": \"__meters__ Meter\",\n    \"map_content_way_lanes_many\": \"__count__ Fahrspuren\",\n    \"map_content_way_surface_paved\": \"Befestigte Oberfläche\",\n    \"map_content_way_surface_unpaved\": \"Unbefestigte Oberfläche\",\n    \"map_content_way_surface_unknown\": \"Belag unbekannt\",\n    \"map_content_way_meta\": \"__type__, __length__\",\n    \"map_content_way_meta_type_only\": \"__type__\",\n    \"map_content_way_meta_length_only\": \"__length__\",\n    \"map_content_way_type_A1_road_construction\": \"Straße im Bau\",\n    \"map_content_way_type_A1_major_roads\": \"Hauptstraße\",\n    \"map_content_way_type_A1_secondary_roads\": \"Nebenstraße\",\n    \"map_content_way_type_A1_local_streets\": \"Ortsstraße\",\n    \"map_content_way_type_A1_service_roads\": \"Serviceweg\",\n    \"map_content_way_type_plural_A1_service_roads\": \"Servicewege\",\n    \"map_content_way_type_A1_track_roads\": \"Forstweg\",\n    \"map_content_way_type_A1_vehicle_unspecified\": \"Sonstige Fahrstraße\",\n    \"map_content_way_type_A2_pedestrian_streets\": \"Fußgängerstraße\",\n    \"map_content_way_type_A2_footpaths_trails\": \"Fußweg / Pfad\",\n    \"map_content_way_type_A2_cycleways\": \"Radweg\",\n    \"map_content_way_type_A2_steps_ramps\": \"Treppe / Rampe\",\n    \"map_content_way_type_A2_pedestrian_unspecified\": \"Sonstiger Gehweg\",\n    \"map_content_way_type_A3_rail_lines\": \"Bahnstrecke\",\n    \"map_content_way_type_A3_tram_light_rail\": \"Straßenbahn / Stadtbahn\",\n    \"map_content_way_type_A3_subway_metro\": \"U-Bahn\",\n    \"map_content_way_type_A3_rail_yards_sidings\": \"Rangier-/Abstellgleis\",\n    \"map_content_way_type_A4_rivers\": \"Fluss\",\n    \"map_content_way_type_A4_streams_canals\": \"Bach / Kanal\",\n    \"map_content_way_type_A4_ditches_drains\": \"Graben / Entwässerung\",\n    \"map_content_way_type_A4_other_waterways\": \"Sonstiger Wasserlauf\",\n    \"map_content_way_type_plural_A4_other_waterways\": \"Sonstige Wasserläufe\",\n    \"map_content_way_type_A_other_ways\": \"Sonstiger Weg\",\n    \"map_content_water_area_unnamed\": \"Unbenannte __type__\",\n    \"map_content_water_area_type_generic\": \"Wasserfläche\",\n    \"map_content_water_area_type_generic_plural\": \"Wasserflächen\",\n    \"map_content_water_area_type_B1_lakes\": \"See\",\n    \"map_content_water_area_type_B1_ponds\": \"Teich\",\n    \"map_content_water_area_type_B1_reservoirs\": \"Stausee\",\n    \"map_content_water_area_type_B1_sea_coast\": \"Meer\",\n    \"map_content_water_area_type_B1_riverbanks\": \"Flussufer\",\n    \"map_content_water_area_type_B1_other_water\": \"Wasserfläche\",\n    \"map_content_building_unnamed\": \"Unbenanntes Gebäude\",\n    \"map_content_no_buildings\": \"Keine Gebäude für diese Karte aufgelistet.\",\n    \"map_content_no_poi_familiar\": \"Keine bekannten Orte für diese Karte aufgelistet.\",\n    \"map_content_no_poi_daily\": \"Keine alltäglichen Ziele für diese Karte aufgelistet.\",\n    \"map_content_no_poi_transport\": \"Keine Verkehrspunkte für diese Karte aufgelistet.\",\n    \"map_content_no_poi_accessibility\": \"Für diese Karte sind keine Hinweise zur Barrierefreiheit getaggt.\",\n    \"map_content_show_more_buildings_one\": \"1 weiteres Gebäude anzeigen\",\n    \"map_content_show_more_buildings_many\": \"__count__ weitere Gebäude anzeigen\",\n    \"map_content_show_less_buildings\": \"Weniger Gebäude anzeigen\",\n    \"map_content_show_more_features_one\": \"1 weiteres Merkmal anzeigen\",\n    \"map_content_show_more_features_many\": \"__count__ weitere Merkmale anzeigen\",\n    \"map_content_show_less_features\": \"Weniger Merkmale anzeigen\",\n    \"map_content_summary_show_more\": \"Mehr anzeigen\",\n    \"map_content_summary_show_only\": \"Nur Zusammenfassung anzeigen\",\n    \"map_content_height_note_raised_mm\": \"Um __millimeters__ mm erhöht\",\n    \"map_content_height_note_varying\": \"Unterschiedlich stark erhöht\",\n    \"map_content_height_note_waved_surface\": \"Gewellte Oberfläche\",\n    \"map_content_height_note_text_only\": \"Nur textbasierte Informationen\",\n    \"map_content_show_more_pois_one\": \"1 weiteren Ort anzeigen\",\n    \"map_content_show_more_pois_many\": \"__count__ weitere Orte anzeigen\",\n    \"map_content_show_less_pois\": \"Weniger Orte anzeigen\",\n    \"map_content_poi_unknown_type\": \"Unbekannter Ortstyp\",\n    \"map_content_poi_unnamed\": \"Unbenannter __type__\",\n    \"map_content_poi_unnamed_many\": \"__count__ unbenannte __type__\",\n    \"map_content_poi_sparse_note\": \"In diesem Gebiet sind nur wenige POIs getaggt; fehlende Orte können an der OpenStreetMap-Datenabdeckung liegen.\",\n    \"map_content_poi_access_crossings_signals_one\": \"__count__ Ort mit signalisierten Querungen\",\n    \"map_content_poi_access_crossings_signals_many\": \"__count__ Orte mit signalisierten Querungen\",\n    \"map_content_poi_access_tactile_summary\": \"Leitstreifen-Tags: __yes__ ja, __no__ nein\",\n    \"map_content_poi_access_kerb_summary\": \"Bordstein-Tags: abgesenkt __lowered__, niveaugleich __flush__, erhöht __raised__\",\n    \"map_content_poi_access_steps_summary\": \"Treppen an __count__ Orten getaggt (größte Stufenzahl __max__)\",\n    \"map_content_poi_access_incline_summary\": \"Steigungs-Tags: aufwärts __up__, abwärts __down__\",\n    \"map_content_poi_access_wheelchair_summary\": \"Rollstuhl-Tags: ja __yes__, eingeschränkt __limited__, nein __no__\",\n    \"map_content_poi_example\": \"Beispiel: __example__\",\n    \"map_content_building_join_at\": \"in\",\n    \"map_content_building_type_from_value\": \"__value__gebäude\",\n    \"map_content_unavailable\": \"Karteninhalt ist nicht verfügbar.\",\n    \"map_content_list_and\": \"und\",\n    \"map_content_edge_phrase\": \"den __edge__rand\",\n    \"map_content_touches_edge\": \"Berührt den __edge__rand\",\n    \"map_content_touches_edges\": \"Berührt die Ränder: __edges__\",\n    \"map_content_touches_items\": \"Berührt __items__\",\n    \"map_content_crosses_edge\": \"Kreuzt den __edge__rand\",\n    \"map_content_crosses_edges\": \"Kreuzt die Ränder: __edges__\",\n    \"map_content_crosses_items\": \"Kreuzt __items__\",\n    \"map_content_touches_edge_percent\": \"berührt __percent__ % des __edge__rands\",\n    \"map_content_touch_pos_north\": \"im Norden\",\n    \"map_content_touch_pos_south\": \"im Süden\",\n    \"map_content_touch_pos_west\": \"im Westen\",\n    \"map_content_touch_pos_east\": \"im Osten\",\n    \"map_content_touch_pos_center\": \"nahe der Mitte\",\n    \"map_content_touch_pos_multiple\": \"an mehreren Stellen\",\n    \"map_content_edge_north\": \"Nord\",\n    \"map_content_edge_south\": \"Süd\",\n    \"map_content_edge_east\": \"Ost\",\n    \"map_content_edge_west\": \"West\",\n    \"map_content_dir_north\": \"Nord\",\n    \"map_content_dir_northeast\": \"Nordost\",\n    \"map_content_dir_east\": \"Ost\",\n    \"map_content_dir_southeast\": \"Südost\",\n    \"map_content_dir_south\": \"Süd\",\n    \"map_content_dir_southwest\": \"Südwest\",\n    \"map_content_dir_west\": \"West\",\n    \"map_content_dir_northwest\": \"Nordwest\",\n    \"map_content_corner_northwest\": \"Nordwestecke\",\n    \"map_content_corner_northeast\": \"Nordostecke\",\n    \"map_content_corner_southwest\": \"Südwestecke\",\n    \"map_content_corner_southeast\": \"Südostecke\",\n    \"map_content_loc_center\": \"Mitte\",\n    \"map_content_loc_offset_center\": \"__dir__ der Mitte\",\n    \"map_content_loc_part\": \"__dir__teil\",\n    \"map_content_loc_edge\": \"__dir__rand\",\n    \"map_content_loc_corner\": \"__corner__\",\n    \"map_content_loc_near_edge\": \"nahe am __dir__rand\",\n    \"map_content_loc_near_corner\": \"__corner__\",\n    \"map_content_loc_full_center\": \"in der Mitte\",\n    \"map_content_loc_full_offset_center\": \"etwas __dir__ der Mitte\",\n    \"map_content_loc_full_part\": \"im __dir__teil\",\n    \"map_content_loc_full_edge\": \"nahe am __dir__rand\",\n    \"map_content_loc_full_corner\": \"in der __corner__\",\n    \"map_content_loc_full_near_edge\": \"nahe am __dir__rand\",\n    \"map_content_loc_full_near_corner\": \"in der __corner__\",\n    \"map_content_loc_endpoint_center\": \"der Mitte\",\n    \"map_content_loc_endpoint_part\": \"dem __dir__teil\",\n    \"map_content_loc_endpoint_near_edge\": \"nahe am __dir__rand\",\n    \"map_content_loc_endpoint_corner\": \"der __corner__\",\n    \"map_content_loc_route_from_center\": \"der Mitte\",\n    \"map_content_loc_route_from_part\": \"dem __dir__teil\",\n    \"map_content_loc_route_from_near_edge\": \"nahe am __dir__rand\",\n    \"map_content_loc_route_from_corner\": \"der __corner__\",\n    \"map_content_loc_route_to_center\": \"zur Mitte\",\n    \"map_content_loc_route_to_part\": \"zum __dir__teil\",\n    \"map_content_loc_route_to_near_edge\": \"nahe am __dir__rand\",\n    \"map_content_loc_route_to_corner\": \"zur __corner__\",\n    \"map_content_region_center\": \"Mitte\",\n    \"map_content_region_part\": \"__dir__teil\",\n    \"map_content_region_edge\": \"__dir__rand\",\n    \"map_content_region_corner\": \"__corner__\",\n    \"map_content_summary_in\": \"Im Bereich __region__.\",\n    \"map_content_summary_near\": \"Nahe bei __region__.\",\n    \"map_content_summary_mostly_in_lead\": \"Meist im Bereich __region__\",\n    \"map_content_summary_mainly_near_lead\": \"Meist nahe bei __region__\",\n    \"map_content_summary_mostly_extending\": \"__lead__, mit Ausdehnung Richtung __target__.\",\n    \"map_content_summary_in_and\": \"Im __first__ und __second__.\",\n    \"map_content_summary_in_and_near\": \"Im __first__ und nahe bei __second__.\",\n    \"map_content_summary_near_and\": \"Nahe bei __first__ und __second__.\",\n    \"map_content_summary_mainly_in_and\": \"In mehreren Bereichen, meist im __first__ und __second__.\",\n    \"map_content_summary_mainly_in_and_near\": \"In mehreren Bereichen, meist im __first__ und nahe bei __second__.\",\n    \"map_content_summary_mainly_near_and\": \"In mehreren Bereichen, meist nahe bei __first__ und __second__.\",\n    \"map_content_summary_mainly_near_and_in\": \"In mehreren Bereichen, meist nahe bei __first__ und im __second__.\",\n    \"map_content_summary_several_mainly_in\": \"In mehreren Bereichen, meist im __region__.\",\n    \"map_content_summary_several_mainly_near\": \"In mehreren Bereichen, meist nahe bei __region__.\",\n    \"map_content_summary_equal_clauses\": \"__first__ und __second__.\",\n    \"map_content_summary_mostly_extending_clauses\": \"Meist __lead__, mit Ausdehnung Richtung __target__.\",\n    \"map_content_summary_distributed_top2\": \"In mehreren Bereichen, meist __first__ und __second__.\",\n    \"map_content_summary_distributed_top1\": \"In mehreren Bereichen, meist __first__.\",\n    \"map_content_summary_three_regions_clauses\": \"In mehreren Bereichen, meist __first__, __second__ und __third__.\",\n    \"map_content_parts_one\": \"__count__ Teil\",\n    \"map_content_parts_many\": \"__count__ Teile\",\n    \"map_content_total_area\": \"Bedeckt __percent__ % der Karte\",\n    \"map_content_shape_elongated\": \"Länglich\",\n    \"map_content_shape_long_thin\": \"Sehr schmal\",\n    \"map_content_shape_irregular\": \"Unregelmäßige Form\",\n    \"map_content_shape_generic\": \"__shape__-Form\",\n    \"map_content_aspect_label\": \"Seitenverhältnis __ratio__\",\n    \"map_content_degrees_abbrev\": \"°\",\n    \"map_content_orientation\": \"Ausrichtung __label__\",\n    \"map_content_orientation_label_east_west\": \"Ost-West\",\n    \"map_content_orientation_label_north_south\": \"Nord-Süd\",\n    \"map_content_orientation_label_northeast_southwest\": \"Nordost bis Südwest\",\n    \"map_content_orientation_label_east_northeast_to_west_southwest\": \"Ost-Nordost bis West-Südwest\",\n    \"map_content_orientation_label_north_northeast_to_south_southwest\": \"Nord-Nordost bis Süd-Südwest\",\n    \"map_content_coverage_label\": \"Bedeckt:\",\n    \"map_content_coverage_much\": \"viel von __segment__\",\n    \"map_content_coverage_some\": \"etwas von __segment__\",\n    \"map_content_coverage_little\": \"wenig von __segment__\",\n    \"map_content_osm_value_kindergarten\": \"kindergarten\",\n    \"map_content_osm_value_school\": \"schule\",\n    \"map_content_osm_value_hospital\": \"krankenhaus\",\n    \"map_content_osm_value_church\": \"kirche\",\n    \"map_content_osm_value_university\": \"universität\",\n    \"map_content_osm_value_college\": \"Hochschule\",\n    \"map_content_osm_value_library\": \"bibliothek\",\n    \"map_content_osm_value_gallery\": \"Galerie\",\n    \"map_content_osm_value_museum\": \"museum\",\n    \"map_content_osm_value_theatre\": \"Theater\",\n    \"map_content_osm_value_supermarket\": \"supermarkt\",\n    \"map_content_osm_value_cafe\": \"café\",\n    \"map_content_osm_value_restaurant\": \"restaurant\",\n    \"map_content_osm_value_hotel\": \"hotel\",\n    \"map_content_osm_value_parking\": \"parkplatz\",\n    \"map_content_osm_value_bicycle_parking\": \"Fahrradstellplatz\",\n    \"map_content_osm_value_residential\": \"wohn\",\n    \"map_content_osm_value_commercial\": \"gewerbe\",\n    \"map_content_osm_value_industrial\": \"industrie\",\n    \"map_content_osm_value_public\": \"Öffentlich\",\n    \"map_content_osm_value_roof\": \"überdachung\",\n    \"map_content_osm_value_transportation\": \"Verkehrs\",\n    \"map_content_osm_value_service\": \"dienstleistungs\",\n    \"map_content_osm_value_civic\": \"Kommunal\",\n    \"map_content_osm_value_office\": \"büro\",\n    \"map_content_osm_value_apartments\": \"mehrfamilienhaus\",\n    \"map_content_osm_value_house\": \"haus\",\n    \"map_content_osm_value_detached\": \"einfamilienhaus\",\n    \"map_content_osm_value_terrace\": \"reihenhauszeile\",\n    \"map_content_osm_value_semidetached_house\": \"Doppelhaus\",\n    \"map_content_osm_value_farm\": \"Bauernhof\",\n    \"map_content_osm_value_warehouse\": \"lagerhalle\",\n    \"map_content_osm_value_garages\": \"Garagen\",\n    \"map_content_osm_value_garage\": \"garage\",\n    \"map_content_osm_value_retail\": \"einzelhandel\",\n    \"map_content_osm_value_kiosk\": \"Kiosk\",\n    \"map_content_osm_value_playground\": \"Spielplatz\",\n    \"map_content_osm_value_park\": \"Park\",\n    \"map_content_osm_value_zoo\": \"Zoo\",\n    \"map_content_osm_value_sports_centre\": \"Sportzentrum\",\n    \"map_content_osm_value_station\": \"Bahnhof\",\n    \"map_content_osm_value_rail\": \"Bahnstrecke\",\n    \"map_content_osm_value_platform\": \"Bahnsteig\",\n    \"map_content_osm_value_stop_position\": \"Halteposition\",\n    \"map_content_osm_value_stop_area\": \"Haltestellenbereich\",\n    \"map_content_osm_value_stop_area_group\": \"Haltestellenverbund\",\n    \"map_content_osm_value_halt\": \"Haltepunkt\",\n    \"map_content_osm_value_bus_stop\": \"Bushaltestelle\",\n    \"map_content_osm_value_bus_station\": \"Busbahnhof\",\n    \"map_content_osm_value_tram_stop\": \"Straßenbahnhaltestelle\",\n    \"map_content_osm_value_tram\": \"Straßenbahn\",\n    \"map_content_osm_value_subway_entrance\": \"U-Bahneingang\",\n    \"map_content_osm_value_ferry_terminal\": \"Fährterminal\",\n    \"map_content_osm_value_taxi\": \"Taxi\",\n    \"map_content_osm_value_department_store\": \"Kaufhaus\",\n    \"map_content_osm_value_mall\": \"Einkaufszentrum\",\n    \"map_content_osm_value_attraction\": \"Sehenswürdigkeit\",\n    \"map_content_osm_value_artwork\": \"Kunstwerk\",\n    \"map_content_osm_value_fountain\": \"Brunnen\",\n    \"map_content_osm_value_viewpoint\": \"aussichtspunkt\",\n    \"map_content_osm_value_theme_park\": \"Freizeitpark\",\n    \"map_content_osm_value_aquarium\": \"Aquarium\",\n    \"map_content_osm_value_arts_centre\": \"Kunstzentrum\",\n    \"map_content_osm_value_community_centre\": \"Gemeindezentrum\",\n    \"map_content_osm_value_place_of_worship\": \"Gotteshaus\",\n    \"map_content_osm_value_townhall\": \"Rathaus\",\n    \"map_content_osm_value_courthouse\": \"Gerichtsgebäude\",\n    \"map_content_osm_value_pharmacy\": \"apotheke\",\n    \"map_content_osm_value_clinic\": \"klinik\",\n    \"map_content_osm_value_doctors\": \"arztpraxis\",\n    \"map_content_osm_value_toilets\": \"toiletten\",\n    \"map_content_osm_value_post_office\": \"post\",\n    \"map_content_osm_value_bank\": \"bank\",\n    \"map_content_osm_value_atm\": \"geldautomat\",\n    \"map_content_osm_value_fuel\": \"tankstelle\",\n    \"map_content_osm_value_convenience\": \"kiosk/Minimarkt\",\n    \"map_content_osm_value_fast_food\": \"imbiss\",\n    \"map_content_osm_value_bar\": \"bar\",\n    \"map_content_osm_value_pub\": \"pub\",\n    \"map_content_osm_value_marketplace\": \"Marktplatz\",\n    \"map_content_osm_value_bench\": \"Sitzbank\",\n    \"map_content_osm_value_waste_basket\": \"Abfalleimer\",\n    \"map_content_osm_value_vending_machine\": \"Verkaufsautomat\",\n    \"map_content_osm_value_street_lamp\": \"Straßenlampe\",\n    \"map_content_osm_value_outdoor_seating\": \"Außensitzplätze\",\n    \"map_content_osm_value_sauna\": \"Sauna\",\n    \"map_content_osm_value_shelter\": \"Unterstand\",\n    \"map_content_osm_value_abandoned\": \"Aufgegeben\",\n    \"map_content_osm_value_razed\": \"Abgerissen\",\n    \"map_content_osm_value_bicycle\": \"Fahrrad\",\n    \"map_content_osm_value_building\": \"gebäude\",\n    \"map_content_osm_value_residential_building\": \"wohngebäude\",\n    \"map_content_osm_value_commercial_building\": \"gewerbegebäude\",\n    \"map_content_osm_value_industrial_building\": \"industriegebäude\",\n    \"map_content_osm_value_public_building\": \"Öffentliches Gebäude\",\n    \"map_content_osm_value_garages_building\": \"Garagengebäude\",\n    \"map_content_osm_value_yes\": \"Allgemein\",\n    \"what_is_the_map_like__railways\": \"Eisenbahnschienen\",\n    \"what_is_the_map_like__water\": \"Gewässer (mit welliger Oberfläche, aber kleinere Flüsse als Linien)\",\n    \"what_is_the_map_like__north\": \"Markierung auf der nordöstlichen Ecke der Karte, um die korrekte Ausrichtung anzuzeigen\",\n    \"what_is_the_map_like__selected_addr\": \"Gewählte Adresse (oder Koordinaten) mit Kegel markiert\",\n    \"what_is_the_map_like__clarity\": \"Die taktilen Karten von Touch Mapper sind für den praktischen Gebrauch sehbehinderter Menschen optimiert. Sie bilden das Gebiet nicht naturgetreu ab.\",\n    \"larger_scale_maps\": \"Karten für größere Bereiche und Innenräume\",\n    \"larger_scale_maps_answer\": \"Touch Mapper unterstützt derzeit nur Karten, die jede Straße und jedes Gebäude im Gebiet zeigen. Karten, die größere Bereiche abdecken, Höhenunterschiede zeigen oder Innenbereiche darstellen, könnten später unterstützt werden. Beachten Sie, dass Sie durch Auswahl von „Teil einer mehrteiligen Karte drucken“ einen größeren Bereich abdecken können, indem Sie mehrere Karten drucken. Es gibt massenproduzierte Karten im Ländermaßstab, z. B. im __aphLinkStart__American Printing House for the Blind Shop__aphLinkEnd__.\",\n    \"data_comes_from_osm\": \"Woher kommen die Kartendaten?\",\n    \"data_comes\": \"Touch Mapper verwendet Daten von __startTag__OpenStreetMap__endTag__. Obwohl dies in der Regel eine ausgezeichnete Quelle ist, kann die Genauigkeit der Karten nicht garantiert werden. Das häufigste Problem sind fehlende Gebäude. Wenn wichtige Merkmale fehlen, können Sie sie frei zu OpenStreetMap hinzufügen. Kommerzielle Kartendatenquellen wie Google Maps können leider nicht genutzt werden, da sie den erforderlichen Zugriff auf ihre Daten nicht erlauben.\",\n    \"commercial_use\": \"Kommerzielle Nutzung\",\n    \"commercial_use_answer\": \"Sie dürfen Touch Mapper für kommerzielle Zwecke verwenden, z. B. zum Drucken und Verkaufen der Karten. Ich bitte nur darum, dass Sie Ihren Kunden deutlich machen, dass die Karten von www.touch-mapper.org stammen, sowohl in der Werbung als auch in der Verpackung. Ich würde mich außerdem freuen, wenn Sie mich informieren, falls Sie einen Dienst auf Basis von Touch Mapper anbieten.\",\n    \"users_and_purposes\": \"Wozu taktile Karten drucken?\",\n    \"tactile_maps_have_many_different_user_groups\": \"Taktile Karten können vielen verschiedenen Nutzergruppen zugutekommen.\",\n    \"visually_impaired_individuals\": \"Blinde und sehbehinderte Menschen: bessere Orientierung und besseres Planen von Wegen\",\n    \"outdoors_tourist_attractions\": \"Touristenattraktionen im Freien: besseres Eingehen auf blinde und sehbehinderte Gäste\",\n    \"public_areas_such_as_parks\": \"Öffentliche Orte wie Parks und Spielplätze: Taktile Karten können blinden und sehbehinderten Besuchern helfen, den Park selbstständiger zu nutzen\",\n    \"orientation_and_mobility_specialists\": \"Rehabilitationslehrer für Orientierung und Mobilität: um es Schulungsteilnehmern zu erleichtern, sich ein geistiges Bild einer Umgebung zu machen\",\n    \"multipart_maps_heading\": \"Karten in mehreren Teilen drucken\",\n    \"multipart_maps\": \"Wenn eine einzelne Karte nicht groß genug ist, um das gewünschte Gebiet abzudecken, z. B. eine lange Straße, können Sie mehrere Drucke erstellen und sie nebeneinander auf eine flache Oberfläche legen. Wählen Sie „Erweiterte Optionen anzeigen“ und danach „Teil einer mehrteiligen Karte“. In diesem Modus gibt es keine Ränder um die Kanten des 3D-Drucks. Außerdem erscheinen Pfeiltasten, mit denen sich der ausgewählte Bereich in jede Richtung um 100 % (d. h. den Kartendurchmesser) verschieben lässt, was die Auswahl angrenzender Bereiche erleichtert.\",\n    \"how_to_3d_print\": \"3D-Druck\",\n    \"map_can_be_printed\": \"Die Karte kann mit fast jedem 3D-Drucker gedruckt werden, einschließlich günstiger Hobbygeräte. Am wichtigsten sind die Straßen: Einige Dienste halten sie möglicherweise für zu schmal zum Drucken.\",\n    \"if_you_print_yourself\": \"Wenn Sie mit Ihrem eigenen 3D-Drucker drucken, verwenden Sie eine Schichthöhe von 0,25 mm oder 0,3 mm. Die Basis ist 0,6 mm dick, und es reicht, sie in zwei Schichten zu drucken. Zwei obere Schichten und eine horizontale Außenhülle sind ausreichend. Sie sollten die oberste Schicht ziemlich langsam drucken, um glatte Oberflächen zu erhalten. Wenn Sie den Simplify3D-Slicer verwenden, müssen Sie „Separate Connected Surfaces“ aktivieren, um einen korrekten Druck zu erhalten. Die Druckzeit für eine 17-cm-Karte mit der Basis in zwei Schichten beträgt etwa 3–4 Stunden.\",\n    \"about_touch_mapper\": \"Über Touch Mapper\",\n    \"about_touch_mapper_answer\": \"Mein Name ist Sofia Pahaoja. Ich habe Touch Mapper entwickelt, nachdem ich von den Herausforderungen blinder und sehbehinderter Menschen beim selbstständigen Reisen erfahren hatte und erkannt hatte, wie 3D-Druck in Kombination mit frei verfügbaren Kartendaten helfen kann. <!-- Ich habe den 3D-Druckanbieter __startTag__„Playful Pixels“__endTag__ in die Seite von Touch Mapper integriert, damit die Karten so einfach und preisgünstig wie möglich bestellt werden können. -->\",\n    \"news_from\": \"Informationen über neue Funktionen werden auf __linkFB__ und __linkTwitter__ veröffentlicht.\",\n    \"want_to_improve_please\": \"Ich möchte Touch Mapper so weit wie möglich verbessern. Nutzer-Feedback spielt dabei eine maßgebliche Rolle. Wenn Sie Fragen oder Verbesserungsvorschläge haben, können Sie mir gern eine\",\n    \"email_me\": \"E-Mail schicken\",\n    \"address_search_by\": \"Adresssuche von __linkTag__.\",\n    \"map_data_by\": \"Kartendaten von __linkTag__.\",\n    \"touch_mapper_is_os\": \"Touch Mapper ist Open-Source-Software.\",\n    \"see_github\": \"Nutzen Sie __startTag__GitHub__endTag__ für Fehlerberichte und Vorschläge für neue Funktionen.\",\n    \"map_content_osm_value_dormitory\": \"wohnheim\",\n    \"map_content_osm_value_motorway\": \"autobahn\",\n    \"map_content_osm_value_trunk\": \"schnellstraße\",\n    \"map_content_osm_value_primary\": \"hauptstraße\",\n    \"map_content_osm_value_secondary\": \"landesstraße\",\n    \"map_content_osm_value_tertiary\": \"nebenstraße\",\n    \"map_content_osm_value_unclassified\": \"unklassifizierte Straße\",\n    \"map_content_osm_value_living_street\": \"spielstraße\",\n    \"map_content_osm_value_pedestrian\": \"fußgängerzone\",\n    \"map_content_osm_value_footway\": \"fußweg\",\n    \"map_content_osm_value_path\": \"pfad\",\n    \"map_content_osm_value_cycleway\": \"radweg\",\n    \"map_content_osm_value_steps\": \"treppe\",\n    \"map_content_osm_value_track\": \"feldweg\",\n    \"map_content_osm_value_road\": \"straße\",\n    \"map_content_osm_value_dentist\": \"zahnarzt\",\n    \"map_content_osm_value_police\": \"polizei\",\n    \"map_content_osm_value_fire_station\": \"feuerwache\",\n    \"map_content_osm_value_charging_station\": \"ladestation\",\n    \"map_content_osm_value_hostel\": \"hostel\",\n    \"map_content_osm_value_information\": \"touristeninformation\",\n    \"map_content_osm_value_bakery\": \"bäckerei\",\n    \"map_content_osm_value_clothes\": \"bekleidungsgeschäft\",\n    \"map_content_osm_value_apartments_building\": \"mehrfamilienhaus\",\n    \"map_content_osm_value_house_building\": \"haus\",\n    \"map_content_osm_value_detached_building\": \"einfamilienhaus\",\n    \"map_content_osm_value_terrace_building\": \"reihenhauszeile\",\n    \"map_content_osm_value_dormitory_building\": \"wohnheim\",\n    \"map_content_osm_value_retail_building\": \"einzelhandelsgebäude\",\n    \"map_content_osm_value_office_building\": \"bürogebäude\",\n    \"map_content_osm_value_warehouse_building\": \"lagerhalle\",\n    \"map_content_osm_value_school_building\": \"schulgebäude\",\n    \"map_content_osm_value_university_building\": \"universitätsgebäude\",\n    \"map_content_osm_value_hospital_building\": \"krankenhaus\",\n    \"map_content_osm_value_church_building\": \"kirche\",\n    \"map_content_osm_value_garage_building\": \"garage\",\n    \"map_content_osm_value_roof_building\": \"überdachung\"\n}\n"
  },
  {
    "path": "web/locales/en/tm.json",
    "content": "{\n    \"location2_top_left\": \"top left quarter\",\n    \"location2_top_row\": \"top half\",\n    \"location2_top_right\": \"top right quarter\",\n    \"location2_right_column\": \"right half\",\n    \"location2_bottom_right\": \"bottom right quarter\",\n    \"location2_bottom_row\": \"bottom half\",\n    \"location2_bottom_left\": \"bottom left quarter\",\n    \"location2_left_column\": \"left half\",\n    \"location3_middle_center\": \"center\",\n    \"location3_top_center\": \"top center\",\n    \"location3_bottom_center\": \"bottom center\",\n    \"location3_middle_left\": \"left center\",\n    \"location3_middle_right\": \"right center\",\n    \"location3_top_left\": \"top left corner\",\n    \"location3_top_row\": \"top edge\",\n    \"location3_mostly_top_row\": \"mostly top edge\",\n    \"location3_top_left_to_mc\": \"top left corner to center\",\n    \"location3_top_left_and_center\": \"top edge left side\",\n    \"location3_top_right_and_center\": \"top edge right side\",\n    \"location3_top_right\": \"top right corner\",\n    \"location3_right_column\": \"right edge\",\n    \"location3_mostly_right_column\": \"mostly right edge\",\n    \"location3_top_right_to_mc\": \"center to top right corner\",\n    \"location3_top_right_and_middle\": \"right edge top side\",\n    \"location3_bottom_right_and_middle\": \"center to bottom right corner\",\n    \"location3_bottom_right\": \"bottom right corner\",\n    \"location3_bottom_row\": \"bottom edge\",\n    \"location3_mostly_bottom_row\": \"mostly bottom edge\",\n    \"location3_bottom_right_to_mc\": \"center to bottom right corner\",\n    \"location3_bottom_right_and_center\": \"bottom edge right side\",\n    \"location3_bottom_left_and_center\": \"bottom edge left side\",\n    \"location3_bottom_left\": \"bottom left corner\",\n    \"location3_left_column\": \"left edge\",\n    \"location3_mostly_left_column\": \"mostly left edge\",\n    \"location3_bottom_left_to_mc\": \"center to bottom left corner\",\n    \"location3_bottom_left_and_middle\": \"left edge bottom side\",\n    \"location3_top_left_and_middle\": \"left edge top side\",\n    \"location3_middle_row\": \"left to right in the middle\",\n    \"location3_center_column\": \"top to bottom in the middle\",\n    \"location3_top_left_diagonal\": \"top left to bottom right corner\",\n    \"location3_top_right_diagonal\": \"bottom left to top right corner\",\n    \"location3_top_left_to_middle_right\": \"top left corner to middle right\",\n    \"location3_top_right_to_middle_left\": \"left center to top right corner\",\n    \"location3_top_right_to_bottom_center\": \"bottom center to top right corner\",\n    \"location3_bottom_right_to_top_center\": \"top center to bottom right corner\",\n    \"location3_bottom_right_to_middle_left\": \"left center to bottom right corner\",\n    \"location3_bottom_left_to_middle_right\": \"bottom left corner to right center\",\n    \"location3_bottom_left_to_top_center\": \"bottom left corner to top center\",\n    \"location3_top_left_to_bottom_center\": \"top left corner to bottom center\",\n    \"location3_near_top_left\": \"near top left corner\",\n    \"location3_top_center_to_middle_left\": \"left center to top center\",\n    \"location3_near_top_right\": \"near top right corner\",\n    \"location3_top_center_to_middle_right\": \"top center to right center\",\n    \"location3_near_bottom_right\": \"near bottom right corner\",\n    \"location3_middle_right_to_bottom_center\": \"bottom center to right center\",\n    \"location3_near_bottom_left\": \"near bottom left corner\",\n    \"location3_middle_left_to_bottom_center\": \"left center to bottom center\",\n    \"location_general\": \"many areas\",\n    \"skip_to_main_content\": \"Skip to main content\",\n    \"meta_description\": \"Tactile maps easily for any address. 3D-print them yourself or order.\",\n    \"meta_keywords\": \"tactile map,blind,visual impairment,blind aid,visual impairment aid,blind technology,visual impairment technology,3d printing\",\n    \"meta_og_site_name\": \"Touch Mapper - Tactile Maps for the Visually Impaired\",\n    \"title\": \"Tactile Maps Easily\",\n    \"blurb\": \"Create tactile maps easily for any address\",\n    \"language\": \"Language\",\n    \"help\": \"Help\",\n    \"open_help_aria\": \"Open help in a new tab\",\n    \"map_content_external_link_aria\": \"External link, opens in a new tab\",\n    \"breadcrumb_nav_title\": \"Breadcrumb\",\n    \"breadcrumb_home\": \"Address search\",\n    \"breadcrumb_area\": \"Settings\",\n    \"breadcrumb_map\": \"Map\",\n    \"intro\": \"Tactile maps are a great aid for people who are blind or partially sighted, helping them to orient themselves and to plan routes. Using Touch Mapper, you can easily create custom outdoor maps for any address of your choice. You can either print the map yourself at no charge using a 3D printer, or you can order a 3D print, starting from 29 euro/USD. Embossers and swell paper printers are also supported.\",\n    \"it_is_simple\": \"It's simple\",\n    \"simple__enter_address\": \"Enter an address and click \\\"Search\\\"\",\n    \"simple__click_create_tactile_map\": \"Click \\\"Create tactile map\\\"\",\n    \"simple__intro_order_or_print\": \"Print or order the map\",\n    \"address\": \"Address\",\n    \"street_address\": \"Street address\",\n    \"search\": \"Search\",\n    \"searching\": \"Searching...\",\n    \"nothing_found\": \"Nothing found\",\n    \"map_parameters\": \"Map parameters\",\n    \"show_all_addresses\": \"Show more\",\n    \"show_all_addresses_aria\": \"Show more found addresses\",\n    \"map_printing\": \"Map printing\",\n    \"map_printing__order\": \"Order a 3D print\",\n    \"map_printing__self\": \"Print the map yourself\",\n    \"printing_tech\": \"Printing technology\",\n    \"printing_tech__3d\": \"3D printing\",\n    \"printing_tech__2d\": \"Embossing or swell paper\",\n    \"map_size\": \"Print size\",\n    \"can_print_any_size\": \"You can print in any size, but scale selection below assumes 27.9 cm / 11 inches.\",\n    \"size_order_17\": \"17 cm / 6.7 inches across (fits almost all 3D printers)\",\n    \"size_order_20\": \"20 cm / 7.9 inches across (fits most 3D printers)\",\n    \"size_self_8\": \"8 cm / 3.1 inches across (good for test prints)\",\n    \"size_self_17\": \"17 cm / 6.7 inches across (fits almost all 3D printers)\",\n    \"size_self_20\": \"20 cm / 7.9 inches across (fits most 3D printers)\",\n    \"map_scale\": \"Map scale\",\n    \"map_scale__1000\": \"single buildings or points of interest\",\n    \"map_scale__1800\": \"dense cities\",\n    \"map_scale__2400\": \"default\",\n    \"map_scale__3200\": \"average suburbs\",\n    \"map_scale__5600\": \"very sparsely built areas\",\n    \"map_scale__9999\": \"areas with only large roads\",\n    \"map_scale_coverage\": \"At this scale a map __size__ across covers an area __area_metric__ / __area_imperial__ across.\",\n    \"map_content\": \"Content\",\n    \"content_mode_normal\": \"Normal\",\n    \"content_mode_no_buildings\": \"No buildings\",\n    \"content_mode_only_big_roads\": \"Big roads, railways and water areas only\",\n    \"target_road_density_ui\": \"Road retention\",\n    \"target_road_density_ui_hint\": \"Higher number means more roads, default is 10\",\n    \"location_marker\": \"Location marker\",\n    \"hide_location_marker\": \"Hide location marker\",\n    \"advanced_settings\": \"Advanced\",\n    \"show_advanced_settings\": \"Show advanced options\",\n    \"coordinates\": \"Coordinates\",\n    \"longitude_x\": \"Longitude (X)\",\n    \"latitude_y\": \"Latitude (Y)\",\n    \"area_adjustment\": \"Location adjustment\",\n    \"x_offset\": \"X offset\",\n    \"positive_is_right\": \"positive is right\",\n    \"y_offset\": \"Y offset\",\n    \"positive_is_up\": \"positive is up\",\n    \"custom_map_size\": \"Custom print size\",\n    \"unit_cm\": \"cm\",\n    \"custom_scale\": \"Custom scale\",\n    \"multipart_map\": \"Multipart map\",\n    \"multipart_map_label\": \"Print a part of a multipart map (hides location marker)\",\n    \"adjust_area_for_multipart\": \"Adjust map part location\",\n    \"move_map_area_left\": \"Move map area left by __amount__ percent\",\n    \"move_map_area_up\": \"Move map area up by __amount__ percent\",\n    \"move_map_area_down\": \"Move map area down by __amount__ percent\",\n    \"move_map_area_right\": \"Move map area right by __amount__ percent\",\n    \"multipart_current_adjustment\": \"Current\",\n    \"multipart_current_adjustment_aria\": \"Current adjustment of map part location\",\n    \"create_tactile_map\": \"Create tactile map\",\n    \"progress__connecting\": \"Connecting...\",\n    \"progress__reading_osm\": \"20% created...\",\n    \"progress__converting\": \"60% created...\",\n    \"progress__uploading\": \"80% uploaded...\",\n    \"map_for_address\": \"Map for\",\n    \"content__heading\": \"Content\",\n    \"content__road\": \"Roads\",\n    \"content__bus_stop\": \"Bus stops\",\n    \"content__shop\": \"Shops\",\n    \"content__restaurant\": \"Restaurants\",\n    \"content__no_roads\": \"No roads\",\n    \"content__unnamed_roads\": \"Unnamed roads\",\n    \"warning_no_buildings\": \"Warning: penStreetMap might have incomplete data for this area. See help for more information.\",\n    \"download\": \"Download printable STL file\",\n    \"other_downloads\": \"Other downloads\",\n    \"show_more_downloads\": \"Show more downloads\",\n    \"download_map_descriptions\": \"Map descriptions\",\n    \"download_stl_ways\": \"STL: Only roadways\",\n    \"download_stl_rest\": \"STL: All but roadways\",\n    \"download_pdf_embossers\": \"PDF\",\n    \"download_svg_embossers\": \"SVG\",\n    \"download_blender\": \"Original Blender file\",\n    \"order_from_1\": \"Order from __companyName__.\",\n    \"order_from_2\": \"29€ for a 20 cm / 6.9 inch map, 99€ for 5. Ask about other options.\",\n    \"open_or_download_svg\": \"Open SVG file (for embossers), or click right mouse button to download\",\n    \"open_or_download_pdf\": \"Open PDF file (for swell form machines), or click right mouse button to download\",\n    \"emboss_ordering_later\": \"Ordering option for embossed / swell paper maps coming later.\",\n    \"osm_attribution\": \"Map data © __startTag__OpenStreetMap contributors__endTag__. If you create a product using Touch Mapper, you must attribute OpenStreetMap as the data source.\",\n    \"email_map_to\": \"Email a map link to\",\n    \"send_submit\": \"Send\",\n    \"send_email\": \"Send email\",\n    \"email_sent\": \"Email sent\",\n    \"error_sending_email\": \"Error sending email\",\n    \"conversion_error_unknown\": \"Map conversion failed.\",\n    \"conversion_error_too_large\": \"Map is too dense to process.\",\n    \"drag_map\": \"Drag the map to adjust location\",\n    \"map_diameter\": \"Map diameter\",\n    \"label_3d_preview\": \"3D preview of the tactile map\",\n    \"map_preview_controls_hint\": \"You can zoom, rotate and pan the map\",\n    \"tooltip_selected_address\": \"Selected address\",\n    \"meters\": \"meters\",\n    \"yards\": \"yards\",\n    \"data_no_longer_available\": \"This map's cached files have expired. Go back to map parameters and click \\\"Create tactile map\\\" to create a similar map.\",\n    \"back_to_index\": \"Back to address search\",\n    \"back_to_area\": \"Back to map parameters\",\n    \"help_has_more_info\": \"More information about Touch Mapper on the __startTag__help page__endTag__\",\n    \"close_tab_to_exit_help\": \"Close this tab to exit help\",\n    \"what_is_the_map_like\": \"Map content\",\n    \"what_is_the_map_like__map_content\": \"Maps contain the following:\",\n    \"what_is_the_map_like__roads\": \"Roads of all size. Pedestrian roads are raised more than others, because they are more important for the blind.\",\n    \"what_is_the_map_like__buildings\": \"Buildings\",\n    \"map_content_unnamed_roads_one\": \"__count__ unnamed road\",\n    \"map_content_unnamed_roads_many\": \"__count__ unnamed roads\",\n    \"map_content_heading\": \"Map content\",\n    \"map_content_subheading_ways\": \"Ways\",\n    \"map_content_subheading_roads\": \"Roads\",\n    \"map_content_subheading_paths\": \"Paths\",\n    \"map_content_subheading_railways\": \"Railways\",\n    \"map_content_subheading_waterways\": \"Waterways\",\n    \"map_content_subheading_water_areas\": \"Water areas\",\n    \"map_content_subheading_other_linear\": \"Other linear features\",\n    \"map_content_subheading_buildings\": \"Buildings\",\n    \"map_content_subheading_poi_familiar\": \"Familiar places\",\n    \"map_content_subheading_poi_daily\": \"Daily essentials\",\n    \"map_content_subheading_poi_transport\": \"Transport points\",\n    \"map_content_subheading_poi_accessibility\": \"Accessibility cues\",\n    \"map_content_way_unnamed\": \"Unnamed way\",\n    \"map_content_way_unnamed_waterway\": \"Unnamed waterway\",\n    \"map_content_no_ways\": \"No ways listed for this map.\",\n    \"map_content_no_roads\": \"No roads listed for this map.\",\n    \"map_content_no_paths\": \"No paths listed for this map.\",\n    \"map_content_no_railways\": \"No railways listed for this map.\",\n    \"map_content_no_waterways\": \"No waterways listed for this map.\",\n    \"map_content_no_water_areas\": \"No water areas listed for this map.\",\n    \"map_content_no_other_linear\": \"No other linear features listed for this map.\",\n    \"map_content_unnamed_features_one\": \"__count__ unnamed __type__\",\n    \"map_content_unnamed_features_many\": \"__count__ unnamed __type__\",\n    \"map_content_way_route_from_to\": \"From __start__ to __end__\",\n    \"map_content_way_route_near\": \"__location__\",\n    \"map_content_connects_to_way\": \"Connects to way __way__\",\n    \"map_content_connects_to_ways\": \"Connects to ways __ways__\",\n    \"map_content_connects_to_type\": \"Connects to __type__\",\n    \"map_content_connects_to_type_many\": \"Connects to __count__ __type__\",\n    \"map_content_connects_to_type_many_fallback\": \"Connects to __count__ ways of type __type__\",\n    \"map_content_way_junction_one\": \"1 junction\",\n    \"map_content_way_junction_many\": \"__count__ junctions\",\n    \"map_content_way_length_m\": \"__meters__ meters\",\n    \"map_content_way_lanes_many\": \"__count__ lanes\",\n    \"map_content_way_surface_paved\": \"Paved surface\",\n    \"map_content_way_surface_unpaved\": \"Unpaved surface\",\n    \"map_content_way_surface_unknown\": \"Paving not known\",\n    \"map_content_way_meta\": \"__type__, __length__\",\n    \"map_content_way_meta_type_only\": \"__type__\",\n    \"map_content_way_meta_length_only\": \"__length__\",\n    \"map_content_way_type_A1_road_construction\": \"road under construction\",\n    \"map_content_way_type_A1_major_roads\": \"major road\",\n    \"map_content_way_type_A1_secondary_roads\": \"secondary road\",\n    \"map_content_way_type_A1_local_streets\": \"local street\",\n    \"map_content_way_type_A1_service_roads\": \"service road\",\n    \"map_content_way_type_plural_A1_service_roads\": \"service roads\",\n    \"map_content_way_type_A1_track_roads\": \"track road\",\n    \"map_content_way_type_A1_vehicle_unspecified\": \"other vehicular road\",\n    \"map_content_way_type_A2_pedestrian_streets\": \"pedestrian street\",\n    \"map_content_way_type_A2_footpaths_trails\": \"footpath / trail\",\n    \"map_content_way_type_A2_cycleways\": \"cycleway\",\n    \"map_content_way_type_A2_steps_ramps\": \"step / ramp\",\n    \"map_content_way_type_A2_pedestrian_unspecified\": \"other pedestrian path\",\n    \"map_content_way_type_A3_rail_lines\": \"rail line\",\n    \"map_content_way_type_A3_tram_light_rail\": \"tram / light rail\",\n    \"map_content_way_type_A3_subway_metro\": \"subway / metro\",\n    \"map_content_way_type_A3_rail_yards_sidings\": \"rail yard / siding\",\n    \"map_content_way_type_A4_rivers\": \"river\",\n    \"map_content_way_type_A4_streams_canals\": \"stream / canal\",\n    \"map_content_way_type_A4_ditches_drains\": \"ditch / drain\",\n    \"map_content_way_type_A4_other_waterways\": \"other waterway\",\n    \"map_content_way_type_plural_A4_other_waterways\": \"other waterways\",\n    \"map_content_way_type_A_other_ways\": \"other way\",\n    \"map_content_water_area_unnamed\": \"Unnamed __type__\",\n    \"map_content_water_area_type_generic\": \"water area\",\n    \"map_content_water_area_type_generic_plural\": \"water areas\",\n    \"map_content_water_area_type_B1_lakes\": \"lake\",\n    \"map_content_water_area_type_B1_ponds\": \"pond\",\n    \"map_content_water_area_type_B1_reservoirs\": \"reservoir\",\n    \"map_content_water_area_type_B1_sea_coast\": \"sea\",\n    \"map_content_water_area_type_B1_riverbanks\": \"riverbank\",\n    \"map_content_water_area_type_B1_other_water\": \"water area\",\n    \"map_content_building_unnamed\": \"Unnamed building\",\n    \"map_content_no_buildings\": \"No buildings listed for this map.\",\n    \"map_content_no_poi_familiar\": \"No familiar places listed for this map.\",\n    \"map_content_no_poi_daily\": \"No daily essentials listed for this map.\",\n    \"map_content_no_poi_transport\": \"No transport points listed for this map.\",\n    \"map_content_no_poi_accessibility\": \"No accessibility cues are tagged for this map.\",\n    \"map_content_show_more_buildings_one\": \"Show 1 more building\",\n    \"map_content_show_more_buildings_many\": \"Show __count__ more buildings\",\n    \"map_content_show_less_buildings\": \"Show fewer buildings\",\n    \"map_content_show_more_features_one\": \"Show 1 more feature\",\n    \"map_content_show_more_features_many\": \"Show __count__ more features\",\n    \"map_content_show_less_features\": \"Show fewer features\",\n    \"map_content_summary_show_more\": \"Show more\",\n    \"map_content_summary_show_only\": \"Show summary only\",\n    \"map_content_height_note_raised_mm\": \"Raised __millimeters__ mm\",\n    \"map_content_height_note_varying\": \"Raised by varying amounts\",\n    \"map_content_height_note_waved_surface\": \"Waved surface\",\n    \"map_content_height_note_text_only\": \"Textual information only\",\n    \"map_content_show_more_pois_one\": \"Show 1 more place\",\n    \"map_content_show_more_pois_many\": \"Show __count__ more places\",\n    \"map_content_show_less_pois\": \"Show fewer places\",\n    \"map_content_poi_unknown_type\": \"Unknown place type\",\n    \"map_content_poi_unnamed\": \"Unnamed __type__\",\n    \"map_content_poi_unnamed_many\": \"__count__ unnamed __type__\",\n    \"map_content_poi_sparse_note\": \"Only a few POIs are tagged in this area; missing places may be due to OpenStreetMap data coverage.\",\n    \"map_content_poi_access_crossings_signals_one\": \"__count__ place with signalized crossings\",\n    \"map_content_poi_access_crossings_signals_many\": \"__count__ places with signalized crossings\",\n    \"map_content_poi_access_tactile_summary\": \"Tactile paving tags: __yes__ yes, __no__ no\",\n    \"map_content_poi_access_kerb_summary\": \"Kerb tags: lowered __lowered__, flush __flush__, raised __raised__\",\n    \"map_content_poi_access_steps_summary\": \"Steps tagged at __count__ places (largest step count __max__)\",\n    \"map_content_poi_access_incline_summary\": \"Incline tags: up __up__, down __down__\",\n    \"map_content_poi_access_wheelchair_summary\": \"Wheelchair tags: yes __yes__, limited __limited__, no __no__\",\n    \"map_content_poi_example\": \"Example: __example__\",\n    \"map_content_building_join_at\": \"at\",\n    \"map_content_building_type_from_value\": \"__value__ building\",\n    \"map_content_unavailable\": \"Map content is not available.\",\n    \"map_content_list_and\": \"and\",\n    \"map_content_edge_phrase\": \"__edge__ edge\",\n    \"map_content_touches_edge\": \"Touches __edge__ edge\",\n    \"map_content_touches_edges\": \"Touches __edges__ edges\",\n    \"map_content_touches_items\": \"Touches __items__\",\n    \"map_content_crosses_edge\": \"Crosses __edge__ edge\",\n    \"map_content_crosses_edges\": \"Crosses __edges__ edges\",\n    \"map_content_crosses_items\": \"Crosses __items__\",\n    \"map_content_touches_edge_percent\": \"touches __percent__% of __edge__ edge\",\n    \"map_content_touch_pos_north\": \"in the north\",\n    \"map_content_touch_pos_south\": \"in the south\",\n    \"map_content_touch_pos_west\": \"in the west\",\n    \"map_content_touch_pos_east\": \"in the east\",\n    \"map_content_touch_pos_center\": \"near the center\",\n    \"map_content_touch_pos_multiple\": \"in multiple places\",\n    \"map_content_edge_north\": \"north\",\n    \"map_content_edge_south\": \"south\",\n    \"map_content_edge_east\": \"east\",\n    \"map_content_edge_west\": \"west\",\n    \"map_content_dir_north\": \"north\",\n    \"map_content_dir_northeast\": \"north-east\",\n    \"map_content_dir_east\": \"east\",\n    \"map_content_dir_southeast\": \"south-east\",\n    \"map_content_dir_south\": \"south\",\n    \"map_content_dir_southwest\": \"south-west\",\n    \"map_content_dir_west\": \"west\",\n    \"map_content_dir_northwest\": \"north-west\",\n    \"map_content_corner_northwest\": \"north-west corner\",\n    \"map_content_corner_northeast\": \"north-east corner\",\n    \"map_content_corner_southwest\": \"south-west corner\",\n    \"map_content_corner_southeast\": \"south-east corner\",\n    \"map_content_loc_center\": \"center\",\n    \"map_content_loc_offset_center\": \"__dir__ of the center\",\n    \"map_content_loc_part\": \"__dir__ part\",\n    \"map_content_loc_edge\": \"__dir__ edge\",\n    \"map_content_loc_corner\": \"__corner__\",\n    \"map_content_loc_near_edge\": \"near __dir__ edge\",\n    \"map_content_loc_near_corner\": \"__corner__\",\n    \"map_content_loc_full_center\": \"in the center\",\n    \"map_content_loc_full_offset_center\": \"a little __dir__ of the center\",\n    \"map_content_loc_full_part\": \"in the __dir__ part\",\n    \"map_content_loc_full_edge\": \"near the __dir__ edge\",\n    \"map_content_loc_full_corner\": \"in the __corner__\",\n    \"map_content_loc_full_near_edge\": \"near the __dir__ edge\",\n    \"map_content_loc_full_near_corner\": \"in the __corner__\",\n    \"map_content_loc_endpoint_center\": \"the center\",\n    \"map_content_loc_endpoint_part\": \"the __dir__ part\",\n    \"map_content_loc_endpoint_near_edge\": \"near the __dir__ edge\",\n    \"map_content_loc_endpoint_corner\": \"the __corner__\",\n    \"map_content_loc_route_from_center\": \"the center\",\n    \"map_content_loc_route_from_part\": \"the __dir__ part\",\n    \"map_content_loc_route_from_near_edge\": \"near the __dir__ edge\",\n    \"map_content_loc_route_from_corner\": \"the __corner__\",\n    \"map_content_loc_route_to_center\": \"the center\",\n    \"map_content_loc_route_to_part\": \"the __dir__ part\",\n    \"map_content_loc_route_to_near_edge\": \"near the __dir__ edge\",\n    \"map_content_loc_route_to_corner\": \"the __corner__\",\n    \"map_content_region_center\": \"center\",\n    \"map_content_region_part\": \"__dir__ part\",\n    \"map_content_region_edge\": \"__dir__ edge\",\n    \"map_content_region_corner\": \"__corner__\",\n    \"map_content_summary_in\": \"In the __region__.\",\n    \"map_content_summary_near\": \"Near the __region__.\",\n    \"map_content_summary_mostly_in_lead\": \"Mostly in the __region__\",\n    \"map_content_summary_mainly_near_lead\": \"Mostly near the __region__\",\n    \"map_content_summary_mostly_extending\": \"__lead__, extending toward the __target__.\",\n    \"map_content_summary_in_and\": \"In the __first__ and __second__.\",\n    \"map_content_summary_in_and_near\": \"In the __first__ and near the __second__.\",\n    \"map_content_summary_near_and\": \"Near the __first__ and __second__.\",\n    \"map_content_summary_mainly_in_and\": \"In several areas, mostly in the __first__ and __second__.\",\n    \"map_content_summary_mainly_in_and_near\": \"In several areas, mostly in the __first__ and near the __second__.\",\n    \"map_content_summary_mainly_near_and\": \"In several areas, mostly near the __first__ and __second__.\",\n    \"map_content_summary_mainly_near_and_in\": \"In several areas, mostly near the __first__ and in the __second__.\",\n    \"map_content_summary_several_mainly_in\": \"In several areas, mostly in the __region__.\",\n    \"map_content_summary_several_mainly_near\": \"In several areas, mostly near the __region__.\",\n    \"map_content_summary_equal_clauses\": \"__first__ and __second__.\",\n    \"map_content_summary_mostly_extending_clauses\": \"Mostly __lead__, extending toward __target__.\",\n    \"map_content_summary_distributed_top2\": \"In several areas, mostly __first__ and __second__.\",\n    \"map_content_summary_distributed_top1\": \"In several areas, mostly __first__.\",\n    \"map_content_summary_three_regions_clauses\": \"Mostly __first__, __second__, and __third__.\",\n    \"map_content_parts_one\": \"__count__ part\",\n    \"map_content_parts_many\": \"__count__ parts\",\n    \"map_content_total_area\": \"Covers __percent__% of map\",\n    \"map_content_shape_elongated\": \"Thin\",\n    \"map_content_shape_long_thin\": \"Very thin\",\n    \"map_content_shape_irregular\": \"Irregular shape\",\n    \"map_content_shape_generic\": \"__shape__ shape\",\n    \"map_content_aspect_label\": \"aspect __ratio__\",\n    \"map_content_degrees_abbrev\": \"deg\",\n    \"map_content_orientation\": \"Orientation __label__\",\n    \"map_content_orientation_label_east_west\": \"east-west\",\n    \"map_content_orientation_label_north_south\": \"north-south\",\n    \"map_content_orientation_label_northeast_southwest\": \"north-east to south-west\",\n    \"map_content_orientation_label_east_northeast_to_west_southwest\": \"east-north-east to west-south-west\",\n    \"map_content_orientation_label_north_northeast_to_south_southwest\": \"north-north-east to south-south-west\",\n    \"map_content_coverage_label\": \"Covers:\",\n    \"map_content_coverage_much\": \"much of __segment__\",\n    \"map_content_coverage_some\": \"some of __segment__\",\n    \"map_content_coverage_little\": \"a little of __segment__\",\n    \"map_content_osm_value_kindergarten\": \"kindergarten\",\n    \"map_content_osm_value_school\": \"school\",\n    \"map_content_osm_value_hospital\": \"hospital\",\n    \"map_content_osm_value_church\": \"church\",\n    \"map_content_osm_value_university\": \"university\",\n    \"map_content_osm_value_college\": \"college\",\n    \"map_content_osm_value_library\": \"library\",\n    \"map_content_osm_value_gallery\": \"gallery\",\n    \"map_content_osm_value_museum\": \"museum\",\n    \"map_content_osm_value_theatre\": \"theatre\",\n    \"map_content_osm_value_supermarket\": \"supermarket\",\n    \"map_content_osm_value_cafe\": \"cafe\",\n    \"map_content_osm_value_restaurant\": \"restaurant\",\n    \"map_content_osm_value_hotel\": \"hotel\",\n    \"map_content_osm_value_parking\": \"parking\",\n    \"map_content_osm_value_bicycle_parking\": \"bicycle parking\",\n    \"map_content_osm_value_residential\": \"residential\",\n    \"map_content_osm_value_commercial\": \"commercial\",\n    \"map_content_osm_value_industrial\": \"industrial\",\n    \"map_content_osm_value_public\": \"public\",\n    \"map_content_osm_value_roof\": \"roof\",\n    \"map_content_osm_value_transportation\": \"transportation\",\n    \"map_content_osm_value_service\": \"service\",\n    \"map_content_osm_value_civic\": \"civic\",\n    \"map_content_osm_value_office\": \"office\",\n    \"map_content_osm_value_apartments\": \"apartment building\",\n    \"map_content_osm_value_house\": \"house\",\n    \"map_content_osm_value_detached\": \"detached house\",\n    \"map_content_osm_value_terrace\": \"terraced houses\",\n    \"map_content_osm_value_semidetached_house\": \"semi-detached house\",\n    \"map_content_osm_value_farm\": \"farm\",\n    \"map_content_osm_value_warehouse\": \"warehouse\",\n    \"map_content_osm_value_garages\": \"garages\",\n    \"map_content_osm_value_garage\": \"garage\",\n    \"map_content_osm_value_retail\": \"retail\",\n    \"map_content_osm_value_kiosk\": \"kiosk\",\n    \"map_content_osm_value_playground\": \"playground\",\n    \"map_content_osm_value_park\": \"park\",\n    \"map_content_osm_value_zoo\": \"zoo\",\n    \"map_content_osm_value_sports_centre\": \"sports centre\",\n    \"map_content_osm_value_station\": \"station\",\n    \"map_content_osm_value_rail\": \"rail\",\n    \"map_content_osm_value_platform\": \"platform\",\n    \"map_content_osm_value_stop_position\": \"stop position\",\n    \"map_content_osm_value_stop_area\": \"stop area\",\n    \"map_content_osm_value_stop_area_group\": \"stop area group\",\n    \"map_content_osm_value_halt\": \"halt\",\n    \"map_content_osm_value_bus_stop\": \"bus stop\",\n    \"map_content_osm_value_bus_station\": \"bus station\",\n    \"map_content_osm_value_tram_stop\": \"tram stop\",\n    \"map_content_osm_value_tram\": \"tram\",\n    \"map_content_osm_value_subway_entrance\": \"subway entrance\",\n    \"map_content_osm_value_ferry_terminal\": \"ferry terminal\",\n    \"map_content_osm_value_taxi\": \"taxi\",\n    \"map_content_osm_value_department_store\": \"department store\",\n    \"map_content_osm_value_mall\": \"mall\",\n    \"map_content_osm_value_attraction\": \"attraction\",\n    \"map_content_osm_value_artwork\": \"artwork\",\n    \"map_content_osm_value_fountain\": \"fountain\",\n    \"map_content_osm_value_viewpoint\": \"viewpoint\",\n    \"map_content_osm_value_theme_park\": \"theme park\",\n    \"map_content_osm_value_aquarium\": \"aquarium\",\n    \"map_content_osm_value_arts_centre\": \"arts centre\",\n    \"map_content_osm_value_community_centre\": \"community centre\",\n    \"map_content_osm_value_place_of_worship\": \"place of worship\",\n    \"map_content_osm_value_townhall\": \"town hall\",\n    \"map_content_osm_value_courthouse\": \"courthouse\",\n    \"map_content_osm_value_pharmacy\": \"pharmacy\",\n    \"map_content_osm_value_clinic\": \"clinic\",\n    \"map_content_osm_value_doctors\": \"doctors\",\n    \"map_content_osm_value_toilets\": \"toilets\",\n    \"map_content_osm_value_post_office\": \"post office\",\n    \"map_content_osm_value_bank\": \"bank\",\n    \"map_content_osm_value_atm\": \"ATM\",\n    \"map_content_osm_value_fuel\": \"fuel station\",\n    \"map_content_osm_value_convenience\": \"convenience store\",\n    \"map_content_osm_value_fast_food\": \"fast food\",\n    \"map_content_osm_value_bar\": \"bar\",\n    \"map_content_osm_value_pub\": \"pub\",\n    \"map_content_osm_value_marketplace\": \"marketplace\",\n    \"map_content_osm_value_bench\": \"bench\",\n    \"map_content_osm_value_waste_basket\": \"waste basket\",\n    \"map_content_osm_value_vending_machine\": \"vending machine\",\n    \"map_content_osm_value_street_lamp\": \"street lamp\",\n    \"map_content_osm_value_outdoor_seating\": \"outdoor seating\",\n    \"map_content_osm_value_sauna\": \"sauna\",\n    \"map_content_osm_value_shelter\": \"shelter\",\n    \"map_content_osm_value_abandoned\": \"abandoned\",\n    \"map_content_osm_value_razed\": \"razed\",\n    \"map_content_osm_value_bicycle\": \"bicycle\",\n    \"map_content_osm_value_building\": \"building\",\n    \"map_content_osm_value_residential_building\": \"residential building\",\n    \"map_content_osm_value_commercial_building\": \"commercial building\",\n    \"map_content_osm_value_industrial_building\": \"industrial building\",\n    \"map_content_osm_value_public_building\": \"public building\",\n    \"map_content_osm_value_garages_building\": \"garages building\",\n    \"map_content_osm_value_yes\": \"generic\",\n    \"what_is_the_map_like__railways\": \"Railways\",\n    \"what_is_the_map_like__water\": \"Waterbodies (with wavy surface, but narrow streams as lines)\",\n    \"what_is_the_map_like__north\": \"North-east corner marked to indicate correct orientation\",\n    \"what_is_the_map_like__selected_addr\": \"In a 3D-print the selected address (or entered coordinates) is marked with a cone\",\n    \"what_is_the_map_like__clarity\": \"Touch Mapper tactile maps are optimized for clarity and the practical needs of people who are visually impaired: the 3D-printed maps are not a true-to-life representations of the area.\",\n    \"larger_scale_maps\": \"Larger area and indoor maps\",\n    \"larger_scale_maps_answer\": \"Touch Mapper currently supports only maps that show each street and building in the area. Maps that cover larger areas, show ground elevation, or indoors areas could be supported later. Notice that by selecting \\\"Print a part of a multipart map\\\" you can cover a larger area by printing multiple maps. There are mass-produced country scale maps for example in __aphLinkStart__American Printing House for the Blind Shop__aphLinkEnd__.\",\n    \"data_comes_from_osm\": \"Map data source\",\n    \"data_comes\": \"Touch Mapper uses __startTag__OpenStreetMap__endTag__ data. Although this is usually an excellent source, accuracy of the maps cannot be guaranteed. The most common problem is missing buildings. If important features are missing, you may freely add them to OpenStreetMap. Unfortunately commercial map data sources like Google Maps can't be used, because they don't allow required type of access to their data.\",\n    \"commercial_use\": \"Commercial use\",\n    \"commercial_use_answer\": \"You are welcome to use Touch Mapper for commercial purposes, such as printing and selling the maps. I only ask that you let your customers clearly know that the maps come from www.touch-mapper.org, both in advertising and packaging. I also hope you'll let me know if you start offering a service based on Touch Mapper.\",\n    \"users_and_purposes\": \"Uses and benefits\",\n    \"tactile_maps_have_many_different_user_groups\": \"Tactile maps can benefit many different user groups.\",\n    \"visually_impaired_individuals\": \"People who are blind or partially sighted: better orientation and route planning\",\n    \"outdoors_tourist_attractions\": \"Outdoor tourist attractions: better service for customers who are blind or partially sighted\",\n    \"public_areas_such_as_parks\": \"Public areas such as parks and playgrounds: tactile maps can help visitors who are blind or partially sighted to enjoy the park more independently.\",\n    \"orientation_and_mobility_specialists\": \"Orientation and mobility specialists: to help clients to build a mental map of an area more easily\",\n    \"multipart_maps_heading\": \"Printing maps in multiple parts\",\n    \"multipart_maps\": \"If a single map isn't large enough to cover the required area, for example a long road, you can make multiple prints and lay them side by side on a flat surface. Select \\\"Advanced options\\\" and then \\\"Print a part of multipart map\\\". In this mode, there will be no borders around the edges of a 3D print. Also, there appears arrow buttons for adjusting the selected area by 100% (that is, map's diameter) in each direction, which makes it easy to select adjacent areas for the maps.\",\n    \"how_to_3d_print\": \"3D printing\",\n    \"map_can_be_printed\": \"The map can be printed by almost any 3D printer, including affordable hobbyist devices. The most important thing to pay attention to are the roads. Some services may consider them too narrow to print.\",\n    \"if_you_print_yourself\": \"If you print using your own 3D printer, use layer thickness of 0.25 mm or 0.3 mm. The base is 0.6 mm thick, and it's enough to print it in two layers. Two top layers and one horizontal shell are enough. You may want to print the top layer quite slowly to get smooth surfaces. If you use Simplify3D slicer, you will need to invoke \\\"Separate Connected Surfaces\\\" to get a correct print. Printing time for a 17 cm map with the base printed in two layers is around 3–4 hours.\",\n    \"about_touch_mapper\": \"About Touch Mapper\",\n    \"about_touch_mapper_answer\": \"My name is Sofia Pahaoja. I created Touch Mapper after learning about the challenges faced by people who are blind or partially sighted when travelling independently, and realizing how 3D printing combined with freely available map data can help.\",\n    \"news_from\": \"Information about new features and relevant events are posted to __linkFB__ and __linkTwitter__.\",\n    \"want_to_improve_please\": \"I want to make Touch Mapper as good as possible, and user feedback is key in that. If you have any questions or suggestions for improvement, please\",\n    \"email_me\": \"Email me\",\n    \"address_search_by\": \"Address search __linkTag__.\",\n    \"map_data_by\": \"Map data by __linkTag__.\",\n    \"touch_mapper_is_os\": \"Touch Mapper is open source software.\",\n    \"see_github\": \"Use __startTag__GitHub__endTag__ for bug reports and feature suggestions.\",\n    \"map_content_osm_value_dormitory\": \"dormitory\",\n    \"map_content_osm_value_motorway\": \"motorway\",\n    \"map_content_osm_value_trunk\": \"trunk road\",\n    \"map_content_osm_value_primary\": \"primary road\",\n    \"map_content_osm_value_secondary\": \"secondary road\",\n    \"map_content_osm_value_tertiary\": \"tertiary road\",\n    \"map_content_osm_value_unclassified\": \"unclassified road\",\n    \"map_content_osm_value_living_street\": \"living street\",\n    \"map_content_osm_value_pedestrian\": \"pedestrian street\",\n    \"map_content_osm_value_footway\": \"footway\",\n    \"map_content_osm_value_path\": \"path\",\n    \"map_content_osm_value_cycleway\": \"cycleway\",\n    \"map_content_osm_value_steps\": \"steps\",\n    \"map_content_osm_value_track\": \"track\",\n    \"map_content_osm_value_road\": \"road\",\n    \"map_content_osm_value_dentist\": \"dentist\",\n    \"map_content_osm_value_police\": \"police\",\n    \"map_content_osm_value_fire_station\": \"fire station\",\n    \"map_content_osm_value_charging_station\": \"charging station\",\n    \"map_content_osm_value_hostel\": \"hostel\",\n    \"map_content_osm_value_information\": \"tourist information\",\n    \"map_content_osm_value_bakery\": \"bakery\",\n    \"map_content_osm_value_clothes\": \"clothes shop\",\n    \"map_content_osm_value_apartments_building\": \"apartment building\",\n    \"map_content_osm_value_house_building\": \"house\",\n    \"map_content_osm_value_detached_building\": \"detached house\",\n    \"map_content_osm_value_terrace_building\": \"terraced houses\",\n    \"map_content_osm_value_dormitory_building\": \"dormitory\",\n    \"map_content_osm_value_retail_building\": \"retail building\",\n    \"map_content_osm_value_office_building\": \"office building\",\n    \"map_content_osm_value_warehouse_building\": \"warehouse\",\n    \"map_content_osm_value_school_building\": \"school building\",\n    \"map_content_osm_value_university_building\": \"university building\",\n    \"map_content_osm_value_hospital_building\": \"hospital building\",\n    \"map_content_osm_value_church_building\": \"church\",\n    \"map_content_osm_value_garage_building\": \"garage\",\n    \"map_content_osm_value_roof_building\": \"roof\"\n}\n"
  },
  {
    "path": "web/locales/es/tm.json",
    "content": "{\n    \"location2_top_left\": \"cuarto superior izquierdo\",\n    \"location2_top_row\": \"mitad superior\",\n    \"location2_top_right\": \"cuarto superior derecho\",\n    \"location2_right_column\": \"mitad derecha\",\n    \"location2_bottom_right\": \"cuarto inferior derecho\",\n    \"location2_bottom_row\": \"mitad inferior\",\n    \"location2_bottom_left\": \"cuarto inferior izquierdo\",\n    \"location2_left_column\": \"mitad izquierda\",\n    \"location3_middle_center\": \"centro\",\n    \"location3_top_center\": \"centro superior\",\n    \"location3_bottom_center\": \"centro inferior\",\n    \"location3_middle_left\": \"centro izquierda\",\n    \"location3_middle_right\": \"centro derecho\",\n    \"location3_top_left\": \"esquina superior izquierda\",\n    \"location3_top_row\": \"borde superior\",\n    \"location3_mostly_top_row\": \"principalmente borde superior\",\n    \"location3_top_left_to_mc\": \"esquina superior izquierda al centro\",\n    \"location3_top_left_and_center\": \"borde superior lado izquierdo\",\n    \"location3_top_right_and_center\": \"borde superior lado derecho\",\n    \"location3_top_right\": \"esquina superior derecha\",\n    \"location3_right_column\": \"borde derecho\",\n    \"location3_mostly_right_column\": \"mayoritariamente borde derecho\",\n    \"location3_top_right_to_mc\": \"centro a la esquina superior derecha\",\n    \"location3_top_right_and_middle\": \"lado superior del borde derecho\",\n    \"location3_bottom_right_and_middle\": \"centro a la esquina inferior derecha\",\n    \"location3_bottom_right\": \"esquina inferior derecha\",\n    \"location3_bottom_row\": \"borde inferior\",\n    \"location3_mostly_bottom_row\": \"principalmente borde inferior\",\n    \"location3_bottom_right_to_mc\": \"centro a la esquina inferior derecha\",\n    \"location3_bottom_right_and_center\": \"borde inferior lado derecho\",\n    \"location3_bottom_left_and_center\": \"borde inferior lado izquierdo\",\n    \"location3_bottom_left\": \"esquina inferior izquierda\",\n    \"location3_left_column\": \"borde izquierdo\",\n    \"location3_mostly_left_column\": \"borde mayoritariamente izquierdo\",\n    \"location3_bottom_left_to_mc\": \"centro a la esquina inferior izquierda\",\n    \"location3_bottom_left_and_middle\": \"borde izquierdo lado inferior\",\n    \"location3_top_left_and_middle\": \"lado superior del borde izquierdo\",\n    \"location3_middle_row\": \"de izquierda a derecha en el medio\",\n    \"location3_center_column\": \"de arriba a abajo en el medio\",\n    \"location3_top_left_diagonal\": \"esquina superior izquierda a inferior derecha\",\n    \"location3_top_right_diagonal\": \"esquina inferior izquierda a la esquina superior derecha\",\n    \"location3_top_left_to_middle_right\": \"desde la esquina superior izquierda hasta el centro derecho\",\n    \"location3_top_right_to_middle_left\": \"desde el centro izquierdo hasta la esquina superior derecha\",\n    \"location3_top_right_to_bottom_center\": \"centro inferior a la esquina superior derecha\",\n    \"location3_bottom_right_to_top_center\": \"centro superior a la esquina inferior derecha\",\n    \"location3_bottom_right_to_middle_left\": \"desde el centro izquierdo hasta la esquina inferior derecha\",\n    \"location3_bottom_left_to_middle_right\": \"esquina inferior izquierda al centro derecho\",\n    \"location3_bottom_left_to_top_center\": \"esquina inferior izquierda al centro superior\",\n    \"location3_top_left_to_bottom_center\": \"esquina superior izquierda al centro inferior\",\n    \"location3_near_top_left\": \"cerca de la esquina superior izquierda\",\n    \"location3_top_center_to_middle_left\": \"centro izquierdo al centro superior\",\n    \"location3_near_top_right\": \"cerca de la esquina superior derecha\",\n    \"location3_top_center_to_middle_right\": \"centro superior al centro derecho\",\n    \"location3_near_bottom_right\": \"cerca de la esquina inferior derecha\",\n    \"location3_middle_right_to_bottom_center\": \"centro inferior al centro derecho\",\n    \"location3_near_bottom_left\": \"cerca de la esquina inferior izquierda\",\n    \"location3_middle_left_to_bottom_center\": \"centro izquierdo al centro inferior\",\n    \"location_general\": \"muchas áreas\",\n    \"skip_to_main_content\": \"Saltar al contenido principal\",\n    \"meta_description\": \"Mapas táctiles fácilmente para cualquier dirección. Imprímelos en 3D tú mismo o haz tu pedido.\",\n    \"meta_keywords\": \"mapa táctil, ciego, discapacidad visual, ayuda para ciegos, ayuda para personas con discapacidad visual, tecnología para ciegos, tecnología para personas con discapacidad visual, impresión 3d\",\n    \"meta_og_site_name\": \"Touch Mapper - ​​Mapas táctiles para personas con discapacidad visual\",\n    \"title\": \"Mapas táctiles fácilmente\",\n    \"blurb\": \"Crea mapas táctiles fácilmente para cualquier dirección\",\n    \"language\": \"Idioma\",\n    \"help\": \"Ayuda\",\n    \"open_help_aria\": \"Abrir ayuda en una nueva pestaña\",\n    \"map_content_external_link_aria\": \"Enlace externo, se abre en una pestaña nueva\",\n    \"breadcrumb_nav_title\": \"Migaja de pan\",\n    \"breadcrumb_home\": \"Búsqueda de direcciones\",\n    \"breadcrumb_area\": \"Ajustes\",\n    \"breadcrumb_map\": \"Mapa\",\n    \"intro\": \"Los mapas táctiles son una gran ayuda para las personas ciegas o deficientes visuales, ayudándoles a orientarse y planificar rutas. Con Touch Mapper, puede crear fácilmente mapas exteriores personalizados para cualquier dirección que elija. Puede imprimir el mapa usted mismo sin coste alguno utilizando una impresora 3D, o puede solicitar una impresión 3D, a partir de 29 euros/USD. También se admiten impresoras en relieve e impresoras de papel hinchado.\",\n    \"it_is_simple\": \"Es simple\",\n    \"simple__enter_address\": \"Ingrese una dirección y haga clic en \\\"Buscar\\\"\",\n    \"simple__click_create_tactile_map\": \"Haga clic en \\\"Crear mapa táctil\\\"\",\n    \"simple__intro_order_or_print\": \"Imprima o solicite el mapa\",\n    \"address\": \"Dirección\",\n    \"street_address\": \"Dirección\",\n    \"search\": \"Buscar\",\n    \"searching\": \"Búsqueda...\",\n    \"nothing_found\": \"No se encontró nada\",\n    \"map_parameters\": \"Parámetros del mapa\",\n    \"show_all_addresses\": \"Mostrar más\",\n    \"show_all_addresses_aria\": \"Mostrar más direcciones encontradas\",\n    \"map_printing\": \"Impresión de mapas\",\n    \"map_printing__order\": \"Solicite una impresión 3D\",\n    \"map_printing__self\": \"Imprime el mapa tú mismo\",\n    \"printing_tech\": \"Tecnología de impresión\",\n    \"printing_tech__3d\": \"Impresión 3D\",\n    \"printing_tech__2d\": \"Papel para gofrar o hinchar\",\n    \"map_size\": \"Tamaño de impresión\",\n    \"can_print_any_size\": \"Puede imprimir en cualquier tamaño, pero la selección de escala a continuación supone 27,9 cm / 11 pulgadas.\",\n    \"size_order_17\": \"17 cm / 6,7 pulgadas de ancho (se adapta a casi todas las impresoras 3D)\",\n    \"size_order_20\": \"20 cm / 7,9 pulgadas de ancho (se adapta a la mayoría de las impresoras 3D)\",\n    \"size_self_8\": \"8 cm / 3,1 pulgadas de ancho (bueno para impresiones de prueba)\",\n    \"size_self_17\": \"17 cm / 6,7 pulgadas de ancho (se adapta a casi todas las impresoras 3D)\",\n    \"size_self_20\": \"20 cm / 7,9 pulgadas de ancho (se adapta a la mayoría de las impresoras 3D)\",\n    \"map_scale\": \"Escala del mapa\",\n    \"map_scale__1000\": \"edificios individuales o puntos de interés\",\n    \"map_scale__1800\": \"ciudades densas\",\n    \"map_scale__2400\": \"por defecto\",\n    \"map_scale__3200\": \"suburbios promedio\",\n    \"map_scale__5600\": \"áreas muy escasamente construidas\",\n    \"map_scale__9999\": \"áreas con solo carreteras grandes\",\n    \"map_scale_coverage\": \"A esta escala, un mapa __size__ de ancho cubre un área __area_metric__ / __area_imperial__ de ancho.\",\n    \"map_content\": \"Contenido\",\n    \"content_mode_normal\": \"Normal\",\n    \"content_mode_no_buildings\": \"Sin edificios\",\n    \"content_mode_only_big_roads\": \"Solo carreteras grandes, ferrocarriles y áreas de agua\",\n    \"target_road_density_ui\": \"Retención de carreteras\",\n    \"target_road_density_ui_hint\": \"Un número mayor significa más carreteras, el valor predeterminado es 10\",\n    \"location_marker\": \"Marcador de ubicación\",\n    \"hide_location_marker\": \"Ocultar marcador de ubicación\",\n    \"advanced_settings\": \"Avanzado\",\n    \"show_advanced_settings\": \"Mostrar opciones avanzadas\",\n    \"coordinates\": \"Coordenadas\",\n    \"longitude_x\": \"Longitud (X)\",\n    \"latitude_y\": \"Latitud (Y)\",\n    \"area_adjustment\": \"Ajuste de ubicación\",\n    \"x_offset\": \"Desplazamiento X\",\n    \"positive_is_right\": \"positivo hacia la derecha\",\n    \"y_offset\": \"Desplazamiento Y\",\n    \"positive_is_up\": \"positivo hacia arriba\",\n    \"custom_map_size\": \"Tamaño de impresión personalizado\",\n    \"unit_cm\": \"cm\",\n    \"custom_scale\": \"Escala personalizada\",\n    \"multipart_map\": \"Mapa multiparte\",\n    \"multipart_map_label\": \"Imprime una parte de un mapa de varias partes (oculta el marcador de ubicación)\",\n    \"adjust_area_for_multipart\": \"Ajustar la ubicación de la parte del mapa\",\n    \"move_map_area_left\": \"Mover el área del mapa a la izquierda por __amount__ por ciento\",\n    \"move_map_area_up\": \"Mover el área del mapa hacia arriba en un __amount__ por ciento\",\n    \"move_map_area_down\": \"Mover el área del mapa hacia abajo en un __amount__ por ciento\",\n    \"move_map_area_right\": \"Mover el área del mapa a la derecha en un __amount__ por ciento\",\n    \"multipart_current_adjustment\": \"Actual\",\n    \"multipart_current_adjustment_aria\": \"Ajuste actual de la ubicación de la parte del mapa.\",\n    \"create_tactile_map\": \"Crear mapa táctil\",\n    \"progress__connecting\": \"Conectando...\",\n    \"progress__reading_osm\": \"20% creado...\",\n    \"progress__converting\": \"60% creado...\",\n    \"progress__uploading\": \"80% subido...\",\n    \"map_for_address\": \"Mapa para\",\n    \"content__heading\": \"Contenido\",\n    \"content__road\": \"Carreteras\",\n    \"content__bus_stop\": \"Paradas de autobús\",\n    \"content__shop\": \"Tiendas\",\n    \"content__restaurant\": \"Restaurantes\",\n    \"content__no_roads\": \"Sin carreteras\",\n    \"content__unnamed_roads\": \"Carreteras sin nombre\",\n    \"warning_no_buildings\": \"Advertencia: OpenStreetMap puede tener datos incompletos para esta zona. Consulta la ayuda para más información.\",\n    \"download\": \"Descargue el archivo STL imprimible\",\n    \"other_downloads\": \"Otras descargas\",\n    \"show_more_downloads\": \"Mostrar más descargas\",\n    \"download_map_descriptions\": \"Descripciones de mapas\",\n    \"download_stl_ways\": \"STL: Sólo carreteras\",\n    \"download_stl_rest\": \"STL: Todos menos las carreteras\",\n    \"download_pdf_embossers\": \"PDF\",\n    \"download_svg_embossers\": \"SVG\",\n    \"download_blender\": \"Archivo Blender original\",\n    \"order_from_1\": \"Ordene desde __companyName__.\",\n    \"order_from_2\": \"29€ el mapa de 20 cm / 6,9 pulgadas, 99€ el de 5. Consultar por otras opciones.\",\n    \"open_or_download_svg\": \"Abra el archivo SVG (para impresoras) o haga clic con el botón derecho del ratón para descargarlo.\",\n    \"open_or_download_pdf\": \"Abra el archivo PDF (para máquinas de papel hinchado), o haga clic con el botón derecho para descargarlo\",\n    \"emboss_ordering_later\": \"La opción de pedido de mapas en relieve/papel hinchado llegará más adelante.\",\n    \"osm_attribution\": \"Datos del mapa © __startTag__OpenStreetMap contributors__endTag__. Si crea un producto utilizando Touch Mapper, debe atribuir OpenStreetMap como fuente de datos.\",\n    \"email_map_to\": \"Envíe por correo electrónico un enlace de mapa a\",\n    \"send_submit\": \"Enviar\",\n    \"send_email\": \"Enviar correo electrónico\",\n    \"email_sent\": \"Correo electrónico enviado\",\n    \"error_sending_email\": \"Error al enviar correo electrónico\",\n    \"conversion_error_unknown\": \"La creación del mapa falló.\",\n    \"conversion_error_too_large\": \"El mapa es demasiado denso para procesarlo.\",\n    \"drag_map\": \"Arrastra el mapa para ajustar la ubicación\",\n    \"map_diameter\": \"Diámetro del mapa\",\n    \"label_3d_preview\": \"Vista previa 3D del mapa táctil\",\n    \"map_preview_controls_hint\": \"Puedes hacer zoom, rotar y desplazarte por el mapa.\",\n    \"tooltip_selected_address\": \"Dirección seleccionada\",\n    \"meters\": \"metros\",\n    \"yards\": \"yardas\",\n    \"data_no_longer_available\": \"Los archivos en caché de este mapa han caducado. Vuelva a los parámetros del mapa y haga clic en \\\"Crear mapa táctil\\\" para crear un mapa similar.\",\n    \"back_to_index\": \"Volver a la búsqueda de direcciones\",\n    \"back_to_area\": \"Volver a los parámetros del mapa\",\n    \"help_has_more_info\": \"Más información sobre Touch Mapper en la __startTag__página de ayuda__endTag__\",\n    \"close_tab_to_exit_help\": \"Cierra esta pestaña para salir de la ayuda\",\n    \"what_is_the_map_like\": \"Contenido del mapa\",\n    \"what_is_the_map_like__map_content\": \"Los mapas contienen lo siguiente:\",\n    \"what_is_the_map_like__roads\": \"Carreteras de todos los tamaños. Las vías peatonales están más elevadas que otras porque son más importantes para los ciegos.\",\n    \"what_is_the_map_like__buildings\": \"Edificios\",\n    \"map_content_unnamed_roads_one\": \"__count__ camino sin nombre\",\n    \"map_content_unnamed_roads_many\": \"__count__ carreteras sin nombre\",\n    \"map_content_heading\": \"Contenido del mapa\",\n    \"map_content_subheading_ways\": \"Vías\",\n    \"map_content_subheading_roads\": \"Carreteras\",\n    \"map_content_subheading_paths\": \"Caminos\",\n    \"map_content_subheading_railways\": \"Ferrocarriles\",\n    \"map_content_subheading_waterways\": \"Vías navegables\",\n    \"map_content_subheading_water_areas\": \"Zonas de agua\",\n    \"map_content_subheading_other_linear\": \"Otras características lineales\",\n    \"map_content_subheading_buildings\": \"Edificios\",\n    \"map_content_subheading_poi_familiar\": \"Lugares familiares\",\n    \"map_content_subheading_poi_daily\": \"Esenciales diarios\",\n    \"map_content_subheading_poi_transport\": \"Puntos de transporte\",\n    \"map_content_subheading_poi_accessibility\": \"Señales de accesibilidad\",\n    \"map_content_way_unnamed\": \"Vía sin nombre\",\n    \"map_content_way_unnamed_waterway\": \"Vía navegable sin nombre\",\n    \"map_content_no_ways\": \"No hay vías listadas para este mapa.\",\n    \"map_content_no_roads\": \"No hay carreteras enumeradas para este mapa.\",\n    \"map_content_no_paths\": \"No hay rutas enumeradas para este mapa.\",\n    \"map_content_no_railways\": \"No hay ferrocarriles listados para este mapa.\",\n    \"map_content_no_waterways\": \"No hay vías navegables enumeradas para este mapa.\",\n    \"map_content_no_water_areas\": \"No hay áreas de agua enumeradas para este mapa.\",\n    \"map_content_no_other_linear\": \"No se enumeran otras características lineales para este mapa.\",\n    \"map_content_unnamed_features_one\": \"__count__ sin nombre __type__\",\n    \"map_content_unnamed_features_many\": \"__count__ sin nombre __type__\",\n    \"map_content_way_route_from_to\": \"De __start__ a __end__\",\n    \"map_content_way_route_near\": \"__location__\",\n    \"map_content_connects_to_way\": \"Se conecta a la vía __way__\",\n    \"map_content_connects_to_ways\": \"Se conecta a las vías __ways__\",\n    \"map_content_connects_to_type\": \"Se conecta a __type__\",\n    \"map_content_connects_to_type_many\": \"Se conecta a __count__ __type__\",\n    \"map_content_connects_to_type_many_fallback\": \"Se conecta a __count__ vías del tipo __type__\",\n    \"map_content_way_junction_one\": \"1 cruce\",\n    \"map_content_way_junction_many\": \"__count__ cruces\",\n    \"map_content_way_length_m\": \"__meters__ metros\",\n    \"map_content_way_lanes_many\": \"__count__ carriles\",\n    \"map_content_way_surface_paved\": \"Superficie pavimentada\",\n    \"map_content_way_surface_unpaved\": \"Superficie sin pavimentar\",\n    \"map_content_way_surface_unknown\": \"Pavimentación desconocida\",\n    \"map_content_way_meta\": \"__type__, __length__\",\n    \"map_content_way_meta_type_only\": \"__type__\",\n    \"map_content_way_meta_length_only\": \"__length__\",\n    \"map_content_way_type_A1_road_construction\": \"camino en construcción\",\n    \"map_content_way_type_A1_major_roads\": \"carretera principal\",\n    \"map_content_way_type_A1_secondary_roads\": \"camino secundario\",\n    \"map_content_way_type_A1_local_streets\": \"calle local\",\n    \"map_content_way_type_A1_service_roads\": \"camino de servicio\",\n    \"map_content_way_type_plural_A1_service_roads\": \"caminos de servicio\",\n    \"map_content_way_type_A1_track_roads\": \"camino de pista\",\n    \"map_content_way_type_A1_vehicle_unspecified\": \"otra vía vehicular\",\n    \"map_content_way_type_A2_pedestrian_streets\": \"calle peatonal\",\n    \"map_content_way_type_A2_footpaths_trails\": \"sendero / camino\",\n    \"map_content_way_type_A2_cycleways\": \"carril bici\",\n    \"map_content_way_type_A2_steps_ramps\": \"escalón/rampa\",\n    \"map_content_way_type_A2_pedestrian_unspecified\": \"otro camino peatonal\",\n    \"map_content_way_type_A3_rail_lines\": \"línea ferroviaria\",\n    \"map_content_way_type_A3_tram_light_rail\": \"tranvía / tren ligero\",\n    \"map_content_way_type_A3_subway_metro\": \"metro\",\n    \"map_content_way_type_A3_rail_yards_sidings\": \"playa de vías / apartadero\",\n    \"map_content_way_type_A4_rivers\": \"río\",\n    \"map_content_way_type_A4_streams_canals\": \"arroyo / canal\",\n    \"map_content_way_type_A4_ditches_drains\": \"zanja / drenaje\",\n    \"map_content_way_type_A4_other_waterways\": \"otra vía fluvial\",\n    \"map_content_way_type_plural_A4_other_waterways\": \"otras vías navegables\",\n    \"map_content_way_type_A_other_ways\": \"otra vía\",\n    \"map_content_water_area_unnamed\": \"__type__ sin nombre\",\n    \"map_content_water_area_type_generic\": \"zona de agua\",\n    \"map_content_water_area_type_generic_plural\": \"zonas de agua\",\n    \"map_content_water_area_type_B1_lakes\": \"lago\",\n    \"map_content_water_area_type_B1_ponds\": \"estanque\",\n    \"map_content_water_area_type_B1_reservoirs\": \"depósito\",\n    \"map_content_water_area_type_B1_sea_coast\": \"mar\",\n    \"map_content_water_area_type_B1_riverbanks\": \"orilla del río\",\n    \"map_content_water_area_type_B1_other_water\": \"zona de agua\",\n    \"map_content_building_unnamed\": \"Edificio sin nombre\",\n    \"map_content_no_buildings\": \"No hay edificios listados para este mapa.\",\n    \"map_content_no_poi_familiar\": \"No hay lugares familiares enumerados para este mapa.\",\n    \"map_content_no_poi_daily\": \"No hay elementos esenciales diarios enumerados para este mapa.\",\n    \"map_content_no_poi_transport\": \"No hay puntos de transporte enumerados para este mapa.\",\n    \"map_content_no_poi_accessibility\": \"No hay señales de accesibilidad etiquetadas para este mapa.\",\n    \"map_content_show_more_buildings_one\": \"Mostrar 1 edificio más\",\n    \"map_content_show_more_buildings_many\": \"Mostrar __count__ más edificios\",\n    \"map_content_show_less_buildings\": \"Mostrar menos edificios\",\n    \"map_content_show_more_features_one\": \"Mostrar 1 característica más\",\n    \"map_content_show_more_features_many\": \"Mostrar __count__ más funciones\",\n    \"map_content_show_less_features\": \"Mostrar menos funciones\",\n    \"map_content_summary_show_more\": \"Mostrar más\",\n    \"map_content_summary_show_only\": \"Mostrar solo el resumen\",\n    \"map_content_height_note_raised_mm\": \"Elevado __millimeters__ mm\",\n    \"map_content_height_note_varying\": \"Elevado en distintas alturas\",\n    \"map_content_height_note_waved_surface\": \"superficie ondulada\",\n    \"map_content_height_note_text_only\": \"Solo información textual\",\n    \"map_content_show_more_pois_one\": \"Mostrar 1 lugar más\",\n    \"map_content_show_more_pois_many\": \"Mostrar __count__ más lugares\",\n    \"map_content_show_less_pois\": \"Mostrar menos lugares\",\n    \"map_content_poi_unknown_type\": \"Tipo de lugar desconocido\",\n    \"map_content_poi_unnamed\": \"__type__ sin nombre\",\n    \"map_content_poi_unnamed_many\": \"__count__ sin nombre __type__\",\n    \"map_content_poi_sparse_note\": \"Sólo unos pocos POIs están etiquetados en esta área; Los lugares faltantes pueden deberse a la cobertura de datos OpenStreetMap.\",\n    \"map_content_poi_access_crossings_signals_one\": \"__count__ lugar con cruces señalizados\",\n    \"map_content_poi_access_crossings_signals_many\": \"__count__ lugares con cruces señalizados\",\n    \"map_content_poi_access_tactile_summary\": \"Etiquetas de pavimento táctiles: __yes__ sí, __no__ no\",\n    \"map_content_poi_access_kerb_summary\": \"Etiquetas de acera: bajadas __lowered__, al ras __flush__, elevadas __raised__\",\n    \"map_content_poi_access_steps_summary\": \"Pasos etiquetados en lugares __count__ (mayor número de pasos __max__)\",\n    \"map_content_poi_access_incline_summary\": \"Etiquetas de inclinación: arriba __up__, abajo __down__\",\n    \"map_content_poi_access_wheelchair_summary\": \"Etiquetas para sillas de ruedas: sí __yes__, limitada __limited__, no __no__\",\n    \"map_content_poi_example\": \"Ejemplo: __example__\",\n    \"map_content_building_join_at\": \"en\",\n    \"map_content_building_type_from_value\": \"__value__ edificio\",\n    \"map_content_unavailable\": \"El contenido del mapa no está disponible.\",\n    \"map_content_list_and\": \"y\",\n    \"map_content_edge_phrase\": \"__edge__ borde\",\n    \"map_content_touches_edge\": \"Toca el borde __edge__\",\n    \"map_content_touches_edges\": \"Toca los bordes __edges__\",\n    \"map_content_touches_items\": \"Toca __items__\",\n    \"map_content_crosses_edge\": \"Cruza el borde __edge__\",\n    \"map_content_crosses_edges\": \"Cruza los bordes __edges__\",\n    \"map_content_crosses_items\": \"Cruces __items__\",\n    \"map_content_touches_edge_percent\": \"toca __percent__% del borde __edge__\",\n    \"map_content_touch_pos_north\": \"en el norte\",\n    \"map_content_touch_pos_south\": \"en el sur\",\n    \"map_content_touch_pos_west\": \"en el oeste\",\n    \"map_content_touch_pos_east\": \"en el este\",\n    \"map_content_touch_pos_center\": \"cerca del centro\",\n    \"map_content_touch_pos_multiple\": \"en múltiples lugares\",\n    \"map_content_edge_north\": \"norte\",\n    \"map_content_edge_south\": \"sur\",\n    \"map_content_edge_east\": \"este\",\n    \"map_content_edge_west\": \"oeste\",\n    \"map_content_dir_north\": \"norte\",\n    \"map_content_dir_northeast\": \"nordeste\",\n    \"map_content_dir_east\": \"este\",\n    \"map_content_dir_southeast\": \"sudeste\",\n    \"map_content_dir_south\": \"sur\",\n    \"map_content_dir_southwest\": \"suroeste\",\n    \"map_content_dir_west\": \"oeste\",\n    \"map_content_dir_northwest\": \"noroeste\",\n    \"map_content_corner_northwest\": \"esquina noroeste\",\n    \"map_content_corner_northeast\": \"esquina noreste\",\n    \"map_content_corner_southwest\": \"esquina suroeste\",\n    \"map_content_corner_southeast\": \"esquina sureste\",\n    \"map_content_loc_center\": \"centro\",\n    \"map_content_loc_offset_center\": \"__dir__ del centro\",\n    \"map_content_loc_part\": \"__dir__ parte\",\n    \"map_content_loc_edge\": \"__dir__ borde\",\n    \"map_content_loc_corner\": \"__corner__\",\n    \"map_content_loc_near_edge\": \"cerca del borde __dir__\",\n    \"map_content_loc_near_corner\": \"__corner__\",\n    \"map_content_loc_full_center\": \"en el centro\",\n    \"map_content_loc_full_offset_center\": \"un poco __dir__ del centro\",\n    \"map_content_loc_full_part\": \"en la parte __dir__\",\n    \"map_content_loc_full_edge\": \"cerca del borde __dir__\",\n    \"map_content_loc_full_corner\": \"en el __corner__\",\n    \"map_content_loc_full_near_edge\": \"cerca del borde __dir__\",\n    \"map_content_loc_full_near_corner\": \"en el __corner__\",\n    \"map_content_loc_endpoint_center\": \"el centro\",\n    \"map_content_loc_endpoint_part\": \"la parte __dir__\",\n    \"map_content_loc_endpoint_near_edge\": \"cerca del borde __dir__\",\n    \"map_content_loc_endpoint_corner\": \"el __corner__\",\n    \"map_content_loc_route_from_center\": \"el centro\",\n    \"map_content_loc_route_from_part\": \"la parte __dir__\",\n    \"map_content_loc_route_from_near_edge\": \"cerca del borde __dir__\",\n    \"map_content_loc_route_from_corner\": \"el __corner__\",\n    \"map_content_loc_route_to_center\": \"el centro\",\n    \"map_content_loc_route_to_part\": \"la parte __dir__\",\n    \"map_content_loc_route_to_near_edge\": \"cerca del borde __dir__\",\n    \"map_content_loc_route_to_corner\": \"el __corner__\",\n    \"map_content_region_center\": \"centro\",\n    \"map_content_region_part\": \"__dir__ parte\",\n    \"map_content_region_edge\": \"__dir__ borde\",\n    \"map_content_region_corner\": \"__corner__\",\n    \"map_content_summary_in\": \"En el __region__.\",\n    \"map_content_summary_near\": \"Cerca del __region__.\",\n    \"map_content_summary_mostly_in_lead\": \"Principalmente en el __region__\",\n    \"map_content_summary_mainly_near_lead\": \"Principalmente cerca del __region__\",\n    \"map_content_summary_mostly_extending\": \"__lead__, extendiéndose hacia __target__.\",\n    \"map_content_summary_in_and\": \"En __first__ y __second__.\",\n    \"map_content_summary_in_and_near\": \"En el __first__ y cerca del __second__.\",\n    \"map_content_summary_near_and\": \"Cerca del __first__ y __second__.\",\n    \"map_content_summary_mainly_in_and\": \"En varias zonas, principalmente en __first__ y __second__.\",\n    \"map_content_summary_mainly_in_and_near\": \"En varias zonas, principalmente en el __first__ y cerca del __second__.\",\n    \"map_content_summary_mainly_near_and\": \"En varias zonas, principalmente cerca de __first__ y __second__.\",\n    \"map_content_summary_mainly_near_and_in\": \"En varias zonas, principalmente cerca del __first__ y en el __second__.\",\n    \"map_content_summary_several_mainly_in\": \"En varias zonas, principalmente en el __region__.\",\n    \"map_content_summary_several_mainly_near\": \"En varias zonas, principalmente cerca del __region__.\",\n    \"map_content_summary_equal_clauses\": \"__first__ y __second__.\",\n    \"map_content_summary_mostly_extending_clauses\": \"Principalmente __lead__, extendiéndose hacia __target__.\",\n    \"map_content_summary_distributed_top2\": \"En varias áreas, principalmente __first__ y __second__.\",\n    \"map_content_summary_distributed_top1\": \"En varias áreas, principalmente __first__.\",\n    \"map_content_summary_three_regions_clauses\": \"Principalmente __first__, __second__ y __third__.\",\n    \"map_content_parts_one\": \"__count__ parte\",\n    \"map_content_parts_many\": \"__count__ partes\",\n    \"map_content_total_area\": \"Cubre __percent__% del mapa\",\n    \"map_content_shape_elongated\": \"Delgado\",\n    \"map_content_shape_long_thin\": \"Muy delgado\",\n    \"map_content_shape_irregular\": \"Forma irregular\",\n    \"map_content_shape_generic\": \"__shape__ forma\",\n    \"map_content_aspect_label\": \"aspecto __ratio__\",\n    \"map_content_degrees_abbrev\": \"grados\",\n    \"map_content_orientation\": \"Orientación __label__\",\n    \"map_content_orientation_label_east_west\": \"este-oeste\",\n    \"map_content_orientation_label_north_south\": \"norte-sur\",\n    \"map_content_orientation_label_northeast_southwest\": \"de noreste a suroeste\",\n    \"map_content_orientation_label_east_northeast_to_west_southwest\": \"este-noreste a oeste-suroeste\",\n    \"map_content_orientation_label_north_northeast_to_south_southwest\": \"de norte-noreste a sur-suroeste\",\n    \"map_content_coverage_label\": \"Cubre:\",\n    \"map_content_coverage_much\": \"gran parte de __segment__\",\n    \"map_content_coverage_some\": \"algunos de __segment__\",\n    \"map_content_coverage_little\": \"un poco de __segment__\",\n    \"map_content_osm_value_kindergarten\": \"kindergarten\",\n    \"map_content_osm_value_school\": \"escuela\",\n    \"map_content_osm_value_hospital\": \"hospital\",\n    \"map_content_osm_value_church\": \"iglesia\",\n    \"map_content_osm_value_university\": \"universidad\",\n    \"map_content_osm_value_college\": \"escuela superior\",\n    \"map_content_osm_value_library\": \"biblioteca\",\n    \"map_content_osm_value_gallery\": \"galería\",\n    \"map_content_osm_value_museum\": \"museo\",\n    \"map_content_osm_value_theatre\": \"teatro\",\n    \"map_content_osm_value_supermarket\": \"supermercado\",\n    \"map_content_osm_value_cafe\": \"cafetería\",\n    \"map_content_osm_value_restaurant\": \"restaurante\",\n    \"map_content_osm_value_hotel\": \"hotel\",\n    \"map_content_osm_value_parking\": \"aparcamiento\",\n    \"map_content_osm_value_bicycle_parking\": \"aparcamiento de bicicletas\",\n    \"map_content_osm_value_residential\": \"Residencial\",\n    \"map_content_osm_value_commercial\": \"comercial\",\n    \"map_content_osm_value_industrial\": \"industrial\",\n    \"map_content_osm_value_public\": \"público\",\n    \"map_content_osm_value_roof\": \"techo\",\n    \"map_content_osm_value_transportation\": \"transporte\",\n    \"map_content_osm_value_service\": \"servicio\",\n    \"map_content_osm_value_civic\": \"cívico\",\n    \"map_content_osm_value_office\": \"oficina\",\n    \"map_content_osm_value_apartments\": \"edificio de apartamentos\",\n    \"map_content_osm_value_house\": \"casa\",\n    \"map_content_osm_value_detached\": \"casa unifamiliar\",\n    \"map_content_osm_value_terrace\": \"casas adosadas\",\n    \"map_content_osm_value_semidetached_house\": \"casa adosada\",\n    \"map_content_osm_value_farm\": \"granja\",\n    \"map_content_osm_value_warehouse\": \"depósito\",\n    \"map_content_osm_value_garages\": \"garajes\",\n    \"map_content_osm_value_garage\": \"cochera\",\n    \"map_content_osm_value_retail\": \"minorista\",\n    \"map_content_osm_value_kiosk\": \"quiosco\",\n    \"map_content_osm_value_playground\": \"patio de juegos\",\n    \"map_content_osm_value_park\": \"parque\",\n    \"map_content_osm_value_zoo\": \"zoo\",\n    \"map_content_osm_value_sports_centre\": \"centro deportivo\",\n    \"map_content_osm_value_station\": \"estación\",\n    \"map_content_osm_value_rail\": \"carril\",\n    \"map_content_osm_value_platform\": \"plataforma\",\n    \"map_content_osm_value_stop_position\": \"posición de parada\",\n    \"map_content_osm_value_stop_area\": \"área de parada\",\n    \"map_content_osm_value_stop_area_group\": \"grupo de área de parada\",\n    \"map_content_osm_value_halt\": \"apeadero\",\n    \"map_content_osm_value_bus_stop\": \"parada de autobús\",\n    \"map_content_osm_value_bus_station\": \"estación de autobuses\",\n    \"map_content_osm_value_tram_stop\": \"parada de tranvía\",\n    \"map_content_osm_value_tram\": \"tranvía\",\n    \"map_content_osm_value_subway_entrance\": \"entrada del metro\",\n    \"map_content_osm_value_ferry_terminal\": \"terminal de ferry\",\n    \"map_content_osm_value_taxi\": \"taxi\",\n    \"map_content_osm_value_department_store\": \"grandes almacenes\",\n    \"map_content_osm_value_mall\": \"centro comercial\",\n    \"map_content_osm_value_attraction\": \"atracción\",\n    \"map_content_osm_value_artwork\": \"obra de arte\",\n    \"map_content_osm_value_fountain\": \"fuente\",\n    \"map_content_osm_value_viewpoint\": \"punto de vista\",\n    \"map_content_osm_value_theme_park\": \"parque temático\",\n    \"map_content_osm_value_aquarium\": \"acuario\",\n    \"map_content_osm_value_arts_centre\": \"centro de artes\",\n    \"map_content_osm_value_community_centre\": \"centro comunitario\",\n    \"map_content_osm_value_place_of_worship\": \"lugar de culto\",\n    \"map_content_osm_value_townhall\": \"Ayuntamiento\",\n    \"map_content_osm_value_courthouse\": \"palacio de justicia\",\n    \"map_content_osm_value_pharmacy\": \"farmacia\",\n    \"map_content_osm_value_clinic\": \"clínica\",\n    \"map_content_osm_value_doctors\": \"doctores\",\n    \"map_content_osm_value_toilets\": \"baños\",\n    \"map_content_osm_value_post_office\": \"oficina de correos\",\n    \"map_content_osm_value_bank\": \"banco\",\n    \"map_content_osm_value_atm\": \"ATM\",\n    \"map_content_osm_value_fuel\": \"estación de combustible\",\n    \"map_content_osm_value_convenience\": \"tienda de conveniencia\",\n    \"map_content_osm_value_fast_food\": \"Comida rápida\",\n    \"map_content_osm_value_bar\": \"bar\",\n    \"map_content_osm_value_pub\": \"pub\",\n    \"map_content_osm_value_marketplace\": \"mercado\",\n    \"map_content_osm_value_bench\": \"banco\",\n    \"map_content_osm_value_waste_basket\": \"cesto de basura\",\n    \"map_content_osm_value_vending_machine\": \"maquina expendedora\",\n    \"map_content_osm_value_street_lamp\": \"farola\",\n    \"map_content_osm_value_outdoor_seating\": \"asientos al aire libre\",\n    \"map_content_osm_value_sauna\": \"sauna\",\n    \"map_content_osm_value_shelter\": \"refugio\",\n    \"map_content_osm_value_abandoned\": \"abandonado\",\n    \"map_content_osm_value_razed\": \"arrasado\",\n    \"map_content_osm_value_bicycle\": \"bicicleta\",\n    \"map_content_osm_value_building\": \"edificio\",\n    \"map_content_osm_value_residential_building\": \"edificio residencial\",\n    \"map_content_osm_value_commercial_building\": \"edificio comercial\",\n    \"map_content_osm_value_industrial_building\": \"edificio industrial\",\n    \"map_content_osm_value_public_building\": \"edificio público\",\n    \"map_content_osm_value_garages_building\": \"edificio de garajes\",\n    \"map_content_osm_value_yes\": \"genérico\",\n    \"what_is_the_map_like__railways\": \"Ferrocarriles\",\n    \"what_is_the_map_like__water\": \"Cuerpos de agua (con superficie ondulada, pero arroyos estrechos como líneas)\",\n    \"what_is_the_map_like__north\": \"Esquina noreste marcada para indicar la orientación correcta\",\n    \"what_is_the_map_like__selected_addr\": \"En una impresión 3D, la dirección seleccionada (o las coordenadas ingresadas) está marcada con un cono\",\n    \"what_is_the_map_like__clarity\": \"Los mapas táctiles Touch Mapper están optimizados para brindar claridad y satisfacer las necesidades prácticas de las personas con discapacidad visual: los mapas impresos en 3D no son representaciones reales del área.\",\n    \"larger_scale_maps\": \"Mapas de áreas e interiores más grandes\",\n    \"larger_scale_maps_answer\": \"Touch Mapper actualmente solo admite mapas que muestran cada calle y edificio en el área. Los mapas que cubren áreas más grandes, muestran la elevación del terreno o áreas interiores podrían admitirse más adelante. Tenga en cuenta que al seleccionar \\\"Imprimir una parte de un mapa de varias partes\\\" puede cubrir un área más grande imprimiendo varios mapas. Hay mapas a escala de país producidos en masa, por ejemplo en __aphLinkStart__American Printing House for the Blind Shop__aphLinkEnd__.\",\n    \"data_comes_from_osm\": \"Fuente de datos del mapa\",\n    \"data_comes\": \"Touch Mapper usa datos __startTag__OpenStreetMap__endTag__. Aunque suele ser una fuente excelente, no se puede garantizar la precisión de los mapas. El problema más común es la falta de edificios. Si faltan funciones importantes, puede agregarlas libremente a OpenStreetMap. Lamentablemente, las fuentes de datos de mapas comerciales como Google Maps no se pueden utilizar porque no permiten el tipo de acceso requerido a sus datos.\",\n    \"commercial_use\": \"Uso comercial\",\n    \"commercial_use_answer\": \"Le invitamos a utilizar Touch Mapper con fines comerciales, como imprimir y vender los mapas. Sólo te pido que dejes claro a tus clientes que los mapas proceden de www.touch-mapper.org, tanto en la publicidad como en el packaging. También espero que me avises si empiezas a ofrecer un servicio basado en Touch Mapper.\",\n    \"users_and_purposes\": \"Usos y beneficios\",\n    \"tactile_maps_have_many_different_user_groups\": \"Los mapas táctiles pueden beneficiar a muchos grupos de usuarios diferentes.\",\n    \"visually_impaired_individuals\": \"Personas ciegas o deficientes visuales: mejor orientación y planificación de rutas\",\n    \"outdoors_tourist_attractions\": \"Atracciones turísticas al aire libre: mejor servicio para clientes ciegos o deficientes visuales\",\n    \"public_areas_such_as_parks\": \"Áreas públicas como parques y áreas de juego: los mapas táctiles pueden ayudar a los visitantes ciegos o deficientes visuales a disfrutar del parque de forma más independiente.\",\n    \"orientation_and_mobility_specialists\": \"Especialistas en orientación y movilidad: para ayudar a los clientes a construir un mapa mental de una zona más fácilmente\",\n    \"multipart_maps_heading\": \"Imprimir mapas en varias partes\",\n    \"multipart_maps\": \"Si un solo mapa no es lo suficientemente grande para cubrir el área requerida, por ejemplo, un camino largo, puede hacer varias impresiones y colocarlas una al lado de la otra sobre una superficie plana. Seleccione \\\"Opciones avanzadas\\\" y luego \\\"Imprimir una parte de un mapa multiparte\\\". En este modo, no habrá bordes alrededor de los bordes de una impresión 3D. Además, aparecen botones de flecha para ajustar el área seleccionada al 100% (es decir, el diámetro del mapa) en cada dirección, lo que facilita la selección de áreas adyacentes para los mapas.\",\n    \"how_to_3d_print\": \"Impresión 3D\",\n    \"map_can_be_printed\": \"El mapa puede imprimirse con casi cualquier impresora 3D, incluidas las de aficionado asequibles. Lo más importante a tener en cuenta son las carreteras. Algunos servicios pueden considerarlas demasiado estrechas para imprimir.\",\n    \"if_you_print_yourself\": \"Si imprime con su propia impresora 3D, utilice un espesor de capa de 0,25 mm o 0,3 mm. La base tiene un grosor de 0,6 mm y basta con imprimirla en dos capas. Bastan dos capas superiores y una capa horizontal. Es posible que desees imprimir la capa superior con bastante lentitud para obtener superficies lisas. Si utiliza la cortadora Simplify3D, deberá invocar \\\"Separar superficies conectadas\\\" para obtener una impresión correcta. El tiempo de impresión de un mapa de 17 cm con la base impresa en dos capas es de unas 3 a 4 horas.\",\n    \"about_touch_mapper\": \"Acerca de Touch Mapper\",\n    \"about_touch_mapper_answer\": \"Mi nombre es Sofía Pahaoja. Creé Touch Mapper después de conocer los desafíos que enfrentan las personas ciegas o con visión parcial cuando viajan de forma independiente y de darme cuenta de cómo la impresión 3D combinada con datos de mapas disponibles gratuitamente puede ayudar.\",\n    \"news_from\": \"La información sobre nuevas funciones y eventos relevantes se publica en __linkFB__ y __linkTwitter__.\",\n    \"want_to_improve_please\": \"Quiero que Touch Mapper sea lo mejor posible y los comentarios de los usuarios son clave para ello. Si tiene alguna pregunta o sugerencia de mejora, por favor\",\n    \"email_me\": \"Envíame un correo electrónico\",\n    \"address_search_by\": \"Búsqueda de dirección __linkTag__.\",\n    \"map_data_by\": \"Datos del mapa por __linkTag__.\",\n    \"touch_mapper_is_os\": \"Touch Mapper es un software de código abierto.\",\n    \"see_github\": \"Utilice __startTag__GitHub__endTag__ para informes de errores y sugerencias de funciones.\",\n    \"map_content_osm_value_dormitory\": \"dormitorio\",\n    \"map_content_osm_value_motorway\": \"autopista\",\n    \"map_content_osm_value_trunk\": \"carretera troncal\",\n    \"map_content_osm_value_primary\": \"camino principal\",\n    \"map_content_osm_value_secondary\": \"camino secundario\",\n    \"map_content_osm_value_tertiary\": \"camino terciario\",\n    \"map_content_osm_value_unclassified\": \"camino no clasificado\",\n    \"map_content_osm_value_living_street\": \"calle viva\",\n    \"map_content_osm_value_pedestrian\": \"calle peatonal\",\n    \"map_content_osm_value_footway\": \"senda\",\n    \"map_content_osm_value_path\": \"camino\",\n    \"map_content_osm_value_cycleway\": \"carril bici\",\n    \"map_content_osm_value_steps\": \"pasos\",\n    \"map_content_osm_value_track\": \"pista\",\n    \"map_content_osm_value_road\": \"camino\",\n    \"map_content_osm_value_dentist\": \"dentista\",\n    \"map_content_osm_value_police\": \"policía\",\n    \"map_content_osm_value_fire_station\": \"estación de bomberos\",\n    \"map_content_osm_value_charging_station\": \"estación de carga\",\n    \"map_content_osm_value_hostel\": \"albergue\",\n    \"map_content_osm_value_information\": \"información turística\",\n    \"map_content_osm_value_bakery\": \"panadería\",\n    \"map_content_osm_value_clothes\": \"tienda de ropa\",\n    \"map_content_osm_value_apartments_building\": \"edificio de apartamentos\",\n    \"map_content_osm_value_house_building\": \"casa\",\n    \"map_content_osm_value_detached_building\": \"casa unifamiliar\",\n    \"map_content_osm_value_terrace_building\": \"casas adosadas\",\n    \"map_content_osm_value_dormitory_building\": \"dormitorio\",\n    \"map_content_osm_value_retail_building\": \"edificio comercial\",\n    \"map_content_osm_value_office_building\": \"edificio de oficinas\",\n    \"map_content_osm_value_warehouse_building\": \"depósito\",\n    \"map_content_osm_value_school_building\": \"edificio escolar\",\n    \"map_content_osm_value_university_building\": \"edificio universitario\",\n    \"map_content_osm_value_hospital_building\": \"edificio hospitalario\",\n    \"map_content_osm_value_church_building\": \"iglesia\",\n    \"map_content_osm_value_garage_building\": \"cochera\",\n    \"map_content_osm_value_roof_building\": \"techo\"\n}\n"
  },
  {
    "path": "web/locales/fi/tm.json",
    "content": "{\n    \"location2_top_left\": \"ylävasen neljännes\",\n    \"location2_top_row\": \"yläpuolisko\",\n    \"location2_top_right\": \"yläoikea neljännes\",\n    \"location2_right_column\": \"oikea puolisko\",\n    \"location2_bottom_right\": \"alaoikea neljännes\",\n    \"location2_bottom_row\": \"alapuolisko\",\n    \"location2_bottom_left\": \"alavasen neljännes\",\n    \"location2_left_column\": \"vasen puolisko\",\n    \"location3_middle_center\": \"keskellä\",\n    \"location3_top_center\": \"yläkeskellä\",\n    \"location3_bottom_center\": \"alhaalla keskellä\",\n    \"location3_middle_left\": \"vasemmalla keskellä\",\n    \"location3_middle_right\": \"oikealla keskellä\",\n    \"location3_top_left\": \"ylävasen kulma\",\n    \"location3_top_row\": \"yläreuna\",\n    \"location3_mostly_top_row\": \"enimmäkseen yläreunalla\",\n    \"location3_top_left_to_mc\": \"ylävasemmasta kulmasta keskelle\",\n    \"location3_top_left_and_center\": \"yläreuna vasemmalla\",\n    \"location3_top_right_and_center\": \"yläreuna oikealla\",\n    \"location3_top_right\": \"yläoikea kulma\",\n    \"location3_right_column\": \"oikea reuna\",\n    \"location3_mostly_right_column\": \"enimmäkseen oikealla reunalla\",\n    \"location3_top_right_to_mc\": \"keskeltä yläoikeaan kulmaan\",\n    \"location3_top_right_and_middle\": \"oikea reuna ylhäällä\",\n    \"location3_bottom_right_and_middle\": \"keskeltä alaoikeaan kulmaan\",\n    \"location3_bottom_right\": \"alaoikea kulma\",\n    \"location3_bottom_row\": \"alareuna\",\n    \"location3_mostly_bottom_row\": \"enimmäkseen alareunalla\",\n    \"location3_bottom_right_to_mc\": \"keskeltä alaoikeaan kulmaan\",\n    \"location3_bottom_right_and_center\": \"alareuna oikealla\",\n    \"location3_bottom_left_and_center\": \"alareuna vasemmalla\",\n    \"location3_bottom_left\": \"alavasen kulma\",\n    \"location3_left_column\": \"vasen reuna\",\n    \"location3_mostly_left_column\": \"enimmäkseen vasemmalla reunalla\",\n    \"location3_bottom_left_to_mc\": \"keskeltä alavasempaan kulmaan\",\n    \"location3_bottom_left_and_middle\": \"vasen reuna alhaalla\",\n    \"location3_top_left_and_middle\": \"vasen reuna ylhäällä\",\n    \"location3_middle_row\": \"keskellä vasemmalta oikealle\",\n    \"location3_center_column\": \"keskellä ylhäältä alas\",\n    \"location3_top_left_diagonal\": \"diagonaali ylävasemmalta alaoikeaan\",\n    \"location3_top_right_diagonal\": \"diagonaali alavasemmalta yläoikeaan\",\n    \"location3_top_left_to_middle_right\": \"ylävasemmasta kulmasta oikealle keskelle\",\n    \"location3_top_right_to_middle_left\": \"vasemmalta keskeltä yläoikeaan kulmaan\",\n    \"location3_top_right_to_bottom_center\": \"alhaalta keskeltä yläoikeaan kulmaan\",\n    \"location3_bottom_right_to_top_center\": \"ylhäältä keskeltä alaoikeaan kulmaan\",\n    \"location3_bottom_right_to_middle_left\": \"vasemmalta keskeltä alaoikeaan kulmaan\",\n    \"location3_bottom_left_to_middle_right\": \"alavasemmasta kulmasta oikealle keskelle\",\n    \"location3_bottom_left_to_top_center\": \"alavasemmasta kulmasta yläkeskelle\",\n    \"location3_top_left_to_bottom_center\": \"ylävasemmasta kulmasta alakeskelle\",\n    \"location3_near_top_left\": \"lähellä ylävasenta kulmaa\",\n    \"location3_top_center_to_middle_left\": \"vasemmalta keskeltä yläkeskelle\",\n    \"location3_near_top_right\": \"lähellä yläoikeaa kulmaa\",\n    \"location3_top_center_to_middle_right\": \"yläkeskeltä oikealle keskelle\",\n    \"location3_near_bottom_right\": \"lähellä alaoikeaa kulmaa\",\n    \"location3_middle_right_to_bottom_center\": \"oikealta keskeltä alakeskelle\",\n    \"location3_near_bottom_left\": \"lähellä alavasenta kulmaa\",\n    \"location3_middle_left_to_bottom_center\": \"vasemmalta keskeltä alakeskelle\",\n    \"location_general\": \"useita alueita\",\n    \"skip_to_main_content\": \"Hyppää pääsisältöön\",\n    \"meta_description\": \"Kohokarttoja helposti mihin tahansa osoitteeseen. Tulosta itse 3D-tulostimella tai tilaa.\",\n    \"meta_keywords\": \"kohokartta,sokea,näkövamma,näkövammaisten apuväline,sokeiden apuväline,näkövammaisten teknologia,3D-tulostus\",\n    \"meta_og_site_name\": \"Touch Mapper - Kohokarttoja näkövammaisille\",\n    \"title\": \"Kohokarttoja helposti\",\n    \"blurb\": \"Kohokarttoja helposti mistä tahansa osoitteesta\",\n    \"language\": \"Kieli\",\n    \"help\": \"Ohje\",\n    \"open_help_aria\": \"Avaa ohje uudessa välilehdessä\",\n    \"map_content_external_link_aria\": \"Ulkoinen linkki, avautuu uudessa välilehdessä\",\n    \"breadcrumb_nav_title\": \"Sijaintisi\",\n    \"breadcrumb_home\": \"Osoitehaku\",\n    \"breadcrumb_area\": \"Asetukset\",\n    \"breadcrumb_map\": \"Kartta\",\n    \"intro\": \"Kohokartoista on näkövammaisille suuri apu ympäristön hahmottamisessa ja reittien suunnittelemisessa. Touch Mapper tekee ulkotilojen kohokarttojen hankkimisen mistä tahansa haluamastasi osoitteesta erittäin helpoksi. Voit joko tulostaa kartan itse 3D-tulostimella ilmaiseksi, tai tilata 3D-tulosteen (hinnat lähtien noin 29€ 20x20 cm kartasta). Myös pistekirjoittimet ja kuohupaperitulostimet ovat tuettuja.\",\n    \"it_is_simple\": \"Se on yksinkertaista\",\n    \"simple__enter_address\": \"Syötä osoite ja klikkaa \\\"Etsi\\\"\",\n    \"simple__click_create_tactile_map\": \"Klikkaa \\\"Luo kohokartta\\\"\",\n    \"simple__intro_order_or_print\": \"Tulosta tai tilaa kartta\",\n    \"address\": \"Osoite\",\n    \"street_address\": \"Katuosoite\",\n    \"search\": \"Etsi\",\n    \"searching\": \"Etsitään...\",\n    \"nothing_found\": \"Yhtään osoitetta ei löytynyt\",\n    \"map_parameters\": \"Kartan asetukset\",\n    \"show_all_addresses\": \"Näytä lisää\",\n    \"show_all_addresses_aria\": \"Näytä lisää löytyneitä osoitteita\",\n    \"map_printing\": \"Kartan tulostus\",\n    \"map_printing__order\": \"Tilaa 3D-tuloste\",\n    \"map_printing__self\": \"Tulosta kartta itse\",\n    \"printing_tech\": \"Tulostustekniikka\",\n    \"printing_tech__3d\": \"3D-tulostus\",\n    \"printing_tech__2d\": \"Pistekirjoitin tai kuohupaperi\",\n    \"map_size\": \"Tulosteen koko\",\n    \"can_print_any_size\": \"Voit tulostaa missä vain koossa, mutta mittakaavan valinta olettaa kooksi 27,9 cm / 11 tuumaa.\",\n    \"size_order_17\": \"17 cm kanttiinsa (mahtuu lähes kaikkiin 3D-tulostimiin)\",\n    \"size_order_20\": \"20 cm kanttiinsa (mahtuu useimpiin 3D-tulostimiin)\",\n    \"size_self_8\": \"8 cm kanttiinsa (hyvä testitulosteeksi)\",\n    \"size_self_17\": \"17 cm kanttiinsa (mahtuu lähes kaikkiin 3D-tulostimiin)\",\n    \"size_self_20\": \"20 cm kanttiinsa (mahtuu useimpiin 3D-tulostimiin)\",\n    \"map_scale\": \"Mittakaava\",\n    \"map_scale__1000\": \"yksittäiset rakennukset tai kohteet\",\n    \"map_scale__1800\": \"tiheästi rakennetut kaupungit\",\n    \"map_scale__2400\": \"oletus\",\n    \"map_scale__3200\": \"lähiöt\",\n    \"map_scale__5600\": \"hyvin harvaan rakennetut alueet\",\n    \"map_scale__9999\": \"vain isoja teitä sisältävät alueet\",\n    \"map_scale_coverage\": \"Tällä mittakaavalla __size__ leveä kartta kattaa __area_metric__ / __area_imperial__.\",\n    \"map_content\": \"Sisältö\",\n    \"content_mode_normal\": \"Normaali\",\n    \"content_mode_no_buildings\": \"Ei rakennuksia\",\n    \"content_mode_only_big_roads\": \"Vain isot tiet, rautatiet ja vesialueet\",\n    \"target_road_density_ui\": \"Teiden määrä\",\n    \"target_road_density_ui_hint\": \"Suurempi luku tarkoittaa enemmän teitä, oletus on 10\",\n    \"location_marker\": \"Sijaintimerkki\",\n    \"hide_location_marker\": \"Piilota sijaintimerkki\",\n    \"advanced_settings\": \"Lisävalinnat\",\n    \"show_advanced_settings\": \"Näytä lisävalinta-asetukset\",\n    \"coordinates\": \"Koordinaatit\",\n    \"longitude_x\": \"Pituusaste (X)\",\n    \"latitude_y\": \"Leveysaste (Y)\",\n    \"area_adjustment\": \"Alueen säätö\",\n    \"x_offset\": \"X-suunta\",\n    \"positive_is_right\": \"positiivinen on oikealle\",\n    \"y_offset\": \"Y-suunta\",\n    \"positive_is_up\": \"positiivinen ylöspäin\",\n    \"custom_map_size\": \"Muokattu koko\",\n    \"unit_cm\": \"cm\",\n    \"custom_scale\": \"Muokattu mittakaava\",\n    \"multipart_map\": \"Moniosainen kartta\",\n    \"multipart_map_label\": \"Tulosta osa moniosaisesta kartasta (piilottaa sijaintimerkin)\",\n    \"adjust_area_for_multipart\": \"Säädä kartan sijaintia\",\n    \"move_map_area_left\": \"Siirrä aluetta vasemmalle __amount__ prosenttia\",\n    \"move_map_area_up\": \"Siirrä aluetta ylös __amount__ prosenttia\",\n    \"move_map_area_down\": \"Siirrä aluetta alas __amount__ prosenttia\",\n    \"move_map_area_right\": \"Siirrä aluetta oikealle __amount__ prosenttia\",\n    \"multipart_current_adjustment\": \"Valittu\",\n    \"multipart_current_adjustment_aria\": \"Tämänhetkinen kartan sijainnin säätö\",\n    \"create_tactile_map\": \"Luo kohokartta\",\n    \"progress__connecting\": \"Yhdistetään palvelimeen...\",\n    \"progress__reading_osm\": \"20% luotu...\",\n    \"progress__converting\": \"60% luotu...\",\n    \"progress__uploading\": \"80% ladattu...\",\n    \"map_for_address\": \"Kartta osoitteesta\",\n    \"content__heading\": \"Sisältö\",\n    \"content__road\": \"Tiet\",\n    \"content__bus_stop\": \"Bussipysäkit\",\n    \"content__shop\": \"Kaupat\",\n    \"content__restaurant\": \"Ravintolat\",\n    \"content__no_roads\": \"Ei teitä\",\n    \"content__unnamed_roads\": \"Nimeämättömät tiet\",\n    \"warning_no_buildings\": \"Varoitus: OpenStreetMapissa saattaa olla puutteellista dataa tältä alueelta. Lisätietoja ohjesivulla.\",\n    \"download\": \"Lataa tulostettava STL-tiedosto\",\n    \"other_downloads\": \"Muut lataukset\",\n    \"show_more_downloads\": \"Näytä lisää latauksia\",\n    \"download_map_descriptions\": \"Kartan kuvaukset\",\n    \"download_stl_ways\": \"STL: Vain tiet\",\n    \"download_stl_rest\": \"STL: Kaikki paitsi tiet\",\n    \"download_pdf_embossers\": \"PDF\",\n    \"download_svg_embossers\": \"SVG\",\n    \"download_blender\": \"Alkuperäinen Blender-tiedosto\",\n    \"order_from_1\": \"Tilaa yritykseltä __companyName__.\",\n    \"order_from_2\": \"20x20 cm kartta 29€, tai 99€ viisi kpl. Kysy muista vaihtoehdoista.\",\n    \"open_or_download_svg\": \"Avaa SVG-tiedosto (pistekirjoittimille), tai klikkaa hiiren oikealla napilla tallentaaksesi\",\n    \"open_or_download_pdf\": \"Avaa PDF-tiedosto (kuohupaperitulostimille), tai klikkaa hiiren oikealla napilla tallentaaksesi\",\n    \"emboss_ordering_later\": \"Tilausmahdollisuus tulossa myöhemmin.\",\n    \"osm_attribution\": \"Karttadata © __startTag__OpenStreetMapin osallistujat__endTag__. Jos teet tuotteen Touch Mapperilla, sinun on mainittava OpenStreetMap datalähteenä.\",\n    \"email_map_to\": \"Mailaa karttalinkki osoitteeseen\",\n    \"send_submit\": \"Lähetä\",\n    \"send_email\": \"Lähetä sähköposti\",\n    \"email_sent\": \"Sähköposti lähetetty\",\n    \"error_sending_email\": \"Virhe sähköpostin lähetyksessä\",\n    \"conversion_error_unknown\": \"Kartan luonti epäonnistui.\",\n    \"conversion_error_too_large\": \"Kartta on liian täysi käsiteltäväksi.\",\n    \"drag_map\": \"Raahaa karttaa sopivaan kohtaan\",\n    \"map_diameter\": \"Kartan halkaisija\",\n    \"label_3d_preview\": \"Kohokartan 3D-esikatselu\",\n    \"map_preview_controls_hint\": \"Voit zoomata, kiertää ja siirtää karttaa\",\n    \"tooltip_selected_address\": \"Valittu osoite\",\n    \"meters\": \"metriä\",\n    \"yards\": \"jaardia\",\n    \"data_no_longer_available\": \"Tämän kartan välimuistissa olevat tiedostot ovat vanhentuneet. Palaa kartan asetuksiin, ja klikkaa \\\"Luo kohokartta\\\" luodaksesi uuden samanlaisen kartan.\",\n    \"back_to_index\": \"Palaa osoitehakuun\",\n    \"back_to_area\": \"Palaa kartan asetuksiin\",\n    \"help_has_more_info\": \"Lisää tietoa Touch Mapperista __startTag__ohjesivulla__endTag__\",\n    \"close_tab_to_exit_help\": \"Sulje välilehti poistuaksesi ohjesivulta\",\n    \"what_is_the_map_like\": \"Karttojen sisältö\",\n    \"what_is_the_map_like__map_content\": \"Kartasta löytyy seuraavaa:\",\n    \"what_is_the_map_like__roads\": \"Kaikenkokoiset tiet. Kävelytiet ovat enemmän koholla, koska ne ovat sokealle tärkeämpiä.\",\n    \"what_is_the_map_like__buildings\": \"Rakennukset\",\n    \"map_content_unnamed_roads_one\": \"__count__ nimetön tie\",\n    \"map_content_unnamed_roads_many\": \"__count__ nimetöntä tietä\",\n    \"map_content_heading\": \"Kartan sisältö\",\n    \"map_content_subheading_ways\": \"Väylät\",\n    \"map_content_subheading_roads\": \"Tiet\",\n    \"map_content_subheading_paths\": \"Polut\",\n    \"map_content_subheading_railways\": \"Rautatiet\",\n    \"map_content_subheading_waterways\": \"Vesiväylät\",\n    \"map_content_subheading_water_areas\": \"Vesialueet\",\n    \"map_content_subheading_other_linear\": \"Muut lineaariset kohteet\",\n    \"map_content_subheading_buildings\": \"Rakennukset\",\n    \"map_content_subheading_poi_familiar\": \"Tutut paikat\",\n    \"map_content_subheading_poi_daily\": \"Arjen palvelut\",\n    \"map_content_subheading_poi_transport\": \"Liikennekohteet\",\n    \"map_content_subheading_poi_accessibility\": \"Esteettömyysvihjeet\",\n    \"map_content_way_unnamed\": \"Nimetön tie\",\n    \"map_content_way_unnamed_waterway\": \"Nimetön vesiväylä\",\n    \"map_content_no_ways\": \"Kartalle ei löytynyt teitä.\",\n    \"map_content_no_roads\": \"Kartalle ei löytynyt teitä.\",\n    \"map_content_no_paths\": \"Kartalle ei löytynyt polkuja.\",\n    \"map_content_no_railways\": \"Kartalle ei löytynyt rautateitä.\",\n    \"map_content_no_waterways\": \"Kartalle ei löytynyt vesiväyliä.\",\n    \"map_content_no_water_areas\": \"Kartalle ei löytynyt vesialueita.\",\n    \"map_content_no_other_linear\": \"Kartalle ei löytynyt muita lineaarisia kohteita.\",\n    \"map_content_unnamed_features_one\": \"__count__ nimetön __type__\",\n    \"map_content_unnamed_features_many\": \"__count__ nimetöntä __type__\",\n    \"map_content_way_route_from_to\": \"Alkaa __start__ ja päättyy __end__\",\n    \"map_content_way_route_near\": \"__location__\",\n    \"map_content_connects_to_way\": \"Yhdistyy väylään __way__\",\n    \"map_content_connects_to_ways\": \"Yhdistyy väyliin __ways__\",\n    \"map_content_connects_to_type\": \"Yhdistyy väylään __type__\",\n    \"map_content_connects_to_type_many\": \"Yhdistyy __count__ __type__\",\n    \"map_content_connects_to_type_many_fallback\": \"Yhdistyy __count__ väylään, joiden tyyppi on __type__\",\n    \"map_content_way_junction_one\": \"1 risteys\",\n    \"map_content_way_junction_many\": \"__count__ risteystä\",\n    \"map_content_way_length_m\": \"__meters__ metriä\",\n    \"map_content_way_lanes_many\": \"__count__ kaistaa\",\n    \"map_content_way_surface_paved\": \"Päällystetty pinta\",\n    \"map_content_way_surface_unpaved\": \"Päällystämätön pinta\",\n    \"map_content_way_surface_unknown\": \"Päällystettä ei tiedossa\",\n    \"map_content_way_meta\": \"__type__, __length__\",\n    \"map_content_way_meta_type_only\": \"__type__\",\n    \"map_content_way_meta_length_only\": \"__length__\",\n    \"map_content_way_type_A1_road_construction\": \"rakenteilla oleva tie\",\n    \"map_content_way_type_A1_major_roads\": \"pääkatu\",\n    \"map_content_way_type_A1_secondary_roads\": \"seututie\",\n    \"map_content_way_type_A1_local_streets\": \"paikalliskatu\",\n    \"map_content_way_type_A1_service_roads\": \"huoltotie\",\n    \"map_content_way_type_plural_A1_service_roads\": \"huoltotietä\",\n    \"map_content_way_type_A1_track_roads\": \"metsätie\",\n    \"map_content_way_type_A1_vehicle_unspecified\": \"muu ajoneuvotie\",\n    \"map_content_way_type_A2_pedestrian_streets\": \"kävelykatu\",\n    \"map_content_way_type_A2_footpaths_trails\": \"jalankulkureitti\",\n    \"map_content_way_type_A2_cycleways\": \"pyörätie\",\n    \"map_content_way_type_A2_steps_ramps\": \"portaat / ramppi\",\n    \"map_content_way_type_A2_pedestrian_unspecified\": \"muu jalankulkureitti\",\n    \"map_content_way_type_A3_rail_lines\": \"rautatie\",\n    \"map_content_way_type_A3_tram_light_rail\": \"raitiotie / pikaraitiotie\",\n    \"map_content_way_type_A3_subway_metro\": \"metro\",\n    \"map_content_way_type_A3_rail_yards_sidings\": \"ratapiha / sivuraide\",\n    \"map_content_way_type_A4_rivers\": \"joki\",\n    \"map_content_way_type_A4_streams_canals\": \"puro / kanava\",\n    \"map_content_way_type_A4_ditches_drains\": \"oja\",\n    \"map_content_way_type_A4_other_waterways\": \"muu vesiväylä\",\n    \"map_content_way_type_plural_A4_other_waterways\": \"muut vesiväylät\",\n    \"map_content_way_type_A_other_ways\": \"muu tie\",\n    \"map_content_water_area_unnamed\": \"Nimetön __type__\",\n    \"map_content_water_area_type_generic\": \"vesialue\",\n    \"map_content_water_area_type_generic_plural\": \"vesialuetta\",\n    \"map_content_water_area_type_B1_lakes\": \"järvi\",\n    \"map_content_water_area_type_B1_ponds\": \"lampi\",\n    \"map_content_water_area_type_B1_reservoirs\": \"tekojärvi\",\n    \"map_content_water_area_type_B1_sea_coast\": \"meri\",\n    \"map_content_water_area_type_B1_riverbanks\": \"jokialue\",\n    \"map_content_water_area_type_B1_other_water\": \"vesialue\",\n    \"map_content_building_unnamed\": \"Nimetön rakennus\",\n    \"map_content_no_buildings\": \"Kartalle ei löytynyt rakennuksia.\",\n    \"map_content_no_poi_familiar\": \"Kartalle ei löytynyt tuttuja paikkoja.\",\n    \"map_content_no_poi_daily\": \"Kartalle ei löytynyt arjen palveluita.\",\n    \"map_content_no_poi_transport\": \"Kartalle ei löytynyt liikennekohteita.\",\n    \"map_content_no_poi_accessibility\": \"Kartalle ei löytynyt esteettömyysvihjeitä.\",\n    \"map_content_show_more_buildings_one\": \"Näytä vielä 1 rakennus\",\n    \"map_content_show_more_buildings_many\": \"Näytä vielä __count__ rakennusta\",\n    \"map_content_show_less_buildings\": \"Näytä vähemmän rakennuksia\",\n    \"map_content_show_more_features_one\": \"Näytä vielä 1 kohde\",\n    \"map_content_show_more_features_many\": \"Näytä vielä __count__ kohdetta\",\n    \"map_content_show_less_features\": \"Näytä vähemmän kohteita\",\n    \"map_content_summary_show_more\": \"Näytä lisää\",\n    \"map_content_summary_show_only\": \"Näytä vain yhteenveto\",\n    \"map_content_height_note_raised_mm\": \"Koholla __millimeters__ milliä\",\n    \"map_content_height_note_varying\": \"Koholla vaihtelevasti\",\n    \"map_content_height_note_waved_surface\": \"Aallotettu pinta\",\n    \"map_content_height_note_text_only\": \"Vain tekstimuotoista tietoa\",\n    \"map_content_show_more_pois_one\": \"Näytä vielä 1 paikka\",\n    \"map_content_show_more_pois_many\": \"Näytä vielä __count__ paikkaa\",\n    \"map_content_show_less_pois\": \"Näytä vähemmän paikkoja\",\n    \"map_content_poi_unknown_type\": \"Tuntematon paikkatyyppi\",\n    \"map_content_poi_unnamed\": \"Nimetön __type__\",\n    \"map_content_poi_unnamed_many\": \"__count__ nimetöntä __type__\",\n    \"map_content_poi_sparse_note\": \"Tällä alueella on merkitty vain vähän POI-kohteita; puuttuvat paikat voivat johtua OpenStreetMapin tietojen kattavuudesta.\",\n    \"map_content_poi_access_crossings_signals_one\": \"__count__ paikka, jossa on liikennevalo-ohjattu ylitys\",\n    \"map_content_poi_access_crossings_signals_many\": \"__count__ paikkaa, joissa on liikennevalo-ohjattuja ylityksiä\",\n    \"map_content_poi_access_tactile_summary\": \"Taktiiliset merkinnät: __yes__ kyllä, __no__ ei\",\n    \"map_content_poi_access_kerb_summary\": \"Reunakivi-tagit: madallettu __lowered__, tasainen __flush__, korotettu __raised__\",\n    \"map_content_poi_access_steps_summary\": \"Portaita merkitty __count__ paikkaan (suurin askelmäärä __max__)\",\n    \"map_content_poi_access_incline_summary\": \"Kaltevuus-tagit: ylöspäin __up__, alaspäin __down__\",\n    \"map_content_poi_access_wheelchair_summary\": \"Pyörätuoli-tagit: kyllä __yes__, rajoitettu __limited__, ei __no__\",\n    \"map_content_poi_example\": \"Esimerkki: __example__\",\n    \"map_content_building_join_at\": \"osoitteessa\",\n    \"map_content_building_type_from_value\": \"__value__rakennus\",\n    \"map_content_unavailable\": \"Kartan sisältöä ei ole saatavilla.\",\n    \"map_content_list_and\": \"ja\",\n    \"map_content_edge_phrase\": \"__edge__reunaa\",\n    \"map_content_touches_edge\": \"Koskettaa __edge__reunaa\",\n    \"map_content_touches_edges\": \"Koskettaa __edges__reunoja\",\n    \"map_content_touches_items\": \"Koskettaa __items__\",\n    \"map_content_crosses_edge\": \"Ylittää __edge__reunan\",\n    \"map_content_crosses_edges\": \"Ylittää __edges__reunat\",\n    \"map_content_crosses_items\": \"Ylittää __items__\",\n    \"map_content_touches_edge_percent\": \"koskettaa __percent__ % __edge__reunasta\",\n    \"map_content_touch_pos_north\": \"pohjoisosassa\",\n    \"map_content_touch_pos_south\": \"eteläosassa\",\n    \"map_content_touch_pos_west\": \"länsiosassa\",\n    \"map_content_touch_pos_east\": \"itäosassa\",\n    \"map_content_touch_pos_center\": \"lähellä keskustaa\",\n    \"map_content_touch_pos_multiple\": \"useassa kohdassa\",\n    \"map_content_edge_north\": \"pohjois\",\n    \"map_content_edge_south\": \"etelä\",\n    \"map_content_edge_east\": \"itä\",\n    \"map_content_edge_west\": \"länsi\",\n    \"map_content_dir_north\": \"pohjois\",\n    \"map_content_dir_northeast\": \"koillis\",\n    \"map_content_dir_east\": \"itä\",\n    \"map_content_dir_southeast\": \"kaakkois\",\n    \"map_content_dir_south\": \"etelä\",\n    \"map_content_dir_southwest\": \"lounais\",\n    \"map_content_dir_west\": \"länsi\",\n    \"map_content_dir_northwest\": \"luoteis\",\n    \"map_content_corner_northwest\": \"luoteiskulma\",\n    \"map_content_corner_northeast\": \"koilliskulma\",\n    \"map_content_corner_southwest\": \"lounaiskulma\",\n    \"map_content_corner_southeast\": \"kaakkoiskulma\",\n    \"map_content_loc_center\": \"keskustaa\",\n    \"map_content_loc_offset_center\": \"__dir__ keskustasta\",\n    \"map_content_loc_part\": \"__dir__osaa\",\n    \"map_content_loc_edge\": \"__dir__reunaa\",\n    \"map_content_loc_corner\": \"__corner__a\",\n    \"map_content_loc_near_edge\": \"__dir__reunan lähellä\",\n    \"map_content_loc_near_corner\": \"__corner__\",\n    \"map_content_loc_full_center\": \"keskellä\",\n    \"map_content_loc_full_offset_center\": \"hieman __dir__ keskustasta\",\n    \"map_content_loc_full_part\": \"__dir__osassa\",\n    \"map_content_loc_full_edge\": \"__dir__reunan lähellä\",\n    \"map_content_loc_full_corner\": \"__corner__ssa\",\n    \"map_content_loc_full_near_edge\": \"__dir__reunan lähellä\",\n    \"map_content_loc_full_near_corner\": \"__corner__ssa\",\n    \"map_content_loc_endpoint_center\": \"keskustassa\",\n    \"map_content_loc_endpoint_part\": \"__dir__osassa\",\n    \"map_content_loc_endpoint_near_edge\": \"__dir__reunan lähellä\",\n    \"map_content_loc_endpoint_corner\": \"__corner__ssa\",\n    \"map_content_loc_route_from_center\": \"keskustasta\",\n    \"map_content_loc_route_from_part\": \"__dir__osasta\",\n    \"map_content_loc_route_from_near_edge\": \"__dir__reunan läheltä\",\n    \"map_content_loc_route_from_corner\": \"__corner__sta\",\n    \"map_content_loc_route_to_center\": \"keskustaan\",\n    \"map_content_loc_route_to_part\": \"__dir__osaan\",\n    \"map_content_loc_route_to_near_edge\": \"__dir__reunan lähelle\",\n    \"map_content_loc_route_to_corner\": \"__corner__an\",\n    \"map_content_region_center\": \"keskusta\",\n    \"map_content_region_part\": \"__dir__osa\",\n    \"map_content_region_edge\": \"__dir__reuna\",\n    \"map_content_region_corner\": \"__corner__\",\n    \"map_content_summary_in\": \"Alueella __region__.\",\n    \"map_content_summary_near\": \"Lähellä kohdetta __region__.\",\n    \"map_content_summary_mostly_in_lead\": \"Enimmäkseen alueella __region__\",\n    \"map_content_summary_mainly_near_lead\": \"Enimmäkseen lähellä kohdetta __region__\",\n    \"map_content_summary_mostly_extending\": \"__lead__, ulottuen kohti __target__.\",\n    \"map_content_summary_in_and\": \"Alueilla __first__ ja __second__.\",\n    \"map_content_summary_in_and_near\": \"Alueella __first__ ja lähellä kohdetta __second__.\",\n    \"map_content_summary_near_and\": \"Lähellä kohteita __first__ ja __second__.\",\n    \"map_content_summary_mainly_in_and\": \"Usealla alueella, enimmäkseen __first__ ja __second__.\",\n    \"map_content_summary_mainly_in_and_near\": \"Usealla alueella, enimmäkseen __first__ ja lähellä kohdetta __second__.\",\n    \"map_content_summary_mainly_near_and\": \"Usealla alueella, enimmäkseen lähellä kohteita __first__ ja __second__.\",\n    \"map_content_summary_mainly_near_and_in\": \"Usealla alueella, enimmäkseen lähellä kohdetta __first__ ja alueella __second__.\",\n    \"map_content_summary_several_mainly_in\": \"Usealla alueella, enimmäkseen alueella __region__.\",\n    \"map_content_summary_several_mainly_near\": \"Usealla alueella, enimmäkseen lähellä kohdetta __region__.\",\n    \"map_content_summary_equal_clauses\": \"__first__ ja __second__.\",\n    \"map_content_summary_mostly_extending_clauses\": \"Enimmäkseen __lead__, ulottuen kohti __target__.\",\n    \"map_content_summary_distributed_top2\": \"Usealla alueella, enimmäkseen __first__ ja __second__.\",\n    \"map_content_summary_distributed_top1\": \"Usealla alueella, enimmäkseen __first__.\",\n    \"map_content_summary_three_regions_clauses\": \"Usealla alueella, enimmäkseen __first__, __second__ ja __third__.\",\n    \"map_content_parts_one\": \"__count__ osa\",\n    \"map_content_parts_many\": \"__count__ osaa\",\n    \"map_content_total_area\": \"Kattaa __percent__ % kartasta\",\n    \"map_content_shape_elongated\": \"Pitkänomainen\",\n    \"map_content_shape_long_thin\": \"Erittäin kapea\",\n    \"map_content_shape_irregular\": \"Epäsäännöllinen muoto\",\n    \"map_content_shape_generic\": \"__shape__ muoto\",\n    \"map_content_aspect_label\": \"kuvasuhde __ratio__\",\n    \"map_content_degrees_abbrev\": \"ast.\",\n    \"map_content_orientation\": \"Suunta __label__\",\n    \"map_content_orientation_label_east_west\": \"itä-länsi\",\n    \"map_content_orientation_label_north_south\": \"pohjois-etelä\",\n    \"map_content_orientation_label_northeast_southwest\": \"koillis-lounais\",\n    \"map_content_orientation_label_east_northeast_to_west_southwest\": \"itä-koillis - länsi-lounais\",\n    \"map_content_orientation_label_north_northeast_to_south_southwest\": \"pohjois-koillis - etelä-lounais\",\n    \"map_content_coverage_label\": \"Kattaa:\",\n    \"map_content_coverage_much\": \"suuri osa alueesta __segment__\",\n    \"map_content_coverage_some\": \"osa alueesta __segment__\",\n    \"map_content_coverage_little\": \"vähän alueesta __segment__\",\n    \"map_content_osm_value_kindergarten\": \"päiväkoti\",\n    \"map_content_osm_value_school\": \"koulu\",\n    \"map_content_osm_value_hospital\": \"sairaala\",\n    \"map_content_osm_value_church\": \"kirkko\",\n    \"map_content_osm_value_university\": \"yliopisto\",\n    \"map_content_osm_value_college\": \"korkeakoulu\",\n    \"map_content_osm_value_library\": \"kirjasto\",\n    \"map_content_osm_value_gallery\": \"galleria\",\n    \"map_content_osm_value_museum\": \"museo\",\n    \"map_content_osm_value_theatre\": \"teatteri\",\n    \"map_content_osm_value_supermarket\": \"supermarket\",\n    \"map_content_osm_value_cafe\": \"kahvila\",\n    \"map_content_osm_value_restaurant\": \"ravintola\",\n    \"map_content_osm_value_hotel\": \"hotelli\",\n    \"map_content_osm_value_parking\": \"pysäköinti\",\n    \"map_content_osm_value_bicycle_parking\": \"pyöräpysäköinti\",\n    \"map_content_osm_value_residential\": \"asuin\",\n    \"map_content_osm_value_commercial\": \"liike\",\n    \"map_content_osm_value_industrial\": \"teollinen\",\n    \"map_content_osm_value_public\": \"julkinen\",\n    \"map_content_osm_value_roof\": \"katos\",\n    \"map_content_osm_value_transportation\": \"liikenne\",\n    \"map_content_osm_value_service\": \"palvelu\",\n    \"map_content_osm_value_civic\": \"kunnallinen\",\n    \"map_content_osm_value_office\": \"toimisto\",\n    \"map_content_osm_value_apartments\": \"kerrostalo\",\n    \"map_content_osm_value_house\": \"talo\",\n    \"map_content_osm_value_detached\": \"omakotitalo\",\n    \"map_content_osm_value_terrace\": \"rivitalo\",\n    \"map_content_osm_value_semidetached_house\": \"paritalo\",\n    \"map_content_osm_value_farm\": \"maatila\",\n    \"map_content_osm_value_warehouse\": \"varasto\",\n    \"map_content_osm_value_garages\": \"autotalli\",\n    \"map_content_osm_value_garage\": \"autotalli\",\n    \"map_content_osm_value_retail\": \"myymälä\",\n    \"map_content_osm_value_kiosk\": \"kioski\",\n    \"map_content_osm_value_playground\": \"leikkipuisto\",\n    \"map_content_osm_value_park\": \"puisto\",\n    \"map_content_osm_value_zoo\": \"eläintarha\",\n    \"map_content_osm_value_sports_centre\": \"liikuntakeskus\",\n    \"map_content_osm_value_station\": \"asema\",\n    \"map_content_osm_value_rail\": \"raide\",\n    \"map_content_osm_value_platform\": \"laituri\",\n    \"map_content_osm_value_stop_position\": \"pysähtymispaikka\",\n    \"map_content_osm_value_stop_area\": \"pysäkkialue\",\n    \"map_content_osm_value_stop_area_group\": \"pysäkkialueiden ryhmä\",\n    \"map_content_osm_value_halt\": \"seisake\",\n    \"map_content_osm_value_bus_stop\": \"bussipysäkki\",\n    \"map_content_osm_value_bus_station\": \"linja-autoasema\",\n    \"map_content_osm_value_tram_stop\": \"raitiovaunupysäkki\",\n    \"map_content_osm_value_tram\": \"raitiovaunu\",\n    \"map_content_osm_value_subway_entrance\": \"metron sisäänkäynti\",\n    \"map_content_osm_value_ferry_terminal\": \"lauttaterminaali\",\n    \"map_content_osm_value_taxi\": \"taksi\",\n    \"map_content_osm_value_department_store\": \"tavaratalo\",\n    \"map_content_osm_value_mall\": \"ostoskeskus\",\n    \"map_content_osm_value_attraction\": \"nähtävyys\",\n    \"map_content_osm_value_artwork\": \"taideteos\",\n    \"map_content_osm_value_fountain\": \"suihkulähde\",\n    \"map_content_osm_value_viewpoint\": \"näköalapaikka\",\n    \"map_content_osm_value_theme_park\": \"huvipuisto\",\n    \"map_content_osm_value_aquarium\": \"akvaario\",\n    \"map_content_osm_value_arts_centre\": \"taidekeskus\",\n    \"map_content_osm_value_community_centre\": \"yhteisökeskus\",\n    \"map_content_osm_value_place_of_worship\": \"uskonnollinen paikka\",\n    \"map_content_osm_value_townhall\": \"kaupungintalo\",\n    \"map_content_osm_value_courthouse\": \"oikeustalo\",\n    \"map_content_osm_value_pharmacy\": \"apteekki\",\n    \"map_content_osm_value_clinic\": \"klinikka\",\n    \"map_content_osm_value_doctors\": \"lääkäri\",\n    \"map_content_osm_value_toilets\": \"WC\",\n    \"map_content_osm_value_post_office\": \"posti\",\n    \"map_content_osm_value_bank\": \"pankki\",\n    \"map_content_osm_value_atm\": \"pankkiautomaatti\",\n    \"map_content_osm_value_fuel\": \"huoltoasema\",\n    \"map_content_osm_value_convenience\": \"kioski/lähikauppa\",\n    \"map_content_osm_value_fast_food\": \"pikaruoka\",\n    \"map_content_osm_value_bar\": \"baari\",\n    \"map_content_osm_value_pub\": \"pubi\",\n    \"map_content_osm_value_marketplace\": \"tori\",\n    \"map_content_osm_value_bench\": \"penkki\",\n    \"map_content_osm_value_waste_basket\": \"roskakori\",\n    \"map_content_osm_value_vending_machine\": \"myyntiautomaatti\",\n    \"map_content_osm_value_street_lamp\": \"katuvalaisin\",\n    \"map_content_osm_value_outdoor_seating\": \"ulkoterassi\",\n    \"map_content_osm_value_sauna\": \"sauna\",\n    \"map_content_osm_value_shelter\": \"katos\",\n    \"map_content_osm_value_abandoned\": \"hylätty\",\n    \"map_content_osm_value_razed\": \"purettu\",\n    \"map_content_osm_value_bicycle\": \"polkupyörä\",\n    \"map_content_osm_value_building\": \"rakennus\",\n    \"map_content_osm_value_residential_building\": \"asuinrakennus\",\n    \"map_content_osm_value_commercial_building\": \"liikerakennus\",\n    \"map_content_osm_value_industrial_building\": \"teollisuusrakennus\",\n    \"map_content_osm_value_public_building\": \"julkinen rakennus\",\n    \"map_content_osm_value_garages_building\": \"autotallirakennus\",\n    \"map_content_osm_value_yes\": \"yleinen\",\n    \"what_is_the_map_like__railways\": \"Raiteet\",\n    \"what_is_the_map_like__water\": \"Vesialueet (aaltoileva pinta, kapeat joet viivoina)\",\n    \"what_is_the_map_like__north\": \"Koillisnurkassa on merkki, jotta kartan saa asetettua oikein päin\",\n    \"what_is_the_map_like__selected_addr\": \"3D-tulosteessa valittu osoite (tai syötetyt koordinaatit) on merkitty kartiolla\",\n    \"what_is_the_map_like__clarity\": \"Touch Mapperin kohokartat on optimoitu näkövammaisten tarpeisiin järkeviksi ja selkeiksi, ei niinkään realistisiksi miniatyyreiksi.\",\n    \"larger_scale_maps\": \"Suuremman alueen ja sisätilojen kartat\",\n    \"larger_scale_maps_answer\": \"Tällä hetkellä Touch Mapper tukee vain karttoja, jotka näyttävät alueen kaikki tiet ja rakennukset. Kartat, jotka kattavat paljon suuremman alueen, näyttävät korkeuseroja tai sisätiloja, voisivat olla tuettuja myöhemmin. Huomaa, että valinnalla \\\"Tulosta osa moniosaisesta kartasta\\\" voit kattaa suuremman alueen tulostamalla useita karttoja. Maatasolla on myös massatuotettuja karttoja, esimerkiksi __aphLinkStart__American Printing House for the Blind Shop__aphLinkEnd__.\",\n    \"data_comes_from_osm\": \"Karttadatan lähde\",\n    \"data_comes\": \"Touch Mapper käyttää __startTag__OpenStreetMap__endTag__-dataa. Vaikka se on yleensä erinomainen lähde, karttojen tarkkuutta ei voida taata. Yleisin ongelma ovat puuttuvat rakennukset. Jos tärkeitä kohteita puuttuu, voit lisätä ne vapaasti OpenStreetMapiin. Valitettavasti kaupallisia karttalähteitä kuten Google Mapsia ei voida käyttää, koska ne eivät salli vaadittua pääsyä dataan.\",\n    \"commercial_use\": \"Kaupallinen käyttö\",\n    \"commercial_use_answer\": \"Voit käyttää Touch Mapperia vapaasti kaupallisiin tarkoituksiin, kuten karttojen tulostaminen ja myynti. Edellytän vain, että kerrot asiakkaillesi selkeästi että kartat tulevat osoitteesta www.touch-mapper.org. Toivon myös että kerrot minulle jos rupeat tarjoamaan tällaista palvelua.\",\n    \"users_and_purposes\": \"Miksi tulostaa kohokarttoja\",\n    \"tactile_maps_have_many_different_user_groups\": \"Kohokartoilla on paljon erilaisia käyttäjäryhmiä ja käyttötarkoituksia\",\n    \"visually_impaired_individuals\": \"Yksittäiset näkövammaiset, helpottamaan orientoitumista ja reittien suunnittelua\",\n    \"outdoors_tourist_attractions\": \"Ulkotiloissa olevat turistikohteet parantaakseen näkövammaisten asiakkaidensa palvelua\",\n    \"public_areas_such_as_parks\": \"Julkiset alueet, kuten puistot ja leikkipuistot: kohokartat voivat auttaa näkövammaisia vierailijoita nauttimaan puistosta itsenäisemmin.\",\n    \"orientation_and_mobility_specialists\": \"Liikkumistaidonohjaajat parantaakseen asiakkaidensa ymmärrystä ympäristöstään\",\n    \"multipart_maps_heading\": \"Karttojen tulostus useassa osassa\",\n    \"multipart_maps\": \"Jos yksittäinen kartta ei ole tarpeeksi suuri kattaakseen halutun alueen, esimerkiksi pitkän tien, voit tehdä useita tulosteita ja asettaa ne vierekkäin tasaiselle pinnalle. Valitse \\\"Näytä lisävalinta-asetukset\\\" ja sitten \\\"Tulosta osa moniosaisesta kartasta\\\". Tässä tilassa 3D-tulosteessa ei ole reunoja. Lisäksi näkyviin tulee nuolipainikkeita, joilla voit siirtää valittua aluetta 100 % (eli kartan halkaisijan) verran jokaiseen suuntaan, mikä helpottaa vierekkäisten alueiden valintaa.\",\n    \"how_to_3d_print\": \"3D-tulostus\",\n    \"map_can_be_printed\": \"Kartan voi tulostaa lähes millä tahansa 3D-tulostimella, myös edullisilla harrastelaitteilla. Tärkeintä on kiinnittää huomiota teihin: jotkin palvelut voivat pitää niitä liian kapeina tulostettaviksi.\",\n    \"if_you_print_yourself\": \"Jos tulostat omalla 3D-tulostimella, käytä kerrospaksuutta 0,25 mm tai 0,3 mm. Pohja on 0,6 mm paksu, ja sen tulostaminen kahdessa kerroksessa riittää. Kaksi pintakerrosta ja yksi vaakasuuntainen kuori riittävät. Voit haluta tulostaa ylimmän kerroksen melko hitaasti, jotta pinnat ovat sileitä. Jos käytät Simplify3D-slicer-ohjelmaa, sinun täytyy ottaa käyttöön \\\"Separate Connected Surfaces\\\", jotta tuloste onnistuu. 17 cm kartan tulostus, kun pohja on kahdessa kerroksessa, kestää noin 3–4 tuntia.\",\n    \"about_touch_mapper\": \"Tietoa Touch Mapperista\",\n    \"about_touch_mapper_answer\": \"Nimeni on Sofia Pahaoja. Tein Touch Mapperin opittuani näkövammaisten haasteista itsenäisessä liikkumisessä, ja tajuttuani kuinka 3D tulostus yhdistettynä vapaaseen karttadataan voi parantaa tilannetta.\",\n    \"news_from\": \"Tietoa uusista ominaisuuksista: __linkFB__, __linkTwitter__.\",\n    \"want_to_improve_please\": \"Haluan tehdä Touch Mapperista niin hyvän kuin mahdollista ja käyttäjäpalaute on siinä avainasemassa. Jos sinulla on kysyttävää tai kehitysehdotuksia,\",\n    \"email_me\": \"Lähetä minulle sähköpostia\",\n    \"address_search_by\": \"Osoitehaku __linkTag__.\",\n    \"map_data_by\": \"Karttadata __linkTag__.\",\n    \"touch_mapper_is_os\": \"Touch Mapper on vapaa ohjelmisto.\",\n    \"see_github\": \"Käytä __startTag__GitHub__endTag__-sivua bugiraportteihin ja ominaisuusehdotuksiin.\",\n    \"map_content_osm_value_dormitory\": \"asuntola\",\n    \"map_content_osm_value_motorway\": \"moottoritie\",\n    \"map_content_osm_value_trunk\": \"valtatie\",\n    \"map_content_osm_value_primary\": \"päätie\",\n    \"map_content_osm_value_secondary\": \"seututie\",\n    \"map_content_osm_value_tertiary\": \"yhdystie\",\n    \"map_content_osm_value_unclassified\": \"luokittelematon tie\",\n    \"map_content_osm_value_living_street\": \"pihakatu\",\n    \"map_content_osm_value_pedestrian\": \"kävelykatu\",\n    \"map_content_osm_value_footway\": \"jalkakäytävä/polku\",\n    \"map_content_osm_value_path\": \"polku\",\n    \"map_content_osm_value_cycleway\": \"pyörätie\",\n    \"map_content_osm_value_steps\": \"portaat\",\n    \"map_content_osm_value_track\": \"ura\",\n    \"map_content_osm_value_road\": \"tie\",\n    \"map_content_osm_value_dentist\": \"hammaslääkäri\",\n    \"map_content_osm_value_police\": \"poliisi\",\n    \"map_content_osm_value_fire_station\": \"paloasema\",\n    \"map_content_osm_value_charging_station\": \"latausasema\",\n    \"map_content_osm_value_hostel\": \"hostelli\",\n    \"map_content_osm_value_information\": \"matkailuneuvonta\",\n    \"map_content_osm_value_bakery\": \"leipomo\",\n    \"map_content_osm_value_clothes\": \"vaatekauppa\",\n    \"map_content_osm_value_apartments_building\": \"kerrostalo\",\n    \"map_content_osm_value_house_building\": \"talo\",\n    \"map_content_osm_value_detached_building\": \"omakotitalo\",\n    \"map_content_osm_value_terrace_building\": \"rivitalo\",\n    \"map_content_osm_value_dormitory_building\": \"asuntola\",\n    \"map_content_osm_value_retail_building\": \"myymälärakennus\",\n    \"map_content_osm_value_office_building\": \"toimistorakennus\",\n    \"map_content_osm_value_warehouse_building\": \"varasto\",\n    \"map_content_osm_value_school_building\": \"koulurakennus\",\n    \"map_content_osm_value_university_building\": \"yliopistorakennus\",\n    \"map_content_osm_value_hospital_building\": \"sairaalarakennus\",\n    \"map_content_osm_value_church_building\": \"kirkko\",\n    \"map_content_osm_value_garage_building\": \"autotalli\",\n    \"map_content_osm_value_roof_building\": \"katos\"\n}\n"
  },
  {
    "path": "web/locales/nl/tm.json",
    "content": "{\n    \"location2_top_left\": \"linksboven kwart\",\n    \"location2_top_row\": \"bovenste helft\",\n    \"location2_top_right\": \"rechtsboven kwart\",\n    \"location2_right_column\": \"rechter helft\",\n    \"location2_bottom_right\": \"rechtsonder kwart\",\n    \"location2_bottom_row\": \"onderste helft\",\n    \"location2_bottom_left\": \"linksonder kwart\",\n    \"location2_left_column\": \"linker helft\",\n    \"location3_middle_center\": \"midden\",\n    \"location3_top_center\": \"boven midden\",\n    \"location3_bottom_center\": \"onder midden\",\n    \"location3_middle_left\": \"midden links\",\n    \"location3_middle_right\": \"midden rechts\",\n    \"location3_top_left\": \"linkerbovenhoek\",\n    \"location3_top_row\": \"bovenrand\",\n    \"location3_mostly_top_row\": \"voornamelijk bovenrand\",\n    \"location3_top_left_to_mc\": \"van linkerbovenhoek naar midden\",\n    \"location3_top_left_and_center\": \"bovenrand links\",\n    \"location3_top_right_and_center\": \"bovenrand rechts\",\n    \"location3_top_right\": \"rechterbovenhoek\",\n    \"location3_right_column\": \"rechterrand\",\n    \"location3_mostly_right_column\": \"voornamelijk rechterrand\",\n    \"location3_top_right_to_mc\": \"van midden naar rechterbovenhoek\",\n    \"location3_top_right_and_middle\": \"rechterrand boven\",\n    \"location3_bottom_right_and_middle\": \"van midden naar rechteronderhoek\",\n    \"location3_bottom_right\": \"rechteronderhoek\",\n    \"location3_bottom_row\": \"onderrand\",\n    \"location3_mostly_bottom_row\": \"voornamelijk onderrand\",\n    \"location3_bottom_right_to_mc\": \"van midden naar rechteronderhoek\",\n    \"location3_bottom_right_and_center\": \"onderrand rechts\",\n    \"location3_bottom_left_and_center\": \"onderrand links\",\n    \"location3_bottom_left\": \"linksonderhoek\",\n    \"location3_left_column\": \"linkerrand\",\n    \"location3_mostly_left_column\": \"voornamelijk linkerrand\",\n    \"location3_bottom_left_to_mc\": \"van midden naar linkeronderhoek\",\n    \"location3_bottom_left_and_middle\": \"linkerrand onder\",\n    \"location3_top_left_and_middle\": \"linkerrand boven\",\n    \"location3_middle_row\": \"van links naar rechts in het midden\",\n    \"location3_center_column\": \"van boven naar onder in het midden\",\n    \"location3_top_left_diagonal\": \"diagonaal van linkerbovenhoek naar rechteronderhoek\",\n    \"location3_top_right_diagonal\": \"diagonaal van linkeronderhoek naar rechterbovenhoek\",\n    \"location3_top_left_to_middle_right\": \"van linkerbovenhoek naar midden rechts\",\n    \"location3_top_right_to_middle_left\": \"van midden links naar rechterbovenhoek\",\n    \"location3_top_right_to_bottom_center\": \"van onder midden naar rechterbovenhoek\",\n    \"location3_bottom_right_to_top_center\": \"van boven midden naar rechteronderhoek\",\n    \"location3_bottom_right_to_middle_left\": \"van midden links naar rechteronderhoek\",\n    \"location3_bottom_left_to_middle_right\": \"van linkeronderhoek naar midden rechts\",\n    \"location3_bottom_left_to_top_center\": \"van linkeronderhoek naar boven midden\",\n    \"location3_top_left_to_bottom_center\": \"van linkerbovenhoek naar onder midden\",\n    \"location3_near_top_left\": \"dicht bij linkerbovenhoek\",\n    \"location3_top_center_to_middle_left\": \"van midden links naar boven midden\",\n    \"location3_near_top_right\": \"dicht bij rechterbovenhoek\",\n    \"location3_top_center_to_middle_right\": \"van boven midden naar midden rechts\",\n    \"location3_near_bottom_right\": \"dicht bij rechteronderhoek\",\n    \"location3_middle_right_to_bottom_center\": \"van midden rechts naar onder midden\",\n    \"location3_near_bottom_left\": \"dicht bij linkeronderhoek\",\n    \"location3_middle_left_to_bottom_center\": \"van midden links naar onder midden\",\n    \"location_general\": \"veel gebieden\",\n    \"skip_to_main_content\": \"Spring naar de hoofdinhoud\",\n    \"meta_description\": \"Maak gemakkelijk tactiele kaarten voor ieder adres. 3D-print ze zelf of bestel.\",\n    \"meta_keywords\": \"tactiele kaart,blind,visuele beperking,hulpmiddel,hulpmiddel voor blinden,technologie voor blinden,technologie voor slechtzienden,3d-printen\",\n    \"meta_og_site_name\": \"Touch Mapper - Tactiele Kaarten voor blinden en slechtzienden\",\n    \"title\": \"Gemakkelijk Tactiele Kaarten Maken\",\n    \"blurb\": \"Maak gemakkelijk tactiele kaarten voor ieder adres\",\n    \"language\": \"Taal\",\n    \"help\": \"Help\",\n    \"open_help_aria\": \"Open help in een nieuw tabblad\",\n    \"map_content_external_link_aria\": \"Externe link, opent in een nieuw tabblad\",\n    \"breadcrumb_nav_title\": \"Kruimelpad\",\n    \"breadcrumb_home\": \"Adres zoeken\",\n    \"breadcrumb_area\": \"Instellingen\",\n    \"breadcrumb_map\": \"Kaart\",\n    \"intro\": \"Tactiele kaarten zijn een geweldig hulpmiddel voor blinden en slechtzienden om zich te oriënteren en routes te plannen. Met Touch Mapper kunt u eenvoudig aangepaste buitenkaarten maken voor elk gewenst adres. U kunt de kaart gratis zelf printen met een 3D-printer, of een 3D-print bestellen vanaf 29 euro/USD. Embossers en swell-paperprinters worden ook ondersteund.\",\n    \"it_is_simple\": \"Het is eenvoudig\",\n    \"simple__enter_address\": \"Voer een adres in en klik op \\\"Zoeken\\\"\",\n    \"simple__click_create_tactile_map\": \"Klik op \\\"Tactiele kaart maken\\\"\",\n    \"simple__intro_order_or_print\": \"Print of bestel de kaart\",\n    \"address\": \"Adres\",\n    \"street_address\": \"Adres\",\n    \"search\": \"Zoeken\",\n    \"searching\": \"Bezig met zoeken...\",\n    \"nothing_found\": \"Niets gevonden\",\n    \"map_parameters\": \"Kaartparameters\",\n    \"show_all_addresses\": \"Toon meer\",\n    \"show_all_addresses_aria\": \"Toon meer gevonden adressen\",\n    \"map_printing\": \"Kaart printen\",\n    \"map_printing__order\": \"Bestel een 3D-print\",\n    \"map_printing__self\": \"Print de kaart zelf\",\n    \"printing_tech\": \"Printtechnologie\",\n    \"printing_tech__3d\": \"3D-printen\",\n    \"printing_tech__2d\": \"Reliëfdruk of zwelpapier\",\n    \"map_size\": \"Printformaat\",\n    \"can_print_any_size\": \"U kunt printen op ieder formaat, maar de schaalselectie hieronder gaat uit van 27,9 cm / 11 inch.\",\n    \"size_order_17\": \"17 cm / 6,7 inch overdwars (past in bijna alle 3D-printers)\",\n    \"size_order_20\": \"20 cm / 7,9 inch overdwars (past in de meeste 3D-printers)\",\n    \"size_self_8\": \"8 cm / 3,1 inch overdwars (goed voor testprints)\",\n    \"size_self_17\": \"17 cm / 6,7 inch overdwars (past in bijna alle 3D-printers)\",\n    \"size_self_20\": \"20 cm / 7,9 inch overdwars (past in de meeste 3D-printers)\",\n    \"map_scale\": \"Kaartschaal\",\n    \"map_scale__1000\": \"losse gebouwen of bezienswaardigheden\",\n    \"map_scale__1800\": \"dichtbebouwde steden\",\n    \"map_scale__2400\": \"standaard\",\n    \"map_scale__3200\": \"gemiddelde buitenwijk\",\n    \"map_scale__5600\": \"zeer dunbebouwde gebieden\",\n    \"map_scale__9999\": \"gebieden met alleen grote wegen\",\n    \"map_scale_coverage\": \"Op deze schaal beslaat een kaart van __size__ breed een gebied van __area_metric__ / __area_imperial__.\",\n    \"map_content\": \"Inhoud\",\n    \"content_mode_normal\": \"Normaal\",\n    \"content_mode_no_buildings\": \"Geen gebouwen\",\n    \"content_mode_only_big_roads\": \"Alleen grote wegen, spoorwegen en watergebieden\",\n    \"target_road_density_ui\": \"Wegbehoud\",\n    \"target_road_density_ui_hint\": \"Hoger getal betekent meer wegen, standaard is 10\",\n    \"location_marker\": \"Locatiemarkering\",\n    \"hide_location_marker\": \"Locatiemarkering verbergen\",\n    \"advanced_settings\": \"Geavanceerd\",\n    \"show_advanced_settings\": \"Toon geavanceerde opties\",\n    \"coordinates\": \"Coördinaten\",\n    \"longitude_x\": \"Lengtegraad (X)\",\n    \"latitude_y\": \"Breedtegraad (Y)\",\n    \"area_adjustment\": \"Locatie aanpassen\",\n    \"x_offset\": \"X verschuiving\",\n    \"positive_is_right\": \"positief is naar rechts\",\n    \"y_offset\": \"Y verschuiving\",\n    \"positive_is_up\": \"positief is naar boven\",\n    \"custom_map_size\": \"Aangepast printformaat\",\n    \"unit_cm\": \"cm\",\n    \"custom_scale\": \"Aangepaste schaal\",\n    \"multipart_map\": \"Meerdelige kaart\",\n    \"multipart_map_label\": \"Print een deel van een meerdelige kaart (verbergt locatiemarkering)\",\n    \"adjust_area_for_multipart\": \"Locatie van het kaartdeel aanpassen\",\n    \"move_map_area_left\": \"Verplaats het kaartgebied naar links met __amount__ procent\",\n    \"move_map_area_up\": \"Verplaats het kaartgebied naar boven met __amount__ procent\",\n    \"move_map_area_down\": \"Verplaats het kaartgebied naar onder met __amount__ procent\",\n    \"move_map_area_right\": \"Verplaats het kaartgebied naar rechts met __amount__ procent\",\n    \"multipart_current_adjustment\": \"Momenteel\",\n    \"multipart_current_adjustment_aria\": \"Huidige aanpassing van de locatie van het kaartdeel\",\n    \"create_tactile_map\": \"Tactiele kaart maken\",\n    \"progress__connecting\": \"Bezig met verbinden...\",\n    \"progress__reading_osm\": \"20% gemaakt...\",\n    \"progress__converting\": \"60% gemaakt...\",\n    \"progress__uploading\": \"80% geüpload...\",\n    \"map_for_address\": \"Kaart voor\",\n    \"content__heading\": \"Inhoud\",\n    \"content__road\": \"Wegen\",\n    \"content__bus_stop\": \"Bushaltes\",\n    \"content__shop\": \"Winkels\",\n    \"content__restaurant\": \"Restaurants\",\n    \"content__no_roads\": \"Geen wegen\",\n    \"content__unnamed_roads\": \"Naamloze wegen\",\n    \"warning_no_buildings\": \"Waarschuwing: Het kan zijn dat OpenStreetMap incomplete data heeft voor dit gebied. Zie help voor meer informatie.\",\n    \"download\": \"Download printbaar STL-bestand\",\n    \"other_downloads\": \"Andere downloads\",\n    \"show_more_downloads\": \"Meer downloads tonen\",\n    \"download_map_descriptions\": \"Kaartbeschrijvingen\",\n    \"download_stl_ways\": \"STL: Alleen wegen\",\n    \"download_stl_rest\": \"STL: Alles behalve wegen\",\n    \"download_pdf_embossers\": \"PDF\",\n    \"download_svg_embossers\": \"SVG\",\n    \"download_blender\": \"Origineel Blender-bestand\",\n    \"order_from_1\": \"Bestel bij __companyName__.\",\n    \"order_from_2\": \"29€ voor een kaart van 20 cm / 6,9 inch, 99€ voor 5. Vraag naar andere opties.\",\n    \"open_or_download_svg\": \"Open SVG-bestand (voor brailleprinters), of klik met de rechter muisknop om te downloaden\",\n    \"open_or_download_pdf\": \"Open PDF-bestand (voor swell form machines), of klik met de rechter muisknop om te downloaden\",\n    \"emboss_ordering_later\": \"Een besteloptie voor kaarten als brailleprint of swell paper volgt later.\",\n    \"osm_attribution\": \"Kaartgegevens © __startTag__OpenStreetMap-bijdragers__endTag__. Als u een product maakt met Touch Mapper, moet u OpenStreetMap als gegevensbron vermelden.\",\n    \"email_map_to\": \"E-mail een link naar de kaart naar\",\n    \"send_submit\": \"Verzenden\",\n    \"send_email\": \"E-mail verzenden\",\n    \"email_sent\": \"De e-mail is verzonden\",\n    \"error_sending_email\": \"Fout bij het verzenden van de e-mail\",\n    \"conversion_error_unknown\": \"Maken van de kaart is mislukt.\",\n    \"conversion_error_too_large\": \"De kaart is te dicht om te verwerken.\",\n    \"drag_map\": \"Sleep de kaart om de locatie aan te passen\",\n    \"map_diameter\": \"Kaartdiameter\",\n    \"label_3d_preview\": \"3D-preview van de tactiele kaart\",\n    \"map_preview_controls_hint\": \"U kunt inzoomen, roteren en de kaart verschuiven\",\n    \"tooltip_selected_address\": \"Geselecteerd adres\",\n    \"meters\": \"meter\",\n    \"yards\": \"yard\",\n    \"data_no_longer_available\": \"De opgeslagen bestanden van deze kaart zijn verlopen. Ga terug naar kaartparameters en klik op \\\"Tactiele kaart maken\\\" om een vergelijkbare kaart te maken.\",\n    \"back_to_index\": \"Terug naar adres zoeken\",\n    \"back_to_area\": \"Terug naar kaart parameters\",\n    \"help_has_more_info\": \"Meer informatie over Touch Mapper op de __startTag__help pagina__endTag__\",\n    \"close_tab_to_exit_help\": \"Sluit dit tabblad om help te verlaten\",\n    \"what_is_the_map_like\": \"Inhoud van de kaart\",\n    \"what_is_the_map_like__map_content\": \"De kaart bevat het volgende:\",\n    \"what_is_the_map_like__roads\": \"Wegen van elke grootte. Voetgangerswegen zijn meer verhoogd dan andere, omdat deze belangrijker zijn voor blinden.\",\n    \"what_is_the_map_like__buildings\": \"Gebouwen\",\n    \"map_content_unnamed_roads_one\": \"__count__ naamloze weg\",\n    \"map_content_unnamed_roads_many\": \"__count__ naamloze wegen\",\n    \"map_content_heading\": \"Kaartinhoud\",\n    \"map_content_subheading_ways\": \"Wegen\",\n    \"map_content_subheading_roads\": \"Wegen\",\n    \"map_content_subheading_paths\": \"Paden\",\n    \"map_content_subheading_railways\": \"Spoorwegen\",\n    \"map_content_subheading_waterways\": \"Waterwegen\",\n    \"map_content_subheading_water_areas\": \"Watergebieden\",\n    \"map_content_subheading_other_linear\": \"Andere lineaire objecten\",\n    \"map_content_subheading_buildings\": \"Gebouwen\",\n    \"map_content_subheading_poi_familiar\": \"Bekende plekken\",\n    \"map_content_subheading_poi_daily\": \"Dagelijkse voorzieningen\",\n    \"map_content_subheading_poi_transport\": \"Vervoerspunten\",\n    \"map_content_subheading_poi_accessibility\": \"Toegankelijkheidsaanwijzingen\",\n    \"map_content_way_unnamed\": \"Naamloze weg\",\n    \"map_content_way_unnamed_waterway\": \"Naamloze waterweg\",\n    \"map_content_no_ways\": \"Geen wegen gevonden voor deze kaart.\",\n    \"map_content_no_roads\": \"Geen wegen gevonden voor deze kaart.\",\n    \"map_content_no_paths\": \"Geen paden gevonden voor deze kaart.\",\n    \"map_content_no_railways\": \"Geen spoorwegen gevonden voor deze kaart.\",\n    \"map_content_no_waterways\": \"Geen waterwegen gevonden voor deze kaart.\",\n    \"map_content_no_water_areas\": \"Geen watergebieden gevonden voor deze kaart.\",\n    \"map_content_no_other_linear\": \"Geen andere lineaire objecten gevonden voor deze kaart.\",\n    \"map_content_unnamed_features_one\": \"__count__ naamloze __type__\",\n    \"map_content_unnamed_features_many\": \"__count__ naamloze __type__\",\n    \"map_content_way_route_from_to\": \"Van __start__ naar __end__\",\n    \"map_content_way_route_near\": \"__location__\",\n    \"map_content_connects_to_way\": \"Sluit aan op weg __way__\",\n    \"map_content_connects_to_ways\": \"Sluit aan op wegen __ways__\",\n    \"map_content_connects_to_type\": \"Sluit aan op __type__\",\n    \"map_content_connects_to_type_many\": \"Sluit aan op __count__ __type__\",\n    \"map_content_connects_to_type_many_fallback\": \"Sluit aan op __count__ wegen van het type __type__\",\n    \"map_content_way_junction_one\": \"1 kruising\",\n    \"map_content_way_junction_many\": \"__count__ kruisingen\",\n    \"map_content_way_length_m\": \"__meters__ meter\",\n    \"map_content_way_lanes_many\": \"__count__ rijstroken\",\n    \"map_content_way_surface_paved\": \"Verhard oppervlak\",\n    \"map_content_way_surface_unpaved\": \"Onverhard oppervlak\",\n    \"map_content_way_surface_unknown\": \"Verharding onbekend\",\n    \"map_content_way_meta\": \"__type__, __length__\",\n    \"map_content_way_meta_type_only\": \"__type__\",\n    \"map_content_way_meta_length_only\": \"__length__\",\n    \"map_content_way_type_A1_road_construction\": \"weg in aanbouw\",\n    \"map_content_way_type_A1_major_roads\": \"hoofdweg\",\n    \"map_content_way_type_A1_secondary_roads\": \"secundaire weg\",\n    \"map_content_way_type_A1_local_streets\": \"lokale straat\",\n    \"map_content_way_type_A1_service_roads\": \"dienstweg\",\n    \"map_content_way_type_plural_A1_service_roads\": \"dienstwegen\",\n    \"map_content_way_type_A1_track_roads\": \"landweg\",\n    \"map_content_way_type_A1_vehicle_unspecified\": \"overige autoweg\",\n    \"map_content_way_type_A2_pedestrian_streets\": \"voetgangersstraat\",\n    \"map_content_way_type_A2_footpaths_trails\": \"voetpad / wandelpad\",\n    \"map_content_way_type_A2_cycleways\": \"fietspad\",\n    \"map_content_way_type_A2_steps_ramps\": \"trap / helling\",\n    \"map_content_way_type_A2_pedestrian_unspecified\": \"overig voetpad\",\n    \"map_content_way_type_A3_rail_lines\": \"spoorlijn\",\n    \"map_content_way_type_A3_tram_light_rail\": \"tram / lightrail\",\n    \"map_content_way_type_A3_subway_metro\": \"metro\",\n    \"map_content_way_type_A3_rail_yards_sidings\": \"rangeer-/opstelspoor\",\n    \"map_content_way_type_A4_rivers\": \"rivier\",\n    \"map_content_way_type_A4_streams_canals\": \"beek / kanaal\",\n    \"map_content_way_type_A4_ditches_drains\": \"sloot / afwatering\",\n    \"map_content_way_type_A4_other_waterways\": \"overige waterweg\",\n    \"map_content_way_type_plural_A4_other_waterways\": \"overige waterwegen\",\n    \"map_content_way_type_A_other_ways\": \"overige weg\",\n    \"map_content_water_area_unnamed\": \"Naamloos __type__\",\n    \"map_content_water_area_type_generic\": \"watergebied\",\n    \"map_content_water_area_type_generic_plural\": \"watergebieden\",\n    \"map_content_water_area_type_B1_lakes\": \"meer\",\n    \"map_content_water_area_type_B1_ponds\": \"vijver\",\n    \"map_content_water_area_type_B1_reservoirs\": \"stuwmeer\",\n    \"map_content_water_area_type_B1_sea_coast\": \"zee\",\n    \"map_content_water_area_type_B1_riverbanks\": \"rivieroever\",\n    \"map_content_water_area_type_B1_other_water\": \"watergebied\",\n    \"map_content_building_unnamed\": \"Naamloos gebouw\",\n    \"map_content_no_buildings\": \"Geen gebouwen gevonden voor deze kaart.\",\n    \"map_content_no_poi_familiar\": \"Geen bekende plekken gevonden voor deze kaart.\",\n    \"map_content_no_poi_daily\": \"Geen dagelijkse voorzieningen gevonden voor deze kaart.\",\n    \"map_content_no_poi_transport\": \"Geen vervoerspunten gevonden voor deze kaart.\",\n    \"map_content_no_poi_accessibility\": \"Geen toegankelijkheidsaanwijzingen gevonden voor deze kaart.\",\n    \"map_content_show_more_buildings_one\": \"Nog 1 gebouw tonen\",\n    \"map_content_show_more_buildings_many\": \"Nog __count__ gebouwen tonen\",\n    \"map_content_show_less_buildings\": \"Minder gebouwen tonen\",\n    \"map_content_show_more_features_one\": \"Nog 1 object tonen\",\n    \"map_content_show_more_features_many\": \"Nog __count__ objecten tonen\",\n    \"map_content_show_less_features\": \"Minder objecten tonen\",\n    \"map_content_summary_show_more\": \"Meer tonen\",\n    \"map_content_summary_show_only\": \"Alleen samenvatting tonen\",\n    \"map_content_height_note_raised_mm\": \"Verhoogd met __millimeters__ mm\",\n    \"map_content_height_note_varying\": \"Verhoogd met wisselende hoogte\",\n    \"map_content_height_note_waved_surface\": \"Golvende oppervlakte\",\n    \"map_content_height_note_text_only\": \"Alleen tekstuele informatie\",\n    \"map_content_show_more_pois_one\": \"Nog 1 plek tonen\",\n    \"map_content_show_more_pois_many\": \"Nog __count__ plekken tonen\",\n    \"map_content_show_less_pois\": \"Minder plekken tonen\",\n    \"map_content_poi_unknown_type\": \"Onbekend type plek\",\n    \"map_content_poi_unnamed\": \"Naamloze __type__\",\n    \"map_content_poi_unnamed_many\": \"__count__ naamloze __type__\",\n    \"map_content_poi_sparse_note\": \"In dit gebied zijn maar weinig POI's getagd; ontbrekende plekken kunnen komen door de datadekking van OpenStreetMap.\",\n    \"map_content_poi_access_crossings_signals_one\": \"__count__ plek met oversteekplaatsen met verkeerslichten\",\n    \"map_content_poi_access_crossings_signals_many\": \"__count__ plekken met oversteekplaatsen met verkeerslichten\",\n    \"map_content_poi_access_tactile_summary\": \"Tactiele bestrating-tags: __yes__ ja, __no__ nee\",\n    \"map_content_poi_access_kerb_summary\": \"Stoeprand-tags: verlaagd __lowered__, gelijkvloers __flush__, verhoogd __raised__\",\n    \"map_content_poi_access_steps_summary\": \"Trappen getagd op __count__ plekken (hoogste trede-aantal __max__)\",\n    \"map_content_poi_access_incline_summary\": \"Hellings-tags: omhoog __up__, omlaag __down__\",\n    \"map_content_poi_access_wheelchair_summary\": \"Rolstoel-tags: ja __yes__, beperkt __limited__, nee __no__\",\n    \"map_content_poi_example\": \"Voorbeeld: __example__\",\n    \"map_content_building_join_at\": \"bij\",\n    \"map_content_building_type_from_value\": \"__value__ gebouw\",\n    \"map_content_unavailable\": \"Kaartinhoud is niet beschikbaar.\",\n    \"map_content_list_and\": \"en\",\n    \"map_content_edge_phrase\": \"de __edge__rand\",\n    \"map_content_touches_edge\": \"Raakt de __edge__rand\",\n    \"map_content_touches_edges\": \"Raakt de randen: __edges__\",\n    \"map_content_touches_items\": \"Raakt __items__\",\n    \"map_content_crosses_edge\": \"Kruist de __edge__rand\",\n    \"map_content_crosses_edges\": \"Kruist de randen: __edges__\",\n    \"map_content_crosses_items\": \"Kruist __items__\",\n    \"map_content_touches_edge_percent\": \"raakt __percent__ % van de __edge__rand\",\n    \"map_content_touch_pos_north\": \"in het noorden\",\n    \"map_content_touch_pos_south\": \"in het zuiden\",\n    \"map_content_touch_pos_west\": \"in het westen\",\n    \"map_content_touch_pos_east\": \"in het oosten\",\n    \"map_content_touch_pos_center\": \"dicht bij het midden\",\n    \"map_content_touch_pos_multiple\": \"op meerdere plekken\",\n    \"map_content_edge_north\": \"noord\",\n    \"map_content_edge_south\": \"zuid\",\n    \"map_content_edge_east\": \"oost\",\n    \"map_content_edge_west\": \"west\",\n    \"map_content_dir_north\": \"noord\",\n    \"map_content_dir_northeast\": \"noord-oost\",\n    \"map_content_dir_east\": \"oost\",\n    \"map_content_dir_southeast\": \"zuid-oost\",\n    \"map_content_dir_south\": \"zuid\",\n    \"map_content_dir_southwest\": \"zuid-west\",\n    \"map_content_dir_west\": \"west\",\n    \"map_content_dir_northwest\": \"noord-west\",\n    \"map_content_corner_northwest\": \"noord-westhoek\",\n    \"map_content_corner_northeast\": \"noord-oosthoek\",\n    \"map_content_corner_southwest\": \"zuid-westhoek\",\n    \"map_content_corner_southeast\": \"zuid-oosthoek\",\n    \"map_content_loc_center\": \"het midden\",\n    \"map_content_loc_offset_center\": \"__dir__ van het midden\",\n    \"map_content_loc_part\": \"het __dir__deel\",\n    \"map_content_loc_edge\": \"de __dir__rand\",\n    \"map_content_loc_corner\": \"de __corner__\",\n    \"map_content_loc_near_edge\": \"dicht bij de __dir__rand\",\n    \"map_content_loc_near_corner\": \"__corner__\",\n    \"map_content_loc_full_center\": \"in het midden\",\n    \"map_content_loc_full_offset_center\": \"een beetje __dir__ van het midden\",\n    \"map_content_loc_full_part\": \"in het __dir__deel\",\n    \"map_content_loc_full_edge\": \"dicht bij de __dir__rand\",\n    \"map_content_loc_full_corner\": \"in de __corner__\",\n    \"map_content_loc_full_near_edge\": \"dicht bij de __dir__rand\",\n    \"map_content_loc_full_near_corner\": \"in de __corner__\",\n    \"map_content_loc_endpoint_center\": \"het midden\",\n    \"map_content_loc_endpoint_part\": \"het __dir__deel\",\n    \"map_content_loc_endpoint_near_edge\": \"dicht bij de __dir__rand\",\n    \"map_content_loc_endpoint_corner\": \"de __corner__\",\n    \"map_content_loc_route_from_center\": \"het midden\",\n    \"map_content_loc_route_from_part\": \"het __dir__deel\",\n    \"map_content_loc_route_from_near_edge\": \"de buurt van de __dir__rand\",\n    \"map_content_loc_route_from_corner\": \"de __corner__\",\n    \"map_content_loc_route_to_center\": \"het midden\",\n    \"map_content_loc_route_to_part\": \"het __dir__deel\",\n    \"map_content_loc_route_to_near_edge\": \"de buurt van de __dir__rand\",\n    \"map_content_loc_route_to_corner\": \"de __corner__\",\n    \"map_content_region_center\": \"midden\",\n    \"map_content_region_part\": \"__dir__deel\",\n    \"map_content_region_edge\": \"__dir__rand\",\n    \"map_content_region_corner\": \"__corner__\",\n    \"map_content_summary_in\": \"In de __region__.\",\n    \"map_content_summary_near\": \"Dicht bij de __region__.\",\n    \"map_content_summary_mostly_in_lead\": \"Vooral in de __region__\",\n    \"map_content_summary_mainly_near_lead\": \"Vooral dicht bij de __region__\",\n    \"map_content_summary_mostly_extending\": \"__lead__, uitstrekkend richting de __target__.\",\n    \"map_content_summary_in_and\": \"In de __first__ en __second__.\",\n    \"map_content_summary_in_and_near\": \"In de __first__ en dicht bij de __second__.\",\n    \"map_content_summary_near_and\": \"Dicht bij de __first__ en __second__.\",\n    \"map_content_summary_mainly_in_and\": \"In meerdere gebieden, vooral in de __first__ en __second__.\",\n    \"map_content_summary_mainly_in_and_near\": \"In meerdere gebieden, vooral in de __first__ en dicht bij de __second__.\",\n    \"map_content_summary_mainly_near_and\": \"In meerdere gebieden, vooral dicht bij de __first__ en __second__.\",\n    \"map_content_summary_mainly_near_and_in\": \"In meerdere gebieden, vooral dicht bij de __first__ en in de __second__.\",\n    \"map_content_summary_several_mainly_in\": \"In meerdere gebieden, vooral in de __region__.\",\n    \"map_content_summary_several_mainly_near\": \"In meerdere gebieden, vooral dicht bij de __region__.\",\n    \"map_content_summary_equal_clauses\": \"__first__ en __second__.\",\n    \"map_content_summary_mostly_extending_clauses\": \"Vooral __lead__, uitstrekkend richting __target__.\",\n    \"map_content_summary_distributed_top2\": \"In meerdere gebieden, vooral __first__ en __second__.\",\n    \"map_content_summary_distributed_top1\": \"In meerdere gebieden, vooral __first__.\",\n    \"map_content_summary_three_regions_clauses\": \"In meerdere gebieden, vooral __first__, __second__ en __third__.\",\n    \"map_content_parts_one\": \"__count__ deel\",\n    \"map_content_parts_many\": \"__count__ delen\",\n    \"map_content_total_area\": \"Beslaat __percent__ % van de kaart\",\n    \"map_content_shape_elongated\": \"Langwerpig\",\n    \"map_content_shape_long_thin\": \"Zeer smal\",\n    \"map_content_shape_irregular\": \"Onregelmatige vorm\",\n    \"map_content_shape_generic\": \"__shape__ vorm\",\n    \"map_content_aspect_label\": \"verhouding __ratio__\",\n    \"map_content_degrees_abbrev\": \"gr.\",\n    \"map_content_orientation\": \"Oriëntatie __label__\",\n    \"map_content_orientation_label_east_west\": \"oost-west\",\n    \"map_content_orientation_label_north_south\": \"noord-zuid\",\n    \"map_content_orientation_label_northeast_southwest\": \"noord-oost naar zuid-west\",\n    \"map_content_orientation_label_east_northeast_to_west_southwest\": \"oost-noord-oost naar west-zuid-west\",\n    \"map_content_orientation_label_north_northeast_to_south_southwest\": \"noord-noord-oost naar zuid-zuid-west\",\n    \"map_content_coverage_label\": \"Beslaat:\",\n    \"map_content_coverage_much\": \"veel van __segment__\",\n    \"map_content_coverage_some\": \"een deel van __segment__\",\n    \"map_content_coverage_little\": \"een beetje van __segment__\",\n    \"map_content_osm_value_kindergarten\": \"kinderdagverblijf\",\n    \"map_content_osm_value_school\": \"school\",\n    \"map_content_osm_value_hospital\": \"ziekenhuis\",\n    \"map_content_osm_value_church\": \"kerk\",\n    \"map_content_osm_value_university\": \"universiteit\",\n    \"map_content_osm_value_college\": \"hogeschool\",\n    \"map_content_osm_value_library\": \"bibliotheek\",\n    \"map_content_osm_value_gallery\": \"galerij\",\n    \"map_content_osm_value_museum\": \"museum\",\n    \"map_content_osm_value_theatre\": \"theater\",\n    \"map_content_osm_value_supermarket\": \"supermarkt\",\n    \"map_content_osm_value_cafe\": \"café\",\n    \"map_content_osm_value_restaurant\": \"restaurant\",\n    \"map_content_osm_value_hotel\": \"hotel\",\n    \"map_content_osm_value_parking\": \"parkeren\",\n    \"map_content_osm_value_bicycle_parking\": \"fietsenstalling\",\n    \"map_content_osm_value_residential\": \"woon\",\n    \"map_content_osm_value_commercial\": \"commercieel\",\n    \"map_content_osm_value_industrial\": \"industrieel\",\n    \"map_content_osm_value_public\": \"openbaar\",\n    \"map_content_osm_value_roof\": \"overkapping\",\n    \"map_content_osm_value_transportation\": \"vervoer\",\n    \"map_content_osm_value_service\": \"dienst\",\n    \"map_content_osm_value_civic\": \"gemeentelijk\",\n    \"map_content_osm_value_office\": \"kantoor\",\n    \"map_content_osm_value_apartments\": \"appartementengebouw\",\n    \"map_content_osm_value_house\": \"huis\",\n    \"map_content_osm_value_detached\": \"vrijstaande woning\",\n    \"map_content_osm_value_terrace\": \"rijtjeshuizen\",\n    \"map_content_osm_value_semidetached_house\": \"twee-onder-een-kap\",\n    \"map_content_osm_value_farm\": \"boerderij\",\n    \"map_content_osm_value_warehouse\": \"magazijn\",\n    \"map_content_osm_value_garages\": \"garages\",\n    \"map_content_osm_value_garage\": \"garage\",\n    \"map_content_osm_value_retail\": \"winkel\",\n    \"map_content_osm_value_kiosk\": \"kiosk\",\n    \"map_content_osm_value_playground\": \"speeltuin\",\n    \"map_content_osm_value_park\": \"park\",\n    \"map_content_osm_value_zoo\": \"dierentuin\",\n    \"map_content_osm_value_sports_centre\": \"sportcentrum\",\n    \"map_content_osm_value_station\": \"station\",\n    \"map_content_osm_value_rail\": \"spoor\",\n    \"map_content_osm_value_platform\": \"perron\",\n    \"map_content_osm_value_stop_position\": \"haltepositie\",\n    \"map_content_osm_value_stop_area\": \"haltegebied\",\n    \"map_content_osm_value_stop_area_group\": \"groep haltegebieden\",\n    \"map_content_osm_value_halt\": \"halte\",\n    \"map_content_osm_value_bus_stop\": \"bushalte\",\n    \"map_content_osm_value_bus_station\": \"busstation\",\n    \"map_content_osm_value_tram_stop\": \"tramhalte\",\n    \"map_content_osm_value_tram\": \"tram\",\n    \"map_content_osm_value_subway_entrance\": \"metro-ingang\",\n    \"map_content_osm_value_ferry_terminal\": \"veerterminal\",\n    \"map_content_osm_value_taxi\": \"taxi\",\n    \"map_content_osm_value_department_store\": \"warenhuis\",\n    \"map_content_osm_value_mall\": \"winkelcentrum\",\n    \"map_content_osm_value_attraction\": \"attractie\",\n    \"map_content_osm_value_artwork\": \"kunstwerk\",\n    \"map_content_osm_value_fountain\": \"fontein\",\n    \"map_content_osm_value_viewpoint\": \"uitzichtpunt\",\n    \"map_content_osm_value_theme_park\": \"pretpark\",\n    \"map_content_osm_value_aquarium\": \"aquarium\",\n    \"map_content_osm_value_arts_centre\": \"kunstcentrum\",\n    \"map_content_osm_value_community_centre\": \"gemeenschapscentrum\",\n    \"map_content_osm_value_place_of_worship\": \"gebedshuis\",\n    \"map_content_osm_value_townhall\": \"stadhuis\",\n    \"map_content_osm_value_courthouse\": \"gerechtsgebouw\",\n    \"map_content_osm_value_pharmacy\": \"apotheek\",\n    \"map_content_osm_value_clinic\": \"kliniek\",\n    \"map_content_osm_value_doctors\": \"huisarts\",\n    \"map_content_osm_value_toilets\": \"toiletten\",\n    \"map_content_osm_value_post_office\": \"postkantoor\",\n    \"map_content_osm_value_bank\": \"bank\",\n    \"map_content_osm_value_atm\": \"geldautomaat\",\n    \"map_content_osm_value_fuel\": \"tankstation\",\n    \"map_content_osm_value_convenience\": \"gemakswinkel\",\n    \"map_content_osm_value_fast_food\": \"fastfood\",\n    \"map_content_osm_value_bar\": \"bar\",\n    \"map_content_osm_value_pub\": \"pub\",\n    \"map_content_osm_value_marketplace\": \"marktplein\",\n    \"map_content_osm_value_bench\": \"bankje\",\n    \"map_content_osm_value_waste_basket\": \"prullenbak\",\n    \"map_content_osm_value_vending_machine\": \"verkoopautomaat\",\n    \"map_content_osm_value_street_lamp\": \"straatlantaarn\",\n    \"map_content_osm_value_outdoor_seating\": \"buitenzitplaatsen\",\n    \"map_content_osm_value_sauna\": \"sauna\",\n    \"map_content_osm_value_shelter\": \"schuilplaats\",\n    \"map_content_osm_value_abandoned\": \"verlaten\",\n    \"map_content_osm_value_razed\": \"gesloopt\",\n    \"map_content_osm_value_bicycle\": \"fiets\",\n    \"map_content_osm_value_building\": \"gebouw\",\n    \"map_content_osm_value_residential_building\": \"woongebouw\",\n    \"map_content_osm_value_commercial_building\": \"commercieel gebouw\",\n    \"map_content_osm_value_industrial_building\": \"industrieel gebouw\",\n    \"map_content_osm_value_public_building\": \"openbaar gebouw\",\n    \"map_content_osm_value_garages_building\": \"garagegebouw\",\n    \"map_content_osm_value_yes\": \"algemeen\",\n    \"what_is_the_map_like__railways\": \"Spoorwegen\",\n    \"what_is_the_map_like__water\": \"Water (met een golvend oppervlak, maar smalle stroompjes als lijnen)\",\n    \"what_is_the_map_like__north\": \"De noord-oostelijke hoek is gemarkeerd om de juiste oriëntatie aan te geven\",\n    \"what_is_the_map_like__selected_addr\": \"In een 3D-print is het geselecteerde adres (of de ingevoerde coördinaten) gemarkeerd met een kegeltje\",\n    \"what_is_the_map_like__clarity\": \"De tactiele kaarten van Touch Mapper zijn geoptimaliseerd voor duidelijkheid en de praktische behoeften van blinden en slechtzienden: de 3D-geprinte kaarten vormen geen natuurgetrouwe representatie van het gebied.\",\n    \"larger_scale_maps\": \"Grotere gebieden en binnenkaarten\",\n    \"larger_scale_maps_answer\": \"Touch Mapper ondersteunt momenteel alleen kaarten die elke straat en elk gebouw in het gebied tonen. Kaarten die grotere gebieden bestrijken, hoogteverschillen tonen of binnenruimtes weergeven, zouden later ondersteund kunnen worden. Merk op dat u door \\\"Print een deel van een meerdelige kaart\\\" te selecteren een groter gebied kunt bestrijken door meerdere kaarten te printen. Er zijn massaproductiekaarten op landschaal, bijvoorbeeld bij __aphLinkStart__American Printing House for the Blind Shop__aphLinkEnd__.\",\n    \"data_comes_from_osm\": \"Bron van kaartdata\",\n    \"data_comes\": \"Touch Mapper gebruikt data van __startTag__OpenStreetMap__endTag__. Alhoewel dit doorgaans een uitstekende bron is, kan de nauwkeurigheid van de kaarten niet worden gegarandeerd. Het meestvoorkomende probleem is ontbrekende gebouwen. Belangrijke eigenschappen die ontbreken kunt u vrij toevoegen aan OpenStreetMap. Commerciële kaartbronnen zoals Google Maps kunnen helaas niet gebruikt worden, omdat ze de vereiste manier van toegang tot hun data niet toestaan.\",\n    \"commercial_use\": \"Commercieel gebruik\",\n    \"commercial_use_answer\": \"U mag Touch Mapper gebruiken voor commerciële doeleinden, zoals het printen en verkopen van de kaarten. Alleen vraag ik dat u uw klanten duidelijk laat weten dat de kaarten afkomstig zijn van www.touch-mapper.org, zowel bij het adverteren als in verpakkingsmateriaal. Ook hoop ik dat u het mij laat weten als u begint met het aanbieden van een op Touch Mapper gebaseerde service.\",\n    \"users_and_purposes\": \"Gebruiksmogelijkheden en voordelen\",\n    \"tactile_maps_have_many_different_user_groups\": \"Tactiele kaarten kunnen nuttig zijn voor veel verschillende gebruikersgroepen.\",\n    \"visually_impaired_individuals\": \"Blinden en slechtzienden: betere oriëntatie en plannen van routes\",\n    \"outdoors_tourist_attractions\": \"Buitentoeristische attracties: betere service voor blinde en slechtziende klanten\",\n    \"public_areas_such_as_parks\": \"Publieke ruimtes zoals parken en speeltuinen: tactiele kaarten kunnen blinde en slechtziende bezoekers helpen om het park zelfstandiger te gebruiken.\",\n    \"orientation_and_mobility_specialists\": \"Oriëntatie- en mobiliteitsspecialisten: om cliënten te helpen gemakkelijker een mentale kaart van een gebied te vormen\",\n    \"multipart_maps_heading\": \"Kaarten bestaande uit meerdere delen printen\",\n    \"multipart_maps\": \"Als een enkele kaart niet groot genoeg is om het gewenste gebied te bestrijken, bijvoorbeeld een lange weg, kunt u meerdere prints maken en deze naast elkaar op een vlak oppervlak leggen. Selecteer \\\"Geavanceerde opties\\\" en vervolgens \\\"Print een deel van een meerdelige kaart\\\". In deze modus zijn er geen randen rond de 3D-print. Er verschijnen ook pijltjes om het geselecteerde gebied in elke richting met 100 % (dat wil zeggen, de kaartdiameter) te verschuiven, waardoor het eenvoudig is om aangrenzende gebieden te selecteren.\",\n    \"how_to_3d_print\": \"3D-printen\",\n    \"map_can_be_printed\": \"De kaart kan worden geprint door bijna elke 3D-printer, inclusief betaalbare hobby-apparaten. Het belangrijkste om op te letten zijn de wegen. Sommige services kunnen ze te smal vinden om te printen.\",\n    \"if_you_print_yourself\": \"Als u met uw eigen 3D-printer print, gebruik dan een laagdikte van 0,25 mm of 0,3 mm. De basis is 0,6 mm dik en het is voldoende om die in twee lagen te printen. Twee toplagen en een horizontale shell zijn voldoende. U wilt de toplaag misschien vrij langzaam printen om gladde oppervlakken te krijgen. Als u de Simplify3D-slicer gebruikt, moet u \\\"Separate Connected Surfaces\\\" inschakelen om een correcte print te krijgen. De printtijd voor een kaart van 17 cm met de basis in twee lagen is ongeveer 3–4 uur.\",\n    \"about_touch_mapper\": \"Over Touch Mapper\",\n    \"about_touch_mapper_answer\": \"Mijn naam is Sofia Pahaoja. Ik heb Touch Mapper gecreëerd nadat ik bekend werd met de uitdagingen waar blinden en slechtzienden tegenaan lopen bij het zelfstandig reizen, en inzag hoe 3D-printen gecombineerd met vrij beschikbare kaartdata kan helpen. <!-- Ik heb Touch Mapper geïntegreerd met __startTag__Playful Pixels'__endTag__ 3D-printservice om het verkrijgen van kaarten van hoge kwaliteit zo makkelijk en betaalbaar mogelijk te maken. -->\",\n    \"news_from\": \"Informatie over nieuwe functies en relevante gebeurtenissen wordt geplaatst op __linkFB__ en __linkTwitter__.\",\n    \"want_to_improve_please\": \"Ik wil Touch Mapper zo goed mogelijk maken, en gebruikersfeedback is daarbij onontbeerlijk. Als u vragen of suggesties voor verbetering heeft,\",\n    \"email_me\": \"Stuur me een e-mail\",\n    \"address_search_by\": \"Adres zoeken __linkTag__.\",\n    \"map_data_by\": \"Kaartdata door __linkTag__.\",\n    \"touch_mapper_is_os\": \"Touch Mapper is open source software.\",\n    \"see_github\": \"Gebruik __startTag__GitHub__endTag__ voor bugmeldingen en suggesties voor nieuwe functies.\",\n    \"map_content_osm_value_dormitory\": \"studentenhuis/woongebouw\",\n    \"map_content_osm_value_motorway\": \"snelweg\",\n    \"map_content_osm_value_trunk\": \"autoweg\",\n    \"map_content_osm_value_primary\": \"hoofdweg\",\n    \"map_content_osm_value_secondary\": \"secundaire weg\",\n    \"map_content_osm_value_tertiary\": \"tertiaire weg\",\n    \"map_content_osm_value_unclassified\": \"ongeclassificeerde weg\",\n    \"map_content_osm_value_living_street\": \"woonerf\",\n    \"map_content_osm_value_pedestrian\": \"voetgangersgebied\",\n    \"map_content_osm_value_footway\": \"voetpad\",\n    \"map_content_osm_value_path\": \"pad\",\n    \"map_content_osm_value_cycleway\": \"fietspad\",\n    \"map_content_osm_value_steps\": \"trap\",\n    \"map_content_osm_value_track\": \"landweg\",\n    \"map_content_osm_value_road\": \"weg\",\n    \"map_content_osm_value_dentist\": \"tandarts\",\n    \"map_content_osm_value_police\": \"politie\",\n    \"map_content_osm_value_fire_station\": \"brandweer\",\n    \"map_content_osm_value_charging_station\": \"laadstation\",\n    \"map_content_osm_value_hostel\": \"hostel\",\n    \"map_content_osm_value_information\": \"toeristeninformatie\",\n    \"map_content_osm_value_bakery\": \"bakkerij\",\n    \"map_content_osm_value_clothes\": \"kledingwinkel\",\n    \"map_content_osm_value_apartments_building\": \"appartementengebouw\",\n    \"map_content_osm_value_house_building\": \"huis\",\n    \"map_content_osm_value_detached_building\": \"vrijstaande woning\",\n    \"map_content_osm_value_terrace_building\": \"rijtjeshuizen\",\n    \"map_content_osm_value_dormitory_building\": \"studentenhuis/woongebouw\",\n    \"map_content_osm_value_retail_building\": \"winkelgebouw\",\n    \"map_content_osm_value_office_building\": \"kantoorgebouw\",\n    \"map_content_osm_value_warehouse_building\": \"magazijn\",\n    \"map_content_osm_value_school_building\": \"schoolgebouw\",\n    \"map_content_osm_value_university_building\": \"universiteitsgebouw\",\n    \"map_content_osm_value_hospital_building\": \"ziekenhuis\",\n    \"map_content_osm_value_church_building\": \"kerk\",\n    \"map_content_osm_value_garage_building\": \"garage\",\n    \"map_content_osm_value_roof_building\": \"overkapping\"\n}\n"
  },
  {
    "path": "web/npm-shrinkwrap.json",
    "content": "{\n  \"name\": \"touch-mapper-web\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"touch-mapper-web\",\n      \"dependencies\": {\n        \"ect\": \"^0.5.9\",\n        \"metalsmith\": \"^1.0.0\",\n        \"metalsmith-concat\": \"^5.0.1\",\n        \"metalsmith-copy\": \"^0.2.1\",\n        \"metalsmith-eslint\": \"^2.5.3\",\n        \"metalsmith-i18next\": \"^0.2.20\",\n        \"metalsmith-ignore\": \"^0.1.2\",\n        \"metalsmith-in-place\": \"^1.4.3\",\n        \"metalsmith-less\": \"^2.0.0\",\n        \"metalsmith-text-replace\": \"^1.0.1\"\n      },\n      \"devDependencies\": {\n        \"http-server\": \"^0.9.0\"\n      }\n    },\n    \"node_modules/absolute\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/absolute/-/absolute-0.0.1.tgz\",\n      \"integrity\": \"sha512-Zs3Sz3lmEIH76fvnc4jto4CpuI5bhU2TD+jtBYVIfXnjFm+49y1opKxNy4gtNF+xSLBL7hXRpLwypc+o2MzBIQ==\"\n    },\n    \"node_modules/acorn\": {\n      \"version\": \"5.7.4\",\n      \"resolved\": \"https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz\",\n      \"integrity\": \"sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==\",\n      \"bin\": {\n        \"acorn\": \"bin/acorn\"\n      },\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/acorn-jsx\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz\",\n      \"integrity\": \"sha512-AU7pnZkguthwBjKgCg6998ByQNIMjbuDQZ8bb78QAFZwPfmKia8AIzgY/gWgqCjnht8JLdXmB4YxA0KaV60ncQ==\",\n      \"dependencies\": {\n        \"acorn\": \"^3.0.4\"\n      }\n    },\n    \"node_modules/acorn-jsx/node_modules/acorn\": {\n      \"version\": \"3.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz\",\n      \"integrity\": \"sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==\",\n      \"bin\": {\n        \"acorn\": \"bin/acorn\"\n      },\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/ajv\": {\n      \"version\": \"4.11.8\",\n      \"resolved\": \"https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz\",\n      \"integrity\": \"sha512-I/bSHSNEcFFqXLf91nchoNB9D1Kie3QKcWdchYUaoIg1+1bdWDkdfdlvdIOJbi9U8xR0y+MWc5D+won9v95WlQ==\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"co\": \"^4.6.0\",\n        \"json-stable-stringify\": \"^1.0.1\"\n      }\n    },\n    \"node_modules/ajv-keywords\": {\n      \"version\": \"1.5.1\",\n      \"resolved\": \"https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz\",\n      \"integrity\": \"sha512-vuBv+fm2s6cqUyey2A7qYcvsik+GMDJsw8BARP2sDE76cqmaZVarsvHf7Vx6VJ0Xk8gLl+u3MoAPf6gKzJefeA==\",\n      \"peerDependencies\": {\n        \"ajv\": \">=4.10.0\"\n      }\n    },\n    \"node_modules/ajv/node_modules/co\": {\n      \"version\": \"4.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/co/-/co-4.6.0.tgz\",\n      \"integrity\": \"sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==\",\n      \"engines\": {\n        \"iojs\": \">= 1.0.0\",\n        \"node\": \">= 0.12.0\"\n      }\n    },\n    \"node_modules/align-text\": {\n      \"version\": \"0.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz\",\n      \"integrity\": \"sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==\",\n      \"dependencies\": {\n        \"kind-of\": \"^3.0.2\",\n        \"longest\": \"^1.0.1\",\n        \"repeat-string\": \"^1.5.2\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/alter\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/alter/-/alter-0.2.0.tgz\",\n      \"integrity\": \"sha512-Wuss6JIZ6h4j2+NgU2t+9mSwS7gBSZJbU4Dg8xETguAD2veJUSuCrvTIiC78QgZE7/zX7h6OnXw2PiiCBirEGw==\",\n      \"dependencies\": {\n        \"stable\": \"~0.1.3\"\n      }\n    },\n    \"node_modules/ansi-escapes\": {\n      \"version\": \"1.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz\",\n      \"integrity\": \"sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/ansi-red\": {\n      \"version\": \"0.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz\",\n      \"integrity\": \"sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==\",\n      \"dependencies\": {\n        \"ansi-wrap\": \"0.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/ansi-regex\": {\n      \"version\": \"0.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz\",\n      \"integrity\": \"sha512-sGwIGMjhYdW26/IhwK2gkWWI8DRCVO6uj3hYgHT+zD+QL1pa37tM3ujhyfcJIYSbsxp7Gxhy7zrRW/1AHm4BmA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/ansi-styles\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz\",\n      \"integrity\": \"sha512-f2PKUkN5QngiSemowa6Mrk9MPCdtFiOSmibjZ+j1qhLGHHYsqZwmBMRF3IRMVXo8sybDqx2fJl2d/8OphBoWkA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/ansi-wrap\": {\n      \"version\": \"0.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz\",\n      \"integrity\": \"sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/argparse\": {\n      \"version\": \"1.0.10\",\n      \"resolved\": \"https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz\",\n      \"integrity\": \"sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==\",\n      \"dependencies\": {\n        \"sprintf-js\": \"~1.0.2\"\n      }\n    },\n    \"node_modules/array-differ\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz\",\n      \"integrity\": \"sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/array-union\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz\",\n      \"integrity\": \"sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==\",\n      \"dependencies\": {\n        \"array-uniq\": \"^1.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/array-uniq\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz\",\n      \"integrity\": \"sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/arrify\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz\",\n      \"integrity\": \"sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/asap\": {\n      \"version\": \"2.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/asap/-/asap-2.0.6.tgz\",\n      \"integrity\": \"sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==\",\n      \"optional\": true\n    },\n    \"node_modules/asn1\": {\n      \"version\": \"0.2.6\",\n      \"resolved\": \"https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz\",\n      \"integrity\": \"sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"safer-buffer\": \"~2.1.0\"\n      }\n    },\n    \"node_modules/assert-plus\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz\",\n      \"integrity\": \"sha512-u1L0ZLywRziOVjUhRxI0Qg9G+4RnFB9H/Rq40YWn0dieDgO7vAYeJz6jKAO6t/aruzlDFLAPkQTT87e+f8Imaw==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=0.8\"\n      }\n    },\n    \"node_modules/ast-traverse\": {\n      \"version\": \"0.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/ast-traverse/-/ast-traverse-0.1.1.tgz\",\n      \"integrity\": \"sha512-CPuE4BWIhJjsNMvFkrzjiBgOl56NJTuBPBkBqyRyfq/nZtx1Z1f5I+qx7G/Zt+FAOS+ABhghkEuWJrfW9Njjog==\"\n    },\n    \"node_modules/ast-types\": {\n      \"version\": \"0.9.6\",\n      \"resolved\": \"https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz\",\n      \"integrity\": \"sha512-qEdtR2UH78yyHX/AUNfXmJTlM48XoFZKBdwi1nzkI1mJL21cmbu0cvjxjpkXJ5NENMq42H+hNs8VLJcqXLerBQ==\",\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/async\": {\n      \"version\": \"0.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/async/-/async-0.9.2.tgz\",\n      \"integrity\": \"sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==\"\n    },\n    \"node_modules/asynckit\": {\n      \"version\": \"0.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz\",\n      \"integrity\": \"sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==\",\n      \"optional\": true\n    },\n    \"node_modules/aws-sign2\": {\n      \"version\": \"0.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz\",\n      \"integrity\": \"sha512-JnJpAS0p9RmixkOvW2XwDxxzs1bd4/VAGIl6Q0EC5YOo+p+hqIhtDhn/nmFnB/xUNXbLkpE2mOjgVIBRKD4xYw==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/aws4\": {\n      \"version\": \"1.12.0\",\n      \"resolved\": \"https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz\",\n      \"integrity\": \"sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==\",\n      \"optional\": true\n    },\n    \"node_modules/balanced-match\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz\",\n      \"integrity\": \"sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==\"\n    },\n    \"node_modules/bcrypt-pbkdf\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz\",\n      \"integrity\": \"sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"tweetnacl\": \"^0.14.3\"\n      }\n    },\n    \"node_modules/bluebird\": {\n      \"version\": \"3.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz\",\n      \"integrity\": \"sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==\"\n    },\n    \"node_modules/boom\": {\n      \"version\": \"2.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/boom/-/boom-2.10.1.tgz\",\n      \"integrity\": \"sha512-KbiZEa9/vofNcVJXGwdWWn25reQ3V3dHBWbS07FTF3/TOehLnm9GEhJV4T6ZvGPkShRpmUqYwnaCrkj0mRnP6Q==\",\n      \"deprecated\": \"This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"hoek\": \"2.x.x\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.40\"\n      }\n    },\n    \"node_modules/brace-expansion\": {\n      \"version\": \"1.1.11\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz\",\n      \"integrity\": \"sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==\",\n      \"dependencies\": {\n        \"balanced-match\": \"^1.0.0\",\n        \"concat-map\": \"0.0.1\"\n      }\n    },\n    \"node_modules/breakable\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/breakable/-/breakable-1.0.0.tgz\",\n      \"integrity\": \"sha512-+ityJqcjhozQNrezzTd2dtH/lkIXmE52HL+FohK2TOLQDl3QURTNkim+2C0xcso4Zehq/HM4Wkumcdz7Ue+XmA==\"\n    },\n    \"node_modules/buffer-from\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz\",\n      \"integrity\": \"sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==\"\n    },\n    \"node_modules/caller-path\": {\n      \"version\": \"0.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz\",\n      \"integrity\": \"sha512-UJiE1otjXPF5/x+T3zTnSFiTOEmJoGTD9HmBoxnCUwho61a2eSNn/VwtwuIBDAo2SEOv1AJ7ARI5gCmohFLu/g==\",\n      \"dependencies\": {\n        \"callsites\": \"^0.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/callsites\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz\",\n      \"integrity\": \"sha512-Zv4Dns9IbXXmPkgRRUjAaJQgfN4xX5p6+RQFhWUqscdvvK2xK/ZL8b3IXIJsj+4sD+f24NwnWy2BY8AJ82JB0A==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/camelcase\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz\",\n      \"integrity\": \"sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/caseless\": {\n      \"version\": \"0.12.0\",\n      \"resolved\": \"https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz\",\n      \"integrity\": \"sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==\",\n      \"optional\": true\n    },\n    \"node_modules/center-align\": {\n      \"version\": \"0.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz\",\n      \"integrity\": \"sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==\",\n      \"dependencies\": {\n        \"align-text\": \"^0.1.3\",\n        \"lazy-cache\": \"^1.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/chalk\": {\n      \"version\": \"0.5.1\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz\",\n      \"integrity\": \"sha512-bIKA54hP8iZhyDT81TOsJiQvR1gW+ZYSXFaZUAvoD4wCHdbHY2actmpTE4x344ZlFqHbvoxKOaESULTZN2gstg==\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^1.1.0\",\n        \"escape-string-regexp\": \"^1.0.0\",\n        \"has-ansi\": \"^0.1.0\",\n        \"strip-ansi\": \"^0.3.0\",\n        \"supports-color\": \"^0.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/circular-json\": {\n      \"version\": \"0.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz\",\n      \"integrity\": \"sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==\",\n      \"deprecated\": \"CircularJSON is in maintenance only, flatted is its successor.\"\n    },\n    \"node_modules/cli-cursor\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz\",\n      \"integrity\": \"sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A==\",\n      \"dependencies\": {\n        \"restore-cursor\": \"^1.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/cli-width\": {\n      \"version\": \"2.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz\",\n      \"integrity\": \"sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==\"\n    },\n    \"node_modules/cliui\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz\",\n      \"integrity\": \"sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==\",\n      \"dependencies\": {\n        \"center-align\": \"^0.1.1\",\n        \"right-align\": \"^0.1.1\",\n        \"wordwrap\": \"0.0.2\"\n      }\n    },\n    \"node_modules/cliui/node_modules/wordwrap\": {\n      \"version\": \"0.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz\",\n      \"integrity\": \"sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==\",\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/clone\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/clone/-/clone-0.1.19.tgz\",\n      \"integrity\": \"sha512-IO78I0y6JcSpEPHzK4obKdsL7E7oLdRVDVOLwr2Hkbjsb+Eoz0dxW6tef0WizoKu0gLC4oZSZuEF4U2K6w1WQw==\",\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/co\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/co/-/co-3.1.0.tgz\",\n      \"integrity\": \"sha512-CQsjCRiNObI8AtTsNIBDRMQ4oMR83CzEswHYahClvul7gKk+lDQiOKv+5qh7LQWf5sh6jkZNispz/QlsZxyNgA==\"\n    },\n    \"node_modules/co-from-stream\": {\n      \"version\": \"0.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/co-from-stream/-/co-from-stream-0.0.0.tgz\",\n      \"integrity\": \"sha512-w1GOkQmvYMWr5B3VsjyS/gxXd5YLhy4wcC1YxwajoGgMFJQLSsuzTxb6o9SiK+TMKN+DRJpsj4MN0CeOKSDAQA==\",\n      \"dependencies\": {\n        \"co-read\": \"0.0.1\"\n      }\n    },\n    \"node_modules/co-fs\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/co-fs/-/co-fs-1.2.0.tgz\",\n      \"integrity\": \"sha512-a7pWQjGecVliQyvP5ZU3vMz+7D1S23jiScOiH+AWW269wdN6JjvDmjEOyfGnJH9+DBQZKkPc/QVfqBgVOwjhng==\",\n      \"dependencies\": {\n        \"co-from-stream\": \"0.0.0\",\n        \"thunkify\": \"0.0.1\"\n      }\n    },\n    \"node_modules/co-fs-extra\": {\n      \"version\": \"0.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/co-fs-extra/-/co-fs-extra-0.0.2.tgz\",\n      \"integrity\": \"sha512-97AQYzklzxAnjgNphbYRG89B5f5j/jl5JIGrxbUpeThA0ZoLa3HgMZKhG/u7GBQN6DhIOeOm/WVwtvAgmKI72A==\",\n      \"dependencies\": {\n        \"co-fs\": \"1.2.0\",\n        \"fs-extra\": \"0.12.0\",\n        \"thunkify\": \"2.1.2\"\n      }\n    },\n    \"node_modules/co-fs-extra/node_modules/fs-extra\": {\n      \"version\": \"0.12.0\",\n      \"resolved\": \"https://registry.npmjs.org/fs-extra/-/fs-extra-0.12.0.tgz\",\n      \"integrity\": \"sha512-1NVXFJOe5DBL5uqoELqGkYaakGhAuCZmcLz7yJSzfgdfvmaHxadegUiqSXkzzsBqiA/Oy01wJnLoIT26LfEPtA==\",\n      \"dependencies\": {\n        \"jsonfile\": \"^2.0.0\",\n        \"mkdirp\": \"^0.5.0\",\n        \"ncp\": \"^0.6.0\",\n        \"rimraf\": \"^2.2.8\"\n      }\n    },\n    \"node_modules/co-fs/node_modules/thunkify\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/thunkify/-/thunkify-0.0.1.tgz\",\n      \"integrity\": \"sha512-PlJsf0UYJw6Z4bdCYljva3x5tI1y8Imc7hsaVauy099I0dg5bFF6Y0FL7C1Dn7n3vIumN9GNuEgzsQbzucYJrA==\"\n    },\n    \"node_modules/co-read\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/co-read/-/co-read-0.0.1.tgz\",\n      \"integrity\": \"sha512-OLceyyztHxwNtjuS2NjQ3QlczQIwOIW+n18DXAk89ej0wDso3exNvNrB7A3AiTVvFNEFe8LdqETIvRhtpkvLeA==\"\n    },\n    \"node_modules/code-point-at\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz\",\n      \"integrity\": \"sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/coffee-script\": {\n      \"version\": \"1.12.7\",\n      \"resolved\": \"https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz\",\n      \"integrity\": \"sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==\",\n      \"deprecated\": \"CoffeeScript on NPM has moved to \\\"coffeescript\\\" (no hyphen)\",\n      \"bin\": {\n        \"cake\": \"bin/cake\",\n        \"coffee\": \"bin/coffee\"\n      },\n      \"engines\": {\n        \"node\": \">=0.8.0\"\n      }\n    },\n    \"node_modules/colors\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/colors/-/colors-1.0.3.tgz\",\n      \"integrity\": \"sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==\",\n      \"dev\": true,\n      \"engines\": {\n        \"node\": \">=0.1.90\"\n      }\n    },\n    \"node_modules/combined-stream\": {\n      \"version\": \"1.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz\",\n      \"integrity\": \"sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"delayed-stream\": \"~1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/commander\": {\n      \"version\": \"2.20.3\",\n      \"resolved\": \"https://registry.npmjs.org/commander/-/commander-2.20.3.tgz\",\n      \"integrity\": \"sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==\"\n    },\n    \"node_modules/commoner\": {\n      \"version\": \"0.10.8\",\n      \"resolved\": \"https://registry.npmjs.org/commoner/-/commoner-0.10.8.tgz\",\n      \"integrity\": \"sha512-3/qHkNMM6o/KGXHITA14y78PcfmXh4+AOCJpSoF73h4VY1JpdGv3CHMS5+JW6SwLhfJt4RhNmLAa7+RRX/62EQ==\",\n      \"dependencies\": {\n        \"commander\": \"^2.5.0\",\n        \"detective\": \"^4.3.1\",\n        \"glob\": \"^5.0.15\",\n        \"graceful-fs\": \"^4.1.2\",\n        \"iconv-lite\": \"^0.4.5\",\n        \"mkdirp\": \"^0.5.0\",\n        \"private\": \"^0.1.6\",\n        \"q\": \"^1.1.2\",\n        \"recast\": \"^0.11.17\"\n      },\n      \"bin\": {\n        \"commonize\": \"bin/commonize\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/commoner/node_modules/esprima\": {\n      \"version\": \"3.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz\",\n      \"integrity\": \"sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg==\",\n      \"bin\": {\n        \"esparse\": \"bin/esparse.js\",\n        \"esvalidate\": \"bin/esvalidate.js\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/commoner/node_modules/glob\": {\n      \"version\": \"5.0.15\",\n      \"resolved\": \"https://registry.npmjs.org/glob/-/glob-5.0.15.tgz\",\n      \"integrity\": \"sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==\",\n      \"deprecated\": \"Glob versions prior to v9 are no longer supported\",\n      \"dependencies\": {\n        \"inflight\": \"^1.0.4\",\n        \"inherits\": \"2\",\n        \"minimatch\": \"2 || 3\",\n        \"once\": \"^1.3.0\",\n        \"path-is-absolute\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/commoner/node_modules/recast\": {\n      \"version\": \"0.11.23\",\n      \"resolved\": \"https://registry.npmjs.org/recast/-/recast-0.11.23.tgz\",\n      \"integrity\": \"sha512-+nixG+3NugceyR8O1bLU45qs84JgI3+8EauyRZafLgC9XbdAOIVgwV1Pe2da0YzGo62KzWoZwUpVEQf6qNAXWA==\",\n      \"dependencies\": {\n        \"ast-types\": \"0.9.6\",\n        \"esprima\": \"~3.1.0\",\n        \"private\": \"~0.1.5\",\n        \"source-map\": \"~0.5.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/concat-map\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz\",\n      \"integrity\": \"sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==\"\n    },\n    \"node_modules/concat-stream\": {\n      \"version\": \"1.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz\",\n      \"integrity\": \"sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==\",\n      \"engines\": [\n        \"node >= 0.8\"\n      ],\n      \"dependencies\": {\n        \"buffer-from\": \"^1.0.0\",\n        \"inherits\": \"^2.0.3\",\n        \"readable-stream\": \"^2.2.2\",\n        \"typedarray\": \"^0.0.6\"\n      }\n    },\n    \"node_modules/consolidate\": {\n      \"version\": \"0.14.5\",\n      \"resolved\": \"https://registry.npmjs.org/consolidate/-/consolidate-0.14.5.tgz\",\n      \"integrity\": \"sha512-PZFskfj64QnpKVK9cPdY36pyWEhZNM+srRVqtwMiVTlnViSoZcvX35PpBhhUcyLTHXYvz7pZRmxvsqwzJqg9kA==\",\n      \"deprecated\": \"Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog\",\n      \"dependencies\": {\n        \"bluebird\": \"^3.1.1\"\n      }\n    },\n    \"node_modules/cookies\": {\n      \"version\": \"0.8.0\",\n      \"resolved\": \"https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz\",\n      \"integrity\": \"sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==\",\n      \"dependencies\": {\n        \"depd\": \"~2.0.0\",\n        \"keygrip\": \"~1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/core-util-is\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz\",\n      \"integrity\": \"sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==\"\n    },\n    \"node_modules/corser\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/corser/-/corser-2.0.1.tgz\",\n      \"integrity\": \"sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==\",\n      \"dev\": true,\n      \"engines\": {\n        \"node\": \">= 0.4.0\"\n      }\n    },\n    \"node_modules/cryptiles\": {\n      \"version\": \"2.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz\",\n      \"integrity\": \"sha512-FFN5KwpvvQTTS5hWPxrU8/QE4kQUc6uwZcrnlMBN82t1MgAtq8mnoDwINBly9Tdr02seeIIhtdF+UH1feBYGog==\",\n      \"deprecated\": \"This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"boom\": \"2.x.x\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.40\"\n      }\n    },\n    \"node_modules/d\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/d/-/d-1.0.1.tgz\",\n      \"integrity\": \"sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==\",\n      \"dependencies\": {\n        \"es5-ext\": \"^0.10.50\",\n        \"type\": \"^1.0.1\"\n      }\n    },\n    \"node_modules/dashdash\": {\n      \"version\": \"1.14.1\",\n      \"resolved\": \"https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz\",\n      \"integrity\": \"sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"assert-plus\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/dashdash/node_modules/assert-plus\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz\",\n      \"integrity\": \"sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=0.8\"\n      }\n    },\n    \"node_modules/debug\": {\n      \"version\": \"0.8.1\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-0.8.1.tgz\",\n      \"integrity\": \"sha512-HlXEJm99YsRjLJ8xmuz0Lq8YUwrv7hAJkTEr6/Em3sUlSUNl0UdFA+1SrY4fnykeq1FVkUEUtwRGHs9VvlYbGA==\",\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/decamelize\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz\",\n      \"integrity\": \"sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/deep-is\": {\n      \"version\": \"0.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz\",\n      \"integrity\": \"sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==\"\n    },\n    \"node_modules/defined\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/defined/-/defined-1.0.1.tgz\",\n      \"integrity\": \"sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/defs\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/defs/-/defs-1.1.1.tgz\",\n      \"integrity\": \"sha512-KgGV1vmSa2UPKeDXaWE5FiXjix8BOtYMgiPGpYhd/42wxiC6YGwtscj/zU9gD5/xk4K2iLDpyGhGA5puZxaeMg==\",\n      \"dependencies\": {\n        \"alter\": \"~0.2.0\",\n        \"ast-traverse\": \"~0.1.1\",\n        \"breakable\": \"~1.0.0\",\n        \"esprima-fb\": \"~15001.1001.0-dev-harmony-fb\",\n        \"simple-fmt\": \"~0.1.0\",\n        \"simple-is\": \"~0.2.0\",\n        \"stringmap\": \"~0.2.2\",\n        \"stringset\": \"~0.2.1\",\n        \"tryor\": \"~0.1.2\",\n        \"yargs\": \"~3.27.0\"\n      },\n      \"bin\": {\n        \"defs\": \"build/es5/defs\"\n      }\n    },\n    \"node_modules/delayed-stream\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz\",\n      \"integrity\": \"sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/depd\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/depd/-/depd-2.0.0.tgz\",\n      \"integrity\": \"sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==\",\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/detective\": {\n      \"version\": \"4.7.1\",\n      \"resolved\": \"https://registry.npmjs.org/detective/-/detective-4.7.1.tgz\",\n      \"integrity\": \"sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==\",\n      \"dependencies\": {\n        \"acorn\": \"^5.2.1\",\n        \"defined\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/doctrine\": {\n      \"version\": \"1.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz\",\n      \"integrity\": \"sha512-lsGyRuYr4/PIB0txi+Fy2xOMI2dGaTguCaotzFGkVZuKR5usKfcRWIFKNM3QNrU7hh/+w2bwTW+ZeXPK5l8uVg==\",\n      \"dependencies\": {\n        \"esutils\": \"^2.0.2\",\n        \"isarray\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/ecc-jsbn\": {\n      \"version\": \"0.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz\",\n      \"integrity\": \"sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"jsbn\": \"~0.1.0\",\n        \"safer-buffer\": \"^2.1.0\"\n      }\n    },\n    \"node_modules/ecstatic\": {\n      \"version\": \"1.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/ecstatic/-/ecstatic-1.4.1.tgz\",\n      \"integrity\": \"sha512-VC4nmLt4/3KmKbSXxREmJMs+iAB+VdOzMyugV4YzG6d/2NcHa+b0J5/zKGRWS7efwNLEZP2AIodUUm12wOglng==\",\n      \"deprecated\": \"This package is unmaintained and deprecated. See the GH Issue 259.\",\n      \"dev\": true,\n      \"dependencies\": {\n        \"he\": \"^0.5.0\",\n        \"mime\": \"^1.2.11\",\n        \"minimist\": \"^1.1.0\",\n        \"url-join\": \"^1.0.0\"\n      },\n      \"bin\": {\n        \"ecstatic\": \"lib/ecstatic.js\"\n      }\n    },\n    \"node_modules/ect\": {\n      \"version\": \"0.5.9\",\n      \"resolved\": \"https://registry.npmjs.org/ect/-/ect-0.5.9.tgz\",\n      \"integrity\": \"sha512-acU9Ms8ivH7/8V9MmN/QjkmwnfmW5wsjXCNdwxru1YaqlSLVwGN/mhUa297JHf7gnvc8dBfzbqIDgnvU4zQ6nw==\",\n      \"dependencies\": {\n        \"coffee-script\": \">= 1.5.0\",\n        \"optimist\": \"0.4.0\"\n      },\n      \"bin\": {\n        \"ect\": \"bin/ect\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4.0\"\n      }\n    },\n    \"node_modules/errno\": {\n      \"version\": \"0.1.8\",\n      \"resolved\": \"https://registry.npmjs.org/errno/-/errno-0.1.8.tgz\",\n      \"integrity\": \"sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"prr\": \"~1.0.1\"\n      },\n      \"bin\": {\n        \"errno\": \"cli.js\"\n      }\n    },\n    \"node_modules/es5-ext\": {\n      \"version\": \"0.10.62\",\n      \"resolved\": \"https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz\",\n      \"integrity\": \"sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==\",\n      \"hasInstallScript\": true,\n      \"dependencies\": {\n        \"es6-iterator\": \"^2.0.3\",\n        \"es6-symbol\": \"^3.1.3\",\n        \"next-tick\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/es6-iterator\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz\",\n      \"integrity\": \"sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==\",\n      \"dependencies\": {\n        \"d\": \"1\",\n        \"es5-ext\": \"^0.10.35\",\n        \"es6-symbol\": \"^3.1.1\"\n      }\n    },\n    \"node_modules/es6-map\": {\n      \"version\": \"0.1.5\",\n      \"resolved\": \"https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz\",\n      \"integrity\": \"sha512-mz3UqCh0uPCIqsw1SSAkB/p0rOzF/M0V++vyN7JqlPtSW/VsYgQBvVvqMLmfBuyMzTpLnNqi6JmcSizs4jy19A==\",\n      \"dependencies\": {\n        \"d\": \"1\",\n        \"es5-ext\": \"~0.10.14\",\n        \"es6-iterator\": \"~2.0.1\",\n        \"es6-set\": \"~0.1.5\",\n        \"es6-symbol\": \"~3.1.1\",\n        \"event-emitter\": \"~0.3.5\"\n      }\n    },\n    \"node_modules/es6-set\": {\n      \"version\": \"0.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/es6-set/-/es6-set-0.1.6.tgz\",\n      \"integrity\": \"sha512-TE3LgGLDIBX332jq3ypv6bcOpkLO0AslAQo7p2VqX/1N46YNsvIWgvjojjSEnWEGWMhr1qUbYeTSir5J6mFHOw==\",\n      \"dependencies\": {\n        \"d\": \"^1.0.1\",\n        \"es5-ext\": \"^0.10.62\",\n        \"es6-iterator\": \"~2.0.3\",\n        \"es6-symbol\": \"^3.1.3\",\n        \"event-emitter\": \"^0.3.5\",\n        \"type\": \"^2.7.2\"\n      },\n      \"engines\": {\n        \"node\": \">=0.12\"\n      }\n    },\n    \"node_modules/es6-set/node_modules/type\": {\n      \"version\": \"2.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/type/-/type-2.7.2.tgz\",\n      \"integrity\": \"sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==\"\n    },\n    \"node_modules/es6-symbol\": {\n      \"version\": \"3.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz\",\n      \"integrity\": \"sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==\",\n      \"dependencies\": {\n        \"d\": \"^1.0.1\",\n        \"ext\": \"^1.1.2\"\n      }\n    },\n    \"node_modules/es6-weak-map\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz\",\n      \"integrity\": \"sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==\",\n      \"dependencies\": {\n        \"d\": \"1\",\n        \"es5-ext\": \"^0.10.46\",\n        \"es6-iterator\": \"^2.0.3\",\n        \"es6-symbol\": \"^3.1.1\"\n      }\n    },\n    \"node_modules/escape-string-regexp\": {\n      \"version\": \"1.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz\",\n      \"integrity\": \"sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==\",\n      \"engines\": {\n        \"node\": \">=0.8.0\"\n      }\n    },\n    \"node_modules/escope\": {\n      \"version\": \"3.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/escope/-/escope-3.6.0.tgz\",\n      \"integrity\": \"sha512-75IUQsusDdalQEW/G/2esa87J7raqdJF+Ca0/Xm5C3Q58Nr4yVYjZGp/P1+2xiEVgXRrA39dpRb8LcshajbqDQ==\",\n      \"dependencies\": {\n        \"es6-map\": \"^0.1.3\",\n        \"es6-weak-map\": \"^2.0.1\",\n        \"esrecurse\": \"^4.1.0\",\n        \"estraverse\": \"^4.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/eslint\": {\n      \"version\": \"2.13.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz\",\n      \"integrity\": \"sha512-29PFGeV6lLQrPaPHeCkjfgLRQPFflDiicoNZOw+c/JkaQ0Am55yUICdYZbmCiM+DSef+q7oCercimHvjNI0GAw==\",\n      \"deprecated\": \"This version is no longer supported. Please see https://eslint.org/version-support for other options.\",\n      \"dependencies\": {\n        \"chalk\": \"^1.1.3\",\n        \"concat-stream\": \"^1.4.6\",\n        \"debug\": \"^2.1.1\",\n        \"doctrine\": \"^1.2.2\",\n        \"es6-map\": \"^0.1.3\",\n        \"escope\": \"^3.6.0\",\n        \"espree\": \"^3.1.6\",\n        \"estraverse\": \"^4.2.0\",\n        \"esutils\": \"^2.0.2\",\n        \"file-entry-cache\": \"^1.1.1\",\n        \"glob\": \"^7.0.3\",\n        \"globals\": \"^9.2.0\",\n        \"ignore\": \"^3.1.2\",\n        \"imurmurhash\": \"^0.1.4\",\n        \"inquirer\": \"^0.12.0\",\n        \"is-my-json-valid\": \"^2.10.0\",\n        \"is-resolvable\": \"^1.0.0\",\n        \"js-yaml\": \"^3.5.1\",\n        \"json-stable-stringify\": \"^1.0.0\",\n        \"levn\": \"^0.3.0\",\n        \"lodash\": \"^4.0.0\",\n        \"mkdirp\": \"^0.5.0\",\n        \"optionator\": \"^0.8.1\",\n        \"path-is-absolute\": \"^1.0.0\",\n        \"path-is-inside\": \"^1.0.1\",\n        \"pluralize\": \"^1.2.1\",\n        \"progress\": \"^1.1.8\",\n        \"require-uncached\": \"^1.0.2\",\n        \"shelljs\": \"^0.6.0\",\n        \"strip-json-comments\": \"~1.0.1\",\n        \"table\": \"^3.7.8\",\n        \"text-table\": \"~0.2.0\",\n        \"user-home\": \"^2.0.0\"\n      },\n      \"bin\": {\n        \"eslint\": \"bin/eslint.js\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/eslint/node_modules/ansi-regex\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz\",\n      \"integrity\": \"sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/eslint/node_modules/ansi-styles\": {\n      \"version\": \"2.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz\",\n      \"integrity\": \"sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/eslint/node_modules/chalk\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz\",\n      \"integrity\": \"sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^2.2.1\",\n        \"escape-string-regexp\": \"^1.0.2\",\n        \"has-ansi\": \"^2.0.0\",\n        \"strip-ansi\": \"^3.0.0\",\n        \"supports-color\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/eslint/node_modules/debug\": {\n      \"version\": \"2.6.9\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-2.6.9.tgz\",\n      \"integrity\": \"sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==\",\n      \"dependencies\": {\n        \"ms\": \"2.0.0\"\n      }\n    },\n    \"node_modules/eslint/node_modules/globals\": {\n      \"version\": \"9.18.0\",\n      \"resolved\": \"https://registry.npmjs.org/globals/-/globals-9.18.0.tgz\",\n      \"integrity\": \"sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/eslint/node_modules/has-ansi\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz\",\n      \"integrity\": \"sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/eslint/node_modules/lodash\": {\n      \"version\": \"4.17.21\",\n      \"resolved\": \"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz\",\n      \"integrity\": \"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==\"\n    },\n    \"node_modules/eslint/node_modules/strip-ansi\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz\",\n      \"integrity\": \"sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/eslint/node_modules/supports-color\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz\",\n      \"integrity\": \"sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==\",\n      \"engines\": {\n        \"node\": \">=0.8.0\"\n      }\n    },\n    \"node_modules/espree\": {\n      \"version\": \"3.5.4\",\n      \"resolved\": \"https://registry.npmjs.org/espree/-/espree-3.5.4.tgz\",\n      \"integrity\": \"sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==\",\n      \"dependencies\": {\n        \"acorn\": \"^5.5.0\",\n        \"acorn-jsx\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/esprima-fb\": {\n      \"version\": \"15001.1001.0-dev-harmony-fb\",\n      \"resolved\": \"https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz\",\n      \"integrity\": \"sha512-m7OsYzocA8OQ3+9CxmhIv7NPHtyDR2ixaLCO7kLZ+YH+xQ/BpaZmll9EXmc+kBxzWA8BRBXbNEuEQqQ6vfsgDw==\",\n      \"bin\": {\n        \"esparse\": \"bin/esparse.js\",\n        \"esvalidate\": \"bin/esvalidate.js\"\n      },\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/esrecurse\": {\n      \"version\": \"4.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz\",\n      \"integrity\": \"sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==\",\n      \"dependencies\": {\n        \"estraverse\": \"^5.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/esrecurse/node_modules/estraverse\": {\n      \"version\": \"5.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz\",\n      \"integrity\": \"sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==\",\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/estraverse\": {\n      \"version\": \"4.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz\",\n      \"integrity\": \"sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==\",\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/esutils\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz\",\n      \"integrity\": \"sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/event-emitter\": {\n      \"version\": \"0.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz\",\n      \"integrity\": \"sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==\",\n      \"dependencies\": {\n        \"d\": \"1\",\n        \"es5-ext\": \"~0.10.14\"\n      }\n    },\n    \"node_modules/eventemitter3\": {\n      \"version\": \"4.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz\",\n      \"integrity\": \"sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==\",\n      \"dev\": true\n    },\n    \"node_modules/exit-hook\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz\",\n      \"integrity\": \"sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/ext\": {\n      \"version\": \"1.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/ext/-/ext-1.7.0.tgz\",\n      \"integrity\": \"sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==\",\n      \"dependencies\": {\n        \"type\": \"^2.7.2\"\n      }\n    },\n    \"node_modules/ext/node_modules/type\": {\n      \"version\": \"2.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/type/-/type-2.7.2.tgz\",\n      \"integrity\": \"sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==\"\n    },\n    \"node_modules/extend\": {\n      \"version\": \"3.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/extend/-/extend-3.0.2.tgz\",\n      \"integrity\": \"sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==\"\n    },\n    \"node_modules/extend-shallow\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz\",\n      \"integrity\": \"sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==\",\n      \"dependencies\": {\n        \"is-extendable\": \"^0.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/extsprintf\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz\",\n      \"integrity\": \"sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==\",\n      \"engines\": [\n        \"node >=0.6.0\"\n      ],\n      \"optional\": true\n    },\n    \"node_modules/fast-levenshtein\": {\n      \"version\": \"2.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz\",\n      \"integrity\": \"sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==\"\n    },\n    \"node_modules/figures\": {\n      \"version\": \"1.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/figures/-/figures-1.7.0.tgz\",\n      \"integrity\": \"sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==\",\n      \"dependencies\": {\n        \"escape-string-regexp\": \"^1.0.5\",\n        \"object-assign\": \"^4.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/file-entry-cache\": {\n      \"version\": \"1.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz\",\n      \"integrity\": \"sha512-JyVk7P0Hvw6uEAwH4Y0j+rZMvaMWvLBYRmRGAF2S6jKTycf0mMDcC7d21Y2KyrKJk3XI8YghSsk5KmRdbvg0VQ==\",\n      \"dependencies\": {\n        \"flat-cache\": \"^1.2.1\",\n        \"object-assign\": \"^4.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/flat-cache\": {\n      \"version\": \"1.3.4\",\n      \"resolved\": \"https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz\",\n      \"integrity\": \"sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==\",\n      \"dependencies\": {\n        \"circular-json\": \"^0.3.1\",\n        \"graceful-fs\": \"^4.1.2\",\n        \"rimraf\": \"~2.6.2\",\n        \"write\": \"^0.2.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/flat-cache/node_modules/rimraf\": {\n      \"version\": \"2.6.3\",\n      \"resolved\": \"https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz\",\n      \"integrity\": \"sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==\",\n      \"deprecated\": \"Rimraf versions prior to v4 are no longer supported\",\n      \"dependencies\": {\n        \"glob\": \"^7.1.3\"\n      },\n      \"bin\": {\n        \"rimraf\": \"bin.js\"\n      }\n    },\n    \"node_modules/follow-redirects\": {\n      \"version\": \"1.15.2\",\n      \"resolved\": \"https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz\",\n      \"integrity\": \"sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==\",\n      \"dev\": true,\n      \"funding\": [\n        {\n          \"type\": \"individual\",\n          \"url\": \"https://github.com/sponsors/RubenVerborgh\"\n        }\n      ],\n      \"engines\": {\n        \"node\": \">=4.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"debug\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/forever-agent\": {\n      \"version\": \"0.6.1\",\n      \"resolved\": \"https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz\",\n      \"integrity\": \"sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/form-data\": {\n      \"version\": \"2.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz\",\n      \"integrity\": \"sha512-8HWGSLAPr+AG0hBpsqi5Ob8HrLStN/LWeqhpFl14d7FJgHK48TmgLoALPz69XSUR65YJzDfLUX/BM8+MLJLghQ==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"asynckit\": \"^0.4.0\",\n        \"combined-stream\": \"^1.0.5\",\n        \"mime-types\": \"^2.1.12\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.12\"\n      }\n    },\n    \"node_modules/fs-extra\": {\n      \"version\": \"0.10.0\",\n      \"resolved\": \"https://registry.npmjs.org/fs-extra/-/fs-extra-0.10.0.tgz\",\n      \"integrity\": \"sha512-jaIHyMh8Vr7kbIVjLAFhI9BAvKckNU9uRXBt45OEUx8CTG2GjzOh5vWxxIQ5e2UK1kI4Jsm9DmqOf3IceaEvWg==\",\n      \"dependencies\": {\n        \"jsonfile\": \"^1.2.0\",\n        \"mkdirp\": \"^0.5.0\",\n        \"ncp\": \"^0.5.1\",\n        \"rimraf\": \"^2.2.8\"\n      }\n    },\n    \"node_modules/fs-extra/node_modules/jsonfile\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/jsonfile/-/jsonfile-1.2.0.tgz\",\n      \"integrity\": \"sha512-9Re1pI74qhRL7j94lp/Y69OqCR+XkEMvjunFo70Dcr6kT5vmRcfyfdyZrxmm7j6yLySBRVooiUy32FvI4lcNTg==\"\n    },\n    \"node_modules/fs-extra/node_modules/ncp\": {\n      \"version\": \"0.5.1\",\n      \"resolved\": \"https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz\",\n      \"integrity\": \"sha512-l+pJxuLlzwp11Dy72MJgCPNwIbXdv6imaACLiEMb2TIDyr54qz+nAZeD5qDlJefveaJ+R9Ug6KuozCxRpQXO0Q==\",\n      \"bin\": {\n        \"ncp\": \"bin/ncp\"\n      }\n    },\n    \"node_modules/fs-readdir-recursive\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz\",\n      \"integrity\": \"sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==\"\n    },\n    \"node_modules/fs.realpath\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz\",\n      \"integrity\": \"sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==\"\n    },\n    \"node_modules/generate-function\": {\n      \"version\": \"2.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz\",\n      \"integrity\": \"sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==\",\n      \"dependencies\": {\n        \"is-property\": \"^1.0.2\"\n      }\n    },\n    \"node_modules/generate-object-property\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz\",\n      \"integrity\": \"sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==\",\n      \"dependencies\": {\n        \"is-property\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/getpass\": {\n      \"version\": \"0.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz\",\n      \"integrity\": \"sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"assert-plus\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/getpass/node_modules/assert-plus\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz\",\n      \"integrity\": \"sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=0.8\"\n      }\n    },\n    \"node_modules/glob\": {\n      \"version\": \"7.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/glob/-/glob-7.2.3.tgz\",\n      \"integrity\": \"sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==\",\n      \"deprecated\": \"Glob versions prior to v9 are no longer supported\",\n      \"dependencies\": {\n        \"fs.realpath\": \"^1.0.0\",\n        \"inflight\": \"^1.0.4\",\n        \"inherits\": \"2\",\n        \"minimatch\": \"^3.1.1\",\n        \"once\": \"^1.3.0\",\n        \"path-is-absolute\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/gnode\": {\n      \"version\": \"0.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/gnode/-/gnode-0.1.2.tgz\",\n      \"integrity\": \"sha512-I6s6Pt/6kiGubgn/jezM5AediSwpyPWzSg/ZwTOF/wHlpDuGuAMuLiUcHO6Yq/YwHRMDqaWDY1fyHwRF05Zvag==\",\n      \"dependencies\": {\n        \"regenerator\": \"~0.8.8\"\n      },\n      \"bin\": {\n        \"gnode\": \"bin/gnode\"\n      }\n    },\n    \"node_modules/graceful-fs\": {\n      \"version\": \"4.2.11\",\n      \"resolved\": \"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz\",\n      \"integrity\": \"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\"\n    },\n    \"node_modules/gray-matter\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/gray-matter/-/gray-matter-2.1.1.tgz\",\n      \"integrity\": \"sha512-vbmvP1Fe/fxuT2QuLVcqb2BfK7upGhhbLIt9/owWEvPYrZZEkelLcq2HqzxosV+PQ67dUFLaAeNpH7C4hhICAA==\",\n      \"dependencies\": {\n        \"ansi-red\": \"^0.1.1\",\n        \"coffee-script\": \"^1.12.4\",\n        \"extend-shallow\": \"^2.0.1\",\n        \"js-yaml\": \"^3.8.1\",\n        \"toml\": \"^2.3.2\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/har-schema\": {\n      \"version\": \"1.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz\",\n      \"integrity\": \"sha512-f8xf2GOR6Rgwc9FPTLNzgwB+JQ2/zMauYXSWmX5YV5acex6VomT0ocSuwR7BfXo5MpHi+jL+saaux2fwsGJDKQ==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/har-validator\": {\n      \"version\": \"4.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz\",\n      \"integrity\": \"sha512-5Gbp6RAftMYYV3UEI4c4Vv3+a4dQ7taVyvHt+/L6kRt+f4HX1GweAk5UDWN0SvdVnRBzGQ6OG89pGaD9uSFnVw==\",\n      \"deprecated\": \"this library is no longer supported\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"ajv\": \"^4.9.1\",\n        \"har-schema\": \"^1.0.5\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/has-ansi\": {\n      \"version\": \"0.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz\",\n      \"integrity\": \"sha512-1YsTg1fk2/6JToQhtZkArMkurq8UoWU1Qe0aR3VUHjgij4nOylSWLWAtBXoZ4/dXOmugfLGm1c+QhuD0JyedFA==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^0.2.0\"\n      },\n      \"bin\": {\n        \"has-ansi\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/hawk\": {\n      \"version\": \"3.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz\",\n      \"integrity\": \"sha512-X8xbmTc1cbPXcQV4WkLcRMALuyoxhfpFATmyuCxJPOAvrDS4DNnsTAOmKUxMTOWU6TzrTOkxPKwIx5ZOpJVSrg==\",\n      \"deprecated\": \"This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"boom\": \"2.x.x\",\n        \"cryptiles\": \"2.x.x\",\n        \"hoek\": \"2.x.x\",\n        \"sntp\": \"1.x.x\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.32\"\n      }\n    },\n    \"node_modules/he\": {\n      \"version\": \"0.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/he/-/he-0.5.0.tgz\",\n      \"integrity\": \"sha512-DoufbNNOFzwRPy8uecq+j+VCPQ+JyDelHTmSgygrA5TsR8Cbw4Qcir5sGtWiusB4BdT89nmlaVDhSJOqC/33vw==\",\n      \"dev\": true,\n      \"bin\": {\n        \"he\": \"bin/he\"\n      }\n    },\n    \"node_modules/hoek\": {\n      \"version\": \"2.16.3\",\n      \"resolved\": \"https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz\",\n      \"integrity\": \"sha512-V6Yw1rIcYV/4JsnggjBU0l4Kr+EXhpwqXRusENU1Xx6ro00IHPHYNynCuBTOZAPlr3AAmLvchH9I7N/VUdvOwQ==\",\n      \"deprecated\": \"This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).\",\n      \"engines\": {\n        \"node\": \">=0.10.40\"\n      }\n    },\n    \"node_modules/http-proxy\": {\n      \"version\": \"1.18.1\",\n      \"resolved\": \"https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz\",\n      \"integrity\": \"sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==\",\n      \"dev\": true,\n      \"dependencies\": {\n        \"eventemitter3\": \"^4.0.0\",\n        \"follow-redirects\": \"^1.0.0\",\n        \"requires-port\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8.0.0\"\n      }\n    },\n    \"node_modules/http-server\": {\n      \"version\": \"0.9.0\",\n      \"resolved\": \"https://registry.npmjs.org/http-server/-/http-server-0.9.0.tgz\",\n      \"integrity\": \"sha512-ufsBg3xl4RRokW37Kg1uGS4MKkpfXqRrtuKYDT9QaBxqix9IHZ2QbOk2RDTOM8MJjWRzjozCC5heZX/azjKB6Q==\",\n      \"dev\": true,\n      \"dependencies\": {\n        \"colors\": \"1.0.3\",\n        \"corser\": \"~2.0.0\",\n        \"ecstatic\": \"^1.4.0\",\n        \"http-proxy\": \"^1.8.1\",\n        \"opener\": \"~1.4.0\",\n        \"optimist\": \"0.6.x\",\n        \"portfinder\": \"0.4.x\",\n        \"union\": \"~0.4.3\"\n      },\n      \"bin\": {\n        \"hs\": \"bin/http-server\",\n        \"http-server\": \"bin/http-server\"\n      }\n    },\n    \"node_modules/http-server/node_modules/minimist\": {\n      \"version\": \"0.0.10\",\n      \"resolved\": \"https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz\",\n      \"integrity\": \"sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==\",\n      \"dev\": true\n    },\n    \"node_modules/http-server/node_modules/optimist\": {\n      \"version\": \"0.6.1\",\n      \"resolved\": \"https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz\",\n      \"integrity\": \"sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==\",\n      \"dev\": true,\n      \"dependencies\": {\n        \"minimist\": \"~0.0.1\",\n        \"wordwrap\": \"~0.0.2\"\n      }\n    },\n    \"node_modules/http-signature\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz\",\n      \"integrity\": \"sha512-iUn0NcRULlDGtqNLN1Jxmzayk8ogm7NToldASyZBpM2qggbphjXzNOiw3piN8tgz+e/DRs6X5gAzFwTI6BCRcg==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"assert-plus\": \"^0.2.0\",\n        \"jsprim\": \"^1.2.2\",\n        \"sshpk\": \"^1.7.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.8\",\n        \"npm\": \">=1.3.7\"\n      }\n    },\n    \"node_modules/i18next\": {\n      \"version\": \"1.10.6\",\n      \"resolved\": \"https://registry.npmjs.org/i18next/-/i18next-1.10.6.tgz\",\n      \"integrity\": \"sha512-dWqoNEjzG+Dxgm0gxJlSsNu/PUn6d8wXmCEsY1mJgwCsTE/5hac29krq92IPpj59dKypD59bMJyNAd0VYvvIXw==\",\n      \"dependencies\": {\n        \"cookies\": \">= 0.2.2\",\n        \"i18next-client\": \"1.10.3\",\n        \"json5\": \"^0.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4.0\"\n      }\n    },\n    \"node_modules/i18next-client\": {\n      \"version\": \"1.10.3\",\n      \"resolved\": \"https://registry.npmjs.org/i18next-client/-/i18next-client-1.10.3.tgz\",\n      \"integrity\": \"sha512-f0Hmy8ES4B5+PolvMbu9TyATA8Sc9klHW9QwDGNTwqoN8A090BECCz3/e6yePF/HNDvAQgwTxpGQgA3+qfWTeA==\",\n      \"deprecated\": \"you can use npm install i18next from version 2.0.0\",\n      \"engines\": {\n        \"node\": \">=0.4.12\"\n      }\n    },\n    \"node_modules/iconv-lite\": {\n      \"version\": \"0.4.24\",\n      \"resolved\": \"https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz\",\n      \"integrity\": \"sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==\",\n      \"dependencies\": {\n        \"safer-buffer\": \">= 2.1.2 < 3\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/ignore\": {\n      \"version\": \"3.3.10\",\n      \"resolved\": \"https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz\",\n      \"integrity\": \"sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==\"\n    },\n    \"node_modules/image-size\": {\n      \"version\": \"0.5.5\",\n      \"resolved\": \"https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz\",\n      \"integrity\": \"sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==\",\n      \"optional\": true,\n      \"bin\": {\n        \"image-size\": \"bin/image-size.js\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/imurmurhash\": {\n      \"version\": \"0.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz\",\n      \"integrity\": \"sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==\",\n      \"engines\": {\n        \"node\": \">=0.8.19\"\n      }\n    },\n    \"node_modules/inflight\": {\n      \"version\": \"1.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz\",\n      \"integrity\": \"sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==\",\n      \"deprecated\": \"This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.\",\n      \"dependencies\": {\n        \"once\": \"^1.3.0\",\n        \"wrappy\": \"1\"\n      }\n    },\n    \"node_modules/inherits\": {\n      \"version\": \"2.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz\",\n      \"integrity\": \"sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==\"\n    },\n    \"node_modules/inquirer\": {\n      \"version\": \"0.12.0\",\n      \"resolved\": \"https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz\",\n      \"integrity\": \"sha512-bOetEz5+/WpgaW4D1NYOk1aD+JCqRjqu/FwRFgnIfiP7FC/zinsrfyO1vlS3nyH/R7S0IH3BIHBu4DBIDSqiGQ==\",\n      \"dependencies\": {\n        \"ansi-escapes\": \"^1.1.0\",\n        \"ansi-regex\": \"^2.0.0\",\n        \"chalk\": \"^1.0.0\",\n        \"cli-cursor\": \"^1.0.1\",\n        \"cli-width\": \"^2.0.0\",\n        \"figures\": \"^1.3.5\",\n        \"lodash\": \"^4.3.0\",\n        \"readline2\": \"^1.0.1\",\n        \"run-async\": \"^0.1.0\",\n        \"rx-lite\": \"^3.1.2\",\n        \"string-width\": \"^1.0.1\",\n        \"strip-ansi\": \"^3.0.0\",\n        \"through\": \"^2.3.6\"\n      }\n    },\n    \"node_modules/inquirer/node_modules/ansi-regex\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz\",\n      \"integrity\": \"sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/inquirer/node_modules/ansi-styles\": {\n      \"version\": \"2.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz\",\n      \"integrity\": \"sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/inquirer/node_modules/chalk\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz\",\n      \"integrity\": \"sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^2.2.1\",\n        \"escape-string-regexp\": \"^1.0.2\",\n        \"has-ansi\": \"^2.0.0\",\n        \"strip-ansi\": \"^3.0.0\",\n        \"supports-color\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/inquirer/node_modules/has-ansi\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz\",\n      \"integrity\": \"sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/inquirer/node_modules/lodash\": {\n      \"version\": \"4.17.21\",\n      \"resolved\": \"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz\",\n      \"integrity\": \"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==\"\n    },\n    \"node_modules/inquirer/node_modules/strip-ansi\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz\",\n      \"integrity\": \"sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/inquirer/node_modules/supports-color\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz\",\n      \"integrity\": \"sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==\",\n      \"engines\": {\n        \"node\": \">=0.8.0\"\n      }\n    },\n    \"node_modules/invert-kv\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz\",\n      \"integrity\": \"sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/is\": {\n      \"version\": \"2.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/is/-/is-2.2.1.tgz\",\n      \"integrity\": \"sha512-pLgrufBTE1FI6cnOT47mB1t8iBFYjKKB5g0NDH7gTE2WNqNMW56UBB6VbDJsf1EPFEdPUsk43WtsDW8MxJG10A==\",\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/is-buffer\": {\n      \"version\": \"1.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz\",\n      \"integrity\": \"sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==\"\n    },\n    \"node_modules/is-extendable\": {\n      \"version\": \"0.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz\",\n      \"integrity\": \"sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/is-fullwidth-code-point\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz\",\n      \"integrity\": \"sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==\",\n      \"dependencies\": {\n        \"number-is-nan\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/is-my-ip-valid\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz\",\n      \"integrity\": \"sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==\"\n    },\n    \"node_modules/is-my-json-valid\": {\n      \"version\": \"2.20.6\",\n      \"resolved\": \"https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz\",\n      \"integrity\": \"sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==\",\n      \"dependencies\": {\n        \"generate-function\": \"^2.0.0\",\n        \"generate-object-property\": \"^1.1.0\",\n        \"is-my-ip-valid\": \"^1.0.0\",\n        \"jsonpointer\": \"^5.0.0\",\n        \"xtend\": \"^4.0.0\"\n      }\n    },\n    \"node_modules/is-property\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz\",\n      \"integrity\": \"sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==\"\n    },\n    \"node_modules/is-resolvable\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz\",\n      \"integrity\": \"sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==\"\n    },\n    \"node_modules/is-typedarray\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz\",\n      \"integrity\": \"sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==\",\n      \"optional\": true\n    },\n    \"node_modules/is-utf8\": {\n      \"version\": \"0.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz\",\n      \"integrity\": \"sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==\"\n    },\n    \"node_modules/isarray\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz\",\n      \"integrity\": \"sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==\"\n    },\n    \"node_modules/isemail\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz\",\n      \"integrity\": \"sha512-pZMb1rDrWRAPtVY92VCxWtF+1gExWrCnao+GL1EKHx6z19ovW+xNcnC1iNB7WkbSYWlyl3uwlaH5eaBx2s2crw==\",\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/isstream\": {\n      \"version\": \"0.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz\",\n      \"integrity\": \"sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==\",\n      \"optional\": true\n    },\n    \"node_modules/joi\": {\n      \"version\": \"6.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/joi/-/joi-6.10.1.tgz\",\n      \"integrity\": \"sha512-K6+OwGaWM1sBEu+XMbgC4zDmg6hnddS2DWiCVtjnhkcrzv+ejSfh7HGUsoxmWQkv6kHEsVFAywttfkpmIE2QwQ==\",\n      \"deprecated\": \"This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).\",\n      \"dependencies\": {\n        \"hoek\": \"2.x.x\",\n        \"isemail\": \"1.x.x\",\n        \"moment\": \"2.x.x\",\n        \"topo\": \"1.x.x\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.40\",\n        \"npm\": \">=2.0.0\"\n      }\n    },\n    \"node_modules/js-yaml\": {\n      \"version\": \"3.14.1\",\n      \"resolved\": \"https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz\",\n      \"integrity\": \"sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==\",\n      \"dependencies\": {\n        \"argparse\": \"^1.0.7\",\n        \"esprima\": \"^4.0.0\"\n      },\n      \"bin\": {\n        \"js-yaml\": \"bin/js-yaml.js\"\n      }\n    },\n    \"node_modules/js-yaml/node_modules/esprima\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz\",\n      \"integrity\": \"sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==\",\n      \"bin\": {\n        \"esparse\": \"bin/esparse.js\",\n        \"esvalidate\": \"bin/esvalidate.js\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/jsbn\": {\n      \"version\": \"0.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz\",\n      \"integrity\": \"sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==\",\n      \"optional\": true\n    },\n    \"node_modules/json-schema\": {\n      \"version\": \"0.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz\",\n      \"integrity\": \"sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==\",\n      \"optional\": true\n    },\n    \"node_modules/json-stable-stringify\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz\",\n      \"integrity\": \"sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==\",\n      \"dependencies\": {\n        \"jsonify\": \"^0.0.1\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/json-stringify-safe\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz\",\n      \"integrity\": \"sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==\",\n      \"optional\": true\n    },\n    \"node_modules/json5\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/json5/-/json5-0.2.0.tgz\",\n      \"integrity\": \"sha512-jzu3hxGhztAzldgKTbsW240mtPIgR6foddu9HqQgpv0ML2RcjE0mjyLro0XE92YAQYpTpcByY80vVzlKOM64xA==\",\n      \"bin\": {\n        \"json5\": \"lib/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/jsonfile\": {\n      \"version\": \"2.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz\",\n      \"integrity\": \"sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==\",\n      \"optionalDependencies\": {\n        \"graceful-fs\": \"^4.1.6\"\n      }\n    },\n    \"node_modules/jsonify\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz\",\n      \"integrity\": \"sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/jsonpointer\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz\",\n      \"integrity\": \"sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/jsprim\": {\n      \"version\": \"1.4.2\",\n      \"resolved\": \"https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz\",\n      \"integrity\": \"sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"assert-plus\": \"1.0.0\",\n        \"extsprintf\": \"1.3.0\",\n        \"json-schema\": \"0.4.0\",\n        \"verror\": \"1.10.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.6.0\"\n      }\n    },\n    \"node_modules/jsprim/node_modules/assert-plus\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz\",\n      \"integrity\": \"sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=0.8\"\n      }\n    },\n    \"node_modules/keygrip\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz\",\n      \"integrity\": \"sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==\",\n      \"dependencies\": {\n        \"tsscmp\": \"1.0.6\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/kind-of\": {\n      \"version\": \"3.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz\",\n      \"integrity\": \"sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==\",\n      \"dependencies\": {\n        \"is-buffer\": \"^1.1.5\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/lazy-cache\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz\",\n      \"integrity\": \"sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/lcid\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz\",\n      \"integrity\": \"sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==\",\n      \"dependencies\": {\n        \"invert-kv\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/less\": {\n      \"version\": \"2.7.3\",\n      \"resolved\": \"https://registry.npmjs.org/less/-/less-2.7.3.tgz\",\n      \"integrity\": \"sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==\",\n      \"bin\": {\n        \"lessc\": \"bin/lessc\"\n      },\n      \"engines\": {\n        \"node\": \">=0.12\"\n      },\n      \"optionalDependencies\": {\n        \"errno\": \"^0.1.1\",\n        \"graceful-fs\": \"^4.1.2\",\n        \"image-size\": \"~0.5.0\",\n        \"mime\": \"^1.2.11\",\n        \"mkdirp\": \"^0.5.0\",\n        \"promise\": \"^7.1.1\",\n        \"request\": \"2.81.0\",\n        \"source-map\": \"^0.5.3\"\n      }\n    },\n    \"node_modules/levn\": {\n      \"version\": \"0.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/levn/-/levn-0.3.0.tgz\",\n      \"integrity\": \"sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==\",\n      \"dependencies\": {\n        \"prelude-ls\": \"~1.1.2\",\n        \"type-check\": \"~0.3.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/lodash\": {\n      \"version\": \"2.4.2\",\n      \"resolved\": \"https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz\",\n      \"integrity\": \"sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==\",\n      \"engines\": [\n        \"node\",\n        \"rhino\"\n      ]\n    },\n    \"node_modules/lodash.omit\": {\n      \"version\": \"4.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz\",\n      \"integrity\": \"sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==\",\n      \"deprecated\": \"This package is deprecated. Use destructuring assignment syntax instead.\"\n    },\n    \"node_modules/longest\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/longest/-/longest-1.0.1.tgz\",\n      \"integrity\": \"sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/lru-cache\": {\n      \"version\": \"2.7.3\",\n      \"resolved\": \"https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz\",\n      \"integrity\": \"sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==\"\n    },\n    \"node_modules/metalsmith\": {\n      \"version\": \"1.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/metalsmith/-/metalsmith-1.7.0.tgz\",\n      \"integrity\": \"sha512-JSu70FzGnvpYEyLS86yDComhCQq/e49pztJTmvT8AcpGEOJsk8DRW391+wYqokSyJdL9L4wnhZ5/KIt+PFQ3eA==\",\n      \"deprecated\": \"This version of Metalsmith is no longer supported\",\n      \"dependencies\": {\n        \"absolute\": \"0.0.1\",\n        \"async\": \"~0.9.0\",\n        \"chalk\": \"^0.5.0\",\n        \"clone\": \"^0.1.11\",\n        \"co-fs-extra\": \"0.0.2\",\n        \"commander\": \"^2.6.0\",\n        \"fs-extra\": \"~0.10.0\",\n        \"gnode\": \"^0.1.0\",\n        \"gray-matter\": \"^2.0.0\",\n        \"is\": \"^2.2.0\",\n        \"is-utf8\": \"~0.2.0\",\n        \"recursive-readdir\": \"^1.2.1\",\n        \"rimraf\": \"^2.2.8\",\n        \"stat-mode\": \"^0.2.0\",\n        \"thunkify\": \"^2.1.2\",\n        \"unyield\": \"0.0.1\",\n        \"ware\": \"^1.2.0\"\n      },\n      \"bin\": {\n        \"metalsmith\": \"bin/metalsmith\"\n      }\n    },\n    \"node_modules/metalsmith-concat\": {\n      \"version\": \"5.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/metalsmith-concat/-/metalsmith-concat-5.0.2.tgz\",\n      \"integrity\": \"sha512-GzLTXQh0Pv7HZQyAG776OYyeJY7iz/P5mnw2U/+2cilMl6LdSdkWYJ0q09g5uXar+VUy2ioYk5cJOwjfn6GkdQ==\",\n      \"dependencies\": {\n        \"async\": \"2.0.0-rc.2\",\n        \"glob\": \"7.0.3\",\n        \"minimatch\": \"3.0.0\"\n      }\n    },\n    \"node_modules/metalsmith-concat/node_modules/async\": {\n      \"version\": \"2.0.0-rc.2\",\n      \"resolved\": \"https://registry.npmjs.org/async/-/async-2.0.0-rc.2.tgz\",\n      \"integrity\": \"sha512-3DwnVa+sifQB22NIEPOHR7YeCTSvX3H63Sfytjt26V/EF0rmAyBw1QQRgUe2rjz7X+c1ilxBVXxFVv9T0/OBfw==\",\n      \"dependencies\": {\n        \"lodash\": \"^4.3.0\"\n      }\n    },\n    \"node_modules/metalsmith-concat/node_modules/balanced-match\": {\n      \"version\": \"0.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz\",\n      \"integrity\": \"sha512-bgB9RrUMd3G7drkg5+Gv+dMZTUSFbfrrp61qsQGlTdCdIPqdzF9UG2G5Ndlg6zR3ArNeGGXMIYSYFZRRtZaT9Q==\"\n    },\n    \"node_modules/metalsmith-concat/node_modules/brace-expansion\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz\",\n      \"integrity\": \"sha512-JzSkuJYnfzmR0jZiCE/Nbw1I9/NL2Z2diIfhffu5Aq3nihHtfO8CNYcwxmAyTKYKWyte1b1vYBHMVhMbe+WZdw==\",\n      \"dependencies\": {\n        \"balanced-match\": \"^0.3.0\",\n        \"concat-map\": \"0.0.1\"\n      }\n    },\n    \"node_modules/metalsmith-concat/node_modules/concat-map\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz\",\n      \"integrity\": \"sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==\"\n    },\n    \"node_modules/metalsmith-concat/node_modules/glob\": {\n      \"version\": \"7.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/glob/-/glob-7.0.3.tgz\",\n      \"integrity\": \"sha512-29GbP/ojh64xLytvuPybLeLD4zw5fO8XoHkGujSGJI4qRilQ3czhFdnlVVrdH2o7DZ4pgyqNgDafaF0EZIU4oQ==\",\n      \"deprecated\": \"Glob versions prior to v9 are no longer supported\",\n      \"dependencies\": {\n        \"inflight\": \"^1.0.4\",\n        \"inherits\": \"2\",\n        \"minimatch\": \"2 || 3\",\n        \"once\": \"^1.3.0\",\n        \"path-is-absolute\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/metalsmith-concat/node_modules/inflight\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz\",\n      \"integrity\": \"sha512-yMOGXregdgA+mer4gLyGWfj/gx0vzqmXtIY3YTxaQg4OZdk4GsrgiFCf/G96kVQt/aJDJyQPDeb/NtQRZp13xg==\",\n      \"deprecated\": \"This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.\",\n      \"dependencies\": {\n        \"once\": \"^1.3.0\",\n        \"wrappy\": \"1\"\n      }\n    },\n    \"node_modules/metalsmith-concat/node_modules/inherits\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz\",\n      \"integrity\": \"sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==\"\n    },\n    \"node_modules/metalsmith-concat/node_modules/lodash\": {\n      \"version\": \"4.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/lodash/-/lodash-4.7.0.tgz\",\n      \"integrity\": \"sha512-d4bJYO5ctbajdv2vz64ECUWJG8rgo73FeF6j64bGT47I0krA0DMQSoQW1EOPGibkJB86MoNvZVUwJBHxiwmFDQ==\"\n    },\n    \"node_modules/metalsmith-concat/node_modules/minimatch\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz\",\n      \"integrity\": \"sha512-ekKdP/98gMbw+JdQaHZlS5/irFw63ktA3FXHaal7TXkvdaUJ9M6BewwNyEujYzRsTirZGmEVDho+Gh8bfcpVxw==\",\n      \"deprecated\": \"Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/metalsmith-concat/node_modules/once\": {\n      \"version\": \"1.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/once/-/once-1.3.3.tgz\",\n      \"integrity\": \"sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==\",\n      \"dependencies\": {\n        \"wrappy\": \"1\"\n      }\n    },\n    \"node_modules/metalsmith-concat/node_modules/path-is-absolute\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz\",\n      \"integrity\": \"sha512-hUUTsB/vByumPhn43R+Azhsx4TQPvyQqW+XyCR6UA8ae+FGIjf5Oygj6o/FYK4ZdwnrXth2eIBKk4YlrUU0ElQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/metalsmith-concat/node_modules/wrappy\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz\",\n      \"integrity\": \"sha512-42h1d25nW6G/N7l16Oz4vqCOLIFobFBOwZrBYlCxJ/QuS2o1Gdn1PzSoiYndbnL9rgGIGZ6Qn09AIpyhrkepfw==\"\n    },\n    \"node_modules/metalsmith-copy\": {\n      \"version\": \"0.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/metalsmith-copy/-/metalsmith-copy-0.2.1.tgz\",\n      \"integrity\": \"sha512-1YCaIltYH+N+0xhEb2lx1C+ap/6RCZCkatxuL+XnMc1xiOb503BsWYDwnqkaDrrDfjXB4GNRsx5h25u5qMwaPg==\",\n      \"dependencies\": {\n        \"debug\": \"^0.8.1\",\n        \"lodash\": \"^2.4.1\",\n        \"minimatch\": \"^0.2.14\"\n      }\n    },\n    \"node_modules/metalsmith-copy/node_modules/minimatch\": {\n      \"version\": \"0.2.14\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz\",\n      \"integrity\": \"sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==\",\n      \"deprecated\": \"Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue\",\n      \"dependencies\": {\n        \"lru-cache\": \"2\",\n        \"sigmund\": \"~1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/metalsmith-eslint\": {\n      \"version\": \"2.5.4\",\n      \"resolved\": \"https://registry.npmjs.org/metalsmith-eslint/-/metalsmith-eslint-2.5.4.tgz\",\n      \"integrity\": \"sha512-M15SGUi55wMzlE6faRGpc9YvqY8p5gIZa3IJTgcteE8RiAGIjtZHoyGNAg2Az0++70sEXT9vhLQrg04Jfa0qlQ==\",\n      \"dependencies\": {\n        \"eslint\": \"^2.11.0\",\n        \"lodash\": \"^4.13.1\",\n        \"multimatch\": \"^2.1.0\"\n      }\n    },\n    \"node_modules/metalsmith-eslint/node_modules/lodash\": {\n      \"version\": \"4.17.21\",\n      \"resolved\": \"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz\",\n      \"integrity\": \"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==\"\n    },\n    \"node_modules/metalsmith-i18next\": {\n      \"version\": \"0.2.20\",\n      \"resolved\": \"https://registry.npmjs.org/metalsmith-i18next/-/metalsmith-i18next-0.2.20.tgz\",\n      \"integrity\": \"sha512-kElR/dnn2Yqyab4EejUfUBP6nb8olBk8MFAw7fkWG7UYBzSNvAGxPDkJrCqfopxI2LbjjkzE7QqiDq4TmPNsPg==\",\n      \"dependencies\": {\n        \"debug\": \"2.2.0\",\n        \"i18next\": \"1.10.6\",\n        \"multimatch\": \"2.1.0\"\n      }\n    },\n    \"node_modules/metalsmith-i18next/node_modules/debug\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-2.2.0.tgz\",\n      \"integrity\": \"sha512-X0rGvJcskG1c3TgSCPqHJ0XJgwlcvOC7elJ5Y0hYuKBZoVqWpAMfLOeIh2UI/DCQ5ruodIjvsugZtjUYUw2pUw==\",\n      \"dependencies\": {\n        \"ms\": \"0.7.1\"\n      }\n    },\n    \"node_modules/metalsmith-i18next/node_modules/ms\": {\n      \"version\": \"0.7.1\",\n      \"resolved\": \"https://registry.npmjs.org/ms/-/ms-0.7.1.tgz\",\n      \"integrity\": \"sha512-lRLiIR9fSNpnP6TC4v8+4OU7oStC01esuNowdQ34L+Gk8e5Puoc88IqJ+XAY/B3Mn2ZKis8l8HX90oU8ivzUHg==\"\n    },\n    \"node_modules/metalsmith-ignore\": {\n      \"version\": \"0.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/metalsmith-ignore/-/metalsmith-ignore-0.1.2.tgz\",\n      \"integrity\": \"sha512-v5oX7efbZLTTTlw8tKiFPAbzuM0PFo929PdRYtf1QpORGzC8a0rWkNZNFJdKGKeu87k07iuJYOBQ8Wh6HJGXjA==\",\n      \"deprecated\": \"This package is no longer maintained and has moved to the @metalsmith org; Please migrate to the following package: @metalsmith/remove\",\n      \"dependencies\": {\n        \"multimatch\": \"^0.1.0\"\n      }\n    },\n    \"node_modules/metalsmith-ignore/node_modules/minimatch\": {\n      \"version\": \"0.2.14\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz\",\n      \"integrity\": \"sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==\",\n      \"deprecated\": \"Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue\",\n      \"dependencies\": {\n        \"lru-cache\": \"2\",\n        \"sigmund\": \"~1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/metalsmith-ignore/node_modules/multimatch\": {\n      \"version\": \"0.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/multimatch/-/multimatch-0.1.0.tgz\",\n      \"integrity\": \"sha512-y468lJfPKbYYqmWbVx/jQVTo+myuG3wkVc2lLUe0IbtN7nKBmG57tJFtSztVmHlEX9JyjRdSZhqzvKGK8uOhAg==\",\n      \"dependencies\": {\n        \"lodash\": \"~2.4.1\",\n        \"minimatch\": \"~0.2.14\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/metalsmith-in-place\": {\n      \"version\": \"1.4.4\",\n      \"resolved\": \"https://registry.npmjs.org/metalsmith-in-place/-/metalsmith-in-place-1.4.4.tgz\",\n      \"integrity\": \"sha512-sh+PaLTpNirWzl3q1ka8K6MBuWBYS4oarxliqxWctmZ7sZz4kMGLo2jmlCnO3oYmesO1qcgg+M1TxCpRxNMZhg==\",\n      \"deprecated\": \"This package is no longer maintained and has moved to the @metalsmith org; Please migrate to the following package: @metalsmith/in-place\",\n      \"dependencies\": {\n        \"async\": \"^1.4.2\",\n        \"consolidate\": \"^0.14.0\",\n        \"debug\": \"^2.2.0\",\n        \"extend\": \"^3.0.0\",\n        \"fs-readdir-recursive\": \"^1.0.0\",\n        \"is-utf8\": \"^0.2.0\",\n        \"lodash.omit\": \"^4.0.2\",\n        \"multimatch\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/metalsmith-in-place/node_modules/async\": {\n      \"version\": \"1.5.2\",\n      \"resolved\": \"https://registry.npmjs.org/async/-/async-1.5.2.tgz\",\n      \"integrity\": \"sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==\"\n    },\n    \"node_modules/metalsmith-in-place/node_modules/debug\": {\n      \"version\": \"2.6.9\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-2.6.9.tgz\",\n      \"integrity\": \"sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==\",\n      \"dependencies\": {\n        \"ms\": \"2.0.0\"\n      }\n    },\n    \"node_modules/metalsmith-less\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/metalsmith-less/-/metalsmith-less-2.0.0.tgz\",\n      \"integrity\": \"sha512-PBjEAxgaWr+cxlcZW8HC9kzugcFBl9k7kQH1lrDmuV63USPcEDE28Esl+nLoGc4Esg8b5ppmto/cACRayzEL8Q==\",\n      \"dependencies\": {\n        \"bluebird\": \"^2.9.14\",\n        \"joi\": \"^6.0.8\",\n        \"less\": \"^2.4.0\",\n        \"lodash\": \"^3.5.0\",\n        \"multimatch\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/metalsmith-less/node_modules/bluebird\": {\n      \"version\": \"2.11.0\",\n      \"resolved\": \"https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz\",\n      \"integrity\": \"sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==\"\n    },\n    \"node_modules/metalsmith-less/node_modules/lodash\": {\n      \"version\": \"3.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz\",\n      \"integrity\": \"sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==\"\n    },\n    \"node_modules/metalsmith-text-replace\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/metalsmith-text-replace/-/metalsmith-text-replace-1.0.1.tgz\",\n      \"integrity\": \"sha512-VRAu4ZCszqxgHrrPmnKPcjpwN6I6/9VMXd86DnkX2JYKZ5wE5OWeEnZ0OI4r5KO3WwWsXFeJxIyKLblSkRdEbQ==\",\n      \"dependencies\": {\n        \"debug\": \"^1.0.2\",\n        \"minimatch\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/metalsmith-text-replace/node_modules/debug\": {\n      \"version\": \"1.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-1.0.5.tgz\",\n      \"integrity\": \"sha512-SIKSrp4+XqcUaNWhwaPJbLFnvSXPsZ4xBdH2WRK0Xo++UzMC4eepYghGAVhVhOwmfq3kqowqJ5w45R3pmYZnuA==\",\n      \"dependencies\": {\n        \"ms\": \"2.0.0\"\n      }\n    },\n    \"node_modules/metalsmith-text-replace/node_modules/minimatch\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-1.0.0.tgz\",\n      \"integrity\": \"sha512-Ejh5Odk/uFXAj5nf/NSXk0UamqcGAfOdHI7nY0zvCHyn4f3nKLFoUTp+lYxDxSih/40uW8lpwDplOWHdWkQXWA==\",\n      \"deprecated\": \"Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue\",\n      \"dependencies\": {\n        \"lru-cache\": \"2\",\n        \"sigmund\": \"~1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/mime\": {\n      \"version\": \"1.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/mime/-/mime-1.6.0.tgz\",\n      \"integrity\": \"sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==\",\n      \"devOptional\": true,\n      \"bin\": {\n        \"mime\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/mime-db\": {\n      \"version\": \"1.52.0\",\n      \"resolved\": \"https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz\",\n      \"integrity\": \"sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/mime-types\": {\n      \"version\": \"2.1.35\",\n      \"resolved\": \"https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz\",\n      \"integrity\": \"sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"mime-db\": \"1.52.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/minimatch\": {\n      \"version\": \"3.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz\",\n      \"integrity\": \"sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^1.1.7\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/minimist\": {\n      \"version\": \"1.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz\",\n      \"integrity\": \"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/mkdirp\": {\n      \"version\": \"0.5.6\",\n      \"resolved\": \"https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz\",\n      \"integrity\": \"sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==\",\n      \"dependencies\": {\n        \"minimist\": \"^1.2.6\"\n      },\n      \"bin\": {\n        \"mkdirp\": \"bin/cmd.js\"\n      }\n    },\n    \"node_modules/moment\": {\n      \"version\": \"2.29.4\",\n      \"resolved\": \"https://registry.npmjs.org/moment/-/moment-2.29.4.tgz\",\n      \"integrity\": \"sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==\",\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/ms\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/ms/-/ms-2.0.0.tgz\",\n      \"integrity\": \"sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==\"\n    },\n    \"node_modules/multimatch\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz\",\n      \"integrity\": \"sha512-0mzK8ymiWdehTBiJh0vClAzGyQbdtyWqzSVx//EK4N/D+599RFlGfTAsKw2zMSABtDG9C6Ul2+t8f2Lbdjf5mA==\",\n      \"dependencies\": {\n        \"array-differ\": \"^1.0.0\",\n        \"array-union\": \"^1.0.1\",\n        \"arrify\": \"^1.0.0\",\n        \"minimatch\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/mute-stream\": {\n      \"version\": \"0.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz\",\n      \"integrity\": \"sha512-EbrziT4s8cWPmzr47eYVW3wimS4HsvlnV5ri1xw1aR6JQo/OrJX5rkl32K/QQHdxeabJETtfeaROGhd8W7uBgg==\"\n    },\n    \"node_modules/ncp\": {\n      \"version\": \"0.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/ncp/-/ncp-0.6.0.tgz\",\n      \"integrity\": \"sha512-z5F/duoqgCu095ADYgO0rNh1z7Yc1AA6VQk9tUOEwd8sPeCwTMPRST+f0UiDBLAEQ3joAgdKqSCRs09sgvU1xA==\",\n      \"bin\": {\n        \"ncp\": \"bin/ncp\"\n      }\n    },\n    \"node_modules/next-tick\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz\",\n      \"integrity\": \"sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==\"\n    },\n    \"node_modules/number-is-nan\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz\",\n      \"integrity\": \"sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/oauth-sign\": {\n      \"version\": \"0.8.2\",\n      \"resolved\": \"https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz\",\n      \"integrity\": \"sha512-VlF07iu3VV3+BTXj43Nmp6Irt/G7j/NgEctUS6IweH1RGhURjjCc2NWtzXFPXXWWfc7hgbXQdtiQu2LGp6MxUg==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/object-assign\": {\n      \"version\": \"4.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz\",\n      \"integrity\": \"sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/once\": {\n      \"version\": \"1.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/once/-/once-1.4.0.tgz\",\n      \"integrity\": \"sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==\",\n      \"dependencies\": {\n        \"wrappy\": \"1\"\n      }\n    },\n    \"node_modules/onetime\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz\",\n      \"integrity\": \"sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/opener\": {\n      \"version\": \"1.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/opener/-/opener-1.4.3.tgz\",\n      \"integrity\": \"sha512-4Im9TrPJcjAYyGR5gBe3yZnBzw5n3Bfh1ceHHGNOpMurINKc6RdSIPXMyon4BZacJbJc36lLkhipioGbWh5pwg==\",\n      \"dev\": true,\n      \"bin\": {\n        \"opener\": \"opener.js\"\n      }\n    },\n    \"node_modules/optimist\": {\n      \"version\": \"0.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/optimist/-/optimist-0.4.0.tgz\",\n      \"integrity\": \"sha512-v/ueFqighwEQeAnw8ZJofuEsHDvAiUGyfojtJdKurxd4KUanznxvK9hJocaxWpn+nAAbxTA4qfh7orYHgNZIWA==\",\n      \"dependencies\": {\n        \"wordwrap\": \"~0.0.2\"\n      }\n    },\n    \"node_modules/optionator\": {\n      \"version\": \"0.8.3\",\n      \"resolved\": \"https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz\",\n      \"integrity\": \"sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==\",\n      \"dependencies\": {\n        \"deep-is\": \"~0.1.3\",\n        \"fast-levenshtein\": \"~2.0.6\",\n        \"levn\": \"~0.3.0\",\n        \"prelude-ls\": \"~1.1.2\",\n        \"type-check\": \"~0.3.2\",\n        \"word-wrap\": \"~1.2.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/os-homedir\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz\",\n      \"integrity\": \"sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/os-locale\": {\n      \"version\": \"1.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz\",\n      \"integrity\": \"sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==\",\n      \"dependencies\": {\n        \"lcid\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/path-is-absolute\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz\",\n      \"integrity\": \"sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/path-is-inside\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz\",\n      \"integrity\": \"sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==\"\n    },\n    \"node_modules/performance-now\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz\",\n      \"integrity\": \"sha512-YHk5ez1hmMR5LOkb9iJkLKqoBlL7WD5M8ljC75ZfzXriuBIVNuecaXuU7e+hOwyqf24Wxhh7Vxgt7Hnw9288Tg==\",\n      \"optional\": true\n    },\n    \"node_modules/pluralize\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz\",\n      \"integrity\": \"sha512-TH+BeeL6Ct98C7as35JbZLf8lgsRzlNJb5gklRIGHKaPkGl1esOKBc5ALUMd+q08Sr6tiEKM+Icbsxg5vuhMKQ==\"\n    },\n    \"node_modules/portfinder\": {\n      \"version\": \"0.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/portfinder/-/portfinder-0.4.0.tgz\",\n      \"integrity\": \"sha512-SZ3hp61WVhwNSS0gf0Fdrx5Yb/wl35qisHuPVM1S0StV8t5XlVZmmJy7/417OELJA7t6ecEmeEzvOaBwq3kCiQ==\",\n      \"dev\": true,\n      \"dependencies\": {\n        \"async\": \"0.9.0\",\n        \"mkdirp\": \"0.5.x\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/portfinder/node_modules/async\": {\n      \"version\": \"0.9.0\",\n      \"resolved\": \"https://registry.npmjs.org/async/-/async-0.9.0.tgz\",\n      \"integrity\": \"sha512-XQJ3MipmCHAIBBMFfu2jaSetneOrXbSyyqeU3Nod867oNOpS+i9FEms5PWgjMxSgBybRf2IVVLtr1YfrDO+okg==\",\n      \"dev\": true\n    },\n    \"node_modules/prelude-ls\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz\",\n      \"integrity\": \"sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==\",\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/private\": {\n      \"version\": \"0.1.8\",\n      \"resolved\": \"https://registry.npmjs.org/private/-/private-0.1.8.tgz\",\n      \"integrity\": \"sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==\",\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/process-nextick-args\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz\",\n      \"integrity\": \"sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==\"\n    },\n    \"node_modules/progress\": {\n      \"version\": \"1.1.8\",\n      \"resolved\": \"https://registry.npmjs.org/progress/-/progress-1.1.8.tgz\",\n      \"integrity\": \"sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==\",\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/promise\": {\n      \"version\": \"7.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/promise/-/promise-7.3.1.tgz\",\n      \"integrity\": \"sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"asap\": \"~2.0.3\"\n      }\n    },\n    \"node_modules/prr\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/prr/-/prr-1.0.1.tgz\",\n      \"integrity\": \"sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==\",\n      \"optional\": true\n    },\n    \"node_modules/punycode\": {\n      \"version\": \"1.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz\",\n      \"integrity\": \"sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==\",\n      \"optional\": true\n    },\n    \"node_modules/q\": {\n      \"version\": \"1.5.1\",\n      \"resolved\": \"https://registry.npmjs.org/q/-/q-1.5.1.tgz\",\n      \"integrity\": \"sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==\",\n      \"deprecated\": \"You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\\n\\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)\",\n      \"engines\": {\n        \"node\": \">=0.6.0\",\n        \"teleport\": \">=0.2.0\"\n      }\n    },\n    \"node_modules/qs\": {\n      \"version\": \"6.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/qs/-/qs-6.4.1.tgz\",\n      \"integrity\": \"sha512-LQy1Q1fcva/UsnP/6Iaa4lVeM49WiOitu2T4hZCyA/elLKu37L99qcBJk4VCCk+rdLvnMzfKyiN3SZTqdAZGSQ==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=0.6\"\n      }\n    },\n    \"node_modules/readable-stream\": {\n      \"version\": \"2.3.8\",\n      \"resolved\": \"https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz\",\n      \"integrity\": \"sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==\",\n      \"dependencies\": {\n        \"core-util-is\": \"~1.0.0\",\n        \"inherits\": \"~2.0.3\",\n        \"isarray\": \"~1.0.0\",\n        \"process-nextick-args\": \"~2.0.0\",\n        \"safe-buffer\": \"~5.1.1\",\n        \"string_decoder\": \"~1.1.1\",\n        \"util-deprecate\": \"~1.0.1\"\n      }\n    },\n    \"node_modules/readline2\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz\",\n      \"integrity\": \"sha512-8/td4MmwUB6PkZUbV25uKz7dfrmjYWxsW8DVfibWdlHRk/l/DfHKn4pU+dfcoGLFgWOdyGCzINRQD7jn+Bv+/g==\",\n      \"dependencies\": {\n        \"code-point-at\": \"^1.0.0\",\n        \"is-fullwidth-code-point\": \"^1.0.0\",\n        \"mute-stream\": \"0.0.5\"\n      }\n    },\n    \"node_modules/recast\": {\n      \"version\": \"0.10.33\",\n      \"resolved\": \"https://registry.npmjs.org/recast/-/recast-0.10.33.tgz\",\n      \"integrity\": \"sha512-RxAVgvgWGzfFYsdc3PB6TM4/cq8HMgBH8PC9r+SkO7j1MeHZvIMxLSVlUhin3sv9wbAy8CMAPXSGSGkWPovyKQ==\",\n      \"dependencies\": {\n        \"ast-types\": \"0.8.12\",\n        \"esprima-fb\": \"~15001.1001.0-dev-harmony-fb\",\n        \"private\": \"~0.1.5\",\n        \"source-map\": \"~0.5.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/recast/node_modules/ast-types\": {\n      \"version\": \"0.8.12\",\n      \"resolved\": \"https://registry.npmjs.org/ast-types/-/ast-types-0.8.12.tgz\",\n      \"integrity\": \"sha512-rWhsoD0aHKpx+aKIP0Sf92bai1HC5iZcB1n/HCnkIMR8Bhx0gYRQySo062Y65ND4oRgcuxpLcPrcco09I1shpg==\",\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/recursive-readdir\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-1.3.0.tgz\",\n      \"integrity\": \"sha512-xPLahSmi5cp0id46BHffsrWDpU1z0zt2Hl1B8Qn/GQhOv9UGE2u/qQQHwdg+H/Nk3Emx5JYN4fKbmc3XvQA2Lw==\",\n      \"dependencies\": {\n        \"minimatch\": \"0.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.6.6\"\n      }\n    },\n    \"node_modules/recursive-readdir/node_modules/minimatch\": {\n      \"version\": \"0.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz\",\n      \"integrity\": \"sha512-WFX1jI1AaxNTZVOHLBVazwTWKaQjoykSzCBNXB72vDTCzopQGtyP91tKdFK5cv1+qMwPyiTu1HqUriqplI8pcA==\",\n      \"deprecated\": \"Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue\",\n      \"dependencies\": {\n        \"lru-cache\": \"2\",\n        \"sigmund\": \"~1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/regenerator\": {\n      \"version\": \"0.8.46\",\n      \"resolved\": \"https://registry.npmjs.org/regenerator/-/regenerator-0.8.46.tgz\",\n      \"integrity\": \"sha512-jFhAPsOnJ/UIZCG1+iISKiczjnLWaQ4RatPkg5BZIcgz0m3C+txY28U0H7ij+fLSADmY+kHj9HZff6CLq9hrVA==\",\n      \"dependencies\": {\n        \"commoner\": \"~0.10.3\",\n        \"defs\": \"~1.1.0\",\n        \"esprima-fb\": \"~15001.1001.0-dev-harmony-fb\",\n        \"private\": \"~0.1.5\",\n        \"recast\": \"0.10.33\",\n        \"regenerator-runtime\": \"~0.9.5\",\n        \"through\": \"~2.3.8\"\n      },\n      \"bin\": {\n        \"regenerator\": \"bin/regenerator\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/regenerator-runtime\": {\n      \"version\": \"0.9.6\",\n      \"resolved\": \"https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz\",\n      \"integrity\": \"sha512-D0Y/JJ4VhusyMOd/o25a3jdUqN/bC85EFsaoL9Oqmy/O4efCh+xhp7yj2EEOsj974qvMkcW8AwUzJ1jB/MbxCw==\"\n    },\n    \"node_modules/repeat-string\": {\n      \"version\": \"1.6.1\",\n      \"resolved\": \"https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz\",\n      \"integrity\": \"sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==\",\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/request\": {\n      \"version\": \"2.81.0\",\n      \"resolved\": \"https://registry.npmjs.org/request/-/request-2.81.0.tgz\",\n      \"integrity\": \"sha512-IZnsR7voF0miGSu29EXPRgPTuEsI/+aibNSBbN1pplrfartF5wDYGADz3iD9vmBVf2r00rckWZf8BtS5kk7Niw==\",\n      \"deprecated\": \"request has been deprecated, see https://github.com/request/request/issues/3142\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"aws-sign2\": \"~0.6.0\",\n        \"aws4\": \"^1.2.1\",\n        \"caseless\": \"~0.12.0\",\n        \"combined-stream\": \"~1.0.5\",\n        \"extend\": \"~3.0.0\",\n        \"forever-agent\": \"~0.6.1\",\n        \"form-data\": \"~2.1.1\",\n        \"har-validator\": \"~4.2.1\",\n        \"hawk\": \"~3.1.3\",\n        \"http-signature\": \"~1.1.0\",\n        \"is-typedarray\": \"~1.0.0\",\n        \"isstream\": \"~0.1.2\",\n        \"json-stringify-safe\": \"~5.0.1\",\n        \"mime-types\": \"~2.1.7\",\n        \"oauth-sign\": \"~0.8.1\",\n        \"performance-now\": \"^0.2.0\",\n        \"qs\": \"~6.4.0\",\n        \"safe-buffer\": \"^5.0.1\",\n        \"stringstream\": \"~0.0.4\",\n        \"tough-cookie\": \"~2.3.0\",\n        \"tunnel-agent\": \"^0.6.0\",\n        \"uuid\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/require-uncached\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz\",\n      \"integrity\": \"sha512-Xct+41K3twrbBHdxAgMoOS+cNcoqIjfM2/VxBF4LL2hVph7YsF8VSKyQ3BDFZwEVbok9yeDl2le/qo0S77WG2w==\",\n      \"dependencies\": {\n        \"caller-path\": \"^0.1.0\",\n        \"resolve-from\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/requires-port\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz\",\n      \"integrity\": \"sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==\",\n      \"dev\": true\n    },\n    \"node_modules/resolve-from\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz\",\n      \"integrity\": \"sha512-kT10v4dhrlLNcnO084hEjvXCI1wUG9qZLoz2RogxqDQQYy7IxjI/iMUkOtQTNEh6rzHxvdQWHsJyel1pKOVCxg==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/restore-cursor\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz\",\n      \"integrity\": \"sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw==\",\n      \"dependencies\": {\n        \"exit-hook\": \"^1.0.0\",\n        \"onetime\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/right-align\": {\n      \"version\": \"0.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz\",\n      \"integrity\": \"sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==\",\n      \"dependencies\": {\n        \"align-text\": \"^0.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/rimraf\": {\n      \"version\": \"2.7.1\",\n      \"resolved\": \"https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz\",\n      \"integrity\": \"sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==\",\n      \"deprecated\": \"Rimraf versions prior to v4 are no longer supported\",\n      \"dependencies\": {\n        \"glob\": \"^7.1.3\"\n      },\n      \"bin\": {\n        \"rimraf\": \"bin.js\"\n      }\n    },\n    \"node_modules/run-async\": {\n      \"version\": \"0.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz\",\n      \"integrity\": \"sha512-qOX+w+IxFgpUpJfkv2oGN0+ExPs68F4sZHfaRRx4dDexAQkG83atugKVEylyT5ARees3HBbfmuvnjbrd8j9Wjw==\",\n      \"dependencies\": {\n        \"once\": \"^1.3.0\"\n      }\n    },\n    \"node_modules/rx-lite\": {\n      \"version\": \"3.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz\",\n      \"integrity\": \"sha512-1I1+G2gteLB8Tkt8YI1sJvSIfa0lWuRtC8GjvtyPBcLSF5jBCCJJqKrpER5JU5r6Bhe+i9/pK3VMuUcXu0kdwQ==\"\n    },\n    \"node_modules/safe-buffer\": {\n      \"version\": \"5.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz\",\n      \"integrity\": \"sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==\"\n    },\n    \"node_modules/safer-buffer\": {\n      \"version\": \"2.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz\",\n      \"integrity\": \"sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==\"\n    },\n    \"node_modules/shelljs\": {\n      \"version\": \"0.6.1\",\n      \"resolved\": \"https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz\",\n      \"integrity\": \"sha512-B1vvzXQlJ77SURr3SIUQ/afh+LwecDKAVKE1wqkBlr2PCHoZDaF6MFD+YX1u9ddQjR4z2CKx1tdqvS2Xfs5h1A==\",\n      \"bin\": {\n        \"shjs\": \"bin/shjs\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/sigmund\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz\",\n      \"integrity\": \"sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==\"\n    },\n    \"node_modules/simple-fmt\": {\n      \"version\": \"0.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz\",\n      \"integrity\": \"sha512-9a3zTDDh9LXbTR37qBhACWIQ/mP/ry5xtmbE98BJM8GR02sanCkfMzp7AdCTqYhkBZggK/w7hJtc8Pb9nmo16A==\"\n    },\n    \"node_modules/simple-is\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz\",\n      \"integrity\": \"sha512-GJXhv3r5vdj5tGWO+rcrWgjU2azLB+fb7Ehh3SmZpXE0o4KrrFLti0w4mdDCbR29X/z0Ls20ApjZitlpAXhAeg==\"\n    },\n    \"node_modules/slice-ansi\": {\n      \"version\": \"0.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz\",\n      \"integrity\": \"sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/sntp\": {\n      \"version\": \"1.0.9\",\n      \"resolved\": \"https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz\",\n      \"integrity\": \"sha512-7bgVOAnPj3XjrKY577S+puCKGCRlUrcrEdsMeRXlg9Ghf5df/xNi6sONUa43WrHUd3TjJBF7O04jYoiY0FVa0A==\",\n      \"deprecated\": \"This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"hoek\": \"2.x.x\"\n      },\n      \"engines\": {\n        \"node\": \">=0.8.0\"\n      }\n    },\n    \"node_modules/source-map\": {\n      \"version\": \"0.5.7\",\n      \"resolved\": \"https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz\",\n      \"integrity\": \"sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/sprintf-js\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz\",\n      \"integrity\": \"sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==\"\n    },\n    \"node_modules/sshpk\": {\n      \"version\": \"1.17.0\",\n      \"resolved\": \"https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz\",\n      \"integrity\": \"sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"asn1\": \"~0.2.3\",\n        \"assert-plus\": \"^1.0.0\",\n        \"bcrypt-pbkdf\": \"^1.0.0\",\n        \"dashdash\": \"^1.12.0\",\n        \"ecc-jsbn\": \"~0.1.1\",\n        \"getpass\": \"^0.1.1\",\n        \"jsbn\": \"~0.1.0\",\n        \"safer-buffer\": \"^2.0.2\",\n        \"tweetnacl\": \"~0.14.0\"\n      },\n      \"bin\": {\n        \"sshpk-conv\": \"bin/sshpk-conv\",\n        \"sshpk-sign\": \"bin/sshpk-sign\",\n        \"sshpk-verify\": \"bin/sshpk-verify\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/sshpk/node_modules/assert-plus\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz\",\n      \"integrity\": \"sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=0.8\"\n      }\n    },\n    \"node_modules/stable\": {\n      \"version\": \"0.1.8\",\n      \"resolved\": \"https://registry.npmjs.org/stable/-/stable-0.1.8.tgz\",\n      \"integrity\": \"sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==\",\n      \"deprecated\": \"Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility\"\n    },\n    \"node_modules/stat-mode\": {\n      \"version\": \"0.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz\",\n      \"integrity\": \"sha512-o+7DC0OM5Jt3+gratXXqfXf62V/CBoqQbT7Kp7jCxTYW2PLOB2/ZSGIfm9T5/QZe1Vw1MCbu6DoB6JnhVtxcJw==\"\n    },\n    \"node_modules/string_decoder\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz\",\n      \"integrity\": \"sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==\",\n      \"dependencies\": {\n        \"safe-buffer\": \"~5.1.0\"\n      }\n    },\n    \"node_modules/string-width\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz\",\n      \"integrity\": \"sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==\",\n      \"dependencies\": {\n        \"code-point-at\": \"^1.0.0\",\n        \"is-fullwidth-code-point\": \"^1.0.0\",\n        \"strip-ansi\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/string-width/node_modules/ansi-regex\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz\",\n      \"integrity\": \"sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/string-width/node_modules/strip-ansi\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz\",\n      \"integrity\": \"sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/stringmap\": {\n      \"version\": \"0.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/stringmap/-/stringmap-0.2.2.tgz\",\n      \"integrity\": \"sha512-mR1LEHDw6TsHa+LwJeeBc9ZqZqEOm7bHidgxMmDg8HB/rbA1HhDeT08gS67CCCG/xrgIfQx5tW42pd8vFpLUow==\"\n    },\n    \"node_modules/stringset\": {\n      \"version\": \"0.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/stringset/-/stringset-0.2.1.tgz\",\n      \"integrity\": \"sha512-km3jeiRpmySChl1oLiBE2ESdG5k/4+6tjENVL6BB3mdmKBiUikI5ks4paad2WAKsxzpNiBqBBbXCC12QqlpLWA==\"\n    },\n    \"node_modules/stringstream\": {\n      \"version\": \"0.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz\",\n      \"integrity\": \"sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==\",\n      \"optional\": true\n    },\n    \"node_modules/strip-ansi\": {\n      \"version\": \"0.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz\",\n      \"integrity\": \"sha512-DerhZL7j6i6/nEnVG0qViKXI0OKouvvpsAiaj7c+LfqZZZxdwZtv8+UiA/w4VUJpT8UzX0pR1dcHOii1GbmruQ==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^0.2.1\"\n      },\n      \"bin\": {\n        \"strip-ansi\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/strip-json-comments\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz\",\n      \"integrity\": \"sha512-AOPG8EBc5wAikaG1/7uFCNFJwnKOuQwFTpYBdTW6OvWHeZBQBrAA/amefHGrEiOnCPcLFZK6FUPtWVKpQVIRgg==\",\n      \"bin\": {\n        \"strip-json-comments\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=0.8.0\"\n      }\n    },\n    \"node_modules/supports-color\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz\",\n      \"integrity\": \"sha512-tdCZ28MnM7k7cJDJc7Eq80A9CsRFAAOZUy41npOZCs++qSjfIy7o5Rh46CBk+Dk5FbKJ33X3Tqg4YrV07N5RaA==\",\n      \"bin\": {\n        \"supports-color\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/table\": {\n      \"version\": \"3.8.3\",\n      \"resolved\": \"https://registry.npmjs.org/table/-/table-3.8.3.tgz\",\n      \"integrity\": \"sha512-RZuzIOtzFbprLCE0AXhkI0Xi42ZJLZhCC+qkwuMLf/Vjz3maWpA8gz1qMdbmNoI9cOROT2Am/DxeRyXenrL11g==\",\n      \"dependencies\": {\n        \"ajv\": \"^4.7.0\",\n        \"ajv-keywords\": \"^1.0.0\",\n        \"chalk\": \"^1.1.1\",\n        \"lodash\": \"^4.0.0\",\n        \"slice-ansi\": \"0.0.4\",\n        \"string-width\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/table/node_modules/ansi-regex\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz\",\n      \"integrity\": \"sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/table/node_modules/ansi-styles\": {\n      \"version\": \"2.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz\",\n      \"integrity\": \"sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/table/node_modules/chalk\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz\",\n      \"integrity\": \"sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^2.2.1\",\n        \"escape-string-regexp\": \"^1.0.2\",\n        \"has-ansi\": \"^2.0.0\",\n        \"strip-ansi\": \"^3.0.0\",\n        \"supports-color\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/table/node_modules/has-ansi\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz\",\n      \"integrity\": \"sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/table/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz\",\n      \"integrity\": \"sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==\",\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/table/node_modules/lodash\": {\n      \"version\": \"4.17.21\",\n      \"resolved\": \"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz\",\n      \"integrity\": \"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==\"\n    },\n    \"node_modules/table/node_modules/string-width\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz\",\n      \"integrity\": \"sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==\",\n      \"dependencies\": {\n        \"is-fullwidth-code-point\": \"^2.0.0\",\n        \"strip-ansi\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/table/node_modules/string-width/node_modules/ansi-regex\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz\",\n      \"integrity\": \"sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==\",\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/table/node_modules/string-width/node_modules/strip-ansi\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz\",\n      \"integrity\": \"sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/table/node_modules/strip-ansi\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz\",\n      \"integrity\": \"sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/table/node_modules/supports-color\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz\",\n      \"integrity\": \"sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==\",\n      \"engines\": {\n        \"node\": \">=0.8.0\"\n      }\n    },\n    \"node_modules/text-table\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz\",\n      \"integrity\": \"sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==\"\n    },\n    \"node_modules/through\": {\n      \"version\": \"2.3.8\",\n      \"resolved\": \"https://registry.npmjs.org/through/-/through-2.3.8.tgz\",\n      \"integrity\": \"sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==\"\n    },\n    \"node_modules/thunkify\": {\n      \"version\": \"2.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz\",\n      \"integrity\": \"sha512-w9foI80XcGImrhMQ19pxunaEC5Rp2uzxZZg4XBAFRfiLOplk3F0l7wo+bO16vC2/nlQfR/mXZxcduo0MF2GWLg==\"\n    },\n    \"node_modules/toml\": {\n      \"version\": \"2.3.6\",\n      \"resolved\": \"https://registry.npmjs.org/toml/-/toml-2.3.6.tgz\",\n      \"integrity\": \"sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==\"\n    },\n    \"node_modules/topo\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/topo/-/topo-1.1.0.tgz\",\n      \"integrity\": \"sha512-vpmONxdZoD0R3hzH0lovwv8QmsqZmGCDE1wXW9YGD/reiDOAbPKEgRDlBCAt8u8nJhav/s/I+r+1gvdpA11x7Q==\",\n      \"deprecated\": \"This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).\",\n      \"dependencies\": {\n        \"hoek\": \"2.x.x\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.40\"\n      }\n    },\n    \"node_modules/tough-cookie\": {\n      \"version\": \"2.3.4\",\n      \"resolved\": \"https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz\",\n      \"integrity\": \"sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"punycode\": \"^1.4.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.8\"\n      }\n    },\n    \"node_modules/tryor\": {\n      \"version\": \"0.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/tryor/-/tryor-0.1.2.tgz\",\n      \"integrity\": \"sha512-2+ilNA00DGvbUYYbRrm3ux+snbo7I6uPXMw8I4p/QMl7HUOWBBZFbk+Mpr8/IAPDQE+LQ8vOdlI6xEzjc+e/BQ==\"\n    },\n    \"node_modules/tsscmp\": {\n      \"version\": \"1.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz\",\n      \"integrity\": \"sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==\",\n      \"engines\": {\n        \"node\": \">=0.6.x\"\n      }\n    },\n    \"node_modules/tunnel-agent\": {\n      \"version\": \"0.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz\",\n      \"integrity\": \"sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"safe-buffer\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/tweetnacl\": {\n      \"version\": \"0.14.5\",\n      \"resolved\": \"https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz\",\n      \"integrity\": \"sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==\",\n      \"optional\": true\n    },\n    \"node_modules/type\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/type/-/type-1.2.0.tgz\",\n      \"integrity\": \"sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==\"\n    },\n    \"node_modules/type-check\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz\",\n      \"integrity\": \"sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==\",\n      \"dependencies\": {\n        \"prelude-ls\": \"~1.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/typedarray\": {\n      \"version\": \"0.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz\",\n      \"integrity\": \"sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==\"\n    },\n    \"node_modules/union\": {\n      \"version\": \"0.4.6\",\n      \"resolved\": \"https://registry.npmjs.org/union/-/union-0.4.6.tgz\",\n      \"integrity\": \"sha512-2qtrvSgD0GKotLRCNYkIMUOzoaHjXKCtbAP0kc5Po6D+RWTBb+BxlcHlHCYcf+Y+YM7eQicPgAg9mnWQvtoFVA==\",\n      \"dev\": true,\n      \"dependencies\": {\n        \"qs\": \"~2.3.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/union/node_modules/qs\": {\n      \"version\": \"2.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/qs/-/qs-2.3.3.tgz\",\n      \"integrity\": \"sha512-f5M0HQqZWkzU8GELTY8LyMrGkr3bPjKoFtTkwUEqJQbcljbeK8M7mliP9Ia2xoOI6oMerp+QPS7oYJtpGmWe/A==\",\n      \"dev\": true\n    },\n    \"node_modules/unyield\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/unyield/-/unyield-0.0.1.tgz\",\n      \"integrity\": \"sha512-XDofWbYIEnQiFFHQX4Gh0jDvL6FxZIxnEHrYbZ9GzpiAMuhje7KMlUFH2deomr0L2A9/IW2NIHBcE7hqfWUjBw==\",\n      \"dependencies\": {\n        \"co\": \"~3.1.0\"\n      }\n    },\n    \"node_modules/url-join\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz\",\n      \"integrity\": \"sha512-zz1wZk4Lb5PTVwZ3HWDmm8XnlPvmOof6/fjdDPA5yBrUcbtV64U6bV832Zf1BtU2WkBBWaUT46wCs+l0HP5nhg==\",\n      \"dev\": true\n    },\n    \"node_modules/user-home\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz\",\n      \"integrity\": \"sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==\",\n      \"dependencies\": {\n        \"os-homedir\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/util-deprecate\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz\",\n      \"integrity\": \"sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==\"\n    },\n    \"node_modules/uuid\": {\n      \"version\": \"3.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz\",\n      \"integrity\": \"sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==\",\n      \"deprecated\": \"Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.\",\n      \"optional\": true,\n      \"bin\": {\n        \"uuid\": \"bin/uuid\"\n      }\n    },\n    \"node_modules/verror\": {\n      \"version\": \"1.10.0\",\n      \"resolved\": \"https://registry.npmjs.org/verror/-/verror-1.10.0.tgz\",\n      \"integrity\": \"sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==\",\n      \"engines\": [\n        \"node >=0.6.0\"\n      ],\n      \"optional\": true,\n      \"dependencies\": {\n        \"assert-plus\": \"^1.0.0\",\n        \"core-util-is\": \"1.0.2\",\n        \"extsprintf\": \"^1.2.0\"\n      }\n    },\n    \"node_modules/verror/node_modules/assert-plus\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz\",\n      \"integrity\": \"sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=0.8\"\n      }\n    },\n    \"node_modules/verror/node_modules/core-util-is\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz\",\n      \"integrity\": \"sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==\",\n      \"optional\": true\n    },\n    \"node_modules/ware\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/ware/-/ware-1.3.0.tgz\",\n      \"integrity\": \"sha512-Y2HUDMktriUm+SR2gZWxlrszcgtXExlhQYZ8QJNYbl22jum00KIUcHJ/h/sdAXhWTJcbSkiMYN9Z2tWbWYSrrw==\",\n      \"dependencies\": {\n        \"wrap-fn\": \"^0.1.0\"\n      }\n    },\n    \"node_modules/window-size\": {\n      \"version\": \"0.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz\",\n      \"integrity\": \"sha512-2thx4pB0cV3h+Bw7QmMXcEbdmOzv9t0HFplJH/Lz6yu60hXYy5RT8rUu+wlIreVxWsGN20mo+MHeCSfUpQBwPw==\",\n      \"bin\": {\n        \"window-size\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.10.0\"\n      }\n    },\n    \"node_modules/word-wrap\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz\",\n      \"integrity\": \"sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/wordwrap\": {\n      \"version\": \"0.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz\",\n      \"integrity\": \"sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==\",\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/wrap-fn\": {\n      \"version\": \"0.1.5\",\n      \"resolved\": \"https://registry.npmjs.org/wrap-fn/-/wrap-fn-0.1.5.tgz\",\n      \"integrity\": \"sha512-xDLdGx0M8JQw9QDAC9s5NUxtg9MI09F6Vbxa2LYoSoCvzJnx2n81YMIfykmXEGsUvuLaxnblJTzhSOjUOX37ag==\",\n      \"dependencies\": {\n        \"co\": \"3.1.0\"\n      }\n    },\n    \"node_modules/wrappy\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz\",\n      \"integrity\": \"sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==\"\n    },\n    \"node_modules/write\": {\n      \"version\": \"0.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/write/-/write-0.2.1.tgz\",\n      \"integrity\": \"sha512-CJ17OoULEKXpA5pef3qLj5AxTJ6mSt7g84he2WIskKwqFO4T97d5V7Tadl0DYDk7qyUOQD5WlUlOMChaYrhxeA==\",\n      \"dependencies\": {\n        \"mkdirp\": \"^0.5.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/xtend\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz\",\n      \"integrity\": \"sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==\",\n      \"engines\": {\n        \"node\": \">=0.4\"\n      }\n    },\n    \"node_modules/y18n\": {\n      \"version\": \"3.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz\",\n      \"integrity\": \"sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==\"\n    },\n    \"node_modules/yargs\": {\n      \"version\": \"3.27.0\",\n      \"resolved\": \"https://registry.npmjs.org/yargs/-/yargs-3.27.0.tgz\",\n      \"integrity\": \"sha512-6atYjGACjX/OYWico7LwdBx9eiGlkMnIw6OwqfBb+uJQpaT82tQ7oI+BI6Dvq62qZvSbzGzQCVLQdMd59tR2eA==\",\n      \"dependencies\": {\n        \"camelcase\": \"^1.2.1\",\n        \"cliui\": \"^2.1.0\",\n        \"decamelize\": \"^1.0.0\",\n        \"os-locale\": \"^1.4.0\",\n        \"window-size\": \"^0.1.2\",\n        \"y18n\": \"^3.2.0\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "web/package.json",
    "content": "{\n  \"name\": \"touch-mapper-web\",\n  \"private\": true,\n  \"dependencies\": {\n    \"ect\": \"^0.5.9\",\n    \"metalsmith\": \"^1.0.0\",\n    \"metalsmith-concat\": \"^5.0.1\",\n    \"metalsmith-copy\": \"^0.2.1\",\n    \"metalsmith-eslint\": \"^2.5.3\",\n    \"metalsmith-i18next\": \"^0.2.20\",\n    \"metalsmith-ignore\": \"^0.1.2\",\n    \"metalsmith-in-place\": \"^1.4.3\",\n    \"metalsmith-less\": \"^2.0.0\",\n    \"metalsmith-text-replace\": \"^1.0.1\"\n  },\n  \"devDependencies\": {\n    \"http-server\": \"^0.9.0\"\n  }\n}\n"
  },
  {
    "path": "web/pre-src/area.pre",
    "content": "<div class=\"show-on-load\" style=\"display: none\">\n  <script src=\"../scripts/aws-sdk-2.3.3.min.js\"></script>\n  <script src=\"../scripts/ol-3.15.1-custom.min.js\"></script>\n  <script>\n    window.TM = {\n      translations: {\n        /* For map-creation.js */\n        \"progress__connecting\": \"{{ progress__connecting }}\",\n        \"progress__reading_osm\": \"{{ progress__reading_osm }}\",\n        \"progress__converting\": \"{{ progress__converting }}\",\n        \"progress__uploading\": \"{{ progress__uploading }}\",\n        \"conversion_error_unknown\": \"{{ conversion_error_unknown }}\",\n        \"conversion_error_too_large\": \"{{ conversion_error_too_large }}\",\n        \"map_scale_coverage\": \"{{ map_scale_coverage }}\",\n        \"unit_cm\": \"{{ unit_cm }}\",\n        \"meters\": \"{{ meters }}\",\n        \"yards\": \"{{ yards }}\"\n      }\n    };\n  </script>\n\n  <nav class=\"breadcrumb\" aria-label=\"{{ breadcrumb_nav_title }}\" role=\"navigation\">\n    <ul>\n      <li><a href=\".\">{{ breadcrumb_home }}</a></li>\n      <li aria-current=\"location\">{{ breadcrumb_area }}</li>\n      <li>{{ breadcrumb_map }}</li>\n    </ul>\n  </nav>\n\n  <h2>{{ map_parameters }}</h2>\n\n  <div class=\"row main-row search-results-row\">\n    <div class=\"col col1\"><label for=\"search-results\">{{ address }}:</label></div>\n    <div class=\"col col2\" id=\"search-results\" tabindex=\"-1\">\n      <span class=\"first-address\"></span>\n      <button class=\"show-all-addresses\" style=\"display: none\" aria-description=\"{{ show_all_addresses_aria }}\">{{ show_all_addresses }}</button>\n      <ul class=\"all-addresses\" style=\"display: none\"></ul>\n    </div>\n  </div>\n\n  <div class=\"main-row printing-tech-row\">\n    <div class=\"col col1\">\n      <label>{{ printing_tech }}:</label>\n    </div>\n    <div class=\"col col2\">\n      <span class=\"printing-tech-container\" style=\"margin-right: 15px\">\n       <input type=\"radio\" name=\"printing-tech\" id=\"printing-tech-3d\">\n       <label for=\"printing-tech-3d\">{{ printing_tech__3d }}</label>\n      </span>\n\n      <span class=\"printing-tech-container\">\n       <input type=\"radio\" name=\"printing-tech\" id=\"printing-tech-2d\">\n       <label for=\"printing-tech-2d\">{{ printing_tech__2d }}</label>\n      </span>\n    </div>\n  </div>\n\n  <div class=\"row main-row map-size-preset-row grey-in-advanced\">\n    <div class=\"col col1\">\n      <label for=\"map-size-preset\">{{ map_size }}:</label>\n    </div>\n    <div class=\"col col2\">\n      <select class=\"grey-in-advanced hidden-for-2d\" id=\"map-size-preset\">\n        <option value=\"17\">{{ size_order_17 }}</option>\n        <option value=\"20\">{{ size_order_20 }}</option>\n      </select>\n      <span class=\"hidden-for-3d\">{{ can_print_any_size }}</span>\n    </div>\n  </div>\n\n  <div class=\"row main-row map-scale-preset-row grey-in-advanced\">\n    <div class=\"col col1\">\n      <label for=\"map-scale-preset\">{{ map_scale }}:</label>\n    </div>\n    <div class=\"col col2\">\n      <select class=\"grey-in-advanced\" id=\"map-scale-preset\" data-initial=\"2400\">\n        <option value=\"1000\">1:1000 &ndash; {{ map_scale__1000 }}</option>\n        <option value=\"1400\">1:1400</option>\n        <option value=\"1800\">1:1800 &ndash; {{ map_scale__1800 }}</option>\n        <option value=\"2400\">1:2400 &ndash; {{ map_scale__2400 }}</option>\n        <option value=\"3200\">1:3200 &ndash; {{ map_scale__3200 }}</option>\n        <option value=\"4200\">1:4200</option>\n        <option value=\"5600\">1:5600 &ndash; {{ map_scale__5600 }}</option>\n        <option value=\"7500\">1:7500</option>\n        <option value=\"10000\">1:10000</option>\n        <option value=\"13000\">1:13000</option>\n        <option value=\"17000\">1:17000</option>\n        <option value=\"23000\">1:23000</option>\n        <option value=\"30000\">1:30000</option>\n        <option value=\"40000\">1:40000</option>\n      </select>\n      <div class=\"map-scale-coverage\" aria-live=\"polite\"></div>\n    </div>\n  </div>\n\n  <div class=\"row main-row\">\n    <div class=\"col col1\">\n      <label for=\"content-mode\">{{ map_content }}:</label>\n    </div>\n    <div class=\"col col2 content-selection-container\">\n      <select id=\"content-mode\">\n        <option value=\"normal\">{{ content_mode_normal }}</option>\n        <option value=\"no-buildings\">{{ content_mode_no_buildings }}</option>\n        <option value=\"only-big-roads\">{{ content_mode_only_big_roads }}</option>\n      </select>\n    </div>\n  </div>\n\n  <div class=\"row main-row target-road-density-row\" style=\"display: none\">\n    <div class=\"col col1\">\n      <label for=\"target-road-density-ui\">{{ target_road_density_ui }}:</label>\n    </div>\n    <div class=\"col col2 content-selection-container\">\n      <input id=\"target-road-density-ui\" type=\"number\" min=\"1\" max=\"100\" step=\"1\" value=\"10\" />\n      <span class=\"target-road-density-ui-hint\">{{ target_road_density_ui_hint }}</span>\n    </div>\n  </div>\n\n  <div class=\"row main-row\">\n    <div class=\"col col1\">\n      <label>{{ location_marker }}:</label>\n    </div>\n    <div class=\"col col2 content-selection-container\">\n      <input id=\"hide-location-marker\" type=\"checkbox\" />\n      <label for=\"hide-location-marker\">{{ hide_location_marker }}</label>\n    </div>\n  </div>\n\n  <div class=\"row main-row advanced-toggle\">\n    <div class=\"col col1\">\n      <label>{{ advanced_settings }}:</label>\n    </div>\n    <div class=\"col col2 advanced-input-container\">\n      <input id=\"advanced-input\" type=\"checkbox\" />\n      <label for=\"advanced-input\">{{ show_advanced_settings }}</label>\n    </div>\n  </div>\n\n  <div id=\"advanced-controls\" style=\"display: none\">\n\n    <div class=\"row main-row coords-input-row\">\n      <div class=\"col col1\">\n        <label id=\"address-coords-label\">{{ coordinates }}:</label>\n      </div>\n      <div class=\"col col2\" aria-labelledby=\"address-coords-label\">\n        <div class=\"single-input\">\n         <label for=\"lon-input\">{{ longitude_x }}:</label>\n         <input id=\"lon-input\" value=\"\" pattern=\"-?[0-9]+\\.[0-9]+\" />\n        </div>\n        <div class=\"single-input\">\n         <label for=\"lat-input\">{{ latitude_y }}:</label>\n         <input id=\"lat-input\" value=\"\" pattern=\"-?[0-9]+\\.[0-9]+\" />\n        </div>\n      </div>\n    </div>\n\n    <div class=\"row main-row map-offset-row\">\n      <div class=\"col col1\">\n        <label id=\"area-offset-label\">{{ area_adjustment }}:</label>\n      </div>\n      <div class=\"col col2\" aria-labelledby=\"area-offset-label\">\n        <div class=\"single-input\">\n          <label for=\"x-offset-input\">{{ x_offset }}:</label>\n          <input id=\"x-offset-input\" type=\"number\" value=\"0\" pattern=\"-?[0-9]+\" />\n          <span class=\"units\">{{ meters }} <span class=\"more\">({{ positive_is_right }})</span></span>\n        </div>\n        <div class=\"single-input\">\n          <span class=\"label\"><label for=\"y-offset-input\">{{ y_offset }}:</label></span>\n          <span class=\"input\"><input id=\"y-offset-input\" type=\"number\" value=\"0\" pattern=\"-?[0-9]+\" /></span>\n          <span class=\"units\">{{ meters }} <span class=\"more\">({{ positive_is_up }})</span></span>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"row main-row map-size-row\">\n      <div class=\"col col1\">\n        <label for=\"map-size\">{{ custom_map_size }}:</label>\n      </div>\n      <div class=\"col col2\">\n        <input id=\"map-size-input\" type=\"number\" max=\"99.9\" step=\"0.1\" pattern=\"[0-9]+(\\.[0-9]+)?\" required />\n        <span class=\"map-size-unit\">{{ unit_cm }}</span>\n      </div>\n    </div>\n\n    <div class=\"row main-row print-scale-row\">\n      <div class=\"col col1\">\n        <label for=\"scale-input\">{{ custom_scale }}:</label>\n      </div>\n      <div class=\"col col2\">\n        <span class=\"one-to\">1 :</span>\n        <input id=\"scale-input\" type=\"number\" value=\"2400\" min=\"500\" max=\"99999\" required />\n      </div>\n    </div>\n\n    <div class=\"row main-row multipart-map\">\n      <div class=\"col col1\">\n        <label>{{ multipart_map }}:</label>\n      </div>\n      <div class=\"col col2\">\n        <input id=\"multipart-map-input\" type=\"checkbox\" />\n        <label for=\"multipart-map-input\">{{ multipart_map_label }}</label>\n      </div>\n    </div>\n\n    <div class=\"area-movement-buttons\">\n      <h3>{{ adjust_area_for_multipart }}</h3>\n\n      <div class=\"column left\">\n        <button class=\"btn left-100 ib\" title=\"<%- @t('move_map_area_left', { amount: '100' }) %>\">◀ 100%</button>\n        <button class=\"btn left-10 ib\" title=\"<%- @t('move_map_area_left', { amount: '10' }) %>\">◀ 10%</button>\n      </div>\n      <div class=\"column middle\">\n        <button class=\"btn up-100\" title=\"<%- @t('move_map_area_up', { amount: '100' }) %>\">▲ 100%</button>\n        <button class=\"btn up-10\" title=\"<%- @t('move_map_area_up', { amount: '10' }) %>\">▲ 10%</button>\n        <div class=\"spacer\"></div>\n        <button class=\"btn down-10\" title=\"<%- @t('move_map_area_down', { amount: '10' }) %>\">▼ 10%</button>\n        <button class=\"btn down-100\" title=\"<%- @t('move_map_area_down', { amount: '100' }) %>\">▼ 100%</button>\n      </div>\n      <div class=\"column right\">\n        <button class=\"btn right-10 ib\" title=\"<%- @t('move_map_area_right', { amount: '10' }) %>\">▶ 10%</button>\n        <button class=\"btn right-100 ib\" title=\"<%- @t('move_map_area_right', { amount: '100' }) %>\">▶ 100%</button>\n      </div>\n\n      <div class=\"current-values\" aria-label=\"{{ multipart_current_adjustment_aria }}\">{{ multipart_current_adjustment }}:\n        <span class=\"multipart-adjustment-x\"></span>% X,\n        <span class=\"multipart-adjustment-y\"></span>% Y\n      </div>\n    </div>\n  </div>\n\n  <div class=\"row main-row submit-row\">\n    <input type=\"submit\" id=\"submit-button\" class=\"large-row green-button\"\n           value=\"{{ create_tactile_map }}\" data-original-text=\"{{ create_tactile_map }}\"\n           aria-live=\"polite\" />\n  </div>\n\n  <div class=\"drag-map-with-mouse\" aria-hidden=\"true\">\n    {{ drag_map }}\n  </div>\n\n  <div id=\"map-area-preview-container\" class=\"square-block-container\" aria-hidden=\"true\" style=\"display: none\">\n    <div class=\"dummy\">\n      <div id=\"map-area-preview\" class=\"square-block\" frameborder=\"0\" scrolling=\"no\"\n           marginheight=\"0\" marginwidth=\"0\" tabindex=\"100\"></div>\n    </div>\n  </div>\n\n  <div aria-live=\"off\" class=\"map-info\">\n    <label class=\"visuallyhidden\">{{ map_diameter }}</label>\n    <span class=\"texts\">\n      <span class=\"current-diameter-meters\" aria-live=\"off\"></span> {{ meters }}\n      (<span class=\"current-diameter-yards\" aria-live=\"off\"></span> {{ yards }})\n    </span>\n    <div class=\"map-info-arrow\">\n      <div class=\"arrow left\"></div>\n      <div class=\"arrow right\"></div>\n    </div>\n  </div>\n\n  <div class=\"back-to-previous-page-container\">\n    <a class=\"back-to-previous-page\" href=\".\">{{ back_to_index }}</a>\n  </div>\n\n  <div id=\"marker1-overlay\" class=\"marker-container\" title=\"{{ tooltip_selected_address }}\">\n    <div class=\"marker1-star part1\"></div>\n    <div class=\"marker1-star part2\"></div>\n    <div class=\"marker1-star part3\"></div>\n    <div class=\"marker1-star part4\"></div>\n    <div class=\"marker-tag\"></div>\n  </div>\n  <div id=\"marker1-overlay-empty\"></div>\n</div>\n"
  },
  {
    "path": "web/pre-src/end.pre",
    "content": "        <footer>\n          <p class=\"credits\">\n            <span class=\"sentence\"><%- @t('map_data_by', { linkTag: '<a href=\"https://en.wikipedia.org/wiki/OpenStreetMap\">OpenStreetMap</a>' }) %></span>\n            <div class=\"open-source\">\n              {{ touch_mapper_is_os }}\n              <%- @t('see_github', { startTag: '<a href=\"https://github.com/skarkkai/touch-mapper\">', endTag: '</a>' }) %>\n            </div>\n          </p>\n        </footer>\n      </div>\n    </div>\n\n    <script src=\"../scripts/app-common.js\"></script>\n    <script src=\"../scripts/[% pagename %].js\"></script>\n\n    <script>\n    \ttry {\n    \t\tvar storage = window.localStorage,\n    \t\t\tx = '__storage_test__';\n    \t\tstorage.setItem(x, x);\n    \t\tstorage.removeItem(x);\n    \t} catch(e) {\n        $(\".main-wrapper\").empty().html(\"<h1>Your browser is too old for Touch Mapper to work</h1><p>Support for localStorage required.</p>\");\n    \t}\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "web/pre-src/help.pre",
    "content": "<div class=\"close-help\">{{ close_tab_to_exit_help }}</div>\n\n<h2>{{ help }}</h2>\n\n<div class=\"help-content\">\n  <h3>{{ what_is_the_map_like }}</h3>\n  <p>\n    {{ what_is_the_map_like__map_content }}\n    <ul class=\"info-list\">\n      <li>{{ what_is_the_map_like__roads }}</li>\n      <li>{{ what_is_the_map_like__buildings }}</li>\n      <li>{{ what_is_the_map_like__railways }}</li>\n      <li>{{ what_is_the_map_like__water }}</li>\n      <li>{{ what_is_the_map_like__north }}</li>\n      <li>{{ what_is_the_map_like__selected_addr }}</li>\n    </ul>\n  </p>\n  {{ what_is_the_map_like__clarity }}\n\n  <h3 id=\"larger-scale-maps\">{{ larger_scale_maps }}</h3>\n  <p>\n    <%- @t('larger_scale_maps_answer', { aphLinkStart: '<a href=\"http://shop.aph.org/webapp/wcs/stores/servlet/Category_413A9B_10001_11051_20905__20815\">', aphLinkEnd: '</a>' }) %>\n  </p>\n\n  <h3 id=\"data-from-osm\">{{ data_comes_from_osm }}</h3>\n  <p>\n    <%- @t('data_comes', { startTag: '<a href=\"https://en.wikipedia.org/wiki/OpenStreetMap\">', endTag: '</a>' }) %>\n  </p>\n\n  <h3 id=\"commercial-use\">{{ commercial_use }}</h3>\n  <p>{{ commercial_use_answer }}</p>\n\n  <h3 id=\"users\">{{ users_and_purposes }}</h3>\n  <p>{{ tactile_maps_have_many_different_user_groups }}</p>\n  <ul class=\"info-list\">\n    <li>{{ visually_impaired_individuals }}</li>\n    <li>{{ outdoors_tourist_attractions }}</li>\n    <li>{{ public_areas_such_as_parks }}</li>\n    <li>{{ orientation_and_mobility_specialists }}</li>\n  </ul>\n\n  <h3 id=\"multipart-maps\">{{ multipart_maps_heading }}</h3>\n  <p>\n    {{ multipart_maps }}\n  </p>\n\n  <h3 id=\"how-to-3d-print\">{{ how_to_3d_print }}</a></h3>\n\n  <p>{{ map_can_be_printed }}</p>\n  <p>{{ if_you_print_yourself }}</p>\n\n  <h3 id=\"about\">{{ about_touch_mapper }}</h3>\n\n  <p>\n    <%- @t('about_touch_mapper_answer') %>\n  </p>\n</div>\n"
  },
  {
    "path": "web/pre-src/index.pre",
    "content": "<div class=\"intro\">\n  <p>{{ intro }}</p>\n</div>\n\n<div class=\"steps\">\n  <h2>{{ it_is_simple }}</h2>\n\n  <ul class=\"info-list\">\n    <li><span class=\"bullet\">1</span>{{ simple__enter_address }}</li>\n    <li><span class=\"bullet\">2</span>{{ simple__click_create_tactile_map }}</li>\n    <li><span class=\"bullet\">3</span>{{ simple__intro_order_or_print }}</li>\n  </ul>\n</div>\n\n<div class=\"row main-row address-input-row\">\n  <div class=\"col col1\">\n    <label for=\"address-input\">{{ street_address }}:</label>\n  </div>\n  <div class=\"col col2\">\n    <form id=\"address-search-form\" action=\"#\">\n      <div class=\"address-input-container\">\n        <input id=\"address-input\" value=\"\" />\n       </div>\n      <div class=\"address-search-submit-container\">\n        <input id=\"address-search-submit\" value=\"{{ search }}\" type=\"submit\" />\n      </div>\n    </form>\n\n    <div id=\"searching\" class=\"search-result-info\" style=\"display: none\">{{ searching }}</div>\n    <div id=\"no-search-results\" class=\"search-result-info\" role=\"alert\" aria-live=\"assertive\" tabindex=\"-1\" style=\"display: none\">{{ nothing_found }}</div>\n  </div>\n</div>\n\n<p class=\"help-has-more-info\">\n  <%- @t('help_has_more_info', { startTag: '<a href=\"help\" target=\"_blank\">', endTag: '</a>' }) %>.\n</p>\n\n<div id=\"dummy-google-map\" style=\"display: none\"></div>\n"
  },
  {
    "path": "web/pre-src/map.pre",
    "content": "<div class=\"show-on-load\" style=\"display: none\">\n  <script type=\"importmap\">\n{\n  \"imports\": {\n    \"three\": \"../scripts/three-r182.module.js\",\n    \"three/addons/\": \"../scripts/three-addons/\"\n  }\n}\n  </script>\n  <script type=\"module\">\nwindow.tmImportModule = function(modulePath) {\n  return import(modulePath);\n};\n  </script>\n  <script>\nwindow.TM = { translations: {\n  \"location2_bottom_left\": \"{{ location2_bottom_left }}\",\n  \"location2_bottom_right\": \"{{ location2_bottom_right }}\",\n  \"location2_bottom_row\": \"{{ location2_bottom_row }}\",\n  \"location2_left_column\": \"{{ location2_left_column }}\",\n  \"location2_right_column\": \"{{ location2_right_column }}\",\n  \"location2_top_left\": \"{{ location2_top_left }}\",\n  \"location2_top_right\": \"{{ location2_top_right }}\",\n  \"location2_top_row\": \"{{ location2_top_row }}\",\n\n  \"location3_middle_center\": \"{{ location3_middle_center }}\",\n  \"location3_bottom_center\": \"{{ location3_bottom_center }}\",\n  \"location3_bottom_left\": \"{{ location3_bottom_left }}\",\n  \"location3_bottom_left_and_center\": \"{{ location3_bottom_left_and_center }}\",\n  \"location3_bottom_left_and_middle\": \"{{ location3_bottom_left_and_middle }}\",\n  \"location3_bottom_left_to_mc\": \"{{ location3_bottom_left_to_mc }}\",\n  \"location3_bottom_left_to_middle_right\": \"{{ location3_bottom_left_to_middle_right }}\",\n  \"location3_bottom_left_to_top_center\": \"{{ location3_bottom_left_to_top_center }}\",\n  \"location3_bottom_right\": \"{{ location3_bottom_right }}\",\n  \"location3_bottom_right_and_center\": \"{{ location3_bottom_right_and_center }}\",\n  \"location3_bottom_right_and_middle\": \"{{ location3_bottom_right_and_middle }}\",\n  \"location3_bottom_right_to_mc\": \"{{ location3_bottom_right_to_mc }}\",\n  \"location3_bottom_right_to_middle_left\": \"{{ location3_bottom_right_to_middle_left }}\",\n  \"location3_bottom_right_to_top_center\": \"{{ location3_bottom_right_to_top_center }}\",\n  \"location3_bottom_row\": \"{{ location3_bottom_row }}\",\n  \"location3_center_column\": \"{{ location3_center_column }}\",\n  \"location3_left_column\": \"{{ location3_left_column }}\",\n  \"location3_middle_left\": \"{{ location3_middle_left }}\",\n  \"location3_middle_left_to_bottom_center\": \"{{ location3_middle_left_to_bottom_center }}\",\n  \"location3_middle_right\": \"{{ location3_middle_right }}\",\n  \"location3_middle_right_to_bottom_center\": \"{{ location3_middle_right_to_bottom_center }}\",\n  \"location3_middle_row\": \"{{ location3_middle_row }}\",\n  \"location3_near_bottom_left\": \"{{ location3_near_bottom_left }}\",\n  \"location3_near_bottom_right\": \"{{ location3_near_bottom_right }}\",\n  \"location3_near_top_left\": \"{{ location3_near_top_left }}\",\n  \"location3_near_top_right\": \"{{ location3_near_top_right }}\",\n  \"location3_right_column\": \"{{ location3_right_column }}\",\n  \"location3_top_center\": \"{{ location3_top_center }}\",\n  \"location3_top_center_to_middle_left\": \"{{ location3_top_center_to_middle_left }}\",\n  \"location3_top_center_to_middle_right\": \"{{ location3_top_center_to_middle_right }}\",\n  \"location3_top_left\": \"{{ location3_top_left }}\",\n  \"location3_top_left_and_center\": \"{{ location3_top_left_and_center }}\",\n  \"location3_top_left_and_middle\": \"{{ location3_top_left_and_middle }}\",\n  \"location3_top_left_diagonal\": \"{{ location3_top_left_diagonal }}\",\n  \"location3_top_left_to_bottom_center\": \"{{ location3_top_left_to_bottom_center }}\",\n  \"location3_top_left_to_mc\": \"{{ location3_top_left_to_mc }}\",\n  \"location3_top_left_to_middle_right\": \"{{ location3_top_left_to_middle_right }}\",\n  \"location3_top_right\": \"{{ location3_top_right }}\",\n  \"location3_top_right_and_center\": \"{{ location3_top_right_and_center }}\",\n  \"location3_top_right_and_middle\": \"{{ location3_top_right_and_middle }}\",\n  \"location3_top_right_diagonal\": \"{{ location3_top_right_diagonal }}\",\n  \"location3_top_right_to_bottom_center\": \"{{ location3_top_right_to_bottom_center }}\",\n  \"location3_top_right_to_mc\": \"{{ location3_top_right_to_mc }}\",\n  \"location3_top_right_to_middle_left\": \"{{ location3_top_right_to_middle_left }}\",\n  \"location3_top_row\": \"{{ location3_top_row }}\",\n\n  \"location3_mostly_top_row\": \"{{ location3_mostly_top_row }}\",\n  \"location3_mostly_bottom_row\": \"{{ location3_mostly_bottom_row }}\",\n  \"location3_mostly_right_column\": \"{{ location3_mostly_right_column }}\",\n  \"location3_mostly_left_column\": \"{{ location3_mostly_left_column }}\",\n\n  \"content__unnamed_roads\": \"{{ content__unnamed_roads }}\",\n  \"map_content_unnamed_roads_one\": \"{{ map_content_unnamed_roads_one }}\",\n  \"map_content_unnamed_roads_many\": \"{{ map_content_unnamed_roads_many }}\",\n  \"location_general\": \"{{ location_general }}\",\n  \"map_content_heading\": \"{{ map_content_heading }}\",\n  \"map_content_subheading_roads\": \"{{ map_content_subheading_roads }}\",\n  \"map_content_subheading_paths\": \"{{ map_content_subheading_paths }}\",\n  \"map_content_subheading_railways\": \"{{ map_content_subheading_railways }}\",\n  \"map_content_subheading_waterways\": \"{{ map_content_subheading_waterways }}\",\n  \"map_content_subheading_water_areas\": \"{{ map_content_subheading_water_areas }}\",\n  \"map_content_subheading_other_linear\": \"{{ map_content_subheading_other_linear }}\",\n  \"map_content_subheading_buildings\": \"{{ map_content_subheading_buildings }}\",\n  \"map_content_subheading_poi_familiar\": \"{{ map_content_subheading_poi_familiar }}\",\n  \"map_content_subheading_poi_daily\": \"{{ map_content_subheading_poi_daily }}\",\n  \"map_content_subheading_poi_transport\": \"{{ map_content_subheading_poi_transport }}\",\n  \"map_content_subheading_poi_accessibility\": \"{{ map_content_subheading_poi_accessibility }}\",\n  \"map_content_way_unnamed\": \"{{ map_content_way_unnamed }}\",\n  \"map_content_way_unnamed_waterway\": \"{{ map_content_way_unnamed_waterway }}\",\n  \"map_content_no_roads\": \"{{ map_content_no_roads }}\",\n  \"map_content_no_paths\": \"{{ map_content_no_paths }}\",\n  \"map_content_no_railways\": \"{{ map_content_no_railways }}\",\n  \"map_content_no_waterways\": \"{{ map_content_no_waterways }}\",\n  \"map_content_no_water_areas\": \"{{ map_content_no_water_areas }}\",\n  \"map_content_no_other_linear\": \"{{ map_content_no_other_linear }}\",\n  \"map_content_unnamed_features_one\": \"{{ map_content_unnamed_features_one }}\",\n  \"map_content_unnamed_features_many\": \"{{ map_content_unnamed_features_many }}\",\n  \"map_content_way_route_from_to\": \"{{ map_content_way_route_from_to }}\",\n  \"map_content_way_route_near\": \"{{ map_content_way_route_near }}\",\n  \"map_content_connects_to_way\": \"{{ map_content_connects_to_way }}\",\n  \"map_content_connects_to_ways\": \"{{ map_content_connects_to_ways }}\",\n  \"map_content_connects_to_type\": \"{{ map_content_connects_to_type }}\",\n  \"map_content_connects_to_type_many\": \"{{ map_content_connects_to_type_many }}\",\n  \"map_content_connects_to_type_many_fallback\": \"{{ map_content_connects_to_type_many_fallback }}\",\n  \"map_content_way_junction_one\": \"{{ map_content_way_junction_one }}\",\n  \"map_content_way_junction_many\": \"{{ map_content_way_junction_many }}\",\n  \"map_content_way_length_m\": \"{{ map_content_way_length_m }}\",\n  \"map_content_way_lanes_many\": \"{{ map_content_way_lanes_many }}\",\n  \"map_content_way_surface_paved\": \"{{ map_content_way_surface_paved }}\",\n  \"map_content_way_surface_unpaved\": \"{{ map_content_way_surface_unpaved }}\",\n  \"map_content_way_surface_unknown\": \"{{ map_content_way_surface_unknown }}\",\n  \"map_content_way_meta\": \"{{ map_content_way_meta }}\",\n  \"map_content_way_meta_type_only\": \"{{ map_content_way_meta_type_only }}\",\n  \"map_content_way_meta_length_only\": \"{{ map_content_way_meta_length_only }}\",\n  \"map_content_way_type_A1_road_construction\": \"{{ map_content_way_type_A1_road_construction }}\",\n  \"map_content_way_type_A1_major_roads\": \"{{ map_content_way_type_A1_major_roads }}\",\n  \"map_content_way_type_A1_secondary_roads\": \"{{ map_content_way_type_A1_secondary_roads }}\",\n  \"map_content_way_type_A1_local_streets\": \"{{ map_content_way_type_A1_local_streets }}\",\n  \"map_content_way_type_A1_service_roads\": \"{{ map_content_way_type_A1_service_roads }}\",\n  \"map_content_way_type_plural_A1_service_roads\": \"{{ map_content_way_type_plural_A1_service_roads }}\",\n  \"map_content_way_type_A1_track_roads\": \"{{ map_content_way_type_A1_track_roads }}\",\n  \"map_content_way_type_A1_vehicle_unspecified\": \"{{ map_content_way_type_A1_vehicle_unspecified }}\",\n  \"map_content_way_type_A2_pedestrian_streets\": \"{{ map_content_way_type_A2_pedestrian_streets }}\",\n  \"map_content_way_type_A2_footpaths_trails\": \"{{ map_content_way_type_A2_footpaths_trails }}\",\n  \"map_content_way_type_A2_cycleways\": \"{{ map_content_way_type_A2_cycleways }}\",\n  \"map_content_way_type_A2_steps_ramps\": \"{{ map_content_way_type_A2_steps_ramps }}\",\n  \"map_content_way_type_A2_pedestrian_unspecified\": \"{{ map_content_way_type_A2_pedestrian_unspecified }}\",\n  \"map_content_way_type_A3_rail_lines\": \"{{ map_content_way_type_A3_rail_lines }}\",\n  \"map_content_way_type_A3_tram_light_rail\": \"{{ map_content_way_type_A3_tram_light_rail }}\",\n  \"map_content_way_type_A3_subway_metro\": \"{{ map_content_way_type_A3_subway_metro }}\",\n  \"map_content_way_type_A3_rail_yards_sidings\": \"{{ map_content_way_type_A3_rail_yards_sidings }}\",\n  \"map_content_way_type_A4_rivers\": \"{{ map_content_way_type_A4_rivers }}\",\n  \"map_content_way_type_A4_streams_canals\": \"{{ map_content_way_type_A4_streams_canals }}\",\n  \"map_content_way_type_A4_ditches_drains\": \"{{ map_content_way_type_A4_ditches_drains }}\",\n  \"map_content_way_type_A4_other_waterways\": \"{{ map_content_way_type_A4_other_waterways }}\",\n  \"map_content_way_type_A_other_ways\": \"{{ map_content_way_type_A_other_ways }}\",\n  \"map_content_building_unnamed\": \"{{ map_content_building_unnamed }}\",\n  \"map_content_water_area_unnamed\": \"{{ map_content_water_area_unnamed }}\",\n  \"map_content_water_area_type_generic\": \"{{ map_content_water_area_type_generic }}\",\n  \"map_content_water_area_type_generic_plural\": \"{{ map_content_water_area_type_generic_plural }}\",\n  \"map_content_water_area_type_B1_lakes\": \"{{ map_content_water_area_type_B1_lakes }}\",\n  \"map_content_water_area_type_B1_ponds\": \"{{ map_content_water_area_type_B1_ponds }}\",\n  \"map_content_water_area_type_B1_reservoirs\": \"{{ map_content_water_area_type_B1_reservoirs }}\",\n  \"map_content_water_area_type_B1_sea_coast\": \"{{ map_content_water_area_type_B1_sea_coast }}\",\n  \"map_content_water_area_type_B1_riverbanks\": \"{{ map_content_water_area_type_B1_riverbanks }}\",\n  \"map_content_water_area_type_B1_other_water\": \"{{ map_content_water_area_type_B1_other_water }}\",\n  \"map_content_no_buildings\": \"{{ map_content_no_buildings }}\",\n  \"map_content_no_poi_familiar\": \"{{ map_content_no_poi_familiar }}\",\n  \"map_content_no_poi_daily\": \"{{ map_content_no_poi_daily }}\",\n  \"map_content_no_poi_transport\": \"{{ map_content_no_poi_transport }}\",\n  \"map_content_no_poi_accessibility\": \"{{ map_content_no_poi_accessibility }}\",\n  \"map_content_show_more_buildings_one\": \"{{ map_content_show_more_buildings_one }}\",\n  \"map_content_show_more_buildings_many\": \"{{ map_content_show_more_buildings_many }}\",\n  \"map_content_show_less_buildings\": \"{{ map_content_show_less_buildings }}\",\n  \"map_content_show_more_features_one\": \"{{ map_content_show_more_features_one }}\",\n  \"map_content_show_more_features_many\": \"{{ map_content_show_more_features_many }}\",\n  \"map_content_show_less_features\": \"{{ map_content_show_less_features }}\",\n  \"map_content_summary_show_more\": \"{{ map_content_summary_show_more }}\",\n  \"map_content_summary_show_only\": \"{{ map_content_summary_show_only }}\",\n  \"map_content_height_note_raised_mm\": \"{{ map_content_height_note_raised_mm }}\",\n  \"map_content_height_note_varying\": \"{{ map_content_height_note_varying }}\",\n  \"map_content_height_note_waved_surface\": \"{{ map_content_height_note_waved_surface }}\",\n  \"map_content_height_note_text_only\": \"{{ map_content_height_note_text_only }}\",\n  \"map_content_show_more_pois_one\": \"{{ map_content_show_more_pois_one }}\",\n  \"map_content_show_more_pois_many\": \"{{ map_content_show_more_pois_many }}\",\n  \"map_content_show_less_pois\": \"{{ map_content_show_less_pois }}\",\n  \"map_content_poi_unknown_type\": \"{{ map_content_poi_unknown_type }}\",\n  \"map_content_poi_unnamed\": \"{{ map_content_poi_unnamed }}\",\n  \"map_content_poi_unnamed_many\": \"{{ map_content_poi_unnamed_many }}\",\n  \"map_content_poi_sparse_note\": \"{{ map_content_poi_sparse_note }}\",\n  \"map_content_poi_access_crossings_signals_one\": \"{{ map_content_poi_access_crossings_signals_one }}\",\n  \"map_content_poi_access_crossings_signals_many\": \"{{ map_content_poi_access_crossings_signals_many }}\",\n  \"map_content_poi_access_tactile_summary\": \"{{ map_content_poi_access_tactile_summary }}\",\n  \"map_content_poi_access_kerb_summary\": \"{{ map_content_poi_access_kerb_summary }}\",\n  \"map_content_poi_access_steps_summary\": \"{{ map_content_poi_access_steps_summary }}\",\n  \"map_content_poi_access_incline_summary\": \"{{ map_content_poi_access_incline_summary }}\",\n  \"map_content_poi_access_wheelchair_summary\": \"{{ map_content_poi_access_wheelchair_summary }}\",\n  \"map_content_poi_example\": \"{{ map_content_poi_example }}\",\n  \"map_content_building_join_at\": \"{{ map_content_building_join_at }}\",\n  \"map_content_building_type_from_value\": \"{{ map_content_building_type_from_value }}\",\n  \"map_content_unavailable\": \"{{ map_content_unavailable }}\",\n  \"map_content_list_and\": \"{{ map_content_list_and }}\",\n  \"map_content_edge_phrase\": \"{{ map_content_edge_phrase }}\",\n  \"map_content_touches_edge\": \"{{ map_content_touches_edge }}\",\n  \"map_content_touches_edges\": \"{{ map_content_touches_edges }}\",\n  \"map_content_touches_items\": \"{{ map_content_touches_items }}\",\n  \"map_content_crosses_edge\": \"{{ map_content_crosses_edge }}\",\n  \"map_content_crosses_edges\": \"{{ map_content_crosses_edges }}\",\n  \"map_content_crosses_items\": \"{{ map_content_crosses_items }}\",\n  \"map_content_touches_edge_percent\": \"{{ map_content_touches_edge_percent }}\",\n  \"map_content_touch_pos_north\": \"{{ map_content_touch_pos_north }}\",\n  \"map_content_touch_pos_south\": \"{{ map_content_touch_pos_south }}\",\n  \"map_content_touch_pos_west\": \"{{ map_content_touch_pos_west }}\",\n  \"map_content_touch_pos_east\": \"{{ map_content_touch_pos_east }}\",\n  \"map_content_touch_pos_center\": \"{{ map_content_touch_pos_center }}\",\n  \"map_content_touch_pos_multiple\": \"{{ map_content_touch_pos_multiple }}\",\n  \"map_content_edge_north\": \"{{ map_content_edge_north }}\",\n  \"map_content_edge_south\": \"{{ map_content_edge_south }}\",\n  \"map_content_edge_east\": \"{{ map_content_edge_east }}\",\n  \"map_content_edge_west\": \"{{ map_content_edge_west }}\",\n  \"map_content_dir_north\": \"{{ map_content_dir_north }}\",\n  \"map_content_dir_northeast\": \"{{ map_content_dir_northeast }}\",\n  \"map_content_dir_east\": \"{{ map_content_dir_east }}\",\n  \"map_content_dir_southeast\": \"{{ map_content_dir_southeast }}\",\n  \"map_content_dir_south\": \"{{ map_content_dir_south }}\",\n  \"map_content_dir_southwest\": \"{{ map_content_dir_southwest }}\",\n  \"map_content_dir_west\": \"{{ map_content_dir_west }}\",\n  \"map_content_dir_northwest\": \"{{ map_content_dir_northwest }}\",\n  \"map_content_corner_northwest\": \"{{ map_content_corner_northwest }}\",\n  \"map_content_corner_northeast\": \"{{ map_content_corner_northeast }}\",\n  \"map_content_corner_southwest\": \"{{ map_content_corner_southwest }}\",\n  \"map_content_corner_southeast\": \"{{ map_content_corner_southeast }}\",\n  \"map_content_loc_center\": \"{{ map_content_loc_center }}\",\n  \"map_content_loc_offset_center\": \"{{ map_content_loc_offset_center }}\",\n  \"map_content_loc_part\": \"{{ map_content_loc_part }}\",\n  \"map_content_loc_edge\": \"{{ map_content_loc_edge }}\",\n  \"map_content_loc_corner\": \"{{ map_content_loc_corner }}\",\n  \"map_content_loc_near_edge\": \"{{ map_content_loc_near_edge }}\",\n  \"map_content_loc_near_corner\": \"{{ map_content_loc_near_corner }}\",\n  \"map_content_loc_full_center\": \"{{ map_content_loc_full_center }}\",\n  \"map_content_loc_full_offset_center\": \"{{ map_content_loc_full_offset_center }}\",\n  \"map_content_loc_full_part\": \"{{ map_content_loc_full_part }}\",\n  \"map_content_loc_full_edge\": \"{{ map_content_loc_full_edge }}\",\n  \"map_content_loc_full_corner\": \"{{ map_content_loc_full_corner }}\",\n  \"map_content_loc_full_near_edge\": \"{{ map_content_loc_full_near_edge }}\",\n  \"map_content_loc_full_near_corner\": \"{{ map_content_loc_full_near_corner }}\",\n  \"map_content_loc_endpoint_center\": \"{{ map_content_loc_endpoint_center }}\",\n  \"map_content_loc_endpoint_part\": \"{{ map_content_loc_endpoint_part }}\",\n  \"map_content_loc_endpoint_near_edge\": \"{{ map_content_loc_endpoint_near_edge }}\",\n  \"map_content_loc_endpoint_corner\": \"{{ map_content_loc_endpoint_corner }}\",\n  \"map_content_loc_route_from_center\": \"{{ map_content_loc_route_from_center }}\",\n  \"map_content_loc_route_from_part\": \"{{ map_content_loc_route_from_part }}\",\n  \"map_content_loc_route_from_near_edge\": \"{{ map_content_loc_route_from_near_edge }}\",\n  \"map_content_loc_route_from_corner\": \"{{ map_content_loc_route_from_corner }}\",\n  \"map_content_loc_route_to_center\": \"{{ map_content_loc_route_to_center }}\",\n  \"map_content_loc_route_to_part\": \"{{ map_content_loc_route_to_part }}\",\n  \"map_content_loc_route_to_near_edge\": \"{{ map_content_loc_route_to_near_edge }}\",\n  \"map_content_loc_route_to_corner\": \"{{ map_content_loc_route_to_corner }}\",\n  \"map_content_region_center\": \"{{ map_content_region_center }}\",\n  \"map_content_region_part\": \"{{ map_content_region_part }}\",\n  \"map_content_region_edge\": \"{{ map_content_region_edge }}\",\n  \"map_content_region_corner\": \"{{ map_content_region_corner }}\",\n  \"map_content_summary_in\": \"{{ map_content_summary_in }}\",\n  \"map_content_summary_near\": \"{{ map_content_summary_near }}\",\n  \"map_content_summary_mostly_in_lead\": \"{{ map_content_summary_mostly_in_lead }}\",\n  \"map_content_summary_mainly_near_lead\": \"{{ map_content_summary_mainly_near_lead }}\",\n  \"map_content_summary_mostly_extending\": \"{{ map_content_summary_mostly_extending }}\",\n  \"map_content_summary_in_and\": \"{{ map_content_summary_in_and }}\",\n  \"map_content_summary_in_and_near\": \"{{ map_content_summary_in_and_near }}\",\n  \"map_content_summary_near_and\": \"{{ map_content_summary_near_and }}\",\n  \"map_content_summary_mainly_in_and\": \"{{ map_content_summary_mainly_in_and }}\",\n  \"map_content_summary_mainly_in_and_near\": \"{{ map_content_summary_mainly_in_and_near }}\",\n  \"map_content_summary_mainly_near_and\": \"{{ map_content_summary_mainly_near_and }}\",\n  \"map_content_summary_mainly_near_and_in\": \"{{ map_content_summary_mainly_near_and_in }}\",\n  \"map_content_summary_several_mainly_in\": \"{{ map_content_summary_several_mainly_in }}\",\n  \"map_content_summary_several_mainly_near\": \"{{ map_content_summary_several_mainly_near }}\",\n  \"map_content_summary_equal_clauses\": \"{{ map_content_summary_equal_clauses }}\",\n  \"map_content_summary_mostly_extending_clauses\": \"{{ map_content_summary_mostly_extending_clauses }}\",\n  \"map_content_summary_distributed_top2\": \"{{ map_content_summary_distributed_top2 }}\",\n  \"map_content_summary_distributed_top1\": \"{{ map_content_summary_distributed_top1 }}\",\n  \"map_content_summary_three_regions_clauses\": \"{{ map_content_summary_three_regions_clauses }}\",\n  \"map_content_parts_one\": \"{{ map_content_parts_one }}\",\n  \"map_content_parts_many\": \"{{ map_content_parts_many }}\",\n  \"map_content_total_area\": \"{{ map_content_total_area }}\",\n  \"map_content_shape_elongated\": \"{{ map_content_shape_elongated }}\",\n  \"map_content_shape_long_thin\": \"{{ map_content_shape_long_thin }}\",\n  \"map_content_shape_irregular\": \"{{ map_content_shape_irregular }}\",\n  \"map_content_shape_generic\": \"{{ map_content_shape_generic }}\",\n  \"map_content_shape_thin\": \"{{ map_content_shape_thin }}\",\n  \"map_content_aspect_label\": \"{{ map_content_aspect_label }}\",\n  \"map_content_degrees_abbrev\": \"{{ map_content_degrees_abbrev }}\",\n  \"map_content_orientation\": \"{{ map_content_orientation }}\",\n  \"map_content_orientation_label_east_west\": \"{{ map_content_orientation_label_east_west }}\",\n  \"map_content_orientation_label_north_south\": \"{{ map_content_orientation_label_north_south }}\",\n  \"map_content_orientation_label_northeast_southwest\": \"{{ map_content_orientation_label_northeast_southwest }}\",\n  \"map_content_orientation_label_east_northeast_to_west_southwest\": \"{{ map_content_orientation_label_east_northeast_to_west_southwest }}\",\n  \"map_content_orientation_label_north_northeast_to_south_southwest\": \"{{ map_content_orientation_label_north_northeast_to_south_southwest }}\",\n  \"map_content_coverage_label\": \"{{ map_content_coverage_label }}\",\n  \"map_content_coverage_much\": \"{{ map_content_coverage_much }}\",\n  \"map_content_coverage_some\": \"{{ map_content_coverage_some }}\",\n  \"map_content_coverage_little\": \"{{ map_content_coverage_little }}\",\n  \"map_content_osm_value_kindergarten\": \"{{ map_content_osm_value_kindergarten }}\",\n  \"map_content_osm_value_school\": \"{{ map_content_osm_value_school }}\",\n  \"map_content_osm_value_hospital\": \"{{ map_content_osm_value_hospital }}\",\n  \"map_content_osm_value_church\": \"{{ map_content_osm_value_church }}\",\n  \"map_content_osm_value_university\": \"{{ map_content_osm_value_university }}\",\n  \"map_content_osm_value_college\": \"{{ map_content_osm_value_college }}\",\n  \"map_content_osm_value_library\": \"{{ map_content_osm_value_library }}\",\n  \"map_content_osm_value_gallery\": \"{{ map_content_osm_value_gallery }}\",\n  \"map_content_osm_value_museum\": \"{{ map_content_osm_value_museum }}\",\n  \"map_content_osm_value_theatre\": \"{{ map_content_osm_value_theatre }}\",\n  \"map_content_osm_value_supermarket\": \"{{ map_content_osm_value_supermarket }}\",\n  \"map_content_osm_value_cafe\": \"{{ map_content_osm_value_cafe }}\",\n  \"map_content_osm_value_restaurant\": \"{{ map_content_osm_value_restaurant }}\",\n  \"map_content_osm_value_hotel\": \"{{ map_content_osm_value_hotel }}\",\n  \"map_content_osm_value_hostel\": \"{{ map_content_osm_value_hostel }}\",\n  \"map_content_osm_value_parking\": \"{{ map_content_osm_value_parking }}\",\n  \"map_content_osm_value_bicycle_parking\": \"{{ map_content_osm_value_bicycle_parking }}\",\n  \"map_content_osm_value_residential\": \"{{ map_content_osm_value_residential }}\",\n  \"map_content_osm_value_commercial\": \"{{ map_content_osm_value_commercial }}\",\n  \"map_content_osm_value_industrial\": \"{{ map_content_osm_value_industrial }}\",\n  \"map_content_osm_value_public\": \"{{ map_content_osm_value_public }}\",\n  \"map_content_osm_value_roof\": \"{{ map_content_osm_value_roof }}\",\n  \"map_content_osm_value_transportation\": \"{{ map_content_osm_value_transportation }}\",\n  \"map_content_osm_value_service\": \"{{ map_content_osm_value_service }}\",\n  \"map_content_osm_value_civic\": \"{{ map_content_osm_value_civic }}\",\n  \"map_content_osm_value_office\": \"{{ map_content_osm_value_office }}\",\n  \"map_content_osm_value_apartments\": \"{{ map_content_osm_value_apartments }}\",\n  \"map_content_osm_value_house\": \"{{ map_content_osm_value_house }}\",\n  \"map_content_osm_value_detached\": \"{{ map_content_osm_value_detached }}\",\n  \"map_content_osm_value_terrace\": \"{{ map_content_osm_value_terrace }}\",\n  \"map_content_osm_value_dormitory\": \"{{ map_content_osm_value_dormitory }}\",\n  \"map_content_osm_value_semidetached_house\": \"{{ map_content_osm_value_semidetached_house }}\",\n  \"map_content_osm_value_farm\": \"{{ map_content_osm_value_farm }}\",\n  \"map_content_osm_value_warehouse\": \"{{ map_content_osm_value_warehouse }}\",\n  \"map_content_osm_value_garages\": \"{{ map_content_osm_value_garages }}\",\n  \"map_content_osm_value_garage\": \"{{ map_content_osm_value_garage }}\",\n  \"map_content_osm_value_retail\": \"{{ map_content_osm_value_retail }}\",\n  \"map_content_osm_value_kiosk\": \"{{ map_content_osm_value_kiosk }}\",\n  \"map_content_osm_value_playground\": \"{{ map_content_osm_value_playground }}\",\n  \"map_content_osm_value_park\": \"{{ map_content_osm_value_park }}\",\n  \"map_content_osm_value_zoo\": \"{{ map_content_osm_value_zoo }}\",\n  \"map_content_osm_value_sports_centre\": \"{{ map_content_osm_value_sports_centre }}\",\n  \"map_content_osm_value_station\": \"{{ map_content_osm_value_station }}\",\n  \"map_content_osm_value_rail\": \"{{ map_content_osm_value_rail }}\",\n  \"map_content_osm_value_motorway\": \"{{ map_content_osm_value_motorway }}\",\n  \"map_content_osm_value_trunk\": \"{{ map_content_osm_value_trunk }}\",\n  \"map_content_osm_value_primary\": \"{{ map_content_osm_value_primary }}\",\n  \"map_content_osm_value_secondary\": \"{{ map_content_osm_value_secondary }}\",\n  \"map_content_osm_value_tertiary\": \"{{ map_content_osm_value_tertiary }}\",\n  \"map_content_osm_value_unclassified\": \"{{ map_content_osm_value_unclassified }}\",\n  \"map_content_osm_value_living_street\": \"{{ map_content_osm_value_living_street }}\",\n  \"map_content_osm_value_pedestrian\": \"{{ map_content_osm_value_pedestrian }}\",\n  \"map_content_osm_value_footway\": \"{{ map_content_osm_value_footway }}\",\n  \"map_content_osm_value_path\": \"{{ map_content_osm_value_path }}\",\n  \"map_content_osm_value_cycleway\": \"{{ map_content_osm_value_cycleway }}\",\n  \"map_content_osm_value_steps\": \"{{ map_content_osm_value_steps }}\",\n  \"map_content_osm_value_track\": \"{{ map_content_osm_value_track }}\",\n  \"map_content_osm_value_road\": \"{{ map_content_osm_value_road }}\",\n  \"map_content_osm_value_platform\": \"{{ map_content_osm_value_platform }}\",\n  \"map_content_osm_value_stop_position\": \"{{ map_content_osm_value_stop_position }}\",\n  \"map_content_osm_value_stop_area\": \"{{ map_content_osm_value_stop_area }}\",\n  \"map_content_osm_value_stop_area_group\": \"{{ map_content_osm_value_stop_area_group }}\",\n  \"map_content_osm_value_halt\": \"{{ map_content_osm_value_halt }}\",\n  \"map_content_osm_value_bus_stop\": \"{{ map_content_osm_value_bus_stop }}\",\n  \"map_content_osm_value_bus_station\": \"{{ map_content_osm_value_bus_station }}\",\n  \"map_content_osm_value_tram_stop\": \"{{ map_content_osm_value_tram_stop }}\",\n  \"map_content_osm_value_tram\": \"{{ map_content_osm_value_tram }}\",\n  \"map_content_osm_value_subway_entrance\": \"{{ map_content_osm_value_subway_entrance }}\",\n  \"map_content_osm_value_ferry_terminal\": \"{{ map_content_osm_value_ferry_terminal }}\",\n  \"map_content_osm_value_taxi\": \"{{ map_content_osm_value_taxi }}\",\n  \"map_content_osm_value_department_store\": \"{{ map_content_osm_value_department_store }}\",\n  \"map_content_osm_value_mall\": \"{{ map_content_osm_value_mall }}\",\n  \"map_content_osm_value_attraction\": \"{{ map_content_osm_value_attraction }}\",\n  \"map_content_osm_value_information\": \"{{ map_content_osm_value_information }}\",\n  \"map_content_osm_value_fountain\": \"{{ map_content_osm_value_fountain }}\",\n  \"map_content_osm_value_viewpoint\": \"{{ map_content_osm_value_viewpoint }}\",\n  \"map_content_osm_value_theme_park\": \"{{ map_content_osm_value_theme_park }}\",\n  \"map_content_osm_value_aquarium\": \"{{ map_content_osm_value_aquarium }}\",\n  \"map_content_osm_value_arts_centre\": \"{{ map_content_osm_value_arts_centre }}\",\n  \"map_content_osm_value_community_centre\": \"{{ map_content_osm_value_community_centre }}\",\n  \"map_content_osm_value_place_of_worship\": \"{{ map_content_osm_value_place_of_worship }}\",\n  \"map_content_osm_value_townhall\": \"{{ map_content_osm_value_townhall }}\",\n  \"map_content_osm_value_courthouse\": \"{{ map_content_osm_value_courthouse }}\",\n  \"map_content_osm_value_pharmacy\": \"{{ map_content_osm_value_pharmacy }}\",\n  \"map_content_osm_value_clinic\": \"{{ map_content_osm_value_clinic }}\",\n  \"map_content_osm_value_doctors\": \"{{ map_content_osm_value_doctors }}\",\n  \"map_content_osm_value_dentist\": \"{{ map_content_osm_value_dentist }}\",\n  \"map_content_osm_value_toilets\": \"{{ map_content_osm_value_toilets }}\",\n  \"map_content_osm_value_post_office\": \"{{ map_content_osm_value_post_office }}\",\n  \"map_content_osm_value_bank\": \"{{ map_content_osm_value_bank }}\",\n  \"map_content_osm_value_atm\": \"{{ map_content_osm_value_atm }}\",\n  \"map_content_osm_value_police\": \"{{ map_content_osm_value_police }}\",\n  \"map_content_osm_value_fire_station\": \"{{ map_content_osm_value_fire_station }}\",\n  \"map_content_osm_value_fuel\": \"{{ map_content_osm_value_fuel }}\",\n  \"map_content_osm_value_charging_station\": \"{{ map_content_osm_value_charging_station }}\",\n  \"map_content_osm_value_convenience\": \"{{ map_content_osm_value_convenience }}\",\n  \"map_content_osm_value_bakery\": \"{{ map_content_osm_value_bakery }}\",\n  \"map_content_osm_value_clothes\": \"{{ map_content_osm_value_clothes }}\",\n  \"map_content_osm_value_fast_food\": \"{{ map_content_osm_value_fast_food }}\",\n  \"map_content_osm_value_bar\": \"{{ map_content_osm_value_bar }}\",\n  \"map_content_osm_value_pub\": \"{{ map_content_osm_value_pub }}\",\n  \"map_content_osm_value_marketplace\": \"{{ map_content_osm_value_marketplace }}\",\n  \"map_content_osm_value_bench\": \"{{ map_content_osm_value_bench }}\",\n  \"map_content_osm_value_waste_basket\": \"{{ map_content_osm_value_waste_basket }}\",\n  \"map_content_osm_value_vending_machine\": \"{{ map_content_osm_value_vending_machine }}\",\n  \"map_content_osm_value_street_lamp\": \"{{ map_content_osm_value_street_lamp }}\",\n  \"map_content_osm_value_outdoor_seating\": \"{{ map_content_osm_value_outdoor_seating }}\",\n  \"map_content_osm_value_sauna\": \"{{ map_content_osm_value_sauna }}\",\n  \"map_content_osm_value_shelter\": \"{{ map_content_osm_value_shelter }}\",\n  \"map_content_osm_value_abandoned\": \"{{ map_content_osm_value_abandoned }}\",\n  \"map_content_osm_value_razed\": \"{{ map_content_osm_value_razed }}\",\n  \"map_content_osm_value_bicycle\": \"{{ map_content_osm_value_bicycle }}\",\n  \"map_content_osm_value_building\": \"{{ map_content_osm_value_building }}\",\n  \"map_content_osm_value_apartments_building\": \"{{ map_content_osm_value_apartments_building }}\",\n  \"map_content_osm_value_house_building\": \"{{ map_content_osm_value_house_building }}\",\n  \"map_content_osm_value_detached_building\": \"{{ map_content_osm_value_detached_building }}\",\n  \"map_content_osm_value_terrace_building\": \"{{ map_content_osm_value_terrace_building }}\",\n  \"map_content_osm_value_dormitory_building\": \"{{ map_content_osm_value_dormitory_building }}\",\n  \"map_content_osm_value_residential_building\": \"{{ map_content_osm_value_residential_building }}\",\n  \"map_content_osm_value_commercial_building\": \"{{ map_content_osm_value_commercial_building }}\",\n  \"map_content_osm_value_retail_building\": \"{{ map_content_osm_value_retail_building }}\",\n  \"map_content_osm_value_office_building\": \"{{ map_content_osm_value_office_building }}\",\n  \"map_content_osm_value_industrial_building\": \"{{ map_content_osm_value_industrial_building }}\",\n  \"map_content_osm_value_warehouse_building\": \"{{ map_content_osm_value_warehouse_building }}\",\n  \"map_content_osm_value_school_building\": \"{{ map_content_osm_value_school_building }}\",\n  \"map_content_osm_value_university_building\": \"{{ map_content_osm_value_university_building }}\",\n  \"map_content_osm_value_hospital_building\": \"{{ map_content_osm_value_hospital_building }}\",\n  \"map_content_osm_value_church_building\": \"{{ map_content_osm_value_church_building }}\",\n  \"map_content_osm_value_garage_building\": \"{{ map_content_osm_value_garage_building }}\",\n  \"map_content_osm_value_roof_building\": \"{{ map_content_osm_value_roof_building }}\",\n  \"map_content_osm_value_public_building\": \"{{ map_content_osm_value_public_building }}\",\n  \"map_content_osm_value_garages_building\": \"{{ map_content_osm_value_garages_building }}\",\n  \"map_content_osm_value_yes\": \"{{ map_content_osm_value_yes }}\"\n} };\n</script>\n\n  <nav class=\"breadcrumb\" aria-label=\"{{ breadcrumb_nav_title }}\" role=\"navigation\">\n    <ul>\n      <li><a href=\".\">{{ breadcrumb_home }}</a></li>\n      <li><a href=\"area\">{{ breadcrumb_area }}</a></li>\n      <li aria-current=\"location\">{{ breadcrumb_map }}</li>\n    </ul>\n  </nav>\n\n  <h2>{{ map_for_address }} <span class=\"map-address\"></span></h2>\n\n  <div class=\"hidden-for-2d\">\n    <!--div class=\"main-row\">\n      <span class=\"printing-method-container\" style=\"margin-right: 25px\">\n       <input type=\"radio\" name=\"printing-method\" id=\"printing-method-order\" checked=\"checked\">\n       <label for=\"printing-method-order\">{{ map_printing__order }}</label>\n      </span>\n\n      <span class=\"printing-method-container\">\n       <input type=\"radio\" name=\"printing-method\" id=\"printing-method-self\">\n       <label for=\"printing-method-self\">{{ map_printing__self }}</label>\n      </span>\n    </div-->\n\n    <a class=\"download-link main-row large-row green-button x-hidden-for-order\" id=\"download-map\" target=\"_blank\">\n      <span>{{ download }}</span>\n    </a>\n\n    <!--\n    <a class=\"download-link main-row large-row green-button x-hidden-for-self-print\" id=\"order-map\">\n      <span class=\"order-download-link\">\n        <%- @t('order_from_1', { companyName: 'HeKeyTech' }) %><br>\n        <%- @t('order_from_2') %>\n      </span>\n    </a>\n    -->\n  </div>\n\n  <div class=\"main-row other-downloads\">\n      <details class=\"other-downloads-details\">\n        <summary class=\"other-downloads-summary\">{{ show_more_downloads }}</summary>\n        <ul class=\"other-downloads-list\">\n          <li><a class=\"other-downloads-link\" id=\"download-stl-ways\" target=\"_blank\" rel=\"noopener noreferrer\">{{ download_stl_ways }}</a></li>\n          <li><a class=\"other-downloads-link\" id=\"download-stl-rest\" target=\"_blank\" rel=\"noopener noreferrer\">{{ download_stl_rest }}</a></li>\n          <li class=\"hidden-for-2d\"><a class=\"other-downloads-link\" id=\"download-pdf-embossers\" target=\"_blank\" rel=\"noopener noreferrer\">{{ download_pdf_embossers }}</a></li>\n          <li class=\"hidden-for-2d\"><a class=\"other-downloads-link\" id=\"download-svg-embossers\" target=\"_blank\" rel=\"noopener noreferrer\">{{ download_svg_embossers }}</a></li>\n          <li><a class=\"other-downloads-link\" id=\"download-map-content\" target=\"_blank\" rel=\"noopener noreferrer\">{{ download_map_descriptions }}</a></li>\n          <li><a class=\"other-downloads-link\" id=\"download-blender\" target=\"_blank\" rel=\"noopener noreferrer\">{{ download_blender }}</a></li>\n        </ul>\n      </details>\n  </div>\n\n  <div class=\"hidden-for-3d\">\n    <a class=\"main-row large-row green-button\" id=\"download-svg\">\n      {{ open_or_download_svg }}\n    </a>\n\n    <a class=\"main-row large-row green-button\" id=\"download-pdf\">\n      {{ open_or_download_pdf }}\n    </a>\n\n    <!--p class=\"ordering-2d-later\">{{ emboss_ordering_later }}</p-->\n  </div>\n\n  <!-- ---------------- Email sending -------------------- -->\n  <div class=\"email-sending-row main-row large-row green-button\">\n    <span class=\"email-sending\">\n      <form action=\"#\" class=\"email-sending-form\">\n        <label for=\"email-addr\">{{ email_map_to }}</label>\n        <span class=\"inputs\">\n          <input name=\"email-addr\" id=\"email-addr\" type=\"email\">\n          <input type=\"submit\" value=\"{{ send_submit }}\" aria-label=\"{{ send_email }}\" class=\"submit-email\" />\n        </span>\n      </form>\n    </span>\n  </div>\n  <div class=\"email-sending-success email-sending-result\" style=\"display: none\" aria-live=\"assertive\" tabindex=\"-1\">\n    <span class=\"success-msg-text\">{{ email_sent }}</span>\n  </div>\n  <div class=\"email-sending-error email-sending-result error-msg\" style=\"display: none\" aria-live=\"assertive\" tabindex=\"-1\">\n    {{ error_sending_email }}: <span class=\"error-msg-text\"></span>\n  </div>\n\n  <div class=\"osm-attribution\">\n    <%- @t('osm_attribution', { startTag: '<a href=\"https://www.openstreetmap.org/copyright\">', endTag: '</a>' }) %>\n  </div>\n\n  <div class=\"hidden-for-2d\">\n    <div class=\"ready-map\">\n      <div class=\"preview-3d-container square-block-container\" aria-label=\"{{ label_3d_preview }}\">\n          <div class=\"dummy\">\n            <div class=\"preview-3d square-block\" frameborder=\"0\" scrolling=\"no\" marginheight=\"0\" marginwidth=\"0\"></div>\n          </div>\n      </div>\n      <p class=\"preview-3d-hint\">{{ map_preview_controls_hint }}</p>\n    </div>\n  </div>\n\n  <div class=\"hidden-for-3d\">\n    <div class=\"preview-2d-container\" aria-hidden=\"true\">\n      <img id=\"svg-preview\" />\n    </div>\n  </div>\n\n  <div class=\"map-description map-content-row\">\n    <h3>{{ map_content_heading }}</h3>\n    <div class=\"map-content\">\n      <div class=\"map-content-summary-block\">\n        <ul class=\"map-content-summary\"></ul>\n        <button type=\"button\"\n                class=\"map-content-summary-toggle\"\n                aria-expanded=\"false\"\n                aria-controls=\"map-content-full\"\n                style=\"display: none\">\n          {{ map_content_summary_show_more }}\n        </button>\n      </div>\n      <div id=\"map-content-full\" class=\"map-content-full\" hidden>\n        <div class=\"row map-content-roads-row\">\n          <h4>{{ map_content_subheading_roads }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-roads\"></ul>\n        </div>\n        <div class=\"row map-content-paths-row\">\n          <h4>{{ map_content_subheading_paths }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-paths\"></ul>\n        </div>\n        <div class=\"row map-content-railways-row\">\n          <h4>{{ map_content_subheading_railways }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-railways\"></ul>\n        </div>\n        <div class=\"row map-content-waterways-row\">\n          <h4>{{ map_content_subheading_waterways }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-waterways\"></ul>\n        </div>\n        <div class=\"row map-content-water-areas-row\">\n          <h4>{{ map_content_subheading_water_areas }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-water-areas\"></ul>\n        </div>\n        <div class=\"row map-content-other-linear-row\">\n          <h4>{{ map_content_subheading_other_linear }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-other-linear\"></ul>\n        </div>\n        <div class=\"row map-content-buildings-row\">\n          <h4>{{ map_content_subheading_buildings }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-buildings\"></ul>\n        </div>\n        <div class=\"row map-content-poi-familiar-row\">\n          <h4>{{ map_content_subheading_poi_familiar }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-poi-familiar\"></ul>\n        </div>\n        <div class=\"row map-content-poi-daily-row\">\n          <h4>{{ map_content_subheading_poi_daily }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-poi-daily\"></ul>\n        </div>\n        <div class=\"row map-content-poi-transport-row\">\n          <h4>{{ map_content_subheading_poi_transport }}</h4>\n          <div class=\"map-content-section-height\"></div>\n          <ul class=\"map-content-poi-transport\"></ul>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"no-data-available-msg\" style=\"display: none\">\n    <span>{{ data_no_longer_available }}</span>\n  </div>\n\n  <div class=\"back-to-previous-page-container\">\n    <a class=\"back-to-previous-page\" href=\"area\">{{ back_to_area }}</a>\n  </div>\n</div>\n"
  },
  {
    "path": "web/pre-src/start.pre",
    "content": "<!doctype html>\n<html class=\"page-[% pagename %]\">\n  <!-- ex: set tabstop=2 shiftwidth=2 softtabstop=2 expandtab: -->\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"description\" content=\"{{ meta_description }}\">\n    <meta name=\"keywords\" content=\"{{ meta_keywords }}\">\n    <meta name=\"author\" content=\"Sofia Pahaoja\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta property=\"og:site_name\" content=\"{{ meta_og_site_name }}\" />\n    <meta property=\"og:url\" content=\"https://touch-mapper.org/\" />\n    <meta property=\"og:image\" content=\"https://touch-mapper.org/images/og_image.jpg\" />\n    <title>{{ title }} | Touch Mapper</title>\n\n    <link rel=\"icon\" type=\"image/png\" href=\"/images/favicon.png\" />\n    <link href='//fonts.googleapis.com/css?family=Quattrocento+Sans:400,700' rel='stylesheet' type='text/css'>\n    <link rel=\"stylesheet\" href=\"../styles/common.css\">\n    <link rel=\"stylesheet\" href=\"../styles/[% pagename %].css\">\n    <link rel=\"alternate\" href=\"/en\" hreflang=\"en\" />\n    <link rel=\"alternate\" href=\"/de\" hreflang=\"de\" />\n    <link rel=\"alternate\" href=\"/es\" hreflang=\"es\" />\n    <link rel=\"alternate\" href=\"/fi\" hreflang=\"fi\" />\n    <link rel=\"alternate\" href=\"/nl\" hreflang=\"nl\" />\n  </head>\n  <body>\n\n    <a class=\"visuallyhidden\" href=\"#main-content\">{{ skip_to_main_content }}</a>\n\n    <div class=\"main-wrapper\">\n      <div role=\"banner\" class=\"banner\">\n        <div class=\"banner-texts\">\n          <h1>\n            <a href=\".\" style=\"display: table;margin: auto;\">\n              <div class=\"logo-bg\"></div>\n              <svg class=\"logo\" alt=\"Touch Mapper\" aria-labelledby=\"h1-title\" role=\"img\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"431.05673\" height=\"192.177\" viewBox=\"0 0 431.05673 192.177\" id=\"Layer_1\" xml:space=\"preserve\">\n                <title id=\"h1-title\">Touch Mapper</title>\n                <metadata id=\"metadata35\"><rdf:RDF><cc:Work rdf:about=\"\"><dc:format>image/svg+xml</dc:format><dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs id=\"defs33\" />\n                <g transform=\"translate(-284.47227,-403.912)\" id=\"g3\" style=\"fill:#84ff00;fill-opacity:1\">\n                  <path d=\"m 359.929,526.013 c -0.01,0.054 -0.013,0.06 0,0 l 0,0 z\" id=\"path5\" style=\"fill:#84ff00;fill-opacity:1\" />\n                  <polygon points=\"517.073,564.263 522.902,564.263 519.988,544.794 \" id=\"polygon7\" style=\"fill:#84ff00;fill-opacity:1\" /> <path d=\"m 603.972,552.878 v -5.773 c 0,-2.75 -1.045,-3.631 -2.97,-3.631 h -2.859 v 13.035 h 2.859 c 1.925,0 2.97,-0.881 2.97,-3.631 z\" id=\"path9\" style=\"fill:#84ff00;fill-opacity:1\" />\n                  <path d=\"m 564.263,552.878 v -5.773 c 0,-2.75 -1.045,-3.631 -2.97,-3.631 h -2.859 v 13.035 h 2.859 c 1.925,0 2.97,-0.881 2.97,-3.631 z\" id=\"path11\" style=\"fill:#84ff00;fill-opacity:1\" /> <path d=\"m 682.787,551.229 v -3.795 c 0,-2.75 -0.935,-3.961 -3.08,-3.961 h -2.914 v 11.824 h 2.364 c 2.255,10e-4 3.63,-0.989 3.63,-4.068 z\" id=\"path13\" style=\"fill:#84ff00;fill-opacity:1\" /> <path d=\"M 710.468,527.46 H 422.885 c 2.878,-5.143 5.445,-10.423 7.633,-15.832 9.05,-22.387 7.333,-47.568 -5.336,-68.198 -13.079,-21.295 -36.813,-33.897 -61.537,-34.484 -0.257,-0.022 -0.526,-0.034 -0.806,-0.034 -0.279,0 -0.542,0.012 -0.8,0.034 -24.629,0.575 -46.729,12.771 -60.635,33.087 -14.168,20.695 -14.924,47.293 -6.444,70.271 11.5,31.18 37.472,58.615 64.663,77.199 0.168,0.115 0.359,0.211 0.551,0.304 l 1.148,1.282 H 710.529 L 710.468,527.46 z M 381.6,559.251 c -5.034,5.002 -10.315,9.75 -15.757,14.305 -1.046,0.871 -2.12,1.711 -3.183,2.561 -21.077,-15.176 -40.621,-36.52 -52.261,-59.936 -9.7,-19.518 -11.584,-42.266 -1.432,-61.977 10.348,-20.099 31.514,-32.271 53.872,-32.562 22.162,0.291 41.953,11.931 53.067,31.089 10.847,18.7 9.214,40.777 0.442,59.852 -8.15,17.717 -20.993,33.031 -34.748,46.668 z m 106.597,17.219 h -5.72 v -27.605 l -4.18,27.605 h -5.719 l -4.51,-27.221 v 27.221 h -5.279 v -38.494 h 8.414 l 4.454,27.332 4.125,-27.332 h 8.414 v 38.494 z m 36.575,0 -1.045,-6.984 h -7.424 l -1.045,6.984 h -5.555 l 6.159,-38.494 h 8.854 l 6.159,38.494 h -6.103 z m 45.54,-23.977 c 0,6.16 -2.97,9.514 -9.019,9.514 h -2.859 v 14.463 h -6.049 v -38.494 h 8.908 c 6.049,0 9.019,3.354 9.019,9.514 v 5.003 z m 39.709,0 c 0,6.16 -2.97,9.514 -9.019,9.514 h -2.859 v 14.463 h -6.049 v -38.494 h 8.908 c 6.049,0 9.019,3.354 9.019,9.514 v 5.003 z m 38.28,-9.019 h -10.448 v 10.725 h 8.304 v 5.5 h -8.304 v 11.273 h 10.448 v 5.498 h -16.498 v -38.494 h 16.498 v 5.498 z m 35.091,32.996 c -0.329,-0.99 -0.55,-1.594 -0.55,-4.729 v -6.049 c 0,-3.576 -1.21,-4.895 -3.959,-4.895 h -2.09 v 15.672 h -6.049 v -38.494 h 9.128 c 6.27,0 8.964,2.914 8.964,8.854 v 3.025 c 0,3.959 -1.265,6.543 -3.959,7.809 3.024,1.264 4.015,4.18 4.015,8.193 v 5.939 c 0,1.869 0.055,3.244 0.659,4.674 h -6.159 z\" id=\"path15\" style=\"fill:#84ff00;fill-opacity:1\" /> <path d=\"m 362.839,444.067 c -21.312,-0.576 -37.841,17.939 -38.389,38.395 -0.565,20.932 17.324,37.26 37.349,38.334 0.33,0.039 0.677,0.066 1.041,0.055 0.364,0.012 0.699,-0.016 1.029,-0.055 20.803,-1.068 36.801,-17.375 37.36,-38.334 0.576,-21.306 -17.945,-37.846 -38.39,-38.395 z m 0,64.053 c -14.286,-0.381 -25.277,-11.371 -25.664,-25.658 -0.38,-14.303 11.942,-25.294 25.664,-25.669 14.292,-0.386 25.289,11.954 25.658,25.669 0.392,14.281 -11.948,25.289 -25.658,25.658 z\" id=\"path17\" style=\"fill:#84ff00;fill-opacity:1\" />\n                  <g id=\"g19\" style=\"fill:#84ff00;fill-opacity:1\"> <path d=\"m 450.879,448.095 h 35.423 v 10.42 H 474.32 v 62.51 h -11.46 v -62.51 h -11.981 v -10.42 z\" id=\"path21\" style=\"fill:#84ff00;fill-opacity:1\" /> <path d=\"m 504.948,465.599 c 0,-11.668 6.146,-18.336 17.399,-18.336 11.252,0 17.398,6.668 17.398,18.336 v 37.922 c 0,11.67 -6.146,18.338 -17.398,18.338 -11.253,0 -17.399,-6.668 -17.399,-18.338 v -37.922 z m 11.461,38.652 c 0,5.209 2.291,7.189 5.938,7.189 3.646,0 5.938,-1.98 5.938,-7.189 v -39.383 c 0,-5.207 -2.293,-7.188 -5.938,-7.188 -3.647,0 -5.938,1.98 -5.938,7.188 v 39.383 z\" id=\"path23\" style=\"fill:#84ff00;fill-opacity:1\" /> <path d=\"m 572.667,448.095 v 56.26 c 0,5.209 2.292,7.086 5.938,7.086 3.646,0 5.938,-1.877 5.938,-7.086 v -56.26 h 10.836 v 55.531 c 0,11.668 -5.835,18.336 -17.087,18.336 -11.252,0 -17.087,-6.668 -17.087,-18.336 v -55.531 h 11.462 z\" id=\"path25\" style=\"fill:#84ff00;fill-opacity:1\" /> <path d=\"m 651.013,493.831 v 9.689 c 0,11.67 -5.835,18.338 -17.087,18.338 -11.252,0 -17.086,-6.668 -17.086,-18.338 v -37.922 c 0,-11.668 5.834,-18.336 17.086,-18.336 11.252,0 17.087,6.668 17.087,18.336 v 7.084 h -10.836 v -7.814 c 0,-5.207 -2.291,-7.188 -5.938,-7.188 -3.647,0 -5.939,1.98 -5.939,7.188 v 39.383 c 0,5.209 2.293,7.084 5.939,7.084 3.646,0 5.938,-1.875 5.938,-7.084 v -10.42 h 10.836 z\" id=\"path27\" style=\"fill:#84ff00;fill-opacity:1\" /> <path d=\"m 684.036,521.024 h -11.46 v -72.93 h 11.46 v 31.256 h 13.023 v -31.256 h 11.668 v 72.93 H 697.06 v -31.256 h -13.023 v 31.256 z\" id=\"path29\" style=\"fill:#84ff00;fill-opacity:1\" /> </g>\n                </g>\n              </svg>\n            </a>\n          </h1>\n          <p>{{ blurb }}</p>\n          <div class=\"top-corner\">\n            <label for=\"language-selector\" class=\"visuallyhidden\">{{ language }}</label>\n            <select class=\"language-selector\" id=\"language-selector\">\n              <option value=\"en\">English</option>\n              <option value=\"de\">Deutsch</option>\n              <option value=\"es\">Español</option>\n              <option value=\"nl\">Nederlands</option>\n              <option value=\"fi\">Suomi</option>\n            </select><a class=\"help-link\" href=\"help\" target=\"_blank\" aria-label=\"{{ open_help_aria }}\">{{ help }}</a>\n          </div>\n        </div>\n      </div>\n\n      <div role=\"main\" class=\"controls content\" id=\"main-content\">\n\n        <script src=\"../scripts/vendor-common.js\"></script>\n        <script src=\"../scripts/environment.js\"></script>\n\n        <!--[if lt IE 10]>\n          <p class=\"browserupgrade\">You are using an <strong>outdated</strong> browser. Please <a href=\"http://browsehappy.com/\">upgrade your browser</a> to improve your experience.</p>\n        <![endif]-->\n"
  },
  {
    "path": "web/pre2src.py",
    "content": "#!/usr/bin/python3\n\nfrom pathlib import Path\nimport re\n\nFROM = 'pre-src'\nTO = 'src'\nSPECS = {\n    'index': {\n        'vars': {\n        },\n    },\n    'area': {\n        'vars': {\n        },\n    },\n    'map': {\n        'vars': {\n        },\n    },\n    'help': {\n        'vars': {\n        },\n    },\n}\nfor name, spec in SPECS.items():\n    contents = ''\n    for filename in ('start', name, 'end'):\n        contents += Path(FROM + '/' + filename + '.pre').read_text(encoding='utf-8')\n\n    spec['vars']['pagename'] = name\n    def var_replacer(matchobj):\n        return spec['vars'][matchobj.group(1)]\n    contents = re.sub(r\"\\[% *(\\w+) *%\\]\", var_replacer, contents)\n\n    with open(TO + '/' + name + '.ect', 'w', encoding='utf-8') as f:\n        f.write(contents)\n"
  },
  {
    "path": "web/src/.gitignore",
    "content": "/*.ect\n\n"
  },
  {
    "path": "web/src/index.html",
    "content": "<!doctype html>\n<html>\n  <!-- ex: set tabstop=2 shiftwidth=2 softtabstop=2 expandtab: -->\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Touch Mapper</title>\n    <link rel=\"icon\" type=\"image/png\" href=\"/images/favicon.png\" />\n  </head>\n  <body>\n    <script src=\"../scripts/app-common.js\"></script>\n    <script>\n      var id = getUrlParam(\"map\");\n      var origin = getUrlParam('origin');\n      if (id) {\n        url = \"/en/map?map=\" + id;\n      } else if (origin === 'BlindSquare') {\n        // BlindSquare URL is like\n        // https://touch-mapper.org/?lat=60.666&lon=25.2525&addrName=Foo+place&lang=XX&origin=BlindSquare\n        // Parameter \"lang\" is optional.\n        var lang = getUrlParam(\"lang\");\n        lang = ['en', 'de', 'es', 'fi', 'nl'].indexOf(lang) > -1 ? lang : 'en';\n        url = \"/\" + lang + \"/area\" + window.location.search;\n      } else {\n        url = \"/en\";\n      }\n      location.href = url;\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "web/src/scripts/area.js",
    "content": "'use strict';\n/* global $ mapCalc Backbone isNan _ ol THREE performance google ga fbq TRANSLATIONS i18next show3dPreview */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\nvar TARGET_ROAD_DENSITY_UI_MIN = 1;\nvar TARGET_ROAD_DENSITY_UI_MAX = 100;\nvar TARGET_ROAD_DENSITY_UI_DEFAULT = 10;\n\nfunction normalizeTargetRoadDensityUiValue(value) {\n  var number = parseInt(value, 10);\n  if (isNaN(number)) {\n    return TARGET_ROAD_DENSITY_UI_DEFAULT;\n  }\n  if (number < TARGET_ROAD_DENSITY_UI_MIN) {\n    return TARGET_ROAD_DENSITY_UI_MIN;\n  }\n  if (number > TARGET_ROAD_DENSITY_UI_MAX) {\n    return TARGET_ROAD_DENSITY_UI_MAX;\n  }\n  return number;\n}\n\nfunction resetParameters(addr) {\n  $(\"#lat-input\").val(addr.lat).trigger('change');\n  $(\"#lon-input\").val(addr.lon).trigger('change');\n  $(\"#x-offset-input\").val(\"0\").change();\n  $(\"#y-offset-input\").val(\"0\").change();\n  data.set(\"multipartXpc\", 0);\n  data.set(\"multipartYpc\", 0);\n}\n\nfunction selectAddress(addr, index) {\n  data.set(\"selected_addr_short\", addr.addrShort);\n  data.set(\"selected_addr_full\", addr.addrLong);\n  setLocalStorage(\"addressesSelectedIndex\", index);\n}\n\nfunction showAllAddresses(addresses) {\n  var selectedIndex = getLocalStorageInt(\"addressesSelectedIndex\", 0);\n  $(\".first-address, .show-all-addresses\").hide();\n  _.each(addresses, function(addr, index){\n    var id = \"address-match-\" + index;\n    var input = $(\"<input>\")\n      .attr(\"type\", \"radio\")\n      .attr(\"name\", \"address-match\")\n      .attr(\"id\", id);\n    if (index === selectedIndex) {\n      input.prop('checked', true);\n    }\n    input.change(function(ev) {\n      var elem = $(ev.target);\n      if (elem.attr('name') !== 'address-match') {\n        return;\n      }\n      selectAddress(addr, index);\n      resetParameters(addr);\n    });\n    $(\".all-addresses\").append(\n      $(\"<li>\")\n        .append(input)\n        .append($(\"<label>\")\n          .attr(\"for\", id)\n          .text(addr.addrLong)));\n  });\n  $(\".all-addresses\").slideDown(700);\n  $(\"#search-results\").focus();\n}\n\nfunction initInputs(outputs, osmDragPanInteraction) {\n  var DEFAULT_PRINT_SIZE_2D = \"27.9\";\n  var DEFAULT_PRINT_SIZE_3D = \"17\";\n  var mapScaleCoverage = $(\".map-scale-coverage\");\n  var targetRoadDensityInput = $(\"#target-road-density-ui\");\n  var targetRoadDensityRow = $(\".target-road-density-row\");\n  var VALID_CONTENT_MODES = {\n    \"normal\": true,\n    \"no-buildings\": true,\n    \"only-big-roads\": true\n  };\n\n  function normalizeContentMode(value) {\n    if (value === undefined || value === null) {\n      return \"normal\";\n    }\n    var normalized = (\"\" + value).toLowerCase();\n    if (Object.prototype.hasOwnProperty.call(VALID_CONTENT_MODES, normalized)) {\n      return normalized;\n    }\n    return \"normal\";\n  }\n\n  function interpolateTranslation(key, replacements) {\n    var text = window.TM && window.TM.translations ? window.TM.translations[key] : \"\";\n    if (!replacements) {\n      return text;\n    }\n    return text.replace(/__([a-zA-Z0-9_]+)__/g, function(match, name) {\n      if (replacements[name] === undefined) {\n        return match;\n      }\n      return replacements[name];\n    });\n  }\n\n  function formatSizeCm(sizeCm) {\n    var rounded = Math.round(sizeCm * 10) / 10;\n    var isWhole = Math.abs(rounded - Math.round(rounded)) < 0.0001;\n    var formatted = isWhole ? rounded.toFixed(0) : rounded.toFixed(1);\n    var unit = window.TM && window.TM.translations ? window.TM.translations.unit_cm : \"cm\";\n    return formatted + \" \" + unit;\n  }\n\n  function updateMapScaleCoverage() {\n    if (!mapScaleCoverage.length) {\n      return;\n    }\n    var scale = data.get(\"scale\");\n    if (!scale) {\n      return;\n    }\n    var sizeCm = data.get(\"size\");\n    if (!sizeCm) {\n      return;\n    }\n    var sizeLabel = formatSizeCm(sizeCm);\n    var metersRaw = sizeCm / 100 * scale;\n    var metersAcross = Math.round(metersRaw);\n    var yardsAcross = Math.round(metersRaw * 1.0936133);\n    var metersUnit = window.TM && window.TM.translations ? window.TM.translations.meters : \"meters\";\n    var yardsUnit = window.TM && window.TM.translations ? window.TM.translations.yards : \"yards\";\n    var metricText = metersAcross + \" \" + metersUnit;\n    var imperialText = yardsAcross + \" \" + yardsUnit;\n    var text = interpolateTranslation(\"map_scale_coverage\", {\n      size: sizeLabel,\n      area_metric: metricText,\n      area_imperial: imperialText\n    });\n    mapScaleCoverage.text(text);\n  }\n\n  function updateTargetRoadDensityVisibility() {\n    var contentMode = normalizeContentMode(data.get(\"content-mode\"));\n    targetRoadDensityRow.toggle(contentMode === \"only-big-roads\");\n  }\n\n  data.on(\"change:scale change:size change:printing-tech\", updateMapScaleCoverage);\n\n  var initDone = false;\n  var initialPrintingTech = getLocalStorageStr('printing-tech', '3d');\n\n  // Printing technology\n  $(\"#printing-tech-3d\").change(function(){\n    setData(\"printing-tech\", \"3d\");\n    $(\".hidden-for-3d\").hide();\n    $(\".hidden-for-2d\").show();\n    $(\"#map-size-preset\").change();\n  });\n  $(\"#printing-tech-2d\").change(function(){\n    setData(\"printing-tech\", \"2d\");\n    $(\".hidden-for-3d\").show();\n    $(\".hidden-for-2d\").hide();\n    $(\"#map-size-input\").val(DEFAULT_PRINT_SIZE_2D).change();\n  });\n  if (initialPrintingTech === '2d') {\n    $(\"#printing-tech-2d\").prop('checked', true).change();\n  } else {\n    $(\"#printing-tech-3d\").prop('checked', true).change();\n  }\n\n  // Map content selection\n  localStorage.removeItem(\"exclude-buildings\");\n  setLocalStorage(\"content-mode\", normalizeContentMode(getLocalStorageStr(\"content-mode\", \"normal\")));\n  setLocalStorage(\n    \"target-road-density-ui\",\n    normalizeTargetRoadDensityUiValue(getLocalStorageInt(\"target-road-density-ui\", TARGET_ROAD_DENSITY_UI_DEFAULT))\n  );\n  initSimpleInput(\"content-mode\", $(\"#content-mode\"), \"str\", \"normal\");\n  initSimpleInput(\"target-road-density-ui\", targetRoadDensityInput, \"int\", TARGET_ROAD_DENSITY_UI_DEFAULT);\n  initSimpleInput(\"hide-location-marker\", $(\"#hide-location-marker\"), \"checkbox\", false);\n  data.on(\"change:content-mode\", updateTargetRoadDensityVisibility);\n  data.on(\"change:target-road-density-ui\", function() {\n    var normalized = normalizeTargetRoadDensityUiValue(data.get(\"target-road-density-ui\"));\n    if (normalized !== data.get(\"target-road-density-ui\")) {\n      setData(\"target-road-density-ui\", normalized);\n      return;\n    }\n    if (String(targetRoadDensityInput.val()) !== String(normalized)) {\n      targetRoadDensityInput.val(normalized);\n    }\n  });\n  updateTargetRoadDensityVisibility();\n\n  // Print size preset\n  $(\"#map-size-preset\").change(function(){\n    var preset = $(this).val();\n    $(\"#map-size-input\").val(parseFloat(preset).toFixed(1)).change();\n    setLocalStorage(\"map-size-preset\", preset);\n  }).val(getLocalStorageStr(\"map-size-preset\", DEFAULT_PRINT_SIZE_3D))\n    .change();\n\n  // Scale preset\n  var mapScalePresetElem = $(\"#map-scale-preset\");\n  var storedScalePreset = getLocalStorageStr(\"map-scale-preset\", \"2400\");\n  if (! optionExistsInSelect(mapScalePresetElem, storedScalePreset)) {\n    storedScalePreset = \"\";\n    setLocalStorage(\"map-scale-preset\", storedScalePreset);\n  }\n  $(\"#map-scale-preset\").change(function(){\n    var preset = $(this).val() || \"\";\n    if (preset !== \"\") {\n      $(\"#scale-input\").val(preset).change();\n    }\n    setLocalStorage(\"map-scale-preset\", preset);\n  }).val(storedScalePreset)\n    .change();\n\n  // Advanced mode\n  $(\"#advanced-input\").click(function(){\n    if (initDone) {\n      $(\"#advanced-controls\").slideToggle();\n    } else {\n      $(\"#advanced-controls\").toggle();\n    }\n    $(\"html\").toggleClass(\"advanced-mode\");\n    if (! $(\"html\").hasClass(\"advanced-mode\")) {\n      // Reset offsets, else they may end up outside of map\n      $(\"#x-offset-input\").val(\"0\").change();\n      $(\"#y-offset-input\").val(\"0\").change();\n      // Basic mode can't be combined with multipart mode\n      $(\"#multipart-map-input\").prop(\"checked\", \"\").change();\n      // Set advanced values from non-advanced presets\n      if (data.get(\"printing-tech\") === '3d') {\n        $(\"#map-size-preset\").change();\n      }\n      $(\"#map-scale-preset\").change();\n    }\n  });\n  initSimpleInput(\"advancedMode\", $(\"#advanced-input\"), \"checkbox\", false);\n\n  // Coordinates\n  initSimpleInput(\"lat\", $(\"#lat-input\"), 'float', 0);\n  initSimpleInput(\"lon\", $(\"#lon-input\"), 'float', 0);\n\n  // Offset\n  initSimpleInput(\"offsetX\", $(\"#x-offset-input\"), 'int', 0);\n  initSimpleInput(\"offsetY\", $(\"#y-offset-input\"), 'int', 0);\n\n  // Map size\n  initSimpleInput(\"size\", $(\"#map-size-input\"), 'float',\n    initialPrintingTech === '3d' ? DEFAULT_PRINT_SIZE_3D : DEFAULT_PRINT_SIZE_2D);\n\n  // Scale\n  initSimpleInput(\"scale\", $(\"#scale-input\"), 'int', 2400);\n\n  // Multipart mode\n  initMultipartMode(data, osmDragPanInteraction); // in multipart-mode.js\n\n  // Submit map creation\n  $(\"#submit-button\").click(window.submitMapCreation); // in map-creation.js\n\n  updateMapScaleCoverage();\n  initDone = true;\n}\n\nfunction setParametersByMapId(id) {\n  return loadInfoJson(id).done(function(data, textStatus, jqXHR){\n      storeMapSettingsFromInfo(data);\n      return true;\n    });\n}\n\nfunction setParametersFromBlindSquare() {\n  function hasNonEmpty(value) {\n    return value !== null && value !== undefined && value !== \"\";\n  }\n\n  function parseBoolean(value) {\n    if (!hasNonEmpty(value)) {\n      return null;\n    }\n    var normalized = (\"\" + value).toLowerCase();\n    if (normalized === \"true\" || normalized === \"1\" || normalized === \"yes\") {\n      return true;\n    }\n    if (normalized === \"false\" || normalized === \"0\" || normalized === \"no\") {\n      return false;\n    }\n    return null;\n  }\n\n  function parseIntParam(value) {\n    if (!hasNonEmpty(value)) {\n      return null;\n    }\n    var number = parseInt(value, 10);\n    return isNaN(number) ? null : number;\n  }\n\n  function parseFloatParam(value) {\n    if (!hasNonEmpty(value)) {\n      return null;\n    }\n    var number = parseFloat(value);\n    return isNaN(number) ? null : number;\n  }\n\n  function parseMapSizeParam(value) {\n    var number = parseFloatParam(value);\n    if (number === null || number >= 100) {\n      return null;\n    }\n    return number;\n  }\n\n  function parseContentMode(value) {\n    if (!hasNonEmpty(value)) {\n      return null;\n    }\n    var normalized = (\"\" + value).toLowerCase();\n    if (normalized === \"normal\" || normalized === \"no-buildings\" || normalized === \"only-big-roads\") {\n      return normalized;\n    }\n    return null;\n  }\n\n  function parseTargetRoadDensityUi(value) {\n    if (!hasNonEmpty(value)) {\n      return null;\n    }\n    var number = parseFloat(value);\n    if (isNaN(number)) {\n      return null;\n    }\n    return normalizeTargetRoadDensityUiValue(Math.round(number));\n  }\n\n  function getUrlParamFromNames(paramNames) {\n    var names = Array.isArray(paramNames) ? paramNames : [paramNames];\n    var i;\n    for (i = 0; i < names.length; i++) {\n      var raw = getUrlParam(names[i]);\n      if (hasNonEmpty(raw)) {\n        return raw;\n      }\n    }\n    return null;\n  }\n\n  function setLocalStorageIfPresent(localStorageKey, paramNames, parser) {\n    var raw = getUrlParamFromNames(paramNames);\n    if (!hasNonEmpty(raw)) {\n      return null;\n    }\n    var parsed = parser ? parser(raw) : raw;\n    if (parsed === null || parsed === undefined || parsed === \"\") {\n      return null;\n    }\n    setLocalStorage(localStorageKey, parsed);\n    return parsed;\n  }\n\n  function selectedAddressFromStorage() {\n    if (!localStorage.addresses) {\n      return null;\n    }\n    try {\n      var addresses = JSON.parse(localStorage.addresses);\n      if (!Array.isArray(addresses) || addresses.length === 0) {\n        return null;\n      }\n      var index = getLocalStorageInt(\"addressesSelectedIndex\", 0);\n      if (index < 0 || index >= addresses.length) {\n        index = 0;\n      }\n      return addresses[index] || null;\n    } catch (e) {\n      return null;\n    }\n  }\n\n  var currentAddress = selectedAddressFromStorage() || {};\n  var urlAddrName = getUrlParamFromNames([\"addrName\", \"addr_name\"]);\n  var urlLat = getUrlParamFromNames([\"lat\", \"latitude\"]);\n  var urlLon = getUrlParamFromNames([\"lon\", \"lng\", \"longitude\"]);\n  if (hasNonEmpty(urlAddrName) || hasNonEmpty(urlLat) || hasNonEmpty(urlLon)) {\n    var addrShort = hasNonEmpty(urlAddrName) ? urlAddrName : currentAddress.addrShort;\n    var addrLong = hasNonEmpty(urlAddrName) ? urlAddrName : currentAddress.addrLong;\n    var lat = hasNonEmpty(urlLat) ? urlLat : currentAddress.lat;\n    var lon = hasNonEmpty(urlLon) ? urlLon : currentAddress.lon;\n    if (hasNonEmpty(addrShort) || hasNonEmpty(addrLong) || hasNonEmpty(lat) || hasNonEmpty(lon)) {\n      setLocalStorage(\"addresses\", JSON.stringify([{\n        addrShort: addrShort,\n        addrLong: addrLong,\n        lat: lat,\n        lon: lon\n      }]));\n      setLocalStorage(\"addressesSelectedIndex\", 0);\n    }\n  }\n\n  setLocalStorageIfPresent(\"lat\", [\"lat\", \"latitude\"], parseFloatParam);\n  setLocalStorageIfPresent(\"lon\", [\"lon\", \"lng\", \"longitude\"], parseFloatParam);\n  setLocalStorageIfPresent(\"printing-tech\", [\"printingTech\", \"printing-tech\", \"printing_tech\"], function(value) {\n    var normalized = (\"\" + value).toLowerCase();\n    return normalized === \"2d\" || normalized === \"3d\" ? normalized : null;\n  });\n  setLocalStorageIfPresent(\"offsetX\", [\"offsetX\", \"offset_x\", \"x-offset\", \"xOffset\"], parseIntParam);\n  setLocalStorageIfPresent(\"offsetY\", [\"offsetY\", \"offset_y\", \"y-offset\", \"yOffset\"], parseIntParam);\n  setLocalStorageIfPresent(\"advancedMode\", [\"advancedMode\", \"advanced_mode\", \"advanced-mode\"], parseBoolean);\n  setLocalStorageIfPresent(\"content-mode\", [\"contentMode\"], parseContentMode);\n  setLocalStorageIfPresent(\n    \"target-road-density-ui\",\n    [\"targetRoadDensityUi\", \"targetRoadDensity\", \"target_road_density\"],\n    parseTargetRoadDensityUi\n  );\n  setLocalStorageIfPresent(\"hide-location-marker\", [\"hideLocationMarker\", \"hide_location_marker\", \"hide-location-marker\"], parseBoolean);\n  setLocalStorageIfPresent(\"multipartMode\", [\"multipartMode\", \"multipart_mode\", \"multipart-mode\"], parseBoolean);\n  setLocalStorageIfPresent(\"multipartXpc\", [\"multipartXpc\", \"multipart_xpc\", \"multipart-xpc\"], parseIntParam);\n  setLocalStorageIfPresent(\"multipartYpc\", [\"multipartYpc\", \"multipart_ypc\", \"multipart-ypc\"], parseIntParam);\n\n  var sizeValue = setLocalStorageIfPresent(\"size\", [\"size\", \"mapSize\", \"map_size\"], parseMapSizeParam);\n  if (sizeValue !== null) {\n    setLocalStorage(\n      \"map-size-preset\",\n      optionExistsInSelect($(\"#map-size-preset\"), sizeValue) ? sizeValue : \"\"\n    );\n  }\n  var scaleValue = setLocalStorageIfPresent(\"scale\", [\"scale\", \"mapScale\", \"map_scale\"], parseIntParam);\n  if (scaleValue !== null) {\n    setLocalStorage(\n      \"map-scale-preset\",\n      optionExistsInSelect($(\"#map-scale-preset\"), scaleValue) ? scaleValue : \"\"\n    );\n  }\n\n  return $.when(); // return a resolved promise\n}\n\n// - If URL has param \"id\", fetch matching map metadata, and store into localStorage, from\n//   which UI initialization will read them. Used when arriving from map page or QR code.\n// - If URL has param origin=BlindSquare, set address, coordinates and maybe lang from URL params.\nfunction setParametersByUrlQuery() {\n  var id = getUrlParam(\"map\");\n  if (id) {\n    return setParametersByMapId(id);\n  } else if (getUrlParam(\"origin\") === 'BlindSquare') {\n    return setParametersFromBlindSquare();\n  } else {\n    return $.when(); // return a resolved promise\n  }\n}\n\nfunction initialAddressAndParameters() {\n  if (! localStorage.addresses || localStorage.addresses.length === 0) {\n    location.href = \".\";\n  }\n  var addrIndex = getLocalStorageInt(\"addressesSelectedIndex\", 0);\n  var addresses = JSON.parse(localStorage.addresses);\n  if (addrIndex >= addresses.length) {\n    addrIndex = 0;\n  }\n  var addr = addresses[addrIndex];\n  if (! addr) {\n    location.href = \".\";\n  }\n  if (addresses.length > 1) {\n    $(\".show-all-addresses\")\n      .show()\n      .click(function(){\n        showAllAddresses(addresses);\n      });\n  }\n\n  $(\".first-address\").text(addr.addrLong);\n  selectAddress(addr, addrIndex);\n  // If we are showing different address than on previous page load, reset some parameters.\n  if (getLocalStorageStr('previousAddress', '') !== addr.addrLong) {\n    resetParameters(addr);\n  }\n  setLocalStorage('previousAddress', addr.addrLong);\n}\n\n$(window).ready(function(){\n  var outputs = {\n    currentDiameterMeters: $(\".current-diameter-meters\"),\n    currentDiameterYards: $(\".current-diameter-yards\"),\n    map: $(\"#map-area-preview\")\n  };\n\n  setParametersByUrlQuery().done(function(){\n    var osmDragPanInteraction = window.initOsmPreview(outputs); // in osm-preview.js\n    initInputs(outputs, osmDragPanInteraction);\n    initialAddressAndParameters();\n    $(\".show-on-load\").show(); // Don't use CSS for this to make screen readers happier\n    data.trigger(\"initdone\");\n  });\n});\n"
  },
  {
    "path": "web/src/scripts/backbone-helpers.js",
    "content": "'use strict';\n/* global $ mapCalc Backbone isNan _ ol THREE performance google ga fbq TRANSLATIONS i18next readCookie createCookie data */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\n$(function(){\n  window.data = new Backbone.Model();\n\n  window.setData = function(name, value) {\n    data.set(name, value);\n    setLocalStorage(name, value);\n  };\n\n  window.initSimpleInput = function(name, elem, type, defaultValue) {\n    elem.change(function(ev) {\n      if ($(ev.target).is(':invalid')) {\n        return false;\n      }\n      var value;\n      switch (type) {\n        case 'float':\n          value = parseFloat($(ev.target).val());\n          if (! isNaN(value)) {\n            setData(name, value);\n          }\n          break;\n        case 'int':\n          value = parseInt($(ev.target).val(), 10);\n          if (! isNaN(value)) {\n            setData(name, value);\n          }\n          break;\n        case 'str':\n          setData(name, $(ev.target).val().trim());\n          break;\n        case 'checkbox':\n          setData(name, $(ev.target).is(':checked')); // converts to strings 'true' / 'false'\n          break;\n        default:\n          throw new Error(\"unknown type: \" + type);\n      }\n      return false;\n    });\n    if (type === 'checkbox') {\n      var booleanValue = getLocalStorageStr(name, defaultValue) === 'true';\n      if (booleanValue) {\n        elem.click();\n      }\n    } else {\n      elem.val(getLocalStorageStr(name, defaultValue));\n      elem.change();\n    }\n  };\n});\n"
  },
  {
    "path": "web/src/scripts/email.js",
    "content": "/* global $ mapCalc Backbone isNan _ ol THREE performance google ga fbq TRANSLATIONS i18next */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\n(function(){\n    'use strict';\n\n    function showError(errorMsg) {\n      $('.email-sending-error').slideDown().find('.error-msg-text').text(errorMsg).focus();\n    }\n\n    function initEmailSending(container, meta) {\n        container.find('.email-sending-form').submit(function(ev){\n            ev.preventDefault();\n\n            var email = container.find('#email-addr');\n            if (email.val().trim() === '') {\n                email.addClass('invalid');\n                return;\n            }\n            email.removeClass('invalid');\n\n            $('.email-sending-success').slideUp();\n            $('.email-sending-error').slideUp();\n\n            // Disable submit button for a bit\n            var submit = container.find('.submit-email');\n            submit.attr('disabled', 'disabled');\n            setTimeout(function(){\n                submit.removeAttr('disabled');\n            }, 4000);\n\n            var params = {\n                meta: meta,\n                to: email.val()\n            };\n            $.ajax({\n                type: \"POST\",\n                url: \"https://6ww05s3p8k.execute-api.eu-west-1.amazonaws.com/prod\", // FIXME\n                contentType: \"application/json; charset=utf-8\",\n                data: JSON.stringify(params)\n            }).fail(function(jqXHR, textStatus, errorThrown){\n                console.log(jqXHR.status, textStatus, errorThrown);\n                showError(textStatus);\n            }).done(function(d, textStatus, jqXHR){\n                $('.email-sending-success').slideDown().focus();\n            });\n        });\n    }\n\n    window.initEmailSending = initEmailSending;\n})();\n"
  },
  {
    "path": "web/src/scripts/environment.js.dev",
    "content": "window.TM_ENVIRONMENT = 'dev';\nwindow.TM_DOMAIN = 'dev';\n"
  },
  {
    "path": "web/src/scripts/environment.js.prod",
    "content": "window.TM_ENVIRONMENT = 'prod';\n"
  },
  {
    "path": "web/src/scripts/environment.js.test",
    "content": "window.TM_ENVIRONMENT = 'test';\n"
  },
  {
    "path": "web/src/scripts/help.js",
    "content": "'use strict';\n/* global $ mapCalc Backbone isNan _ ol THREE performance google ga fbq TRANSLATIONS i18next show3dPreview */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\n$(window).load(function(){\n  $(\".show-on-load\").show();\n});\n"
  },
  {
    "path": "web/src/scripts/index.js",
    "content": "'use strict';\n/* global $ mapCalc Backbone isNan _ ol THREE performance TRANSLATIONS i18next show3dPreview data */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\nconst MAX_ADDRESSES = 5;\nconst SEARCH_MAX_ATTEMPTS = 2;\nconst SEARCH_RETRY_DELAY_MS = 500;\nconst SEARCH_TIMEOUT_MS = 8000;\n\nconst getAddress = (g) => {\n  let streetAddr = g.street;\n  if (streetAddr && g.housenumber) {\n    streetAddr += ' ' + g.housenumber;\n  }\n  let addrShort = g.name || streetAddr || g.label;\n  return [\n    addrShort,\n    [g.name, streetAddr, g.city, g.state || g.county, g.country].filter((x) => x != undefined).join(', ')\n      || g.label,\n  ];\n};\n\n// Wait for window load so the DOM is fully ready.\n$(window).load(function(){\n\n  // Address search\n  initSimpleInput(\"address\", $(\"#address-input\"), 'str');\n  if (localStorage.searchString && localStorage.searchString.length > 0) {\n    $(\"#address-input\").val(localStorage.searchString).change();\n  }\n  var prevAddr;\n  var currentSearchRequestId = 0;\n  var activeSearchXhr = null;\n  $(\"#address-search-form\").submit(function(ev){\n    ev.preventDefault();\n\n    // Avoid duplicate searches\n    var addr = data.get(\"address\").trim();\n    if (addr === prevAddr || addr === '') {\n      return;\n    }\n    prevAddr = addr;\n\n    $(\"#searching\").show();\n    $(\"#no-search-results\").hide();\n\n    function handleSearchResults(response) {\n      var results = response.features;\n      if (results.length === 0) {\n        $(\"#searching\").hide();\n        $(\"#no-search-results\").show().focus();\n        return;\n      }\n\n      const seenLabels = {};\n      let addresses = results.map((result) => {\n        var g = result.properties.geocoding;\n        // There may be effectively duplicates, only show first one of those\n        if (seenLabels[g.label]) return null;\n        seenLabels[g.label] = true;\n\n        const addr = getAddress(g);\n        const coords = result.geometry.coordinates;\n        return {\n          addrShort: addr[0],\n          addrLong: addr[1],\n          lat: coords[1],\n          lon: coords[0],\n        };\n      }).filter((x) => x != undefined);\n      if (addresses.length > MAX_ADDRESSES) {\n        addresses = addresses.slice(0, MAX_ADDRESSES);\n      }\n      localStorage.addresses = JSON.stringify(addresses);\n      localStorage.searchString = addr;\n      location.href = \"area\";\n    }\n\n    const requestId = ++currentSearchRequestId;\n\n    function isCurrentRequest() {\n      return requestId === currentSearchRequestId;\n    }\n\n    function failSearch(statusText, statusCode, attemptNumber) {\n      if (!isCurrentRequest()) {\n        return;\n      }\n      if (attemptNumber < SEARCH_MAX_ATTEMPTS) {\n        setTimeout(function() {\n          sendSearchRequest(attemptNumber + 1);\n        }, SEARCH_RETRY_DELAY_MS);\n        return;\n      }\n\n      activeSearchXhr = null;\n      $(\"#searching\").hide();\n      alert(\"search failed: \" + (statusText || (\"HTTP \" + statusCode)));\n      prevAddr = undefined;\n    }\n\n    function sendSearchRequest(attemptNumber) {\n      if (!isCurrentRequest()) {\n        return;\n      }\n\n      if (activeSearchXhr) {\n        activeSearchXhr.abort();\n      }\n\n      var xhr = new XMLHttpRequest();\n      activeSearchXhr = xhr;\n      xhr.open(\"GET\", \"https://nominatim.openstreetmap.org/search?\" + new URLSearchParams({\n        q: addr,\n        addressdetails: 1,\n        format: 'geocodejson',\n        limit: MAX_ADDRESSES * 3, // *3 to have room for duplicate removal\n        email: 'sofia.pahaoja@gmail.com',\n      }).toString(), true);\n      xhr.timeout = SEARCH_TIMEOUT_MS;\n      xhr.onload = () => {\n        if (!isCurrentRequest() || xhr !== activeSearchXhr || xhr.readyState !== 4) {\n          return;\n        }\n        if (xhr.status === 200) {\n          try {\n            activeSearchXhr = null;\n            $(\"#searching\").hide();\n            handleSearchResults(JSON.parse(xhr.responseText));\n          } catch (e) {\n            failSearch(\"invalid response\", xhr.status, attemptNumber);\n          }\n        } else {\n          failSearch(xhr.statusText, xhr.status, attemptNumber);\n        }\n      };\n      xhr.onerror = () => {\n        if (xhr !== activeSearchXhr) {\n          return;\n        }\n        failSearch(xhr.statusText, xhr.status, attemptNumber);\n      };\n      xhr.ontimeout = () => {\n        if (xhr !== activeSearchXhr) {\n          return;\n        }\n        failSearch(\"timeout\", xhr.status, attemptNumber);\n      };\n      xhr.send(null);\n    }\n\n    sendSearchRequest(1);\n  });\n\n  data.trigger(\"init\");\n  $(\".show-on-load\").show(); // Don't use CSS for this to make screen readers happier\n});\n"
  },
  {
    "path": "web/src/scripts/language.js",
    "content": "'use strict';\n/* global $ mapCalc Backbone isNan _ ol THREE performance google ga fbq TRANSLATIONS i18next readCookie createCookie */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\n$(function(){\n  // Change URL to one with given language\n  function changeLang(newLang) {\n    var newHref = location.href.replace(/\\/[a-z][a-z]\\b(\\/.*)/, '/' + newLang + '$1');\n    location.href = newHref;\n  }\n\n  // Set current lang\n  var match = window.location.pathname.match(/([a-z][a-z])\\b.*/);\n  if (match) {\n    $(\".language-selector\").val(match[1]);\n  }\n\n  // Change language callback\n  $(\".language-selector\").change(function(){\n    changeLang($(this).val());\n  });\n\n  // Get current lang from URL\n  var requestedLang = getUrlParam('lang');\n  var langSelected = $(\".language-selector\");\n  if (requestedLang && langSelected.val() !== requestedLang) {\n    // URL has \"lang\" param; make sure its value is among supported languages\n    langSelected.find(\"option\").each(function(index, option) {\n      if ($(option).val() === requestedLang) {\n        changeLang(requestedLang);\n      }\n    });\n  }\n});\n"
  },
  {
    "path": "web/src/scripts/map-calc.js",
    "content": "/* eslint camelcase:0, quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\n(function (exports) {\n  'use strict';\n\n  function deg2rad(deg) {\n    var conv_factor = (2.0 * Math.PI) / 360.0;\n    return (deg * conv_factor);\n  }\n\n  function metersPerDegree(latitude) // Compute lengths of degrees\n  {\n    // Convert latitude to radians\n    var lat = deg2rad(latitude);\n\n    // Set up \"Constants\"\n    var m1 = 111132.92; // latitude calculation term 1\n    var m2 = -559.82; // latitude calculation term 2\n    var m3 = 1.175; // latitude calculation term 3\n    var m4 = -0.0023; // latitude calculation term 4\n    var p1 = 111412.84; // longitude calculation term 1\n    var p2 = -93.5; // longitude calculation term 2\n    var p3 = 0.118; // longitude calculation term 3\n\n    // Calculate the length of a degree of latitude and longitude in meters\n    var latlen = m1 + (m2 * Math.cos(2 * lat)) + (m3 * Math.cos(4 * lat)) + (m4 * Math.cos(6 * lat));\n    var longlen = (p1 * Math.cos(lat)) + (p2 * Math.cos(3 * lat)) + (p3 * Math.cos(5 * lat));\n\n    return {\n      lat: latlen,\n      lon: longlen\n    };\n  }\n\n  exports.mapCalc = {\n    metersPerDegree: metersPerDegree\n  };\n})(window);\n"
  },
  {
    "path": "web/src/scripts/map-creation.js",
    "content": "/* global $ mapCalc Backbone isNan _ ol THREE performance google ga fbq TRANSLATIONS i18next mapDiameter */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\n(function(){\n  var MAX_WAIT = 10 * 60; // time out after this many seconds\n  var TARGET_ROAD_DENSITY_UI_MIN = 1;\n  var TARGET_ROAD_DENSITY_UI_MAX = 100;\n  var TARGET_ROAD_DENSITY_UI_DEFAULT = 10;\n\n  function normalizeTargetRoadDensityUiValue(value) {\n    var number = parseInt(value, 10);\n    if (isNaN(number)) {\n      return TARGET_ROAD_DENSITY_UI_DEFAULT;\n    }\n    if (number < TARGET_ROAD_DENSITY_UI_MIN) {\n      return TARGET_ROAD_DENSITY_UI_MIN;\n    }\n    if (number > TARGET_ROAD_DENSITY_UI_MAX) {\n      return TARGET_ROAD_DENSITY_UI_MAX;\n    }\n    return number;\n  }\n\n  function pollProgress(startTime, requestId) {\n      function showPollingError(message, consoleMessage) {\n        $(\"#submit-button\").prop(\"disabled\", false);\n        $(\"#submit-button\").val(message);\n        showError(message);\n        if (consoleMessage && window.console && window.console.error) {\n          window.console.error(consoleMessage);\n        }\n      }\n      var pollAgain = function(){\n        setTimeout(function(){\n          pollProgress(startTime, requestId);\n        }, 1000);\n      };\n      var progressLabelKeyByValue = {\n        20: \"progress__reading_osm\",\n        60: \"progress__converting\",\n        80: \"progress__uploading\"\n      };\n\n      // Timeout\n      if (new Date().getTime() / 1000 - startTime > MAX_WAIT) {\n        showPollingError(\"Processing took too long\", \"Map conversion polling timed out.\");\n        return;\n      }\n\n      // Check for processing status\n      $.ajax({\n          type: \"GET\",\n          url: makeS3InfoUrl(requestId), // CloudFront caches 404 responses, so can't poll through it\n          cache: false\n      }).fail(function(jqXHR, textStatus, errorThrown){\n        if (jqXHR.status === 404) {\n            pollAgain();\n        } else {\n            showPollingError(\"Error: \" + textStatus, \"Map conversion polling request failed: \" + textStatus + \": \" + errorThrown);\n        }\n      }).done(function(d, textStatus, jqXHR){\n        var payload = d;\n        if (typeof payload === \"string\") {\n          try {\n            payload = JSON.parse(payload);\n          } catch (e) {\n            pollAgain();\n            return;\n          }\n        }\n        var status = payload && payload.status ? payload.status : null;\n        if (!status) {\n          pollAgain();\n          return;\n        }\n\n        if (status.errorCode) {\n          var translationKey = \"conversion_error_\" + status.errorCode;\n          var genericError = window.TM.translations.conversion_error_unknown || \"Map conversion failed.\";\n          var localized = window.TM.translations[translationKey] || genericError;\n          showPollingError(localized);\n          if (status.errorDescription && window.console && window.console.error) {\n            window.console.error(\"Map conversion failed (\" + status.errorCode + \"): \" + status.errorDescription);\n          }\n          return;\n        }\n\n        var progress = parseInt(status.progress, 10);\n        if (isNaN(progress)) {\n          pollAgain();\n          return;\n        }\n        if (progress >= 100) {\n          location.href = makeMapPageUrlRelative(requestId);\n        } else {\n          var progressKey = progressLabelKeyByValue[progress];\n          var desc = progressKey ? (window.TM.translations[progressKey] || progressKey) : (progress + \"%\");\n          $(\"#submit-button\").val(desc);\n          pollAgain();\n        }\n      });\n  }\n\n  function sqsSendDone(requestId){\n    var startTime = new Date().getTime() / 1000;\n    pollProgress(startTime, requestId);\n  }\n\n  function sendSqsRequest(msg) {\n    var body = encodeURIComponent(JSON.stringify(msg));\n    $.ajax({\n        type: \"GET\",\n        url: window.TM_MAP_REQUEST_SQS_QUEUE + \"?Action=SendMessage&MessageBody=\" + body + \"&Version=2012-11-05\"\n    }).done(function(d, textStatus, jqXHR){\n      sqsSendDone(msg.requestId);\n    }).fail(function(jqXHR, textStatus, errorThrown) {\n      showError(\"can't access SQS queue: \" + textStatus + \": \" + errorThrown);\n    });\n  }\n\n  function withBrowserIp(msg, done) {\n    $.ajax({\n      type: \"GET\",\n      url: \"https://api.ipify.org?format=json\",\n      dataType: \"json\",\n      timeout: 1500\n    }).done(function(response) {\n      if (response && typeof response.ip === \"string\" && response.ip.length > 0) {\n        msg.browserIp = response.ip;\n      }\n    }).always(function() {\n      done();\n    });\n  }\n\n  function hashStringFNV1a(text) {\n    var hash = 2166136261;\n    for (var i = 0; i < text.length; i += 1) {\n      hash ^= text.charCodeAt(i);\n      hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);\n    }\n    return (\"00000000\" + (hash >>> 0).toString(16)).slice(-8);\n  }\n\n  function buildBrowserFingerprint() {\n    var timezone = \"\";\n    try {\n      timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || \"\";\n    } catch (e) {\n      timezone = \"\";\n    }\n    var parts = [\n      navigator.userAgent || \"\",\n      navigator.language || \"\",\n      (navigator.languages || []).join(\",\"),\n      navigator.platform || \"\",\n      timezone,\n      String(screen.width || \"\"),\n      String(screen.height || \"\"),\n      String(screen.colorDepth || \"\"),\n      String(window.devicePixelRatio || \"\"),\n      String(navigator.hardwareConcurrency || \"\"),\n      String(navigator.deviceMemory || \"\"),\n      String(navigator.maxTouchPoints || \"\")\n    ];\n    return \"fp1-\" + hashStringFNV1a(parts.join(\"|\"));\n  }\n\n  function browserReferrer() {\n    if (typeof document === \"undefined\" || typeof document.referrer !== \"string\") {\n      return null;\n    }\n    var referrer = document.referrer.trim();\n    if (!referrer) {\n      return null;\n    }\n    // Keep payload size bounded; browser referrer headers are typically much shorter.\n    return referrer.slice(0, 2048);\n  }\n\n  window.submitMapCreation = function() {\n    var radius = mapDiameter() / 2;\n    if (Math.abs(data.get(\"offsetX\")) >= radius || Math.abs(data.get(\"offsetY\")) >= radius) {\n      alert(\"Area adjustment offset can't be greater than map radius.\");\n      return;\n    }\n\n    $(\"#submit-button\").attr(\"disabled\", true);\n    $(\"#output\").slideUp().empty();\n    var msg = {\n      addrShort: data.get(\"selected_addr_short\"),\n      addrLong: data.get(\"selected_addr_full\"),\n      printingTech: data.get(\"printing-tech\"),\n      offsetX: data.get(\"offsetX\"),\n      offsetY: data.get(\"offsetY\"),\n      size: data.get(\"size\"),\n      contentMode: data.get(\"content-mode\") || \"normal\",\n      hideLocationMarker: data.get(\"hide-location-marker\") || false,\n      lon: data.get(\"lon\"),\n      lat: data.get(\"lat\"),\n      effectiveArea: (function(){\n        var metersPerDeg = mapCalc.metersPerDegree(data.get(\"lat\"));\n        var degreesLon = 1 / metersPerDeg.lon * radius;\n        var degreesLat = 1 / metersPerDeg.lat * radius;\n        var posLonLat = computeLonLat(data);\n        return {\n          lonMin: posLonLat[0] - degreesLon,\n          lonMax: posLonLat[0] + degreesLon,\n          latMin: posLonLat[1] - degreesLat,\n          latMax: posLonLat[1] + degreesLat\n        };\n      })(),\n      scale: parseInt(data.get(\"scale\"), 10),\n      diameter: Math.round(radius * 2), // larger of x and y diameter in meters\n      multipartMode: data.get(\"multipartMode\") || false,\n      noBorders: data.get(\"multipartMode\"),\n      multipartXpc: data.get(\"multipartXpc\"),\n      multipartYpc: data.get(\"multipartYpc\"),\n      advancedMode: data.get(\"advancedMode\") || false,\n      browserFingerprint: buildBrowserFingerprint(),\n      browserReferrer: browserReferrer(),\n      requestId: (function(){\n          var id = newMapId() + \"/\" + data.get(\"selected_addr_short\").replace(/[\\x00-\\x1F\\x80-\\x9F/]/g, '_');\n          var xpc = data.get(\"multipartXpc\");\n          var ypc = data.get(\"multipartYpc\");\n          if (data.get(\"multipartMode\")) {\n              id += \"-\" + (xpc < 0 ? \"m\" : \"p\") + Math.abs(xpc) + \"X\";\n              id += \"-\" + (ypc < 0 ? \"m\" : \"p\") + Math.abs(ypc) + \"Y\";\n          }\n          return id;\n      })()\n    };\n    if (msg.contentMode === \"only-big-roads\") {\n      msg.targetRoadDensity = normalizeTargetRoadDensityUiValue(data.get(\"target-road-density-ui\"));\n    }\n    if (! msg.hideLocationMarker) {\n      msg.marker1 = {\n        lat: parseFloat(data.get(\"lat\")),\n        lon: parseFloat(data.get(\"lon\"))\n      };\n    }\n    $(\"#submit-button\").val(window.TM.translations.progress__connecting);\n    withBrowserIp(msg, function() {\n      sendSqsRequest(msg);\n    });\n    //fbq('track', 'ViewContent');\n  };\n\n})();\n"
  },
  {
    "path": "web/src/scripts/map-desc-areas.js",
    "content": "/* global $ isNaN */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0, space-infix-ops:0, camelcase:0, dot-notation:0 */\n\n(function(){\n  'use strict';\n\n  const EXTERNAL_LINK_TYPE_PRIORITY = {\n    wikipedia: 0,\n    wikidata: 1,\n    commons: 2,\n    website: 3,\n    search: 4\n  };\n  const WATER_AREA_SUBCLASS_LABELS = {\n    B1_lakes: \"lake\",\n    B1_ponds: \"pond\",\n    B1_reservoirs: \"reservoir\",\n    B1_sea_coast: \"sea\",\n    B1_riverbanks: \"riverbank\",\n    B1_other_water: \"water area\"\n  };\n  const COVERAGE_COMPASS_DIRECTIONS = [\n    \"north\",\n    \"northeast\",\n    \"east\",\n    \"southeast\",\n    \"south\",\n    \"southwest\",\n    \"west\",\n    \"northwest\"\n  ];\n\n  function fallbackTranslate(key, fallback) {\n    return fallback !== undefined ? fallback : key;\n  }\n\n  let translate = fallbackTranslate;\n\n  function setTranslator(helpers) {\n    translate = helpers && typeof helpers.t === 'function' ? helpers.t : fallbackTranslate;\n  }\n\n  function t(key, fallback) {\n    return translate(key, fallback);\n  }\n\n  function normalizeOptions(options) {\n    const section = options && typeof options.section === \"string\" ? options.section : \"buildings\";\n    return {\n      section: section\n    };\n  }\n\n  function interpolate(text, replacements) {\n    if (!replacements) {\n      return text;\n    }\n    return text.replace(/__([a-zA-Z0-9_]+)__/g, function(match, name) {\n      if (replacements[name] === undefined) {\n        return match;\n      }\n      return replacements[name];\n    });\n  }\n\n  function capitalizeFirst(text) {\n    if (!text || typeof text !== 'string') {\n      return text;\n    }\n    return text.charAt(0).toUpperCase() + text.slice(1);\n  }\n\n  function lowercaseFirst(text) {\n    if (!text || typeof text !== 'string') {\n      return text;\n    }\n    return text.charAt(0).toLowerCase() + text.slice(1);\n  }\n\n  function splitLabelWithFallback(label, fallbackTitle) {\n    const fallback = fallbackTitle || t(\"map_content_building_unnamed\", \"Unnamed building\");\n    if (!label || typeof label !== 'string') {\n      return { title: fallback };\n    }\n    const parts = label.split(\",\");\n    let title = parts[0] ? parts[0].trim() : label.trim();\n    if (!title) {\n      return { title: fallback };\n    }\n    title = capitalizeFirst(title);\n    if (parts.length <= 1) {\n      return { title: title };\n    }\n    const subtitle = parts.slice(1).join(\",\").trim();\n    return { title: title, subtitle: subtitle || null };\n  }\n\n  function splitLabel(label) {\n    return splitLabelWithFallback(label, t(\"map_content_building_unnamed\", \"Unnamed building\"));\n  }\n\n  function trimString(value) {\n    if (typeof value !== \"string\") {\n      return \"\";\n    }\n    return value.trim();\n  }\n\n  function isUnnamedText(text) {\n    if (!text || typeof text !== \"string\") {\n      return true;\n    }\n    const normalized = text.trim().toLowerCase();\n    return !normalized ||\n      normalized === \"(unnamed)\" ||\n      normalized.indexOf(\"(unnamed)\") === 0 ||\n      normalized.indexOf(\"(unnamed)\") >= 0 ||\n      normalized.indexOf(\"unnamed\") === 0;\n  }\n\n  function slugifyOsmValue(value) {\n    if (!value || typeof value !== 'string') {\n      return \"\";\n    }\n    const normalized = value.normalize\n      ? value.normalize(\"NFKD\").replace(/[\\u0300-\\u036f]/g, \"\")\n      : value;\n    return normalized\n      .toLowerCase()\n      .replace(/[^a-z0-9]+/g, \"_\")\n      .replace(/^_+|_+$/g, \"\");\n  }\n\n  function translatedOsmValue(value) {\n    const slug = slugifyOsmValue(value);\n    if (!slug) {\n      return null;\n    }\n    const key = \"map_content_osm_value_\" + slug;\n    const translated = t(key, key);\n    if (translated === key) {\n      return null;\n    }\n    return translated;\n  }\n\n  function translatedOsmValueWithSuffix(value, suffix) {\n    const slug = slugifyOsmValue(value);\n    const suffixSlug = slugifyOsmValue(suffix);\n    if (!slug || !suffixSlug) {\n      return null;\n    }\n    const key = \"map_content_osm_value_\" + slug + \"_\" + suffixSlug;\n    const translated = t(key, key);\n    if (translated === key) {\n      return null;\n    }\n    return translated;\n  }\n\n  function localizeBuildingTitle(title) {\n    if (!title || typeof title !== 'string') {\n      return title;\n    }\n    const exactBuilding = translatedOsmValueWithSuffix(title, \"building\");\n    if (exactBuilding) {\n      return exactBuilding;\n    }\n    const exact = translatedOsmValue(title);\n    if (exact) {\n      return exact;\n    }\n    const match = title.match(/^(.*)\\s+building$/i);\n    if (match) {\n      const translatedBuilding = translatedOsmValueWithSuffix(match[1].trim(), \"building\");\n      if (translatedBuilding) {\n        return translatedBuilding;\n      }\n      const translatedValue = translatedOsmValue(match[1].trim());\n      if (translatedValue) {\n        return interpolate(\n          t(\"map_content_building_type_from_value\", \"__value__ building\"),\n          { value: translatedValue }\n        );\n      }\n    }\n    return title;\n  }\n\n  function pickPrimaryItem(group) {\n    if (!group || !group.items || !group.items.length) {\n      return null;\n    }\n    return group.items[0];\n  }\n\n  function locationPhrase(group, item) {\n    const components = item && item.visibleGeometry && Array.isArray(item.visibleGeometry.components)\n      ? item.visibleGeometry.components\n      : null;\n    if (components && components.length && components[0].location) {\n      const locationText = locationPhraseFromLoc(components[0].location.loc, \"clause\");\n      if (locationText) {\n        return locationText;\n      }\n    }\n    if (item && item.location && item.location.center) {\n      return locationTextFromValue(item.location.center, \"clause\");\n    }\n    if (group && group.location && group.location.center) {\n      return locationTextFromValue(group.location.center, \"clause\");\n    }\n    return null;\n  }\n\n  function edgeLabel(edge) {\n    if (edge === \"north\") return t(\"map_content_edge_north\", \"north\");\n    if (edge === \"south\") return t(\"map_content_edge_south\", \"south\");\n    if (edge === \"east\") return t(\"map_content_edge_east\", \"east\");\n    if (edge === \"west\") return t(\"map_content_edge_west\", \"west\");\n    return edge;\n  }\n\n  function directionLabel(direction) {\n    if (direction === \"north\") return t(\"map_content_dir_north\", \"north\");\n    if (direction === \"northeast\") return t(\"map_content_dir_northeast\", \"north-east\");\n    if (direction === \"east\") return t(\"map_content_dir_east\", \"east\");\n    if (direction === \"southeast\") return t(\"map_content_dir_southeast\", \"south-east\");\n    if (direction === \"south\") return t(\"map_content_dir_south\", \"south\");\n    if (direction === \"southwest\") return t(\"map_content_dir_southwest\", \"south-west\");\n    if (direction === \"west\") return t(\"map_content_dir_west\", \"west\");\n    if (direction === \"northwest\") return t(\"map_content_dir_northwest\", \"north-west\");\n    return direction;\n  }\n\n  function cornerLabel(direction) {\n    if (direction === \"northwest\") return t(\"map_content_corner_northwest\", \"north-west corner\");\n    if (direction === \"northeast\") return t(\"map_content_corner_northeast\", \"north-east corner\");\n    if (direction === \"southwest\") return t(\"map_content_corner_southwest\", \"south-west corner\");\n    if (direction === \"southeast\") return t(\"map_content_corner_southeast\", \"south-east corner\");\n    return direction;\n  }\n\n  /**\n   * Location phrase grammar (distance classification only):\n   * 1) center, 2) part + dir, 3) near_edge + dir (diagonal near_edge means corner).\n   * Forms:\n   * - atom: \"center\", \"east part\", \"north edge\", \"north-east corner\"\n   * - clause: \"in the center\", \"in the east part\", \"near the north edge\", \"in the north-east corner\"\n   * - endpoint: \"the center\", \"the east part\", \"near the north edge\", \"the north-east corner\"\n   * Rules:\n   * - avoid duplicated prepositions by selecting form by context.\n   */\n  function locationPhraseFromLoc(loc, form) {\n    if (!loc || typeof loc !== 'object') {\n      return null;\n    }\n    const phraseForm = form || \"clause\";\n    const kind = loc.kind;\n    const dirLabel = loc.dir ? directionLabel(loc.dir) : null;\n    const corner = loc.dir ? cornerLabel(loc.dir) : null;\n    const atomCenter = t(\"map_content_loc_center\", \"center\");\n    const clauseCenter = t(\"map_content_loc_full_center\", \"in the center\");\n    const endpointCenter = t(\"map_content_loc_endpoint_center\", \"the center\");\n    if (kind === \"center\") {\n      if (phraseForm === \"route_from\") {\n        return t(\"map_content_loc_route_from_center\", endpointCenter);\n      }\n      if (phraseForm === \"route_to\") {\n        return t(\"map_content_loc_route_to_center\", endpointCenter);\n      }\n      if (phraseForm === \"atom\") {\n        return atomCenter;\n      }\n      if (phraseForm === \"endpoint\") {\n        return endpointCenter;\n      }\n      return clauseCenter;\n    }\n    if (kind === \"part\") {\n      if (!dirLabel) {\n        if (phraseForm === \"atom\") {\n          return atomCenter;\n        }\n        if (phraseForm === \"endpoint\") {\n          return endpointCenter;\n        }\n        return clauseCenter;\n      }\n      if (phraseForm === \"route_from\") {\n        return interpolate(\n          t(\"map_content_loc_route_from_part\", t(\"map_content_loc_endpoint_part\", \"the __dir__ part\")),\n          { dir: dirLabel }\n        );\n      }\n      if (phraseForm === \"route_to\") {\n        return interpolate(\n          t(\"map_content_loc_route_to_part\", t(\"map_content_loc_endpoint_part\", \"the __dir__ part\")),\n          { dir: dirLabel }\n        );\n      }\n      const atomPart = interpolate(t(\"map_content_loc_part\", \"__dir__ part\"), { dir: dirLabel });\n      if (phraseForm === \"atom\") {\n        return atomPart;\n      }\n      if (phraseForm === \"endpoint\") {\n        return interpolate(t(\"map_content_loc_endpoint_part\", \"the __dir__ part\"), { dir: dirLabel });\n      }\n      return interpolate(t(\"map_content_loc_full_part\", \"in the __dir__ part\"), { dir: dirLabel });\n    }\n    if (kind === \"near_edge\") {\n      if (loc.dir === \"northwest\" || loc.dir === \"northeast\" ||\n          loc.dir === \"southwest\" || loc.dir === \"southeast\") {\n        if (!corner) {\n          return null;\n        }\n        if (phraseForm === \"route_from\") {\n          return interpolate(\n            t(\"map_content_loc_route_from_corner\", t(\"map_content_loc_endpoint_corner\", \"the __corner__\")),\n            { corner: corner }\n          );\n        }\n        if (phraseForm === \"route_to\") {\n          return interpolate(\n            t(\"map_content_loc_route_to_corner\", t(\"map_content_loc_endpoint_corner\", \"the __corner__\")),\n            { corner: corner }\n          );\n        }\n        if (phraseForm === \"atom\") {\n          return interpolate(t(\"map_content_loc_corner\", \"__corner__\"), { corner: corner });\n        }\n        if (phraseForm === \"endpoint\") {\n          return interpolate(t(\"map_content_loc_endpoint_corner\", \"the __corner__\"), { corner: corner });\n        }\n        return interpolate(t(\"map_content_loc_full_near_corner\", \"in the __corner__\"), { corner: corner });\n      }\n      if (!dirLabel) {\n        return null;\n      }\n      const atomEdge = interpolate(t(\"map_content_loc_edge\", \"__dir__ edge\"), { dir: dirLabel });\n      if (phraseForm === \"route_from\") {\n        return interpolate(\n          t(\"map_content_loc_route_from_near_edge\", t(\"map_content_loc_endpoint_near_edge\", \"near the __dir__ edge\")),\n          { dir: dirLabel }\n        );\n      }\n      if (phraseForm === \"route_to\") {\n        return interpolate(\n          t(\"map_content_loc_route_to_near_edge\", t(\"map_content_loc_endpoint_near_edge\", \"near the __dir__ edge\")),\n          { dir: dirLabel }\n        );\n      }\n      if (phraseForm === \"atom\") {\n        return atomEdge;\n      }\n      return interpolate(\n        phraseForm === \"endpoint\"\n          ? t(\"map_content_loc_endpoint_near_edge\", \"near the __dir__ edge\")\n          : t(\"map_content_loc_full_near_edge\", \"near the __dir__ edge\"),\n        { dir: dirLabel }\n      );\n    }\n    return null;\n  }\n\n  function locationTextFromValue(value, form) {\n    if (value && typeof value === 'object') {\n      const loc = value.loc && typeof value.loc === 'object' ? value.loc : value;\n      return locationPhraseFromLoc(loc, form);\n    }\n    return null;\n  }\n\n  function joinWithAnd(parts) {\n    const andWord = t(\"map_content_list_and\", \"and\");\n    if (!parts.length) {\n      return \"\";\n    }\n    if (parts.length === 1) {\n      return parts[0];\n    }\n    if (parts.length === 2) {\n      return parts[0] + \" \" + andWord + \" \" + parts[1];\n    }\n    return parts.slice(0, -1).join(\", \") + \", \" + andWord + \" \" + parts[parts.length - 1];\n  }\n\n  function parseEdgeTouches(edgesTouched) {\n    if (!edgesTouched || !edgesTouched.length) {\n      return [];\n    }\n    const touches = [];\n    edgesTouched.forEach(function(entry){\n      if (entry && typeof entry === 'object') {\n        Object.keys(entry).forEach(function(edge){\n          touches.push({ edge: edge, percent: entry[edge], order: touches.length });\n        });\n      }\n    });\n    return touches;\n  }\n\n  function edgePositionBucketFromDirection(edge, direction) {\n    if (edge === \"east\") {\n      if (direction === \"northeast\") return \"north\";\n      if (direction === \"east\") return \"center\";\n      if (direction === \"southeast\") return \"south\";\n      return null;\n    }\n    if (edge === \"west\") {\n      if (direction === \"northwest\") return \"north\";\n      if (direction === \"west\") return \"center\";\n      if (direction === \"southwest\") return \"south\";\n      return null;\n    }\n    if (edge === \"north\") {\n      if (direction === \"northwest\") return \"west\";\n      if (direction === \"north\") return \"center\";\n      if (direction === \"northeast\") return \"east\";\n      return null;\n    }\n    if (edge === \"south\") {\n      if (direction === \"southwest\") return \"west\";\n      if (direction === \"south\") return \"center\";\n      if (direction === \"southeast\") return \"east\";\n      return null;\n    }\n    return null;\n  }\n\n  function edgePositionFromCoverage(coverage, edge) {\n    const segments = coverage && Array.isArray(coverage.segments) ? coverage.segments : [];\n    if (!segments.length) {\n      return null;\n    }\n    const buckets = { north: 0, center: 0, south: 0, east: 0, west: 0 };\n    let matchedSegments = 0;\n    segments.forEach(function(segment){\n      if (!segment || !segment.loc || segment.loc.kind !== \"near_edge\") {\n        return;\n      }\n      const direction = segment.loc.dir;\n      const bucket = edgePositionBucketFromDirection(edge, direction);\n      if (!bucket) {\n        return;\n      }\n      matchedSegments += 1;\n      const count = Number(segment.insideCount);\n      if (!isFinite(count) || count <= 0) {\n        return;\n      }\n      buckets[bucket] += count;\n    });\n    if (matchedSegments <= 1) {\n      return null;\n    }\n\n    const present = Object.keys(buckets).filter(function(bucket){ return buckets[bucket] > 0; });\n    if (present.length === 0) {\n      return null;\n    }\n    if (present.length > 1) {\n      return \"multiple\";\n    }\n    return present[0];\n  }\n\n  function edgePositionQualifier(position) {\n    if (!position) {\n      return null;\n    }\n    if (position === \"multiple\") return t(\"map_content_touch_pos_multiple\", \"in multiple places\");\n    if (position === \"center\") return t(\"map_content_touch_pos_center\", \"near the center\");\n    if (position === \"north\") return t(\"map_content_touch_pos_north\", \"in the north\");\n    if (position === \"south\") return t(\"map_content_touch_pos_south\", \"in the south\");\n    if (position === \"east\") return t(\"map_content_touch_pos_east\", \"in the east\");\n    if (position === \"west\") return t(\"map_content_touch_pos_west\", \"in the west\");\n    return null;\n  }\n\n  function edgesText(edgesTouched, coverage) {\n    const touches = parseEdgeTouches(edgesTouched);\n    if (!touches.length) {\n      return null;\n    }\n    const orderedTouches = touches.slice().sort(function(a, b){\n      const aValue = Number(a.percent);\n      const bValue = Number(b.percent);\n      const aHasPercent = isFinite(aValue);\n      const bHasPercent = isFinite(bValue);\n      if (aHasPercent && bHasPercent) {\n        if (Math.abs(bValue - aValue) > 1e-9) {\n          return bValue - aValue;\n        }\n        return a.order - b.order;\n      }\n      if (aHasPercent !== bHasPercent) {\n        return aHasPercent ? -1 : 1;\n      }\n      return a.order - b.order;\n    });\n    const parts = orderedTouches.map(function(touch){\n      const edge = edgeLabel(touch.edge);\n      if (!edge) {\n        return null;\n      }\n      let base = null;\n      if (touch.percent !== null && touch.percent !== undefined && !isNaN(touch.percent)) {\n        base = interpolate(t(\"map_content_touches_edge_percent\", \"touches __percent__% of __edge__ edge\"), {\n          edge: edge,\n          percent: formatPercent(touch.percent)\n        });\n      } else {\n        base = interpolate(t(\"map_content_touches_edge\", \"Touches __edge__ edge\"), { edge: edge });\n      }\n      const position = edgePositionFromCoverage(coverage, touch.edge);\n      const qualifier = edgePositionQualifier(position);\n      return qualifier ? base + \" \" + qualifier : base;\n    }).filter(Boolean);\n    return joinWithAnd(parts);\n  }\n\n  function formatPercent(value) {\n    if (value === null || value === undefined || isNaN(value)) {\n      return null;\n    }\n    const num = Number(value);\n    if (!isFinite(num)) {\n      return null;\n    }\n    if (num < 10) {\n      return num.toFixed(1);\n    }\n    return Math.round(num).toFixed(0);\n  }\n\n  function formatAspect(value) {\n    if (value === null || value === undefined || isNaN(value)) {\n      return null;\n    }\n    const num = Number(value);\n    if (!isFinite(num)) {\n      return null;\n    }\n    return num.toFixed(1);\n  }\n\n  function orientationLabel(label) {\n    if (!label || typeof label !== 'string') {\n      return null;\n    }\n    if (label === \"east-west\") {\n      return t(\"map_content_orientation_label_east_west\", \"east-west\");\n    }\n    if (label === \"north-south\") {\n      return t(\"map_content_orientation_label_north_south\", \"north-south\");\n    }\n    if (label === \"northeast-southwest\") {\n      return t(\"map_content_orientation_label_northeast_southwest\", \"north-east to south-west\");\n    }\n    if (label === \"east-northeast to west-southwest\") {\n      return t(\n        \"map_content_orientation_label_east_northeast_to_west_southwest\",\n        \"east-north-east to west-south-west\"\n      );\n    }\n    if (label === \"north-northeast to south-southwest\") {\n      return t(\n        \"map_content_orientation_label_north_northeast_to_south_southwest\",\n        \"north-north-east to south-south-west\"\n      );\n    }\n    return label;\n  }\n\n  function shapeTypeLabel(shapeType) {\n    if (!shapeType || typeof shapeType !== 'string') {\n      return null;\n    }\n    if (shapeType === \"regular\") {\n      return null;\n    }\n    if (shapeType === \"complex\") {\n      return t(\"map_content_shape_irregular\", \"Irregular shape\");\n    }\n    if (shapeType === \"thin\") {\n      return t(\"map_content_shape_elongated\", \"Thin\");\n    }\n    return interpolate(\n      t(\"map_content_shape_generic\", \"__shape__ shape\"),\n      { shape: capitalizeFirst(shapeType) }\n    );\n  }\n\n  function isDiagonalDirection(direction) {\n    return direction === \"northwest\" ||\n      direction === \"northeast\" ||\n      direction === \"southwest\" ||\n      direction === \"southeast\";\n  }\n\n  function normalizeCoverageLoc(loc) {\n    if (!loc || typeof loc !== 'object') {\n      return null;\n    }\n    const direction = loc.dir || null;\n    if (loc.kind === \"center\") {\n      return { kind: \"center\", dir: null };\n    }\n    if (loc.kind === \"part\") {\n      return direction ? { kind: \"part\", dir: direction } : { kind: \"center\", dir: null };\n    }\n    if (loc.kind === \"near_edge\") {\n      return direction ? { kind: \"near_edge\", dir: direction } : null;\n    }\n    return null;\n  }\n\n  function coverageBucketKey(bucket) {\n    if (!bucket) {\n      return \"\";\n    }\n    return bucket.kind + \":\" + (bucket.dir || \"\");\n  }\n\n  function normalizedCoverageBuckets(coverage) {\n    if (!coverage || !Array.isArray(coverage.segments) || !coverage.segments.length) {\n      return [];\n    }\n    const bucketMap = {};\n    let total = 0;\n    coverage.segments.forEach(function(segment){\n      if (!segment || typeof segment.insideCount !== 'number' || segment.insideCount <= 0) {\n        return;\n      }\n      const bucket = normalizeCoverageLoc(segment.loc);\n      if (!bucket) {\n        return;\n      }\n      const key = coverageBucketKey(bucket);\n      if (!key) {\n        return;\n      }\n      if (!bucketMap[key]) {\n        bucketMap[key] = { kind: bucket.kind, dir: bucket.dir, insideCount: 0 };\n      }\n      bucketMap[key].insideCount += segment.insideCount;\n      total += segment.insideCount;\n    });\n    if (total <= 0) {\n      return [];\n    }\n    const buckets = Object.keys(bucketMap).map(function(key){\n      const bucket = bucketMap[key];\n      return {\n        kind: bucket.kind,\n        dir: bucket.dir,\n        insideCount: bucket.insideCount,\n        share: bucket.insideCount / total\n      };\n    });\n    buckets.sort(function(a, b){\n      return b.share - a.share;\n    });\n    return buckets;\n  }\n\n  function bucketClauseText(bucket) {\n    if (!bucket) {\n      return null;\n    }\n    return locationPhraseFromLoc({ kind: bucket.kind, dir: bucket.dir }, \"clause\");\n  }\n\n  function bucketTargetText(bucket) {\n    if (!bucket) {\n      return null;\n    }\n    return locationPhraseFromLoc({ kind: bucket.kind, dir: bucket.dir }, \"atom\");\n  }\n\n  function coverageSingleSentence(bucket) {\n    const clause = locationPhraseFromLoc({ kind: bucket.kind, dir: bucket.dir }, \"clause\");\n    if (!clause) {\n      return null;\n    }\n    return capitalizeFirst(clause) + \".\";\n  }\n\n  function coverageEqualSentence(first, second) {\n    const firstText = bucketClauseText(first);\n    const secondText = bucketClauseText(second);\n    if (!firstText || !secondText) {\n      return null;\n    }\n    return capitalizeFirst(interpolate(\n      t(\"map_content_summary_equal_clauses\", \"__first__ and __second__.\"),\n      {\n        first: firstText,\n        second: secondText\n      }\n    ));\n  }\n\n  function coverageMostlySentence(top, second) {\n    const lead = bucketClauseText(top);\n    const target = bucketTargetText(second);\n    if (!lead || !target) {\n      return null;\n    }\n    return interpolate(\n      t(\"map_content_summary_mostly_extending_clauses\", \"Mostly __lead__, extending toward __target__.\"),\n      {\n        lead: lead,\n        target: target\n      }\n    );\n  }\n\n  function coverageDistributedSentence(top, second) {\n    const first = bucketClauseText(top);\n    if (!first) {\n      return null;\n    }\n    if (!second || second.share < 0.15) {\n      return interpolate(\n        t(\"map_content_summary_distributed_top1\", \"In several areas, mostly __first__.\"),\n        { first: first }\n      );\n    }\n    const secondText = bucketClauseText(second);\n    if (!secondText) {\n      return null;\n    }\n    return interpolate(\n      t(\"map_content_summary_distributed_top2\", \"In several areas, mostly __first__ and __second__.\"),\n      {\n        first: first,\n        second: secondText\n      }\n    );\n  }\n\n  // --- Helpers: graph + distance ------------------------------------------------\n\n  function coverageAllDirectionNodes() {\n    // 8 compass dirs + center\n    return [\"center\"].concat(COVERAGE_COMPASS_DIRECTIONS);\n  }\n\n  function coverageDirectionNeighbors(direction) {\n    if (!direction) return [];\n    if (direction === \"center\") {\n      // center adjacent to all compass directions\n      return COVERAGE_COMPASS_DIRECTIONS.slice();\n    }\n    const idx = COVERAGE_COMPASS_DIRECTIONS.indexOf(direction);\n    if (idx < 0) return [];\n    const prev = (idx - 1 + COVERAGE_COMPASS_DIRECTIONS.length) % COVERAGE_COMPASS_DIRECTIONS.length;\n    const next = (idx + 1) % COVERAGE_COMPASS_DIRECTIONS.length;\n    // also adjacent to center (optional; but makes distance model symmetric)\n    return [\"center\", COVERAGE_COMPASS_DIRECTIONS[prev], COVERAGE_COMPASS_DIRECTIONS[next]];\n  }\n\n  function coverageBuildDistanceMatrix() {\n    const nodes = coverageAllDirectionNodes();\n    const dist = {};\n    for (let i = 0; i < nodes.length; i += 1) {\n      const start = nodes[i];\n      dist[start] = {};\n      // BFS from start\n      const queue = [start];\n      dist[start][start] = 0;\n      while (queue.length) {\n        const cur = queue.shift();\n        const curDist = dist[start][cur];\n        const neighbors = coverageDirectionNeighbors(cur);\n        for (let j = 0; j < neighbors.length; j += 1) {\n          const nb = neighbors[j];\n          if (dist[start][nb] !== undefined) continue;\n          dist[start][nb] = curDist + 1;\n          queue.push(nb);\n        }\n      }\n    }\n    return dist;\n  }\n\n  const COVERAGE_DIRECTION_DIST = coverageBuildDistanceMatrix();\n\n  function coverageMinDistanceToCluster(direction, clusterDirs) {\n    if (!direction) return Infinity;\n    let best = Infinity;\n    for (let i = 0; i < clusterDirs.length; i += 1) {\n      const c = clusterDirs[i];\n      const d = (COVERAGE_DIRECTION_DIST[c] && COVERAGE_DIRECTION_DIST[c][direction]);\n      if (typeof d === \"number\" && d < best) best = d;\n    }\n    return best;\n  }\n\n  function coverageClusterIsConnected(clusterDirs) {\n    if (!Array.isArray(clusterDirs) || clusterDirs.length === 0) return false;\n    const set = {};\n    for (let i = 0; i < clusterDirs.length; i += 1) {\n      set[clusterDirs[i]] = true;\n    }\n    const start = clusterDirs[0];\n    const visited = {};\n    const stack = [start];\n    visited[start] = true;\n\n    while (stack.length) {\n      const cur = stack.pop();\n      const neighbors = coverageDirectionNeighbors(cur);\n      for (let i = 0; i < neighbors.length; i += 1) {\n        const nb = neighbors[i];\n        if (!set[nb] || visited[nb]) continue;\n        visited[nb] = true;\n        stack.push(nb);\n      }\n    }\n\n    for (let i = 0; i < clusterDirs.length; i += 1) {\n      if (!visited[clusterDirs[i]]) return false;\n    }\n    return true;\n  }\n\n  // --- Cluster search -----------------------------------------------------------\n\n  function coverageDirectionComponent(bucket) {\n    if (!bucket || typeof bucket !== \"object\") {\n      return null;\n    }\n    if (bucket.kind === \"center\") {\n      return \"center\";\n    }\n    if (typeof bucket.dir !== \"string\") {\n      return null;\n    }\n    return COVERAGE_COMPASS_DIRECTIONS.indexOf(bucket.dir) >= 0 ? bucket.dir : null;\n  }\n\n  function coverageDirectionSharesFromBuckets(buckets) {\n    const shares = {};\n    const nodes = coverageAllDirectionNodes();\n    for (let i = 0; i < nodes.length; i += 1) shares[nodes[i]] = 0;\n\n    for (let i = 0; i < buckets.length; i += 1) {\n      const dir = coverageDirectionComponent(buckets[i]);\n      if (!dir) continue;\n      const s = typeof buckets[i].share === \"number\" ? buckets[i].share : 0;\n      shares[dir] += s;\n    }\n    return shares;\n  }\n\n  function coverageScoreCluster(directionShares, clusterDirs) {\n    // cluster share: sum of shares in cluster\n    let clusterShare = 0;\n    const nodes = coverageAllDirectionNodes();\n    const inCluster = {};\n    for (let i = 0; i < clusterDirs.length; i += 1) inCluster[clusterDirs[i]] = true;\n\n    for (let i = 0; i < clusterDirs.length; i += 1) {\n      clusterShare += directionShares[clusterDirs[i]] || 0;\n    }\n\n    // far leak: any share at distance >= 2 from cluster\n    let farLeak = 0;\n    for (let i = 0; i < nodes.length; i += 1) {\n      const dir = nodes[i];\n      if (inCluster[dir]) continue;\n      const share = directionShares[dir] || 0;\n      if (share <= 0) continue;\n      const minDist = coverageMinDistanceToCluster(dir, clusterDirs);\n      if (minDist >= 2) farLeak += share;\n    }\n\n    return { clusterShare, farLeak };\n  }\n\n  function coverageEnumerateCombinations(items, k, onCombo) {\n    const combo = [];\n    function rec(start, depth) {\n      if (depth === k) {\n        onCombo(combo.slice());\n        return;\n      }\n      for (let i = start; i <= items.length - (k - depth); i += 1) {\n        combo.push(items[i]);\n        rec(i + 1, depth + 1);\n        combo.pop();\n      }\n    }\n    rec(0, 0);\n  }\n\n  function coverageFindBestCluster(buckets) {\n    const directionShares = coverageDirectionSharesFromBuckets(buckets);\n    const nodes = coverageAllDirectionNodes();\n\n    let best = null;\n\n    function consider(clusterDirs) {\n      if (!coverageClusterIsConnected(clusterDirs)) return;\n      const score = coverageScoreCluster(directionShares, clusterDirs);\n\n      // basic dominance requirement first (cheap pruning)\n      // (Keep thresholds here so we don't waste time comparing weak clusters)\n      if (score.clusterShare < 0.75) return;\n\n      if (!best) {\n        best = { clusterDirs, clusterShare: score.clusterShare, farLeak: score.farLeak };\n        return;\n      }\n\n      // Prefer higher clusterShare, then lower farLeak, then smaller cluster size (more concise)\n      if (score.clusterShare > best.clusterShare + 1e-9) {\n        best = { clusterDirs, clusterShare: score.clusterShare, farLeak: score.farLeak };\n        return;\n      }\n      if (Math.abs(score.clusterShare - best.clusterShare) <= 1e-9) {\n        if (score.farLeak < best.farLeak - 1e-9) {\n          best = { clusterDirs, clusterShare: score.clusterShare, farLeak: score.farLeak };\n          return;\n        }\n        if (Math.abs(score.farLeak - best.farLeak) <= 1e-9) {\n          if (clusterDirs.length < best.clusterDirs.length) {\n            best = { clusterDirs, clusterShare: score.clusterShare, farLeak: score.farLeak };\n          }\n        }\n      }\n    }\n\n    // search connected clusters of size 3..5\n    for (let size = 3; size <= 5; size += 1) {\n      coverageEnumerateCombinations(nodes, size, consider);\n    }\n\n    return best;\n  }\n\n  // --- Phrase selection ---------------------------------------------------------\n\n  function threeRegionsPhrase(top, second, third) {\n    const firstText = bucketClauseText(top);\n    const secondText = bucketClauseText(second);\n    const thirdText = bucketClauseText(third);\n    if (!firstText || !secondText || !thirdText) {\n      return null;\n    }\n    return interpolate(\n      t(\"map_content_summary_three_regions_clauses\", \"Mostly __first__, __second__, and __third__.\"),\n      { first: firstText, second: secondText, third: thirdText }\n    );\n  }\n\n  /**\n   * Coverage phrase selection:\n   * - meaningful bucket threshold: share >= 0.15\n   * - cluster phrase (threeRegionsPhrase) if:\n   *   a) there exists a connected cluster of 3..5 adjacent directions whose total share >= 0.75, and\n   *   b) far-away leakage (distance >= 2 from that cluster) is small (<= 0.10), and\n   *   c) at least 3 meaningful buckets fall inside the chosen cluster (so we can name them)\n   * - distributed / single / equal / mostly-extending as before.\n   */\n  function coverageBreakdown(coverage) {\n    const buckets = normalizedCoverageBuckets(coverage);\n    if (!buckets.length) {\n      return null;\n    }\n\n    const top = buckets[0];\n    const second = buckets.length > 1 ? buckets[1] : null;\n\n    const topShare = top ? top.share : 0;\n    const secondShare = second ? second.share : 0;\n\n    const meaningful = buckets.filter(function(bucket) {\n      return bucket.share >= 0.15;\n    });\n\n    // clustered >=3 adjacent regions, minimal far-away spill -------------\n    const bestCluster = coverageFindBestCluster(buckets);\n    if (bestCluster && bestCluster.farLeak <= 0.10) {\n      const inCluster = {};\n      for (let i = 0; i < bestCluster.clusterDirs.length; i += 1) {\n        inCluster[bestCluster.clusterDirs[i]] = true;\n      }\n\n      // Pick the top 3 meaningful buckets within the cluster to mention\n      const clusterMeaningful = meaningful.filter(function(bucket) {\n        const dir = coverageDirectionComponent(bucket);\n        return !!(dir && inCluster[dir]);\n      });\n\n      if (clusterMeaningful.length >= 3) {\n        return threeRegionsPhrase(clusterMeaningful[0], clusterMeaningful[1], clusterMeaningful[2]);\n      }\n    }\n\n    // --- Existing distributed logic (unchanged, except no forced \"not connected\")-\n    const moreDistributed =\n      meaningful.length >= 4 ||\n      topShare < 0.55 ||\n      (topShare + secondShare) < 0.80;\n\n    if (moreDistributed) {\n      return coverageDistributedSentence(top, second);\n    }\n    if (!second || secondShare < 0.15 || meaningful.length <= 1 || topShare >= 0.85) {\n      return coverageSingleSentence(top);\n    }\n    if (Math.abs(topShare - secondShare) <= 0.07) {\n      return coverageEqualSentence(top, second);\n    }\n    return coverageMostlySentence(top, second);\n  }\n\n  function buildingGroupIsNamed(group) {\n    const primary = pickPrimaryItem(group);\n    const explicitName = trimString(group && group.label) || trimString(primary && primary.label);\n    if (explicitName && !isUnnamedText(explicitName)) {\n      return true;\n    }\n    let labelSource = group && group.displayLabel;\n    if (!labelSource && primary) {\n      labelSource = primary.displayLabel;\n    }\n    const nameParts = splitLabel(labelSource);\n    return !!(nameParts && typeof nameParts.subtitle === \"string\" && nameParts.subtitle.trim());\n  }\n\n  function collectBuildingGroups(mapContent) {\n    const groups = [];\n    if (!mapContent || typeof mapContent !== 'object') {\n      return groups;\n    }\n    Object.keys(mapContent).forEach(function(key){\n      const entry = mapContent[key];\n      if (!entry || !Array.isArray(entry.subclasses) || !entry.subclasses.length) {\n        return;\n      }\n      entry.subclasses.forEach(function(sub){\n        if (!sub || sub.kind !== \"building\" || !Array.isArray(sub.groups) || !sub.groups.length) {\n          return;\n        }\n        sub.groups.forEach(function(group){\n          if (group) {\n            groups.push(group);\n          }\n        });\n      });\n    });\n    groups.sort(function(a, b){\n      const aNamed = buildingGroupIsNamed(a);\n      const bNamed = buildingGroupIsNamed(b);\n      if (aNamed !== bNamed) {\n        return aNamed ? -1 : 1;\n      }\n      const importanceDiff = groupImportanceScore(b) - groupImportanceScore(a);\n      if (importanceDiff !== 0) {\n        return importanceDiff;\n      }\n      const coverageDiff = groupCoveragePercent(b) - groupCoveragePercent(a);\n      if (Math.abs(coverageDiff) > 1e-9) {\n        return coverageDiff;\n      }\n      const aTitle = groupTitleKey(a);\n      const bTitle = groupTitleKey(b);\n      return aTitle.localeCompare(bTitle);\n    });\n    return groups;\n  }\n\n  function isWaterAreaSubClass(subClass) {\n    return !!(subClass && WATER_AREA_SUBCLASS_LABELS[subClass]);\n  }\n\n  function waterAreaTypeLabel(subClass) {\n    if (isWaterAreaSubClass(subClass)) {\n      const key = \"map_content_water_area_type_\" + subClass;\n      return t(key, WATER_AREA_SUBCLASS_LABELS[subClass]);\n    }\n    return t(\"map_content_water_area_type_generic\", \"water area\");\n  }\n\n  function waterAreaTypeLabelPlural() {\n    return t(\"map_content_water_area_type_generic_plural\", \"water areas\");\n  }\n\n  function waterAreaNameFromDisplayLabel(group, primary) {\n    const sources = [\n      trimString(group && group.displayLabel),\n      trimString(primary && primary.displayLabel)\n    ].filter(Boolean);\n    for (let i = 0; i < sources.length; i += 1) {\n      const source = sources[i];\n      const colonIndex = source.indexOf(\":\");\n      let candidate = colonIndex >= 0 ? source.slice(colonIndex + 1).trim() : source.trim();\n      if (!candidate || isUnnamedText(candidate)) {\n        continue;\n      }\n      const lower = candidate.toLowerCase();\n      if (lower === \"water area\") {\n        continue;\n      }\n      return candidate;\n    }\n    return null;\n  }\n\n  function waterAreaEntryName(entry) {\n    if (!entry) {\n      return null;\n    }\n    const group = entry.group;\n    const primary = pickPrimaryItem(group);\n    const explicit = trimString(group && group.label) || trimString(primary && primary.label);\n    if (explicit && !isUnnamedText(explicit)) {\n      return explicit;\n    }\n    return waterAreaNameFromDisplayLabel(group, primary);\n  }\n\n  function waterAreaEntryIsNamed(entry) {\n    return !!waterAreaEntryName(entry);\n  }\n\n  function waterAreaEntryTitleKey(entry) {\n    const name = waterAreaEntryName(entry) || \"\";\n    const typeText = waterAreaTypeLabel(entry && entry.subClass).toLowerCase();\n    return (typeText + \" \" + name).trim();\n  }\n\n  function collectWaterAreaEntries(mapContent) {\n    const entries = [];\n    const classB = mapContent && typeof mapContent === \"object\" ? mapContent.B : null;\n    const subclasses = classB && Array.isArray(classB.subclasses) ? classB.subclasses : [];\n    subclasses.forEach(function(subclass){\n      if (!subclass || subclass.kind !== \"area\" || !isWaterAreaSubClass(subclass.key) || !Array.isArray(subclass.groups)) {\n        return;\n      }\n      subclass.groups.forEach(function(group){\n        if (!group || typeof group !== \"object\") {\n          return;\n        }\n        entries.push({\n          group: group,\n          subClass: subclass.key\n        });\n      });\n    });\n    entries.sort(function(a, b){\n      const aNamed = waterAreaEntryIsNamed(a);\n      const bNamed = waterAreaEntryIsNamed(b);\n      if (aNamed !== bNamed) {\n        return aNamed ? -1 : 1;\n      }\n      const importanceDiff = groupImportanceScore(b.group) - groupImportanceScore(a.group);\n      if (importanceDiff !== 0) {\n        return importanceDiff;\n      }\n      const coverageDiff = groupCoveragePercent(b.group) - groupCoveragePercent(a.group);\n      if (Math.abs(coverageDiff) > 1e-9) {\n        return coverageDiff;\n      }\n      return waterAreaEntryTitleKey(a).localeCompare(waterAreaEntryTitleKey(b));\n    });\n    return entries;\n  }\n\n  function groupImportanceScore(group) {\n    const value = group ? group.importanceScore : null;\n    const resolved = value && typeof value === \"object\" ? value.final : value;\n    if (resolved === null || resolved === undefined || isNaN(resolved)) {\n      return 0;\n    }\n    const number = Number(resolved);\n    if (!isFinite(number)) {\n      return 0;\n    }\n    return number;\n  }\n\n  function importanceScoreTooltip(importanceScore) {\n    if (!importanceScore || typeof importanceScore !== \"object\" || Array.isArray(importanceScore)) {\n      return null;\n    }\n    try {\n      return JSON.stringify(importanceScore, null, 2);\n    } catch (_error) {\n      return null;\n    }\n  }\n\n  function isSafeExternalUrl(url) {\n    return typeof url === \"string\" && /^https?:\\/\\/\\S+$/i.test(url);\n  }\n\n  function isGoogleSearchExternalLink(link) {\n    if (!link || typeof link !== \"object\") {\n      return false;\n    }\n    if (typeof link.label !== \"string\" || link.label.trim().toLowerCase() !== \"search\") {\n      return false;\n    }\n    if (typeof link.url !== \"string\") {\n      return false;\n    }\n    try {\n      const parsed = new URL(link.url);\n      const host = parsed.hostname.toLowerCase();\n      if (!/^([a-z0-9-]+\\.)*google\\./.test(host)) {\n        return false;\n      }\n      return parsed.pathname === \"/search\";\n    } catch (_error) {\n      return false;\n    }\n  }\n\n  function normalizedExternalLink(link) {\n    if (!link || typeof link !== \"object\") {\n      return null;\n    }\n    if (!isSafeExternalUrl(link.url)) {\n      return null;\n    }\n    if (isGoogleSearchExternalLink(link)) {\n      return null;\n    }\n    return {\n      type: typeof link.type === \"string\" ? link.type : \"\",\n      url: link.url,\n      label: typeof link.label === \"string\" ? link.label : \"\"\n    };\n  }\n\n  function externalLinkPriority(link) {\n    if (!link || !link.type || EXTERNAL_LINK_TYPE_PRIORITY[link.type] === undefined) {\n      return Number.MAX_SAFE_INTEGER;\n    }\n    return EXTERNAL_LINK_TYPE_PRIORITY[link.type];\n  }\n\n  function bestGroupExternalLink(group) {\n    if (!group || !Array.isArray(group.items)) {\n      return null;\n    }\n    let best = null;\n    let bestPriority = Number.MAX_SAFE_INTEGER;\n    group.items.forEach(function(item){\n      if (!item || typeof item !== \"object\") {\n        return;\n      }\n      const candidate = normalizedExternalLink(item.externalLink);\n      if (!candidate) {\n        return;\n      }\n      const priority = externalLinkPriority(candidate);\n      if (!best || priority < bestPriority) {\n        best = candidate;\n        bestPriority = priority;\n      }\n    });\n    return best;\n  }\n\n  function normalizeLineParts(parts) {\n    if (!parts || !parts.length) {\n      return [];\n    }\n    return parts.filter(function(part){\n      return part && part.text !== undefined && part.text !== null && part.text !== \"\";\n    }).map(function(part){\n      return {\n        text: String(part.text),\n        className: part.className || null,\n        wrap: part.wrap === false ? false : true\n      };\n    });\n  }\n\n  function addModelLine(item, parts, className, titleText, link) {\n    if (!item || !Array.isArray(item.lines)) {\n      return;\n    }\n    const lineParts = normalizeLineParts(parts);\n    if (!lineParts.length) {\n      return;\n    }\n    item.lines.push({\n      className: className || null,\n      parts: lineParts,\n      title: titleText || null,\n      link: link || null\n    });\n  }\n\n  function applyItemAttrs(listItem, attrs) {\n    if (!attrs || typeof attrs !== \"object\") {\n      return;\n    }\n    if (attrs.dataOsmId !== undefined && attrs.dataOsmId !== null) {\n      listItem.attr(\"data-osm-id\", String(attrs.dataOsmId));\n    }\n    if (attrs.initiallyHidden) {\n      listItem.attr(\"data-initially-hidden\", \"true\");\n    }\n  }\n\n  function appendLineParts(target, parts) {\n    parts.forEach(function(part){\n      if (!part || part.text === undefined || part.text === null || part.text === \"\") {\n        return;\n      }\n      if (part.wrap === false) {\n        target.append(String(part.text));\n        return;\n      }\n      const span = $(\"<span>\").text(String(part.text));\n      if (part.className) {\n        span.addClass(part.className);\n      }\n      target.append(span);\n    });\n  }\n\n  function renderLineFromModel(listItem, lineModel) {\n    if (!lineModel || !Array.isArray(lineModel.parts) || !lineModel.parts.length) {\n      return;\n    }\n    const tagName = lineModel.className === \"map-content-title-line\" ? \"h5\" : \"div\";\n    const line = $(\"<\" + tagName + \">\");\n    if (lineModel.className) {\n      line.addClass(lineModel.className);\n    }\n    const titleLink = lineModel.className === \"map-content-title-line\"\n      ? normalizedExternalLink(lineModel.link)\n      : null;\n    appendLineParts(line, lineModel.parts);\n    if (titleLink) {\n      const anchor = $(\"<a>\")\n        .addClass(\"map-content-inline-external-link\")\n        .attr(\"href\", titleLink.url)\n        .attr(\"target\", \"_blank\")\n        .attr(\"rel\", \"noopener noreferrer\");\n      anchor.append($(\"<span>\").text(titleLink.label || \"Website\"));\n      anchor.append($(\"<span>\").addClass(\"map-content-external-indicator\").attr(\"aria-hidden\", \"true\").text(\"↗\"));\n      anchor.append($(\"<span>\").addClass(\"visuallyhidden\").text(\n        \" \" + t(\"map_content_external_link_aria\", \"External link, opens in a new tab\")\n      ));\n      line.append($(\"<span>\").addClass(\"map-content-external-link-sep\").text(\" \"));\n      line.append(anchor);\n    }\n    if (lineModel.title) {\n      line.addClass(\"map-content-has-importance-popup\");\n      const popup = $(\"<span>\").addClass(\"map-content-importance-popup\");\n      popup.append($(\"<pre>\").text(lineModel.title));\n      line.append(popup);\n    }\n    if (!line.text()) {\n      return;\n    }\n    listItem.append(line);\n  }\n\n  function renderItemFromModel(item, listElem) {\n    if (!item || !listElem || !listElem.length) {\n      return;\n    }\n    const listItem = $(\"<li>\").addClass(\"map-content-building\");\n    applyItemAttrs(listItem, item.attrs);\n    const lines = Array.isArray(item.lines) ? item.lines : [];\n    lines.forEach(function(lineModel){\n      renderLineFromModel(listItem, lineModel);\n    });\n    listElem.append(listItem);\n  }\n\n  function interpolatedParts(template, replacements, varClassName) {\n    if (!template || typeof template !== 'string') {\n      return [];\n    }\n    const parts = [];\n    const pattern = /__([a-zA-Z0-9_]+)__/g;\n    let lastIndex = 0;\n    let match = pattern.exec(template);\n    while (match) {\n      if (match.index > lastIndex) {\n        parts.push({ text: template.slice(lastIndex, match.index), wrap: false });\n      }\n      const key = match[1];\n      const replacement = replacements && replacements[key] !== undefined\n        ? String(replacements[key])\n        : match[0];\n      parts.push({ text: replacement, className: varClassName });\n      lastIndex = match.index + match[0].length;\n      match = pattern.exec(template);\n    }\n    if (lastIndex < template.length) {\n      parts.push({ text: template.slice(lastIndex), wrap: false });\n    }\n    return parts;\n  }\n\n  function groupTitleKey(group) {\n    const primary = pickPrimaryItem(group);\n    let labelSource = group && group.displayLabel;\n    if (!labelSource && primary) {\n      labelSource = primary.displayLabel;\n    }\n    const nameParts = splitLabel(labelSource);\n    const title = nameParts.title || \"\";\n    const subtitle = nameParts.subtitle || \"\";\n    return (title + \" \" + subtitle).trim().toLowerCase();\n  }\n\n  function groupCoveragePercent(group) {\n    const primary = pickPrimaryItem(group);\n    if (!primary || !primary.visibleGeometry || !primary.visibleGeometry.coverage) {\n      return 0;\n    }\n    const percent = primary.visibleGeometry.coverage.coveragePercent;\n    if (percent === undefined || percent === null || isNaN(percent)) {\n      return 0;\n    }\n    return Number(percent) || 0;\n  }\n\n  function buildBuildingModel(group) {\n    const primary = pickPrimaryItem(group);\n    let labelSource = group && group.displayLabel;\n    if (!labelSource && primary) {\n      labelSource = primary.displayLabel;\n    }\n    const explicitName = group && typeof group.label === \"string\" && group.label.trim()\n      ? group.label.trim()\n      : (primary && typeof primary.label === \"string\" && primary.label.trim() ? primary.label.trim() : \"\");\n    const nameParts = splitLabel(labelSource);\n    const hasSubtitle = !!(nameParts && typeof nameParts.subtitle === \"string\" && nameParts.subtitle.trim());\n    const isNamedForSummary = !!explicitName || hasSubtitle;\n    const visibleGeometry = primary && primary.visibleGeometry ? primary.visibleGeometry : null;\n    const coverage = visibleGeometry && visibleGeometry.coverage ? visibleGeometry.coverage : null;\n    const coverageLine = coverageBreakdown(coverage);\n    const location = locationPhrase(group, primary);\n    const edges = visibleGeometry && Array.isArray(visibleGeometry.edgesTouched)\n      ? visibleGeometry.edgesTouched\n      : [];\n    const touches = edgesText(edges, coverage);\n\n    const item = {\n      type: \"building\",\n      attrs: {\n        dataIsNamed: isNamedForSummary\n      },\n      lines: []\n    };\n    const scoreTooltip = importanceScoreTooltip(group && group.importanceScore ? group.importanceScore : null);\n    const titleLink = bestGroupExternalLink(group);\n    if (primary && primary.osmId !== undefined && primary.osmId !== null) {\n      item.attrs.dataOsmId = String(primary.osmId);\n    }\n    const localizedTitle = capitalizeFirst(localizeBuildingTitle(nameParts.title));\n    const titleParts = [{ text: localizedTitle, className: \"map-content-title\" }];\n    if (nameParts.subtitle) {\n      titleParts.push({\n        text: \" \" + t(\"map_content_building_join_at\", \"at\") + \" \",\n        className: \"map-content-title-sep\",\n        wrap: false\n      });\n      titleParts.push({ text: nameParts.subtitle, className: \"map-content-subtitle\" });\n    }\n    addModelLine(item, titleParts, \"map-content-title-line\", scoreTooltip, titleLink);\n\n    const primaryLocation = coverageLine || (location ? capitalizeFirst(location) : null);\n    const primaryLocationText = primaryLocation ? primaryLocation.replace(/[.]+$/, \"\") : null;\n    if (primaryLocationText) {\n      addModelLine(item, [\n        { text: primaryLocationText, className: \"map-content-location-text\" }\n      ], \"map-content-location\");\n    }\n    if (touches) {\n      addModelLine(item, [\n        { text: capitalizeFirst(touches), className: \"map-content-touches\" }\n      ], \"map-content-location\");\n    }\n\n    const components = visibleGeometry && Array.isArray(visibleGeometry.components)\n      ? visibleGeometry.components\n      : [];\n    const partsLine = [];\n    if (components && components.length > 1) {\n      partsLine.push.apply(partsLine, interpolatedParts(\n        t(\"map_content_parts_many\", \"__count__ parts\"),\n        { count: components.length },\n        \"map-content-parts-count\"\n      ));\n    }\n    if (coverage && coverage.coveragePercent !== undefined) {\n      const percentText = formatPercent(coverage.coveragePercent);\n      if (percentText !== null) {\n        if (partsLine.length) {\n          partsLine.push({ text: \", \", className: \"map-content-parts-sep\", wrap: false });\n        }\n        partsLine.push.apply(partsLine, interpolatedParts(\n          t(\"map_content_total_area\", \"Covers __percent__% of map\"),\n          { percent: percentText },\n          \"map-content-parts-coverage\"\n        ));\n      }\n    }\n    const shape = visibleGeometry && visibleGeometry.shape ? visibleGeometry.shape : null;\n    const shapeLineParts = [];\n    if (shape) {\n      const aspectRatio = Number(shape.aspectRatio);\n      const localizedOrientation = orientationLabel(shape.orientationLabel);\n      if (localizedOrientation && isFinite(aspectRatio) && aspectRatio > 2.0) {\n        shapeLineParts.push.apply(shapeLineParts, interpolatedParts(\n          t(\"map_content_orientation\", \"Orientation __label__\"),\n          { label: localizedOrientation },\n          \"map-content-shape-orientation\"\n        ));\n      }\n\n      if (shape.type && shape.type !== \"regular\") {\n        let descriptor = null;\n        if (aspectRatio >= 3.5) {\n          descriptor = t(\"map_content_shape_long_thin\", \"Very thin\");\n        } else if (aspectRatio >= 2.5) {\n          descriptor = t(\"map_content_shape_elongated\", \"Thin\");\n        }\n        if (descriptor) {\n          if (shapeLineParts.length) {\n            shapeLineParts.push({ text: \", \", className: \"map-content-shape-sep\", wrap: false });\n          }\n          shapeLineParts.push({\n            text: shapeLineParts.length ? lowercaseFirst(descriptor) : descriptor,\n            className: \"map-content-shape-aspect\"\n          });\n        }\n        const shapeType = shapeTypeLabel(shape.type);\n        if (shapeType) {\n          if (shapeLineParts.length) {\n            shapeLineParts.push({ text: \", \", className: \"map-content-shape-sep\", wrap: false });\n          }\n          shapeLineParts.push({\n            text: shapeLineParts.length ? lowercaseFirst(shapeType) : shapeType,\n            className: \"map-content-shape-type\"\n          });\n        }\n      }\n    }\n    if (partsLine.length) {\n      addModelLine(item, partsLine, \"map-content-parts\");\n    }\n    if (shapeLineParts.length) {\n      addModelLine(item, shapeLineParts, \"map-content-shape\");\n    }\n\n    return item;\n  }\n\n  function waterAreaCoveragePercent(entry) {\n    if (!entry || !entry.group) {\n      return 0;\n    }\n    return groupCoveragePercent(entry.group);\n  }\n\n  function waterAreaPrimaryLocationText(entry) {\n    if (!entry || !entry.group) {\n      return null;\n    }\n    const group = entry.group;\n    const primary = pickPrimaryItem(group);\n    const visibleGeometry = primary && primary.visibleGeometry ? primary.visibleGeometry : null;\n    const coverage = visibleGeometry && visibleGeometry.coverage ? visibleGeometry.coverage : null;\n    const coverageLine = coverageBreakdown(coverage);\n    const location = locationPhrase(group, primary);\n    const primaryLocation = coverageLine || (location ? capitalizeFirst(location) : null);\n    if (!primaryLocation || typeof primaryLocation !== \"string\") {\n      return null;\n    }\n    const trimmed = primaryLocation.replace(/[.]+$/, \"\").trim();\n    return trimmed || null;\n  }\n\n  function waterAreaPrimaryLocationKey(text) {\n    if (!text || typeof text !== \"string\") {\n      return null;\n    }\n    const trimmed = text.trim();\n    return trimmed ? trimmed.toLowerCase() : null;\n  }\n\n  function mergeWaterAreaEdgesTouched(entries) {\n    const byEdge = {};\n    const orderedEdges = [\"north\", \"south\", \"east\", \"west\"];\n    entries.forEach(function(entry){\n      const group = entry && entry.group ? entry.group : null;\n      const primary = pickPrimaryItem(group);\n      const visibleGeometry = primary && primary.visibleGeometry ? primary.visibleGeometry : null;\n      const edges = visibleGeometry && Array.isArray(visibleGeometry.edgesTouched)\n        ? visibleGeometry.edgesTouched\n        : [];\n      edges.forEach(function(edgeInfo){\n        if (!edgeInfo || typeof edgeInfo !== \"object\") {\n          return;\n        }\n        Object.keys(edgeInfo).forEach(function(edge){\n          const rawValue = edgeInfo[edge];\n          const numValue = Number(rawValue);\n          if (!isFinite(numValue)) {\n            if (!Object.prototype.hasOwnProperty.call(byEdge, edge)) {\n              byEdge[edge] = null;\n            }\n            return;\n          }\n          if (!Object.prototype.hasOwnProperty.call(byEdge, edge) || byEdge[edge] === null || numValue > byEdge[edge]) {\n            byEdge[edge] = numValue;\n          }\n        });\n      });\n    });\n    return orderedEdges\n      .filter(function(edge){ return Object.prototype.hasOwnProperty.call(byEdge, edge); })\n      .map(function(edge){\n        const out = {};\n        out[edge] = byEdge[edge];\n        return out;\n      });\n  }\n\n  function buildWaterAreaModel(entry) {\n    if (!entry || !entry.group) {\n      return null;\n    }\n    const group = entry.group;\n    const primary = pickPrimaryItem(group);\n    const visibleGeometry = primary && primary.visibleGeometry ? primary.visibleGeometry : null;\n    const coverage = visibleGeometry && visibleGeometry.coverage ? visibleGeometry.coverage : null;\n    const coverageLine = coverageBreakdown(coverage);\n    const location = locationPhrase(group, primary);\n    const edges = visibleGeometry && Array.isArray(visibleGeometry.edgesTouched)\n      ? visibleGeometry.edgesTouched\n      : [];\n    const touches = edgesText(edges, coverage);\n    const name = waterAreaEntryName(entry);\n    const typeText = waterAreaTypeLabel(entry.subClass);\n    const isNamed = !!name;\n\n    const item = {\n      type: \"water_area\",\n      attrs: {\n        dataIsNamed: isNamed\n      },\n      lines: []\n    };\n    const scoreTooltip = importanceScoreTooltip(group && group.importanceScore ? group.importanceScore : null);\n    const titleLink = bestGroupExternalLink(group);\n    if (primary && primary.osmId !== undefined && primary.osmId !== null) {\n      item.attrs.dataOsmId = String(primary.osmId);\n    }\n\n    let titleText = \"\";\n    if (isNamed) {\n      titleText = typeText + \" \" + name;\n    } else {\n      titleText = interpolate(\n        t(\"map_content_water_area_unnamed\", \"Unnamed __type__\"),\n        { type: typeText }\n      );\n    }\n    addModelLine(item, [\n      { text: capitalizeFirst(titleText), className: \"map-content-title\" }\n    ], \"map-content-title-line\", scoreTooltip, titleLink);\n\n    const primaryLocation = coverageLine || (location ? capitalizeFirst(location) : null);\n    const primaryLocationText = primaryLocation ? primaryLocation.replace(/[.]+$/, \"\") : null;\n    if (primaryLocationText) {\n      addModelLine(item, [\n        { text: primaryLocationText, className: \"map-content-location-text\" }\n      ], \"map-content-location\");\n    }\n    if (touches) {\n      addModelLine(item, [\n        { text: capitalizeFirst(touches), className: \"map-content-touches\" }\n      ], \"map-content-location\");\n    }\n\n    if (coverage && coverage.coveragePercent !== undefined) {\n      const percentText = formatPercent(coverage.coveragePercent);\n      if (percentText !== null) {\n        addModelLine(item, interpolatedParts(\n          t(\"map_content_total_area\", \"Covers __percent__% of map\"),\n          { percent: percentText },\n          \"map-content-parts-coverage\"\n        ), \"map-content-parts\");\n      }\n    }\n    return item;\n  }\n\n  function buildMergedUnnamedWaterAreaModel(summary) {\n    if (!summary || !summary.count || summary.count < 2) {\n      return null;\n    }\n    const item = {\n      type: \"water_area_summary\",\n      attrs: {\n        dataIsNamed: false\n      },\n      lines: []\n    };\n    const key = summary.count === 1 ? \"map_content_unnamed_features_one\" : \"map_content_unnamed_features_many\";\n    const typeText = summary.count === 1\n      ? t(\"map_content_water_area_type_generic\", \"water area\")\n      : waterAreaTypeLabelPlural();\n    const titleText = interpolate(\n      t(key, \"__count__ unnamed __type__\"),\n      { count: summary.count, type: typeText }\n    );\n    addModelLine(item, [\n      { text: capitalizeFirst(titleText), className: \"map-content-title\" }\n    ], \"map-content-title-line\");\n\n    if (summary.locationText) {\n      addModelLine(item, [\n        { text: capitalizeFirst(summary.locationText), className: \"map-content-location-text\" }\n      ], \"map-content-location\");\n    }\n\n    const touches = edgesText(summary.edgesTouched, null);\n    if (touches) {\n      addModelLine(item, [\n        { text: capitalizeFirst(touches), className: \"map-content-touches\" }\n      ], \"map-content-location\");\n    }\n\n    const percentText = formatPercent(summary.coveragePercent);\n    if (percentText !== null) {\n      addModelLine(item, interpolatedParts(\n        t(\"map_content_total_area\", \"Covers __percent__% of map\"),\n        { percent: percentText },\n        \"map-content-parts-coverage\"\n      ), \"map-content-parts\");\n    }\n    return item;\n  }\n\n  function buildWaterAreasModel(mapContent) {\n    const entries = collectWaterAreaEntries(mapContent);\n    const namedEntries = [];\n    const unnamedEntries = [];\n    const model = [];\n    entries.forEach(function(entry){\n      if (waterAreaEntryIsNamed(entry)) {\n        namedEntries.push(entry);\n      } else {\n        unnamedEntries.push(entry);\n      }\n    });\n\n    namedEntries.forEach(function(entry){\n      const item = buildWaterAreaModel(entry);\n      if (item) {\n        model.push(item);\n      }\n    });\n\n    const unnamedBucketsByKey = {};\n    const unnamedKeyByIndex = {};\n    unnamedEntries.forEach(function(entry, index){\n      const locationText = waterAreaPrimaryLocationText(entry);\n      const locationKey = waterAreaPrimaryLocationKey(locationText);\n      if (!locationKey) {\n        return;\n      }\n      unnamedKeyByIndex[index] = locationKey;\n      if (!unnamedBucketsByKey[locationKey]) {\n        unnamedBucketsByKey[locationKey] = {\n          count: 0,\n          coveragePercent: 0,\n          locationText: locationText,\n          firstIndex: index,\n          entries: []\n        };\n      }\n      const bucket = unnamedBucketsByKey[locationKey];\n      bucket.count += 1;\n      bucket.coveragePercent += waterAreaCoveragePercent(entry);\n      bucket.entries.push(entry);\n      if (index < bucket.firstIndex) {\n        bucket.firstIndex = index;\n      }\n    });\n\n    unnamedEntries.forEach(function(entry, index){\n      const locationKey = unnamedKeyByIndex[index];\n      if (locationKey) {\n        const bucket = unnamedBucketsByKey[locationKey];\n        if (bucket && bucket.count > 1) {\n          if (bucket.firstIndex === index) {\n            bucket.edgesTouched = mergeWaterAreaEdgesTouched(bucket.entries);\n            const mergedItem = buildMergedUnnamedWaterAreaModel(bucket);\n            if (mergedItem) {\n              model.push(mergedItem);\n            }\n          }\n          return;\n        }\n      }\n      const item = buildWaterAreaModel(entry);\n      if (item) {\n        model.push(item);\n      }\n    });\n\n    return model;\n  }\n\n  function buildModel(mapContent, helpers, options) {\n    setTranslator(helpers);\n    const resolved = normalizeOptions(options);\n    if (resolved.section === \"water_areas\") {\n      return buildWaterAreasModel(mapContent);\n    }\n    const groups = collectBuildingGroups(mapContent);\n    const model = [];\n    groups.forEach(function(group){\n      const item = buildBuildingModel(group);\n      if (item) {\n        model.push(item);\n      }\n    });\n    return model;\n  }\n\n  function renderFromModel(model, listElem) {\n    if (!listElem || !listElem.length) {\n      return 0;\n    }\n    const items = Array.isArray(model) ? model : [];\n    listElem.empty();\n    items.forEach(function(item){\n      renderItemFromModel(item, listElem);\n    });\n    return items.length;\n  }\n\n  function render(mapContent, listElem, helpers, options) {\n    if (!listElem || !listElem.length) {\n      return 0;\n    }\n    const model = buildModel(mapContent, helpers, options);\n    return renderFromModel(model, listElem);\n  }\n\n  function emptyMessage(helpers, options) {\n    setTranslator(helpers);\n    const resolved = normalizeOptions(options);\n    if (resolved.section === \"water_areas\") {\n      return t(\"map_content_no_water_areas\", \"No water areas listed for this map.\");\n    }\n    return t(\"map_content_no_buildings\", \"No buildings listed for this map.\");\n  }\n\n  window.TM = window.TM || {};\n  window.TM.mapDescAreas = {\n    buildModel: buildModel,\n    renderFromModel: renderFromModel,\n    render: render,\n    emptyMessage: emptyMessage\n  };\n})();\n"
  },
  {
    "path": "web/src/scripts/map-desc-pois.js",
    "content": "/* global $ */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0, space-infix-ops:0, camelcase:0, dot-notation:0 */\n\n(function(){\n  'use strict';\n\n  const TRANSPORT_TYPES = {\n    \"station\": true,\n    \"stop position\": true,\n    \"platform\": true,\n    \"stop area\": true,\n    \"stop area group\": true,\n    \"halt\": true,\n    \"tram stop\": true,\n    \"subway entrance\": true,\n    \"bus station\": true,\n    \"ferry terminal\": true,\n    \"taxi\": true\n  };\n\n  const DAILY_TYPES = {\n    \"pharmacy\": true,\n    \"hospital\": true,\n    \"clinic\": true,\n    \"doctors\": true,\n    \"toilets\": true,\n    \"post office\": true,\n    \"bank\": true,\n    \"atm\": true,\n    \"fuel\": true,\n    \"supermarket\": true,\n    \"convenience\": true,\n    \"kiosk\": true,\n    \"restaurant\": true,\n    \"cafe\": true,\n    \"fast food\": true,\n    \"bar\": true,\n    \"pub\": true,\n    \"marketplace\": true,\n    \"mall\": true,\n    \"department store\": true\n  };\n\n  const FAMILIAR_TYPES = {\n    \"library\": true,\n    \"place of worship\": true,\n    \"townhall\": true,\n    \"courthouse\": true,\n    \"community centre\": true,\n    \"university\": true,\n    \"college\": true,\n    \"museum\": true,\n    \"gallery\": true,\n    \"attraction\": true,\n    \"viewpoint\": true,\n    \"zoo\": true,\n    \"theme park\": true,\n    \"aquarium\": true,\n    \"park\": true,\n    \"arts centre\": true,\n    \"mall\": true,\n    \"department store\": true\n  };\n\n  const LOW_SALIENCE_TYPES = {\n    \"bench\": true,\n    \"waste basket\": true,\n    \"vending machine\": true,\n    \"bicycle parking\": true,\n    \"street lamp\": true,\n    \"outdoor seating\": true\n  };\n  const EXTERNAL_LINK_TYPE_PRIORITY = {\n    wikipedia: 0,\n    wikidata: 1,\n    commons: 2,\n    website: 3,\n    search: 4\n  };\n\n  function fallbackTranslate(key, fallback) {\n    return fallback !== undefined ? fallback : key;\n  }\n\n  let translate = fallbackTranslate;\n\n  function setTranslator(helpers) {\n    translate = helpers && typeof helpers.t === 'function' ? helpers.t : fallbackTranslate;\n  }\n\n  function t(key, fallback) {\n    return translate(key, fallback);\n  }\n\n  function interpolate(text, replacements) {\n    if (!replacements) {\n      return text;\n    }\n    return text.replace(/__([a-zA-Z0-9_]+)__/g, function(match, name) {\n      if (replacements[name] === undefined) {\n        return match;\n      }\n      return replacements[name];\n    });\n  }\n\n  function capitalizeFirst(text) {\n    if (!text || typeof text !== 'string') {\n      return text;\n    }\n    return text.charAt(0).toUpperCase() + text.slice(1);\n  }\n\n  function slugifyOsmValue(value) {\n    if (!value || typeof value !== 'string') {\n      return \"\";\n    }\n    const normalized = value.normalize\n      ? value.normalize(\"NFKD\").replace(/[\\u0300-\\u036f]/g, \"\")\n      : value;\n    return normalized\n      .toLowerCase()\n      .replace(/[^a-z0-9]+/g, \"_\")\n      .replace(/^_+|_+$/g, \"\");\n  }\n\n  function translatedOsmValue(value) {\n    const slug = slugifyOsmValue(value);\n    if (!slug) {\n      return null;\n    }\n    const key = \"map_content_osm_value_\" + slug;\n    const translated = t(key, key);\n    if (translated === key) {\n      return null;\n    }\n    return translated;\n  }\n\n  function normalizeTypeLabel(typeLabel) {\n    if (!typeLabel || typeof typeLabel !== 'string') {\n      return t(\"map_content_poi_unknown_type\", \"Unknown place type\");\n    }\n    const trimmed = typeLabel.trim();\n    if (!trimmed || trimmed.toLowerCase() === \"poi\") {\n      return t(\"map_content_poi_unknown_type\", \"Unknown place type\");\n    }\n    const translated = translatedOsmValue(trimmed);\n    return translated || trimmed;\n  }\n\n  function directionLabel(direction) {\n    if (direction === \"north\") return t(\"map_content_dir_north\", \"north\");\n    if (direction === \"northeast\") return t(\"map_content_dir_northeast\", \"north-east\");\n    if (direction === \"east\") return t(\"map_content_dir_east\", \"east\");\n    if (direction === \"southeast\") return t(\"map_content_dir_southeast\", \"south-east\");\n    if (direction === \"south\") return t(\"map_content_dir_south\", \"south\");\n    if (direction === \"southwest\") return t(\"map_content_dir_southwest\", \"south-west\");\n    if (direction === \"west\") return t(\"map_content_dir_west\", \"west\");\n    if (direction === \"northwest\") return t(\"map_content_dir_northwest\", \"north-west\");\n    return direction;\n  }\n\n  function cornerLabel(direction) {\n    if (direction === \"northwest\") return t(\"map_content_corner_northwest\", \"north-west corner\");\n    if (direction === \"northeast\") return t(\"map_content_corner_northeast\", \"north-east corner\");\n    if (direction === \"southwest\") return t(\"map_content_corner_southwest\", \"south-west corner\");\n    if (direction === \"southeast\") return t(\"map_content_corner_southeast\", \"south-east corner\");\n    return direction;\n  }\n\n  function locationPhraseFromLoc(loc, form) {\n    if (!loc || typeof loc !== 'object') {\n      return null;\n    }\n    const phraseForm = form || \"clause\";\n    const kind = loc.kind;\n    const dirLabel = loc.dir ? directionLabel(loc.dir) : null;\n    const corner = loc.dir ? cornerLabel(loc.dir) : null;\n\n    if (kind === \"center\") {\n      if (phraseForm === \"endpoint\") {\n        return t(\"map_content_loc_endpoint_center\", \"the center\");\n      }\n      return t(\"map_content_loc_full_center\", \"in the center\");\n    }\n    if (kind === \"part\") {\n      if (!dirLabel) {\n        return t(\"map_content_loc_full_center\", \"in the center\");\n      }\n      if (phraseForm === \"endpoint\") {\n        return interpolate(t(\"map_content_loc_endpoint_part\", \"the __dir__ part\"), { dir: dirLabel });\n      }\n      return interpolate(t(\"map_content_loc_full_part\", \"in the __dir__ part\"), { dir: dirLabel });\n    }\n    if (kind === \"near_edge\") {\n      if (loc.dir === \"northwest\" || loc.dir === \"northeast\" ||\n          loc.dir === \"southwest\" || loc.dir === \"southeast\") {\n        if (!corner) {\n          return null;\n        }\n        if (phraseForm === \"endpoint\") {\n          return interpolate(t(\"map_content_loc_endpoint_corner\", \"the __corner__\"), { corner: corner });\n        }\n        return interpolate(t(\"map_content_loc_full_near_corner\", \"in the __corner__\"), { corner: corner });\n      }\n      if (!dirLabel) {\n        return null;\n      }\n      const key = phraseForm === \"endpoint\"\n        ? \"map_content_loc_endpoint_near_edge\"\n        : \"map_content_loc_full_near_edge\";\n      return interpolate(t(key, \"near the __dir__ edge\"), { dir: dirLabel });\n    }\n    return null;\n  }\n\n  function locationTextFromValue(value, form) {\n    if (value && typeof value === 'object') {\n      const loc = value.loc && typeof value.loc === 'object' ? value.loc : value;\n      return locationPhraseFromLoc(loc, form);\n    }\n    return null;\n  }\n\n  function locationValue(entry) {\n    const group = entry && entry.group ? entry.group : null;\n    const item = entry && entry.item ? entry.item : null;\n    if (item && item.location && item.location.point) {\n      return item.location.point;\n    }\n    if (group && group.location && group.location.point) {\n      return group.location.point;\n    }\n    if (item && item.location && item.location.center) {\n      return item.location.center;\n    }\n    if (group && group.location && group.location.center) {\n      return group.location.center;\n    }\n    return null;\n  }\n\n  function locationKey(entry) {\n    const value = locationValue(entry);\n    if (!value || !value.loc) {\n      return \"\";\n    }\n    const kind = value.loc.kind || \"\";\n    const dir = value.loc.dir || \"\";\n    return String(kind) + \":\" + String(dir);\n  }\n\n  function locationRank(entry) {\n    const value = locationValue(entry);\n    const loc = value && value.loc ? value.loc : null;\n    if (!loc) {\n      return 0;\n    }\n    if (loc.kind === \"center\") {\n      return 3;\n    }\n    if (loc.kind === \"part\") {\n      return 2;\n    }\n    if (loc.kind === \"near_edge\") {\n      return 1;\n    }\n    return 0;\n  }\n\n  function parseTypeAndName(group, item) {\n    const displayLabel = group && typeof group.displayLabel === 'string'\n      ? group.displayLabel\n      : (item && typeof item.displayLabel === 'string' ? item.displayLabel : \"\");\n    const label = group && typeof group.label === 'string'\n      ? group.label\n      : (item && typeof item.label === 'string' ? item.label : null);\n    const parts = displayLabel ? displayLabel.split(\":\") : [];\n    if (parts.length >= 2) {\n      return {\n        typeLabel: parts[0].trim(),\n        name: label ? label.trim() : parts.slice(1).join(\":\").trim()\n      };\n    }\n    return {\n      typeLabel: displayLabel ? displayLabel.trim() : \"\",\n      name: label ? label.trim() : null\n    };\n  }\n\n  function normalizedLower(value) {\n    if (!value || typeof value !== 'string') {\n      return \"\";\n    }\n    return value.trim().toLowerCase();\n  }\n\n  function hasMeaningfulPoiTypeLabel(typeLabel) {\n    if (!typeLabel || typeof typeLabel !== \"string\") {\n      return false;\n    }\n    const trimmed = typeLabel.trim();\n    if (!trimmed) {\n      return false;\n    }\n    const normalized = trimmed.toLowerCase();\n    if (normalized === \"poi\" || normalized === \"unnamed\" || normalized === \"(unnamed)\") {\n      return false;\n    }\n    return true;\n  }\n\n  function sectionForEntry(entry) {\n    const typeNorm = normalizedLower(entry.typeLabel);\n    if (TRANSPORT_TYPES[typeNorm]) {\n      return \"transport_points\";\n    }\n    if (DAILY_TYPES[typeNorm]) {\n      return \"daily_essentials\";\n    }\n    if (FAMILIAR_TYPES[typeNorm]) {\n      return \"familiar_places\";\n    }\n    if (entry.subClass === \"D1_transport\") {\n      return \"transport_points\";\n    }\n    if (entry.subClass === \"D3_commercial\") {\n      return \"daily_essentials\";\n    }\n    if (entry.subClass === \"D2_civic\" || entry.subClass === \"D4_leisure_cultural\") {\n      return \"familiar_places\";\n    }\n    if (entry.hasName) {\n      return \"familiar_places\";\n    }\n    return \"daily_essentials\";\n  }\n\n  function salienceScore(entry) {\n    let score = 0;\n    if (entry.subClass === \"D1_transport\") score += 80;\n    if (entry.subClass === \"D2_civic\") score += 90;\n    if (entry.subClass === \"D3_commercial\") score += 65;\n    if (entry.subClass === \"D4_leisure_cultural\") score += 85;\n    if (entry.hasName) score += 18;\n    const typeNorm = normalizedLower(entry.typeLabel);\n    if (FAMILIAR_TYPES[typeNorm]) score += 26;\n    if (DAILY_TYPES[typeNorm]) score += 18;\n    if (TRANSPORT_TYPES[typeNorm]) score += 24;\n    if (LOW_SALIENCE_TYPES[typeNorm]) score -= 30;\n    score += locationRank(entry) * 3;\n    return score;\n  }\n\n  function listItems(group) {\n    if (!group) {\n      return [];\n    }\n    if (Array.isArray(group.items)) {\n      return group.items;\n    }\n    if (Array.isArray(group.ways)) {\n      return group.ways;\n    }\n    return [];\n  }\n\n  function isSafeExternalUrl(url) {\n    return typeof url === \"string\" && /^https?:\\/\\/\\S+$/i.test(url);\n  }\n\n  function isGoogleSearchExternalLink(link) {\n    if (!link || typeof link !== \"object\") {\n      return false;\n    }\n    if (typeof link.label !== \"string\" || link.label.trim().toLowerCase() !== \"search\") {\n      return false;\n    }\n    if (typeof link.url !== \"string\") {\n      return false;\n    }\n    try {\n      const parsed = new URL(link.url);\n      const host = parsed.hostname.toLowerCase();\n      if (!/^([a-z0-9-]+\\.)*google\\./.test(host)) {\n        return false;\n      }\n      return parsed.pathname === \"/search\";\n    } catch (_error) {\n      return false;\n    }\n  }\n\n  function normalizedExternalLink(link) {\n    if (!link || typeof link !== \"object\") {\n      return null;\n    }\n    if (!isSafeExternalUrl(link.url)) {\n      return null;\n    }\n    if (isGoogleSearchExternalLink(link)) {\n      return null;\n    }\n    return {\n      type: typeof link.type === \"string\" ? link.type : \"\",\n      url: link.url,\n      label: typeof link.label === \"string\" ? link.label : \"\"\n    };\n  }\n\n  function externalLinkPriority(link) {\n    if (!link || !link.type || EXTERNAL_LINK_TYPE_PRIORITY[link.type] === undefined) {\n      return Number.MAX_SAFE_INTEGER;\n    }\n    return EXTERNAL_LINK_TYPE_PRIORITY[link.type];\n  }\n\n  function bestGroupExternalLink(group) {\n    const items = listItems(group);\n    let best = null;\n    let bestPriority = Number.MAX_SAFE_INTEGER;\n    items.forEach(function(item){\n      if (!item || typeof item !== \"object\") {\n        return;\n      }\n      const candidate = normalizedExternalLink(item.externalLink);\n      if (!candidate) {\n        return;\n      }\n      const priority = externalLinkPriority(candidate);\n      if (!best || priority < bestPriority) {\n        best = candidate;\n        bestPriority = priority;\n      }\n    });\n    return best;\n  }\n\n  function groupImportanceScore(group) {\n    const value = group ? group.importanceScore : null;\n    const resolved = value && typeof value === \"object\" ? value.final : value;\n    if (resolved === null || resolved === undefined || isNaN(resolved)) {\n      return 0;\n    }\n    const number = Number(resolved);\n    if (!isFinite(number)) {\n      return 0;\n    }\n    return number;\n  }\n\n  function importanceScoreTooltip(importanceScore) {\n    if (!importanceScore || typeof importanceScore !== \"object\" || Array.isArray(importanceScore)) {\n      return null;\n    }\n    try {\n      return JSON.stringify(importanceScore, null, 2);\n    } catch (_error) {\n      return null;\n    }\n  }\n\n  function collectPoiEntries(mapContent) {\n    const output = [];\n    const classD = mapContent && typeof mapContent === \"object\" ? mapContent.D : null;\n    const subclasses = classD && Array.isArray(classD.subclasses) ? classD.subclasses : [];\n    subclasses.forEach(function(subclass){\n      if (!subclass || subclass.kind !== \"poi\" || !Array.isArray(subclass.groups)) {\n        return;\n      }\n      subclass.groups.forEach(function(group){\n        const items = listItems(group);\n        if (!items.length) {\n          return;\n        }\n        const item = items[0];\n        const parsed = parseTypeAndName(group, item);\n        const name = parsed.name && parsed.name.trim() ? parsed.name.trim() : null;\n        const typeLabel = parsed.typeLabel && parsed.typeLabel.trim() ? parsed.typeLabel.trim() : \"\";\n        const hasTypeLabel = hasMeaningfulPoiTypeLabel(typeLabel);\n        if (!name && !hasTypeLabel) {\n          return;\n        }\n        const entry = {\n          subClass: subclass.key,\n          group: group,\n          item: item,\n          typeLabel: typeLabel,\n          name: name,\n          hasName: !!name,\n          itemCount: items.length,\n          importanceScore: groupImportanceScore(group)\n        };\n        entry.section = sectionForEntry(entry);\n        entry.score = salienceScore(entry);\n        output.push(entry);\n      });\n    });\n\n    const deduped = {};\n    output.forEach(function(entry){\n      const typeNorm = normalizedLower(entry.typeLabel || \"poi\");\n      const nameNorm = normalizedLower(entry.name || \"\");\n      const key = entry.section + \"|\" + typeNorm + \"|\" + nameNorm + \"|\" + locationKey(entry);\n      if (!deduped[key]) {\n        deduped[key] = entry;\n        return;\n      }\n      deduped[key].itemCount += entry.itemCount;\n      if (entry.importanceScore > deduped[key].importanceScore) {\n        deduped[key] = entry;\n      } else if (entry.importanceScore === deduped[key].importanceScore && entry.score > deduped[key].score) {\n        deduped[key] = entry;\n      }\n    });\n\n    return Object.keys(deduped).map(function(key){ return deduped[key]; });\n  }\n\n  function sortEntries(entries) {\n    return entries.slice().sort(function(a, b){\n      if (a.hasName !== b.hasName) {\n        return a.hasName ? -1 : 1;\n      }\n      if (a.importanceScore !== b.importanceScore) {\n        return b.importanceScore - a.importanceScore;\n      }\n      if (a.score !== b.score) {\n        return b.score - a.score;\n      }\n      const locDiff = locationRank(b) - locationRank(a);\n      if (locDiff !== 0) {\n        return locDiff;\n      }\n      const aType = normalizeTypeLabel(a.typeLabel).toLowerCase();\n      const bType = normalizeTypeLabel(b.typeLabel).toLowerCase();\n      if (aType !== bType) {\n        return aType.localeCompare(bType);\n      }\n      const aName = (a.name || \"\").toLowerCase();\n      const bName = (b.name || \"\").toLowerCase();\n      return aName.localeCompare(bName);\n    });\n  }\n\n  function unnamedPoiLocationText(entry) {\n    const location = locationTextFromValue(locationValue(entry), \"clause\");\n    if (!location || typeof location !== \"string\") {\n      return null;\n    }\n    const trimmed = location.trim();\n    return trimmed || null;\n  }\n\n  function unnamedPoiLocationKey(locationText) {\n    if (!locationText || typeof locationText !== \"string\") {\n      return null;\n    }\n    const trimmed = locationText.trim();\n    return trimmed ? trimmed.toLowerCase() : null;\n  }\n\n  function unnamedPoiTypeKey(entry) {\n    if (!entry || entry.hasName) {\n      return null;\n    }\n    const normalizedType = normalizeTypeLabel(entry.typeLabel);\n    const lowered = normalizedLower(normalizedType);\n    return lowered || null;\n  }\n\n  function shouldReplaceUnnamedPoiRepresentative(candidate, current) {\n    if (!current) {\n      return true;\n    }\n    if ((candidate.importanceScore || 0) !== (current.importanceScore || 0)) {\n      return (candidate.importanceScore || 0) > (current.importanceScore || 0);\n    }\n    if ((candidate.score || 0) !== (current.score || 0)) {\n      return (candidate.score || 0) > (current.score || 0);\n    }\n    return locationRank(candidate) > locationRank(current);\n  }\n\n  function unnamedPoiEntryCount(entry) {\n    const entryCount = Number(entry && entry.itemCount);\n    return isFinite(entryCount) && entryCount > 0 ? entryCount : 1;\n  }\n\n  function sortedUnnamedPoiLocationBuckets(locationBucketsByKey) {\n    const buckets = Object.keys(locationBucketsByKey || {}).map(function(key){\n      return locationBucketsByKey[key];\n    }).filter(function(bucket){\n      return !!(bucket && bucket.locationText);\n    });\n    buckets.sort(function(a, b){\n      if (a.count !== b.count) {\n        return b.count - a.count;\n      }\n      if (a.locationRank !== b.locationRank) {\n        return b.locationRank - a.locationRank;\n      }\n      return a.firstIndex - b.firstIndex;\n    });\n    return buckets;\n  }\n\n  function mergedUnnamedPoiLocationSummary(bucket) {\n    if (!bucket || !bucket.locationBucketsByKey) {\n      return null;\n    }\n    const locationBuckets = sortedUnnamedPoiLocationBuckets(bucket.locationBucketsByKey);\n    if (!locationBuckets.length) {\n      return null;\n    }\n    const orderedLocationClauses = locationBuckets.map(function(locationBucket){\n      return locationBucket.locationText;\n    });\n    let text = null;\n    if (orderedLocationClauses.length === 1) {\n      text = capitalizeFirst(orderedLocationClauses[0]);\n    } else if (orderedLocationClauses.length === 2) {\n      text = interpolate(\n        t(\"map_content_summary_distributed_top2\", \"In several areas, mostly __first__ and __second__.\"),\n        { first: orderedLocationClauses[0], second: orderedLocationClauses[1] }\n      );\n    } else {\n      text = interpolate(\n        t(\"map_content_summary_three_regions_clauses\", \"Mostly __first__, __second__, and __third__.\"),\n        { first: orderedLocationClauses[0], second: orderedLocationClauses[1], third: orderedLocationClauses[2] }\n      );\n    }\n    return {\n      distinctLocationCount: orderedLocationClauses.length,\n      orderedLocationClauses: orderedLocationClauses,\n      text: text\n    };\n  }\n\n  function mergeUnnamedEntriesByTypeAndLocation(entries) {\n    const bucketsByTypeKey = {};\n    const typeKeyByIndex = {};\n\n    entries.forEach(function(entry, index){\n      if (!entry || entry.hasName) {\n        return;\n      }\n      const typeKey = unnamedPoiTypeKey(entry);\n      const locationText = unnamedPoiLocationText(entry);\n      const locationKey = unnamedPoiLocationKey(locationText);\n      if (!typeKey || !locationKey) {\n        return;\n      }\n      typeKeyByIndex[index] = typeKey;\n      if (!bucketsByTypeKey[typeKey]) {\n        bucketsByTypeKey[typeKey] = {\n          count: 0,\n          firstIndex: index,\n          representative: entry,\n          locationBucketsByKey: {}\n        };\n      }\n      const bucket = bucketsByTypeKey[typeKey];\n      const entryCount = unnamedPoiEntryCount(entry);\n      bucket.count += entryCount;\n      if (index < bucket.firstIndex) {\n        bucket.firstIndex = index;\n      }\n      if (shouldReplaceUnnamedPoiRepresentative(entry, bucket.representative)) {\n        bucket.representative = entry;\n      }\n\n      if (!bucket.locationBucketsByKey[locationKey]) {\n        bucket.locationBucketsByKey[locationKey] = {\n          count: 0,\n          locationText: locationText,\n          locationRank: locationRank(entry),\n          firstIndex: index\n        };\n      }\n      const locationBucket = bucket.locationBucketsByKey[locationKey];\n      locationBucket.count += entryCount;\n      if (!locationBucket.locationText && locationText) {\n        locationBucket.locationText = locationText;\n      }\n      if (locationRank(entry) > locationBucket.locationRank) {\n        locationBucket.locationRank = locationRank(entry);\n      }\n      if (index < locationBucket.firstIndex) {\n        locationBucket.firstIndex = index;\n      }\n    });\n\n    const merged = [];\n    entries.forEach(function(entry, index){\n      const typeKey = typeKeyByIndex[index];\n      if (typeKey) {\n        const bucket = bucketsByTypeKey[typeKey];\n        if (bucket && bucket.count > 1) {\n          if (bucket.firstIndex === index && bucket.representative) {\n            const locationSummary = mergedUnnamedPoiLocationSummary(bucket);\n            const mergedEntry = Object.assign({}, bucket.representative, {\n              hasName: false,\n              itemCount: bucket.count,\n              mergedLocationSummary: locationSummary\n            });\n            merged.push(mergedEntry);\n          }\n          return;\n        }\n      }\n      merged.push(entry);\n    });\n\n    return merged;\n  }\n\n  function lineModel(text, className, titleText, link) {\n    return {\n      className: className || null,\n      parts: [{ text: text }],\n      title: titleText || null,\n      link: link || null\n    };\n  }\n\n  function entryToModelItem(entry) {\n    const normalizedType = normalizeTypeLabel(entry.typeLabel);\n    const location = locationTextFromValue(locationValue(entry), \"clause\");\n    const mergedLocationSummary = entry && entry.mergedLocationSummary &&\n      typeof entry.mergedLocationSummary === \"object\"\n      ? entry.mergedLocationSummary\n      : null;\n    const mergedLocationText = mergedLocationSummary && typeof mergedLocationSummary.text === \"string\"\n      ? mergedLocationSummary.text.trim()\n      : \"\";\n    const locationLineText = mergedLocationText || location;\n    const scoreTooltip = importanceScoreTooltip(entry && entry.group ? entry.group.importanceScore : null);\n    const titleLink = bestGroupExternalLink(entry && entry.group ? entry.group : null);\n    const lines = [];\n    if (entry.hasName) {\n      lines.push(lineModel(entry.name + \", \" + normalizedType, \"map-content-title-line\", scoreTooltip, titleLink));\n    } else if (entry.itemCount > 1) {\n      lines.push(lineModel(interpolate(\n        t(\"map_content_poi_unnamed_many\", \"__count__ unnamed __type__\"),\n        { count: entry.itemCount, type: normalizedType }\n      ), \"map-content-title-line\", scoreTooltip, titleLink));\n    } else {\n      lines.push(lineModel(interpolate(\n        t(\"map_content_poi_unnamed\", \"Unnamed __type__\"),\n        { type: normalizedType }\n      ), \"map-content-title-line\", scoreTooltip, titleLink));\n    }\n    if (locationLineText) {\n      lines.push(lineModel(capitalizeFirst(locationLineText), \"map-content-location\"));\n    }\n    return {\n      type: \"poi\",\n      attrs: {\n        dataOsmId: entry.item && entry.item.osmId !== undefined && entry.item.osmId !== null\n          ? String(entry.item.osmId)\n          : null,\n        dataIsNamed: !!entry.hasName\n      },\n      lines: lines\n    };\n  }\n\n  function collectAllFeatureItems(mapContent) {\n    const items = [];\n    if (!mapContent || typeof mapContent !== \"object\") {\n      return items;\n    }\n    Object.keys(mapContent).forEach(function(mainKey){\n      const mainEntry = mapContent[mainKey];\n      if (!mainEntry || !Array.isArray(mainEntry.subclasses)) {\n        return;\n      }\n      mainEntry.subclasses.forEach(function(subclass){\n        if (!subclass || !Array.isArray(subclass.groups)) {\n          return;\n        }\n        subclass.groups.forEach(function(group){\n          listItems(group).forEach(function(item){\n            if (!item || typeof item !== \"object\") {\n              return;\n            }\n            items.push({\n              mainKey: mainKey,\n              subClass: subclass.key,\n              group: group,\n              item: item,\n              semantics: item.semantics || null\n            });\n          });\n        });\n      });\n    });\n    return items;\n  }\n\n  function featureLabel(feature) {\n    const item = feature && feature.item ? feature.item : null;\n    if (!item) {\n      return null;\n    }\n    const name = typeof item.label === \"string\" && item.label.trim() ? item.label.trim() : null;\n    if (name) {\n      if (name === \"(unnamed)\" || /\\(unnamed\\)/i.test(name)) {\n        return null;\n      }\n      return name;\n    }\n    if (typeof item.displayLabel === \"string\" && item.displayLabel.trim()) {\n      const display = item.displayLabel.trim();\n      if (display === \"(unnamed)\" || /\\(unnamed\\)/i.test(display)) {\n        return null;\n      }\n      return display;\n    }\n    return null;\n  }\n\n  function featureLocation(feature) {\n    const item = feature && feature.item ? feature.item : null;\n    if (!item || !item.location) {\n      return null;\n    }\n    return locationTextFromValue(item.location.point || item.location.center, \"clause\");\n  }\n\n  function cueSummaryItems(mapContent) {\n    const all = collectAllFeatureItems(mapContent);\n    const withSemantics = all.filter(function(feature){\n      return !!(feature && feature.semantics && typeof feature.semantics === \"object\");\n    });\n\n    const crossingSignals = [];\n    let tactileYes = 0;\n    let tactileNo = 0;\n    const kerbCounts = { lowered: 0, flush: 0, raised: 0 };\n    let stepsCount = 0;\n    let maxStepCount = 0;\n    let inclineUp = 0;\n    let inclineDown = 0;\n    let wheelchairYes = 0;\n    let wheelchairLimited = 0;\n    let wheelchairNo = 0;\n\n    withSemantics.forEach(function(feature){\n      const sem = feature.semantics;\n      if (sem.crossing && sem.crossing.type === \"traffic_signals\") {\n        crossingSignals.push(feature);\n      }\n      if (sem.crossing && sem.crossing.tactile_paving === \"yes\") {\n        tactileYes += 1;\n      } else if (sem.crossing && sem.crossing.tactile_paving === \"no\") {\n        tactileNo += 1;\n      }\n      const kerb = sem.kerb && sem.kerb.value;\n      if (kerbCounts[kerb] !== undefined) {\n        kerbCounts[kerb] += 1;\n      }\n      const stepCount = sem.steps && sem.steps.step_count;\n      if (stepCount !== undefined && stepCount !== null && !isNaN(stepCount)) {\n        stepsCount += 1;\n        maxStepCount = Math.max(maxStepCount, Number(stepCount));\n      }\n      const incline = sem.incline && sem.incline.value;\n      if (incline === \"up\") {\n        inclineUp += 1;\n      } else if (incline === \"down\") {\n        inclineDown += 1;\n      }\n      const wheelchair = sem.wheelchair && sem.wheelchair.value;\n      if (wheelchair === \"yes\") wheelchairYes += 1;\n      if (wheelchair === \"limited\") wheelchairLimited += 1;\n      if (wheelchair === \"no\") wheelchairNo += 1;\n    });\n\n    const items = [];\n    if (crossingSignals.length) {\n      const key = crossingSignals.length === 1\n        ? \"map_content_poi_access_crossings_signals_one\"\n        : \"map_content_poi_access_crossings_signals_many\";\n      const title = interpolate(\n        t(key, \"__count__ places with signalized crossings\"),\n        { count: crossingSignals.length }\n      );\n      const lines = [lineModel(capitalizeFirst(title), \"map-content-title-line\")];\n      const exampleFeature = crossingSignals.find(function(feature){\n        return !!featureLabel(feature);\n      }) || null;\n      const exampleLabel = exampleFeature ? featureLabel(exampleFeature) : null;\n      const exampleLoc = exampleFeature ? featureLocation(exampleFeature) : null;\n      if (exampleLabel) {\n        const example = exampleLoc ? (exampleLabel + \", \" + exampleLoc) : exampleLabel;\n        lines.push(lineModel(\n          interpolate(t(\"map_content_poi_example\", \"Example: __example__\"), { example: example }),\n          \"map-content-location\"\n        ));\n      }\n      items.push({ type: \"poi_cue\", attrs: {}, lines: lines });\n    }\n    if (tactileYes > 0 || tactileNo > 0) {\n      items.push({\n        type: \"poi_cue\",\n        attrs: {},\n        lines: [lineModel(interpolate(\n          t(\"map_content_poi_access_tactile_summary\", \"Tactile paving tags: __yes__ yes, __no__ no\"),\n          { yes: tactileYes, no: tactileNo }\n        ), \"map-content-title-line\")]\n      });\n    }\n    if (kerbCounts.lowered > 0 || kerbCounts.flush > 0 || kerbCounts.raised > 0) {\n      items.push({\n        type: \"poi_cue\",\n        attrs: {},\n        lines: [lineModel(interpolate(\n          t(\"map_content_poi_access_kerb_summary\", \"Kerb tags: lowered __lowered__, flush __flush__, raised __raised__\"),\n          {\n            lowered: kerbCounts.lowered,\n            flush: kerbCounts.flush,\n            raised: kerbCounts.raised\n          }\n        ), \"map-content-title-line\")]\n      });\n    }\n    if (stepsCount > 0) {\n      items.push({\n        type: \"poi_cue\",\n        attrs: {},\n        lines: [lineModel(interpolate(\n          t(\"map_content_poi_access_steps_summary\", \"Steps tagged at __count__ places (largest step count __max__)\"),\n          { count: stepsCount, max: maxStepCount }\n        ), \"map-content-title-line\")]\n      });\n    }\n    if (inclineUp > 0 || inclineDown > 0) {\n      items.push({\n        type: \"poi_cue\",\n        attrs: {},\n        lines: [lineModel(interpolate(\n          t(\"map_content_poi_access_incline_summary\", \"Incline tags: up __up__, down __down__\"),\n          { up: inclineUp, down: inclineDown }\n        ), \"map-content-title-line\")]\n      });\n    }\n    if (wheelchairYes > 0 || wheelchairLimited > 0 || wheelchairNo > 0) {\n      items.push({\n        type: \"poi_cue\",\n        attrs: {},\n        lines: [lineModel(interpolate(\n          t(\"map_content_poi_access_wheelchair_summary\", \"Wheelchair tags: yes __yes__, limited __limited__, no __no__\"),\n          { yes: wheelchairYes, limited: wheelchairLimited, no: wheelchairNo }\n        ), \"map-content-title-line\")]\n      });\n    }\n    return items;\n  }\n\n  function normalizedOptions(options) {\n    return {\n      section: options && typeof options.section === \"string\" ? options.section : \"familiar_places\",\n      includeSparseNote: !!(options && options.includeSparseNote)\n    };\n  }\n\n  function sectionEntriesByKey(mapContent) {\n    const entries = collectPoiEntries(mapContent);\n    const sections = {\n      familiar_places: [],\n      daily_essentials: [],\n      transport_points: []\n    };\n    entries.forEach(function(entry){\n      if (!sections[entry.section]) {\n        return;\n      }\n      sections[entry.section].push(entry);\n    });\n    sections.familiar_places = mergeUnnamedEntriesByTypeAndLocation(sortEntries(sections.familiar_places));\n    sections.daily_essentials = mergeUnnamedEntriesByTypeAndLocation(sortEntries(sections.daily_essentials));\n    sections.transport_points = mergeUnnamedEntriesByTypeAndLocation(sortEntries(sections.transport_points));\n    return sections;\n  }\n\n  function buildModel(mapContent, helpers, options) {\n    setTranslator(helpers);\n    const resolved = normalizedOptions(options);\n    if (resolved.section === \"accessibility_cues\") {\n      return cueSummaryItems(mapContent);\n    }\n    const bySection = sectionEntriesByKey(mapContent);\n    const selected = bySection[resolved.section] || [];\n    return selected.map(entryToModelItem);\n  }\n\n  function applyItemAttrs(listItem, attrs) {\n    if (!attrs || typeof attrs !== \"object\") {\n      return;\n    }\n    if (attrs.dataOsmId !== undefined && attrs.dataOsmId !== null) {\n      listItem.attr(\"data-osm-id\", String(attrs.dataOsmId));\n    }\n    if (attrs.initiallyHidden) {\n      listItem.attr(\"data-initially-hidden\", \"true\");\n    }\n  }\n\n  function appendLineParts(target, parts) {\n    parts.forEach(function(part){\n      if (!part || part.text === undefined || part.text === null || part.text === \"\") {\n        return;\n      }\n      target.append($(\"<span>\").text(String(part.text)));\n    });\n  }\n\n  function renderLineFromModel(listItem, lineModel) {\n    if (!lineModel || !Array.isArray(lineModel.parts) || !lineModel.parts.length) {\n      return;\n    }\n    const tagName = lineModel.className === \"map-content-title-line\" ? \"h5\" : \"div\";\n    const line = $(\"<\" + tagName + \">\");\n    if (lineModel.className) {\n      line.addClass(lineModel.className);\n    }\n    const titleLink = lineModel.className === \"map-content-title-line\"\n      ? normalizedExternalLink(lineModel.link)\n      : null;\n    appendLineParts(line, lineModel.parts);\n    if (titleLink) {\n      const anchor = $(\"<a>\")\n        .addClass(\"map-content-inline-external-link\")\n        .attr(\"href\", titleLink.url)\n        .attr(\"target\", \"_blank\")\n        .attr(\"rel\", \"noopener noreferrer\");\n      anchor.append($(\"<span>\").text(titleLink.label || \"Website\"));\n      anchor.append($(\"<span>\").addClass(\"map-content-external-indicator\").attr(\"aria-hidden\", \"true\").text(\"↗\"));\n      anchor.append($(\"<span>\").addClass(\"visuallyhidden\").text(\n        \" \" + t(\"map_content_external_link_aria\", \"External link, opens in a new tab\")\n      ));\n      line.append($(\"<span>\").addClass(\"map-content-external-link-sep\").text(\" \"));\n      line.append(anchor);\n    }\n    if (lineModel.title) {\n      line.addClass(\"map-content-has-importance-popup\");\n      const popup = $(\"<span>\").addClass(\"map-content-importance-popup\");\n      popup.append($(\"<pre>\").text(lineModel.title));\n      line.append(popup);\n    }\n    if (!line.text()) {\n      return;\n    }\n    listItem.append(line);\n  }\n\n  function renderItemFromModel(item, listElem) {\n    if (!item || !listElem || !listElem.length) {\n      return;\n    }\n    const listItem = $(\"<li>\").addClass(\"map-content-poi\");\n    applyItemAttrs(listItem, item.attrs);\n    const lines = Array.isArray(item.lines) ? item.lines : [];\n    lines.forEach(function(lineModel){\n      renderLineFromModel(listItem, lineModel);\n    });\n    listElem.append(listItem);\n  }\n\n  function renderFromModel(model, listElem) {\n    if (!listElem || !listElem.length) {\n      return 0;\n    }\n    const items = Array.isArray(model) ? model : [];\n    listElem.empty();\n    items.forEach(function(item){\n      renderItemFromModel(item, listElem);\n    });\n    return items.length;\n  }\n\n  function emptyMessage(helpers, options) {\n    setTranslator(helpers);\n    const resolved = normalizedOptions(options);\n    if (resolved.section === \"familiar_places\") {\n      return t(\"map_content_no_poi_familiar\", \"No familiar places listed for this map.\");\n    }\n    if (resolved.section === \"daily_essentials\") {\n      return t(\"map_content_no_poi_daily\", \"No daily essentials listed for this map.\");\n    }\n    if (resolved.section === \"transport_points\") {\n      return t(\"map_content_no_poi_transport\", \"No transport points listed for this map.\");\n    }\n    return t(\"map_content_no_poi_accessibility\", \"No accessibility cues are tagged for this map.\");\n  }\n\n  window.TM = window.TM || {};\n  window.TM.mapDescPois = {\n    buildModel: buildModel,\n    renderFromModel: renderFromModel,\n    emptyMessage: emptyMessage\n  };\n})();\n"
  },
  {
    "path": "web/src/scripts/map-desc-ways.js",
    "content": "/* global $ isNaN */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0, space-infix-ops:0, camelcase:0, dot-notation:0 */\n\n(function(){\n  'use strict';\n\n  const EXTERNAL_LINK_TYPE_PRIORITY = {\n    wikipedia: 0,\n    wikidata: 1,\n    commons: 2,\n    website: 3,\n    search: 4\n  };\n\n  function fallbackTranslate(key, fallback) {\n    return fallback !== undefined ? fallback : key;\n  }\n\n  let translate = fallbackTranslate;\n\n  function setTranslator(helpers) {\n    translate = helpers && typeof helpers.t === 'function' ? helpers.t : fallbackTranslate;\n  }\n\n  function t(key, fallback) {\n    return translate(key, fallback);\n  }\n\n  function interpolate(text, replacements) {\n    if (!replacements) {\n      return text;\n    }\n    return text.replace(/__([a-zA-Z0-9_]+)__/g, function(match, name) {\n      if (replacements[name] === undefined) {\n        return match;\n      }\n      return replacements[name];\n    });\n  }\n\n  function capitalizeFirst(text) {\n    if (!text || typeof text !== 'string') {\n      return text;\n    }\n    return text.charAt(0).toUpperCase() + text.slice(1);\n  }\n\n  function edgeLabel(edge) {\n    if (edge === \"north\") return t(\"map_content_edge_north\", \"north\");\n    if (edge === \"south\") return t(\"map_content_edge_south\", \"south\");\n    if (edge === \"east\") return t(\"map_content_edge_east\", \"east\");\n    if (edge === \"west\") return t(\"map_content_edge_west\", \"west\");\n    return edge;\n  }\n\n  function edgePositionBucketFromDirection(edge, direction) {\n    if (edge === \"east\") {\n      if (direction === \"northeast\") return \"north\";\n      if (direction === \"east\") return \"center\";\n      if (direction === \"southeast\") return \"south\";\n      return null;\n    }\n    if (edge === \"west\") {\n      if (direction === \"northwest\") return \"north\";\n      if (direction === \"west\") return \"center\";\n      if (direction === \"southwest\") return \"south\";\n      return null;\n    }\n    if (edge === \"north\") {\n      if (direction === \"northwest\") return \"west\";\n      if (direction === \"north\") return \"center\";\n      if (direction === \"northeast\") return \"east\";\n      return null;\n    }\n    if (edge === \"south\") {\n      if (direction === \"southwest\") return \"west\";\n      if (direction === \"south\") return \"center\";\n      if (direction === \"southeast\") return \"east\";\n      return null;\n    }\n    return null;\n  }\n\n  function edgePositionQualifier(position) {\n    if (!position) {\n      return null;\n    }\n    if (position === \"multiple\") return t(\"map_content_touch_pos_multiple\", \"in multiple places\");\n    if (position === \"center\") return t(\"map_content_touch_pos_center\", \"near the center\");\n    if (position === \"north\") return t(\"map_content_touch_pos_north\", \"in the north\");\n    if (position === \"south\") return t(\"map_content_touch_pos_south\", \"in the south\");\n    if (position === \"east\") return t(\"map_content_touch_pos_east\", \"in the east\");\n    if (position === \"west\") return t(\"map_content_touch_pos_west\", \"in the west\");\n    return null;\n  }\n\n  function directionLabel(direction) {\n    if (direction === \"north\") return t(\"map_content_dir_north\", \"north\");\n    if (direction === \"northeast\") return t(\"map_content_dir_northeast\", \"north-east\");\n    if (direction === \"east\") return t(\"map_content_dir_east\", \"east\");\n    if (direction === \"southeast\") return t(\"map_content_dir_southeast\", \"south-east\");\n    if (direction === \"south\") return t(\"map_content_dir_south\", \"south\");\n    if (direction === \"southwest\") return t(\"map_content_dir_southwest\", \"south-west\");\n    if (direction === \"west\") return t(\"map_content_dir_west\", \"west\");\n    if (direction === \"northwest\") return t(\"map_content_dir_northwest\", \"north-west\");\n    return direction;\n  }\n\n  function cornerLabel(direction) {\n    if (direction === \"northwest\") return t(\"map_content_corner_northwest\", \"north-west corner\");\n    if (direction === \"northeast\") return t(\"map_content_corner_northeast\", \"north-east corner\");\n    if (direction === \"southwest\") return t(\"map_content_corner_southwest\", \"south-west corner\");\n    if (direction === \"southeast\") return t(\"map_content_corner_southeast\", \"south-east corner\");\n    return direction;\n  }\n\n  function isCornerDirection(direction) {\n    return direction === \"northwest\" || direction === \"northeast\" ||\n      direction === \"southwest\" || direction === \"southeast\";\n  }\n\n  /**\n   * Location phrase grammar (distance classification only):\n   * 1) center, 2) part + dir, 3) near_edge + dir (diagonal near_edge means corner).\n   * Forms:\n   * - atom: \"center\", \"east part\", \"north edge\", \"north-east corner\"\n   * - clause: \"in the center\", \"in the east part\", \"near the north edge\", \"in the north-east corner\"\n   * - endpoint: \"the center\", \"the east part\", \"near the north edge\", \"the north-east corner\"\n   * Rules:\n   * - no \"of the map\" suffix\n   * - avoid duplicated prepositions (\"Near near ...\", \"From in ...\") by choosing form per context.\n   */\n  function locationPhraseFromLoc(loc, form) {\n    if (!loc || typeof loc !== 'object') {\n      return null;\n    }\n    const phraseForm = form || \"clause\";\n    const kind = loc.kind;\n    const dirLabel = loc.dir ? directionLabel(loc.dir) : null;\n    const corner = loc.dir ? cornerLabel(loc.dir) : null;\n    const atomCenter = t(\"map_content_loc_center\", \"center\");\n    const clauseCenter = t(\"map_content_loc_full_center\", \"in the center\");\n    const endpointCenter = t(\"map_content_loc_endpoint_center\", \"the center\");\n\n    if (kind === \"center\") {\n      if (phraseForm === \"route_from\") {\n        return t(\"map_content_loc_route_from_center\", endpointCenter);\n      }\n      if (phraseForm === \"route_to\") {\n        return t(\"map_content_loc_route_to_center\", endpointCenter);\n      }\n      if (phraseForm === \"atom\") {\n        return atomCenter;\n      }\n      if (phraseForm === \"endpoint\") {\n        return endpointCenter;\n      }\n      return clauseCenter;\n    }\n    if (kind === \"part\") {\n      if (!dirLabel) {\n        if (phraseForm === \"atom\") {\n          return atomCenter;\n        }\n        if (phraseForm === \"endpoint\") {\n          return endpointCenter;\n        }\n        return clauseCenter;\n      }\n      if (phraseForm === \"route_from\") {\n        return interpolate(\n          t(\"map_content_loc_route_from_part\", t(\"map_content_loc_endpoint_part\", \"the __dir__ part\")),\n          { dir: dirLabel }\n        );\n      }\n      if (phraseForm === \"route_to\") {\n        return interpolate(\n          t(\"map_content_loc_route_to_part\", t(\"map_content_loc_endpoint_part\", \"the __dir__ part\")),\n          { dir: dirLabel }\n        );\n      }\n      const atomPart = interpolate(t(\"map_content_loc_part\", \"__dir__ part\"), { dir: dirLabel });\n      if (phraseForm === \"atom\") {\n        return atomPart;\n      }\n      if (phraseForm === \"endpoint\") {\n        return interpolate(t(\"map_content_loc_endpoint_part\", \"the __dir__ part\"), { dir: dirLabel });\n      }\n      return interpolate(t(\"map_content_loc_full_part\", \"in the __dir__ part\"), { dir: dirLabel });\n    }\n    if (kind === \"near_edge\") {\n      if (isCornerDirection(loc.dir)) {\n        if (!corner) {\n          return null;\n        }\n        if (phraseForm === \"route_from\") {\n          return interpolate(\n            t(\"map_content_loc_route_from_corner\", t(\"map_content_loc_endpoint_corner\", \"the __corner__\")),\n            { corner: corner }\n          );\n        }\n        if (phraseForm === \"route_to\") {\n          return interpolate(\n            t(\"map_content_loc_route_to_corner\", t(\"map_content_loc_endpoint_corner\", \"the __corner__\")),\n            { corner: corner }\n          );\n        }\n        if (phraseForm === \"atom\") {\n          return interpolate(t(\"map_content_loc_corner\", \"__corner__\"), { corner: corner });\n        }\n        if (phraseForm === \"endpoint\") {\n          return interpolate(t(\"map_content_loc_endpoint_corner\", \"the __corner__\"), { corner: corner });\n        }\n        return interpolate(t(\"map_content_loc_full_near_corner\", \"in the __corner__\"), { corner: corner });\n      }\n      if (!dirLabel) {\n        return null;\n      }\n      const atomEdge = interpolate(t(\"map_content_loc_edge\", \"__dir__ edge\"), { dir: dirLabel });\n      if (phraseForm === \"route_from\") {\n        return interpolate(\n          t(\"map_content_loc_route_from_near_edge\", t(\"map_content_loc_endpoint_near_edge\", \"near the __dir__ edge\")),\n          { dir: dirLabel }\n        );\n      }\n      if (phraseForm === \"route_to\") {\n        return interpolate(\n          t(\"map_content_loc_route_to_near_edge\", t(\"map_content_loc_endpoint_near_edge\", \"near the __dir__ edge\")),\n          { dir: dirLabel }\n        );\n      }\n      if (phraseForm === \"atom\") {\n        return atomEdge;\n      }\n      return interpolate(\n        phraseForm === \"endpoint\"\n          ? t(\"map_content_loc_endpoint_near_edge\", \"near the __dir__ edge\")\n          : t(\"map_content_loc_full_near_edge\", \"near the __dir__ edge\"),\n        { dir: dirLabel }\n      );\n    }\n    return null;\n  }\n\n  function joinWithAnd(parts) {\n    const andWord = t(\"map_content_list_and\", \"and\");\n    if (!parts.length) {\n      return \"\";\n    }\n    if (parts.length === 1) {\n      return parts[0];\n    }\n    if (parts.length === 2) {\n      return parts[0] + \" \" + andWord + \" \" + parts[1];\n    }\n    return parts.slice(0, -1).join(\", \") + \", \" + andWord + \" \" + parts[parts.length - 1];\n  }\n\n  function normalizeOptions(options) {\n    const section = options && typeof options.section === \"string\" ? options.section : \"all\";\n    return {\n      section: section\n    };\n  }\n\n  function sectionForSubClassKey(subClassKey) {\n    if (!subClassKey || typeof subClassKey !== \"string\") {\n      return null;\n    }\n    if (subClassKey.indexOf(\"A1_\") === 0) {\n      return \"roads\";\n    }\n    if (subClassKey.indexOf(\"A2_\") === 0) {\n      return \"paths\";\n    }\n    if (subClassKey.indexOf(\"A3_\") === 0) {\n      return \"railways\";\n    }\n    if (subClassKey.indexOf(\"A4_\") === 0) {\n      return \"waterways\";\n    }\n    if (subClassKey.indexOf(\"A5_\") === 0) {\n      return null;\n    }\n    if (subClassKey.indexOf(\"A\") === 0) {\n      return \"otherLinear\";\n    }\n    return null;\n  }\n\n  function isRailSubClass(subClassKey) {\n    return !!(subClassKey && typeof subClassKey === \"string\" && subClassKey.indexOf(\"A3_\") === 0);\n  }\n\n  function collectWayGroups(mapContent, options) {\n    const resolved = normalizeOptions(options);\n    const ways = [];\n    const classA = mapContent && typeof mapContent === 'object' ? mapContent.A : null;\n    const subclasses = classA && Array.isArray(classA.subclasses) ? classA.subclasses : [];\n\n    subclasses.forEach(function(subclass, subclassOrder){\n      if (!subclass || subclass.kind !== \"linear\" || !Array.isArray(subclass.groups)) {\n        return;\n      }\n      const sectionKey = sectionForSubClassKey(subclass.key);\n      if (!sectionKey) {\n        return;\n      }\n      if (resolved.section !== \"all\" && sectionKey !== resolved.section) {\n        return;\n      }\n      subclass.groups.forEach(function(group){\n        if (!group || typeof group !== 'object') {\n          return;\n        }\n        ways.push({\n          group: group,\n          subclassOrder: subclassOrder,\n          subclassType: singularSubclassType(subclass.key, subclass.name),\n          subClass: subclass.key,\n          sectionKey: sectionKey\n        });\n      });\n    });\n\n    function mergeGroupWays(targetGroup, sourceGroup) {\n      if (!targetGroup || typeof targetGroup !== \"object\" || !sourceGroup || typeof sourceGroup !== \"object\") {\n        return;\n      }\n      const targetWays = Array.isArray(targetGroup.ways) ? targetGroup.ways.slice() : [];\n      const sourceWays = Array.isArray(sourceGroup.ways) ? sourceGroup.ways : [];\n      const seenWayIds = {};\n      targetWays.forEach(function(way){\n        const id = way && way.osmId !== undefined && way.osmId !== null ? String(way.osmId) : null;\n        if (id) {\n          seenWayIds[id] = true;\n        }\n      });\n      sourceWays.forEach(function(way){\n        const id = way && way.osmId !== undefined && way.osmId !== null ? String(way.osmId) : null;\n        if (id && seenWayIds[id]) {\n          return;\n        }\n        if (id) {\n          seenWayIds[id] = true;\n        }\n        targetWays.push(way);\n      });\n      targetGroup.ways = targetWays;\n\n      const targetVisible = Array.isArray(targetGroup.visibleGeometry) ? targetGroup.visibleGeometry.slice() : [];\n      const sourceVisible = Array.isArray(sourceGroup.visibleGeometry) ? sourceGroup.visibleGeometry : [];\n      const visibleSeen = {};\n      targetVisible.forEach(function(bucket){\n        const id = bucket && bucket.osmId !== undefined && bucket.osmId !== null ? String(bucket.osmId) : null;\n        if (id) {\n          visibleSeen[id] = true;\n        }\n      });\n      sourceVisible.forEach(function(bucket){\n        const id = bucket && bucket.osmId !== undefined && bucket.osmId !== null ? String(bucket.osmId) : null;\n        if (id && visibleSeen[id]) {\n          return;\n        }\n        if (id) {\n          visibleSeen[id] = true;\n        }\n        targetVisible.push(bucket);\n      });\n      targetGroup.visibleGeometry = targetVisible;\n\n      const targetLength = wayLengthValue(targetGroup);\n      const sourceLength = wayLengthValue(sourceGroup);\n      targetGroup.totalLength = targetLength + sourceLength;\n    }\n\n    function normalizeMergeText(value) {\n      if (!value || typeof value !== \"string\") {\n        return \"\";\n      }\n      return value\n        .replace(/[‐‑‒–—]/g, \"-\")\n        .replace(/\\s+/g, \" \")\n        .trim()\n        .toLowerCase();\n    }\n\n    function appendUniqueMergedText(list, value) {\n      if (!Array.isArray(list) || !value || typeof value !== \"string\") {\n        return list;\n      }\n      const trimmed = value.trim();\n      if (!trimmed) {\n        return list;\n      }\n      const normalized = normalizeMergeText(trimmed);\n      for (let i = 0; i < list.length; i += 1) {\n        if (normalizeMergeText(list[i]) === normalized) {\n          return list;\n        }\n      }\n      list.push(trimmed);\n      return list;\n    }\n\n    function unnamedMergeSignature(entry) {\n      if (!entry) {\n        return null;\n      }\n      if (wayName(entry && entry.group ? entry.group : null)) {\n        return null;\n      }\n      if (entry.sectionKey !== \"railways\" &&\n          entry.sectionKey !== \"waterways\" &&\n          entry.sectionKey !== \"otherLinear\") {\n        return null;\n      }\n      const routeSig = routeText(entry.group) || \"\";\n      const edgeSig = edgesText(entry.group) || \"\";\n      if (!routeSig && !edgeSig) {\n        return null;\n      }\n      const lengthSig = formatLength(entry.group) || \"\";\n      const normalizedRouteSig = normalizeMergeText(routeSig);\n      const normalizedEdgeSig = normalizeMergeText(edgeSig);\n      const normalizedLocationParts = [];\n      if (normalizedRouteSig) {\n        normalizedLocationParts.push(normalizedRouteSig);\n      }\n      if (normalizedEdgeSig && normalizedLocationParts.indexOf(normalizedEdgeSig) === -1) {\n        normalizedLocationParts.push(normalizedEdgeSig);\n      }\n      normalizedLocationParts.sort();\n      const normalizedLocationSig = normalizedLocationParts.join(\"||\");\n      const normalizedLengthSig = normalizeMergeText(lengthSig);\n      // Unnamed railways should merge like roads in length aggregation:\n      // equivalent entries merge and lengths sum, so length does not belong in railway merge key.\n      // Keep route/edge distinctions in the key to avoid collapsing unrelated rail segments\n      // that happen to share a coarse location phrase.\n      const unnamedKey = entry.sectionKey === \"railways\"\n        ? (\"unnamed|\" + (entry.sectionKey || \"\") + \"|\" + (entry.subClass || \"\") + \"|\" + normalizedLocationSig)\n        : (\"unnamed|\" + (entry.sectionKey || \"\") + \"|\" + (entry.subClass || \"\") + \"|\" + normalizedLocationSig + \"|\" + normalizedLengthSig);\n      return {\n        key: unnamedKey,\n        routeSig: routeSig,\n        edgeSig: edgeSig\n      };\n    }\n\n    function mergeEquivalentWays(entries) {\n      const merged = [];\n      const byKey = {};\n      entries.forEach(function(entry){\n        const name = wayName(entry && entry.group ? entry.group : null);\n        const normName = normalizedWayName(name);\n        const unnamedSig = normName ? null : unnamedMergeSignature(entry);\n        const key = normName\n          ? ((entry.sectionKey || \"\") + \"|\" + (entry.subClass || \"\") + \"|\" + normName)\n          : (unnamedSig ? unnamedSig.key : null);\n        if (!key) {\n          merged.push(entry);\n          return;\n        }\n        if (!byKey[key]) {\n          const groupClone = Object.assign({}, entry.group || {});\n          if (unnamedSig) {\n            groupClone._mergedRouteText = unnamedSig.routeSig || null;\n            groupClone._mergedEdgeText = unnamedSig.edgeSig || null;\n            groupClone._mergedEdgeTexts = appendUniqueMergedText([], unnamedSig.edgeSig || \"\");\n          }\n          byKey[key] = {\n            group: groupClone,\n            subclassOrder: entry.subclassOrder,\n            subclassType: entry.subclassType,\n            subClass: entry.subClass,\n            sectionKey: entry.sectionKey\n          };\n          merged.push(byKey[key]);\n          return;\n        }\n        mergeGroupWays(byKey[key].group, entry.group || {});\n        if (unnamedSig) {\n          const existingEdgeTexts = Array.isArray(byKey[key].group._mergedEdgeTexts)\n            ? byKey[key].group._mergedEdgeTexts.slice()\n            : [];\n          appendUniqueMergedText(existingEdgeTexts, unnamedSig.edgeSig || \"\");\n          byKey[key].group._mergedEdgeTexts = existingEdgeTexts;\n          if (existingEdgeTexts.length > 1) {\n            byKey[key].group._mergedEdgeText = joinWithAnd(existingEdgeTexts);\n          } else if (existingEdgeTexts.length === 1) {\n            byKey[key].group._mergedEdgeText = existingEdgeTexts[0];\n          }\n        }\n      });\n      return merged;\n    }\n\n    const mergedWays = mergeEquivalentWays(ways);\n\n    mergedWays.sort(function(a, b){\n      const aNamed = !!wayName(a.group);\n      const bNamed = !!wayName(b.group);\n      if (aNamed !== bNamed) {\n        return aNamed ? -1 : 1;\n      }\n      const importanceDiff = wayImportanceScore(b.group) - wayImportanceScore(a.group);\n      if (importanceDiff !== 0) {\n        return importanceDiff;\n      }\n      if (a.subclassOrder !== b.subclassOrder) {\n        return a.subclassOrder - b.subclassOrder;\n      }\n      const lengthDiff = wayLengthValue(b.group) - wayLengthValue(a.group);\n      if (Math.abs(lengthDiff) > 1e-9) {\n        return lengthDiff;\n      }\n      return wayTitle(a.group).localeCompare(wayTitle(b.group));\n    });\n    return mergedWays;\n  }\n\n  function wayImportanceScore(group) {\n    const value = group ? group.importanceScore : null;\n    const resolved = value && typeof value === \"object\" ? value.final : value;\n    if (resolved === null || resolved === undefined || isNaN(resolved)) {\n      return 0;\n    }\n    const number = Number(resolved);\n    if (!isFinite(number)) {\n      return 0;\n    }\n    return number;\n  }\n\n  function importanceScoreTooltip(importanceScore) {\n    if (!importanceScore || typeof importanceScore !== \"object\" || Array.isArray(importanceScore)) {\n      return null;\n    }\n    try {\n      return JSON.stringify(importanceScore, null, 2);\n    } catch (_error) {\n      return null;\n    }\n  }\n\n  function isSafeExternalUrl(url) {\n    return typeof url === \"string\" && /^https?:\\/\\/\\S+$/i.test(url);\n  }\n\n  function isGoogleSearchExternalLink(link) {\n    if (!link || typeof link !== \"object\") {\n      return false;\n    }\n    if (typeof link.label !== \"string\" || link.label.trim().toLowerCase() !== \"search\") {\n      return false;\n    }\n    if (typeof link.url !== \"string\") {\n      return false;\n    }\n    try {\n      const parsed = new URL(link.url);\n      const host = parsed.hostname.toLowerCase();\n      if (!/^([a-z0-9-]+\\.)*google\\./.test(host)) {\n        return false;\n      }\n      return parsed.pathname === \"/search\";\n    } catch (_error) {\n      return false;\n    }\n  }\n\n  function normalizedExternalLink(link) {\n    if (!link || typeof link !== \"object\") {\n      return null;\n    }\n    if (!isSafeExternalUrl(link.url)) {\n      return null;\n    }\n    if (isGoogleSearchExternalLink(link)) {\n      return null;\n    }\n    return {\n      type: typeof link.type === \"string\" ? link.type : \"\",\n      url: link.url,\n      label: typeof link.label === \"string\" ? link.label : \"\"\n    };\n  }\n\n  function externalLinkPriority(link) {\n    if (!link || !link.type || EXTERNAL_LINK_TYPE_PRIORITY[link.type] === undefined) {\n      return Number.MAX_SAFE_INTEGER;\n    }\n    return EXTERNAL_LINK_TYPE_PRIORITY[link.type];\n  }\n\n  function bestGroupExternalLink(group) {\n    if (!group || !Array.isArray(group.ways)) {\n      return null;\n    }\n    let best = null;\n    let bestPriority = Number.MAX_SAFE_INTEGER;\n    group.ways.forEach(function(way){\n      if (!way || typeof way !== \"object\") {\n        return;\n      }\n      const candidate = normalizedExternalLink(way.externalLink);\n      if (!candidate) {\n        return;\n      }\n      const priority = externalLinkPriority(candidate);\n      if (!best || priority < bestPriority) {\n        best = candidate;\n        bestPriority = priority;\n      }\n    });\n    return best;\n  }\n\n  function wayLengthValue(group) {\n    const value = group && (group.totalLength !== undefined ? group.totalLength : group.length);\n    if (value === null || value === undefined || isNaN(value)) {\n      return 0;\n    }\n    const number = Number(value);\n    if (!isFinite(number)) {\n      return 0;\n    }\n    return Math.max(0, number);\n  }\n\n  function stripWayModifierSuffix(label) {\n    if (!label || typeof label !== \"string\") {\n      return label;\n    }\n    const trimmed = label.trim();\n    const match = trimmed.match(/^(.*)\\s+\\[([^\\]]+)\\]$/);\n    if (!match) {\n      return trimmed;\n    }\n    const rawBase = match[1] ? match[1].trim() : \"\";\n    const rawModifiers = match[2] ? match[2].trim() : \"\";\n    if (!rawBase || !rawModifiers) {\n      return trimmed;\n    }\n\n    const modifiers = rawModifiers.split(\",\").map(function(part){\n      return part.trim();\n    }).filter(Boolean);\n    if (!modifiers.length) {\n      return trimmed;\n    }\n\n    const allModifierTokens = modifiers.every(function(part){\n      return /^[a-z_]+(?:=-?\\d+)?$/i.test(part);\n    });\n    if (!allModifierTokens) {\n      return trimmed;\n    }\n\n    const hasWayModifier = modifiers.some(function(part){\n      const key = part.split(\"=\")[0].toLowerCase();\n      return key === \"layer\" ||\n        key === \"tunnel\" ||\n        key === \"bridge\" ||\n        key === \"covered\" ||\n        key === \"ford\" ||\n        key === \"embankment\" ||\n        key === \"cutting\";\n    });\n\n    return hasWayModifier ? rawBase : trimmed;\n  }\n\n  function wayTitle(group) {\n    const label = stripWayModifierSuffix(group && group.displayLabel);\n    if (isUnnamedWayLabel(label)) {\n      return t(\"map_content_way_unnamed\", \"Unnamed way\");\n    }\n    return capitalizeFirst(label);\n  }\n\n  function isUnnamedWayLabel(label) {\n    if (!label || typeof label !== \"string\") {\n      return true;\n    }\n    const trimmed = stripWayModifierSuffix(label).trim();\n    return !trimmed || trimmed === \"(unnamed)\" || trimmed.indexOf(\"(unnamed)\") === 0;\n  }\n\n  function wayName(group) {\n    const label = stripWayModifierSuffix(group && group.displayLabel);\n    if (isUnnamedWayLabel(label)) {\n      return null;\n    }\n    return label;\n  }\n\n  function singularSubclassType(subclassKey, fallbackName) {\n    if (subclassKey) {\n      const typeKey = \"map_content_way_type_\" + subclassKey;\n      const translated = t(typeKey, typeKey);\n      if (translated !== typeKey) {\n        return translated;\n      }\n    }\n    if (!fallbackName || typeof fallbackName !== 'string') {\n      return null;\n    }\n    const name = fallbackName.trim();\n    if (!name) {\n      return null;\n    }\n    if (name.endsWith(\"ies\")) {\n      return name.slice(0, -3).toLowerCase() + \"y\";\n    }\n    if (name.endsWith(\"s\")) {\n      return name.slice(0, -1).toLowerCase();\n    }\n    return name.toLowerCase();\n  }\n\n  function formatLength(item) {\n    const length = wayLengthValue(item);\n    if (!length) {\n      return null;\n    }\n    let rounded = 0;\n    if (length >= 1000) {\n      rounded = Math.round(length / 10) * 10;\n    } else if (length >= 100) {\n      rounded = Math.round(length / 5) * 5;\n    } else {\n      rounded = Math.round(length);\n    }\n    return interpolate(t(\"map_content_way_length_m\", \"__meters__ meters\"), { meters: rounded });\n  }\n\n  function unnamedRoadsCountText(count) {\n    if (count === null || count === undefined || isNaN(count)) {\n      return null;\n    }\n    const rounded = Math.max(0, Math.round(Number(count)));\n    if (!rounded) {\n      return null;\n    }\n    const isOne = rounded === 1;\n    return interpolate(\n      t(\n        isOne ? \"map_content_unnamed_roads_one\" : \"map_content_unnamed_roads_many\",\n        isOne ? \"__count__ unnamed road\" : \"__count__ unnamed roads\"\n      ),\n      { count: rounded }\n    );\n  }\n\n  function collectSegmentPoints(segment) {\n    const points = [];\n    const events = segment && Array.isArray(segment.events) ? segment.events : [];\n    events.forEach(function(event){\n      if (!event || event.zone === undefined || event.zone === null) {\n        return;\n      }\n      const tValue = event.t !== undefined && !isNaN(event.t) ? Number(event.t) : 0;\n      points.push({ t: tValue, zone: event.zone });\n    });\n    points.sort(function(a, b){ return a.t - b.t; });\n    return points;\n  }\n\n  function locationTextFromZone(zone, form) {\n    if (zone && typeof zone === 'object') {\n      return locationPhraseFromLoc(zone, form) || null;\n    }\n    return null;\n  }\n\n  function segmentList(target) {\n    const visibleGeometry = target && Array.isArray(target.visibleGeometry) ? target.visibleGeometry : [];\n    const flattened = [];\n    visibleGeometry.forEach(function(bucket){\n      const segments = bucket && Array.isArray(bucket.segments) ? bucket.segments : [];\n      segments.forEach(function(segment){\n        flattened.push(segment);\n      });\n    });\n    return flattened;\n  }\n\n  function primarySegmentInfo(target) {\n    const visibleGeometry = target && Array.isArray(target.visibleGeometry) ? target.visibleGeometry : [];\n    for (let i = 0; i < visibleGeometry.length; i += 1) {\n      const bucket = visibleGeometry[i];\n      const segments = bucket && Array.isArray(bucket.segments) ? bucket.segments : [];\n      if (!segments.length) {\n        continue;\n      }\n      return {\n        segment: segments[0],\n        osmId: bucket && bucket.osmId !== undefined ? bucket.osmId : null\n      };\n    }\n    return null;\n  }\n\n  /**\n   * Route line grammar:\n   * - single visible location: sentence(clause(loc))\n   * - start->end: \"From \" + endpoint(start) + \" to \" + endpoint(end)\n   * This avoids \"Near near ...\" and \"From in ...\".\n   */\n  function routeText(target) {\n    if (target && typeof target._mergedRouteText === \"string\" && target._mergedRouteText.trim()) {\n      return target._mergedRouteText.trim();\n    }\n    const segments = segmentList(target);\n    if (segments.length !== 1) {\n      return null;\n    }\n    const points = collectSegmentPoints(segments[0]);\n    if (!points.length) {\n      return null;\n    }\n    const startEndpoint = locationTextFromZone(points[0].zone, \"route_from\") ||\n      locationTextFromZone(points[0].zone, \"endpoint\");\n    const endEndpoint = locationTextFromZone(points[points.length - 1].zone, \"route_to\") ||\n      locationTextFromZone(points[points.length - 1].zone, \"endpoint\");\n\n    if (startEndpoint && endEndpoint && startEndpoint !== endEndpoint) {\n      return interpolate(\n        t(\"map_content_way_route_from_to\", \"From __start__ to __end__\"),\n        { start: startEndpoint, end: endEndpoint }\n      );\n    }\n    const single = locationTextFromZone(points[0].zone, \"clause\") || startEndpoint;\n    if (single) {\n      return interpolate(\n        t(\"map_content_way_route_near\", \"__location__\"),\n        { location: single.replace(/[.]+$/, \"\") }\n      );\n    }\n    return null;\n  }\n\n  function translatedWayType(subClass) {\n    if (!subClass || typeof subClass !== 'string') {\n      return null;\n    }\n    const typeKey = \"map_content_way_type_\" + subClass;\n    const translated = t(typeKey, typeKey);\n    if (translated !== typeKey) {\n      return translated;\n    }\n    return null;\n  }\n\n  function translatedWayTypePlural(subClass) {\n    if (!subClass || typeof subClass !== 'string') {\n      return null;\n    }\n    const typeKey = \"map_content_way_type_plural_\" + subClass;\n    const translated = t(typeKey, typeKey);\n    if (translated !== typeKey) {\n      return translated;\n    }\n    return null;\n  }\n\n  function normalizedWayName(name) {\n    if (!name || typeof name !== 'string') {\n      return null;\n    }\n    const trimmed = name.trim();\n    if (isUnnamedWayLabel(trimmed)) {\n      return null;\n    }\n    return trimmed.toLowerCase();\n  }\n\n  function wayOsmIds(group) {\n    const ways = group && Array.isArray(group.ways) ? group.ways : [];\n    const ids = [];\n    const seen = {};\n    ways.forEach(function(way){\n      if (!way || way.osmId === undefined || way.osmId === null) {\n        return;\n      }\n      const id = String(way.osmId);\n      if (seen[id]) {\n        return;\n      }\n      seen[id] = true;\n      ids.push(id);\n    });\n    return ids;\n  }\n\n  function groupConnectionKey(group) {\n    const ids = wayOsmIds(group);\n    if (ids.length) {\n      return \"ways:\" + ids.slice().sort().join(\"|\");\n    }\n    const normName = normalizedWayName(wayName(group));\n    return normName ? (\"name:\" + normName) : null;\n  }\n\n  function collectJunctionEvents(group) {\n    const events = [];\n    segmentList(group).forEach(function(segment){\n      const segmentEvents = segment && Array.isArray(segment.events) ? segment.events : [];\n      segmentEvents.forEach(function(event){\n        if (event && event.type === \"junction\") {\n          events.push(event);\n        }\n      });\n    });\n    return events;\n  }\n\n  function connectionSentence(labelInfo) {\n    if (!labelInfo || !labelInfo.value) {\n      if (!labelInfo || (labelInfo.mode !== \"type\" && labelInfo.mode !== \"type_many\")) {\n        return null;\n      }\n    }\n    if (labelInfo.mode === \"way\") {\n      return interpolate(\n        t(\"map_content_connects_to_way\", \"Connects to way __way__\"),\n        { way: labelInfo.value }\n      );\n    }\n    if (labelInfo.mode === \"ways\") {\n      return interpolate(\n        t(\"map_content_connects_to_ways\", \"Connects to ways __ways__\"),\n        { ways: labelInfo.value }\n      );\n    }\n    const singularType = translatedWayType(labelInfo.subClass) ||\n      t(\"map_content_way_type_A_other_ways\", \"other way\");\n    if (labelInfo.mode === \"type_many\") {\n      const pluralType = translatedWayTypePlural(labelInfo.subClass);\n      if (pluralType) {\n        return interpolate(\n          t(\"map_content_connects_to_type_many\", \"Connects to __count__ __type__\"),\n          { count: labelInfo.count, type: pluralType }\n        );\n      }\n      return interpolate(\n        t(\"map_content_connects_to_type_many_fallback\", \"Connects to __count__ ways of type __type__\"),\n        { count: labelInfo.count, type: singularType }\n      );\n    }\n    return interpolate(\n      t(\"map_content_connects_to_type\", \"Connects to __type__\"),\n      { type: singularType }\n    );\n  }\n\n  function buildNamedConnectionTextsMap(namedEntries) {\n    const groupsByKey = {};\n    const bucketsByKey = {};\n    const osmIdToKey = {};\n\n    namedEntries.forEach(function(entry){\n      const group = entry && entry.group ? entry.group : null;\n      if (!group) {\n        return;\n      }\n      const key = groupConnectionKey(group);\n      if (!key) {\n        return;\n      }\n      entry.connectionKey = key;\n      groupsByKey[key] = group;\n      bucketsByKey[key] = {\n        currentName: normalizedWayName(wayName(group)),\n        namedKeys: {},\n        namedLabels: {},\n        typeBuckets: {}\n      };\n      wayOsmIds(group).forEach(function(id){\n        osmIdToKey[id] = key;\n      });\n    });\n\n    Object.keys(groupsByKey).forEach(function(key){\n      const group = groupsByKey[key];\n      const bucket = bucketsByKey[key];\n      const ownIds = {};\n      wayOsmIds(group).forEach(function(id){\n        ownIds[id] = true;\n      });\n      let unknownCounter = 0;\n\n      collectJunctionEvents(group).forEach(function(event, eventIndex){\n        const connections = event && Array.isArray(event.connections) ? event.connections : [];\n        connections.forEach(function(connection, connectionIndex){\n          if (!connection || typeof connection !== 'object') {\n            return;\n          }\n          if (connection.osmType && connection.osmType !== \"way\") {\n            return;\n          }\n          const connectionId = connection.osmId !== undefined && connection.osmId !== null\n            ? String(connection.osmId)\n            : null;\n          const connectionSubClass = typeof connection.subClass === \"string\" ? connection.subClass : \"\";\n          if (isRailSubClass(connectionSubClass)) {\n            return;\n          }\n          if (connectionId && ownIds[connectionId]) {\n            return;\n          }\n\n          const rawName = typeof connection.name === 'string' ? connection.name.trim() : \"\";\n          const normName = normalizedWayName(rawName);\n          if (normName && bucket.currentName && normName === bucket.currentName) {\n            return;\n          }\n\n          if (connectionId) {\n            const namedTargetKey = osmIdToKey[connectionId];\n            if (namedTargetKey && namedTargetKey !== key) {\n              bucket.namedKeys[namedTargetKey] = true;\n              return;\n            }\n          }\n\n          if (normName) {\n            if (!bucket.namedLabels[normName]) {\n              bucket.namedLabels[normName] = rawName;\n            }\n            return;\n          }\n\n          const subClass = connectionSubClass\n            ? connectionSubClass\n            : \"A_other_ways\";\n          if (!bucket.typeBuckets[subClass]) {\n            bucket.typeBuckets[subClass] = {};\n          }\n          const dedupeToken = connectionId\n            ? (\"id:\" + connectionId)\n            : (\"event:\" + eventIndex + \":\" + connectionIndex + \":\" + (unknownCounter += 1));\n          bucket.typeBuckets[subClass][dedupeToken] = true;\n        });\n      });\n    });\n\n    Object.keys(bucketsByKey).forEach(function(sourceKey){\n      const source = bucketsByKey[sourceKey];\n      Object.keys(source.namedKeys).forEach(function(targetKey){\n        if (!bucketsByKey[targetKey] || targetKey === sourceKey) {\n          return;\n        }\n        bucketsByKey[targetKey].namedKeys[sourceKey] = true;\n      });\n    });\n\n    const connectionTextsByKey = {};\n    Object.keys(bucketsByKey).forEach(function(key){\n      const bucket = bucketsByKey[key];\n\n      Object.keys(bucket.namedKeys).forEach(function(targetKey){\n        const targetName = wayName(groupsByKey[targetKey]);\n        const norm = normalizedWayName(targetName);\n        if (norm && !bucket.namedLabels[norm]) {\n          bucket.namedLabels[norm] = targetName;\n        }\n      });\n\n      const texts = [];\n      const namedNames = Object.keys(bucket.namedLabels).map(function(norm){\n        return bucket.namedLabels[norm];\n      }).filter(function(value){ return !!value; });\n      namedNames.sort(function(a, b){ return a.localeCompare(b); });\n      if (namedNames.length === 1) {\n        const singleText = connectionSentence({ mode: \"way\", value: namedNames[0] });\n        if (singleText) {\n          texts.push(singleText);\n        }\n      } else if (namedNames.length > 1) {\n        const combinedText = connectionSentence({ mode: \"ways\", value: namedNames.join(\", \") });\n        if (combinedText) {\n          texts.push(combinedText);\n        }\n      }\n\n      const typeEntries = Object.keys(bucket.typeBuckets).map(function(subClass){\n        return {\n          subClass: subClass,\n          count: Object.keys(bucket.typeBuckets[subClass]).length\n        };\n      }).filter(function(entry){ return entry.count > 0; });\n      typeEntries.sort(function(a, b){\n        if (a.count !== b.count) {\n          return b.count - a.count;\n        }\n        const aLabel = translatedWayType(a.subClass) || t(\"map_content_way_type_A_other_ways\", \"other way\");\n        const bLabel = translatedWayType(b.subClass) || t(\"map_content_way_type_A_other_ways\", \"other way\");\n        return aLabel.localeCompare(bLabel);\n      });\n      typeEntries.forEach(function(entry){\n        const text = connectionSentence({\n          mode: entry.count > 1 ? \"type_many\" : \"type\",\n          subClass: entry.subClass,\n          count: entry.count\n        });\n        if (text) {\n          texts.push(text);\n        }\n      });\n\n      connectionTextsByKey[key] = texts;\n    });\n\n    return connectionTextsByKey;\n  }\n\n  function collectEdgeDetails(target) {\n    // Keep edge narration consistent with route narration: both should describe\n    // the same representative segment instead of mixing all grouped segments.\n    const primaryInfo = primarySegmentInfo(target);\n    const segments = primaryInfo && primaryInfo.segment ? [primaryInfo.segment] : [];\n    const found = {};\n    segments.forEach(function(segment){\n      const events = segment && Array.isArray(segment.events) ? segment.events : [];\n      events.forEach(function(event){\n        if (!event || event.type !== \"map_edge_crossing\" || !event.edge) {\n          return;\n        }\n        if (!found[event.edge]) {\n          found[event.edge] = { edge: event.edge, buckets: {} };\n        }\n        const zone = event.zone && typeof event.zone === 'object' ? event.zone : null;\n        if (!zone || zone.kind !== \"near_edge\") {\n          return;\n        }\n        const bucket = edgePositionBucketFromDirection(event.edge, zone.dir);\n        if (!bucket) {\n          return;\n        }\n        found[event.edge].buckets[bucket] = (found[event.edge].buckets[bucket] || 0) + 1;\n      });\n    });\n    const preferred = [\"north\", \"south\", \"east\", \"west\"];\n    const ordered = [];\n    preferred.forEach(function(edge){\n      if (found[edge]) {\n        ordered.push(found[edge]);\n        delete found[edge];\n      }\n    });\n    Object.keys(found).forEach(function(edge){\n      ordered.push(found[edge]);\n    });\n    return ordered.map(function(detail){\n      const present = Object.keys(detail.buckets).filter(function(bucket){\n        return detail.buckets[bucket] > 0;\n      });\n      let position = null;\n      if (present.length > 1) {\n        position = \"multiple\";\n      } else if (present.length === 1) {\n        position = present[0];\n      }\n      return {\n        edge: detail.edge,\n        position: position\n      };\n    });\n  }\n\n  function edgesText(target) {\n    if (target && Array.isArray(target._mergedEdgeTexts)) {\n      const mergedTexts = target._mergedEdgeTexts\n        .map(function(text){ return typeof text === \"string\" ? text.trim() : \"\"; })\n        .filter(function(text){ return !!text; });\n      if (mergedTexts.length > 1) {\n        return joinWithAnd(mergedTexts);\n      }\n      if (mergedTexts.length === 1) {\n        return mergedTexts[0];\n      }\n    }\n    if (target && typeof target._mergedEdgeText === \"string\" && target._mergedEdgeText.trim()) {\n      return target._mergedEdgeText.trim();\n    }\n    const details = collectEdgeDetails(target);\n    if (!details.length) {\n      return null;\n    }\n    const detailTexts = details.map(function(detail){\n      const base = interpolate(t(\"map_content_edge_phrase\", \"__edge__ edge\"), {\n        edge: edgeLabel(detail.edge)\n      });\n      const qualifier = edgePositionQualifier(detail.position);\n      return qualifier ? base + \" \" + qualifier : base;\n    });\n    const hasPosition = details.some(function(detail){ return !!detail.position; });\n    if (!hasPosition && details.length > 1) {\n      return interpolate(t(\"map_content_crosses_edges\", \"Crosses __edges__ edges\"), {\n        edges: joinWithAnd(details.map(function(detail){ return edgeLabel(detail.edge); }))\n      });\n    }\n    if (details.length === 1) {\n      const detail = details[0];\n      const base = interpolate(t(\"map_content_crosses_edge\", \"Crosses __edge__ edge\"), {\n        edge: edgeLabel(detail.edge)\n      });\n      const qualifier = edgePositionQualifier(detail.position);\n      return qualifier ? base + \" \" + qualifier : base;\n    }\n    return interpolate(\n      t(\"map_content_crosses_items\", \"Crosses __items__\"),\n      { items: joinWithAnd(detailTexts) }\n    );\n  }\n\n  function lanesText(item) {\n    const sem = item && item.semantics && typeof item.semantics === 'object' ? item.semantics : null;\n    const lanes = sem && sem.lanes && typeof sem.lanes === 'object' ? sem.lanes : null;\n    const total = lanes ? lanes.total : null;\n    if (total === null || total === undefined || isNaN(total)) {\n      return null;\n    }\n    const count = Math.round(Number(total));\n    if (!isFinite(count) || count <= 2) {\n      return null;\n    }\n    return interpolate(t(\"map_content_way_lanes_many\", \"__count__ lanes\"), { count: count });\n  }\n\n  function surfacePavingText(item) {\n    const klass = surfaceClass(item);\n    return surfacePavingTextFromClass(klass);\n  }\n\n  function surfaceClass(item) {\n    const sem = item && item.semantics && typeof item.semantics === 'object' ? item.semantics : null;\n    const surface = sem && sem.surface && typeof sem.surface === 'object' ? sem.surface : null;\n    const klass = surface && typeof surface.class === 'string' ? surface.class : null;\n    if (klass === \"paved\") {\n      return \"paved\";\n    }\n    if (klass === \"unpaved\") {\n      return \"unpaved\";\n    }\n    return \"unknown\";\n  }\n\n  function surfacePavingTextFromClass(klass) {\n    if (klass === \"paved\") {\n      return t(\"map_content_way_surface_paved\", \"Paved surface\");\n    }\n    if (klass === \"unpaved\") {\n      return t(\"map_content_way_surface_unpaved\", \"Unpaved surface\");\n    }\n    return t(\"map_content_way_surface_unknown\", \"Paving not known\");\n  }\n\n  function primaryWay(group) {\n    const ways = group && Array.isArray(group.ways) ? group.ways : [];\n    if (!ways.length) {\n      return null;\n    }\n    return ways[0];\n  }\n\n  function wayDetailsText(group) {\n    const parts = [];\n    const item = primaryWay(group);\n    if (!item) {\n      return \"\";\n    }\n    const lanes = lanesText(item);\n    if (lanes) {\n      parts.push(lanes);\n    }\n    const surface = surfacePavingText(item);\n    if (surface) {\n      parts.push(surface);\n    }\n    return parts.join(\", \");\n  }\n\n  function showConnectionsForEntry(entry) {\n    return !!(entry && (entry.sectionKey === \"roads\" || entry.sectionKey === \"paths\"));\n  }\n\n  function showRoadDetailsForEntry(entry) {\n    return !!(entry && entry.sectionKey === \"roads\");\n  }\n\n  function normalizeLineParts(parts) {\n    if (!parts || !parts.length) {\n      return [];\n    }\n    return parts.filter(function(part){\n      return part && part.text !== undefined && part.text !== null && part.text !== \"\";\n    }).map(function(part){\n      return {\n        text: String(part.text),\n        className: part.className || null,\n        wrap: part.wrap === false ? false : true\n      };\n    });\n  }\n\n  function addModelLine(item, parts, className, titleText, link) {\n    if (!item || !Array.isArray(item.lines)) {\n      return;\n    }\n    const lineParts = normalizeLineParts(parts);\n    if (!lineParts.length) {\n      return;\n    }\n    item.lines.push({\n      className: className || null,\n      parts: lineParts,\n      title: titleText || null,\n      link: link || null\n    });\n  }\n\n  function applyItemAttrs(listItem, attrs) {\n    if (!attrs || typeof attrs !== \"object\") {\n      return;\n    }\n    if (attrs.dataOsmId !== undefined && attrs.dataOsmId !== null) {\n      listItem.attr(\"data-osm-id\", String(attrs.dataOsmId));\n    }\n    if (attrs.dataUnnamedSurface) {\n      listItem.attr(\"data-unnamed-surface\", String(attrs.dataUnnamedSurface));\n    }\n    if (attrs.initiallyHidden) {\n      listItem.attr(\"data-initially-hidden\", \"true\");\n    }\n  }\n\n  function appendLineParts(target, parts) {\n    parts.forEach(function(part){\n      if (!part || part.text === undefined || part.text === null || part.text === \"\") {\n        return;\n      }\n      if (part.wrap === false) {\n        target.append(String(part.text));\n        return;\n      }\n      const span = $(\"<span>\").text(String(part.text));\n      if (part.className) {\n        span.addClass(part.className);\n      }\n      target.append(span);\n    });\n  }\n\n  function renderLineFromModel(listItem, lineModel) {\n    if (!lineModel || !Array.isArray(lineModel.parts) || !lineModel.parts.length) {\n      return;\n    }\n    const tagName = lineModel.className === \"map-content-title-line\" ? \"h5\" : \"div\";\n    const line = $(\"<\" + tagName + \">\");\n    if (lineModel.className) {\n      line.addClass(lineModel.className);\n    }\n    const titleLink = lineModel.className === \"map-content-title-line\"\n      ? normalizedExternalLink(lineModel.link)\n      : null;\n    appendLineParts(line, lineModel.parts);\n\n    if (titleLink) {\n      const anchor = $(\"<a>\")\n        .addClass(\"map-content-inline-external-link\")\n        .attr(\"href\", titleLink.url)\n        .attr(\"target\", \"_blank\")\n        .attr(\"rel\", \"noopener noreferrer\");\n      anchor.append($(\"<span>\").text(titleLink.label || \"Website\"));\n      anchor.append($(\"<span>\").addClass(\"map-content-external-indicator\").attr(\"aria-hidden\", \"true\").text(\"↗\"));\n      anchor.append($(\"<span>\").addClass(\"visuallyhidden\").text(\n        \" \" + t(\"map_content_external_link_aria\", \"External link, opens in a new tab\")\n      ));\n      line.append($(\"<span>\").addClass(\"map-content-external-link-sep\").text(\" \"));\n      line.append(anchor);\n    }\n\n    if (lineModel.title) {\n      line.addClass(\"map-content-has-importance-popup\");\n      const popup = $(\"<span>\").addClass(\"map-content-importance-popup\");\n      popup.append($(\"<pre>\").text(lineModel.title));\n      line.append(popup);\n    }\n    if (!line.text()) {\n      return;\n    }\n    listItem.append(line);\n  }\n\n  function renderItemFromModel(item, listElem) {\n    if (!item || !listElem || !listElem.length) {\n      return;\n    }\n    const listItem = $(\"<li>\").addClass(\"map-content-way\");\n    applyItemAttrs(listItem, item.attrs);\n    const lines = Array.isArray(item.lines) ? item.lines : [];\n    lines.forEach(function(lineModel){\n      renderLineFromModel(listItem, lineModel);\n    });\n    listElem.append(listItem);\n  }\n\n  function buildWayItemModel(entry, connectionTextsByKey) {\n    const group = entry && entry.group ? entry.group : null;\n    if (!group) {\n      return null;\n    }\n    const item = {\n      type: \"way\",\n      attrs: {},\n      lines: []\n    };\n    const mainWay = primaryWay(group);\n    if (mainWay && mainWay.osmId !== undefined && mainWay.osmId !== null) {\n      item.attrs.dataOsmId = String(mainWay.osmId);\n    }\n\n    const typeText = entry.subclassType || null;\n    const nameText = wayName(group);\n    const lengthText = formatLength(group);\n    const scoreTooltip = importanceScoreTooltip(group.importanceScore);\n    const titleLink = bestGroupExternalLink(group);\n    let lineText = \"\";\n\n    if (!nameText) {\n      if (entry && entry.sectionKey === \"waterways\") {\n        lineText = t(\"map_content_way_unnamed_waterway\", \"Unnamed waterway\");\n      } else if (entry && entry.sectionKey === \"railways\") {\n        lineText = t(\"map_content_way_unnamed_railway\", \"Unnamed railway\");\n      } else {\n        lineText = t(\"map_content_way_unnamed\", \"Unnamed way\");\n      }\n      item.attrs.dataIsNamed = false;\n    } else if (typeText) {\n      lineText = typeText + \" \" + nameText;\n      item.attrs.dataIsNamed = true;\n    } else {\n      lineText = nameText;\n      item.attrs.dataIsNamed = true;\n    }\n    item.summaryTitle = capitalizeFirst(lineText);\n    if (lengthText) {\n      lineText += \", \" + lengthText;\n    }\n\n    addModelLine(item, [\n      { text: capitalizeFirst(lineText), className: \"map-content-title\" }\n    ], \"map-content-title-line\", scoreTooltip, titleLink);\n\n    const routeSummary = routeText(group);\n    if (routeSummary) {\n      addModelLine(item, [\n        { text: capitalizeFirst(routeSummary), className: \"map-content-location-text\" }\n      ], \"map-content-location\");\n    }\n\n    const edgeSummary = edgesText(group);\n    if (edgeSummary) {\n      addModelLine(item, [\n        { text: capitalizeFirst(edgeSummary), className: \"map-content-location-text\" }\n      ], \"map-content-location\");\n    }\n\n    if (showConnectionsForEntry(entry)) {\n      const key = entry && entry.connectionKey ? entry.connectionKey : null;\n      const connectionLines = key && connectionTextsByKey && connectionTextsByKey[key]\n        ? connectionTextsByKey[key]\n        : [];\n      connectionLines.forEach(function(connectionLine){\n        addModelLine(item, [\n          { text: capitalizeFirst(connectionLine), className: \"map-content-location-text\" }\n        ], \"map-content-location\");\n      });\n    }\n\n    if (showRoadDetailsForEntry(entry)) {\n      const detailsText = wayDetailsText(group);\n      if (detailsText) {\n        addModelLine(item, [\n          { text: capitalizeFirst(detailsText), className: \"map-content-way-details-text\" }\n        ], \"map-content-way-details\");\n      }\n    }\n\n    return item;\n  }\n\n  function unnamedSurfaceClass(entry) {\n    const item = primaryWay(entry && entry.group ? entry.group : null);\n    return surfaceClass(item);\n  }\n\n  function summarizeUnnamedWays(entries) {\n    const buckets = {\n      paved: { surfaceClass: \"paved\", count: 0, totalLength: 0 },\n      unpaved: { surfaceClass: \"unpaved\", count: 0, totalLength: 0 },\n      unknown: { surfaceClass: \"unknown\", count: 0, totalLength: 0 }\n    };\n    const orderedClasses = [\"paved\", \"unpaved\", \"unknown\"];\n\n    entries.forEach(function(entry){\n      const klass = unnamedSurfaceClass(entry);\n      const bucket = buckets[klass] || buckets.unknown;\n      bucket.count += 1;\n      bucket.totalLength += wayLengthValue(entry.group);\n    });\n\n    return orderedClasses\n      .map(function(klass){ return buckets[klass]; })\n      .filter(function(summary){ return summary.count > 0; });\n  }\n\n  function summarizeUnnamedNonRoadWays(entries) {\n    const buckets = {};\n    entries.forEach(function(entry){\n      if (!entry || !entry.subClass) {\n        return;\n      }\n      if (!buckets[entry.subClass]) {\n        buckets[entry.subClass] = {\n          subClass: entry.subClass,\n          count: 0,\n          totalLength: 0\n        };\n      }\n      buckets[entry.subClass].count += 1;\n      buckets[entry.subClass].totalLength += wayLengthValue(entry.group);\n    });\n\n    return Object.keys(buckets).map(function(subClass){\n      return buckets[subClass];\n    }).sort(function(a, b){\n      if (a.count !== b.count) {\n        return b.count - a.count;\n      }\n      const aLabel = translatedWayType(a.subClass) || t(\"map_content_way_type_A_other_ways\", \"other way\");\n      const bLabel = translatedWayType(b.subClass) || t(\"map_content_way_type_A_other_ways\", \"other way\");\n      return aLabel.localeCompare(bLabel);\n    });\n  }\n\n  function buildUnnamedWaySummaryModel(summary) {\n    if (!summary) {\n      return null;\n    }\n    const item = {\n      type: \"summary\",\n      attrs: {\n        dataUnnamedSurface: summary.surfaceClass,\n        dataIsNamed: false\n      },\n      lines: []\n    };\n\n    const lengthText = formatLength({ totalLength: summary.totalLength });\n    let titleText = unnamedRoadsCountText(summary.count) || t(\"content__unnamed_roads\", \"Unnamed roads\");\n    if (lengthText) {\n      titleText += \", \" + lengthText;\n    }\n    addModelLine(item, [\n      { text: capitalizeFirst(titleText), className: \"map-content-title\" }\n    ], \"map-content-title-line\");\n\n    const surfaceText = surfacePavingTextFromClass(summary.surfaceClass);\n    if (surfaceText) {\n      addModelLine(item, [\n        { text: capitalizeFirst(surfaceText), className: \"map-content-way-details-text\" }\n      ], \"map-content-way-details\");\n    }\n\n    return item;\n  }\n\n  function buildUnnamedNonRoadSummaryModel(summary) {\n    if (!summary) {\n      return null;\n    }\n    const item = {\n      type: \"summary\",\n      attrs: {\n        dataIsNamed: false\n      },\n      lines: []\n    };\n    const singularType = translatedWayType(summary.subClass) || t(\"map_content_way_type_A_other_ways\", \"other way\");\n    const pluralType = translatedWayTypePlural(summary.subClass) || singularType;\n    const typeText = summary.count === 1 ? singularType : pluralType;\n    const key = summary.count === 1 ? \"map_content_unnamed_features_one\" : \"map_content_unnamed_features_many\";\n    let titleText = interpolate(\n      t(key, \"__count__ unnamed __type__\"),\n      { count: summary.count, type: typeText }\n    );\n    const lengthText = formatLength({ totalLength: summary.totalLength });\n    if (lengthText) {\n      titleText += \", \" + lengthText;\n    }\n    addModelLine(item, [\n      { text: capitalizeFirst(titleText), className: \"map-content-title\" }\n    ], \"map-content-title-line\");\n    return item;\n  }\n\n  function showUnnamedAsItem(entry) {\n    if (!entry || !entry.sectionKey) {\n      return false;\n    }\n    return entry.sectionKey === \"railways\" ||\n      entry.sectionKey === \"waterways\" ||\n      entry.sectionKey === \"otherLinear\";\n  }\n\n  function isUnnamedWaterwayEntry(entry) {\n    return !!(entry && entry.sectionKey === \"waterways\" && !wayName(entry.group));\n  }\n\n  function unnamedWaterwayLocationText(entry) {\n    if (!entry || !entry.group) {\n      return null;\n    }\n    const text = routeText(entry.group);\n    if (!text || typeof text !== \"string\") {\n      return null;\n    }\n    const trimmed = text.trim();\n    return trimmed || null;\n  }\n\n  function unnamedWaterwayLocationKey(locationText) {\n    if (!locationText || typeof locationText !== \"string\") {\n      return null;\n    }\n    const trimmed = locationText.trim();\n    return trimmed ? trimmed.toLowerCase() : null;\n  }\n\n  function buildMergedUnnamedWaterwayModel(summary) {\n    if (!summary || summary.count < 2) {\n      return null;\n    }\n    const item = {\n      type: \"summary\",\n      attrs: {\n        dataIsNamed: false\n      },\n      lines: []\n    };\n    const lengthText = formatLength({ totalLength: summary.totalLength });\n    const singularType = translatedWayType(\"A4_other_waterways\") ||\n      t(\"map_content_way_type_A4_other_waterways\", \"other waterway\");\n    const pluralType = translatedWayTypePlural(\"A4_other_waterways\") || singularType;\n    const typeText = summary.count === 1 ? singularType : pluralType;\n    const key = summary.count === 1 ? \"map_content_unnamed_features_one\" : \"map_content_unnamed_features_many\";\n    let titleText = interpolate(\n      t(key, \"__count__ unnamed __type__\"),\n      { count: summary.count, type: typeText }\n    );\n    if (lengthText) {\n      titleText += \", \" + lengthText;\n    }\n    addModelLine(item, [\n      { text: capitalizeFirst(titleText), className: \"map-content-title\" }\n    ], \"map-content-title-line\");\n    if (summary.locationText) {\n      addModelLine(item, [\n        { text: capitalizeFirst(summary.locationText), className: \"map-content-location-text\" }\n      ], \"map-content-location\");\n    }\n    return item;\n  }\n\n  function buildModel(mapContent, helpers, options) {\n    setTranslator(helpers);\n    const resolved = normalizeOptions(options);\n    const entries = collectWayGroups(mapContent, resolved);\n    const itemEntries = [];\n    const unnamedEntries = [];\n    const model = [];\n\n    entries.forEach(function(entry){\n      if (wayName(entry.group) || showUnnamedAsItem(entry)) {\n        itemEntries.push(entry);\n      } else {\n        unnamedEntries.push(entry);\n      }\n    });\n\n    const connectionTextsByKey = buildNamedConnectionTextsMap(\n      itemEntries.filter(function(entry){\n        return showConnectionsForEntry(entry) && !!wayName(entry.group);\n      })\n    );\n\n    const unnamedWaterwayBucketsByKey = {};\n    const unnamedWaterwayKeyByIndex = {};\n    itemEntries.forEach(function(entry, index){\n      if (!isUnnamedWaterwayEntry(entry)) {\n        return;\n      }\n      const locationText = unnamedWaterwayLocationText(entry);\n      const locationKey = unnamedWaterwayLocationKey(locationText);\n      if (!locationKey) {\n        return;\n      }\n      unnamedWaterwayKeyByIndex[index] = locationKey;\n      if (!unnamedWaterwayBucketsByKey[locationKey]) {\n        unnamedWaterwayBucketsByKey[locationKey] = {\n          count: 0,\n          totalLength: 0,\n          locationText: locationText,\n          firstIndex: index\n        };\n      }\n      const bucket = unnamedWaterwayBucketsByKey[locationKey];\n      bucket.count += 1;\n      bucket.totalLength += wayLengthValue(entry.group);\n      if (index < bucket.firstIndex) {\n        bucket.firstIndex = index;\n      }\n    });\n\n    itemEntries.forEach(function(entry, index){\n      const locationKey = unnamedWaterwayKeyByIndex[index];\n      if (locationKey) {\n        const bucket = unnamedWaterwayBucketsByKey[locationKey];\n        if (bucket && bucket.count > 1) {\n          if (bucket.firstIndex === index) {\n            const mergedItem = buildMergedUnnamedWaterwayModel(bucket);\n            if (mergedItem) {\n              model.push(mergedItem);\n            }\n          }\n          return;\n        }\n      }\n\n      const item = buildWayItemModel(entry, connectionTextsByKey);\n      if (item) {\n        model.push(item);\n      }\n    });\n\n    const unnamedRoadEntries = unnamedEntries.filter(function(entry){\n      return entry.sectionKey === \"roads\";\n    });\n    const unnamedPathEntries = unnamedEntries.filter(function(entry){\n      return entry.sectionKey === \"paths\";\n    });\n\n    const unnamedRoadSummaries = summarizeUnnamedWays(unnamedRoadEntries);\n    unnamedRoadSummaries.forEach(function(summary){\n      const item = buildUnnamedWaySummaryModel(summary);\n      if (item) {\n        model.push(item);\n      }\n    });\n\n    const unnamedNonRoadSummaries = summarizeUnnamedNonRoadWays(unnamedPathEntries);\n    unnamedNonRoadSummaries.forEach(function(summary){\n      const item = buildUnnamedNonRoadSummaryModel(summary);\n      if (item) {\n        model.push(item);\n      }\n    });\n\n    return model;\n  }\n\n  function renderFromModel(model, listElem) {\n    if (!listElem || !listElem.length) {\n      return 0;\n    }\n    const items = Array.isArray(model) ? model : [];\n    listElem.empty();\n    items.forEach(function(item){\n      renderItemFromModel(item, listElem);\n    });\n    return items.length;\n  }\n\n  function render(mapContent, listElem, helpers, options) {\n    if (!listElem || !listElem.length) {\n      return 0;\n    }\n    const model = buildModel(mapContent, helpers, options);\n    return renderFromModel(model, listElem);\n  }\n\n  function emptyMessage(helpers, options) {\n    setTranslator(helpers);\n    const resolved = normalizeOptions(options);\n    if (resolved.section === \"roads\") {\n      return t(\"map_content_no_roads\", \"No roads listed for this map.\");\n    }\n    if (resolved.section === \"paths\") {\n      return t(\"map_content_no_paths\", \"No paths listed for this map.\");\n    }\n    if (resolved.section === \"railways\") {\n      return t(\"map_content_no_railways\", \"No railways listed for this map.\");\n    }\n    if (resolved.section === \"waterways\") {\n      return t(\"map_content_no_waterways\", \"No waterways listed for this map.\");\n    }\n    if (resolved.section === \"otherLinear\") {\n      return t(\"map_content_no_other_linear\", \"No other linear features listed for this map.\");\n    }\n    return t(\"map_content_no_roads\", \"No roads listed for this map.\");\n  }\n\n  window.TM = window.TM || {};\n  window.TM.mapDescWays = {\n    buildModel: buildModel,\n    renderFromModel: renderFromModel,\n    render: render,\n    emptyMessage: emptyMessage\n  };\n})();\n"
  },
  {
    "path": "web/src/scripts/map-description.js",
    "content": "/* global $ */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0, space-infix-ops:0, camelcase:0, dot-notation:0 */\n\n/*\n * Build short, human-friendly map descriptions from map-content.json.\n */\n(function(){\n  'use strict';\n\n  const MAX_VISIBLE_BUILDINGS = 10;\n  const MAX_VISIBLE_LINEAR_ITEMS = 10;\n  const MAX_VISIBLE_WATER_AREA_ITEMS = 10;\n  const MAX_VISIBLE_POI_ITEMS = 8;\n  const SUMMARY_MAX_ITEMS = 10;\n  const SUMMARY_SECTION_PENALTY = 0.5;\n  const SHOW_IMPORTANCE_TOOLTIPS = false;\n  const LINEAR_SECTION_CONFIGS = [\n    {\n      key: \"roads\",\n      rowSelector: \".map-content-roads-row\",\n      listSelector: \".map-content-roads\",\n      alwaysShow: true,\n      maxVisible: MAX_VISIBLE_LINEAR_ITEMS,\n      toggleClass: \"map-content-roads-toggle\"\n    },\n    {\n      key: \"paths\",\n      rowSelector: \".map-content-paths-row\",\n      listSelector: \".map-content-paths\",\n      alwaysShow: false,\n      maxVisible: MAX_VISIBLE_LINEAR_ITEMS,\n      toggleClass: \"map-content-paths-toggle\"\n    },\n    {\n      key: \"railways\",\n      rowSelector: \".map-content-railways-row\",\n      listSelector: \".map-content-railways\",\n      alwaysShow: false,\n      maxVisible: MAX_VISIBLE_LINEAR_ITEMS,\n      toggleClass: \"map-content-railways-toggle\"\n    },\n    {\n      key: \"waterways\",\n      rowSelector: \".map-content-waterways-row\",\n      listSelector: \".map-content-waterways\",\n      alwaysShow: false,\n      maxVisible: MAX_VISIBLE_LINEAR_ITEMS,\n      toggleClass: \"map-content-waterways-toggle\"\n    },\n    {\n      key: \"otherLinear\",\n      rowSelector: \".map-content-other-linear-row\",\n      listSelector: \".map-content-other-linear\",\n      alwaysShow: false,\n      maxVisible: MAX_VISIBLE_LINEAR_ITEMS,\n      toggleClass: \"map-content-other-linear-toggle\"\n    }\n  ];\n  const POI_SECTION_CONFIGS = [\n    {\n      key: \"poiFamiliar\",\n      rendererSection: \"familiar_places\",\n      rowSelector: \".map-content-poi-familiar-row\",\n      listSelector: \".map-content-poi-familiar\",\n      alwaysShow: false,\n      maxVisible: MAX_VISIBLE_POI_ITEMS,\n      fallbackKey: \"map_content_no_poi_familiar\",\n      fallbackText: \"No familiar places listed for this map.\",\n      toggleClass: \"map-content-poi-familiar-toggle\",\n      includeSparseNote: false\n    },\n    {\n      key: \"poiDaily\",\n      rendererSection: \"daily_essentials\",\n      rowSelector: \".map-content-poi-daily-row\",\n      listSelector: \".map-content-poi-daily\",\n      alwaysShow: false,\n      maxVisible: MAX_VISIBLE_POI_ITEMS,\n      fallbackKey: \"map_content_no_poi_daily\",\n      fallbackText: \"No daily essentials listed for this map.\",\n      toggleClass: \"map-content-poi-daily-toggle\",\n      includeSparseNote: false\n    },\n    {\n      key: \"poiTransport\",\n      rendererSection: \"transport_points\",\n      rowSelector: \".map-content-poi-transport-row\",\n      listSelector: \".map-content-poi-transport\",\n      alwaysShow: false,\n      maxVisible: MAX_VISIBLE_POI_ITEMS,\n      fallbackKey: \"map_content_no_poi_transport\",\n      fallbackText: \"No transport points listed for this map.\",\n      toggleClass: \"map-content-poi-transport-toggle\",\n      includeSparseNote: false\n    },\n  ];\n  const WATER_AREA_SECTION_CONFIG = {\n    key: \"waterAreas\",\n    rendererSection: \"water_areas\",\n    rowSelector: \".map-content-water-areas-row\",\n    listSelector: \".map-content-water-areas\",\n    alwaysShow: false,\n    maxVisible: MAX_VISIBLE_WATER_AREA_ITEMS,\n    fallbackKey: \"map_content_no_water_areas\",\n    fallbackText: \"No water areas listed for this map.\",\n    toggleClass: \"map-content-water-areas-toggle\"\n  };\n  const SECTION_HEIGHT_DEFAULT_PROFILES = {\n    roads: [\"raised:0.82\"],\n    paths: [\"raised:1.5\"],\n    railways: [\"raised:0.81\"],\n    waterways: [\"waved_surface\"],\n    waterAreas: [\"waved_surface\"],\n    otherLinear: [\"varying\"],\n    buildings: [\"raised:2.9\"],\n    poiFamiliar: [\"text_only\"],\n    poiDaily: [\"text_only\"],\n    poiTransport: [\"text_only\"]\n  };\n\n  function translations() {\n    return window.TM && window.TM.translations ? window.TM.translations : {};\n  }\n\n  function t(key, fallback) {\n    const dict = translations();\n    if (dict[key] !== undefined && dict[key] !== null && dict[key] !== \"\") {\n      return dict[key];\n    }\n    return fallback !== undefined ? fallback : key;\n  }\n\n  function interpolate(text, replacements) {\n    if (!replacements) {\n      return text;\n    }\n    return text.replace(/__([a-zA-Z0-9_]+)__/g, function(match, name) {\n      if (replacements[name] === undefined) {\n        return match;\n      }\n      return replacements[name];\n    });\n  }\n\n  function showMoreBuildingsLabel(hiddenCount, translateFn) {\n    const tr = typeof translateFn === \"function\" ? translateFn : t;\n    if (hiddenCount === 1) {\n      return tr(\"map_content_show_more_buildings_one\", \"Show 1 more building\");\n    }\n    return interpolate(\n      tr(\"map_content_show_more_buildings_many\", \"Show __count__ more buildings\"),\n      { count: hiddenCount }\n    );\n  }\n\n  function showMorePoisLabel(hiddenCount, translateFn) {\n    const tr = typeof translateFn === \"function\" ? translateFn : t;\n    if (hiddenCount === 1) {\n      return tr(\"map_content_show_more_pois_one\", \"Show 1 more place\");\n    }\n    return interpolate(\n      tr(\"map_content_show_more_pois_many\", \"Show __count__ more places\"),\n      { count: hiddenCount }\n    );\n  }\n\n  function showMoreFeaturesLabel(hiddenCount, translateFn) {\n    const tr = typeof translateFn === \"function\" ? translateFn : t;\n    if (hiddenCount === 1) {\n      return tr(\"map_content_show_more_features_one\", \"Show 1 more feature\");\n    }\n    return interpolate(\n      tr(\"map_content_show_more_features_many\", \"Show __count__ more features\"),\n      { count: hiddenCount }\n    );\n  }\n\n  function formatHeightMillimeters(mm) {\n    const number = Number(mm);\n    if (!isFinite(number)) {\n      return null;\n    }\n    const rounded = Math.round(number * 10) / 10;\n    if (Math.abs(rounded - Math.round(rounded)) < 1e-9) {\n      return String(Math.round(rounded));\n    }\n    return String(rounded);\n  }\n\n  function sectionForLinearSubClassKey(subClassKey) {\n    if (!subClassKey || typeof subClassKey !== \"string\") {\n      return null;\n    }\n    if (subClassKey.indexOf(\"A1_\") === 0) {\n      return \"roads\";\n    }\n    if (subClassKey.indexOf(\"A2_\") === 0) {\n      return \"paths\";\n    }\n    if (subClassKey.indexOf(\"A3_\") === 0) {\n      return \"railways\";\n    }\n    if (subClassKey.indexOf(\"A4_\") === 0) {\n      return \"waterways\";\n    }\n    if (subClassKey.indexOf(\"A5_\") === 0) {\n      return null;\n    }\n    if (subClassKey.indexOf(\"A\") === 0) {\n      return \"otherLinear\";\n    }\n    return null;\n  }\n\n  function profileForLinearSubClassKey(subClassKey) {\n    const sectionKey = sectionForLinearSubClassKey(subClassKey);\n    if (sectionKey === \"roads\") {\n      return \"raised:0.82\";\n    }\n    if (sectionKey === \"paths\") {\n      return \"raised:1.5\";\n    }\n    if (sectionKey === \"railways\") {\n      return \"raised:0.81\";\n    }\n    if (sectionKey === \"waterways\") {\n      return \"waved_surface\";\n    }\n    if (sectionKey === \"otherLinear\") {\n      return \"varying\";\n    }\n    return null;\n  }\n\n  function collectSectionHeightProfiles(mapContent) {\n    const profilesBySection = {};\n    Object.keys(SECTION_HEIGHT_DEFAULT_PROFILES).forEach(function(sectionKey){\n      profilesBySection[sectionKey] = new Set();\n    });\n    const payload = mapContent && typeof mapContent === \"object\" ? mapContent : {};\n    const classA = payload.A;\n    const linearSubclasses = classA && Array.isArray(classA.subclasses) ? classA.subclasses : [];\n    linearSubclasses.forEach(function(subclass){\n      if (!subclass || subclass.kind !== \"linear\") {\n        return;\n      }\n      const groups = Array.isArray(subclass.groups) ? subclass.groups : [];\n      if (!groups.length) {\n        return;\n      }\n      const sectionKey = sectionForLinearSubClassKey(subclass.key);\n      if (!sectionKey || !profilesBySection[sectionKey]) {\n        return;\n      }\n      const profile = profileForLinearSubClassKey(subclass.key);\n      if (!profile) {\n        return;\n      }\n      profilesBySection[sectionKey].add(profile);\n    });\n\n    const classC = payload.C;\n    const buildingSubclasses = classC && Array.isArray(classC.subclasses) ? classC.subclasses : [];\n    buildingSubclasses.forEach(function(subclass){\n      if (!subclass || subclass.kind !== \"building\") {\n        return;\n      }\n      const groups = Array.isArray(subclass.groups) ? subclass.groups : [];\n      if (!groups.length) {\n        return;\n      }\n      profilesBySection.buildings.add(\"raised:2.9\");\n    });\n\n    const classB = payload.B;\n    const areaSubclasses = classB && Array.isArray(classB.subclasses) ? classB.subclasses : [];\n    areaSubclasses.forEach(function(subclass){\n      if (!subclass || subclass.kind !== \"area\" || typeof subclass.key !== \"string\") {\n        return;\n      }\n      if (subclass.key.indexOf(\"B1_\") !== 0) {\n        return;\n      }\n      const groups = Array.isArray(subclass.groups) ? subclass.groups : [];\n      if (!groups.length) {\n        return;\n      }\n      profilesBySection.waterAreas.add(\"waved_surface\");\n    });\n\n    return profilesBySection;\n  }\n\n  function sectionHeightNoteForProfiles(profiles, helpers) {\n    const profileSet = profiles instanceof Set ? profiles : new Set();\n    if (!profileSet.size) {\n      return translateWithHelpers(\n        helpers,\n        \"map_content_height_note_varying\",\n        \"Raised by varying amounts\"\n      );\n    }\n    if (profileSet.has(\"varying\") || profileSet.size > 1) {\n      return translateWithHelpers(\n        helpers,\n        \"map_content_height_note_varying\",\n        \"Raised by varying amounts\"\n      );\n    }\n    if (profileSet.has(\"waved_surface\")) {\n      return translateWithHelpers(\n        helpers,\n        \"map_content_height_note_waved_surface\",\n        \"Waved surface\"\n      );\n    }\n    if (profileSet.has(\"text_only\")) {\n      return translateWithHelpers(\n        helpers,\n        \"map_content_height_note_text_only\",\n        \"Textual information only\"\n      );\n    }\n    const onlyProfile = Array.from(profileSet)[0];\n    if (onlyProfile && onlyProfile.indexOf(\"raised:\") === 0) {\n      const millimeters = formatHeightMillimeters(onlyProfile.slice(\"raised:\".length));\n      if (millimeters !== null) {\n        return interpolate(\n          translateWithHelpers(\n            helpers,\n            \"map_content_height_note_raised_mm\",\n            \"Raised __millimeters__ mm\"\n          ),\n          { millimeters: millimeters }\n        );\n      }\n    }\n    return translateWithHelpers(\n      helpers,\n      \"map_content_height_note_varying\",\n      \"Raised by varying amounts\"\n    );\n  }\n\n  function buildSectionHeightNotes(mapContent, helpers) {\n    const discoveredProfiles = collectSectionHeightProfiles(mapContent);\n    const notes = {};\n    Object.keys(SECTION_HEIGHT_DEFAULT_PROFILES).forEach(function(sectionKey){\n      const defaultProfiles = new Set(SECTION_HEIGHT_DEFAULT_PROFILES[sectionKey] || []);\n      const discovered = discoveredProfiles[sectionKey];\n      const profiles = discovered && discovered.size ? discovered : defaultProfiles;\n      notes[sectionKey] = sectionHeightNoteForProfiles(profiles, helpers);\n    });\n    return notes;\n  }\n\n  function parseMapContent(payload) {\n    function currentUiLanguage() {\n      const fromPath = (typeof window !== \"undefined\" && window.location && typeof window.location.pathname === \"string\")\n        ? window.location.pathname.match(/^\\/([a-z]{2})(?:[\\/?#]|$)/i)\n        : null;\n      if (fromPath && fromPath[1]) {\n        return fromPath[1].toLowerCase();\n      }\n      const docLang = (typeof document !== \"undefined\" && document.documentElement && typeof document.documentElement.lang === \"string\")\n        ? document.documentElement.lang.trim().toLowerCase()\n        : \"\";\n      if (docLang) {\n        return docLang.split(\"-\")[0];\n      }\n      return null;\n    }\n\n    function localeCandidates(lang) {\n      if (!lang || typeof lang !== \"string\") {\n        return [];\n      }\n      const normalized = lang.trim().toLowerCase();\n      if (!normalized) {\n        return [];\n      }\n      const parts = normalized.split(\"-\").filter(Boolean);\n      const candidates = [];\n      if (parts.length > 1) {\n        candidates.push(parts.join(\"-\"));\n      }\n      if (parts.length) {\n        candidates.push(parts[0]);\n      }\n      return candidates.filter(function(value, index, arr){\n        return arr.indexOf(value) === index;\n      });\n    }\n\n    function pickLocalizedName(extraNames, candidates) {\n      if (!extraNames || typeof extraNames !== \"object\" || !Array.isArray(candidates) || !candidates.length) {\n        return null;\n      }\n      for (let i = 0; i < candidates.length; i += 1) {\n        const key = \"name:\" + candidates[i];\n        if (typeof extraNames[key] === \"string\" && extraNames[key].trim()) {\n          return extraNames[key].trim();\n        }\n      }\n      for (let i = 0; i < candidates.length; i += 1) {\n        const prefix = \"name:\" + candidates[i] + \"-\";\n        const keys = Object.keys(extraNames);\n        for (let j = 0; j < keys.length; j += 1) {\n          const key = keys[j];\n          if (typeof key === \"string\" && key.indexOf(prefix) === 0 &&\n              typeof extraNames[key] === \"string\" && extraNames[key].trim()) {\n            return extraNames[key].trim();\n          }\n        }\n      }\n      return null;\n    }\n\n    function replaceLabelInDisplayLabel(displayLabel, currentLabel, localizedLabel) {\n      if (typeof displayLabel !== \"string\" || !displayLabel.trim() ||\n          typeof currentLabel !== \"string\" || !currentLabel.trim() ||\n          typeof localizedLabel !== \"string\" || !localizedLabel.trim()) {\n        return displayLabel;\n      }\n      if (displayLabel === currentLabel) {\n        return localizedLabel;\n      }\n      return displayLabel.indexOf(currentLabel) >= 0\n        ? displayLabel.replace(currentLabel, localizedLabel)\n        : displayLabel;\n    }\n\n    function localizedNameFromChildren(entity, candidates) {\n      if (!entity || typeof entity !== \"object\") {\n        return null;\n      }\n      const childLists = [\n        Array.isArray(entity.ways) ? entity.ways : [],\n        Array.isArray(entity.items) ? entity.items : []\n      ];\n      const labels = [];\n      childLists.forEach(function(list){\n        list.forEach(function(child){\n          if (!child || typeof child !== \"object\") {\n            return;\n          }\n          const extraNames = child.importanceTags && child.importanceTags.extraNames;\n          const localized = pickLocalizedName(extraNames, candidates);\n          if (localized) {\n            labels.push(localized);\n          }\n        });\n      });\n      if (!labels.length) {\n        return null;\n      }\n      const first = labels[0];\n      const uniform = labels.every(function(label){ return label === first; });\n      return uniform ? first : null;\n    }\n\n    function localizeEntityName(entity, candidates) {\n      if (!entity || typeof entity !== \"object\" || Array.isArray(entity)) {\n        return;\n      }\n      const currentLabel = typeof entity.label === \"string\" ? entity.label.trim() : \"\";\n      if (!currentLabel) {\n        return;\n      }\n      const selfExtraNames = entity.importanceTags && entity.importanceTags.extraNames;\n      const localized = pickLocalizedName(selfExtraNames, candidates) || localizedNameFromChildren(entity, candidates);\n      if (!localized || localized === currentLabel) {\n        return;\n      }\n      entity.label = localized;\n      if (typeof entity.displayLabel === \"string\") {\n        entity.displayLabel = replaceLabelInDisplayLabel(entity.displayLabel, currentLabel, localized);\n      }\n    }\n\n    function localizePayloadNames(root, lang) {\n      const candidates = localeCandidates(lang);\n      if (!root || !candidates.length) {\n        return root;\n      }\n      function walk(value) {\n        if (!value || typeof value !== \"object\") {\n          return;\n        }\n        if (Array.isArray(value)) {\n          value.forEach(walk);\n          return;\n        }\n        localizeEntityName(value, candidates);\n        Object.keys(value).forEach(function(key){\n          walk(value[key]);\n        });\n      }\n      walk(root);\n      return root;\n    }\n\n    if (!payload) {\n      return null;\n    }\n    if (typeof payload === 'string') {\n      try {\n        return localizePayloadNames(JSON.parse(payload), currentUiLanguage());\n      } catch (err) {\n        return null;\n      }\n    }\n    return localizePayloadNames(payload, currentUiLanguage());\n  }\n\n  function loadMapContent(requestId) {\n    if (!requestId || typeof makeCloudFrontMapContentUrl !== 'function') {\n      return null;\n    }\n    const retryCount = 20;\n    const retryDelayMs = 500;\n    const deferred = $.Deferred();\n    let attemptsLeft = retryCount;\n\n    function attempt() {\n      const request = $.ajax({\n        url: makeCloudFrontMapContentUrl(requestId)\n      });\n      request.done(function(payload){\n        deferred.resolve(payload);\n      }).fail(function(){\n        if (attemptsLeft > 0) {\n          attemptsLeft -= 1;\n          setTimeout(attempt, retryDelayMs);\n        } else {\n          deferred.reject.apply(deferred, arguments);\n        }\n      });\n    }\n\n    attempt();\n    return deferred.promise();\n  }\n\n  function showMessage(listElem, message) {\n    listElem.empty();\n    listElem.append($(\"<li>\").text(message));\n  }\n\n  function getRenderer(name) {\n    if (!window.TM || !window.TM[name]) {\n      return null;\n    }\n    return window.TM[name];\n  }\n\n  function translateWithHelpers(helpers, key, fallback) {\n    if (helpers && typeof helpers.t === \"function\") {\n      return helpers.t(key, fallback);\n    }\n    return t(key, fallback);\n  }\n\n  function normalizeCount(value) {\n    if (typeof value === \"number\" && isFinite(value)) {\n      return value;\n    }\n    return parseInt(value, 10) || 0;\n  }\n\n  function messageItem(message) {\n    return {\n      type: \"message\",\n      attrs: {},\n      lines: [\n        {\n          className: \"map-content-message\",\n          parts: [{ text: message }]\n        }\n      ]\n    };\n  }\n\n  function sectionEntriesInDisplayOrder(model) {\n    const entries = [];\n    LINEAR_SECTION_CONFIGS.forEach(function(sectionConfig){\n      entries.push({\n        key: sectionConfig.key,\n        section: model ? model[sectionConfig.key] : null\n      });\n      if (sectionConfig.key === \"waterways\") {\n        entries.push({\n          key: WATER_AREA_SECTION_CONFIG.key,\n          section: model ? model[WATER_AREA_SECTION_CONFIG.key] : null\n        });\n      }\n    });\n    entries.push({\n      key: \"buildings\",\n      section: model ? model.buildings : null\n    });\n    POI_SECTION_CONFIGS.forEach(function(sectionConfig){\n      entries.push({\n        key: sectionConfig.key,\n        section: model ? model[sectionConfig.key] : null\n      });\n    });\n    return entries;\n  }\n\n  function summaryPenaltyBucket(sectionKey) {\n    if (sectionKey === \"roads\" || sectionKey === \"paths\") {\n      return \"road_path\";\n    }\n    if (sectionKey === \"poiFamiliar\" || sectionKey === \"poiDaily\" || sectionKey === \"poiTransport\") {\n      return \"poi\";\n    }\n    return sectionKey;\n  }\n\n  function lineTextFromParts(parts) {\n    if (!Array.isArray(parts)) {\n      return \"\";\n    }\n    return parts.map(function(part){\n      if (!part || part.text === null || part.text === undefined) {\n        return \"\";\n      }\n      return String(part.text);\n    }).join(\"\");\n  }\n\n  function titleLineFromItem(item) {\n    const lines = item && Array.isArray(item.lines) ? item.lines : [];\n    for (let i = 0; i < lines.length; i += 1) {\n      const line = lines[i];\n      if (line && line.className === \"map-content-title-line\") {\n        return line;\n      }\n    }\n    return null;\n  }\n\n  function parsedImportanceScoreFromTitleLine(line) {\n    if (!line || typeof line.title !== \"string\") {\n      return null;\n    }\n    try {\n      const parsed = JSON.parse(line.title);\n      if (!parsed || typeof parsed !== \"object\") {\n        return null;\n      }\n      return parsed;\n    } catch (_error) {\n      return null;\n    }\n  }\n\n  function importanceScoreFromTitleLine(line) {\n    const parsed = parsedImportanceScoreFromTitleLine(line);\n    if (!parsed) {\n      return null;\n    }\n    const value = parsed.final;\n    if (value === null || value === undefined || isNaN(value)) {\n      return null;\n    }\n    const number = Number(value);\n    if (!isFinite(number) || number <= 0) {\n      return null;\n    }\n    return number;\n  }\n\n  function roundedSummaryValue(value) {\n    if (!isFinite(value)) {\n      return null;\n    }\n    return Math.round(value * 1000) / 1000;\n  }\n\n  function summaryTooltipText(item) {\n    if (!item || !item.importanceScoreObject) {\n      return null;\n    }\n    const payload = {\n      importanceScore: item.importanceScoreObject,\n      summaryPenalty: {\n        bucket: item.penaltyBucket,\n        factorUsed: roundedSummaryValue(item.penaltyFactorUsed),\n        pickIndexInBucket: (item.penaltyCountBeforePick || 0) + 1,\n        effectiveScore: roundedSummaryValue(item.effectiveScore)\n      }\n    };\n    try {\n      return JSON.stringify(payload, null, 2);\n    } catch (_error) {\n      return null;\n    }\n  }\n\n  function collectSummaryCandidates(model) {\n    const candidates = [];\n    let fullIndex = 0;\n    sectionEntriesInDisplayOrder(model).forEach(function(entry){\n      const items = entry && entry.section && Array.isArray(entry.section.items) ? entry.section.items : [];\n      items.forEach(function(item){\n        const titleLine = titleLineFromItem(item);\n        const importanceScoreObject = parsedImportanceScoreFromTitleLine(titleLine);\n        const importanceScore = importanceScoreFromTitleLine(titleLine);\n        let titleText = titleLine ? lineTextFromParts(titleLine.parts).trim() : \"\";\n        if ((entry.key === \"roads\" || entry.key === \"paths\") &&\n            item && typeof item.summaryTitle === \"string\" && item.summaryTitle.trim()) {\n          titleText = item.summaryTitle.trim();\n        }\n        const isNamed = !(item && item.attrs && item.attrs.dataIsNamed === false);\n        if (importanceScore && titleText && isNamed) {\n          candidates.push({\n            fullIndex: fullIndex,\n            titleText: titleText,\n            sectionKey: entry.key,\n            penaltyBucket: summaryPenaltyBucket(entry.key),\n            importanceScore: importanceScore,\n            importanceScoreObject: importanceScoreObject\n          });\n        }\n        fullIndex += 1;\n      });\n    });\n    return candidates;\n  }\n\n  function pickSummaryCandidates(candidates, maxItems) {\n    const remaining = Array.isArray(candidates) ? candidates.slice() : [];\n    const picked = [];\n    const pickCountsByBucket = {};\n    while (remaining.length && picked.length < maxItems) {\n      let bestIndex = -1;\n      let bestEffective = -Infinity;\n      let bestOrder = Number.MAX_SAFE_INTEGER;\n      remaining.forEach(function(candidate, index){\n        const pickCount = pickCountsByBucket[candidate.penaltyBucket] || 0;\n        const effective = candidate.importanceScore * Math.pow(SUMMARY_SECTION_PENALTY, pickCount);\n        if (effective > bestEffective + 1e-9) {\n          bestIndex = index;\n          bestEffective = effective;\n          bestOrder = candidate.fullIndex;\n          return;\n        }\n        if (Math.abs(effective - bestEffective) <= 1e-9) {\n          // For equal effective scores, keep full-list order as the only ordering rule.\n          if (candidate.fullIndex < bestOrder) {\n            bestIndex = index;\n            bestEffective = effective;\n            bestOrder = candidate.fullIndex;\n          }\n        }\n      });\n      if (bestIndex < 0) {\n        break;\n      }\n      const winner = remaining.splice(bestIndex, 1)[0];\n      winner.penaltyCountBeforePick = pickCountsByBucket[winner.penaltyBucket] || 0;\n      winner.penaltyFactorUsed = Math.pow(SUMMARY_SECTION_PENALTY, winner.penaltyCountBeforePick);\n      winner.effectiveScore = winner.importanceScore * winner.penaltyFactorUsed;\n      picked.push(winner);\n      pickCountsByBucket[winner.penaltyBucket] = (pickCountsByBucket[winner.penaltyBucket] || 0) + 1;\n    }\n    picked.sort(function(a, b){\n      return a.fullIndex - b.fullIndex;\n    });\n    return picked;\n  }\n\n  function buildSummaryModel(model) {\n    const candidates = collectSummaryCandidates(model);\n    const picked = pickSummaryCandidates(candidates, SUMMARY_MAX_ITEMS);\n    return {\n      items: picked.map(function(item){\n        return {\n          text: item.titleText,\n          tooltip: SHOW_IMPORTANCE_TOOLTIPS ? summaryTooltipText(item) : null\n        };\n      }),\n      candidateCount: candidates.length\n    };\n  }\n\n  function hasFullContentRows(model) {\n    return sectionEntriesInDisplayOrder(model).some(function(entry){\n      return !!(entry && entry.section && Array.isArray(entry.section.items) && entry.section.items.length);\n    });\n  }\n\n  function buildSectionModel(renderer, mapContent, helpers, fallbackKey, fallbackText, rendererOptions) {\n    const unavailableMessage = translateWithHelpers(\n      helpers,\n      \"map_content_unavailable\",\n      \"Map content is not available.\"\n    );\n    if (!renderer || typeof renderer.buildModel !== \"function\") {\n      return {\n        items: [messageItem(unavailableMessage)],\n        emptyMessage: unavailableMessage,\n        count: 0\n      };\n    }\n\n    const builtItems = renderer.buildModel(mapContent, helpers, rendererOptions);\n    const items = Array.isArray(builtItems) ? builtItems : [];\n    const count = normalizeCount(items.length);\n    if (count > 0) {\n      return {\n        items: items,\n        emptyMessage: null,\n        count: count\n      };\n    }\n\n    const emptyText = typeof renderer.emptyMessage === \"function\"\n      ? renderer.emptyMessage(helpers, rendererOptions)\n      : translateWithHelpers(helpers, fallbackKey, fallbackText);\n    return {\n      items: [messageItem(emptyText)],\n      emptyMessage: emptyText,\n      count: 0\n    };\n  }\n\n  function applySectionLimitToModel(section, maxVisible, collapsedLabel, expandedLabel) {\n    const maxCount = Number(maxVisible);\n    if (!section || !Array.isArray(section.items) || !isFinite(maxCount) || maxCount < 1) {\n      return { section: section, toggle: null };\n    }\n    if (section.count <= maxCount) {\n      return { section: section, toggle: null };\n    }\n    const hiddenCount = section.items.length - maxCount;\n    const limitedItems = section.items.map(function(item, index){\n      if (index < maxCount) {\n        return item;\n      }\n      const attrs = Object.assign({}, item && item.attrs ? item.attrs : {});\n      attrs.initiallyHidden = true;\n      return Object.assign({}, item, { attrs: attrs });\n    });\n    return {\n      section: {\n        items: limitedItems,\n        emptyMessage: section.emptyMessage,\n        count: section.count\n      },\n      toggle: {\n        hiddenCount: hiddenCount,\n        collapsedLabel: collapsedLabel,\n        expandedLabel: expandedLabel\n      }\n    };\n  }\n\n  function buildModel(mapContent, helpers, options) {\n    const payload = parseMapContent(mapContent);\n    const linearRenderer = getRenderer(\"mapDescWays\");\n    const buildingsRenderer = getRenderer(\"mapDescAreas\");\n    const poiRenderer = getRenderer(\"mapDescPois\");\n    const linearSections = {};\n    const linearToggles = {};\n    LINEAR_SECTION_CONFIGS.forEach(function(sectionConfig){\n      const fallbackKey = \"map_content_no_\" + (sectionConfig.key === \"otherLinear\" ? \"other_linear\" : sectionConfig.key);\n      const fallbackTextByKey = {\n        roads: \"No roads listed for this map.\",\n        paths: \"No paths listed for this map.\",\n        railways: \"No railways listed for this map.\",\n        waterways: \"No waterways listed for this map.\",\n        otherLinear: \"No other linear features listed for this map.\"\n      };\n      const section = buildSectionModel(\n        linearRenderer,\n        payload,\n        helpers,\n        fallbackKey,\n        fallbackTextByKey[sectionConfig.key] || \"No features listed for this map.\",\n        { section: sectionConfig.key }\n      );\n      const limited = applySectionLimitToModel(\n        section,\n        sectionConfig.maxVisible,\n        showMoreFeaturesLabel(section.items.length - sectionConfig.maxVisible, function(key, fallback){\n          return translateWithHelpers(helpers, key, fallback);\n        }),\n        translateWithHelpers(helpers, \"map_content_show_less_features\", \"Show fewer features\")\n      );\n      linearSections[sectionConfig.key] = limited.section;\n      linearToggles[sectionConfig.key] = limited.toggle;\n    });\n    const waterAreas = buildSectionModel(\n      buildingsRenderer,\n      payload,\n      helpers,\n      WATER_AREA_SECTION_CONFIG.fallbackKey,\n      WATER_AREA_SECTION_CONFIG.fallbackText,\n      { section: WATER_AREA_SECTION_CONFIG.rendererSection }\n    );\n    const waterAreasResult = applySectionLimitToModel(\n      waterAreas,\n      WATER_AREA_SECTION_CONFIG.maxVisible,\n      showMoreFeaturesLabel(\n        waterAreas.items.length - WATER_AREA_SECTION_CONFIG.maxVisible,\n        function(key, fallback){\n          return translateWithHelpers(helpers, key, fallback);\n        }\n      ),\n      translateWithHelpers(helpers, \"map_content_show_less_features\", \"Show fewer features\")\n    );\n\n    const buildings = buildSectionModel(\n      buildingsRenderer,\n      payload,\n      helpers,\n      \"map_content_no_buildings\",\n      \"No buildings listed for this map.\"\n    );\n    const poiSections = {};\n    const poiToggles = {};\n    POI_SECTION_CONFIGS.forEach(function(sectionConfig){\n      const section = buildSectionModel(\n        poiRenderer,\n        payload,\n        helpers,\n        sectionConfig.fallbackKey,\n        sectionConfig.fallbackText,\n        {\n          section: sectionConfig.rendererSection,\n          includeSparseNote: sectionConfig.includeSparseNote\n        }\n      );\n      const limited = applySectionLimitToModel(\n        section,\n        sectionConfig.maxVisible,\n        showMorePoisLabel(section.items.length - sectionConfig.maxVisible, function(key, fallback){\n          return translateWithHelpers(helpers, key, fallback);\n        }),\n        translateWithHelpers(helpers, \"map_content_show_less_pois\", \"Show fewer places\")\n      );\n      poiSections[sectionConfig.key] = limited.section;\n      poiToggles[sectionConfig.key] = limited.toggle;\n    });\n    const requestedMaxVisible = options && isFinite(Number(options.maxVisibleBuildings))\n      ? Number(options.maxVisibleBuildings)\n      : MAX_VISIBLE_BUILDINGS;\n    const buildingsResult = applySectionLimitToModel(\n      buildings,\n      requestedMaxVisible,\n      showMoreBuildingsLabel(buildings.items.length - requestedMaxVisible, function(key, fallback){\n        return translateWithHelpers(helpers, key, fallback);\n      }),\n      translateWithHelpers(helpers, \"map_content_show_less_buildings\", \"Show fewer buildings\")\n    );\n    const sectionHeightNotes = buildSectionHeightNotes(payload, helpers);\n    const model = {\n      roads: linearSections.roads,\n      paths: linearSections.paths,\n      railways: linearSections.railways,\n      waterways: linearSections.waterways,\n      waterAreas: waterAreasResult.section,\n      otherLinear: linearSections.otherLinear,\n      poiFamiliar: poiSections.poiFamiliar,\n      poiDaily: poiSections.poiDaily,\n      poiTransport: poiSections.poiTransport,\n      buildings: buildingsResult.section\n    };\n    model.summary = buildSummaryModel(model);\n    model.ui = {\n      sectionHeightNotes: sectionHeightNotes,\n      linearToggles: linearToggles,\n      waterAreasToggle: waterAreasResult.toggle,\n      buildingsToggle: buildingsResult.toggle,\n      poiToggles: poiToggles,\n      hasFullContentRows: hasFullContentRows(model),\n      summaryShowMoreLabel: translateWithHelpers(helpers, \"map_content_summary_show_more\", \"Show more\"),\n      summaryShowOnlyLabel: translateWithHelpers(helpers, \"map_content_summary_show_only\", \"Show summary only\")\n    };\n    return model;\n  }\n\n  function sectionCount(section) {\n    if (!section) {\n      return 0;\n    }\n    return normalizeCount(section.count);\n  }\n\n  function renderSectionFromModel(listElem, section, renderer) {\n    if (!listElem || !listElem.length) {\n      return 0;\n    }\n    if (!section || !Array.isArray(section.items)) {\n      showMessage(listElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n      return 0;\n    }\n    listElem.empty();\n    if (section.count > 0 && renderer && typeof renderer.renderFromModel === \"function\") {\n      const renderItems = SHOW_IMPORTANCE_TOOLTIPS\n        ? section.items\n        : cloneItemsWithoutImportanceTooltips(section.items);\n      return normalizeCount(renderer.renderFromModel(renderItems, listElem));\n    }\n    if (section.emptyMessage) {\n      showMessage(listElem, section.emptyMessage);\n      return 0;\n    }\n    showMessage(listElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n    return 0;\n  }\n\n  function cloneItemsWithoutImportanceTooltips(items) {\n    if (!Array.isArray(items)) {\n      return [];\n    }\n    return items.map(function(item){\n      if (!item || !Array.isArray(item.lines)) {\n        return item;\n      }\n      const linesWithoutTooltips = item.lines.map(function(line){\n        if (!line || typeof line !== \"object\" || !Object.prototype.hasOwnProperty.call(line, \"title\")) {\n          return line;\n        }\n        return Object.assign({}, line, { title: null });\n      });\n      return Object.assign({}, item, { lines: linesWithoutTooltips });\n    });\n  }\n\n  function renderSectionHeightNote(row, noteText) {\n    if (!row || !row.length) {\n      return;\n    }\n    const noteElem = row.find(\".map-content-section-height\");\n    if (!noteElem.length) {\n      return;\n    }\n    noteElem.text(noteText || \"\");\n  }\n\n  function applyToggleFromModel(listElem, rowSelector, toggleClass, toggleModel, collapsedFallback, expandedFallback) {\n    if (!listElem || !listElem.length) {\n      return;\n    }\n    const row = rowSelector ? listElem.closest(rowSelector) : listElem.parent();\n    row.find(\".\" + toggleClass).remove();\n    if (!toggleModel || !toggleModel.hiddenCount) {\n      return;\n    }\n\n    const hiddenItems = listElem.children(\"li[data-initially-hidden='true']\");\n    if (!hiddenItems.length) {\n      return;\n    }\n    hiddenItems.hide();\n\n    const button = $(\"<button>\")\n      .attr(\"type\", \"button\")\n      .addClass(\"map-content-section-toggle\")\n      .addClass(toggleClass)\n      .attr(\"aria-expanded\", \"false\")\n      .text(toggleModel.collapsedLabel || collapsedFallback);\n\n    button.on(\"click\", function(){\n      const isExpanded = button.attr(\"aria-expanded\") === \"true\";\n      if (isExpanded) {\n        hiddenItems.hide();\n        button\n          .attr(\"aria-expanded\", \"false\")\n          .text(toggleModel.collapsedLabel || collapsedFallback);\n        return;\n      }\n      hiddenItems.show();\n      button\n        .attr(\"aria-expanded\", \"true\")\n        .text(toggleModel.expandedLabel || expandedFallback);\n    });\n\n    listElem.after(button);\n  }\n\n  function setFullContentVisibility(container, isVisible) {\n    if (!container || !container.length) {\n      return;\n    }\n    const fullContainer = container.find(\".map-content-full\");\n    if (!fullContainer.length) {\n      return;\n    }\n    if (isVisible) {\n      fullContainer.removeAttr(\"hidden\");\n      return;\n    }\n    fullContainer.attr(\"hidden\", \"hidden\");\n  }\n\n  function renderSummaryFromModel(model, container) {\n    if (!container || !container.length) {\n      return;\n    }\n    const listElem = container.find(\".map-content-summary\");\n    if (!listElem.length) {\n      return;\n    }\n    listElem.empty();\n    const summaryItems = model && model.summary && Array.isArray(model.summary.items)\n      ? model.summary.items\n      : [];\n    summaryItems.forEach(function(item){\n      if (!item || !item.text) {\n        return;\n      }\n      const listItem = $(\"<li>\")\n        .addClass(\"map-content-summary-item\")\n        .text(item.text);\n      if (item.tooltip) {\n        listItem.addClass(\"map-content-has-importance-popup\");\n        const popup = $(\"<span>\").addClass(\"map-content-importance-popup\");\n        popup.append($(\"<pre>\").text(item.tooltip));\n        listItem.append(popup);\n      }\n      listElem.append(listItem);\n    });\n  }\n\n  function updateSummaryToggleButton(container, model, isExpanded) {\n    if (!container || !container.length) {\n      return;\n    }\n    const button = container.find(\".map-content-summary-toggle\");\n    if (!button.length) {\n      return;\n    }\n    const hasFullContentRows = !!(model && model.ui && model.ui.hasFullContentRows);\n    if (!hasFullContentRows) {\n      button.hide();\n      return;\n    }\n    const collapsedLabel = model && model.ui && model.ui.summaryShowMoreLabel\n      ? model.ui.summaryShowMoreLabel\n      : t(\"map_content_summary_show_more\", \"Show more\");\n    const expandedLabel = model && model.ui && model.ui.summaryShowOnlyLabel\n      ? model.ui.summaryShowOnlyLabel\n      : t(\"map_content_summary_show_only\", \"Show summary only\");\n    button\n      .show()\n      .attr(\"aria-expanded\", isExpanded ? \"true\" : \"false\")\n      .text(isExpanded ? expandedLabel : collapsedLabel);\n  }\n\n  function bindSummaryToggle(container, model, state) {\n    if (!container || !container.length) {\n      return;\n    }\n    const button = container.find(\".map-content-summary-toggle\");\n    if (!button.length) {\n      return;\n    }\n    button.off(\"click.map-content-summary-toggle\");\n    button.on(\"click.map-content-summary-toggle\", function(){\n      const nextExpanded = !state.summaryExpanded;\n      if (nextExpanded && !state.fullContentRendered) {\n        renderFromModel(model, container);\n        state.fullContentRendered = true;\n      }\n      state.summaryExpanded = nextExpanded;\n      setFullContentVisibility(container, nextExpanded);\n      updateSummaryToggleButton(container, model, nextExpanded);\n    });\n  }\n\n  function renderFromModel(model, container) {\n    if (!container || !container.length) {\n      return {\n        roads: 0,\n        paths: 0,\n        railways: 0,\n        waterways: 0,\n        waterAreas: 0,\n        otherLinear: 0,\n        poiFamiliar: 0,\n        poiDaily: 0,\n        poiTransport: 0,\n        buildings: 0\n      };\n    }\n    const linearRenderer = getRenderer(\"mapDescWays\");\n    const poiRenderer = getRenderer(\"mapDescPois\");\n    const buildingsListElem = container.find(\".map-content-buildings\");\n    const counts = {\n      roads: 0,\n      paths: 0,\n      railways: 0,\n      waterways: 0,\n      waterAreas: 0,\n      otherLinear: 0,\n      poiFamiliar: 0,\n      poiDaily: 0,\n      poiTransport: 0,\n      buildings: 0\n    };\n\n    LINEAR_SECTION_CONFIGS.forEach(function(sectionConfig){\n      const row = container.find(sectionConfig.rowSelector);\n      const listElem = container.find(sectionConfig.listSelector);\n      const sectionHeightNotes = model && model.ui && model.ui.sectionHeightNotes\n        ? model.ui.sectionHeightNotes\n        : null;\n      if (!listElem.length) {\n        return;\n      }\n      renderSectionHeightNote(row, sectionHeightNotes ? sectionHeightNotes[sectionConfig.key] : null);\n      const section = model ? model[sectionConfig.key] : null;\n      const count = sectionCount(section);\n      if (!sectionConfig.alwaysShow && count <= 0) {\n        listElem.empty();\n        row.hide();\n        counts[sectionConfig.key] = 0;\n        return;\n      }\n      row.show();\n      counts[sectionConfig.key] = renderSectionFromModel(listElem, section, linearRenderer);\n      const linearToggleModel = model && model.ui && model.ui.linearToggles\n        ? model.ui.linearToggles[sectionConfig.key]\n        : null;\n      applyToggleFromModel(\n        listElem,\n        sectionConfig.rowSelector,\n        sectionConfig.toggleClass,\n        linearToggleModel,\n        showMoreFeaturesLabel(\n          linearToggleModel && linearToggleModel.hiddenCount ? linearToggleModel.hiddenCount : 0\n        ),\n        t(\"map_content_show_less_features\", \"Show fewer features\")\n      );\n    });\n\n    const waterAreasRenderer = getRenderer(\"mapDescAreas\");\n    const waterAreasRow = container.find(WATER_AREA_SECTION_CONFIG.rowSelector);\n    const waterAreasListElem = container.find(WATER_AREA_SECTION_CONFIG.listSelector);\n    const waterAreasSectionHeightNotes = model && model.ui && model.ui.sectionHeightNotes\n      ? model.ui.sectionHeightNotes\n      : null;\n    if (waterAreasListElem.length) {\n      renderSectionHeightNote(\n        waterAreasRow,\n        waterAreasSectionHeightNotes ? waterAreasSectionHeightNotes[WATER_AREA_SECTION_CONFIG.key] : null\n      );\n      const waterAreasSection = model ? model[WATER_AREA_SECTION_CONFIG.key] : null;\n      const waterAreasCount = sectionCount(waterAreasSection);\n      if (!WATER_AREA_SECTION_CONFIG.alwaysShow && waterAreasCount <= 0) {\n        waterAreasListElem.empty();\n        waterAreasRow.hide();\n        counts[WATER_AREA_SECTION_CONFIG.key] = 0;\n      } else {\n        waterAreasRow.show();\n        counts[WATER_AREA_SECTION_CONFIG.key] = renderSectionFromModel(\n          waterAreasListElem,\n          waterAreasSection,\n          waterAreasRenderer\n        );\n        const waterAreasToggleModel = model && model.ui\n          ? model.ui.waterAreasToggle\n          : null;\n        applyToggleFromModel(\n          waterAreasListElem,\n          WATER_AREA_SECTION_CONFIG.rowSelector,\n          WATER_AREA_SECTION_CONFIG.toggleClass,\n          waterAreasToggleModel,\n          showMoreFeaturesLabel(\n            waterAreasToggleModel && waterAreasToggleModel.hiddenCount\n              ? waterAreasToggleModel.hiddenCount\n              : 0\n          ),\n          t(\"map_content_show_less_features\", \"Show fewer features\")\n        );\n      }\n    }\n\n    POI_SECTION_CONFIGS.forEach(function(sectionConfig){\n      const row = container.find(sectionConfig.rowSelector);\n      const listElem = container.find(sectionConfig.listSelector);\n      const sectionHeightNotes = model && model.ui && model.ui.sectionHeightNotes\n        ? model.ui.sectionHeightNotes\n        : null;\n      if (!listElem.length) {\n        return;\n      }\n      renderSectionHeightNote(row, sectionHeightNotes ? sectionHeightNotes[sectionConfig.key] : null);\n      const section = model ? model[sectionConfig.key] : null;\n      const count = sectionCount(section);\n      if (!sectionConfig.alwaysShow && count <= 0) {\n        listElem.empty();\n        row.hide();\n        counts[sectionConfig.key] = 0;\n        return;\n      }\n      row.show();\n      counts[sectionConfig.key] = renderSectionFromModel(listElem, section, poiRenderer);\n      const toggleModel = model && model.ui && model.ui.poiToggles\n        ? model.ui.poiToggles[sectionConfig.key]\n        : null;\n      applyToggleFromModel(\n        listElem,\n        sectionConfig.rowSelector,\n        sectionConfig.toggleClass,\n        toggleModel,\n        showMorePoisLabel(toggleModel && toggleModel.hiddenCount ? toggleModel.hiddenCount : 0),\n        t(\"map_content_show_less_pois\", \"Show fewer places\")\n      );\n    });\n\n    const buildingsRenderer = getRenderer(\"mapDescAreas\");\n    const buildingRow = container.find(\".map-content-buildings-row\");\n    const sectionHeightNotes = model && model.ui && model.ui.sectionHeightNotes\n      ? model.ui.sectionHeightNotes\n      : null;\n    renderSectionHeightNote(buildingRow, sectionHeightNotes ? sectionHeightNotes.buildings : null);\n    counts.buildings = buildingsListElem.length\n      ? renderSectionFromModel(buildingsListElem, model ? model.buildings : null, buildingsRenderer)\n      : 0;\n    if (buildingsListElem.length) {\n      applyToggleFromModel(\n        buildingsListElem,\n        \".map-content-buildings-row\",\n        \"map-content-buildings-toggle\",\n        model && model.ui ? model.ui.buildingsToggle : null,\n        showMoreBuildingsLabel(\n          model && model.ui && model.ui.buildingsToggle && model.ui.buildingsToggle.hiddenCount\n            ? model.ui.buildingsToggle.hiddenCount\n            : 0\n        ),\n        t(\"map_content_show_less_buildings\", \"Show fewer buildings\")\n      );\n    }\n    return counts;\n  }\n\n  // Entry point: read map-content.json and populate \"Map content\" block.\n  function insertMapDescription(info, container) {\n    if (!container || !container.length) {\n      return;\n    }\n    const roadsListElem = container.find(\".map-content-roads\");\n    const waterAreasListElem = container.find(WATER_AREA_SECTION_CONFIG.listSelector);\n    const buildingsListElem = container.find(\".map-content-buildings\");\n    const familiarPoisListElem = container.find(\".map-content-poi-familiar\");\n    const summaryListElem = container.find(\".map-content-summary\");\n    const summaryToggleElem = container.find(\".map-content-summary-toggle\");\n    if (!roadsListElem.length && !waterAreasListElem.length && !buildingsListElem.length && !familiarPoisListElem.length && !summaryListElem.length) {\n      return;\n    }\n    summaryListElem.empty();\n    summaryToggleElem\n      .off(\"click.map-content-summary-toggle\")\n      .attr(\"aria-expanded\", \"false\")\n      .hide();\n    setFullContentVisibility(container, false);\n    roadsListElem.empty();\n    LINEAR_SECTION_CONFIGS.forEach(function(sectionConfig){\n      const row = container.find(sectionConfig.rowSelector);\n      const listElem = container.find(sectionConfig.listSelector);\n      if (!listElem.length) {\n        return;\n      }\n      listElem.empty();\n      row.find(\".\" + sectionConfig.toggleClass).remove();\n      renderSectionHeightNote(row, null);\n      if (!sectionConfig.alwaysShow) {\n        row.hide();\n      } else {\n        row.show();\n      }\n    });\n    const waterAreasRow = container.find(WATER_AREA_SECTION_CONFIG.rowSelector);\n    if (waterAreasListElem.length) {\n      waterAreasListElem.empty();\n      waterAreasRow.find(\".\" + WATER_AREA_SECTION_CONFIG.toggleClass).remove();\n      renderSectionHeightNote(waterAreasRow, null);\n      if (!WATER_AREA_SECTION_CONFIG.alwaysShow) {\n        waterAreasRow.hide();\n      } else {\n        waterAreasRow.show();\n      }\n    }\n    POI_SECTION_CONFIGS.forEach(function(sectionConfig){\n      const row = container.find(sectionConfig.rowSelector);\n      const listElem = container.find(sectionConfig.listSelector);\n      if (!listElem.length) {\n        return;\n      }\n      listElem.empty();\n      row.find(\".\" + sectionConfig.toggleClass).remove();\n      renderSectionHeightNote(row, null);\n      if (!sectionConfig.alwaysShow) {\n        row.hide();\n      } else {\n        row.show();\n      }\n    });\n    buildingsListElem.empty();\n    const buildingRow = container.find(\".map-content-buildings-row\");\n    buildingRow.find(\".map-content-buildings-toggle\").remove();\n    renderSectionHeightNote(buildingRow, null);\n\n    const requestId = info ? info.requestId : null;\n    const request = loadMapContent(requestId);\n    if (!request) {\n      setFullContentVisibility(container, true);\n      if (roadsListElem.length) {\n        showMessage(roadsListElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n      }\n      if (buildingsListElem.length) {\n        showMessage(buildingsListElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n      }\n      if (waterAreasListElem.length) {\n        container.find(WATER_AREA_SECTION_CONFIG.rowSelector).show();\n        showMessage(waterAreasListElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n      }\n      POI_SECTION_CONFIGS.forEach(function(sectionConfig){\n        const row = container.find(sectionConfig.rowSelector);\n        const listElem = container.find(sectionConfig.listSelector);\n        if (!listElem.length || !sectionConfig.alwaysShow) {\n          return;\n        }\n        row.show();\n        showMessage(listElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n      });\n      return;\n    }\n\n    request.done(function(payload){\n      const helpers = { t: t };\n      const model = buildModel(payload, helpers, { maxVisibleBuildings: MAX_VISIBLE_BUILDINGS });\n      const state = {\n        summaryExpanded: false,\n        fullContentRendered: false\n      };\n      renderSummaryFromModel(model, container);\n      setFullContentVisibility(container, false);\n      updateSummaryToggleButton(container, model, false);\n      bindSummaryToggle(container, model, state);\n    }).fail(function(){\n      setFullContentVisibility(container, true);\n      if (roadsListElem.length) {\n        showMessage(roadsListElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n      }\n      if (buildingsListElem.length) {\n        showMessage(buildingsListElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n      }\n      if (waterAreasListElem.length) {\n        container.find(WATER_AREA_SECTION_CONFIG.rowSelector).show();\n        showMessage(waterAreasListElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n      }\n      POI_SECTION_CONFIGS.forEach(function(sectionConfig){\n        const row = container.find(sectionConfig.rowSelector);\n        const listElem = container.find(sectionConfig.listSelector);\n        if (!listElem.length || !sectionConfig.alwaysShow) {\n          return;\n        }\n        row.show();\n        showMessage(listElem, t(\"map_content_unavailable\", \"Map content is not available.\"));\n      });\n    });\n  }\n\n  window.TM = window.TM || {};\n  window.TM.mapDescription = {\n    buildModel: buildModel,\n    renderFromModel: renderFromModel\n  };\n  window.insertMapDescription = insertMapDescription;\n})();\n"
  },
  {
    "path": "web/src/scripts/map.js",
    "content": "'use strict';\n/* global $ mapCalc Backbone isNan _ ol THREE performance google ga fbq TRANSLATIONS i18next show3dPreview */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\n(function(){\n  function showMapDescriptionError(error) {\n    var errMsg = error && error.message ? error.message : String(error || \"Unknown error\");\n    var row = $(\".map-content-row\");\n    if (!row.length) {\n      return;\n    }\n    row.show();\n    row.find(\".map-content\")\n      .removeClass(\"initial-state\")\n      .empty()\n      .append($(\"<p>\").addClass(\"error-msg\").text(errMsg));\n    row.find(\".show-more\").hide();\n  }\n\n  function initPrintingMethod() {\n\n    $(\"#printing-method-order\").change(function(){\n      setData(\"printing-method\", \"order\");\n      $(\".hidden-for-order\").hide();\n      $(\".hidden-for-self-print\").show();\n    });\n\n    $(\"#printing-method-self\").change(function(){\n      setData(\"printing-method\", \"self\");\n      $(\".hidden-for-order\").show();\n      $(\".hidden-for-self-print\").hide();\n    });\n    if (getLocalStorageStr('printing-method', 'order') === 'order') {\n      $(\"#printing-method-order\").prop('checked', true).change();\n    } else {\n      $(\"#printing-method-self\").prop('checked', true).change();\n    }\n  }\n\n  function checkDataAvailability(url) {\n    $.ajax({\n        type: \"HEAD\",\n        url: url\n    }).fail(function(jqXHR, textStatus, errorThrown){\n      if (jqXHR.status === 404) {\n          $(\".hidden-for-3d, .hidden-for-2d\").hide();\n          $(\".no-data-available-msg\").attr().show();\n      }\n    });\n  }\n\n  function initPrintingTech(printingTech, requestId) {\n    if (printingTech === '3d') {\n      $(\".hidden-for-3d\").hide();\n      initPrintingMethod();\n      checkDataAvailability(makeS3url(requestId));\n    } else {\n      $(\".hidden-for-2d\").hide();\n      checkDataAvailability(makeS3urlSvg(requestId));\n    }\n  }\n\n  function infoLoadHandler(info, textStatus, jqXHR){\n    storeMapSettingsFromInfo(info);\n\n    $(\".map-address\").text(info.addrLong);\n\n    var meta = {\n        size: info.size,\n        address: info.addrLong,\n        returnUrl: makeReturnUrl(info.requestId),\n        permaUrl: makeMapPermaUrl(info.requestId),\n    };\n\n    try {\n      insertMapDescription(info, $(\".map-content\")); // from map-description.js\n    } catch (error) {\n      if (window.console && window.console.error) {\n        window.console.error(\"Failed to render map description\", error);\n      }\n      showMapDescriptionError(error);\n    }\n\n    //$(\"#order-map\").attr(\"href\", PLAYFUL_PIXELS_URL\n    //  + \"?touchMapFileUrl=\" + encodeURIComponent(makeMapPermaUrl(info.requestId))\n    //  + \"&mapMeta=\" + encodeURIComponent(JSON.stringify(meta)));\n    $(\"#order-map\")\n      .attr(\"href\", \"https://www.hekeytech.com/contact\"\n        + \"?touchMapFileUrl=\" + encodeURIComponent(makeMapPermaUrl(info.requestId))\n        + \"&mapMeta=\" + encodeURIComponent(JSON.stringify(meta)))\n      .attr(\"target\", \"_blank\")\n      .attr(\"rel\", \"noopener noreferrer\");\n\n    // Keep in sync with the email-sending lambda\n    initEmailSending($('.email-sending'), meta);\n\n    $(\"#download-map\").attr(\"href\", makeCloudFrontUrl(info.requestId)); // STL\n    $(\"#download-stl-ways\").attr(\"href\", makeCloudFrontUrlWays(info.requestId));\n    $(\"#download-stl-rest\").attr(\"href\", makeCloudFrontUrlRest(info.requestId));\n    $(\"#download-svg\").attr(\"href\", makeCloudFrontUrlSvg(info.requestId));\n    $(\"#download-svg-embossers\").attr(\"href\", makeCloudFrontUrlSvg(info.requestId));\n    $(\"#download-pdf\").attr(\"href\", makeCloudFrontUrlPdf(info.requestId));\n    $(\"#download-pdf-embossers\").attr(\"href\", makeCloudFrontUrlPdf(info.requestId));\n    $(\"#download-blender\").attr(\"href\", makeCloudFrontUrlBlend(info.requestId));\n    $(\"#download-map-content\").attr(\"href\", makeCloudFrontMapContentUrl(info.requestId));\n    $(\"#svg-preview\").attr(\"src\", makeCloudFrontUrlSvg(info.requestId));\n\n    // Only show download links for chosen map type.\n    var printingTech = info.printingTech || '3d';\n    initPrintingTech(printingTech, info.requestId);\n\n    $(\".show-on-load\").show();\n\n    if (printingTech === '3d') {\n      // Only works after the containing HTML is visible\n      show3dPreview($('.preview-3d'), makeCloudFrontUrl(info.requestId));\n    }\n  };\n\n  $(window).ready(function(){\n    var id = getUrlParam(\"map\");\n    if (! id) {\n      alert(\"Query parameter 'map' required\");\n      return;\n    }\n\n    $(\".back-to-previous-page\").attr(\"href\", \"area?map=\" + id);\n\n    loadInfoJson(id).done(infoLoadHandler);\n  });\n})();\n"
  },
  {
    "path": "web/src/scripts/model-preview.js",
    "content": "/* global $ */\n\n(function() {\n    \"use strict\";\n\n    var renderer; // singleton\n    var modulesPromise;\n    var activePreview;\n    var renderToken = 0;\n    var splitBenchmarkMsPerTriangle;\n    var splitProjectionThresholdMs = 450;\n    var splitDebugEnabled;\n    var PREVIEW_VISUALS = {\n      renderer: {\n        toneMapping: \"LinearToneMapping\",\n        toneMappingExposure: 1.16\n      },\n      lights: {\n        ambient: {\n          color: 0xffffff,\n          intensity: 0.28\n        },\n        hemi: {\n          skyColor: 0xffffff,\n          groundColor: 0xa8a8a8,\n          intensity: 0.42\n        },\n        key: {\n          color: 0xffffff,\n          intensity: 1.9,\n          positionMultiplier: { x: 2.6, y: 2.8, z: 1.9 }\n        },\n        rim: {\n          color: 0xdcdcdc,\n          intensity: 1.0,\n          positionMultiplier: { x: -3.0, y: 1.2, z: -2.2 }\n        }\n      },\n      materials: {\n        featureColor: 0xe4e4e4,\n        buildingColor: 0xffffff,\n        baseColor: 0xc4c4c4,\n        fallbackColor: 0xd7d7d7,\n        roughness: 0.22,\n        metalness: 0.05,\n        buildingHeightMm: 2.9,\n        buildingHeightToleranceMm: 0.35,\n        buildingFlatSpanThresholdMm: 0.02\n      }\n    };\n\n    function getNowMs() {\n      if (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n        return performance.now();\n      }\n      return Date.now();\n    }\n\n    function makeEdgeKey(a, b, multiplier) {\n      if (a < b) {\n        return a * multiplier + b;\n      }\n      return b * multiplier + a;\n    }\n\n    function appendEdgeTriangle(edgeToTriangles, edgeKey, triangleIndex) {\n      var existing = edgeToTriangles.get(edgeKey);\n      if (typeof existing === \"number\") {\n        edgeToTriangles.set(edgeKey, [existing, triangleIndex]);\n        return;\n      }\n      if (existing) {\n        existing.push(triangleIndex);\n        return;\n      }\n      edgeToTriangles.set(edgeKey, triangleIndex);\n    }\n\n    function triangleCountFromGeometry(geometry) {\n      var positionAttr = geometry.getAttribute(\"position\");\n      if (!positionAttr) {\n        return 0;\n      }\n      var indexAttr = geometry.getIndex();\n      if (indexAttr) {\n        return Math.floor(indexAttr.count / 3);\n      }\n      return Math.floor(positionAttr.count / 3);\n    }\n\n    function runSplitBenchmark(sampleTriangleCount) {\n      var triangleCount = Math.max(1500, sampleTriangleCount || 6000);\n      var vertexCount = triangleCount * 3;\n      var positions = new Float32Array(vertexCount * 3);\n      var i;\n      for (i = 0; i < positions.length; i += 3) {\n        var vertex = i / 3;\n        positions[i] = (vertex % 97) * 0.013;\n        positions[i + 1] = (vertex % 89) * 0.017;\n        positions[i + 2] = (vertex % 53) * 0.011;\n      }\n\n      var quantScale = 10000;\n      var edgeMultiplier = vertexCount + 1;\n\n      function runPass() {\n        var vertexBuckets = new Map();\n        var edgeToTriangles = new Map();\n        var nextVertexId = 0;\n        var t0 = getNowMs();\n\n        function getVertexIdFromCoords(x, y, z) {\n          var qx = Math.round(x * quantScale);\n          var qy = Math.round(y * quantScale);\n          var qz = Math.round(z * quantScale);\n          var hash = ((qx * 73856093) ^ (qy * 19349663) ^ (qz * 83492791)) >>> 0;\n          var bucket = vertexBuckets.get(hash);\n          var idx;\n\n          if (!bucket) {\n            bucket = [];\n            vertexBuckets.set(hash, bucket);\n          } else {\n            for (idx = 0; idx < bucket.length; idx += 4) {\n              if (bucket[idx] === qx && bucket[idx + 1] === qy && bucket[idx + 2] === qz) {\n                return bucket[idx + 3];\n              }\n            }\n          }\n\n          var newId = nextVertexId;\n          nextVertexId += 1;\n          bucket.push(qx, qy, qz, newId);\n          return newId;\n        }\n\n        var tri;\n        for (tri = 0; tri < triangleCount; tri += 1) {\n          var base = tri * 9;\n          var x1 = positions[base];\n          var y1 = positions[base + 1];\n          var z1 = positions[base + 2];\n          var x2 = positions[base + 3];\n          var y2 = positions[base + 4];\n          var z2 = positions[base + 5];\n          var x3 = positions[base + 6];\n          var y3 = positions[base + 7];\n          var z3 = positions[base + 8];\n\n          var ux = x2 - x1;\n          var uy = y2 - y1;\n          var uz = z2 - z1;\n          var vx = x3 - x1;\n          var vy = y3 - y1;\n          var vz = z3 - z1;\n          var cx = (uy * vz) - (uz * vy);\n          var cy = (uz * vx) - (ux * vz);\n          var cz = (ux * vy) - (uy * vx);\n          var norm = Math.sqrt((cx * cx) + (cy * cy) + (cz * cz));\n          if (!isFinite(norm)) {\n            return Infinity;\n          }\n\n          var v1 = getVertexIdFromCoords(x1, y1, z1);\n          var v2 = getVertexIdFromCoords(x2, y2, z2);\n          var v3 = getVertexIdFromCoords(x3, y3, z3);\n\n          appendEdgeTriangle(edgeToTriangles, makeEdgeKey(v1, v2, edgeMultiplier), tri);\n          appendEdgeTriangle(edgeToTriangles, makeEdgeKey(v2, v3, edgeMultiplier), tri);\n          appendEdgeTriangle(edgeToTriangles, makeEdgeKey(v3, v1, edgeMultiplier), tri);\n        }\n\n        var edgeRefCount = 0;\n        edgeToTriangles.forEach(function(value) {\n          if (typeof value === \"number\") {\n            edgeRefCount += 1;\n          } else {\n            edgeRefCount += value.length;\n          }\n        });\n\n        var elapsed = getNowMs() - t0;\n        return edgeRefCount > 0 ? elapsed / triangleCount : Infinity;\n      }\n\n      runPass(); // warm-up\n      return runPass();\n    }\n\n    function isSplitDebugEnabled() {\n      if (typeof splitDebugEnabled === \"boolean\") {\n        return splitDebugEnabled;\n      }\n\n      if (!window || !window.location || typeof window.location.search !== \"string\") {\n        splitDebugEnabled = false;\n        return splitDebugEnabled;\n      }\n\n      if (typeof URLSearchParams === \"function\") {\n        var params = new URLSearchParams(window.location.search);\n        splitDebugEnabled = params.get(\"splitdebug\") === \"1\";\n        return splitDebugEnabled;\n      }\n\n      splitDebugEnabled = /(?:\\?|&)splitdebug=1(?:&|$)/.test(window.location.search);\n      return splitDebugEnabled;\n    }\n\n    function splitDebugLog(message) {\n      if (!isSplitDebugEnabled()) {\n        return;\n      }\n      if (window.console && typeof window.console.info === \"function\") {\n        window.console.info(\"[3d-preview]\", message);\n      }\n    }\n\n    function shouldEnableBaseSplit(triangleCount) {\n      if (triangleCount < 4) {\n        splitDebugLog(\"base split skipped: too few triangles (\" + triangleCount + \")\");\n        return false;\n      }\n\n      if (typeof splitBenchmarkMsPerTriangle !== \"number\" || !isFinite(splitBenchmarkMsPerTriangle)) {\n        splitBenchmarkMsPerTriangle = runSplitBenchmark(Math.min(Math.max(triangleCount, 3000), 9000));\n        splitDebugLog(\n          \"base split benchmark computed: \" + splitBenchmarkMsPerTriangle.toFixed(4) + \" ms/triangle\"\n        );\n      }\n\n      if (!isFinite(splitBenchmarkMsPerTriangle) || splitBenchmarkMsPerTriangle <= 0) {\n        splitDebugLog(\"base split skipped: benchmark invalid\");\n        return false;\n      }\n\n      var projectedMs = splitBenchmarkMsPerTriangle * triangleCount;\n      var enabled = projectedMs < splitProjectionThresholdMs;\n      splitDebugLog(\n        \"base split projection \" + projectedMs.toFixed(1) + \" ms for \" + triangleCount +\n        \" triangles (threshold \" + splitProjectionThresholdMs + \"): \" + (enabled ? \"enabled\" : \"disabled\")\n      );\n      return enabled;\n    }\n\n    function splitPositionsToGeometry(THREE, positions) {\n      if (!positions || !positions.length) {\n        return null;\n      }\n      var geometry = new THREE.BufferGeometry();\n      geometry.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\n      geometry.computeVertexNormals();\n      return geometry;\n    }\n\n    function buildSplitGeometries(\n      THREE,\n      sourcePositions,\n      triangleSourceVertexIndices,\n      isBaseTriangle,\n      isBuildingTriangle,\n      baseTriangleCount,\n      buildingTriangleCount\n    ) {\n      var triangleCount = isBaseTriangle.length;\n      var featureTriangleCount = triangleCount - baseTriangleCount - buildingTriangleCount;\n\n      if (baseTriangleCount < 1 || featureTriangleCount < 0 || (featureTriangleCount + buildingTriangleCount) < 1) {\n        return null;\n      }\n\n      var basePositions = new Float32Array(baseTriangleCount * 9);\n      var featurePositions = new Float32Array(featureTriangleCount * 9);\n      var buildingPositions = new Float32Array(buildingTriangleCount * 9);\n      var baseWrite = 0;\n      var featureWrite = 0;\n      var buildingWrite = 0;\n      var tri;\n\n      for (tri = 0; tri < triangleCount; tri += 1) {\n        var srcOffset = tri * 3;\n        var vertexA = triangleSourceVertexIndices[srcOffset] * 3;\n        var vertexB = triangleSourceVertexIndices[srcOffset + 1] * 3;\n        var vertexC = triangleSourceVertexIndices[srcOffset + 2] * 3;\n\n        if (isBaseTriangle[tri]) {\n          basePositions[baseWrite] = sourcePositions[vertexA];\n          basePositions[baseWrite + 1] = sourcePositions[vertexA + 1];\n          basePositions[baseWrite + 2] = sourcePositions[vertexA + 2];\n          basePositions[baseWrite + 3] = sourcePositions[vertexB];\n          basePositions[baseWrite + 4] = sourcePositions[vertexB + 1];\n          basePositions[baseWrite + 5] = sourcePositions[vertexB + 2];\n          basePositions[baseWrite + 6] = sourcePositions[vertexC];\n          basePositions[baseWrite + 7] = sourcePositions[vertexC + 1];\n          basePositions[baseWrite + 8] = sourcePositions[vertexC + 2];\n          baseWrite += 9;\n        } else if (isBuildingTriangle[tri]) {\n          buildingPositions[buildingWrite] = sourcePositions[vertexA];\n          buildingPositions[buildingWrite + 1] = sourcePositions[vertexA + 1];\n          buildingPositions[buildingWrite + 2] = sourcePositions[vertexA + 2];\n          buildingPositions[buildingWrite + 3] = sourcePositions[vertexB];\n          buildingPositions[buildingWrite + 4] = sourcePositions[vertexB + 1];\n          buildingPositions[buildingWrite + 5] = sourcePositions[vertexB + 2];\n          buildingPositions[buildingWrite + 6] = sourcePositions[vertexC];\n          buildingPositions[buildingWrite + 7] = sourcePositions[vertexC + 1];\n          buildingPositions[buildingWrite + 8] = sourcePositions[vertexC + 2];\n          buildingWrite += 9;\n        } else {\n          featurePositions[featureWrite] = sourcePositions[vertexA];\n          featurePositions[featureWrite + 1] = sourcePositions[vertexA + 1];\n          featurePositions[featureWrite + 2] = sourcePositions[vertexA + 2];\n          featurePositions[featureWrite + 3] = sourcePositions[vertexB];\n          featurePositions[featureWrite + 4] = sourcePositions[vertexB + 1];\n          featurePositions[featureWrite + 5] = sourcePositions[vertexB + 2];\n          featurePositions[featureWrite + 6] = sourcePositions[vertexC];\n          featurePositions[featureWrite + 7] = sourcePositions[vertexC + 1];\n          featurePositions[featureWrite + 8] = sourcePositions[vertexC + 2];\n          featureWrite += 9;\n        }\n      }\n\n      return {\n        baseGeometry: splitPositionsToGeometry(THREE, basePositions),\n        featureGeometry: splitPositionsToGeometry(THREE, featurePositions),\n        buildingGeometry: splitPositionsToGeometry(THREE, buildingPositions)\n      };\n    }\n\n    function trySplitBaseGeometry(THREE, geometry) {\n      var positionAttr = geometry.getAttribute(\"position\");\n      if (!positionAttr || positionAttr.itemSize !== 3) {\n        return null;\n      }\n\n      var triangleCount = triangleCountFromGeometry(geometry);\n      if (!shouldEnableBaseSplit(triangleCount)) {\n        return null;\n      }\n\n      var sourcePositions = positionAttr.array;\n      var sourceIndex = geometry.getIndex();\n      var sourceIndices = sourceIndex ? sourceIndex.array : null;\n      var sourceVertexCount = positionAttr.count;\n      var quantIdBySourceVertex = new Int32Array(sourceVertexCount);\n      var quantScale = 10000;\n      var edgeMultiplier = sourceVertexCount + 1;\n\n      var triangleSourceVertexIndices = new Uint32Array(triangleCount * 3);\n      var triangleEdgeKeys = new Float64Array(triangleCount * 3);\n      var triangleAreas = new Float32Array(triangleCount);\n      var triangleNormalZ = new Float32Array(triangleCount);\n      var triangleMinZ = new Float32Array(triangleCount);\n      var triangleMaxZ = new Float32Array(triangleCount);\n      var edgeToTriangles = new Map();\n      var quantVertexBuckets = new Map();\n      var maxArea = 0;\n      var nextQuantVertexId = 0;\n      var tri;\n\n      function getQuantizedVertexId(sourceVertexIndex) {\n        var cached = quantIdBySourceVertex[sourceVertexIndex];\n        if (cached > 0) {\n          return cached - 1;\n        }\n\n        var src = sourceVertexIndex * 3;\n        var qx = Math.round(sourcePositions[src] * quantScale);\n        var qy = Math.round(sourcePositions[src + 1] * quantScale);\n        var qz = Math.round(sourcePositions[src + 2] * quantScale);\n        var hash = ((qx * 73856093) ^ (qy * 19349663) ^ (qz * 83492791)) >>> 0;\n        var bucket = quantVertexBuckets.get(hash);\n        var idx;\n\n        if (!bucket) {\n          bucket = [];\n          quantVertexBuckets.set(hash, bucket);\n        } else {\n          for (idx = 0; idx < bucket.length; idx += 4) {\n            if (bucket[idx] === qx && bucket[idx + 1] === qy && bucket[idx + 2] === qz) {\n              quantIdBySourceVertex[sourceVertexIndex] = bucket[idx + 3] + 1;\n              return bucket[idx + 3];\n            }\n          }\n        }\n\n        var newId = nextQuantVertexId;\n        nextQuantVertexId += 1;\n        bucket.push(qx, qy, qz, newId);\n        quantIdBySourceVertex[sourceVertexIndex] = newId + 1;\n        return newId;\n      }\n\n      for (tri = 0; tri < triangleCount; tri += 1) {\n        var srcOffset = tri * 3;\n        var ia = sourceIndices ? sourceIndices[srcOffset] : srcOffset;\n        var ib = sourceIndices ? sourceIndices[srcOffset + 1] : (srcOffset + 1);\n        var ic = sourceIndices ? sourceIndices[srcOffset + 2] : (srcOffset + 2);\n        triangleSourceVertexIndices[srcOffset] = ia;\n        triangleSourceVertexIndices[srcOffset + 1] = ib;\n        triangleSourceVertexIndices[srcOffset + 2] = ic;\n\n        var a = ia * 3;\n        var b = ib * 3;\n        var c = ic * 3;\n        var ax = sourcePositions[a];\n        var ay = sourcePositions[a + 1];\n        var az = sourcePositions[a + 2];\n        var bx = sourcePositions[b];\n        var by = sourcePositions[b + 1];\n        var bz = sourcePositions[b + 2];\n        var cx = sourcePositions[c];\n        var cy = sourcePositions[c + 1];\n        var cz = sourcePositions[c + 2];\n\n        var ux = bx - ax;\n        var uy = by - ay;\n        var uz = bz - az;\n        var vx = cx - ax;\n        var vy = cy - ay;\n        var vz = cz - az;\n        var crossX = (uy * vz) - (uz * vy);\n        var crossY = (uz * vx) - (ux * vz);\n        var crossZ = (ux * vy) - (uy * vx);\n        var crossLen = Math.sqrt((crossX * crossX) + (crossY * crossY) + (crossZ * crossZ));\n        var triArea = crossLen * 0.5;\n        triangleAreas[tri] = triArea;\n        if (triArea > maxArea) {\n          maxArea = triArea;\n        }\n        triangleNormalZ[tri] = crossLen > 1e-9 ? (crossZ / crossLen) : 0;\n\n        var minZ = az;\n        var maxZ = az;\n        if (bz < minZ) {\n          minZ = bz;\n        } else if (bz > maxZ) {\n          maxZ = bz;\n        }\n        if (cz < minZ) {\n          minZ = cz;\n        } else if (cz > maxZ) {\n          maxZ = cz;\n        }\n        triangleMinZ[tri] = minZ;\n        triangleMaxZ[tri] = maxZ;\n\n        var qa = getQuantizedVertexId(ia);\n        var qb = getQuantizedVertexId(ib);\n        var qc = getQuantizedVertexId(ic);\n        var edgeA = makeEdgeKey(qa, qb, edgeMultiplier);\n        var edgeB = makeEdgeKey(qb, qc, edgeMultiplier);\n        var edgeC = makeEdgeKey(qc, qa, edgeMultiplier);\n        triangleEdgeKeys[srcOffset] = edgeA;\n        triangleEdgeKeys[srcOffset + 1] = edgeB;\n        triangleEdgeKeys[srcOffset + 2] = edgeC;\n        appendEdgeTriangle(edgeToTriangles, edgeA, tri);\n        appendEdgeTriangle(edgeToTriangles, edgeB, tri);\n        appendEdgeTriangle(edgeToTriangles, edgeC, tri);\n      }\n\n      if (!(maxArea > 0)) {\n        return null;\n      }\n\n      var areaThreshold = maxArea * 0.95;\n      var horizontalNormalThreshold = 0.98;\n      var flatSpanThreshold = 0.02;\n      var isBaseTriangle = new Uint8Array(triangleCount);\n      var queue = new Int32Array(triangleCount);\n      var queueHead = 0;\n      var queueTail = 0;\n      var baseTriangleCount = 0;\n\n      for (tri = 0; tri < triangleCount; tri += 1) {\n        var zSpan = triangleMaxZ[tri] - triangleMinZ[tri];\n        if (\n          triangleAreas[tri] >= areaThreshold &&\n          Math.abs(triangleNormalZ[tri]) >= horizontalNormalThreshold &&\n          zSpan <= flatSpanThreshold\n        ) {\n          isBaseTriangle[tri] = 1;\n          queue[queueTail] = tri;\n          queueTail += 1;\n          baseTriangleCount += 1;\n        }\n      }\n\n      if (baseTriangleCount < 2) {\n        splitDebugLog(\"base split skipped: no seed component (seed count \" + baseTriangleCount + \")\");\n        return null;\n      }\n\n      while (queueHead < queueTail) {\n        var current = queue[queueHead];\n        queueHead += 1;\n        var currentOffset = current * 3;\n        var edgeIdx;\n\n        for (edgeIdx = 0; edgeIdx < 3; edgeIdx += 1) {\n          var neighbors = edgeToTriangles.get(triangleEdgeKeys[currentOffset + edgeIdx]);\n          if (typeof neighbors === \"number\") {\n            if (!isBaseTriangle[neighbors]) {\n              isBaseTriangle[neighbors] = 1;\n              queue[queueTail] = neighbors;\n              queueTail += 1;\n              baseTriangleCount += 1;\n            }\n            continue;\n          }\n\n          if (!neighbors) {\n            continue;\n          }\n\n          var n;\n          for (n = 0; n < neighbors.length; n += 1) {\n            var next = neighbors[n];\n            if (!isBaseTriangle[next]) {\n              isBaseTriangle[next] = 1;\n              queue[queueTail] = next;\n              queueTail += 1;\n              baseTriangleCount += 1;\n            }\n          }\n        }\n      }\n\n      if (\n        baseTriangleCount < 4 ||\n        baseTriangleCount >= triangleCount ||\n        baseTriangleCount > (triangleCount * 0.35)\n      ) {\n        splitDebugLog(\n          \"base split skipped: invalid base size \" + baseTriangleCount + \" / \" + triangleCount\n        );\n        return null;\n      }\n\n      var isBuildingTriangle = new Uint8Array(triangleCount);\n      var buildingTriangleCount = 0;\n      var baseTopZ = -Infinity;\n      var expectedBuildingTopZ = -Infinity;\n      for (tri = 0; tri < triangleCount; tri += 1) {\n        if (!isBaseTriangle[tri]) {\n          continue;\n        }\n        var baseSpan = triangleMaxZ[tri] - triangleMinZ[tri];\n        if (\n          Math.abs(triangleNormalZ[tri]) >= horizontalNormalThreshold &&\n          baseSpan <= flatSpanThreshold &&\n          triangleMaxZ[tri] > baseTopZ\n        ) {\n          baseTopZ = triangleMaxZ[tri];\n        }\n      }\n\n      if (isFinite(baseTopZ)) {\n        var buildingHeightTolerance = PREVIEW_VISUALS.materials.buildingHeightToleranceMm;\n        var buildingFlatSpanThreshold = PREVIEW_VISUALS.materials.buildingFlatSpanThresholdMm;\n        expectedBuildingTopZ = baseTopZ + PREVIEW_VISUALS.materials.buildingHeightMm;\n        for (tri = 0; tri < triangleCount; tri += 1) {\n          if (isBaseTriangle[tri]) {\n            continue;\n          }\n          var roofSpan = triangleMaxZ[tri] - triangleMinZ[tri];\n          if (\n            Math.abs(triangleNormalZ[tri]) >= horizontalNormalThreshold &&\n            roofSpan <= buildingFlatSpanThreshold &&\n            Math.abs(triangleMaxZ[tri] - expectedBuildingTopZ) <= buildingHeightTolerance\n          ) {\n            isBuildingTriangle[tri] = 1;\n            buildingTriangleCount += 1;\n          }\n        }\n        splitDebugLog(\n          \"base split building detection: base top z \" + baseTopZ.toFixed(3) +\n          \", expected building z \" + expectedBuildingTopZ.toFixed(3) +\n          \", building triangles \" + buildingTriangleCount\n        );\n      } else {\n        splitDebugLog(\"base split building detection skipped: could not determine base top z\");\n      }\n\n      var featureTriangleCount = triangleCount - baseTriangleCount - buildingTriangleCount;\n      splitDebugLog(\n        \"base split detected: base triangles \" + baseTriangleCount +\n        \", other feature triangles \" + featureTriangleCount +\n        \", building roof triangles \" + buildingTriangleCount\n      );\n\n      return buildSplitGeometries(\n        THREE,\n        sourcePositions,\n        triangleSourceVertexIndices,\n        isBaseTriangle,\n        isBuildingTriangle,\n        baseTriangleCount,\n        buildingTriangleCount\n      );\n    }\n\n    function cleanupActivePreview() {\n      if (!activePreview) {\n        return;\n      }\n      if (activePreview.resizeObserver) {\n        activePreview.resizeObserver.disconnect();\n      }\n      if (activePreview.onWindowResize) {\n        window.removeEventListener(\"resize\", activePreview.onWindowResize);\n      }\n      if (activePreview.controls) {\n        activePreview.controls.dispose();\n      }\n      if (activePreview.disposeScene) {\n        activePreview.disposeScene();\n      }\n      if (renderer) {\n        renderer.setAnimationLoop(null);\n      }\n      activePreview = null;\n    }\n\n    function loadThreeModules() {\n      if (modulesPromise) {\n        return modulesPromise;\n      }\n      if (typeof window.tmImportModule !== \"function\") {\n        return Promise.reject(new Error(\"tmImportModule is not available\"));\n      }\n      modulesPromise = Promise.all([\n        window.tmImportModule(\"three\"),\n        window.tmImportModule(\"three/addons/loaders/STLLoader.js\"),\n        window.tmImportModule(\"three/addons/controls/OrbitControls.js\")\n      ]).then(function(modules) {\n        return {\n          THREE: modules[0],\n          STLLoader: modules[1].STLLoader,\n          OrbitControls: modules[2].OrbitControls\n        };\n      }).catch(function(error) {\n        modulesPromise = null;\n        throw error;\n      });\n      return modulesPromise;\n    }\n\n    function readSize(elem, fallbackWidth, fallbackHeight) {\n      return {\n        width: Math.max(1, Math.round(elem.width() || fallbackWidth)),\n        height: Math.max(1, Math.round(elem.height() || fallbackHeight))\n      };\n    }\n\n    function makePreviewMaterial(THREE, color) {\n      return new THREE.MeshStandardMaterial({\n        color: color,\n        roughness: PREVIEW_VISUALS.materials.roughness,\n        metalness: PREVIEW_VISUALS.materials.metalness,\n        side: THREE.DoubleSide\n      });\n    }\n\n    function showError(elem, msg) {\n      $(\".preview-3d-container\").css(\"opacity\", 1);\n      elem.empty().append(\"<p class='loading-3d-preview'>\" + msg + \"</p>\");\n    }\n\n    window.show3dPreview = function(targetElem, s3url) {\n      var elem = targetElem || $(\"body\");\n      var size = readSize(elem, 680, 500);\n      var token = renderToken + 1;\n      renderToken = token;\n\n      cleanupActivePreview();\n      $(\".preview-3d-container\").css(\"opacity\", 1);\n      elem.empty().append(\"<p class='loading-3d-preview'>Loading 3D preview...</p>\");\n\n      loadThreeModules().then(function(moduleSet) {\n        if (token !== renderToken) {\n          return;\n        }\n\n        var THREE = moduleSet.THREE;\n        var STLLoader = moduleSet.STLLoader;\n        var OrbitControls = moduleSet.OrbitControls;\n\n        if (!renderer) {\n          renderer = new THREE.WebGLRenderer({ antialias: true });\n        }\n        renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n        renderer.setSize(size.width, size.height, false);\n        renderer.setClearColor(0x000000, 0);\n        renderer.outputColorSpace = THREE.SRGBColorSpace;\n        renderer.toneMapping = THREE[PREVIEW_VISUALS.renderer.toneMapping] || THREE.NoToneMapping;\n        renderer.toneMappingExposure = PREVIEW_VISUALS.renderer.toneMappingExposure;\n        renderer.shadowMap.enabled = false;\n\n        var scene = new THREE.Scene();\n        scene.background = null;\n\n        var camera = new THREE.PerspectiveCamera(34, size.width / size.height, 0.1, 1000);\n\n        var ambientLight = new THREE.AmbientLight(\n          PREVIEW_VISUALS.lights.ambient.color,\n          PREVIEW_VISUALS.lights.ambient.intensity\n        );\n        var hemiLight = new THREE.HemisphereLight(\n          PREVIEW_VISUALS.lights.hemi.skyColor,\n          PREVIEW_VISUALS.lights.hemi.groundColor,\n          PREVIEW_VISUALS.lights.hemi.intensity\n        );\n        var keyLight = new THREE.DirectionalLight(\n          PREVIEW_VISUALS.lights.key.color,\n          PREVIEW_VISUALS.lights.key.intensity\n        );\n        var rimLight = new THREE.DirectionalLight(\n          PREVIEW_VISUALS.lights.rim.color,\n          PREVIEW_VISUALS.lights.rim.intensity\n        );\n        scene.add(ambientLight);\n        scene.add(hemiLight);\n        scene.add(keyLight);\n        scene.add(rimLight);\n\n        var loader = new STLLoader();\n        loader.load(s3url, function(geometry) {\n          if (token !== renderToken) {\n            geometry.dispose();\n            return;\n          }\n\n          geometry.computeVertexNormals();\n          geometry.computeBoundingBox();\n          geometry.computeBoundingSphere();\n\n          if (!geometry.boundingBox) {\n            showError(elem, \"Could not render 3D preview.\");\n            return;\n          }\n\n          var center = new THREE.Vector3();\n          geometry.boundingBox.getCenter(center);\n          geometry.translate(-center.x, -center.y, -center.z);\n          geometry.computeBoundingBox();\n          geometry.computeBoundingSphere();\n\n          var radius = geometry.boundingSphere ? geometry.boundingSphere.radius : 1;\n          if (!isFinite(radius) || radius <= 0) {\n            radius = 1;\n          }\n\n          var modelGroup = new THREE.Group();\n          modelGroup.rotation.x = -Math.PI / 4; // keep map tilt from the legacy preview\n          scene.add(modelGroup);\n\n          var managedGeometries = [];\n          var managedMaterials = [];\n          var splitResult = null;\n\n          try {\n            splitResult = trySplitBaseGeometry(THREE, geometry);\n          } catch (splitError) {\n            if (window.console && window.console.warn) {\n              window.console.warn(\"3D preview base split failed, rendering with a single material.\", splitError);\n            }\n          }\n\n          if (splitResult && splitResult.baseGeometry && (splitResult.featureGeometry || splitResult.buildingGeometry)) {\n            var baseMaterial = makePreviewMaterial(THREE, PREVIEW_VISUALS.materials.baseColor);\n            var featureMaterial = splitResult.featureGeometry\n              ? makePreviewMaterial(THREE, PREVIEW_VISUALS.materials.featureColor)\n              : null;\n            var buildingMaterial = splitResult.buildingGeometry\n              ? makePreviewMaterial(THREE, PREVIEW_VISUALS.materials.buildingColor)\n              : null;\n            var baseMesh = new THREE.Mesh(splitResult.baseGeometry, baseMaterial);\n\n            modelGroup.add(baseMesh);\n            if (splitResult.featureGeometry && featureMaterial) {\n              modelGroup.add(new THREE.Mesh(splitResult.featureGeometry, featureMaterial));\n            }\n            if (splitResult.buildingGeometry && buildingMaterial) {\n              modelGroup.add(new THREE.Mesh(splitResult.buildingGeometry, buildingMaterial));\n            }\n            splitDebugLog(\n              \"base split render path active (building highlighting \" +\n              (splitResult.buildingGeometry ? \"enabled\" : \"disabled\") + \")\"\n            );\n\n            managedGeometries.push(splitResult.baseGeometry);\n            if (splitResult.featureGeometry) {\n              managedGeometries.push(splitResult.featureGeometry);\n            }\n            if (splitResult.buildingGeometry) {\n              managedGeometries.push(splitResult.buildingGeometry);\n            }\n            managedMaterials.push(baseMaterial);\n            if (featureMaterial) {\n              managedMaterials.push(featureMaterial);\n            }\n            if (buildingMaterial) {\n              managedMaterials.push(buildingMaterial);\n            }\n            geometry.dispose();\n          } else {\n            splitDebugLog(\"single material fallback render path active\");\n            var material = makePreviewMaterial(THREE, PREVIEW_VISUALS.materials.fallbackColor);\n            var mesh = new THREE.Mesh(geometry, material);\n            modelGroup.add(mesh);\n            managedGeometries.push(geometry);\n            managedMaterials.push(material);\n          }\n\n          var distance = radius * 2.45;\n          camera.near = Math.max(distance / 120, 0.1);\n          camera.far = Math.max(distance * 25, 10);\n          camera.position.set(distance * 0.95, -distance * 0.28, distance * 0.9);\n          camera.lookAt(0, 0, 0);\n          camera.updateProjectionMatrix();\n\n          keyLight.position.set(\n            radius * PREVIEW_VISUALS.lights.key.positionMultiplier.x,\n            radius * PREVIEW_VISUALS.lights.key.positionMultiplier.y,\n            radius * PREVIEW_VISUALS.lights.key.positionMultiplier.z\n          );\n          rimLight.position.set(\n            radius * PREVIEW_VISUALS.lights.rim.positionMultiplier.x,\n            radius * PREVIEW_VISUALS.lights.rim.positionMultiplier.y,\n            radius * PREVIEW_VISUALS.lights.rim.positionMultiplier.z\n          );\n\n          var controls = new OrbitControls(camera, renderer.domElement);\n          controls.enableDamping = true;\n          controls.dampingFactor = 0.08;\n          controls.autoRotate = true;\n          controls.autoRotateSpeed = 0.35;\n          controls.minDistance = radius * 0.9;\n          controls.maxDistance = radius * 8;\n          controls.minPolarAngle = Math.PI * 0.12;\n          controls.maxPolarAngle = Math.PI * 0.58;\n          controls.target.set(0, 0, 0);\n          controls.update();\n\n          function controlsStateSnapshot(orbitControls) {\n            return {\n              azimuth: orbitControls.getAzimuthalAngle(),\n              polar: orbitControls.getPolarAngle(),\n              targetX: orbitControls.target.x,\n              targetY: orbitControls.target.y,\n              targetZ: orbitControls.target.z\n            };\n          }\n\n          function hasRotateOrDragFromSnapshot(startState, currentState) {\n            if (!startState || !currentState) {\n              return false;\n            }\n            var angleEpsilon = 0.0001;\n            var targetEpsilon = 0.0001;\n            var azimuthDiff = Math.abs(currentState.azimuth - startState.azimuth);\n            var polarDiff = Math.abs(currentState.polar - startState.polar);\n            var targetMoved = Math.abs(currentState.targetX - startState.targetX) > targetEpsilon ||\n              Math.abs(currentState.targetY - startState.targetY) > targetEpsilon ||\n              Math.abs(currentState.targetZ - startState.targetZ) > targetEpsilon;\n            return azimuthDiff > angleEpsilon || polarDiff > angleEpsilon || targetMoved;\n          }\n\n          var autoRotateStoppedByUser = false;\n          var interactionStartState = null;\n          controls.addEventListener(\"start\", function() {\n            interactionStartState = controlsStateSnapshot(controls);\n          });\n          controls.addEventListener(\"change\", function() {\n            if (autoRotateStoppedByUser || !interactionStartState) {\n              return;\n            }\n            if (hasRotateOrDragFromSnapshot(interactionStartState, controlsStateSnapshot(controls))) {\n              controls.autoRotate = false;\n              autoRotateStoppedByUser = true;\n              interactionStartState = null;\n            }\n          });\n          controls.addEventListener(\"end\", function() {\n            interactionStartState = null;\n          });\n\n          elem.empty().append(renderer.domElement);\n\n          function updateSize() {\n            var currentSize = readSize(elem, size.width, size.height);\n            camera.aspect = currentSize.width / currentSize.height;\n            camera.updateProjectionMatrix();\n            renderer.setSize(currentSize.width, currentSize.height, false);\n          }\n\n          var onWindowResize = function() {\n            if (token === renderToken) {\n              updateSize();\n            }\n          };\n          window.addEventListener(\"resize\", onWindowResize);\n\n          var resizeObserver = null;\n          if (window.ResizeObserver && elem[0]) {\n            resizeObserver = new window.ResizeObserver(function() {\n              if (token === renderToken) {\n                updateSize();\n              }\n            });\n            resizeObserver.observe(elem[0]);\n          }\n\n          activePreview = {\n            controls: controls,\n            resizeObserver: resizeObserver,\n            onWindowResize: onWindowResize,\n            disposeScene: function() {\n              managedGeometries.forEach(function(item) {\n                item.dispose();\n              });\n              managedMaterials.forEach(function(item) {\n                item.dispose();\n              });\n            }\n          };\n\n          renderer.setAnimationLoop(function() {\n            if (token !== renderToken) {\n              return;\n            }\n            controls.update();\n            renderer.render(scene, camera);\n          });\n\n          $(\".preview-3d-container\").css(\"opacity\", 1);\n        }, undefined, function() {\n          if (token === renderToken) {\n            showError(elem, \"Could not load 3D preview.\");\n          }\n        });\n      }).catch(function(error) {\n        console.error(error);\n        if (token === renderToken) {\n          showError(elem, \"Could not load 3D preview modules.\");\n        }\n      });\n    };\n})();\n"
  },
  {
    "path": "web/src/scripts/multipart-mode.js",
    "content": "'use strict';\n/* global $ mapCalc Backbone isNan _ ol THREE performance google ga fbq TRANSLATIONS i18next show3dPreview */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\nfunction initMultipartMode(data, dragPanInteraction) {\n    data.on(\"change:multipartMode\", function() {\n        $(\"html\").toggleClass(\"multipart-mode\");\n        dragPanInteraction.setActive(!data.get(\"multipartMode\"));\n        if (!data.get(\"multipartMode\")) {\n            setData(\"multipartXpc\", 0);\n            setData(\"multipartYpc\", 0);\n        }\n    });\n\n    data.on(\"change:multipartXpc change:multipartYpc\", function(){\n        $(\".multipart-adjustment-x\").text(data.get(\"multipartXpc\"));\n        $(\".multipart-adjustment-y\").text(data.get(\"multipartYpc\"));\n    });\n\n    // Adjust location of multipart map by N% to each direction\n    $(\".area-movement-buttons .btn.left-10\").click(function(){\n        setData(\"multipartXpc\", data.get(\"multipartXpc\") - 10);\n    });\n    $(\".area-movement-buttons .btn.left-100\").click(function(){\n        setData(\"multipartXpc\", data.get(\"multipartXpc\") - 100);\n    });\n    $(\".area-movement-buttons .btn.right-10\").click(function(){\n        setData(\"multipartXpc\", data.get(\"multipartXpc\") + 10);\n    });\n    $(\".area-movement-buttons .btn.right-100\").click(function(){\n        setData(\"multipartXpc\", data.get(\"multipartXpc\") + 100);\n    });\n    $(\".area-movement-buttons .btn.up-10\").click(function(){\n        setData(\"multipartYpc\", data.get(\"multipartYpc\") + 10);\n    });\n    $(\".area-movement-buttons .btn.up-100\").click(function(){\n        setData(\"multipartYpc\", data.get(\"multipartYpc\") + 100);\n    });\n    $(\".area-movement-buttons .btn.down-10\").click(function(){\n        setData(\"multipartYpc\", data.get(\"multipartYpc\") - 10);\n    });\n    $(\".area-movement-buttons .btn.down-100\").click(function(){\n        setData(\"multipartYpc\", data.get(\"multipartYpc\") - 100);\n    });\n\n    // Initial values\n    setData(\"multipartXpc\", getLocalStorageInt(\"multipartXpc\", 0));\n    setData(\"multipartYpc\", getLocalStorageInt(\"multipartYpc\", 0));\n    initSimpleInput(\"multipartMode\", $(\"#multipart-map-input\"), \"checkbox\", false);\n}\n"
  },
  {
    "path": "web/src/scripts/osm-preview.js",
    "content": "'use strict';\n/* global $ mapCalc Backbone isNan _ ol THREE performance google ga fbq TRANSLATIONS i18next readCookie createCookie data mapDiameter */\n/* eslint quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\nwindow.initOsmPreview = function(outputs) {\n  var previewMapMarker1 = new ol.Overlay({\n    element: $(\"#marker1-overlay\")[0]\n  });\n  var osmDragPanInteraction = new ol.interaction.DragPan();\n  var previewMap = new ol.Map({\n    target: outputs.map[0],\n    interactions: new ol.Collection([\n      osmDragPanInteraction\n    ]),\n    controls: new ol.Collection([\n      new ol.control.ScaleLine()\n    ]),\n    layers: [\n      new ol.layer.Tile({\n        source: new ol.source.OSM({\n        })\n      })\n    ],\n    view: new ol.View({\n    }),\n    overlays: new ol.Collection([\n      previewMapMarker1\n    ])\n  });\n\n  var previewMapShown = false;\n  function updatePreview() {\n    var view = previewMap.getView();\n    var newCenter = ol.proj.fromLonLat(computeLonLat(data));\n    var diameter = mapDiameter();\n    var metersPerPixel = mapDiameter() / outputs.map.width();\n    var metersPerPixel = mapDiameter() / outputs.map.width();\n    var resolutionAtCoords = metersPerPixel / view.getProjection().getPointResolution(1, newCenter);\n    view.setResolution(resolutionAtCoords);\n    view.setCenter(newCenter);\n    previewMapMarker1.setPosition(ol.proj.fromLonLat([ data.get(\"lon\"), data.get(\"lat\") ]));\n\n    outputs.currentDiameterMeters.text(diameter.toFixed(0));\n    outputs.currentDiameterYards.text((diameter * 1.0936133).toFixed(0));\n\n    if (! previewMapShown) {\n      previewMapShown = true;\n      previewMap.updateSize();\n    }\n  }\n\n  // Update when relevant parameters change\n  data.on(\"change:lon change:lat change:size change:offsetX change:offsetY change:scale change:multipartXpc change:multipartYpc\", function() {\n    if (! (data.get(\"lat\") && data.get(\"lon\") && data.get(\"size\"))) {\n      return;\n    }\n    $(\"#map-area-preview-container\").show();\n    updatePreview();\n  });\n\n  // Map panning\n  previewMap.on(\"moveend\", function(ev){\n    if (data.get(\"multipartMode\")) {\n        return;\n    }\n\n    var metersPerDeg = mapCalc.metersPerDegree(data.get(\"lat\"));\n    var newCenter = ol.proj.toLonLat(previewMap.getView().getCenter());\n    var offsetX = Math.round((newCenter[0] - data.get(\"lon\")) * metersPerDeg.lon);\n    var offsetY = Math.round((newCenter[1] - data.get(\"lat\")) * metersPerDeg.lat);\n    var maxOffset = mapDiameter() / 2 * 0.9;\n    var fixPreview = false;\n    if (Math.abs(offsetX) > maxOffset) {\n      fixPreview = true;\n      offsetX = Math.round(Math.sign(offsetX) * maxOffset);\n    }\n    if (Math.abs(offsetY) > maxOffset) {\n      fixPreview = true;\n      offsetY = Math.round(Math.sign(offsetY) * maxOffset);\n    }\n    $(\"#x-offset-input\").val(offsetX);\n    $(\"#y-offset-input\").val(offsetY);\n    data.set(\"offsetX\", offsetX, { silent: true });\n    data.set(\"offsetY\", offsetY, { silent: true });\n    setLocalStorage(\"offsetX\", offsetX);\n    setLocalStorage(\"offsetY\", offsetY);\n    if (fixPreview) {\n      updatePreview();\n    }\n  });\n\n  // Show preview when user arrives via back button or browser wake-up.\n  $(window).on('pageshow', function(){\n    previewMapShown = false;\n    updatePreview();\n  });\n  data.on(\"initdone\", function(){\n    previewMapShown = false;\n    updatePreview();\n  });\n\n  return osmDragPanInteraction;\n};\n"
  },
  {
    "path": "web/src/scripts/util.js",
    "content": "'use strict';\n/* eslint camelcase:0, quotes:0, space-unary-ops:0, no-alert:0, no-unused-vars:0, no-shadow:0, no-extend-native:0, no-trailing-spaces:0 */\n\nfunction createCookie(name, value, days) {\n    var expires;\n    if (days) {\n        var date = new Date();\n        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));\n        expires = \"; expires=\" + date.toGMTString();\n    } else {\n        expires = \"\";\n    }\n    document.cookie = name + \"=\" + value + expires + \"; path=/\";\n}\n\nfunction readCookie(name) {\n    var nameEQ = name + \"=\";\n    var ca = document.cookie.split(';');\n    for (var i = 0; i < ca.length; i++) {\n        var c = ca[i];\n        while (c.charAt(0) === ' ') {\n            c = c.substring(1, c.length);\n        }\n        if (c.indexOf(nameEQ) === 0) {\n            return c.substring(nameEQ.length, c.length);\n        }\n    }\n    return null;\n}\n\nfunction mapDiameter() {\n  // Map diameter in meters\n  return data.get(\"size\") / 100 * data.get(\"scale\");\n}\n\nfunction eraseCookie(name) {\n    createCookie(name, \"\", -1);\n}\n\nfunction computeLonLat(data) {\n  function toNumberOrDefault(value, defaultValue) {\n    var parsed = parseFloat(value);\n    return isNaN(parsed) ? defaultValue : parsed;\n  }\n\n  var lat = toNumberOrDefault(data.get(\"lat\"), 0);\n  var lon = toNumberOrDefault(data.get(\"lon\"), 0);\n  var offsetX = toNumberOrDefault(data.get(\"offsetX\"), 0);\n  var offsetY = toNumberOrDefault(data.get(\"offsetY\"), 0);\n  var multipartXpc = toNumberOrDefault(data.get(\"multipartXpc\"), 0);\n  var multipartYpc = toNumberOrDefault(data.get(\"multipartYpc\"), 0);\n  var diameter = toNumberOrDefault(mapDiameter(), 0);\n  var metersPerDeg = mapCalc.metersPerDegree(lat);\n  return [\n      lon + (offsetX + multipartXpc / 100 * diameter) / metersPerDeg.lon,\n      lat + (offsetY + multipartYpc / 100 * diameter) / metersPerDeg.lat ];\n}\n\nfunction getUrlParam(name, url) {\n    if (!url) {\n        url = window.location.href;\n    }\n    name = name.replace(/[\\[\\]]/g, \"\\\\$&\");\n    var regex = new RegExp(\"[?&]\" + name + \"(=([^&#]*)|&|#|$)\", \"i\"),\n        results = regex.exec(url);\n    if (!results) {\n        return null;\n    }\n    if (!results[2]) {\n        return '';\n    }\n    return decodeURIComponent(results[2].replace(/\\+/g, \" \"));\n}\n\nfunction uriEncodeRequestId(rid) {\n  if (rid === undefined || rid === null || rid === \"\") {\n    return rid;\n  }\n  var parts = rid.split('/', 2);\n  return parts[0] + '/' + encodeURIComponent(parts[1]);\n}\n\n(function(){\n  var TM_HOST = \"https://\" + TM_DOMAIN;\n  // XXX It would make more sense to have the S3 host in environment.js\n  var MAPS_S3_HOST = window.location.protocol + \"//s3-\" + TM_REGION + \".amazonaws.com/\" + window.TM_ENVIRONMENT + \".maps.touch-mapper\";\n\n  function idStart(id) {\n    return id.split('/', 2)[0];\n  }\n\n  function idVersion(id) {\n    if (idStart(id).substring(0, 1) === 'B') {\n      return 2;\n    } else {\n      return 1;\n    }\n  }\n\n  window.makeCloudFrontInfoUrl = function(id) {\n    if (idVersion(id) === 2) {\n      // v2 ID. Has different prefix from the data files, so S3 can be configured to never expire the info files.\n      return TM_HOST + \"/map/info/\" + idStart(id) + '.json';\n    } else {\n      return TM_HOST + \"/map/\" + idStart(id) + '/info.json';\n    }\n  };\n\n  window.makeS3InfoUrl = function(id) {\n    if (idVersion(id) === 2) {\n      return MAPS_S3_HOST + \"/map/info/\" + idStart(id) + '.json';\n    } else {\n      return MAPS_S3_HOST + \"/map/\" + idStart(id) + '/info.json';\n    }\n  };\n\n  function dataPrefix(id) {\n    if (idVersion(id) === 2) {\n      return \"/map/data/\" + uriEncodeRequestId(id);\n    } else {\n      return \"/map/\" + uriEncodeRequestId(id);\n    }\n  }\n\n  window.makeS3url = function(id) {\n    return MAPS_S3_HOST + dataPrefix(id) + '.stl';\n  };\n\n  window.makeS3urlSvg = function(id) {\n    return MAPS_S3_HOST + dataPrefix(id) + '.svg';\n  };\n\n  window.makeCloudFrontUrl = function(id) {\n    return TM_HOST + dataPrefix(id) + '.stl';\n  };\n\n  window.makeCloudFrontUrlWays = function(id) {\n    return TM_HOST + dataPrefix(id) + '-ways.stl';\n  };\n\n  window.makeCloudFrontUrlRest = function(id) {\n    return TM_HOST + dataPrefix(id) + '-rest.stl';\n  };\n\n  window.makeCloudFrontUrlSvg = function(id) {\n    return TM_HOST + dataPrefix(id) + '.svg';\n  };\n\n  window.makeCloudFrontUrlPdf = function(id) {\n    return TM_HOST + dataPrefix(id) + '.pdf';\n  };\n\n  window.makeCloudFrontUrlBlend = function(id) {\n    return TM_HOST + dataPrefix(id) + '.blend';\n  };\n\n  window.makeCloudFrontMapContentUrl = function(id) {\n    return TM_HOST + dataPrefix(id) + '.map-content.json';\n  };\n\n  window.makeMapPageUrlRelative = function(id) {\n    return \"map?map=\" + idStart(id);\n  };\n\n  window.makeMapPermaUrl = function(id) {\n    return TM_HOST + '?map=' + idStart(id);\n  };\n\n  window.makeReturnUrl = window.makeMapPermaUrl;\n})();\n\nfunction showError(errorMsg) {\n  $(\"#output\").append(\n    $(\"<div>\").text(errorMsg).addClass(\"error-msg large-row\").attr(\"role\", \"alert\")\n  ).slideDown();\n}\n\nif (!String.prototype.startsWith) {\n  String.prototype.startsWith = function(searchString, position) {\n    position = position || 0;\n    return this.indexOf(searchString, position) === position;\n  };\n}\n\nfunction optionExistsInSelect($elem, value) {\n  var exists = false;\n  $elem.find('option').each(function(){\n    if (this.value === \"\" + value) {\n        exists = true;\n    }\n  });\n  return exists;\n}\n\nfunction setLocalStorage(key, value) {\n  window.localStorage[key] = value;\n}\n\nfunction getLocalStorageStr(key, defaultValue) {\n  return window.localStorage[key] || defaultValue;\n}\nfunction getLocalStorageInt(key, defaultValue) {\n  var str = getLocalStorageStr(key);\n  var value = str ? parseInt(str, 10) : defaultValue;\n  return isNaN(value) ? defaultValue : value;\n}\n\nfunction newMapId() {\n  function s4() {\n    return Math.floor((1 + Math.random()) * 0x10000)\n      .toString(16)\n      .substring(1);\n  }\n  // Return a v2 ID which starts with capital B.\n  // v1 IDs are all lower case.\n  return 'B' + s4().substring(1) + s4() + s4() + s4();\n}\n\nfunction loadInfoJson(id) {\n  return $.ajax({\n      url: makeCloudFrontInfoUrl(id)\n  }).fail(function(jqXHR, textStatus, errorThrown){\n    if (jqXHR.status === 404) {\n      alert(\"There is no map for ID \" + id);\n    } else {\n      alert(\"Error: \" + textStatus);\n    }\n  }).done(function(data, textStatus, jqXHR){\n    if (typeof data === 'string') {\n      // Old info.json files may have content type text/plain\n      data = JSON.parse(data);\n    };\n    return data;\n  });\n}\n\nwindow.storeMapSettingsFromInfo = function(info) {\n  if (!info || typeof info !== \"object\") {\n    return;\n  }\n\n  var MAP_SIZE_PRESET_VALUES = {\n    \"17\": true,\n    \"20\": true\n  };\n  var MAP_SCALE_PRESET_VALUES = {\n    \"1000\": true,\n    \"1400\": true,\n    \"1800\": true,\n    \"2400\": true,\n    \"3200\": true,\n    \"4200\": true,\n    \"5600\": true,\n    \"7500\": true,\n    \"10000\": true,\n    \"13000\": true,\n    \"17000\": true,\n    \"23000\": true,\n    \"30000\": true,\n    \"40000\": true\n  };\n\n  function withDefault(value, defaultValue) {\n    if (value === undefined || value === null) {\n      return defaultValue;\n    }\n    return value;\n  }\n\n  function toPresetOrEmpty(value, allowedValues) {\n    var key = \"\" + value;\n    if (Object.prototype.hasOwnProperty.call(allowedValues, key)) {\n      return value;\n    }\n    return \"\";\n  }\n\n  function getInfoValue(keys, defaultValue) {\n    var i;\n    for (i = 0; i < keys.length; i++) {\n      if (info[keys[i]] !== undefined && info[keys[i]] !== null) {\n        return info[keys[i]];\n      }\n    }\n    return defaultValue;\n  }\n\n  function normalizeContentMode(value) {\n    if (value === undefined || value === null) {\n      return \"normal\";\n    }\n    var normalized = (\"\" + value).toLowerCase();\n    if (normalized === \"normal\" || normalized === \"no-buildings\" || normalized === \"only-big-roads\") {\n      return normalized;\n    }\n    return \"normal\";\n  }\n\n  function normalizeTargetRoadDensityUi(value) {\n    var number = parseInt(value, 10);\n    if (isNaN(number)) {\n      return 10;\n    }\n    if (number < 1) {\n      return 1;\n    }\n    if (number > 100) {\n      return 100;\n    }\n    return number;\n  }\n\n  var addrShort = getInfoValue([\"addrShort\", \"addr_short\"], \"\");\n  var addrLong = getInfoValue([\"addrLong\", \"addr_long\"], \"\");\n  var lat = getInfoValue([\"lat\"], undefined);\n  var lon = getInfoValue([\"lon\"], undefined);\n  var printingTech = getInfoValue([\"printingTech\", \"printing_tech\"], \"3d\");\n  var contentMode = normalizeContentMode(getInfoValue([\"contentMode\"], \"normal\"));\n  var targetRoadDensityUi = normalizeTargetRoadDensityUi(\n    getInfoValue([\"targetRoadDensity\", \"target_road_density\"], 10)\n  );\n  var hideLocationMarker = getInfoValue([\"hideLocationMarker\", \"hide_location_marker\"], false);\n  var size = getInfoValue([\"size\"], undefined);\n  var scale = getInfoValue([\"scale\"], undefined);\n  var advancedMode = getInfoValue([\"advancedMode\", \"advanced_mode\"], false);\n  var multipartMode = getInfoValue([\"multipartMode\", \"multipart_mode\"], false);\n\n  setLocalStorage(\"addresses\", JSON.stringify([{\n    addrShort: withDefault(addrShort, \"\"),\n    addrLong: withDefault(addrLong, \"\"),\n    lat: lat,\n    lon: lon\n  }]));\n  setLocalStorage(\"addressesSelectedIndex\", 0);\n\n  setLocalStorage(\"offsetX\", getInfoValue([\"offsetX\", \"offset_x\"], 0));\n  setLocalStorage(\"offsetY\", getInfoValue([\"offsetY\", \"offset_y\"], 0));\n  setLocalStorage(\"printing-tech\", printingTech);\n  setLocalStorage(\"content-mode\", contentMode);\n  setLocalStorage(\"target-road-density-ui\", targetRoadDensityUi);\n  localStorage.removeItem(\"exclude-buildings\");\n  setLocalStorage(\"hide-location-marker\", hideLocationMarker);\n  setLocalStorage(\"map-size-preset\", toPresetOrEmpty(size, MAP_SIZE_PRESET_VALUES));\n  setLocalStorage(\"map-scale-preset\", toPresetOrEmpty(scale, MAP_SCALE_PRESET_VALUES));\n  setLocalStorage(\"advancedMode\", advancedMode);\n  setLocalStorage(\"lat\", lat);\n  setLocalStorage(\"lon\", lon);\n  setLocalStorage(\"size\", size);\n  setLocalStorage(\"scale\", scale);\n  setLocalStorage(\"multipartMode\", multipartMode);\n  setLocalStorage(\"multipartXpc\", getInfoValue([\"multipartXpc\", \"multipart_xpc\"], 0));\n  setLocalStorage(\"multipartYpc\", getInfoValue([\"multipartYpc\", \"multipart_ypc\"], 0));\n  setLocalStorage(\"previousAddress\", withDefault(addrLong, \"\"));\n};\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-STLLoader-20160122.js",
    "content": "/**\n * @author aleeper / http://adamleeper.com/\n * @author mrdoob / http://mrdoob.com/\n * @author gero3 / https://github.com/gero3\n *\n * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.\n *\n * Supports both binary and ASCII encoded files, with automatic detection of type.\n *\n * Limitations:\n *  Binary decoding supports \"Magics\" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).\n *  There is perhaps some question as to how valid it is to always assume little-endian-ness.\n *  ASCII decoding assumes file is UTF-8. Seems to work for the examples...\n *\n * Usage:\n *  var loader = new THREE.STLLoader();\n *  loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {\n *    scene.add( new THREE.Mesh( geometry ) );\n *  });\n *\n * For binary STLs geometry might contain colors for vertices. To use it:\n *  // use the same code to load STL as above\n *  if (geometry.hasColors) {\n *    material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: THREE.VertexColors });\n *  } else { .... }\n *  var mesh = new THREE.Mesh( geometry, material );\n */\n\n\nTHREE.STLLoader = function ( manager ) {\n\n\tthis.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;\n\n};\n\nTHREE.STLLoader.prototype = {\n\n\tconstructor: THREE.STLLoader,\n\n\tload: function ( url, onLoad, onProgress, onError ) {\n\n\t\tvar scope = this;\n\n\t\tvar loader = new THREE.XHRLoader( scope.manager );\n\t\tloader.setCrossOrigin( this.crossOrigin );\n\t\tloader.setResponseType( 'arraybuffer' );\n\t\tloader.load( url, function ( text ) {\n\n\t\t\tonLoad( scope.parse( text ) );\n\n\t\t}, onProgress, onError );\n\n\t},\n\n\tsetCrossOrigin: function ( value ) {\n\n\t\tthis.crossOrigin = value;\n\n\t},\n\n\tparse: function ( data ) {\n\n\t\tvar isBinary = function () {\n\n\t\t\tvar expect, face_size, n_faces, reader;\n\t\t\treader = new DataView( binData );\n\t\t\tface_size = ( 32 / 8 * 3 ) + ( ( 32 / 8 * 3 ) * 3 ) + ( 16 / 8 );\n\t\t\tn_faces = reader.getUint32( 80, true );\n\t\t\texpect = 80 + ( 32 / 8 ) + ( n_faces * face_size );\n\n\t\t\tif ( expect === reader.byteLength ) {\n\n\t\t\t\treturn true;\n\n\t\t\t}\n\n\t\t\t// some binary files will have different size from expected,\n\t\t\t// checking characters higher than ASCII to confirm is binary\n\t\t\tvar fileLength = reader.byteLength;\n\t\t\tfor ( var index = 0; index < fileLength; index ++ ) {\n\n\t\t\t\tif ( reader.getUint8( index, false ) > 127 ) {\n\n\t\t\t\t\treturn true;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn false;\n\n\t\t};\n\n\t\tvar binData = this.ensureBinary( data );\n\n\t\treturn isBinary()\n\t\t\t? this.parseBinary( binData )\n\t\t\t: this.parseASCII( this.ensureString( data ) );\n\n\t},\n\n\tparseBinary: function ( data ) {\n\n\t\tvar reader = new DataView( data );\n\t\tvar faces = reader.getUint32( 80, true );\n\n\t\tvar r, g, b, hasColors = false, colors;\n\t\tvar defaultR, defaultG, defaultB, alpha;\n\n\t\t// process STL header\n\t\t// check for default color in header (\"COLOR=rgba\" sequence).\n\n\t\tfor ( var index = 0; index < 80 - 10; index ++ ) {\n\n\t\t\tif ( ( reader.getUint32( index, false ) == 0x434F4C4F /*COLO*/ ) &&\n\t\t\t\t( reader.getUint8( index + 4 ) == 0x52 /*'R'*/ ) &&\n\t\t\t\t( reader.getUint8( index + 5 ) == 0x3D /*'='*/ ) ) {\n\n\t\t\t\thasColors = true;\n\t\t\t\tcolors = new Float32Array( faces * 3 * 3 );\n\n\t\t\t\tdefaultR = reader.getUint8( index + 6 ) / 255;\n\t\t\t\tdefaultG = reader.getUint8( index + 7 ) / 255;\n\t\t\t\tdefaultB = reader.getUint8( index + 8 ) / 255;\n\t\t\t\talpha = reader.getUint8( index + 9 ) / 255;\n\n\t\t\t}\n\n\t\t}\n\n\t\tvar dataOffset = 84;\n\t\tvar faceLength = 12 * 4 + 2;\n\n\t\tvar offset = 0;\n\n\t\tvar geometry = new THREE.BufferGeometry();\n\n\t\tvar vertices = new Float32Array( faces * 3 * 3 );\n\t\tvar normals = new Float32Array( faces * 3 * 3 );\n\n\t\tfor ( var face = 0; face < faces; face ++ ) {\n\n\t\t\tvar start = dataOffset + face * faceLength;\n\t\t\tvar normalX = reader.getFloat32( start, true );\n\t\t\tvar normalY = reader.getFloat32( start + 4, true );\n\t\t\tvar normalZ = reader.getFloat32( start + 8, true );\n\n\t\t\tif ( hasColors ) {\n\n\t\t\t\tvar packedColor = reader.getUint16( start + 48, true );\n\n\t\t\t\tif ( ( packedColor & 0x8000 ) === 0 ) {\n\n\t\t\t\t\t// facet has its own unique color\n\n\t\t\t\t\tr = ( packedColor & 0x1F ) / 31;\n\t\t\t\t\tg = ( ( packedColor >> 5 ) & 0x1F ) / 31;\n\t\t\t\t\tb = ( ( packedColor >> 10 ) & 0x1F ) / 31;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tr = defaultR;\n\t\t\t\t\tg = defaultG;\n\t\t\t\t\tb = defaultB;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tfor ( var i = 1; i <= 3; i ++ ) {\n\n\t\t\t\tvar vertexstart = start + i * 12;\n\n\t\t\t\tvertices[ offset ] = reader.getFloat32( vertexstart, true );\n\t\t\t\tvertices[ offset + 1 ] = reader.getFloat32( vertexstart + 4, true );\n\t\t\t\tvertices[ offset + 2 ] = reader.getFloat32( vertexstart + 8, true );\n\n\t\t\t\tnormals[ offset ] = normalX;\n\t\t\t\tnormals[ offset + 1 ] = normalY;\n\t\t\t\tnormals[ offset + 2 ] = normalZ;\n\n\t\t\t\tif ( hasColors ) {\n\n\t\t\t\t\tcolors[ offset ] = r;\n\t\t\t\t\tcolors[ offset + 1 ] = g;\n\t\t\t\t\tcolors[ offset + 2 ] = b;\n\n\t\t\t\t}\n\n\t\t\t\toffset += 3;\n\n\t\t\t}\n\n\t\t}\n\n\t\tgeometry.addAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );\n\t\tgeometry.addAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );\n\n\t\tif ( hasColors ) {\n\n\t\t\tgeometry.addAttribute( 'color', new THREE.BufferAttribute( colors, 3 ) );\n\t\t\tgeometry.hasColors = true;\n\t\t\tgeometry.alpha = alpha;\n\n\t\t}\n\n\t\treturn geometry;\n\n\t},\n\n\tparseASCII: function ( data ) {\n\n\t\tvar geometry, length, normal, patternFace, patternNormal, patternVertex, result, text;\n\t\tgeometry = new THREE.Geometry();\n\t\tpatternFace = /facet([\\s\\S]*?)endfacet/g;\n\n\t\twhile ( ( result = patternFace.exec( data ) ) !== null ) {\n\n\t\t\ttext = result[ 0 ];\n\t\t\tpatternNormal = /normal[\\s]+([\\-+]?[0-9]+\\.?[0-9]*([eE][\\-+]?[0-9]+)?)+[\\s]+([\\-+]?[0-9]*\\.?[0-9]+([eE][\\-+]?[0-9]+)?)+[\\s]+([\\-+]?[0-9]*\\.?[0-9]+([eE][\\-+]?[0-9]+)?)+/g;\n\n\t\t\twhile ( ( result = patternNormal.exec( text ) ) !== null ) {\n\n\t\t\t\tnormal = new THREE.Vector3( parseFloat( result[ 1 ] ), parseFloat( result[ 3 ] ), parseFloat( result[ 5 ] ) );\n\n\t\t\t}\n\n\t\t\tpatternVertex = /vertex[\\s]+([\\-+]?[0-9]+\\.?[0-9]*([eE][\\-+]?[0-9]+)?)+[\\s]+([\\-+]?[0-9]*\\.?[0-9]+([eE][\\-+]?[0-9]+)?)+[\\s]+([\\-+]?[0-9]*\\.?[0-9]+([eE][\\-+]?[0-9]+)?)+/g;\n\n\t\t\twhile ( ( result = patternVertex.exec( text ) ) !== null ) {\n\n\t\t\t\tgeometry.vertices.push( new THREE.Vector3( parseFloat( result[ 1 ] ), parseFloat( result[ 3 ] ), parseFloat( result[ 5 ] ) ) );\n\n\t\t\t}\n\n\t\t\tlength = geometry.vertices.length;\n\n\t\t\tgeometry.faces.push( new THREE.Face3( length - 3, length - 2, length - 1, normal ) );\n\n\t\t}\n\n\t\tgeometry.computeBoundingBox();\n\t\tgeometry.computeBoundingSphere();\n\n\t\treturn geometry;\n\n\t},\n\n\tensureString: function ( buf ) {\n\n\t\tif ( typeof buf !== \"string\" ) {\n\n\t\t\tvar array_buffer = new Uint8Array( buf );\n\t\t\tvar str = '';\n\t\t\tfor ( var i = 0; i < buf.byteLength; i ++ ) {\n\n\t\t\t\tstr += String.fromCharCode( array_buffer[ i ] ); // implicitly assumes little-endian\n\n\t\t\t}\n\t\t\treturn str;\n\n\t\t} else {\n\n\t\t\treturn buf;\n\n\t\t}\n\n\t},\n\n\tensureBinary: function ( buf ) {\n\n\t\tif ( typeof buf === \"string\" ) {\n\n\t\t\tvar array_buffer = new Uint8Array( buf.length );\n\t\t\tfor ( var i = 0; i < buf.length; i ++ ) {\n\n\t\t\t\tarray_buffer[ i ] = buf.charCodeAt( i ) & 0xff; // implicitly assumes little-endian\n\n\t\t\t}\n\t\t\treturn array_buffer.buffer || array_buffer;\n\n\t\t} else {\n\n\t\t\treturn buf;\n\n\t\t}\n\n\t}\n\n};\n\nif ( typeof DataView === 'undefined' ) {\n\n\tDataView = function( buffer, byteOffset, byteLength ) {\n\n\t\tthis.buffer = buffer;\n\t\tthis.byteOffset = byteOffset || 0;\n\t\tthis.byteLength = byteLength || buffer.byteLength || buffer.length;\n\t\tthis._isString = typeof buffer === \"string\";\n\n\t};\n\n\tDataView.prototype = {\n\n\t\t_getCharCodes: function( buffer, start, length ) {\n\n\t\t\tstart = start || 0;\n\t\t\tlength = length || buffer.length;\n\t\t\tvar end = start + length;\n\t\t\tvar codes = [];\n\t\t\tfor ( var i = start; i < end; i ++ ) {\n\n\t\t\t\tcodes.push( buffer.charCodeAt( i ) & 0xff );\n\n\t\t\t}\n\t\t\treturn codes;\n\n\t\t},\n\n\t\t_getBytes: function ( length, byteOffset, littleEndian ) {\n\n\t\t\tvar result;\n\n\t\t\t// Handle the lack of endianness\n\t\t\tif ( littleEndian === undefined ) {\n\n\t\t\t\tlittleEndian = this._littleEndian;\n\n\t\t\t}\n\n\t\t\t// Handle the lack of byteOffset\n\t\t\tif ( byteOffset === undefined ) {\n\n\t\t\t\tbyteOffset = this.byteOffset;\n\n\t\t\t} else {\n\n\t\t\t\tbyteOffset = this.byteOffset + byteOffset;\n\n\t\t\t}\n\n\t\t\tif ( length === undefined ) {\n\n\t\t\t\tlength = this.byteLength - byteOffset;\n\n\t\t\t}\n\n\t\t\t// Error Checking\n\t\t\tif ( typeof byteOffset !== 'number' ) {\n\n\t\t\t\tthrow new TypeError( 'DataView byteOffset is not a number' );\n\n\t\t\t}\n\n\t\t\tif ( length < 0 || byteOffset + length > this.byteLength ) {\n\n\t\t\t\tthrow new Error( 'DataView length or (byteOffset+length) value is out of bounds' );\n\n\t\t\t}\n\n\t\t\tif ( this.isString ) {\n\n\t\t\t\tresult = this._getCharCodes( this.buffer, byteOffset, byteOffset + length );\n\n\t\t\t} else {\n\n\t\t\t\tresult = this.buffer.slice( byteOffset, byteOffset + length );\n\n\t\t\t}\n\n\t\t\tif ( ! littleEndian && length > 1 ) {\n\n\t\t\t\tif ( Array.isArray( result ) === false ) {\n\n\t\t\t\t\tresult = Array.prototype.slice.call( result );\n\n\t\t\t\t}\n\n\t\t\t\tresult.reverse();\n\n\t\t\t}\n\n\t\t\treturn result;\n\n\t\t},\n\n\t\t// Compatibility functions on a String Buffer\n\n\t\tgetFloat64: function ( byteOffset, littleEndian ) {\n\n\t\t\tvar b = this._getBytes( 8, byteOffset, littleEndian ),\n\n\t\t\t\tsign = 1 - ( 2 * ( b[ 7 ] >> 7 ) ),\n\t\t\t\texponent = ( ( ( ( b[ 7 ] << 1 ) & 0xff ) << 3 ) | ( b[ 6 ] >> 4 ) ) - ( ( 1 << 10 ) - 1 ),\n\n\t\t\t// Binary operators such as | and << operate on 32 bit values, using + and Math.pow(2) instead\n\t\t\t\tmantissa = ( ( b[ 6 ] & 0x0f ) * Math.pow( 2, 48 ) ) + ( b[ 5 ] * Math.pow( 2, 40 ) ) + ( b[ 4 ] * Math.pow( 2, 32 ) ) +\n\t\t\t\t\t\t\t( b[ 3 ] * Math.pow( 2, 24 ) ) + ( b[ 2 ] * Math.pow( 2, 16 ) ) + ( b[ 1 ] * Math.pow( 2, 8 ) ) + b[ 0 ];\n\n\t\t\tif ( exponent === 1024 ) {\n\n\t\t\t\tif ( mantissa !== 0 ) {\n\n\t\t\t\t\treturn NaN;\n\n\t\t\t\t} else {\n\n\t\t\t\t\treturn sign * Infinity;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( exponent === - 1023 ) {\n\n\t\t\t\t// Denormalized\n\t\t\t\treturn sign * mantissa * Math.pow( 2, - 1022 - 52 );\n\n\t\t\t}\n\n\t\t\treturn sign * ( 1 + mantissa * Math.pow( 2, - 52 ) ) * Math.pow( 2, exponent );\n\n\t\t},\n\n\t\tgetFloat32: function ( byteOffset, littleEndian ) {\n\n\t\t\tvar b = this._getBytes( 4, byteOffset, littleEndian ),\n\n\t\t\t\tsign = 1 - ( 2 * ( b[ 3 ] >> 7 ) ),\n\t\t\t\texponent = ( ( ( b[ 3 ] << 1 ) & 0xff ) | ( b[ 2 ] >> 7 ) ) - 127,\n\t\t\t\tmantissa = ( ( b[ 2 ] & 0x7f ) << 16 ) | ( b[ 1 ] << 8 ) | b[ 0 ];\n\n\t\t\tif ( exponent === 128 ) {\n\n\t\t\t\tif ( mantissa !== 0 ) {\n\n\t\t\t\t\treturn NaN;\n\n\t\t\t\t} else {\n\n\t\t\t\t\treturn sign * Infinity;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( exponent === - 127 ) {\n\n\t\t\t\t// Denormalized\n\t\t\t\treturn sign * mantissa * Math.pow( 2, - 126 - 23 );\n\n\t\t\t}\n\n\t\t\treturn sign * ( 1 + mantissa * Math.pow( 2, - 23 ) ) * Math.pow( 2, exponent );\n\n\t\t},\n\n\t\tgetInt32: function ( byteOffset, littleEndian ) {\n\n\t\t\tvar b = this._getBytes( 4, byteOffset, littleEndian );\n\t\t\treturn ( b[ 3 ] << 24 ) | ( b[ 2 ] << 16 ) | ( b[ 1 ] << 8 ) | b[ 0 ];\n\n\t\t},\n\n\t\tgetUint32: function ( byteOffset, littleEndian ) {\n\n\t\t\treturn this.getInt32( byteOffset, littleEndian ) >>> 0;\n\n\t\t},\n\n\t\tgetInt16: function ( byteOffset, littleEndian ) {\n\n\t\t\treturn ( this.getUint16( byteOffset, littleEndian ) << 16 ) >> 16;\n\n\t\t},\n\n\t\tgetUint16: function ( byteOffset, littleEndian ) {\n\n\t\t\tvar b = this._getBytes( 2, byteOffset, littleEndian );\n\t\t\treturn ( b[ 1 ] << 8 ) | b[ 0 ];\n\n\t\t},\n\n\t\tgetInt8: function ( byteOffset ) {\n\n\t\t\treturn ( this.getUint8( byteOffset ) << 24 ) >> 24;\n\n\t\t},\n\n\t\tgetUint8: function ( byteOffset ) {\n\n\t\t\treturn this._getBytes( 1, byteOffset )[ 0 ];\n\n\t\t}\n\n\t };\n\n}\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-addons/controls/OrbitControls.js",
    "content": "import {\n\tControls,\n\tMOUSE,\n\tQuaternion,\n\tSpherical,\n\tTOUCH,\n\tVector2,\n\tVector3,\n\tPlane,\n\tRay,\n\tMathUtils\n} from 'three';\n\n/**\n * Fires when the camera has been transformed by the controls.\n *\n * @event OrbitControls#change\n * @type {Object}\n */\nconst _changeEvent = { type: 'change' };\n\n/**\n * Fires when an interaction was initiated.\n *\n * @event OrbitControls#start\n * @type {Object}\n */\nconst _startEvent = { type: 'start' };\n\n/**\n * Fires when an interaction has finished.\n *\n * @event OrbitControls#end\n * @type {Object}\n */\nconst _endEvent = { type: 'end' };\n\nconst _ray = new Ray();\nconst _plane = new Plane();\nconst _TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );\n\nconst _v = new Vector3();\nconst _twoPI = 2 * Math.PI;\n\nconst _STATE = {\n\tNONE: - 1,\n\tROTATE: 0,\n\tDOLLY: 1,\n\tPAN: 2,\n\tTOUCH_ROTATE: 3,\n\tTOUCH_PAN: 4,\n\tTOUCH_DOLLY_PAN: 5,\n\tTOUCH_DOLLY_ROTATE: 6\n};\nconst _EPS = 0.000001;\n\n\n/**\n * Orbit controls allow the camera to orbit around a target.\n *\n * OrbitControls performs orbiting, dollying (zooming), and panning. Unlike {@link TrackballControls},\n * it maintains the \"up\" direction `object.up` (+Y by default).\n *\n * - Orbit: Left mouse / touch: one-finger move.\n * - Zoom: Middle mouse, or mousewheel / touch: two-finger spread or squish.\n * - Pan: Right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move.\n *\n * ```js\n * const controls = new OrbitControls( camera, renderer.domElement );\n *\n * // controls.update() must be called after any manual changes to the camera's transform\n * camera.position.set( 0, 20, 100 );\n * controls.update();\n *\n * function animate() {\n *\n * \t// required if controls.enableDamping or controls.autoRotate are set to true\n * \tcontrols.update();\n *\n * \trenderer.render( scene, camera );\n *\n * }\n * ```\n *\n * @augments Controls\n * @three_import import { OrbitControls } from 'three/addons/controls/OrbitControls.js';\n */\nclass OrbitControls extends Controls {\n\n\t/**\n\t * Constructs a new controls instance.\n\t *\n\t * @param {Object3D} object - The object that is managed by the controls.\n\t * @param {?HTMLElement} domElement - The HTML element used for event listeners.\n\t */\n\tconstructor( object, domElement = null ) {\n\n\t\tsuper( object, domElement );\n\n\t\tthis.state = _STATE.NONE;\n\n\t\t/**\n\t\t * The focus point of the controls, the `object` orbits around this.\n\t\t * It can be updated manually at any point to change the focus of the controls.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.target = new Vector3();\n\n\t\t/**\n\t\t * The focus point of the `minTargetRadius` and `maxTargetRadius` limits.\n\t\t * It can be updated manually at any point to change the center of interest\n\t\t * for the `target`.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.cursor = new Vector3();\n\n\t\t/**\n\t\t * How far you can dolly in (perspective camera only).\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.minDistance = 0;\n\n\t\t/**\n\t\t * How far you can dolly out (perspective camera only).\n\t\t *\n\t\t * @type {number}\n\t\t * @default Infinity\n\t\t */\n\t\tthis.maxDistance = Infinity;\n\n\t\t/**\n\t\t * How far you can zoom in (orthographic camera only).\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.minZoom = 0;\n\n\t\t/**\n\t\t * How far you can zoom out (orthographic camera only).\n\t\t *\n\t\t * @type {number}\n\t\t * @default Infinity\n\t\t */\n\t\tthis.maxZoom = Infinity;\n\n\t\t/**\n\t\t * How close you can get the target to the 3D `cursor`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.minTargetRadius = 0;\n\n\t\t/**\n\t\t * How far you can move the target from the 3D `cursor`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default Infinity\n\t\t */\n\t\tthis.maxTargetRadius = Infinity;\n\n\t\t/**\n\t\t * How far you can orbit vertically, lower limit. Range is `[0, Math.PI]` radians.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.minPolarAngle = 0;\n\n\t\t/**\n\t\t * How far you can orbit vertically, upper limit. Range is `[0, Math.PI]` radians.\n\t\t *\n\t\t * @type {number}\n\t\t * @default Math.PI\n\t\t */\n\t\tthis.maxPolarAngle = Math.PI;\n\n\t\t/**\n\t\t * How far you can orbit horizontally, lower limit. If set, the interval `[ min, max ]`\n\t\t * must be a sub-interval of `[ - 2 PI, 2 PI ]`, with `( max - min < 2 PI )`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default -Infinity\n\t\t */\n\t\tthis.minAzimuthAngle = - Infinity;\n\n\t\t/**\n\t\t * How far you can orbit horizontally, upper limit. If set, the interval `[ min, max ]`\n\t\t * must be a sub-interval of `[ - 2 PI, 2 PI ]`, with `( max - min < 2 PI )`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default -Infinity\n\t\t */\n\t\tthis.maxAzimuthAngle = Infinity;\n\n\t\t/**\n\t\t * Set to `true` to enable damping (inertia), which can be used to give a sense of weight\n\t\t * to the controls. Note that if this is enabled, you must call `update()` in your animation\n\t\t * loop.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.enableDamping = false;\n\n\t\t/**\n\t\t * The damping inertia used if `enableDamping` is set to `true`.\n\t\t *\n\t\t * Note that for this to work, you must call `update()` in your animation loop.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0.05\n\t\t */\n\t\tthis.dampingFactor = 0.05;\n\n\t\t/**\n\t\t * Enable or disable zooming (dollying) of the camera.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.enableZoom = true;\n\n\t\t/**\n\t\t * Speed of zooming / dollying.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.zoomSpeed = 1.0;\n\n\t\t/**\n\t\t * Enable or disable horizontal and vertical rotation of the camera.\n\t\t *\n\t\t * Note that it is possible to disable a single axis by setting the min and max of the\n\t\t * `minPolarAngle` or `minAzimuthAngle` to the same value, which will cause the vertical\n\t\t * or horizontal rotation to be fixed at that value.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.enableRotate = true;\n\n\t\t/**\n\t\t * Speed of rotation.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.rotateSpeed = 1.0;\n\n\t\t/**\n\t\t * How fast to rotate the camera when the keyboard is used.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.keyRotateSpeed = 1.0;\n\n\t\t/**\n\t\t * Enable or disable camera panning.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.enablePan = true;\n\n\t\t/**\n\t\t * Speed of panning.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.panSpeed = 1.0;\n\n\t\t/**\n\t\t * Defines how the camera's position is translated when panning. If `true`, the camera pans\n\t\t * in screen space. Otherwise, the camera pans in the plane orthogonal to the camera's up\n\t\t * direction.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.screenSpacePanning = true;\n\n\t\t/**\n\t\t * How fast to pan the camera when the keyboard is used in\n\t\t * pixels per keypress.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 7\n\t\t */\n\t\tthis.keyPanSpeed = 7.0;\n\n\t\t/**\n\t\t * Setting this property to `true` allows to zoom to the cursor's position.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.zoomToCursor = false;\n\n\t\t/**\n\t\t * Set to true to automatically rotate around the target\n\t\t *\n\t\t * Note that if this is enabled, you must call `update()` in your animation loop.\n\t\t * If you want the auto-rotate speed to be independent of the frame rate (the refresh\n\t\t * rate of the display), you must pass the time `deltaTime`, in seconds, to `update()`.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.autoRotate = false;\n\n\t\t/**\n\t\t * How fast to rotate around the target if `autoRotate` is `true`. The default  equates to 30 seconds\n\t\t * per orbit at 60fps.\n\t\t *\n\t\t * Note that if `autoRotate` is enabled, you must call `update()` in your animation loop.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 2\n\t\t */\n\t\tthis.autoRotateSpeed = 2.0;\n\n\t\t/**\n\t\t * This object contains references to the keycodes for controlling camera panning.\n\t\t *\n\t\t * ```js\n\t\t * controls.keys = {\n\t\t * \tLEFT: 'ArrowLeft', //left arrow\n\t\t * \tUP: 'ArrowUp', // up arrow\n\t\t * \tRIGHT: 'ArrowRight', // right arrow\n\t\t * \tBOTTOM: 'ArrowDown' // down arrow\n\t\t * }\n\t\t * ```\n\t\t * @type {Object}\n\t\t */\n\t\tthis.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };\n\n\t\t/**\n\t\t * This object contains references to the mouse actions used by the controls.\n\t\t *\n\t\t * ```js\n\t\t * controls.mouseButtons = {\n\t\t * \tLEFT: THREE.MOUSE.ROTATE,\n\t\t * \tMIDDLE: THREE.MOUSE.DOLLY,\n\t\t * \tRIGHT: THREE.MOUSE.PAN\n\t\t * }\n\t\t * ```\n\t\t * @type {Object}\n\t\t */\n\t\tthis.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };\n\n\t\t/**\n\t\t * This object contains references to the touch actions used by the controls.\n\t\t *\n\t\t * ```js\n\t\t * controls.mouseButtons = {\n\t\t * \tONE: THREE.TOUCH.ROTATE,\n\t\t * \tTWO: THREE.TOUCH.DOLLY_PAN\n\t\t * }\n\t\t * ```\n\t\t * @type {Object}\n\t\t */\n\t\tthis.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };\n\n\t\t/**\n\t\t * Used internally by `saveState()` and `reset()`.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.target0 = this.target.clone();\n\n\t\t/**\n\t\t * Used internally by `saveState()` and `reset()`.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.position0 = this.object.position.clone();\n\n\t\t/**\n\t\t * Used internally by `saveState()` and `reset()`.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.zoom0 = this.object.zoom;\n\n\t\t// the target DOM element for key events\n\t\tthis._domElementKeyEvents = null;\n\n\t\t// internals\n\n\t\tthis._lastPosition = new Vector3();\n\t\tthis._lastQuaternion = new Quaternion();\n\t\tthis._lastTargetPosition = new Vector3();\n\n\t\t// so camera.up is the orbit axis\n\t\tthis._quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );\n\t\tthis._quatInverse = this._quat.clone().invert();\n\n\t\t// current position in spherical coordinates\n\t\tthis._spherical = new Spherical();\n\t\tthis._sphericalDelta = new Spherical();\n\n\t\tthis._scale = 1;\n\t\tthis._panOffset = new Vector3();\n\n\t\tthis._rotateStart = new Vector2();\n\t\tthis._rotateEnd = new Vector2();\n\t\tthis._rotateDelta = new Vector2();\n\n\t\tthis._panStart = new Vector2();\n\t\tthis._panEnd = new Vector2();\n\t\tthis._panDelta = new Vector2();\n\n\t\tthis._dollyStart = new Vector2();\n\t\tthis._dollyEnd = new Vector2();\n\t\tthis._dollyDelta = new Vector2();\n\n\t\tthis._dollyDirection = new Vector3();\n\t\tthis._mouse = new Vector2();\n\t\tthis._performCursorZoom = false;\n\n\t\tthis._pointers = [];\n\t\tthis._pointerPositions = {};\n\n\t\tthis._controlActive = false;\n\n\t\t// event listeners\n\n\t\tthis._onPointerMove = onPointerMove.bind( this );\n\t\tthis._onPointerDown = onPointerDown.bind( this );\n\t\tthis._onPointerUp = onPointerUp.bind( this );\n\t\tthis._onContextMenu = onContextMenu.bind( this );\n\t\tthis._onMouseWheel = onMouseWheel.bind( this );\n\t\tthis._onKeyDown = onKeyDown.bind( this );\n\n\t\tthis._onTouchStart = onTouchStart.bind( this );\n\t\tthis._onTouchMove = onTouchMove.bind( this );\n\n\t\tthis._onMouseDown = onMouseDown.bind( this );\n\t\tthis._onMouseMove = onMouseMove.bind( this );\n\n\t\tthis._interceptControlDown = interceptControlDown.bind( this );\n\t\tthis._interceptControlUp = interceptControlUp.bind( this );\n\n\t\t//\n\n\t\tif ( this.domElement !== null ) {\n\n\t\t\tthis.connect( this.domElement );\n\n\t\t}\n\n\t\tthis.update();\n\n\t}\n\n\tconnect( element ) {\n\n\t\tsuper.connect( element );\n\n\t\tthis.domElement.addEventListener( 'pointerdown', this._onPointerDown );\n\t\tthis.domElement.addEventListener( 'pointercancel', this._onPointerUp );\n\n\t\tthis.domElement.addEventListener( 'contextmenu', this._onContextMenu );\n\t\tthis.domElement.addEventListener( 'wheel', this._onMouseWheel, { passive: false } );\n\n\t\tconst document = this.domElement.getRootNode(); // offscreen canvas compatibility\n\t\tdocument.addEventListener( 'keydown', this._interceptControlDown, { passive: true, capture: true } );\n\n\t\tthis.domElement.style.touchAction = 'none'; // disable touch scroll\n\n\t}\n\n\tdisconnect() {\n\n\t\tthis.domElement.removeEventListener( 'pointerdown', this._onPointerDown );\n\t\tthis.domElement.ownerDocument.removeEventListener( 'pointermove', this._onPointerMove );\n\t\tthis.domElement.ownerDocument.removeEventListener( 'pointerup', this._onPointerUp );\n\t\tthis.domElement.removeEventListener( 'pointercancel', this._onPointerUp );\n\n\t\tthis.domElement.removeEventListener( 'wheel', this._onMouseWheel );\n\t\tthis.domElement.removeEventListener( 'contextmenu', this._onContextMenu );\n\n\t\tthis.stopListenToKeyEvents();\n\n\t\tconst document = this.domElement.getRootNode(); // offscreen canvas compatibility\n\t\tdocument.removeEventListener( 'keydown', this._interceptControlDown, { capture: true } );\n\n\t\tthis.domElement.style.touchAction = 'auto';\n\n\t}\n\n\tdispose() {\n\n\t\tthis.disconnect();\n\n\t}\n\n\t/**\n\t * Get the current vertical rotation, in radians.\n\t *\n\t * @return {number} The current vertical rotation, in radians.\n\t */\n\tgetPolarAngle() {\n\n\t\treturn this._spherical.phi;\n\n\t}\n\n\t/**\n\t * Get the current horizontal rotation, in radians.\n\t *\n\t * @return {number} The current horizontal rotation, in radians.\n\t */\n\tgetAzimuthalAngle() {\n\n\t\treturn this._spherical.theta;\n\n\t}\n\n\t/**\n\t * Returns the distance from the camera to the target.\n\t *\n\t * @return {number} The distance from the camera to the target.\n\t */\n\tgetDistance() {\n\n\t\treturn this.object.position.distanceTo( this.target );\n\n\t}\n\n\t/**\n\t * Adds key event listeners to the given DOM element.\n\t * `window` is a recommended argument for using this method.\n\t *\n\t * @param {HTMLElement} domElement - The DOM element\n\t */\n\tlistenToKeyEvents( domElement ) {\n\n\t\tdomElement.addEventListener( 'keydown', this._onKeyDown );\n\t\tthis._domElementKeyEvents = domElement;\n\n\t}\n\n\t/**\n\t * Removes the key event listener previously defined with `listenToKeyEvents()`.\n\t */\n\tstopListenToKeyEvents() {\n\n\t\tif ( this._domElementKeyEvents !== null ) {\n\n\t\t\tthis._domElementKeyEvents.removeEventListener( 'keydown', this._onKeyDown );\n\t\t\tthis._domElementKeyEvents = null;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Save the current state of the controls. This can later be recovered with `reset()`.\n\t */\n\tsaveState() {\n\n\t\tthis.target0.copy( this.target );\n\t\tthis.position0.copy( this.object.position );\n\t\tthis.zoom0 = this.object.zoom;\n\n\t}\n\n\t/**\n\t * Reset the controls to their state from either the last time the `saveState()`\n\t * was called, or the initial state.\n\t */\n\treset() {\n\n\t\tthis.target.copy( this.target0 );\n\t\tthis.object.position.copy( this.position0 );\n\t\tthis.object.zoom = this.zoom0;\n\n\t\tthis.object.updateProjectionMatrix();\n\t\tthis.dispatchEvent( _changeEvent );\n\n\t\tthis.update();\n\n\t\tthis.state = _STATE.NONE;\n\n\t}\n\n\tupdate( deltaTime = null ) {\n\n\t\tconst position = this.object.position;\n\n\t\t_v.copy( position ).sub( this.target );\n\n\t\t// rotate offset to \"y-axis-is-up\" space\n\t\t_v.applyQuaternion( this._quat );\n\n\t\t// angle from z-axis around y-axis\n\t\tthis._spherical.setFromVector3( _v );\n\n\t\tif ( this.autoRotate && this.state === _STATE.NONE ) {\n\n\t\t\tthis._rotateLeft( this._getAutoRotationAngle( deltaTime ) );\n\n\t\t}\n\n\t\tif ( this.enableDamping ) {\n\n\t\t\tthis._spherical.theta += this._sphericalDelta.theta * this.dampingFactor;\n\t\t\tthis._spherical.phi += this._sphericalDelta.phi * this.dampingFactor;\n\n\t\t} else {\n\n\t\t\tthis._spherical.theta += this._sphericalDelta.theta;\n\t\t\tthis._spherical.phi += this._sphericalDelta.phi;\n\n\t\t}\n\n\t\t// restrict theta to be between desired limits\n\n\t\tlet min = this.minAzimuthAngle;\n\t\tlet max = this.maxAzimuthAngle;\n\n\t\tif ( isFinite( min ) && isFinite( max ) ) {\n\n\t\t\tif ( min < - Math.PI ) min += _twoPI; else if ( min > Math.PI ) min -= _twoPI;\n\n\t\t\tif ( max < - Math.PI ) max += _twoPI; else if ( max > Math.PI ) max -= _twoPI;\n\n\t\t\tif ( min <= max ) {\n\n\t\t\t\tthis._spherical.theta = Math.max( min, Math.min( max, this._spherical.theta ) );\n\n\t\t\t} else {\n\n\t\t\t\tthis._spherical.theta = ( this._spherical.theta > ( min + max ) / 2 ) ?\n\t\t\t\t\tMath.max( min, this._spherical.theta ) :\n\t\t\t\t\tMath.min( max, this._spherical.theta );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// restrict phi to be between desired limits\n\t\tthis._spherical.phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, this._spherical.phi ) );\n\n\t\tthis._spherical.makeSafe();\n\n\n\t\t// move target to panned location\n\n\t\tif ( this.enableDamping === true ) {\n\n\t\t\tthis.target.addScaledVector( this._panOffset, this.dampingFactor );\n\n\t\t} else {\n\n\t\t\tthis.target.add( this._panOffset );\n\n\t\t}\n\n\t\t// Limit the target distance from the cursor to create a sphere around the center of interest\n\t\tthis.target.sub( this.cursor );\n\t\tthis.target.clampLength( this.minTargetRadius, this.maxTargetRadius );\n\t\tthis.target.add( this.cursor );\n\n\t\tlet zoomChanged = false;\n\t\t// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera\n\t\t// we adjust zoom later in these cases\n\t\tif ( this.zoomToCursor && this._performCursorZoom || this.object.isOrthographicCamera ) {\n\n\t\t\tthis._spherical.radius = this._clampDistance( this._spherical.radius );\n\n\t\t} else {\n\n\t\t\tconst prevRadius = this._spherical.radius;\n\t\t\tthis._spherical.radius = this._clampDistance( this._spherical.radius * this._scale );\n\t\t\tzoomChanged = prevRadius != this._spherical.radius;\n\n\t\t}\n\n\t\t_v.setFromSpherical( this._spherical );\n\n\t\t// rotate offset back to \"camera-up-vector-is-up\" space\n\t\t_v.applyQuaternion( this._quatInverse );\n\n\t\tposition.copy( this.target ).add( _v );\n\n\t\tthis.object.lookAt( this.target );\n\n\t\tif ( this.enableDamping === true ) {\n\n\t\t\tthis._sphericalDelta.theta *= ( 1 - this.dampingFactor );\n\t\t\tthis._sphericalDelta.phi *= ( 1 - this.dampingFactor );\n\n\t\t\tthis._panOffset.multiplyScalar( 1 - this.dampingFactor );\n\n\t\t} else {\n\n\t\t\tthis._sphericalDelta.set( 0, 0, 0 );\n\n\t\t\tthis._panOffset.set( 0, 0, 0 );\n\n\t\t}\n\n\t\t// adjust camera position\n\t\tif ( this.zoomToCursor && this._performCursorZoom ) {\n\n\t\t\tlet newRadius = null;\n\t\t\tif ( this.object.isPerspectiveCamera ) {\n\n\t\t\t\t// move the camera down the pointer ray\n\t\t\t\t// this method avoids floating point error\n\t\t\t\tconst prevRadius = _v.length();\n\t\t\t\tnewRadius = this._clampDistance( prevRadius * this._scale );\n\n\t\t\t\tconst radiusDelta = prevRadius - newRadius;\n\t\t\t\tthis.object.position.addScaledVector( this._dollyDirection, radiusDelta );\n\t\t\t\tthis.object.updateMatrixWorld();\n\n\t\t\t\tzoomChanged = !! radiusDelta;\n\n\t\t\t} else if ( this.object.isOrthographicCamera ) {\n\n\t\t\t\t// adjust the ortho camera position based on zoom changes\n\t\t\t\tconst mouseBefore = new Vector3( this._mouse.x, this._mouse.y, 0 );\n\t\t\t\tmouseBefore.unproject( this.object );\n\n\t\t\t\tconst prevZoom = this.object.zoom;\n\t\t\t\tthis.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) );\n\t\t\t\tthis.object.updateProjectionMatrix();\n\n\t\t\t\tzoomChanged = prevZoom !== this.object.zoom;\n\n\t\t\t\tconst mouseAfter = new Vector3( this._mouse.x, this._mouse.y, 0 );\n\t\t\t\tmouseAfter.unproject( this.object );\n\n\t\t\t\tthis.object.position.sub( mouseAfter ).add( mouseBefore );\n\t\t\t\tthis.object.updateMatrixWorld();\n\n\t\t\t\tnewRadius = _v.length();\n\n\t\t\t} else {\n\n\t\t\t\tconsole.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );\n\t\t\t\tthis.zoomToCursor = false;\n\n\t\t\t}\n\n\t\t\t// handle the placement of the target\n\t\t\tif ( newRadius !== null ) {\n\n\t\t\t\tif ( this.screenSpacePanning ) {\n\n\t\t\t\t\t// position the orbit target in front of the new camera position\n\t\t\t\t\tthis.target.set( 0, 0, - 1 )\n\t\t\t\t\t\t.transformDirection( this.object.matrix )\n\t\t\t\t\t\t.multiplyScalar( newRadius )\n\t\t\t\t\t\t.add( this.object.position );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// get the ray and translation plane to compute target\n\t\t\t\t\t_ray.origin.copy( this.object.position );\n\t\t\t\t\t_ray.direction.set( 0, 0, - 1 ).transformDirection( this.object.matrix );\n\n\t\t\t\t\t// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid\n\t\t\t\t\t// extremely large values\n\t\t\t\t\tif ( Math.abs( this.object.up.dot( _ray.direction ) ) < _TILT_LIMIT ) {\n\n\t\t\t\t\t\tthis.object.lookAt( this.target );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_plane.setFromNormalAndCoplanarPoint( this.object.up, this.target );\n\t\t\t\t\t\t_ray.intersectPlane( _plane, this.target );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else if ( this.object.isOrthographicCamera ) {\n\n\t\t\tconst prevZoom = this.object.zoom;\n\t\t\tthis.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) );\n\n\t\t\tif ( prevZoom !== this.object.zoom ) {\n\n\t\t\t\tthis.object.updateProjectionMatrix();\n\t\t\t\tzoomChanged = true;\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis._scale = 1;\n\t\tthis._performCursorZoom = false;\n\n\t\t// update condition is:\n\t\t// min(camera displacement, camera rotation in radians)^2 > EPS\n\t\t// using small-angle approximation cos(x/2) = 1 - x^2 / 8\n\n\t\tif ( zoomChanged ||\n\t\t\tthis._lastPosition.distanceToSquared( this.object.position ) > _EPS ||\n\t\t\t8 * ( 1 - this._lastQuaternion.dot( this.object.quaternion ) ) > _EPS ||\n\t\t\tthis._lastTargetPosition.distanceToSquared( this.target ) > _EPS ) {\n\n\t\t\tthis.dispatchEvent( _changeEvent );\n\n\t\t\tthis._lastPosition.copy( this.object.position );\n\t\t\tthis._lastQuaternion.copy( this.object.quaternion );\n\t\t\tthis._lastTargetPosition.copy( this.target );\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t_getAutoRotationAngle( deltaTime ) {\n\n\t\tif ( deltaTime !== null ) {\n\n\t\t\treturn ( _twoPI / 60 * this.autoRotateSpeed ) * deltaTime;\n\n\t\t} else {\n\n\t\t\treturn _twoPI / 60 / 60 * this.autoRotateSpeed;\n\n\t\t}\n\n\t}\n\n\t_getZoomScale( delta ) {\n\n\t\tconst normalizedDelta = Math.abs( delta * 0.01 );\n\t\treturn Math.pow( 0.95, this.zoomSpeed * normalizedDelta );\n\n\t}\n\n\t_rotateLeft( angle ) {\n\n\t\tthis._sphericalDelta.theta -= angle;\n\n\t}\n\n\t_rotateUp( angle ) {\n\n\t\tthis._sphericalDelta.phi -= angle;\n\n\t}\n\n\t_panLeft( distance, objectMatrix ) {\n\n\t\t_v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix\n\t\t_v.multiplyScalar( - distance );\n\n\t\tthis._panOffset.add( _v );\n\n\t}\n\n\t_panUp( distance, objectMatrix ) {\n\n\t\tif ( this.screenSpacePanning === true ) {\n\n\t\t\t_v.setFromMatrixColumn( objectMatrix, 1 );\n\n\t\t} else {\n\n\t\t\t_v.setFromMatrixColumn( objectMatrix, 0 );\n\t\t\t_v.crossVectors( this.object.up, _v );\n\n\t\t}\n\n\t\t_v.multiplyScalar( distance );\n\n\t\tthis._panOffset.add( _v );\n\n\t}\n\n\t// deltaX and deltaY are in pixels; right and down are positive\n\t_pan( deltaX, deltaY ) {\n\n\t\tconst element = this.domElement;\n\n\t\tif ( this.object.isPerspectiveCamera ) {\n\n\t\t\t// perspective\n\t\t\tconst position = this.object.position;\n\t\t\t_v.copy( position ).sub( this.target );\n\t\t\tlet targetDistance = _v.length();\n\n\t\t\t// half of the fov is center to top of screen\n\t\t\ttargetDistance *= Math.tan( ( this.object.fov / 2 ) * Math.PI / 180.0 );\n\n\t\t\t// we use only clientHeight here so aspect ratio does not distort speed\n\t\t\tthis._panLeft( 2 * deltaX * targetDistance / element.clientHeight, this.object.matrix );\n\t\t\tthis._panUp( 2 * deltaY * targetDistance / element.clientHeight, this.object.matrix );\n\n\t\t} else if ( this.object.isOrthographicCamera ) {\n\n\t\t\t// orthographic\n\t\t\tthis._panLeft( deltaX * ( this.object.right - this.object.left ) / this.object.zoom / element.clientWidth, this.object.matrix );\n\t\t\tthis._panUp( deltaY * ( this.object.top - this.object.bottom ) / this.object.zoom / element.clientHeight, this.object.matrix );\n\n\t\t} else {\n\n\t\t\t// camera neither orthographic nor perspective\n\t\t\tconsole.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );\n\t\t\tthis.enablePan = false;\n\n\t\t}\n\n\t}\n\n\t_dollyOut( dollyScale ) {\n\n\t\tif ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) {\n\n\t\t\tthis._scale /= dollyScale;\n\n\t\t} else {\n\n\t\t\tconsole.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );\n\t\t\tthis.enableZoom = false;\n\n\t\t}\n\n\t}\n\n\t_dollyIn( dollyScale ) {\n\n\t\tif ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) {\n\n\t\t\tthis._scale *= dollyScale;\n\n\t\t} else {\n\n\t\t\tconsole.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );\n\t\t\tthis.enableZoom = false;\n\n\t\t}\n\n\t}\n\n\t_updateZoomParameters( x, y ) {\n\n\t\tif ( ! this.zoomToCursor ) {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tthis._performCursorZoom = true;\n\n\t\tconst rect = this.domElement.getBoundingClientRect();\n\t\tconst dx = x - rect.left;\n\t\tconst dy = y - rect.top;\n\t\tconst w = rect.width;\n\t\tconst h = rect.height;\n\n\t\tthis._mouse.x = ( dx / w ) * 2 - 1;\n\t\tthis._mouse.y = - ( dy / h ) * 2 + 1;\n\n\t\tthis._dollyDirection.set( this._mouse.x, this._mouse.y, 1 ).unproject( this.object ).sub( this.object.position ).normalize();\n\n\t}\n\n\t_clampDistance( dist ) {\n\n\t\treturn Math.max( this.minDistance, Math.min( this.maxDistance, dist ) );\n\n\t}\n\n\t//\n\t// event callbacks - update the object state\n\t//\n\n\t_handleMouseDownRotate( event ) {\n\n\t\tthis._rotateStart.set( event.clientX, event.clientY );\n\n\t}\n\n\t_handleMouseDownDolly( event ) {\n\n\t\tthis._updateZoomParameters( event.clientX, event.clientX );\n\t\tthis._dollyStart.set( event.clientX, event.clientY );\n\n\t}\n\n\t_handleMouseDownPan( event ) {\n\n\t\tthis._panStart.set( event.clientX, event.clientY );\n\n\t}\n\n\t_handleMouseMoveRotate( event ) {\n\n\t\tthis._rotateEnd.set( event.clientX, event.clientY );\n\n\t\tthis._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed );\n\n\t\tconst element = this.domElement;\n\n\t\tthis._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height\n\n\t\tthis._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight );\n\n\t\tthis._rotateStart.copy( this._rotateEnd );\n\n\t\tthis.update();\n\n\t}\n\n\t_handleMouseMoveDolly( event ) {\n\n\t\tthis._dollyEnd.set( event.clientX, event.clientY );\n\n\t\tthis._dollyDelta.subVectors( this._dollyEnd, this._dollyStart );\n\n\t\tif ( this._dollyDelta.y > 0 ) {\n\n\t\t\tthis._dollyOut( this._getZoomScale( this._dollyDelta.y ) );\n\n\t\t} else if ( this._dollyDelta.y < 0 ) {\n\n\t\t\tthis._dollyIn( this._getZoomScale( this._dollyDelta.y ) );\n\n\t\t}\n\n\t\tthis._dollyStart.copy( this._dollyEnd );\n\n\t\tthis.update();\n\n\t}\n\n\t_handleMouseMovePan( event ) {\n\n\t\tthis._panEnd.set( event.clientX, event.clientY );\n\n\t\tthis._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed );\n\n\t\tthis._pan( this._panDelta.x, this._panDelta.y );\n\n\t\tthis._panStart.copy( this._panEnd );\n\n\t\tthis.update();\n\n\t}\n\n\t_handleMouseWheel( event ) {\n\n\t\tthis._updateZoomParameters( event.clientX, event.clientY );\n\n\t\tif ( event.deltaY < 0 ) {\n\n\t\t\tthis._dollyIn( this._getZoomScale( event.deltaY ) );\n\n\t\t} else if ( event.deltaY > 0 ) {\n\n\t\t\tthis._dollyOut( this._getZoomScale( event.deltaY ) );\n\n\t\t}\n\n\t\tthis.update();\n\n\t}\n\n\t_handleKeyDown( event ) {\n\n\t\tlet needsUpdate = false;\n\n\t\tswitch ( event.code ) {\n\n\t\t\tcase this.keys.UP:\n\n\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\tif ( this.enableRotate ) {\n\n\t\t\t\t\t\tthis._rotateUp( _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( this.enablePan ) {\n\n\t\t\t\t\t\tthis._pan( 0, this.keyPanSpeed );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tneedsUpdate = true;\n\t\t\t\tbreak;\n\n\t\t\tcase this.keys.BOTTOM:\n\n\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\tif ( this.enableRotate ) {\n\n\t\t\t\t\t\tthis._rotateUp( - _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( this.enablePan ) {\n\n\t\t\t\t\t\tthis._pan( 0, - this.keyPanSpeed );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tneedsUpdate = true;\n\t\t\t\tbreak;\n\n\t\t\tcase this.keys.LEFT:\n\n\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\tif ( this.enableRotate ) {\n\n\t\t\t\t\t\tthis._rotateLeft( _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( this.enablePan ) {\n\n\t\t\t\t\t\tthis._pan( this.keyPanSpeed, 0 );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tneedsUpdate = true;\n\t\t\t\tbreak;\n\n\t\t\tcase this.keys.RIGHT:\n\n\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\tif ( this.enableRotate ) {\n\n\t\t\t\t\t\tthis._rotateLeft( - _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( this.enablePan ) {\n\n\t\t\t\t\t\tthis._pan( - this.keyPanSpeed, 0 );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tneedsUpdate = true;\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\tif ( needsUpdate ) {\n\n\t\t\t// prevent the browser from scrolling on cursor keys\n\t\t\tevent.preventDefault();\n\n\t\t\tthis.update();\n\n\t\t}\n\n\n\t}\n\n\t_handleTouchStartRotate( event ) {\n\n\t\tif ( this._pointers.length === 1 ) {\n\n\t\t\tthis._rotateStart.set( event.pageX, event.pageY );\n\n\t\t} else {\n\n\t\t\tconst position = this._getSecondPointerPosition( event );\n\n\t\t\tconst x = 0.5 * ( event.pageX + position.x );\n\t\t\tconst y = 0.5 * ( event.pageY + position.y );\n\n\t\t\tthis._rotateStart.set( x, y );\n\n\t\t}\n\n\t}\n\n\t_handleTouchStartPan( event ) {\n\n\t\tif ( this._pointers.length === 1 ) {\n\n\t\t\tthis._panStart.set( event.pageX, event.pageY );\n\n\t\t} else {\n\n\t\t\tconst position = this._getSecondPointerPosition( event );\n\n\t\t\tconst x = 0.5 * ( event.pageX + position.x );\n\t\t\tconst y = 0.5 * ( event.pageY + position.y );\n\n\t\t\tthis._panStart.set( x, y );\n\n\t\t}\n\n\t}\n\n\t_handleTouchStartDolly( event ) {\n\n\t\tconst position = this._getSecondPointerPosition( event );\n\n\t\tconst dx = event.pageX - position.x;\n\t\tconst dy = event.pageY - position.y;\n\n\t\tconst distance = Math.sqrt( dx * dx + dy * dy );\n\n\t\tthis._dollyStart.set( 0, distance );\n\n\t}\n\n\t_handleTouchStartDollyPan( event ) {\n\n\t\tif ( this.enableZoom ) this._handleTouchStartDolly( event );\n\n\t\tif ( this.enablePan ) this._handleTouchStartPan( event );\n\n\t}\n\n\t_handleTouchStartDollyRotate( event ) {\n\n\t\tif ( this.enableZoom ) this._handleTouchStartDolly( event );\n\n\t\tif ( this.enableRotate ) this._handleTouchStartRotate( event );\n\n\t}\n\n\t_handleTouchMoveRotate( event ) {\n\n\t\tif ( this._pointers.length == 1 ) {\n\n\t\t\tthis._rotateEnd.set( event.pageX, event.pageY );\n\n\t\t} else {\n\n\t\t\tconst position = this._getSecondPointerPosition( event );\n\n\t\t\tconst x = 0.5 * ( event.pageX + position.x );\n\t\t\tconst y = 0.5 * ( event.pageY + position.y );\n\n\t\t\tthis._rotateEnd.set( x, y );\n\n\t\t}\n\n\t\tthis._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed );\n\n\t\tconst element = this.domElement;\n\n\t\tthis._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height\n\n\t\tthis._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight );\n\n\t\tthis._rotateStart.copy( this._rotateEnd );\n\n\t}\n\n\t_handleTouchMovePan( event ) {\n\n\t\tif ( this._pointers.length === 1 ) {\n\n\t\t\tthis._panEnd.set( event.pageX, event.pageY );\n\n\t\t} else {\n\n\t\t\tconst position = this._getSecondPointerPosition( event );\n\n\t\t\tconst x = 0.5 * ( event.pageX + position.x );\n\t\t\tconst y = 0.5 * ( event.pageY + position.y );\n\n\t\t\tthis._panEnd.set( x, y );\n\n\t\t}\n\n\t\tthis._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed );\n\n\t\tthis._pan( this._panDelta.x, this._panDelta.y );\n\n\t\tthis._panStart.copy( this._panEnd );\n\n\t}\n\n\t_handleTouchMoveDolly( event ) {\n\n\t\tconst position = this._getSecondPointerPosition( event );\n\n\t\tconst dx = event.pageX - position.x;\n\t\tconst dy = event.pageY - position.y;\n\n\t\tconst distance = Math.sqrt( dx * dx + dy * dy );\n\n\t\tthis._dollyEnd.set( 0, distance );\n\n\t\tthis._dollyDelta.set( 0, Math.pow( this._dollyEnd.y / this._dollyStart.y, this.zoomSpeed ) );\n\n\t\tthis._dollyOut( this._dollyDelta.y );\n\n\t\tthis._dollyStart.copy( this._dollyEnd );\n\n\t\tconst centerX = ( event.pageX + position.x ) * 0.5;\n\t\tconst centerY = ( event.pageY + position.y ) * 0.5;\n\n\t\tthis._updateZoomParameters( centerX, centerY );\n\n\t}\n\n\t_handleTouchMoveDollyPan( event ) {\n\n\t\tif ( this.enableZoom ) this._handleTouchMoveDolly( event );\n\n\t\tif ( this.enablePan ) this._handleTouchMovePan( event );\n\n\t}\n\n\t_handleTouchMoveDollyRotate( event ) {\n\n\t\tif ( this.enableZoom ) this._handleTouchMoveDolly( event );\n\n\t\tif ( this.enableRotate ) this._handleTouchMoveRotate( event );\n\n\t}\n\n\t// pointers\n\n\t_addPointer( event ) {\n\n\t\tthis._pointers.push( event.pointerId );\n\n\t}\n\n\t_removePointer( event ) {\n\n\t\tdelete this._pointerPositions[ event.pointerId ];\n\n\t\tfor ( let i = 0; i < this._pointers.length; i ++ ) {\n\n\t\t\tif ( this._pointers[ i ] == event.pointerId ) {\n\n\t\t\t\tthis._pointers.splice( i, 1 );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t_isTrackingPointer( event ) {\n\n\t\tfor ( let i = 0; i < this._pointers.length; i ++ ) {\n\n\t\t\tif ( this._pointers[ i ] == event.pointerId ) return true;\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t_trackPointer( event ) {\n\n\t\tlet position = this._pointerPositions[ event.pointerId ];\n\n\t\tif ( position === undefined ) {\n\n\t\t\tposition = new Vector2();\n\t\t\tthis._pointerPositions[ event.pointerId ] = position;\n\n\t\t}\n\n\t\tposition.set( event.pageX, event.pageY );\n\n\t}\n\n\t_getSecondPointerPosition( event ) {\n\n\t\tconst pointerId = ( event.pointerId === this._pointers[ 0 ] ) ? this._pointers[ 1 ] : this._pointers[ 0 ];\n\n\t\treturn this._pointerPositions[ pointerId ];\n\n\t}\n\n\t//\n\n\t_customWheelEvent( event ) {\n\n\t\tconst mode = event.deltaMode;\n\n\t\t// minimal wheel event altered to meet delta-zoom demand\n\t\tconst newEvent = {\n\t\t\tclientX: event.clientX,\n\t\t\tclientY: event.clientY,\n\t\t\tdeltaY: event.deltaY,\n\t\t};\n\n\t\tswitch ( mode ) {\n\n\t\t\tcase 1: // LINE_MODE\n\t\t\t\tnewEvent.deltaY *= 16;\n\t\t\t\tbreak;\n\n\t\t\tcase 2: // PAGE_MODE\n\t\t\t\tnewEvent.deltaY *= 100;\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\t// detect if event was triggered by pinching\n\t\tif ( event.ctrlKey && ! this._controlActive ) {\n\n\t\t\tnewEvent.deltaY *= 10;\n\n\t\t}\n\n\t\treturn newEvent;\n\n\t}\n\n}\n\nfunction onPointerDown( event ) {\n\n\tif ( this.enabled === false ) return;\n\n\tif ( this._pointers.length === 0 ) {\n\n\t\tthis.domElement.setPointerCapture( event.pointerId );\n\n\t\tthis.domElement.ownerDocument.addEventListener( 'pointermove', this._onPointerMove );\n\t\tthis.domElement.ownerDocument.addEventListener( 'pointerup', this._onPointerUp );\n\n\t}\n\n\t//\n\n\tif ( this._isTrackingPointer( event ) ) return;\n\n\t//\n\n\tthis._addPointer( event );\n\n\tif ( event.pointerType === 'touch' ) {\n\n\t\tthis._onTouchStart( event );\n\n\t} else {\n\n\t\tthis._onMouseDown( event );\n\n\t}\n\n}\n\nfunction onPointerMove( event ) {\n\n\tif ( this.enabled === false ) return;\n\n\tif ( event.pointerType === 'touch' ) {\n\n\t\tthis._onTouchMove( event );\n\n\t} else {\n\n\t\tthis._onMouseMove( event );\n\n\t}\n\n}\n\nfunction onPointerUp( event ) {\n\n\tthis._removePointer( event );\n\n\tswitch ( this._pointers.length ) {\n\n\t\tcase 0:\n\n\t\t\tthis.domElement.releasePointerCapture( event.pointerId );\n\n\t\t\tthis.domElement.ownerDocument.removeEventListener( 'pointermove', this._onPointerMove );\n\t\t\tthis.domElement.ownerDocument.removeEventListener( 'pointerup', this._onPointerUp );\n\n\t\t\tthis.dispatchEvent( _endEvent );\n\n\t\t\tthis.state = _STATE.NONE;\n\n\t\t\tbreak;\n\n\t\tcase 1:\n\n\t\t\tconst pointerId = this._pointers[ 0 ];\n\t\t\tconst position = this._pointerPositions[ pointerId ];\n\n\t\t\t// minimal placeholder event - allows state correction on pointer-up\n\t\t\tthis._onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );\n\n\t\t\tbreak;\n\n\t}\n\n}\n\nfunction onMouseDown( event ) {\n\n\tlet mouseAction;\n\n\tswitch ( event.button ) {\n\n\t\tcase 0:\n\n\t\t\tmouseAction = this.mouseButtons.LEFT;\n\t\t\tbreak;\n\n\t\tcase 1:\n\n\t\t\tmouseAction = this.mouseButtons.MIDDLE;\n\t\t\tbreak;\n\n\t\tcase 2:\n\n\t\t\tmouseAction = this.mouseButtons.RIGHT;\n\t\t\tbreak;\n\n\t\tdefault:\n\n\t\t\tmouseAction = - 1;\n\n\t}\n\n\tswitch ( mouseAction ) {\n\n\t\tcase MOUSE.DOLLY:\n\n\t\t\tif ( this.enableZoom === false ) return;\n\n\t\t\tthis._handleMouseDownDolly( event );\n\n\t\t\tthis.state = _STATE.DOLLY;\n\n\t\t\tbreak;\n\n\t\tcase MOUSE.ROTATE:\n\n\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\tif ( this.enablePan === false ) return;\n\n\t\t\t\tthis._handleMouseDownPan( event );\n\n\t\t\t\tthis.state = _STATE.PAN;\n\n\t\t\t} else {\n\n\t\t\t\tif ( this.enableRotate === false ) return;\n\n\t\t\t\tthis._handleMouseDownRotate( event );\n\n\t\t\t\tthis.state = _STATE.ROTATE;\n\n\t\t\t}\n\n\t\t\tbreak;\n\n\t\tcase MOUSE.PAN:\n\n\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\tif ( this.enableRotate === false ) return;\n\n\t\t\t\tthis._handleMouseDownRotate( event );\n\n\t\t\t\tthis.state = _STATE.ROTATE;\n\n\t\t\t} else {\n\n\t\t\t\tif ( this.enablePan === false ) return;\n\n\t\t\t\tthis._handleMouseDownPan( event );\n\n\t\t\t\tthis.state = _STATE.PAN;\n\n\t\t\t}\n\n\t\t\tbreak;\n\n\t\tdefault:\n\n\t\t\tthis.state = _STATE.NONE;\n\n\t}\n\n\tif ( this.state !== _STATE.NONE ) {\n\n\t\tthis.dispatchEvent( _startEvent );\n\n\t}\n\n}\n\nfunction onMouseMove( event ) {\n\n\tswitch ( this.state ) {\n\n\t\tcase _STATE.ROTATE:\n\n\t\t\tif ( this.enableRotate === false ) return;\n\n\t\t\tthis._handleMouseMoveRotate( event );\n\n\t\t\tbreak;\n\n\t\tcase _STATE.DOLLY:\n\n\t\t\tif ( this.enableZoom === false ) return;\n\n\t\t\tthis._handleMouseMoveDolly( event );\n\n\t\t\tbreak;\n\n\t\tcase _STATE.PAN:\n\n\t\t\tif ( this.enablePan === false ) return;\n\n\t\t\tthis._handleMouseMovePan( event );\n\n\t\t\tbreak;\n\n\t}\n\n}\n\nfunction onMouseWheel( event ) {\n\n\tif ( this.enabled === false || this.enableZoom === false || this.state !== _STATE.NONE ) return;\n\n\tevent.preventDefault();\n\n\tthis.dispatchEvent( _startEvent );\n\n\tthis._handleMouseWheel( this._customWheelEvent( event ) );\n\n\tthis.dispatchEvent( _endEvent );\n\n}\n\nfunction onKeyDown( event ) {\n\n\tif ( this.enabled === false ) return;\n\n\tthis._handleKeyDown( event );\n\n}\n\nfunction onTouchStart( event ) {\n\n\tthis._trackPointer( event );\n\n\tswitch ( this._pointers.length ) {\n\n\t\tcase 1:\n\n\t\t\tswitch ( this.touches.ONE ) {\n\n\t\t\t\tcase TOUCH.ROTATE:\n\n\t\t\t\t\tif ( this.enableRotate === false ) return;\n\n\t\t\t\t\tthis._handleTouchStartRotate( event );\n\n\t\t\t\t\tthis.state = _STATE.TOUCH_ROTATE;\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase TOUCH.PAN:\n\n\t\t\t\t\tif ( this.enablePan === false ) return;\n\n\t\t\t\t\tthis._handleTouchStartPan( event );\n\n\t\t\t\t\tthis.state = _STATE.TOUCH_PAN;\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\n\t\t\t\t\tthis.state = _STATE.NONE;\n\n\t\t\t}\n\n\t\t\tbreak;\n\n\t\tcase 2:\n\n\t\t\tswitch ( this.touches.TWO ) {\n\n\t\t\t\tcase TOUCH.DOLLY_PAN:\n\n\t\t\t\t\tif ( this.enableZoom === false && this.enablePan === false ) return;\n\n\t\t\t\t\tthis._handleTouchStartDollyPan( event );\n\n\t\t\t\t\tthis.state = _STATE.TOUCH_DOLLY_PAN;\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase TOUCH.DOLLY_ROTATE:\n\n\t\t\t\t\tif ( this.enableZoom === false && this.enableRotate === false ) return;\n\n\t\t\t\t\tthis._handleTouchStartDollyRotate( event );\n\n\t\t\t\t\tthis.state = _STATE.TOUCH_DOLLY_ROTATE;\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\n\t\t\t\t\tthis.state = _STATE.NONE;\n\n\t\t\t}\n\n\t\t\tbreak;\n\n\t\tdefault:\n\n\t\t\tthis.state = _STATE.NONE;\n\n\t}\n\n\tif ( this.state !== _STATE.NONE ) {\n\n\t\tthis.dispatchEvent( _startEvent );\n\n\t}\n\n}\n\nfunction onTouchMove( event ) {\n\n\tthis._trackPointer( event );\n\n\tswitch ( this.state ) {\n\n\t\tcase _STATE.TOUCH_ROTATE:\n\n\t\t\tif ( this.enableRotate === false ) return;\n\n\t\t\tthis._handleTouchMoveRotate( event );\n\n\t\t\tthis.update();\n\n\t\t\tbreak;\n\n\t\tcase _STATE.TOUCH_PAN:\n\n\t\t\tif ( this.enablePan === false ) return;\n\n\t\t\tthis._handleTouchMovePan( event );\n\n\t\t\tthis.update();\n\n\t\t\tbreak;\n\n\t\tcase _STATE.TOUCH_DOLLY_PAN:\n\n\t\t\tif ( this.enableZoom === false && this.enablePan === false ) return;\n\n\t\t\tthis._handleTouchMoveDollyPan( event );\n\n\t\t\tthis.update();\n\n\t\t\tbreak;\n\n\t\tcase _STATE.TOUCH_DOLLY_ROTATE:\n\n\t\t\tif ( this.enableZoom === false && this.enableRotate === false ) return;\n\n\t\t\tthis._handleTouchMoveDollyRotate( event );\n\n\t\t\tthis.update();\n\n\t\t\tbreak;\n\n\t\tdefault:\n\n\t\t\tthis.state = _STATE.NONE;\n\n\t}\n\n}\n\nfunction onContextMenu( event ) {\n\n\tif ( this.enabled === false ) return;\n\n\tevent.preventDefault();\n\n}\n\nfunction interceptControlDown( event ) {\n\n\tif ( event.key === 'Control' ) {\n\n\t\tthis._controlActive = true;\n\n\t\tconst document = this.domElement.getRootNode(); // offscreen canvas compatibility\n\n\t\tdocument.addEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } );\n\n\t}\n\n}\n\nfunction interceptControlUp( event ) {\n\n\tif ( event.key === 'Control' ) {\n\n\t\tthis._controlActive = false;\n\n\t\tconst document = this.domElement.getRootNode(); // offscreen canvas compatibility\n\n\t\tdocument.removeEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } );\n\n\t}\n\n}\n\nexport { OrbitControls };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-addons/loaders/STLLoader.js",
    "content": "import {\n\tBufferAttribute,\n\tBufferGeometry,\n\tColor,\n\tFileLoader,\n\tFloat32BufferAttribute,\n\tLoader,\n\tVector3,\n\tSRGBColorSpace\n} from 'three';\n\n/**\n * A loader for the STL format, as created by Solidworks and other CAD programs.\n *\n * Supports both binary and ASCII encoded files. The loader returns a non-indexed buffer geometry.\n *\n * Limitations:\n * - Binary decoding supports \"Magics\" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).\n * - There is perhaps some question as to how valid it is to always assume little-endian-ness.\n * - ASCII decoding assumes file is UTF-8.\n *\n * ```js\n * const loader = new STLLoader();\n * const geometry = await loader.loadAsync( './models/stl/slotted_disk.stl' )\n * scene.add( new THREE.Mesh( geometry ) );\n * ```\n * For binary STLs geometry might contain colors for vertices. To use it:\n * ```js\n * // use the same code to load STL as above\n * if ( geometry.hasColors ) {\n * \tmaterial = new THREE.MeshPhongMaterial( { opacity: geometry.alpha, vertexColors: true } );\n * }\n * const mesh = new THREE.Mesh( geometry, material );\n * ```\n * For ASCII STLs containing multiple solids, each solid is assigned to a different group.\n * Groups can be used to assign a different color by defining an array of materials with the same length of\n * geometry.groups and passing it to the Mesh constructor:\n *\n * ```js\n * const materials = [];\n * const nGeometryGroups = geometry.groups.length;\n *\n * for ( let i = 0; i < nGeometryGroups; i ++ ) {\n * \tconst material = new THREE.MeshPhongMaterial( { color: colorMap[ i ], wireframe: false } );\n * \tmaterials.push( material );\n * }\n *\n * const mesh = new THREE.Mesh(geometry, materials);\n * ```\n *\n * @augments Loader\n * @three_import import { STLLoader } from 'three/addons/loaders/STLLoader.js';\n */\nclass STLLoader extends Loader {\n\n\t/**\n\t * Constructs a new STL loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and passes the loaded STL asset\n\t * to the `onLoad()` callback.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(BufferGeometry)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Executed while the loading is in progress.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tconst scope = this;\n\n\t\tconst loader = new FileLoader( this.manager );\n\t\tloader.setPath( this.path );\n\t\tloader.setResponseType( 'arraybuffer' );\n\t\tloader.setRequestHeader( this.requestHeader );\n\t\tloader.setWithCredentials( this.withCredentials );\n\n\t\tloader.load( url, function ( text ) {\n\n\t\t\ttry {\n\n\t\t\t\tonLoad( scope.parse( text ) );\n\n\t\t\t} catch ( e ) {\n\n\t\t\t\tif ( onError ) {\n\n\t\t\t\t\tonError( e );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tconsole.error( e );\n\n\t\t\t\t}\n\n\t\t\t\tscope.manager.itemError( url );\n\n\t\t\t}\n\n\t\t}, onProgress, onError );\n\n\t}\n\n\t/**\n\t * Parses the given STL data and returns the resulting geometry.\n\t *\n\t * @param {ArrayBuffer} data - The raw STL data as an array buffer.\n\t * @return {BufferGeometry} The parsed geometry.\n\t */\n\tparse( data ) {\n\n\t\tfunction isBinary( data ) {\n\n\t\t\tconst reader = new DataView( data );\n\t\t\tconst face_size = ( 32 / 8 * 3 ) + ( ( 32 / 8 * 3 ) * 3 ) + ( 16 / 8 );\n\t\t\tconst n_faces = reader.getUint32( 80, true );\n\t\t\tconst expect = 80 + ( 32 / 8 ) + ( n_faces * face_size );\n\n\t\t\tif ( expect === reader.byteLength ) {\n\n\t\t\t\treturn true;\n\n\t\t\t}\n\n\t\t\t// An ASCII STL data must begin with 'solid ' as the first six bytes.\n\t\t\t// However, ASCII STLs lacking the SPACE after the 'd' are known to be\n\t\t\t// plentiful.  So, check the first 5 bytes for 'solid'.\n\n\t\t\t// Several encodings, such as UTF-8, precede the text with up to 5 bytes:\n\t\t\t// https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding\n\t\t\t// Search for \"solid\" to start anywhere after those prefixes.\n\n\t\t\t// US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd'\n\n\t\t\tconst solid = [ 115, 111, 108, 105, 100 ];\n\n\t\t\tfor ( let off = 0; off < 5; off ++ ) {\n\n\t\t\t\t// If \"solid\" text is matched to the current offset, declare it to be an ASCII STL.\n\n\t\t\t\tif ( matchDataViewAt( solid, reader, off ) ) return false;\n\n\t\t\t}\n\n\t\t\t// Couldn't find \"solid\" text at the beginning; it is binary STL.\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\tfunction matchDataViewAt( query, reader, offset ) {\n\n\t\t\t// Check if each byte in query matches the corresponding byte from the current offset\n\n\t\t\tfor ( let i = 0, il = query.length; i < il; i ++ ) {\n\n\t\t\t\tif ( query[ i ] !== reader.getUint8( offset + i ) ) return false;\n\n\t\t\t}\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\tfunction parseBinary( data ) {\n\n\t\t\tconst reader = new DataView( data );\n\t\t\tconst faces = reader.getUint32( 80, true );\n\n\t\t\tlet r, g, b, hasColors = false, colors;\n\t\t\tlet defaultR, defaultG, defaultB, alpha;\n\n\t\t\t// process STL header\n\t\t\t// check for default color in header (\"COLOR=rgba\" sequence).\n\n\t\t\tfor ( let index = 0; index < 80 - 10; index ++ ) {\n\n\t\t\t\tif ( ( reader.getUint32( index, false ) == 0x434F4C4F /*COLO*/ ) &&\n\t\t\t\t\t( reader.getUint8( index + 4 ) == 0x52 /*'R'*/ ) &&\n\t\t\t\t\t( reader.getUint8( index + 5 ) == 0x3D /*'='*/ ) ) {\n\n\t\t\t\t\thasColors = true;\n\t\t\t\t\tcolors = new Float32Array( faces * 3 * 3 );\n\n\t\t\t\t\tdefaultR = reader.getUint8( index + 6 ) / 255;\n\t\t\t\t\tdefaultG = reader.getUint8( index + 7 ) / 255;\n\t\t\t\t\tdefaultB = reader.getUint8( index + 8 ) / 255;\n\t\t\t\t\talpha = reader.getUint8( index + 9 ) / 255;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tconst dataOffset = 84;\n\t\t\tconst faceLength = 12 * 4 + 2;\n\n\t\t\tconst geometry = new BufferGeometry();\n\n\t\t\tconst vertices = new Float32Array( faces * 3 * 3 );\n\t\t\tconst normals = new Float32Array( faces * 3 * 3 );\n\n\t\t\tconst color = new Color();\n\n\t\t\tfor ( let face = 0; face < faces; face ++ ) {\n\n\t\t\t\tconst start = dataOffset + face * faceLength;\n\t\t\t\tconst normalX = reader.getFloat32( start, true );\n\t\t\t\tconst normalY = reader.getFloat32( start + 4, true );\n\t\t\t\tconst normalZ = reader.getFloat32( start + 8, true );\n\n\t\t\t\tif ( hasColors ) {\n\n\t\t\t\t\tconst packedColor = reader.getUint16( start + 48, true );\n\n\t\t\t\t\tif ( ( packedColor & 0x8000 ) === 0 ) {\n\n\t\t\t\t\t\t// facet has its own unique color\n\n\t\t\t\t\t\tr = ( packedColor & 0x1F ) / 31;\n\t\t\t\t\t\tg = ( ( packedColor >> 5 ) & 0x1F ) / 31;\n\t\t\t\t\t\tb = ( ( packedColor >> 10 ) & 0x1F ) / 31;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tr = defaultR;\n\t\t\t\t\t\tg = defaultG;\n\t\t\t\t\t\tb = defaultB;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tfor ( let i = 1; i <= 3; i ++ ) {\n\n\t\t\t\t\tconst vertexstart = start + i * 12;\n\t\t\t\t\tconst componentIdx = ( face * 3 * 3 ) + ( ( i - 1 ) * 3 );\n\n\t\t\t\t\tvertices[ componentIdx ] = reader.getFloat32( vertexstart, true );\n\t\t\t\t\tvertices[ componentIdx + 1 ] = reader.getFloat32( vertexstart + 4, true );\n\t\t\t\t\tvertices[ componentIdx + 2 ] = reader.getFloat32( vertexstart + 8, true );\n\n\t\t\t\t\tnormals[ componentIdx ] = normalX;\n\t\t\t\t\tnormals[ componentIdx + 1 ] = normalY;\n\t\t\t\t\tnormals[ componentIdx + 2 ] = normalZ;\n\n\t\t\t\t\tif ( hasColors ) {\n\n\t\t\t\t\t\tcolor.setRGB( r, g, b, SRGBColorSpace );\n\n\t\t\t\t\t\tcolors[ componentIdx ] = color.r;\n\t\t\t\t\t\tcolors[ componentIdx + 1 ] = color.g;\n\t\t\t\t\t\tcolors[ componentIdx + 2 ] = color.b;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tgeometry.setAttribute( 'position', new BufferAttribute( vertices, 3 ) );\n\t\t\tgeometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );\n\n\t\t\tif ( hasColors ) {\n\n\t\t\t\tgeometry.setAttribute( 'color', new BufferAttribute( colors, 3 ) );\n\t\t\t\tgeometry.hasColors = true;\n\t\t\t\tgeometry.alpha = alpha;\n\n\t\t\t}\n\n\t\t\treturn geometry;\n\n\t\t}\n\n\t\tfunction parseASCII( data ) {\n\n\t\t\tconst geometry = new BufferGeometry();\n\t\t\tconst patternSolid = /solid([\\s\\S]*?)endsolid/g;\n\t\t\tconst patternFace = /facet([\\s\\S]*?)endfacet/g;\n\t\t\tconst patternName = /solid\\s(.+)/;\n\t\t\tlet faceCounter = 0;\n\n\t\t\tconst patternFloat = /[\\s]+([+-]?(?:\\d*)(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)/.source;\n\t\t\tconst patternVertex = new RegExp( 'vertex' + patternFloat + patternFloat + patternFloat, 'g' );\n\t\t\tconst patternNormal = new RegExp( 'normal' + patternFloat + patternFloat + patternFloat, 'g' );\n\n\t\t\tconst vertices = [];\n\t\t\tconst normals = [];\n\t\t\tconst groupNames = [];\n\n\t\t\tconst normal = new Vector3();\n\n\t\t\tlet result;\n\n\t\t\tlet groupCount = 0;\n\t\t\tlet startVertex = 0;\n\t\t\tlet endVertex = 0;\n\n\t\t\twhile ( ( result = patternSolid.exec( data ) ) !== null ) {\n\n\t\t\t\tstartVertex = endVertex;\n\n\t\t\t\tconst solid = result[ 0 ];\n\n\t\t\t\tconst name = ( result = patternName.exec( solid ) ) !== null ? result[ 1 ] : '';\n\t\t\t\tgroupNames.push( name );\n\n\t\t\t\twhile ( ( result = patternFace.exec( solid ) ) !== null ) {\n\n\t\t\t\t\tlet vertexCountPerFace = 0;\n\t\t\t\t\tlet normalCountPerFace = 0;\n\n\t\t\t\t\tconst text = result[ 0 ];\n\n\t\t\t\t\twhile ( ( result = patternNormal.exec( text ) ) !== null ) {\n\n\t\t\t\t\t\tnormal.x = parseFloat( result[ 1 ] );\n\t\t\t\t\t\tnormal.y = parseFloat( result[ 2 ] );\n\t\t\t\t\t\tnormal.z = parseFloat( result[ 3 ] );\n\t\t\t\t\t\tnormalCountPerFace ++;\n\n\t\t\t\t\t}\n\n\t\t\t\t\twhile ( ( result = patternVertex.exec( text ) ) !== null ) {\n\n\t\t\t\t\t\tvertices.push( parseFloat( result[ 1 ] ), parseFloat( result[ 2 ] ), parseFloat( result[ 3 ] ) );\n\t\t\t\t\t\tnormals.push( normal.x, normal.y, normal.z );\n\t\t\t\t\t\tvertexCountPerFace ++;\n\t\t\t\t\t\tendVertex ++;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// every face have to own ONE valid normal\n\n\t\t\t\t\tif ( normalCountPerFace !== 1 ) {\n\n\t\t\t\t\t\tconsole.error( 'THREE.STLLoader: Something isn\\'t right with the normal of face number ' + faceCounter );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// each face have to own THREE valid vertices\n\n\t\t\t\t\tif ( vertexCountPerFace !== 3 ) {\n\n\t\t\t\t\t\tconsole.error( 'THREE.STLLoader: Something isn\\'t right with the vertices of face number ' + faceCounter );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfaceCounter ++;\n\n\t\t\t\t}\n\n\t\t\t\tconst start = startVertex;\n\t\t\t\tconst count = endVertex - startVertex;\n\n\t\t\t\tgeometry.userData.groupNames = groupNames;\n\n\t\t\t\tgeometry.addGroup( start, count, groupCount );\n\t\t\t\tgroupCount ++;\n\n\t\t\t}\n\n\t\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\t\tgeometry.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\n\t\t\treturn geometry;\n\n\t\t}\n\n\t\tfunction ensureString( buffer ) {\n\n\t\t\tif ( typeof buffer !== 'string' ) {\n\n\t\t\t\treturn new TextDecoder().decode( buffer );\n\n\t\t\t}\n\n\t\t\treturn buffer;\n\n\t\t}\n\n\t\tfunction ensureBinary( buffer ) {\n\n\t\t\tif ( typeof buffer === 'string' ) {\n\n\t\t\t\tconst array_buffer = new Uint8Array( buffer.length );\n\t\t\t\tfor ( let i = 0; i < buffer.length; i ++ ) {\n\n\t\t\t\t\tarray_buffer[ i ] = buffer.charCodeAt( i ) & 0xff; // implicitly assumes little-endian\n\n\t\t\t\t}\n\n\t\t\t\treturn array_buffer.buffer || array_buffer;\n\n\t\t\t} else {\n\n\t\t\t\treturn buffer;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// start\n\n\t\tconst binData = ensureBinary( data );\n\n\t\treturn isBinary( binData ) ? parseBinary( binData ) : parseASCII( ensureString( data ) );\n\n\t}\n\n}\n\nexport { STLLoader };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-addons/math/SimplexNoise.js",
    "content": "/**\n * A utility class providing noise functions.\n *\n * The code is based on [Simplex noise demystified](https://web.archive.org/web/20210210162332/http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf)\n * by Stefan Gustavson, 2005.\n *\n * @three_import import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';\n */\nclass SimplexNoise {\n\n\t/**\n\t * Constructs a new simplex noise object.\n\t *\n\t * @param {Object} [r=Math] - A math utility class that holds a `random()` method. This makes it\n\t * possible to pass in custom random number generator.\n\t */\n\tconstructor( r = Math ) {\n\n\t\tthis.grad3 = [[ 1, 1, 0 ], [ - 1, 1, 0 ], [ 1, - 1, 0 ], [ - 1, - 1, 0 ],\n\t\t\t[ 1, 0, 1 ], [ - 1, 0, 1 ], [ 1, 0, - 1 ], [ - 1, 0, - 1 ],\n\t\t\t[ 0, 1, 1 ], [ 0, - 1, 1 ], [ 0, 1, - 1 ], [ 0, - 1, - 1 ]];\n\n\t\tthis.grad4 = [[ 0, 1, 1, 1 ], [ 0, 1, 1, - 1 ], [ 0, 1, - 1, 1 ], [ 0, 1, - 1, - 1 ],\n\t\t\t[ 0, - 1, 1, 1 ], [ 0, - 1, 1, - 1 ], [ 0, - 1, - 1, 1 ], [ 0, - 1, - 1, - 1 ],\n\t\t\t[ 1, 0, 1, 1 ], [ 1, 0, 1, - 1 ], [ 1, 0, - 1, 1 ], [ 1, 0, - 1, - 1 ],\n\t\t\t[ - 1, 0, 1, 1 ], [ - 1, 0, 1, - 1 ], [ - 1, 0, - 1, 1 ], [ - 1, 0, - 1, - 1 ],\n\t\t\t[ 1, 1, 0, 1 ], [ 1, 1, 0, - 1 ], [ 1, - 1, 0, 1 ], [ 1, - 1, 0, - 1 ],\n\t\t\t[ - 1, 1, 0, 1 ], [ - 1, 1, 0, - 1 ], [ - 1, - 1, 0, 1 ], [ - 1, - 1, 0, - 1 ],\n\t\t\t[ 1, 1, 1, 0 ], [ 1, 1, - 1, 0 ], [ 1, - 1, 1, 0 ], [ 1, - 1, - 1, 0 ],\n\t\t\t[ - 1, 1, 1, 0 ], [ - 1, 1, - 1, 0 ], [ - 1, - 1, 1, 0 ], [ - 1, - 1, - 1, 0 ]];\n\n\t\tthis.p = [];\n\n\t\tfor ( let i = 0; i < 256; i ++ ) {\n\n\t\t\tthis.p[ i ] = Math.floor( r.random() * 256 );\n\n\t\t}\n\n\t\t// To remove the need for index wrapping, double the permutation table length\n\t\tthis.perm = [];\n\n\t\tfor ( let i = 0; i < 512; i ++ ) {\n\n\t\t\tthis.perm[ i ] = this.p[ i & 255 ];\n\n\t\t}\n\n\t\t// A lookup table to traverse the simplex around a given point in 4D.\n\t\t// Details can be found where this table is used, in the 4D noise method.\n\t\tthis.simplex = [\n\t\t\t[ 0, 1, 2, 3 ], [ 0, 1, 3, 2 ], [ 0, 0, 0, 0 ], [ 0, 2, 3, 1 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 1, 2, 3, 0 ],\n\t\t\t[ 0, 2, 1, 3 ], [ 0, 0, 0, 0 ], [ 0, 3, 1, 2 ], [ 0, 3, 2, 1 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 1, 3, 2, 0 ],\n\t\t\t[ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ],\n\t\t\t[ 1, 2, 0, 3 ], [ 0, 0, 0, 0 ], [ 1, 3, 0, 2 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 2, 3, 0, 1 ], [ 2, 3, 1, 0 ],\n\t\t\t[ 1, 0, 2, 3 ], [ 1, 0, 3, 2 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 2, 0, 3, 1 ], [ 0, 0, 0, 0 ], [ 2, 1, 3, 0 ],\n\t\t\t[ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ],\n\t\t\t[ 2, 0, 1, 3 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 3, 0, 1, 2 ], [ 3, 0, 2, 1 ], [ 0, 0, 0, 0 ], [ 3, 1, 2, 0 ],\n\t\t\t[ 2, 1, 0, 3 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 3, 1, 0, 2 ], [ 0, 0, 0, 0 ], [ 3, 2, 0, 1 ], [ 3, 2, 1, 0 ]];\n\n\t}\n\n\t/**\n\t * A 2D simplex noise method.\n\t *\n\t * @param {number} xin - The x coordinate.\n\t * @param {number} yin - The y coordinate.\n\t * @return {number} The noise value.\n\t */\n\tnoise( xin, yin ) {\n\n\t\tlet n0; // Noise contributions from the three corners\n\t\tlet n1;\n\t\tlet n2;\n\t\t// Skew the input space to determine which simplex cell we're in\n\t\tconst F2 = 0.5 * ( Math.sqrt( 3.0 ) - 1.0 );\n\t\tconst s = ( xin + yin ) * F2; // Hairy factor for 2D\n\t\tconst i = Math.floor( xin + s );\n\t\tconst j = Math.floor( yin + s );\n\t\tconst G2 = ( 3.0 - Math.sqrt( 3.0 ) ) / 6.0;\n\t\tconst t = ( i + j ) * G2;\n\t\tconst X0 = i - t; // Unskew the cell origin back to (x,y) space\n\t\tconst Y0 = j - t;\n\t\tconst x0 = xin - X0; // The x,y distances from the cell origin\n\t\tconst y0 = yin - Y0;\n\n\t\t// For the 2D case, the simplex shape is an equilateral triangle.\n\t\t// Determine which simplex we are in.\n\t\tlet i1; // Offsets for second (middle) corner of simplex in (i,j) coords\n\n\t\tlet j1;\n\t\tif ( x0 > y0 ) {\n\n\t\t\ti1 = 1; j1 = 0;\n\n\t\t\t// lower triangle, XY order: (0,0)->(1,0)->(1,1)\n\n\t\t}\telse {\n\n\t\t\ti1 = 0; j1 = 1;\n\n\t\t} // upper triangle, YX order: (0,0)->(0,1)->(1,1)\n\n\t\t// A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and\n\t\t// a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where\n\t\t// c = (3-sqrt(3))/6\n\t\tconst x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords\n\t\tconst y1 = y0 - j1 + G2;\n\t\tconst x2 = x0 - 1.0 + 2.0 * G2; // Offsets for last corner in (x,y) unskewed coords\n\t\tconst y2 = y0 - 1.0 + 2.0 * G2;\n\t\t// Work out the hashed gradient indices of the three simplex corners\n\t\tconst ii = i & 255;\n\t\tconst jj = j & 255;\n\t\tconst gi0 = this.perm[ ii + this.perm[ jj ] ] % 12;\n\t\tconst gi1 = this.perm[ ii + i1 + this.perm[ jj + j1 ] ] % 12;\n\t\tconst gi2 = this.perm[ ii + 1 + this.perm[ jj + 1 ] ] % 12;\n\t\t// Calculate the contribution from the three corners\n\t\tlet t0 = 0.5 - x0 * x0 - y0 * y0;\n\t\tif ( t0 < 0 ) n0 = 0.0;\n\t\telse {\n\n\t\t\tt0 *= t0;\n\t\t\tn0 = t0 * t0 * this._dot( this.grad3[ gi0 ], x0, y0 ); // (x,y) of grad3 used for 2D gradient\n\n\t\t}\n\n\t\tlet t1 = 0.5 - x1 * x1 - y1 * y1;\n\t\tif ( t1 < 0 ) n1 = 0.0;\n\t\telse {\n\n\t\t\tt1 *= t1;\n\t\t\tn1 = t1 * t1 * this._dot( this.grad3[ gi1 ], x1, y1 );\n\n\t\t}\n\n\t\tlet t2 = 0.5 - x2 * x2 - y2 * y2;\n\t\tif ( t2 < 0 ) n2 = 0.0;\n\t\telse {\n\n\t\t\tt2 *= t2;\n\t\t\tn2 = t2 * t2 * this._dot( this.grad3[ gi2 ], x2, y2 );\n\n\t\t}\n\n\t\t// Add contributions from each corner to get the final noise value.\n\t\t// The result is scaled to return values in the interval [-1,1].\n\t\treturn 70.0 * ( n0 + n1 + n2 );\n\n\t}\n\n\t/**\n\t * A 3D simplex noise method.\n\t *\n\t * @param {number} xin - The x coordinate.\n\t * @param {number} yin - The y coordinate.\n\t * @param {number} zin - The z coordinate.\n\t * @return {number} The noise value.\n\t */\n\tnoise3d( xin, yin, zin ) {\n\n\t\tlet n0; // Noise contributions from the four corners\n\t\tlet n1;\n\t\tlet n2;\n\t\tlet n3;\n\t\t// Skew the input space to determine which simplex cell we're in\n\t\tconst F3 = 1.0 / 3.0;\n\t\tconst s = ( xin + yin + zin ) * F3; // Very nice and simple skew factor for 3D\n\t\tconst i = Math.floor( xin + s );\n\t\tconst j = Math.floor( yin + s );\n\t\tconst k = Math.floor( zin + s );\n\t\tconst G3 = 1.0 / 6.0; // Very nice and simple unskew factor, too\n\t\tconst t = ( i + j + k ) * G3;\n\t\tconst X0 = i - t; // Unskew the cell origin back to (x,y,z) space\n\t\tconst Y0 = j - t;\n\t\tconst Z0 = k - t;\n\t\tconst x0 = xin - X0; // The x,y,z distances from the cell origin\n\t\tconst y0 = yin - Y0;\n\t\tconst z0 = zin - Z0;\n\n\t\t// For the 3D case, the simplex shape is a slightly irregular tetrahedron.\n\t\t// Determine which simplex we are in.\n\t\tlet i1; // Offsets for second corner of simplex in (i,j,k) coords\n\n\t\tlet j1;\n\t\tlet k1;\n\t\tlet i2; // Offsets for third corner of simplex in (i,j,k) coords\n\t\tlet j2;\n\t\tlet k2;\n\t\tif ( x0 >= y0 ) {\n\n\t\t\tif ( y0 >= z0 ) {\n\n\t\t\t\ti1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 1; k2 = 0;\n\n\t\t\t\t// X Y Z order\n\n\t\t\t} else if ( x0 >= z0 ) {\n\n\t\t\t\ti1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 0; k2 = 1;\n\n\t\t\t\t// X Z Y order\n\n\t\t\t} else {\n\n\t\t\t\ti1 = 0; j1 = 0; k1 = 1; i2 = 1; j2 = 0; k2 = 1;\n\n\t\t\t} // Z X Y order\n\n\t\t} else { // x0<y0\n\n\t\t\tif ( y0 < z0 ) {\n\n\t\t\t\ti1 = 0; j1 = 0; k1 = 1; i2 = 0; j2 = 1; k2 = 1;\n\n\t\t\t\t// Z Y X order\n\n\t\t\t} else if ( x0 < z0 ) {\n\n\t\t\t\ti1 = 0; j1 = 1; k1 = 0; i2 = 0; j2 = 1; k2 = 1;\n\n\t\t\t\t// Y Z X order\n\n\t\t\t} else {\n\n\t\t\t\ti1 = 0; j1 = 1; k1 = 0; i2 = 1; j2 = 1; k2 = 0;\n\n\t\t\t} // Y X Z order\n\n\t\t}\n\n\t\t// A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z),\n\t\t// a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and\n\t\t// a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where\n\t\t// c = 1/6.\n\t\tconst x1 = x0 - i1 + G3; // Offsets for second corner in (x,y,z) coords\n\t\tconst y1 = y0 - j1 + G3;\n\t\tconst z1 = z0 - k1 + G3;\n\t\tconst x2 = x0 - i2 + 2.0 * G3; // Offsets for third corner in (x,y,z) coords\n\t\tconst y2 = y0 - j2 + 2.0 * G3;\n\t\tconst z2 = z0 - k2 + 2.0 * G3;\n\t\tconst x3 = x0 - 1.0 + 3.0 * G3; // Offsets for last corner in (x,y,z) coords\n\t\tconst y3 = y0 - 1.0 + 3.0 * G3;\n\t\tconst z3 = z0 - 1.0 + 3.0 * G3;\n\t\t// Work out the hashed gradient indices of the four simplex corners\n\t\tconst ii = i & 255;\n\t\tconst jj = j & 255;\n\t\tconst kk = k & 255;\n\t\tconst gi0 = this.perm[ ii + this.perm[ jj + this.perm[ kk ] ] ] % 12;\n\t\tconst gi1 = this.perm[ ii + i1 + this.perm[ jj + j1 + this.perm[ kk + k1 ] ] ] % 12;\n\t\tconst gi2 = this.perm[ ii + i2 + this.perm[ jj + j2 + this.perm[ kk + k2 ] ] ] % 12;\n\t\tconst gi3 = this.perm[ ii + 1 + this.perm[ jj + 1 + this.perm[ kk + 1 ] ] ] % 12;\n\t\t// Calculate the contribution from the four corners\n\t\tlet t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;\n\t\tif ( t0 < 0 ) n0 = 0.0;\n\t\telse {\n\n\t\t\tt0 *= t0;\n\t\t\tn0 = t0 * t0 * this._dot3( this.grad3[ gi0 ], x0, y0, z0 );\n\n\t\t}\n\n\t\tlet t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;\n\t\tif ( t1 < 0 ) n1 = 0.0;\n\t\telse {\n\n\t\t\tt1 *= t1;\n\t\t\tn1 = t1 * t1 * this._dot3( this.grad3[ gi1 ], x1, y1, z1 );\n\n\t\t}\n\n\t\tlet t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;\n\t\tif ( t2 < 0 ) n2 = 0.0;\n\t\telse {\n\n\t\t\tt2 *= t2;\n\t\t\tn2 = t2 * t2 * this._dot3( this.grad3[ gi2 ], x2, y2, z2 );\n\n\t\t}\n\n\t\tlet t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;\n\t\tif ( t3 < 0 ) n3 = 0.0;\n\t\telse {\n\n\t\t\tt3 *= t3;\n\t\t\tn3 = t3 * t3 * this._dot3( this.grad3[ gi3 ], x3, y3, z3 );\n\n\t\t}\n\n\t\t// Add contributions from each corner to get the final noise value.\n\t\t// The result is scaled to stay just inside [-1,1]\n\t\treturn 32.0 * ( n0 + n1 + n2 + n3 );\n\n\t}\n\n\t/**\n\t * A 4D simplex noise method.\n\t *\n\t * @param {number} x - The x coordinate.\n\t * @param {number} y - The y coordinate.\n\t * @param {number} z - The z coordinate.\n\t * @param {number} w - The w coordinate.\n\t * @return {number} The noise value.\n\t */\n\tnoise4d( x, y, z, w ) {\n\n\t\t// For faster and easier lookups\n\t\tconst grad4 = this.grad4;\n\t\tconst simplex = this.simplex;\n\t\tconst perm = this.perm;\n\n\t\t// The skewing and unskewing factors are hairy again for the 4D case\n\t\tconst F4 = ( Math.sqrt( 5.0 ) - 1.0 ) / 4.0;\n\t\tconst G4 = ( 5.0 - Math.sqrt( 5.0 ) ) / 20.0;\n\t\tlet n0; // Noise contributions from the five corners\n\t\tlet n1;\n\t\tlet n2;\n\t\tlet n3;\n\t\tlet n4;\n\t\t// Skew the (x,y,z,w) space to determine which cell of 24 simplices we're in\n\t\tconst s = ( x + y + z + w ) * F4; // Factor for 4D skewing\n\t\tconst i = Math.floor( x + s );\n\t\tconst j = Math.floor( y + s );\n\t\tconst k = Math.floor( z + s );\n\t\tconst l = Math.floor( w + s );\n\t\tconst t = ( i + j + k + l ) * G4; // Factor for 4D unskewing\n\t\tconst X0 = i - t; // Unskew the cell origin back to (x,y,z,w) space\n\t\tconst Y0 = j - t;\n\t\tconst Z0 = k - t;\n\t\tconst W0 = l - t;\n\t\tconst x0 = x - X0; // The x,y,z,w distances from the cell origin\n\t\tconst y0 = y - Y0;\n\t\tconst z0 = z - Z0;\n\t\tconst w0 = w - W0;\n\n\t\t// For the 4D case, the simplex is a 4D shape I won't even try to describe.\n\t\t// To find out which of the 24 possible simplices we're in, we need to\n\t\t// determine the magnitude ordering of x0, y0, z0 and w0.\n\t\t// The method below is a good way of finding the ordering of x,y,z,w and\n\t\t// then find the correct traversal order for the simplex we’re in.\n\t\t// First, six pair-wise comparisons are performed between each possible pair\n\t\t// of the four coordinates, and the results are used to add up binary bits\n\t\t// for an integer index.\n\t\tconst c1 = ( x0 > y0 ) ? 32 : 0;\n\t\tconst c2 = ( x0 > z0 ) ? 16 : 0;\n\t\tconst c3 = ( y0 > z0 ) ? 8 : 0;\n\t\tconst c4 = ( x0 > w0 ) ? 4 : 0;\n\t\tconst c5 = ( y0 > w0 ) ? 2 : 0;\n\t\tconst c6 = ( z0 > w0 ) ? 1 : 0;\n\t\tconst c = c1 + c2 + c3 + c4 + c5 + c6;\n\n\t\t// simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order.\n\t\t// Many values of c will never occur, since e.g. x>y>z>w makes x<z, y<w and x<w\n\t\t// impossible. Only the 24 indices which have non-zero entries make any sense.\n\t\t// We use a thresholding to set the coordinates in turn from the largest magnitude.\n\t\t// The number 3 in the \"simplex\" array is at the position of the largest coordinate.\n\t\tconst i1 = simplex[ c ][ 0 ] >= 3 ? 1 : 0;\n\t\tconst j1 = simplex[ c ][ 1 ] >= 3 ? 1 : 0;\n\t\tconst k1 = simplex[ c ][ 2 ] >= 3 ? 1 : 0;\n\t\tconst l1 = simplex[ c ][ 3 ] >= 3 ? 1 : 0;\n\t\t// The number 2 in the \"simplex\" array is at the second largest coordinate.\n\t\tconst i2 = simplex[ c ][ 0 ] >= 2 ? 1 : 0;\n\t\tconst j2 = simplex[ c ][ 1 ] >= 2 ? 1 : 0;\n\t\tconst k2 = simplex[ c ][ 2 ] >= 2 ? 1 : 0;\n\t\tconst l2 = simplex[ c ][ 3 ] >= 2 ? 1 : 0;\n\t\t// The number 1 in the \"simplex\" array is at the second smallest coordinate.\n\t\tconst i3 = simplex[ c ][ 0 ] >= 1 ? 1 : 0;\n\t\tconst j3 = simplex[ c ][ 1 ] >= 1 ? 1 : 0;\n\t\tconst k3 = simplex[ c ][ 2 ] >= 1 ? 1 : 0;\n\t\tconst l3 = simplex[ c ][ 3 ] >= 1 ? 1 : 0;\n\t\t// The fifth corner has all coordinate offsets = 1, so no need to look that up.\n\t\tconst x1 = x0 - i1 + G4; // Offsets for second corner in (x,y,z,w) coords\n\t\tconst y1 = y0 - j1 + G4;\n\t\tconst z1 = z0 - k1 + G4;\n\t\tconst w1 = w0 - l1 + G4;\n\t\tconst x2 = x0 - i2 + 2.0 * G4; // Offsets for third corner in (x,y,z,w) coords\n\t\tconst y2 = y0 - j2 + 2.0 * G4;\n\t\tconst z2 = z0 - k2 + 2.0 * G4;\n\t\tconst w2 = w0 - l2 + 2.0 * G4;\n\t\tconst x3 = x0 - i3 + 3.0 * G4; // Offsets for fourth corner in (x,y,z,w) coords\n\t\tconst y3 = y0 - j3 + 3.0 * G4;\n\t\tconst z3 = z0 - k3 + 3.0 * G4;\n\t\tconst w3 = w0 - l3 + 3.0 * G4;\n\t\tconst x4 = x0 - 1.0 + 4.0 * G4; // Offsets for last corner in (x,y,z,w) coords\n\t\tconst y4 = y0 - 1.0 + 4.0 * G4;\n\t\tconst z4 = z0 - 1.0 + 4.0 * G4;\n\t\tconst w4 = w0 - 1.0 + 4.0 * G4;\n\t\t// Work out the hashed gradient indices of the five simplex corners\n\t\tconst ii = i & 255;\n\t\tconst jj = j & 255;\n\t\tconst kk = k & 255;\n\t\tconst ll = l & 255;\n\t\tconst gi0 = perm[ ii + perm[ jj + perm[ kk + perm[ ll ] ] ] ] % 32;\n\t\tconst gi1 = perm[ ii + i1 + perm[ jj + j1 + perm[ kk + k1 + perm[ ll + l1 ] ] ] ] % 32;\n\t\tconst gi2 = perm[ ii + i2 + perm[ jj + j2 + perm[ kk + k2 + perm[ ll + l2 ] ] ] ] % 32;\n\t\tconst gi3 = perm[ ii + i3 + perm[ jj + j3 + perm[ kk + k3 + perm[ ll + l3 ] ] ] ] % 32;\n\t\tconst gi4 = perm[ ii + 1 + perm[ jj + 1 + perm[ kk + 1 + perm[ ll + 1 ] ] ] ] % 32;\n\t\t// Calculate the contribution from the five corners\n\t\tlet t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0;\n\t\tif ( t0 < 0 ) n0 = 0.0;\n\t\telse {\n\n\t\t\tt0 *= t0;\n\t\t\tn0 = t0 * t0 * this._dot4( grad4[ gi0 ], x0, y0, z0, w0 );\n\n\t\t}\n\n\t\tlet t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1;\n\t\tif ( t1 < 0 ) n1 = 0.0;\n\t\telse {\n\n\t\t\tt1 *= t1;\n\t\t\tn1 = t1 * t1 * this._dot4( grad4[ gi1 ], x1, y1, z1, w1 );\n\n\t\t}\n\n\t\tlet t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2;\n\t\tif ( t2 < 0 ) n2 = 0.0;\n\t\telse {\n\n\t\t\tt2 *= t2;\n\t\t\tn2 = t2 * t2 * this._dot4( grad4[ gi2 ], x2, y2, z2, w2 );\n\n\t\t}\n\n\t\tlet t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3;\n\t\tif ( t3 < 0 ) n3 = 0.0;\n\t\telse {\n\n\t\t\tt3 *= t3;\n\t\t\tn3 = t3 * t3 * this._dot4( grad4[ gi3 ], x3, y3, z3, w3 );\n\n\t\t}\n\n\t\tlet t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4;\n\t\tif ( t4 < 0 ) n4 = 0.0;\n\t\telse {\n\n\t\t\tt4 *= t4;\n\t\t\tn4 = t4 * t4 * this._dot4( grad4[ gi4 ], x4, y4, z4, w4 );\n\n\t\t}\n\n\t\t// Sum up and scale the result to cover the range [-1,1]\n\t\treturn 27.0 * ( n0 + n1 + n2 + n3 + n4 );\n\n\t}\n\n\t// private\n\n\t_dot( g, x, y ) {\n\n\t\treturn g[ 0 ] * x + g[ 1 ] * y;\n\n\t}\n\n\t_dot3( g, x, y, z ) {\n\n\t\treturn g[ 0 ] * x + g[ 1 ] * y + g[ 2 ] * z;\n\n\t}\n\n\t_dot4( g, x, y, z, w ) {\n\n\t\treturn g[ 0 ] * x + g[ 1 ] * y + g[ 2 ] * z + g[ 3 ] * w;\n\n\t}\n\n}\n\nexport { SimplexNoise };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-addons/postprocessing/EffectComposer.js",
    "content": "import {\n\tClock,\n\tHalfFloatType,\n\tNoBlending,\n\tVector2,\n\tWebGLRenderTarget\n} from 'three';\nimport { CopyShader } from '../shaders/CopyShader.js';\nimport { ShaderPass } from './ShaderPass.js';\nimport { ClearMaskPass, MaskPass } from './MaskPass.js';\n\n/**\n * Used to implement post-processing effects in three.js.\n * The class manages a chain of post-processing passes to produce the final visual result.\n * Post-processing passes are executed in order of their addition/insertion.\n * The last pass is automatically rendered to screen.\n *\n * This module can only be used with {@link WebGLRenderer}.\n *\n * ```js\n * const composer = new EffectComposer( renderer );\n *\n * // adding some passes\n * const renderPass = new RenderPass( scene, camera );\n * composer.addPass( renderPass );\n *\n * const glitchPass = new GlitchPass();\n * composer.addPass( glitchPass );\n *\n * const outputPass = new OutputPass()\n * composer.addPass( outputPass );\n *\n * function animate() {\n *\n * \tcomposer.render(); // instead of renderer.render()\n *\n * }\n * ```\n *\n * @three_import import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';\n */\nclass EffectComposer {\n\n\t/**\n\t * Constructs a new effect composer.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {WebGLRenderTarget} [renderTarget] - This render target and a clone will\n\t * be used as the internal read and write buffers. If not given, the composer creates\n\t * the buffers automatically.\n\t */\n\tconstructor( renderer, renderTarget ) {\n\n\t\t/**\n\t\t * The renderer.\n\t\t *\n\t\t * @type {WebGLRenderer}\n\t\t */\n\t\tthis.renderer = renderer;\n\n\t\tthis._pixelRatio = renderer.getPixelRatio();\n\n\t\tif ( renderTarget === undefined ) {\n\n\t\t\tconst size = renderer.getSize( new Vector2() );\n\t\t\tthis._width = size.width;\n\t\t\tthis._height = size.height;\n\n\t\t\trenderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );\n\t\t\trenderTarget.texture.name = 'EffectComposer.rt1';\n\n\t\t} else {\n\n\t\t\tthis._width = renderTarget.width;\n\t\t\tthis._height = renderTarget.height;\n\n\t\t}\n\n\t\tthis.renderTarget1 = renderTarget;\n\t\tthis.renderTarget2 = renderTarget.clone();\n\t\tthis.renderTarget2.texture.name = 'EffectComposer.rt2';\n\n\t\t/**\n\t\t * A reference to the internal write buffer. Passes usually write\n\t\t * their result into this buffer.\n\t\t *\n\t\t * @type {WebGLRenderTarget}\n\t\t */\n\t\tthis.writeBuffer = this.renderTarget1;\n\n\t\t/**\n\t\t * A reference to the internal read buffer. Passes usually read\n\t\t * the previous render result from this buffer.\n\t\t *\n\t\t * @type {WebGLRenderTarget}\n\t\t */\n\t\tthis.readBuffer = this.renderTarget2;\n\n\t\t/**\n\t\t * Whether the final pass is rendered to the screen (default framebuffer) or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.renderToScreen = true;\n\n\t\t/**\n\t\t * An array representing the (ordered) chain of post-processing passes.\n\t\t *\n\t\t * @type {Array<Pass>}\n\t\t */\n\t\tthis.passes = [];\n\n\t\t/**\n\t\t * A copy pass used for internal swap operations.\n\t\t *\n\t\t * @private\n\t\t * @type {ShaderPass}\n\t\t */\n\t\tthis.copyPass = new ShaderPass( CopyShader );\n\t\tthis.copyPass.material.blending = NoBlending;\n\n\t\t/**\n\t\t * The internal clock for managing time data.\n\t\t *\n\t\t * @private\n\t\t * @type {Clock}\n\t\t */\n\t\tthis.clock = new Clock();\n\n\t}\n\n\t/**\n\t * Swaps the internal read/write buffers.\n\t */\n\tswapBuffers() {\n\n\t\tconst tmp = this.readBuffer;\n\t\tthis.readBuffer = this.writeBuffer;\n\t\tthis.writeBuffer = tmp;\n\n\t}\n\n\t/**\n\t * Adds the given pass to the pass chain.\n\t *\n\t * @param {Pass} pass - The pass to add.\n\t */\n\taddPass( pass ) {\n\n\t\tthis.passes.push( pass );\n\t\tpass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );\n\n\t}\n\n\t/**\n\t * Inserts the given pass at a given index.\n\t *\n\t * @param {Pass} pass - The pass to insert.\n\t * @param {number} index - The index into the pass chain.\n\t */\n\tinsertPass( pass, index ) {\n\n\t\tthis.passes.splice( index, 0, pass );\n\t\tpass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );\n\n\t}\n\n\t/**\n\t * Removes the given pass from the pass chain.\n\t *\n\t * @param {Pass} pass - The pass to remove.\n\t */\n\tremovePass( pass ) {\n\n\t\tconst index = this.passes.indexOf( pass );\n\n\t\tif ( index !== - 1 ) {\n\n\t\t\tthis.passes.splice( index, 1 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns `true` if the pass for the given index is the last enabled pass in the pass chain.\n\t *\n\t * @param {number} passIndex - The pass index.\n\t * @return {boolean} Whether the pass for the given index is the last pass in the pass chain.\n\t */\n\tisLastEnabledPass( passIndex ) {\n\n\t\tfor ( let i = passIndex + 1; i < this.passes.length; i ++ ) {\n\n\t\t\tif ( this.passes[ i ].enabled ) {\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Executes all enabled post-processing passes in order to produce the final frame.\n\t *\n\t * @param {number} deltaTime - The delta time in seconds. If not given, the composer computes\n\t * its own time delta value.\n\t */\n\trender( deltaTime ) {\n\n\t\t// deltaTime value is in seconds\n\n\t\tif ( deltaTime === undefined ) {\n\n\t\t\tdeltaTime = this.clock.getDelta();\n\n\t\t}\n\n\t\tconst currentRenderTarget = this.renderer.getRenderTarget();\n\n\t\tlet maskActive = false;\n\n\t\tfor ( let i = 0, il = this.passes.length; i < il; i ++ ) {\n\n\t\t\tconst pass = this.passes[ i ];\n\n\t\t\tif ( pass.enabled === false ) continue;\n\n\t\t\tpass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );\n\t\t\tpass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );\n\n\t\t\tif ( pass.needsSwap ) {\n\n\t\t\t\tif ( maskActive ) {\n\n\t\t\t\t\tconst context = this.renderer.getContext();\n\t\t\t\t\tconst stencil = this.renderer.state.buffers.stencil;\n\n\t\t\t\t\t//context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );\n\t\t\t\t\tstencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );\n\n\t\t\t\t\tthis.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );\n\n\t\t\t\t\t//context.stencilFunc( context.EQUAL, 1, 0xffffffff );\n\t\t\t\t\tstencil.setFunc( context.EQUAL, 1, 0xffffffff );\n\n\t\t\t\t}\n\n\t\t\t\tthis.swapBuffers();\n\n\t\t\t}\n\n\t\t\tif ( MaskPass !== undefined ) {\n\n\t\t\t\tif ( pass instanceof MaskPass ) {\n\n\t\t\t\t\tmaskActive = true;\n\n\t\t\t\t} else if ( pass instanceof ClearMaskPass ) {\n\n\t\t\t\t\tmaskActive = false;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis.renderer.setRenderTarget( currentRenderTarget );\n\n\t}\n\n\t/**\n\t * Resets the internal state of the EffectComposer.\n\t *\n\t * @param {WebGLRenderTarget} [renderTarget] - This render target has the same purpose like\n\t * the one from the constructor. If set, it is used to setup the read and write buffers.\n\t */\n\treset( renderTarget ) {\n\n\t\tif ( renderTarget === undefined ) {\n\n\t\t\tconst size = this.renderer.getSize( new Vector2() );\n\t\t\tthis._pixelRatio = this.renderer.getPixelRatio();\n\t\t\tthis._width = size.width;\n\t\t\tthis._height = size.height;\n\n\t\t\trenderTarget = this.renderTarget1.clone();\n\t\t\trenderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );\n\n\t\t}\n\n\t\tthis.renderTarget1.dispose();\n\t\tthis.renderTarget2.dispose();\n\t\tthis.renderTarget1 = renderTarget;\n\t\tthis.renderTarget2 = renderTarget.clone();\n\n\t\tthis.writeBuffer = this.renderTarget1;\n\t\tthis.readBuffer = this.renderTarget2;\n\n\t}\n\n\t/**\n\t * Resizes the internal read and write buffers as well as all passes. Similar to {@link WebGLRenderer#setSize},\n\t * this method honors the current pixel ration.\n\t *\n\t * @param {number} width - The width in logical pixels.\n\t * @param {number} height - The height in logical pixels.\n\t */\n\tsetSize( width, height ) {\n\n\t\tthis._width = width;\n\t\tthis._height = height;\n\n\t\tconst effectiveWidth = this._width * this._pixelRatio;\n\t\tconst effectiveHeight = this._height * this._pixelRatio;\n\n\t\tthis.renderTarget1.setSize( effectiveWidth, effectiveHeight );\n\t\tthis.renderTarget2.setSize( effectiveWidth, effectiveHeight );\n\n\t\tfor ( let i = 0; i < this.passes.length; i ++ ) {\n\n\t\t\tthis.passes[ i ].setSize( effectiveWidth, effectiveHeight );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sets device pixel ratio. This is usually used for HiDPI device to prevent blurring output.\n\t * Setting the pixel ratio will automatically resize the composer.\n\t *\n\t * @param {number} pixelRatio - The pixel ratio to set.\n\t */\n\tsetPixelRatio( pixelRatio ) {\n\n\t\tthis._pixelRatio = pixelRatio;\n\n\t\tthis.setSize( this._width, this._height );\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever the composer is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.renderTarget1.dispose();\n\t\tthis.renderTarget2.dispose();\n\n\t\tthis.copyPass.dispose();\n\n\t}\n\n}\n\nexport { EffectComposer };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-addons/postprocessing/MaskPass.js",
    "content": "import { Pass } from './Pass.js';\n\n/**\n * This pass can be used to define a mask during post processing.\n * Meaning only areas of subsequent post processing are affected\n * which lie in the masking area of this pass. Internally, the masking\n * is implemented with the stencil buffer.\n *\n * ```js\n * const maskPass = new MaskPass( scene, camera );\n * composer.addPass( maskPass );\n * ```\n *\n * @augments Pass\n * @three_import import { MaskPass } from 'three/addons/postprocessing/MaskPass.js';\n */\nclass MaskPass extends Pass {\n\n\t/**\n\t * Constructs a new mask pass.\n\t *\n\t * @param {Scene} scene - The 3D objects in this scene will define the mask.\n\t * @param {Camera} camera - The camera.\n\t */\n\tconstructor( scene, camera ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * The scene that defines the mask.\n\t\t *\n\t\t * @type {Scene}\n\t\t */\n\t\tthis.scene = scene;\n\n\t\t/**\n\t\t * The camera.\n\t\t *\n\t\t * @type {Camera}\n\t\t */\n\t\tthis.camera = camera;\n\n\t\t/**\n\t\t * Overwritten to perform a clear operation by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.clear = true;\n\n\t\t/**\n\t\t * Overwritten to disable the swap.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.needsSwap = false;\n\n\t\t/**\n\t\t * Whether to inverse the mask or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.inverse = false;\n\n\t}\n\n\t/**\n\t * Performs a mask pass with the configured scene and camera.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering\n\t * destination for the pass.\n\t * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the\n\t * previous pass from this buffer.\n\t * @param {number} deltaTime - The delta time in seconds.\n\t * @param {boolean} maskActive - Whether masking is active or not.\n\t */\n\trender( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {\n\n\t\tconst context = renderer.getContext();\n\t\tconst state = renderer.state;\n\n\t\t// don't update color or depth\n\n\t\tstate.buffers.color.setMask( false );\n\t\tstate.buffers.depth.setMask( false );\n\n\t\t// lock buffers\n\n\t\tstate.buffers.color.setLocked( true );\n\t\tstate.buffers.depth.setLocked( true );\n\n\t\t// set up stencil\n\n\t\tlet writeValue, clearValue;\n\n\t\tif ( this.inverse ) {\n\n\t\t\twriteValue = 0;\n\t\t\tclearValue = 1;\n\n\t\t} else {\n\n\t\t\twriteValue = 1;\n\t\t\tclearValue = 0;\n\n\t\t}\n\n\t\tstate.buffers.stencil.setTest( true );\n\t\tstate.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );\n\t\tstate.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );\n\t\tstate.buffers.stencil.setClear( clearValue );\n\t\tstate.buffers.stencil.setLocked( true );\n\n\t\t// draw into the stencil buffer\n\n\t\trenderer.setRenderTarget( readBuffer );\n\t\tif ( this.clear ) renderer.clear();\n\t\trenderer.render( this.scene, this.camera );\n\n\t\trenderer.setRenderTarget( writeBuffer );\n\t\tif ( this.clear ) renderer.clear();\n\t\trenderer.render( this.scene, this.camera );\n\n\t\t// unlock color and depth buffer and make them writable for subsequent rendering/clearing\n\n\t\tstate.buffers.color.setLocked( false );\n\t\tstate.buffers.depth.setLocked( false );\n\n\t\tstate.buffers.color.setMask( true );\n\t\tstate.buffers.depth.setMask( true );\n\n\t\t// only render where stencil is set to 1\n\n\t\tstate.buffers.stencil.setLocked( false );\n\t\tstate.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1\n\t\tstate.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );\n\t\tstate.buffers.stencil.setLocked( true );\n\n\t}\n\n}\n\n/**\n * This pass can be used to clear a mask previously defined with {@link MaskPass}.\n *\n * ```js\n * const clearPass = new ClearMaskPass();\n * composer.addPass( clearPass );\n * ```\n *\n * @augments Pass\n */\nclass ClearMaskPass extends Pass {\n\n\t/**\n\t * Constructs a new clear mask pass.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * Overwritten to disable the swap.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.needsSwap = false;\n\n\t}\n\n\t/**\n\t * Performs the clear of the currently defined mask.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering\n\t * destination for the pass.\n\t * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the\n\t * previous pass from this buffer.\n\t * @param {number} deltaTime - The delta time in seconds.\n\t * @param {boolean} maskActive - Whether masking is active or not.\n\t */\n\trender( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {\n\n\t\trenderer.state.buffers.stencil.setLocked( false );\n\t\trenderer.state.buffers.stencil.setTest( false );\n\n\t}\n\n}\n\nexport { MaskPass, ClearMaskPass };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-addons/postprocessing/Pass.js",
    "content": "import {\n\tBufferGeometry,\n\tFloat32BufferAttribute,\n\tOrthographicCamera,\n\tMesh\n} from 'three';\n\n/**\n * Abstract base class for all post processing passes.\n *\n * This module is only relevant for post processing with {@link WebGLRenderer}.\n *\n * @abstract\n * @three_import import { Pass } from 'three/addons/postprocessing/Pass.js';\n */\nclass Pass {\n\n\t/**\n\t * Constructs a new pass.\n\t */\n\tconstructor() {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isPass = true;\n\n\t\t/**\n\t\t * If set to `true`, the pass is processed by the composer.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.enabled = true;\n\n\t\t/**\n\t\t * If set to `true`, the pass indicates to swap read and write buffer after rendering.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.needsSwap = true;\n\n\t\t/**\n\t\t * If set to `true`, the pass clears its buffer before rendering\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.clear = false;\n\n\t\t/**\n\t\t * If set to `true`, the result of the pass is rendered to screen. The last pass in the composers\n\t\t * pass chain gets automatically rendered to screen, no matter how this property is configured.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.renderToScreen = false;\n\n\t}\n\n\t/**\n\t * Sets the size of the pass.\n\t *\n\t * @abstract\n\t * @param {number} width - The width to set.\n\t * @param {number} height - The height to set.\n\t */\n\tsetSize( /* width, height */ ) {}\n\n\t/**\n\t * This method holds the render logic of a pass. It must be implemented in all derived classes.\n\t *\n\t * @abstract\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering\n\t * destination for the pass.\n\t * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the\n\t * previous pass from this buffer.\n\t * @param {number} deltaTime - The delta time in seconds.\n\t * @param {boolean} maskActive - Whether masking is active or not.\n\t */\n\trender( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {\n\n\t\tconsole.error( 'THREE.Pass: .render() must be implemented in derived pass.' );\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever the pass is no longer used in your app.\n\t *\n\t * @abstract\n\t */\n\tdispose() {}\n\n}\n\n// Helper for passes that need to fill the viewport with a single quad.\n\nconst _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );\n\n// https://github.com/mrdoob/three.js/pull/21358\n\nclass FullscreenTriangleGeometry extends BufferGeometry {\n\n\tconstructor() {\n\n\t\tsuper();\n\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );\n\n\t}\n\n}\n\nconst _geometry = new FullscreenTriangleGeometry();\n\n\n/**\n * This module is a helper for passes which need to render a full\n * screen effect which is quite common in context of post processing.\n *\n * The intended usage is to reuse a single full screen quad for rendering\n * subsequent passes by just reassigning the `material` reference.\n *\n * This module can only be used with {@link WebGLRenderer}.\n *\n * @augments Mesh\n * @three_import import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';\n */\nclass FullScreenQuad {\n\n\t/**\n\t * Constructs a new full screen quad.\n\t *\n\t * @param {?Material} material - The material to render te full screen quad with.\n\t */\n\tconstructor( material ) {\n\n\t\tthis._mesh = new Mesh( _geometry, material );\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever the instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis._mesh.geometry.dispose();\n\n\t}\n\n\t/**\n\t * Renders the full screen quad.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t */\n\trender( renderer ) {\n\n\t\trenderer.render( this._mesh, _camera );\n\n\t}\n\n\t/**\n\t * The quad's material.\n\t *\n\t * @type {?Material}\n\t */\n\tget material() {\n\n\t\treturn this._mesh.material;\n\n\t}\n\n\tset material( value ) {\n\n\t\tthis._mesh.material = value;\n\n\t}\n\n}\n\nexport { Pass, FullScreenQuad };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-addons/postprocessing/RenderPass.js",
    "content": "import {\n\tColor\n} from 'three';\nimport { Pass } from './Pass.js';\n\n/**\n * This class represents a render pass. It takes a camera and a scene and produces\n * a beauty pass for subsequent post processing effects.\n *\n * ```js\n * const renderPass = new RenderPass( scene, camera );\n * composer.addPass( renderPass );\n * ```\n *\n * @augments Pass\n * @three_import import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';\n */\nclass RenderPass extends Pass {\n\n\t/**\n\t * Constructs a new render pass.\n\t *\n\t * @param {Scene} scene - The scene to render.\n\t * @param {Camera} camera - The camera.\n\t * @param {?Material} [overrideMaterial=null] - The override material. If set, this material is used\n\t * for all objects in the scene.\n\t * @param {?(number|Color|string)} [clearColor=null] - The clear color of the render pass.\n\t * @param {?number} [clearAlpha=null] - The clear alpha of the render pass.\n\t */\n\tconstructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * The scene to render.\n\t\t *\n\t\t * @type {Scene}\n\t\t */\n\t\tthis.scene = scene;\n\n\t\t/**\n\t\t * The camera.\n\t\t *\n\t\t * @type {Camera}\n\t\t */\n\t\tthis.camera = camera;\n\n\t\t/**\n\t\t * The override material. If set, this material is used\n\t\t * for all objects in the scene.\n\t\t *\n\t\t * @type {?Material}\n\t\t * @default null\n\t\t */\n\t\tthis.overrideMaterial = overrideMaterial;\n\n\t\t/**\n\t\t * The clear color of the render pass.\n\t\t *\n\t\t * @type {?(number|Color|string)}\n\t\t * @default null\n\t\t */\n\t\tthis.clearColor = clearColor;\n\n\t\t/**\n\t\t * The clear alpha of the render pass.\n\t\t *\n\t\t * @type {?number}\n\t\t * @default null\n\t\t */\n\t\tthis.clearAlpha = clearAlpha;\n\n\t\t/**\n\t\t * Overwritten to perform a clear operation by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.clear = true;\n\n\t\t/**\n\t\t * If set to `true`, only the depth can be cleared when `clear` is to `false`.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.clearDepth = false;\n\n\t\t/**\n\t\t * Overwritten to disable the swap.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.needsSwap = false;\n\n\t\t/**\n\t\t * This flag indicates that this pass renders the scene itself.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isRenderPass = true;\n\n\t\tthis._oldClearColor = new Color();\n\n\t}\n\n\t/**\n\t * Performs a beauty pass with the configured scene and camera.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering\n\t * destination for the pass.\n\t * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the\n\t * previous pass from this buffer.\n\t * @param {number} deltaTime - The delta time in seconds.\n\t * @param {boolean} maskActive - Whether masking is active or not.\n\t */\n\trender( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {\n\n\t\tconst oldAutoClear = renderer.autoClear;\n\t\trenderer.autoClear = false;\n\n\t\tlet oldClearAlpha, oldOverrideMaterial;\n\n\t\tif ( this.overrideMaterial !== null ) {\n\n\t\t\toldOverrideMaterial = this.scene.overrideMaterial;\n\n\t\t\tthis.scene.overrideMaterial = this.overrideMaterial;\n\n\t\t}\n\n\t\tif ( this.clearColor !== null ) {\n\n\t\t\trenderer.getClearColor( this._oldClearColor );\n\t\t\trenderer.setClearColor( this.clearColor, renderer.getClearAlpha() );\n\n\t\t}\n\n\t\tif ( this.clearAlpha !== null ) {\n\n\t\t\toldClearAlpha = renderer.getClearAlpha();\n\t\t\trenderer.setClearAlpha( this.clearAlpha );\n\n\t\t}\n\n\t\tif ( this.clearDepth == true ) {\n\n\t\t\trenderer.clearDepth();\n\n\t\t}\n\n\t\trenderer.setRenderTarget( this.renderToScreen ? null : readBuffer );\n\n\t\tif ( this.clear === true ) {\n\n\t\t\t// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600\n\t\t\trenderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );\n\n\t\t}\n\n\t\trenderer.render( this.scene, this.camera );\n\n\t\t// restore\n\n\t\tif ( this.clearColor !== null ) {\n\n\t\t\trenderer.setClearColor( this._oldClearColor );\n\n\t\t}\n\n\t\tif ( this.clearAlpha !== null ) {\n\n\t\t\trenderer.setClearAlpha( oldClearAlpha );\n\n\t\t}\n\n\t\tif ( this.overrideMaterial !== null ) {\n\n\t\t\tthis.scene.overrideMaterial = oldOverrideMaterial;\n\n\t\t}\n\n\t\trenderer.autoClear = oldAutoClear;\n\n\t}\n\n}\n\nexport { RenderPass };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-addons/postprocessing/ShaderPass.js",
    "content": "import {\n\tShaderMaterial,\n\tUniformsUtils\n} from 'three';\nimport { Pass, FullScreenQuad } from './Pass.js';\n\n/**\n * This pass can be used to create a post processing effect\n * with a raw GLSL shader object. Useful for implementing custom\n * effects.\n *\n * ```js\n * const fxaaPass = new ShaderPass( FXAAShader );\n * composer.addPass( fxaaPass );\n * ```\n *\n * @augments Pass\n * @three_import import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';\n */\nclass ShaderPass extends Pass {\n\n\t/**\n\t * Constructs a new shader pass.\n\t *\n\t * @param {Object|ShaderMaterial} [shader] - A shader object holding vertex and fragment shader as well as\n\t * defines and uniforms. It's also valid to pass a custom shader material.\n\t * @param {string} [textureID='tDiffuse'] - The name of the texture uniform that should sample\n\t * the read buffer.\n\t */\n\tconstructor( shader, textureID = 'tDiffuse' ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * The name of the texture uniform that should sample the read buffer.\n\t\t *\n\t\t * @type {string}\n\t\t * @default 'tDiffuse'\n\t\t */\n\t\tthis.textureID = textureID;\n\n\t\t/**\n\t\t * The pass uniforms.\n\t\t *\n\t\t * @type {?Object}\n\t\t */\n\t\tthis.uniforms = null;\n\n\t\t/**\n\t\t * The pass material.\n\t\t *\n\t\t * @type {?ShaderMaterial}\n\t\t */\n\t\tthis.material = null;\n\n\t\tif ( shader instanceof ShaderMaterial ) {\n\n\t\t\tthis.uniforms = shader.uniforms;\n\n\t\t\tthis.material = shader;\n\n\t\t} else if ( shader ) {\n\n\t\t\tthis.uniforms = UniformsUtils.clone( shader.uniforms );\n\n\t\t\tthis.material = new ShaderMaterial( {\n\n\t\t\t\tname: ( shader.name !== undefined ) ? shader.name : 'unspecified',\n\t\t\t\tdefines: Object.assign( {}, shader.defines ),\n\t\t\t\tuniforms: this.uniforms,\n\t\t\t\tvertexShader: shader.vertexShader,\n\t\t\t\tfragmentShader: shader.fragmentShader\n\n\t\t\t} );\n\n\t\t}\n\n\t\t// internals\n\n\t\tthis._fsQuad = new FullScreenQuad( this.material );\n\n\t}\n\n\t/**\n\t * Performs the shader pass.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering\n\t * destination for the pass.\n\t * @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the\n\t * previous pass from this buffer.\n\t * @param {number} deltaTime - The delta time in seconds.\n\t * @param {boolean} maskActive - Whether masking is active or not.\n\t */\n\trender( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {\n\n\t\tif ( this.uniforms[ this.textureID ] ) {\n\n\t\t\tthis.uniforms[ this.textureID ].value = readBuffer.texture;\n\n\t\t}\n\n\t\tthis._fsQuad.material = this.material;\n\n\t\tif ( this.renderToScreen ) {\n\n\t\t\trenderer.setRenderTarget( null );\n\t\t\tthis._fsQuad.render( renderer );\n\n\t\t} else {\n\n\t\t\trenderer.setRenderTarget( writeBuffer );\n\t\t\t// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600\n\t\t\tif ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );\n\t\t\tthis._fsQuad.render( renderer );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever the pass is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.material.dispose();\n\n\t\tthis._fsQuad.dispose();\n\n\t}\n\n}\n\nexport { ShaderPass };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-addons/shaders/CopyShader.js",
    "content": "/**\n * @module CopyShader\n * @three_import import { CopyShader } from 'three/addons/shaders/CopyShader.js';\n */\n\n/**\n * Full-screen copy shader pass.\n *\n * @constant\n * @type {ShaderMaterial~Shader}\n */\nconst CopyShader = {\n\n\tname: 'CopyShader',\n\n\tuniforms: {\n\n\t\t'tDiffuse': { value: null },\n\t\t'opacity': { value: 1.0 }\n\n\t},\n\n\tvertexShader: /* glsl */`\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvUv = uv;\n\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n\t\t}`,\n\n\tfragmentShader: /* glsl */`\n\n\t\tuniform float opacity;\n\n\t\tuniform sampler2D tDiffuse;\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvec4 texel = texture2D( tDiffuse, vUv );\n\t\t\tgl_FragColor = opacity * texel;\n\n\n\t\t}`\n\n};\n\nexport { CopyShader };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-r182.core.js",
    "content": "/**\n * @license\n * Copyright 2010-2025 Three.js Authors\n * SPDX-License-Identifier: MIT\n */\nconst REVISION = '182';\n\n/**\n * Represents mouse buttons and interaction types in context of controls.\n *\n * @type {ConstantsMouse}\n * @constant\n */\nconst MOUSE = { LEFT: 0, MIDDLE: 1, RIGHT: 2, ROTATE: 0, DOLLY: 1, PAN: 2 };\n\n/**\n * Represents touch interaction types in context of controls.\n *\n * @type {ConstantsTouch}\n * @constant\n */\nconst TOUCH = { ROTATE: 0, PAN: 1, DOLLY_PAN: 2, DOLLY_ROTATE: 3 };\n\n/**\n * Disables face culling.\n *\n * @type {number}\n * @constant\n */\nconst CullFaceNone = 0;\n\n/**\n * Culls back faces.\n *\n * @type {number}\n * @constant\n */\nconst CullFaceBack = 1;\n\n/**\n * Culls front faces.\n *\n * @type {number}\n * @constant\n */\nconst CullFaceFront = 2;\n\n/**\n * Culls both front and back faces.\n *\n * @type {number}\n * @constant\n */\nconst CullFaceFrontBack = 3;\n\n/**\n * Gives unfiltered shadow maps - fastest, but lowest quality.\n *\n * @type {number}\n * @constant\n */\nconst BasicShadowMap = 0;\n\n/**\n * Filters shadow maps using the Percentage-Closer Filtering (PCF) algorithm.\n *\n * @type {number}\n * @constant\n */\nconst PCFShadowMap = 1;\n\n/**\n * Filters shadow maps using the Percentage-Closer Filtering (PCF) algorithm with\n * better soft shadows especially when using low-resolution shadow maps.\n *\n * @type {number}\n * @constant\n */\nconst PCFSoftShadowMap = 2;\n\n/**\n * Filters shadow maps using the Variance Shadow Map (VSM) algorithm.\n * When using VSMShadowMap all shadow receivers will also cast shadows.\n *\n * @type {number}\n * @constant\n */\nconst VSMShadowMap = 3;\n\n/**\n * Only front faces are rendered.\n *\n * @type {number}\n * @constant\n */\nconst FrontSide = 0;\n\n/**\n * Only back faces are rendered.\n *\n * @type {number}\n * @constant\n */\nconst BackSide = 1;\n\n/**\n * Both front and back faces are rendered.\n *\n * @type {number}\n * @constant\n */\nconst DoubleSide = 2;\n\n/**\n * No blending is performed which effectively disables\n * alpha transparency.\n *\n * @type {number}\n * @constant\n */\nconst NoBlending = 0;\n\n/**\n * The default blending.\n *\n * @type {number}\n * @constant\n */\nconst NormalBlending = 1;\n\n/**\n * Represents additive blending.\n *\n * @type {number}\n * @constant\n */\nconst AdditiveBlending = 2;\n\n/**\n * Represents subtractive blending.\n *\n * @type {number}\n * @constant\n */\nconst SubtractiveBlending = 3;\n\n/**\n * Represents multiply blending.\n *\n * @type {number}\n * @constant\n */\nconst MultiplyBlending = 4;\n\n/**\n * Represents custom blending.\n *\n * @type {number}\n * @constant\n */\nconst CustomBlending = 5;\n\n/**\n * A `source + destination` blending equation.\n *\n * @type {number}\n * @constant\n */\nconst AddEquation = 100;\n\n/**\n * A `source - destination` blending equation.\n *\n * @type {number}\n * @constant\n */\nconst SubtractEquation = 101;\n\n/**\n * A `destination - source` blending equation.\n *\n * @type {number}\n * @constant\n */\nconst ReverseSubtractEquation = 102;\n\n/**\n * A blend equation that uses the minimum of source and destination.\n *\n * @type {number}\n * @constant\n */\nconst MinEquation = 103;\n\n/**\n * A blend equation that uses the maximum of source and destination.\n *\n * @type {number}\n * @constant\n */\nconst MaxEquation = 104;\n\n/**\n * Multiplies all colors by `0`.\n *\n * @type {number}\n * @constant\n */\nconst ZeroFactor = 200;\n\n/**\n * Multiplies all colors by `1`.\n *\n * @type {number}\n * @constant\n */\nconst OneFactor = 201;\n\n/**\n * Multiplies all colors by the source colors.\n *\n * @type {number}\n * @constant\n */\nconst SrcColorFactor = 202;\n\n/**\n * Multiplies all colors by `1` minus each source color.\n *\n * @type {number}\n * @constant\n */\nconst OneMinusSrcColorFactor = 203;\n\n/**\n * Multiplies all colors by the source alpha value.\n *\n * @type {number}\n * @constant\n */\nconst SrcAlphaFactor = 204;\n\n/**\n * Multiplies all colors by 1 minus the source alpha value.\n *\n * @type {number}\n * @constant\n */\nconst OneMinusSrcAlphaFactor = 205;\n\n/**\n * Multiplies all colors by the destination alpha value.\n *\n * @type {number}\n * @constant\n */\nconst DstAlphaFactor = 206;\n\n/**\n * Multiplies all colors by `1` minus the destination alpha value.\n *\n * @type {number}\n * @constant\n */\nconst OneMinusDstAlphaFactor = 207;\n\n/**\n * Multiplies all colors by the destination color.\n *\n * @type {number}\n * @constant\n */\nconst DstColorFactor = 208;\n\n/**\n * Multiplies all colors by `1` minus each destination color.\n *\n * @type {number}\n * @constant\n */\nconst OneMinusDstColorFactor = 209;\n\n/**\n * Multiplies the RGB colors by the smaller of either the source alpha\n * value or the value of `1` minus the destination alpha value. The alpha\n * value is multiplied by `1`.\n *\n * @type {number}\n * @constant\n */\nconst SrcAlphaSaturateFactor = 210;\n\n/**\n * Multiplies all colors by a constant color.\n *\n * @type {number}\n * @constant\n */\nconst ConstantColorFactor = 211;\n\n/**\n * Multiplies all colors by `1` minus a constant color.\n *\n * @type {number}\n * @constant\n */\nconst OneMinusConstantColorFactor = 212;\n\n/**\n * Multiplies all colors by a constant alpha value.\n *\n * @type {number}\n * @constant\n */\nconst ConstantAlphaFactor = 213;\n\n/**\n * Multiplies all colors by 1 minus a constant alpha value.\n *\n * @type {number}\n * @constant\n */\nconst OneMinusConstantAlphaFactor = 214;\n\n/**\n * Never pass.\n *\n * @type {number}\n * @constant\n */\nconst NeverDepth = 0;\n\n/**\n * Always pass.\n *\n * @type {number}\n * @constant\n */\nconst AlwaysDepth = 1;\n\n/**\n * Pass if the incoming value is less than the depth buffer value.\n *\n * @type {number}\n * @constant\n */\nconst LessDepth = 2;\n\n/**\n * Pass if the incoming value is less than or equal to the depth buffer value.\n *\n * @type {number}\n * @constant\n */\nconst LessEqualDepth = 3;\n\n/**\n * Pass if the incoming value equals the depth buffer value.\n *\n * @type {number}\n * @constant\n */\nconst EqualDepth = 4;\n\n/**\n * Pass if the incoming value is greater than or equal to the depth buffer value.\n *\n * @type {number}\n * @constant\n */\nconst GreaterEqualDepth = 5;\n\n/**\n * Pass if the incoming value is greater than the depth buffer value.\n *\n * @type {number}\n * @constant\n */\nconst GreaterDepth = 6;\n\n/**\n * Pass if the incoming value is not equal to the depth buffer value.\n *\n * @type {number}\n * @constant\n */\nconst NotEqualDepth = 7;\n\n/**\n * Multiplies the environment map color with the surface color.\n *\n * @type {number}\n * @constant\n */\nconst MultiplyOperation = 0;\n\n/**\n * Uses reflectivity to blend between the two colors.\n *\n * @type {number}\n * @constant\n */\nconst MixOperation = 1;\n\n/**\n * Adds the two colors.\n *\n * @type {number}\n * @constant\n */\nconst AddOperation = 2;\n\n/**\n * No tone mapping is applied.\n *\n * @type {number}\n * @constant\n */\nconst NoToneMapping = 0;\n\n/**\n * Linear tone mapping.\n *\n * @type {number}\n * @constant\n */\nconst LinearToneMapping = 1;\n\n/**\n * Reinhard tone mapping.\n *\n * @type {number}\n * @constant\n */\nconst ReinhardToneMapping = 2;\n\n/**\n * Cineon tone mapping.\n *\n * @type {number}\n * @constant\n */\nconst CineonToneMapping = 3;\n\n/**\n * ACES Filmic tone mapping.\n *\n * @type {number}\n * @constant\n */\nconst ACESFilmicToneMapping = 4;\n\n/**\n * Custom tone mapping.\n *\n * Expects a custom implementation by modifying shader code of the material's fragment shader.\n *\n * @type {number}\n * @constant\n */\nconst CustomToneMapping = 5;\n\n/**\n * AgX tone mapping.\n *\n * @type {number}\n * @constant\n */\nconst AgXToneMapping = 6;\n\n/**\n * Neutral tone mapping.\n *\n * Implementation based on the Khronos 3D Commerce Group standard tone mapping.\n *\n * @type {number}\n * @constant\n */\nconst NeutralToneMapping = 7;\n\n/**\n * The skinned mesh shares the same world space as the skeleton.\n *\n * @type {string}\n * @constant\n */\nconst AttachedBindMode = 'attached';\n\n/**\n * The skinned mesh does not share the same world space as the skeleton.\n * This is useful when a skeleton is shared across multiple skinned meshes.\n *\n * @type {string}\n * @constant\n */\nconst DetachedBindMode = 'detached';\n\n/**\n * Maps textures using the geometry's UV coordinates.\n *\n * @type {number}\n * @constant\n */\nconst UVMapping = 300;\n\n/**\n * Reflection mapping for cube textures.\n *\n * @type {number}\n * @constant\n */\nconst CubeReflectionMapping = 301;\n\n/**\n * Refraction mapping for cube textures.\n *\n * @type {number}\n * @constant\n */\nconst CubeRefractionMapping = 302;\n\n/**\n * Reflection mapping for equirectangular textures.\n *\n * @type {number}\n * @constant\n */\nconst EquirectangularReflectionMapping = 303;\n\n/**\n * Refraction mapping for equirectangular textures.\n *\n * @type {number}\n * @constant\n */\nconst EquirectangularRefractionMapping = 304;\n\n/**\n * Reflection mapping for PMREM textures.\n *\n * @type {number}\n * @constant\n */\nconst CubeUVReflectionMapping = 306;\n\n/**\n * The texture will simply repeat to infinity.\n *\n * @type {number}\n * @constant\n */\nconst RepeatWrapping = 1000;\n\n/**\n * The last pixel of the texture stretches to the edge of the mesh.\n *\n * @type {number}\n * @constant\n */\nconst ClampToEdgeWrapping = 1001;\n\n/**\n * The texture will repeats to infinity, mirroring on each repeat.\n *\n * @type {number}\n * @constant\n */\nconst MirroredRepeatWrapping = 1002;\n\n/**\n * Returns the value of the texture element that is nearest (in Manhattan distance)\n * to the specified texture coordinates.\n *\n * @type {number}\n * @constant\n */\nconst NearestFilter = 1003;\n\n/**\n * Chooses the mipmap that most closely matches the size of the pixel being textured\n * and uses the `NearestFilter` criterion (the texel nearest to the center of the pixel)\n * to produce a texture value.\n *\n * @type {number}\n * @constant\n */\nconst NearestMipmapNearestFilter = 1004;\nconst NearestMipMapNearestFilter = 1004; // legacy\n\n/**\n * Chooses the two mipmaps that most closely match the size of the pixel being textured and\n * uses the `NearestFilter` criterion to produce a texture value from each mipmap.\n * The final texture value is a weighted average of those two values.\n *\n * @type {number}\n * @constant\n */\nconst NearestMipmapLinearFilter = 1005;\nconst NearestMipMapLinearFilter = 1005; // legacy\n\n/**\n * Returns the weighted average of the four texture elements that are closest to the specified\n * texture coordinates, and can include items wrapped or repeated from other parts of a texture,\n * depending on the values of `wrapS` and `wrapT`, and on the exact mapping.\n *\n * @type {number}\n * @constant\n */\nconst LinearFilter = 1006;\n\n/**\n * Chooses the mipmap that most closely matches the size of the pixel being textured and uses\n * the `LinearFilter` criterion (a weighted average of the four texels that are closest to the\n * center of the pixel) to produce a texture value.\n *\n * @type {number}\n * @constant\n */\nconst LinearMipmapNearestFilter = 1007;\nconst LinearMipMapNearestFilter = 1007; // legacy\n\n/**\n * Chooses the two mipmaps that most closely match the size of the pixel being textured and uses\n * the `LinearFilter` criterion to produce a texture value from each mipmap. The final texture value\n * is a weighted average of those two values.\n *\n * @type {number}\n * @constant\n */\nconst LinearMipmapLinearFilter = 1008;\nconst LinearMipMapLinearFilter = 1008; // legacy\n\n/**\n * An unsigned byte data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst UnsignedByteType = 1009;\n\n/**\n * A byte data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst ByteType = 1010;\n\n/**\n * A short data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst ShortType = 1011;\n\n/**\n * An unsigned short data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst UnsignedShortType = 1012;\n\n/**\n * An int data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst IntType = 1013;\n\n/**\n * An unsigned int data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst UnsignedIntType = 1014;\n\n/**\n * A float data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst FloatType = 1015;\n\n/**\n * A half float data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst HalfFloatType = 1016;\n\n/**\n * An unsigned short 4_4_4_4 (packed) data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst UnsignedShort4444Type = 1017;\n\n/**\n * An unsigned short 5_5_5_1 (packed) data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst UnsignedShort5551Type = 1018;\n\n/**\n * An unsigned int 24_8 data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst UnsignedInt248Type = 1020;\n\n/**\n * An unsigned int 5_9_9_9 (packed) data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst UnsignedInt5999Type = 35902;\n\n/**\n * An unsigned int 10_11_11 (packed) data type for textures.\n *\n * @type {number}\n * @constant\n */\nconst UnsignedInt101111Type = 35899;\n\n/**\n * Discards the red, green and blue components and reads just the alpha component.\n *\n * @type {number}\n * @constant\n */\nconst AlphaFormat = 1021;\n\n/**\n * Discards the alpha component and reads the red, green and blue component.\n *\n * @type {number}\n * @constant\n */\nconst RGBFormat = 1022;\n\n/**\n * Reads the red, green, blue and alpha components.\n *\n * @type {number}\n * @constant\n */\nconst RGBAFormat = 1023;\n\n/**\n * Reads each element as a single depth value, converts it to floating point, and clamps to the range `[0,1]`.\n *\n * @type {number}\n * @constant\n */\nconst DepthFormat = 1026;\n\n/**\n * Reads each element is a pair of depth and stencil values. The depth component of the pair is interpreted as\n * in `DepthFormat`. The stencil component is interpreted based on the depth + stencil internal format.\n *\n * @type {number}\n * @constant\n */\nconst DepthStencilFormat = 1027;\n\n/**\n * Discards the green, blue and alpha components and reads just the red component.\n *\n * @type {number}\n * @constant\n */\nconst RedFormat = 1028;\n\n/**\n * Discards the green, blue and alpha components and reads just the red component. The texels are read as integers instead of floating point.\n *\n * @type {number}\n * @constant\n */\nconst RedIntegerFormat = 1029;\n\n/**\n * Discards the alpha, and blue components and reads the red, and green components.\n *\n * @type {number}\n * @constant\n */\nconst RGFormat = 1030;\n\n/**\n * Discards the alpha, and blue components and reads the red, and green components. The texels are read as integers instead of floating point.\n *\n * @type {number}\n * @constant\n */\nconst RGIntegerFormat = 1031;\n\n/**\n * Discards the alpha component and reads the red, green and blue component. The texels are read as integers instead of floating point.\n *\n * @type {number}\n * @constant\n */\nconst RGBIntegerFormat = 1032;\n\n/**\n * Reads the red, green, blue and alpha components. The texels are read as integers instead of floating point.\n *\n * @type {number}\n * @constant\n */\nconst RGBAIntegerFormat = 1033;\n\n/**\n * A DXT1-compressed image in an RGB image format.\n *\n * @type {number}\n * @constant\n */\nconst RGB_S3TC_DXT1_Format = 33776;\n\n/**\n * A DXT1-compressed image in an RGB image format with a simple on/off alpha value.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_S3TC_DXT1_Format = 33777;\n\n/**\n * A DXT3-compressed image in an RGBA image format. Compared to a 32-bit RGBA texture, it offers 4:1 compression.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_S3TC_DXT3_Format = 33778;\n\n/**\n * A DXT5-compressed image in an RGBA image format. It also provides a 4:1 compression, but differs to the DXT3\n * compression in how the alpha compression is done.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_S3TC_DXT5_Format = 33779;\n\n/**\n * PVRTC RGB compression in 4-bit mode. One block for each 4×4 pixels.\n *\n * @type {number}\n * @constant\n */\nconst RGB_PVRTC_4BPPV1_Format = 35840;\n\n/**\n * PVRTC RGB compression in 2-bit mode. One block for each 8×4 pixels.\n *\n * @type {number}\n * @constant\n */\nconst RGB_PVRTC_2BPPV1_Format = 35841;\n\n/**\n * PVRTC RGBA compression in 4-bit mode. One block for each 4×4 pixels.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_PVRTC_4BPPV1_Format = 35842;\n\n/**\n * PVRTC RGBA compression in 2-bit mode. One block for each 8×4 pixels.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_PVRTC_2BPPV1_Format = 35843;\n\n/**\n * ETC1 RGB format.\n *\n * @type {number}\n * @constant\n */\nconst RGB_ETC1_Format = 36196;\n\n/**\n * ETC2 RGB format.\n *\n * @type {number}\n * @constant\n */\nconst RGB_ETC2_Format = 37492;\n\n/**\n * ETC2 RGBA format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ETC2_EAC_Format = 37496;\n\n/**\n * EAC R11 UNORM format.\n *\n * @type {number}\n * @constant\n */\nconst R11_EAC_Format = 37488; // 0x9270\n\n/**\n * EAC R11 SNORM format.\n *\n * @type {number}\n * @constant\n */\nconst SIGNED_R11_EAC_Format = 37489; // 0x9271\n\n/**\n * EAC RG11 UNORM format.\n *\n * @type {number}\n * @constant\n */\nconst RG11_EAC_Format = 37490; // 0x9272\n\n/**\n * EAC RG11 SNORM format.\n *\n * @type {number}\n * @constant\n */\nconst SIGNED_RG11_EAC_Format = 37491; // 0x9273\n\n/**\n * ASTC RGBA 4x4 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_4x4_Format = 37808;\n\n/**\n * ASTC RGBA 5x4 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_5x4_Format = 37809;\n\n/**\n * ASTC RGBA 5x5 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_5x5_Format = 37810;\n\n/**\n * ASTC RGBA 6x5 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_6x5_Format = 37811;\n\n/**\n * ASTC RGBA 6x6 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_6x6_Format = 37812;\n\n/**\n * ASTC RGBA 8x5 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_8x5_Format = 37813;\n\n/**\n * ASTC RGBA 8x6 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_8x6_Format = 37814;\n\n/**\n * ASTC RGBA 8x8 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_8x8_Format = 37815;\n\n/**\n * ASTC RGBA 10x5 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_10x5_Format = 37816;\n\n/**\n * ASTC RGBA 10x6 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_10x6_Format = 37817;\n\n/**\n * ASTC RGBA 10x8 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_10x8_Format = 37818;\n\n/**\n * ASTC RGBA 10x10 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_10x10_Format = 37819;\n\n/**\n * ASTC RGBA 12x10 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_12x10_Format = 37820;\n\n/**\n * ASTC RGBA 12x12 format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_ASTC_12x12_Format = 37821;\n\n/**\n * BPTC RGBA format.\n *\n * @type {number}\n * @constant\n */\nconst RGBA_BPTC_Format = 36492;\n\n/**\n * BPTC Signed RGB format.\n *\n * @type {number}\n * @constant\n */\nconst RGB_BPTC_SIGNED_Format = 36494;\n\n/**\n * BPTC Unsigned RGB format.\n *\n * @type {number}\n * @constant\n */\nconst RGB_BPTC_UNSIGNED_Format = 36495;\n\n/**\n * RGTC1 Red format.\n *\n * @type {number}\n * @constant\n */\nconst RED_RGTC1_Format = 36283;\n\n/**\n * RGTC1 Signed Red format.\n *\n * @type {number}\n * @constant\n */\nconst SIGNED_RED_RGTC1_Format = 36284;\n\n/**\n * RGTC2 Red Green format.\n *\n * @type {number}\n * @constant\n */\nconst RED_GREEN_RGTC2_Format = 36285;\n\n/**\n * RGTC2 Signed Red Green format.\n *\n * @type {number}\n * @constant\n */\nconst SIGNED_RED_GREEN_RGTC2_Format = 36286;\n\n/**\n * Animations are played once.\n *\n * @type {number}\n * @constant\n */\nconst LoopOnce = 2200;\n\n/**\n * Animations are played with a chosen number of repetitions, each time jumping from\n * the end of the clip directly to its beginning.\n *\n * @type {number}\n * @constant\n */\nconst LoopRepeat = 2201;\n\n/**\n * Animations are played with a chosen number of repetitions, alternately playing forward\n * and backward.\n *\n * @type {number}\n * @constant\n */\nconst LoopPingPong = 2202;\n\n/**\n * Discrete interpolation mode for keyframe tracks.\n *\n * @type {number}\n * @constant\n */\nconst InterpolateDiscrete = 2300;\n\n/**\n * Linear interpolation mode for keyframe tracks.\n *\n * @type {number}\n * @constant\n */\nconst InterpolateLinear = 2301;\n\n/**\n * Smooth interpolation mode for keyframe tracks.\n *\n * @type {number}\n * @constant\n */\nconst InterpolateSmooth = 2302;\n\n/**\n * Zero curvature ending for animations.\n *\n * @type {number}\n * @constant\n */\nconst ZeroCurvatureEnding = 2400;\n\n/**\n * Zero slope ending for animations.\n *\n * @type {number}\n * @constant\n */\nconst ZeroSlopeEnding = 2401;\n\n/**\n * Wrap around ending for animations.\n *\n * @type {number}\n * @constant\n */\nconst WrapAroundEnding = 2402;\n\n/**\n * Default animation blend mode.\n *\n * @type {number}\n * @constant\n */\nconst NormalAnimationBlendMode = 2500;\n\n/**\n * Additive animation blend mode. Can be used to layer motions on top of\n * each other to build complex performances from smaller re-usable assets.\n *\n * @type {number}\n * @constant\n */\nconst AdditiveAnimationBlendMode = 2501;\n\n/**\n * For every three vertices draw a single triangle.\n *\n * @type {number}\n * @constant\n */\nconst TrianglesDrawMode = 0;\n\n/**\n * For each vertex draw a triangle from the last three vertices.\n *\n * @type {number}\n * @constant\n */\nconst TriangleStripDrawMode = 1;\n\n/**\n * For each vertex draw a triangle from the first vertex and the last two vertices.\n *\n * @type {number}\n * @constant\n */\nconst TriangleFanDrawMode = 2;\n\n/**\n * The depth value is inverted (1.0 - z) for visualization purposes.\n *\n * @type {number}\n * @constant\n */\nconst BasicDepthPacking = 3200;\n\n/**\n * The depth value is packed into 32 bit RGBA.\n *\n * @type {number}\n * @constant\n */\nconst RGBADepthPacking = 3201;\n\n/**\n * The depth value is packed into 24 bit RGB.\n *\n * @type {number}\n * @constant\n */\nconst RGBDepthPacking = 3202;\n\n/**\n * The depth value is packed into 16 bit RG.\n *\n * @type {number}\n * @constant\n */\nconst RGDepthPacking = 3203;\n\n/**\n * Normal information is relative to the underlying surface.\n *\n * @type {number}\n * @constant\n */\nconst TangentSpaceNormalMap = 0;\n\n/**\n * Normal information is relative to the object orientation.\n *\n * @type {number}\n * @constant\n */\nconst ObjectSpaceNormalMap = 1;\n\n// Color space string identifiers, matching CSS Color Module Level 4 and WebGPU names where available.\n\n/**\n * No color space.\n *\n * @type {string}\n * @constant\n */\nconst NoColorSpace = '';\n\n/**\n * sRGB color space.\n *\n * @type {string}\n * @constant\n */\nconst SRGBColorSpace = 'srgb';\n\n/**\n * sRGB-linear color space.\n *\n * @type {string}\n * @constant\n */\nconst LinearSRGBColorSpace = 'srgb-linear';\n\n/**\n * Linear transfer function.\n *\n * @type {string}\n * @constant\n */\nconst LinearTransfer = 'linear';\n\n/**\n * sRGB transfer function.\n *\n * @type {string}\n * @constant\n */\nconst SRGBTransfer = 'srgb';\n\n/**\n * No normal map packing.\n *\n * @type {string}\n * @constant\n */\nconst NoNormalPacking = '';\n\n/**\n * Normal RG packing.\n *\n * @type {string}\n * @constant\n */\nconst NormalRGPacking = 'rg';\n\n/**\n * Normal GA packing.\n *\n * @type {string}\n * @constant\n */\nconst NormalGAPacking = 'ga';\n\n/**\n * Sets the stencil buffer value to `0`.\n *\n * @type {number}\n * @constant\n */\nconst ZeroStencilOp = 0;\n\n/**\n * Keeps the current value.\n *\n * @type {number}\n * @constant\n */\nconst KeepStencilOp = 7680;\n\n/**\n * Sets the stencil buffer value to the specified reference value.\n *\n * @type {number}\n * @constant\n */\nconst ReplaceStencilOp = 7681;\n\n/**\n * Increments the current stencil buffer value. Clamps to the maximum representable unsigned value.\n *\n * @type {number}\n * @constant\n */\nconst IncrementStencilOp = 7682;\n\n/**\n * Decrements the current stencil buffer value. Clamps to `0`.\n *\n * @type {number}\n * @constant\n */\nconst DecrementStencilOp = 7683;\n\n/**\n * Increments the current stencil buffer value. Wraps stencil buffer value to zero when incrementing\n * the maximum representable unsigned value.\n *\n * @type {number}\n * @constant\n */\nconst IncrementWrapStencilOp = 34055;\n\n/**\n * Decrements the current stencil buffer value. Wraps stencil buffer value to the maximum representable\n * unsigned value when decrementing a stencil buffer value of `0`.\n *\n * @type {number}\n * @constant\n */\nconst DecrementWrapStencilOp = 34056;\n\n/**\n * Inverts the current stencil buffer value bitwise.\n *\n * @type {number}\n * @constant\n */\nconst InvertStencilOp = 5386;\n\n/**\n * Will never return true.\n *\n * @type {number}\n * @constant\n */\nconst NeverStencilFunc = 512;\n\n/**\n * Will return true if the stencil reference value is less than the current stencil value.\n *\n * @type {number}\n * @constant\n */\nconst LessStencilFunc = 513;\n\n/**\n * Will return true if the stencil reference value is equal to the current stencil value.\n *\n * @type {number}\n * @constant\n */\nconst EqualStencilFunc = 514;\n\n/**\n * Will return true if the stencil reference value is less than or equal to the current stencil value.\n *\n * @type {number}\n * @constant\n */\nconst LessEqualStencilFunc = 515;\n\n/**\n * Will return true if the stencil reference value is greater than the current stencil value.\n *\n * @type {number}\n * @constant\n */\nconst GreaterStencilFunc = 516;\n\n/**\n * Will return true if the stencil reference value is not equal to the current stencil value.\n *\n * @type {number}\n * @constant\n */\nconst NotEqualStencilFunc = 517;\n\n/**\n * Will return true if the stencil reference value is greater than or equal to the current stencil value.\n *\n * @type {number}\n * @constant\n */\nconst GreaterEqualStencilFunc = 518;\n\n/**\n * Will always return true.\n *\n * @type {number}\n * @constant\n */\nconst AlwaysStencilFunc = 519;\n\n/**\n * Never pass.\n *\n * @type {number}\n * @constant\n */\nconst NeverCompare = 512;\n\n/**\n * Pass if the incoming value is less than the texture value.\n *\n * @type {number}\n * @constant\n */\nconst LessCompare = 513;\n\n/**\n * Pass if the incoming value equals the texture value.\n *\n * @type {number}\n * @constant\n */\nconst EqualCompare = 514;\n\n/**\n * Pass if the incoming value is less than or equal to the texture value.\n *\n * @type {number}\n * @constant\n */\nconst LessEqualCompare = 515;\n\n/**\n * Pass if the incoming value is greater than the texture value.\n *\n * @type {number}\n * @constant\n */\nconst GreaterCompare = 516;\n\n/**\n * Pass if the incoming value is not equal to the texture value.\n *\n * @type {number}\n * @constant\n */\nconst NotEqualCompare = 517;\n\n/**\n * Pass if the incoming value is greater than or equal to the texture value.\n *\n * @type {number}\n * @constant\n */\nconst GreaterEqualCompare = 518;\n\n/**\n * Always pass.\n *\n * @type {number}\n * @constant\n */\nconst AlwaysCompare = 519;\n\n/**\n * The contents are intended to be specified once by the application, and used many\n * times as the source for drawing and image specification commands.\n *\n * @type {number}\n * @constant\n */\nconst StaticDrawUsage = 35044;\n\n/**\n * The contents are intended to be respecified repeatedly by the application, and\n * used many times as the source for drawing and image specification commands.\n *\n * @type {number}\n * @constant\n */\nconst DynamicDrawUsage = 35048;\n\n/**\n * The contents are intended to be specified once by the application, and used at most\n * a few times as the source for drawing and image specification commands.\n *\n * @type {number}\n * @constant\n */\nconst StreamDrawUsage = 35040;\n\n/**\n * The contents are intended to be specified once by reading data from the 3D API, and queried\n * many times by the application.\n *\n * @type {number}\n * @constant\n */\nconst StaticReadUsage = 35045;\n\n/**\n * The contents are intended to be respecified repeatedly by reading data from the 3D API, and queried\n * many times by the application.\n *\n * @type {number}\n * @constant\n */\nconst DynamicReadUsage = 35049;\n\n/**\n * The contents are intended to be specified once by reading data from the 3D API, and queried at most\n * a few times by the application\n *\n * @type {number}\n * @constant\n */\nconst StreamReadUsage = 35041;\n\n/**\n * The contents are intended to be specified once by reading data from the 3D API, and used many times as\n * the source for WebGL drawing and image specification commands.\n *\n * @type {number}\n * @constant\n */\nconst StaticCopyUsage = 35046;\n\n/**\n * The contents are intended to be respecified repeatedly by reading data from the 3D API, and used many times\n * as the source for WebGL drawing and image specification commands.\n *\n * @type {number}\n * @constant\n */\nconst DynamicCopyUsage = 35050;\n\n/**\n * The contents are intended to be specified once by reading data from the 3D API, and used at most a few times\n * as the source for WebGL drawing and image specification commands.\n *\n * @type {number}\n * @constant\n */\nconst StreamCopyUsage = 35042;\n\n/**\n * GLSL 1 shader code.\n *\n * @type {string}\n * @constant\n */\nconst GLSL1 = '100';\n\n/**\n * GLSL 3 shader code.\n *\n * @type {string}\n * @constant\n */\nconst GLSL3 = '300 es';\n\n/**\n * WebGL coordinate system.\n *\n * @type {number}\n * @constant\n */\nconst WebGLCoordinateSystem = 2000;\n\n/**\n * WebGPU coordinate system.\n *\n * @type {number}\n * @constant\n */\nconst WebGPUCoordinateSystem = 2001;\n\n/**\n * Represents the different timestamp query types.\n *\n * @type {ConstantsTimestampQuery}\n * @constant\n */\nconst TimestampQuery = {\n\tCOMPUTE: 'compute',\n\tRENDER: 'render'\n};\n\n/**\n * Represents mouse buttons and interaction types in context of controls.\n *\n * @type {ConstantsInterpolationSamplingType}\n * @constant\n */\nconst InterpolationSamplingType = {\n\tPERSPECTIVE: 'perspective',\n\tLINEAR: 'linear',\n\tFLAT: 'flat'\n};\n\n/**\n * Represents the different interpolation sampling modes.\n *\n * @type {ConstantsInterpolationSamplingMode}\n * @constant\n */\nconst InterpolationSamplingMode = {\n\tNORMAL: 'normal',\n\tCENTROID: 'centroid',\n\tSAMPLE: 'sample',\n\tFIRST: 'first',\n\tEITHER: 'either'\n};\n\n/**\n * This type represents mouse buttons and interaction types in context of controls.\n *\n * @typedef {Object} ConstantsMouse\n * @property {number} MIDDLE - The left mouse button.\n * @property {number} LEFT - The middle mouse button.\n * @property {number} RIGHT - The right mouse button.\n * @property {number} ROTATE - A rotate interaction.\n * @property {number} DOLLY - A dolly interaction.\n * @property {number} PAN - A pan interaction.\n **/\n\n/**\n * This type represents touch interaction types in context of controls.\n *\n * @typedef {Object} ConstantsTouch\n * @property {number} ROTATE - A rotate interaction.\n * @property {number} PAN - A pan interaction.\n * @property {number} DOLLY_PAN - The dolly-pan interaction.\n * @property {number} DOLLY_ROTATE - A dolly-rotate interaction.\n **/\n\n/**\n * This type represents the different timestamp query types.\n *\n * @typedef {Object} ConstantsTimestampQuery\n * @property {string} COMPUTE - A `compute` timestamp query.\n * @property {string} RENDER - A `render` timestamp query.\n **/\n\n/**\n * Represents the different interpolation sampling types.\n *\n * @typedef {Object} ConstantsInterpolationSamplingType\n * @property {string} PERSPECTIVE - Perspective-correct interpolation.\n * @property {string} LINEAR - Linear interpolation.\n * @property {string} FLAT - Flat interpolation.\n */\n\n/**\n * Represents the different interpolation sampling modes.\n *\n * @typedef {Object} ConstantsInterpolationSamplingMode\n * @property {string} NORMAL - Normal sampling mode.\n * @property {string} CENTROID - Centroid sampling mode.\n * @property {string} SAMPLE - Sample-specific sampling mode.\n * @property {string} FIRST - Flat interpolation using the first vertex.\n * @property {string} EITHER - Flat interpolation using either vertex.\n */\n\nfunction arrayNeedsUint32( array ) {\n\n\t// assumes larger values usually on last\n\n\tfor ( let i = array.length - 1; i >= 0; -- i ) {\n\n\t\tif ( array[ i ] >= 65535 ) return true; // account for PRIMITIVE_RESTART_FIXED_INDEX, #24565\n\n\t}\n\n\treturn false;\n\n}\n\nconst TYPED_ARRAYS = {\n\tInt8Array: Int8Array,\n\tUint8Array: Uint8Array,\n\tUint8ClampedArray: Uint8ClampedArray,\n\tInt16Array: Int16Array,\n\tUint16Array: Uint16Array,\n\tInt32Array: Int32Array,\n\tUint32Array: Uint32Array,\n\tFloat32Array: Float32Array,\n\tFloat64Array: Float64Array\n};\n\nfunction getTypedArray( type, buffer ) {\n\n\treturn new TYPED_ARRAYS[ type ]( buffer );\n\n}\n\n/**\n * Returns `true` if the given object is a typed array.\n *\n * @param {any} array - The object to check.\n * @return {boolean} Whether the given object is a typed array.\n */\nfunction isTypedArray( array ) {\n\n\treturn ArrayBuffer.isView( array ) && ! ( array instanceof DataView );\n\n}\n\nfunction createElementNS( name ) {\n\n\treturn document.createElementNS( 'http://www.w3.org/1999/xhtml', name );\n\n}\n\nfunction createCanvasElement() {\n\n\tconst canvas = createElementNS( 'canvas' );\n\tcanvas.style.display = 'block';\n\treturn canvas;\n\n}\n\nconst _cache = {};\n\nlet _setConsoleFunction = null;\n\nfunction setConsoleFunction( fn ) {\n\n\t_setConsoleFunction = fn;\n\n}\n\nfunction getConsoleFunction() {\n\n\treturn _setConsoleFunction;\n\n}\n\nfunction log( ...params ) {\n\n\tconst message = 'THREE.' + params.shift();\n\n\tif ( _setConsoleFunction ) {\n\n\t\t_setConsoleFunction( 'log', message, ...params );\n\n\t} else {\n\n\t\tconsole.log( message, ...params );\n\n\t}\n\n}\n\nfunction warn( ...params ) {\n\n\tconst message = 'THREE.' + params.shift();\n\n\tif ( _setConsoleFunction ) {\n\n\t\t_setConsoleFunction( 'warn', message, ...params );\n\n\t} else {\n\n\t\tconsole.warn( message, ...params );\n\n\t}\n\n}\n\nfunction error( ...params ) {\n\n\tconst message = 'THREE.' + params.shift();\n\n\tif ( _setConsoleFunction ) {\n\n\t\t_setConsoleFunction( 'error', message, ...params );\n\n\t} else {\n\n\t\tconsole.error( message, ...params );\n\n\t}\n\n}\n\nfunction warnOnce( ...params ) {\n\n\tconst message = params.join( ' ' );\n\n\tif ( message in _cache ) return;\n\n\t_cache[ message ] = true;\n\n\twarn( ...params );\n\n}\n\nfunction probeAsync( gl, sync, interval ) {\n\n\treturn new Promise( function ( resolve, reject ) {\n\n\t\tfunction probe() {\n\n\t\t\tswitch ( gl.clientWaitSync( sync, gl.SYNC_FLUSH_COMMANDS_BIT, 0 ) ) {\n\n\t\t\t\tcase gl.WAIT_FAILED:\n\t\t\t\t\treject();\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase gl.TIMEOUT_EXPIRED:\n\t\t\t\t\tsetTimeout( probe, interval );\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\tresolve();\n\n\t\t\t}\n\n\t\t}\n\n\t\tsetTimeout( probe, interval );\n\n\t} );\n\n}\n\n/**\n * This modules allows to dispatch event objects on custom JavaScript objects.\n *\n * Main repository: [eventdispatcher.js](https://github.com/mrdoob/eventdispatcher.js/)\n *\n * Code Example:\n * ```js\n * class Car extends EventDispatcher {\n * \tstart() {\n *\t\tthis.dispatchEvent( { type: 'start', message: 'vroom vroom!' } );\n *\t}\n *};\n *\n * // Using events with the custom object\n * const car = new Car();\n * car.addEventListener( 'start', function ( event ) {\n * \talert( event.message );\n * } );\n *\n * car.start();\n * ```\n */\nclass EventDispatcher {\n\n\t/**\n\t * Adds the given event listener to the given event type.\n\t *\n\t * @param {string} type - The type of event to listen to.\n\t * @param {Function} listener - The function that gets called when the event is fired.\n\t */\n\taddEventListener( type, listener ) {\n\n\t\tif ( this._listeners === undefined ) this._listeners = {};\n\n\t\tconst listeners = this._listeners;\n\n\t\tif ( listeners[ type ] === undefined ) {\n\n\t\t\tlisteners[ type ] = [];\n\n\t\t}\n\n\t\tif ( listeners[ type ].indexOf( listener ) === -1 ) {\n\n\t\t\tlisteners[ type ].push( listener );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns `true` if the given event listener has been added to the given event type.\n\t *\n\t * @param {string} type - The type of event.\n\t * @param {Function} listener - The listener to check.\n\t * @return {boolean} Whether the given event listener has been added to the given event type.\n\t */\n\thasEventListener( type, listener ) {\n\n\t\tconst listeners = this._listeners;\n\n\t\tif ( listeners === undefined ) return false;\n\n\t\treturn listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== -1;\n\n\t}\n\n\t/**\n\t * Removes the given event listener from the given event type.\n\t *\n\t * @param {string} type - The type of event.\n\t * @param {Function} listener - The listener to remove.\n\t */\n\tremoveEventListener( type, listener ) {\n\n\t\tconst listeners = this._listeners;\n\n\t\tif ( listeners === undefined ) return;\n\n\t\tconst listenerArray = listeners[ type ];\n\n\t\tif ( listenerArray !== undefined ) {\n\n\t\t\tconst index = listenerArray.indexOf( listener );\n\n\t\t\tif ( index !== -1 ) {\n\n\t\t\t\tlistenerArray.splice( index, 1 );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Dispatches an event object.\n\t *\n\t * @param {Object} event - The event that gets fired.\n\t */\n\tdispatchEvent( event ) {\n\n\t\tconst listeners = this._listeners;\n\n\t\tif ( listeners === undefined ) return;\n\n\t\tconst listenerArray = listeners[ event.type ];\n\n\t\tif ( listenerArray !== undefined ) {\n\n\t\t\tevent.target = this;\n\n\t\t\t// Make a copy, in case listeners are removed while iterating.\n\t\t\tconst array = listenerArray.slice( 0 );\n\n\t\t\tfor ( let i = 0, l = array.length; i < l; i ++ ) {\n\n\t\t\t\tarray[ i ].call( this, event );\n\n\t\t\t}\n\n\t\t\tevent.target = null;\n\n\t\t}\n\n\t}\n\n}\n\nconst _lut = [ '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf', 'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df', 'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef', 'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff' ];\n\nlet _seed = 1234567;\n\n\nconst DEG2RAD = Math.PI / 180;\nconst RAD2DEG = 180 / Math.PI;\n\n/**\n * Generate a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier)\n * (universally unique identifier).\n *\n * @return {string} The UUID.\n */\nfunction generateUUID() {\n\n\t// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136\n\n\tconst d0 = Math.random() * 0xffffffff | 0;\n\tconst d1 = Math.random() * 0xffffffff | 0;\n\tconst d2 = Math.random() * 0xffffffff | 0;\n\tconst d3 = Math.random() * 0xffffffff | 0;\n\tconst uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' +\n\t\t\t_lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' +\n\t\t\t_lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] +\n\t\t\t_lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ];\n\n\t// .toLowerCase() here flattens concatenated strings to save heap memory space.\n\treturn uuid.toLowerCase();\n\n}\n\n/**\n * Clamps the given value between min and max.\n *\n * @param {number} value - The value to clamp.\n * @param {number} min - The min value.\n * @param {number} max - The max value.\n * @return {number} The clamped value.\n */\nfunction clamp( value, min, max ) {\n\n\treturn Math.max( min, Math.min( max, value ) );\n\n}\n\n/**\n * Computes the Euclidean modulo of the given parameters that\n * is `( ( n % m ) + m ) % m`.\n *\n * @param {number} n - The first parameter.\n * @param {number} m - The second parameter.\n * @return {number} The Euclidean modulo.\n */\nfunction euclideanModulo( n, m ) {\n\n\t// https://en.wikipedia.org/wiki/Modulo_operation\n\n\treturn ( ( n % m ) + m ) % m;\n\n}\n\n/**\n * Performs a linear mapping from range `<a1, a2>` to range `<b1, b2>`\n * for the given value.\n *\n * @param {number} x - The value to be mapped.\n * @param {number} a1 - Minimum value for range A.\n * @param {number} a2 - Maximum value for range A.\n * @param {number} b1 - Minimum value for range B.\n * @param {number} b2 - Maximum value for range B.\n * @return {number} The mapped value.\n */\nfunction mapLinear( x, a1, a2, b1, b2 ) {\n\n\treturn b1 + ( x - a1 ) * ( b2 - b1 ) / ( a2 - a1 );\n\n}\n\n/**\n * Returns the percentage in the closed interval `[0, 1]` of the given value\n * between the start and end point.\n *\n * @param {number} x - The start point\n * @param {number} y - The end point.\n * @param {number} value - A value between start and end.\n * @return {number} The interpolation factor.\n */\nfunction inverseLerp( x, y, value ) {\n\n\t// https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/inverse-lerp-a-super-useful-yet-often-overlooked-function-r5230/\n\n\tif ( x !== y ) {\n\n\t\treturn ( value - x ) / ( y - x );\n\n\t} else {\n\n\t\treturn 0;\n\n\t}\n\n}\n\n/**\n * Returns a value linearly interpolated from two known points based on the given interval -\n * `t = 0` will return `x` and `t = 1` will return `y`.\n *\n * @param {number} x - The start point\n * @param {number} y - The end point.\n * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.\n * @return {number} The interpolated value.\n */\nfunction lerp( x, y, t ) {\n\n\treturn ( 1 - t ) * x + t * y;\n\n}\n\n/**\n * Smoothly interpolate a number from `x` to `y` in  a spring-like manner using a delta\n * time to maintain frame rate independent movement. For details, see\n * [Frame rate independent damping using lerp](http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/).\n *\n * @param {number} x - The current point.\n * @param {number} y - The target point.\n * @param {number} lambda - A higher lambda value will make the movement more sudden,\n * and a lower value will make the movement more gradual.\n * @param {number} dt - Delta time in seconds.\n * @return {number} The interpolated value.\n */\nfunction damp( x, y, lambda, dt ) {\n\n\treturn lerp( x, y, 1 - Math.exp( - lambda * dt ) );\n\n}\n\n/**\n * Returns a value that alternates between `0` and the given `length` parameter.\n *\n * @param {number} x - The value to pingpong.\n * @param {number} [length=1] - The positive value the function will pingpong to.\n * @return {number} The alternated value.\n */\nfunction pingpong( x, length = 1 ) {\n\n\t// https://www.desmos.com/calculator/vcsjnyz7x4\n\n\treturn length - Math.abs( euclideanModulo( x, length * 2 ) - length );\n\n}\n\n/**\n * Returns a value in the range `[0,1]` that represents the percentage that `x` has\n * moved between `min` and `max`, but smoothed or slowed down the closer `x` is to\n * the `min` and `max`.\n *\n * See [Smoothstep](http://en.wikipedia.org/wiki/Smoothstep) for more details.\n *\n * @param {number} x - The value to evaluate based on its position between min and max.\n * @param {number} min - The min value. Any x value below min will be `0`.\n * @param {number} max - The max value. Any x value above max will be `1`.\n * @return {number} The alternated value.\n */\nfunction smoothstep( x, min, max ) {\n\n\tif ( x <= min ) return 0;\n\tif ( x >= max ) return 1;\n\n\tx = ( x - min ) / ( max - min );\n\n\treturn x * x * ( 3 - 2 * x );\n\n}\n\n/**\n * A [variation on smoothstep](https://en.wikipedia.org/wiki/Smoothstep#Variations)\n * that has zero 1st and 2nd order derivatives at x=0 and x=1.\n *\n * @param {number} x - The value to evaluate based on its position between min and max.\n * @param {number} min - The min value. Any x value below min will be `0`.\n * @param {number} max - The max value. Any x value above max will be `1`.\n * @return {number} The alternated value.\n */\nfunction smootherstep( x, min, max ) {\n\n\tif ( x <= min ) return 0;\n\tif ( x >= max ) return 1;\n\n\tx = ( x - min ) / ( max - min );\n\n\treturn x * x * x * ( x * ( x * 6 - 15 ) + 10 );\n\n}\n\n/**\n * Returns a random integer from `<low, high>` interval.\n *\n * @param {number} low - The lower value boundary.\n * @param {number} high - The upper value boundary\n * @return {number} A random integer.\n */\nfunction randInt( low, high ) {\n\n\treturn low + Math.floor( Math.random() * ( high - low + 1 ) );\n\n}\n\n/**\n * Returns a random float from `<low, high>` interval.\n *\n * @param {number} low - The lower value boundary.\n * @param {number} high - The upper value boundary\n * @return {number} A random float.\n */\nfunction randFloat( low, high ) {\n\n\treturn low + Math.random() * ( high - low );\n\n}\n\n/**\n * Returns a random integer from `<-range/2, range/2>` interval.\n *\n * @param {number} range - Defines the value range.\n * @return {number} A random float.\n */\nfunction randFloatSpread( range ) {\n\n\treturn range * ( 0.5 - Math.random() );\n\n}\n\n/**\n * Returns a deterministic pseudo-random float in the interval `[0, 1]`.\n *\n * @param {number} [s] - The integer seed.\n * @return {number} A random float.\n */\nfunction seededRandom( s ) {\n\n\tif ( s !== undefined ) _seed = s;\n\n\t// Mulberry32 generator\n\n\tlet t = _seed += 0x6D2B79F5;\n\n\tt = Math.imul( t ^ t >>> 15, t | 1 );\n\n\tt ^= t + Math.imul( t ^ t >>> 7, t | 61 );\n\n\treturn ( ( t ^ t >>> 14 ) >>> 0 ) / 4294967296;\n\n}\n\n/**\n * Converts degrees to radians.\n *\n * @param {number} degrees - A value in degrees.\n * @return {number} The converted value in radians.\n */\nfunction degToRad( degrees ) {\n\n\treturn degrees * DEG2RAD;\n\n}\n\n/**\n * Converts radians to degrees.\n *\n * @param {number} radians - A value in radians.\n * @return {number} The converted value in degrees.\n */\nfunction radToDeg( radians ) {\n\n\treturn radians * RAD2DEG;\n\n}\n\n/**\n * Returns `true` if the given number is a power of two.\n *\n * @param {number} value - The value to check.\n * @return {boolean} Whether the given number is a power of two or not.\n */\nfunction isPowerOfTwo( value ) {\n\n\treturn ( value & ( value - 1 ) ) === 0 && value !== 0;\n\n}\n\n/**\n * Returns the smallest power of two that is greater than or equal to the given number.\n *\n * @param {number} value - The value to find a POT for.\n * @return {number} The smallest power of two that is greater than or equal to the given number.\n */\nfunction ceilPowerOfTwo( value ) {\n\n\treturn Math.pow( 2, Math.ceil( Math.log( value ) / Math.LN2 ) );\n\n}\n\n/**\n * Returns the largest power of two that is less than or equal to the given number.\n *\n * @param {number} value - The value to find a POT for.\n * @return {number} The largest power of two that is less than or equal to the given number.\n */\nfunction floorPowerOfTwo( value ) {\n\n\treturn Math.pow( 2, Math.floor( Math.log( value ) / Math.LN2 ) );\n\n}\n\n/**\n * Sets the given quaternion from the [Intrinsic Proper Euler Angles](https://en.wikipedia.org/wiki/Euler_angles)\n * defined by the given angles and order.\n *\n * Rotations are applied to the axes in the order specified by order:\n * rotation by angle `a` is applied first, then by angle `b`, then by angle `c`.\n *\n * @param {Quaternion} q - The quaternion to set.\n * @param {number} a - The rotation applied to the first axis, in radians.\n * @param {number} b - The rotation applied to the second axis, in radians.\n * @param {number} c - The rotation applied to the third axis, in radians.\n * @param {('XYX'|'XZX'|'YXY'|'YZY'|'ZXZ'|'ZYZ')} order - A string specifying the axes order.\n */\nfunction setQuaternionFromProperEuler( q, a, b, c, order ) {\n\n\tconst cos = Math.cos;\n\tconst sin = Math.sin;\n\n\tconst c2 = cos( b / 2 );\n\tconst s2 = sin( b / 2 );\n\n\tconst c13 = cos( ( a + c ) / 2 );\n\tconst s13 = sin( ( a + c ) / 2 );\n\n\tconst c1_3 = cos( ( a - c ) / 2 );\n\tconst s1_3 = sin( ( a - c ) / 2 );\n\n\tconst c3_1 = cos( ( c - a ) / 2 );\n\tconst s3_1 = sin( ( c - a ) / 2 );\n\n\tswitch ( order ) {\n\n\t\tcase 'XYX':\n\t\t\tq.set( c2 * s13, s2 * c1_3, s2 * s1_3, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'YZY':\n\t\t\tq.set( s2 * s1_3, c2 * s13, s2 * c1_3, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'ZXZ':\n\t\t\tq.set( s2 * c1_3, s2 * s1_3, c2 * s13, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'XZX':\n\t\t\tq.set( c2 * s13, s2 * s3_1, s2 * c3_1, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'YXY':\n\t\t\tq.set( s2 * c3_1, c2 * s13, s2 * s3_1, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'ZYZ':\n\t\t\tq.set( s2 * s3_1, s2 * c3_1, c2 * s13, c2 * c13 );\n\t\t\tbreak;\n\n\t\tdefault:\n\t\t\twarn( 'MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: ' + order );\n\n\t}\n\n}\n\n/**\n * Denormalizes the given value according to the given typed array.\n *\n * @param {number} value - The value to denormalize.\n * @param {TypedArray} array - The typed array that defines the data type of the value.\n * @return {number} The denormalize (float) value in the range `[0,1]`.\n */\nfunction denormalize( value, array ) {\n\n\tswitch ( array.constructor ) {\n\n\t\tcase Float32Array:\n\n\t\t\treturn value;\n\n\t\tcase Uint32Array:\n\n\t\t\treturn value / 4294967295.0;\n\n\t\tcase Uint16Array:\n\n\t\t\treturn value / 65535.0;\n\n\t\tcase Uint8Array:\n\n\t\t\treturn value / 255.0;\n\n\t\tcase Int32Array:\n\n\t\t\treturn Math.max( value / 2147483647.0, -1 );\n\n\t\tcase Int16Array:\n\n\t\t\treturn Math.max( value / 32767.0, -1 );\n\n\t\tcase Int8Array:\n\n\t\t\treturn Math.max( value / 127.0, -1 );\n\n\t\tdefault:\n\n\t\t\tthrow new Error( 'Invalid component type.' );\n\n\t}\n\n}\n\n/**\n * Normalizes the given value according to the given typed array.\n *\n * @param {number} value - The float value in the range `[0,1]` to normalize.\n * @param {TypedArray} array - The typed array that defines the data type of the value.\n * @return {number} The normalize value.\n */\nfunction normalize( value, array ) {\n\n\tswitch ( array.constructor ) {\n\n\t\tcase Float32Array:\n\n\t\t\treturn value;\n\n\t\tcase Uint32Array:\n\n\t\t\treturn Math.round( value * 4294967295.0 );\n\n\t\tcase Uint16Array:\n\n\t\t\treturn Math.round( value * 65535.0 );\n\n\t\tcase Uint8Array:\n\n\t\t\treturn Math.round( value * 255.0 );\n\n\t\tcase Int32Array:\n\n\t\t\treturn Math.round( value * 2147483647.0 );\n\n\t\tcase Int16Array:\n\n\t\t\treturn Math.round( value * 32767.0 );\n\n\t\tcase Int8Array:\n\n\t\t\treturn Math.round( value * 127.0 );\n\n\t\tdefault:\n\n\t\t\tthrow new Error( 'Invalid component type.' );\n\n\t}\n\n}\n\n/**\n * @class\n * @classdesc A collection of math utility functions.\n * @hideconstructor\n */\nconst MathUtils = {\n\tDEG2RAD: DEG2RAD,\n\tRAD2DEG: RAD2DEG,\n\t/**\n\t * Generate a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier)\n\t * (universally unique identifier).\n\t *\n\t * @static\n\t * @method\n\t * @return {string} The UUID.\n\t */\n\tgenerateUUID: generateUUID,\n\t/**\n\t * Clamps the given value between min and max.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} value - The value to clamp.\n\t * @param {number} min - The min value.\n\t * @param {number} max - The max value.\n\t * @return {number} The clamped value.\n\t */\n\tclamp: clamp,\n\t/**\n\t * Computes the Euclidean modulo of the given parameters that\n\t * is `( ( n % m ) + m ) % m`.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} n - The first parameter.\n\t * @param {number} m - The second parameter.\n\t * @return {number} The Euclidean modulo.\n\t */\n\teuclideanModulo: euclideanModulo,\n\t/**\n\t * Performs a linear mapping from range `<a1, a2>` to range `<b1, b2>`\n\t * for the given value.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} x - The value to be mapped.\n\t * @param {number} a1 - Minimum value for range A.\n\t * @param {number} a2 - Maximum value for range A.\n\t * @param {number} b1 - Minimum value for range B.\n\t * @param {number} b2 - Maximum value for range B.\n\t * @return {number} The mapped value.\n\t */\n\tmapLinear: mapLinear,\n\t/**\n\t * Returns the percentage in the closed interval `[0, 1]` of the given value\n\t * between the start and end point.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} x - The start point\n\t * @param {number} y - The end point.\n\t * @param {number} value - A value between start and end.\n\t * @return {number} The interpolation factor.\n\t */\n\tinverseLerp: inverseLerp,\n\t/**\n\t * Returns a value linearly interpolated from two known points based on the given interval -\n\t * `t = 0` will return `x` and `t = 1` will return `y`.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} x - The start point\n\t * @param {number} y - The end point.\n\t * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.\n\t * @return {number} The interpolated value.\n\t */\n\tlerp: lerp,\n\t/**\n\t * Smoothly interpolate a number from `x` to `y` in  a spring-like manner using a delta\n\t * time to maintain frame rate independent movement. For details, see\n\t * [Frame rate independent damping using lerp](http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/).\n\t *\n\t * @static\n\t * @method\n\t * @param {number} x - The current point.\n\t * @param {number} y - The target point.\n\t * @param {number} lambda - A higher lambda value will make the movement more sudden,\n\t * and a lower value will make the movement more gradual.\n\t * @param {number} dt - Delta time in seconds.\n\t * @return {number} The interpolated value.\n\t */\n\tdamp: damp,\n\t/**\n\t * Returns a value that alternates between `0` and the given `length` parameter.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} x - The value to pingpong.\n\t * @param {number} [length=1] - The positive value the function will pingpong to.\n\t * @return {number} The alternated value.\n\t */\n\tpingpong: pingpong,\n\t/**\n\t * Returns a value in the range `[0,1]` that represents the percentage that `x` has\n\t * moved between `min` and `max`, but smoothed or slowed down the closer `x` is to\n\t * the `min` and `max`.\n\t *\n\t * See [Smoothstep](http://en.wikipedia.org/wiki/Smoothstep) for more details.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} x - The value to evaluate based on its position between min and max.\n\t * @param {number} min - The min value. Any x value below min will be `0`.\n\t * @param {number} max - The max value. Any x value above max will be `1`.\n\t * @return {number} The alternated value.\n\t */\n\tsmoothstep: smoothstep,\n\t/**\n\t * A [variation on smoothstep](https://en.wikipedia.org/wiki/Smoothstep#Variations)\n\t * that has zero 1st and 2nd order derivatives at x=0 and x=1.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} x - The value to evaluate based on its position between min and max.\n\t * @param {number} min - The min value. Any x value below min will be `0`.\n\t * @param {number} max - The max value. Any x value above max will be `1`.\n\t * @return {number} The alternated value.\n\t */\n\tsmootherstep: smootherstep,\n\t/**\n\t * Returns a random integer from `<low, high>` interval.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} low - The lower value boundary.\n\t * @param {number} high - The upper value boundary\n\t * @return {number} A random integer.\n\t */\n\trandInt: randInt,\n\t/**\n\t * Returns a random float from `<low, high>` interval.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} low - The lower value boundary.\n\t * @param {number} high - The upper value boundary\n\t * @return {number} A random float.\n\t */\n\trandFloat: randFloat,\n\t/**\n\t * Returns a random integer from `<-range/2, range/2>` interval.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} range - Defines the value range.\n\t * @return {number} A random float.\n\t */\n\trandFloatSpread: randFloatSpread,\n\t/**\n\t * Returns a deterministic pseudo-random float in the interval `[0, 1]`.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} [s] - The integer seed.\n\t * @return {number} A random float.\n\t */\n\tseededRandom: seededRandom,\n\t/**\n\t * Converts degrees to radians.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} degrees - A value in degrees.\n\t * @return {number} The converted value in radians.\n\t */\n\tdegToRad: degToRad,\n\t/**\n\t * Converts radians to degrees.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} radians - A value in radians.\n\t * @return {number} The converted value in degrees.\n\t */\n\tradToDeg: radToDeg,\n\t/**\n\t * Returns `true` if the given number is a power of two.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} value - The value to check.\n\t * @return {boolean} Whether the given number is a power of two or not.\n\t */\n\tisPowerOfTwo: isPowerOfTwo,\n\t/**\n\t * Returns the smallest power of two that is greater than or equal to the given number.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} value - The value to find a POT for.\n\t * @return {number} The smallest power of two that is greater than or equal to the given number.\n\t */\n\tceilPowerOfTwo: ceilPowerOfTwo,\n\t/**\n\t * Returns the largest power of two that is less than or equal to the given number.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} value - The value to find a POT for.\n\t * @return {number} The largest power of two that is less than or equal to the given number.\n\t */\n\tfloorPowerOfTwo: floorPowerOfTwo,\n\t/**\n\t * Sets the given quaternion from the [Intrinsic Proper Euler Angles](https://en.wikipedia.org/wiki/Euler_angles)\n\t * defined by the given angles and order.\n\t *\n\t * Rotations are applied to the axes in the order specified by order:\n\t * rotation by angle `a` is applied first, then by angle `b`, then by angle `c`.\n\t *\n\t * @static\n\t * @method\n\t * @param {Quaternion} q - The quaternion to set.\n\t * @param {number} a - The rotation applied to the first axis, in radians.\n\t * @param {number} b - The rotation applied to the second axis, in radians.\n\t * @param {number} c - The rotation applied to the third axis, in radians.\n\t * @param {('XYX'|'XZX'|'YXY'|'YZY'|'ZXZ'|'ZYZ')} order - A string specifying the axes order.\n\t */\n\tsetQuaternionFromProperEuler: setQuaternionFromProperEuler,\n\t/**\n\t * Normalizes the given value according to the given typed array.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} value - The float value in the range `[0,1]` to normalize.\n\t * @param {TypedArray} array - The typed array that defines the data type of the value.\n\t * @return {number} The normalize value.\n\t */\n\tnormalize: normalize,\n\t/**\n\t * Denormalizes the given value according to the given typed array.\n\t *\n\t * @static\n\t * @method\n\t * @param {number} value - The value to denormalize.\n\t * @param {TypedArray} array - The typed array that defines the data type of the value.\n\t * @return {number} The denormalize (float) value in the range `[0,1]`.\n\t */\n\tdenormalize: denormalize\n};\n\n/**\n * Class representing a 2D vector. A 2D vector is an ordered pair of numbers\n * (labeled x and y), which can be used to represent a number of things, such as:\n *\n * - A point in 2D space (i.e. a position on a plane).\n * - A direction and length across a plane. In three.js the length will\n * always be the Euclidean distance(straight-line distance) from `(0, 0)` to `(x, y)`\n * and the direction is also measured from `(0, 0)` towards `(x, y)`.\n * - Any arbitrary ordered pair of numbers.\n *\n * There are other things a 2D vector can be used to represent, such as\n * momentum vectors, complex numbers and so on, however these are the most\n * common uses in three.js.\n *\n * Iterating through a vector instance will yield its components `(x, y)` in\n * the corresponding order.\n * ```js\n * const a = new THREE.Vector2( 0, 1 );\n *\n * //no arguments; will be initialised to (0, 0)\n * const b = new THREE.Vector2( );\n *\n * const d = a.distanceTo( b );\n * ```\n */\nclass Vector2 {\n\n\t/**\n\t * Constructs a new 2D vector.\n\t *\n\t * @param {number} [x=0] - The x value of this vector.\n\t * @param {number} [y=0] - The y value of this vector.\n\t */\n\tconstructor( x = 0, y = 0 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tVector2.prototype.isVector2 = true;\n\n\t\t/**\n\t\t * The x value of this vector.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.x = x;\n\n\t\t/**\n\t\t * The y value of this vector.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.y = y;\n\n\t}\n\n\t/**\n\t * Alias for {@link Vector2#x}.\n\t *\n\t * @type {number}\n\t */\n\tget width() {\n\n\t\treturn this.x;\n\n\t}\n\n\tset width( value ) {\n\n\t\tthis.x = value;\n\n\t}\n\n\t/**\n\t * Alias for {@link Vector2#y}.\n\t *\n\t * @type {number}\n\t */\n\tget height() {\n\n\t\treturn this.y;\n\n\t}\n\n\tset height( value ) {\n\n\t\tthis.y = value;\n\n\t}\n\n\t/**\n\t * Sets the vector components.\n\t *\n\t * @param {number} x - The value of the x component.\n\t * @param {number} y - The value of the y component.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tset( x, y ) {\n\n\t\tthis.x = x;\n\t\tthis.y = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector components to the same value.\n\t *\n\t * @param {number} scalar - The value to set for all vector components.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tsetScalar( scalar ) {\n\n\t\tthis.x = scalar;\n\t\tthis.y = scalar;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector's x component to the given value\n\t *\n\t * @param {number} x - The value to set.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tsetX( x ) {\n\n\t\tthis.x = x;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector's y component to the given value\n\t *\n\t * @param {number} y - The value to set.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tsetY( y ) {\n\n\t\tthis.y = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Allows to set a vector component with an index.\n\t *\n\t * @param {number} index - The component index. `0` equals to x, `1` equals to y.\n\t * @param {number} value - The value to set.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tsetComponent( index, value ) {\n\n\t\tswitch ( index ) {\n\n\t\t\tcase 0: this.x = value; break;\n\t\t\tcase 1: this.y = value; break;\n\t\t\tdefault: throw new Error( 'index is out of range: ' + index );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the value of the vector component which matches the given index.\n\t *\n\t * @param {number} index - The component index. `0` equals to x, `1` equals to y.\n\t * @return {number} A vector component value.\n\t */\n\tgetComponent( index ) {\n\n\t\tswitch ( index ) {\n\n\t\t\tcase 0: return this.x;\n\t\t\tcase 1: return this.y;\n\t\t\tdefault: throw new Error( 'index is out of range: ' + index );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns a new vector with copied values from this instance.\n\t *\n\t * @return {Vector2} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor( this.x, this.y );\n\n\t}\n\n\t/**\n\t * Copies the values of the given vector to this instance.\n\t *\n\t * @param {Vector2} v - The vector to copy.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tcopy( v ) {\n\n\t\tthis.x = v.x;\n\t\tthis.y = v.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given vector to this instance.\n\t *\n\t * @param {Vector2} v - The vector to add.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tadd( v ) {\n\n\t\tthis.x += v.x;\n\t\tthis.y += v.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given scalar value to all components of this instance.\n\t *\n\t * @param {number} s - The scalar to add.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\taddScalar( s ) {\n\n\t\tthis.x += s;\n\t\tthis.y += s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given vectors and stores the result in this instance.\n\t *\n\t * @param {Vector2} a - The first vector.\n\t * @param {Vector2} b - The second vector.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\taddVectors( a, b ) {\n\n\t\tthis.x = a.x + b.x;\n\t\tthis.y = a.y + b.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given vector scaled by the given factor to this instance.\n\t *\n\t * @param {Vector2} v - The vector.\n\t * @param {number} s - The factor that scales `v`.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\taddScaledVector( v, s ) {\n\n\t\tthis.x += v.x * s;\n\t\tthis.y += v.y * s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the given vector from this instance.\n\t *\n\t * @param {Vector2} v - The vector to subtract.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tsub( v ) {\n\n\t\tthis.x -= v.x;\n\t\tthis.y -= v.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the given scalar value from all components of this instance.\n\t *\n\t * @param {number} s - The scalar to subtract.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tsubScalar( s ) {\n\n\t\tthis.x -= s;\n\t\tthis.y -= s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the given vectors and stores the result in this instance.\n\t *\n\t * @param {Vector2} a - The first vector.\n\t * @param {Vector2} b - The second vector.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tsubVectors( a, b ) {\n\n\t\tthis.x = a.x - b.x;\n\t\tthis.y = a.y - b.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the given vector with this instance.\n\t *\n\t * @param {Vector2} v - The vector to multiply.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tmultiply( v ) {\n\n\t\tthis.x *= v.x;\n\t\tthis.y *= v.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the given scalar value with all components of this instance.\n\t *\n\t * @param {number} scalar - The scalar to multiply.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tmultiplyScalar( scalar ) {\n\n\t\tthis.x *= scalar;\n\t\tthis.y *= scalar;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Divides this instance by the given vector.\n\t *\n\t * @param {Vector2} v - The vector to divide.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tdivide( v ) {\n\n\t\tthis.x /= v.x;\n\t\tthis.y /= v.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Divides this vector by the given scalar.\n\t *\n\t * @param {number} scalar - The scalar to divide.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tdivideScalar( scalar ) {\n\n\t\treturn this.multiplyScalar( 1 / scalar );\n\n\t}\n\n\t/**\n\t * Multiplies this vector (with an implicit 1 as the 3rd component) by\n\t * the given 3x3 matrix.\n\t *\n\t * @param {Matrix3} m - The matrix to apply.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tapplyMatrix3( m ) {\n\n\t\tconst x = this.x, y = this.y;\n\t\tconst e = m.elements;\n\n\t\tthis.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ];\n\t\tthis.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x or y value is greater than the given vector's x or y\n\t * value, replace that value with the corresponding min value.\n\t *\n\t * @param {Vector2} v - The vector.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tmin( v ) {\n\n\t\tthis.x = Math.min( this.x, v.x );\n\t\tthis.y = Math.min( this.y, v.y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x or y value is less than the given vector's x or y\n\t * value, replace that value with the corresponding max value.\n\t *\n\t * @param {Vector2} v - The vector.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tmax( v ) {\n\n\t\tthis.x = Math.max( this.x, v.x );\n\t\tthis.y = Math.max( this.y, v.y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x or y value is greater than the max vector's x or y\n\t * value, it is replaced by the corresponding value.\n\t * If this vector's x or y value is less than the min vector's x or y value,\n\t * it is replaced by the corresponding value.\n\t *\n\t * @param {Vector2} min - The minimum x and y values.\n\t * @param {Vector2} max - The maximum x and y values in the desired range.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tclamp( min, max ) {\n\n\t\t// assumes min < max, componentwise\n\n\t\tthis.x = clamp( this.x, min.x, max.x );\n\t\tthis.y = clamp( this.y, min.y, max.y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x or y values are greater than the max value, they are\n\t * replaced by the max value.\n\t * If this vector's x or y values are less than the min value, they are\n\t * replaced by the min value.\n\t *\n\t * @param {number} minVal - The minimum value the components will be clamped to.\n\t * @param {number} maxVal - The maximum value the components will be clamped to.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tclampScalar( minVal, maxVal ) {\n\n\t\tthis.x = clamp( this.x, minVal, maxVal );\n\t\tthis.y = clamp( this.y, minVal, maxVal );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's length is greater than the max value, it is replaced by\n\t * the max value.\n\t * If this vector's length is less than the min value, it is replaced by the\n\t * min value.\n\t *\n\t * @param {number} min - The minimum value the vector length will be clamped to.\n\t * @param {number} max - The maximum value the vector length will be clamped to.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tclampLength( min, max ) {\n\n\t\tconst length = this.length();\n\n\t\treturn this.divideScalar( length || 1 ).multiplyScalar( clamp( length, min, max ) );\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded down to the nearest integer value.\n\t *\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tfloor() {\n\n\t\tthis.x = Math.floor( this.x );\n\t\tthis.y = Math.floor( this.y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded up to the nearest integer value.\n\t *\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tceil() {\n\n\t\tthis.x = Math.ceil( this.x );\n\t\tthis.y = Math.ceil( this.y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded to the nearest integer value\n\t *\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tround() {\n\n\t\tthis.x = Math.round( this.x );\n\t\tthis.y = Math.round( this.y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded towards zero (up if negative,\n\t * down if positive) to an integer value.\n\t *\n\t * @return {Vector2} A reference to this vector.\n\t */\n\troundToZero() {\n\n\t\tthis.x = Math.trunc( this.x );\n\t\tthis.y = Math.trunc( this.y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Inverts this vector - i.e. sets x = -x and y = -y.\n\t *\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tnegate() {\n\n\t\tthis.x = - this.x;\n\t\tthis.y = - this.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Calculates the dot product of the given vector with this instance.\n\t *\n\t * @param {Vector2} v - The vector to compute the dot product with.\n\t * @return {number} The result of the dot product.\n\t */\n\tdot( v ) {\n\n\t\treturn this.x * v.x + this.y * v.y;\n\n\t}\n\n\t/**\n\t * Calculates the cross product of the given vector with this instance.\n\t *\n\t * @param {Vector2} v - The vector to compute the cross product with.\n\t * @return {number} The result of the cross product.\n\t */\n\tcross( v ) {\n\n\t\treturn this.x * v.y - this.y * v.x;\n\n\t}\n\n\t/**\n\t * Computes the square of the Euclidean length (straight-line length) from\n\t * (0, 0) to (x, y). If you are comparing the lengths of vectors, you should\n\t * compare the length squared instead as it is slightly more efficient to calculate.\n\t *\n\t * @return {number} The square length of this vector.\n\t */\n\tlengthSq() {\n\n\t\treturn this.x * this.x + this.y * this.y;\n\n\t}\n\n\t/**\n\t * Computes the  Euclidean length (straight-line length) from (0, 0) to (x, y).\n\t *\n\t * @return {number} The length of this vector.\n\t */\n\tlength() {\n\n\t\treturn Math.sqrt( this.x * this.x + this.y * this.y );\n\n\t}\n\n\t/**\n\t * Computes the Manhattan length of this vector.\n\t *\n\t * @return {number} The length of this vector.\n\t */\n\tmanhattanLength() {\n\n\t\treturn Math.abs( this.x ) + Math.abs( this.y );\n\n\t}\n\n\t/**\n\t * Converts this vector to a unit vector - that is, sets it equal to a vector\n\t * with the same direction as this one, but with a vector length of `1`.\n\t *\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tnormalize() {\n\n\t\treturn this.divideScalar( this.length() || 1 );\n\n\t}\n\n\t/**\n\t * Computes the angle in radians of this vector with respect to the positive x-axis.\n\t *\n\t * @return {number} The angle in radians.\n\t */\n\tangle() {\n\n\t\tconst angle = Math.atan2( - this.y, - this.x ) + Math.PI;\n\n\t\treturn angle;\n\n\t}\n\n\t/**\n\t * Returns the angle between the given vector and this instance in radians.\n\t *\n\t * @param {Vector2} v - The vector to compute the angle with.\n\t * @return {number} The angle in radians.\n\t */\n\tangleTo( v ) {\n\n\t\tconst denominator = Math.sqrt( this.lengthSq() * v.lengthSq() );\n\n\t\tif ( denominator === 0 ) return Math.PI / 2;\n\n\t\tconst theta = this.dot( v ) / denominator;\n\n\t\t// clamp, to handle numerical problems\n\n\t\treturn Math.acos( clamp( theta, -1, 1 ) );\n\n\t}\n\n\t/**\n\t * Computes the distance from the given vector to this instance.\n\t *\n\t * @param {Vector2} v - The vector to compute the distance to.\n\t * @return {number} The distance.\n\t */\n\tdistanceTo( v ) {\n\n\t\treturn Math.sqrt( this.distanceToSquared( v ) );\n\n\t}\n\n\t/**\n\t * Computes the squared distance from the given vector to this instance.\n\t * If you are just comparing the distance with another distance, you should compare\n\t * the distance squared instead as it is slightly more efficient to calculate.\n\t *\n\t * @param {Vector2} v - The vector to compute the squared distance to.\n\t * @return {number} The squared distance.\n\t */\n\tdistanceToSquared( v ) {\n\n\t\tconst dx = this.x - v.x, dy = this.y - v.y;\n\t\treturn dx * dx + dy * dy;\n\n\t}\n\n\t/**\n\t * Computes the Manhattan distance from the given vector to this instance.\n\t *\n\t * @param {Vector2} v - The vector to compute the Manhattan distance to.\n\t * @return {number} The Manhattan distance.\n\t */\n\tmanhattanDistanceTo( v ) {\n\n\t\treturn Math.abs( this.x - v.x ) + Math.abs( this.y - v.y );\n\n\t}\n\n\t/**\n\t * Sets this vector to a vector with the same direction as this one, but\n\t * with the specified length.\n\t *\n\t * @param {number} length - The new length of this vector.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tsetLength( length ) {\n\n\t\treturn this.normalize().multiplyScalar( length );\n\n\t}\n\n\t/**\n\t * Linearly interpolates between the given vector and this instance, where\n\t * alpha is the percent distance along the line - alpha = 0 will be this\n\t * vector, and alpha = 1 will be the given one.\n\t *\n\t * @param {Vector2} v - The vector to interpolate towards.\n\t * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tlerp( v, alpha ) {\n\n\t\tthis.x += ( v.x - this.x ) * alpha;\n\t\tthis.y += ( v.y - this.y ) * alpha;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Linearly interpolates between the given vectors, where alpha is the percent\n\t * distance along the line - alpha = 0 will be first vector, and alpha = 1 will\n\t * be the second one. The result is stored in this instance.\n\t *\n\t * @param {Vector2} v1 - The first vector.\n\t * @param {Vector2} v2 - The second vector.\n\t * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tlerpVectors( v1, v2, alpha ) {\n\n\t\tthis.x = v1.x + ( v2.x - v1.x ) * alpha;\n\t\tthis.y = v1.y + ( v2.y - v1.y ) * alpha;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this vector is equal with the given one.\n\t *\n\t * @param {Vector2} v - The vector to test for equality.\n\t * @return {boolean} Whether this vector is equal with the given one.\n\t */\n\tequals( v ) {\n\n\t\treturn ( ( v.x === this.x ) && ( v.y === this.y ) );\n\n\t}\n\n\t/**\n\t * Sets this vector's x value to be `array[ offset ]` and y\n\t * value to be `array[ offset + 1 ]`.\n\t *\n\t * @param {Array<number>} array - An array holding the vector component values.\n\t * @param {number} [offset=0] - The offset into the array.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tfromArray( array, offset = 0 ) {\n\n\t\tthis.x = array[ offset ];\n\t\tthis.y = array[ offset + 1 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Writes the components of this vector to the given array. If no array is provided,\n\t * the method returns a new instance.\n\t *\n\t * @param {Array<number>} [array=[]] - The target array holding the vector components.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Array<number>} The vector components.\n\t */\n\ttoArray( array = [], offset = 0 ) {\n\n\t\tarray[ offset ] = this.x;\n\t\tarray[ offset + 1 ] = this.y;\n\n\t\treturn array;\n\n\t}\n\n\t/**\n\t * Sets the components of this vector from the given buffer attribute.\n\t *\n\t * @param {BufferAttribute} attribute - The buffer attribute holding vector data.\n\t * @param {number} index - The index into the attribute.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\tfromBufferAttribute( attribute, index ) {\n\n\t\tthis.x = attribute.getX( index );\n\t\tthis.y = attribute.getY( index );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Rotates this vector around the given center by the given angle.\n\t *\n\t * @param {Vector2} center - The point around which to rotate.\n\t * @param {number} angle - The angle to rotate, in radians.\n\t * @return {Vector2} A reference to this vector.\n\t */\n\trotateAround( center, angle ) {\n\n\t\tconst c = Math.cos( angle ), s = Math.sin( angle );\n\n\t\tconst x = this.x - center.x;\n\t\tconst y = this.y - center.y;\n\n\t\tthis.x = x * c - y * s + center.x;\n\t\tthis.y = x * s + y * c + center.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets each component of this vector to a pseudo-random value between `0` and\n\t * `1`, excluding `1`.\n\t *\n\t * @return {Vector2} A reference to this vector.\n\t */\n\trandom() {\n\n\t\tthis.x = Math.random();\n\t\tthis.y = Math.random();\n\n\t\treturn this;\n\n\t}\n\n\t*[ Symbol.iterator ]() {\n\n\t\tyield this.x;\n\t\tyield this.y;\n\n\t}\n\n}\n\n/**\n * Class for representing a Quaternion. Quaternions are used in three.js to represent rotations.\n *\n * Iterating through a vector instance will yield its components `(x, y, z, w)` in\n * the corresponding order.\n *\n * Note that three.js expects Quaternions to be normalized.\n * ```js\n * const quaternion = new THREE.Quaternion();\n * quaternion.setFromAxisAngle( new THREE.Vector3( 0, 1, 0 ), Math.PI / 2 );\n *\n * const vector = new THREE.Vector3( 1, 0, 0 );\n * vector.applyQuaternion( quaternion );\n * ```\n */\nclass Quaternion {\n\n\t/**\n\t * Constructs a new quaternion.\n\t *\n\t * @param {number} [x=0] - The x value of this quaternion.\n\t * @param {number} [y=0] - The y value of this quaternion.\n\t * @param {number} [z=0] - The z value of this quaternion.\n\t * @param {number} [w=1] - The w value of this quaternion.\n\t */\n\tconstructor( x = 0, y = 0, z = 0, w = 1 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isQuaternion = true;\n\n\t\tthis._x = x;\n\t\tthis._y = y;\n\t\tthis._z = z;\n\t\tthis._w = w;\n\n\t}\n\n\t/**\n\t * Interpolates between two quaternions via SLERP. This implementation assumes the\n\t * quaternion data are managed in flat arrays.\n\t *\n\t * @param {Array<number>} dst - The destination array.\n\t * @param {number} dstOffset - An offset into the destination array.\n\t * @param {Array<number>} src0 - The source array of the first quaternion.\n\t * @param {number} srcOffset0 - An offset into the first source array.\n\t * @param {Array<number>} src1 -  The source array of the second quaternion.\n\t * @param {number} srcOffset1 - An offset into the second source array.\n\t * @param {number} t - The interpolation factor in the range `[0,1]`.\n\t * @see {@link Quaternion#slerp}\n\t */\n\tstatic slerpFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1, t ) {\n\n\t\tlet x0 = src0[ srcOffset0 + 0 ],\n\t\t\ty0 = src0[ srcOffset0 + 1 ],\n\t\t\tz0 = src0[ srcOffset0 + 2 ],\n\t\t\tw0 = src0[ srcOffset0 + 3 ];\n\n\t\tlet x1 = src1[ srcOffset1 + 0 ],\n\t\t\ty1 = src1[ srcOffset1 + 1 ],\n\t\t\tz1 = src1[ srcOffset1 + 2 ],\n\t\t\tw1 = src1[ srcOffset1 + 3 ];\n\n\t\tif ( t <= 0 ) {\n\n\t\t\tdst[ dstOffset + 0 ] = x0;\n\t\t\tdst[ dstOffset + 1 ] = y0;\n\t\t\tdst[ dstOffset + 2 ] = z0;\n\t\t\tdst[ dstOffset + 3 ] = w0;\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( t >= 1 ) {\n\n\t\t\tdst[ dstOffset + 0 ] = x1;\n\t\t\tdst[ dstOffset + 1 ] = y1;\n\t\t\tdst[ dstOffset + 2 ] = z1;\n\t\t\tdst[ dstOffset + 3 ] = w1;\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( w0 !== w1 || x0 !== x1 || y0 !== y1 || z0 !== z1 ) {\n\n\t\t\tlet dot = x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1;\n\n\t\t\tif ( dot < 0 ) {\n\n\t\t\t\tx1 = - x1;\n\t\t\t\ty1 = - y1;\n\t\t\t\tz1 = - z1;\n\t\t\t\tw1 = - w1;\n\n\t\t\t\tdot = - dot;\n\n\t\t\t}\n\n\t\t\tlet s = 1 - t;\n\n\t\t\tif ( dot < 0.9995 ) {\n\n\t\t\t\t// slerp\n\n\t\t\t\tconst theta = Math.acos( dot );\n\t\t\t\tconst sin = Math.sin( theta );\n\n\t\t\t\ts = Math.sin( s * theta ) / sin;\n\t\t\t\tt = Math.sin( t * theta ) / sin;\n\n\t\t\t\tx0 = x0 * s + x1 * t;\n\t\t\t\ty0 = y0 * s + y1 * t;\n\t\t\t\tz0 = z0 * s + z1 * t;\n\t\t\t\tw0 = w0 * s + w1 * t;\n\n\t\t\t} else {\n\n\t\t\t\t// for small angles, lerp then normalize\n\n\t\t\t\tx0 = x0 * s + x1 * t;\n\t\t\t\ty0 = y0 * s + y1 * t;\n\t\t\t\tz0 = z0 * s + z1 * t;\n\t\t\t\tw0 = w0 * s + w1 * t;\n\n\t\t\t\tconst f = 1 / Math.sqrt( x0 * x0 + y0 * y0 + z0 * z0 + w0 * w0 );\n\n\t\t\t\tx0 *= f;\n\t\t\t\ty0 *= f;\n\t\t\t\tz0 *= f;\n\t\t\t\tw0 *= f;\n\n\t\t\t}\n\n\t\t}\n\n\t\tdst[ dstOffset ] = x0;\n\t\tdst[ dstOffset + 1 ] = y0;\n\t\tdst[ dstOffset + 2 ] = z0;\n\t\tdst[ dstOffset + 3 ] = w0;\n\n\t}\n\n\t/**\n\t * Multiplies two quaternions. This implementation assumes the quaternion data are managed\n\t * in flat arrays.\n\t *\n\t * @param {Array<number>} dst - The destination array.\n\t * @param {number} dstOffset - An offset into the destination array.\n\t * @param {Array<number>} src0 - The source array of the first quaternion.\n\t * @param {number} srcOffset0 - An offset into the first source array.\n\t * @param {Array<number>} src1 -  The source array of the second quaternion.\n\t * @param {number} srcOffset1 - An offset into the second source array.\n\t * @return {Array<number>} The destination array.\n\t * @see {@link Quaternion#multiplyQuaternions}.\n\t */\n\tstatic multiplyQuaternionsFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1 ) {\n\n\t\tconst x0 = src0[ srcOffset0 ];\n\t\tconst y0 = src0[ srcOffset0 + 1 ];\n\t\tconst z0 = src0[ srcOffset0 + 2 ];\n\t\tconst w0 = src0[ srcOffset0 + 3 ];\n\n\t\tconst x1 = src1[ srcOffset1 ];\n\t\tconst y1 = src1[ srcOffset1 + 1 ];\n\t\tconst z1 = src1[ srcOffset1 + 2 ];\n\t\tconst w1 = src1[ srcOffset1 + 3 ];\n\n\t\tdst[ dstOffset ] = x0 * w1 + w0 * x1 + y0 * z1 - z0 * y1;\n\t\tdst[ dstOffset + 1 ] = y0 * w1 + w0 * y1 + z0 * x1 - x0 * z1;\n\t\tdst[ dstOffset + 2 ] = z0 * w1 + w0 * z1 + x0 * y1 - y0 * x1;\n\t\tdst[ dstOffset + 3 ] = w0 * w1 - x0 * x1 - y0 * y1 - z0 * z1;\n\n\t\treturn dst;\n\n\t}\n\n\t/**\n\t * The x value of this quaternion.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget x() {\n\n\t\treturn this._x;\n\n\t}\n\n\tset x( value ) {\n\n\t\tthis._x = value;\n\t\tthis._onChangeCallback();\n\n\t}\n\n\t/**\n\t * The y value of this quaternion.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget y() {\n\n\t\treturn this._y;\n\n\t}\n\n\tset y( value ) {\n\n\t\tthis._y = value;\n\t\tthis._onChangeCallback();\n\n\t}\n\n\t/**\n\t * The z value of this quaternion.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget z() {\n\n\t\treturn this._z;\n\n\t}\n\n\tset z( value ) {\n\n\t\tthis._z = value;\n\t\tthis._onChangeCallback();\n\n\t}\n\n\t/**\n\t * The w value of this quaternion.\n\t *\n\t * @type {number}\n\t * @default 1\n\t */\n\tget w() {\n\n\t\treturn this._w;\n\n\t}\n\n\tset w( value ) {\n\n\t\tthis._w = value;\n\t\tthis._onChangeCallback();\n\n\t}\n\n\t/**\n\t * Sets the quaternion components.\n\t *\n\t * @param {number} x - The x value of this quaternion.\n\t * @param {number} y - The y value of this quaternion.\n\t * @param {number} z - The z value of this quaternion.\n\t * @param {number} w - The w value of this quaternion.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tset( x, y, z, w ) {\n\n\t\tthis._x = x;\n\t\tthis._y = y;\n\t\tthis._z = z;\n\t\tthis._w = w;\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new quaternion with copied values from this instance.\n\t *\n\t * @return {Quaternion} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor( this._x, this._y, this._z, this._w );\n\n\t}\n\n\t/**\n\t * Copies the values of the given quaternion to this instance.\n\t *\n\t * @param {Quaternion} quaternion - The quaternion to copy.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tcopy( quaternion ) {\n\n\t\tthis._x = quaternion.x;\n\t\tthis._y = quaternion.y;\n\t\tthis._z = quaternion.z;\n\t\tthis._w = quaternion.w;\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this quaternion from the rotation specified by the given\n\t * Euler angles.\n\t *\n\t * @param {Euler} euler - The Euler angles.\n\t * @param {boolean} [update=true] - Whether the internal `onChange` callback should be executed or not.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tsetFromEuler( euler, update = true ) {\n\n\t\tconst x = euler._x, y = euler._y, z = euler._z, order = euler._order;\n\n\t\t// http://www.mathworks.com/matlabcentral/fileexchange/\n\t\t// \t20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/\n\t\t//\tcontent/SpinCalc.m\n\n\t\tconst cos = Math.cos;\n\t\tconst sin = Math.sin;\n\n\t\tconst c1 = cos( x / 2 );\n\t\tconst c2 = cos( y / 2 );\n\t\tconst c3 = cos( z / 2 );\n\n\t\tconst s1 = sin( x / 2 );\n\t\tconst s2 = sin( y / 2 );\n\t\tconst s3 = sin( z / 2 );\n\n\t\tswitch ( order ) {\n\n\t\t\tcase 'XYZ':\n\t\t\t\tthis._x = s1 * c2 * c3 + c1 * s2 * s3;\n\t\t\t\tthis._y = c1 * s2 * c3 - s1 * c2 * s3;\n\t\t\t\tthis._z = c1 * c2 * s3 + s1 * s2 * c3;\n\t\t\t\tthis._w = c1 * c2 * c3 - s1 * s2 * s3;\n\t\t\t\tbreak;\n\n\t\t\tcase 'YXZ':\n\t\t\t\tthis._x = s1 * c2 * c3 + c1 * s2 * s3;\n\t\t\t\tthis._y = c1 * s2 * c3 - s1 * c2 * s3;\n\t\t\t\tthis._z = c1 * c2 * s3 - s1 * s2 * c3;\n\t\t\t\tthis._w = c1 * c2 * c3 + s1 * s2 * s3;\n\t\t\t\tbreak;\n\n\t\t\tcase 'ZXY':\n\t\t\t\tthis._x = s1 * c2 * c3 - c1 * s2 * s3;\n\t\t\t\tthis._y = c1 * s2 * c3 + s1 * c2 * s3;\n\t\t\t\tthis._z = c1 * c2 * s3 + s1 * s2 * c3;\n\t\t\t\tthis._w = c1 * c2 * c3 - s1 * s2 * s3;\n\t\t\t\tbreak;\n\n\t\t\tcase 'ZYX':\n\t\t\t\tthis._x = s1 * c2 * c3 - c1 * s2 * s3;\n\t\t\t\tthis._y = c1 * s2 * c3 + s1 * c2 * s3;\n\t\t\t\tthis._z = c1 * c2 * s3 - s1 * s2 * c3;\n\t\t\t\tthis._w = c1 * c2 * c3 + s1 * s2 * s3;\n\t\t\t\tbreak;\n\n\t\t\tcase 'YZX':\n\t\t\t\tthis._x = s1 * c2 * c3 + c1 * s2 * s3;\n\t\t\t\tthis._y = c1 * s2 * c3 + s1 * c2 * s3;\n\t\t\t\tthis._z = c1 * c2 * s3 - s1 * s2 * c3;\n\t\t\t\tthis._w = c1 * c2 * c3 - s1 * s2 * s3;\n\t\t\t\tbreak;\n\n\t\t\tcase 'XZY':\n\t\t\t\tthis._x = s1 * c2 * c3 - c1 * s2 * s3;\n\t\t\t\tthis._y = c1 * s2 * c3 - s1 * c2 * s3;\n\t\t\t\tthis._z = c1 * c2 * s3 + s1 * s2 * c3;\n\t\t\t\tthis._w = c1 * c2 * c3 + s1 * s2 * s3;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\twarn( 'Quaternion: .setFromEuler() encountered an unknown order: ' + order );\n\n\t\t}\n\n\t\tif ( update === true ) this._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this quaternion from the given axis and angle.\n\t *\n\t * @param {Vector3} axis - The normalized axis.\n\t * @param {number} angle - The angle in radians.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tsetFromAxisAngle( axis, angle ) {\n\n\t\t// http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm\n\n\t\tconst halfAngle = angle / 2, s = Math.sin( halfAngle );\n\n\t\tthis._x = axis.x * s;\n\t\tthis._y = axis.y * s;\n\t\tthis._z = axis.z * s;\n\t\tthis._w = Math.cos( halfAngle );\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this quaternion from the given rotation matrix.\n\t *\n\t * @param {Matrix4} m - A 4x4 matrix of which the upper 3x3 of matrix is a pure rotation matrix (i.e. unscaled).\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tsetFromRotationMatrix( m ) {\n\n\t\t// http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm\n\n\t\t// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)\n\n\t\tconst te = m.elements,\n\n\t\t\tm11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ],\n\t\t\tm21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ],\n\t\t\tm31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ],\n\n\t\t\ttrace = m11 + m22 + m33;\n\n\t\tif ( trace > 0 ) {\n\n\t\t\tconst s = 0.5 / Math.sqrt( trace + 1.0 );\n\n\t\t\tthis._w = 0.25 / s;\n\t\t\tthis._x = ( m32 - m23 ) * s;\n\t\t\tthis._y = ( m13 - m31 ) * s;\n\t\t\tthis._z = ( m21 - m12 ) * s;\n\n\t\t} else if ( m11 > m22 && m11 > m33 ) {\n\n\t\t\tconst s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 );\n\n\t\t\tthis._w = ( m32 - m23 ) / s;\n\t\t\tthis._x = 0.25 * s;\n\t\t\tthis._y = ( m12 + m21 ) / s;\n\t\t\tthis._z = ( m13 + m31 ) / s;\n\n\t\t} else if ( m22 > m33 ) {\n\n\t\t\tconst s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 );\n\n\t\t\tthis._w = ( m13 - m31 ) / s;\n\t\t\tthis._x = ( m12 + m21 ) / s;\n\t\t\tthis._y = 0.25 * s;\n\t\t\tthis._z = ( m23 + m32 ) / s;\n\n\t\t} else {\n\n\t\t\tconst s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 );\n\n\t\t\tthis._w = ( m21 - m12 ) / s;\n\t\t\tthis._x = ( m13 + m31 ) / s;\n\t\t\tthis._y = ( m23 + m32 ) / s;\n\t\t\tthis._z = 0.25 * s;\n\n\t\t}\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this quaternion to the rotation required to rotate the direction vector\n\t * `vFrom` to the direction vector `vTo`.\n\t *\n\t * @param {Vector3} vFrom - The first (normalized) direction vector.\n\t * @param {Vector3} vTo - The second (normalized) direction vector.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tsetFromUnitVectors( vFrom, vTo ) {\n\n\t\t// assumes direction vectors vFrom and vTo are normalized\n\n\t\tlet r = vFrom.dot( vTo ) + 1;\n\n\t\tif ( r < 1e-8 ) { // the epsilon value has been discussed in #31286\n\n\t\t\t// vFrom and vTo point in opposite directions\n\n\t\t\tr = 0;\n\n\t\t\tif ( Math.abs( vFrom.x ) > Math.abs( vFrom.z ) ) {\n\n\t\t\t\tthis._x = - vFrom.y;\n\t\t\t\tthis._y = vFrom.x;\n\t\t\t\tthis._z = 0;\n\t\t\t\tthis._w = r;\n\n\t\t\t} else {\n\n\t\t\t\tthis._x = 0;\n\t\t\t\tthis._y = - vFrom.z;\n\t\t\t\tthis._z = vFrom.y;\n\t\t\t\tthis._w = r;\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// crossVectors( vFrom, vTo ); // inlined to avoid cyclic dependency on Vector3\n\n\t\t\tthis._x = vFrom.y * vTo.z - vFrom.z * vTo.y;\n\t\t\tthis._y = vFrom.z * vTo.x - vFrom.x * vTo.z;\n\t\t\tthis._z = vFrom.x * vTo.y - vFrom.y * vTo.x;\n\t\t\tthis._w = r;\n\n\t\t}\n\n\t\treturn this.normalize();\n\n\t}\n\n\t/**\n\t * Returns the angle between this quaternion and the given one in radians.\n\t *\n\t * @param {Quaternion} q - The quaternion to compute the angle with.\n\t * @return {number} The angle in radians.\n\t */\n\tangleTo( q ) {\n\n\t\treturn 2 * Math.acos( Math.abs( clamp( this.dot( q ), -1, 1 ) ) );\n\n\t}\n\n\t/**\n\t * Rotates this quaternion by a given angular step to the given quaternion.\n\t * The method ensures that the final quaternion will not overshoot `q`.\n\t *\n\t * @param {Quaternion} q - The target quaternion.\n\t * @param {number} step - The angular step in radians.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\trotateTowards( q, step ) {\n\n\t\tconst angle = this.angleTo( q );\n\n\t\tif ( angle === 0 ) return this;\n\n\t\tconst t = Math.min( 1, step / angle );\n\n\t\tthis.slerp( q, t );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this quaternion to the identity quaternion; that is, to the\n\t * quaternion that represents \"no rotation\".\n\t *\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tidentity() {\n\n\t\treturn this.set( 0, 0, 0, 1 );\n\n\t}\n\n\t/**\n\t * Inverts this quaternion via {@link Quaternion#conjugate}. The\n\t * quaternion is assumed to have unit length.\n\t *\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tinvert() {\n\n\t\treturn this.conjugate();\n\n\t}\n\n\t/**\n\t * Returns the rotational conjugate of this quaternion. The conjugate of a\n\t * quaternion represents the same rotation in the opposite direction about\n\t * the rotational axis.\n\t *\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tconjugate() {\n\n\t\tthis._x *= -1;\n\t\tthis._y *= -1;\n\t\tthis._z *= -1;\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Calculates the dot product of this quaternion and the given one.\n\t *\n\t * @param {Quaternion} v - The quaternion to compute the dot product with.\n\t * @return {number} The result of the dot product.\n\t */\n\tdot( v ) {\n\n\t\treturn this._x * v._x + this._y * v._y + this._z * v._z + this._w * v._w;\n\n\t}\n\n\t/**\n\t * Computes the squared Euclidean length (straight-line length) of this quaternion,\n\t * considered as a 4 dimensional vector. This can be useful if you are comparing the\n\t * lengths of two quaternions, as this is a slightly more efficient calculation than\n\t * {@link Quaternion#length}.\n\t *\n\t * @return {number} The squared Euclidean length.\n\t */\n\tlengthSq() {\n\n\t\treturn this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w;\n\n\t}\n\n\t/**\n\t * Computes the Euclidean length (straight-line length) of this quaternion,\n\t * considered as a 4 dimensional vector.\n\t *\n\t * @return {number} The Euclidean length.\n\t */\n\tlength() {\n\n\t\treturn Math.sqrt( this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w );\n\n\t}\n\n\t/**\n\t * Normalizes this quaternion - that is, calculated the quaternion that performs\n\t * the same rotation as this one, but has a length equal to `1`.\n\t *\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tnormalize() {\n\n\t\tlet l = this.length();\n\n\t\tif ( l === 0 ) {\n\n\t\t\tthis._x = 0;\n\t\t\tthis._y = 0;\n\t\t\tthis._z = 0;\n\t\t\tthis._w = 1;\n\n\t\t} else {\n\n\t\t\tl = 1 / l;\n\n\t\t\tthis._x = this._x * l;\n\t\t\tthis._y = this._y * l;\n\t\t\tthis._z = this._z * l;\n\t\t\tthis._w = this._w * l;\n\n\t\t}\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies this quaternion by the given one.\n\t *\n\t * @param {Quaternion} q - The quaternion.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tmultiply( q ) {\n\n\t\treturn this.multiplyQuaternions( this, q );\n\n\t}\n\n\t/**\n\t * Pre-multiplies this quaternion by the given one.\n\t *\n\t * @param {Quaternion} q - The quaternion.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tpremultiply( q ) {\n\n\t\treturn this.multiplyQuaternions( q, this );\n\n\t}\n\n\t/**\n\t * Multiplies the given quaternions and stores the result in this instance.\n\t *\n\t * @param {Quaternion} a - The first quaternion.\n\t * @param {Quaternion} b - The second quaternion.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tmultiplyQuaternions( a, b ) {\n\n\t\t// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm\n\n\t\tconst qax = a._x, qay = a._y, qaz = a._z, qaw = a._w;\n\t\tconst qbx = b._x, qby = b._y, qbz = b._z, qbw = b._w;\n\n\t\tthis._x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;\n\t\tthis._y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;\n\t\tthis._z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;\n\t\tthis._w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Performs a spherical linear interpolation between quaternions.\n\t *\n\t * @param {Quaternion} qb - The target quaternion.\n\t * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tslerp( qb, t ) {\n\n\t\tif ( t <= 0 ) return this;\n\n\t\tif ( t >= 1 ) return this.copy( qb ); // copy calls _onChangeCallback()\n\n\t\tlet x = qb._x, y = qb._y, z = qb._z, w = qb._w;\n\n\t\tlet dot = this.dot( qb );\n\n\t\tif ( dot < 0 ) {\n\n\t\t\tx = - x;\n\t\t\ty = - y;\n\t\t\tz = - z;\n\t\t\tw = - w;\n\n\t\t\tdot = - dot;\n\n\t\t}\n\n\t\tlet s = 1 - t;\n\n\t\tif ( dot < 0.9995 ) {\n\n\t\t\t// slerp\n\n\t\t\tconst theta = Math.acos( dot );\n\t\t\tconst sin = Math.sin( theta );\n\n\t\t\ts = Math.sin( s * theta ) / sin;\n\t\t\tt = Math.sin( t * theta ) / sin;\n\n\t\t\tthis._x = this._x * s + x * t;\n\t\t\tthis._y = this._y * s + y * t;\n\t\t\tthis._z = this._z * s + z * t;\n\t\t\tthis._w = this._w * s + w * t;\n\n\t\t\tthis._onChangeCallback();\n\n\t\t} else {\n\n\t\t\t// for small angles, lerp then normalize\n\n\t\t\tthis._x = this._x * s + x * t;\n\t\t\tthis._y = this._y * s + y * t;\n\t\t\tthis._z = this._z * s + z * t;\n\t\t\tthis._w = this._w * s + w * t;\n\n\t\t\tthis.normalize(); // normalize calls _onChangeCallback()\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Performs a spherical linear interpolation between the given quaternions\n\t * and stores the result in this quaternion.\n\t *\n\t * @param {Quaternion} qa - The source quaternion.\n\t * @param {Quaternion} qb - The target quaternion.\n\t * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tslerpQuaternions( qa, qb, t ) {\n\n\t\treturn this.copy( qa ).slerp( qb, t );\n\n\t}\n\n\t/**\n\t * Sets this quaternion to a uniformly random, normalized quaternion.\n\t *\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\trandom() {\n\n\t\t// Ken Shoemake\n\t\t// Uniform random rotations\n\t\t// D. Kirk, editor, Graphics Gems III, pages 124-132. Academic Press, New York, 1992.\n\n\t\tconst theta1 = 2 * Math.PI * Math.random();\n\t\tconst theta2 = 2 * Math.PI * Math.random();\n\n\t\tconst x0 = Math.random();\n\t\tconst r1 = Math.sqrt( 1 - x0 );\n\t\tconst r2 = Math.sqrt( x0 );\n\n\t\treturn this.set(\n\t\t\tr1 * Math.sin( theta1 ),\n\t\t\tr1 * Math.cos( theta1 ),\n\t\t\tr2 * Math.sin( theta2 ),\n\t\t\tr2 * Math.cos( theta2 ),\n\t\t);\n\n\t}\n\n\t/**\n\t * Returns `true` if this quaternion is equal with the given one.\n\t *\n\t * @param {Quaternion} quaternion - The quaternion to test for equality.\n\t * @return {boolean} Whether this quaternion is equal with the given one.\n\t */\n\tequals( quaternion ) {\n\n\t\treturn ( quaternion._x === this._x ) && ( quaternion._y === this._y ) && ( quaternion._z === this._z ) && ( quaternion._w === this._w );\n\n\t}\n\n\t/**\n\t * Sets this quaternion's components from the given array.\n\t *\n\t * @param {Array<number>} array - An array holding the quaternion component values.\n\t * @param {number} [offset=0] - The offset into the array.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tfromArray( array, offset = 0 ) {\n\n\t\tthis._x = array[ offset ];\n\t\tthis._y = array[ offset + 1 ];\n\t\tthis._z = array[ offset + 2 ];\n\t\tthis._w = array[ offset + 3 ];\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Writes the components of this quaternion to the given array. If no array is provided,\n\t * the method returns a new instance.\n\t *\n\t * @param {Array<number>} [array=[]] - The target array holding the quaternion components.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Array<number>} The quaternion components.\n\t */\n\ttoArray( array = [], offset = 0 ) {\n\n\t\tarray[ offset ] = this._x;\n\t\tarray[ offset + 1 ] = this._y;\n\t\tarray[ offset + 2 ] = this._z;\n\t\tarray[ offset + 3 ] = this._w;\n\n\t\treturn array;\n\n\t}\n\n\t/**\n\t * Sets the components of this quaternion from the given buffer attribute.\n\t *\n\t * @param {BufferAttribute} attribute - The buffer attribute holding quaternion data.\n\t * @param {number} index - The index into the attribute.\n\t * @return {Quaternion} A reference to this quaternion.\n\t */\n\tfromBufferAttribute( attribute, index ) {\n\n\t\tthis._x = attribute.getX( index );\n\t\tthis._y = attribute.getY( index );\n\t\tthis._z = attribute.getZ( index );\n\t\tthis._w = attribute.getW( index );\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * This methods defines the serialization result of this class. Returns the\n\t * numerical elements of this quaternion in an array of format `[x, y, z, w]`.\n\t *\n\t * @return {Array<number>} The serialized quaternion.\n\t */\n\ttoJSON() {\n\n\t\treturn this.toArray();\n\n\t}\n\n\t_onChange( callback ) {\n\n\t\tthis._onChangeCallback = callback;\n\n\t\treturn this;\n\n\t}\n\n\t_onChangeCallback() {}\n\n\t*[ Symbol.iterator ]() {\n\n\t\tyield this._x;\n\t\tyield this._y;\n\t\tyield this._z;\n\t\tyield this._w;\n\n\t}\n\n}\n\n/**\n * Class representing a 3D vector. A 3D vector is an ordered triplet of numbers\n * (labeled x, y and z), which can be used to represent a number of things, such as:\n *\n * - A point in 3D space.\n * - A direction and length in 3D space. In three.js the length will\n * always be the Euclidean distance(straight-line distance) from `(0, 0, 0)` to `(x, y, z)`\n * and the direction is also measured from `(0, 0, 0)` towards `(x, y, z)`.\n * - Any arbitrary ordered triplet of numbers.\n *\n * There are other things a 3D vector can be used to represent, such as\n * momentum vectors and so on, however these are the most\n * common uses in three.js.\n *\n * Iterating through a vector instance will yield its components `(x, y, z)` in\n * the corresponding order.\n * ```js\n * const a = new THREE.Vector3( 0, 1, 0 );\n *\n * //no arguments; will be initialised to (0, 0, 0)\n * const b = new THREE.Vector3( );\n *\n * const d = a.distanceTo( b );\n * ```\n */\nclass Vector3 {\n\n\t/**\n\t * Constructs a new 3D vector.\n\t *\n\t * @param {number} [x=0] - The x value of this vector.\n\t * @param {number} [y=0] - The y value of this vector.\n\t * @param {number} [z=0] - The z value of this vector.\n\t */\n\tconstructor( x = 0, y = 0, z = 0 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tVector3.prototype.isVector3 = true;\n\n\t\t/**\n\t\t * The x value of this vector.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.x = x;\n\n\t\t/**\n\t\t * The y value of this vector.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.y = y;\n\n\t\t/**\n\t\t * The z value of this vector.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.z = z;\n\n\t}\n\n\t/**\n\t * Sets the vector components.\n\t *\n\t * @param {number} x - The value of the x component.\n\t * @param {number} y - The value of the y component.\n\t * @param {number} z - The value of the z component.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tset( x, y, z ) {\n\n\t\tif ( z === undefined ) z = this.z; // sprite.scale.set(x,y)\n\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tthis.z = z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector components to the same value.\n\t *\n\t * @param {number} scalar - The value to set for all vector components.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetScalar( scalar ) {\n\n\t\tthis.x = scalar;\n\t\tthis.y = scalar;\n\t\tthis.z = scalar;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector's x component to the given value\n\t *\n\t * @param {number} x - The value to set.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetX( x ) {\n\n\t\tthis.x = x;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector's y component to the given value\n\t *\n\t * @param {number} y - The value to set.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetY( y ) {\n\n\t\tthis.y = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector's z component to the given value\n\t *\n\t * @param {number} z - The value to set.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetZ( z ) {\n\n\t\tthis.z = z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Allows to set a vector component with an index.\n\t *\n\t * @param {number} index - The component index. `0` equals to x, `1` equals to y, `2` equals to z.\n\t * @param {number} value - The value to set.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetComponent( index, value ) {\n\n\t\tswitch ( index ) {\n\n\t\t\tcase 0: this.x = value; break;\n\t\t\tcase 1: this.y = value; break;\n\t\t\tcase 2: this.z = value; break;\n\t\t\tdefault: throw new Error( 'index is out of range: ' + index );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the value of the vector component which matches the given index.\n\t *\n\t * @param {number} index - The component index. `0` equals to x, `1` equals to y, `2` equals to z.\n\t * @return {number} A vector component value.\n\t */\n\tgetComponent( index ) {\n\n\t\tswitch ( index ) {\n\n\t\t\tcase 0: return this.x;\n\t\t\tcase 1: return this.y;\n\t\t\tcase 2: return this.z;\n\t\t\tdefault: throw new Error( 'index is out of range: ' + index );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns a new vector with copied values from this instance.\n\t *\n\t * @return {Vector3} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor( this.x, this.y, this.z );\n\n\t}\n\n\t/**\n\t * Copies the values of the given vector to this instance.\n\t *\n\t * @param {Vector3} v - The vector to copy.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tcopy( v ) {\n\n\t\tthis.x = v.x;\n\t\tthis.y = v.y;\n\t\tthis.z = v.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given vector to this instance.\n\t *\n\t * @param {Vector3} v - The vector to add.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tadd( v ) {\n\n\t\tthis.x += v.x;\n\t\tthis.y += v.y;\n\t\tthis.z += v.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given scalar value to all components of this instance.\n\t *\n\t * @param {number} s - The scalar to add.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\taddScalar( s ) {\n\n\t\tthis.x += s;\n\t\tthis.y += s;\n\t\tthis.z += s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given vectors and stores the result in this instance.\n\t *\n\t * @param {Vector3} a - The first vector.\n\t * @param {Vector3} b - The second vector.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\taddVectors( a, b ) {\n\n\t\tthis.x = a.x + b.x;\n\t\tthis.y = a.y + b.y;\n\t\tthis.z = a.z + b.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given vector scaled by the given factor to this instance.\n\t *\n\t * @param {Vector3|Vector4} v - The vector.\n\t * @param {number} s - The factor that scales `v`.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\taddScaledVector( v, s ) {\n\n\t\tthis.x += v.x * s;\n\t\tthis.y += v.y * s;\n\t\tthis.z += v.z * s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the given vector from this instance.\n\t *\n\t * @param {Vector3} v - The vector to subtract.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsub( v ) {\n\n\t\tthis.x -= v.x;\n\t\tthis.y -= v.y;\n\t\tthis.z -= v.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the given scalar value from all components of this instance.\n\t *\n\t * @param {number} s - The scalar to subtract.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsubScalar( s ) {\n\n\t\tthis.x -= s;\n\t\tthis.y -= s;\n\t\tthis.z -= s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the given vectors and stores the result in this instance.\n\t *\n\t * @param {Vector3} a - The first vector.\n\t * @param {Vector3} b - The second vector.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsubVectors( a, b ) {\n\n\t\tthis.x = a.x - b.x;\n\t\tthis.y = a.y - b.y;\n\t\tthis.z = a.z - b.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the given vector with this instance.\n\t *\n\t * @param {Vector3} v - The vector to multiply.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tmultiply( v ) {\n\n\t\tthis.x *= v.x;\n\t\tthis.y *= v.y;\n\t\tthis.z *= v.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the given scalar value with all components of this instance.\n\t *\n\t * @param {number} scalar - The scalar to multiply.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tmultiplyScalar( scalar ) {\n\n\t\tthis.x *= scalar;\n\t\tthis.y *= scalar;\n\t\tthis.z *= scalar;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the given vectors and stores the result in this instance.\n\t *\n\t * @param {Vector3} a - The first vector.\n\t * @param {Vector3} b - The second vector.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tmultiplyVectors( a, b ) {\n\n\t\tthis.x = a.x * b.x;\n\t\tthis.y = a.y * b.y;\n\t\tthis.z = a.z * b.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Applies the given Euler rotation to this vector.\n\t *\n\t * @param {Euler} euler - The Euler angles.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tapplyEuler( euler ) {\n\n\t\treturn this.applyQuaternion( _quaternion$4.setFromEuler( euler ) );\n\n\t}\n\n\t/**\n\t * Applies a rotation specified by an axis and an angle to this vector.\n\t *\n\t * @param {Vector3} axis - A normalized vector representing the rotation axis.\n\t * @param {number} angle - The angle in radians.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tapplyAxisAngle( axis, angle ) {\n\n\t\treturn this.applyQuaternion( _quaternion$4.setFromAxisAngle( axis, angle ) );\n\n\t}\n\n\t/**\n\t * Multiplies this vector with the given 3x3 matrix.\n\t *\n\t * @param {Matrix3} m - The 3x3 matrix.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tapplyMatrix3( m ) {\n\n\t\tconst x = this.x, y = this.y, z = this.z;\n\t\tconst e = m.elements;\n\n\t\tthis.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ] * z;\n\t\tthis.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ] * z;\n\t\tthis.z = e[ 2 ] * x + e[ 5 ] * y + e[ 8 ] * z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies this vector by the given normal matrix and normalizes\n\t * the result.\n\t *\n\t * @param {Matrix3} m - The normal matrix.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tapplyNormalMatrix( m ) {\n\n\t\treturn this.applyMatrix3( m ).normalize();\n\n\t}\n\n\t/**\n\t * Multiplies this vector (with an implicit 1 in the 4th dimension) by m, and\n\t * divides by perspective.\n\t *\n\t * @param {Matrix4} m - The matrix to apply.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tapplyMatrix4( m ) {\n\n\t\tconst x = this.x, y = this.y, z = this.z;\n\t\tconst e = m.elements;\n\n\t\tconst w = 1 / ( e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] );\n\n\t\tthis.x = ( e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] ) * w;\n\t\tthis.y = ( e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] ) * w;\n\t\tthis.z = ( e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] ) * w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Applies the given Quaternion to this vector.\n\t *\n\t * @param {Quaternion} q - The Quaternion.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tapplyQuaternion( q ) {\n\n\t\t// quaternion q is assumed to have unit length\n\n\t\tconst vx = this.x, vy = this.y, vz = this.z;\n\t\tconst qx = q.x, qy = q.y, qz = q.z, qw = q.w;\n\n\t\t// t = 2 * cross( q.xyz, v );\n\t\tconst tx = 2 * ( qy * vz - qz * vy );\n\t\tconst ty = 2 * ( qz * vx - qx * vz );\n\t\tconst tz = 2 * ( qx * vy - qy * vx );\n\n\t\t// v + q.w * t + cross( q.xyz, t );\n\t\tthis.x = vx + qw * tx + qy * tz - qz * ty;\n\t\tthis.y = vy + qw * ty + qz * tx - qx * tz;\n\t\tthis.z = vz + qw * tz + qx * ty - qy * tx;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Projects this vector from world space into the camera's normalized\n\t * device coordinate (NDC) space.\n\t *\n\t * @param {Camera} camera - The camera.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tproject( camera ) {\n\n\t\treturn this.applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix );\n\n\t}\n\n\t/**\n\t * Unprojects this vector from the camera's normalized device coordinate (NDC)\n\t * space into world space.\n\t *\n\t * @param {Camera} camera - The camera.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tunproject( camera ) {\n\n\t\treturn this.applyMatrix4( camera.projectionMatrixInverse ).applyMatrix4( camera.matrixWorld );\n\n\t}\n\n\t/**\n\t * Transforms the direction of this vector by a matrix (the upper left 3 x 3\n\t * subset of the given 4x4 matrix and then normalizes the result.\n\t *\n\t * @param {Matrix4} m - The matrix.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\ttransformDirection( m ) {\n\n\t\t// input: THREE.Matrix4 affine matrix\n\t\t// vector interpreted as a direction\n\n\t\tconst x = this.x, y = this.y, z = this.z;\n\t\tconst e = m.elements;\n\n\t\tthis.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z;\n\t\tthis.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z;\n\t\tthis.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z;\n\n\t\treturn this.normalize();\n\n\t}\n\n\t/**\n\t * Divides this instance by the given vector.\n\t *\n\t * @param {Vector3} v - The vector to divide.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tdivide( v ) {\n\n\t\tthis.x /= v.x;\n\t\tthis.y /= v.y;\n\t\tthis.z /= v.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Divides this vector by the given scalar.\n\t *\n\t * @param {number} scalar - The scalar to divide.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tdivideScalar( scalar ) {\n\n\t\treturn this.multiplyScalar( 1 / scalar );\n\n\t}\n\n\t/**\n\t * If this vector's x, y or z value is greater than the given vector's x, y or z\n\t * value, replace that value with the corresponding min value.\n\t *\n\t * @param {Vector3} v - The vector.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tmin( v ) {\n\n\t\tthis.x = Math.min( this.x, v.x );\n\t\tthis.y = Math.min( this.y, v.y );\n\t\tthis.z = Math.min( this.z, v.z );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x, y or z value is less than the given vector's x, y or z\n\t * value, replace that value with the corresponding max value.\n\t *\n\t * @param {Vector3} v - The vector.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tmax( v ) {\n\n\t\tthis.x = Math.max( this.x, v.x );\n\t\tthis.y = Math.max( this.y, v.y );\n\t\tthis.z = Math.max( this.z, v.z );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x, y or z value is greater than the max vector's x, y or z\n\t * value, it is replaced by the corresponding value.\n\t * If this vector's x, y or z value is less than the min vector's x, y or z value,\n\t * it is replaced by the corresponding value.\n\t *\n\t * @param {Vector3} min - The minimum x, y and z values.\n\t * @param {Vector3} max - The maximum x, y and z values in the desired range.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tclamp( min, max ) {\n\n\t\t// assumes min < max, componentwise\n\n\t\tthis.x = clamp( this.x, min.x, max.x );\n\t\tthis.y = clamp( this.y, min.y, max.y );\n\t\tthis.z = clamp( this.z, min.z, max.z );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x, y or z values are greater than the max value, they are\n\t * replaced by the max value.\n\t * If this vector's x, y or z values are less than the min value, they are\n\t * replaced by the min value.\n\t *\n\t * @param {number} minVal - The minimum value the components will be clamped to.\n\t * @param {number} maxVal - The maximum value the components will be clamped to.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tclampScalar( minVal, maxVal ) {\n\n\t\tthis.x = clamp( this.x, minVal, maxVal );\n\t\tthis.y = clamp( this.y, minVal, maxVal );\n\t\tthis.z = clamp( this.z, minVal, maxVal );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's length is greater than the max value, it is replaced by\n\t * the max value.\n\t * If this vector's length is less than the min value, it is replaced by the\n\t * min value.\n\t *\n\t * @param {number} min - The minimum value the vector length will be clamped to.\n\t * @param {number} max - The maximum value the vector length will be clamped to.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tclampLength( min, max ) {\n\n\t\tconst length = this.length();\n\n\t\treturn this.divideScalar( length || 1 ).multiplyScalar( clamp( length, min, max ) );\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded down to the nearest integer value.\n\t *\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tfloor() {\n\n\t\tthis.x = Math.floor( this.x );\n\t\tthis.y = Math.floor( this.y );\n\t\tthis.z = Math.floor( this.z );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded up to the nearest integer value.\n\t *\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tceil() {\n\n\t\tthis.x = Math.ceil( this.x );\n\t\tthis.y = Math.ceil( this.y );\n\t\tthis.z = Math.ceil( this.z );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded to the nearest integer value\n\t *\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tround() {\n\n\t\tthis.x = Math.round( this.x );\n\t\tthis.y = Math.round( this.y );\n\t\tthis.z = Math.round( this.z );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded towards zero (up if negative,\n\t * down if positive) to an integer value.\n\t *\n\t * @return {Vector3} A reference to this vector.\n\t */\n\troundToZero() {\n\n\t\tthis.x = Math.trunc( this.x );\n\t\tthis.y = Math.trunc( this.y );\n\t\tthis.z = Math.trunc( this.z );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Inverts this vector - i.e. sets x = -x, y = -y and z = -z.\n\t *\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tnegate() {\n\n\t\tthis.x = - this.x;\n\t\tthis.y = - this.y;\n\t\tthis.z = - this.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Calculates the dot product of the given vector with this instance.\n\t *\n\t * @param {Vector3} v - The vector to compute the dot product with.\n\t * @return {number} The result of the dot product.\n\t */\n\tdot( v ) {\n\n\t\treturn this.x * v.x + this.y * v.y + this.z * v.z;\n\n\t}\n\n\t/**\n\t * Computes the square of the Euclidean length (straight-line length) from\n\t * (0, 0, 0) to (x, y, z). If you are comparing the lengths of vectors, you should\n\t * compare the length squared instead as it is slightly more efficient to calculate.\n\t *\n\t * @return {number} The square length of this vector.\n\t */\n\tlengthSq() {\n\n\t\treturn this.x * this.x + this.y * this.y + this.z * this.z;\n\n\t}\n\n\t/**\n\t * Computes the  Euclidean length (straight-line length) from (0, 0, 0) to (x, y, z).\n\t *\n\t * @return {number} The length of this vector.\n\t */\n\tlength() {\n\n\t\treturn Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );\n\n\t}\n\n\t/**\n\t * Computes the Manhattan length of this vector.\n\t *\n\t * @return {number} The length of this vector.\n\t */\n\tmanhattanLength() {\n\n\t\treturn Math.abs( this.x ) + Math.abs( this.y ) + Math.abs( this.z );\n\n\t}\n\n\t/**\n\t * Converts this vector to a unit vector - that is, sets it equal to a vector\n\t * with the same direction as this one, but with a vector length of `1`.\n\t *\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tnormalize() {\n\n\t\treturn this.divideScalar( this.length() || 1 );\n\n\t}\n\n\t/**\n\t * Sets this vector to a vector with the same direction as this one, but\n\t * with the specified length.\n\t *\n\t * @param {number} length - The new length of this vector.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetLength( length ) {\n\n\t\treturn this.normalize().multiplyScalar( length );\n\n\t}\n\n\t/**\n\t * Linearly interpolates between the given vector and this instance, where\n\t * alpha is the percent distance along the line - alpha = 0 will be this\n\t * vector, and alpha = 1 will be the given one.\n\t *\n\t * @param {Vector3} v - The vector to interpolate towards.\n\t * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tlerp( v, alpha ) {\n\n\t\tthis.x += ( v.x - this.x ) * alpha;\n\t\tthis.y += ( v.y - this.y ) * alpha;\n\t\tthis.z += ( v.z - this.z ) * alpha;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Linearly interpolates between the given vectors, where alpha is the percent\n\t * distance along the line - alpha = 0 will be first vector, and alpha = 1 will\n\t * be the second one. The result is stored in this instance.\n\t *\n\t * @param {Vector3} v1 - The first vector.\n\t * @param {Vector3} v2 - The second vector.\n\t * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tlerpVectors( v1, v2, alpha ) {\n\n\t\tthis.x = v1.x + ( v2.x - v1.x ) * alpha;\n\t\tthis.y = v1.y + ( v2.y - v1.y ) * alpha;\n\t\tthis.z = v1.z + ( v2.z - v1.z ) * alpha;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Calculates the cross product of the given vector with this instance.\n\t *\n\t * @param {Vector3} v - The vector to compute the cross product with.\n\t * @return {Vector3} The result of the cross product.\n\t */\n\tcross( v ) {\n\n\t\treturn this.crossVectors( this, v );\n\n\t}\n\n\t/**\n\t * Calculates the cross product of the given vectors and stores the result\n\t * in this instance.\n\t *\n\t * @param {Vector3} a - The first vector.\n\t * @param {Vector3} b - The second vector.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tcrossVectors( a, b ) {\n\n\t\tconst ax = a.x, ay = a.y, az = a.z;\n\t\tconst bx = b.x, by = b.y, bz = b.z;\n\n\t\tthis.x = ay * bz - az * by;\n\t\tthis.y = az * bx - ax * bz;\n\t\tthis.z = ax * by - ay * bx;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Projects this vector onto the given one.\n\t *\n\t * @param {Vector3} v - The vector to project to.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tprojectOnVector( v ) {\n\n\t\tconst denominator = v.lengthSq();\n\n\t\tif ( denominator === 0 ) return this.set( 0, 0, 0 );\n\n\t\tconst scalar = v.dot( this ) / denominator;\n\n\t\treturn this.copy( v ).multiplyScalar( scalar );\n\n\t}\n\n\t/**\n\t * Projects this vector onto a plane by subtracting this\n\t * vector projected onto the plane's normal from this vector.\n\t *\n\t * @param {Vector3} planeNormal - The plane normal.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tprojectOnPlane( planeNormal ) {\n\n\t\t_vector$c.copy( this ).projectOnVector( planeNormal );\n\n\t\treturn this.sub( _vector$c );\n\n\t}\n\n\t/**\n\t * Reflects this vector off a plane orthogonal to the given normal vector.\n\t *\n\t * @param {Vector3} normal - The (normalized) normal vector.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\treflect( normal ) {\n\n\t\treturn this.sub( _vector$c.copy( normal ).multiplyScalar( 2 * this.dot( normal ) ) );\n\n\t}\n\t/**\n\t * Returns the angle between the given vector and this instance in radians.\n\t *\n\t * @param {Vector3} v - The vector to compute the angle with.\n\t * @return {number} The angle in radians.\n\t */\n\tangleTo( v ) {\n\n\t\tconst denominator = Math.sqrt( this.lengthSq() * v.lengthSq() );\n\n\t\tif ( denominator === 0 ) return Math.PI / 2;\n\n\t\tconst theta = this.dot( v ) / denominator;\n\n\t\t// clamp, to handle numerical problems\n\n\t\treturn Math.acos( clamp( theta, -1, 1 ) );\n\n\t}\n\n\t/**\n\t * Computes the distance from the given vector to this instance.\n\t *\n\t * @param {Vector3} v - The vector to compute the distance to.\n\t * @return {number} The distance.\n\t */\n\tdistanceTo( v ) {\n\n\t\treturn Math.sqrt( this.distanceToSquared( v ) );\n\n\t}\n\n\t/**\n\t * Computes the squared distance from the given vector to this instance.\n\t * If you are just comparing the distance with another distance, you should compare\n\t * the distance squared instead as it is slightly more efficient to calculate.\n\t *\n\t * @param {Vector3} v - The vector to compute the squared distance to.\n\t * @return {number} The squared distance.\n\t */\n\tdistanceToSquared( v ) {\n\n\t\tconst dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z;\n\n\t\treturn dx * dx + dy * dy + dz * dz;\n\n\t}\n\n\t/**\n\t * Computes the Manhattan distance from the given vector to this instance.\n\t *\n\t * @param {Vector3} v - The vector to compute the Manhattan distance to.\n\t * @return {number} The Manhattan distance.\n\t */\n\tmanhattanDistanceTo( v ) {\n\n\t\treturn Math.abs( this.x - v.x ) + Math.abs( this.y - v.y ) + Math.abs( this.z - v.z );\n\n\t}\n\n\t/**\n\t * Sets the vector components from the given spherical coordinates.\n\t *\n\t * @param {Spherical} s - The spherical coordinates.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromSpherical( s ) {\n\n\t\treturn this.setFromSphericalCoords( s.radius, s.phi, s.theta );\n\n\t}\n\n\t/**\n\t * Sets the vector components from the given spherical coordinates.\n\t *\n\t * @param {number} radius - The radius.\n\t * @param {number} phi - The phi angle in radians.\n\t * @param {number} theta - The theta angle in radians.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromSphericalCoords( radius, phi, theta ) {\n\n\t\tconst sinPhiRadius = Math.sin( phi ) * radius;\n\n\t\tthis.x = sinPhiRadius * Math.sin( theta );\n\t\tthis.y = Math.cos( phi ) * radius;\n\t\tthis.z = sinPhiRadius * Math.cos( theta );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector components from the given cylindrical coordinates.\n\t *\n\t * @param {Cylindrical} c - The cylindrical coordinates.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromCylindrical( c ) {\n\n\t\treturn this.setFromCylindricalCoords( c.radius, c.theta, c.y );\n\n\t}\n\n\t/**\n\t * Sets the vector components from the given cylindrical coordinates.\n\t *\n\t * @param {number} radius - The radius.\n\t * @param {number} theta - The theta angle in radians.\n\t * @param {number} y - The y value.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromCylindricalCoords( radius, theta, y ) {\n\n\t\tthis.x = radius * Math.sin( theta );\n\t\tthis.y = y;\n\t\tthis.z = radius * Math.cos( theta );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector components to the position elements of the\n\t * given transformation matrix.\n\t *\n\t * @param {Matrix4} m - The 4x4 matrix.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromMatrixPosition( m ) {\n\n\t\tconst e = m.elements;\n\n\t\tthis.x = e[ 12 ];\n\t\tthis.y = e[ 13 ];\n\t\tthis.z = e[ 14 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector components to the scale elements of the\n\t * given transformation matrix.\n\t *\n\t * @param {Matrix4} m - The 4x4 matrix.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromMatrixScale( m ) {\n\n\t\tconst sx = this.setFromMatrixColumn( m, 0 ).length();\n\t\tconst sy = this.setFromMatrixColumn( m, 1 ).length();\n\t\tconst sz = this.setFromMatrixColumn( m, 2 ).length();\n\n\t\tthis.x = sx;\n\t\tthis.y = sy;\n\t\tthis.z = sz;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector components from the specified matrix column.\n\t *\n\t * @param {Matrix4} m - The 4x4 matrix.\n\t * @param {number} index - The column index.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromMatrixColumn( m, index ) {\n\n\t\treturn this.fromArray( m.elements, index * 4 );\n\n\t}\n\n\t/**\n\t * Sets the vector components from the specified matrix column.\n\t *\n\t * @param {Matrix3} m - The 3x3 matrix.\n\t * @param {number} index - The column index.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromMatrix3Column( m, index ) {\n\n\t\treturn this.fromArray( m.elements, index * 3 );\n\n\t}\n\n\t/**\n\t * Sets the vector components from the given Euler angles.\n\t *\n\t * @param {Euler} e - The Euler angles to set.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromEuler( e ) {\n\n\t\tthis.x = e._x;\n\t\tthis.y = e._y;\n\t\tthis.z = e._z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector components from the RGB components of the\n\t * given color.\n\t *\n\t * @param {Color} c - The color to set.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tsetFromColor( c ) {\n\n\t\tthis.x = c.r;\n\t\tthis.y = c.g;\n\t\tthis.z = c.b;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this vector is equal with the given one.\n\t *\n\t * @param {Vector3} v - The vector to test for equality.\n\t * @return {boolean} Whether this vector is equal with the given one.\n\t */\n\tequals( v ) {\n\n\t\treturn ( ( v.x === this.x ) && ( v.y === this.y ) && ( v.z === this.z ) );\n\n\t}\n\n\t/**\n\t * Sets this vector's x value to be `array[ offset ]`, y value to be `array[ offset + 1 ]`\n\t * and z value to be `array[ offset + 2 ]`.\n\t *\n\t * @param {Array<number>} array - An array holding the vector component values.\n\t * @param {number} [offset=0] - The offset into the array.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tfromArray( array, offset = 0 ) {\n\n\t\tthis.x = array[ offset ];\n\t\tthis.y = array[ offset + 1 ];\n\t\tthis.z = array[ offset + 2 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Writes the components of this vector to the given array. If no array is provided,\n\t * the method returns a new instance.\n\t *\n\t * @param {Array<number>} [array=[]] - The target array holding the vector components.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Array<number>} The vector components.\n\t */\n\ttoArray( array = [], offset = 0 ) {\n\n\t\tarray[ offset ] = this.x;\n\t\tarray[ offset + 1 ] = this.y;\n\t\tarray[ offset + 2 ] = this.z;\n\n\t\treturn array;\n\n\t}\n\n\t/**\n\t * Sets the components of this vector from the given buffer attribute.\n\t *\n\t * @param {BufferAttribute} attribute - The buffer attribute holding vector data.\n\t * @param {number} index - The index into the attribute.\n\t * @return {Vector3} A reference to this vector.\n\t */\n\tfromBufferAttribute( attribute, index ) {\n\n\t\tthis.x = attribute.getX( index );\n\t\tthis.y = attribute.getY( index );\n\t\tthis.z = attribute.getZ( index );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets each component of this vector to a pseudo-random value between `0` and\n\t * `1`, excluding `1`.\n\t *\n\t * @return {Vector3} A reference to this vector.\n\t */\n\trandom() {\n\n\t\tthis.x = Math.random();\n\t\tthis.y = Math.random();\n\t\tthis.z = Math.random();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this vector to a uniformly random point on a unit sphere.\n\t *\n\t * @return {Vector3} A reference to this vector.\n\t */\n\trandomDirection() {\n\n\t\t// https://mathworld.wolfram.com/SpherePointPicking.html\n\n\t\tconst theta = Math.random() * Math.PI * 2;\n\t\tconst u = Math.random() * 2 - 1;\n\t\tconst c = Math.sqrt( 1 - u * u );\n\n\t\tthis.x = c * Math.cos( theta );\n\t\tthis.y = u;\n\t\tthis.z = c * Math.sin( theta );\n\n\t\treturn this;\n\n\t}\n\n\t*[ Symbol.iterator ]() {\n\n\t\tyield this.x;\n\t\tyield this.y;\n\t\tyield this.z;\n\n\t}\n\n}\n\nconst _vector$c = /*@__PURE__*/ new Vector3();\nconst _quaternion$4 = /*@__PURE__*/ new Quaternion();\n\n/**\n * Represents a 3x3 matrix.\n *\n * A Note on Row-Major and Column-Major Ordering:\n *\n * The constructor and {@link Matrix3#set} method take arguments in\n * [row-major](https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order)\n * order, while internally they are stored in the {@link Matrix3#elements} array in column-major order.\n * This means that calling:\n * ```js\n * const m = new THREE.Matrix();\n * m.set( 11, 12, 13,\n *        21, 22, 23,\n *        31, 32, 33 );\n * ```\n * will result in the elements array containing:\n * ```js\n * m.elements = [ 11, 21, 31,\n *                12, 22, 32,\n *                13, 23, 33 ];\n * ```\n * and internally all calculations are performed using column-major ordering.\n * However, as the actual ordering makes no difference mathematically and\n * most people are used to thinking about matrices in row-major order, the\n * three.js documentation shows matrices in row-major order. Just bear in\n * mind that if you are reading the source code, you'll have to take the\n * transpose of any matrices outlined here to make sense of the calculations.\n */\nclass Matrix3 {\n\n\t/**\n\t * Constructs a new 3x3 matrix. The arguments are supposed to be\n\t * in row-major order. If no arguments are provided, the constructor\n\t * initializes the matrix as an identity matrix.\n\t *\n\t * @param {number} [n11] - 1-1 matrix element.\n\t * @param {number} [n12] - 1-2 matrix element.\n\t * @param {number} [n13] - 1-3 matrix element.\n\t * @param {number} [n21] - 2-1 matrix element.\n\t * @param {number} [n22] - 2-2 matrix element.\n\t * @param {number} [n23] - 2-3 matrix element.\n\t * @param {number} [n31] - 3-1 matrix element.\n\t * @param {number} [n32] - 3-2 matrix element.\n\t * @param {number} [n33] - 3-3 matrix element.\n\t */\n\tconstructor( n11, n12, n13, n21, n22, n23, n31, n32, n33 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tMatrix3.prototype.isMatrix3 = true;\n\n\t\t/**\n\t\t * A column-major list of matrix values.\n\t\t *\n\t\t * @type {Array<number>}\n\t\t */\n\t\tthis.elements = [\n\n\t\t\t1, 0, 0,\n\t\t\t0, 1, 0,\n\t\t\t0, 0, 1\n\n\t\t];\n\n\t\tif ( n11 !== undefined ) {\n\n\t\t\tthis.set( n11, n12, n13, n21, n22, n23, n31, n32, n33 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sets the elements of the matrix.The arguments are supposed to be\n\t * in row-major order.\n\t *\n\t * @param {number} [n11] - 1-1 matrix element.\n\t * @param {number} [n12] - 1-2 matrix element.\n\t * @param {number} [n13] - 1-3 matrix element.\n\t * @param {number} [n21] - 2-1 matrix element.\n\t * @param {number} [n22] - 2-2 matrix element.\n\t * @param {number} [n23] - 2-3 matrix element.\n\t * @param {number} [n31] - 3-1 matrix element.\n\t * @param {number} [n32] - 3-2 matrix element.\n\t * @param {number} [n33] - 3-3 matrix element.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tset( n11, n12, n13, n21, n22, n23, n31, n32, n33 ) {\n\n\t\tconst te = this.elements;\n\n\t\tte[ 0 ] = n11; te[ 1 ] = n21; te[ 2 ] = n31;\n\t\tte[ 3 ] = n12; te[ 4 ] = n22; te[ 5 ] = n32;\n\t\tte[ 6 ] = n13; te[ 7 ] = n23; te[ 8 ] = n33;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix to the 3x3 identity matrix.\n\t *\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tidentity() {\n\n\t\tthis.set(\n\n\t\t\t1, 0, 0,\n\t\t\t0, 1, 0,\n\t\t\t0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the values of the given matrix to this instance.\n\t *\n\t * @param {Matrix3} m - The matrix to copy.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tcopy( m ) {\n\n\t\tconst te = this.elements;\n\t\tconst me = m.elements;\n\n\t\tte[ 0 ] = me[ 0 ]; te[ 1 ] = me[ 1 ]; te[ 2 ] = me[ 2 ];\n\t\tte[ 3 ] = me[ 3 ]; te[ 4 ] = me[ 4 ]; te[ 5 ] = me[ 5 ];\n\t\tte[ 6 ] = me[ 6 ]; te[ 7 ] = me[ 7 ]; te[ 8 ] = me[ 8 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Extracts the basis of this matrix into the three axis vectors provided.\n\t *\n\t * @param {Vector3} xAxis - The basis's x axis.\n\t * @param {Vector3} yAxis - The basis's y axis.\n\t * @param {Vector3} zAxis - The basis's z axis.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\textractBasis( xAxis, yAxis, zAxis ) {\n\n\t\txAxis.setFromMatrix3Column( this, 0 );\n\t\tyAxis.setFromMatrix3Column( this, 1 );\n\t\tzAxis.setFromMatrix3Column( this, 2 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Set this matrix to the upper 3x3 matrix of the given 4x4 matrix.\n\t *\n\t * @param {Matrix4} m - The 4x4 matrix.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tsetFromMatrix4( m ) {\n\n\t\tconst me = m.elements;\n\n\t\tthis.set(\n\n\t\t\tme[ 0 ], me[ 4 ], me[ 8 ],\n\t\t\tme[ 1 ], me[ 5 ], me[ 9 ],\n\t\t\tme[ 2 ], me[ 6 ], me[ 10 ]\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Post-multiplies this matrix by the given 3x3 matrix.\n\t *\n\t * @param {Matrix3} m - The matrix to multiply with.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tmultiply( m ) {\n\n\t\treturn this.multiplyMatrices( this, m );\n\n\t}\n\n\t/**\n\t * Pre-multiplies this matrix by the given 3x3 matrix.\n\t *\n\t * @param {Matrix3} m - The matrix to multiply with.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tpremultiply( m ) {\n\n\t\treturn this.multiplyMatrices( m, this );\n\n\t}\n\n\t/**\n\t * Multiples the given 3x3 matrices and stores the result\n\t * in this matrix.\n\t *\n\t * @param {Matrix3} a - The first matrix.\n\t * @param {Matrix3} b - The second matrix.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tmultiplyMatrices( a, b ) {\n\n\t\tconst ae = a.elements;\n\t\tconst be = b.elements;\n\t\tconst te = this.elements;\n\n\t\tconst a11 = ae[ 0 ], a12 = ae[ 3 ], a13 = ae[ 6 ];\n\t\tconst a21 = ae[ 1 ], a22 = ae[ 4 ], a23 = ae[ 7 ];\n\t\tconst a31 = ae[ 2 ], a32 = ae[ 5 ], a33 = ae[ 8 ];\n\n\t\tconst b11 = be[ 0 ], b12 = be[ 3 ], b13 = be[ 6 ];\n\t\tconst b21 = be[ 1 ], b22 = be[ 4 ], b23 = be[ 7 ];\n\t\tconst b31 = be[ 2 ], b32 = be[ 5 ], b33 = be[ 8 ];\n\n\t\tte[ 0 ] = a11 * b11 + a12 * b21 + a13 * b31;\n\t\tte[ 3 ] = a11 * b12 + a12 * b22 + a13 * b32;\n\t\tte[ 6 ] = a11 * b13 + a12 * b23 + a13 * b33;\n\n\t\tte[ 1 ] = a21 * b11 + a22 * b21 + a23 * b31;\n\t\tte[ 4 ] = a21 * b12 + a22 * b22 + a23 * b32;\n\t\tte[ 7 ] = a21 * b13 + a22 * b23 + a23 * b33;\n\n\t\tte[ 2 ] = a31 * b11 + a32 * b21 + a33 * b31;\n\t\tte[ 5 ] = a31 * b12 + a32 * b22 + a33 * b32;\n\t\tte[ 8 ] = a31 * b13 + a32 * b23 + a33 * b33;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies every component of the matrix by the given scalar.\n\t *\n\t * @param {number} s - The scalar.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tmultiplyScalar( s ) {\n\n\t\tconst te = this.elements;\n\n\t\tte[ 0 ] *= s; te[ 3 ] *= s; te[ 6 ] *= s;\n\t\tte[ 1 ] *= s; te[ 4 ] *= s; te[ 7 ] *= s;\n\t\tte[ 2 ] *= s; te[ 5 ] *= s; te[ 8 ] *= s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes and returns the determinant of this matrix.\n\t *\n\t * @return {number} The determinant.\n\t */\n\tdeterminant() {\n\n\t\tconst te = this.elements;\n\n\t\tconst a = te[ 0 ], b = te[ 1 ], c = te[ 2 ],\n\t\t\td = te[ 3 ], e = te[ 4 ], f = te[ 5 ],\n\t\t\tg = te[ 6 ], h = te[ 7 ], i = te[ 8 ];\n\n\t\treturn a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g;\n\n\t}\n\n\t/**\n\t * Inverts this matrix, using the [analytic method](https://en.wikipedia.org/wiki/Invertible_matrix#Analytic_solution).\n\t * You can not invert with a determinant of zero. If you attempt this, the method produces\n\t * a zero matrix instead.\n\t *\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tinvert() {\n\n\t\tconst te = this.elements,\n\n\t\t\tn11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ],\n\t\t\tn12 = te[ 3 ], n22 = te[ 4 ], n32 = te[ 5 ],\n\t\t\tn13 = te[ 6 ], n23 = te[ 7 ], n33 = te[ 8 ],\n\n\t\t\tt11 = n33 * n22 - n32 * n23,\n\t\t\tt12 = n32 * n13 - n33 * n12,\n\t\t\tt13 = n23 * n12 - n22 * n13,\n\n\t\t\tdet = n11 * t11 + n21 * t12 + n31 * t13;\n\n\t\tif ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0 );\n\n\t\tconst detInv = 1 / det;\n\n\t\tte[ 0 ] = t11 * detInv;\n\t\tte[ 1 ] = ( n31 * n23 - n33 * n21 ) * detInv;\n\t\tte[ 2 ] = ( n32 * n21 - n31 * n22 ) * detInv;\n\n\t\tte[ 3 ] = t12 * detInv;\n\t\tte[ 4 ] = ( n33 * n11 - n31 * n13 ) * detInv;\n\t\tte[ 5 ] = ( n31 * n12 - n32 * n11 ) * detInv;\n\n\t\tte[ 6 ] = t13 * detInv;\n\t\tte[ 7 ] = ( n21 * n13 - n23 * n11 ) * detInv;\n\t\tte[ 8 ] = ( n22 * n11 - n21 * n12 ) * detInv;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Transposes this matrix in place.\n\t *\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\ttranspose() {\n\n\t\tlet tmp;\n\t\tconst m = this.elements;\n\n\t\ttmp = m[ 1 ]; m[ 1 ] = m[ 3 ]; m[ 3 ] = tmp;\n\t\ttmp = m[ 2 ]; m[ 2 ] = m[ 6 ]; m[ 6 ] = tmp;\n\t\ttmp = m[ 5 ]; m[ 5 ] = m[ 7 ]; m[ 7 ] = tmp;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes the normal matrix which is the inverse transpose of the upper\n\t * left 3x3 portion of the given 4x4 matrix.\n\t *\n\t * @param {Matrix4} matrix4 - The 4x4 matrix.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tgetNormalMatrix( matrix4 ) {\n\n\t\treturn this.setFromMatrix4( matrix4 ).invert().transpose();\n\n\t}\n\n\t/**\n\t * Transposes this matrix into the supplied array, and returns itself unchanged.\n\t *\n\t * @param {Array<number>} r - An array to store the transposed matrix elements.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\ttransposeIntoArray( r ) {\n\n\t\tconst m = this.elements;\n\n\t\tr[ 0 ] = m[ 0 ];\n\t\tr[ 1 ] = m[ 3 ];\n\t\tr[ 2 ] = m[ 6 ];\n\t\tr[ 3 ] = m[ 1 ];\n\t\tr[ 4 ] = m[ 4 ];\n\t\tr[ 5 ] = m[ 7 ];\n\t\tr[ 6 ] = m[ 2 ];\n\t\tr[ 7 ] = m[ 5 ];\n\t\tr[ 8 ] = m[ 8 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the UV transform matrix from offset, repeat, rotation, and center.\n\t *\n\t * @param {number} tx - Offset x.\n\t * @param {number} ty - Offset y.\n\t * @param {number} sx - Repeat x.\n\t * @param {number} sy - Repeat y.\n\t * @param {number} rotation - Rotation, in radians. Positive values rotate counterclockwise.\n\t * @param {number} cx - Center x of rotation.\n\t * @param {number} cy - Center y of rotation\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tsetUvTransform( tx, ty, sx, sy, rotation, cx, cy ) {\n\n\t\tconst c = Math.cos( rotation );\n\t\tconst s = Math.sin( rotation );\n\n\t\tthis.set(\n\t\t\tsx * c, sx * s, - sx * ( c * cx + s * cy ) + cx + tx,\n\t\t\t- sy * s, sy * c, - sy * ( - s * cx + c * cy ) + cy + ty,\n\t\t\t0, 0, 1\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Scales this matrix with the given scalar values.\n\t *\n\t * @param {number} sx - The amount to scale in the X axis.\n\t * @param {number} sy - The amount to scale in the Y axis.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tscale( sx, sy ) {\n\n\t\tthis.premultiply( _m3.makeScale( sx, sy ) );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Rotates this matrix by the given angle.\n\t *\n\t * @param {number} theta - The rotation in radians.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\trotate( theta ) {\n\n\t\tthis.premultiply( _m3.makeRotation( - theta ) );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Translates this matrix by the given scalar values.\n\t *\n\t * @param {number} tx - The amount to translate in the X axis.\n\t * @param {number} ty - The amount to translate in the Y axis.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\ttranslate( tx, ty ) {\n\n\t\tthis.premultiply( _m3.makeTranslation( tx, ty ) );\n\n\t\treturn this;\n\n\t}\n\n\t// for 2D Transforms\n\n\t/**\n\t * Sets this matrix as a 2D translation transform.\n\t *\n\t * @param {number|Vector2} x - The amount to translate in the X axis or alternatively a translation vector.\n\t * @param {number} y - The amount to translate in the Y axis.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tmakeTranslation( x, y ) {\n\n\t\tif ( x.isVector2 ) {\n\n\t\t\tthis.set(\n\n\t\t\t\t1, 0, x.x,\n\t\t\t\t0, 1, x.y,\n\t\t\t\t0, 0, 1\n\n\t\t\t);\n\n\t\t} else {\n\n\t\t\tthis.set(\n\n\t\t\t\t1, 0, x,\n\t\t\t\t0, 1, y,\n\t\t\t\t0, 0, 1\n\n\t\t\t);\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix as a 2D rotational transformation.\n\t *\n\t * @param {number} theta - The rotation in radians.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tmakeRotation( theta ) {\n\n\t\t// counterclockwise\n\n\t\tconst c = Math.cos( theta );\n\t\tconst s = Math.sin( theta );\n\n\t\tthis.set(\n\n\t\t\tc, - s, 0,\n\t\t\ts, c, 0,\n\t\t\t0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix as a 2D scale transform.\n\t *\n\t * @param {number} x - The amount to scale in the X axis.\n\t * @param {number} y - The amount to scale in the Y axis.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tmakeScale( x, y ) {\n\n\t\tthis.set(\n\n\t\t\tx, 0, 0,\n\t\t\t0, y, 0,\n\t\t\t0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this matrix is equal with the given one.\n\t *\n\t * @param {Matrix3} matrix - The matrix to test for equality.\n\t * @return {boolean} Whether this matrix is equal with the given one.\n\t */\n\tequals( matrix ) {\n\n\t\tconst te = this.elements;\n\t\tconst me = matrix.elements;\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tif ( te[ i ] !== me[ i ] ) return false;\n\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Sets the elements of the matrix from the given array.\n\t *\n\t * @param {Array<number>} array - The matrix elements in column-major order.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Matrix3} A reference to this matrix.\n\t */\n\tfromArray( array, offset = 0 ) {\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tthis.elements[ i ] = array[ i + offset ];\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Writes the elements of this matrix to the given array. If no array is provided,\n\t * the method returns a new instance.\n\t *\n\t * @param {Array<number>} [array=[]] - The target array holding the matrix elements in column-major order.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Array<number>} The matrix elements in column-major order.\n\t */\n\ttoArray( array = [], offset = 0 ) {\n\n\t\tconst te = this.elements;\n\n\t\tarray[ offset ] = te[ 0 ];\n\t\tarray[ offset + 1 ] = te[ 1 ];\n\t\tarray[ offset + 2 ] = te[ 2 ];\n\n\t\tarray[ offset + 3 ] = te[ 3 ];\n\t\tarray[ offset + 4 ] = te[ 4 ];\n\t\tarray[ offset + 5 ] = te[ 5 ];\n\n\t\tarray[ offset + 6 ] = te[ 6 ];\n\t\tarray[ offset + 7 ] = te[ 7 ];\n\t\tarray[ offset + 8 ] = te[ 8 ];\n\n\t\treturn array;\n\n\t}\n\n\t/**\n\t * Returns a matrix with copied values from this instance.\n\t *\n\t * @return {Matrix3} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().fromArray( this.elements );\n\n\t}\n\n}\n\nconst _m3 = /*@__PURE__*/ new Matrix3();\n\nconst LINEAR_REC709_TO_XYZ = /*@__PURE__*/ new Matrix3().set(\n\t0.4123908, 0.3575843, 0.1804808,\n\t0.2126390, 0.7151687, 0.0721923,\n\t0.0193308, 0.1191948, 0.9505322\n);\n\nconst XYZ_TO_LINEAR_REC709 = /*@__PURE__*/ new Matrix3().set(\n\t3.2409699, -1.5373832, -0.4986108,\n\t-0.9692436, 1.8759675, 0.0415551,\n\t0.0556301, -0.203977, 1.0569715\n);\n\nfunction createColorManagement() {\n\n\tconst ColorManagement = {\n\n\t\tenabled: true,\n\n\t\tworkingColorSpace: LinearSRGBColorSpace,\n\n\t\t/**\n\t\t * Implementations of supported color spaces.\n\t\t *\n\t\t * Required:\n\t\t *\t- primaries: chromaticity coordinates [ rx ry gx gy bx by ]\n\t\t *\t- whitePoint: reference white [ x y ]\n\t\t *\t- transfer: transfer function (pre-defined)\n\t\t *\t- toXYZ: Matrix3 RGB to XYZ transform\n\t\t *\t- fromXYZ: Matrix3 XYZ to RGB transform\n\t\t *\t- luminanceCoefficients: RGB luminance coefficients\n\t\t *\n\t\t * Optional:\n\t\t *  - outputColorSpaceConfig: { drawingBufferColorSpace: ColorSpace, toneMappingMode: 'extended' | 'standard' }\n\t\t *  - workingColorSpaceConfig: { unpackColorSpace: ColorSpace }\n\t\t *\n\t\t * Reference:\n\t\t * - https://www.russellcottrell.com/photo/matrixCalculator.htm\n\t\t */\n\t\tspaces: {},\n\n\t\tconvert: function ( color, sourceColorSpace, targetColorSpace ) {\n\n\t\t\tif ( this.enabled === false || sourceColorSpace === targetColorSpace || ! sourceColorSpace || ! targetColorSpace ) {\n\n\t\t\t\treturn color;\n\n\t\t\t}\n\n\t\t\tif ( this.spaces[ sourceColorSpace ].transfer === SRGBTransfer ) {\n\n\t\t\t\tcolor.r = SRGBToLinear( color.r );\n\t\t\t\tcolor.g = SRGBToLinear( color.g );\n\t\t\t\tcolor.b = SRGBToLinear( color.b );\n\n\t\t\t}\n\n\t\t\tif ( this.spaces[ sourceColorSpace ].primaries !== this.spaces[ targetColorSpace ].primaries ) {\n\n\t\t\t\tcolor.applyMatrix3( this.spaces[ sourceColorSpace ].toXYZ );\n\t\t\t\tcolor.applyMatrix3( this.spaces[ targetColorSpace ].fromXYZ );\n\n\t\t\t}\n\n\t\t\tif ( this.spaces[ targetColorSpace ].transfer === SRGBTransfer ) {\n\n\t\t\t\tcolor.r = LinearToSRGB( color.r );\n\t\t\t\tcolor.g = LinearToSRGB( color.g );\n\t\t\t\tcolor.b = LinearToSRGB( color.b );\n\n\t\t\t}\n\n\t\t\treturn color;\n\n\t\t},\n\n\t\tworkingToColorSpace: function ( color, targetColorSpace ) {\n\n\t\t\treturn this.convert( color, this.workingColorSpace, targetColorSpace );\n\n\t\t},\n\n\t\tcolorSpaceToWorking: function ( color, sourceColorSpace ) {\n\n\t\t\treturn this.convert( color, sourceColorSpace, this.workingColorSpace );\n\n\t\t},\n\n\t\tgetPrimaries: function ( colorSpace ) {\n\n\t\t\treturn this.spaces[ colorSpace ].primaries;\n\n\t\t},\n\n\t\tgetTransfer: function ( colorSpace ) {\n\n\t\t\tif ( colorSpace === NoColorSpace ) return LinearTransfer;\n\n\t\t\treturn this.spaces[ colorSpace ].transfer;\n\n\t\t},\n\n\t\tgetToneMappingMode: function ( colorSpace ) {\n\n\t\t\treturn this.spaces[ colorSpace ].outputColorSpaceConfig.toneMappingMode || 'standard';\n\n\t\t},\n\n\t\tgetLuminanceCoefficients: function ( target, colorSpace = this.workingColorSpace ) {\n\n\t\t\treturn target.fromArray( this.spaces[ colorSpace ].luminanceCoefficients );\n\n\t\t},\n\n\t\tdefine: function ( colorSpaces ) {\n\n\t\t\tObject.assign( this.spaces, colorSpaces );\n\n\t\t},\n\n\t\t// Internal APIs\n\n\t\t_getMatrix: function ( targetMatrix, sourceColorSpace, targetColorSpace ) {\n\n\t\t\treturn targetMatrix\n\t\t\t\t.copy( this.spaces[ sourceColorSpace ].toXYZ )\n\t\t\t\t.multiply( this.spaces[ targetColorSpace ].fromXYZ );\n\n\t\t},\n\n\t\t_getDrawingBufferColorSpace: function ( colorSpace ) {\n\n\t\t\treturn this.spaces[ colorSpace ].outputColorSpaceConfig.drawingBufferColorSpace;\n\n\t\t},\n\n\t\t_getUnpackColorSpace: function ( colorSpace = this.workingColorSpace ) {\n\n\t\t\treturn this.spaces[ colorSpace ].workingColorSpaceConfig.unpackColorSpace;\n\n\t\t},\n\n\t\t// Deprecated\n\n\t\tfromWorkingColorSpace: function ( color, targetColorSpace ) {\n\n\t\t\twarnOnce( 'ColorManagement: .fromWorkingColorSpace() has been renamed to .workingToColorSpace().' ); // @deprecated, r177\n\n\t\t\treturn ColorManagement.workingToColorSpace( color, targetColorSpace );\n\n\t\t},\n\n\t\ttoWorkingColorSpace: function ( color, sourceColorSpace ) {\n\n\t\t\twarnOnce( 'ColorManagement: .toWorkingColorSpace() has been renamed to .colorSpaceToWorking().' ); // @deprecated, r177\n\n\t\t\treturn ColorManagement.colorSpaceToWorking( color, sourceColorSpace );\n\n\t\t},\n\n\t};\n\n\t/******************************************************************************\n\t * sRGB definitions\n\t */\n\n\tconst REC709_PRIMARIES = [ 0.640, 0.330, 0.300, 0.600, 0.150, 0.060 ];\n\tconst REC709_LUMINANCE_COEFFICIENTS = [ 0.2126, 0.7152, 0.0722 ];\n\tconst D65 = [ 0.3127, 0.3290 ];\n\n\tColorManagement.define( {\n\n\t\t[ LinearSRGBColorSpace ]: {\n\t\t\tprimaries: REC709_PRIMARIES,\n\t\t\twhitePoint: D65,\n\t\t\ttransfer: LinearTransfer,\n\t\t\ttoXYZ: LINEAR_REC709_TO_XYZ,\n\t\t\tfromXYZ: XYZ_TO_LINEAR_REC709,\n\t\t\tluminanceCoefficients: REC709_LUMINANCE_COEFFICIENTS,\n\t\t\tworkingColorSpaceConfig: { unpackColorSpace: SRGBColorSpace },\n\t\t\toutputColorSpaceConfig: { drawingBufferColorSpace: SRGBColorSpace }\n\t\t},\n\n\t\t[ SRGBColorSpace ]: {\n\t\t\tprimaries: REC709_PRIMARIES,\n\t\t\twhitePoint: D65,\n\t\t\ttransfer: SRGBTransfer,\n\t\t\ttoXYZ: LINEAR_REC709_TO_XYZ,\n\t\t\tfromXYZ: XYZ_TO_LINEAR_REC709,\n\t\t\tluminanceCoefficients: REC709_LUMINANCE_COEFFICIENTS,\n\t\t\toutputColorSpaceConfig: { drawingBufferColorSpace: SRGBColorSpace }\n\t\t},\n\n\t} );\n\n\treturn ColorManagement;\n\n}\n\nconst ColorManagement = /*@__PURE__*/ createColorManagement();\n\nfunction SRGBToLinear( c ) {\n\n\treturn ( c < 0.04045 ) ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 );\n\n}\n\nfunction LinearToSRGB( c ) {\n\n\treturn ( c < 0.0031308 ) ? c * 12.92 : 1.055 * ( Math.pow( c, 0.41666 ) ) - 0.055;\n\n}\n\nlet _canvas;\n\n/**\n * A class containing utility functions for images.\n *\n * @hideconstructor\n */\nclass ImageUtils {\n\n\t/**\n\t * Returns a data URI containing a representation of the given image.\n\t *\n\t * @param {(HTMLImageElement|HTMLCanvasElement)} image - The image object.\n\t * @param {string} [type='image/png'] - Indicates the image format.\n\t * @return {string} The data URI.\n\t */\n\tstatic getDataURL( image, type = 'image/png' ) {\n\n\t\tif ( /^data:/i.test( image.src ) ) {\n\n\t\t\treturn image.src;\n\n\t\t}\n\n\t\tif ( typeof HTMLCanvasElement === 'undefined' ) {\n\n\t\t\treturn image.src;\n\n\t\t}\n\n\t\tlet canvas;\n\n\t\tif ( image instanceof HTMLCanvasElement ) {\n\n\t\t\tcanvas = image;\n\n\t\t} else {\n\n\t\t\tif ( _canvas === undefined ) _canvas = createElementNS( 'canvas' );\n\n\t\t\t_canvas.width = image.width;\n\t\t\t_canvas.height = image.height;\n\n\t\t\tconst context = _canvas.getContext( '2d' );\n\n\t\t\tif ( image instanceof ImageData ) {\n\n\t\t\t\tcontext.putImageData( image, 0, 0 );\n\n\t\t\t} else {\n\n\t\t\t\tcontext.drawImage( image, 0, 0, image.width, image.height );\n\n\t\t\t}\n\n\t\t\tcanvas = _canvas;\n\n\t\t}\n\n\t\treturn canvas.toDataURL( type );\n\n\t}\n\n\t/**\n\t * Converts the given sRGB image data to linear color space.\n\t *\n\t * @param {(HTMLImageElement|HTMLCanvasElement|ImageBitmap|Object)} image - The image object.\n\t * @return {HTMLCanvasElement|Object} The converted image.\n\t */\n\tstatic sRGBToLinear( image ) {\n\n\t\tif ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||\n\t\t\t( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||\n\t\t\t( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {\n\n\t\t\tconst canvas = createElementNS( 'canvas' );\n\n\t\t\tcanvas.width = image.width;\n\t\t\tcanvas.height = image.height;\n\n\t\t\tconst context = canvas.getContext( '2d' );\n\t\t\tcontext.drawImage( image, 0, 0, image.width, image.height );\n\n\t\t\tconst imageData = context.getImageData( 0, 0, image.width, image.height );\n\t\t\tconst data = imageData.data;\n\n\t\t\tfor ( let i = 0; i < data.length; i ++ ) {\n\n\t\t\t\tdata[ i ] = SRGBToLinear( data[ i ] / 255 ) * 255;\n\n\t\t\t}\n\n\t\t\tcontext.putImageData( imageData, 0, 0 );\n\n\t\t\treturn canvas;\n\n\t\t} else if ( image.data ) {\n\n\t\t\tconst data = image.data.slice( 0 );\n\n\t\t\tfor ( let i = 0; i < data.length; i ++ ) {\n\n\t\t\t\tif ( data instanceof Uint8Array || data instanceof Uint8ClampedArray ) {\n\n\t\t\t\t\tdata[ i ] = Math.floor( SRGBToLinear( data[ i ] / 255 ) * 255 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// assuming float\n\n\t\t\t\t\tdata[ i ] = SRGBToLinear( data[ i ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tdata: data,\n\t\t\t\twidth: image.width,\n\t\t\t\theight: image.height\n\t\t\t};\n\n\t\t} else {\n\n\t\t\twarn( 'ImageUtils.sRGBToLinear(): Unsupported image type. No color space conversion applied.' );\n\t\t\treturn image;\n\n\t\t}\n\n\t}\n\n}\n\nlet _sourceId = 0;\n\n/**\n * Represents the data source of a texture.\n *\n * The main purpose of this class is to decouple the data definition from the texture\n * definition so the same data can be used with multiple texture instances.\n */\nclass Source {\n\n\t/**\n\t * Constructs a new video texture.\n\t *\n\t * @param {any} [data=null] - The data definition of a texture.\n\t */\n\tconstructor( data = null ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSource = true;\n\n\t\t/**\n\t\t * The ID of the source.\n\t\t *\n\t\t * @name Source#id\n\t\t * @type {number}\n\t\t * @readonly\n\t\t */\n\t\tObject.defineProperty( this, 'id', { value: _sourceId ++ } );\n\n\t\t/**\n\t\t * The UUID of the source.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.uuid = generateUUID();\n\n\t\t/**\n\t\t * The data definition of a texture.\n\t\t *\n\t\t * @type {any}\n\t\t */\n\t\tthis.data = data;\n\n\t\t/**\n\t\t * This property is only relevant when {@link Source#needsUpdate} is set to `true` and\n\t\t * provides more control on how texture data should be processed. When `dataReady` is set\n\t\t * to `false`, the engine performs the memory allocation (if necessary) but does not transfer\n\t\t * the data into the GPU memory.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.dataReady = true;\n\n\t\t/**\n\t\t * This starts at `0` and counts how many times {@link Source#needsUpdate} is set to `true`.\n\t\t *\n\t\t * @type {number}\n\t\t * @readonly\n\t\t * @default 0\n\t\t */\n\t\tthis.version = 0;\n\n\t}\n\n\t/**\n\t * Returns the dimensions of the source into the given target vector.\n\t *\n\t * @param {(Vector2|Vector3)} target - The target object the result is written into.\n\t * @return {(Vector2|Vector3)} The dimensions of the source.\n\t */\n\tgetSize( target ) {\n\n\t\tconst data = this.data;\n\n\t\tif ( ( typeof HTMLVideoElement !== 'undefined' ) && ( data instanceof HTMLVideoElement ) ) {\n\n\t\t\ttarget.set( data.videoWidth, data.videoHeight, 0 );\n\n\t\t} else if ( ( typeof VideoFrame !== 'undefined' ) && ( data instanceof VideoFrame ) ) {\n\n\t\t\ttarget.set( data.displayHeight, data.displayWidth, 0 );\n\n\t\t} else if ( data !== null ) {\n\n\t\t\ttarget.set( data.width, data.height, data.depth || 0 );\n\n\t\t} else {\n\n\t\t\ttarget.set( 0, 0, 0 );\n\n\t\t}\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * When the property is set to `true`, the engine allocates the memory\n\t * for the texture (if necessary) and triggers the actual texture upload\n\t * to the GPU next time the source is used.\n\t *\n\t * @type {boolean}\n\t * @default false\n\t * @param {boolean} value\n\t */\n\tset needsUpdate( value ) {\n\n\t\tif ( value === true ) this.version ++;\n\n\t}\n\n\t/**\n\t * Serializes the source into JSON.\n\t *\n\t * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.\n\t * @return {Object} A JSON object representing the serialized source.\n\t * @see {@link ObjectLoader#parse}\n\t */\n\ttoJSON( meta ) {\n\n\t\tconst isRootObject = ( meta === undefined || typeof meta === 'string' );\n\n\t\tif ( ! isRootObject && meta.images[ this.uuid ] !== undefined ) {\n\n\t\t\treturn meta.images[ this.uuid ];\n\n\t\t}\n\n\t\tconst output = {\n\t\t\tuuid: this.uuid,\n\t\t\turl: ''\n\t\t};\n\n\t\tconst data = this.data;\n\n\t\tif ( data !== null ) {\n\n\t\t\tlet url;\n\n\t\t\tif ( Array.isArray( data ) ) {\n\n\t\t\t\t// cube texture\n\n\t\t\t\turl = [];\n\n\t\t\t\tfor ( let i = 0, l = data.length; i < l; i ++ ) {\n\n\t\t\t\t\tif ( data[ i ].isDataTexture ) {\n\n\t\t\t\t\t\turl.push( serializeImage( data[ i ].image ) );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\turl.push( serializeImage( data[ i ] ) );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\t// texture\n\n\t\t\t\turl = serializeImage( data );\n\n\t\t\t}\n\n\t\t\toutput.url = url;\n\n\t\t}\n\n\t\tif ( ! isRootObject ) {\n\n\t\t\tmeta.images[ this.uuid ] = output;\n\n\t\t}\n\n\t\treturn output;\n\n\t}\n\n}\n\nfunction serializeImage( image ) {\n\n\tif ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||\n\t\t( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||\n\t\t( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {\n\n\t\t// default images\n\n\t\treturn ImageUtils.getDataURL( image );\n\n\t} else {\n\n\t\tif ( image.data ) {\n\n\t\t\t// images of DataTexture\n\n\t\t\treturn {\n\t\t\t\tdata: Array.from( image.data ),\n\t\t\t\twidth: image.width,\n\t\t\t\theight: image.height,\n\t\t\t\ttype: image.data.constructor.name\n\t\t\t};\n\n\t\t} else {\n\n\t\t\twarn( 'Texture: Unable to serialize Texture.' );\n\t\t\treturn {};\n\n\t\t}\n\n\t}\n\n}\n\nlet _textureId = 0;\n\nconst _tempVec3 = /*@__PURE__*/ new Vector3();\n\n/**\n * Base class for all textures.\n *\n * Note: After the initial use of a texture, its dimensions, format, and type\n * cannot be changed. Instead, call {@link Texture#dispose} on the texture and instantiate a new one.\n *\n * @augments EventDispatcher\n */\nclass Texture extends EventDispatcher {\n\n\t/**\n\t * Constructs a new texture.\n\t *\n\t * @param {?Object} [image=Texture.DEFAULT_IMAGE] - The image holding the texture data.\n\t * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.\n\t * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.\n\t * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.\n\t * @param {number} [magFilter=LinearFilter] - The mag filter value.\n\t * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.\n\t * @param {number} [format=RGBAFormat] - The texture format.\n\t * @param {number} [type=UnsignedByteType] - The texture type.\n\t * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.\n\t * @param {string} [colorSpace=NoColorSpace] - The color space.\n\t */\n\tconstructor( image = Texture.DEFAULT_IMAGE, mapping = Texture.DEFAULT_MAPPING, wrapS = ClampToEdgeWrapping, wrapT = ClampToEdgeWrapping, magFilter = LinearFilter, minFilter = LinearMipmapLinearFilter, format = RGBAFormat, type = UnsignedByteType, anisotropy = Texture.DEFAULT_ANISOTROPY, colorSpace = NoColorSpace ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isTexture = true;\n\n\t\t/**\n\t\t * The ID of the texture.\n\t\t *\n\t\t * @name Texture#id\n\t\t * @type {number}\n\t\t * @readonly\n\t\t */\n\t\tObject.defineProperty( this, 'id', { value: _textureId ++ } );\n\n\t\t/**\n\t\t * The UUID of the material.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.uuid = generateUUID();\n\n\t\t/**\n\t\t * The name of the material.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\n\t\t/**\n\t\t * The data definition of a texture. A reference to the data source can be\n\t\t * shared across textures. This is often useful in context of spritesheets\n\t\t * where multiple textures render the same data but with different texture\n\t\t * transformations.\n\t\t *\n\t\t * @type {Source}\n\t\t */\n\t\tthis.source = new Source( image );\n\n\t\t/**\n\t\t * An array holding user-defined mipmaps.\n\t\t *\n\t\t * @type {Array<Object>}\n\t\t */\n\t\tthis.mipmaps = [];\n\n\t\t/**\n\t\t * How the texture is applied to the object. The value `UVMapping`\n\t\t * is the default, where texture or uv coordinates are used to apply the map.\n\t\t *\n\t\t * @type {(UVMapping|CubeReflectionMapping|CubeRefractionMapping|EquirectangularReflectionMapping|EquirectangularRefractionMapping|CubeUVReflectionMapping)}\n\t\t * @default UVMapping\n\t\t*/\n\t\tthis.mapping = mapping;\n\n\t\t/**\n\t\t * Lets you select the uv attribute to map the texture to. `0` for `uv`,\n\t\t * `1` for `uv1`, `2` for `uv2` and `3` for `uv3`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.channel = 0;\n\n\t\t/**\n\t\t * This defines how the texture is wrapped horizontally and corresponds to\n\t\t * *U* in UV mapping.\n\t\t *\n\t\t * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}\n\t\t * @default ClampToEdgeWrapping\n\t\t */\n\t\tthis.wrapS = wrapS;\n\n\t\t/**\n\t\t * This defines how the texture is wrapped horizontally and corresponds to\n\t\t * *V* in UV mapping.\n\t\t *\n\t\t * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}\n\t\t * @default ClampToEdgeWrapping\n\t\t */\n\t\tthis.wrapT = wrapT;\n\n\t\t/**\n\t\t * How the texture is sampled when a texel covers more than one pixel.\n\t\t *\n\t\t * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}\n\t\t * @default LinearFilter\n\t\t */\n\t\tthis.magFilter = magFilter;\n\n\t\t/**\n\t\t * How the texture is sampled when a texel covers less than one pixel.\n\t\t *\n\t\t * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}\n\t\t * @default LinearMipmapLinearFilter\n\t\t */\n\t\tthis.minFilter = minFilter;\n\n\t\t/**\n\t\t * The number of samples taken along the axis through the pixel that has the\n\t\t * highest density of texels. By default, this value is `1`. A higher value\n\t\t * gives a less blurry result than a basic mipmap, at the cost of more\n\t\t * texture samples being used.\n\t\t *\n\t\t * @type {number}\n\t\t * @default Texture.DEFAULT_ANISOTROPY\n\t\t */\n\t\tthis.anisotropy = anisotropy;\n\n\t\t/**\n\t\t * The format of the texture.\n\t\t *\n\t\t * @type {number}\n\t\t * @default RGBAFormat\n\t\t */\n\t\tthis.format = format;\n\n\t\t/**\n\t\t * The default internal format is derived from {@link Texture#format} and {@link Texture#type} and\n\t\t * defines how the texture data is going to be stored on the GPU.\n\t\t *\n\t\t * This property allows to overwrite the default format.\n\t\t *\n\t\t * @type {?string}\n\t\t * @default null\n\t\t */\n\t\tthis.internalFormat = null;\n\n\t\t/**\n\t\t * The data type of the texture.\n\t\t *\n\t\t * @type {number}\n\t\t * @default UnsignedByteType\n\t\t */\n\t\tthis.type = type;\n\n\t\t/**\n\t\t * How much a single repetition of the texture is offset from the beginning,\n\t\t * in each direction U and V. Typical range is `0.0` to `1.0`.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (0,0)\n\t\t */\n\t\tthis.offset = new Vector2( 0, 0 );\n\n\t\t/**\n\t\t * How many times the texture is repeated across the surface, in each\n\t\t * direction U and V. If repeat is set greater than `1` in either direction,\n\t\t * the corresponding wrap parameter should also be set to `RepeatWrapping`\n\t\t * or `MirroredRepeatWrapping` to achieve the desired tiling effect.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (1,1)\n\t\t */\n\t\tthis.repeat = new Vector2( 1, 1 );\n\n\t\t/**\n\t\t * The point around which rotation occurs. A value of `(0.5, 0.5)` corresponds\n\t\t * to the center of the texture. Default is `(0, 0)`, the lower left.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (0,0)\n\t\t */\n\t\tthis.center = new Vector2( 0, 0 );\n\n\t\t/**\n\t\t * How much the texture is rotated around the center point, in radians.\n\t\t * Positive values are counter-clockwise.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.rotation = 0;\n\n\t\t/**\n\t\t * Whether to update the texture's uv-transformation {@link Texture#matrix}\n\t\t * from the properties {@link Texture#offset}, {@link Texture#repeat},\n\t\t * {@link Texture#rotation}, and {@link Texture#center}.\n\t\t *\n\t\t * Set this to `false` if you are specifying the uv-transform matrix directly.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.matrixAutoUpdate = true;\n\n\t\t/**\n\t\t * The uv-transformation matrix of the texture.\n\t\t *\n\t\t * @type {Matrix3}\n\t\t */\n\t\tthis.matrix = new Matrix3();\n\n\t\t/**\n\t\t * Whether to generate mipmaps (if possible) for a texture.\n\t\t *\n\t\t * Set this to `false` if you are creating mipmaps manually.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.generateMipmaps = true;\n\n\t\t/**\n\t\t * If set to `true`, the alpha channel, if present, is multiplied into the\n\t\t * color channels when the texture is uploaded to the GPU.\n\t\t *\n\t\t * Note that this property has no effect when using `ImageBitmap`. You need to\n\t\t * configure premultiply alpha on bitmap creation instead.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.premultiplyAlpha = false;\n\n\t\t/**\n\t\t * If set to `true`, the texture is flipped along the vertical axis when\n\t\t * uploaded to the GPU.\n\t\t *\n\t\t * Note that this property has no effect when using `ImageBitmap`. You need to\n\t\t * configure the flip on bitmap creation instead.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.flipY = true;\n\n\t\t/**\n\t\t * Specifies the alignment requirements for the start of each pixel row in memory.\n\t\t * The allowable values are `1` (byte-alignment), `2` (rows aligned to even-numbered bytes),\n\t\t * `4` (word-alignment), and `8` (rows start on double-word boundaries).\n\t\t *\n\t\t * @type {number}\n\t\t * @default 4\n\t\t */\n\t\tthis.unpackAlignment = 4;\t// valid values: 1, 2, 4, 8 (see http://www.khronos.org/opengles/sdk/docs/man/xhtml/glPixelStorei.xml)\n\n\t\t/**\n\t\t * Textures containing color data should be annotated with `SRGBColorSpace` or `LinearSRGBColorSpace`.\n\t\t *\n\t\t * @type {string}\n\t\t * @default NoColorSpace\n\t\t */\n\t\tthis.colorSpace = colorSpace;\n\n\t\t/**\n\t\t * An object that can be used to store custom data about the texture. It\n\t\t * should not hold references to functions as these will not be cloned.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.userData = {};\n\n\t\t/**\n\t\t * This can be used to only update a subregion or specific rows of the texture (for example, just the\n\t\t * first 3 rows). Use the `addUpdateRange()` function to add ranges to this array.\n\t\t *\n\t\t * @type {Array<Object>}\n\t\t */\n\t\tthis.updateRanges = [];\n\n\t\t/**\n\t\t * This starts at `0` and counts how many times {@link Texture#needsUpdate} is set to `true`.\n\t\t *\n\t\t * @type {number}\n\t\t * @readonly\n\t\t * @default 0\n\t\t */\n\t\tthis.version = 0;\n\n\t\t/**\n\t\t * A callback function, called when the texture is updated (e.g., when\n\t\t * {@link Texture#needsUpdate} has been set to true and then the texture is used).\n\t\t *\n\t\t * @type {?Function}\n\t\t * @default null\n\t\t */\n\t\tthis.onUpdate = null;\n\n\t\t/**\n\t\t * An optional back reference to the textures render target.\n\t\t *\n\t\t * @type {?(RenderTarget|WebGLRenderTarget)}\n\t\t * @default null\n\t\t */\n\t\tthis.renderTarget = null;\n\n\t\t/**\n\t\t * Indicates whether a texture belongs to a render target or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default false\n\t\t */\n\t\tthis.isRenderTargetTexture = false;\n\n\t\t/**\n\t\t * Indicates if a texture should be handled like a texture array.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default false\n\t\t */\n\t\tthis.isArrayTexture = image && image.depth && image.depth > 1 ? true : false;\n\n\t\t/**\n\t\t * Indicates whether this texture should be processed by `PMREMGenerator` or not\n\t\t * (only relevant for render target textures).\n\t\t *\n\t\t * @type {number}\n\t\t * @readonly\n\t\t * @default 0\n\t\t */\n\t\tthis.pmremVersion = 0;\n\n\t}\n\n\t/**\n\t * The width of the texture in pixels.\n\t */\n\tget width() {\n\n\t\treturn this.source.getSize( _tempVec3 ).x;\n\n\t}\n\n\t/**\n\t * The height of the texture in pixels.\n\t */\n\tget height() {\n\n\t\treturn this.source.getSize( _tempVec3 ).y;\n\n\t}\n\n\t/**\n\t * The depth of the texture in pixels.\n\t */\n\tget depth() {\n\n\t\treturn this.source.getSize( _tempVec3 ).z;\n\n\t}\n\n\t/**\n\t * The image object holding the texture data.\n\t *\n\t * @type {?Object}\n\t */\n\tget image() {\n\n\t\treturn this.source.data;\n\n\t}\n\n\tset image( value = null ) {\n\n\t\tthis.source.data = value;\n\n\t}\n\n\t/**\n\t * Updates the texture transformation matrix from the from the properties {@link Texture#offset},\n\t * {@link Texture#repeat}, {@link Texture#rotation}, and {@link Texture#center}.\n\t */\n\tupdateMatrix() {\n\n\t\tthis.matrix.setUvTransform( this.offset.x, this.offset.y, this.repeat.x, this.repeat.y, this.rotation, this.center.x, this.center.y );\n\n\t}\n\n\t/**\n\t * Adds a range of data in the data texture to be updated on the GPU.\n\t *\n\t * @param {number} start - Position at which to start update.\n\t * @param {number} count - The number of components to update.\n\t */\n\taddUpdateRange( start, count ) {\n\n\t\tthis.updateRanges.push( { start, count } );\n\n\t}\n\n\t/**\n\t * Clears the update ranges.\n\t */\n\tclearUpdateRanges() {\n\n\t\tthis.updateRanges.length = 0;\n\n\t}\n\n\t/**\n\t * Returns a new texture with copied values from this instance.\n\t *\n\t * @return {Texture} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Copies the values of the given texture to this instance.\n\t *\n\t * @param {Texture} source - The texture to copy.\n\t * @return {Texture} A reference to this instance.\n\t */\n\tcopy( source ) {\n\n\t\tthis.name = source.name;\n\n\t\tthis.source = source.source;\n\t\tthis.mipmaps = source.mipmaps.slice( 0 );\n\n\t\tthis.mapping = source.mapping;\n\t\tthis.channel = source.channel;\n\n\t\tthis.wrapS = source.wrapS;\n\t\tthis.wrapT = source.wrapT;\n\n\t\tthis.magFilter = source.magFilter;\n\t\tthis.minFilter = source.minFilter;\n\n\t\tthis.anisotropy = source.anisotropy;\n\n\t\tthis.format = source.format;\n\t\tthis.internalFormat = source.internalFormat;\n\t\tthis.type = source.type;\n\n\t\tthis.offset.copy( source.offset );\n\t\tthis.repeat.copy( source.repeat );\n\t\tthis.center.copy( source.center );\n\t\tthis.rotation = source.rotation;\n\n\t\tthis.matrixAutoUpdate = source.matrixAutoUpdate;\n\t\tthis.matrix.copy( source.matrix );\n\n\t\tthis.generateMipmaps = source.generateMipmaps;\n\t\tthis.premultiplyAlpha = source.premultiplyAlpha;\n\t\tthis.flipY = source.flipY;\n\t\tthis.unpackAlignment = source.unpackAlignment;\n\t\tthis.colorSpace = source.colorSpace;\n\n\t\tthis.renderTarget = source.renderTarget;\n\t\tthis.isRenderTargetTexture = source.isRenderTargetTexture;\n\t\tthis.isArrayTexture = source.isArrayTexture;\n\n\t\tthis.userData = JSON.parse( JSON.stringify( source.userData ) );\n\n\t\tthis.needsUpdate = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this texture's properties based on `values`.\n\t * @param {Object} values - A container with texture parameters.\n\t */\n\tsetValues( values ) {\n\n\t\tfor ( const key in values ) {\n\n\t\t\tconst newValue = values[ key ];\n\n\t\t\tif ( newValue === undefined ) {\n\n\t\t\t\twarn( `Texture.setValues(): parameter '${ key }' has value of undefined.` );\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\tconst currentValue = this[ key ];\n\n\t\t\tif ( currentValue === undefined ) {\n\n\t\t\t\twarn( `Texture.setValues(): property '${ key }' does not exist.` );\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\tif ( ( currentValue && newValue ) && ( currentValue.isVector2 && newValue.isVector2 ) ) {\n\n\t\t\t\tcurrentValue.copy( newValue );\n\n\t\t\t} else if ( ( currentValue && newValue ) && ( currentValue.isVector3 && newValue.isVector3 ) ) {\n\n\t\t\t\tcurrentValue.copy( newValue );\n\n\t\t\t} else if ( ( currentValue && newValue ) && ( currentValue.isMatrix3 && newValue.isMatrix3 ) ) {\n\n\t\t\t\tcurrentValue.copy( newValue );\n\n\t\t\t} else {\n\n\t\t\t\tthis[ key ] = newValue;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Serializes the texture into JSON.\n\t *\n\t * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.\n\t * @return {Object} A JSON object representing the serialized texture.\n\t * @see {@link ObjectLoader#parse}\n\t */\n\ttoJSON( meta ) {\n\n\t\tconst isRootObject = ( meta === undefined || typeof meta === 'string' );\n\n\t\tif ( ! isRootObject && meta.textures[ this.uuid ] !== undefined ) {\n\n\t\t\treturn meta.textures[ this.uuid ];\n\n\t\t}\n\n\t\tconst output = {\n\n\t\t\tmetadata: {\n\t\t\t\tversion: 4.7,\n\t\t\t\ttype: 'Texture',\n\t\t\t\tgenerator: 'Texture.toJSON'\n\t\t\t},\n\n\t\t\tuuid: this.uuid,\n\t\t\tname: this.name,\n\n\t\t\timage: this.source.toJSON( meta ).uuid,\n\n\t\t\tmapping: this.mapping,\n\t\t\tchannel: this.channel,\n\n\t\t\trepeat: [ this.repeat.x, this.repeat.y ],\n\t\t\toffset: [ this.offset.x, this.offset.y ],\n\t\t\tcenter: [ this.center.x, this.center.y ],\n\t\t\trotation: this.rotation,\n\n\t\t\twrap: [ this.wrapS, this.wrapT ],\n\n\t\t\tformat: this.format,\n\t\t\tinternalFormat: this.internalFormat,\n\t\t\ttype: this.type,\n\t\t\tcolorSpace: this.colorSpace,\n\n\t\t\tminFilter: this.minFilter,\n\t\t\tmagFilter: this.magFilter,\n\t\t\tanisotropy: this.anisotropy,\n\n\t\t\tflipY: this.flipY,\n\n\t\t\tgenerateMipmaps: this.generateMipmaps,\n\t\t\tpremultiplyAlpha: this.premultiplyAlpha,\n\t\t\tunpackAlignment: this.unpackAlignment\n\n\t\t};\n\n\t\tif ( Object.keys( this.userData ).length > 0 ) output.userData = this.userData;\n\n\t\tif ( ! isRootObject ) {\n\n\t\t\tmeta.textures[ this.uuid ] = output;\n\n\t\t}\n\n\t\treturn output;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t *\n\t * @fires Texture#dispose\n\t */\n\tdispose() {\n\n\t\t/**\n\t\t * Fires when the texture has been disposed of.\n\t\t *\n\t\t * @event Texture#dispose\n\t\t * @type {Object}\n\t\t */\n\t\tthis.dispatchEvent( { type: 'dispose' } );\n\n\t}\n\n\t/**\n\t * Transforms the given uv vector with the textures uv transformation matrix.\n\t *\n\t * @param {Vector2} uv - The uv vector.\n\t * @return {Vector2} The transformed uv vector.\n\t */\n\ttransformUv( uv ) {\n\n\t\tif ( this.mapping !== UVMapping ) return uv;\n\n\t\tuv.applyMatrix3( this.matrix );\n\n\t\tif ( uv.x < 0 || uv.x > 1 ) {\n\n\t\t\tswitch ( this.wrapS ) {\n\n\t\t\t\tcase RepeatWrapping:\n\n\t\t\t\t\tuv.x = uv.x - Math.floor( uv.x );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase ClampToEdgeWrapping:\n\n\t\t\t\t\tuv.x = uv.x < 0 ? 0 : 1;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase MirroredRepeatWrapping:\n\n\t\t\t\t\tif ( Math.abs( Math.floor( uv.x ) % 2 ) === 1 ) {\n\n\t\t\t\t\t\tuv.x = Math.ceil( uv.x ) - uv.x;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tuv.x = uv.x - Math.floor( uv.x );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( uv.y < 0 || uv.y > 1 ) {\n\n\t\t\tswitch ( this.wrapT ) {\n\n\t\t\t\tcase RepeatWrapping:\n\n\t\t\t\t\tuv.y = uv.y - Math.floor( uv.y );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase ClampToEdgeWrapping:\n\n\t\t\t\t\tuv.y = uv.y < 0 ? 0 : 1;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase MirroredRepeatWrapping:\n\n\t\t\t\t\tif ( Math.abs( Math.floor( uv.y ) % 2 ) === 1 ) {\n\n\t\t\t\t\t\tuv.y = Math.ceil( uv.y ) - uv.y;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tuv.y = uv.y - Math.floor( uv.y );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( this.flipY ) {\n\n\t\t\tuv.y = 1 - uv.y;\n\n\t\t}\n\n\t\treturn uv;\n\n\t}\n\n\t/**\n\t * Setting this property to `true` indicates the engine the texture\n\t * must be updated in the next render. This triggers a texture upload\n\t * to the GPU and ensures correct texture parameter configuration.\n\t *\n\t * @type {boolean}\n\t * @default false\n\t * @param {boolean} value\n\t */\n\tset needsUpdate( value ) {\n\n\t\tif ( value === true ) {\n\n\t\t\tthis.version ++;\n\t\t\tthis.source.needsUpdate = true;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Setting this property to `true` indicates the engine the PMREM\n\t * must be regenerated.\n\t *\n\t * @type {boolean}\n\t * @default false\n\t * @param {boolean} value\n\t */\n\tset needsPMREMUpdate( value ) {\n\n\t\tif ( value === true ) {\n\n\t\t\tthis.pmremVersion ++;\n\n\t\t}\n\n\t}\n\n}\n\n/**\n * The default image for all textures.\n *\n * @static\n * @type {?Image}\n * @default null\n */\nTexture.DEFAULT_IMAGE = null;\n\n/**\n * The default mapping for all textures.\n *\n * @static\n * @type {number}\n * @default UVMapping\n */\nTexture.DEFAULT_MAPPING = UVMapping;\n\n/**\n * The default anisotropy value for all textures.\n *\n * @static\n * @type {number}\n * @default 1\n */\nTexture.DEFAULT_ANISOTROPY = 1;\n\n/**\n * Class representing a 4D vector. A 4D vector is an ordered quadruplet of numbers\n * (labeled x, y, z and w), which can be used to represent a number of things, such as:\n *\n * - A point in 4D space.\n * - A direction and length in 4D space. In three.js the length will\n * always be the Euclidean distance(straight-line distance) from `(0, 0, 0, 0)` to `(x, y, z, w)`\n * and the direction is also measured from `(0, 0, 0, 0)` towards `(x, y, z, w)`.\n * - Any arbitrary ordered quadruplet of numbers.\n *\n * There are other things a 4D vector can be used to represent, however these\n * are the most common uses in *three.js*.\n *\n * Iterating through a vector instance will yield its components `(x, y, z, w)` in\n * the corresponding order.\n * ```js\n * const a = new THREE.Vector4( 0, 1, 0, 0 );\n *\n * //no arguments; will be initialised to (0, 0, 0, 1)\n * const b = new THREE.Vector4( );\n *\n * const d = a.dot( b );\n * ```\n */\nclass Vector4 {\n\n\t/**\n\t * Constructs a new 4D vector.\n\t *\n\t * @param {number} [x=0] - The x value of this vector.\n\t * @param {number} [y=0] - The y value of this vector.\n\t * @param {number} [z=0] - The z value of this vector.\n\t * @param {number} [w=1] - The w value of this vector.\n\t */\n\tconstructor( x = 0, y = 0, z = 0, w = 1 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tVector4.prototype.isVector4 = true;\n\n\t\t/**\n\t\t * The x value of this vector.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.x = x;\n\n\t\t/**\n\t\t * The y value of this vector.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.y = y;\n\n\t\t/**\n\t\t * The z value of this vector.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.z = z;\n\n\t\t/**\n\t\t * The w value of this vector.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.w = w;\n\n\t}\n\n\t/**\n\t * Alias for {@link Vector4#z}.\n\t *\n\t * @type {number}\n\t */\n\tget width() {\n\n\t\treturn this.z;\n\n\t}\n\n\tset width( value ) {\n\n\t\tthis.z = value;\n\n\t}\n\n\t/**\n\t * Alias for {@link Vector4#w}.\n\t *\n\t * @type {number}\n\t */\n\tget height() {\n\n\t\treturn this.w;\n\n\t}\n\n\tset height( value ) {\n\n\t\tthis.w = value;\n\n\t}\n\n\t/**\n\t * Sets the vector components.\n\t *\n\t * @param {number} x - The value of the x component.\n\t * @param {number} y - The value of the y component.\n\t * @param {number} z - The value of the z component.\n\t * @param {number} w - The value of the w component.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tset( x, y, z, w ) {\n\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tthis.z = z;\n\t\tthis.w = w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector components to the same value.\n\t *\n\t * @param {number} scalar - The value to set for all vector components.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetScalar( scalar ) {\n\n\t\tthis.x = scalar;\n\t\tthis.y = scalar;\n\t\tthis.z = scalar;\n\t\tthis.w = scalar;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector's x component to the given value\n\t *\n\t * @param {number} x - The value to set.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetX( x ) {\n\n\t\tthis.x = x;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector's y component to the given value\n\t *\n\t * @param {number} y - The value to set.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetY( y ) {\n\n\t\tthis.y = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector's z component to the given value\n\t *\n\t * @param {number} z - The value to set.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetZ( z ) {\n\n\t\tthis.z = z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector's w component to the given value\n\t *\n\t * @param {number} w - The value to set.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetW( w ) {\n\n\t\tthis.w = w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Allows to set a vector component with an index.\n\t *\n\t * @param {number} index - The component index. `0` equals to x, `1` equals to y,\n\t * `2` equals to z, `3` equals to w.\n\t * @param {number} value - The value to set.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetComponent( index, value ) {\n\n\t\tswitch ( index ) {\n\n\t\t\tcase 0: this.x = value; break;\n\t\t\tcase 1: this.y = value; break;\n\t\t\tcase 2: this.z = value; break;\n\t\t\tcase 3: this.w = value; break;\n\t\t\tdefault: throw new Error( 'index is out of range: ' + index );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the value of the vector component which matches the given index.\n\t *\n\t * @param {number} index - The component index. `0` equals to x, `1` equals to y,\n\t * `2` equals to z, `3` equals to w.\n\t * @return {number} A vector component value.\n\t */\n\tgetComponent( index ) {\n\n\t\tswitch ( index ) {\n\n\t\t\tcase 0: return this.x;\n\t\t\tcase 1: return this.y;\n\t\t\tcase 2: return this.z;\n\t\t\tcase 3: return this.w;\n\t\t\tdefault: throw new Error( 'index is out of range: ' + index );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns a new vector with copied values from this instance.\n\t *\n\t * @return {Vector4} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor( this.x, this.y, this.z, this.w );\n\n\t}\n\n\t/**\n\t * Copies the values of the given vector to this instance.\n\t *\n\t * @param {Vector3|Vector4} v - The vector to copy.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tcopy( v ) {\n\n\t\tthis.x = v.x;\n\t\tthis.y = v.y;\n\t\tthis.z = v.z;\n\t\tthis.w = ( v.w !== undefined ) ? v.w : 1;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given vector to this instance.\n\t *\n\t * @param {Vector4} v - The vector to add.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tadd( v ) {\n\n\t\tthis.x += v.x;\n\t\tthis.y += v.y;\n\t\tthis.z += v.z;\n\t\tthis.w += v.w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given scalar value to all components of this instance.\n\t *\n\t * @param {number} s - The scalar to add.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\taddScalar( s ) {\n\n\t\tthis.x += s;\n\t\tthis.y += s;\n\t\tthis.z += s;\n\t\tthis.w += s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given vectors and stores the result in this instance.\n\t *\n\t * @param {Vector4} a - The first vector.\n\t * @param {Vector4} b - The second vector.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\taddVectors( a, b ) {\n\n\t\tthis.x = a.x + b.x;\n\t\tthis.y = a.y + b.y;\n\t\tthis.z = a.z + b.z;\n\t\tthis.w = a.w + b.w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given vector scaled by the given factor to this instance.\n\t *\n\t * @param {Vector4} v - The vector.\n\t * @param {number} s - The factor that scales `v`.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\taddScaledVector( v, s ) {\n\n\t\tthis.x += v.x * s;\n\t\tthis.y += v.y * s;\n\t\tthis.z += v.z * s;\n\t\tthis.w += v.w * s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the given vector from this instance.\n\t *\n\t * @param {Vector4} v - The vector to subtract.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsub( v ) {\n\n\t\tthis.x -= v.x;\n\t\tthis.y -= v.y;\n\t\tthis.z -= v.z;\n\t\tthis.w -= v.w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the given scalar value from all components of this instance.\n\t *\n\t * @param {number} s - The scalar to subtract.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsubScalar( s ) {\n\n\t\tthis.x -= s;\n\t\tthis.y -= s;\n\t\tthis.z -= s;\n\t\tthis.w -= s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the given vectors and stores the result in this instance.\n\t *\n\t * @param {Vector4} a - The first vector.\n\t * @param {Vector4} b - The second vector.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsubVectors( a, b ) {\n\n\t\tthis.x = a.x - b.x;\n\t\tthis.y = a.y - b.y;\n\t\tthis.z = a.z - b.z;\n\t\tthis.w = a.w - b.w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the given vector with this instance.\n\t *\n\t * @param {Vector4} v - The vector to multiply.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tmultiply( v ) {\n\n\t\tthis.x *= v.x;\n\t\tthis.y *= v.y;\n\t\tthis.z *= v.z;\n\t\tthis.w *= v.w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the given scalar value with all components of this instance.\n\t *\n\t * @param {number} scalar - The scalar to multiply.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tmultiplyScalar( scalar ) {\n\n\t\tthis.x *= scalar;\n\t\tthis.y *= scalar;\n\t\tthis.z *= scalar;\n\t\tthis.w *= scalar;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies this vector with the given 4x4 matrix.\n\t *\n\t * @param {Matrix4} m - The 4x4 matrix.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tapplyMatrix4( m ) {\n\n\t\tconst x = this.x, y = this.y, z = this.z, w = this.w;\n\t\tconst e = m.elements;\n\n\t\tthis.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] * w;\n\t\tthis.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] * w;\n\t\tthis.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] * w;\n\t\tthis.w = e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] * w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Divides this instance by the given vector.\n\t *\n\t * @param {Vector4} v - The vector to divide.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tdivide( v ) {\n\n\t\tthis.x /= v.x;\n\t\tthis.y /= v.y;\n\t\tthis.z /= v.z;\n\t\tthis.w /= v.w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Divides this vector by the given scalar.\n\t *\n\t * @param {number} scalar - The scalar to divide.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tdivideScalar( scalar ) {\n\n\t\treturn this.multiplyScalar( 1 / scalar );\n\n\t}\n\n\t/**\n\t * Sets the x, y and z components of this\n\t * vector to the quaternion's axis and w to the angle.\n\t *\n\t * @param {Quaternion} q - The Quaternion to set.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetAxisAngleFromQuaternion( q ) {\n\n\t\t// http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/index.htm\n\n\t\t// q is assumed to be normalized\n\n\t\tthis.w = 2 * Math.acos( q.w );\n\n\t\tconst s = Math.sqrt( 1 - q.w * q.w );\n\n\t\tif ( s < 0.0001 ) {\n\n\t\t\tthis.x = 1;\n\t\t\tthis.y = 0;\n\t\t\tthis.z = 0;\n\n\t\t} else {\n\n\t\t\tthis.x = q.x / s;\n\t\t\tthis.y = q.y / s;\n\t\t\tthis.z = q.z / s;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the x, y and z components of this\n\t * vector to the axis of rotation and w to the angle.\n\t *\n\t * @param {Matrix4} m - A 4x4 matrix of which the upper left 3x3 matrix is a pure rotation matrix.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetAxisAngleFromRotationMatrix( m ) {\n\n\t\t// http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToAngle/index.htm\n\n\t\t// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)\n\n\t\tlet angle, x, y, z; // variables for result\n\t\tconst epsilon = 0.01,\t\t// margin to allow for rounding errors\n\t\t\tepsilon2 = 0.1,\t\t// margin to distinguish between 0 and 180 degrees\n\n\t\t\tte = m.elements,\n\n\t\t\tm11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ],\n\t\t\tm21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ],\n\t\t\tm31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ];\n\n\t\tif ( ( Math.abs( m12 - m21 ) < epsilon ) &&\n\t\t     ( Math.abs( m13 - m31 ) < epsilon ) &&\n\t\t     ( Math.abs( m23 - m32 ) < epsilon ) ) {\n\n\t\t\t// singularity found\n\t\t\t// first check for identity matrix which must have +1 for all terms\n\t\t\t// in leading diagonal and zero in other terms\n\n\t\t\tif ( ( Math.abs( m12 + m21 ) < epsilon2 ) &&\n\t\t\t     ( Math.abs( m13 + m31 ) < epsilon2 ) &&\n\t\t\t     ( Math.abs( m23 + m32 ) < epsilon2 ) &&\n\t\t\t     ( Math.abs( m11 + m22 + m33 - 3 ) < epsilon2 ) ) {\n\n\t\t\t\t// this singularity is identity matrix so angle = 0\n\n\t\t\t\tthis.set( 1, 0, 0, 0 );\n\n\t\t\t\treturn this; // zero angle, arbitrary axis\n\n\t\t\t}\n\n\t\t\t// otherwise this singularity is angle = 180\n\n\t\t\tangle = Math.PI;\n\n\t\t\tconst xx = ( m11 + 1 ) / 2;\n\t\t\tconst yy = ( m22 + 1 ) / 2;\n\t\t\tconst zz = ( m33 + 1 ) / 2;\n\t\t\tconst xy = ( m12 + m21 ) / 4;\n\t\t\tconst xz = ( m13 + m31 ) / 4;\n\t\t\tconst yz = ( m23 + m32 ) / 4;\n\n\t\t\tif ( ( xx > yy ) && ( xx > zz ) ) {\n\n\t\t\t\t// m11 is the largest diagonal term\n\n\t\t\t\tif ( xx < epsilon ) {\n\n\t\t\t\t\tx = 0;\n\t\t\t\t\ty = 0.707106781;\n\t\t\t\t\tz = 0.707106781;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tx = Math.sqrt( xx );\n\t\t\t\t\ty = xy / x;\n\t\t\t\t\tz = xz / x;\n\n\t\t\t\t}\n\n\t\t\t} else if ( yy > zz ) {\n\n\t\t\t\t// m22 is the largest diagonal term\n\n\t\t\t\tif ( yy < epsilon ) {\n\n\t\t\t\t\tx = 0.707106781;\n\t\t\t\t\ty = 0;\n\t\t\t\t\tz = 0.707106781;\n\n\t\t\t\t} else {\n\n\t\t\t\t\ty = Math.sqrt( yy );\n\t\t\t\t\tx = xy / y;\n\t\t\t\t\tz = yz / y;\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\t// m33 is the largest diagonal term so base result on this\n\n\t\t\t\tif ( zz < epsilon ) {\n\n\t\t\t\t\tx = 0.707106781;\n\t\t\t\t\ty = 0.707106781;\n\t\t\t\t\tz = 0;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tz = Math.sqrt( zz );\n\t\t\t\t\tx = xz / z;\n\t\t\t\t\ty = yz / z;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis.set( x, y, z, angle );\n\n\t\t\treturn this; // return 180 deg rotation\n\n\t\t}\n\n\t\t// as we have reached here there are no singularities so we can handle normally\n\n\t\tlet s = Math.sqrt( ( m32 - m23 ) * ( m32 - m23 ) +\n\t\t\t( m13 - m31 ) * ( m13 - m31 ) +\n\t\t\t( m21 - m12 ) * ( m21 - m12 ) ); // used to normalize\n\n\t\tif ( Math.abs( s ) < 0.001 ) s = 1;\n\n\t\t// prevent divide by zero, should not happen if matrix is orthogonal and should be\n\t\t// caught by singularity test above, but I've left it in just in case\n\n\t\tthis.x = ( m32 - m23 ) / s;\n\t\tthis.y = ( m13 - m31 ) / s;\n\t\tthis.z = ( m21 - m12 ) / s;\n\t\tthis.w = Math.acos( ( m11 + m22 + m33 - 1 ) / 2 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the vector components to the position elements of the\n\t * given transformation matrix.\n\t *\n\t * @param {Matrix4} m - The 4x4 matrix.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetFromMatrixPosition( m ) {\n\n\t\tconst e = m.elements;\n\n\t\tthis.x = e[ 12 ];\n\t\tthis.y = e[ 13 ];\n\t\tthis.z = e[ 14 ];\n\t\tthis.w = e[ 15 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x, y, z or w value is greater than the given vector's x, y, z or w\n\t * value, replace that value with the corresponding min value.\n\t *\n\t * @param {Vector4} v - The vector.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tmin( v ) {\n\n\t\tthis.x = Math.min( this.x, v.x );\n\t\tthis.y = Math.min( this.y, v.y );\n\t\tthis.z = Math.min( this.z, v.z );\n\t\tthis.w = Math.min( this.w, v.w );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x, y, z or w value is less than the given vector's x, y, z or w\n\t * value, replace that value with the corresponding max value.\n\t *\n\t * @param {Vector4} v - The vector.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tmax( v ) {\n\n\t\tthis.x = Math.max( this.x, v.x );\n\t\tthis.y = Math.max( this.y, v.y );\n\t\tthis.z = Math.max( this.z, v.z );\n\t\tthis.w = Math.max( this.w, v.w );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x, y, z or w value is greater than the max vector's x, y, z or w\n\t * value, it is replaced by the corresponding value.\n\t * If this vector's x, y, z or w value is less than the min vector's x, y, z or w value,\n\t * it is replaced by the corresponding value.\n\t *\n\t * @param {Vector4} min - The minimum x, y and z values.\n\t * @param {Vector4} max - The maximum x, y and z values in the desired range.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tclamp( min, max ) {\n\n\t\t// assumes min < max, componentwise\n\n\t\tthis.x = clamp( this.x, min.x, max.x );\n\t\tthis.y = clamp( this.y, min.y, max.y );\n\t\tthis.z = clamp( this.z, min.z, max.z );\n\t\tthis.w = clamp( this.w, min.w, max.w );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's x, y, z or w values are greater than the max value, they are\n\t * replaced by the max value.\n\t * If this vector's x, y, z or w values are less than the min value, they are\n\t * replaced by the min value.\n\t *\n\t * @param {number} minVal - The minimum value the components will be clamped to.\n\t * @param {number} maxVal - The maximum value the components will be clamped to.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tclampScalar( minVal, maxVal ) {\n\n\t\tthis.x = clamp( this.x, minVal, maxVal );\n\t\tthis.y = clamp( this.y, minVal, maxVal );\n\t\tthis.z = clamp( this.z, minVal, maxVal );\n\t\tthis.w = clamp( this.w, minVal, maxVal );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * If this vector's length is greater than the max value, it is replaced by\n\t * the max value.\n\t * If this vector's length is less than the min value, it is replaced by the\n\t * min value.\n\t *\n\t * @param {number} min - The minimum value the vector length will be clamped to.\n\t * @param {number} max - The maximum value the vector length will be clamped to.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tclampLength( min, max ) {\n\n\t\tconst length = this.length();\n\n\t\treturn this.divideScalar( length || 1 ).multiplyScalar( clamp( length, min, max ) );\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded down to the nearest integer value.\n\t *\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tfloor() {\n\n\t\tthis.x = Math.floor( this.x );\n\t\tthis.y = Math.floor( this.y );\n\t\tthis.z = Math.floor( this.z );\n\t\tthis.w = Math.floor( this.w );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded up to the nearest integer value.\n\t *\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tceil() {\n\n\t\tthis.x = Math.ceil( this.x );\n\t\tthis.y = Math.ceil( this.y );\n\t\tthis.z = Math.ceil( this.z );\n\t\tthis.w = Math.ceil( this.w );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded to the nearest integer value\n\t *\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tround() {\n\n\t\tthis.x = Math.round( this.x );\n\t\tthis.y = Math.round( this.y );\n\t\tthis.z = Math.round( this.z );\n\t\tthis.w = Math.round( this.w );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The components of this vector are rounded towards zero (up if negative,\n\t * down if positive) to an integer value.\n\t *\n\t * @return {Vector4} A reference to this vector.\n\t */\n\troundToZero() {\n\n\t\tthis.x = Math.trunc( this.x );\n\t\tthis.y = Math.trunc( this.y );\n\t\tthis.z = Math.trunc( this.z );\n\t\tthis.w = Math.trunc( this.w );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Inverts this vector - i.e. sets x = -x, y = -y, z = -z, w = -w.\n\t *\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tnegate() {\n\n\t\tthis.x = - this.x;\n\t\tthis.y = - this.y;\n\t\tthis.z = - this.z;\n\t\tthis.w = - this.w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Calculates the dot product of the given vector with this instance.\n\t *\n\t * @param {Vector4} v - The vector to compute the dot product with.\n\t * @return {number} The result of the dot product.\n\t */\n\tdot( v ) {\n\n\t\treturn this.x * v.x + this.y * v.y + this.z * v.z + this.w * v.w;\n\n\t}\n\n\t/**\n\t * Computes the square of the Euclidean length (straight-line length) from\n\t * (0, 0, 0, 0) to (x, y, z, w). If you are comparing the lengths of vectors, you should\n\t * compare the length squared instead as it is slightly more efficient to calculate.\n\t *\n\t * @return {number} The square length of this vector.\n\t */\n\tlengthSq() {\n\n\t\treturn this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w;\n\n\t}\n\n\t/**\n\t * Computes the  Euclidean length (straight-line length) from (0, 0, 0, 0) to (x, y, z, w).\n\t *\n\t * @return {number} The length of this vector.\n\t */\n\tlength() {\n\n\t\treturn Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w );\n\n\t}\n\n\t/**\n\t * Computes the Manhattan length of this vector.\n\t *\n\t * @return {number} The length of this vector.\n\t */\n\tmanhattanLength() {\n\n\t\treturn Math.abs( this.x ) + Math.abs( this.y ) + Math.abs( this.z ) + Math.abs( this.w );\n\n\t}\n\n\t/**\n\t * Converts this vector to a unit vector - that is, sets it equal to a vector\n\t * with the same direction as this one, but with a vector length of `1`.\n\t *\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tnormalize() {\n\n\t\treturn this.divideScalar( this.length() || 1 );\n\n\t}\n\n\t/**\n\t * Sets this vector to a vector with the same direction as this one, but\n\t * with the specified length.\n\t *\n\t * @param {number} length - The new length of this vector.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tsetLength( length ) {\n\n\t\treturn this.normalize().multiplyScalar( length );\n\n\t}\n\n\t/**\n\t * Linearly interpolates between the given vector and this instance, where\n\t * alpha is the percent distance along the line - alpha = 0 will be this\n\t * vector, and alpha = 1 will be the given one.\n\t *\n\t * @param {Vector4} v - The vector to interpolate towards.\n\t * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tlerp( v, alpha ) {\n\n\t\tthis.x += ( v.x - this.x ) * alpha;\n\t\tthis.y += ( v.y - this.y ) * alpha;\n\t\tthis.z += ( v.z - this.z ) * alpha;\n\t\tthis.w += ( v.w - this.w ) * alpha;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Linearly interpolates between the given vectors, where alpha is the percent\n\t * distance along the line - alpha = 0 will be first vector, and alpha = 1 will\n\t * be the second one. The result is stored in this instance.\n\t *\n\t * @param {Vector4} v1 - The first vector.\n\t * @param {Vector4} v2 - The second vector.\n\t * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tlerpVectors( v1, v2, alpha ) {\n\n\t\tthis.x = v1.x + ( v2.x - v1.x ) * alpha;\n\t\tthis.y = v1.y + ( v2.y - v1.y ) * alpha;\n\t\tthis.z = v1.z + ( v2.z - v1.z ) * alpha;\n\t\tthis.w = v1.w + ( v2.w - v1.w ) * alpha;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this vector is equal with the given one.\n\t *\n\t * @param {Vector4} v - The vector to test for equality.\n\t * @return {boolean} Whether this vector is equal with the given one.\n\t */\n\tequals( v ) {\n\n\t\treturn ( ( v.x === this.x ) && ( v.y === this.y ) && ( v.z === this.z ) && ( v.w === this.w ) );\n\n\t}\n\n\t/**\n\t * Sets this vector's x value to be `array[ offset ]`, y value to be `array[ offset + 1 ]`,\n\t * z value to be `array[ offset + 2 ]`, w value to be `array[ offset + 3 ]`.\n\t *\n\t * @param {Array<number>} array - An array holding the vector component values.\n\t * @param {number} [offset=0] - The offset into the array.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tfromArray( array, offset = 0 ) {\n\n\t\tthis.x = array[ offset ];\n\t\tthis.y = array[ offset + 1 ];\n\t\tthis.z = array[ offset + 2 ];\n\t\tthis.w = array[ offset + 3 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Writes the components of this vector to the given array. If no array is provided,\n\t * the method returns a new instance.\n\t *\n\t * @param {Array<number>} [array=[]] - The target array holding the vector components.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Array<number>} The vector components.\n\t */\n\ttoArray( array = [], offset = 0 ) {\n\n\t\tarray[ offset ] = this.x;\n\t\tarray[ offset + 1 ] = this.y;\n\t\tarray[ offset + 2 ] = this.z;\n\t\tarray[ offset + 3 ] = this.w;\n\n\t\treturn array;\n\n\t}\n\n\t/**\n\t * Sets the components of this vector from the given buffer attribute.\n\t *\n\t * @param {BufferAttribute} attribute - The buffer attribute holding vector data.\n\t * @param {number} index - The index into the attribute.\n\t * @return {Vector4} A reference to this vector.\n\t */\n\tfromBufferAttribute( attribute, index ) {\n\n\t\tthis.x = attribute.getX( index );\n\t\tthis.y = attribute.getY( index );\n\t\tthis.z = attribute.getZ( index );\n\t\tthis.w = attribute.getW( index );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets each component of this vector to a pseudo-random value between `0` and\n\t * `1`, excluding `1`.\n\t *\n\t * @return {Vector4} A reference to this vector.\n\t */\n\trandom() {\n\n\t\tthis.x = Math.random();\n\t\tthis.y = Math.random();\n\t\tthis.z = Math.random();\n\t\tthis.w = Math.random();\n\n\t\treturn this;\n\n\t}\n\n\t*[ Symbol.iterator ]() {\n\n\t\tyield this.x;\n\t\tyield this.y;\n\t\tyield this.z;\n\t\tyield this.w;\n\n\t}\n\n}\n\n/**\n * A render target is a buffer where the video card draws pixels for a scene\n * that is being rendered in the background. It is used in different effects,\n * such as applying postprocessing to a rendered image before displaying it\n * on the screen.\n *\n * @augments EventDispatcher\n */\nclass RenderTarget extends EventDispatcher {\n\n\t/**\n\t * Render target options.\n\t *\n\t * @typedef {Object} RenderTarget~Options\n\t * @property {boolean} [generateMipmaps=false] - Whether to generate mipmaps or not.\n\t * @property {number} [magFilter=LinearFilter] - The mag filter.\n\t * @property {number} [minFilter=LinearFilter] - The min filter.\n\t * @property {number} [format=RGBAFormat] - The texture format.\n\t * @property {number} [type=UnsignedByteType] - The texture type.\n\t * @property {?string} [internalFormat=null] - The texture's internal format.\n\t * @property {number} [wrapS=ClampToEdgeWrapping] - The texture's uv wrapping mode.\n\t * @property {number} [wrapT=ClampToEdgeWrapping] - The texture's uv wrapping mode.\n\t * @property {number} [anisotropy=1] - The texture's anisotropy value.\n\t * @property {string} [colorSpace=NoColorSpace] - The texture's color space.\n\t * @property {boolean} [depthBuffer=true] - Whether to allocate a depth buffer or not.\n\t * @property {boolean} [stencilBuffer=false] - Whether to allocate a stencil buffer or not.\n\t * @property {boolean} [resolveDepthBuffer=true] - Whether to resolve the depth buffer or not.\n\t * @property {boolean} [resolveStencilBuffer=true] - Whether  to resolve the stencil buffer or not.\n\t * @property {?Texture} [depthTexture=null] - Reference to a depth texture.\n\t * @property {number} [samples=0] - The MSAA samples count.\n\t * @property {number} [count=1] - Defines the number of color attachments . Must be at least `1`.\n\t * @property {number} [depth=1] - The texture depth.\n\t * @property {boolean} [multiview=false] - Whether this target is used for multiview rendering.\n\t */\n\n\t/**\n\t * Constructs a new render target.\n\t *\n\t * @param {number} [width=1] - The width of the render target.\n\t * @param {number} [height=1] - The height of the render target.\n\t * @param {RenderTarget~Options} [options] - The configuration object.\n\t */\n\tconstructor( width = 1, height = 1, options = {} ) {\n\n\t\tsuper();\n\n\t\toptions = Object.assign( {\n\t\t\tgenerateMipmaps: false,\n\t\t\tinternalFormat: null,\n\t\t\tminFilter: LinearFilter,\n\t\t\tdepthBuffer: true,\n\t\t\tstencilBuffer: false,\n\t\t\tresolveDepthBuffer: true,\n\t\t\tresolveStencilBuffer: true,\n\t\t\tdepthTexture: null,\n\t\t\tsamples: 0,\n\t\t\tcount: 1,\n\t\t\tdepth: 1,\n\t\t\tmultiview: false\n\t\t}, options );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isRenderTarget = true;\n\n\t\t/**\n\t\t * The width of the render target.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.width = width;\n\n\t\t/**\n\t\t * The height of the render target.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.height = height;\n\n\t\t/**\n\t\t * The depth of the render target.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.depth = options.depth;\n\n\t\t/**\n\t\t * A rectangular area inside the render target's viewport. Fragments that are\n\t\t * outside the area will be discarded.\n\t\t *\n\t\t * @type {Vector4}\n\t\t * @default (0,0,width,height)\n\t\t */\n\t\tthis.scissor = new Vector4( 0, 0, width, height );\n\n\t\t/**\n\t\t * Indicates whether the scissor test should be enabled when rendering into\n\t\t * this render target or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.scissorTest = false;\n\n\t\t/**\n\t\t * A rectangular area representing the render target's viewport.\n\t\t *\n\t\t * @type {Vector4}\n\t\t * @default (0,0,width,height)\n\t\t */\n\t\tthis.viewport = new Vector4( 0, 0, width, height );\n\n\t\tconst image = { width: width, height: height, depth: options.depth };\n\n\t\tconst texture = new Texture( image );\n\n\t\t/**\n\t\t * An array of textures. Each color attachment is represented as a separate texture.\n\t\t * Has at least a single entry for the default color attachment.\n\t\t *\n\t\t * @type {Array<Texture>}\n\t\t */\n\t\tthis.textures = [];\n\n\t\tconst count = options.count;\n\t\tfor ( let i = 0; i < count; i ++ ) {\n\n\t\t\tthis.textures[ i ] = texture.clone();\n\t\t\tthis.textures[ i ].isRenderTargetTexture = true;\n\t\t\tthis.textures[ i ].renderTarget = this;\n\n\t\t}\n\n\t\tthis._setTextureOptions( options );\n\n\t\t/**\n\t\t * Whether to allocate a depth buffer or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.depthBuffer = options.depthBuffer;\n\n\t\t/**\n\t\t * Whether to allocate a stencil buffer or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.stencilBuffer = options.stencilBuffer;\n\n\t\t/**\n\t\t * Whether to resolve the depth buffer or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.resolveDepthBuffer = options.resolveDepthBuffer;\n\n\t\t/**\n\t\t * Whether to resolve the stencil buffer or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.resolveStencilBuffer = options.resolveStencilBuffer;\n\n\t\tthis._depthTexture = null;\n\t\tthis.depthTexture = options.depthTexture;\n\n\t\t/**\n\t\t * The number of MSAA samples.\n\t\t *\n\t\t * A value of `0` disables MSAA.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.samples = options.samples;\n\n\t\t/**\n\t\t * Whether to this target is used in multiview rendering.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.multiview = options.multiview;\n\n\t}\n\n\t_setTextureOptions( options = {} ) {\n\n\t\tconst values = {\n\t\t\tminFilter: LinearFilter,\n\t\t\tgenerateMipmaps: false,\n\t\t\tflipY: false,\n\t\t\tinternalFormat: null\n\t\t};\n\n\t\tif ( options.mapping !== undefined ) values.mapping = options.mapping;\n\t\tif ( options.wrapS !== undefined ) values.wrapS = options.wrapS;\n\t\tif ( options.wrapT !== undefined ) values.wrapT = options.wrapT;\n\t\tif ( options.wrapR !== undefined ) values.wrapR = options.wrapR;\n\t\tif ( options.magFilter !== undefined ) values.magFilter = options.magFilter;\n\t\tif ( options.minFilter !== undefined ) values.minFilter = options.minFilter;\n\t\tif ( options.format !== undefined ) values.format = options.format;\n\t\tif ( options.type !== undefined ) values.type = options.type;\n\t\tif ( options.anisotropy !== undefined ) values.anisotropy = options.anisotropy;\n\t\tif ( options.colorSpace !== undefined ) values.colorSpace = options.colorSpace;\n\t\tif ( options.flipY !== undefined ) values.flipY = options.flipY;\n\t\tif ( options.generateMipmaps !== undefined ) values.generateMipmaps = options.generateMipmaps;\n\t\tif ( options.internalFormat !== undefined ) values.internalFormat = options.internalFormat;\n\n\t\tfor ( let i = 0; i < this.textures.length; i ++ ) {\n\n\t\t\tconst texture = this.textures[ i ];\n\t\t\ttexture.setValues( values );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * The texture representing the default color attachment.\n\t *\n\t * @type {Texture}\n\t */\n\tget texture() {\n\n\t\treturn this.textures[ 0 ];\n\n\t}\n\n\tset texture( value ) {\n\n\t\tthis.textures[ 0 ] = value;\n\n\t}\n\n\tset depthTexture( current ) {\n\n\t\tif ( this._depthTexture !== null ) this._depthTexture.renderTarget = null;\n\t\tif ( current !== null ) current.renderTarget = this;\n\n\t\tthis._depthTexture = current;\n\n\t}\n\n\t/**\n\t * Instead of saving the depth in a renderbuffer, a texture\n\t * can be used instead which is useful for further processing\n\t * e.g. in context of post-processing.\n\t *\n\t * @type {?DepthTexture}\n\t * @default null\n\t */\n\tget depthTexture() {\n\n\t\treturn this._depthTexture;\n\n\t}\n\n\t/**\n\t * Sets the size of this render target.\n\t *\n\t * @param {number} width - The width.\n\t * @param {number} height - The height.\n\t * @param {number} [depth=1] - The depth.\n\t */\n\tsetSize( width, height, depth = 1 ) {\n\n\t\tif ( this.width !== width || this.height !== height || this.depth !== depth ) {\n\n\t\t\tthis.width = width;\n\t\t\tthis.height = height;\n\t\t\tthis.depth = depth;\n\n\t\t\tfor ( let i = 0, il = this.textures.length; i < il; i ++ ) {\n\n\t\t\t\tthis.textures[ i ].image.width = width;\n\t\t\t\tthis.textures[ i ].image.height = height;\n\t\t\t\tthis.textures[ i ].image.depth = depth;\n\n\t\t\t\tif ( this.textures[ i ].isData3DTexture !== true ) { // Fix for #31693\n\n\t\t\t\t\t// TODO: Reconsider setting isArrayTexture flag here and in the ctor of Texture.\n\t\t\t\t\t// Maybe a method `isArrayTexture()` or just a getter could replace a flag since\n\t\t\t\t\t// both are evaluated on each call?\n\n\t\t\t\t\tthis.textures[ i ].isArrayTexture = this.textures[ i ].image.depth > 1;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis.dispose();\n\n\t\t}\n\n\t\tthis.viewport.set( 0, 0, width, height );\n\t\tthis.scissor.set( 0, 0, width, height );\n\n\t}\n\n\t/**\n\t * Returns a new render target with copied values from this instance.\n\t *\n\t * @return {RenderTarget} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Copies the settings of the given render target. This is a structural copy so\n\t * no resources are shared between render targets after the copy. That includes\n\t * all MRT textures and the depth texture.\n\t *\n\t * @param {RenderTarget} source - The render target to copy.\n\t * @return {RenderTarget} A reference to this instance.\n\t */\n\tcopy( source ) {\n\n\t\tthis.width = source.width;\n\t\tthis.height = source.height;\n\t\tthis.depth = source.depth;\n\n\t\tthis.scissor.copy( source.scissor );\n\t\tthis.scissorTest = source.scissorTest;\n\n\t\tthis.viewport.copy( source.viewport );\n\n\t\tthis.textures.length = 0;\n\n\t\tfor ( let i = 0, il = source.textures.length; i < il; i ++ ) {\n\n\t\t\tthis.textures[ i ] = source.textures[ i ].clone();\n\t\t\tthis.textures[ i ].isRenderTargetTexture = true;\n\t\t\tthis.textures[ i ].renderTarget = this;\n\n\t\t\t// ensure image object is not shared, see #20328\n\n\t\t\tconst image = Object.assign( {}, source.textures[ i ].image );\n\t\t\tthis.textures[ i ].source = new Source( image );\n\n\t\t}\n\n\t\tthis.depthBuffer = source.depthBuffer;\n\t\tthis.stencilBuffer = source.stencilBuffer;\n\n\t\tthis.resolveDepthBuffer = source.resolveDepthBuffer;\n\t\tthis.resolveStencilBuffer = source.resolveStencilBuffer;\n\n\t\tif ( source.depthTexture !== null ) this.depthTexture = source.depthTexture.clone();\n\n\t\tthis.samples = source.samples;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t *\n\t * @fires RenderTarget#dispose\n\t */\n\tdispose() {\n\n\t\tthis.dispatchEvent( { type: 'dispose' } );\n\n\t}\n\n}\n\n/**\n * A render target used in context of {@link WebGLRenderer}.\n *\n * @augments RenderTarget\n */\nclass WebGLRenderTarget extends RenderTarget {\n\n\t/**\n\t * Constructs a new 3D render target.\n\t *\n\t * @param {number} [width=1] - The width of the render target.\n\t * @param {number} [height=1] - The height of the render target.\n\t * @param {RenderTarget~Options} [options] - The configuration object.\n\t */\n\tconstructor( width = 1, height = 1, options = {} ) {\n\n\t\tsuper( width, height, options );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isWebGLRenderTarget = true;\n\n\t}\n\n}\n\n/**\n * Creates an array of textures directly from raw buffer data.\n *\n * @augments Texture\n */\nclass DataArrayTexture extends Texture {\n\n\t/**\n\t * Constructs a new data array texture.\n\t *\n\t * @param {?TypedArray} [data=null] - The buffer data.\n\t * @param {number} [width=1] - The width of the texture.\n\t * @param {number} [height=1] - The height of the texture.\n\t * @param {number} [depth=1] - The depth of the texture.\n\t */\n\tconstructor( data = null, width = 1, height = 1, depth = 1 ) {\n\n\t\tsuper( null );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isDataArrayTexture = true;\n\n\t\t/**\n\t\t * The image definition of a data texture.\n\t\t *\n\t\t * @type {{data:TypedArray,width:number,height:number,depth:number}}\n\t\t */\n\t\tthis.image = { data, width, height, depth };\n\n\t\t/**\n\t\t * How the texture is sampled when a texel covers more than one pixel.\n\t\t *\n\t\t * Overwritten and set to `NearestFilter` by default.\n\t\t *\n\t\t * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}\n\t\t * @default NearestFilter\n\t\t */\n\t\tthis.magFilter = NearestFilter;\n\n\t\t/**\n\t\t * How the texture is sampled when a texel covers less than one pixel.\n\t\t *\n\t\t * Overwritten and set to `NearestFilter` by default.\n\t\t *\n\t\t * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}\n\t\t * @default NearestFilter\n\t\t */\n\t\tthis.minFilter = NearestFilter;\n\n\t\t/**\n\t\t * This defines how the texture is wrapped in the depth and corresponds to\n\t\t * *W* in UVW mapping.\n\t\t *\n\t\t * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}\n\t\t * @default ClampToEdgeWrapping\n\t\t */\n\t\tthis.wrapR = ClampToEdgeWrapping;\n\n\t\t/**\n\t\t * Whether to generate mipmaps (if possible) for a texture.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.generateMipmaps = false;\n\n\t\t/**\n\t\t * If set to `true`, the texture is flipped along the vertical axis when\n\t\t * uploaded to the GPU.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flipY = false;\n\n\t\t/**\n\t\t * Specifies the alignment requirements for the start of each pixel row in memory.\n\t\t *\n\t\t * Overwritten and set to `1` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default 1\n\t\t */\n\t\tthis.unpackAlignment = 1;\n\n\t\t/**\n\t\t * A set of all layers which need to be updated in the texture.\n\t\t *\n\t\t * @type {Set<number>}\n\t\t */\n\t\tthis.layerUpdates = new Set();\n\n\t}\n\n\t/**\n\t * Describes that a specific layer of the texture needs to be updated.\n\t * Normally when {@link Texture#needsUpdate} is set to `true`, the\n\t * entire data texture array is sent to the GPU. Marking specific\n\t * layers will only transmit subsets of all mipmaps associated with a\n\t * specific depth in the array which is often much more performant.\n\t *\n\t * @param {number} layerIndex - The layer index that should be updated.\n\t */\n\taddLayerUpdate( layerIndex ) {\n\n\t\tthis.layerUpdates.add( layerIndex );\n\n\t}\n\n\t/**\n\t * Resets the layer updates registry.\n\t */\n\tclearLayerUpdates() {\n\n\t\tthis.layerUpdates.clear();\n\n\t}\n\n}\n\n/**\n * An array render target used in context of {@link WebGLRenderer}.\n *\n * @augments WebGLRenderTarget\n */\nclass WebGLArrayRenderTarget extends WebGLRenderTarget {\n\n\t/**\n\t * Constructs a new array render target.\n\t *\n\t * @param {number} [width=1] - The width of the render target.\n\t * @param {number} [height=1] - The height of the render target.\n\t * @param {number} [depth=1] - The height of the render target.\n\t * @param {RenderTarget~Options} [options] - The configuration object.\n\t */\n\tconstructor( width = 1, height = 1, depth = 1, options = {} ) {\n\n\t\tsuper( width, height, options );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isWebGLArrayRenderTarget = true;\n\n\t\tthis.depth = depth;\n\n\t\t/**\n\t\t * Overwritten with a different texture type.\n\t\t *\n\t\t * @type {DataArrayTexture}\n\t\t */\n\t\tthis.texture = new DataArrayTexture( null, width, height, depth );\n\t\tthis._setTextureOptions( options );\n\n\t\tthis.texture.isRenderTargetTexture = true;\n\n\t}\n\n}\n\n/**\n * Creates a three-dimensional texture from raw data, with parameters to\n * divide it into width, height, and depth.\n *\n * @augments Texture\n */\nclass Data3DTexture extends Texture {\n\n\t/**\n\t * Constructs a new data array texture.\n\t *\n\t * @param {?TypedArray} [data=null] - The buffer data.\n\t * @param {number} [width=1] - The width of the texture.\n\t * @param {number} [height=1] - The height of the texture.\n\t * @param {number} [depth=1] - The depth of the texture.\n\t */\n\tconstructor( data = null, width = 1, height = 1, depth = 1 ) {\n\n\t\t// We're going to add .setXXX() methods for setting properties later.\n\t\t// Users can still set in Data3DTexture directly.\n\t\t//\n\t\t//\tconst texture = new THREE.Data3DTexture( data, width, height, depth );\n\t\t// \ttexture.anisotropy = 16;\n\t\t//\n\t\t// See #14839\n\n\t\tsuper( null );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isData3DTexture = true;\n\n\t\t/**\n\t\t * The image definition of a data texture.\n\t\t *\n\t\t * @type {{data:TypedArray,width:number,height:number,depth:number}}\n\t\t */\n\t\tthis.image = { data, width, height, depth };\n\n\t\t/**\n\t\t * How the texture is sampled when a texel covers more than one pixel.\n\t\t *\n\t\t * Overwritten and set to `NearestFilter` by default.\n\t\t *\n\t\t * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}\n\t\t * @default NearestFilter\n\t\t */\n\t\tthis.magFilter = NearestFilter;\n\n\t\t/**\n\t\t * How the texture is sampled when a texel covers less than one pixel.\n\t\t *\n\t\t * Overwritten and set to `NearestFilter` by default.\n\t\t *\n\t\t * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}\n\t\t * @default NearestFilter\n\t\t */\n\t\tthis.minFilter = NearestFilter;\n\n\t\t/**\n\t\t * This defines how the texture is wrapped in the depth and corresponds to\n\t\t * *W* in UVW mapping.\n\t\t *\n\t\t * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}\n\t\t * @default ClampToEdgeWrapping\n\t\t */\n\t\tthis.wrapR = ClampToEdgeWrapping;\n\n\t\t/**\n\t\t * Whether to generate mipmaps (if possible) for a texture.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.generateMipmaps = false;\n\n\t\t/**\n\t\t * If set to `true`, the texture is flipped along the vertical axis when\n\t\t * uploaded to the GPU.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flipY = false;\n\n\t\t/**\n\t\t * Specifies the alignment requirements for the start of each pixel row in memory.\n\t\t *\n\t\t * Overwritten and set to `1` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default 1\n\t\t */\n\t\tthis.unpackAlignment = 1;\n\n\t}\n\n}\n\n/**\n * A 3D render target used in context of {@link WebGLRenderer}.\n *\n * @augments WebGLRenderTarget\n */\nclass WebGL3DRenderTarget extends WebGLRenderTarget {\n\n\t/**\n\t * Constructs a new 3D render target.\n\t *\n\t * @param {number} [width=1] - The width of the render target.\n\t * @param {number} [height=1] - The height of the render target.\n\t * @param {number} [depth=1] - The height of the render target.\n\t * @param {RenderTarget~Options} [options] - The configuration object.\n\t */\n\tconstructor( width = 1, height = 1, depth = 1, options = {} ) {\n\n\t\tsuper( width, height, options );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isWebGL3DRenderTarget = true;\n\n\t\tthis.depth = depth;\n\n\t\t/**\n\t\t * Overwritten with a different texture type.\n\t\t *\n\t\t * @type {Data3DTexture}\n\t\t */\n\t\tthis.texture = new Data3DTexture( null, width, height, depth );\n\t\tthis._setTextureOptions( options );\n\n\t\tthis.texture.isRenderTargetTexture = true;\n\n\t}\n\n}\n\n/**\n * Represents an axis-aligned bounding box (AABB) in 3D space.\n */\nclass Box3 {\n\n\t/**\n\t * Constructs a new bounding box.\n\t *\n\t * @param {Vector3} [min=(Infinity,Infinity,Infinity)] - A vector representing the lower boundary of the box.\n\t * @param {Vector3} [max=(-Infinity,-Infinity,-Infinity)] - A vector representing the upper boundary of the box.\n\t */\n\tconstructor( min = new Vector3( + Infinity, + Infinity, + Infinity ), max = new Vector3( - Infinity, - Infinity, - Infinity ) ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isBox3 = true;\n\n\t\t/**\n\t\t * The lower boundary of the box.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.min = min;\n\n\t\t/**\n\t\t * The upper boundary of the box.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.max = max;\n\n\t}\n\n\t/**\n\t * Sets the lower and upper boundaries of this box.\n\t * Please note that this method only copies the values from the given objects.\n\t *\n\t * @param {Vector3} min - The lower boundary of the box.\n\t * @param {Vector3} max - The upper boundary of the box.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tset( min, max ) {\n\n\t\tthis.min.copy( min );\n\t\tthis.max.copy( max );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the upper and lower bounds of this box so it encloses the position data\n\t * in the given array.\n\t *\n\t * @param {Array<number>} array - An array holding 3D position data.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tsetFromArray( array ) {\n\n\t\tthis.makeEmpty();\n\n\t\tfor ( let i = 0, il = array.length; i < il; i += 3 ) {\n\n\t\t\tthis.expandByPoint( _vector$b.fromArray( array, i ) );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the upper and lower bounds of this box so it encloses the position data\n\t * in the given buffer attribute.\n\t *\n\t * @param {BufferAttribute} attribute - A buffer attribute holding 3D position data.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tsetFromBufferAttribute( attribute ) {\n\n\t\tthis.makeEmpty();\n\n\t\tfor ( let i = 0, il = attribute.count; i < il; i ++ ) {\n\n\t\t\tthis.expandByPoint( _vector$b.fromBufferAttribute( attribute, i ) );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the upper and lower bounds of this box so it encloses the position data\n\t * in the given array.\n\t *\n\t * @param {Array<Vector3>} points - An array holding 3D position data as instances of {@link Vector3}.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tsetFromPoints( points ) {\n\n\t\tthis.makeEmpty();\n\n\t\tfor ( let i = 0, il = points.length; i < il; i ++ ) {\n\n\t\t\tthis.expandByPoint( points[ i ] );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Centers this box on the given center vector and sets this box's width, height and\n\t * depth to the given size values.\n\t *\n\t * @param {Vector3} center - The center of the box.\n\t * @param {Vector3} size - The x, y and z dimensions of the box.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tsetFromCenterAndSize( center, size ) {\n\n\t\tconst halfSize = _vector$b.copy( size ).multiplyScalar( 0.5 );\n\n\t\tthis.min.copy( center ).sub( halfSize );\n\t\tthis.max.copy( center ).add( halfSize );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes the world-axis-aligned bounding box for the given 3D object\n\t * (including its children), accounting for the object's, and children's,\n\t * world transforms. The function may result in a larger box than strictly necessary.\n\t *\n\t * @param {Object3D} object - The 3D object to compute the bounding box for.\n\t * @param {boolean} [precise=false] - If set to `true`, the method computes the smallest\n\t * world-axis-aligned bounding box at the expense of more computation.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tsetFromObject( object, precise = false ) {\n\n\t\tthis.makeEmpty();\n\n\t\treturn this.expandByObject( object, precise );\n\n\t}\n\n\t/**\n\t * Returns a new box with copied values from this instance.\n\t *\n\t * @return {Box3} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Copies the values of the given box to this instance.\n\t *\n\t * @param {Box3} box - The box to copy.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tcopy( box ) {\n\n\t\tthis.min.copy( box.min );\n\t\tthis.max.copy( box.max );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Makes this box empty which means in encloses a zero space in 3D.\n\t *\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tmakeEmpty() {\n\n\t\tthis.min.x = this.min.y = this.min.z = + Infinity;\n\t\tthis.max.x = this.max.y = this.max.z = - Infinity;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns true if this box includes zero points within its bounds.\n\t * Note that a box with equal lower and upper bounds still includes one\n\t * point, the one both bounds share.\n\t *\n\t * @return {boolean} Whether this box is empty or not.\n\t */\n\tisEmpty() {\n\n\t\t// this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes\n\n\t\treturn ( this.max.x < this.min.x ) || ( this.max.y < this.min.y ) || ( this.max.z < this.min.z );\n\n\t}\n\n\t/**\n\t * Returns the center point of this box.\n\t *\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The center point.\n\t */\n\tgetCenter( target ) {\n\n\t\treturn this.isEmpty() ? target.set( 0, 0, 0 ) : target.addVectors( this.min, this.max ).multiplyScalar( 0.5 );\n\n\t}\n\n\t/**\n\t * Returns the dimensions of this box.\n\t *\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The size.\n\t */\n\tgetSize( target ) {\n\n\t\treturn this.isEmpty() ? target.set( 0, 0, 0 ) : target.subVectors( this.max, this.min );\n\n\t}\n\n\t/**\n\t * Expands the boundaries of this box to include the given point.\n\t *\n\t * @param {Vector3} point - The point that should be included by the bounding box.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\texpandByPoint( point ) {\n\n\t\tthis.min.min( point );\n\t\tthis.max.max( point );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Expands this box equilaterally by the given vector. The width of this\n\t * box will be expanded by the x component of the vector in both\n\t * directions. The height of this box will be expanded by the y component of\n\t * the vector in both directions. The depth of this box will be\n\t * expanded by the z component of the vector in both directions.\n\t *\n\t * @param {Vector3} vector - The vector that should expand the bounding box.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\texpandByVector( vector ) {\n\n\t\tthis.min.sub( vector );\n\t\tthis.max.add( vector );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Expands each dimension of the box by the given scalar. If negative, the\n\t * dimensions of the box will be contracted.\n\t *\n\t * @param {number} scalar - The scalar value that should expand the bounding box.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\texpandByScalar( scalar ) {\n\n\t\tthis.min.addScalar( - scalar );\n\t\tthis.max.addScalar( scalar );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Expands the boundaries of this box to include the given 3D object and\n\t * its children, accounting for the object's, and children's, world\n\t * transforms. The function may result in a larger box than strictly\n\t * necessary (unless the precise parameter is set to true).\n\t *\n\t * @param {Object3D} object - The 3D object that should expand the bounding box.\n\t * @param {boolean} precise - If set to `true`, the method expands the bounding box\n\t * as little as necessary at the expense of more computation.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\texpandByObject( object, precise = false ) {\n\n\t\t// Computes the world-axis-aligned bounding box of an object (including its children),\n\t\t// accounting for both the object's, and children's, world transforms\n\n\t\tobject.updateWorldMatrix( false, false );\n\n\t\tconst geometry = object.geometry;\n\n\t\tif ( geometry !== undefined ) {\n\n\t\t\tconst positionAttribute = geometry.getAttribute( 'position' );\n\n\t\t\t// precise AABB computation based on vertex data requires at least a position attribute.\n\t\t\t// instancing isn't supported so far and uses the normal (conservative) code path.\n\n\t\t\tif ( precise === true && positionAttribute !== undefined && object.isInstancedMesh !== true ) {\n\n\t\t\t\tfor ( let i = 0, l = positionAttribute.count; i < l; i ++ ) {\n\n\t\t\t\t\tif ( object.isMesh === true ) {\n\n\t\t\t\t\t\tobject.getVertexPosition( i, _vector$b );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_vector$b.fromBufferAttribute( positionAttribute, i );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t_vector$b.applyMatrix4( object.matrixWorld );\n\t\t\t\t\tthis.expandByPoint( _vector$b );\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tif ( object.boundingBox !== undefined ) {\n\n\t\t\t\t\t// object-level bounding box\n\n\t\t\t\t\tif ( object.boundingBox === null ) {\n\n\t\t\t\t\t\tobject.computeBoundingBox();\n\n\t\t\t\t\t}\n\n\t\t\t\t\t_box$4.copy( object.boundingBox );\n\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// geometry-level bounding box\n\n\t\t\t\t\tif ( geometry.boundingBox === null ) {\n\n\t\t\t\t\t\tgeometry.computeBoundingBox();\n\n\t\t\t\t\t}\n\n\t\t\t\t\t_box$4.copy( geometry.boundingBox );\n\n\t\t\t\t}\n\n\t\t\t\t_box$4.applyMatrix4( object.matrixWorld );\n\n\t\t\t\tthis.union( _box$4 );\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst children = object.children;\n\n\t\tfor ( let i = 0, l = children.length; i < l; i ++ ) {\n\n\t\t\tthis.expandByObject( children[ i ], precise );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if the given point lies within or on the boundaries of this box.\n\t *\n\t * @param {Vector3} point - The point to test.\n\t * @return {boolean} Whether the bounding box contains the given point or not.\n\t */\n\tcontainsPoint( point ) {\n\n\t\treturn point.x >= this.min.x && point.x <= this.max.x &&\n\t\t\tpoint.y >= this.min.y && point.y <= this.max.y &&\n\t\t\tpoint.z >= this.min.z && point.z <= this.max.z;\n\n\t}\n\n\t/**\n\t * Returns `true` if this bounding box includes the entirety of the given bounding box.\n\t * If this box and the given one are identical, this function also returns `true`.\n\t *\n\t * @param {Box3} box - The bounding box to test.\n\t * @return {boolean} Whether the bounding box contains the given bounding box or not.\n\t */\n\tcontainsBox( box ) {\n\n\t\treturn this.min.x <= box.min.x && box.max.x <= this.max.x &&\n\t\t\tthis.min.y <= box.min.y && box.max.y <= this.max.y &&\n\t\t\tthis.min.z <= box.min.z && box.max.z <= this.max.z;\n\n\t}\n\n\t/**\n\t * Returns a point as a proportion of this box's width, height and depth.\n\t *\n\t * @param {Vector3} point - A point in 3D space.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} A point as a proportion of this box's width, height and depth.\n\t */\n\tgetParameter( point, target ) {\n\n\t\t// This can potentially have a divide by zero if the box\n\t\t// has a size dimension of 0.\n\n\t\treturn target.set(\n\t\t\t( point.x - this.min.x ) / ( this.max.x - this.min.x ),\n\t\t\t( point.y - this.min.y ) / ( this.max.y - this.min.y ),\n\t\t\t( point.z - this.min.z ) / ( this.max.z - this.min.z )\n\t\t);\n\n\t}\n\n\t/**\n\t * Returns `true` if the given bounding box intersects with this bounding box.\n\t *\n\t * @param {Box3} box - The bounding box to test.\n\t * @return {boolean} Whether the given bounding box intersects with this bounding box.\n\t */\n\tintersectsBox( box ) {\n\n\t\t// using 6 splitting planes to rule out intersections.\n\t\treturn box.max.x >= this.min.x && box.min.x <= this.max.x &&\n\t\t\tbox.max.y >= this.min.y && box.min.y <= this.max.y &&\n\t\t\tbox.max.z >= this.min.z && box.min.z <= this.max.z;\n\n\t}\n\n\t/**\n\t * Returns `true` if the given bounding sphere intersects with this bounding box.\n\t *\n\t * @param {Sphere} sphere - The bounding sphere to test.\n\t * @return {boolean} Whether the given bounding sphere intersects with this bounding box.\n\t */\n\tintersectsSphere( sphere ) {\n\n\t\t// Find the point on the AABB closest to the sphere center.\n\t\tthis.clampPoint( sphere.center, _vector$b );\n\n\t\t// If that point is inside the sphere, the AABB and sphere intersect.\n\t\treturn _vector$b.distanceToSquared( sphere.center ) <= ( sphere.radius * sphere.radius );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given plane intersects with this bounding box.\n\t *\n\t * @param {Plane} plane - The plane to test.\n\t * @return {boolean} Whether the given plane intersects with this bounding box.\n\t */\n\tintersectsPlane( plane ) {\n\n\t\t// We compute the minimum and maximum dot product values. If those values\n\t\t// are on the same side (back or front) of the plane, then there is no intersection.\n\n\t\tlet min, max;\n\n\t\tif ( plane.normal.x > 0 ) {\n\n\t\t\tmin = plane.normal.x * this.min.x;\n\t\t\tmax = plane.normal.x * this.max.x;\n\n\t\t} else {\n\n\t\t\tmin = plane.normal.x * this.max.x;\n\t\t\tmax = plane.normal.x * this.min.x;\n\n\t\t}\n\n\t\tif ( plane.normal.y > 0 ) {\n\n\t\t\tmin += plane.normal.y * this.min.y;\n\t\t\tmax += plane.normal.y * this.max.y;\n\n\t\t} else {\n\n\t\t\tmin += plane.normal.y * this.max.y;\n\t\t\tmax += plane.normal.y * this.min.y;\n\n\t\t}\n\n\t\tif ( plane.normal.z > 0 ) {\n\n\t\t\tmin += plane.normal.z * this.min.z;\n\t\t\tmax += plane.normal.z * this.max.z;\n\n\t\t} else {\n\n\t\t\tmin += plane.normal.z * this.max.z;\n\t\t\tmax += plane.normal.z * this.min.z;\n\n\t\t}\n\n\t\treturn ( min <= - plane.constant && max >= - plane.constant );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given triangle intersects with this bounding box.\n\t *\n\t * @param {Triangle} triangle - The triangle to test.\n\t * @return {boolean} Whether the given triangle intersects with this bounding box.\n\t */\n\tintersectsTriangle( triangle ) {\n\n\t\tif ( this.isEmpty() ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// compute box center and extents\n\t\tthis.getCenter( _center );\n\t\t_extents.subVectors( this.max, _center );\n\n\t\t// translate triangle to aabb origin\n\t\t_v0$2.subVectors( triangle.a, _center );\n\t\t_v1$7.subVectors( triangle.b, _center );\n\t\t_v2$4.subVectors( triangle.c, _center );\n\n\t\t// compute edge vectors for triangle\n\t\t_f0.subVectors( _v1$7, _v0$2 );\n\t\t_f1.subVectors( _v2$4, _v1$7 );\n\t\t_f2.subVectors( _v0$2, _v2$4 );\n\n\t\t// test against axes that are given by cross product combinations of the edges of the triangle and the edges of the aabb\n\t\t// make an axis testing of each of the 3 sides of the aabb against each of the 3 sides of the triangle = 9 axis of separation\n\t\t// axis_ij = u_i x f_j (u0, u1, u2 = face normals of aabb = x,y,z axes vectors since aabb is axis aligned)\n\t\tlet axes = [\n\t\t\t0, - _f0.z, _f0.y, 0, - _f1.z, _f1.y, 0, - _f2.z, _f2.y,\n\t\t\t_f0.z, 0, - _f0.x, _f1.z, 0, - _f1.x, _f2.z, 0, - _f2.x,\n\t\t\t- _f0.y, _f0.x, 0, - _f1.y, _f1.x, 0, - _f2.y, _f2.x, 0\n\t\t];\n\t\tif ( ! satForAxes( axes, _v0$2, _v1$7, _v2$4, _extents ) ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// test 3 face normals from the aabb\n\t\taxes = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];\n\t\tif ( ! satForAxes( axes, _v0$2, _v1$7, _v2$4, _extents ) ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// finally testing the face normal of the triangle\n\t\t// use already existing triangle edge vectors here\n\t\t_triangleNormal.crossVectors( _f0, _f1 );\n\t\taxes = [ _triangleNormal.x, _triangleNormal.y, _triangleNormal.z ];\n\n\t\treturn satForAxes( axes, _v0$2, _v1$7, _v2$4, _extents );\n\n\t}\n\n\t/**\n\t * Clamps the given point within the bounds of this box.\n\t *\n\t * @param {Vector3} point - The point to clamp.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The clamped point.\n\t */\n\tclampPoint( point, target ) {\n\n\t\treturn target.copy( point ).clamp( this.min, this.max );\n\n\t}\n\n\t/**\n\t * Returns the euclidean distance from any edge of this box to the specified point. If\n\t * the given point lies inside of this box, the distance will be `0`.\n\t *\n\t * @param {Vector3} point - The point to compute the distance to.\n\t * @return {number} The euclidean distance.\n\t */\n\tdistanceToPoint( point ) {\n\n\t\treturn this.clampPoint( point, _vector$b ).distanceTo( point );\n\n\t}\n\n\t/**\n\t * Returns a bounding sphere that encloses this bounding box.\n\t *\n\t * @param {Sphere} target - The target sphere that is used to store the method's result.\n\t * @return {Sphere} The bounding sphere that encloses this bounding box.\n\t */\n\tgetBoundingSphere( target ) {\n\n\t\tif ( this.isEmpty() ) {\n\n\t\t\ttarget.makeEmpty();\n\n\t\t} else {\n\n\t\t\tthis.getCenter( target.center );\n\n\t\t\ttarget.radius = this.getSize( _vector$b ).length() * 0.5;\n\n\t\t}\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Computes the intersection of this bounding box and the given one, setting the upper\n\t * bound of this box to the lesser of the two boxes' upper bounds and the\n\t * lower bound of this box to the greater of the two boxes' lower bounds. If\n\t * there's no overlap, makes this box empty.\n\t *\n\t * @param {Box3} box - The bounding box to intersect with.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tintersect( box ) {\n\n\t\tthis.min.max( box.min );\n\t\tthis.max.min( box.max );\n\n\t\t// ensure that if there is no overlap, the result is fully empty, not slightly empty with non-inf/+inf values that will cause subsequence intersects to erroneously return valid values.\n\t\tif ( this.isEmpty() ) this.makeEmpty();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes the union of this box and another and the given one, setting the upper\n\t * bound of this box to the greater of the two boxes' upper bounds and the\n\t * lower bound of this box to the lesser of the two boxes' lower bounds.\n\t *\n\t * @param {Box3} box - The bounding box that will be unioned with this instance.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tunion( box ) {\n\n\t\tthis.min.min( box.min );\n\t\tthis.max.max( box.max );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Transforms this bounding box by the given 4x4 transformation matrix.\n\t *\n\t * @param {Matrix4} matrix - The transformation matrix.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tapplyMatrix4( matrix ) {\n\n\t\t// transform of empty box is an empty box.\n\t\tif ( this.isEmpty() ) return this;\n\n\t\t// NOTE: I am using a binary pattern to specify all 2^3 combinations below\n\t\t_points[ 0 ].set( this.min.x, this.min.y, this.min.z ).applyMatrix4( matrix ); // 000\n\t\t_points[ 1 ].set( this.min.x, this.min.y, this.max.z ).applyMatrix4( matrix ); // 001\n\t\t_points[ 2 ].set( this.min.x, this.max.y, this.min.z ).applyMatrix4( matrix ); // 010\n\t\t_points[ 3 ].set( this.min.x, this.max.y, this.max.z ).applyMatrix4( matrix ); // 011\n\t\t_points[ 4 ].set( this.max.x, this.min.y, this.min.z ).applyMatrix4( matrix ); // 100\n\t\t_points[ 5 ].set( this.max.x, this.min.y, this.max.z ).applyMatrix4( matrix ); // 101\n\t\t_points[ 6 ].set( this.max.x, this.max.y, this.min.z ).applyMatrix4( matrix ); // 110\n\t\t_points[ 7 ].set( this.max.x, this.max.y, this.max.z ).applyMatrix4( matrix ); // 111\n\n\t\tthis.setFromPoints( _points );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given offset to both the upper and lower bounds of this bounding box,\n\t * effectively moving it in 3D space.\n\t *\n\t * @param {Vector3} offset - The offset that should be used to translate the bounding box.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\ttranslate( offset ) {\n\n\t\tthis.min.add( offset );\n\t\tthis.max.add( offset );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this bounding box is equal with the given one.\n\t *\n\t * @param {Box3} box - The box to test for equality.\n\t * @return {boolean} Whether this bounding box is equal with the given one.\n\t */\n\tequals( box ) {\n\n\t\treturn box.min.equals( this.min ) && box.max.equals( this.max );\n\n\t}\n\n\t/**\n\t * Returns a serialized structure of the bounding box.\n\t *\n\t * @return {Object} Serialized structure with fields representing the object state.\n\t */\n\ttoJSON() {\n\n\t\treturn {\n\t\t\tmin: this.min.toArray(),\n\t\t\tmax: this.max.toArray()\n\t\t};\n\n\t}\n\n\t/**\n\t * Returns a serialized structure of the bounding box.\n\t *\n\t * @param {Object} json - The serialized json to set the box from.\n\t * @return {Box3} A reference to this bounding box.\n\t */\n\tfromJSON( json ) {\n\n\t\tthis.min.fromArray( json.min );\n\t\tthis.max.fromArray( json.max );\n\t\treturn this;\n\n\t}\n\n}\n\nconst _points = [\n\t/*@__PURE__*/ new Vector3(),\n\t/*@__PURE__*/ new Vector3(),\n\t/*@__PURE__*/ new Vector3(),\n\t/*@__PURE__*/ new Vector3(),\n\t/*@__PURE__*/ new Vector3(),\n\t/*@__PURE__*/ new Vector3(),\n\t/*@__PURE__*/ new Vector3(),\n\t/*@__PURE__*/ new Vector3()\n];\n\nconst _vector$b = /*@__PURE__*/ new Vector3();\n\nconst _box$4 = /*@__PURE__*/ new Box3();\n\n// triangle centered vertices\n\nconst _v0$2 = /*@__PURE__*/ new Vector3();\nconst _v1$7 = /*@__PURE__*/ new Vector3();\nconst _v2$4 = /*@__PURE__*/ new Vector3();\n\n// triangle edge vectors\n\nconst _f0 = /*@__PURE__*/ new Vector3();\nconst _f1 = /*@__PURE__*/ new Vector3();\nconst _f2 = /*@__PURE__*/ new Vector3();\n\nconst _center = /*@__PURE__*/ new Vector3();\nconst _extents = /*@__PURE__*/ new Vector3();\nconst _triangleNormal = /*@__PURE__*/ new Vector3();\nconst _testAxis = /*@__PURE__*/ new Vector3();\n\nfunction satForAxes( axes, v0, v1, v2, extents ) {\n\n\tfor ( let i = 0, j = axes.length - 3; i <= j; i += 3 ) {\n\n\t\t_testAxis.fromArray( axes, i );\n\t\t// project the aabb onto the separating axis\n\t\tconst r = extents.x * Math.abs( _testAxis.x ) + extents.y * Math.abs( _testAxis.y ) + extents.z * Math.abs( _testAxis.z );\n\t\t// project all 3 vertices of the triangle onto the separating axis\n\t\tconst p0 = v0.dot( _testAxis );\n\t\tconst p1 = v1.dot( _testAxis );\n\t\tconst p2 = v2.dot( _testAxis );\n\t\t// actual test, basically see if either of the most extreme of the triangle points intersects r\n\t\tif ( Math.max( - Math.max( p0, p1, p2 ), Math.min( p0, p1, p2 ) ) > r ) {\n\n\t\t\t// points of the projected triangle are outside the projected half-length of the aabb\n\t\t\t// the axis is separating and we can exit\n\t\t\treturn false;\n\n\t\t}\n\n\t}\n\n\treturn true;\n\n}\n\nconst _box$3 = /*@__PURE__*/ new Box3();\nconst _v1$6 = /*@__PURE__*/ new Vector3();\nconst _v2$3 = /*@__PURE__*/ new Vector3();\n\n/**\n * An analytical 3D sphere defined by a center and radius. This class is mainly\n * used as a Bounding Sphere for 3D objects.\n */\nclass Sphere {\n\n\t/**\n\t * Constructs a new sphere.\n\t *\n\t * @param {Vector3} [center=(0,0,0)] - The center of the sphere\n\t * @param {number} [radius=-1] - The radius of the sphere.\n\t */\n\tconstructor( center = new Vector3(), radius = -1 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSphere = true;\n\n\t\t/**\n\t\t * The center of the sphere\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.center = center;\n\n\t\t/**\n\t\t * The radius of the sphere.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.radius = radius;\n\n\t}\n\n\t/**\n\t * Sets the sphere's components by copying the given values.\n\t *\n\t * @param {Vector3} center - The center.\n\t * @param {number} radius - The radius.\n\t * @return {Sphere} A reference to this sphere.\n\t */\n\tset( center, radius ) {\n\n\t\tthis.center.copy( center );\n\t\tthis.radius = radius;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes the minimum bounding sphere for list of points.\n\t * If the optional center point is given, it is used as the sphere's\n\t * center. Otherwise, the center of the axis-aligned bounding box\n\t * encompassing the points is calculated.\n\t *\n\t * @param {Array<Vector3>} points - A list of points in 3D space.\n\t * @param {Vector3} [optionalCenter] - The center of the sphere.\n\t * @return {Sphere} A reference to this sphere.\n\t */\n\tsetFromPoints( points, optionalCenter ) {\n\n\t\tconst center = this.center;\n\n\t\tif ( optionalCenter !== undefined ) {\n\n\t\t\tcenter.copy( optionalCenter );\n\n\t\t} else {\n\n\t\t\t_box$3.setFromPoints( points ).getCenter( center );\n\n\t\t}\n\n\t\tlet maxRadiusSq = 0;\n\n\t\tfor ( let i = 0, il = points.length; i < il; i ++ ) {\n\n\t\t\tmaxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( points[ i ] ) );\n\n\t\t}\n\n\t\tthis.radius = Math.sqrt( maxRadiusSq );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the values of the given sphere to this instance.\n\t *\n\t * @param {Sphere} sphere - The sphere to copy.\n\t * @return {Sphere} A reference to this sphere.\n\t */\n\tcopy( sphere ) {\n\n\t\tthis.center.copy( sphere.center );\n\t\tthis.radius = sphere.radius;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if the sphere is empty (the radius set to a negative number).\n\t *\n\t * Spheres with a radius of `0` contain only their center point and are not\n\t * considered to be empty.\n\t *\n\t * @return {boolean} Whether this sphere is empty or not.\n\t */\n\tisEmpty() {\n\n\t\treturn ( this.radius < 0 );\n\n\t}\n\n\t/**\n\t * Makes this sphere empty which means in encloses a zero space in 3D.\n\t *\n\t * @return {Sphere} A reference to this sphere.\n\t */\n\tmakeEmpty() {\n\n\t\tthis.center.set( 0, 0, 0 );\n\t\tthis.radius = -1;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this sphere contains the given point inclusive of\n\t * the surface of the sphere.\n\t *\n\t * @param {Vector3} point - The point to check.\n\t * @return {boolean} Whether this sphere contains the given point or not.\n\t */\n\tcontainsPoint( point ) {\n\n\t\treturn ( point.distanceToSquared( this.center ) <= ( this.radius * this.radius ) );\n\n\t}\n\n\t/**\n\t * Returns the closest distance from the boundary of the sphere to the\n\t * given point. If the sphere contains the point, the distance will\n\t * be negative.\n\t *\n\t * @param {Vector3} point - The point to compute the distance to.\n\t * @return {number} The distance to the point.\n\t */\n\tdistanceToPoint( point ) {\n\n\t\treturn ( point.distanceTo( this.center ) - this.radius );\n\n\t}\n\n\t/**\n\t * Returns `true` if this sphere intersects with the given one.\n\t *\n\t * @param {Sphere} sphere - The sphere to test.\n\t * @return {boolean} Whether this sphere intersects with the given one or not.\n\t */\n\tintersectsSphere( sphere ) {\n\n\t\tconst radiusSum = this.radius + sphere.radius;\n\n\t\treturn sphere.center.distanceToSquared( this.center ) <= ( radiusSum * radiusSum );\n\n\t}\n\n\t/**\n\t * Returns `true` if this sphere intersects with the given box.\n\t *\n\t * @param {Box3} box - The box to test.\n\t * @return {boolean} Whether this sphere intersects with the given box or not.\n\t */\n\tintersectsBox( box ) {\n\n\t\treturn box.intersectsSphere( this );\n\n\t}\n\n\t/**\n\t * Returns `true` if this sphere intersects with the given plane.\n\t *\n\t * @param {Plane} plane - The plane to test.\n\t * @return {boolean} Whether this sphere intersects with the given plane or not.\n\t */\n\tintersectsPlane( plane ) {\n\n\t\treturn Math.abs( plane.distanceToPoint( this.center ) ) <= this.radius;\n\n\t}\n\n\t/**\n\t * Clamps a point within the sphere. If the point is outside the sphere, it\n\t * will clamp it to the closest point on the edge of the sphere. Points\n\t * already inside the sphere will not be affected.\n\t *\n\t * @param {Vector3} point - The plane to clamp.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The clamped point.\n\t */\n\tclampPoint( point, target ) {\n\n\t\tconst deltaLengthSq = this.center.distanceToSquared( point );\n\n\t\ttarget.copy( point );\n\n\t\tif ( deltaLengthSq > ( this.radius * this.radius ) ) {\n\n\t\t\ttarget.sub( this.center ).normalize();\n\t\t\ttarget.multiplyScalar( this.radius ).add( this.center );\n\n\t\t}\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Returns a bounding box that encloses this sphere.\n\t *\n\t * @param {Box3} target - The target box that is used to store the method's result.\n\t * @return {Box3} The bounding box that encloses this sphere.\n\t */\n\tgetBoundingBox( target ) {\n\n\t\tif ( this.isEmpty() ) {\n\n\t\t\t// Empty sphere produces empty bounding box\n\t\t\ttarget.makeEmpty();\n\t\t\treturn target;\n\n\t\t}\n\n\t\ttarget.set( this.center, this.center );\n\t\ttarget.expandByScalar( this.radius );\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Transforms this sphere with the given 4x4 transformation matrix.\n\t *\n\t * @param {Matrix4} matrix - The transformation matrix.\n\t * @return {Sphere} A reference to this sphere.\n\t */\n\tapplyMatrix4( matrix ) {\n\n\t\tthis.center.applyMatrix4( matrix );\n\t\tthis.radius = this.radius * matrix.getMaxScaleOnAxis();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Translates the sphere's center by the given offset.\n\t *\n\t * @param {Vector3} offset - The offset.\n\t * @return {Sphere} A reference to this sphere.\n\t */\n\ttranslate( offset ) {\n\n\t\tthis.center.add( offset );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Expands the boundaries of this sphere to include the given point.\n\t *\n\t * @param {Vector3} point - The point to include.\n\t * @return {Sphere} A reference to this sphere.\n\t */\n\texpandByPoint( point ) {\n\n\t\tif ( this.isEmpty() ) {\n\n\t\t\tthis.center.copy( point );\n\n\t\t\tthis.radius = 0;\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\t_v1$6.subVectors( point, this.center );\n\n\t\tconst lengthSq = _v1$6.lengthSq();\n\n\t\tif ( lengthSq > ( this.radius * this.radius ) ) {\n\n\t\t\t// calculate the minimal sphere\n\n\t\t\tconst length = Math.sqrt( lengthSq );\n\n\t\t\tconst delta = ( length - this.radius ) * 0.5;\n\n\t\t\tthis.center.addScaledVector( _v1$6, delta / length );\n\n\t\t\tthis.radius += delta;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Expands this sphere to enclose both the original sphere and the given sphere.\n\t *\n\t * @param {Sphere} sphere - The sphere to include.\n\t * @return {Sphere} A reference to this sphere.\n\t */\n\tunion( sphere ) {\n\n\t\tif ( sphere.isEmpty() ) {\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\tif ( this.isEmpty() ) {\n\n\t\t\tthis.copy( sphere );\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\tif ( this.center.equals( sphere.center ) === true ) {\n\n\t\t\t this.radius = Math.max( this.radius, sphere.radius );\n\n\t\t} else {\n\n\t\t\t_v2$3.subVectors( sphere.center, this.center ).setLength( sphere.radius );\n\n\t\t\tthis.expandByPoint( _v1$6.copy( sphere.center ).add( _v2$3 ) );\n\n\t\t\tthis.expandByPoint( _v1$6.copy( sphere.center ).sub( _v2$3 ) );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this sphere is equal with the given one.\n\t *\n\t * @param {Sphere} sphere - The sphere to test for equality.\n\t * @return {boolean} Whether this bounding sphere is equal with the given one.\n\t */\n\tequals( sphere ) {\n\n\t\treturn sphere.center.equals( this.center ) && ( sphere.radius === this.radius );\n\n\t}\n\n\t/**\n\t * Returns a new sphere with copied values from this instance.\n\t *\n\t * @return {Sphere} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Returns a serialized structure of the bounding sphere.\n\t *\n\t * @return {Object} Serialized structure with fields representing the object state.\n\t */\n\ttoJSON() {\n\n\t\treturn {\n\t\t\tradius: this.radius,\n\t\t\tcenter: this.center.toArray()\n\t\t};\n\n\t}\n\n\t/**\n\t * Returns a serialized structure of the bounding sphere.\n\t *\n\t * @param {Object} json - The serialized json to set the sphere from.\n\t * @return {Sphere} A reference to this bounding sphere.\n\t */\n\tfromJSON( json ) {\n\n\t\tthis.radius = json.radius;\n\t\tthis.center.fromArray( json.center );\n\t\treturn this;\n\n\t}\n\n}\n\nconst _vector$a = /*@__PURE__*/ new Vector3();\nconst _segCenter = /*@__PURE__*/ new Vector3();\nconst _segDir = /*@__PURE__*/ new Vector3();\nconst _diff = /*@__PURE__*/ new Vector3();\n\nconst _edge1 = /*@__PURE__*/ new Vector3();\nconst _edge2 = /*@__PURE__*/ new Vector3();\nconst _normal$1 = /*@__PURE__*/ new Vector3();\n\n/**\n * A ray that emits from an origin in a certain direction. The class is used by\n * {@link Raycaster} to assist with raycasting. Raycasting is used for\n * mouse picking (working out what objects in the 3D space the mouse is over)\n * amongst other things.\n */\nclass Ray {\n\n\t/**\n\t * Constructs a new ray.\n\t *\n\t * @param {Vector3} [origin=(0,0,0)] - The origin of the ray.\n\t * @param {Vector3} [direction=(0,0,-1)] - The (normalized) direction of the ray.\n\t */\n\tconstructor( origin = new Vector3(), direction = new Vector3( 0, 0, -1 ) ) {\n\n\t\t/**\n\t\t * The origin of the ray.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.origin = origin;\n\n\t\t/**\n\t\t * The (normalized) direction of the ray.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.direction = direction;\n\n\t}\n\n\t/**\n\t * Sets the ray's components by copying the given values.\n\t *\n\t * @param {Vector3} origin - The origin.\n\t * @param {Vector3} direction - The direction.\n\t * @return {Ray} A reference to this ray.\n\t */\n\tset( origin, direction ) {\n\n\t\tthis.origin.copy( origin );\n\t\tthis.direction.copy( direction );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the values of the given ray to this instance.\n\t *\n\t * @param {Ray} ray - The ray to copy.\n\t * @return {Ray} A reference to this ray.\n\t */\n\tcopy( ray ) {\n\n\t\tthis.origin.copy( ray.origin );\n\t\tthis.direction.copy( ray.direction );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a vector that is located at a given distance along this ray.\n\t *\n\t * @param {number} t - The distance along the ray to retrieve a position for.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} A position on the ray.\n\t */\n\tat( t, target ) {\n\n\t\treturn target.copy( this.origin ).addScaledVector( this.direction, t );\n\n\t}\n\n\t/**\n\t * Adjusts the direction of the ray to point at the given vector in world space.\n\t *\n\t * @param {Vector3} v - The target position.\n\t * @return {Ray} A reference to this ray.\n\t */\n\tlookAt( v ) {\n\n\t\tthis.direction.copy( v ).sub( this.origin ).normalize();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Shift the origin of this ray along its direction by the given distance.\n\t *\n\t * @param {number} t - The distance along the ray to interpolate.\n\t * @return {Ray} A reference to this ray.\n\t */\n\trecast( t ) {\n\n\t\tthis.origin.copy( this.at( t, _vector$a ) );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the point along this ray that is closest to the given point.\n\t *\n\t * @param {Vector3} point - A point in 3D space to get the closet location on the ray for.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The closest point on this ray.\n\t */\n\tclosestPointToPoint( point, target ) {\n\n\t\ttarget.subVectors( point, this.origin );\n\n\t\tconst directionDistance = target.dot( this.direction );\n\n\t\tif ( directionDistance < 0 ) {\n\n\t\t\treturn target.copy( this.origin );\n\n\t\t}\n\n\t\treturn target.copy( this.origin ).addScaledVector( this.direction, directionDistance );\n\n\t}\n\n\t/**\n\t * Returns the distance of the closest approach between this ray and the given point.\n\t *\n\t * @param {Vector3} point - A point in 3D space to compute the distance to.\n\t * @return {number} The distance.\n\t */\n\tdistanceToPoint( point ) {\n\n\t\treturn Math.sqrt( this.distanceSqToPoint( point ) );\n\n\t}\n\n\t/**\n\t * Returns the squared distance of the closest approach between this ray and the given point.\n\t *\n\t * @param {Vector3} point - A point in 3D space to compute the distance to.\n\t * @return {number} The squared distance.\n\t */\n\tdistanceSqToPoint( point ) {\n\n\t\tconst directionDistance = _vector$a.subVectors( point, this.origin ).dot( this.direction );\n\n\t\t// point behind the ray\n\n\t\tif ( directionDistance < 0 ) {\n\n\t\t\treturn this.origin.distanceToSquared( point );\n\n\t\t}\n\n\t\t_vector$a.copy( this.origin ).addScaledVector( this.direction, directionDistance );\n\n\t\treturn _vector$a.distanceToSquared( point );\n\n\t}\n\n\t/**\n\t * Returns the squared distance between this ray and the given line segment.\n\t *\n\t * @param {Vector3} v0 - The start point of the line segment.\n\t * @param {Vector3} v1 - The end point of the line segment.\n\t * @param {Vector3} [optionalPointOnRay] - When provided, it receives the point on this ray that is closest to the segment.\n\t * @param {Vector3} [optionalPointOnSegment] - When provided, it receives the point on the line segment that is closest to this ray.\n\t * @return {number} The squared distance.\n\t */\n\tdistanceSqToSegment( v0, v1, optionalPointOnRay, optionalPointOnSegment ) {\n\n\t\t// from https://github.com/pmjoniak/GeometricTools/blob/master/GTEngine/Include/Mathematics/GteDistRaySegment.h\n\t\t// It returns the min distance between the ray and the segment\n\t\t// defined by v0 and v1\n\t\t// It can also set two optional targets :\n\t\t// - The closest point on the ray\n\t\t// - The closest point on the segment\n\n\t\t_segCenter.copy( v0 ).add( v1 ).multiplyScalar( 0.5 );\n\t\t_segDir.copy( v1 ).sub( v0 ).normalize();\n\t\t_diff.copy( this.origin ).sub( _segCenter );\n\n\t\tconst segExtent = v0.distanceTo( v1 ) * 0.5;\n\t\tconst a01 = - this.direction.dot( _segDir );\n\t\tconst b0 = _diff.dot( this.direction );\n\t\tconst b1 = - _diff.dot( _segDir );\n\t\tconst c = _diff.lengthSq();\n\t\tconst det = Math.abs( 1 - a01 * a01 );\n\t\tlet s0, s1, sqrDist, extDet;\n\n\t\tif ( det > 0 ) {\n\n\t\t\t// The ray and segment are not parallel.\n\n\t\t\ts0 = a01 * b1 - b0;\n\t\t\ts1 = a01 * b0 - b1;\n\t\t\textDet = segExtent * det;\n\n\t\t\tif ( s0 >= 0 ) {\n\n\t\t\t\tif ( s1 >= - extDet ) {\n\n\t\t\t\t\tif ( s1 <= extDet ) {\n\n\t\t\t\t\t\t// region 0\n\t\t\t\t\t\t// Minimum at interior points of ray and segment.\n\n\t\t\t\t\t\tconst invDet = 1 / det;\n\t\t\t\t\t\ts0 *= invDet;\n\t\t\t\t\t\ts1 *= invDet;\n\t\t\t\t\t\tsqrDist = s0 * ( s0 + a01 * s1 + 2 * b0 ) + s1 * ( a01 * s0 + s1 + 2 * b1 ) + c;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// region 1\n\n\t\t\t\t\t\ts1 = segExtent;\n\t\t\t\t\t\ts0 = Math.max( 0, - ( a01 * s1 + b0 ) );\n\t\t\t\t\t\tsqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// region 5\n\n\t\t\t\t\ts1 = - segExtent;\n\t\t\t\t\ts0 = Math.max( 0, - ( a01 * s1 + b0 ) );\n\t\t\t\t\tsqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tif ( s1 <= - extDet ) {\n\n\t\t\t\t\t// region 4\n\n\t\t\t\t\ts0 = Math.max( 0, - ( - a01 * segExtent + b0 ) );\n\t\t\t\t\ts1 = ( s0 > 0 ) ? - segExtent : Math.min( Math.max( - segExtent, - b1 ), segExtent );\n\t\t\t\t\tsqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;\n\n\t\t\t\t} else if ( s1 <= extDet ) {\n\n\t\t\t\t\t// region 3\n\n\t\t\t\t\ts0 = 0;\n\t\t\t\t\ts1 = Math.min( Math.max( - segExtent, - b1 ), segExtent );\n\t\t\t\t\tsqrDist = s1 * ( s1 + 2 * b1 ) + c;\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// region 2\n\n\t\t\t\t\ts0 = Math.max( 0, - ( a01 * segExtent + b0 ) );\n\t\t\t\t\ts1 = ( s0 > 0 ) ? segExtent : Math.min( Math.max( - segExtent, - b1 ), segExtent );\n\t\t\t\t\tsqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// Ray and segment are parallel.\n\n\t\t\ts1 = ( a01 > 0 ) ? - segExtent : segExtent;\n\t\t\ts0 = Math.max( 0, - ( a01 * s1 + b0 ) );\n\t\t\tsqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;\n\n\t\t}\n\n\t\tif ( optionalPointOnRay ) {\n\n\t\t\toptionalPointOnRay.copy( this.origin ).addScaledVector( this.direction, s0 );\n\n\t\t}\n\n\t\tif ( optionalPointOnSegment ) {\n\n\t\t\toptionalPointOnSegment.copy( _segCenter ).addScaledVector( _segDir, s1 );\n\n\t\t}\n\n\t\treturn sqrDist;\n\n\t}\n\n\t/**\n\t * Intersects this ray with the given sphere, returning the intersection\n\t * point or `null` if there is no intersection.\n\t *\n\t * @param {Sphere} sphere - The sphere to intersect.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {?Vector3} The intersection point.\n\t */\n\tintersectSphere( sphere, target ) {\n\n\t\t_vector$a.subVectors( sphere.center, this.origin );\n\t\tconst tca = _vector$a.dot( this.direction );\n\t\tconst d2 = _vector$a.dot( _vector$a ) - tca * tca;\n\t\tconst radius2 = sphere.radius * sphere.radius;\n\n\t\tif ( d2 > radius2 ) return null;\n\n\t\tconst thc = Math.sqrt( radius2 - d2 );\n\n\t\t// t0 = first intersect point - entrance on front of sphere\n\t\tconst t0 = tca - thc;\n\n\t\t// t1 = second intersect point - exit point on back of sphere\n\t\tconst t1 = tca + thc;\n\n\t\t// test to see if t1 is behind the ray - if so, return null\n\t\tif ( t1 < 0 ) return null;\n\n\t\t// test to see if t0 is behind the ray:\n\t\t// if it is, the ray is inside the sphere, so return the second exit point scaled by t1,\n\t\t// in order to always return an intersect point that is in front of the ray.\n\t\tif ( t0 < 0 ) return this.at( t1, target );\n\n\t\t// else t0 is in front of the ray, so return the first collision point scaled by t0\n\t\treturn this.at( t0, target );\n\n\t}\n\n\t/**\n\t * Returns `true` if this ray intersects with the given sphere.\n\t *\n\t * @param {Sphere} sphere - The sphere to intersect.\n\t * @return {boolean} Whether this ray intersects with the given sphere or not.\n\t */\n\tintersectsSphere( sphere ) {\n\n\t\tif ( sphere.radius < 0 ) return false; // handle empty spheres, see #31187\n\n\t\treturn this.distanceSqToPoint( sphere.center ) <= ( sphere.radius * sphere.radius );\n\n\t}\n\n\t/**\n\t * Computes the distance from the ray's origin to the given plane. Returns `null` if the ray\n\t * does not intersect with the plane.\n\t *\n\t * @param {Plane} plane - The plane to compute the distance to.\n\t * @return {?number} Whether this ray intersects with the given sphere or not.\n\t */\n\tdistanceToPlane( plane ) {\n\n\t\tconst denominator = plane.normal.dot( this.direction );\n\n\t\tif ( denominator === 0 ) {\n\n\t\t\t// line is coplanar, return origin\n\t\t\tif ( plane.distanceToPoint( this.origin ) === 0 ) {\n\n\t\t\t\treturn 0;\n\n\t\t\t}\n\n\t\t\t// Null is preferable to undefined since undefined means.... it is undefined\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\tconst t = - ( this.origin.dot( plane.normal ) + plane.constant ) / denominator;\n\n\t\t// Return if the ray never intersects the plane\n\n\t\treturn t >= 0 ? t : null;\n\n\t}\n\n\t/**\n\t * Intersects this ray with the given plane, returning the intersection\n\t * point or `null` if there is no intersection.\n\t *\n\t * @param {Plane} plane - The plane to intersect.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {?Vector3} The intersection point.\n\t */\n\tintersectPlane( plane, target ) {\n\n\t\tconst t = this.distanceToPlane( plane );\n\n\t\tif ( t === null ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\treturn this.at( t, target );\n\n\t}\n\n\t/**\n\t * Returns `true` if this ray intersects with the given plane.\n\t *\n\t * @param {Plane} plane - The plane to intersect.\n\t * @return {boolean} Whether this ray intersects with the given plane or not.\n\t */\n\tintersectsPlane( plane ) {\n\n\t\t// check if the ray lies on the plane first\n\n\t\tconst distToPoint = plane.distanceToPoint( this.origin );\n\n\t\tif ( distToPoint === 0 ) {\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\tconst denominator = plane.normal.dot( this.direction );\n\n\t\tif ( denominator * distToPoint < 0 ) {\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\t// ray origin is behind the plane (and is pointing behind it)\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Intersects this ray with the given bounding box, returning the intersection\n\t * point or `null` if there is no intersection.\n\t *\n\t * @param {Box3} box - The box to intersect.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {?Vector3} The intersection point.\n\t */\n\tintersectBox( box, target ) {\n\n\t\tlet tmin, tmax, tymin, tymax, tzmin, tzmax;\n\n\t\tconst invdirx = 1 / this.direction.x,\n\t\t\tinvdiry = 1 / this.direction.y,\n\t\t\tinvdirz = 1 / this.direction.z;\n\n\t\tconst origin = this.origin;\n\n\t\tif ( invdirx >= 0 ) {\n\n\t\t\ttmin = ( box.min.x - origin.x ) * invdirx;\n\t\t\ttmax = ( box.max.x - origin.x ) * invdirx;\n\n\t\t} else {\n\n\t\t\ttmin = ( box.max.x - origin.x ) * invdirx;\n\t\t\ttmax = ( box.min.x - origin.x ) * invdirx;\n\n\t\t}\n\n\t\tif ( invdiry >= 0 ) {\n\n\t\t\ttymin = ( box.min.y - origin.y ) * invdiry;\n\t\t\ttymax = ( box.max.y - origin.y ) * invdiry;\n\n\t\t} else {\n\n\t\t\ttymin = ( box.max.y - origin.y ) * invdiry;\n\t\t\ttymax = ( box.min.y - origin.y ) * invdiry;\n\n\t\t}\n\n\t\tif ( ( tmin > tymax ) || ( tymin > tmax ) ) return null;\n\n\t\tif ( tymin > tmin || isNaN( tmin ) ) tmin = tymin;\n\n\t\tif ( tymax < tmax || isNaN( tmax ) ) tmax = tymax;\n\n\t\tif ( invdirz >= 0 ) {\n\n\t\t\ttzmin = ( box.min.z - origin.z ) * invdirz;\n\t\t\ttzmax = ( box.max.z - origin.z ) * invdirz;\n\n\t\t} else {\n\n\t\t\ttzmin = ( box.max.z - origin.z ) * invdirz;\n\t\t\ttzmax = ( box.min.z - origin.z ) * invdirz;\n\n\t\t}\n\n\t\tif ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return null;\n\n\t\tif ( tzmin > tmin || tmin !== tmin ) tmin = tzmin;\n\n\t\tif ( tzmax < tmax || tmax !== tmax ) tmax = tzmax;\n\n\t\t//return point closest to the ray (positive side)\n\n\t\tif ( tmax < 0 ) return null;\n\n\t\treturn this.at( tmin >= 0 ? tmin : tmax, target );\n\n\t}\n\n\t/**\n\t * Returns `true` if this ray intersects with the given box.\n\t *\n\t * @param {Box3} box - The box to intersect.\n\t * @return {boolean} Whether this ray intersects with the given box or not.\n\t */\n\tintersectsBox( box ) {\n\n\t\treturn this.intersectBox( box, _vector$a ) !== null;\n\n\t}\n\n\t/**\n\t * Intersects this ray with the given triangle, returning the intersection\n\t * point or `null` if there is no intersection.\n\t *\n\t * @param {Vector3} a - The first vertex of the triangle.\n\t * @param {Vector3} b - The second vertex of the triangle.\n\t * @param {Vector3} c - The third vertex of the triangle.\n\t * @param {boolean} backfaceCulling - Whether to use backface culling or not.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {?Vector3} The intersection point.\n\t */\n\tintersectTriangle( a, b, c, backfaceCulling, target ) {\n\n\t\t// Compute the offset origin, edges, and normal.\n\n\t\t// from https://github.com/pmjoniak/GeometricTools/blob/master/GTEngine/Include/Mathematics/GteIntrRay3Triangle3.h\n\n\t\t_edge1.subVectors( b, a );\n\t\t_edge2.subVectors( c, a );\n\t\t_normal$1.crossVectors( _edge1, _edge2 );\n\n\t\t// Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction,\n\t\t// E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by\n\t\t//   |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2))\n\t\t//   |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q))\n\t\t//   |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N)\n\t\tlet DdN = this.direction.dot( _normal$1 );\n\t\tlet sign;\n\n\t\tif ( DdN > 0 ) {\n\n\t\t\tif ( backfaceCulling ) return null;\n\t\t\tsign = 1;\n\n\t\t} else if ( DdN < 0 ) {\n\n\t\t\tsign = -1;\n\t\t\tDdN = - DdN;\n\n\t\t} else {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\t_diff.subVectors( this.origin, a );\n\t\tconst DdQxE2 = sign * this.direction.dot( _edge2.crossVectors( _diff, _edge2 ) );\n\n\t\t// b1 < 0, no intersection\n\t\tif ( DdQxE2 < 0 ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\tconst DdE1xQ = sign * this.direction.dot( _edge1.cross( _diff ) );\n\n\t\t// b2 < 0, no intersection\n\t\tif ( DdE1xQ < 0 ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\t// b1+b2 > 1, no intersection\n\t\tif ( DdQxE2 + DdE1xQ > DdN ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\t// Line intersects triangle, check if ray does.\n\t\tconst QdN = - sign * _diff.dot( _normal$1 );\n\n\t\t// t < 0, no intersection\n\t\tif ( QdN < 0 ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\t// Ray intersects triangle.\n\t\treturn this.at( QdN / DdN, target );\n\n\t}\n\n\t/**\n\t * Transforms this ray with the given 4x4 transformation matrix.\n\t *\n\t * @param {Matrix4} matrix4 - The transformation matrix.\n\t * @return {Ray} A reference to this ray.\n\t */\n\tapplyMatrix4( matrix4 ) {\n\n\t\tthis.origin.applyMatrix4( matrix4 );\n\t\tthis.direction.transformDirection( matrix4 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this ray is equal with the given one.\n\t *\n\t * @param {Ray} ray - The ray to test for equality.\n\t * @return {boolean} Whether this ray is equal with the given one.\n\t */\n\tequals( ray ) {\n\n\t\treturn ray.origin.equals( this.origin ) && ray.direction.equals( this.direction );\n\n\t}\n\n\t/**\n\t * Returns a new ray with copied values from this instance.\n\t *\n\t * @return {Ray} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n}\n\n/**\n * Represents a 4x4 matrix.\n *\n * The most common use of a 4x4 matrix in 3D computer graphics is as a transformation matrix.\n * For an introduction to transformation matrices as used in WebGL, check out [this tutorial](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices)\n *\n * This allows a 3D vector representing a point in 3D space to undergo\n * transformations such as translation, rotation, shear, scale, reflection,\n * orthogonal or perspective projection and so on, by being multiplied by the\n * matrix. This is known as `applying` the matrix to the vector.\n *\n * A Note on Row-Major and Column-Major Ordering:\n *\n * The constructor and {@link Matrix3#set} method take arguments in\n * [row-major](https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order)\n * order, while internally they are stored in the {@link Matrix3#elements} array in column-major order.\n * This means that calling:\n * ```js\n * const m = new THREE.Matrix4();\n * m.set( 11, 12, 13, 14,\n *        21, 22, 23, 24,\n *        31, 32, 33, 34,\n *        41, 42, 43, 44 );\n * ```\n * will result in the elements array containing:\n * ```js\n * m.elements = [ 11, 21, 31, 41,\n *                12, 22, 32, 42,\n *                13, 23, 33, 43,\n *                14, 24, 34, 44 ];\n * ```\n * and internally all calculations are performed using column-major ordering.\n * However, as the actual ordering makes no difference mathematically and\n * most people are used to thinking about matrices in row-major order, the\n * three.js documentation shows matrices in row-major order. Just bear in\n * mind that if you are reading the source code, you'll have to take the\n * transpose of any matrices outlined here to make sense of the calculations.\n */\nclass Matrix4 {\n\n\t/**\n\t * Constructs a new 4x4 matrix. The arguments are supposed to be\n\t * in row-major order. If no arguments are provided, the constructor\n\t * initializes the matrix as an identity matrix.\n\t *\n\t * @param {number} [n11] - 1-1 matrix element.\n\t * @param {number} [n12] - 1-2 matrix element.\n\t * @param {number} [n13] - 1-3 matrix element.\n\t * @param {number} [n14] - 1-4 matrix element.\n\t * @param {number} [n21] - 2-1 matrix element.\n\t * @param {number} [n22] - 2-2 matrix element.\n\t * @param {number} [n23] - 2-3 matrix element.\n\t * @param {number} [n24] - 2-4 matrix element.\n\t * @param {number} [n31] - 3-1 matrix element.\n\t * @param {number} [n32] - 3-2 matrix element.\n\t * @param {number} [n33] - 3-3 matrix element.\n\t * @param {number} [n34] - 3-4 matrix element.\n\t * @param {number} [n41] - 4-1 matrix element.\n\t * @param {number} [n42] - 4-2 matrix element.\n\t * @param {number} [n43] - 4-3 matrix element.\n\t * @param {number} [n44] - 4-4 matrix element.\n\t */\n\tconstructor( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tMatrix4.prototype.isMatrix4 = true;\n\n\t\t/**\n\t\t * A column-major list of matrix values.\n\t\t *\n\t\t * @type {Array<number>}\n\t\t */\n\t\tthis.elements = [\n\n\t\t\t1, 0, 0, 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\t0, 0, 0, 1\n\n\t\t];\n\n\t\tif ( n11 !== undefined ) {\n\n\t\t\tthis.set( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sets the elements of the matrix.The arguments are supposed to be\n\t * in row-major order.\n\t *\n\t * @param {number} [n11] - 1-1 matrix element.\n\t * @param {number} [n12] - 1-2 matrix element.\n\t * @param {number} [n13] - 1-3 matrix element.\n\t * @param {number} [n14] - 1-4 matrix element.\n\t * @param {number} [n21] - 2-1 matrix element.\n\t * @param {number} [n22] - 2-2 matrix element.\n\t * @param {number} [n23] - 2-3 matrix element.\n\t * @param {number} [n24] - 2-4 matrix element.\n\t * @param {number} [n31] - 3-1 matrix element.\n\t * @param {number} [n32] - 3-2 matrix element.\n\t * @param {number} [n33] - 3-3 matrix element.\n\t * @param {number} [n34] - 3-4 matrix element.\n\t * @param {number} [n41] - 4-1 matrix element.\n\t * @param {number} [n42] - 4-2 matrix element.\n\t * @param {number} [n43] - 4-3 matrix element.\n\t * @param {number} [n44] - 4-4 matrix element.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tset( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {\n\n\t\tconst te = this.elements;\n\n\t\tte[ 0 ] = n11; te[ 4 ] = n12; te[ 8 ] = n13; te[ 12 ] = n14;\n\t\tte[ 1 ] = n21; te[ 5 ] = n22; te[ 9 ] = n23; te[ 13 ] = n24;\n\t\tte[ 2 ] = n31; te[ 6 ] = n32; te[ 10 ] = n33; te[ 14 ] = n34;\n\t\tte[ 3 ] = n41; te[ 7 ] = n42; te[ 11 ] = n43; te[ 15 ] = n44;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix to the 4x4 identity matrix.\n\t *\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tidentity() {\n\n\t\tthis.set(\n\n\t\t\t1, 0, 0, 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\t0, 0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a matrix with copied values from this instance.\n\t *\n\t * @return {Matrix4} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new Matrix4().fromArray( this.elements );\n\n\t}\n\n\t/**\n\t * Copies the values of the given matrix to this instance.\n\t *\n\t * @param {Matrix4} m - The matrix to copy.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tcopy( m ) {\n\n\t\tconst te = this.elements;\n\t\tconst me = m.elements;\n\n\t\tte[ 0 ] = me[ 0 ]; te[ 1 ] = me[ 1 ]; te[ 2 ] = me[ 2 ]; te[ 3 ] = me[ 3 ];\n\t\tte[ 4 ] = me[ 4 ]; te[ 5 ] = me[ 5 ]; te[ 6 ] = me[ 6 ]; te[ 7 ] = me[ 7 ];\n\t\tte[ 8 ] = me[ 8 ]; te[ 9 ] = me[ 9 ]; te[ 10 ] = me[ 10 ]; te[ 11 ] = me[ 11 ];\n\t\tte[ 12 ] = me[ 12 ]; te[ 13 ] = me[ 13 ]; te[ 14 ] = me[ 14 ]; te[ 15 ] = me[ 15 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the translation component of the given matrix\n\t * into this matrix's translation component.\n\t *\n\t * @param {Matrix4} m - The matrix to copy the translation component.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tcopyPosition( m ) {\n\n\t\tconst te = this.elements, me = m.elements;\n\n\t\tte[ 12 ] = me[ 12 ];\n\t\tte[ 13 ] = me[ 13 ];\n\t\tte[ 14 ] = me[ 14 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Set the upper 3x3 elements of this matrix to the values of given 3x3 matrix.\n\t *\n\t * @param {Matrix3} m - The 3x3 matrix.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tsetFromMatrix3( m ) {\n\n\t\tconst me = m.elements;\n\n\t\tthis.set(\n\n\t\t\tme[ 0 ], me[ 3 ], me[ 6 ], 0,\n\t\t\tme[ 1 ], me[ 4 ], me[ 7 ], 0,\n\t\t\tme[ 2 ], me[ 5 ], me[ 8 ], 0,\n\t\t\t0, 0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Extracts the basis of this matrix into the three axis vectors provided.\n\t *\n\t * @param {Vector3} xAxis - The basis's x axis.\n\t * @param {Vector3} yAxis - The basis's y axis.\n\t * @param {Vector3} zAxis - The basis's z axis.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\textractBasis( xAxis, yAxis, zAxis ) {\n\n\t\tif ( this.determinant() === 0 ) {\n\n\t\t\txAxis.set( 1, 0, 0 );\n\t\t\tyAxis.set( 0, 1, 0 );\n\t\t\tzAxis.set( 0, 0, 1 );\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\txAxis.setFromMatrixColumn( this, 0 );\n\t\tyAxis.setFromMatrixColumn( this, 1 );\n\t\tzAxis.setFromMatrixColumn( this, 2 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given basis vectors to this matrix.\n\t *\n\t * @param {Vector3} xAxis - The basis's x axis.\n\t * @param {Vector3} yAxis - The basis's y axis.\n\t * @param {Vector3} zAxis - The basis's z axis.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeBasis( xAxis, yAxis, zAxis ) {\n\n\t\tthis.set(\n\t\t\txAxis.x, yAxis.x, zAxis.x, 0,\n\t\t\txAxis.y, yAxis.y, zAxis.y, 0,\n\t\t\txAxis.z, yAxis.z, zAxis.z, 0,\n\t\t\t0, 0, 0, 1\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Extracts the rotation component of the given matrix\n\t * into this matrix's rotation component.\n\t *\n\t * Note: This method does not support reflection matrices.\n\t *\n\t * @param {Matrix4} m - The matrix.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\textractRotation( m ) {\n\n\t\tif ( m.determinant() === 0 ) {\n\n\t\t\treturn this.identity();\n\n\t\t}\n\n\t\tconst te = this.elements;\n\t\tconst me = m.elements;\n\n\t\tconst scaleX = 1 / _v1$5.setFromMatrixColumn( m, 0 ).length();\n\t\tconst scaleY = 1 / _v1$5.setFromMatrixColumn( m, 1 ).length();\n\t\tconst scaleZ = 1 / _v1$5.setFromMatrixColumn( m, 2 ).length();\n\n\t\tte[ 0 ] = me[ 0 ] * scaleX;\n\t\tte[ 1 ] = me[ 1 ] * scaleX;\n\t\tte[ 2 ] = me[ 2 ] * scaleX;\n\t\tte[ 3 ] = 0;\n\n\t\tte[ 4 ] = me[ 4 ] * scaleY;\n\t\tte[ 5 ] = me[ 5 ] * scaleY;\n\t\tte[ 6 ] = me[ 6 ] * scaleY;\n\t\tte[ 7 ] = 0;\n\n\t\tte[ 8 ] = me[ 8 ] * scaleZ;\n\t\tte[ 9 ] = me[ 9 ] * scaleZ;\n\t\tte[ 10 ] = me[ 10 ] * scaleZ;\n\t\tte[ 11 ] = 0;\n\n\t\tte[ 12 ] = 0;\n\t\tte[ 13 ] = 0;\n\t\tte[ 14 ] = 0;\n\t\tte[ 15 ] = 1;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the rotation component (the upper left 3x3 matrix) of this matrix to\n\t * the rotation specified by the given Euler angles. The rest of\n\t * the matrix is set to the identity. Depending on the {@link Euler#order},\n\t * there are six possible outcomes. See [this page](https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix)\n\t * for a complete list.\n\t *\n\t * @param {Euler} euler - The Euler angles.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeRotationFromEuler( euler ) {\n\n\t\tconst te = this.elements;\n\n\t\tconst x = euler.x, y = euler.y, z = euler.z;\n\t\tconst a = Math.cos( x ), b = Math.sin( x );\n\t\tconst c = Math.cos( y ), d = Math.sin( y );\n\t\tconst e = Math.cos( z ), f = Math.sin( z );\n\n\t\tif ( euler.order === 'XYZ' ) {\n\n\t\t\tconst ae = a * e, af = a * f, be = b * e, bf = b * f;\n\n\t\t\tte[ 0 ] = c * e;\n\t\t\tte[ 4 ] = - c * f;\n\t\t\tte[ 8 ] = d;\n\n\t\t\tte[ 1 ] = af + be * d;\n\t\t\tte[ 5 ] = ae - bf * d;\n\t\t\tte[ 9 ] = - b * c;\n\n\t\t\tte[ 2 ] = bf - ae * d;\n\t\t\tte[ 6 ] = be + af * d;\n\t\t\tte[ 10 ] = a * c;\n\n\t\t} else if ( euler.order === 'YXZ' ) {\n\n\t\t\tconst ce = c * e, cf = c * f, de = d * e, df = d * f;\n\n\t\t\tte[ 0 ] = ce + df * b;\n\t\t\tte[ 4 ] = de * b - cf;\n\t\t\tte[ 8 ] = a * d;\n\n\t\t\tte[ 1 ] = a * f;\n\t\t\tte[ 5 ] = a * e;\n\t\t\tte[ 9 ] = - b;\n\n\t\t\tte[ 2 ] = cf * b - de;\n\t\t\tte[ 6 ] = df + ce * b;\n\t\t\tte[ 10 ] = a * c;\n\n\t\t} else if ( euler.order === 'ZXY' ) {\n\n\t\t\tconst ce = c * e, cf = c * f, de = d * e, df = d * f;\n\n\t\t\tte[ 0 ] = ce - df * b;\n\t\t\tte[ 4 ] = - a * f;\n\t\t\tte[ 8 ] = de + cf * b;\n\n\t\t\tte[ 1 ] = cf + de * b;\n\t\t\tte[ 5 ] = a * e;\n\t\t\tte[ 9 ] = df - ce * b;\n\n\t\t\tte[ 2 ] = - a * d;\n\t\t\tte[ 6 ] = b;\n\t\t\tte[ 10 ] = a * c;\n\n\t\t} else if ( euler.order === 'ZYX' ) {\n\n\t\t\tconst ae = a * e, af = a * f, be = b * e, bf = b * f;\n\n\t\t\tte[ 0 ] = c * e;\n\t\t\tte[ 4 ] = be * d - af;\n\t\t\tte[ 8 ] = ae * d + bf;\n\n\t\t\tte[ 1 ] = c * f;\n\t\t\tte[ 5 ] = bf * d + ae;\n\t\t\tte[ 9 ] = af * d - be;\n\n\t\t\tte[ 2 ] = - d;\n\t\t\tte[ 6 ] = b * c;\n\t\t\tte[ 10 ] = a * c;\n\n\t\t} else if ( euler.order === 'YZX' ) {\n\n\t\t\tconst ac = a * c, ad = a * d, bc = b * c, bd = b * d;\n\n\t\t\tte[ 0 ] = c * e;\n\t\t\tte[ 4 ] = bd - ac * f;\n\t\t\tte[ 8 ] = bc * f + ad;\n\n\t\t\tte[ 1 ] = f;\n\t\t\tte[ 5 ] = a * e;\n\t\t\tte[ 9 ] = - b * e;\n\n\t\t\tte[ 2 ] = - d * e;\n\t\t\tte[ 6 ] = ad * f + bc;\n\t\t\tte[ 10 ] = ac - bd * f;\n\n\t\t} else if ( euler.order === 'XZY' ) {\n\n\t\t\tconst ac = a * c, ad = a * d, bc = b * c, bd = b * d;\n\n\t\t\tte[ 0 ] = c * e;\n\t\t\tte[ 4 ] = - f;\n\t\t\tte[ 8 ] = d * e;\n\n\t\t\tte[ 1 ] = ac * f + bd;\n\t\t\tte[ 5 ] = a * e;\n\t\t\tte[ 9 ] = ad * f - bc;\n\n\t\t\tte[ 2 ] = bc * f - ad;\n\t\t\tte[ 6 ] = b * e;\n\t\t\tte[ 10 ] = bd * f + ac;\n\n\t\t}\n\n\t\t// bottom row\n\t\tte[ 3 ] = 0;\n\t\tte[ 7 ] = 0;\n\t\tte[ 11 ] = 0;\n\n\t\t// last column\n\t\tte[ 12 ] = 0;\n\t\tte[ 13 ] = 0;\n\t\tte[ 14 ] = 0;\n\t\tte[ 15 ] = 1;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the rotation component of this matrix to the rotation specified by\n\t * the given Quaternion as outlined [here](https://en.wikipedia.org/wiki/Rotation_matrix#Quaternion)\n\t * The rest of the matrix is set to the identity.\n\t *\n\t * @param {Quaternion} q - The Quaternion.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeRotationFromQuaternion( q ) {\n\n\t\treturn this.compose( _zero, q, _one );\n\n\t}\n\n\t/**\n\t * Sets the rotation component of the transformation matrix, looking from `eye` towards\n\t * `target`, and oriented by the up-direction.\n\t *\n\t * @param {Vector3} eye - The eye vector.\n\t * @param {Vector3} target - The target vector.\n\t * @param {Vector3} up - The up vector.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tlookAt( eye, target, up ) {\n\n\t\tconst te = this.elements;\n\n\t\t_z.subVectors( eye, target );\n\n\t\tif ( _z.lengthSq() === 0 ) {\n\n\t\t\t// eye and target are in the same position\n\n\t\t\t_z.z = 1;\n\n\t\t}\n\n\t\t_z.normalize();\n\t\t_x.crossVectors( up, _z );\n\n\t\tif ( _x.lengthSq() === 0 ) {\n\n\t\t\t// up and z are parallel\n\n\t\t\tif ( Math.abs( up.z ) === 1 ) {\n\n\t\t\t\t_z.x += 0.0001;\n\n\t\t\t} else {\n\n\t\t\t\t_z.z += 0.0001;\n\n\t\t\t}\n\n\t\t\t_z.normalize();\n\t\t\t_x.crossVectors( up, _z );\n\n\t\t}\n\n\t\t_x.normalize();\n\t\t_y.crossVectors( _z, _x );\n\n\t\tte[ 0 ] = _x.x; te[ 4 ] = _y.x; te[ 8 ] = _z.x;\n\t\tte[ 1 ] = _x.y; te[ 5 ] = _y.y; te[ 9 ] = _z.y;\n\t\tte[ 2 ] = _x.z; te[ 6 ] = _y.z; te[ 10 ] = _z.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Post-multiplies this matrix by the given 4x4 matrix.\n\t *\n\t * @param {Matrix4} m - The matrix to multiply with.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmultiply( m ) {\n\n\t\treturn this.multiplyMatrices( this, m );\n\n\t}\n\n\t/**\n\t * Pre-multiplies this matrix by the given 4x4 matrix.\n\t *\n\t * @param {Matrix4} m - The matrix to multiply with.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tpremultiply( m ) {\n\n\t\treturn this.multiplyMatrices( m, this );\n\n\t}\n\n\t/**\n\t * Multiples the given 4x4 matrices and stores the result\n\t * in this matrix.\n\t *\n\t * @param {Matrix4} a - The first matrix.\n\t * @param {Matrix4} b - The second matrix.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmultiplyMatrices( a, b ) {\n\n\t\tconst ae = a.elements;\n\t\tconst be = b.elements;\n\t\tconst te = this.elements;\n\n\t\tconst a11 = ae[ 0 ], a12 = ae[ 4 ], a13 = ae[ 8 ], a14 = ae[ 12 ];\n\t\tconst a21 = ae[ 1 ], a22 = ae[ 5 ], a23 = ae[ 9 ], a24 = ae[ 13 ];\n\t\tconst a31 = ae[ 2 ], a32 = ae[ 6 ], a33 = ae[ 10 ], a34 = ae[ 14 ];\n\t\tconst a41 = ae[ 3 ], a42 = ae[ 7 ], a43 = ae[ 11 ], a44 = ae[ 15 ];\n\n\t\tconst b11 = be[ 0 ], b12 = be[ 4 ], b13 = be[ 8 ], b14 = be[ 12 ];\n\t\tconst b21 = be[ 1 ], b22 = be[ 5 ], b23 = be[ 9 ], b24 = be[ 13 ];\n\t\tconst b31 = be[ 2 ], b32 = be[ 6 ], b33 = be[ 10 ], b34 = be[ 14 ];\n\t\tconst b41 = be[ 3 ], b42 = be[ 7 ], b43 = be[ 11 ], b44 = be[ 15 ];\n\n\t\tte[ 0 ] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;\n\t\tte[ 4 ] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;\n\t\tte[ 8 ] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;\n\t\tte[ 12 ] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;\n\n\t\tte[ 1 ] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;\n\t\tte[ 5 ] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;\n\t\tte[ 9 ] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;\n\t\tte[ 13 ] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;\n\n\t\tte[ 2 ] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;\n\t\tte[ 6 ] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;\n\t\tte[ 10 ] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;\n\t\tte[ 14 ] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;\n\n\t\tte[ 3 ] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;\n\t\tte[ 7 ] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;\n\t\tte[ 11 ] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;\n\t\tte[ 15 ] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies every component of the matrix by the given scalar.\n\t *\n\t * @param {number} s - The scalar.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmultiplyScalar( s ) {\n\n\t\tconst te = this.elements;\n\n\t\tte[ 0 ] *= s; te[ 4 ] *= s; te[ 8 ] *= s; te[ 12 ] *= s;\n\t\tte[ 1 ] *= s; te[ 5 ] *= s; te[ 9 ] *= s; te[ 13 ] *= s;\n\t\tte[ 2 ] *= s; te[ 6 ] *= s; te[ 10 ] *= s; te[ 14 ] *= s;\n\t\tte[ 3 ] *= s; te[ 7 ] *= s; te[ 11 ] *= s; te[ 15 ] *= s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes and returns the determinant of this matrix.\n\t *\n\t * Based on the method outlined [here](http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.html).\n\t *\n\t * @return {number} The determinant.\n\t */\n\tdeterminant() {\n\n\t\tconst te = this.elements;\n\n\t\tconst n11 = te[ 0 ], n12 = te[ 4 ], n13 = te[ 8 ], n14 = te[ 12 ];\n\t\tconst n21 = te[ 1 ], n22 = te[ 5 ], n23 = te[ 9 ], n24 = te[ 13 ];\n\t\tconst n31 = te[ 2 ], n32 = te[ 6 ], n33 = te[ 10 ], n34 = te[ 14 ];\n\t\tconst n41 = te[ 3 ], n42 = te[ 7 ], n43 = te[ 11 ], n44 = te[ 15 ];\n\n\t\tconst t11 = n23 * n34 - n24 * n33;\n\t\tconst t12 = n22 * n34 - n24 * n32;\n\t\tconst t13 = n22 * n33 - n23 * n32;\n\n\t\tconst t21 = n21 * n34 - n24 * n31;\n\t\tconst t22 = n21 * n33 - n23 * n31;\n\t\tconst t23 = n21 * n32 - n22 * n31;\n\n\t\treturn n11 * ( n42 * t11 - n43 * t12 + n44 * t13 ) -\n\t\t\tn12 * ( n41 * t11 - n43 * t21 + n44 * t22 ) +\n\t\t\tn13 * ( n41 * t12 - n42 * t21 + n44 * t23 ) -\n\t\t\tn14 * ( n41 * t13 - n42 * t22 + n43 * t23 );\n\n\t}\n\n\t/**\n\t * Transposes this matrix in place.\n\t *\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\ttranspose() {\n\n\t\tconst te = this.elements;\n\t\tlet tmp;\n\n\t\ttmp = te[ 1 ]; te[ 1 ] = te[ 4 ]; te[ 4 ] = tmp;\n\t\ttmp = te[ 2 ]; te[ 2 ] = te[ 8 ]; te[ 8 ] = tmp;\n\t\ttmp = te[ 6 ]; te[ 6 ] = te[ 9 ]; te[ 9 ] = tmp;\n\n\t\ttmp = te[ 3 ]; te[ 3 ] = te[ 12 ]; te[ 12 ] = tmp;\n\t\ttmp = te[ 7 ]; te[ 7 ] = te[ 13 ]; te[ 13 ] = tmp;\n\t\ttmp = te[ 11 ]; te[ 11 ] = te[ 14 ]; te[ 14 ] = tmp;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the position component for this matrix from the given vector,\n\t * without affecting the rest of the matrix.\n\t *\n\t * @param {number|Vector3} x - The x component of the vector or alternatively the vector object.\n\t * @param {number} y - The y component of the vector.\n\t * @param {number} z - The z component of the vector.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tsetPosition( x, y, z ) {\n\n\t\tconst te = this.elements;\n\n\t\tif ( x.isVector3 ) {\n\n\t\t\tte[ 12 ] = x.x;\n\t\t\tte[ 13 ] = x.y;\n\t\t\tte[ 14 ] = x.z;\n\n\t\t} else {\n\n\t\t\tte[ 12 ] = x;\n\t\t\tte[ 13 ] = y;\n\t\t\tte[ 14 ] = z;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Inverts this matrix, using the [analytic method](https://en.wikipedia.org/wiki/Invertible_matrix#Analytic_solution).\n\t * You can not invert with a determinant of zero. If you attempt this, the method produces\n\t * a zero matrix instead.\n\t *\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tinvert() {\n\n\t\t// based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm\n\t\tconst te = this.elements,\n\n\t\t\tn11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ], n41 = te[ 3 ],\n\t\t\tn12 = te[ 4 ], n22 = te[ 5 ], n32 = te[ 6 ], n42 = te[ 7 ],\n\t\t\tn13 = te[ 8 ], n23 = te[ 9 ], n33 = te[ 10 ], n43 = te[ 11 ],\n\t\t\tn14 = te[ 12 ], n24 = te[ 13 ], n34 = te[ 14 ], n44 = te[ 15 ],\n\n\t\t\tt11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44,\n\t\t\tt12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44,\n\t\t\tt13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44,\n\t\t\tt14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34;\n\n\t\tconst det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14;\n\n\t\tif ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 );\n\n\t\tconst detInv = 1 / det;\n\n\t\tte[ 0 ] = t11 * detInv;\n\t\tte[ 1 ] = ( n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44 ) * detInv;\n\t\tte[ 2 ] = ( n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44 ) * detInv;\n\t\tte[ 3 ] = ( n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43 ) * detInv;\n\n\t\tte[ 4 ] = t12 * detInv;\n\t\tte[ 5 ] = ( n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44 ) * detInv;\n\t\tte[ 6 ] = ( n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44 ) * detInv;\n\t\tte[ 7 ] = ( n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43 ) * detInv;\n\n\t\tte[ 8 ] = t13 * detInv;\n\t\tte[ 9 ] = ( n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44 ) * detInv;\n\t\tte[ 10 ] = ( n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44 ) * detInv;\n\t\tte[ 11 ] = ( n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43 ) * detInv;\n\n\t\tte[ 12 ] = t14 * detInv;\n\t\tte[ 13 ] = ( n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34 ) * detInv;\n\t\tte[ 14 ] = ( n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34 ) * detInv;\n\t\tte[ 15 ] = ( n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33 ) * detInv;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the columns of this matrix by the given vector.\n\t *\n\t * @param {Vector3} v - The scale vector.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tscale( v ) {\n\n\t\tconst te = this.elements;\n\t\tconst x = v.x, y = v.y, z = v.z;\n\n\t\tte[ 0 ] *= x; te[ 4 ] *= y; te[ 8 ] *= z;\n\t\tte[ 1 ] *= x; te[ 5 ] *= y; te[ 9 ] *= z;\n\t\tte[ 2 ] *= x; te[ 6 ] *= y; te[ 10 ] *= z;\n\t\tte[ 3 ] *= x; te[ 7 ] *= y; te[ 11 ] *= z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Gets the maximum scale value of the three axes.\n\t *\n\t * @return {number} The maximum scale.\n\t */\n\tgetMaxScaleOnAxis() {\n\n\t\tconst te = this.elements;\n\n\t\tconst scaleXSq = te[ 0 ] * te[ 0 ] + te[ 1 ] * te[ 1 ] + te[ 2 ] * te[ 2 ];\n\t\tconst scaleYSq = te[ 4 ] * te[ 4 ] + te[ 5 ] * te[ 5 ] + te[ 6 ] * te[ 6 ];\n\t\tconst scaleZSq = te[ 8 ] * te[ 8 ] + te[ 9 ] * te[ 9 ] + te[ 10 ] * te[ 10 ];\n\n\t\treturn Math.sqrt( Math.max( scaleXSq, scaleYSq, scaleZSq ) );\n\n\t}\n\n\t/**\n\t * Sets this matrix as a translation transform from the given vector.\n\t *\n\t * @param {number|Vector3} x - The amount to translate in the X axis or alternatively a translation vector.\n\t * @param {number} y - The amount to translate in the Y axis.\n\t * @param {number} z - The amount to translate in the z axis.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeTranslation( x, y, z ) {\n\n\t\tif ( x.isVector3 ) {\n\n\t\t\tthis.set(\n\n\t\t\t\t1, 0, 0, x.x,\n\t\t\t\t0, 1, 0, x.y,\n\t\t\t\t0, 0, 1, x.z,\n\t\t\t\t0, 0, 0, 1\n\n\t\t\t);\n\n\t\t} else {\n\n\t\t\tthis.set(\n\n\t\t\t\t1, 0, 0, x,\n\t\t\t\t0, 1, 0, y,\n\t\t\t\t0, 0, 1, z,\n\t\t\t\t0, 0, 0, 1\n\n\t\t\t);\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix as a rotational transformation around the X axis by\n\t * the given angle.\n\t *\n\t * @param {number} theta - The rotation in radians.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeRotationX( theta ) {\n\n\t\tconst c = Math.cos( theta ), s = Math.sin( theta );\n\n\t\tthis.set(\n\n\t\t\t1, 0, 0, 0,\n\t\t\t0, c, - s, 0,\n\t\t\t0, s, c, 0,\n\t\t\t0, 0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix as a rotational transformation around the Y axis by\n\t * the given angle.\n\t *\n\t * @param {number} theta - The rotation in radians.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeRotationY( theta ) {\n\n\t\tconst c = Math.cos( theta ), s = Math.sin( theta );\n\n\t\tthis.set(\n\n\t\t\t c, 0, s, 0,\n\t\t\t 0, 1, 0, 0,\n\t\t\t- s, 0, c, 0,\n\t\t\t 0, 0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix as a rotational transformation around the Z axis by\n\t * the given angle.\n\t *\n\t * @param {number} theta - The rotation in radians.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeRotationZ( theta ) {\n\n\t\tconst c = Math.cos( theta ), s = Math.sin( theta );\n\n\t\tthis.set(\n\n\t\t\tc, - s, 0, 0,\n\t\t\ts, c, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\t0, 0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix as a rotational transformation around the given axis by\n\t * the given angle.\n\t *\n\t * This is a somewhat controversial but mathematically sound alternative to\n\t * rotating via Quaternions. See the discussion [here](https://www.gamedev.net/articles/programming/math-and-physics/do-we-really-need-quaternions-r1199).\n\t *\n\t * @param {Vector3} axis - The normalized rotation axis.\n\t * @param {number} angle - The rotation in radians.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeRotationAxis( axis, angle ) {\n\n\t\t// Based on http://www.gamedev.net/reference/articles/article1199.asp\n\n\t\tconst c = Math.cos( angle );\n\t\tconst s = Math.sin( angle );\n\t\tconst t = 1 - c;\n\t\tconst x = axis.x, y = axis.y, z = axis.z;\n\t\tconst tx = t * x, ty = t * y;\n\n\t\tthis.set(\n\n\t\t\ttx * x + c, tx * y - s * z, tx * z + s * y, 0,\n\t\t\ttx * y + s * z, ty * y + c, ty * z - s * x, 0,\n\t\t\ttx * z - s * y, ty * z + s * x, t * z * z + c, 0,\n\t\t\t0, 0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix as a scale transformation.\n\t *\n\t * @param {number} x - The amount to scale in the X axis.\n\t * @param {number} y - The amount to scale in the Y axis.\n\t * @param {number} z - The amount to scale in the Z axis.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeScale( x, y, z ) {\n\n\t\tthis.set(\n\n\t\t\tx, 0, 0, 0,\n\t\t\t0, y, 0, 0,\n\t\t\t0, 0, z, 0,\n\t\t\t0, 0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix as a shear transformation.\n\t *\n\t * @param {number} xy - The amount to shear X by Y.\n\t * @param {number} xz - The amount to shear X by Z.\n\t * @param {number} yx - The amount to shear Y by X.\n\t * @param {number} yz - The amount to shear Y by Z.\n\t * @param {number} zx - The amount to shear Z by X.\n\t * @param {number} zy - The amount to shear Z by Y.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeShear( xy, xz, yx, yz, zx, zy ) {\n\n\t\tthis.set(\n\n\t\t\t1, yx, zx, 0,\n\t\t\txy, 1, zy, 0,\n\t\t\txz, yz, 1, 0,\n\t\t\t0, 0, 0, 1\n\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this matrix to the transformation composed of the given position,\n\t * rotation (Quaternion) and scale.\n\t *\n\t * @param {Vector3} position - The position vector.\n\t * @param {Quaternion} quaternion - The rotation as a Quaternion.\n\t * @param {Vector3} scale - The scale vector.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tcompose( position, quaternion, scale ) {\n\n\t\tconst te = this.elements;\n\n\t\tconst x = quaternion._x, y = quaternion._y, z = quaternion._z, w = quaternion._w;\n\t\tconst x2 = x + x,\ty2 = y + y, z2 = z + z;\n\t\tconst xx = x * x2, xy = x * y2, xz = x * z2;\n\t\tconst yy = y * y2, yz = y * z2, zz = z * z2;\n\t\tconst wx = w * x2, wy = w * y2, wz = w * z2;\n\n\t\tconst sx = scale.x, sy = scale.y, sz = scale.z;\n\n\t\tte[ 0 ] = ( 1 - ( yy + zz ) ) * sx;\n\t\tte[ 1 ] = ( xy + wz ) * sx;\n\t\tte[ 2 ] = ( xz - wy ) * sx;\n\t\tte[ 3 ] = 0;\n\n\t\tte[ 4 ] = ( xy - wz ) * sy;\n\t\tte[ 5 ] = ( 1 - ( xx + zz ) ) * sy;\n\t\tte[ 6 ] = ( yz + wx ) * sy;\n\t\tte[ 7 ] = 0;\n\n\t\tte[ 8 ] = ( xz + wy ) * sz;\n\t\tte[ 9 ] = ( yz - wx ) * sz;\n\t\tte[ 10 ] = ( 1 - ( xx + yy ) ) * sz;\n\t\tte[ 11 ] = 0;\n\n\t\tte[ 12 ] = position.x;\n\t\tte[ 13 ] = position.y;\n\t\tte[ 14 ] = position.z;\n\t\tte[ 15 ] = 1;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Decomposes this matrix into its position, rotation and scale components\n\t * and provides the result in the given objects.\n\t *\n\t * Note: Not all matrices are decomposable in this way. For example, if an\n\t * object has a non-uniformly scaled parent, then the object's world matrix\n\t * may not be decomposable, and this method may not be appropriate.\n\t *\n\t * @param {Vector3} position - The position vector.\n\t * @param {Quaternion} quaternion - The rotation as a Quaternion.\n\t * @param {Vector3} scale - The scale vector.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tdecompose( position, quaternion, scale ) {\n\n\t\tconst te = this.elements;\n\n\t\tposition.x = te[ 12 ];\n\t\tposition.y = te[ 13 ];\n\t\tposition.z = te[ 14 ];\n\n\t\tif ( this.determinant() === 0 ) {\n\n\t\t\tscale.set( 1, 1, 1 );\n\t\t\tquaternion.identity();\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\tlet sx = _v1$5.set( te[ 0 ], te[ 1 ], te[ 2 ] ).length();\n\t\tconst sy = _v1$5.set( te[ 4 ], te[ 5 ], te[ 6 ] ).length();\n\t\tconst sz = _v1$5.set( te[ 8 ], te[ 9 ], te[ 10 ] ).length();\n\n\t\t// if determine is negative, we need to invert one scale\n\t\tconst det = this.determinant();\n\t\tif ( det < 0 ) sx = - sx;\n\n\t\t// scale the rotation part\n\t\t_m1$2.copy( this );\n\n\t\tconst invSX = 1 / sx;\n\t\tconst invSY = 1 / sy;\n\t\tconst invSZ = 1 / sz;\n\n\t\t_m1$2.elements[ 0 ] *= invSX;\n\t\t_m1$2.elements[ 1 ] *= invSX;\n\t\t_m1$2.elements[ 2 ] *= invSX;\n\n\t\t_m1$2.elements[ 4 ] *= invSY;\n\t\t_m1$2.elements[ 5 ] *= invSY;\n\t\t_m1$2.elements[ 6 ] *= invSY;\n\n\t\t_m1$2.elements[ 8 ] *= invSZ;\n\t\t_m1$2.elements[ 9 ] *= invSZ;\n\t\t_m1$2.elements[ 10 ] *= invSZ;\n\n\t\tquaternion.setFromRotationMatrix( _m1$2 );\n\n\t\tscale.x = sx;\n\t\tscale.y = sy;\n\t\tscale.z = sz;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Creates a perspective projection matrix. This is used internally by\n\t * {@link PerspectiveCamera#updateProjectionMatrix}.\n\n\t * @param {number} left - Left boundary of the viewing frustum at the near plane.\n\t * @param {number} right - Right boundary of the viewing frustum at the near plane.\n\t * @param {number} top - Top boundary of the viewing frustum at the near plane.\n\t * @param {number} bottom - Bottom boundary of the viewing frustum at the near plane.\n\t * @param {number} near - The distance from the camera to the near plane.\n\t * @param {number} far - The distance from the camera to the far plane.\n\t * @param {(WebGLCoordinateSystem|WebGPUCoordinateSystem)} [coordinateSystem=WebGLCoordinateSystem] - The coordinate system.\n\t * @param {boolean} [reversedDepth=false] - Whether to use a reversed depth.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakePerspective( left, right, top, bottom, near, far, coordinateSystem = WebGLCoordinateSystem, reversedDepth = false ) {\n\n\t\tconst te = this.elements;\n\n\t\tconst x = 2 * near / ( right - left );\n\t\tconst y = 2 * near / ( top - bottom );\n\n\t\tconst a = ( right + left ) / ( right - left );\n\t\tconst b = ( top + bottom ) / ( top - bottom );\n\n\t\tlet c, d;\n\n\t\tif ( reversedDepth ) {\n\n\t\t\tc = near / ( far - near );\n\t\t\td = ( far * near ) / ( far - near );\n\n\t\t} else {\n\n\t\t\tif ( coordinateSystem === WebGLCoordinateSystem ) {\n\n\t\t\t\tc = - ( far + near ) / ( far - near );\n\t\t\t\td = ( -2 * far * near ) / ( far - near );\n\n\t\t\t} else if ( coordinateSystem === WebGPUCoordinateSystem ) {\n\n\t\t\t\tc = - far / ( far - near );\n\t\t\t\td = ( - far * near ) / ( far - near );\n\n\t\t\t} else {\n\n\t\t\t\tthrow new Error( 'THREE.Matrix4.makePerspective(): Invalid coordinate system: ' + coordinateSystem );\n\n\t\t\t}\n\n\t\t}\n\n\t\tte[ 0 ] = x;\tte[ 4 ] = 0;\tte[ 8 ] = a; \tte[ 12 ] = 0;\n\t\tte[ 1 ] = 0;\tte[ 5 ] = y;\tte[ 9 ] = b; \tte[ 13 ] = 0;\n\t\tte[ 2 ] = 0;\tte[ 6 ] = 0;\tte[ 10 ] = c; \tte[ 14 ] = d;\n\t\tte[ 3 ] = 0;\tte[ 7 ] = 0;\tte[ 11 ] = -1;\tte[ 15 ] = 0;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Creates a orthographic projection matrix. This is used internally by\n\t * {@link OrthographicCamera#updateProjectionMatrix}.\n\n\t * @param {number} left - Left boundary of the viewing frustum at the near plane.\n\t * @param {number} right - Right boundary of the viewing frustum at the near plane.\n\t * @param {number} top - Top boundary of the viewing frustum at the near plane.\n\t * @param {number} bottom - Bottom boundary of the viewing frustum at the near plane.\n\t * @param {number} near - The distance from the camera to the near plane.\n\t * @param {number} far - The distance from the camera to the far plane.\n\t * @param {(WebGLCoordinateSystem|WebGPUCoordinateSystem)} [coordinateSystem=WebGLCoordinateSystem] - The coordinate system.\n\t * @param {boolean} [reversedDepth=false] - Whether to use a reversed depth.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tmakeOrthographic( left, right, top, bottom, near, far, coordinateSystem = WebGLCoordinateSystem, reversedDepth = false ) {\n\n\t\tconst te = this.elements;\n\n\t\tconst x = 2 / ( right - left );\n\t\tconst y = 2 / ( top - bottom );\n\n\t\tconst a = - ( right + left ) / ( right - left );\n\t\tconst b = - ( top + bottom ) / ( top - bottom );\n\n\t\tlet c, d;\n\n\t\tif ( reversedDepth ) {\n\n\t\t\tc = 1 / ( far - near );\n\t\t\td = far / ( far - near );\n\n\t\t} else {\n\n\t\t\tif ( coordinateSystem === WebGLCoordinateSystem ) {\n\n\t\t\t\tc = -2 / ( far - near );\n\t\t\t\td = - ( far + near ) / ( far - near );\n\n\t\t\t} else if ( coordinateSystem === WebGPUCoordinateSystem ) {\n\n\t\t\t\tc = -1 / ( far - near );\n\t\t\t\td = - near / ( far - near );\n\n\t\t\t} else {\n\n\t\t\t\tthrow new Error( 'THREE.Matrix4.makeOrthographic(): Invalid coordinate system: ' + coordinateSystem );\n\n\t\t\t}\n\n\t\t}\n\n\t\tte[ 0 ] = x;\t\tte[ 4 ] = 0;\t\tte[ 8 ] = 0; \t\tte[ 12 ] = a;\n\t\tte[ 1 ] = 0; \t\tte[ 5 ] = y;\t\tte[ 9 ] = 0; \t\tte[ 13 ] = b;\n\t\tte[ 2 ] = 0; \t\tte[ 6 ] = 0;\t\tte[ 10 ] = c;\t\tte[ 14 ] = d;\n\t\tte[ 3 ] = 0; \t\tte[ 7 ] = 0;\t\tte[ 11 ] = 0;\t\tte[ 15 ] = 1;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this matrix is equal with the given one.\n\t *\n\t * @param {Matrix4} matrix - The matrix to test for equality.\n\t * @return {boolean} Whether this matrix is equal with the given one.\n\t */\n\tequals( matrix ) {\n\n\t\tconst te = this.elements;\n\t\tconst me = matrix.elements;\n\n\t\tfor ( let i = 0; i < 16; i ++ ) {\n\n\t\t\tif ( te[ i ] !== me[ i ] ) return false;\n\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Sets the elements of the matrix from the given array.\n\t *\n\t * @param {Array<number>} array - The matrix elements in column-major order.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Matrix4} A reference to this matrix.\n\t */\n\tfromArray( array, offset = 0 ) {\n\n\t\tfor ( let i = 0; i < 16; i ++ ) {\n\n\t\t\tthis.elements[ i ] = array[ i + offset ];\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Writes the elements of this matrix to the given array. If no array is provided,\n\t * the method returns a new instance.\n\t *\n\t * @param {Array<number>} [array=[]] - The target array holding the matrix elements in column-major order.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Array<number>} The matrix elements in column-major order.\n\t */\n\ttoArray( array = [], offset = 0 ) {\n\n\t\tconst te = this.elements;\n\n\t\tarray[ offset ] = te[ 0 ];\n\t\tarray[ offset + 1 ] = te[ 1 ];\n\t\tarray[ offset + 2 ] = te[ 2 ];\n\t\tarray[ offset + 3 ] = te[ 3 ];\n\n\t\tarray[ offset + 4 ] = te[ 4 ];\n\t\tarray[ offset + 5 ] = te[ 5 ];\n\t\tarray[ offset + 6 ] = te[ 6 ];\n\t\tarray[ offset + 7 ] = te[ 7 ];\n\n\t\tarray[ offset + 8 ] = te[ 8 ];\n\t\tarray[ offset + 9 ] = te[ 9 ];\n\t\tarray[ offset + 10 ] = te[ 10 ];\n\t\tarray[ offset + 11 ] = te[ 11 ];\n\n\t\tarray[ offset + 12 ] = te[ 12 ];\n\t\tarray[ offset + 13 ] = te[ 13 ];\n\t\tarray[ offset + 14 ] = te[ 14 ];\n\t\tarray[ offset + 15 ] = te[ 15 ];\n\n\t\treturn array;\n\n\t}\n\n}\n\nconst _v1$5 = /*@__PURE__*/ new Vector3();\nconst _m1$2 = /*@__PURE__*/ new Matrix4();\nconst _zero = /*@__PURE__*/ new Vector3( 0, 0, 0 );\nconst _one = /*@__PURE__*/ new Vector3( 1, 1, 1 );\nconst _x = /*@__PURE__*/ new Vector3();\nconst _y = /*@__PURE__*/ new Vector3();\nconst _z = /*@__PURE__*/ new Vector3();\n\nconst _matrix$2 = /*@__PURE__*/ new Matrix4();\nconst _quaternion$3 = /*@__PURE__*/ new Quaternion();\n\n/**\n * A class representing Euler angles.\n *\n * Euler angles describe a rotational transformation by rotating an object on\n * its various axes in specified amounts per axis, and a specified axis\n * order.\n *\n * Iterating through an instance will yield its components (x, y, z,\n * order) in the corresponding order.\n *\n * ```js\n * const a = new THREE.Euler( 0, 1, 1.57, 'XYZ' );\n * const b = new THREE.Vector3( 1, 0, 1 );\n * b.applyEuler(a);\n * ```\n */\nclass Euler {\n\n\t/**\n\t * Constructs a new euler instance.\n\t *\n\t * @param {number} [x=0] - The angle of the x axis in radians.\n\t * @param {number} [y=0] - The angle of the y axis in radians.\n\t * @param {number} [z=0] - The angle of the z axis in radians.\n\t * @param {string} [order=Euler.DEFAULT_ORDER] - A string representing the order that the rotations are applied.\n\t */\n\tconstructor( x = 0, y = 0, z = 0, order = Euler.DEFAULT_ORDER ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isEuler = true;\n\n\t\tthis._x = x;\n\t\tthis._y = y;\n\t\tthis._z = z;\n\t\tthis._order = order;\n\n\t}\n\n\t/**\n\t * The angle of the x axis in radians.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget x() {\n\n\t\treturn this._x;\n\n\t}\n\n\tset x( value ) {\n\n\t\tthis._x = value;\n\t\tthis._onChangeCallback();\n\n\t}\n\n\t/**\n\t * The angle of the y axis in radians.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget y() {\n\n\t\treturn this._y;\n\n\t}\n\n\tset y( value ) {\n\n\t\tthis._y = value;\n\t\tthis._onChangeCallback();\n\n\t}\n\n\t/**\n\t * The angle of the z axis in radians.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget z() {\n\n\t\treturn this._z;\n\n\t}\n\n\tset z( value ) {\n\n\t\tthis._z = value;\n\t\tthis._onChangeCallback();\n\n\t}\n\n\t/**\n\t * A string representing the order that the rotations are applied.\n\t *\n\t * @type {string}\n\t * @default 'XYZ'\n\t */\n\tget order() {\n\n\t\treturn this._order;\n\n\t}\n\n\tset order( value ) {\n\n\t\tthis._order = value;\n\t\tthis._onChangeCallback();\n\n\t}\n\n\t/**\n\t * Sets the Euler components.\n\t *\n\t * @param {number} x - The angle of the x axis in radians.\n\t * @param {number} y - The angle of the y axis in radians.\n\t * @param {number} z - The angle of the z axis in radians.\n\t * @param {string} [order] - A string representing the order that the rotations are applied.\n\t * @return {Euler} A reference to this Euler instance.\n\t */\n\tset( x, y, z, order = this._order ) {\n\n\t\tthis._x = x;\n\t\tthis._y = y;\n\t\tthis._z = z;\n\t\tthis._order = order;\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new Euler instance with copied values from this instance.\n\t *\n\t * @return {Euler} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor( this._x, this._y, this._z, this._order );\n\n\t}\n\n\t/**\n\t * Copies the values of the given Euler instance to this instance.\n\t *\n\t * @param {Euler} euler - The Euler instance to copy.\n\t * @return {Euler} A reference to this Euler instance.\n\t */\n\tcopy( euler ) {\n\n\t\tthis._x = euler._x;\n\t\tthis._y = euler._y;\n\t\tthis._z = euler._z;\n\t\tthis._order = euler._order;\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the angles of this Euler instance from a pure rotation matrix.\n\t *\n\t * @param {Matrix4} m - A 4x4 matrix of which the upper 3x3 of matrix is a pure rotation matrix (i.e. unscaled).\n\t * @param {string} [order] - A string representing the order that the rotations are applied.\n\t * @param {boolean} [update=true] - Whether the internal `onChange` callback should be executed or not.\n\t * @return {Euler} A reference to this Euler instance.\n\t */\n\tsetFromRotationMatrix( m, order = this._order, update = true ) {\n\n\t\tconst te = m.elements;\n\t\tconst m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ];\n\t\tconst m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ];\n\t\tconst m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ];\n\n\t\tswitch ( order ) {\n\n\t\t\tcase 'XYZ':\n\n\t\t\t\tthis._y = Math.asin( clamp( m13, -1, 1 ) );\n\n\t\t\t\tif ( Math.abs( m13 ) < 0.9999999 ) {\n\n\t\t\t\t\tthis._x = Math.atan2( - m23, m33 );\n\t\t\t\t\tthis._z = Math.atan2( - m12, m11 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis._x = Math.atan2( m32, m22 );\n\t\t\t\t\tthis._z = 0;\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'YXZ':\n\n\t\t\t\tthis._x = Math.asin( - clamp( m23, -1, 1 ) );\n\n\t\t\t\tif ( Math.abs( m23 ) < 0.9999999 ) {\n\n\t\t\t\t\tthis._y = Math.atan2( m13, m33 );\n\t\t\t\t\tthis._z = Math.atan2( m21, m22 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis._y = Math.atan2( - m31, m11 );\n\t\t\t\t\tthis._z = 0;\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'ZXY':\n\n\t\t\t\tthis._x = Math.asin( clamp( m32, -1, 1 ) );\n\n\t\t\t\tif ( Math.abs( m32 ) < 0.9999999 ) {\n\n\t\t\t\t\tthis._y = Math.atan2( - m31, m33 );\n\t\t\t\t\tthis._z = Math.atan2( - m12, m22 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis._y = 0;\n\t\t\t\t\tthis._z = Math.atan2( m21, m11 );\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'ZYX':\n\n\t\t\t\tthis._y = Math.asin( - clamp( m31, -1, 1 ) );\n\n\t\t\t\tif ( Math.abs( m31 ) < 0.9999999 ) {\n\n\t\t\t\t\tthis._x = Math.atan2( m32, m33 );\n\t\t\t\t\tthis._z = Math.atan2( m21, m11 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis._x = 0;\n\t\t\t\t\tthis._z = Math.atan2( - m12, m22 );\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'YZX':\n\n\t\t\t\tthis._z = Math.asin( clamp( m21, -1, 1 ) );\n\n\t\t\t\tif ( Math.abs( m21 ) < 0.9999999 ) {\n\n\t\t\t\t\tthis._x = Math.atan2( - m23, m22 );\n\t\t\t\t\tthis._y = Math.atan2( - m31, m11 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis._x = 0;\n\t\t\t\t\tthis._y = Math.atan2( m13, m33 );\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'XZY':\n\n\t\t\t\tthis._z = Math.asin( - clamp( m12, -1, 1 ) );\n\n\t\t\t\tif ( Math.abs( m12 ) < 0.9999999 ) {\n\n\t\t\t\t\tthis._x = Math.atan2( m32, m22 );\n\t\t\t\t\tthis._y = Math.atan2( m13, m11 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis._x = Math.atan2( - m23, m33 );\n\t\t\t\t\tthis._y = 0;\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\n\t\t\t\twarn( 'Euler: .setFromRotationMatrix() encountered an unknown order: ' + order );\n\n\t\t}\n\n\t\tthis._order = order;\n\n\t\tif ( update === true ) this._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the angles of this Euler instance from a normalized quaternion.\n\t *\n\t * @param {Quaternion} q - A normalized Quaternion.\n\t * @param {string} [order] - A string representing the order that the rotations are applied.\n\t * @param {boolean} [update=true] - Whether the internal `onChange` callback should be executed or not.\n\t * @return {Euler} A reference to this Euler instance.\n\t */\n\tsetFromQuaternion( q, order, update ) {\n\n\t\t_matrix$2.makeRotationFromQuaternion( q );\n\n\t\treturn this.setFromRotationMatrix( _matrix$2, order, update );\n\n\t}\n\n\t/**\n\t * Sets the angles of this Euler instance from the given vector.\n\t *\n\t * @param {Vector3} v - The vector.\n\t * @param {string} [order] - A string representing the order that the rotations are applied.\n\t * @return {Euler} A reference to this Euler instance.\n\t */\n\tsetFromVector3( v, order = this._order ) {\n\n\t\treturn this.set( v.x, v.y, v.z, order );\n\n\t}\n\n\t/**\n\t * Resets the euler angle with a new order by creating a quaternion from this\n\t * euler angle and then setting this euler angle with the quaternion and the\n\t * new order.\n\t *\n\t * Warning: This discards revolution information.\n\t *\n\t * @param {string} [newOrder] - A string representing the new order that the rotations are applied.\n\t * @return {Euler} A reference to this Euler instance.\n\t */\n\treorder( newOrder ) {\n\n\t\t_quaternion$3.setFromEuler( this );\n\n\t\treturn this.setFromQuaternion( _quaternion$3, newOrder );\n\n\t}\n\n\t/**\n\t * Returns `true` if this Euler instance is equal with the given one.\n\t *\n\t * @param {Euler} euler - The Euler instance to test for equality.\n\t * @return {boolean} Whether this Euler instance is equal with the given one.\n\t */\n\tequals( euler ) {\n\n\t\treturn ( euler._x === this._x ) && ( euler._y === this._y ) && ( euler._z === this._z ) && ( euler._order === this._order );\n\n\t}\n\n\t/**\n\t * Sets this Euler instance's components to values from the given array. The first three\n\t * entries of the array are assign to the x,y and z components. An optional fourth entry\n\t * defines the Euler order.\n\t *\n\t * @param {Array<number,number,number,?string>} array - An array holding the Euler component values.\n\t * @return {Euler} A reference to this Euler instance.\n\t */\n\tfromArray( array ) {\n\n\t\tthis._x = array[ 0 ];\n\t\tthis._y = array[ 1 ];\n\t\tthis._z = array[ 2 ];\n\t\tif ( array[ 3 ] !== undefined ) this._order = array[ 3 ];\n\n\t\tthis._onChangeCallback();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Writes the components of this Euler instance to the given array. If no array is provided,\n\t * the method returns a new instance.\n\t *\n\t * @param {Array<number,number,number,string>} [array=[]] - The target array holding the Euler components.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Array<number,number,number,string>} The Euler components.\n\t */\n\ttoArray( array = [], offset = 0 ) {\n\n\t\tarray[ offset ] = this._x;\n\t\tarray[ offset + 1 ] = this._y;\n\t\tarray[ offset + 2 ] = this._z;\n\t\tarray[ offset + 3 ] = this._order;\n\n\t\treturn array;\n\n\t}\n\n\t_onChange( callback ) {\n\n\t\tthis._onChangeCallback = callback;\n\n\t\treturn this;\n\n\t}\n\n\t_onChangeCallback() {}\n\n\t*[ Symbol.iterator ]() {\n\n\t\tyield this._x;\n\t\tyield this._y;\n\t\tyield this._z;\n\t\tyield this._order;\n\n\t}\n\n}\n\n/**\n * The default Euler angle order.\n *\n * @static\n * @type {string}\n * @default 'XYZ'\n */\nEuler.DEFAULT_ORDER = 'XYZ';\n\n/**\n * A layers object assigns an 3D object to 1 or more of 32\n * layers numbered `0` to `31` - internally the layers are stored as a\n * bit mask], and by default all 3D objects are a member of layer `0`.\n *\n * This can be used to control visibility - an object must share a layer with\n * a camera to be visible when that camera's view is\n * rendered.\n *\n * All classes that inherit from {@link Object3D} have an `layers` property which\n * is an instance of this class.\n */\nclass Layers {\n\n\t/**\n\t * Constructs a new layers instance, with membership\n\t * initially set to layer `0`.\n\t */\n\tconstructor() {\n\n\t\t/**\n\t\t * A bit mask storing which of the 32 layers this layers object is currently\n\t\t * a member of.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.mask = 1 | 0;\n\n\t}\n\n\t/**\n\t * Sets membership to the given layer, and remove membership all other layers.\n\t *\n\t * @param {number} layer - The layer to set.\n\t */\n\tset( layer ) {\n\n\t\tthis.mask = ( 1 << layer | 0 ) >>> 0;\n\n\t}\n\n\t/**\n\t * Adds membership of the given layer.\n\t *\n\t * @param {number} layer - The layer to enable.\n\t */\n\tenable( layer ) {\n\n\t\tthis.mask |= 1 << layer | 0;\n\n\t}\n\n\t/**\n\t * Adds membership to all layers.\n\t */\n\tenableAll() {\n\n\t\tthis.mask = 0xffffffff | 0;\n\n\t}\n\n\t/**\n\t * Toggles the membership of the given layer.\n\t *\n\t * @param {number} layer - The layer to toggle.\n\t */\n\ttoggle( layer ) {\n\n\t\tthis.mask ^= 1 << layer | 0;\n\n\t}\n\n\t/**\n\t * Removes membership of the given layer.\n\t *\n\t * @param {number} layer - The layer to enable.\n\t */\n\tdisable( layer ) {\n\n\t\tthis.mask &= ~ ( 1 << layer | 0 );\n\n\t}\n\n\t/**\n\t * Removes the membership from all layers.\n\t */\n\tdisableAll() {\n\n\t\tthis.mask = 0;\n\n\t}\n\n\t/**\n\t * Returns `true` if this and the given layers object have at least one\n\t * layer in common.\n\t *\n\t * @param {Layers} layers - The layers to test.\n\t * @return {boolean } Whether this and the given layers object have at least one layer in common or not.\n\t */\n\ttest( layers ) {\n\n\t\treturn ( this.mask & layers.mask ) !== 0;\n\n\t}\n\n\t/**\n\t * Returns `true` if the given layer is enabled.\n\t *\n\t * @param {number} layer - The layer to test.\n\t * @return {boolean } Whether the given layer is enabled or not.\n\t */\n\tisEnabled( layer ) {\n\n\t\treturn ( this.mask & ( 1 << layer | 0 ) ) !== 0;\n\n\t}\n\n}\n\nlet _object3DId = 0;\n\nconst _v1$4 = /*@__PURE__*/ new Vector3();\nconst _q1 = /*@__PURE__*/ new Quaternion();\nconst _m1$1 = /*@__PURE__*/ new Matrix4();\nconst _target = /*@__PURE__*/ new Vector3();\n\nconst _position$3 = /*@__PURE__*/ new Vector3();\nconst _scale$2 = /*@__PURE__*/ new Vector3();\nconst _quaternion$2 = /*@__PURE__*/ new Quaternion();\n\nconst _xAxis = /*@__PURE__*/ new Vector3( 1, 0, 0 );\nconst _yAxis = /*@__PURE__*/ new Vector3( 0, 1, 0 );\nconst _zAxis = /*@__PURE__*/ new Vector3( 0, 0, 1 );\n\n/**\n * Fires when the object has been added to its parent object.\n *\n * @event Object3D#added\n * @type {Object}\n */\nconst _addedEvent = { type: 'added' };\n\n/**\n * Fires when the object has been removed from its parent object.\n *\n * @event Object3D#removed\n * @type {Object}\n */\nconst _removedEvent = { type: 'removed' };\n\n/**\n * Fires when a new child object has been added.\n *\n * @event Object3D#childadded\n * @type {Object}\n */\nconst _childaddedEvent = { type: 'childadded', child: null };\n\n/**\n * Fires when a child object has been removed.\n *\n * @event Object3D#childremoved\n * @type {Object}\n */\nconst _childremovedEvent = { type: 'childremoved', child: null };\n\n/**\n * This is the base class for most objects in three.js and provides a set of\n * properties and methods for manipulating objects in 3D space.\n *\n * @augments EventDispatcher\n */\nclass Object3D extends EventDispatcher {\n\n\t/**\n\t * Constructs a new 3D object.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isObject3D = true;\n\n\t\t/**\n\t\t * The ID of the 3D object.\n\t\t *\n\t\t * @name Object3D#id\n\t\t * @type {number}\n\t\t * @readonly\n\t\t */\n\t\tObject.defineProperty( this, 'id', { value: _object3DId ++ } );\n\n\t\t/**\n\t\t * The UUID of the 3D object.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.uuid = generateUUID();\n\n\t\t/**\n\t\t * The name of the 3D object.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\n\t\t/**\n\t\t * The type property is used for detecting the object type\n\t\t * in context of serialization/deserialization.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.type = 'Object3D';\n\n\t\t/**\n\t\t * A reference to the parent object.\n\t\t *\n\t\t * @type {?Object3D}\n\t\t * @default null\n\t\t */\n\t\tthis.parent = null;\n\n\t\t/**\n\t\t * An array holding the child 3D objects of this instance.\n\t\t *\n\t\t * @type {Array<Object3D>}\n\t\t */\n\t\tthis.children = [];\n\n\t\t/**\n\t\t * Defines the `up` direction of the 3D object which influences\n\t\t * the orientation via methods like {@link Object3D#lookAt}.\n\t\t *\n\t\t * The default values for all 3D objects is defined by `Object3D.DEFAULT_UP`.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.up = Object3D.DEFAULT_UP.clone();\n\n\t\tconst position = new Vector3();\n\t\tconst rotation = new Euler();\n\t\tconst quaternion = new Quaternion();\n\t\tconst scale = new Vector3( 1, 1, 1 );\n\n\t\tfunction onRotationChange() {\n\n\t\t\tquaternion.setFromEuler( rotation, false );\n\n\t\t}\n\n\t\tfunction onQuaternionChange() {\n\n\t\t\trotation.setFromQuaternion( quaternion, undefined, false );\n\n\t\t}\n\n\t\trotation._onChange( onRotationChange );\n\t\tquaternion._onChange( onQuaternionChange );\n\n\t\tObject.defineProperties( this, {\n\t\t\t/**\n\t\t\t * Represents the object's local position.\n\t\t\t *\n\t\t\t * @name Object3D#position\n\t\t\t * @type {Vector3}\n\t\t\t * @default (0,0,0)\n\t\t\t */\n\t\t\tposition: {\n\t\t\t\tconfigurable: true,\n\t\t\t\tenumerable: true,\n\t\t\t\tvalue: position\n\t\t\t},\n\t\t\t/**\n\t\t\t * Represents the object's local rotation as Euler angles, in radians.\n\t\t\t *\n\t\t\t * @name Object3D#rotation\n\t\t\t * @type {Euler}\n\t\t\t * @default (0,0,0)\n\t\t\t */\n\t\t\trotation: {\n\t\t\t\tconfigurable: true,\n\t\t\t\tenumerable: true,\n\t\t\t\tvalue: rotation\n\t\t\t},\n\t\t\t/**\n\t\t\t * Represents the object's local rotation as Quaternions.\n\t\t\t *\n\t\t\t * @name Object3D#quaternion\n\t\t\t * @type {Quaternion}\n\t\t\t */\n\t\t\tquaternion: {\n\t\t\t\tconfigurable: true,\n\t\t\t\tenumerable: true,\n\t\t\t\tvalue: quaternion\n\t\t\t},\n\t\t\t/**\n\t\t\t * Represents the object's local scale.\n\t\t\t *\n\t\t\t * @name Object3D#scale\n\t\t\t * @type {Vector3}\n\t\t\t * @default (1,1,1)\n\t\t\t */\n\t\t\tscale: {\n\t\t\t\tconfigurable: true,\n\t\t\t\tenumerable: true,\n\t\t\t\tvalue: scale\n\t\t\t},\n\t\t\t/**\n\t\t\t * Represents the object's model-view matrix.\n\t\t\t *\n\t\t\t * @name Object3D#modelViewMatrix\n\t\t\t * @type {Matrix4}\n\t\t\t */\n\t\t\tmodelViewMatrix: {\n\t\t\t\tvalue: new Matrix4()\n\t\t\t},\n\t\t\t/**\n\t\t\t * Represents the object's normal matrix.\n\t\t\t *\n\t\t\t * @name Object3D#normalMatrix\n\t\t\t * @type {Matrix3}\n\t\t\t */\n\t\t\tnormalMatrix: {\n\t\t\t\tvalue: new Matrix3()\n\t\t\t}\n\t\t} );\n\n\t\t/**\n\t\t * Represents the object's transformation matrix in local space.\n\t\t *\n\t\t * @type {Matrix4}\n\t\t */\n\t\tthis.matrix = new Matrix4();\n\n\t\t/**\n\t\t * Represents the object's transformation matrix in world space.\n\t\t * If the 3D object has no parent, then it's identical to the local transformation matrix\n\t\t *\n\t\t * @type {Matrix4}\n\t\t */\n\t\tthis.matrixWorld = new Matrix4();\n\n\t\t/**\n\t\t * When set to `true`, the engine automatically computes the local matrix from position,\n\t\t * rotation and scale every frame.\n\t\t *\n\t\t * The default values for all 3D objects is defined by `Object3D.DEFAULT_MATRIX_AUTO_UPDATE`.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.matrixAutoUpdate = Object3D.DEFAULT_MATRIX_AUTO_UPDATE;\n\n\t\t/**\n\t\t * When set to `true`, the engine automatically computes the world matrix from the current local\n\t\t * matrix and the object's transformation hierarchy.\n\t\t *\n\t\t * The default values for all 3D objects is defined by `Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE`.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.matrixWorldAutoUpdate = Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE; // checked by the renderer\n\n\t\t/**\n\t\t * When set to `true`, it calculates the world matrix in that frame and resets this property\n\t\t * to `false`.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.matrixWorldNeedsUpdate = false;\n\n\t\t/**\n\t\t * The layer membership of the 3D object. The 3D object is only visible if it has\n\t\t * at least one layer in common with the camera in use. This property can also be\n\t\t * used to filter out unwanted objects in ray-intersection tests when using {@link Raycaster}.\n\t\t *\n\t\t * @type {Layers}\n\t\t */\n\t\tthis.layers = new Layers();\n\n\t\t/**\n\t\t * When set to `true`, the 3D object gets rendered.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.visible = true;\n\n\t\t/**\n\t\t * When set to `true`, the 3D object gets rendered into shadow maps.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.castShadow = false;\n\n\t\t/**\n\t\t * When set to `true`, the 3D object is affected by shadows in the scene.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.receiveShadow = false;\n\n\t\t/**\n\t\t * When set to `true`, the 3D object is honored by view frustum culling.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.frustumCulled = true;\n\n\t\t/**\n\t\t * This value allows the default rendering order of scene graph objects to be\n\t\t * overridden although opaque and transparent objects remain sorted independently.\n\t\t * When this property is set for an instance of {@link Group},all descendants\n\t\t * objects will be sorted and rendered together. Sorting is from lowest to highest\n\t\t * render order.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.renderOrder = 0;\n\n\t\t/**\n\t\t * An array holding the animation clips of the 3D object.\n\t\t *\n\t\t * @type {Array<AnimationClip>}\n\t\t */\n\t\tthis.animations = [];\n\n\t\t/**\n\t\t * Custom depth material to be used when rendering to the depth map. Can only be used\n\t\t * in context of meshes. When shadow-casting with a {@link DirectionalLight} or {@link SpotLight},\n\t\t * if you are modifying vertex positions in the vertex shader you must specify a custom depth\n\t\t * material for proper shadows.\n\t\t *\n\t\t * Only relevant in context of {@link WebGLRenderer}.\n\t\t *\n\t\t * @type {(Material|undefined)}\n\t\t * @default undefined\n\t\t */\n\t\tthis.customDepthMaterial = undefined;\n\n\t\t/**\n\t\t * Same as {@link Object3D#customDepthMaterial}, but used with {@link PointLight}.\n\t\t *\n\t\t * Only relevant in context of {@link WebGLRenderer}.\n\t\t *\n\t\t * @type {(Material|undefined)}\n\t\t * @default undefined\n\t\t */\n\t\tthis.customDistanceMaterial = undefined;\n\n\t\t/**\n\t\t * An object that can be used to store custom data about the 3D object. It\n\t\t * should not hold references to functions as these will not be cloned.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.userData = {};\n\n\t}\n\n\t/**\n\t * A callback that is executed immediately before a 3D object is rendered to a shadow map.\n\t *\n\t * @param {Renderer|WebGLRenderer} renderer - The renderer.\n\t * @param {Object3D} object - The 3D object.\n\t * @param {Camera} camera - The camera that is used to render the scene.\n\t * @param {Camera} shadowCamera - The shadow camera.\n\t * @param {BufferGeometry} geometry - The 3D object's geometry.\n\t * @param {Material} depthMaterial - The depth material.\n\t * @param {Object} group - The geometry group data.\n\t */\n\tonBeforeShadow( /* renderer, object, camera, shadowCamera, geometry, depthMaterial, group */ ) {}\n\n\t/**\n\t * A callback that is executed immediately after a 3D object is rendered to a shadow map.\n\t *\n\t * @param {Renderer|WebGLRenderer} renderer - The renderer.\n\t * @param {Object3D} object - The 3D object.\n\t * @param {Camera} camera - The camera that is used to render the scene.\n\t * @param {Camera} shadowCamera - The shadow camera.\n\t * @param {BufferGeometry} geometry - The 3D object's geometry.\n\t * @param {Material} depthMaterial - The depth material.\n\t * @param {Object} group - The geometry group data.\n\t */\n\tonAfterShadow( /* renderer, object, camera, shadowCamera, geometry, depthMaterial, group */ ) {}\n\n\t/**\n\t * A callback that is executed immediately before a 3D object is rendered.\n\t *\n\t * @param {Renderer|WebGLRenderer} renderer - The renderer.\n\t * @param {Object3D} object - The 3D object.\n\t * @param {Camera} camera - The camera that is used to render the scene.\n\t * @param {BufferGeometry} geometry - The 3D object's geometry.\n\t * @param {Material} material - The 3D object's material.\n\t * @param {Object} group - The geometry group data.\n\t */\n\tonBeforeRender( /* renderer, scene, camera, geometry, material, group */ ) {}\n\n\t/**\n\t * A callback that is executed immediately after a 3D object is rendered.\n\t *\n\t * @param {Renderer|WebGLRenderer} renderer - The renderer.\n\t * @param {Object3D} object - The 3D object.\n\t * @param {Camera} camera - The camera that is used to render the scene.\n\t * @param {BufferGeometry} geometry - The 3D object's geometry.\n\t * @param {Material} material - The 3D object's material.\n\t * @param {Object} group - The geometry group data.\n\t */\n\tonAfterRender( /* renderer, scene, camera, geometry, material, group */ ) {}\n\n\t/**\n\t * Applies the given transformation matrix to the object and updates the object's position,\n\t * rotation and scale.\n\t *\n\t * @param {Matrix4} matrix - The transformation matrix.\n\t */\n\tapplyMatrix4( matrix ) {\n\n\t\tif ( this.matrixAutoUpdate ) this.updateMatrix();\n\n\t\tthis.matrix.premultiply( matrix );\n\n\t\tthis.matrix.decompose( this.position, this.quaternion, this.scale );\n\n\t}\n\n\t/**\n\t * Applies a rotation represented by given the quaternion to the 3D object.\n\t *\n\t * @param {Quaternion} q - The quaternion.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\tapplyQuaternion( q ) {\n\n\t\tthis.quaternion.premultiply( q );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given rotation represented as an axis/angle couple to the 3D object.\n\t *\n\t * @param {Vector3} axis - The (normalized) axis vector.\n\t * @param {number} angle - The angle in radians.\n\t */\n\tsetRotationFromAxisAngle( axis, angle ) {\n\n\t\t// assumes axis is normalized\n\n\t\tthis.quaternion.setFromAxisAngle( axis, angle );\n\n\t}\n\n\t/**\n\t * Sets the given rotation represented as Euler angles to the 3D object.\n\t *\n\t * @param {Euler} euler - The Euler angles.\n\t */\n\tsetRotationFromEuler( euler ) {\n\n\t\tthis.quaternion.setFromEuler( euler, true );\n\n\t}\n\n\t/**\n\t * Sets the given rotation represented as rotation matrix to the 3D object.\n\t *\n\t * @param {Matrix4} m - Although a 4x4 matrix is expected, the upper 3x3 portion must be\n\t * a pure rotation matrix (i.e, unscaled).\n\t */\n\tsetRotationFromMatrix( m ) {\n\n\t\t// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)\n\n\t\tthis.quaternion.setFromRotationMatrix( m );\n\n\t}\n\n\t/**\n\t * Sets the given rotation represented as a Quaternion to the 3D object.\n\t *\n\t * @param {Quaternion} q - The Quaternion\n\t */\n\tsetRotationFromQuaternion( q ) {\n\n\t\t// assumes q is normalized\n\n\t\tthis.quaternion.copy( q );\n\n\t}\n\n\t/**\n\t * Rotates the 3D object along an axis in local space.\n\t *\n\t * @param {Vector3} axis - The (normalized) axis vector.\n\t * @param {number} angle - The angle in radians.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\trotateOnAxis( axis, angle ) {\n\n\t\t// rotate object on axis in object space\n\t\t// axis is assumed to be normalized\n\n\t\t_q1.setFromAxisAngle( axis, angle );\n\n\t\tthis.quaternion.multiply( _q1 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Rotates the 3D object along an axis in world space.\n\t *\n\t * @param {Vector3} axis - The (normalized) axis vector.\n\t * @param {number} angle - The angle in radians.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\trotateOnWorldAxis( axis, angle ) {\n\n\t\t// rotate object on axis in world space\n\t\t// axis is assumed to be normalized\n\t\t// method assumes no rotated parent\n\n\t\t_q1.setFromAxisAngle( axis, angle );\n\n\t\tthis.quaternion.premultiply( _q1 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Rotates the 3D object around its X axis in local space.\n\t *\n\t * @param {number} angle - The angle in radians.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\trotateX( angle ) {\n\n\t\treturn this.rotateOnAxis( _xAxis, angle );\n\n\t}\n\n\t/**\n\t * Rotates the 3D object around its Y axis in local space.\n\t *\n\t * @param {number} angle - The angle in radians.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\trotateY( angle ) {\n\n\t\treturn this.rotateOnAxis( _yAxis, angle );\n\n\t}\n\n\t/**\n\t * Rotates the 3D object around its Z axis in local space.\n\t *\n\t * @param {number} angle - The angle in radians.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\trotateZ( angle ) {\n\n\t\treturn this.rotateOnAxis( _zAxis, angle );\n\n\t}\n\n\t/**\n\t * Translate the 3D object by a distance along the given axis in local space.\n\t *\n\t * @param {Vector3} axis - The (normalized) axis vector.\n\t * @param {number} distance - The distance in world units.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\ttranslateOnAxis( axis, distance ) {\n\n\t\t// translate object by distance along axis in object space\n\t\t// axis is assumed to be normalized\n\n\t\t_v1$4.copy( axis ).applyQuaternion( this.quaternion );\n\n\t\tthis.position.add( _v1$4.multiplyScalar( distance ) );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Translate the 3D object by a distance along its X-axis in local space.\n\t *\n\t * @param {number} distance - The distance in world units.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\ttranslateX( distance ) {\n\n\t\treturn this.translateOnAxis( _xAxis, distance );\n\n\t}\n\n\t/**\n\t * Translate the 3D object by a distance along its Y-axis in local space.\n\t *\n\t * @param {number} distance - The distance in world units.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\ttranslateY( distance ) {\n\n\t\treturn this.translateOnAxis( _yAxis, distance );\n\n\t}\n\n\t/**\n\t * Translate the 3D object by a distance along its Z-axis in local space.\n\t *\n\t * @param {number} distance - The distance in world units.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\ttranslateZ( distance ) {\n\n\t\treturn this.translateOnAxis( _zAxis, distance );\n\n\t}\n\n\t/**\n\t * Converts the given vector from this 3D object's local space to world space.\n\t *\n\t * @param {Vector3} vector - The vector to convert.\n\t * @return {Vector3} The converted vector.\n\t */\n\tlocalToWorld( vector ) {\n\n\t\tthis.updateWorldMatrix( true, false );\n\n\t\treturn vector.applyMatrix4( this.matrixWorld );\n\n\t}\n\n\t/**\n\t * Converts the given vector from this 3D object's word space to local space.\n\t *\n\t * @param {Vector3} vector - The vector to convert.\n\t * @return {Vector3} The converted vector.\n\t */\n\tworldToLocal( vector ) {\n\n\t\tthis.updateWorldMatrix( true, false );\n\n\t\treturn vector.applyMatrix4( _m1$1.copy( this.matrixWorld ).invert() );\n\n\t}\n\n\t/**\n\t * Rotates the object to face a point in world space.\n\t *\n\t * This method does not support objects having non-uniformly-scaled parent(s).\n\t *\n\t * @param {number|Vector3} x - The x coordinate in world space. Alternatively, a vector representing a position in world space\n\t * @param {number} [y] - The y coordinate in world space.\n\t * @param {number} [z] - The z coordinate in world space.\n\t */\n\tlookAt( x, y, z ) {\n\n\t\t// This method does not support objects having non-uniformly-scaled parent(s)\n\n\t\tif ( x.isVector3 ) {\n\n\t\t\t_target.copy( x );\n\n\t\t} else {\n\n\t\t\t_target.set( x, y, z );\n\n\t\t}\n\n\t\tconst parent = this.parent;\n\n\t\tthis.updateWorldMatrix( true, false );\n\n\t\t_position$3.setFromMatrixPosition( this.matrixWorld );\n\n\t\tif ( this.isCamera || this.isLight ) {\n\n\t\t\t_m1$1.lookAt( _position$3, _target, this.up );\n\n\t\t} else {\n\n\t\t\t_m1$1.lookAt( _target, _position$3, this.up );\n\n\t\t}\n\n\t\tthis.quaternion.setFromRotationMatrix( _m1$1 );\n\n\t\tif ( parent ) {\n\n\t\t\t_m1$1.extractRotation( parent.matrixWorld );\n\t\t\t_q1.setFromRotationMatrix( _m1$1 );\n\t\t\tthis.quaternion.premultiply( _q1.invert() );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Adds the given 3D object as a child to this 3D object. An arbitrary number of\n\t * objects may be added. Any current parent on an object passed in here will be\n\t * removed, since an object can have at most one parent.\n\t *\n\t * @fires Object3D#added\n\t * @fires Object3D#childadded\n\t * @param {Object3D} object - The 3D object to add.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\tadd( object ) {\n\n\t\tif ( arguments.length > 1 ) {\n\n\t\t\tfor ( let i = 0; i < arguments.length; i ++ ) {\n\n\t\t\t\tthis.add( arguments[ i ] );\n\n\t\t\t}\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\tif ( object === this ) {\n\n\t\t\terror( 'Object3D.add: object can\\'t be added as a child of itself.', object );\n\t\t\treturn this;\n\n\t\t}\n\n\t\tif ( object && object.isObject3D ) {\n\n\t\t\tobject.removeFromParent();\n\t\t\tobject.parent = this;\n\t\t\tthis.children.push( object );\n\n\t\t\tobject.dispatchEvent( _addedEvent );\n\n\t\t\t_childaddedEvent.child = object;\n\t\t\tthis.dispatchEvent( _childaddedEvent );\n\t\t\t_childaddedEvent.child = null;\n\n\t\t} else {\n\n\t\t\terror( 'Object3D.add: object not an instance of THREE.Object3D.', object );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Removes the given 3D object as child from this 3D object.\n\t * An arbitrary number of objects may be removed.\n\t *\n\t * @fires Object3D#removed\n\t * @fires Object3D#childremoved\n\t * @param {Object3D} object - The 3D object to remove.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\tremove( object ) {\n\n\t\tif ( arguments.length > 1 ) {\n\n\t\t\tfor ( let i = 0; i < arguments.length; i ++ ) {\n\n\t\t\t\tthis.remove( arguments[ i ] );\n\n\t\t\t}\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\tconst index = this.children.indexOf( object );\n\n\t\tif ( index !== -1 ) {\n\n\t\t\tobject.parent = null;\n\t\t\tthis.children.splice( index, 1 );\n\n\t\t\tobject.dispatchEvent( _removedEvent );\n\n\t\t\t_childremovedEvent.child = object;\n\t\t\tthis.dispatchEvent( _childremovedEvent );\n\t\t\t_childremovedEvent.child = null;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Removes this 3D object from its current parent.\n\t *\n\t * @fires Object3D#removed\n\t * @fires Object3D#childremoved\n\t * @return {Object3D} A reference to this instance.\n\t */\n\tremoveFromParent() {\n\n\t\tconst parent = this.parent;\n\n\t\tif ( parent !== null ) {\n\n\t\t\tparent.remove( this );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Removes all child objects.\n\t *\n\t * @fires Object3D#removed\n\t * @fires Object3D#childremoved\n\t * @return {Object3D} A reference to this instance.\n\t */\n\tclear() {\n\n\t\treturn this.remove( ... this.children );\n\n\t}\n\n\t/**\n\t * Adds the given 3D object as a child of this 3D object, while maintaining the object's world\n\t * transform. This method does not support scene graphs having non-uniformly-scaled nodes(s).\n\t *\n\t * @fires Object3D#added\n\t * @fires Object3D#childadded\n\t * @param {Object3D} object - The 3D object to attach.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\tattach( object ) {\n\n\t\t// adds object as a child of this, while maintaining the object's world transform\n\n\t\t// Note: This method does not support scene graphs having non-uniformly-scaled nodes(s)\n\n\t\tthis.updateWorldMatrix( true, false );\n\n\t\t_m1$1.copy( this.matrixWorld ).invert();\n\n\t\tif ( object.parent !== null ) {\n\n\t\t\tobject.parent.updateWorldMatrix( true, false );\n\n\t\t\t_m1$1.multiply( object.parent.matrixWorld );\n\n\t\t}\n\n\t\tobject.applyMatrix4( _m1$1 );\n\n\t\tobject.removeFromParent();\n\t\tobject.parent = this;\n\t\tthis.children.push( object );\n\n\t\tobject.updateWorldMatrix( false, true );\n\n\t\tobject.dispatchEvent( _addedEvent );\n\n\t\t_childaddedEvent.child = object;\n\t\tthis.dispatchEvent( _childaddedEvent );\n\t\t_childaddedEvent.child = null;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Searches through the 3D object and its children, starting with the 3D object\n\t * itself, and returns the first with a matching ID.\n\t *\n\t * @param {number} id - The id.\n\t * @return {Object3D|undefined} The found 3D object. Returns `undefined` if no 3D object has been found.\n\t */\n\tgetObjectById( id ) {\n\n\t\treturn this.getObjectByProperty( 'id', id );\n\n\t}\n\n\t/**\n\t * Searches through the 3D object and its children, starting with the 3D object\n\t * itself, and returns the first with a matching name.\n\t *\n\t * @param {string} name - The name.\n\t * @return {Object3D|undefined} The found 3D object. Returns `undefined` if no 3D object has been found.\n\t */\n\tgetObjectByName( name ) {\n\n\t\treturn this.getObjectByProperty( 'name', name );\n\n\t}\n\n\t/**\n\t * Searches through the 3D object and its children, starting with the 3D object\n\t * itself, and returns the first with a matching property value.\n\t *\n\t * @param {string} name - The name of the property.\n\t * @param {any} value - The value.\n\t * @return {Object3D|undefined} The found 3D object. Returns `undefined` if no 3D object has been found.\n\t */\n\tgetObjectByProperty( name, value ) {\n\n\t\tif ( this[ name ] === value ) return this;\n\n\t\tfor ( let i = 0, l = this.children.length; i < l; i ++ ) {\n\n\t\t\tconst child = this.children[ i ];\n\t\t\tconst object = child.getObjectByProperty( name, value );\n\n\t\t\tif ( object !== undefined ) {\n\n\t\t\t\treturn object;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn undefined;\n\n\t}\n\n\t/**\n\t * Searches through the 3D object and its children, starting with the 3D object\n\t * itself, and returns all 3D objects with a matching property value.\n\t *\n\t * @param {string} name - The name of the property.\n\t * @param {any} value - The value.\n\t * @param {Array<Object3D>} result - The method stores the result in this array.\n\t * @return {Array<Object3D>} The found 3D objects.\n\t */\n\tgetObjectsByProperty( name, value, result = [] ) {\n\n\t\tif ( this[ name ] === value ) result.push( this );\n\n\t\tconst children = this.children;\n\n\t\tfor ( let i = 0, l = children.length; i < l; i ++ ) {\n\n\t\t\tchildren[ i ].getObjectsByProperty( name, value, result );\n\n\t\t}\n\n\t\treturn result;\n\n\t}\n\n\t/**\n\t * Returns a vector representing the position of the 3D object in world space.\n\t *\n\t * @param {Vector3} target - The target vector the result is stored to.\n\t * @return {Vector3} The 3D object's position in world space.\n\t */\n\tgetWorldPosition( target ) {\n\n\t\tthis.updateWorldMatrix( true, false );\n\n\t\treturn target.setFromMatrixPosition( this.matrixWorld );\n\n\t}\n\n\t/**\n\t * Returns a Quaternion representing the position of the 3D object in world space.\n\t *\n\t * @param {Quaternion} target - The target Quaternion the result is stored to.\n\t * @return {Quaternion} The 3D object's rotation in world space.\n\t */\n\tgetWorldQuaternion( target ) {\n\n\t\tthis.updateWorldMatrix( true, false );\n\n\t\tthis.matrixWorld.decompose( _position$3, target, _scale$2 );\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Returns a vector representing the scale of the 3D object in world space.\n\t *\n\t * @param {Vector3} target - The target vector the result is stored to.\n\t * @return {Vector3} The 3D object's scale in world space.\n\t */\n\tgetWorldScale( target ) {\n\n\t\tthis.updateWorldMatrix( true, false );\n\n\t\tthis.matrixWorld.decompose( _position$3, _quaternion$2, target );\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Returns a vector representing the (\"look\") direction of the 3D object in world space.\n\t *\n\t * @param {Vector3} target - The target vector the result is stored to.\n\t * @return {Vector3} The 3D object's direction in world space.\n\t */\n\tgetWorldDirection( target ) {\n\n\t\tthis.updateWorldMatrix( true, false );\n\n\t\tconst e = this.matrixWorld.elements;\n\n\t\treturn target.set( e[ 8 ], e[ 9 ], e[ 10 ] ).normalize();\n\n\t}\n\n\t/**\n\t * Abstract method to get intersections between a casted ray and this\n\t * 3D object. Renderable 3D objects such as {@link Mesh}, {@link Line} or {@link Points}\n\t * implement this method in order to use raycasting.\n\t *\n\t * @abstract\n\t * @param {Raycaster} raycaster - The raycaster.\n\t * @param {Array<Object>} intersects - An array holding the result of the method.\n\t */\n\traycast( /* raycaster, intersects */ ) {}\n\n\t/**\n\t * Executes the callback on this 3D object and all descendants.\n\t *\n\t * Note: Modifying the scene graph inside the callback is discouraged.\n\t *\n\t * @param {Function} callback - A callback function that allows to process the current 3D object.\n\t */\n\ttraverse( callback ) {\n\n\t\tcallback( this );\n\n\t\tconst children = this.children;\n\n\t\tfor ( let i = 0, l = children.length; i < l; i ++ ) {\n\n\t\t\tchildren[ i ].traverse( callback );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Like {@link Object3D#traverse}, but the callback will only be executed for visible 3D objects.\n\t * Descendants of invisible 3D objects are not traversed.\n\t *\n\t * Note: Modifying the scene graph inside the callback is discouraged.\n\t *\n\t * @param {Function} callback - A callback function that allows to process the current 3D object.\n\t */\n\ttraverseVisible( callback ) {\n\n\t\tif ( this.visible === false ) return;\n\n\t\tcallback( this );\n\n\t\tconst children = this.children;\n\n\t\tfor ( let i = 0, l = children.length; i < l; i ++ ) {\n\n\t\t\tchildren[ i ].traverseVisible( callback );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Like {@link Object3D#traverse}, but the callback will only be executed for all ancestors.\n\t *\n\t * Note: Modifying the scene graph inside the callback is discouraged.\n\t *\n\t * @param {Function} callback - A callback function that allows to process the current 3D object.\n\t */\n\ttraverseAncestors( callback ) {\n\n\t\tconst parent = this.parent;\n\n\t\tif ( parent !== null ) {\n\n\t\t\tcallback( parent );\n\n\t\t\tparent.traverseAncestors( callback );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Updates the transformation matrix in local space by computing it from the current\n\t * position, rotation and scale values.\n\t */\n\tupdateMatrix() {\n\n\t\tthis.matrix.compose( this.position, this.quaternion, this.scale );\n\n\t\tthis.matrixWorldNeedsUpdate = true;\n\n\t}\n\n\t/**\n\t * Updates the transformation matrix in world space of this 3D objects and its descendants.\n\t *\n\t * To ensure correct results, this method also recomputes the 3D object's transformation matrix in\n\t * local space. The computation of the local and world matrix can be controlled with the\n\t * {@link Object3D#matrixAutoUpdate} and {@link Object3D#matrixWorldAutoUpdate} flags which are both\n\t * `true` by default.  Set these flags to `false` if you need more control over the update matrix process.\n\t *\n\t * @param {boolean} [force=false] - When set to `true`, a recomputation of world matrices is forced even\n\t * when {@link Object3D#matrixWorldAutoUpdate} is set to `false`.\n\t */\n\tupdateMatrixWorld( force ) {\n\n\t\tif ( this.matrixAutoUpdate ) this.updateMatrix();\n\n\t\tif ( this.matrixWorldNeedsUpdate || force ) {\n\n\t\t\tif ( this.matrixWorldAutoUpdate === true ) {\n\n\t\t\t\tif ( this.parent === null ) {\n\n\t\t\t\t\tthis.matrixWorld.copy( this.matrix );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis.matrixWorldNeedsUpdate = false;\n\n\t\t\tforce = true;\n\n\t\t}\n\n\t\t// make sure descendants are updated if required\n\n\t\tconst children = this.children;\n\n\t\tfor ( let i = 0, l = children.length; i < l; i ++ ) {\n\n\t\t\tconst child = children[ i ];\n\n\t\t\tchild.updateMatrixWorld( force );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * An alternative version of {@link Object3D#updateMatrixWorld} with more control over the\n\t * update of ancestor and descendant nodes.\n\t *\n\t * @param {boolean} [updateParents=false] Whether ancestor nodes should be updated or not.\n\t * @param {boolean} [updateChildren=false] Whether descendant nodes should be updated or not.\n\t */\n\tupdateWorldMatrix( updateParents, updateChildren ) {\n\n\t\tconst parent = this.parent;\n\n\t\tif ( updateParents === true && parent !== null ) {\n\n\t\t\tparent.updateWorldMatrix( true, false );\n\n\t\t}\n\n\t\tif ( this.matrixAutoUpdate ) this.updateMatrix();\n\n\t\tif ( this.matrixWorldAutoUpdate === true ) {\n\n\t\t\tif ( this.parent === null ) {\n\n\t\t\t\tthis.matrixWorld.copy( this.matrix );\n\n\t\t\t} else {\n\n\t\t\t\tthis.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// make sure descendants are updated\n\n\t\tif ( updateChildren === true ) {\n\n\t\t\tconst children = this.children;\n\n\t\t\tfor ( let i = 0, l = children.length; i < l; i ++ ) {\n\n\t\t\t\tconst child = children[ i ];\n\n\t\t\t\tchild.updateWorldMatrix( false, true );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Serializes the 3D object into JSON.\n\t *\n\t * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.\n\t * @return {Object} A JSON object representing the serialized 3D object.\n\t * @see {@link ObjectLoader#parse}\n\t */\n\ttoJSON( meta ) {\n\n\t\t// meta is a string when called from JSON.stringify\n\t\tconst isRootObject = ( meta === undefined || typeof meta === 'string' );\n\n\t\tconst output = {};\n\n\t\t// meta is a hash used to collect geometries, materials.\n\t\t// not providing it implies that this is the root object\n\t\t// being serialized.\n\t\tif ( isRootObject ) {\n\n\t\t\t// initialize meta obj\n\t\t\tmeta = {\n\t\t\t\tgeometries: {},\n\t\t\t\tmaterials: {},\n\t\t\t\ttextures: {},\n\t\t\t\timages: {},\n\t\t\t\tshapes: {},\n\t\t\t\tskeletons: {},\n\t\t\t\tanimations: {},\n\t\t\t\tnodes: {}\n\t\t\t};\n\n\t\t\toutput.metadata = {\n\t\t\t\tversion: 4.7,\n\t\t\t\ttype: 'Object',\n\t\t\t\tgenerator: 'Object3D.toJSON'\n\t\t\t};\n\n\t\t}\n\n\t\t// standard Object3D serialization\n\n\t\tconst object = {};\n\n\t\tobject.uuid = this.uuid;\n\t\tobject.type = this.type;\n\n\t\tif ( this.name !== '' ) object.name = this.name;\n\t\tif ( this.castShadow === true ) object.castShadow = true;\n\t\tif ( this.receiveShadow === true ) object.receiveShadow = true;\n\t\tif ( this.visible === false ) object.visible = false;\n\t\tif ( this.frustumCulled === false ) object.frustumCulled = false;\n\t\tif ( this.renderOrder !== 0 ) object.renderOrder = this.renderOrder;\n\t\tif ( Object.keys( this.userData ).length > 0 ) object.userData = this.userData;\n\n\t\tobject.layers = this.layers.mask;\n\t\tobject.matrix = this.matrix.toArray();\n\t\tobject.up = this.up.toArray();\n\n\t\tif ( this.matrixAutoUpdate === false ) object.matrixAutoUpdate = false;\n\n\t\t// object specific properties\n\n\t\tif ( this.isInstancedMesh ) {\n\n\t\t\tobject.type = 'InstancedMesh';\n\t\t\tobject.count = this.count;\n\t\t\tobject.instanceMatrix = this.instanceMatrix.toJSON();\n\t\t\tif ( this.instanceColor !== null ) object.instanceColor = this.instanceColor.toJSON();\n\n\t\t}\n\n\t\tif ( this.isBatchedMesh ) {\n\n\t\t\tobject.type = 'BatchedMesh';\n\t\t\tobject.perObjectFrustumCulled = this.perObjectFrustumCulled;\n\t\t\tobject.sortObjects = this.sortObjects;\n\n\t\t\tobject.drawRanges = this._drawRanges;\n\t\t\tobject.reservedRanges = this._reservedRanges;\n\n\t\t\tobject.geometryInfo = this._geometryInfo.map( info => ( {\n\t\t\t\t...info,\n\t\t\t\tboundingBox: info.boundingBox ? info.boundingBox.toJSON() : undefined,\n\t\t\t\tboundingSphere: info.boundingSphere ? info.boundingSphere.toJSON() : undefined\n\t\t\t} ) );\n\t\t\tobject.instanceInfo = this._instanceInfo.map( info => ( { ...info } ) );\n\n\t\t\tobject.availableInstanceIds = this._availableInstanceIds.slice();\n\t\t\tobject.availableGeometryIds = this._availableGeometryIds.slice();\n\n\t\t\tobject.nextIndexStart = this._nextIndexStart;\n\t\t\tobject.nextVertexStart = this._nextVertexStart;\n\t\t\tobject.geometryCount = this._geometryCount;\n\n\t\t\tobject.maxInstanceCount = this._maxInstanceCount;\n\t\t\tobject.maxVertexCount = this._maxVertexCount;\n\t\t\tobject.maxIndexCount = this._maxIndexCount;\n\n\t\t\tobject.geometryInitialized = this._geometryInitialized;\n\n\t\t\tobject.matricesTexture = this._matricesTexture.toJSON( meta );\n\n\t\t\tobject.indirectTexture = this._indirectTexture.toJSON( meta );\n\n\t\t\tif ( this._colorsTexture !== null ) {\n\n\t\t\t\tobject.colorsTexture = this._colorsTexture.toJSON( meta );\n\n\t\t\t}\n\n\t\t\tif ( this.boundingSphere !== null ) {\n\n\t\t\t\tobject.boundingSphere = this.boundingSphere.toJSON();\n\n\t\t\t}\n\n\t\t\tif ( this.boundingBox !== null ) {\n\n\t\t\t\tobject.boundingBox = this.boundingBox.toJSON();\n\n\t\t\t}\n\n\t\t}\n\n\t\t//\n\n\t\tfunction serialize( library, element ) {\n\n\t\t\tif ( library[ element.uuid ] === undefined ) {\n\n\t\t\t\tlibrary[ element.uuid ] = element.toJSON( meta );\n\n\t\t\t}\n\n\t\t\treturn element.uuid;\n\n\t\t}\n\n\t\tif ( this.isScene ) {\n\n\t\t\tif ( this.background ) {\n\n\t\t\t\tif ( this.background.isColor ) {\n\n\t\t\t\t\tobject.background = this.background.toJSON();\n\n\t\t\t\t} else if ( this.background.isTexture ) {\n\n\t\t\t\t\tobject.background = this.background.toJSON( meta ).uuid;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( this.environment && this.environment.isTexture && this.environment.isRenderTargetTexture !== true ) {\n\n\t\t\t\tobject.environment = this.environment.toJSON( meta ).uuid;\n\n\t\t\t}\n\n\t\t} else if ( this.isMesh || this.isLine || this.isPoints ) {\n\n\t\t\tobject.geometry = serialize( meta.geometries, this.geometry );\n\n\t\t\tconst parameters = this.geometry.parameters;\n\n\t\t\tif ( parameters !== undefined && parameters.shapes !== undefined ) {\n\n\t\t\t\tconst shapes = parameters.shapes;\n\n\t\t\t\tif ( Array.isArray( shapes ) ) {\n\n\t\t\t\t\tfor ( let i = 0, l = shapes.length; i < l; i ++ ) {\n\n\t\t\t\t\t\tconst shape = shapes[ i ];\n\n\t\t\t\t\t\tserialize( meta.shapes, shape );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tserialize( meta.shapes, shapes );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( this.isSkinnedMesh ) {\n\n\t\t\tobject.bindMode = this.bindMode;\n\t\t\tobject.bindMatrix = this.bindMatrix.toArray();\n\n\t\t\tif ( this.skeleton !== undefined ) {\n\n\t\t\t\tserialize( meta.skeletons, this.skeleton );\n\n\t\t\t\tobject.skeleton = this.skeleton.uuid;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( this.material !== undefined ) {\n\n\t\t\tif ( Array.isArray( this.material ) ) {\n\n\t\t\t\tconst uuids = [];\n\n\t\t\t\tfor ( let i = 0, l = this.material.length; i < l; i ++ ) {\n\n\t\t\t\t\tuuids.push( serialize( meta.materials, this.material[ i ] ) );\n\n\t\t\t\t}\n\n\t\t\t\tobject.material = uuids;\n\n\t\t\t} else {\n\n\t\t\t\tobject.material = serialize( meta.materials, this.material );\n\n\t\t\t}\n\n\t\t}\n\n\t\t//\n\n\t\tif ( this.children.length > 0 ) {\n\n\t\t\tobject.children = [];\n\n\t\t\tfor ( let i = 0; i < this.children.length; i ++ ) {\n\n\t\t\t\tobject.children.push( this.children[ i ].toJSON( meta ).object );\n\n\t\t\t}\n\n\t\t}\n\n\t\t//\n\n\t\tif ( this.animations.length > 0 ) {\n\n\t\t\tobject.animations = [];\n\n\t\t\tfor ( let i = 0; i < this.animations.length; i ++ ) {\n\n\t\t\t\tconst animation = this.animations[ i ];\n\n\t\t\t\tobject.animations.push( serialize( meta.animations, animation ) );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( isRootObject ) {\n\n\t\t\tconst geometries = extractFromCache( meta.geometries );\n\t\t\tconst materials = extractFromCache( meta.materials );\n\t\t\tconst textures = extractFromCache( meta.textures );\n\t\t\tconst images = extractFromCache( meta.images );\n\t\t\tconst shapes = extractFromCache( meta.shapes );\n\t\t\tconst skeletons = extractFromCache( meta.skeletons );\n\t\t\tconst animations = extractFromCache( meta.animations );\n\t\t\tconst nodes = extractFromCache( meta.nodes );\n\n\t\t\tif ( geometries.length > 0 ) output.geometries = geometries;\n\t\t\tif ( materials.length > 0 ) output.materials = materials;\n\t\t\tif ( textures.length > 0 ) output.textures = textures;\n\t\t\tif ( images.length > 0 ) output.images = images;\n\t\t\tif ( shapes.length > 0 ) output.shapes = shapes;\n\t\t\tif ( skeletons.length > 0 ) output.skeletons = skeletons;\n\t\t\tif ( animations.length > 0 ) output.animations = animations;\n\t\t\tif ( nodes.length > 0 ) output.nodes = nodes;\n\n\t\t}\n\n\t\toutput.object = object;\n\n\t\treturn output;\n\n\t\t// extract data from the cache hash\n\t\t// remove metadata on each item\n\t\t// and return as array\n\t\tfunction extractFromCache( cache ) {\n\n\t\t\tconst values = [];\n\t\t\tfor ( const key in cache ) {\n\n\t\t\t\tconst data = cache[ key ];\n\t\t\t\tdelete data.metadata;\n\t\t\t\tvalues.push( data );\n\n\t\t\t}\n\n\t\t\treturn values;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns a new 3D object with copied values from this instance.\n\t *\n\t * @param {boolean} [recursive=true] - When set to `true`, descendants of the 3D object are also cloned.\n\t * @return {Object3D} A clone of this instance.\n\t */\n\tclone( recursive ) {\n\n\t\treturn new this.constructor().copy( this, recursive );\n\n\t}\n\n\t/**\n\t * Copies the values of the given 3D object to this instance.\n\t *\n\t * @param {Object3D} source - The 3D object to copy.\n\t * @param {boolean} [recursive=true] - When set to `true`, descendants of the 3D object are cloned.\n\t * @return {Object3D} A reference to this instance.\n\t */\n\tcopy( source, recursive = true ) {\n\n\t\tthis.name = source.name;\n\n\t\tthis.up.copy( source.up );\n\n\t\tthis.position.copy( source.position );\n\t\tthis.rotation.order = source.rotation.order;\n\t\tthis.quaternion.copy( source.quaternion );\n\t\tthis.scale.copy( source.scale );\n\n\t\tthis.matrix.copy( source.matrix );\n\t\tthis.matrixWorld.copy( source.matrixWorld );\n\n\t\tthis.matrixAutoUpdate = source.matrixAutoUpdate;\n\n\t\tthis.matrixWorldAutoUpdate = source.matrixWorldAutoUpdate;\n\t\tthis.matrixWorldNeedsUpdate = source.matrixWorldNeedsUpdate;\n\n\t\tthis.layers.mask = source.layers.mask;\n\t\tthis.visible = source.visible;\n\n\t\tthis.castShadow = source.castShadow;\n\t\tthis.receiveShadow = source.receiveShadow;\n\n\t\tthis.frustumCulled = source.frustumCulled;\n\t\tthis.renderOrder = source.renderOrder;\n\n\t\tthis.animations = source.animations.slice();\n\n\t\tthis.userData = JSON.parse( JSON.stringify( source.userData ) );\n\n\t\tif ( recursive === true ) {\n\n\t\t\tfor ( let i = 0; i < source.children.length; i ++ ) {\n\n\t\t\t\tconst child = source.children[ i ];\n\t\t\t\tthis.add( child.clone() );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * The default up direction for objects, also used as the default\n * position for {@link DirectionalLight} and {@link HemisphereLight}.\n *\n * @static\n * @type {Vector3}\n * @default (0,1,0)\n */\nObject3D.DEFAULT_UP = /*@__PURE__*/ new Vector3( 0, 1, 0 );\n\n/**\n * The default setting for {@link Object3D#matrixAutoUpdate} for\n * newly created 3D objects.\n *\n * @static\n * @type {boolean}\n * @default true\n */\nObject3D.DEFAULT_MATRIX_AUTO_UPDATE = true;\n\n/**\n * The default setting for {@link Object3D#matrixWorldAutoUpdate} for\n * newly created 3D objects.\n *\n * @static\n * @type {boolean}\n * @default true\n */\nObject3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE = true;\n\nconst _v0$1 = /*@__PURE__*/ new Vector3();\nconst _v1$3 = /*@__PURE__*/ new Vector3();\nconst _v2$2 = /*@__PURE__*/ new Vector3();\nconst _v3$2 = /*@__PURE__*/ new Vector3();\n\nconst _vab = /*@__PURE__*/ new Vector3();\nconst _vac = /*@__PURE__*/ new Vector3();\nconst _vbc = /*@__PURE__*/ new Vector3();\nconst _vap = /*@__PURE__*/ new Vector3();\nconst _vbp = /*@__PURE__*/ new Vector3();\nconst _vcp = /*@__PURE__*/ new Vector3();\n\nconst _v40 = /*@__PURE__*/ new Vector4();\nconst _v41 = /*@__PURE__*/ new Vector4();\nconst _v42 = /*@__PURE__*/ new Vector4();\n\n/**\n * A geometric triangle as defined by three vectors representing its three corners.\n */\nclass Triangle {\n\n\t/**\n\t * Constructs a new triangle.\n\t *\n\t * @param {Vector3} [a=(0,0,0)] - The first corner of the triangle.\n\t * @param {Vector3} [b=(0,0,0)] - The second corner of the triangle.\n\t * @param {Vector3} [c=(0,0,0)] - The third corner of the triangle.\n\t */\n\tconstructor( a = new Vector3(), b = new Vector3(), c = new Vector3() ) {\n\n\t\t/**\n\t\t * The first corner of the triangle.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.a = a;\n\n\t\t/**\n\t\t * The second corner of the triangle.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.b = b;\n\n\t\t/**\n\t\t * The third corner of the triangle.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.c = c;\n\n\t}\n\n\t/**\n\t * Computes the normal vector of a triangle.\n\t *\n\t * @param {Vector3} a - The first corner of the triangle.\n\t * @param {Vector3} b - The second corner of the triangle.\n\t * @param {Vector3} c - The third corner of the triangle.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The triangle's normal.\n\t */\n\tstatic getNormal( a, b, c, target ) {\n\n\t\ttarget.subVectors( c, b );\n\t\t_v0$1.subVectors( a, b );\n\t\ttarget.cross( _v0$1 );\n\n\t\tconst targetLengthSq = target.lengthSq();\n\t\tif ( targetLengthSq > 0 ) {\n\n\t\t\treturn target.multiplyScalar( 1 / Math.sqrt( targetLengthSq ) );\n\n\t\t}\n\n\t\treturn target.set( 0, 0, 0 );\n\n\t}\n\n\t/**\n\t * Computes a barycentric coordinates from the given vector.\n\t * Returns `null` if the triangle is degenerate.\n\t *\n\t * @param {Vector3} point - A point in 3D space.\n\t * @param {Vector3} a - The first corner of the triangle.\n\t * @param {Vector3} b - The second corner of the triangle.\n\t * @param {Vector3} c - The third corner of the triangle.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {?Vector3} The barycentric coordinates for the given point\n\t */\n\tstatic getBarycoord( point, a, b, c, target ) {\n\n\t\t// based on: http://www.blackpawn.com/texts/pointinpoly/default.html\n\n\t\t_v0$1.subVectors( c, a );\n\t\t_v1$3.subVectors( b, a );\n\t\t_v2$2.subVectors( point, a );\n\n\t\tconst dot00 = _v0$1.dot( _v0$1 );\n\t\tconst dot01 = _v0$1.dot( _v1$3 );\n\t\tconst dot02 = _v0$1.dot( _v2$2 );\n\t\tconst dot11 = _v1$3.dot( _v1$3 );\n\t\tconst dot12 = _v1$3.dot( _v2$2 );\n\n\t\tconst denom = ( dot00 * dot11 - dot01 * dot01 );\n\n\t\t// collinear or singular triangle\n\t\tif ( denom === 0 ) {\n\n\t\t\ttarget.set( 0, 0, 0 );\n\t\t\treturn null;\n\n\t\t}\n\n\t\tconst invDenom = 1 / denom;\n\t\tconst u = ( dot11 * dot02 - dot01 * dot12 ) * invDenom;\n\t\tconst v = ( dot00 * dot12 - dot01 * dot02 ) * invDenom;\n\n\t\t// barycentric coordinates must always sum to 1\n\t\treturn target.set( 1 - u - v, v, u );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given point, when projected onto the plane of the\n\t * triangle, lies within the triangle.\n\t *\n\t * @param {Vector3} point - The point in 3D space to test.\n\t * @param {Vector3} a - The first corner of the triangle.\n\t * @param {Vector3} b - The second corner of the triangle.\n\t * @param {Vector3} c - The third corner of the triangle.\n\t * @return {boolean} Whether the given point, when projected onto the plane of the\n\t * triangle, lies within the triangle or not.\n\t */\n\tstatic containsPoint( point, a, b, c ) {\n\n\t\t// if the triangle is degenerate then we can't contain a point\n\t\tif ( this.getBarycoord( point, a, b, c, _v3$2 ) === null ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\treturn ( _v3$2.x >= 0 ) && ( _v3$2.y >= 0 ) && ( ( _v3$2.x + _v3$2.y ) <= 1 );\n\n\t}\n\n\t/**\n\t * Computes the value barycentrically interpolated for the given point on the\n\t * triangle. Returns `null` if the triangle is degenerate.\n\t *\n\t * @param {Vector3} point - Position of interpolated point.\n\t * @param {Vector3} p1 - The first corner of the triangle.\n\t * @param {Vector3} p2 - The second corner of the triangle.\n\t * @param {Vector3} p3 - The third corner of the triangle.\n\t * @param {Vector3} v1 - Value to interpolate of first vertex.\n\t * @param {Vector3} v2 - Value to interpolate of second vertex.\n\t * @param {Vector3} v3 - Value to interpolate of third vertex.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {?Vector3} The interpolated value.\n\t */\n\tstatic getInterpolation( point, p1, p2, p3, v1, v2, v3, target ) {\n\n\t\tif ( this.getBarycoord( point, p1, p2, p3, _v3$2 ) === null ) {\n\n\t\t\ttarget.x = 0;\n\t\t\ttarget.y = 0;\n\t\t\tif ( 'z' in target ) target.z = 0;\n\t\t\tif ( 'w' in target ) target.w = 0;\n\t\t\treturn null;\n\n\t\t}\n\n\t\ttarget.setScalar( 0 );\n\t\ttarget.addScaledVector( v1, _v3$2.x );\n\t\ttarget.addScaledVector( v2, _v3$2.y );\n\t\ttarget.addScaledVector( v3, _v3$2.z );\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Computes the value barycentrically interpolated for the given attribute and indices.\n\t *\n\t * @param {BufferAttribute} attr - The attribute to interpolate.\n\t * @param {number} i1 - Index of first vertex.\n\t * @param {number} i2 - Index of second vertex.\n\t * @param {number} i3 - Index of third vertex.\n\t * @param {Vector3} barycoord - The barycoordinate value to use to interpolate.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The interpolated attribute value.\n\t */\n\tstatic getInterpolatedAttribute( attr, i1, i2, i3, barycoord, target ) {\n\n\t\t_v40.setScalar( 0 );\n\t\t_v41.setScalar( 0 );\n\t\t_v42.setScalar( 0 );\n\n\t\t_v40.fromBufferAttribute( attr, i1 );\n\t\t_v41.fromBufferAttribute( attr, i2 );\n\t\t_v42.fromBufferAttribute( attr, i3 );\n\n\t\ttarget.setScalar( 0 );\n\t\ttarget.addScaledVector( _v40, barycoord.x );\n\t\ttarget.addScaledVector( _v41, barycoord.y );\n\t\ttarget.addScaledVector( _v42, barycoord.z );\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Returns `true` if the triangle is oriented towards the given direction.\n\t *\n\t * @param {Vector3} a - The first corner of the triangle.\n\t * @param {Vector3} b - The second corner of the triangle.\n\t * @param {Vector3} c - The third corner of the triangle.\n\t * @param {Vector3} direction - The (normalized) direction vector.\n\t * @return {boolean} Whether the triangle is oriented towards the given direction or not.\n\t */\n\tstatic isFrontFacing( a, b, c, direction ) {\n\n\t\t_v0$1.subVectors( c, b );\n\t\t_v1$3.subVectors( a, b );\n\n\t\t// strictly front facing\n\t\treturn ( _v0$1.cross( _v1$3 ).dot( direction ) < 0 ) ? true : false;\n\n\t}\n\n\t/**\n\t * Sets the triangle's vertices by copying the given values.\n\t *\n\t * @param {Vector3} a - The first corner of the triangle.\n\t * @param {Vector3} b - The second corner of the triangle.\n\t * @param {Vector3} c - The third corner of the triangle.\n\t * @return {Triangle} A reference to this triangle.\n\t */\n\tset( a, b, c ) {\n\n\t\tthis.a.copy( a );\n\t\tthis.b.copy( b );\n\t\tthis.c.copy( c );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the triangle's vertices by copying the given array values.\n\t *\n\t * @param {Array<Vector3>} points - An array with 3D points.\n\t * @param {number} i0 - The array index representing the first corner of the triangle.\n\t * @param {number} i1 - The array index representing the second corner of the triangle.\n\t * @param {number} i2 - The array index representing the third corner of the triangle.\n\t * @return {Triangle} A reference to this triangle.\n\t */\n\tsetFromPointsAndIndices( points, i0, i1, i2 ) {\n\n\t\tthis.a.copy( points[ i0 ] );\n\t\tthis.b.copy( points[ i1 ] );\n\t\tthis.c.copy( points[ i2 ] );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the triangle's vertices by copying the given attribute values.\n\t *\n\t * @param {BufferAttribute} attribute - A buffer attribute with 3D points data.\n\t * @param {number} i0 - The attribute index representing the first corner of the triangle.\n\t * @param {number} i1 - The attribute index representing the second corner of the triangle.\n\t * @param {number} i2 - The attribute index representing the third corner of the triangle.\n\t * @return {Triangle} A reference to this triangle.\n\t */\n\tsetFromAttributeAndIndices( attribute, i0, i1, i2 ) {\n\n\t\tthis.a.fromBufferAttribute( attribute, i0 );\n\t\tthis.b.fromBufferAttribute( attribute, i1 );\n\t\tthis.c.fromBufferAttribute( attribute, i2 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new triangle with copied values from this instance.\n\t *\n\t * @return {Triangle} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Copies the values of the given triangle to this instance.\n\t *\n\t * @param {Triangle} triangle - The triangle to copy.\n\t * @return {Triangle} A reference to this triangle.\n\t */\n\tcopy( triangle ) {\n\n\t\tthis.a.copy( triangle.a );\n\t\tthis.b.copy( triangle.b );\n\t\tthis.c.copy( triangle.c );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes the area of the triangle.\n\t *\n\t * @return {number} The triangle's area.\n\t */\n\tgetArea() {\n\n\t\t_v0$1.subVectors( this.c, this.b );\n\t\t_v1$3.subVectors( this.a, this.b );\n\n\t\treturn _v0$1.cross( _v1$3 ).length() * 0.5;\n\n\t}\n\n\t/**\n\t * Computes the midpoint of the triangle.\n\t *\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The triangle's midpoint.\n\t */\n\tgetMidpoint( target ) {\n\n\t\treturn target.addVectors( this.a, this.b ).add( this.c ).multiplyScalar( 1 / 3 );\n\n\t}\n\n\t/**\n\t * Computes the normal of the triangle.\n\t *\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The triangle's normal.\n\t */\n\tgetNormal( target ) {\n\n\t\treturn Triangle.getNormal( this.a, this.b, this.c, target );\n\n\t}\n\n\t/**\n\t * Computes a plane the triangle lies within.\n\t *\n\t * @param {Plane} target - The target vector that is used to store the method's result.\n\t * @return {Plane} The plane the triangle lies within.\n\t */\n\tgetPlane( target ) {\n\n\t\treturn target.setFromCoplanarPoints( this.a, this.b, this.c );\n\n\t}\n\n\t/**\n\t * Computes a barycentric coordinates from the given vector.\n\t * Returns `null` if the triangle is degenerate.\n\t *\n\t * @param {Vector3} point - A point in 3D space.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {?Vector3} The barycentric coordinates for the given point\n\t */\n\tgetBarycoord( point, target ) {\n\n\t\treturn Triangle.getBarycoord( point, this.a, this.b, this.c, target );\n\n\t}\n\n\t/**\n\t * Computes the value barycentrically interpolated for the given point on the\n\t * triangle. Returns `null` if the triangle is degenerate.\n\t *\n\t * @param {Vector3} point - Position of interpolated point.\n\t * @param {Vector3} v1 - Value to interpolate of first vertex.\n\t * @param {Vector3} v2 - Value to interpolate of second vertex.\n\t * @param {Vector3} v3 - Value to interpolate of third vertex.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {?Vector3} The interpolated value.\n\t */\n\tgetInterpolation( point, v1, v2, v3, target ) {\n\n\t\treturn Triangle.getInterpolation( point, this.a, this.b, this.c, v1, v2, v3, target );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given point, when projected onto the plane of the\n\t * triangle, lies within the triangle.\n\t *\n\t * @param {Vector3} point - The point in 3D space to test.\n\t * @return {boolean} Whether the given point, when projected onto the plane of the\n\t * triangle, lies within the triangle or not.\n\t */\n\tcontainsPoint( point ) {\n\n\t\treturn Triangle.containsPoint( point, this.a, this.b, this.c );\n\n\t}\n\n\t/**\n\t * Returns `true` if the triangle is oriented towards the given direction.\n\t *\n\t * @param {Vector3} direction - The (normalized) direction vector.\n\t * @return {boolean} Whether the triangle is oriented towards the given direction or not.\n\t */\n\tisFrontFacing( direction ) {\n\n\t\treturn Triangle.isFrontFacing( this.a, this.b, this.c, direction );\n\n\t}\n\n\t/**\n\t * Returns `true` if this triangle intersects with the given box.\n\t *\n\t * @param {Box3} box - The box to intersect.\n\t * @return {boolean} Whether this triangle intersects with the given box or not.\n\t */\n\tintersectsBox( box ) {\n\n\t\treturn box.intersectsTriangle( this );\n\n\t}\n\n\t/**\n\t * Returns the closest point on the triangle to the given point.\n\t *\n\t * @param {Vector3} p - The point to compute the closest point for.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The closest point on the triangle.\n\t */\n\tclosestPointToPoint( p, target ) {\n\n\t\tconst a = this.a, b = this.b, c = this.c;\n\t\tlet v, w;\n\n\t\t// algorithm thanks to Real-Time Collision Detection by Christer Ericson,\n\t\t// published by Morgan Kaufmann Publishers, (c) 2005 Elsevier Inc.,\n\t\t// under the accompanying license; see chapter 5.1.5 for detailed explanation.\n\t\t// basically, we're distinguishing which of the voronoi regions of the triangle\n\t\t// the point lies in with the minimum amount of redundant computation.\n\n\t\t_vab.subVectors( b, a );\n\t\t_vac.subVectors( c, a );\n\t\t_vap.subVectors( p, a );\n\t\tconst d1 = _vab.dot( _vap );\n\t\tconst d2 = _vac.dot( _vap );\n\t\tif ( d1 <= 0 && d2 <= 0 ) {\n\n\t\t\t// vertex region of A; barycentric coords (1, 0, 0)\n\t\t\treturn target.copy( a );\n\n\t\t}\n\n\t\t_vbp.subVectors( p, b );\n\t\tconst d3 = _vab.dot( _vbp );\n\t\tconst d4 = _vac.dot( _vbp );\n\t\tif ( d3 >= 0 && d4 <= d3 ) {\n\n\t\t\t// vertex region of B; barycentric coords (0, 1, 0)\n\t\t\treturn target.copy( b );\n\n\t\t}\n\n\t\tconst vc = d1 * d4 - d3 * d2;\n\t\tif ( vc <= 0 && d1 >= 0 && d3 <= 0 ) {\n\n\t\t\tv = d1 / ( d1 - d3 );\n\t\t\t// edge region of AB; barycentric coords (1-v, v, 0)\n\t\t\treturn target.copy( a ).addScaledVector( _vab, v );\n\n\t\t}\n\n\t\t_vcp.subVectors( p, c );\n\t\tconst d5 = _vab.dot( _vcp );\n\t\tconst d6 = _vac.dot( _vcp );\n\t\tif ( d6 >= 0 && d5 <= d6 ) {\n\n\t\t\t// vertex region of C; barycentric coords (0, 0, 1)\n\t\t\treturn target.copy( c );\n\n\t\t}\n\n\t\tconst vb = d5 * d2 - d1 * d6;\n\t\tif ( vb <= 0 && d2 >= 0 && d6 <= 0 ) {\n\n\t\t\tw = d2 / ( d2 - d6 );\n\t\t\t// edge region of AC; barycentric coords (1-w, 0, w)\n\t\t\treturn target.copy( a ).addScaledVector( _vac, w );\n\n\t\t}\n\n\t\tconst va = d3 * d6 - d5 * d4;\n\t\tif ( va <= 0 && ( d4 - d3 ) >= 0 && ( d5 - d6 ) >= 0 ) {\n\n\t\t\t_vbc.subVectors( c, b );\n\t\t\tw = ( d4 - d3 ) / ( ( d4 - d3 ) + ( d5 - d6 ) );\n\t\t\t// edge region of BC; barycentric coords (0, 1-w, w)\n\t\t\treturn target.copy( b ).addScaledVector( _vbc, w ); // edge region of BC\n\n\t\t}\n\n\t\t// face region\n\t\tconst denom = 1 / ( va + vb + vc );\n\t\t// u = va * denom\n\t\tv = vb * denom;\n\t\tw = vc * denom;\n\n\t\treturn target.copy( a ).addScaledVector( _vab, v ).addScaledVector( _vac, w );\n\n\t}\n\n\t/**\n\t * Returns `true` if this triangle is equal with the given one.\n\t *\n\t * @param {Triangle} triangle - The triangle to test for equality.\n\t * @return {boolean} Whether this triangle is equal with the given one.\n\t */\n\tequals( triangle ) {\n\n\t\treturn triangle.a.equals( this.a ) && triangle.b.equals( this.b ) && triangle.c.equals( this.c );\n\n\t}\n\n}\n\nconst _colorKeywords = { 'aliceblue': 0xF0F8FF, 'antiquewhite': 0xFAEBD7, 'aqua': 0x00FFFF, 'aquamarine': 0x7FFFD4, 'azure': 0xF0FFFF,\n\t'beige': 0xF5F5DC, 'bisque': 0xFFE4C4, 'black': 0x000000, 'blanchedalmond': 0xFFEBCD, 'blue': 0x0000FF, 'blueviolet': 0x8A2BE2,\n\t'brown': 0xA52A2A, 'burlywood': 0xDEB887, 'cadetblue': 0x5F9EA0, 'chartreuse': 0x7FFF00, 'chocolate': 0xD2691E, 'coral': 0xFF7F50,\n\t'cornflowerblue': 0x6495ED, 'cornsilk': 0xFFF8DC, 'crimson': 0xDC143C, 'cyan': 0x00FFFF, 'darkblue': 0x00008B, 'darkcyan': 0x008B8B,\n\t'darkgoldenrod': 0xB8860B, 'darkgray': 0xA9A9A9, 'darkgreen': 0x006400, 'darkgrey': 0xA9A9A9, 'darkkhaki': 0xBDB76B, 'darkmagenta': 0x8B008B,\n\t'darkolivegreen': 0x556B2F, 'darkorange': 0xFF8C00, 'darkorchid': 0x9932CC, 'darkred': 0x8B0000, 'darksalmon': 0xE9967A, 'darkseagreen': 0x8FBC8F,\n\t'darkslateblue': 0x483D8B, 'darkslategray': 0x2F4F4F, 'darkslategrey': 0x2F4F4F, 'darkturquoise': 0x00CED1, 'darkviolet': 0x9400D3,\n\t'deeppink': 0xFF1493, 'deepskyblue': 0x00BFFF, 'dimgray': 0x696969, 'dimgrey': 0x696969, 'dodgerblue': 0x1E90FF, 'firebrick': 0xB22222,\n\t'floralwhite': 0xFFFAF0, 'forestgreen': 0x228B22, 'fuchsia': 0xFF00FF, 'gainsboro': 0xDCDCDC, 'ghostwhite': 0xF8F8FF, 'gold': 0xFFD700,\n\t'goldenrod': 0xDAA520, 'gray': 0x808080, 'green': 0x008000, 'greenyellow': 0xADFF2F, 'grey': 0x808080, 'honeydew': 0xF0FFF0, 'hotpink': 0xFF69B4,\n\t'indianred': 0xCD5C5C, 'indigo': 0x4B0082, 'ivory': 0xFFFFF0, 'khaki': 0xF0E68C, 'lavender': 0xE6E6FA, 'lavenderblush': 0xFFF0F5, 'lawngreen': 0x7CFC00,\n\t'lemonchiffon': 0xFFFACD, 'lightblue': 0xADD8E6, 'lightcoral': 0xF08080, 'lightcyan': 0xE0FFFF, 'lightgoldenrodyellow': 0xFAFAD2, 'lightgray': 0xD3D3D3,\n\t'lightgreen': 0x90EE90, 'lightgrey': 0xD3D3D3, 'lightpink': 0xFFB6C1, 'lightsalmon': 0xFFA07A, 'lightseagreen': 0x20B2AA, 'lightskyblue': 0x87CEFA,\n\t'lightslategray': 0x778899, 'lightslategrey': 0x778899, 'lightsteelblue': 0xB0C4DE, 'lightyellow': 0xFFFFE0, 'lime': 0x00FF00, 'limegreen': 0x32CD32,\n\t'linen': 0xFAF0E6, 'magenta': 0xFF00FF, 'maroon': 0x800000, 'mediumaquamarine': 0x66CDAA, 'mediumblue': 0x0000CD, 'mediumorchid': 0xBA55D3,\n\t'mediumpurple': 0x9370DB, 'mediumseagreen': 0x3CB371, 'mediumslateblue': 0x7B68EE, 'mediumspringgreen': 0x00FA9A, 'mediumturquoise': 0x48D1CC,\n\t'mediumvioletred': 0xC71585, 'midnightblue': 0x191970, 'mintcream': 0xF5FFFA, 'mistyrose': 0xFFE4E1, 'moccasin': 0xFFE4B5, 'navajowhite': 0xFFDEAD,\n\t'navy': 0x000080, 'oldlace': 0xFDF5E6, 'olive': 0x808000, 'olivedrab': 0x6B8E23, 'orange': 0xFFA500, 'orangered': 0xFF4500, 'orchid': 0xDA70D6,\n\t'palegoldenrod': 0xEEE8AA, 'palegreen': 0x98FB98, 'paleturquoise': 0xAFEEEE, 'palevioletred': 0xDB7093, 'papayawhip': 0xFFEFD5, 'peachpuff': 0xFFDAB9,\n\t'peru': 0xCD853F, 'pink': 0xFFC0CB, 'plum': 0xDDA0DD, 'powderblue': 0xB0E0E6, 'purple': 0x800080, 'rebeccapurple': 0x663399, 'red': 0xFF0000, 'rosybrown': 0xBC8F8F,\n\t'royalblue': 0x4169E1, 'saddlebrown': 0x8B4513, 'salmon': 0xFA8072, 'sandybrown': 0xF4A460, 'seagreen': 0x2E8B57, 'seashell': 0xFFF5EE,\n\t'sienna': 0xA0522D, 'silver': 0xC0C0C0, 'skyblue': 0x87CEEB, 'slateblue': 0x6A5ACD, 'slategray': 0x708090, 'slategrey': 0x708090, 'snow': 0xFFFAFA,\n\t'springgreen': 0x00FF7F, 'steelblue': 0x4682B4, 'tan': 0xD2B48C, 'teal': 0x008080, 'thistle': 0xD8BFD8, 'tomato': 0xFF6347, 'turquoise': 0x40E0D0,\n\t'violet': 0xEE82EE, 'wheat': 0xF5DEB3, 'white': 0xFFFFFF, 'whitesmoke': 0xF5F5F5, 'yellow': 0xFFFF00, 'yellowgreen': 0x9ACD32 };\n\nconst _hslA = { h: 0, s: 0, l: 0 };\nconst _hslB = { h: 0, s: 0, l: 0 };\n\nfunction hue2rgb( p, q, t ) {\n\n\tif ( t < 0 ) t += 1;\n\tif ( t > 1 ) t -= 1;\n\tif ( t < 1 / 6 ) return p + ( q - p ) * 6 * t;\n\tif ( t < 1 / 2 ) return q;\n\tif ( t < 2 / 3 ) return p + ( q - p ) * 6 * ( 2 / 3 - t );\n\treturn p;\n\n}\n\n/**\n * A Color instance is represented by RGB components in the linear <i>working\n * color space</i>, which defaults to `LinearSRGBColorSpace`. Inputs\n * conventionally using `SRGBColorSpace` (such as hexadecimals and CSS\n * strings) are converted to the working color space automatically.\n *\n * ```js\n * // converted automatically from SRGBColorSpace to LinearSRGBColorSpace\n * const color = new THREE.Color().setHex( 0x112233 );\n * ```\n * Source color spaces may be specified explicitly, to ensure correct conversions.\n * ```js\n * // assumed already LinearSRGBColorSpace; no conversion\n * const color = new THREE.Color().setRGB( 0.5, 0.5, 0.5 );\n *\n * // converted explicitly from SRGBColorSpace to LinearSRGBColorSpace\n * const color = new THREE.Color().setRGB( 0.5, 0.5, 0.5, SRGBColorSpace );\n * ```\n * If THREE.ColorManagement is disabled, no conversions occur. For details,\n * see <i>Color management</i>. Iterating through a Color instance will yield\n * its components (r, g, b) in the corresponding order. A Color can be initialised\n * in any of the following ways:\n * ```js\n * //empty constructor - will default white\n * const color1 = new THREE.Color();\n *\n * //Hexadecimal color (recommended)\n * const color2 = new THREE.Color( 0xff0000 );\n *\n * //RGB string\n * const color3 = new THREE.Color(\"rgb(255, 0, 0)\");\n * const color4 = new THREE.Color(\"rgb(100%, 0%, 0%)\");\n *\n * //X11 color name - all 140 color names are supported.\n * //Note the lack of CamelCase in the name\n * const color5 = new THREE.Color( 'skyblue' );\n * //HSL string\n * const color6 = new THREE.Color(\"hsl(0, 100%, 50%)\");\n *\n * //Separate RGB values between 0 and 1\n * const color7 = new THREE.Color( 1, 0, 0 );\n * ```\n */\nclass Color {\n\n\t/**\n\t * Constructs a new color.\n\t *\n\t * Note that standard method of specifying color in three.js is with a hexadecimal triplet,\n\t * and that method is used throughout the rest of the documentation.\n\t *\n\t * @param {(number|string|Color)} [r] - The red component of the color. If `g` and `b` are\n\t * not provided, it can be hexadecimal triplet, a CSS-style string or another `Color` instance.\n\t * @param {number} [g] - The green component.\n\t * @param {number} [b] - The blue component.\n\t */\n\tconstructor( r, g, b ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isColor = true;\n\n\t\t/**\n\t\t * The red component.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.r = 1;\n\n\t\t/**\n\t\t * The green component.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.g = 1;\n\n\t\t/**\n\t\t * The blue component.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.b = 1;\n\n\t\treturn this.set( r, g, b );\n\n\t}\n\n\t/**\n\t * Sets the colors's components from the given values.\n\t *\n\t * @param {(number|string|Color)} [r] - The red component of the color. If `g` and `b` are\n\t * not provided, it can be hexadecimal triplet, a CSS-style string or another `Color` instance.\n\t * @param {number} [g] - The green component.\n\t * @param {number} [b] - The blue component.\n\t * @return {Color} A reference to this color.\n\t */\n\tset( r, g, b ) {\n\n\t\tif ( g === undefined && b === undefined ) {\n\n\t\t\t// r is THREE.Color, hex or string\n\n\t\t\tconst value = r;\n\n\t\t\tif ( value && value.isColor ) {\n\n\t\t\t\tthis.copy( value );\n\n\t\t\t} else if ( typeof value === 'number' ) {\n\n\t\t\t\tthis.setHex( value );\n\n\t\t\t} else if ( typeof value === 'string' ) {\n\n\t\t\t\tthis.setStyle( value );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tthis.setRGB( r, g, b );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the colors's components to the given scalar value.\n\t *\n\t * @param {number} scalar - The scalar value.\n\t * @return {Color} A reference to this color.\n\t */\n\tsetScalar( scalar ) {\n\n\t\tthis.r = scalar;\n\t\tthis.g = scalar;\n\t\tthis.b = scalar;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this color from a hexadecimal value.\n\t *\n\t * @param {number} hex - The hexadecimal value.\n\t * @param {string} [colorSpace=SRGBColorSpace] - The color space.\n\t * @return {Color} A reference to this color.\n\t */\n\tsetHex( hex, colorSpace = SRGBColorSpace ) {\n\n\t\thex = Math.floor( hex );\n\n\t\tthis.r = ( hex >> 16 & 255 ) / 255;\n\t\tthis.g = ( hex >> 8 & 255 ) / 255;\n\t\tthis.b = ( hex & 255 ) / 255;\n\n\t\tColorManagement.colorSpaceToWorking( this, colorSpace );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this color from RGB values.\n\t *\n\t * @param {number} r - Red channel value between `0.0` and `1.0`.\n\t * @param {number} g - Green channel value between `0.0` and `1.0`.\n\t * @param {number} b - Blue channel value between `0.0` and `1.0`.\n\t * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.\n\t * @return {Color} A reference to this color.\n\t */\n\tsetRGB( r, g, b, colorSpace = ColorManagement.workingColorSpace ) {\n\n\t\tthis.r = r;\n\t\tthis.g = g;\n\t\tthis.b = b;\n\n\t\tColorManagement.colorSpaceToWorking( this, colorSpace );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this color from RGB values.\n\t *\n\t * @param {number} h - Hue value between `0.0` and `1.0`.\n\t * @param {number} s - Saturation value between `0.0` and `1.0`.\n\t * @param {number} l - Lightness value between `0.0` and `1.0`.\n\t * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.\n\t * @return {Color} A reference to this color.\n\t */\n\tsetHSL( h, s, l, colorSpace = ColorManagement.workingColorSpace ) {\n\n\t\t// h,s,l ranges are in 0.0 - 1.0\n\t\th = euclideanModulo( h, 1 );\n\t\ts = clamp( s, 0, 1 );\n\t\tl = clamp( l, 0, 1 );\n\n\t\tif ( s === 0 ) {\n\n\t\t\tthis.r = this.g = this.b = l;\n\n\t\t} else {\n\n\t\t\tconst p = l <= 0.5 ? l * ( 1 + s ) : l + s - ( l * s );\n\t\t\tconst q = ( 2 * l ) - p;\n\n\t\t\tthis.r = hue2rgb( q, p, h + 1 / 3 );\n\t\t\tthis.g = hue2rgb( q, p, h );\n\t\t\tthis.b = hue2rgb( q, p, h - 1 / 3 );\n\n\t\t}\n\n\t\tColorManagement.colorSpaceToWorking( this, colorSpace );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this color from a CSS-style string. For example, `rgb(250, 0,0)`,\n\t * `rgb(100%, 0%, 0%)`, `hsl(0, 100%, 50%)`, `#ff0000`, `#f00`, or `red` ( or\n\t * any [X11 color name](https://en.wikipedia.org/wiki/X11_color_names#Color_name_chart) -\n\t * all 140 color names are supported).\n\t *\n\t * @param {string} style - Color as a CSS-style string.\n\t * @param {string} [colorSpace=SRGBColorSpace] - The color space.\n\t * @return {Color} A reference to this color.\n\t */\n\tsetStyle( style, colorSpace = SRGBColorSpace ) {\n\n\t\tfunction handleAlpha( string ) {\n\n\t\t\tif ( string === undefined ) return;\n\n\t\t\tif ( parseFloat( string ) < 1 ) {\n\n\t\t\t\twarn( 'Color: Alpha component of ' + style + ' will be ignored.' );\n\n\t\t\t}\n\n\t\t}\n\n\n\t\tlet m;\n\n\t\tif ( m = /^(\\w+)\\(([^\\)]*)\\)/.exec( style ) ) {\n\n\t\t\t// rgb / hsl\n\n\t\t\tlet color;\n\t\t\tconst name = m[ 1 ];\n\t\t\tconst components = m[ 2 ];\n\n\t\t\tswitch ( name ) {\n\n\t\t\t\tcase 'rgb':\n\t\t\t\tcase 'rgba':\n\n\t\t\t\t\tif ( color = /^\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*(\\d*\\.?\\d+)\\s*)?$/.exec( components ) ) {\n\n\t\t\t\t\t\t// rgb(255,0,0) rgba(255,0,0,0.5)\n\n\t\t\t\t\t\thandleAlpha( color[ 4 ] );\n\n\t\t\t\t\t\treturn this.setRGB(\n\t\t\t\t\t\t\tMath.min( 255, parseInt( color[ 1 ], 10 ) ) / 255,\n\t\t\t\t\t\t\tMath.min( 255, parseInt( color[ 2 ], 10 ) ) / 255,\n\t\t\t\t\t\t\tMath.min( 255, parseInt( color[ 3 ], 10 ) ) / 255,\n\t\t\t\t\t\t\tcolorSpace\n\t\t\t\t\t\t);\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( color = /^\\s*(\\d+)\\%\\s*,\\s*(\\d+)\\%\\s*,\\s*(\\d+)\\%\\s*(?:,\\s*(\\d*\\.?\\d+)\\s*)?$/.exec( components ) ) {\n\n\t\t\t\t\t\t// rgb(100%,0%,0%) rgba(100%,0%,0%,0.5)\n\n\t\t\t\t\t\thandleAlpha( color[ 4 ] );\n\n\t\t\t\t\t\treturn this.setRGB(\n\t\t\t\t\t\t\tMath.min( 100, parseInt( color[ 1 ], 10 ) ) / 100,\n\t\t\t\t\t\t\tMath.min( 100, parseInt( color[ 2 ], 10 ) ) / 100,\n\t\t\t\t\t\t\tMath.min( 100, parseInt( color[ 3 ], 10 ) ) / 100,\n\t\t\t\t\t\t\tcolorSpace\n\t\t\t\t\t\t);\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'hsl':\n\t\t\t\tcase 'hsla':\n\n\t\t\t\t\tif ( color = /^\\s*(\\d*\\.?\\d+)\\s*,\\s*(\\d*\\.?\\d+)\\%\\s*,\\s*(\\d*\\.?\\d+)\\%\\s*(?:,\\s*(\\d*\\.?\\d+)\\s*)?$/.exec( components ) ) {\n\n\t\t\t\t\t\t// hsl(120,50%,50%) hsla(120,50%,50%,0.5)\n\n\t\t\t\t\t\thandleAlpha( color[ 4 ] );\n\n\t\t\t\t\t\treturn this.setHSL(\n\t\t\t\t\t\t\tparseFloat( color[ 1 ] ) / 360,\n\t\t\t\t\t\t\tparseFloat( color[ 2 ] ) / 100,\n\t\t\t\t\t\t\tparseFloat( color[ 3 ] ) / 100,\n\t\t\t\t\t\t\tcolorSpace\n\t\t\t\t\t\t);\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\n\t\t\t\t\twarn( 'Color: Unknown color model ' + style );\n\n\t\t\t}\n\n\t\t} else if ( m = /^\\#([A-Fa-f\\d]+)$/.exec( style ) ) {\n\n\t\t\t// hex color\n\n\t\t\tconst hex = m[ 1 ];\n\t\t\tconst size = hex.length;\n\n\t\t\tif ( size === 3 ) {\n\n\t\t\t\t// #ff0\n\t\t\t\treturn this.setRGB(\n\t\t\t\t\tparseInt( hex.charAt( 0 ), 16 ) / 15,\n\t\t\t\t\tparseInt( hex.charAt( 1 ), 16 ) / 15,\n\t\t\t\t\tparseInt( hex.charAt( 2 ), 16 ) / 15,\n\t\t\t\t\tcolorSpace\n\t\t\t\t);\n\n\t\t\t} else if ( size === 6 ) {\n\n\t\t\t\t// #ff0000\n\t\t\t\treturn this.setHex( parseInt( hex, 16 ), colorSpace );\n\n\t\t\t} else {\n\n\t\t\t\twarn( 'Color: Invalid hex color ' + style );\n\n\t\t\t}\n\n\t\t} else if ( style && style.length > 0 ) {\n\n\t\t\treturn this.setColorName( style, colorSpace );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets this color from a color name. Faster than {@link Color#setStyle} if\n\t * you don't need the other CSS-style formats.\n\t *\n\t * For convenience, the list of names is exposed in `Color.NAMES` as a hash.\n\t * ```js\n\t * Color.NAMES.aliceblue // returns 0xF0F8FF\n\t * ```\n\t *\n\t * @param {string} style - The color name.\n\t * @param {string} [colorSpace=SRGBColorSpace] - The color space.\n\t * @return {Color} A reference to this color.\n\t */\n\tsetColorName( style, colorSpace = SRGBColorSpace ) {\n\n\t\t// color keywords\n\t\tconst hex = _colorKeywords[ style.toLowerCase() ];\n\n\t\tif ( hex !== undefined ) {\n\n\t\t\t// red\n\t\t\tthis.setHex( hex, colorSpace );\n\n\t\t} else {\n\n\t\t\t// unknown color\n\t\t\twarn( 'Color: Unknown color ' + style );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new color with copied values from this instance.\n\t *\n\t * @return {Color} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor( this.r, this.g, this.b );\n\n\t}\n\n\t/**\n\t * Copies the values of the given color to this instance.\n\t *\n\t * @param {Color} color - The color to copy.\n\t * @return {Color} A reference to this color.\n\t */\n\tcopy( color ) {\n\n\t\tthis.r = color.r;\n\t\tthis.g = color.g;\n\t\tthis.b = color.b;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the given color into this color, and then converts this color from\n\t * `SRGBColorSpace` to `LinearSRGBColorSpace`.\n\t *\n\t * @param {Color} color - The color to copy/convert.\n\t * @return {Color} A reference to this color.\n\t */\n\tcopySRGBToLinear( color ) {\n\n\t\tthis.r = SRGBToLinear( color.r );\n\t\tthis.g = SRGBToLinear( color.g );\n\t\tthis.b = SRGBToLinear( color.b );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the given color into this color, and then converts this color from\n\t * `LinearSRGBColorSpace` to `SRGBColorSpace`.\n\t *\n\t * @param {Color} color - The color to copy/convert.\n\t * @return {Color} A reference to this color.\n\t */\n\tcopyLinearToSRGB( color ) {\n\n\t\tthis.r = LinearToSRGB( color.r );\n\t\tthis.g = LinearToSRGB( color.g );\n\t\tthis.b = LinearToSRGB( color.b );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Converts this color from `SRGBColorSpace` to `LinearSRGBColorSpace`.\n\t *\n\t * @return {Color} A reference to this color.\n\t */\n\tconvertSRGBToLinear() {\n\n\t\tthis.copySRGBToLinear( this );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Converts this color from `LinearSRGBColorSpace` to `SRGBColorSpace`.\n\t *\n\t * @return {Color} A reference to this color.\n\t */\n\tconvertLinearToSRGB() {\n\n\t\tthis.copyLinearToSRGB( this );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the hexadecimal value of this color.\n\t *\n\t * @param {string} [colorSpace=SRGBColorSpace] - The color space.\n\t * @return {number} The hexadecimal value.\n\t */\n\tgetHex( colorSpace = SRGBColorSpace ) {\n\n\t\tColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );\n\n\t\treturn Math.round( clamp( _color.r * 255, 0, 255 ) ) * 65536 + Math.round( clamp( _color.g * 255, 0, 255 ) ) * 256 + Math.round( clamp( _color.b * 255, 0, 255 ) );\n\n\t}\n\n\t/**\n\t * Returns the hexadecimal value of this color as a string (for example, 'FFFFFF').\n\t *\n\t * @param {string} [colorSpace=SRGBColorSpace] - The color space.\n\t * @return {string} The hexadecimal value as a string.\n\t */\n\tgetHexString( colorSpace = SRGBColorSpace ) {\n\n\t\treturn ( '000000' + this.getHex( colorSpace ).toString( 16 ) ).slice( -6 );\n\n\t}\n\n\t/**\n\t * Converts the colors RGB values into the HSL format and stores them into the\n\t * given target object.\n\t *\n\t * @param {{h:number,s:number,l:number}} target - The target object that is used to store the method's result.\n\t * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.\n\t * @return {{h:number,s:number,l:number}} The HSL representation of this color.\n\t */\n\tgetHSL( target, colorSpace = ColorManagement.workingColorSpace ) {\n\n\t\t// h,s,l ranges are in 0.0 - 1.0\n\n\t\tColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );\n\n\t\tconst r = _color.r, g = _color.g, b = _color.b;\n\n\t\tconst max = Math.max( r, g, b );\n\t\tconst min = Math.min( r, g, b );\n\n\t\tlet hue, saturation;\n\t\tconst lightness = ( min + max ) / 2.0;\n\n\t\tif ( min === max ) {\n\n\t\t\thue = 0;\n\t\t\tsaturation = 0;\n\n\t\t} else {\n\n\t\t\tconst delta = max - min;\n\n\t\t\tsaturation = lightness <= 0.5 ? delta / ( max + min ) : delta / ( 2 - max - min );\n\n\t\t\tswitch ( max ) {\n\n\t\t\t\tcase r: hue = ( g - b ) / delta + ( g < b ? 6 : 0 ); break;\n\t\t\t\tcase g: hue = ( b - r ) / delta + 2; break;\n\t\t\t\tcase b: hue = ( r - g ) / delta + 4; break;\n\n\t\t\t}\n\n\t\t\thue /= 6;\n\n\t\t}\n\n\t\ttarget.h = hue;\n\t\ttarget.s = saturation;\n\t\ttarget.l = lightness;\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Returns the RGB values of this color and stores them into the given target object.\n\t *\n\t * @param {Color} target - The target color that is used to store the method's result.\n\t * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.\n\t * @return {Color} The RGB representation of this color.\n\t */\n\tgetRGB( target, colorSpace = ColorManagement.workingColorSpace ) {\n\n\t\tColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );\n\n\t\ttarget.r = _color.r;\n\t\ttarget.g = _color.g;\n\t\ttarget.b = _color.b;\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Returns the value of this color as a CSS style string. Example: `rgb(255,0,0)`.\n\t *\n\t * @param {string} [colorSpace=SRGBColorSpace] - The color space.\n\t * @return {string} The CSS representation of this color.\n\t */\n\tgetStyle( colorSpace = SRGBColorSpace ) {\n\n\t\tColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );\n\n\t\tconst r = _color.r, g = _color.g, b = _color.b;\n\n\t\tif ( colorSpace !== SRGBColorSpace ) {\n\n\t\t\t// Requires CSS Color Module Level 4 (https://www.w3.org/TR/css-color-4/).\n\t\t\treturn `color(${ colorSpace } ${ r.toFixed( 3 ) } ${ g.toFixed( 3 ) } ${ b.toFixed( 3 ) })`;\n\n\t\t}\n\n\t\treturn `rgb(${ Math.round( r * 255 ) },${ Math.round( g * 255 ) },${ Math.round( b * 255 ) })`;\n\n\t}\n\n\t/**\n\t * Adds the given HSL values to this color's values.\n\t * Internally, this converts the color's RGB values to HSL, adds HSL\n\t * and then converts the color back to RGB.\n\t *\n\t * @param {number} h - Hue value between `0.0` and `1.0`.\n\t * @param {number} s - Saturation value between `0.0` and `1.0`.\n\t * @param {number} l - Lightness value between `0.0` and `1.0`.\n\t * @return {Color} A reference to this color.\n\t */\n\toffsetHSL( h, s, l ) {\n\n\t\tthis.getHSL( _hslA );\n\n\t\treturn this.setHSL( _hslA.h + h, _hslA.s + s, _hslA.l + l );\n\n\t}\n\n\t/**\n\t * Adds the RGB values of the given color to the RGB values of this color.\n\t *\n\t * @param {Color} color - The color to add.\n\t * @return {Color} A reference to this color.\n\t */\n\tadd( color ) {\n\n\t\tthis.r += color.r;\n\t\tthis.g += color.g;\n\t\tthis.b += color.b;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the RGB values of the given colors and stores the result in this instance.\n\t *\n\t * @param {Color} color1 - The first color.\n\t * @param {Color} color2 - The second color.\n\t * @return {Color} A reference to this color.\n\t */\n\taddColors( color1, color2 ) {\n\n\t\tthis.r = color1.r + color2.r;\n\t\tthis.g = color1.g + color2.g;\n\t\tthis.b = color1.b + color2.b;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given scalar value to the RGB values of this color.\n\t *\n\t * @param {number} s - The scalar to add.\n\t * @return {Color} A reference to this color.\n\t */\n\taddScalar( s ) {\n\n\t\tthis.r += s;\n\t\tthis.g += s;\n\t\tthis.b += s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Subtracts the RGB values of the given color from the RGB values of this color.\n\t *\n\t * @param {Color} color - The color to subtract.\n\t * @return {Color} A reference to this color.\n\t */\n\tsub( color ) {\n\n\t\tthis.r = Math.max( 0, this.r - color.r );\n\t\tthis.g = Math.max( 0, this.g - color.g );\n\t\tthis.b = Math.max( 0, this.b - color.b );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the RGB values of the given color with the RGB values of this color.\n\t *\n\t * @param {Color} color - The color to multiply.\n\t * @return {Color} A reference to this color.\n\t */\n\tmultiply( color ) {\n\n\t\tthis.r *= color.r;\n\t\tthis.g *= color.g;\n\t\tthis.b *= color.b;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Multiplies the given scalar value with the RGB values of this color.\n\t *\n\t * @param {number} s - The scalar to multiply.\n\t * @return {Color} A reference to this color.\n\t */\n\tmultiplyScalar( s ) {\n\n\t\tthis.r *= s;\n\t\tthis.g *= s;\n\t\tthis.b *= s;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Linearly interpolates this color's RGB values toward the RGB values of the\n\t * given color. The alpha argument can be thought of as the ratio between\n\t * the two colors, where `0.0` is this color and `1.0` is the first argument.\n\t *\n\t * @param {Color} color - The color to converge on.\n\t * @param {number} alpha - The interpolation factor in the closed interval `[0,1]`.\n\t * @return {Color} A reference to this color.\n\t */\n\tlerp( color, alpha ) {\n\n\t\tthis.r += ( color.r - this.r ) * alpha;\n\t\tthis.g += ( color.g - this.g ) * alpha;\n\t\tthis.b += ( color.b - this.b ) * alpha;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Linearly interpolates between the given colors and stores the result in this instance.\n\t * The alpha argument can be thought of as the ratio between the two colors, where `0.0`\n\t * is the first and `1.0` is the second color.\n\t *\n\t * @param {Color} color1 - The first color.\n\t * @param {Color} color2 - The second color.\n\t * @param {number} alpha - The interpolation factor in the closed interval `[0,1]`.\n\t * @return {Color} A reference to this color.\n\t */\n\tlerpColors( color1, color2, alpha ) {\n\n\t\tthis.r = color1.r + ( color2.r - color1.r ) * alpha;\n\t\tthis.g = color1.g + ( color2.g - color1.g ) * alpha;\n\t\tthis.b = color1.b + ( color2.b - color1.b ) * alpha;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Linearly interpolates this color's HSL values toward the HSL values of the\n\t * given color. It differs from {@link Color#lerp} by not interpolating straight\n\t * from one color to the other, but instead going through all the hues in between\n\t * those two colors. The alpha argument can be thought of as the ratio between\n\t * the two colors, where 0.0 is this color and 1.0 is the first argument.\n\t *\n\t * @param {Color} color - The color to converge on.\n\t * @param {number} alpha - The interpolation factor in the closed interval `[0,1]`.\n\t * @return {Color} A reference to this color.\n\t */\n\tlerpHSL( color, alpha ) {\n\n\t\tthis.getHSL( _hslA );\n\t\tcolor.getHSL( _hslB );\n\n\t\tconst h = lerp( _hslA.h, _hslB.h, alpha );\n\t\tconst s = lerp( _hslA.s, _hslB.s, alpha );\n\t\tconst l = lerp( _hslA.l, _hslB.l, alpha );\n\n\t\tthis.setHSL( h, s, l );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the color's RGB components from the given 3D vector.\n\t *\n\t * @param {Vector3} v - The vector to set.\n\t * @return {Color} A reference to this color.\n\t */\n\tsetFromVector3( v ) {\n\n\t\tthis.r = v.x;\n\t\tthis.g = v.y;\n\t\tthis.b = v.z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Transforms this color with the given 3x3 matrix.\n\t *\n\t * @param {Matrix3} m - The matrix.\n\t * @return {Color} A reference to this color.\n\t */\n\tapplyMatrix3( m ) {\n\n\t\tconst r = this.r, g = this.g, b = this.b;\n\t\tconst e = m.elements;\n\n\t\tthis.r = e[ 0 ] * r + e[ 3 ] * g + e[ 6 ] * b;\n\t\tthis.g = e[ 1 ] * r + e[ 4 ] * g + e[ 7 ] * b;\n\t\tthis.b = e[ 2 ] * r + e[ 5 ] * g + e[ 8 ] * b;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this color is equal with the given one.\n\t *\n\t * @param {Color} c - The color to test for equality.\n\t * @return {boolean} Whether this bounding color is equal with the given one.\n\t */\n\tequals( c ) {\n\n\t\treturn ( c.r === this.r ) && ( c.g === this.g ) && ( c.b === this.b );\n\n\t}\n\n\t/**\n\t * Sets this color's RGB components from the given array.\n\t *\n\t * @param {Array<number>} array - An array holding the RGB values.\n\t * @param {number} [offset=0] - The offset into the array.\n\t * @return {Color} A reference to this color.\n\t */\n\tfromArray( array, offset = 0 ) {\n\n\t\tthis.r = array[ offset ];\n\t\tthis.g = array[ offset + 1 ];\n\t\tthis.b = array[ offset + 2 ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Writes the RGB components of this color to the given array. If no array is provided,\n\t * the method returns a new instance.\n\t *\n\t * @param {Array<number>} [array=[]] - The target array holding the color components.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Array<number>} The color components.\n\t */\n\ttoArray( array = [], offset = 0 ) {\n\n\t\tarray[ offset ] = this.r;\n\t\tarray[ offset + 1 ] = this.g;\n\t\tarray[ offset + 2 ] = this.b;\n\n\t\treturn array;\n\n\t}\n\n\t/**\n\t * Sets the components of this color from the given buffer attribute.\n\t *\n\t * @param {BufferAttribute} attribute - The buffer attribute holding color data.\n\t * @param {number} index - The index into the attribute.\n\t * @return {Color} A reference to this color.\n\t */\n\tfromBufferAttribute( attribute, index ) {\n\n\t\tthis.r = attribute.getX( index );\n\t\tthis.g = attribute.getY( index );\n\t\tthis.b = attribute.getZ( index );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * This methods defines the serialization result of this class. Returns the color\n\t * as a hexadecimal value.\n\t *\n\t * @return {number} The hexadecimal value.\n\t */\n\ttoJSON() {\n\n\t\treturn this.getHex();\n\n\t}\n\n\t*[ Symbol.iterator ]() {\n\n\t\tyield this.r;\n\t\tyield this.g;\n\t\tyield this.b;\n\n\t}\n\n}\n\nconst _color = /*@__PURE__*/ new Color();\n\n/**\n * A dictionary with X11 color names.\n *\n * Note that multiple words such as Dark Orange become the string 'darkorange'.\n *\n * @static\n * @type {Object}\n */\nColor.NAMES = _colorKeywords;\n\nlet _materialId = 0;\n\n/**\n * Abstract base class for materials.\n *\n * Materials define the appearance of renderable 3D objects.\n *\n * @abstract\n * @augments EventDispatcher\n */\nclass Material extends EventDispatcher {\n\n\t/**\n\t * Constructs a new material.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMaterial = true;\n\n\t\t/**\n\t\t * The ID of the material.\n\t\t *\n\t\t * @name Material#id\n\t\t * @type {number}\n\t\t * @readonly\n\t\t */\n\t\tObject.defineProperty( this, 'id', { value: _materialId ++ } );\n\n\t\t/**\n\t\t * The UUID of the material.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.uuid = generateUUID();\n\n\t\t/**\n\t\t * The name of the material.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\n\t\t/**\n\t\t * The type property is used for detecting the object type\n\t\t * in context of serialization/deserialization.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.type = 'Material';\n\n\t\t/**\n\t\t * Defines the blending type of the material.\n\t\t *\n\t\t * It must be set to `CustomBlending` if custom blending properties like\n\t\t * {@link Material#blendSrc}, {@link Material#blendDst} or {@link Material#blendEquation}\n\t\t * should have any effect.\n\t\t *\n\t\t * @type {(NoBlending|NormalBlending|AdditiveBlending|SubtractiveBlending|MultiplyBlending|CustomBlending)}\n\t\t * @default NormalBlending\n\t\t */\n\t\tthis.blending = NormalBlending;\n\n\t\t/**\n\t\t * Defines which side of faces will be rendered - front, back or both.\n\t\t *\n\t\t * @type {(FrontSide|BackSide|DoubleSide)}\n\t\t * @default FrontSide\n\t\t */\n\t\tthis.side = FrontSide;\n\n\t\t/**\n\t\t * If set to `true`, vertex colors should be used.\n\t\t *\n\t\t * The engine supports RGB and RGBA vertex colors depending on whether a three (RGB) or\n\t\t * four (RGBA) component color buffer attribute is used.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.vertexColors = false;\n\n\t\t/**\n\t\t * Defines how transparent the material is.\n\t\t * A value of `0.0` indicates fully transparent, `1.0` is fully opaque.\n\t\t *\n\t\t * If the {@link Material#transparent} is not set to `true`,\n\t\t * the material will remain fully opaque and this value will only affect its color.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.opacity = 1;\n\n\t\t/**\n\t\t * Defines whether this material is transparent. This has an effect on\n\t\t * rendering as transparent objects need special treatment and are rendered\n\t\t * after non-transparent objects.\n\t\t *\n\t\t * When set to true, the extent to which the material is transparent is\n\t\t * controlled by {@link Material#opacity}.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.transparent = false;\n\n\t\t/**\n\t\t * Enables alpha hashed transparency, an alternative to {@link Material#transparent} or\n\t\t * {@link Material#alphaTest}. The material will not be rendered if opacity is lower than\n\t\t * a random threshold. Randomization introduces some grain or noise, but approximates alpha\n\t\t * blending without the associated problems of sorting. Using TAA can reduce the resulting noise.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.alphaHash = false;\n\n\t\t/**\n\t\t * Defines the blending source factor.\n\t\t *\n\t\t * @type {(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}\n\t\t * @default SrcAlphaFactor\n\t\t */\n\t\tthis.blendSrc = SrcAlphaFactor;\n\n\t\t/**\n\t\t * Defines the blending destination factor.\n\t\t *\n\t\t * @type {(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}\n\t\t * @default OneMinusSrcAlphaFactor\n\t\t */\n\t\tthis.blendDst = OneMinusSrcAlphaFactor;\n\n\t\t/**\n\t\t * Defines the blending equation.\n\t\t *\n\t\t * @type {(AddEquation|SubtractEquation|ReverseSubtractEquation|MinEquation|MaxEquation)}\n\t\t * @default AddEquation\n\t\t */\n\t\tthis.blendEquation = AddEquation;\n\n\t\t/**\n\t\t * Defines the blending source alpha factor.\n\t\t *\n\t\t * @type {?(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}\n\t\t * @default null\n\t\t */\n\t\tthis.blendSrcAlpha = null;\n\n\t\t/**\n\t\t * Defines the blending destination alpha factor.\n\t\t *\n\t\t * @type {?(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}\n\t\t * @default null\n\t\t */\n\t\tthis.blendDstAlpha = null;\n\n\t\t/**\n\t\t * Defines the blending equation of the alpha channel.\n\t\t *\n\t\t * @type {?(AddEquation|SubtractEquation|ReverseSubtractEquation|MinEquation|MaxEquation)}\n\t\t * @default null\n\t\t */\n\t\tthis.blendEquationAlpha = null;\n\n\t\t/**\n\t\t * Represents the RGB values of the constant blend color.\n\t\t *\n\t\t * This property has only an effect when using custom blending with `ConstantColor` or `OneMinusConstantColor`.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.blendColor = new Color( 0, 0, 0 );\n\n\t\t/**\n\t\t * Represents the alpha value of the constant blend color.\n\t\t *\n\t\t * This property has only an effect when using custom blending with `ConstantAlpha` or `OneMinusConstantAlpha`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.blendAlpha = 0;\n\n\t\t/**\n\t\t * Defines the depth function.\n\t\t *\n\t\t * @type {(NeverDepth|AlwaysDepth|LessDepth|LessEqualDepth|EqualDepth|GreaterEqualDepth|GreaterDepth|NotEqualDepth)}\n\t\t * @default LessEqualDepth\n\t\t */\n\t\tthis.depthFunc = LessEqualDepth;\n\n\t\t/**\n\t\t * Whether to have depth test enabled when rendering this material.\n\t\t * When the depth test is disabled, the depth write will also be implicitly disabled.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.depthTest = true;\n\n\t\t/**\n\t\t * Whether rendering this material has any effect on the depth buffer.\n\t\t *\n\t\t * When drawing 2D overlays it can be useful to disable the depth writing in\n\t\t * order to layer several things together without creating z-index artifacts.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.depthWrite = true;\n\n\t\t/**\n\t\t * The bit mask to use when writing to the stencil buffer.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0xff\n\t\t */\n\t\tthis.stencilWriteMask = 0xff;\n\n\t\t/**\n\t\t * The stencil comparison function to use.\n\t\t *\n\t\t * @type {NeverStencilFunc|LessStencilFunc|EqualStencilFunc|LessEqualStencilFunc|GreaterStencilFunc|NotEqualStencilFunc|GreaterEqualStencilFunc|AlwaysStencilFunc}\n\t\t * @default AlwaysStencilFunc\n\t\t */\n\t\tthis.stencilFunc = AlwaysStencilFunc;\n\n\t\t/**\n\t\t * The value to use when performing stencil comparisons or stencil operations.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.stencilRef = 0;\n\n\t\t/**\n\t\t * The bit mask to use when comparing against the stencil buffer.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0xff\n\t\t */\n\t\tthis.stencilFuncMask = 0xff;\n\n\t\t/**\n\t\t * Which stencil operation to perform when the comparison function returns `false`.\n\t\t *\n\t\t * @type {ZeroStencilOp|KeepStencilOp|ReplaceStencilOp|IncrementStencilOp|DecrementStencilOp|IncrementWrapStencilOp|DecrementWrapStencilOp|InvertStencilOp}\n\t\t * @default KeepStencilOp\n\t\t */\n\t\tthis.stencilFail = KeepStencilOp;\n\n\t\t/**\n\t\t * Which stencil operation to perform when the comparison function returns\n\t\t * `true` but the depth test fails.\n\t\t *\n\t\t * @type {ZeroStencilOp|KeepStencilOp|ReplaceStencilOp|IncrementStencilOp|DecrementStencilOp|IncrementWrapStencilOp|DecrementWrapStencilOp|InvertStencilOp}\n\t\t * @default KeepStencilOp\n\t\t */\n\t\tthis.stencilZFail = KeepStencilOp;\n\n\t\t/**\n\t\t * Which stencil operation to perform when the comparison function returns\n\t\t * `true` and the depth test passes.\n\t\t *\n\t\t * @type {ZeroStencilOp|KeepStencilOp|ReplaceStencilOp|IncrementStencilOp|DecrementStencilOp|IncrementWrapStencilOp|DecrementWrapStencilOp|InvertStencilOp}\n\t\t * @default KeepStencilOp\n\t\t */\n\t\tthis.stencilZPass = KeepStencilOp;\n\n\t\t/**\n\t\t * Whether stencil operations are performed against the stencil buffer. In\n\t\t * order to perform writes or comparisons against the stencil buffer this\n\t\t * value must be `true`.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.stencilWrite = false;\n\n\t\t/**\n\t\t * User-defined clipping planes specified as THREE.Plane objects in world\n\t\t * space. These planes apply to the objects this material is attached to.\n\t\t * Points in space whose signed distance to the plane is negative are clipped\n\t\t * (not rendered). This requires {@link WebGLRenderer#localClippingEnabled} to\n\t\t * be `true`.\n\t\t *\n\t\t * @type {?Array<Plane>}\n\t\t * @default null\n\t\t */\n\t\tthis.clippingPlanes = null;\n\n\t\t/**\n\t\t * Changes the behavior of clipping planes so that only their intersection is\n\t\t * clipped, rather than their union.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.clipIntersection = false;\n\n\t\t/**\n\t\t * Defines whether to clip shadows according to the clipping planes specified\n\t\t * on this material.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.clipShadows = false;\n\n\t\t/**\n\t\t * Defines which side of faces cast shadows. If `null`, the side casting shadows\n\t\t * is determined as follows:\n\t\t *\n\t\t * - When {@link Material#side} is set to `FrontSide`, the back side cast shadows.\n\t\t * - When {@link Material#side} is set to `BackSide`, the front side cast shadows.\n\t\t * - When {@link Material#side} is set to `DoubleSide`, both sides cast shadows.\n\t\t *\n\t\t * @type {?(FrontSide|BackSide|DoubleSide)}\n\t\t * @default null\n\t\t */\n\t\tthis.shadowSide = null;\n\n\t\t/**\n\t\t * Whether to render the material's color.\n\t\t *\n\t\t * This can be used in conjunction with {@link Object3D#renderOder} to create invisible\n\t\t * objects that occlude other objects.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.colorWrite = true;\n\n\t\t/**\n\t\t * Override the renderer's default precision for this material.\n\t\t *\n\t\t * @type {?('highp'|'mediump'|'lowp')}\n\t\t * @default null\n\t\t */\n\t\tthis.precision = null;\n\n\t\t/**\n\t\t * Whether to use polygon offset or not. When enabled, each fragment's depth value will\n\t\t * be offset after it is interpolated from the depth values of the appropriate vertices.\n\t\t * The offset is added before the depth test is performed and before the value is written\n\t\t * into the depth buffer.\n\t\t *\n\t\t * Can be useful for rendering hidden-line images, for applying decals to surfaces, and for\n\t\t * rendering solids with highlighted edges.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.polygonOffset = false;\n\n\t\t/**\n\t\t * Specifies a scale factor that is used to create a variable depth offset for each polygon.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.polygonOffsetFactor = 0;\n\n\t\t/**\n\t\t * Is multiplied by an implementation-specific value to create a constant depth offset.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.polygonOffsetUnits = 0;\n\n\t\t/**\n\t\t * Whether to apply dithering to the color to remove the appearance of banding.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.dithering = false;\n\n\t\t/**\n\t\t * Whether alpha to coverage should be enabled or not. Can only be used with MSAA-enabled contexts\n\t\t * (meaning when the renderer was created with *antialias* parameter set to `true`). Enabling this\n\t\t * will smooth aliasing on clip plane edges and alphaTest-clipped edges.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.alphaToCoverage = false;\n\n\t\t/**\n\t\t * Whether to premultiply the alpha (transparency) value.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.premultipliedAlpha = false;\n\n\t\t/**\n\t\t * Whether double-sided, transparent objects should be rendered with a single pass or not.\n\t\t *\n\t\t * The engine renders double-sided, transparent objects with two draw calls (back faces first,\n\t\t * then front faces) to mitigate transparency artifacts. There are scenarios however where this\n\t\t * approach produces no quality gains but still doubles draw calls e.g. when rendering flat\n\t\t * vegetation like grass sprites. In these cases, set the `forceSinglePass` flag to `true` to\n\t\t * disable the two pass rendering to avoid performance issues.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.forceSinglePass = false;\n\n\t\t/**\n\t\t * Whether it's possible to override the material with {@link Scene#overrideMaterial} or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.allowOverride = true;\n\n\t\t/**\n\t\t * Defines whether 3D objects using this material are visible.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.visible = true;\n\n\t\t/**\n\t\t * Defines whether this material is tone mapped according to the renderer's tone mapping setting.\n\t\t *\n\t\t * It is ignored when rendering to a render target or using post processing or when using\n\t\t * `WebGPURenderer`. In all these cases, all materials are honored by tone mapping.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.toneMapped = true;\n\n\t\t/**\n\t\t * An object that can be used to store custom data about the Material. It\n\t\t * should not hold references to functions as these will not be cloned.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.userData = {};\n\n\t\t/**\n\t\t * This starts at `0` and counts how many times {@link Material#needsUpdate} is set to `true`.\n\t\t *\n\t\t * @type {number}\n\t\t * @readonly\n\t\t * @default 0\n\t\t */\n\t\tthis.version = 0;\n\n\t\tthis._alphaTest = 0;\n\n\t}\n\n\t/**\n\t * Sets the alpha value to be used when running an alpha test. The material\n\t * will not be rendered if the opacity is lower than this value.\n\t *\n\t * @type {number}\n\t * @readonly\n\t * @default 0\n\t */\n\tget alphaTest() {\n\n\t\treturn this._alphaTest;\n\n\t}\n\n\tset alphaTest( value ) {\n\n\t\tif ( this._alphaTest > 0 !== value > 0 ) {\n\n\t\t\tthis.version ++;\n\n\t\t}\n\n\t\tthis._alphaTest = value;\n\n\t}\n\n\t/**\n\t * An optional callback that is executed immediately before the material is used to render a 3D object.\n\t *\n\t * This method can only be used when rendering with {@link WebGLRenderer}.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {Scene} scene - The scene.\n\t * @param {Camera} camera - The camera that is used to render the scene.\n\t * @param {BufferGeometry} geometry - The 3D object's geometry.\n\t * @param {Object3D} object - The 3D object.\n\t * @param {Object} group - The geometry group data.\n\t */\n\tonBeforeRender( /* renderer, scene, camera, geometry, object, group */ ) {}\n\n\t/**\n\t * An optional callback that is executed immediately before the shader\n\t * program is compiled. This function is called with the shader source code\n\t * as a parameter. Useful for the modification of built-in materials.\n\t *\n\t * This method can only be used when rendering with {@link WebGLRenderer}. The\n\t * recommended approach when customizing materials is to use `WebGPURenderer` with the new\n\t * Node Material system and [TSL](https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language).\n\t *\n\t * @param {{vertexShader:string,fragmentShader:string,uniforms:Object}} shaderobject - The object holds the uniforms and the vertex and fragment shader source.\n\t * @param {WebGLRenderer} renderer - A reference to the renderer.\n\t */\n\tonBeforeCompile( /* shaderobject, renderer */ ) {}\n\n\t/**\n\t * In case {@link Material#onBeforeCompile} is used, this callback can be used to identify\n\t * values of settings used in `onBeforeCompile()`, so three.js can reuse a cached\n\t * shader or recompile the shader for this material as needed.\n\t *\n\t * This method can only be used when rendering with {@link WebGLRenderer}.\n\t *\n\t * @return {string} The custom program cache key.\n\t */\n\tcustomProgramCacheKey() {\n\n\t\treturn this.onBeforeCompile.toString();\n\n\t}\n\n\t/**\n\t * This method can be used to set default values from parameter objects.\n\t * It is a generic implementation so it can be used with different types\n\t * of materials.\n\t *\n\t * @param {Object} [values] - The material values to set.\n\t */\n\tsetValues( values ) {\n\n\t\tif ( values === undefined ) return;\n\n\t\tfor ( const key in values ) {\n\n\t\t\tconst newValue = values[ key ];\n\n\t\t\tif ( newValue === undefined ) {\n\n\t\t\t\twarn( `Material: parameter '${ key }' has value of undefined.` );\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\tconst currentValue = this[ key ];\n\n\t\t\tif ( currentValue === undefined ) {\n\n\t\t\t\twarn( `Material: '${ key }' is not a property of THREE.${ this.type }.` );\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\tif ( currentValue && currentValue.isColor ) {\n\n\t\t\t\tcurrentValue.set( newValue );\n\n\t\t\t} else if ( ( currentValue && currentValue.isVector3 ) && ( newValue && newValue.isVector3 ) ) {\n\n\t\t\t\tcurrentValue.copy( newValue );\n\n\t\t\t} else {\n\n\t\t\t\tthis[ key ] = newValue;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Serializes the material into JSON.\n\t *\n\t * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.\n\t * @return {Object} A JSON object representing the serialized material.\n\t * @see {@link ObjectLoader#parse}\n\t */\n\ttoJSON( meta ) {\n\n\t\tconst isRootObject = ( meta === undefined || typeof meta === 'string' );\n\n\t\tif ( isRootObject ) {\n\n\t\t\tmeta = {\n\t\t\t\ttextures: {},\n\t\t\t\timages: {}\n\t\t\t};\n\n\t\t}\n\n\t\tconst data = {\n\t\t\tmetadata: {\n\t\t\t\tversion: 4.7,\n\t\t\t\ttype: 'Material',\n\t\t\t\tgenerator: 'Material.toJSON'\n\t\t\t}\n\t\t};\n\n\t\t// standard Material serialization\n\t\tdata.uuid = this.uuid;\n\t\tdata.type = this.type;\n\n\t\tif ( this.name !== '' ) data.name = this.name;\n\n\t\tif ( this.color && this.color.isColor ) data.color = this.color.getHex();\n\n\t\tif ( this.roughness !== undefined ) data.roughness = this.roughness;\n\t\tif ( this.metalness !== undefined ) data.metalness = this.metalness;\n\n\t\tif ( this.sheen !== undefined ) data.sheen = this.sheen;\n\t\tif ( this.sheenColor && this.sheenColor.isColor ) data.sheenColor = this.sheenColor.getHex();\n\t\tif ( this.sheenRoughness !== undefined ) data.sheenRoughness = this.sheenRoughness;\n\t\tif ( this.emissive && this.emissive.isColor ) data.emissive = this.emissive.getHex();\n\t\tif ( this.emissiveIntensity !== undefined && this.emissiveIntensity !== 1 ) data.emissiveIntensity = this.emissiveIntensity;\n\n\t\tif ( this.specular && this.specular.isColor ) data.specular = this.specular.getHex();\n\t\tif ( this.specularIntensity !== undefined ) data.specularIntensity = this.specularIntensity;\n\t\tif ( this.specularColor && this.specularColor.isColor ) data.specularColor = this.specularColor.getHex();\n\t\tif ( this.shininess !== undefined ) data.shininess = this.shininess;\n\t\tif ( this.clearcoat !== undefined ) data.clearcoat = this.clearcoat;\n\t\tif ( this.clearcoatRoughness !== undefined ) data.clearcoatRoughness = this.clearcoatRoughness;\n\n\t\tif ( this.clearcoatMap && this.clearcoatMap.isTexture ) {\n\n\t\t\tdata.clearcoatMap = this.clearcoatMap.toJSON( meta ).uuid;\n\n\t\t}\n\n\t\tif ( this.clearcoatRoughnessMap && this.clearcoatRoughnessMap.isTexture ) {\n\n\t\t\tdata.clearcoatRoughnessMap = this.clearcoatRoughnessMap.toJSON( meta ).uuid;\n\n\t\t}\n\n\t\tif ( this.clearcoatNormalMap && this.clearcoatNormalMap.isTexture ) {\n\n\t\t\tdata.clearcoatNormalMap = this.clearcoatNormalMap.toJSON( meta ).uuid;\n\t\t\tdata.clearcoatNormalScale = this.clearcoatNormalScale.toArray();\n\n\t\t}\n\n\t\tif ( this.sheenColorMap && this.sheenColorMap.isTexture ) {\n\n\t\t\tdata.sheenColorMap = this.sheenColorMap.toJSON( meta ).uuid;\n\n\t\t}\n\n\t\tif ( this.sheenRoughnessMap && this.sheenRoughnessMap.isTexture ) {\n\n\t\t\tdata.sheenRoughnessMap = this.sheenRoughnessMap.toJSON( meta ).uuid;\n\n\t\t}\n\n\t\tif ( this.dispersion !== undefined ) data.dispersion = this.dispersion;\n\n\t\tif ( this.iridescence !== undefined ) data.iridescence = this.iridescence;\n\t\tif ( this.iridescenceIOR !== undefined ) data.iridescenceIOR = this.iridescenceIOR;\n\t\tif ( this.iridescenceThicknessRange !== undefined ) data.iridescenceThicknessRange = this.iridescenceThicknessRange;\n\n\t\tif ( this.iridescenceMap && this.iridescenceMap.isTexture ) {\n\n\t\t\tdata.iridescenceMap = this.iridescenceMap.toJSON( meta ).uuid;\n\n\t\t}\n\n\t\tif ( this.iridescenceThicknessMap && this.iridescenceThicknessMap.isTexture ) {\n\n\t\t\tdata.iridescenceThicknessMap = this.iridescenceThicknessMap.toJSON( meta ).uuid;\n\n\t\t}\n\n\t\tif ( this.anisotropy !== undefined ) data.anisotropy = this.anisotropy;\n\t\tif ( this.anisotropyRotation !== undefined ) data.anisotropyRotation = this.anisotropyRotation;\n\n\t\tif ( this.anisotropyMap && this.anisotropyMap.isTexture ) {\n\n\t\t\tdata.anisotropyMap = this.anisotropyMap.toJSON( meta ).uuid;\n\n\t\t}\n\n\t\tif ( this.map && this.map.isTexture ) data.map = this.map.toJSON( meta ).uuid;\n\t\tif ( this.matcap && this.matcap.isTexture ) data.matcap = this.matcap.toJSON( meta ).uuid;\n\t\tif ( this.alphaMap && this.alphaMap.isTexture ) data.alphaMap = this.alphaMap.toJSON( meta ).uuid;\n\n\t\tif ( this.lightMap && this.lightMap.isTexture ) {\n\n\t\t\tdata.lightMap = this.lightMap.toJSON( meta ).uuid;\n\t\t\tdata.lightMapIntensity = this.lightMapIntensity;\n\n\t\t}\n\n\t\tif ( this.aoMap && this.aoMap.isTexture ) {\n\n\t\t\tdata.aoMap = this.aoMap.toJSON( meta ).uuid;\n\t\t\tdata.aoMapIntensity = this.aoMapIntensity;\n\n\t\t}\n\n\t\tif ( this.bumpMap && this.bumpMap.isTexture ) {\n\n\t\t\tdata.bumpMap = this.bumpMap.toJSON( meta ).uuid;\n\t\t\tdata.bumpScale = this.bumpScale;\n\n\t\t}\n\n\t\tif ( this.normalMap && this.normalMap.isTexture ) {\n\n\t\t\tdata.normalMap = this.normalMap.toJSON( meta ).uuid;\n\t\t\tdata.normalMapType = this.normalMapType;\n\t\t\tdata.normalScale = this.normalScale.toArray();\n\n\t\t}\n\n\t\tif ( this.displacementMap && this.displacementMap.isTexture ) {\n\n\t\t\tdata.displacementMap = this.displacementMap.toJSON( meta ).uuid;\n\t\t\tdata.displacementScale = this.displacementScale;\n\t\t\tdata.displacementBias = this.displacementBias;\n\n\t\t}\n\n\t\tif ( this.roughnessMap && this.roughnessMap.isTexture ) data.roughnessMap = this.roughnessMap.toJSON( meta ).uuid;\n\t\tif ( this.metalnessMap && this.metalnessMap.isTexture ) data.metalnessMap = this.metalnessMap.toJSON( meta ).uuid;\n\n\t\tif ( this.emissiveMap && this.emissiveMap.isTexture ) data.emissiveMap = this.emissiveMap.toJSON( meta ).uuid;\n\t\tif ( this.specularMap && this.specularMap.isTexture ) data.specularMap = this.specularMap.toJSON( meta ).uuid;\n\t\tif ( this.specularIntensityMap && this.specularIntensityMap.isTexture ) data.specularIntensityMap = this.specularIntensityMap.toJSON( meta ).uuid;\n\t\tif ( this.specularColorMap && this.specularColorMap.isTexture ) data.specularColorMap = this.specularColorMap.toJSON( meta ).uuid;\n\n\t\tif ( this.envMap && this.envMap.isTexture ) {\n\n\t\t\tdata.envMap = this.envMap.toJSON( meta ).uuid;\n\n\t\t\tif ( this.combine !== undefined ) data.combine = this.combine;\n\n\t\t}\n\n\t\tif ( this.envMapRotation !== undefined ) data.envMapRotation = this.envMapRotation.toArray();\n\t\tif ( this.envMapIntensity !== undefined ) data.envMapIntensity = this.envMapIntensity;\n\t\tif ( this.reflectivity !== undefined ) data.reflectivity = this.reflectivity;\n\t\tif ( this.refractionRatio !== undefined ) data.refractionRatio = this.refractionRatio;\n\n\t\tif ( this.gradientMap && this.gradientMap.isTexture ) {\n\n\t\t\tdata.gradientMap = this.gradientMap.toJSON( meta ).uuid;\n\n\t\t}\n\n\t\tif ( this.transmission !== undefined ) data.transmission = this.transmission;\n\t\tif ( this.transmissionMap && this.transmissionMap.isTexture ) data.transmissionMap = this.transmissionMap.toJSON( meta ).uuid;\n\t\tif ( this.thickness !== undefined ) data.thickness = this.thickness;\n\t\tif ( this.thicknessMap && this.thicknessMap.isTexture ) data.thicknessMap = this.thicknessMap.toJSON( meta ).uuid;\n\t\tif ( this.attenuationDistance !== undefined && this.attenuationDistance !== Infinity ) data.attenuationDistance = this.attenuationDistance;\n\t\tif ( this.attenuationColor !== undefined ) data.attenuationColor = this.attenuationColor.getHex();\n\n\t\tif ( this.size !== undefined ) data.size = this.size;\n\t\tif ( this.shadowSide !== null ) data.shadowSide = this.shadowSide;\n\t\tif ( this.sizeAttenuation !== undefined ) data.sizeAttenuation = this.sizeAttenuation;\n\n\t\tif ( this.blending !== NormalBlending ) data.blending = this.blending;\n\t\tif ( this.side !== FrontSide ) data.side = this.side;\n\t\tif ( this.vertexColors === true ) data.vertexColors = true;\n\n\t\tif ( this.opacity < 1 ) data.opacity = this.opacity;\n\t\tif ( this.transparent === true ) data.transparent = true;\n\n\t\tif ( this.blendSrc !== SrcAlphaFactor ) data.blendSrc = this.blendSrc;\n\t\tif ( this.blendDst !== OneMinusSrcAlphaFactor ) data.blendDst = this.blendDst;\n\t\tif ( this.blendEquation !== AddEquation ) data.blendEquation = this.blendEquation;\n\t\tif ( this.blendSrcAlpha !== null ) data.blendSrcAlpha = this.blendSrcAlpha;\n\t\tif ( this.blendDstAlpha !== null ) data.blendDstAlpha = this.blendDstAlpha;\n\t\tif ( this.blendEquationAlpha !== null ) data.blendEquationAlpha = this.blendEquationAlpha;\n\t\tif ( this.blendColor && this.blendColor.isColor ) data.blendColor = this.blendColor.getHex();\n\t\tif ( this.blendAlpha !== 0 ) data.blendAlpha = this.blendAlpha;\n\n\t\tif ( this.depthFunc !== LessEqualDepth ) data.depthFunc = this.depthFunc;\n\t\tif ( this.depthTest === false ) data.depthTest = this.depthTest;\n\t\tif ( this.depthWrite === false ) data.depthWrite = this.depthWrite;\n\t\tif ( this.colorWrite === false ) data.colorWrite = this.colorWrite;\n\n\t\tif ( this.stencilWriteMask !== 0xff ) data.stencilWriteMask = this.stencilWriteMask;\n\t\tif ( this.stencilFunc !== AlwaysStencilFunc ) data.stencilFunc = this.stencilFunc;\n\t\tif ( this.stencilRef !== 0 ) data.stencilRef = this.stencilRef;\n\t\tif ( this.stencilFuncMask !== 0xff ) data.stencilFuncMask = this.stencilFuncMask;\n\t\tif ( this.stencilFail !== KeepStencilOp ) data.stencilFail = this.stencilFail;\n\t\tif ( this.stencilZFail !== KeepStencilOp ) data.stencilZFail = this.stencilZFail;\n\t\tif ( this.stencilZPass !== KeepStencilOp ) data.stencilZPass = this.stencilZPass;\n\t\tif ( this.stencilWrite === true ) data.stencilWrite = this.stencilWrite;\n\n\t\t// rotation (SpriteMaterial)\n\t\tif ( this.rotation !== undefined && this.rotation !== 0 ) data.rotation = this.rotation;\n\n\t\tif ( this.polygonOffset === true ) data.polygonOffset = true;\n\t\tif ( this.polygonOffsetFactor !== 0 ) data.polygonOffsetFactor = this.polygonOffsetFactor;\n\t\tif ( this.polygonOffsetUnits !== 0 ) data.polygonOffsetUnits = this.polygonOffsetUnits;\n\n\t\tif ( this.linewidth !== undefined && this.linewidth !== 1 ) data.linewidth = this.linewidth;\n\t\tif ( this.dashSize !== undefined ) data.dashSize = this.dashSize;\n\t\tif ( this.gapSize !== undefined ) data.gapSize = this.gapSize;\n\t\tif ( this.scale !== undefined ) data.scale = this.scale;\n\n\t\tif ( this.dithering === true ) data.dithering = true;\n\n\t\tif ( this.alphaTest > 0 ) data.alphaTest = this.alphaTest;\n\t\tif ( this.alphaHash === true ) data.alphaHash = true;\n\t\tif ( this.alphaToCoverage === true ) data.alphaToCoverage = true;\n\t\tif ( this.premultipliedAlpha === true ) data.premultipliedAlpha = true;\n\t\tif ( this.forceSinglePass === true ) data.forceSinglePass = true;\n\t\tif ( this.allowOverride === false ) data.allowOverride = false;\n\n\t\tif ( this.wireframe === true ) data.wireframe = true;\n\t\tif ( this.wireframeLinewidth > 1 ) data.wireframeLinewidth = this.wireframeLinewidth;\n\t\tif ( this.wireframeLinecap !== 'round' ) data.wireframeLinecap = this.wireframeLinecap;\n\t\tif ( this.wireframeLinejoin !== 'round' ) data.wireframeLinejoin = this.wireframeLinejoin;\n\n\t\tif ( this.flatShading === true ) data.flatShading = true;\n\n\t\tif ( this.visible === false ) data.visible = false;\n\n\t\tif ( this.toneMapped === false ) data.toneMapped = false;\n\n\t\tif ( this.fog === false ) data.fog = false;\n\n\t\tif ( Object.keys( this.userData ).length > 0 ) data.userData = this.userData;\n\n\t\t// TODO: Copied from Object3D.toJSON\n\n\t\tfunction extractFromCache( cache ) {\n\n\t\t\tconst values = [];\n\n\t\t\tfor ( const key in cache ) {\n\n\t\t\t\tconst data = cache[ key ];\n\t\t\t\tdelete data.metadata;\n\t\t\t\tvalues.push( data );\n\n\t\t\t}\n\n\t\t\treturn values;\n\n\t\t}\n\n\t\tif ( isRootObject ) {\n\n\t\t\tconst textures = extractFromCache( meta.textures );\n\t\t\tconst images = extractFromCache( meta.images );\n\n\t\t\tif ( textures.length > 0 ) data.textures = textures;\n\t\t\tif ( images.length > 0 ) data.images = images;\n\n\t\t}\n\n\t\treturn data;\n\n\t}\n\n\t/**\n\t * Returns a new material with copied values from this instance.\n\t *\n\t * @return {Material} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Copies the values of the given material to this instance.\n\t *\n\t * @param {Material} source - The material to copy.\n\t * @return {Material} A reference to this instance.\n\t */\n\tcopy( source ) {\n\n\t\tthis.name = source.name;\n\n\t\tthis.blending = source.blending;\n\t\tthis.side = source.side;\n\t\tthis.vertexColors = source.vertexColors;\n\n\t\tthis.opacity = source.opacity;\n\t\tthis.transparent = source.transparent;\n\n\t\tthis.blendSrc = source.blendSrc;\n\t\tthis.blendDst = source.blendDst;\n\t\tthis.blendEquation = source.blendEquation;\n\t\tthis.blendSrcAlpha = source.blendSrcAlpha;\n\t\tthis.blendDstAlpha = source.blendDstAlpha;\n\t\tthis.blendEquationAlpha = source.blendEquationAlpha;\n\t\tthis.blendColor.copy( source.blendColor );\n\t\tthis.blendAlpha = source.blendAlpha;\n\n\t\tthis.depthFunc = source.depthFunc;\n\t\tthis.depthTest = source.depthTest;\n\t\tthis.depthWrite = source.depthWrite;\n\n\t\tthis.stencilWriteMask = source.stencilWriteMask;\n\t\tthis.stencilFunc = source.stencilFunc;\n\t\tthis.stencilRef = source.stencilRef;\n\t\tthis.stencilFuncMask = source.stencilFuncMask;\n\t\tthis.stencilFail = source.stencilFail;\n\t\tthis.stencilZFail = source.stencilZFail;\n\t\tthis.stencilZPass = source.stencilZPass;\n\t\tthis.stencilWrite = source.stencilWrite;\n\n\t\tconst srcPlanes = source.clippingPlanes;\n\t\tlet dstPlanes = null;\n\n\t\tif ( srcPlanes !== null ) {\n\n\t\t\tconst n = srcPlanes.length;\n\t\t\tdstPlanes = new Array( n );\n\n\t\t\tfor ( let i = 0; i !== n; ++ i ) {\n\n\t\t\t\tdstPlanes[ i ] = srcPlanes[ i ].clone();\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis.clippingPlanes = dstPlanes;\n\t\tthis.clipIntersection = source.clipIntersection;\n\t\tthis.clipShadows = source.clipShadows;\n\n\t\tthis.shadowSide = source.shadowSide;\n\n\t\tthis.colorWrite = source.colorWrite;\n\n\t\tthis.precision = source.precision;\n\n\t\tthis.polygonOffset = source.polygonOffset;\n\t\tthis.polygonOffsetFactor = source.polygonOffsetFactor;\n\t\tthis.polygonOffsetUnits = source.polygonOffsetUnits;\n\n\t\tthis.dithering = source.dithering;\n\n\t\tthis.alphaTest = source.alphaTest;\n\t\tthis.alphaHash = source.alphaHash;\n\t\tthis.alphaToCoverage = source.alphaToCoverage;\n\t\tthis.premultipliedAlpha = source.premultipliedAlpha;\n\t\tthis.forceSinglePass = source.forceSinglePass;\n\t\tthis.allowOverride = source.allowOverride;\n\n\t\tthis.visible = source.visible;\n\n\t\tthis.toneMapped = source.toneMapped;\n\n\t\tthis.userData = JSON.parse( JSON.stringify( source.userData ) );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t *\n\t * @fires Material#dispose\n\t */\n\tdispose() {\n\n\t\t/**\n\t\t * Fires when the material has been disposed of.\n\t\t *\n\t\t * @event Material#dispose\n\t\t * @type {Object}\n\t\t */\n\t\tthis.dispatchEvent( { type: 'dispose' } );\n\n\t}\n\n\t/**\n\t * Setting this property to `true` indicates the engine the material\n\t * needs to be recompiled.\n\t *\n\t * @type {boolean}\n\t * @default false\n\t * @param {boolean} value\n\t */\n\tset needsUpdate( value ) {\n\n\t\tif ( value === true ) this.version ++;\n\n\t}\n\n}\n\n/**\n * A material for drawing geometries in a simple shaded (flat or wireframe) way.\n *\n * This material is not affected by lights.\n *\n * @augments Material\n * @demo scenes/material-browser.html#MeshBasicMaterial\n */\nclass MeshBasicMaterial extends Material {\n\n\t/**\n\t * Constructs a new mesh basic material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshBasicMaterial = true;\n\n\t\tthis.type = 'MeshBasicMaterial';\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.color = new Color( 0xffffff ); // diffuse\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map\n\t\t * color is modulated by the diffuse `color`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The light map. Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.lightMap = null;\n\n\t\t/**\n\t\t * Intensity of the baked light.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.lightMapIntensity = 1.0;\n\n\t\t/**\n\t\t * The red channel of this texture is used as the ambient occlusion map.\n\t\t * Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.aoMap = null;\n\n\t\t/**\n\t\t * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`\n\t\t * disables ambient occlusion. Where intensity is `1` and the AO map's\n\t\t * red channel is also `1`, ambient light is fully occluded on a surface.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.aoMapIntensity = 1.0;\n\n\t\t/**\n\t\t * Specular map used by the material.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.specularMap = null;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * The environment map.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.envMap = null;\n\n\t\t/**\n\t\t * The rotation of the environment map in radians.\n\t\t *\n\t\t * @type {Euler}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.envMapRotation = new Euler();\n\n\t\t/**\n\t\t * How to combine the result of the surface's color with the environment map, if any.\n\t\t *\n\t\t * When set to `MixOperation`, the {@link MeshBasicMaterial#reflectivity} is used to\n\t\t * blend between the two colors.\n\t\t *\n\t\t * @type {(MultiplyOperation|MixOperation|AddOperation)}\n\t\t * @default MultiplyOperation\n\t\t */\n\t\tthis.combine = MultiplyOperation;\n\n\t\t/**\n\t\t * How much the environment map affects the surface.\n\t\t * The valid range is between `0` (no reflections) and `1` (full reflections).\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.reflectivity = 1;\n\n\t\t/**\n\t\t * The index of refraction (IOR) of air (approximately 1) divided by the\n\t\t * index of refraction of the material. It is used with environment mapping\n\t\t * modes {@link CubeRefractionMapping} and {@link EquirectangularRefractionMapping}.\n\t\t * The refraction ratio should not exceed `1`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0.98\n\t\t */\n\t\tthis.refractionRatio = 0.98;\n\n\t\t/**\n\t\t * Renders the geometry as a wireframe.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.wireframe = false;\n\n\t\t/**\n\t\t * Controls the thickness of the wireframe.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.wireframeLinewidth = 1;\n\n\t\t/**\n\t\t * Defines appearance of wireframe ends.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinecap = 'round';\n\n\t\t/**\n\t\t * Defines appearance of wireframe joints.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinejoin = 'round';\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.color.copy( source.color );\n\n\t\tthis.map = source.map;\n\n\t\tthis.lightMap = source.lightMap;\n\t\tthis.lightMapIntensity = source.lightMapIntensity;\n\n\t\tthis.aoMap = source.aoMap;\n\t\tthis.aoMapIntensity = source.aoMapIntensity;\n\n\t\tthis.specularMap = source.specularMap;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.envMap = source.envMap;\n\t\tthis.envMapRotation.copy( source.envMapRotation );\n\t\tthis.combine = source.combine;\n\t\tthis.reflectivity = source.reflectivity;\n\t\tthis.refractionRatio = source.refractionRatio;\n\n\t\tthis.wireframe = source.wireframe;\n\t\tthis.wireframeLinewidth = source.wireframeLinewidth;\n\t\tthis.wireframeLinecap = source.wireframeLinecap;\n\t\tthis.wireframeLinejoin = source.wireframeLinejoin;\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\n// Fast Half Float Conversions, http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf\n\nconst _tables = /*@__PURE__*/ _generateTables();\n\nfunction _generateTables() {\n\n\t// float32 to float16 helpers\n\n\tconst buffer = new ArrayBuffer( 4 );\n\tconst floatView = new Float32Array( buffer );\n\tconst uint32View = new Uint32Array( buffer );\n\n\tconst baseTable = new Uint32Array( 512 );\n\tconst shiftTable = new Uint32Array( 512 );\n\n\tfor ( let i = 0; i < 256; ++ i ) {\n\n\t\tconst e = i - 127;\n\n\t\t// very small number (0, -0)\n\n\t\tif ( e < -27 ) {\n\n\t\t\tbaseTable[ i ] = 0x0000;\n\t\t\tbaseTable[ i | 0x100 ] = 0x8000;\n\t\t\tshiftTable[ i ] = 24;\n\t\t\tshiftTable[ i | 0x100 ] = 24;\n\n\t\t\t// small number (denorm)\n\n\t\t} else if ( e < -14 ) {\n\n\t\t\tbaseTable[ i ] = 0x0400 >> ( - e - 14 );\n\t\t\tbaseTable[ i | 0x100 ] = ( 0x0400 >> ( - e - 14 ) ) | 0x8000;\n\t\t\tshiftTable[ i ] = - e - 1;\n\t\t\tshiftTable[ i | 0x100 ] = - e - 1;\n\n\t\t\t// normal number\n\n\t\t} else if ( e <= 15 ) {\n\n\t\t\tbaseTable[ i ] = ( e + 15 ) << 10;\n\t\t\tbaseTable[ i | 0x100 ] = ( ( e + 15 ) << 10 ) | 0x8000;\n\t\t\tshiftTable[ i ] = 13;\n\t\t\tshiftTable[ i | 0x100 ] = 13;\n\n\t\t\t// large number (Infinity, -Infinity)\n\n\t\t} else if ( e < 128 ) {\n\n\t\t\tbaseTable[ i ] = 0x7c00;\n\t\t\tbaseTable[ i | 0x100 ] = 0xfc00;\n\t\t\tshiftTable[ i ] = 24;\n\t\t\tshiftTable[ i | 0x100 ] = 24;\n\n\t\t\t// stay (NaN, Infinity, -Infinity)\n\n\t\t} else {\n\n\t\t\tbaseTable[ i ] = 0x7c00;\n\t\t\tbaseTable[ i | 0x100 ] = 0xfc00;\n\t\t\tshiftTable[ i ] = 13;\n\t\t\tshiftTable[ i | 0x100 ] = 13;\n\n\t\t}\n\n\t}\n\n\t// float16 to float32 helpers\n\n\tconst mantissaTable = new Uint32Array( 2048 );\n\tconst exponentTable = new Uint32Array( 64 );\n\tconst offsetTable = new Uint32Array( 64 );\n\n\tfor ( let i = 1; i < 1024; ++ i ) {\n\n\t\tlet m = i << 13; // zero pad mantissa bits\n\t\tlet e = 0; // zero exponent\n\n\t\t// normalized\n\t\twhile ( ( m & 0x00800000 ) === 0 ) {\n\n\t\t\tm <<= 1;\n\t\t\te -= 0x00800000; // decrement exponent\n\n\t\t}\n\n\t\tm &= -8388609; // clear leading 1 bit\n\t\te += 0x38800000; // adjust bias\n\n\t\tmantissaTable[ i ] = m | e;\n\n\t}\n\n\tfor ( let i = 1024; i < 2048; ++ i ) {\n\n\t\tmantissaTable[ i ] = 0x38000000 + ( ( i - 1024 ) << 13 );\n\n\t}\n\n\tfor ( let i = 1; i < 31; ++ i ) {\n\n\t\texponentTable[ i ] = i << 23;\n\n\t}\n\n\texponentTable[ 31 ] = 0x47800000;\n\texponentTable[ 32 ] = 0x80000000;\n\n\tfor ( let i = 33; i < 63; ++ i ) {\n\n\t\texponentTable[ i ] = 0x80000000 + ( ( i - 32 ) << 23 );\n\n\t}\n\n\texponentTable[ 63 ] = 0xc7800000;\n\n\tfor ( let i = 1; i < 64; ++ i ) {\n\n\t\tif ( i !== 32 ) {\n\n\t\t\toffsetTable[ i ] = 1024;\n\n\t\t}\n\n\t}\n\n\treturn {\n\t\tfloatView: floatView,\n\t\tuint32View: uint32View,\n\t\tbaseTable: baseTable,\n\t\tshiftTable: shiftTable,\n\t\tmantissaTable: mantissaTable,\n\t\texponentTable: exponentTable,\n\t\toffsetTable: offsetTable\n\t};\n\n}\n\n/**\n * Returns a half precision floating point value (FP16) from the given single\n * precision floating point value (FP32).\n *\n * @param {number} val - A single precision floating point value.\n * @return {number} The FP16 value.\n */\nfunction toHalfFloat( val ) {\n\n\tif ( Math.abs( val ) > 65504 ) warn( 'DataUtils.toHalfFloat(): Value out of range.' );\n\n\tval = clamp( val, -65504, 65504 );\n\n\t_tables.floatView[ 0 ] = val;\n\tconst f = _tables.uint32View[ 0 ];\n\tconst e = ( f >> 23 ) & 0x1ff;\n\treturn _tables.baseTable[ e ] + ( ( f & 0x007fffff ) >> _tables.shiftTable[ e ] );\n\n}\n\n/**\n * Returns a single precision floating point value (FP32) from the given half\n * precision floating point value (FP16).\n *\n * @param {number} val - A half precision floating point value.\n * @return {number} The FP32 value.\n */\nfunction fromHalfFloat( val ) {\n\n\tconst m = val >> 10;\n\t_tables.uint32View[ 0 ] = _tables.mantissaTable[ _tables.offsetTable[ m ] + ( val & 0x3ff ) ] + _tables.exponentTable[ m ];\n\treturn _tables.floatView[ 0 ];\n\n}\n\n/**\n * A class containing utility functions for data.\n *\n * @hideconstructor\n */\nclass DataUtils {\n\n\t/**\n\t * Returns a half precision floating point value (FP16) from the given single\n\t * precision floating point value (FP32).\n\t *\n\t * @param {number} val - A single precision floating point value.\n\t * @return {number} The FP16 value.\n\t */\n\tstatic toHalfFloat( val ) {\n\n\t\treturn toHalfFloat( val );\n\n\t}\n\n\t/**\n\t * Returns a single precision floating point value (FP32) from the given half\n\t * precision floating point value (FP16).\n\t *\n\t * @param {number} val - A half precision floating point value.\n\t * @return {number} The FP32 value.\n\t */\n\tstatic fromHalfFloat( val ) {\n\n\t\treturn fromHalfFloat( val );\n\n\t}\n\n}\n\nconst _vector$9 = /*@__PURE__*/ new Vector3();\nconst _vector2$1 = /*@__PURE__*/ new Vector2();\n\nlet _id$2 = 0;\n\n/**\n * This class stores data for an attribute (such as vertex positions, face\n * indices, normals, colors, UVs, and any custom attributes ) associated with\n * a geometry, which allows for more efficient passing of data to the GPU.\n *\n * When working with vector-like data, the `fromBufferAttribute( attribute, index )`\n * helper methods on vector and color class might be helpful. E.g. {@link Vector3#fromBufferAttribute}.\n */\nclass BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {TypedArray} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized = false ) {\n\n\t\tif ( Array.isArray( array ) ) {\n\n\t\t\tthrow new TypeError( 'THREE.BufferAttribute: array should be a Typed Array.' );\n\n\t\t}\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isBufferAttribute = true;\n\n\t\t/**\n\t\t * The ID of the buffer attribute.\n\t\t *\n\t\t * @name BufferAttribute#id\n\t\t * @type {number}\n\t\t * @readonly\n\t\t */\n\t\tObject.defineProperty( this, 'id', { value: _id$2 ++ } );\n\n\t\t/**\n\t\t * The name of the buffer attribute.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\n\t\t/**\n\t\t * The array holding the attribute data. It should have `itemSize * numVertices`\n\t\t * elements, where `numVertices` is the number of vertices in the associated geometry.\n\t\t *\n\t\t * @type {TypedArray}\n\t\t */\n\t\tthis.array = array;\n\n\t\t/**\n\t\t * The number of values of the array that should be associated with a particular vertex.\n\t\t * For instance, if this attribute is storing a 3-component vector (such as a position,\n\t\t * normal, or color), then the value should be `3`.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.itemSize = itemSize;\n\n\t\t/**\n\t\t * Represents the number of items this buffer attribute stores. It is internally computed\n\t\t * by dividing the `array` length by the `itemSize`.\n\t\t *\n\t\t * @type {number}\n\t\t * @readonly\n\t\t */\n\t\tthis.count = array !== undefined ? array.length / itemSize : 0;\n\n\t\t/**\n\t\t * Applies to integer data only. Indicates how the underlying data in the buffer maps to\n\t\t * the values in the GLSL code. For instance, if `array` is an instance of `UInt16Array`,\n\t\t * and `normalized` is `true`, the values `0 - +65535` in the array data will be mapped to\n\t\t * `0.0f - +1.0f` in the GLSL attribute. If `normalized` is `false`, the values will be converted\n\t\t * to floats unmodified, i.e. `65535` becomes `65535.0f`.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tthis.normalized = normalized;\n\n\t\t/**\n\t\t * Defines the intended usage pattern of the data store for optimization purposes.\n\t\t *\n\t\t * Note: After the initial use of a buffer, its usage cannot be changed. Instead,\n\t\t * instantiate a new one and set the desired usage before the next render.\n\t\t *\n\t\t * @type {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)}\n\t\t * @default StaticDrawUsage\n\t\t */\n\t\tthis.usage = StaticDrawUsage;\n\n\t\t/**\n\t\t * This can be used to only update some components of stored vectors (for example, just the\n\t\t * component related to color). Use the `addUpdateRange()` function to add ranges to this array.\n\t\t *\n\t\t * @type {Array<Object>}\n\t\t */\n\t\tthis.updateRanges = [];\n\n\t\t/**\n\t\t * Configures the bound GPU type for use in shaders.\n\t\t *\n\t\t * Note: this only has an effect for integer arrays and is not configurable for float arrays.\n\t\t * For lower precision float types, use `Float16BufferAttribute`.\n\t\t *\n\t\t * @type {(FloatType|IntType)}\n\t\t * @default FloatType\n\t\t */\n\t\tthis.gpuType = FloatType;\n\n\t\t/**\n\t\t * A version number, incremented every time the `needsUpdate` is set to `true`.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.version = 0;\n\n\t}\n\n\t/**\n\t * A callback function that is executed after the renderer has transferred the attribute\n\t * array data to the GPU.\n\t */\n\tonUploadCallback() {}\n\n\t/**\n\t * Flag to indicate that this attribute has changed and should be re-sent to\n\t * the GPU. Set this to `true` when you modify the value of the array.\n\t *\n\t * @type {number}\n\t * @default false\n\t * @param {boolean} value\n\t */\n\tset needsUpdate( value ) {\n\n\t\tif ( value === true ) this.version ++;\n\n\t}\n\n\t/**\n\t * Sets the usage of this buffer attribute.\n\t *\n\t * @param {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)} value - The usage to set.\n\t * @return {BufferAttribute} A reference to this buffer attribute.\n\t */\n\tsetUsage( value ) {\n\n\t\tthis.usage = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds a range of data in the data array to be updated on the GPU.\n\t *\n\t * @param {number} start - Position at which to start update.\n\t * @param {number} count - The number of components to update.\n\t */\n\taddUpdateRange( start, count ) {\n\n\t\tthis.updateRanges.push( { start, count } );\n\n\t}\n\n\t/**\n\t * Clears the update ranges.\n\t */\n\tclearUpdateRanges() {\n\n\t\tthis.updateRanges.length = 0;\n\n\t}\n\n\t/**\n\t * Copies the values of the given buffer attribute to this instance.\n\t *\n\t * @param {BufferAttribute} source - The buffer attribute to copy.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tcopy( source ) {\n\n\t\tthis.name = source.name;\n\t\tthis.array = new source.array.constructor( source.array );\n\t\tthis.itemSize = source.itemSize;\n\t\tthis.count = source.count;\n\t\tthis.normalized = source.normalized;\n\n\t\tthis.usage = source.usage;\n\t\tthis.gpuType = source.gpuType;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies a vector from the given buffer attribute to this one. The start\n\t * and destination position in the attribute buffers are represented by the\n\t * given indices.\n\t *\n\t * @param {number} index1 - The destination index into this buffer attribute.\n\t * @param {BufferAttribute} attribute - The buffer attribute to copy from.\n\t * @param {number} index2 - The source index into the given buffer attribute.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tcopyAt( index1, attribute, index2 ) {\n\n\t\tindex1 *= this.itemSize;\n\t\tindex2 *= attribute.itemSize;\n\n\t\tfor ( let i = 0, l = this.itemSize; i < l; i ++ ) {\n\n\t\t\tthis.array[ index1 + i ] = attribute.array[ index2 + i ];\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the given array data into this buffer attribute.\n\t *\n\t * @param {(TypedArray|Array)} array - The array to copy.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tcopyArray( array ) {\n\n\t\tthis.array.set( array );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Applies the given 3x3 matrix to the given attribute. Works with\n\t * item size `2` and `3`.\n\t *\n\t * @param {Matrix3} m - The matrix to apply.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tapplyMatrix3( m ) {\n\n\t\tif ( this.itemSize === 2 ) {\n\n\t\t\tfor ( let i = 0, l = this.count; i < l; i ++ ) {\n\n\t\t\t\t_vector2$1.fromBufferAttribute( this, i );\n\t\t\t\t_vector2$1.applyMatrix3( m );\n\n\t\t\t\tthis.setXY( i, _vector2$1.x, _vector2$1.y );\n\n\t\t\t}\n\n\t\t} else if ( this.itemSize === 3 ) {\n\n\t\t\tfor ( let i = 0, l = this.count; i < l; i ++ ) {\n\n\t\t\t\t_vector$9.fromBufferAttribute( this, i );\n\t\t\t\t_vector$9.applyMatrix3( m );\n\n\t\t\t\tthis.setXYZ( i, _vector$9.x, _vector$9.y, _vector$9.z );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Applies the given 4x4 matrix to the given attribute. Only works with\n\t * item size `3`.\n\t *\n\t * @param {Matrix4} m - The matrix to apply.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tapplyMatrix4( m ) {\n\n\t\tfor ( let i = 0, l = this.count; i < l; i ++ ) {\n\n\t\t\t_vector$9.fromBufferAttribute( this, i );\n\n\t\t\t_vector$9.applyMatrix4( m );\n\n\t\t\tthis.setXYZ( i, _vector$9.x, _vector$9.y, _vector$9.z );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Applies the given 3x3 normal matrix to the given attribute. Only works with\n\t * item size `3`.\n\t *\n\t * @param {Matrix3} m - The normal matrix to apply.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tapplyNormalMatrix( m ) {\n\n\t\tfor ( let i = 0, l = this.count; i < l; i ++ ) {\n\n\t\t\t_vector$9.fromBufferAttribute( this, i );\n\n\t\t\t_vector$9.applyNormalMatrix( m );\n\n\t\t\tthis.setXYZ( i, _vector$9.x, _vector$9.y, _vector$9.z );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Applies the given 4x4 matrix to the given attribute. Only works with\n\t * item size `3` and with direction vectors.\n\t *\n\t * @param {Matrix4} m - The matrix to apply.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\ttransformDirection( m ) {\n\n\t\tfor ( let i = 0, l = this.count; i < l; i ++ ) {\n\n\t\t\t_vector$9.fromBufferAttribute( this, i );\n\n\t\t\t_vector$9.transformDirection( m );\n\n\t\t\tthis.setXYZ( i, _vector$9.x, _vector$9.y, _vector$9.z );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given array data in the buffer attribute.\n\t *\n\t * @param {(TypedArray|Array)} value - The array data to set.\n\t * @param {number} [offset=0] - The offset in this buffer attribute's array.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tset( value, offset = 0 ) {\n\n\t\t// Matching BufferAttribute constructor, do not normalize the array.\n\t\tthis.array.set( value, offset );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the given component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} component - The component index.\n\t * @return {number} The returned value.\n\t */\n\tgetComponent( index, component ) {\n\n\t\tlet value = this.array[ index * this.itemSize + component ];\n\n\t\tif ( this.normalized ) value = denormalize( value, this.array );\n\n\t\treturn value;\n\n\t}\n\n\t/**\n\t * Sets the given value to the given component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} component - The component index.\n\t * @param {number} value - The value to set.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetComponent( index, component, value ) {\n\n\t\tif ( this.normalized ) value = normalize( value, this.array );\n\n\t\tthis.array[ index * this.itemSize + component ] = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the x component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @return {number} The x component.\n\t */\n\tgetX( index ) {\n\n\t\tlet x = this.array[ index * this.itemSize ];\n\n\t\tif ( this.normalized ) x = denormalize( x, this.array );\n\n\t\treturn x;\n\n\t}\n\n\t/**\n\t * Sets the x component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} x - The value to set.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetX( index, x ) {\n\n\t\tif ( this.normalized ) x = normalize( x, this.array );\n\n\t\tthis.array[ index * this.itemSize ] = x;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the y component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @return {number} The y component.\n\t */\n\tgetY( index ) {\n\n\t\tlet y = this.array[ index * this.itemSize + 1 ];\n\n\t\tif ( this.normalized ) y = denormalize( y, this.array );\n\n\t\treturn y;\n\n\t}\n\n\t/**\n\t * Sets the y component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} y - The value to set.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetY( index, y ) {\n\n\t\tif ( this.normalized ) y = normalize( y, this.array );\n\n\t\tthis.array[ index * this.itemSize + 1 ] = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the z component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @return {number} The z component.\n\t */\n\tgetZ( index ) {\n\n\t\tlet z = this.array[ index * this.itemSize + 2 ];\n\n\t\tif ( this.normalized ) z = denormalize( z, this.array );\n\n\t\treturn z;\n\n\t}\n\n\t/**\n\t * Sets the z component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} z - The value to set.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetZ( index, z ) {\n\n\t\tif ( this.normalized ) z = normalize( z, this.array );\n\n\t\tthis.array[ index * this.itemSize + 2 ] = z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the w component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @return {number} The w component.\n\t */\n\tgetW( index ) {\n\n\t\tlet w = this.array[ index * this.itemSize + 3 ];\n\n\t\tif ( this.normalized ) w = denormalize( w, this.array );\n\n\t\treturn w;\n\n\t}\n\n\t/**\n\t * Sets the w component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} w - The value to set.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetW( index, w ) {\n\n\t\tif ( this.normalized ) w = normalize( w, this.array );\n\n\t\tthis.array[ index * this.itemSize + 3 ] = w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the x and y component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} x - The value for the x component to set.\n\t * @param {number} y - The value for the y component to set.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetXY( index, x, y ) {\n\n\t\tindex *= this.itemSize;\n\n\t\tif ( this.normalized ) {\n\n\t\t\tx = normalize( x, this.array );\n\t\t\ty = normalize( y, this.array );\n\n\t\t}\n\n\t\tthis.array[ index + 0 ] = x;\n\t\tthis.array[ index + 1 ] = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the x, y and z component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} x - The value for the x component to set.\n\t * @param {number} y - The value for the y component to set.\n\t * @param {number} z - The value for the z component to set.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetXYZ( index, x, y, z ) {\n\n\t\tindex *= this.itemSize;\n\n\t\tif ( this.normalized ) {\n\n\t\t\tx = normalize( x, this.array );\n\t\t\ty = normalize( y, this.array );\n\t\t\tz = normalize( z, this.array );\n\n\t\t}\n\n\t\tthis.array[ index + 0 ] = x;\n\t\tthis.array[ index + 1 ] = y;\n\t\tthis.array[ index + 2 ] = z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the x, y, z and w component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} x - The value for the x component to set.\n\t * @param {number} y - The value for the y component to set.\n\t * @param {number} z - The value for the z component to set.\n\t * @param {number} w - The value for the w component to set.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetXYZW( index, x, y, z, w ) {\n\n\t\tindex *= this.itemSize;\n\n\t\tif ( this.normalized ) {\n\n\t\t\tx = normalize( x, this.array );\n\t\t\ty = normalize( y, this.array );\n\t\t\tz = normalize( z, this.array );\n\t\t\tw = normalize( w, this.array );\n\n\t\t}\n\n\t\tthis.array[ index + 0 ] = x;\n\t\tthis.array[ index + 1 ] = y;\n\t\tthis.array[ index + 2 ] = z;\n\t\tthis.array[ index + 3 ] = w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given callback function that is executed after the Renderer has transferred\n\t * the attribute array data to the GPU. Can be used to perform clean-up operations after\n\t * the upload when attribute data are not needed anymore on the CPU side.\n\t *\n\t * @param {Function} callback - The `onUpload()` callback.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tonUpload( callback ) {\n\n\t\tthis.onUploadCallback = callback;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new buffer attribute with copied values from this instance.\n\t *\n\t * @return {BufferAttribute} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor( this.array, this.itemSize ).copy( this );\n\n\t}\n\n\t/**\n\t * Serializes the buffer attribute into JSON.\n\t *\n\t * @return {Object} A JSON object representing the serialized buffer attribute.\n\t */\n\ttoJSON() {\n\n\t\tconst data = {\n\t\t\titemSize: this.itemSize,\n\t\t\ttype: this.array.constructor.name,\n\t\t\tarray: Array.from( this.array ),\n\t\t\tnormalized: this.normalized\n\t\t};\n\n\t\tif ( this.name !== '' ) data.name = this.name;\n\t\tif ( this.usage !== StaticDrawUsage ) data.usage = this.usage;\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * Convenient class that can be used when creating a `Int8` buffer attribute with\n * a plain `Array` instance.\n *\n * @augments BufferAttribute\n */\nclass Int8BufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {(Array<number>|Int8Array)} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized ) {\n\n\t\tsuper( new Int8Array( array ), itemSize, normalized );\n\n\t}\n\n}\n\n/**\n * Convenient class that can be used when creating a `UInt8` buffer attribute with\n * a plain `Array` instance.\n *\n * @augments BufferAttribute\n */\nclass Uint8BufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {(Array<number>|Uint8Array)} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized ) {\n\n\t\tsuper( new Uint8Array( array ), itemSize, normalized );\n\n\t}\n\n}\n\n/**\n * Convenient class that can be used when creating a `UInt8Clamped` buffer attribute with\n * a plain `Array` instance.\n *\n * @augments BufferAttribute\n */\nclass Uint8ClampedBufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {(Array<number>|Uint8ClampedArray)} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized ) {\n\n\t\tsuper( new Uint8ClampedArray( array ), itemSize, normalized );\n\n\t}\n\n}\n\n/**\n * Convenient class that can be used when creating a `Int16` buffer attribute with\n * a plain `Array` instance.\n *\n * @augments BufferAttribute\n */\nclass Int16BufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {(Array<number>|Int16Array)} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized ) {\n\n\t\tsuper( new Int16Array( array ), itemSize, normalized );\n\n\t}\n\n}\n\n/**\n * Convenient class that can be used when creating a `UInt16` buffer attribute with\n * a plain `Array` instance.\n *\n * @augments BufferAttribute\n */\nclass Uint16BufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {(Array<number>|Uint16Array)} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized ) {\n\n\t\tsuper( new Uint16Array( array ), itemSize, normalized );\n\n\t}\n\n}\n\n/**\n * Convenient class that can be used when creating a `Int32` buffer attribute with\n * a plain `Array` instance.\n *\n * @augments BufferAttribute\n */\nclass Int32BufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {(Array<number>|Int32Array)} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized ) {\n\n\t\tsuper( new Int32Array( array ), itemSize, normalized );\n\n\t}\n\n}\n\n/**\n * Convenient class that can be used when creating a `UInt32` buffer attribute with\n * a plain `Array` instance.\n *\n * @augments BufferAttribute\n */\nclass Uint32BufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {(Array<number>|Uint32Array)} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized ) {\n\n\t\tsuper( new Uint32Array( array ), itemSize, normalized );\n\n\t}\n\n}\n\n/**\n * Convenient class that can be used when creating a `Float16` buffer attribute with\n * a plain `Array` instance.\n *\n * This class automatically converts to and from FP16 via `Uint16Array` since `Float16Array`\n * browser support is still problematic.\n *\n * @augments BufferAttribute\n */\nclass Float16BufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {(Array<number>|Uint16Array)} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized ) {\n\n\t\tsuper( new Uint16Array( array ), itemSize, normalized );\n\n\t\tthis.isFloat16BufferAttribute = true;\n\n\t}\n\n\tgetX( index ) {\n\n\t\tlet x = fromHalfFloat( this.array[ index * this.itemSize ] );\n\n\t\tif ( this.normalized ) x = denormalize( x, this.array );\n\n\t\treturn x;\n\n\t}\n\n\tsetX( index, x ) {\n\n\t\tif ( this.normalized ) x = normalize( x, this.array );\n\n\t\tthis.array[ index * this.itemSize ] = toHalfFloat( x );\n\n\t\treturn this;\n\n\t}\n\n\tgetY( index ) {\n\n\t\tlet y = fromHalfFloat( this.array[ index * this.itemSize + 1 ] );\n\n\t\tif ( this.normalized ) y = denormalize( y, this.array );\n\n\t\treturn y;\n\n\t}\n\n\tsetY( index, y ) {\n\n\t\tif ( this.normalized ) y = normalize( y, this.array );\n\n\t\tthis.array[ index * this.itemSize + 1 ] = toHalfFloat( y );\n\n\t\treturn this;\n\n\t}\n\n\tgetZ( index ) {\n\n\t\tlet z = fromHalfFloat( this.array[ index * this.itemSize + 2 ] );\n\n\t\tif ( this.normalized ) z = denormalize( z, this.array );\n\n\t\treturn z;\n\n\t}\n\n\tsetZ( index, z ) {\n\n\t\tif ( this.normalized ) z = normalize( z, this.array );\n\n\t\tthis.array[ index * this.itemSize + 2 ] = toHalfFloat( z );\n\n\t\treturn this;\n\n\t}\n\n\tgetW( index ) {\n\n\t\tlet w = fromHalfFloat( this.array[ index * this.itemSize + 3 ] );\n\n\t\tif ( this.normalized ) w = denormalize( w, this.array );\n\n\t\treturn w;\n\n\t}\n\n\tsetW( index, w ) {\n\n\t\tif ( this.normalized ) w = normalize( w, this.array );\n\n\t\tthis.array[ index * this.itemSize + 3 ] = toHalfFloat( w );\n\n\t\treturn this;\n\n\t}\n\n\tsetXY( index, x, y ) {\n\n\t\tindex *= this.itemSize;\n\n\t\tif ( this.normalized ) {\n\n\t\t\tx = normalize( x, this.array );\n\t\t\ty = normalize( y, this.array );\n\n\t\t}\n\n\t\tthis.array[ index + 0 ] = toHalfFloat( x );\n\t\tthis.array[ index + 1 ] = toHalfFloat( y );\n\n\t\treturn this;\n\n\t}\n\n\tsetXYZ( index, x, y, z ) {\n\n\t\tindex *= this.itemSize;\n\n\t\tif ( this.normalized ) {\n\n\t\t\tx = normalize( x, this.array );\n\t\t\ty = normalize( y, this.array );\n\t\t\tz = normalize( z, this.array );\n\n\t\t}\n\n\t\tthis.array[ index + 0 ] = toHalfFloat( x );\n\t\tthis.array[ index + 1 ] = toHalfFloat( y );\n\t\tthis.array[ index + 2 ] = toHalfFloat( z );\n\n\t\treturn this;\n\n\t}\n\n\tsetXYZW( index, x, y, z, w ) {\n\n\t\tindex *= this.itemSize;\n\n\t\tif ( this.normalized ) {\n\n\t\t\tx = normalize( x, this.array );\n\t\t\ty = normalize( y, this.array );\n\t\t\tz = normalize( z, this.array );\n\t\t\tw = normalize( w, this.array );\n\n\t\t}\n\n\t\tthis.array[ index + 0 ] = toHalfFloat( x );\n\t\tthis.array[ index + 1 ] = toHalfFloat( y );\n\t\tthis.array[ index + 2 ] = toHalfFloat( z );\n\t\tthis.array[ index + 3 ] = toHalfFloat( w );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * Convenient class that can be used when creating a `Float32` buffer attribute with\n * a plain `Array` instance.\n *\n * @augments BufferAttribute\n */\nclass Float32BufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new buffer attribute.\n\t *\n\t * @param {(Array<number>|Float32Array)} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( array, itemSize, normalized ) {\n\n\t\tsuper( new Float32Array( array ), itemSize, normalized );\n\n\t}\n\n}\n\nlet _id$1 = 0;\n\nconst _m1 = /*@__PURE__*/ new Matrix4();\nconst _obj = /*@__PURE__*/ new Object3D();\nconst _offset = /*@__PURE__*/ new Vector3();\nconst _box$2 = /*@__PURE__*/ new Box3();\nconst _boxMorphTargets = /*@__PURE__*/ new Box3();\nconst _vector$8 = /*@__PURE__*/ new Vector3();\n\n/**\n * A representation of mesh, line, or point geometry. Includes vertex\n * positions, face indices, normals, colors, UVs, and custom attributes\n * within buffers, reducing the cost of passing all this data to the GPU.\n *\n * ```js\n * const geometry = new THREE.BufferGeometry();\n * // create a simple square shape. We duplicate the top left and bottom right\n * // vertices because each vertex needs to appear once per triangle.\n * const vertices = new Float32Array( [\n * \t-1.0, -1.0,  1.0, // v0\n * \t 1.0, -1.0,  1.0, // v1\n * \t 1.0,  1.0,  1.0, // v2\n *\n * \t 1.0,  1.0,  1.0, // v3\n * \t-1.0,  1.0,  1.0, // v4\n * \t-1.0, -1.0,  1.0  // v5\n * ] );\n * // itemSize = 3 because there are 3 values (components) per vertex\n * geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );\n * const material = new THREE.MeshBasicMaterial( { color: 0xff0000 } );\n * const mesh = new THREE.Mesh( geometry, material );\n * ```\n *\n * @augments EventDispatcher\n */\nclass BufferGeometry extends EventDispatcher {\n\n\t/**\n\t * Constructs a new geometry.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isBufferGeometry = true;\n\n\t\t/**\n\t\t * The ID of the geometry.\n\t\t *\n\t\t * @name BufferGeometry#id\n\t\t * @type {number}\n\t\t * @readonly\n\t\t */\n\t\tObject.defineProperty( this, 'id', { value: _id$1 ++ } );\n\n\t\t/**\n\t\t * The UUID of the geometry.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.uuid = generateUUID();\n\n\t\t/**\n\t\t * The name of the geometry.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\t\tthis.type = 'BufferGeometry';\n\n\t\t/**\n\t\t * Allows for vertices to be re-used across multiple triangles; this is\n\t\t * called using \"indexed triangles\". Each triangle is associated with the\n\t\t * indices of three vertices. This attribute therefore stores the index of\n\t\t * each vertex for each triangular face. If this attribute is not set, the\n\t\t * renderer assumes that each three contiguous positions represent a single triangle.\n\t\t *\n\t\t * @type {?BufferAttribute}\n\t\t * @default null\n\t\t */\n\t\tthis.index = null;\n\n\t\t/**\n\t\t * A (storage) buffer attribute which was generated with a compute shader and\n\t\t * now defines indirect draw calls.\n\t\t *\n\t\t * Can only be used with {@link WebGPURenderer} and a WebGPU backend.\n\t\t *\n\t\t * @type {?BufferAttribute}\n\t\t * @default null\n\t\t */\n\t\tthis.indirect = null;\n\n\t\t/**\n\t\t * The offset, in bytes, into the indirect drawing buffer where the value data begins. If an array is provided, multiple indirect draw calls will be made for each offset.\n\t\t *\n\t\t * Can only be used with {@link WebGPURenderer} and a WebGPU backend.\n\t\t *\n\t\t * @type {number|Array<number>}\n\t\t * @default 0\n\t\t */\n\t\tthis.indirectOffset = 0;\n\n\t\t/**\n\t\t * This dictionary has as id the name of the attribute to be set and as value\n\t\t * the buffer attribute to set it to. Rather than accessing this property directly,\n\t\t * use `setAttribute()` and `getAttribute()` to access attributes of this geometry.\n\t\t *\n\t\t * @type {Object<string,(BufferAttribute|InterleavedBufferAttribute)>}\n\t\t */\n\t\tthis.attributes = {};\n\n\t\t/**\n\t\t * This dictionary holds the morph targets of the geometry.\n\t\t *\n\t\t * Note: Once the geometry has been rendered, the morph attribute data cannot\n\t\t * be changed. You will have to call `dispose()`, and create a new geometry instance.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.morphAttributes = {};\n\n\t\t/**\n\t\t * Used to control the morph target behavior; when set to `true`, the morph\n\t\t * target data is treated as relative offsets, rather than as absolute\n\t\t * positions/normals.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.morphTargetsRelative = false;\n\n\t\t/**\n\t\t * Split the geometry into groups, each of which will be rendered in a\n\t\t * separate draw call. This allows an array of materials to be used with the geometry.\n\t\t *\n\t\t * Use `addGroup()` and `clearGroups()` to edit groups, rather than modifying this array directly.\n\t\t *\n\t\t * Every vertex and index must belong to exactly one group — groups must not share vertices or\n\t\t * indices, and must not leave vertices or indices unused.\n\t\t *\n\t\t * @type {Array<Object>}\n\t\t */\n\t\tthis.groups = [];\n\n\t\t/**\n\t\t * Bounding box for the geometry which can be calculated with `computeBoundingBox()`.\n\t\t *\n\t\t * @type {?Box3}\n\t\t * @default null\n\t\t */\n\t\tthis.boundingBox = null;\n\n\t\t/**\n\t\t * Bounding sphere for the geometry which can be calculated with `computeBoundingSphere()`.\n\t\t *\n\t\t * @type {?Sphere}\n\t\t * @default null\n\t\t */\n\t\tthis.boundingSphere = null;\n\n\t\t/**\n\t\t * Determines the part of the geometry to render. This should not be set directly,\n\t\t * instead use `setDrawRange()`.\n\t\t *\n\t\t * @type {{start:number,count:number}}\n\t\t */\n\t\tthis.drawRange = { start: 0, count: Infinity };\n\n\t\t/**\n\t\t * An object that can be used to store custom data about the geometry.\n\t\t * It should not hold references to functions as these will not be cloned.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.userData = {};\n\n\t}\n\n\t/**\n\t * Returns the index of this geometry.\n\t *\n\t * @return {?BufferAttribute} The index. Returns `null` if no index is defined.\n\t */\n\tgetIndex() {\n\n\t\treturn this.index;\n\n\t}\n\n\t/**\n\t * Sets the given index to this geometry.\n\t *\n\t * @param {Array<number>|BufferAttribute} index - The index to set.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tsetIndex( index ) {\n\n\t\tif ( Array.isArray( index ) ) {\n\n\t\t\tthis.index = new ( arrayNeedsUint32( index ) ? Uint32BufferAttribute : Uint16BufferAttribute )( index, 1 );\n\n\t\t} else {\n\n\t\t\tthis.index = index;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given indirect attribute to this geometry.\n\t *\n\t * @param {BufferAttribute} indirect - The attribute holding indirect draw calls.\n\t * @param {number|Array<number>} [indirectOffset=0] - The offset, in bytes, into the indirect drawing buffer where the value data begins. If an array is provided, multiple indirect draw calls will be made for each offset.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tsetIndirect( indirect, indirectOffset = 0 ) {\n\n\t\tthis.indirect = indirect;\n\t\tthis.indirectOffset = indirectOffset;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the indirect attribute of this geometry.\n\t *\n\t * @return {?BufferAttribute} The indirect attribute. Returns `null` if no indirect attribute is defined.\n\t */\n\tgetIndirect() {\n\n\t\treturn this.indirect;\n\n\t}\n\n\t/**\n\t * Returns the buffer attribute for the given name.\n\t *\n\t * @param {string} name - The attribute name.\n\t * @return {BufferAttribute|InterleavedBufferAttribute|undefined} The buffer attribute.\n\t * Returns `undefined` if not attribute has been found.\n\t */\n\tgetAttribute( name ) {\n\n\t\treturn this.attributes[ name ];\n\n\t}\n\n\t/**\n\t * Sets the given attribute for the given name.\n\t *\n\t * @param {string} name - The attribute name.\n\t * @param {BufferAttribute|InterleavedBufferAttribute} attribute - The attribute to set.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tsetAttribute( name, attribute ) {\n\n\t\tthis.attributes[ name ] = attribute;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Deletes the attribute for the given name.\n\t *\n\t * @param {string} name - The attribute name to delete.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tdeleteAttribute( name ) {\n\n\t\tdelete this.attributes[ name ];\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this geometry has an attribute for the given name.\n\t *\n\t * @param {string} name - The attribute name.\n\t * @return {boolean} Whether this geometry has an attribute for the given name or not.\n\t */\n\thasAttribute( name ) {\n\n\t\treturn this.attributes[ name ] !== undefined;\n\n\t}\n\n\t/**\n\t * Adds a group to this geometry.\n\t *\n\t * @param {number} start - The first element in this draw call. That is the first\n\t * vertex for non-indexed geometry, otherwise the first triangle index.\n\t * @param {number} count - Specifies how many vertices (or indices) are part of this group.\n\t * @param {number} [materialIndex=0] - The material array index to use.\n\t */\n\taddGroup( start, count, materialIndex = 0 ) {\n\n\t\tthis.groups.push( {\n\n\t\t\tstart: start,\n\t\t\tcount: count,\n\t\t\tmaterialIndex: materialIndex\n\n\t\t} );\n\n\t}\n\n\t/**\n\t * Clears all groups.\n\t */\n\tclearGroups() {\n\n\t\tthis.groups = [];\n\n\t}\n\n\t/**\n\t * Sets the draw range for this geometry.\n\t *\n\t * @param {number} start - The first vertex for non-indexed geometry, otherwise the first triangle index.\n\t * @param {number} count - For non-indexed BufferGeometry, `count` is the number of vertices to render.\n\t * For indexed BufferGeometry, `count` is the number of indices to render.\n\t */\n\tsetDrawRange( start, count ) {\n\n\t\tthis.drawRange.start = start;\n\t\tthis.drawRange.count = count;\n\n\t}\n\n\t/**\n\t * Applies the given 4x4 transformation matrix to the geometry.\n\t *\n\t * @param {Matrix4} matrix - The matrix to apply.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tapplyMatrix4( matrix ) {\n\n\t\tconst position = this.attributes.position;\n\n\t\tif ( position !== undefined ) {\n\n\t\t\tposition.applyMatrix4( matrix );\n\n\t\t\tposition.needsUpdate = true;\n\n\t\t}\n\n\t\tconst normal = this.attributes.normal;\n\n\t\tif ( normal !== undefined ) {\n\n\t\t\tconst normalMatrix = new Matrix3().getNormalMatrix( matrix );\n\n\t\t\tnormal.applyNormalMatrix( normalMatrix );\n\n\t\t\tnormal.needsUpdate = true;\n\n\t\t}\n\n\t\tconst tangent = this.attributes.tangent;\n\n\t\tif ( tangent !== undefined ) {\n\n\t\t\ttangent.transformDirection( matrix );\n\n\t\t\ttangent.needsUpdate = true;\n\n\t\t}\n\n\t\tif ( this.boundingBox !== null ) {\n\n\t\t\tthis.computeBoundingBox();\n\n\t\t}\n\n\t\tif ( this.boundingSphere !== null ) {\n\n\t\t\tthis.computeBoundingSphere();\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Applies the rotation represented by the Quaternion to the geometry.\n\t *\n\t * @param {Quaternion} q - The Quaternion to apply.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tapplyQuaternion( q ) {\n\n\t\t_m1.makeRotationFromQuaternion( q );\n\n\t\tthis.applyMatrix4( _m1 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Rotates the geometry about the X axis. This is typically done as a one time\n\t * operation, and not during a loop. Use {@link Object3D#rotation} for typical\n\t * real-time mesh rotation.\n\t *\n\t * @param {number} angle - The angle in radians.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\trotateX( angle ) {\n\n\t\t// rotate geometry around world x-axis\n\n\t\t_m1.makeRotationX( angle );\n\n\t\tthis.applyMatrix4( _m1 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Rotates the geometry about the Y axis. This is typically done as a one time\n\t * operation, and not during a loop. Use {@link Object3D#rotation} for typical\n\t * real-time mesh rotation.\n\t *\n\t * @param {number} angle - The angle in radians.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\trotateY( angle ) {\n\n\t\t// rotate geometry around world y-axis\n\n\t\t_m1.makeRotationY( angle );\n\n\t\tthis.applyMatrix4( _m1 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Rotates the geometry about the Z axis. This is typically done as a one time\n\t * operation, and not during a loop. Use {@link Object3D#rotation} for typical\n\t * real-time mesh rotation.\n\t *\n\t * @param {number} angle - The angle in radians.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\trotateZ( angle ) {\n\n\t\t// rotate geometry around world z-axis\n\n\t\t_m1.makeRotationZ( angle );\n\n\t\tthis.applyMatrix4( _m1 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Translates the geometry. This is typically done as a one time\n\t * operation, and not during a loop. Use {@link Object3D#position} for typical\n\t * real-time mesh rotation.\n\t *\n\t * @param {number} x - The x offset.\n\t * @param {number} y - The y offset.\n\t * @param {number} z - The z offset.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\ttranslate( x, y, z ) {\n\n\t\t// translate geometry\n\n\t\t_m1.makeTranslation( x, y, z );\n\n\t\tthis.applyMatrix4( _m1 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Scales the geometry. This is typically done as a one time\n\t * operation, and not during a loop. Use {@link Object3D#scale} for typical\n\t * real-time mesh rotation.\n\t *\n\t * @param {number} x - The x scale.\n\t * @param {number} y - The y scale.\n\t * @param {number} z - The z scale.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tscale( x, y, z ) {\n\n\t\t// scale geometry\n\n\t\t_m1.makeScale( x, y, z );\n\n\t\tthis.applyMatrix4( _m1 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Rotates the geometry to face a point in 3D space. This is typically done as a one time\n\t * operation, and not during a loop. Use {@link Object3D#lookAt} for typical\n\t * real-time mesh rotation.\n\t *\n\t * @param {Vector3} vector - The target point.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tlookAt( vector ) {\n\n\t\t_obj.lookAt( vector );\n\n\t\t_obj.updateMatrix();\n\n\t\tthis.applyMatrix4( _obj.matrix );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Center the geometry based on its bounding box.\n\t *\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tcenter() {\n\n\t\tthis.computeBoundingBox();\n\n\t\tthis.boundingBox.getCenter( _offset ).negate();\n\n\t\tthis.translate( _offset.x, _offset.y, _offset.z );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Defines a geometry by creating a `position` attribute based on the given array of points. The array\n\t * can hold 2D or 3D vectors. When using two-dimensional data, the `z` coordinate for all vertices is\n\t * set to `0`.\n\t *\n\t * If the method is used with an existing `position` attribute, the vertex data are overwritten with the\n\t * data from the array. The length of the array must match the vertex count.\n\t *\n\t * @param {Array<Vector2>|Array<Vector3>} points - The points.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tsetFromPoints( points ) {\n\n\t\tconst positionAttribute = this.getAttribute( 'position' );\n\n\t\tif ( positionAttribute === undefined ) {\n\n\t\t\tconst position = [];\n\n\t\t\tfor ( let i = 0, l = points.length; i < l; i ++ ) {\n\n\t\t\t\tconst point = points[ i ];\n\t\t\t\tposition.push( point.x, point.y, point.z || 0 );\n\n\t\t\t}\n\n\t\t\tthis.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );\n\n\t\t} else {\n\n\t\t\tconst l = Math.min( points.length, positionAttribute.count ); // make sure data do not exceed buffer size\n\n\t\t\tfor ( let i = 0; i < l; i ++ ) {\n\n\t\t\t\tconst point = points[ i ];\n\t\t\t\tpositionAttribute.setXYZ( i, point.x, point.y, point.z || 0 );\n\n\t\t\t}\n\n\t\t\tif ( points.length > positionAttribute.count ) {\n\n\t\t\t\twarn( 'BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry.' );\n\n\t\t\t}\n\n\t\t\tpositionAttribute.needsUpdate = true;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes the bounding box of the geometry, and updates the `boundingBox` member.\n\t * The bounding box is not computed by the engine; it must be computed by your app.\n\t * You may need to recompute the bounding box if the geometry vertices are modified.\n\t */\n\tcomputeBoundingBox() {\n\n\t\tif ( this.boundingBox === null ) {\n\n\t\t\tthis.boundingBox = new Box3();\n\n\t\t}\n\n\t\tconst position = this.attributes.position;\n\t\tconst morphAttributesPosition = this.morphAttributes.position;\n\n\t\tif ( position && position.isGLBufferAttribute ) {\n\n\t\t\terror( 'BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.', this );\n\n\t\t\tthis.boundingBox.set(\n\t\t\t\tnew Vector3( - Infinity, - Infinity, - Infinity ),\n\t\t\t\tnew Vector3( + Infinity, + Infinity, + Infinity )\n\t\t\t);\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( position !== undefined ) {\n\n\t\t\tthis.boundingBox.setFromBufferAttribute( position );\n\n\t\t\t// process morph attributes if present\n\n\t\t\tif ( morphAttributesPosition ) {\n\n\t\t\t\tfor ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst morphAttribute = morphAttributesPosition[ i ];\n\t\t\t\t\t_box$2.setFromBufferAttribute( morphAttribute );\n\n\t\t\t\t\tif ( this.morphTargetsRelative ) {\n\n\t\t\t\t\t\t_vector$8.addVectors( this.boundingBox.min, _box$2.min );\n\t\t\t\t\t\tthis.boundingBox.expandByPoint( _vector$8 );\n\n\t\t\t\t\t\t_vector$8.addVectors( this.boundingBox.max, _box$2.max );\n\t\t\t\t\t\tthis.boundingBox.expandByPoint( _vector$8 );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tthis.boundingBox.expandByPoint( _box$2.min );\n\t\t\t\t\t\tthis.boundingBox.expandByPoint( _box$2.max );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tthis.boundingBox.makeEmpty();\n\n\t\t}\n\n\t\tif ( isNaN( this.boundingBox.min.x ) || isNaN( this.boundingBox.min.y ) || isNaN( this.boundingBox.min.z ) ) {\n\n\t\t\terror( 'BufferGeometry.computeBoundingBox(): Computed min/max have NaN values. The \"position\" attribute is likely to have NaN values.', this );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Computes the bounding sphere of the geometry, and updates the `boundingSphere` member.\n\t * The engine automatically computes the bounding sphere when it is needed, e.g., for ray casting or view frustum culling.\n\t * You may need to recompute the bounding sphere if the geometry vertices are modified.\n\t */\n\tcomputeBoundingSphere() {\n\n\t\tif ( this.boundingSphere === null ) {\n\n\t\t\tthis.boundingSphere = new Sphere();\n\n\t\t}\n\n\t\tconst position = this.attributes.position;\n\t\tconst morphAttributesPosition = this.morphAttributes.position;\n\n\t\tif ( position && position.isGLBufferAttribute ) {\n\n\t\t\terror( 'BufferGeometry.computeBoundingSphere(): GLBufferAttribute requires a manual bounding sphere.', this );\n\n\t\t\tthis.boundingSphere.set( new Vector3(), Infinity );\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( position ) {\n\n\t\t\t// first, find the center of the bounding sphere\n\n\t\t\tconst center = this.boundingSphere.center;\n\n\t\t\t_box$2.setFromBufferAttribute( position );\n\n\t\t\t// process morph attributes if present\n\n\t\t\tif ( morphAttributesPosition ) {\n\n\t\t\t\tfor ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst morphAttribute = morphAttributesPosition[ i ];\n\t\t\t\t\t_boxMorphTargets.setFromBufferAttribute( morphAttribute );\n\n\t\t\t\t\tif ( this.morphTargetsRelative ) {\n\n\t\t\t\t\t\t_vector$8.addVectors( _box$2.min, _boxMorphTargets.min );\n\t\t\t\t\t\t_box$2.expandByPoint( _vector$8 );\n\n\t\t\t\t\t\t_vector$8.addVectors( _box$2.max, _boxMorphTargets.max );\n\t\t\t\t\t\t_box$2.expandByPoint( _vector$8 );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_box$2.expandByPoint( _boxMorphTargets.min );\n\t\t\t\t\t\t_box$2.expandByPoint( _boxMorphTargets.max );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t_box$2.getCenter( center );\n\n\t\t\t// second, try to find a boundingSphere with a radius smaller than the\n\t\t\t// boundingSphere of the boundingBox: sqrt(3) smaller in the best case\n\n\t\t\tlet maxRadiusSq = 0;\n\n\t\t\tfor ( let i = 0, il = position.count; i < il; i ++ ) {\n\n\t\t\t\t_vector$8.fromBufferAttribute( position, i );\n\n\t\t\t\tmaxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector$8 ) );\n\n\t\t\t}\n\n\t\t\t// process morph attributes if present\n\n\t\t\tif ( morphAttributesPosition ) {\n\n\t\t\t\tfor ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst morphAttribute = morphAttributesPosition[ i ];\n\t\t\t\t\tconst morphTargetsRelative = this.morphTargetsRelative;\n\n\t\t\t\t\tfor ( let j = 0, jl = morphAttribute.count; j < jl; j ++ ) {\n\n\t\t\t\t\t\t_vector$8.fromBufferAttribute( morphAttribute, j );\n\n\t\t\t\t\t\tif ( morphTargetsRelative ) {\n\n\t\t\t\t\t\t\t_offset.fromBufferAttribute( position, j );\n\t\t\t\t\t\t\t_vector$8.add( _offset );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmaxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector$8 ) );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis.boundingSphere.radius = Math.sqrt( maxRadiusSq );\n\n\t\t\tif ( isNaN( this.boundingSphere.radius ) ) {\n\n\t\t\t\terror( 'BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The \"position\" attribute is likely to have NaN values.', this );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Calculates and adds a tangent attribute to this geometry.\n\t *\n\t * The computation is only supported for indexed geometries and if position, normal, and uv attributes\n\t * are defined. When using a tangent space normal map, prefer the MikkTSpace algorithm provided by\n\t * {@link BufferGeometryUtils#computeMikkTSpaceTangents} instead.\n\t */\n\tcomputeTangents() {\n\n\t\tconst index = this.index;\n\t\tconst attributes = this.attributes;\n\n\t\t// based on http://www.terathon.com/code/tangent.html\n\t\t// (per vertex tangents)\n\n\t\tif ( index === null ||\n\t\t\t attributes.position === undefined ||\n\t\t\t attributes.normal === undefined ||\n\t\t\t attributes.uv === undefined ) {\n\n\t\t\terror( 'BufferGeometry: .computeTangents() failed. Missing required attributes (index, position, normal or uv)' );\n\t\t\treturn;\n\n\t\t}\n\n\t\tconst positionAttribute = attributes.position;\n\t\tconst normalAttribute = attributes.normal;\n\t\tconst uvAttribute = attributes.uv;\n\n\t\tif ( this.hasAttribute( 'tangent' ) === false ) {\n\n\t\t\tthis.setAttribute( 'tangent', new BufferAttribute( new Float32Array( 4 * positionAttribute.count ), 4 ) );\n\n\t\t}\n\n\t\tconst tangentAttribute = this.getAttribute( 'tangent' );\n\n\t\tconst tan1 = [], tan2 = [];\n\n\t\tfor ( let i = 0; i < positionAttribute.count; i ++ ) {\n\n\t\t\ttan1[ i ] = new Vector3();\n\t\t\ttan2[ i ] = new Vector3();\n\n\t\t}\n\n\t\tconst vA = new Vector3(),\n\t\t\tvB = new Vector3(),\n\t\t\tvC = new Vector3(),\n\n\t\t\tuvA = new Vector2(),\n\t\t\tuvB = new Vector2(),\n\t\t\tuvC = new Vector2(),\n\n\t\t\tsdir = new Vector3(),\n\t\t\ttdir = new Vector3();\n\n\t\tfunction handleTriangle( a, b, c ) {\n\n\t\t\tvA.fromBufferAttribute( positionAttribute, a );\n\t\t\tvB.fromBufferAttribute( positionAttribute, b );\n\t\t\tvC.fromBufferAttribute( positionAttribute, c );\n\n\t\t\tuvA.fromBufferAttribute( uvAttribute, a );\n\t\t\tuvB.fromBufferAttribute( uvAttribute, b );\n\t\t\tuvC.fromBufferAttribute( uvAttribute, c );\n\n\t\t\tvB.sub( vA );\n\t\t\tvC.sub( vA );\n\n\t\t\tuvB.sub( uvA );\n\t\t\tuvC.sub( uvA );\n\n\t\t\tconst r = 1.0 / ( uvB.x * uvC.y - uvC.x * uvB.y );\n\n\t\t\t// silently ignore degenerate uv triangles having coincident or colinear vertices\n\n\t\t\tif ( ! isFinite( r ) ) return;\n\n\t\t\tsdir.copy( vB ).multiplyScalar( uvC.y ).addScaledVector( vC, - uvB.y ).multiplyScalar( r );\n\t\t\ttdir.copy( vC ).multiplyScalar( uvB.x ).addScaledVector( vB, - uvC.x ).multiplyScalar( r );\n\n\t\t\ttan1[ a ].add( sdir );\n\t\t\ttan1[ b ].add( sdir );\n\t\t\ttan1[ c ].add( sdir );\n\n\t\t\ttan2[ a ].add( tdir );\n\t\t\ttan2[ b ].add( tdir );\n\t\t\ttan2[ c ].add( tdir );\n\n\t\t}\n\n\t\tlet groups = this.groups;\n\n\t\tif ( groups.length === 0 ) {\n\n\t\t\tgroups = [ {\n\t\t\t\tstart: 0,\n\t\t\t\tcount: index.count\n\t\t\t} ];\n\n\t\t}\n\n\t\tfor ( let i = 0, il = groups.length; i < il; ++ i ) {\n\n\t\t\tconst group = groups[ i ];\n\n\t\t\tconst start = group.start;\n\t\t\tconst count = group.count;\n\n\t\t\tfor ( let j = start, jl = start + count; j < jl; j += 3 ) {\n\n\t\t\t\thandleTriangle(\n\t\t\t\t\tindex.getX( j + 0 ),\n\t\t\t\t\tindex.getX( j + 1 ),\n\t\t\t\t\tindex.getX( j + 2 )\n\t\t\t\t);\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst tmp = new Vector3(), tmp2 = new Vector3();\n\t\tconst n = new Vector3(), n2 = new Vector3();\n\n\t\tfunction handleVertex( v ) {\n\n\t\t\tn.fromBufferAttribute( normalAttribute, v );\n\t\t\tn2.copy( n );\n\n\t\t\tconst t = tan1[ v ];\n\n\t\t\t// Gram-Schmidt orthogonalize\n\n\t\t\ttmp.copy( t );\n\t\t\ttmp.sub( n.multiplyScalar( n.dot( t ) ) ).normalize();\n\n\t\t\t// Calculate handedness\n\n\t\t\ttmp2.crossVectors( n2, t );\n\t\t\tconst test = tmp2.dot( tan2[ v ] );\n\t\t\tconst w = ( test < 0.0 ) ? -1 : 1.0;\n\n\t\t\ttangentAttribute.setXYZW( v, tmp.x, tmp.y, tmp.z, w );\n\n\t\t}\n\n\t\tfor ( let i = 0, il = groups.length; i < il; ++ i ) {\n\n\t\t\tconst group = groups[ i ];\n\n\t\t\tconst start = group.start;\n\t\t\tconst count = group.count;\n\n\t\t\tfor ( let j = start, jl = start + count; j < jl; j += 3 ) {\n\n\t\t\t\thandleVertex( index.getX( j + 0 ) );\n\t\t\t\thandleVertex( index.getX( j + 1 ) );\n\t\t\t\thandleVertex( index.getX( j + 2 ) );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Computes vertex normals for the given vertex data. For indexed geometries, the method sets\n\t * each vertex normal to be the average of the face normals of the faces that share that vertex.\n\t * For non-indexed geometries, vertices are not shared, and the method sets each vertex normal\n\t * to be the same as the face normal.\n\t */\n\tcomputeVertexNormals() {\n\n\t\tconst index = this.index;\n\t\tconst positionAttribute = this.getAttribute( 'position' );\n\n\t\tif ( positionAttribute !== undefined ) {\n\n\t\t\tlet normalAttribute = this.getAttribute( 'normal' );\n\n\t\t\tif ( normalAttribute === undefined ) {\n\n\t\t\t\tnormalAttribute = new BufferAttribute( new Float32Array( positionAttribute.count * 3 ), 3 );\n\t\t\t\tthis.setAttribute( 'normal', normalAttribute );\n\n\t\t\t} else {\n\n\t\t\t\t// reset existing normals to zero\n\n\t\t\t\tfor ( let i = 0, il = normalAttribute.count; i < il; i ++ ) {\n\n\t\t\t\t\tnormalAttribute.setXYZ( i, 0, 0, 0 );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tconst pA = new Vector3(), pB = new Vector3(), pC = new Vector3();\n\t\t\tconst nA = new Vector3(), nB = new Vector3(), nC = new Vector3();\n\t\t\tconst cb = new Vector3(), ab = new Vector3();\n\n\t\t\t// indexed elements\n\n\t\t\tif ( index ) {\n\n\t\t\t\tfor ( let i = 0, il = index.count; i < il; i += 3 ) {\n\n\t\t\t\t\tconst vA = index.getX( i + 0 );\n\t\t\t\t\tconst vB = index.getX( i + 1 );\n\t\t\t\t\tconst vC = index.getX( i + 2 );\n\n\t\t\t\t\tpA.fromBufferAttribute( positionAttribute, vA );\n\t\t\t\t\tpB.fromBufferAttribute( positionAttribute, vB );\n\t\t\t\t\tpC.fromBufferAttribute( positionAttribute, vC );\n\n\t\t\t\t\tcb.subVectors( pC, pB );\n\t\t\t\t\tab.subVectors( pA, pB );\n\t\t\t\t\tcb.cross( ab );\n\n\t\t\t\t\tnA.fromBufferAttribute( normalAttribute, vA );\n\t\t\t\t\tnB.fromBufferAttribute( normalAttribute, vB );\n\t\t\t\t\tnC.fromBufferAttribute( normalAttribute, vC );\n\n\t\t\t\t\tnA.add( cb );\n\t\t\t\t\tnB.add( cb );\n\t\t\t\t\tnC.add( cb );\n\n\t\t\t\t\tnormalAttribute.setXYZ( vA, nA.x, nA.y, nA.z );\n\t\t\t\t\tnormalAttribute.setXYZ( vB, nB.x, nB.y, nB.z );\n\t\t\t\t\tnormalAttribute.setXYZ( vC, nC.x, nC.y, nC.z );\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\t// non-indexed elements (unconnected triangle soup)\n\n\t\t\t\tfor ( let i = 0, il = positionAttribute.count; i < il; i += 3 ) {\n\n\t\t\t\t\tpA.fromBufferAttribute( positionAttribute, i + 0 );\n\t\t\t\t\tpB.fromBufferAttribute( positionAttribute, i + 1 );\n\t\t\t\t\tpC.fromBufferAttribute( positionAttribute, i + 2 );\n\n\t\t\t\t\tcb.subVectors( pC, pB );\n\t\t\t\t\tab.subVectors( pA, pB );\n\t\t\t\t\tcb.cross( ab );\n\n\t\t\t\t\tnormalAttribute.setXYZ( i + 0, cb.x, cb.y, cb.z );\n\t\t\t\t\tnormalAttribute.setXYZ( i + 1, cb.x, cb.y, cb.z );\n\t\t\t\t\tnormalAttribute.setXYZ( i + 2, cb.x, cb.y, cb.z );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis.normalizeNormals();\n\n\t\t\tnormalAttribute.needsUpdate = true;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Ensures every normal vector in a geometry will have a magnitude of `1`. This will\n\t * correct lighting on the geometry surfaces.\n\t */\n\tnormalizeNormals() {\n\n\t\tconst normals = this.attributes.normal;\n\n\t\tfor ( let i = 0, il = normals.count; i < il; i ++ ) {\n\n\t\t\t_vector$8.fromBufferAttribute( normals, i );\n\n\t\t\t_vector$8.normalize();\n\n\t\t\tnormals.setXYZ( i, _vector$8.x, _vector$8.y, _vector$8.z );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Return a new non-index version of this indexed geometry. If the geometry\n\t * is already non-indexed, the method is a NOOP.\n\t *\n\t * @return {BufferGeometry} The non-indexed version of this indexed geometry.\n\t */\n\ttoNonIndexed() {\n\n\t\tfunction convertBufferAttribute( attribute, indices ) {\n\n\t\t\tconst array = attribute.array;\n\t\t\tconst itemSize = attribute.itemSize;\n\t\t\tconst normalized = attribute.normalized;\n\n\t\t\tconst array2 = new array.constructor( indices.length * itemSize );\n\n\t\t\tlet index = 0, index2 = 0;\n\n\t\t\tfor ( let i = 0, l = indices.length; i < l; i ++ ) {\n\n\t\t\t\tif ( attribute.isInterleavedBufferAttribute ) {\n\n\t\t\t\t\tindex = indices[ i ] * attribute.data.stride + attribute.offset;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tindex = indices[ i ] * itemSize;\n\n\t\t\t\t}\n\n\t\t\t\tfor ( let j = 0; j < itemSize; j ++ ) {\n\n\t\t\t\t\tarray2[ index2 ++ ] = array[ index ++ ];\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn new BufferAttribute( array2, itemSize, normalized );\n\n\t\t}\n\n\t\t//\n\n\t\tif ( this.index === null ) {\n\n\t\t\twarn( 'BufferGeometry.toNonIndexed(): BufferGeometry is already non-indexed.' );\n\t\t\treturn this;\n\n\t\t}\n\n\t\tconst geometry2 = new BufferGeometry();\n\n\t\tconst indices = this.index.array;\n\t\tconst attributes = this.attributes;\n\n\t\t// attributes\n\n\t\tfor ( const name in attributes ) {\n\n\t\t\tconst attribute = attributes[ name ];\n\n\t\t\tconst newAttribute = convertBufferAttribute( attribute, indices );\n\n\t\t\tgeometry2.setAttribute( name, newAttribute );\n\n\t\t}\n\n\t\t// morph attributes\n\n\t\tconst morphAttributes = this.morphAttributes;\n\n\t\tfor ( const name in morphAttributes ) {\n\n\t\t\tconst morphArray = [];\n\t\t\tconst morphAttribute = morphAttributes[ name ]; // morphAttribute: array of Float32BufferAttributes\n\n\t\t\tfor ( let i = 0, il = morphAttribute.length; i < il; i ++ ) {\n\n\t\t\t\tconst attribute = morphAttribute[ i ];\n\n\t\t\t\tconst newAttribute = convertBufferAttribute( attribute, indices );\n\n\t\t\t\tmorphArray.push( newAttribute );\n\n\t\t\t}\n\n\t\t\tgeometry2.morphAttributes[ name ] = morphArray;\n\n\t\t}\n\n\t\tgeometry2.morphTargetsRelative = this.morphTargetsRelative;\n\n\t\t// groups\n\n\t\tconst groups = this.groups;\n\n\t\tfor ( let i = 0, l = groups.length; i < l; i ++ ) {\n\n\t\t\tconst group = groups[ i ];\n\t\t\tgeometry2.addGroup( group.start, group.count, group.materialIndex );\n\n\t\t}\n\n\t\treturn geometry2;\n\n\t}\n\n\t/**\n\t * Serializes the geometry into JSON.\n\t *\n\t * @return {Object} A JSON object representing the serialized geometry.\n\t */\n\ttoJSON() {\n\n\t\tconst data = {\n\t\t\tmetadata: {\n\t\t\t\tversion: 4.7,\n\t\t\t\ttype: 'BufferGeometry',\n\t\t\t\tgenerator: 'BufferGeometry.toJSON'\n\t\t\t}\n\t\t};\n\n\t\t// standard BufferGeometry serialization\n\n\t\tdata.uuid = this.uuid;\n\t\tdata.type = this.type;\n\t\tif ( this.name !== '' ) data.name = this.name;\n\t\tif ( Object.keys( this.userData ).length > 0 ) data.userData = this.userData;\n\n\t\tif ( this.parameters !== undefined ) {\n\n\t\t\tconst parameters = this.parameters;\n\n\t\t\tfor ( const key in parameters ) {\n\n\t\t\t\tif ( parameters[ key ] !== undefined ) data[ key ] = parameters[ key ];\n\n\t\t\t}\n\n\t\t\treturn data;\n\n\t\t}\n\n\t\t// for simplicity the code assumes attributes are not shared across geometries, see #15811\n\n\t\tdata.data = { attributes: {} };\n\n\t\tconst index = this.index;\n\n\t\tif ( index !== null ) {\n\n\t\t\tdata.data.index = {\n\t\t\t\ttype: index.array.constructor.name,\n\t\t\t\tarray: Array.prototype.slice.call( index.array )\n\t\t\t};\n\n\t\t}\n\n\t\tconst attributes = this.attributes;\n\n\t\tfor ( const key in attributes ) {\n\n\t\t\tconst attribute = attributes[ key ];\n\n\t\t\tdata.data.attributes[ key ] = attribute.toJSON( data.data );\n\n\t\t}\n\n\t\tconst morphAttributes = {};\n\t\tlet hasMorphAttributes = false;\n\n\t\tfor ( const key in this.morphAttributes ) {\n\n\t\t\tconst attributeArray = this.morphAttributes[ key ];\n\n\t\t\tconst array = [];\n\n\t\t\tfor ( let i = 0, il = attributeArray.length; i < il; i ++ ) {\n\n\t\t\t\tconst attribute = attributeArray[ i ];\n\n\t\t\t\tarray.push( attribute.toJSON( data.data ) );\n\n\t\t\t}\n\n\t\t\tif ( array.length > 0 ) {\n\n\t\t\t\tmorphAttributes[ key ] = array;\n\n\t\t\t\thasMorphAttributes = true;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( hasMorphAttributes ) {\n\n\t\t\tdata.data.morphAttributes = morphAttributes;\n\t\t\tdata.data.morphTargetsRelative = this.morphTargetsRelative;\n\n\t\t}\n\n\t\tconst groups = this.groups;\n\n\t\tif ( groups.length > 0 ) {\n\n\t\t\tdata.data.groups = JSON.parse( JSON.stringify( groups ) );\n\n\t\t}\n\n\t\tconst boundingSphere = this.boundingSphere;\n\n\t\tif ( boundingSphere !== null ) {\n\n\t\t\tdata.data.boundingSphere = boundingSphere.toJSON();\n\n\t\t}\n\n\t\treturn data;\n\n\t}\n\n\t/**\n\t * Returns a new geometry with copied values from this instance.\n\t *\n\t * @return {BufferGeometry} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Copies the values of the given geometry to this instance.\n\t *\n\t * @param {BufferGeometry} source - The geometry to copy.\n\t * @return {BufferGeometry} A reference to this instance.\n\t */\n\tcopy( source ) {\n\n\t\t// reset\n\n\t\tthis.index = null;\n\t\tthis.attributes = {};\n\t\tthis.morphAttributes = {};\n\t\tthis.groups = [];\n\t\tthis.boundingBox = null;\n\t\tthis.boundingSphere = null;\n\n\t\t// used for storing cloned, shared data\n\n\t\tconst data = {};\n\n\t\t// name\n\n\t\tthis.name = source.name;\n\n\t\t// index\n\n\t\tconst index = source.index;\n\n\t\tif ( index !== null ) {\n\n\t\t\tthis.setIndex( index.clone() );\n\n\t\t}\n\n\t\t// attributes\n\n\t\tconst attributes = source.attributes;\n\n\t\tfor ( const name in attributes ) {\n\n\t\t\tconst attribute = attributes[ name ];\n\t\t\tthis.setAttribute( name, attribute.clone( data ) );\n\n\t\t}\n\n\t\t// morph attributes\n\n\t\tconst morphAttributes = source.morphAttributes;\n\n\t\tfor ( const name in morphAttributes ) {\n\n\t\t\tconst array = [];\n\t\t\tconst morphAttribute = morphAttributes[ name ]; // morphAttribute: array of Float32BufferAttributes\n\n\t\t\tfor ( let i = 0, l = morphAttribute.length; i < l; i ++ ) {\n\n\t\t\t\tarray.push( morphAttribute[ i ].clone( data ) );\n\n\t\t\t}\n\n\t\t\tthis.morphAttributes[ name ] = array;\n\n\t\t}\n\n\t\tthis.morphTargetsRelative = source.morphTargetsRelative;\n\n\t\t// groups\n\n\t\tconst groups = source.groups;\n\n\t\tfor ( let i = 0, l = groups.length; i < l; i ++ ) {\n\n\t\t\tconst group = groups[ i ];\n\t\t\tthis.addGroup( group.start, group.count, group.materialIndex );\n\n\t\t}\n\n\t\t// bounding box\n\n\t\tconst boundingBox = source.boundingBox;\n\n\t\tif ( boundingBox !== null ) {\n\n\t\t\tthis.boundingBox = boundingBox.clone();\n\n\t\t}\n\n\t\t// bounding sphere\n\n\t\tconst boundingSphere = source.boundingSphere;\n\n\t\tif ( boundingSphere !== null ) {\n\n\t\t\tthis.boundingSphere = boundingSphere.clone();\n\n\t\t}\n\n\t\t// draw range\n\n\t\tthis.drawRange.start = source.drawRange.start;\n\t\tthis.drawRange.count = source.drawRange.count;\n\n\t\t// user data\n\n\t\tthis.userData = source.userData;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t *\n\t * @fires BufferGeometry#dispose\n\t */\n\tdispose() {\n\n\t\tthis.dispatchEvent( { type: 'dispose' } );\n\n\t}\n\n}\n\nconst _inverseMatrix$3 = /*@__PURE__*/ new Matrix4();\nconst _ray$3 = /*@__PURE__*/ new Ray();\nconst _sphere$6 = /*@__PURE__*/ new Sphere();\nconst _sphereHitAt = /*@__PURE__*/ new Vector3();\n\nconst _vA$1 = /*@__PURE__*/ new Vector3();\nconst _vB$1 = /*@__PURE__*/ new Vector3();\nconst _vC$1 = /*@__PURE__*/ new Vector3();\n\nconst _tempA = /*@__PURE__*/ new Vector3();\nconst _morphA = /*@__PURE__*/ new Vector3();\n\nconst _intersectionPoint = /*@__PURE__*/ new Vector3();\nconst _intersectionPointWorld = /*@__PURE__*/ new Vector3();\n\n/**\n * Class representing triangular polygon mesh based objects.\n *\n * ```js\n * const geometry = new THREE.BoxGeometry( 1, 1, 1 );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const mesh = new THREE.Mesh( geometry, material );\n * scene.add( mesh );\n * ```\n *\n * @augments Object3D\n */\nclass Mesh extends Object3D {\n\n\t/**\n\t * Constructs a new mesh.\n\t *\n\t * @param {BufferGeometry} [geometry] - The mesh geometry.\n\t * @param {Material|Array<Material>} [material] - The mesh material.\n\t */\n\tconstructor( geometry = new BufferGeometry(), material = new MeshBasicMaterial() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMesh = true;\n\n\t\tthis.type = 'Mesh';\n\n\t\t/**\n\t\t * The mesh geometry.\n\t\t *\n\t\t * @type {BufferGeometry}\n\t\t */\n\t\tthis.geometry = geometry;\n\n\t\t/**\n\t\t * The mesh material.\n\t\t *\n\t\t * @type {Material|Array<Material>}\n\t\t * @default MeshBasicMaterial\n\t\t */\n\t\tthis.material = material;\n\n\t\t/**\n\t\t * A dictionary representing the morph targets in the geometry. The key is the\n\t\t * morph targets name, the value its attribute index. This member is `undefined`\n\t\t * by default and only set when morph targets are detected in the geometry.\n\t\t *\n\t\t * @type {Object<string,number>|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.morphTargetDictionary = undefined;\n\n\t\t/**\n\t\t * An array of weights typically in the range `[0,1]` that specify how much of the morph\n\t\t * is applied. This member is `undefined` by default and only set when morph targets are\n\t\t * detected in the geometry.\n\t\t *\n\t\t * @type {Array<number>|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.morphTargetInfluences = undefined;\n\n\t\t/**\n\t\t * The number of instances of this mesh.\n\t\t * Can only be used with {@link WebGPURenderer}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.count = 1;\n\n\t\tthis.updateMorphTargets();\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tif ( source.morphTargetInfluences !== undefined ) {\n\n\t\t\tthis.morphTargetInfluences = source.morphTargetInfluences.slice();\n\n\t\t}\n\n\t\tif ( source.morphTargetDictionary !== undefined ) {\n\n\t\t\tthis.morphTargetDictionary = Object.assign( {}, source.morphTargetDictionary );\n\n\t\t}\n\n\t\tthis.material = Array.isArray( source.material ) ? source.material.slice() : source.material;\n\t\tthis.geometry = source.geometry;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the values of {@link Mesh#morphTargetDictionary} and {@link Mesh#morphTargetInfluences}\n\t * to make sure existing morph targets can influence this 3D object.\n\t */\n\tupdateMorphTargets() {\n\n\t\tconst geometry = this.geometry;\n\n\t\tconst morphAttributes = geometry.morphAttributes;\n\t\tconst keys = Object.keys( morphAttributes );\n\n\t\tif ( keys.length > 0 ) {\n\n\t\t\tconst morphAttribute = morphAttributes[ keys[ 0 ] ];\n\n\t\t\tif ( morphAttribute !== undefined ) {\n\n\t\t\t\tthis.morphTargetInfluences = [];\n\t\t\t\tthis.morphTargetDictionary = {};\n\n\t\t\t\tfor ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {\n\n\t\t\t\t\tconst name = morphAttribute[ m ].name || String( m );\n\n\t\t\t\t\tthis.morphTargetInfluences.push( 0 );\n\t\t\t\t\tthis.morphTargetDictionary[ name ] = m;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns the local-space position of the vertex at the given index, taking into\n\t * account the current animation state of both morph targets and skinning.\n\t *\n\t * @param {number} index - The vertex index.\n\t * @param {Vector3} target - The target object that is used to store the method's result.\n\t * @return {Vector3} The vertex position in local space.\n\t */\n\tgetVertexPosition( index, target ) {\n\n\t\tconst geometry = this.geometry;\n\t\tconst position = geometry.attributes.position;\n\t\tconst morphPosition = geometry.morphAttributes.position;\n\t\tconst morphTargetsRelative = geometry.morphTargetsRelative;\n\n\t\ttarget.fromBufferAttribute( position, index );\n\n\t\tconst morphInfluences = this.morphTargetInfluences;\n\n\t\tif ( morphPosition && morphInfluences ) {\n\n\t\t\t_morphA.set( 0, 0, 0 );\n\n\t\t\tfor ( let i = 0, il = morphPosition.length; i < il; i ++ ) {\n\n\t\t\t\tconst influence = morphInfluences[ i ];\n\t\t\t\tconst morphAttribute = morphPosition[ i ];\n\n\t\t\t\tif ( influence === 0 ) continue;\n\n\t\t\t\t_tempA.fromBufferAttribute( morphAttribute, index );\n\n\t\t\t\tif ( morphTargetsRelative ) {\n\n\t\t\t\t\t_morphA.addScaledVector( _tempA, influence );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t_morphA.addScaledVector( _tempA.sub( target ), influence );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\ttarget.add( _morphA );\n\n\t\t}\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Computes intersection points between a casted ray and this line.\n\t *\n\t * @param {Raycaster} raycaster - The raycaster.\n\t * @param {Array<Object>} intersects - The target array that holds the intersection points.\n\t */\n\traycast( raycaster, intersects ) {\n\n\t\tconst geometry = this.geometry;\n\t\tconst material = this.material;\n\t\tconst matrixWorld = this.matrixWorld;\n\n\t\tif ( material === undefined ) return;\n\n\t\t// test with bounding sphere in world space\n\n\t\tif ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();\n\n\t\t_sphere$6.copy( geometry.boundingSphere );\n\t\t_sphere$6.applyMatrix4( matrixWorld );\n\n\t\t// check distance from ray origin to bounding sphere\n\n\t\t_ray$3.copy( raycaster.ray ).recast( raycaster.near );\n\n\t\tif ( _sphere$6.containsPoint( _ray$3.origin ) === false ) {\n\n\t\t\tif ( _ray$3.intersectSphere( _sphere$6, _sphereHitAt ) === null ) return;\n\n\t\t\tif ( _ray$3.origin.distanceToSquared( _sphereHitAt ) > ( raycaster.far - raycaster.near ) ** 2 ) return;\n\n\t\t}\n\n\t\t// convert ray to local space of mesh\n\n\t\t_inverseMatrix$3.copy( matrixWorld ).invert();\n\t\t_ray$3.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$3 );\n\n\t\t// test with bounding box in local space\n\n\t\tif ( geometry.boundingBox !== null ) {\n\n\t\t\tif ( _ray$3.intersectsBox( geometry.boundingBox ) === false ) return;\n\n\t\t}\n\n\t\t// test for intersections with geometry\n\n\t\tthis._computeIntersections( raycaster, intersects, _ray$3 );\n\n\t}\n\n\t_computeIntersections( raycaster, intersects, rayLocalSpace ) {\n\n\t\tlet intersection;\n\n\t\tconst geometry = this.geometry;\n\t\tconst material = this.material;\n\n\t\tconst index = geometry.index;\n\t\tconst position = geometry.attributes.position;\n\t\tconst uv = geometry.attributes.uv;\n\t\tconst uv1 = geometry.attributes.uv1;\n\t\tconst normal = geometry.attributes.normal;\n\t\tconst groups = geometry.groups;\n\t\tconst drawRange = geometry.drawRange;\n\n\t\tif ( index !== null ) {\n\n\t\t\t// indexed buffer geometry\n\n\t\t\tif ( Array.isArray( material ) ) {\n\n\t\t\t\tfor ( let i = 0, il = groups.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst group = groups[ i ];\n\t\t\t\t\tconst groupMaterial = material[ group.materialIndex ];\n\n\t\t\t\t\tconst start = Math.max( group.start, drawRange.start );\n\t\t\t\t\tconst end = Math.min( index.count, Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ) );\n\n\t\t\t\t\tfor ( let j = start, jl = end; j < jl; j += 3 ) {\n\n\t\t\t\t\t\tconst a = index.getX( j );\n\t\t\t\t\t\tconst b = index.getX( j + 1 );\n\t\t\t\t\t\tconst c = index.getX( j + 2 );\n\n\t\t\t\t\t\tintersection = checkGeometryIntersection( this, groupMaterial, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );\n\n\t\t\t\t\t\tif ( intersection ) {\n\n\t\t\t\t\t\t\tintersection.faceIndex = Math.floor( j / 3 ); // triangle number in indexed buffer semantics\n\t\t\t\t\t\t\tintersection.face.materialIndex = group.materialIndex;\n\t\t\t\t\t\t\tintersects.push( intersection );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tconst start = Math.max( 0, drawRange.start );\n\t\t\t\tconst end = Math.min( index.count, ( drawRange.start + drawRange.count ) );\n\n\t\t\t\tfor ( let i = start, il = end; i < il; i += 3 ) {\n\n\t\t\t\t\tconst a = index.getX( i );\n\t\t\t\t\tconst b = index.getX( i + 1 );\n\t\t\t\t\tconst c = index.getX( i + 2 );\n\n\t\t\t\t\tintersection = checkGeometryIntersection( this, material, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );\n\n\t\t\t\t\tif ( intersection ) {\n\n\t\t\t\t\t\tintersection.faceIndex = Math.floor( i / 3 ); // triangle number in indexed buffer semantics\n\t\t\t\t\t\tintersects.push( intersection );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else if ( position !== undefined ) {\n\n\t\t\t// non-indexed buffer geometry\n\n\t\t\tif ( Array.isArray( material ) ) {\n\n\t\t\t\tfor ( let i = 0, il = groups.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst group = groups[ i ];\n\t\t\t\t\tconst groupMaterial = material[ group.materialIndex ];\n\n\t\t\t\t\tconst start = Math.max( group.start, drawRange.start );\n\t\t\t\t\tconst end = Math.min( position.count, Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ) );\n\n\t\t\t\t\tfor ( let j = start, jl = end; j < jl; j += 3 ) {\n\n\t\t\t\t\t\tconst a = j;\n\t\t\t\t\t\tconst b = j + 1;\n\t\t\t\t\t\tconst c = j + 2;\n\n\t\t\t\t\t\tintersection = checkGeometryIntersection( this, groupMaterial, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );\n\n\t\t\t\t\t\tif ( intersection ) {\n\n\t\t\t\t\t\t\tintersection.faceIndex = Math.floor( j / 3 ); // triangle number in non-indexed buffer semantics\n\t\t\t\t\t\t\tintersection.face.materialIndex = group.materialIndex;\n\t\t\t\t\t\t\tintersects.push( intersection );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tconst start = Math.max( 0, drawRange.start );\n\t\t\t\tconst end = Math.min( position.count, ( drawRange.start + drawRange.count ) );\n\n\t\t\t\tfor ( let i = start, il = end; i < il; i += 3 ) {\n\n\t\t\t\t\tconst a = i;\n\t\t\t\t\tconst b = i + 1;\n\t\t\t\t\tconst c = i + 2;\n\n\t\t\t\t\tintersection = checkGeometryIntersection( this, material, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );\n\n\t\t\t\t\tif ( intersection ) {\n\n\t\t\t\t\t\tintersection.faceIndex = Math.floor( i / 3 ); // triangle number in non-indexed buffer semantics\n\t\t\t\t\t\tintersects.push( intersection );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}\n\nfunction checkIntersection$1( object, material, raycaster, ray, pA, pB, pC, point ) {\n\n\tlet intersect;\n\n\tif ( material.side === BackSide ) {\n\n\t\tintersect = ray.intersectTriangle( pC, pB, pA, true, point );\n\n\t} else {\n\n\t\tintersect = ray.intersectTriangle( pA, pB, pC, ( material.side === FrontSide ), point );\n\n\t}\n\n\tif ( intersect === null ) return null;\n\n\t_intersectionPointWorld.copy( point );\n\t_intersectionPointWorld.applyMatrix4( object.matrixWorld );\n\n\tconst distance = raycaster.ray.origin.distanceTo( _intersectionPointWorld );\n\n\tif ( distance < raycaster.near || distance > raycaster.far ) return null;\n\n\treturn {\n\t\tdistance: distance,\n\t\tpoint: _intersectionPointWorld.clone(),\n\t\tobject: object\n\t};\n\n}\n\nfunction checkGeometryIntersection( object, material, raycaster, ray, uv, uv1, normal, a, b, c ) {\n\n\tobject.getVertexPosition( a, _vA$1 );\n\tobject.getVertexPosition( b, _vB$1 );\n\tobject.getVertexPosition( c, _vC$1 );\n\n\tconst intersection = checkIntersection$1( object, material, raycaster, ray, _vA$1, _vB$1, _vC$1, _intersectionPoint );\n\n\tif ( intersection ) {\n\n\t\tconst barycoord = new Vector3();\n\t\tTriangle.getBarycoord( _intersectionPoint, _vA$1, _vB$1, _vC$1, barycoord );\n\n\t\tif ( uv ) {\n\n\t\t\tintersection.uv = Triangle.getInterpolatedAttribute( uv, a, b, c, barycoord, new Vector2() );\n\n\t\t}\n\n\t\tif ( uv1 ) {\n\n\t\t\tintersection.uv1 = Triangle.getInterpolatedAttribute( uv1, a, b, c, barycoord, new Vector2() );\n\n\t\t}\n\n\t\tif ( normal ) {\n\n\t\t\tintersection.normal = Triangle.getInterpolatedAttribute( normal, a, b, c, barycoord, new Vector3() );\n\n\t\t\tif ( intersection.normal.dot( ray.direction ) > 0 ) {\n\n\t\t\t\tintersection.normal.multiplyScalar( -1 );\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst face = {\n\t\t\ta: a,\n\t\t\tb: b,\n\t\t\tc: c,\n\t\t\tnormal: new Vector3(),\n\t\t\tmaterialIndex: 0\n\t\t};\n\n\t\tTriangle.getNormal( _vA$1, _vB$1, _vC$1, face.normal );\n\n\t\tintersection.face = face;\n\t\tintersection.barycoord = barycoord;\n\n\t}\n\n\treturn intersection;\n\n}\n\n/**\n * A geometry class for a rectangular cuboid with a given width, height, and depth.\n * On creation, the cuboid is centred on the origin, with each edge parallel to one\n * of the axes.\n *\n * ```js\n * const geometry = new THREE.BoxGeometry( 1, 1, 1 );\n * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );\n * const cube = new THREE.Mesh( geometry, material );\n * scene.add( cube );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#BoxGeometry\n */\nclass BoxGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new box geometry.\n\t *\n\t * @param {number} [width=1] - The width. That is, the length of the edges parallel to the X axis.\n\t * @param {number} [height=1] - The height. That is, the length of the edges parallel to the Y axis.\n\t * @param {number} [depth=1] - The depth. That is, the length of the edges parallel to the Z axis.\n\t * @param {number} [widthSegments=1] - Number of segmented rectangular faces along the width of the sides.\n\t * @param {number} [heightSegments=1] - Number of segmented rectangular faces along the height of the sides.\n\t * @param {number} [depthSegments=1] - Number of segmented rectangular faces along the depth of the sides.\n\t */\n\tconstructor( width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'BoxGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\twidth: width,\n\t\t\theight: height,\n\t\t\tdepth: depth,\n\t\t\twidthSegments: widthSegments,\n\t\t\theightSegments: heightSegments,\n\t\t\tdepthSegments: depthSegments\n\t\t};\n\n\t\tconst scope = this;\n\n\t\t// segments\n\n\t\twidthSegments = Math.floor( widthSegments );\n\t\theightSegments = Math.floor( heightSegments );\n\t\tdepthSegments = Math.floor( depthSegments );\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\t// helper variables\n\n\t\tlet numberOfVertices = 0;\n\t\tlet groupStart = 0;\n\n\t\t// build each side of the box geometry\n\n\t\tbuildPlane( 'z', 'y', 'x', -1, -1, depth, height, width, depthSegments, heightSegments, 0 ); // px\n\t\tbuildPlane( 'z', 'y', 'x', 1, -1, depth, height, - width, depthSegments, heightSegments, 1 ); // nx\n\t\tbuildPlane( 'x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2 ); // py\n\t\tbuildPlane( 'x', 'z', 'y', 1, -1, width, depth, - height, widthSegments, depthSegments, 3 ); // ny\n\t\tbuildPlane( 'x', 'y', 'z', 1, -1, width, height, depth, widthSegments, heightSegments, 4 ); // pz\n\t\tbuildPlane( 'x', 'y', 'z', -1, -1, width, height, - depth, widthSegments, heightSegments, 5 ); // nz\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t\tfunction buildPlane( u, v, w, udir, vdir, width, height, depth, gridX, gridY, materialIndex ) {\n\n\t\t\tconst segmentWidth = width / gridX;\n\t\t\tconst segmentHeight = height / gridY;\n\n\t\t\tconst widthHalf = width / 2;\n\t\t\tconst heightHalf = height / 2;\n\t\t\tconst depthHalf = depth / 2;\n\n\t\t\tconst gridX1 = gridX + 1;\n\t\t\tconst gridY1 = gridY + 1;\n\n\t\t\tlet vertexCounter = 0;\n\t\t\tlet groupCount = 0;\n\n\t\t\tconst vector = new Vector3();\n\n\t\t\t// generate vertices, normals and uvs\n\n\t\t\tfor ( let iy = 0; iy < gridY1; iy ++ ) {\n\n\t\t\t\tconst y = iy * segmentHeight - heightHalf;\n\n\t\t\t\tfor ( let ix = 0; ix < gridX1; ix ++ ) {\n\n\t\t\t\t\tconst x = ix * segmentWidth - widthHalf;\n\n\t\t\t\t\t// set values to correct vector component\n\n\t\t\t\t\tvector[ u ] = x * udir;\n\t\t\t\t\tvector[ v ] = y * vdir;\n\t\t\t\t\tvector[ w ] = depthHalf;\n\n\t\t\t\t\t// now apply vector to vertex buffer\n\n\t\t\t\t\tvertices.push( vector.x, vector.y, vector.z );\n\n\t\t\t\t\t// set values to correct vector component\n\n\t\t\t\t\tvector[ u ] = 0;\n\t\t\t\t\tvector[ v ] = 0;\n\t\t\t\t\tvector[ w ] = depth > 0 ? 1 : -1;\n\n\t\t\t\t\t// now apply vector to normal buffer\n\n\t\t\t\t\tnormals.push( vector.x, vector.y, vector.z );\n\n\t\t\t\t\t// uvs\n\n\t\t\t\t\tuvs.push( ix / gridX );\n\t\t\t\t\tuvs.push( 1 - ( iy / gridY ) );\n\n\t\t\t\t\t// counters\n\n\t\t\t\t\tvertexCounter += 1;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// indices\n\n\t\t\t// 1. you need three indices to draw a single face\n\t\t\t// 2. a single segment consists of two faces\n\t\t\t// 3. so we need to generate six (2*3) indices per segment\n\n\t\t\tfor ( let iy = 0; iy < gridY; iy ++ ) {\n\n\t\t\t\tfor ( let ix = 0; ix < gridX; ix ++ ) {\n\n\t\t\t\t\tconst a = numberOfVertices + ix + gridX1 * iy;\n\t\t\t\t\tconst b = numberOfVertices + ix + gridX1 * ( iy + 1 );\n\t\t\t\t\tconst c = numberOfVertices + ( ix + 1 ) + gridX1 * ( iy + 1 );\n\t\t\t\t\tconst d = numberOfVertices + ( ix + 1 ) + gridX1 * iy;\n\n\t\t\t\t\t// faces\n\n\t\t\t\t\tindices.push( a, b, d );\n\t\t\t\t\tindices.push( b, c, d );\n\n\t\t\t\t\t// increase counter\n\n\t\t\t\t\tgroupCount += 6;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// add a group to the geometry. this will ensure multi material support\n\n\t\t\tscope.addGroup( groupStart, groupCount, materialIndex );\n\n\t\t\t// calculate new start value for groups\n\n\t\t\tgroupStart += groupCount;\n\n\t\t\t// update total number of vertices\n\n\t\t\tnumberOfVertices += vertexCounter;\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {BoxGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new BoxGeometry( data.width, data.height, data.depth, data.widthSegments, data.heightSegments, data.depthSegments );\n\n\t}\n\n}\n\n/**\n * Provides utility functions for managing uniforms.\n *\n * @module UniformsUtils\n */\n\n/**\n * Clones the given uniform definitions by performing a deep-copy. That means\n * if the value of a uniform refers to an object like a Vector3 or Texture,\n * the cloned uniform will refer to a new object reference.\n *\n * @param {Object} src - An object representing uniform definitions.\n * @return {Object} The cloned uniforms.\n */\nfunction cloneUniforms( src ) {\n\n\tconst dst = {};\n\n\tfor ( const u in src ) {\n\n\t\tdst[ u ] = {};\n\n\t\tfor ( const p in src[ u ] ) {\n\n\t\t\tconst property = src[ u ][ p ];\n\n\t\t\tif ( property && ( property.isColor ||\n\t\t\t\tproperty.isMatrix3 || property.isMatrix4 ||\n\t\t\t\tproperty.isVector2 || property.isVector3 || property.isVector4 ||\n\t\t\t\tproperty.isTexture || property.isQuaternion ) ) {\n\n\t\t\t\tif ( property.isRenderTargetTexture ) {\n\n\t\t\t\t\twarn( 'UniformsUtils: Textures of render targets cannot be cloned via cloneUniforms() or mergeUniforms().' );\n\t\t\t\t\tdst[ u ][ p ] = null;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tdst[ u ][ p ] = property.clone();\n\n\t\t\t\t}\n\n\t\t\t} else if ( Array.isArray( property ) ) {\n\n\t\t\t\tdst[ u ][ p ] = property.slice();\n\n\t\t\t} else {\n\n\t\t\t\tdst[ u ][ p ] = property;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn dst;\n\n}\n\n/**\n * Merges the given uniform definitions into a single object. Since the\n * method internally uses cloneUniforms(), it performs a deep-copy when\n * producing the merged uniform definitions.\n *\n * @param {Array} uniforms - An array of objects containing uniform definitions.\n * @return {Object} The merged uniforms.\n */\nfunction mergeUniforms( uniforms ) {\n\n\tconst merged = {};\n\n\tfor ( let u = 0; u < uniforms.length; u ++ ) {\n\n\t\tconst tmp = cloneUniforms( uniforms[ u ] );\n\n\t\tfor ( const p in tmp ) {\n\n\t\t\tmerged[ p ] = tmp[ p ];\n\n\t\t}\n\n\t}\n\n\treturn merged;\n\n}\n\nfunction cloneUniformsGroups( src ) {\n\n\tconst dst = [];\n\n\tfor ( let u = 0; u < src.length; u ++ ) {\n\n\t\tdst.push( src[ u ].clone() );\n\n\t}\n\n\treturn dst;\n\n}\n\nfunction getUnlitUniformColorSpace( renderer ) {\n\n\tconst currentRenderTarget = renderer.getRenderTarget();\n\n\tif ( currentRenderTarget === null ) {\n\n\t\t// https://github.com/mrdoob/three.js/pull/23937#issuecomment-1111067398\n\t\treturn renderer.outputColorSpace;\n\n\t}\n\n\t// https://github.com/mrdoob/three.js/issues/27868\n\tif ( currentRenderTarget.isXRRenderTarget === true ) {\n\n\t\treturn currentRenderTarget.texture.colorSpace;\n\n\t}\n\n\treturn ColorManagement.workingColorSpace;\n\n}\n\n// Legacy\n\nconst UniformsUtils = { clone: cloneUniforms, merge: mergeUniforms };\n\nvar default_vertex = \"void main() {\\n\\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\\n}\";\n\nvar default_fragment = \"void main() {\\n\\tgl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 );\\n}\";\n\n/**\n * A material rendered with custom shaders. A shader is a small program written in GLSL.\n * that runs on the GPU. You may want to use a custom shader if you need to implement an\n * effect not included with any of the built-in materials.\n *\n * There are the following notes to bear in mind when using a `ShaderMaterial`:\n *\n * - `ShaderMaterial` can only be used with {@link WebGLRenderer}.\n * - Built in attributes and uniforms are passed to the shaders along with your code. If\n * you don't want that, use {@link RawShaderMaterial} instead.\n * - You can use the directive `#pragma unroll_loop_start` and `#pragma unroll_loop_end`\n * in order to unroll a `for` loop in GLSL by the shader preprocessor. The directive has\n * to be placed right above the loop. The loop formatting has to correspond to a defined standard.\n *   - The loop has to be [normalized](https://en.wikipedia.org/wiki/Normalized_loop).\n *   - The loop variable has to be *i*.\n *   - The value `UNROLLED_LOOP_INDEX` will be replaced with the explicitly\n * value of *i* for the given iteration and can be used in preprocessor\n * statements.\n *\n * ```js\n * const material = new THREE.ShaderMaterial( {\n * \tuniforms: {\n * \t\ttime: { value: 1.0 },\n * \t\tresolution: { value: new THREE.Vector2() }\n * \t},\n * \tvertexShader: document.getElementById( 'vertexShader' ).textContent,\n * \tfragmentShader: document.getElementById( 'fragmentShader' ).textContent\n * } );\n * ```\n *\n * @augments Material\n */\nclass ShaderMaterial extends Material {\n\n\t/**\n\t * Constructs a new shader material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isShaderMaterial = true;\n\n\t\tthis.type = 'ShaderMaterial';\n\n\t\t/**\n\t\t * Defines custom constants using `#define` directives within the GLSL code\n\t\t * for both the vertex shader and the fragment shader; each key/value pair\n\t\t * yields another directive.\n\t\t * ```js\n\t\t * defines: {\n\t\t * \tFOO: 15,\n\t\t * \tBAR: true\n\t\t * }\n\t\t * ```\n\t\t * Yields the lines:\n\t\t * ```\n\t\t * #define FOO 15\n\t\t * #define BAR true\n\t\t * ```\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.defines = {};\n\n\t\t/**\n\t\t * An object of the form:\n\t\t * ```js\n\t\t * {\n\t\t * \t\"uniform1\": { value: 1.0 },\n\t\t * \t\"uniform2\": { value: 2 }\n\t\t * }\n\t\t * ```\n\t\t * specifying the uniforms to be passed to the shader code; keys are uniform\n\t\t * names, values are definitions of the form\n\t\t * ```\n\t\t * {\n\t\t * \tvalue: 1.0\n\t\t * }\n\t\t * ```\n\t\t * where `value` is the value of the uniform. Names must match the name of\n\t\t * the uniform, as defined in the GLSL code. Note that uniforms are refreshed\n\t\t * on every frame, so updating the value of the uniform will immediately\n\t\t * update the value available to the GLSL code.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.uniforms = {};\n\n\t\t/**\n\t\t * An array holding uniforms groups for configuring UBOs.\n\t\t *\n\t\t * @type {Array<UniformsGroup>}\n\t\t */\n\t\tthis.uniformsGroups = [];\n\n\t\t/**\n\t\t * Vertex shader GLSL code. This is the actual code for the shader.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.vertexShader = default_vertex;\n\n\t\t/**\n\t\t * Fragment shader GLSL code. This is the actual code for the shader.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.fragmentShader = default_fragment;\n\n\t\t/**\n\t\t * Controls line thickness or lines.\n\t\t *\n\t\t * WebGL and WebGPU ignore this setting and always render line primitives with a\n\t\t * width of one pixel.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.linewidth = 1;\n\n\t\t/**\n\t\t * Renders the geometry as a wireframe.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.wireframe = false;\n\n\t\t/**\n\t\t * Controls the thickness of the wireframe.\n\t\t *\n\t\t * WebGL and WebGPU ignore this property and always render\n\t\t * 1 pixel wide lines.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.wireframeLinewidth = 1;\n\n\t\t/**\n\t\t * Defines whether the material color is affected by global fog settings; `true`\n\t\t * to pass fog uniforms to the shader.\n\t\t *\n\t\t * Setting this property to `true` requires the definition of fog uniforms. It is\n\t\t * recommended to use `UniformsUtils.merge()` to combine the custom shader uniforms\n\t\t * with predefined fog uniforms.\n\t\t *\n\t\t * ```js\n\t\t * const material = new ShaderMaterial( {\n\t\t *     uniforms: UniformsUtils.merge( [ UniformsLib[ 'fog' ], shaderUniforms ] );\n\t\t *     vertexShader: vertexShader,\n\t\t *     fragmentShader: fragmentShader,\n\t\t *     fog: true\n\t\t * } );\n\t\t * ```\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.fog = false;\n\n\t\t/**\n\t\t * Defines whether this material uses lighting; `true` to pass uniform data\n\t\t * related to lighting to this shader.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.lights = false;\n\n\t\t/**\n\t\t * Defines whether this material supports clipping; `true` to let the renderer\n\t\t * pass the clippingPlanes uniform.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.clipping = false;\n\n\t\t/**\n\t\t * Overwritten and set to `true` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.forceSinglePass = true;\n\n\t\t/**\n\t\t * This object allows to enable certain WebGL 2 extensions.\n\t\t *\n\t\t * - clipCullDistance: set to `true` to use vertex shader clipping\n\t\t * - multiDraw: set to `true` to use vertex shader multi_draw / enable gl_DrawID\n\t\t *\n\t\t * @type {{clipCullDistance:false,multiDraw:false}}\n\t\t */\n\t\tthis.extensions = {\n\t\t\tclipCullDistance: false, // set to use vertex shader clipping\n\t\t\tmultiDraw: false // set to use vertex shader multi_draw / enable gl_DrawID\n\t\t};\n\n\t\t/**\n\t\t * When the rendered geometry doesn't include these attributes but the\n\t\t * material does, these default values will be passed to the shaders. This\n\t\t * avoids errors when buffer data is missing.\n\t\t *\n\t\t * - color: [ 1, 1, 1 ]\n\t\t * - uv: [ 0, 0 ]\n\t\t * - uv1: [ 0, 0 ]\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.defaultAttributeValues = {\n\t\t\t'color': [ 1, 1, 1 ],\n\t\t\t'uv': [ 0, 0 ],\n\t\t\t'uv1': [ 0, 0 ]\n\t\t};\n\n\t\t/**\n\t\t * If set, this calls [gl.bindAttribLocation](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bindAttribLocation)\n\t\t * to bind a generic vertex index to an attribute variable.\n\t\t *\n\t\t * @type {string|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.index0AttributeName = undefined;\n\n\t\t/**\n\t\t * Can be used to force a uniform update while changing uniforms in\n\t\t * {@link Object3D#onBeforeRender}.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.uniformsNeedUpdate = false;\n\n\t\t/**\n\t\t * Defines the GLSL version of custom shader code.\n\t\t *\n\t\t * @type {?(GLSL1|GLSL3)}\n\t\t * @default null\n\t\t */\n\t\tthis.glslVersion = null;\n\n\t\tif ( parameters !== undefined ) {\n\n\t\t\tthis.setValues( parameters );\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.fragmentShader = source.fragmentShader;\n\t\tthis.vertexShader = source.vertexShader;\n\n\t\tthis.uniforms = cloneUniforms( source.uniforms );\n\t\tthis.uniformsGroups = cloneUniformsGroups( source.uniformsGroups );\n\n\t\tthis.defines = Object.assign( {}, source.defines );\n\n\t\tthis.wireframe = source.wireframe;\n\t\tthis.wireframeLinewidth = source.wireframeLinewidth;\n\n\t\tthis.fog = source.fog;\n\t\tthis.lights = source.lights;\n\t\tthis.clipping = source.clipping;\n\n\t\tthis.extensions = Object.assign( {}, source.extensions );\n\n\t\tthis.glslVersion = source.glslVersion;\n\n\t\tthis.defaultAttributeValues = Object.assign( {}, source.defaultAttributeValues );\n\n\t\tthis.index0AttributeName = source.index0AttributeName;\n\n\t\tthis.uniformsNeedUpdate = source.uniformsNeedUpdate;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.glslVersion = this.glslVersion;\n\t\tdata.uniforms = {};\n\n\t\tfor ( const name in this.uniforms ) {\n\n\t\t\tconst uniform = this.uniforms[ name ];\n\t\t\tconst value = uniform.value;\n\n\t\t\tif ( value && value.isTexture ) {\n\n\t\t\t\tdata.uniforms[ name ] = {\n\t\t\t\t\ttype: 't',\n\t\t\t\t\tvalue: value.toJSON( meta ).uuid\n\t\t\t\t};\n\n\t\t\t} else if ( value && value.isColor ) {\n\n\t\t\t\tdata.uniforms[ name ] = {\n\t\t\t\t\ttype: 'c',\n\t\t\t\t\tvalue: value.getHex()\n\t\t\t\t};\n\n\t\t\t} else if ( value && value.isVector2 ) {\n\n\t\t\t\tdata.uniforms[ name ] = {\n\t\t\t\t\ttype: 'v2',\n\t\t\t\t\tvalue: value.toArray()\n\t\t\t\t};\n\n\t\t\t} else if ( value && value.isVector3 ) {\n\n\t\t\t\tdata.uniforms[ name ] = {\n\t\t\t\t\ttype: 'v3',\n\t\t\t\t\tvalue: value.toArray()\n\t\t\t\t};\n\n\t\t\t} else if ( value && value.isVector4 ) {\n\n\t\t\t\tdata.uniforms[ name ] = {\n\t\t\t\t\ttype: 'v4',\n\t\t\t\t\tvalue: value.toArray()\n\t\t\t\t};\n\n\t\t\t} else if ( value && value.isMatrix3 ) {\n\n\t\t\t\tdata.uniforms[ name ] = {\n\t\t\t\t\ttype: 'm3',\n\t\t\t\t\tvalue: value.toArray()\n\t\t\t\t};\n\n\t\t\t} else if ( value && value.isMatrix4 ) {\n\n\t\t\t\tdata.uniforms[ name ] = {\n\t\t\t\t\ttype: 'm4',\n\t\t\t\t\tvalue: value.toArray()\n\t\t\t\t};\n\n\t\t\t} else {\n\n\t\t\t\tdata.uniforms[ name ] = {\n\t\t\t\t\tvalue: value\n\t\t\t\t};\n\n\t\t\t\t// note: the array variants v2v, v3v, v4v, m4v and tv are not supported so far\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( Object.keys( this.defines ).length > 0 ) data.defines = this.defines;\n\n\t\tdata.vertexShader = this.vertexShader;\n\t\tdata.fragmentShader = this.fragmentShader;\n\n\t\tdata.lights = this.lights;\n\t\tdata.clipping = this.clipping;\n\n\t\tconst extensions = {};\n\n\t\tfor ( const key in this.extensions ) {\n\n\t\t\tif ( this.extensions[ key ] === true ) extensions[ key ] = true;\n\n\t\t}\n\n\t\tif ( Object.keys( extensions ).length > 0 ) data.extensions = extensions;\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * Abstract base class for cameras. This class should always be inherited\n * when you build a new camera.\n *\n * @abstract\n * @augments Object3D\n */\nclass Camera extends Object3D {\n\n\t/**\n\t * Constructs a new camera.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCamera = true;\n\n\t\tthis.type = 'Camera';\n\n\t\t/**\n\t\t * The inverse of the camera's world matrix.\n\t\t *\n\t\t * @type {Matrix4}\n\t\t */\n\t\tthis.matrixWorldInverse = new Matrix4();\n\n\t\t/**\n\t\t * The camera's projection matrix.\n\t\t *\n\t\t * @type {Matrix4}\n\t\t */\n\t\tthis.projectionMatrix = new Matrix4();\n\n\t\t/**\n\t\t * The inverse of the camera's projection matrix.\n\t\t *\n\t\t * @type {Matrix4}\n\t\t */\n\t\tthis.projectionMatrixInverse = new Matrix4();\n\n\t\t/**\n\t\t * The coordinate system in which the camera is used.\n\t\t *\n\t\t * @type {(WebGLCoordinateSystem|WebGPUCoordinateSystem)}\n\t\t */\n\t\tthis.coordinateSystem = WebGLCoordinateSystem;\n\n\t\tthis._reversedDepth = false;\n\n\t}\n\n\t/**\n\t * The flag that indicates whether the camera uses a reversed depth buffer.\n\t *\n\t * @type {boolean}\n\t * @default false\n\t */\n\tget reversedDepth() {\n\n\t\treturn this._reversedDepth;\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.matrixWorldInverse.copy( source.matrixWorldInverse );\n\n\t\tthis.projectionMatrix.copy( source.projectionMatrix );\n\t\tthis.projectionMatrixInverse.copy( source.projectionMatrixInverse );\n\n\t\tthis.coordinateSystem = source.coordinateSystem;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a vector representing the (\"look\") direction of the 3D object in world space.\n\t *\n\t * This method is overwritten since cameras have a different forward vector compared to other\n\t * 3D objects. A camera looks down its local, negative z-axis by default.\n\t *\n\t * @param {Vector3} target - The target vector the result is stored to.\n\t * @return {Vector3} The 3D object's direction in world space.\n\t */\n\tgetWorldDirection( target ) {\n\n\t\treturn super.getWorldDirection( target ).negate();\n\n\t}\n\n\tupdateMatrixWorld( force ) {\n\n\t\tsuper.updateMatrixWorld( force );\n\n\t\tthis.matrixWorldInverse.copy( this.matrixWorld ).invert();\n\n\t}\n\n\tupdateWorldMatrix( updateParents, updateChildren ) {\n\n\t\tsuper.updateWorldMatrix( updateParents, updateChildren );\n\n\t\tthis.matrixWorldInverse.copy( this.matrixWorld ).invert();\n\n\t}\n\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n}\n\nconst _v3$1 = /*@__PURE__*/ new Vector3();\nconst _minTarget = /*@__PURE__*/ new Vector2();\nconst _maxTarget = /*@__PURE__*/ new Vector2();\n\n/**\n * Camera that uses [perspective projection](https://en.wikipedia.org/wiki/Perspective_(graphical)).\n *\n * This projection mode is designed to mimic the way the human eye sees. It\n * is the most common projection mode used for rendering a 3D scene.\n *\n * ```js\n * const camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );\n * scene.add( camera );\n * ```\n *\n * @augments Camera\n */\nclass PerspectiveCamera extends Camera {\n\n\t/**\n\t * Constructs a new perspective camera.\n\t *\n\t * @param {number} [fov=50] - The vertical field of view.\n\t * @param {number} [aspect=1] - The aspect ratio.\n\t * @param {number} [near=0.1] - The camera's near plane.\n\t * @param {number} [far=2000] - The camera's far plane.\n\t */\n\tconstructor( fov = 50, aspect = 1, near = 0.1, far = 2000 ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isPerspectiveCamera = true;\n\n\t\tthis.type = 'PerspectiveCamera';\n\n\t\t/**\n\t\t * The vertical field of view, from bottom to top of view,\n\t\t * in degrees.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 50\n\t\t */\n\t\tthis.fov = fov;\n\n\t\t/**\n\t\t * The zoom factor of the camera.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.zoom = 1;\n\n\t\t/**\n\t\t * The camera's near plane. The valid range is greater than `0`\n\t\t * and less than the current value of {@link PerspectiveCamera#far}.\n\t\t *\n\t\t * Note that, unlike for the {@link OrthographicCamera}, `0` is <em>not</em> a\n\t\t * valid value for a perspective camera's near plane.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0.1\n\t\t */\n\t\tthis.near = near;\n\n\t\t/**\n\t\t * The camera's far plane. Must be greater than the\n\t\t * current value of {@link PerspectiveCamera#near}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 2000\n\t\t */\n\t\tthis.far = far;\n\n\t\t/**\n\t\t * Object distance used for stereoscopy and depth-of-field effects. This\n\t\t * parameter does not influence the projection matrix unless a\n\t\t * {@link StereoCamera} is being used.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 10\n\t\t */\n\t\tthis.focus = 10;\n\n\t\t/**\n\t\t * The aspect ratio, usually the canvas width / canvas height.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.aspect = aspect;\n\n\t\t/**\n\t\t * Represents the frustum window specification. This property should not be edited\n\t\t * directly but via {@link PerspectiveCamera#setViewOffset} and {@link PerspectiveCamera#clearViewOffset}.\n\t\t *\n\t\t * @type {?Object}\n\t\t * @default null\n\t\t */\n\t\tthis.view = null;\n\n\t\t/**\n\t\t * Film size used for the larger axis. Default is `35` (millimeters). This\n\t\t * parameter does not influence the projection matrix unless {@link PerspectiveCamera#filmOffset}\n\t\t * is set to a nonzero value.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 35\n\t\t */\n\t\tthis.filmGauge = 35;\n\n\t\t/**\n\t\t * Horizontal off-center offset in the same unit as {@link PerspectiveCamera#filmGauge}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.filmOffset = 0;\n\n\t\tthis.updateProjectionMatrix();\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.fov = source.fov;\n\t\tthis.zoom = source.zoom;\n\n\t\tthis.near = source.near;\n\t\tthis.far = source.far;\n\t\tthis.focus = source.focus;\n\n\t\tthis.aspect = source.aspect;\n\t\tthis.view = source.view === null ? null : Object.assign( {}, source.view );\n\n\t\tthis.filmGauge = source.filmGauge;\n\t\tthis.filmOffset = source.filmOffset;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the FOV by focal length in respect to the current {@link PerspectiveCamera#filmGauge}.\n\t *\n\t * The default film gauge is 35, so that the focal length can be specified for\n\t * a 35mm (full frame) camera.\n\t *\n\t * @param {number} focalLength - Values for focal length and film gauge must have the same unit.\n\t */\n\tsetFocalLength( focalLength ) {\n\n\t\t/** see {@link http://www.bobatkins.com/photography/technical/field_of_view.html} */\n\t\tconst vExtentSlope = 0.5 * this.getFilmHeight() / focalLength;\n\n\t\tthis.fov = RAD2DEG * 2 * Math.atan( vExtentSlope );\n\t\tthis.updateProjectionMatrix();\n\n\t}\n\n\t/**\n\t * Returns the focal length from the current {@link PerspectiveCamera#fov} and\n\t * {@link PerspectiveCamera#filmGauge}.\n\t *\n\t * @return {number} The computed focal length.\n\t */\n\tgetFocalLength() {\n\n\t\tconst vExtentSlope = Math.tan( DEG2RAD * 0.5 * this.fov );\n\n\t\treturn 0.5 * this.getFilmHeight() / vExtentSlope;\n\n\t}\n\n\t/**\n\t * Returns the current vertical field of view angle in degrees considering {@link PerspectiveCamera#zoom}.\n\t *\n\t * @return {number} The effective FOV.\n\t */\n\tgetEffectiveFOV() {\n\n\t\treturn RAD2DEG * 2 * Math.atan(\n\t\t\tMath.tan( DEG2RAD * 0.5 * this.fov ) / this.zoom );\n\n\t}\n\n\t/**\n\t * Returns the width of the image on the film. If {@link PerspectiveCamera#aspect} is greater than or\n\t * equal to one (landscape format), the result equals {@link PerspectiveCamera#filmGauge}.\n\t *\n\t * @return {number} The film width.\n\t */\n\tgetFilmWidth() {\n\n\t\t// film not completely covered in portrait format (aspect < 1)\n\t\treturn this.filmGauge * Math.min( this.aspect, 1 );\n\n\t}\n\n\t/**\n\t * Returns the height of the image on the film. If {@link PerspectiveCamera#aspect} is greater than or\n\t * equal to one (landscape format), the result equals {@link PerspectiveCamera#filmGauge}.\n\t *\n\t * @return {number} The film width.\n\t */\n\tgetFilmHeight() {\n\n\t\t// film not completely covered in landscape format (aspect > 1)\n\t\treturn this.filmGauge / Math.max( this.aspect, 1 );\n\n\t}\n\n\t/**\n\t * Computes the 2D bounds of the camera's viewable rectangle at a given distance along the viewing direction.\n\t * Sets `minTarget` and `maxTarget` to the coordinates of the lower-left and upper-right corners of the view rectangle.\n\t *\n\t * @param {number} distance - The viewing distance.\n\t * @param {Vector2} minTarget - The lower-left corner of the view rectangle is written into this vector.\n\t * @param {Vector2} maxTarget - The upper-right corner of the view rectangle is written into this vector.\n\t */\n\tgetViewBounds( distance, minTarget, maxTarget ) {\n\n\t\t_v3$1.set( -1, -1, 0.5 ).applyMatrix4( this.projectionMatrixInverse );\n\n\t\tminTarget.set( _v3$1.x, _v3$1.y ).multiplyScalar( - distance / _v3$1.z );\n\n\t\t_v3$1.set( 1, 1, 0.5 ).applyMatrix4( this.projectionMatrixInverse );\n\n\t\tmaxTarget.set( _v3$1.x, _v3$1.y ).multiplyScalar( - distance / _v3$1.z );\n\n\t}\n\n\t/**\n\t * Computes the width and height of the camera's viewable rectangle at a given distance along the viewing direction.\n\t *\n\t * @param {number} distance - The viewing distance.\n\t * @param {Vector2} target - The target vector that is used to store result where x is width and y is height.\n\t * @returns {Vector2} The view size.\n\t */\n\tgetViewSize( distance, target ) {\n\n\t\tthis.getViewBounds( distance, _minTarget, _maxTarget );\n\n\t\treturn target.subVectors( _maxTarget, _minTarget );\n\n\t}\n\n\t/**\n\t * Sets an offset in a larger frustum. This is useful for multi-window or\n\t * multi-monitor/multi-machine setups.\n\t *\n\t * For example, if you have 3x2 monitors and each monitor is 1920x1080 and\n\t * the monitors are in grid like this\n\t *```\n\t *   +---+---+---+\n\t *   | A | B | C |\n\t *   +---+---+---+\n\t *   | D | E | F |\n\t *   +---+---+---+\n\t *```\n\t * then for each monitor you would call it like this:\n\t *```js\n\t * const w = 1920;\n\t * const h = 1080;\n\t * const fullWidth = w * 3;\n\t * const fullHeight = h * 2;\n\t *\n\t * // --A--\n\t * camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 0, w, h );\n\t * // --B--\n\t * camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 0, w, h );\n\t * // --C--\n\t * camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 0, w, h );\n\t * // --D--\n\t * camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 1, w, h );\n\t * // --E--\n\t * camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 1, w, h );\n\t * // --F--\n\t * camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 1, w, h );\n\t * ```\n\t *\n\t * Note there is no reason monitors have to be the same size or in a grid.\n\t *\n\t * @param {number} fullWidth - The full width of multiview setup.\n\t * @param {number} fullHeight - The full height of multiview setup.\n\t * @param {number} x - The horizontal offset of the subcamera.\n\t * @param {number} y - The vertical offset of the subcamera.\n\t * @param {number} width - The width of subcamera.\n\t * @param {number} height - The height of subcamera.\n\t */\n\tsetViewOffset( fullWidth, fullHeight, x, y, width, height ) {\n\n\t\tthis.aspect = fullWidth / fullHeight;\n\n\t\tif ( this.view === null ) {\n\n\t\t\tthis.view = {\n\t\t\t\tenabled: true,\n\t\t\t\tfullWidth: 1,\n\t\t\t\tfullHeight: 1,\n\t\t\t\toffsetX: 0,\n\t\t\t\toffsetY: 0,\n\t\t\t\twidth: 1,\n\t\t\t\theight: 1\n\t\t\t};\n\n\t\t}\n\n\t\tthis.view.enabled = true;\n\t\tthis.view.fullWidth = fullWidth;\n\t\tthis.view.fullHeight = fullHeight;\n\t\tthis.view.offsetX = x;\n\t\tthis.view.offsetY = y;\n\t\tthis.view.width = width;\n\t\tthis.view.height = height;\n\n\t\tthis.updateProjectionMatrix();\n\n\t}\n\n\t/**\n\t * Removes the view offset from the projection matrix.\n\t */\n\tclearViewOffset() {\n\n\t\tif ( this.view !== null ) {\n\n\t\t\tthis.view.enabled = false;\n\n\t\t}\n\n\t\tthis.updateProjectionMatrix();\n\n\t}\n\n\t/**\n\t * Updates the camera's projection matrix. Must be called after any change of\n\t * camera properties.\n\t */\n\tupdateProjectionMatrix() {\n\n\t\tconst near = this.near;\n\t\tlet top = near * Math.tan( DEG2RAD * 0.5 * this.fov ) / this.zoom;\n\t\tlet height = 2 * top;\n\t\tlet width = this.aspect * height;\n\t\tlet left = -0.5 * width;\n\t\tconst view = this.view;\n\n\t\tif ( this.view !== null && this.view.enabled ) {\n\n\t\t\tconst fullWidth = view.fullWidth,\n\t\t\t\tfullHeight = view.fullHeight;\n\n\t\t\tleft += view.offsetX * width / fullWidth;\n\t\t\ttop -= view.offsetY * height / fullHeight;\n\t\t\twidth *= view.width / fullWidth;\n\t\t\theight *= view.height / fullHeight;\n\n\t\t}\n\n\t\tconst skew = this.filmOffset;\n\t\tif ( skew !== 0 ) left += near * skew / this.getFilmWidth();\n\n\t\tthis.projectionMatrix.makePerspective( left, left + width, top, top - height, near, this.far, this.coordinateSystem, this.reversedDepth );\n\n\t\tthis.projectionMatrixInverse.copy( this.projectionMatrix ).invert();\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.object.fov = this.fov;\n\t\tdata.object.zoom = this.zoom;\n\n\t\tdata.object.near = this.near;\n\t\tdata.object.far = this.far;\n\t\tdata.object.focus = this.focus;\n\n\t\tdata.object.aspect = this.aspect;\n\n\t\tif ( this.view !== null ) data.object.view = Object.assign( {}, this.view );\n\n\t\tdata.object.filmGauge = this.filmGauge;\n\t\tdata.object.filmOffset = this.filmOffset;\n\n\t\treturn data;\n\n\t}\n\n}\n\nconst fov = -90; // negative fov is not an error\nconst aspect = 1;\n\n/**\n * A special type of camera that is positioned in 3D space to render its surroundings into a\n * cube render target. The render target can then be used as an environment map for rendering\n * realtime reflections in your scene.\n *\n * ```js\n * // Create cube render target\n * const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter } );\n *\n * // Create cube camera\n * const cubeCamera = new THREE.CubeCamera( 1, 100000, cubeRenderTarget );\n * scene.add( cubeCamera );\n *\n * // Create car\n * const chromeMaterial = new THREE.MeshLambertMaterial( { color: 0xffffff, envMap: cubeRenderTarget.texture } );\n * const car = new THREE.Mesh( carGeometry, chromeMaterial );\n * scene.add( car );\n *\n * // Update the render target cube\n * car.visible = false;\n * cubeCamera.position.copy( car.position );\n * cubeCamera.update( renderer, scene );\n *\n * // Render the scene\n * car.visible = true;\n * renderer.render( scene, camera );\n * ```\n *\n * @augments Object3D\n */\nclass CubeCamera extends Object3D {\n\n\t/**\n\t * Constructs a new cube camera.\n\t *\n\t * @param {number} near - The camera's near plane.\n\t * @param {number} far - The camera's far plane.\n\t * @param {WebGLCubeRenderTarget} renderTarget - The cube render target.\n\t */\n\tconstructor( near, far, renderTarget ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'CubeCamera';\n\n\t\t/**\n\t\t * A reference to the cube render target.\n\t\t *\n\t\t * @type {WebGLCubeRenderTarget}\n\t\t */\n\t\tthis.renderTarget = renderTarget;\n\n\t\t/**\n\t\t * The current active coordinate system.\n\t\t *\n\t\t * @type {?(WebGLCoordinateSystem|WebGPUCoordinateSystem)}\n\t\t * @default null\n\t\t */\n\t\tthis.coordinateSystem = null;\n\n\t\t/**\n\t\t * The current active mipmap level\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.activeMipmapLevel = 0;\n\n\t\tconst cameraPX = new PerspectiveCamera( fov, aspect, near, far );\n\t\tcameraPX.layers = this.layers;\n\t\tthis.add( cameraPX );\n\n\t\tconst cameraNX = new PerspectiveCamera( fov, aspect, near, far );\n\t\tcameraNX.layers = this.layers;\n\t\tthis.add( cameraNX );\n\n\t\tconst cameraPY = new PerspectiveCamera( fov, aspect, near, far );\n\t\tcameraPY.layers = this.layers;\n\t\tthis.add( cameraPY );\n\n\t\tconst cameraNY = new PerspectiveCamera( fov, aspect, near, far );\n\t\tcameraNY.layers = this.layers;\n\t\tthis.add( cameraNY );\n\n\t\tconst cameraPZ = new PerspectiveCamera( fov, aspect, near, far );\n\t\tcameraPZ.layers = this.layers;\n\t\tthis.add( cameraPZ );\n\n\t\tconst cameraNZ = new PerspectiveCamera( fov, aspect, near, far );\n\t\tcameraNZ.layers = this.layers;\n\t\tthis.add( cameraNZ );\n\n\t}\n\n\t/**\n\t * Must be called when the coordinate system of the cube camera is changed.\n\t */\n\tupdateCoordinateSystem() {\n\n\t\tconst coordinateSystem = this.coordinateSystem;\n\n\t\tconst cameras = this.children.concat();\n\n\t\tconst [ cameraPX, cameraNX, cameraPY, cameraNY, cameraPZ, cameraNZ ] = cameras;\n\n\t\tfor ( const camera of cameras ) this.remove( camera );\n\n\t\tif ( coordinateSystem === WebGLCoordinateSystem ) {\n\n\t\t\tcameraPX.up.set( 0, 1, 0 );\n\t\t\tcameraPX.lookAt( 1, 0, 0 );\n\n\t\t\tcameraNX.up.set( 0, 1, 0 );\n\t\t\tcameraNX.lookAt( -1, 0, 0 );\n\n\t\t\tcameraPY.up.set( 0, 0, -1 );\n\t\t\tcameraPY.lookAt( 0, 1, 0 );\n\n\t\t\tcameraNY.up.set( 0, 0, 1 );\n\t\t\tcameraNY.lookAt( 0, -1, 0 );\n\n\t\t\tcameraPZ.up.set( 0, 1, 0 );\n\t\t\tcameraPZ.lookAt( 0, 0, 1 );\n\n\t\t\tcameraNZ.up.set( 0, 1, 0 );\n\t\t\tcameraNZ.lookAt( 0, 0, -1 );\n\n\t\t} else if ( coordinateSystem === WebGPUCoordinateSystem ) {\n\n\t\t\tcameraPX.up.set( 0, -1, 0 );\n\t\t\tcameraPX.lookAt( -1, 0, 0 );\n\n\t\t\tcameraNX.up.set( 0, -1, 0 );\n\t\t\tcameraNX.lookAt( 1, 0, 0 );\n\n\t\t\tcameraPY.up.set( 0, 0, 1 );\n\t\t\tcameraPY.lookAt( 0, 1, 0 );\n\n\t\t\tcameraNY.up.set( 0, 0, -1 );\n\t\t\tcameraNY.lookAt( 0, -1, 0 );\n\n\t\t\tcameraPZ.up.set( 0, -1, 0 );\n\t\t\tcameraPZ.lookAt( 0, 0, 1 );\n\n\t\t\tcameraNZ.up.set( 0, -1, 0 );\n\t\t\tcameraNZ.lookAt( 0, 0, -1 );\n\n\t\t} else {\n\n\t\t\tthrow new Error( 'THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: ' + coordinateSystem );\n\n\t\t}\n\n\t\tfor ( const camera of cameras ) {\n\n\t\t\tthis.add( camera );\n\n\t\t\tcamera.updateMatrixWorld();\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Calling this method will render the given scene with the given renderer\n\t * into the cube render target of the camera.\n\t *\n\t * @param {(Renderer|WebGLRenderer)} renderer - The renderer.\n\t * @param {Scene} scene - The scene to render.\n\t */\n\tupdate( renderer, scene ) {\n\n\t\tif ( this.parent === null ) this.updateMatrixWorld();\n\n\t\tconst { renderTarget, activeMipmapLevel } = this;\n\n\t\tif ( this.coordinateSystem !== renderer.coordinateSystem ) {\n\n\t\t\tthis.coordinateSystem = renderer.coordinateSystem;\n\n\t\t\tthis.updateCoordinateSystem();\n\n\t\t}\n\n\t\tconst [ cameraPX, cameraNX, cameraPY, cameraNY, cameraPZ, cameraNZ ] = this.children;\n\n\t\tconst currentRenderTarget = renderer.getRenderTarget();\n\t\tconst currentActiveCubeFace = renderer.getActiveCubeFace();\n\t\tconst currentActiveMipmapLevel = renderer.getActiveMipmapLevel();\n\n\t\tconst currentXrEnabled = renderer.xr.enabled;\n\n\t\trenderer.xr.enabled = false;\n\n\t\tconst generateMipmaps = renderTarget.texture.generateMipmaps;\n\n\t\trenderTarget.texture.generateMipmaps = false;\n\n\t\trenderer.setRenderTarget( renderTarget, 0, activeMipmapLevel );\n\t\trenderer.render( scene, cameraPX );\n\n\t\trenderer.setRenderTarget( renderTarget, 1, activeMipmapLevel );\n\t\trenderer.render( scene, cameraNX );\n\n\t\trenderer.setRenderTarget( renderTarget, 2, activeMipmapLevel );\n\t\trenderer.render( scene, cameraPY );\n\n\t\trenderer.setRenderTarget( renderTarget, 3, activeMipmapLevel );\n\t\trenderer.render( scene, cameraNY );\n\n\t\trenderer.setRenderTarget( renderTarget, 4, activeMipmapLevel );\n\t\trenderer.render( scene, cameraPZ );\n\n\t\t// mipmaps are generated during the last call of render()\n\t\t// at this point, all sides of the cube render target are defined\n\n\t\trenderTarget.texture.generateMipmaps = generateMipmaps;\n\n\t\trenderer.setRenderTarget( renderTarget, 5, activeMipmapLevel );\n\t\trenderer.render( scene, cameraNZ );\n\n\t\trenderer.setRenderTarget( currentRenderTarget, currentActiveCubeFace, currentActiveMipmapLevel );\n\n\t\trenderer.xr.enabled = currentXrEnabled;\n\n\t\trenderTarget.texture.needsPMREMUpdate = true;\n\n\t}\n\n}\n\n/**\n * Creates a cube texture made up of six images.\n *\n * ```js\n * const loader = new THREE.CubeTextureLoader();\n * loader.setPath( 'textures/cube/pisa/' );\n *\n * const textureCube = loader.load( [\n * \t'px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png'\n * ] );\n *\n * const material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: textureCube } );\n * ```\n *\n * @augments Texture\n */\nclass CubeTexture extends Texture {\n\n\t/**\n\t * Constructs a new cube texture.\n\t *\n\t * @param {Array<Image>} [images=[]] - An array holding a image for each side of a cube.\n\t * @param {number} [mapping=CubeReflectionMapping] - The texture mapping.\n\t * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.\n\t * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.\n\t * @param {number} [magFilter=LinearFilter] - The mag filter value.\n\t * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.\n\t * @param {number} [format=RGBAFormat] - The texture format.\n\t * @param {number} [type=UnsignedByteType] - The texture type.\n\t * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.\n\t * @param {string} [colorSpace=NoColorSpace] - The color space value.\n\t */\n\tconstructor( images = [], mapping = CubeReflectionMapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace ) {\n\n\t\tsuper( images, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCubeTexture = true;\n\n\t\t/**\n\t\t * If set to `true`, the texture is flipped along the vertical axis when\n\t\t * uploaded to the GPU.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flipY = false;\n\n\t}\n\n\t/**\n\t * Alias for {@link CubeTexture#image}.\n\t *\n\t * @type {Array<Image>}\n\t */\n\tget images() {\n\n\t\treturn this.image;\n\n\t}\n\n\tset images( value ) {\n\n\t\tthis.image = value;\n\n\t}\n\n}\n\n/**\n * A cube render target used in context of {@link WebGLRenderer}.\n *\n * @augments WebGLRenderTarget\n */\nclass WebGLCubeRenderTarget extends WebGLRenderTarget {\n\n\t/**\n\t * Constructs a new cube render target.\n\t *\n\t * @param {number} [size=1] - The size of the render target.\n\t * @param {RenderTarget~Options} [options] - The configuration object.\n\t */\n\tconstructor( size = 1, options = {} ) {\n\n\t\tsuper( size, size, options );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isWebGLCubeRenderTarget = true;\n\n\t\tconst image = { width: size, height: size, depth: 1 };\n\t\tconst images = [ image, image, image, image, image, image ];\n\n\t\t/**\n\t\t * Overwritten with a different texture type.\n\t\t *\n\t\t * @type {DataArrayTexture}\n\t\t */\n\t\tthis.texture = new CubeTexture( images );\n\t\tthis._setTextureOptions( options );\n\n\t\t// By convention -- likely based on the RenderMan spec from the 1990's -- cube maps are specified by WebGL (and three.js)\n\t\t// in a coordinate system in which positive-x is to the right when looking up the positive-z axis -- in other words,\n\t\t// in a left-handed coordinate system. By continuing this convention, preexisting cube maps continued to render correctly.\n\n\t\t// three.js uses a right-handed coordinate system. So environment maps used in three.js appear to have px and nx swapped\n\t\t// and the flag isRenderTargetTexture controls this conversion. The flip is not required when using WebGLCubeRenderTarget.texture\n\t\t// as a cube texture (this is detected when isRenderTargetTexture is set to true for cube textures).\n\n\t\tthis.texture.isRenderTargetTexture = true;\n\n\t}\n\n\t/**\n\t * Converts the given equirectangular texture to a cube map.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {Texture} texture - The equirectangular texture.\n\t * @return {WebGLCubeRenderTarget} A reference to this cube render target.\n\t */\n\tfromEquirectangularTexture( renderer, texture ) {\n\n\t\tthis.texture.type = texture.type;\n\t\tthis.texture.colorSpace = texture.colorSpace;\n\n\t\tthis.texture.generateMipmaps = texture.generateMipmaps;\n\t\tthis.texture.minFilter = texture.minFilter;\n\t\tthis.texture.magFilter = texture.magFilter;\n\n\t\tconst shader = {\n\n\t\t\tuniforms: {\n\t\t\t\ttEquirect: { value: null },\n\t\t\t},\n\n\t\t\tvertexShader: /* glsl */`\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\tvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\n\t\t\t\t\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n\n\t\t\t\t}\n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvWorldDirection = transformDirection( position, modelMatrix );\n\n\t\t\t\t\t#include <begin_vertex>\n\t\t\t\t\t#include <project_vertex>\n\n\t\t\t\t}\n\t\t\t`,\n\n\t\t\tfragmentShader: /* glsl */`\n\n\t\t\t\tuniform sampler2D tEquirect;\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\t#include <common>\n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvec3 direction = normalize( vWorldDirection );\n\n\t\t\t\t\tvec2 sampleUV = equirectUv( direction );\n\n\t\t\t\t\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\n\t\t\t\t}\n\t\t\t`\n\t\t};\n\n\t\tconst geometry = new BoxGeometry( 5, 5, 5 );\n\n\t\tconst material = new ShaderMaterial( {\n\n\t\t\tname: 'CubemapFromEquirect',\n\n\t\t\tuniforms: cloneUniforms( shader.uniforms ),\n\t\t\tvertexShader: shader.vertexShader,\n\t\t\tfragmentShader: shader.fragmentShader,\n\t\t\tside: BackSide,\n\t\t\tblending: NoBlending\n\n\t\t} );\n\n\t\tmaterial.uniforms.tEquirect.value = texture;\n\n\t\tconst mesh = new Mesh( geometry, material );\n\n\t\tconst currentMinFilter = texture.minFilter;\n\n\t\t// Avoid blurred poles\n\t\tif ( texture.minFilter === LinearMipmapLinearFilter ) texture.minFilter = LinearFilter;\n\n\t\tconst camera = new CubeCamera( 1, 10, this );\n\t\tcamera.update( renderer, mesh );\n\n\t\ttexture.minFilter = currentMinFilter;\n\n\t\tmesh.geometry.dispose();\n\t\tmesh.material.dispose();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Clears this cube render target.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {boolean} [color=true] - Whether the color buffer should be cleared or not.\n\t * @param {boolean} [depth=true] - Whether the depth buffer should be cleared or not.\n\t * @param {boolean} [stencil=true] - Whether the stencil buffer should be cleared or not.\n\t */\n\tclear( renderer, color = true, depth = true, stencil = true ) {\n\n\t\tconst currentRenderTarget = renderer.getRenderTarget();\n\n\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\trenderer.setRenderTarget( this, i );\n\n\t\t\trenderer.clear( color, depth, stencil );\n\n\t\t}\n\n\t\trenderer.setRenderTarget( currentRenderTarget );\n\n\t}\n\n}\n\n/**\n * This is almost identical to an {@link Object3D}. Its purpose is to\n * make working with groups of objects syntactically clearer.\n *\n * ```js\n * // Create a group and add the two cubes.\n * // These cubes can now be rotated / scaled etc as a group.\n * const group = new THREE.Group();\n *\n * group.add( meshA );\n * group.add( meshB );\n *\n * scene.add( group );\n * ```\n *\n * @augments Object3D\n */\nclass Group extends Object3D {\n\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isGroup = true;\n\n\t\tthis.type = 'Group';\n\n\t}\n\n}\n\nconst _moveEvent = { type: 'move' };\n\n/**\n * Class for representing a XR controller with its\n * different coordinate systems.\n *\n * @private\n */\nclass WebXRController {\n\n\t/**\n\t * Constructs a new XR controller.\n\t */\n\tconstructor() {\n\n\t\t/**\n\t\t * A group representing the target ray space\n\t\t * of the XR controller.\n\t\t *\n\t\t * @private\n\t\t * @type {?Group}\n\t\t * @default null\n\t\t */\n\t\tthis._targetRay = null;\n\n\t\t/**\n\t\t * A group representing the grip space\n\t\t * of the XR controller.\n\t\t *\n\t\t * @private\n\t\t * @type {?Group}\n\t\t * @default null\n\t\t */\n\t\tthis._grip = null;\n\n\t\t/**\n\t\t * A group representing the hand space\n\t\t * of the XR controller.\n\t\t *\n\t\t * @private\n\t\t * @type {?Group}\n\t\t * @default null\n\t\t */\n\t\tthis._hand = null;\n\n\t}\n\n\t/**\n\t * Returns a group representing the hand space of the XR controller.\n\t *\n\t * @return {Group} A group representing the hand space of the XR controller.\n\t */\n\tgetHandSpace() {\n\n\t\tif ( this._hand === null ) {\n\n\t\t\tthis._hand = new Group();\n\t\t\tthis._hand.matrixAutoUpdate = false;\n\t\t\tthis._hand.visible = false;\n\n\t\t\tthis._hand.joints = {};\n\t\t\tthis._hand.inputState = { pinching: false };\n\n\t\t}\n\n\t\treturn this._hand;\n\n\t}\n\n\t/**\n\t * Returns a group representing the target ray space of the XR controller.\n\t *\n\t * @return {Group} A group representing the target ray space of the XR controller.\n\t */\n\tgetTargetRaySpace() {\n\n\t\tif ( this._targetRay === null ) {\n\n\t\t\tthis._targetRay = new Group();\n\t\t\tthis._targetRay.matrixAutoUpdate = false;\n\t\t\tthis._targetRay.visible = false;\n\t\t\tthis._targetRay.hasLinearVelocity = false;\n\t\t\tthis._targetRay.linearVelocity = new Vector3();\n\t\t\tthis._targetRay.hasAngularVelocity = false;\n\t\t\tthis._targetRay.angularVelocity = new Vector3();\n\n\t\t}\n\n\t\treturn this._targetRay;\n\n\t}\n\n\t/**\n\t * Returns a group representing the grip space of the XR controller.\n\t *\n\t * @return {Group} A group representing the grip space of the XR controller.\n\t */\n\tgetGripSpace() {\n\n\t\tif ( this._grip === null ) {\n\n\t\t\tthis._grip = new Group();\n\t\t\tthis._grip.matrixAutoUpdate = false;\n\t\t\tthis._grip.visible = false;\n\t\t\tthis._grip.hasLinearVelocity = false;\n\t\t\tthis._grip.linearVelocity = new Vector3();\n\t\t\tthis._grip.hasAngularVelocity = false;\n\t\t\tthis._grip.angularVelocity = new Vector3();\n\n\t\t}\n\n\t\treturn this._grip;\n\n\t}\n\n\t/**\n\t * Dispatches the given event to the groups representing\n\t * the different coordinate spaces of the XR controller.\n\t *\n\t * @param {Object} event - The event to dispatch.\n\t * @return {WebXRController} A reference to this instance.\n\t */\n\tdispatchEvent( event ) {\n\n\t\tif ( this._targetRay !== null ) {\n\n\t\t\tthis._targetRay.dispatchEvent( event );\n\n\t\t}\n\n\t\tif ( this._grip !== null ) {\n\n\t\t\tthis._grip.dispatchEvent( event );\n\n\t\t}\n\n\t\tif ( this._hand !== null ) {\n\n\t\t\tthis._hand.dispatchEvent( event );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Connects the controller with the given XR input source.\n\t *\n\t * @param {XRInputSource} inputSource - The input source.\n\t * @return {WebXRController} A reference to this instance.\n\t */\n\tconnect( inputSource ) {\n\n\t\tif ( inputSource && inputSource.hand ) {\n\n\t\t\tconst hand = this._hand;\n\n\t\t\tif ( hand ) {\n\n\t\t\t\tfor ( const inputjoint of inputSource.hand.values() ) {\n\n\t\t\t\t\t// Initialize hand with joints when connected\n\t\t\t\t\tthis._getHandJoint( hand, inputjoint );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis.dispatchEvent( { type: 'connected', data: inputSource } );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Disconnects the controller from the given XR input source.\n\t *\n\t * @param {XRInputSource} inputSource - The input source.\n\t * @return {WebXRController} A reference to this instance.\n\t */\n\tdisconnect( inputSource ) {\n\n\t\tthis.dispatchEvent( { type: 'disconnected', data: inputSource } );\n\n\t\tif ( this._targetRay !== null ) {\n\n\t\t\tthis._targetRay.visible = false;\n\n\t\t}\n\n\t\tif ( this._grip !== null ) {\n\n\t\t\tthis._grip.visible = false;\n\n\t\t}\n\n\t\tif ( this._hand !== null ) {\n\n\t\t\tthis._hand.visible = false;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Updates the controller with the given input source, XR frame and reference space.\n\t * This updates the transformations of the groups that represent the different\n\t * coordinate systems of the controller.\n\t *\n\t * @param {XRInputSource} inputSource - The input source.\n\t * @param {XRFrame} frame - The XR frame.\n\t * @param {XRReferenceSpace} referenceSpace - The reference space.\n\t * @return {WebXRController} A reference to this instance.\n\t */\n\tupdate( inputSource, frame, referenceSpace ) {\n\n\t\tlet inputPose = null;\n\t\tlet gripPose = null;\n\t\tlet handPose = null;\n\n\t\tconst targetRay = this._targetRay;\n\t\tconst grip = this._grip;\n\t\tconst hand = this._hand;\n\n\t\tif ( inputSource && frame.session.visibilityState !== 'visible-blurred' ) {\n\n\t\t\tif ( hand && inputSource.hand ) {\n\n\t\t\t\thandPose = true;\n\n\t\t\t\tfor ( const inputjoint of inputSource.hand.values() ) {\n\n\t\t\t\t\t// Update the joints groups with the XRJoint poses\n\t\t\t\t\tconst jointPose = frame.getJointPose( inputjoint, referenceSpace );\n\n\t\t\t\t\t// The transform of this joint will be updated with the joint pose on each frame\n\t\t\t\t\tconst joint = this._getHandJoint( hand, inputjoint );\n\n\t\t\t\t\tif ( jointPose !== null ) {\n\n\t\t\t\t\t\tjoint.matrix.fromArray( jointPose.transform.matrix );\n\t\t\t\t\t\tjoint.matrix.decompose( joint.position, joint.rotation, joint.scale );\n\t\t\t\t\t\tjoint.matrixWorldNeedsUpdate = true;\n\t\t\t\t\t\tjoint.jointRadius = jointPose.radius;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tjoint.visible = jointPose !== null;\n\n\t\t\t\t}\n\n\t\t\t\t// Custom events\n\n\t\t\t\t// Check pinchz\n\t\t\t\tconst indexTip = hand.joints[ 'index-finger-tip' ];\n\t\t\t\tconst thumbTip = hand.joints[ 'thumb-tip' ];\n\t\t\t\tconst distance = indexTip.position.distanceTo( thumbTip.position );\n\n\t\t\t\tconst distanceToPinch = 0.02;\n\t\t\t\tconst threshold = 0.005;\n\n\t\t\t\tif ( hand.inputState.pinching && distance > distanceToPinch + threshold ) {\n\n\t\t\t\t\thand.inputState.pinching = false;\n\t\t\t\t\tthis.dispatchEvent( {\n\t\t\t\t\t\ttype: 'pinchend',\n\t\t\t\t\t\thandedness: inputSource.handedness,\n\t\t\t\t\t\ttarget: this\n\t\t\t\t\t} );\n\n\t\t\t\t} else if ( ! hand.inputState.pinching && distance <= distanceToPinch - threshold ) {\n\n\t\t\t\t\thand.inputState.pinching = true;\n\t\t\t\t\tthis.dispatchEvent( {\n\t\t\t\t\t\ttype: 'pinchstart',\n\t\t\t\t\t\thandedness: inputSource.handedness,\n\t\t\t\t\t\ttarget: this\n\t\t\t\t\t} );\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tif ( grip !== null && inputSource.gripSpace ) {\n\n\t\t\t\t\tgripPose = frame.getPose( inputSource.gripSpace, referenceSpace );\n\n\t\t\t\t\tif ( gripPose !== null ) {\n\n\t\t\t\t\t\tgrip.matrix.fromArray( gripPose.transform.matrix );\n\t\t\t\t\t\tgrip.matrix.decompose( grip.position, grip.rotation, grip.scale );\n\t\t\t\t\t\tgrip.matrixWorldNeedsUpdate = true;\n\n\t\t\t\t\t\tif ( gripPose.linearVelocity ) {\n\n\t\t\t\t\t\t\tgrip.hasLinearVelocity = true;\n\t\t\t\t\t\t\tgrip.linearVelocity.copy( gripPose.linearVelocity );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tgrip.hasLinearVelocity = false;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( gripPose.angularVelocity ) {\n\n\t\t\t\t\t\t\tgrip.hasAngularVelocity = true;\n\t\t\t\t\t\t\tgrip.angularVelocity.copy( gripPose.angularVelocity );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tgrip.hasAngularVelocity = false;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( targetRay !== null ) {\n\n\t\t\t\tinputPose = frame.getPose( inputSource.targetRaySpace, referenceSpace );\n\n\t\t\t\t// Some runtimes (namely Vive Cosmos with Vive OpenXR Runtime) have only grip space and ray space is equal to it\n\t\t\t\tif ( inputPose === null && gripPose !== null ) {\n\n\t\t\t\t\tinputPose = gripPose;\n\n\t\t\t\t}\n\n\t\t\t\tif ( inputPose !== null ) {\n\n\t\t\t\t\ttargetRay.matrix.fromArray( inputPose.transform.matrix );\n\t\t\t\t\ttargetRay.matrix.decompose( targetRay.position, targetRay.rotation, targetRay.scale );\n\t\t\t\t\ttargetRay.matrixWorldNeedsUpdate = true;\n\n\t\t\t\t\tif ( inputPose.linearVelocity ) {\n\n\t\t\t\t\t\ttargetRay.hasLinearVelocity = true;\n\t\t\t\t\t\ttargetRay.linearVelocity.copy( inputPose.linearVelocity );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\ttargetRay.hasLinearVelocity = false;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( inputPose.angularVelocity ) {\n\n\t\t\t\t\t\ttargetRay.hasAngularVelocity = true;\n\t\t\t\t\t\ttargetRay.angularVelocity.copy( inputPose.angularVelocity );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\ttargetRay.hasAngularVelocity = false;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.dispatchEvent( _moveEvent );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\n\t\t}\n\n\t\tif ( targetRay !== null ) {\n\n\t\t\ttargetRay.visible = ( inputPose !== null );\n\n\t\t}\n\n\t\tif ( grip !== null ) {\n\n\t\t\tgrip.visible = ( gripPose !== null );\n\n\t\t}\n\n\t\tif ( hand !== null ) {\n\n\t\t\thand.visible = ( handPose !== null );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a group representing the hand joint for the given input joint.\n\t *\n\t * @private\n\t * @param {Group} hand - The group representing the hand space.\n\t * @param {XRJointSpace} inputjoint - The hand joint data.\n\t * @return {Group} A group representing the hand joint for the given input joint.\n\t */\n\t_getHandJoint( hand, inputjoint ) {\n\n\t\tif ( hand.joints[ inputjoint.jointName ] === undefined ) {\n\n\t\t\tconst joint = new Group();\n\t\t\tjoint.matrixAutoUpdate = false;\n\t\t\tjoint.visible = false;\n\t\t\thand.joints[ inputjoint.jointName ] = joint;\n\n\t\t\thand.add( joint );\n\n\t\t}\n\n\t\treturn hand.joints[ inputjoint.jointName ];\n\n\t}\n\n}\n\n/**\n * This class can be used to define an exponential squared fog,\n * which gives a clear view near the camera and a faster than exponentially\n * densening fog farther from the camera.\n *\n * ```js\n * const scene = new THREE.Scene();\n * scene.fog = new THREE.FogExp2( 0xcccccc, 0.002 );\n * ```\n */\nclass FogExp2 {\n\n\t/**\n\t * Constructs a new fog.\n\t *\n\t * @param {number|Color} color - The fog's color.\n\t * @param {number} [density=0.00025] - Defines how fast the fog will grow dense.\n\t */\n\tconstructor( color, density = 0.00025 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isFogExp2 = true;\n\n\t\t/**\n\t\t * The name of the fog.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\n\t\t/**\n\t\t * The fog's color.\n\t\t *\n\t\t * @type {Color}\n\t\t */\n\t\tthis.color = new Color( color );\n\n\t\t/**\n\t\t *  Defines how fast the fog will grow dense.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0.00025\n\t\t */\n\t\tthis.density = density;\n\n\t}\n\n\t/**\n\t * Returns a new fog with copied values from this instance.\n\t *\n\t * @return {FogExp2} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new FogExp2( this.color, this.density );\n\n\t}\n\n\t/**\n\t * Serializes the fog into JSON.\n\t *\n\t * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.\n\t * @return {Object} A JSON object representing the serialized fog\n\t */\n\ttoJSON( /* meta */ ) {\n\n\t\treturn {\n\t\t\ttype: 'FogExp2',\n\t\t\tname: this.name,\n\t\t\tcolor: this.color.getHex(),\n\t\t\tdensity: this.density\n\t\t};\n\n\t}\n\n}\n\n/**\n * This class can be used to define a linear fog that grows linearly denser\n * with the distance.\n *\n * ```js\n * const scene = new THREE.Scene();\n * scene.fog = new THREE.Fog( 0xcccccc, 10, 15 );\n * ```\n */\nclass Fog {\n\n\t/**\n\t * Constructs a new fog.\n\t *\n\t * @param {number|Color} color - The fog's color.\n\t * @param {number} [near=1] - The minimum distance to start applying fog.\n\t * @param {number} [far=1000] - The maximum distance at which fog stops being calculated and applied.\n\t */\n\tconstructor( color, near = 1, far = 1000 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isFog = true;\n\n\t\t/**\n\t\t * The name of the fog.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\n\t\t/**\n\t\t * The fog's color.\n\t\t *\n\t\t * @type {Color}\n\t\t */\n\t\tthis.color = new Color( color );\n\n\t\t/**\n\t\t * The minimum distance to start applying fog. Objects that are less than\n\t\t * `near` units from the active camera won't be affected by fog.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.near = near;\n\n\t\t/**\n\t\t * The maximum distance at which fog stops being calculated and applied.\n\t\t * Objects that are more than `far` units away from the active camera won't\n\t\t * be affected by fog.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1000\n\t\t */\n\t\tthis.far = far;\n\n\t}\n\n\t/**\n\t * Returns a new fog with copied values from this instance.\n\t *\n\t * @return {Fog} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new Fog( this.color, this.near, this.far );\n\n\t}\n\n\t/**\n\t * Serializes the fog into JSON.\n\t *\n\t * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.\n\t * @return {Object} A JSON object representing the serialized fog\n\t */\n\ttoJSON( /* meta */ ) {\n\n\t\treturn {\n\t\t\ttype: 'Fog',\n\t\t\tname: this.name,\n\t\t\tcolor: this.color.getHex(),\n\t\t\tnear: this.near,\n\t\t\tfar: this.far\n\t\t};\n\n\t}\n\n}\n\n/**\n * Scenes allow you to set up what is to be rendered and where by three.js.\n * This is where you place 3D objects like meshes, lines or lights.\n *\n * @augments Object3D\n */\nclass Scene extends Object3D {\n\n\t/**\n\t * Constructs a new scene.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isScene = true;\n\n\t\tthis.type = 'Scene';\n\n\t\t/**\n\t\t * Defines the background of the scene. Valid inputs are:\n\t\t *\n\t\t * - A color for defining a uniform colored background.\n\t\t * - A texture for defining a (flat) textured background.\n\t\t * - Cube textures or equirectangular textures for defining a skybox.\n\t\t *\n\t\t * @type {?(Color|Texture)}\n\t\t * @default null\n\t\t */\n\t\tthis.background = null;\n\n\t\t/**\n\t\t * Sets the environment map for all physical materials in the scene. However,\n\t\t * it's not possible to overwrite an existing texture assigned to the `envMap`\n\t\t * material property.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.environment = null;\n\n\t\t/**\n\t\t * A fog instance defining the type of fog that affects everything\n\t\t * rendered in the scene.\n\t\t *\n\t\t * @type {?(Fog|FogExp2)}\n\t\t * @default null\n\t\t */\n\t\tthis.fog = null;\n\n\t\t/**\n\t\t * Sets the blurriness of the background. Only influences environment maps\n\t\t * assigned to {@link Scene#background}. Valid input is a float between `0`\n\t\t * and `1`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.backgroundBlurriness = 0;\n\n\t\t/**\n\t\t * Attenuates the color of the background. Only applies to background textures.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.backgroundIntensity = 1;\n\n\t\t/**\n\t\t * The rotation of the background in radians. Only influences environment maps\n\t\t * assigned to {@link Scene#background}.\n\t\t *\n\t\t * @type {Euler}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.backgroundRotation = new Euler();\n\n\t\t/**\n\t\t * Attenuates the color of the environment. Only influences environment maps\n\t\t * assigned to {@link Scene#environment}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.environmentIntensity = 1;\n\n\t\t/**\n\t\t * The rotation of the environment map in radians. Only influences physical materials\n\t\t * in the scene when {@link Scene#environment} is used.\n\t\t *\n\t\t * @type {Euler}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.environmentRotation = new Euler();\n\n\t\t/**\n\t\t * Forces everything in the scene to be rendered with the defined material. It is possible\n\t\t * to exclude materials from override by setting {@link Material#allowOverride} to `false`.\n\t\t *\n\t\t * @type {?Material}\n\t\t * @default null\n\t\t */\n\t\tthis.overrideMaterial = null;\n\n\t\tif ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {\n\n\t\t\t__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );\n\n\t\t}\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tif ( source.background !== null ) this.background = source.background.clone();\n\t\tif ( source.environment !== null ) this.environment = source.environment.clone();\n\t\tif ( source.fog !== null ) this.fog = source.fog.clone();\n\n\t\tthis.backgroundBlurriness = source.backgroundBlurriness;\n\t\tthis.backgroundIntensity = source.backgroundIntensity;\n\t\tthis.backgroundRotation.copy( source.backgroundRotation );\n\n\t\tthis.environmentIntensity = source.environmentIntensity;\n\t\tthis.environmentRotation.copy( source.environmentRotation );\n\n\t\tif ( source.overrideMaterial !== null ) this.overrideMaterial = source.overrideMaterial.clone();\n\n\t\tthis.matrixAutoUpdate = source.matrixAutoUpdate;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tif ( this.fog !== null ) data.object.fog = this.fog.toJSON();\n\n\t\tif ( this.backgroundBlurriness > 0 ) data.object.backgroundBlurriness = this.backgroundBlurriness;\n\t\tif ( this.backgroundIntensity !== 1 ) data.object.backgroundIntensity = this.backgroundIntensity;\n\t\tdata.object.backgroundRotation = this.backgroundRotation.toArray();\n\n\t\tif ( this.environmentIntensity !== 1 ) data.object.environmentIntensity = this.environmentIntensity;\n\t\tdata.object.environmentRotation = this.environmentRotation.toArray();\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * \"Interleaved\" means that multiple attributes, possibly of different types,\n * (e.g., position, normal, uv, color) are packed into a single array buffer.\n *\n * An introduction into interleaved arrays can be found here: [Interleaved array basics](https://blog.tojicode.com/2011/05/interleaved-array-basics.html)\n */\nclass InterleavedBuffer {\n\n\t/**\n\t * Constructs a new interleaved buffer.\n\t *\n\t * @param {TypedArray} array - A typed array with a shared buffer storing attribute data.\n\t * @param {number} stride - The number of typed-array elements per vertex.\n\t */\n\tconstructor( array, stride ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isInterleavedBuffer = true;\n\n\t\t/**\n\t\t * A typed array with a shared buffer storing attribute data.\n\t\t *\n\t\t * @type {TypedArray}\n\t\t */\n\t\tthis.array = array;\n\n\t\t/**\n\t\t * The number of typed-array elements per vertex.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.stride = stride;\n\n\t\t/**\n\t\t * The total number of elements in the array\n\t\t *\n\t\t * @type {number}\n\t\t * @readonly\n\t\t */\n\t\tthis.count = array !== undefined ? array.length / stride : 0;\n\n\t\t/**\n\t\t * Defines the intended usage pattern of the data store for optimization purposes.\n\t\t *\n\t\t * Note: After the initial use of a buffer, its usage cannot be changed. Instead,\n\t\t * instantiate a new one and set the desired usage before the next render.\n\t\t *\n\t\t * @type {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)}\n\t\t * @default StaticDrawUsage\n\t\t */\n\t\tthis.usage = StaticDrawUsage;\n\n\t\t/**\n\t\t * This can be used to only update some components of stored vectors (for example, just the\n\t\t * component related to color). Use the `addUpdateRange()` function to add ranges to this array.\n\t\t *\n\t\t * @type {Array<Object>}\n\t\t */\n\t\tthis.updateRanges = [];\n\n\t\t/**\n\t\t * A version number, incremented every time the `needsUpdate` is set to `true`.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.version = 0;\n\n\t\t/**\n\t\t * The UUID of the interleaved buffer.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.uuid = generateUUID();\n\n\t}\n\n\t/**\n\t * A callback function that is executed after the renderer has transferred the attribute array\n\t * data to the GPU.\n\t */\n\tonUploadCallback() {}\n\n\t/**\n\t * Flag to indicate that this attribute has changed and should be re-sent to\n\t * the GPU. Set this to `true` when you modify the value of the array.\n\t *\n\t * @type {number}\n\t * @default false\n\t * @param {boolean} value\n\t */\n\tset needsUpdate( value ) {\n\n\t\tif ( value === true ) this.version ++;\n\n\t}\n\n\t/**\n\t * Sets the usage of this interleaved buffer.\n\t *\n\t * @param {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)} value - The usage to set.\n\t * @return {InterleavedBuffer} A reference to this interleaved buffer.\n\t */\n\tsetUsage( value ) {\n\n\t\tthis.usage = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds a range of data in the data array to be updated on the GPU.\n\t *\n\t * @param {number} start - Position at which to start update.\n\t * @param {number} count - The number of components to update.\n\t */\n\taddUpdateRange( start, count ) {\n\n\t\tthis.updateRanges.push( { start, count } );\n\n\t}\n\n\t/**\n\t * Clears the update ranges.\n\t */\n\tclearUpdateRanges() {\n\n\t\tthis.updateRanges.length = 0;\n\n\t}\n\n\t/**\n\t * Copies the values of the given interleaved buffer to this instance.\n\t *\n\t * @param {InterleavedBuffer} source - The interleaved buffer to copy.\n\t * @return {InterleavedBuffer} A reference to this instance.\n\t */\n\tcopy( source ) {\n\n\t\tthis.array = new source.array.constructor( source.array );\n\t\tthis.count = source.count;\n\t\tthis.stride = source.stride;\n\t\tthis.usage = source.usage;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies a vector from the given interleaved buffer to this one. The start\n\t * and destination position in the attribute buffers are represented by the\n\t * given indices.\n\t *\n\t * @param {number} index1 - The destination index into this interleaved buffer.\n\t * @param {InterleavedBuffer} interleavedBuffer - The interleaved buffer to copy from.\n\t * @param {number} index2 - The source index into the given interleaved buffer.\n\t * @return {InterleavedBuffer} A reference to this instance.\n\t */\n\tcopyAt( index1, interleavedBuffer, index2 ) {\n\n\t\tindex1 *= this.stride;\n\t\tindex2 *= interleavedBuffer.stride;\n\n\t\tfor ( let i = 0, l = this.stride; i < l; i ++ ) {\n\n\t\t\tthis.array[ index1 + i ] = interleavedBuffer.array[ index2 + i ];\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given array data in the interleaved buffer.\n\t *\n\t * @param {(TypedArray|Array)} value - The array data to set.\n\t * @param {number} [offset=0] - The offset in this interleaved buffer's array.\n\t * @return {InterleavedBuffer} A reference to this instance.\n\t */\n\tset( value, offset = 0 ) {\n\n\t\tthis.array.set( value, offset );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new interleaved buffer with copied values from this instance.\n\t *\n\t * @param {Object} [data] - An object with shared array buffers that allows to retain shared structures.\n\t * @return {InterleavedBuffer} A clone of this instance.\n\t */\n\tclone( data ) {\n\n\t\tif ( data.arrayBuffers === undefined ) {\n\n\t\t\tdata.arrayBuffers = {};\n\n\t\t}\n\n\t\tif ( this.array.buffer._uuid === undefined ) {\n\n\t\t\tthis.array.buffer._uuid = generateUUID();\n\n\t\t}\n\n\t\tif ( data.arrayBuffers[ this.array.buffer._uuid ] === undefined ) {\n\n\t\t\tdata.arrayBuffers[ this.array.buffer._uuid ] = this.array.slice( 0 ).buffer;\n\n\t\t}\n\n\t\tconst array = new this.array.constructor( data.arrayBuffers[ this.array.buffer._uuid ] );\n\n\t\tconst ib = new this.constructor( array, this.stride );\n\t\tib.setUsage( this.usage );\n\n\t\treturn ib;\n\n\t}\n\n\t/**\n\t * Sets the given callback function that is executed after the Renderer has transferred\n\t * the array data to the GPU. Can be used to perform clean-up operations after\n\t * the upload when data are not needed anymore on the CPU side.\n\t *\n\t * @param {Function} callback - The `onUpload()` callback.\n\t * @return {InterleavedBuffer} A reference to this instance.\n\t */\n\tonUpload( callback ) {\n\n\t\tthis.onUploadCallback = callback;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Serializes the interleaved buffer into JSON.\n\t *\n\t * @param {Object} [data] - An optional value holding meta information about the serialization.\n\t * @return {Object} A JSON object representing the serialized interleaved buffer.\n\t */\n\ttoJSON( data ) {\n\n\t\tif ( data.arrayBuffers === undefined ) {\n\n\t\t\tdata.arrayBuffers = {};\n\n\t\t}\n\n\t\t// generate UUID for array buffer if necessary\n\n\t\tif ( this.array.buffer._uuid === undefined ) {\n\n\t\t\tthis.array.buffer._uuid = generateUUID();\n\n\t\t}\n\n\t\tif ( data.arrayBuffers[ this.array.buffer._uuid ] === undefined ) {\n\n\t\t\tdata.arrayBuffers[ this.array.buffer._uuid ] = Array.from( new Uint32Array( this.array.buffer ) );\n\n\t\t}\n\n\t\t//\n\n\t\treturn {\n\t\t\tuuid: this.uuid,\n\t\t\tbuffer: this.array.buffer._uuid,\n\t\t\ttype: this.array.constructor.name,\n\t\t\tstride: this.stride\n\t\t};\n\n\t}\n\n}\n\nconst _vector$7 = /*@__PURE__*/ new Vector3();\n\n/**\n * An alternative version of a buffer attribute with interleaved data. Interleaved\n * attributes share a common interleaved data storage ({@link InterleavedBuffer}) and refer with\n * different offsets into the buffer.\n */\nclass InterleavedBufferAttribute {\n\n\t/**\n\t * Constructs a new interleaved buffer attribute.\n\t *\n\t * @param {InterleavedBuffer} interleavedBuffer - The buffer holding the interleaved data.\n\t * @param {number} itemSize - The item size.\n\t * @param {number} offset - The attribute offset into the buffer.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( interleavedBuffer, itemSize, offset, normalized = false ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isInterleavedBufferAttribute = true;\n\n\t\t/**\n\t\t * The name of the buffer attribute.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\n\t\t/**\n\t\t * The buffer holding the interleaved data.\n\t\t *\n\t\t * @type {InterleavedBuffer}\n\t\t */\n\t\tthis.data = interleavedBuffer;\n\n\t\t/**\n\t\t * The item size, see {@link BufferAttribute#itemSize}.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.itemSize = itemSize;\n\n\t\t/**\n\t\t * The attribute offset into the buffer.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.offset = offset;\n\n\t\t/**\n\t\t * Whether the data are normalized or not, see {@link BufferAttribute#normalized}\n\t\t *\n\t\t * @type {InterleavedBuffer}\n\t\t */\n\t\tthis.normalized = normalized;\n\n\t}\n\n\t/**\n\t * The item count of this buffer attribute.\n\t *\n\t * @type {number}\n\t * @readonly\n\t */\n\tget count() {\n\n\t\treturn this.data.count;\n\n\t}\n\n\t/**\n\t * The array holding the interleaved buffer attribute data.\n\t *\n\t * @type {TypedArray}\n\t */\n\tget array() {\n\n\t\treturn this.data.array;\n\n\t}\n\n\t/**\n\t * Flag to indicate that this attribute has changed and should be re-sent to\n\t * the GPU. Set this to `true` when you modify the value of the array.\n\t *\n\t * @type {number}\n\t * @default false\n\t * @param {boolean} value\n\t */\n\tset needsUpdate( value ) {\n\n\t\tthis.data.needsUpdate = value;\n\n\t}\n\n\t/**\n\t * Applies the given 4x4 matrix to the given attribute. Only works with\n\t * item size `3`.\n\t *\n\t * @param {Matrix4} m - The matrix to apply.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tapplyMatrix4( m ) {\n\n\t\tfor ( let i = 0, l = this.data.count; i < l; i ++ ) {\n\n\t\t\t_vector$7.fromBufferAttribute( this, i );\n\n\t\t\t_vector$7.applyMatrix4( m );\n\n\t\t\tthis.setXYZ( i, _vector$7.x, _vector$7.y, _vector$7.z );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Applies the given 3x3 normal matrix to the given attribute. Only works with\n\t * item size `3`.\n\t *\n\t * @param {Matrix3} m - The normal matrix to apply.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tapplyNormalMatrix( m ) {\n\n\t\tfor ( let i = 0, l = this.count; i < l; i ++ ) {\n\n\t\t\t_vector$7.fromBufferAttribute( this, i );\n\n\t\t\t_vector$7.applyNormalMatrix( m );\n\n\t\t\tthis.setXYZ( i, _vector$7.x, _vector$7.y, _vector$7.z );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Applies the given 4x4 matrix to the given attribute. Only works with\n\t * item size `3` and with direction vectors.\n\t *\n\t * @param {Matrix4} m - The matrix to apply.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\ttransformDirection( m ) {\n\n\t\tfor ( let i = 0, l = this.count; i < l; i ++ ) {\n\n\t\t\t_vector$7.fromBufferAttribute( this, i );\n\n\t\t\t_vector$7.transformDirection( m );\n\n\t\t\tthis.setXYZ( i, _vector$7.x, _vector$7.y, _vector$7.z );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the given component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} component - The component index.\n\t * @return {number} The returned value.\n\t */\n\tgetComponent( index, component ) {\n\n\t\tlet value = this.array[ index * this.data.stride + this.offset + component ];\n\n\t\tif ( this.normalized ) value = denormalize( value, this.array );\n\n\t\treturn value;\n\n\t}\n\n\t/**\n\t * Sets the given value to the given component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} component - The component index.\n\t * @param {number} value - The value to set.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tsetComponent( index, component, value ) {\n\n\t\tif ( this.normalized ) value = normalize( value, this.array );\n\n\t\tthis.data.array[ index * this.data.stride + this.offset + component ] = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the x component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} x - The value to set.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tsetX( index, x ) {\n\n\t\tif ( this.normalized ) x = normalize( x, this.array );\n\n\t\tthis.data.array[ index * this.data.stride + this.offset ] = x;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the y component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} y - The value to set.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tsetY( index, y ) {\n\n\t\tif ( this.normalized ) y = normalize( y, this.array );\n\n\t\tthis.data.array[ index * this.data.stride + this.offset + 1 ] = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the z component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} z - The value to set.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tsetZ( index, z ) {\n\n\t\tif ( this.normalized ) z = normalize( z, this.array );\n\n\t\tthis.data.array[ index * this.data.stride + this.offset + 2 ] = z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the w component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} w - The value to set.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tsetW( index, w ) {\n\n\t\tif ( this.normalized ) w = normalize( w, this.array );\n\n\t\tthis.data.array[ index * this.data.stride + this.offset + 3 ] = w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the x component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @return {number} The x component.\n\t */\n\tgetX( index ) {\n\n\t\tlet x = this.data.array[ index * this.data.stride + this.offset ];\n\n\t\tif ( this.normalized ) x = denormalize( x, this.array );\n\n\t\treturn x;\n\n\t}\n\n\t/**\n\t * Returns the y component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @return {number} The y component.\n\t */\n\tgetY( index ) {\n\n\t\tlet y = this.data.array[ index * this.data.stride + this.offset + 1 ];\n\n\t\tif ( this.normalized ) y = denormalize( y, this.array );\n\n\t\treturn y;\n\n\t}\n\n\t/**\n\t * Returns the z component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @return {number} The z component.\n\t */\n\tgetZ( index ) {\n\n\t\tlet z = this.data.array[ index * this.data.stride + this.offset + 2 ];\n\n\t\tif ( this.normalized ) z = denormalize( z, this.array );\n\n\t\treturn z;\n\n\t}\n\n\t/**\n\t * Returns the w component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @return {number} The w component.\n\t */\n\tgetW( index ) {\n\n\t\tlet w = this.data.array[ index * this.data.stride + this.offset + 3 ];\n\n\t\tif ( this.normalized ) w = denormalize( w, this.array );\n\n\t\treturn w;\n\n\t}\n\n\t/**\n\t * Sets the x and y component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} x - The value for the x component to set.\n\t * @param {number} y - The value for the y component to set.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tsetXY( index, x, y ) {\n\n\t\tindex = index * this.data.stride + this.offset;\n\n\t\tif ( this.normalized ) {\n\n\t\t\tx = normalize( x, this.array );\n\t\t\ty = normalize( y, this.array );\n\n\t\t}\n\n\t\tthis.data.array[ index + 0 ] = x;\n\t\tthis.data.array[ index + 1 ] = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the x, y and z component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} x - The value for the x component to set.\n\t * @param {number} y - The value for the y component to set.\n\t * @param {number} z - The value for the z component to set.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tsetXYZ( index, x, y, z ) {\n\n\t\tindex = index * this.data.stride + this.offset;\n\n\t\tif ( this.normalized ) {\n\n\t\t\tx = normalize( x, this.array );\n\t\t\ty = normalize( y, this.array );\n\t\t\tz = normalize( z, this.array );\n\n\t\t}\n\n\t\tthis.data.array[ index + 0 ] = x;\n\t\tthis.data.array[ index + 1 ] = y;\n\t\tthis.data.array[ index + 2 ] = z;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the x, y, z and w component of the vector at the given index.\n\t *\n\t * @param {number} index - The index into the buffer attribute.\n\t * @param {number} x - The value for the x component to set.\n\t * @param {number} y - The value for the y component to set.\n\t * @param {number} z - The value for the z component to set.\n\t * @param {number} w - The value for the w component to set.\n\t * @return {InterleavedBufferAttribute} A reference to this instance.\n\t */\n\tsetXYZW( index, x, y, z, w ) {\n\n\t\tindex = index * this.data.stride + this.offset;\n\n\t\tif ( this.normalized ) {\n\n\t\t\tx = normalize( x, this.array );\n\t\t\ty = normalize( y, this.array );\n\t\t\tz = normalize( z, this.array );\n\t\t\tw = normalize( w, this.array );\n\n\t\t}\n\n\t\tthis.data.array[ index + 0 ] = x;\n\t\tthis.data.array[ index + 1 ] = y;\n\t\tthis.data.array[ index + 2 ] = z;\n\t\tthis.data.array[ index + 3 ] = w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new buffer attribute with copied values from this instance.\n\t *\n\t * If no parameter is provided, cloning an interleaved buffer attribute will de-interleave buffer data.\n\t *\n\t * @param {Object} [data] - An object with interleaved buffers that allows to retain the interleaved property.\n\t * @return {BufferAttribute|InterleavedBufferAttribute} A clone of this instance.\n\t */\n\tclone( data ) {\n\n\t\tif ( data === undefined ) {\n\n\t\t\tlog( 'InterleavedBufferAttribute.clone(): Cloning an interleaved buffer attribute will de-interleave buffer data.' );\n\n\t\t\tconst array = [];\n\n\t\t\tfor ( let i = 0; i < this.count; i ++ ) {\n\n\t\t\t\tconst index = i * this.data.stride + this.offset;\n\n\t\t\t\tfor ( let j = 0; j < this.itemSize; j ++ ) {\n\n\t\t\t\t\tarray.push( this.data.array[ index + j ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn new BufferAttribute( new this.array.constructor( array ), this.itemSize, this.normalized );\n\n\t\t} else {\n\n\t\t\tif ( data.interleavedBuffers === undefined ) {\n\n\t\t\t\tdata.interleavedBuffers = {};\n\n\t\t\t}\n\n\t\t\tif ( data.interleavedBuffers[ this.data.uuid ] === undefined ) {\n\n\t\t\t\tdata.interleavedBuffers[ this.data.uuid ] = this.data.clone( data );\n\n\t\t\t}\n\n\t\t\treturn new InterleavedBufferAttribute( data.interleavedBuffers[ this.data.uuid ], this.itemSize, this.offset, this.normalized );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Serializes the buffer attribute into JSON.\n\t *\n\t * If no parameter is provided, cloning an interleaved buffer attribute will de-interleave buffer data.\n\t *\n\t * @param {Object} [data] - An optional value holding meta information about the serialization.\n\t * @return {Object} A JSON object representing the serialized buffer attribute.\n\t */\n\ttoJSON( data ) {\n\n\t\tif ( data === undefined ) {\n\n\t\t\tlog( 'InterleavedBufferAttribute.toJSON(): Serializing an interleaved buffer attribute will de-interleave buffer data.' );\n\n\t\t\tconst array = [];\n\n\t\t\tfor ( let i = 0; i < this.count; i ++ ) {\n\n\t\t\t\tconst index = i * this.data.stride + this.offset;\n\n\t\t\t\tfor ( let j = 0; j < this.itemSize; j ++ ) {\n\n\t\t\t\t\tarray.push( this.data.array[ index + j ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// de-interleave data and save it as an ordinary buffer attribute for now\n\n\t\t\treturn {\n\t\t\t\titemSize: this.itemSize,\n\t\t\t\ttype: this.array.constructor.name,\n\t\t\t\tarray: array,\n\t\t\t\tnormalized: this.normalized\n\t\t\t};\n\n\t\t} else {\n\n\t\t\t// save as true interleaved attribute\n\n\t\t\tif ( data.interleavedBuffers === undefined ) {\n\n\t\t\t\tdata.interleavedBuffers = {};\n\n\t\t\t}\n\n\t\t\tif ( data.interleavedBuffers[ this.data.uuid ] === undefined ) {\n\n\t\t\t\tdata.interleavedBuffers[ this.data.uuid ] = this.data.toJSON( data );\n\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tisInterleavedBufferAttribute: true,\n\t\t\t\titemSize: this.itemSize,\n\t\t\t\tdata: this.data.uuid,\n\t\t\t\toffset: this.offset,\n\t\t\t\tnormalized: this.normalized\n\t\t\t};\n\n\t\t}\n\n\t}\n\n}\n\n/**\n * A material for rendering instances of {@link Sprite}.\n *\n * ```js\n * const map = new THREE.TextureLoader().load( 'textures/sprite.png' );\n * const material = new THREE.SpriteMaterial( { map: map, color: 0xffffff } );\n *\n * const sprite = new THREE.Sprite( material );\n * sprite.scale.set(200, 200, 1)\n * scene.add( sprite );\n * ```\n *\n * @augments Material\n */\nclass SpriteMaterial extends Material {\n\n\t/**\n\t * Constructs a new sprite material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSpriteMaterial = true;\n\n\t\tthis.type = 'SpriteMaterial';\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.color = new Color( 0xffffff );\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map\n\t\t * color is modulated by the diffuse `color`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * The rotation of the sprite in radians.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.rotation = 0;\n\n\t\t/**\n\t\t * Specifies whether size of the sprite is attenuated by the camera depth (perspective camera only).\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.sizeAttenuation = true;\n\n\t\t/**\n\t\t * Overwritten since sprite materials are transparent\n\t\t * by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.transparent = true;\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.color.copy( source.color );\n\n\t\tthis.map = source.map;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.rotation = source.rotation;\n\n\t\tthis.sizeAttenuation = source.sizeAttenuation;\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\nlet _geometry;\n\nconst _intersectPoint = /*@__PURE__*/ new Vector3();\nconst _worldScale = /*@__PURE__*/ new Vector3();\nconst _mvPosition = /*@__PURE__*/ new Vector3();\n\nconst _alignedPosition = /*@__PURE__*/ new Vector2();\nconst _rotatedPosition = /*@__PURE__*/ new Vector2();\nconst _viewWorldMatrix = /*@__PURE__*/ new Matrix4();\n\nconst _vA = /*@__PURE__*/ new Vector3();\nconst _vB = /*@__PURE__*/ new Vector3();\nconst _vC = /*@__PURE__*/ new Vector3();\n\nconst _uvA = /*@__PURE__*/ new Vector2();\nconst _uvB = /*@__PURE__*/ new Vector2();\nconst _uvC = /*@__PURE__*/ new Vector2();\n\n/**\n * A sprite is a plane that always faces towards the camera, generally with a\n * partially transparent texture applied.\n *\n * Sprites do not cast shadows, setting {@link Object3D#castShadow} to `true` will\n * have no effect.\n *\n * ```js\n * const map = new THREE.TextureLoader().load( 'sprite.png' );\n * const material = new THREE.SpriteMaterial( { map: map } );\n *\n * const sprite = new THREE.Sprite( material );\n * scene.add( sprite );\n * ```\n *\n * @augments Object3D\n */\nclass Sprite extends Object3D {\n\n\t/**\n\t * Constructs a new sprite.\n\t *\n\t * @param {(SpriteMaterial|SpriteNodeMaterial)} [material] - The sprite material.\n\t */\n\tconstructor( material = new SpriteMaterial() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSprite = true;\n\n\t\tthis.type = 'Sprite';\n\n\t\tif ( _geometry === undefined ) {\n\n\t\t\t_geometry = new BufferGeometry();\n\n\t\t\tconst float32Array = new Float32Array( [\n\t\t\t\t-0.5, -0.5, 0, 0, 0,\n\t\t\t\t0.5, -0.5, 0, 1, 0,\n\t\t\t\t0.5, 0.5, 0, 1, 1,\n\t\t\t\t-0.5, 0.5, 0, 0, 1\n\t\t\t] );\n\n\t\t\tconst interleavedBuffer = new InterleavedBuffer( float32Array, 5 );\n\n\t\t\t_geometry.setIndex( [ 0, 1, 2,\t0, 2, 3 ] );\n\t\t\t_geometry.setAttribute( 'position', new InterleavedBufferAttribute( interleavedBuffer, 3, 0, false ) );\n\t\t\t_geometry.setAttribute( 'uv', new InterleavedBufferAttribute( interleavedBuffer, 2, 3, false ) );\n\n\t\t}\n\n\t\t/**\n\t\t * The sprite geometry.\n\t\t *\n\t\t * @type {BufferGeometry}\n\t\t */\n\t\tthis.geometry = _geometry;\n\n\t\t/**\n\t\t * The sprite material.\n\t\t *\n\t\t * @type {(SpriteMaterial|SpriteNodeMaterial)}\n\t\t */\n\t\tthis.material = material;\n\n\t\t/**\n\t\t * The sprite's anchor point, and the point around which the sprite rotates.\n\t\t * A value of `(0.5, 0.5)` corresponds to the midpoint of the sprite. A value\n\t\t * of `(0, 0)` corresponds to the lower left corner of the sprite.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (0.5,0.5)\n\t\t */\n\t\tthis.center = new Vector2( 0.5, 0.5 );\n\n\t\t/**\n\t\t * The number of instances of this sprite.\n\t\t * Can only be used with {@link WebGPURenderer}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.count = 1;\n\n\t}\n\n\t/**\n\t * Computes intersection points between a casted ray and this sprite.\n\t *\n\t * @param {Raycaster} raycaster - The raycaster.\n\t * @param {Array<Object>} intersects - The target array that holds the intersection points.\n\t */\n\traycast( raycaster, intersects ) {\n\n\t\tif ( raycaster.camera === null ) {\n\n\t\t\terror( 'Sprite: \"Raycaster.camera\" needs to be set in order to raycast against sprites.' );\n\n\t\t}\n\n\t\t_worldScale.setFromMatrixScale( this.matrixWorld );\n\n\t\t_viewWorldMatrix.copy( raycaster.camera.matrixWorld );\n\t\tthis.modelViewMatrix.multiplyMatrices( raycaster.camera.matrixWorldInverse, this.matrixWorld );\n\n\t\t_mvPosition.setFromMatrixPosition( this.modelViewMatrix );\n\n\t\tif ( raycaster.camera.isPerspectiveCamera && this.material.sizeAttenuation === false ) {\n\n\t\t\t_worldScale.multiplyScalar( - _mvPosition.z );\n\n\t\t}\n\n\t\tconst rotation = this.material.rotation;\n\t\tlet sin, cos;\n\n\t\tif ( rotation !== 0 ) {\n\n\t\t\tcos = Math.cos( rotation );\n\t\t\tsin = Math.sin( rotation );\n\n\t\t}\n\n\t\tconst center = this.center;\n\n\t\ttransformVertex( _vA.set( -0.5, -0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );\n\t\ttransformVertex( _vB.set( 0.5, -0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );\n\t\ttransformVertex( _vC.set( 0.5, 0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );\n\n\t\t_uvA.set( 0, 0 );\n\t\t_uvB.set( 1, 0 );\n\t\t_uvC.set( 1, 1 );\n\n\t\t// check first triangle\n\t\tlet intersect = raycaster.ray.intersectTriangle( _vA, _vB, _vC, false, _intersectPoint );\n\n\t\tif ( intersect === null ) {\n\n\t\t\t// check second triangle\n\t\t\ttransformVertex( _vB.set( -0.5, 0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );\n\t\t\t_uvB.set( 0, 1 );\n\n\t\t\tintersect = raycaster.ray.intersectTriangle( _vA, _vC, _vB, false, _intersectPoint );\n\t\t\tif ( intersect === null ) {\n\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst distance = raycaster.ray.origin.distanceTo( _intersectPoint );\n\n\t\tif ( distance < raycaster.near || distance > raycaster.far ) return;\n\n\t\tintersects.push( {\n\n\t\t\tdistance: distance,\n\t\t\tpoint: _intersectPoint.clone(),\n\t\t\tuv: Triangle.getInterpolation( _intersectPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2() ),\n\t\t\tface: null,\n\t\t\tobject: this\n\n\t\t} );\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tif ( source.center !== undefined ) this.center.copy( source.center );\n\n\t\tthis.material = source.material;\n\n\t\treturn this;\n\n\t}\n\n}\n\nfunction transformVertex( vertexPosition, mvPosition, center, scale, sin, cos ) {\n\n\t// compute position in camera space\n\t_alignedPosition.subVectors( vertexPosition, center ).addScalar( 0.5 ).multiply( scale );\n\n\t// to check if rotation is not zero\n\tif ( sin !== undefined ) {\n\n\t\t_rotatedPosition.x = ( cos * _alignedPosition.x ) - ( sin * _alignedPosition.y );\n\t\t_rotatedPosition.y = ( sin * _alignedPosition.x ) + ( cos * _alignedPosition.y );\n\n\t} else {\n\n\t\t_rotatedPosition.copy( _alignedPosition );\n\n\t}\n\n\n\tvertexPosition.copy( mvPosition );\n\tvertexPosition.x += _rotatedPosition.x;\n\tvertexPosition.y += _rotatedPosition.y;\n\n\t// transform to world space\n\tvertexPosition.applyMatrix4( _viewWorldMatrix );\n\n}\n\nconst _v1$2 = /*@__PURE__*/ new Vector3();\nconst _v2$1 = /*@__PURE__*/ new Vector3();\n\n/**\n * A component for providing a basic Level of Detail (LOD) mechanism.\n *\n * Every LOD level is associated with an object, and rendering can be switched\n * between them at the distances specified. Typically you would create, say,\n * three meshes, one for far away (low detail), one for mid range (medium\n * detail) and one for close up (high detail).\n *\n * ```js\n * const lod = new THREE.LOD();\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n *\n * //Create spheres with 3 levels of detail and create new LOD levels for them\n * for( let i = 0; i < 3; i++ ) {\n *\n * \tconst geometry = new THREE.IcosahedronGeometry( 10, 3 - i );\n * \tconst mesh = new THREE.Mesh( geometry, material );\n * \tlod.addLevel( mesh, i * 75 );\n *\n * }\n *\n * scene.add( lod );\n * ```\n *\n * @augments Object3D\n */\nclass LOD extends Object3D {\n\n\t/**\n\t * Constructs a new LOD.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLOD = true;\n\n\t\t/**\n\t\t * The current LOD index.\n\t\t *\n\t\t * @private\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis._currentLevel = 0;\n\n\t\tthis.type = 'LOD';\n\n\t\tObject.defineProperties( this, {\n\t\t\t/**\n\t\t\t * This array holds the LOD levels.\n\t\t\t *\n\t\t\t * @name LOD#levels\n\t\t\t * @type {Array<{object:Object3D,distance:number,hysteresis:number}>}\n\t\t\t */\n\t\t\tlevels: {\n\t\t\t\tenumerable: true,\n\t\t\t\tvalue: []\n\t\t\t}\n\t\t} );\n\n\t\t/**\n\t\t * Whether the LOD object is updated automatically by the renderer per frame\n\t\t * or not. If set to `false`, you have to call {@link LOD#update} in the\n\t\t * render loop by yourself.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.autoUpdate = true;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source, false );\n\n\t\tconst levels = source.levels;\n\n\t\tfor ( let i = 0, l = levels.length; i < l; i ++ ) {\n\n\t\t\tconst level = levels[ i ];\n\n\t\t\tthis.addLevel( level.object.clone(), level.distance, level.hysteresis );\n\n\t\t}\n\n\t\tthis.autoUpdate = source.autoUpdate;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds a mesh that will display at a certain distance and greater. Typically\n\t * the further away the distance, the lower the detail on the mesh.\n\t *\n\t * @param {Object3D} object - The 3D object to display at this level.\n\t * @param {number} [distance=0] - The distance at which to display this level of detail.\n\t * @param {number} [hysteresis=0] - Threshold used to avoid flickering at LOD boundaries, as a fraction of distance.\n\t * @return {LOD} A reference to this instance.\n\t */\n\taddLevel( object, distance = 0, hysteresis = 0 ) {\n\n\t\tdistance = Math.abs( distance );\n\n\t\tconst levels = this.levels;\n\n\t\tlet l;\n\n\t\tfor ( l = 0; l < levels.length; l ++ ) {\n\n\t\t\tif ( distance < levels[ l ].distance ) {\n\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\n\t\tlevels.splice( l, 0, { distance: distance, hysteresis: hysteresis, object: object } );\n\n\t\tthis.add( object );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Removes an existing level, based on the distance from the camera.\n\t * Returns `true` when the level has been removed. Otherwise `false`.\n\t *\n\t * @param {number} distance - Distance of the level to remove.\n\t * @return {boolean} Whether the level has been removed or not.\n\t */\n\tremoveLevel( distance ) {\n\n\t\tconst levels = this.levels;\n\n\t\tfor ( let i = 0; i < levels.length; i ++ ) {\n\n\t\t\tif ( levels[ i ].distance === distance ) {\n\n\t\t\t\tconst removedElements = levels.splice( i, 1 );\n\t\t\t\tthis.remove( removedElements[ 0 ].object );\n\n\t\t\t\treturn true;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Returns the currently active LOD level index.\n\t *\n\t * @return {number} The current active LOD level index.\n\t */\n\tgetCurrentLevel() {\n\n\t\treturn this._currentLevel;\n\n\t}\n\n\t/**\n\t * Returns a reference to the first 3D object that is greater than\n\t * the given distance.\n\t *\n\t * @param {number} distance - The LOD distance.\n\t * @return {?Object3D} The found 3D object. `null` if no 3D object has been found.\n\t */\n\tgetObjectForDistance( distance ) {\n\n\t\tconst levels = this.levels;\n\n\t\tif ( levels.length > 0 ) {\n\n\t\t\tlet i, l;\n\n\t\t\tfor ( i = 1, l = levels.length; i < l; i ++ ) {\n\n\t\t\t\tlet levelDistance = levels[ i ].distance;\n\n\t\t\t\tif ( levels[ i ].object.visible ) {\n\n\t\t\t\t\tlevelDistance -= levelDistance * levels[ i ].hysteresis;\n\n\t\t\t\t}\n\n\t\t\t\tif ( distance < levelDistance ) {\n\n\t\t\t\t\tbreak;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn levels[ i - 1 ].object;\n\n\t\t}\n\n\t\treturn null;\n\n\t}\n\n\t/**\n\t * Computes intersection points between a casted ray and this LOD.\n\t *\n\t * @param {Raycaster} raycaster - The raycaster.\n\t * @param {Array<Object>} intersects - The target array that holds the intersection points.\n\t */\n\traycast( raycaster, intersects ) {\n\n\t\tconst levels = this.levels;\n\n\t\tif ( levels.length > 0 ) {\n\n\t\t\t_v1$2.setFromMatrixPosition( this.matrixWorld );\n\n\t\t\tconst distance = raycaster.ray.origin.distanceTo( _v1$2 );\n\n\t\t\tthis.getObjectForDistance( distance ).raycast( raycaster, intersects );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Updates the LOD by computing which LOD level should be visible according\n\t * to the current distance of the given camera.\n\t *\n\t * @param {Camera} camera - The camera the scene is rendered with.\n\t */\n\tupdate( camera ) {\n\n\t\tconst levels = this.levels;\n\n\t\tif ( levels.length > 1 ) {\n\n\t\t\t_v1$2.setFromMatrixPosition( camera.matrixWorld );\n\t\t\t_v2$1.setFromMatrixPosition( this.matrixWorld );\n\n\t\t\tconst distance = _v1$2.distanceTo( _v2$1 ) / camera.zoom;\n\n\t\t\tlevels[ 0 ].object.visible = true;\n\n\t\t\tlet i, l;\n\n\t\t\tfor ( i = 1, l = levels.length; i < l; i ++ ) {\n\n\t\t\t\tlet levelDistance = levels[ i ].distance;\n\n\t\t\t\tif ( levels[ i ].object.visible ) {\n\n\t\t\t\t\tlevelDistance -= levelDistance * levels[ i ].hysteresis;\n\n\t\t\t\t}\n\n\t\t\t\tif ( distance >= levelDistance ) {\n\n\t\t\t\t\tlevels[ i - 1 ].object.visible = false;\n\t\t\t\t\tlevels[ i ].object.visible = true;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tbreak;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis._currentLevel = i - 1;\n\n\t\t\tfor ( ; i < l; i ++ ) {\n\n\t\t\t\tlevels[ i ].object.visible = false;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tif ( this.autoUpdate === false ) data.object.autoUpdate = false;\n\n\t\tdata.object.levels = [];\n\n\t\tconst levels = this.levels;\n\n\t\tfor ( let i = 0, l = levels.length; i < l; i ++ ) {\n\n\t\t\tconst level = levels[ i ];\n\n\t\t\tdata.object.levels.push( {\n\t\t\t\tobject: level.object.uuid,\n\t\t\t\tdistance: level.distance,\n\t\t\t\thysteresis: level.hysteresis\n\t\t\t} );\n\n\t\t}\n\n\t\treturn data;\n\n\t}\n\n}\n\nconst _basePosition = /*@__PURE__*/ new Vector3();\n\nconst _skinIndex = /*@__PURE__*/ new Vector4();\nconst _skinWeight = /*@__PURE__*/ new Vector4();\n\nconst _vector3 = /*@__PURE__*/ new Vector3();\nconst _matrix4 = /*@__PURE__*/ new Matrix4();\nconst _vertex = /*@__PURE__*/ new Vector3();\n\nconst _sphere$5 = /*@__PURE__*/ new Sphere();\nconst _inverseMatrix$2 = /*@__PURE__*/ new Matrix4();\nconst _ray$2 = /*@__PURE__*/ new Ray();\n\n/**\n * A mesh that has a {@link Skeleton} that can then be used to animate the\n * vertices of the geometry with skinning/skeleton animation.\n *\n * Next to a valid skeleton, the skinned mesh requires skin indices and weights\n * as buffer attributes in its geometry. These attribute define which bones affect a single\n * vertex to a certain extend.\n *\n * Typically skinned meshes are not created manually but loaders like {@link GLTFLoader}\n * or {@link FBXLoader } import respective models.\n *\n * @augments Mesh\n * @demo scenes/bones-browser.html\n */\nclass SkinnedMesh extends Mesh {\n\n\t/**\n\t * Constructs a new skinned mesh.\n\t *\n\t * @param {BufferGeometry} [geometry] - The mesh geometry.\n\t * @param {Material|Array<Material>} [material] - The mesh material.\n\t */\n\tconstructor( geometry, material ) {\n\n\t\tsuper( geometry, material );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSkinnedMesh = true;\n\n\t\tthis.type = 'SkinnedMesh';\n\n\t\t/**\n\t\t * `AttachedBindMode` means the skinned mesh shares the same world space as the skeleton.\n\t\t * This is not true when using `DetachedBindMode` which is useful when sharing a skeleton\n\t\t * across multiple skinned meshes.\n\t\t *\n\t\t * @type {(AttachedBindMode|DetachedBindMode)}\n\t\t * @default AttachedBindMode\n\t\t */\n\t\tthis.bindMode = AttachedBindMode;\n\n\t\t/**\n\t\t * The base matrix that is used for the bound bone transforms.\n\t\t *\n\t\t * @type {Matrix4}\n\t\t */\n\t\tthis.bindMatrix = new Matrix4();\n\n\t\t/**\n\t\t * The base matrix that is used for resetting the bound bone transforms.\n\t\t *\n\t\t * @type {Matrix4}\n\t\t */\n\t\tthis.bindMatrixInverse = new Matrix4();\n\n\t\t/**\n\t\t * The bounding box of the skinned mesh. Can be computed via {@link SkinnedMesh#computeBoundingBox}.\n\t\t *\n\t\t * @type {?Box3}\n\t\t * @default null\n\t\t */\n\t\tthis.boundingBox = null;\n\n\t\t/**\n\t\t * The bounding sphere of the skinned mesh. Can be computed via {@link SkinnedMesh#computeBoundingSphere}.\n\t\t *\n\t\t * @type {?Sphere}\n\t\t * @default null\n\t\t */\n\t\tthis.boundingSphere = null;\n\n\t}\n\n\t/**\n\t * Computes the bounding box of the skinned mesh, and updates {@link SkinnedMesh#boundingBox}.\n\t * The bounding box is not automatically computed by the engine; this method must be called by your app.\n\t * If the skinned mesh is animated, the bounding box should be recomputed per frame in order to reflect\n\t * the current animation state.\n\t */\n\tcomputeBoundingBox() {\n\n\t\tconst geometry = this.geometry;\n\n\t\tif ( this.boundingBox === null ) {\n\n\t\t\tthis.boundingBox = new Box3();\n\n\t\t}\n\n\t\tthis.boundingBox.makeEmpty();\n\n\t\tconst positionAttribute = geometry.getAttribute( 'position' );\n\n\t\tfor ( let i = 0; i < positionAttribute.count; i ++ ) {\n\n\t\t\tthis.getVertexPosition( i, _vertex );\n\t\t\tthis.boundingBox.expandByPoint( _vertex );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Computes the bounding sphere of the skinned mesh, and updates {@link SkinnedMesh#boundingSphere}.\n\t * The bounding sphere is automatically computed by the engine once when it is needed, e.g., for ray casting\n\t * and view frustum culling. If the skinned mesh is animated, the bounding sphere should be recomputed\n\t * per frame in order to reflect the current animation state.\n\t */\n\tcomputeBoundingSphere() {\n\n\t\tconst geometry = this.geometry;\n\n\t\tif ( this.boundingSphere === null ) {\n\n\t\t\tthis.boundingSphere = new Sphere();\n\n\t\t}\n\n\t\tthis.boundingSphere.makeEmpty();\n\n\t\tconst positionAttribute = geometry.getAttribute( 'position' );\n\n\t\tfor ( let i = 0; i < positionAttribute.count; i ++ ) {\n\n\t\t\tthis.getVertexPosition( i, _vertex );\n\t\t\tthis.boundingSphere.expandByPoint( _vertex );\n\n\t\t}\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.bindMode = source.bindMode;\n\t\tthis.bindMatrix.copy( source.bindMatrix );\n\t\tthis.bindMatrixInverse.copy( source.bindMatrixInverse );\n\n\t\tthis.skeleton = source.skeleton;\n\n\t\tif ( source.boundingBox !== null ) this.boundingBox = source.boundingBox.clone();\n\t\tif ( source.boundingSphere !== null ) this.boundingSphere = source.boundingSphere.clone();\n\n\t\treturn this;\n\n\t}\n\n\traycast( raycaster, intersects ) {\n\n\t\tconst material = this.material;\n\t\tconst matrixWorld = this.matrixWorld;\n\n\t\tif ( material === undefined ) return;\n\n\t\t// test with bounding sphere in world space\n\n\t\tif ( this.boundingSphere === null ) this.computeBoundingSphere();\n\n\t\t_sphere$5.copy( this.boundingSphere );\n\t\t_sphere$5.applyMatrix4( matrixWorld );\n\n\t\tif ( raycaster.ray.intersectsSphere( _sphere$5 ) === false ) return;\n\n\t\t// convert ray to local space of skinned mesh\n\n\t\t_inverseMatrix$2.copy( matrixWorld ).invert();\n\t\t_ray$2.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$2 );\n\n\t\t// test with bounding box in local space\n\n\t\tif ( this.boundingBox !== null ) {\n\n\t\t\tif ( _ray$2.intersectsBox( this.boundingBox ) === false ) return;\n\n\t\t}\n\n\t\t// test for intersections with geometry\n\n\t\tthis._computeIntersections( raycaster, intersects, _ray$2 );\n\n\t}\n\n\tgetVertexPosition( index, target ) {\n\n\t\tsuper.getVertexPosition( index, target );\n\n\t\tthis.applyBoneTransform( index, target );\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Binds the given skeleton to the skinned mesh.\n\t *\n\t * @param {Skeleton} skeleton - The skeleton to bind.\n\t * @param {Matrix4} [bindMatrix] - The bind matrix. If no bind matrix is provided,\n\t * the skinned mesh's world matrix will be used instead.\n\t */\n\tbind( skeleton, bindMatrix ) {\n\n\t\tthis.skeleton = skeleton;\n\n\t\tif ( bindMatrix === undefined ) {\n\n\t\t\tthis.updateMatrixWorld( true );\n\n\t\t\tthis.skeleton.calculateInverses();\n\n\t\t\tbindMatrix = this.matrixWorld;\n\n\t\t}\n\n\t\tthis.bindMatrix.copy( bindMatrix );\n\t\tthis.bindMatrixInverse.copy( bindMatrix ).invert();\n\n\t}\n\n\t/**\n\t * This method sets the skinned mesh in the rest pose).\n\t */\n\tpose() {\n\n\t\tthis.skeleton.pose();\n\n\t}\n\n\t/**\n\t * Normalizes the skin weights which are defined as a buffer attribute\n\t * in the skinned mesh's geometry.\n\t */\n\tnormalizeSkinWeights() {\n\n\t\tconst vector = new Vector4();\n\n\t\tconst skinWeight = this.geometry.attributes.skinWeight;\n\n\t\tfor ( let i = 0, l = skinWeight.count; i < l; i ++ ) {\n\n\t\t\tvector.fromBufferAttribute( skinWeight, i );\n\n\t\t\tconst scale = 1.0 / vector.manhattanLength();\n\n\t\t\tif ( scale !== Infinity ) {\n\n\t\t\t\tvector.multiplyScalar( scale );\n\n\t\t\t} else {\n\n\t\t\t\tvector.set( 1, 0, 0, 0 ); // do something reasonable\n\n\t\t\t}\n\n\t\t\tskinWeight.setXYZW( i, vector.x, vector.y, vector.z, vector.w );\n\n\t\t}\n\n\t}\n\n\tupdateMatrixWorld( force ) {\n\n\t\tsuper.updateMatrixWorld( force );\n\n\t\tif ( this.bindMode === AttachedBindMode ) {\n\n\t\t\tthis.bindMatrixInverse.copy( this.matrixWorld ).invert();\n\n\t\t} else if ( this.bindMode === DetachedBindMode ) {\n\n\t\t\tthis.bindMatrixInverse.copy( this.bindMatrix ).invert();\n\n\t\t} else {\n\n\t\t\twarn( 'SkinnedMesh: Unrecognized bindMode: ' + this.bindMode );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Applies the bone transform associated with the given index to the given\n\t * vertex position. Returns the updated vector.\n\t *\n\t * @param {number} index - The vertex index.\n\t * @param {Vector3} target - The target object that is used to store the method's result.\n\t * the skinned mesh's world matrix will be used instead.\n\t * @return {Vector3} The updated vertex position.\n\t */\n\tapplyBoneTransform( index, target ) {\n\n\t\tconst skeleton = this.skeleton;\n\t\tconst geometry = this.geometry;\n\n\t\t_skinIndex.fromBufferAttribute( geometry.attributes.skinIndex, index );\n\t\t_skinWeight.fromBufferAttribute( geometry.attributes.skinWeight, index );\n\n\t\t_basePosition.copy( target ).applyMatrix4( this.bindMatrix );\n\n\t\ttarget.set( 0, 0, 0 );\n\n\t\tfor ( let i = 0; i < 4; i ++ ) {\n\n\t\t\tconst weight = _skinWeight.getComponent( i );\n\n\t\t\tif ( weight !== 0 ) {\n\n\t\t\t\tconst boneIndex = _skinIndex.getComponent( i );\n\n\t\t\t\t_matrix4.multiplyMatrices( skeleton.bones[ boneIndex ].matrixWorld, skeleton.boneInverses[ boneIndex ] );\n\n\t\t\t\ttarget.addScaledVector( _vector3.copy( _basePosition ).applyMatrix4( _matrix4 ), weight );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn target.applyMatrix4( this.bindMatrixInverse );\n\n\t}\n\n}\n\n/**\n * A bone which is part of a {@link Skeleton}. The skeleton in turn is used by\n * the {@link SkinnedMesh}.\n *\n * ```js\n * const root = new THREE.Bone();\n * const child = new THREE.Bone();\n *\n * root.add( child );\n * child.position.y = 5;\n * ```\n *\n * @augments Object3D\n */\nclass Bone extends Object3D {\n\n\t/**\n\t * Constructs a new bone.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isBone = true;\n\n\t\tthis.type = 'Bone';\n\n\t}\n\n}\n\n/**\n * Creates a texture directly from raw buffer data.\n *\n * The interpretation of the data depends on type and format: If the type is\n * `UnsignedByteType`, a `Uint8Array` will be useful for addressing the\n * texel data. If the format is `RGBAFormat`, data needs four values for\n * one texel; Red, Green, Blue and Alpha (typically the opacity).\n *\n * @augments Texture\n */\nclass DataTexture extends Texture {\n\n\t/**\n\t * Constructs a new data texture.\n\t *\n\t * @param {?TypedArray} [data=null] - The buffer data.\n\t * @param {number} [width=1] - The width of the texture.\n\t * @param {number} [height=1] - The height of the texture.\n\t * @param {number} [format=RGBAFormat] - The texture format.\n\t * @param {number} [type=UnsignedByteType] - The texture type.\n\t * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.\n\t * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.\n\t * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.\n\t * @param {number} [magFilter=NearestFilter] - The mag filter value.\n\t * @param {number} [minFilter=NearestFilter] - The min filter value.\n\t * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.\n\t * @param {string} [colorSpace=NoColorSpace] - The color space.\n\t */\n\tconstructor( data = null, width = 1, height = 1, format, type, mapping, wrapS, wrapT, magFilter = NearestFilter, minFilter = NearestFilter, anisotropy, colorSpace ) {\n\n\t\tsuper( null, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isDataTexture = true;\n\n\t\t/**\n\t\t * The image definition of a data texture.\n\t\t *\n\t\t * @type {{data:TypedArray,width:number,height:number}}\n\t\t */\n\t\tthis.image = { data: data, width: width, height: height };\n\n\t\t/**\n\t\t * Whether to generate mipmaps (if possible) for a texture.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.generateMipmaps = false;\n\n\t\t/**\n\t\t * If set to `true`, the texture is flipped along the vertical axis when\n\t\t * uploaded to the GPU.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flipY = false;\n\n\t\t/**\n\t\t * Specifies the alignment requirements for the start of each pixel row in memory.\n\t\t *\n\t\t * Overwritten and set to `1` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default 1\n\t\t */\n\t\tthis.unpackAlignment = 1;\n\n\t}\n\n}\n\nconst _offsetMatrix = /*@__PURE__*/ new Matrix4();\nconst _identityMatrix = /*@__PURE__*/ new Matrix4();\n\n/**\n * Class for representing the armatures in `three.js`. The skeleton\n * is defined by a hierarchy of bones.\n *\n * ```js\n * const bones = [];\n *\n * const shoulder = new THREE.Bone();\n * const elbow = new THREE.Bone();\n * const hand = new THREE.Bone();\n *\n * shoulder.add( elbow );\n * elbow.add( hand );\n *\n * bones.push( shoulder , elbow, hand);\n *\n * shoulder.position.y = -5;\n * elbow.position.y = 0;\n * hand.position.y = 5;\n *\n * const armSkeleton = new THREE.Skeleton( bones );\n * ```\n */\nclass Skeleton {\n\n\t/**\n\t * Constructs a new skeleton.\n\t *\n\t * @param {Array<Bone>} [bones] - An array of bones.\n\t * @param {Array<Matrix4>} [boneInverses] - An array of bone inverse matrices.\n\t * If not provided, these matrices will be computed automatically via {@link Skeleton#calculateInverses}.\n\t */\n\tconstructor( bones = [], boneInverses = [] ) {\n\n\t\tthis.uuid = generateUUID();\n\n\t\t/**\n\t\t * An array of bones defining the skeleton.\n\t\t *\n\t\t * @type {Array<Bone>}\n\t\t */\n\t\tthis.bones = bones.slice( 0 );\n\n\t\t/**\n\t\t * An array of bone inverse matrices.\n\t\t *\n\t\t * @type {Array<Matrix4>}\n\t\t */\n\t\tthis.boneInverses = boneInverses;\n\n\t\t/**\n\t\t * An array buffer holding the bone data.\n\t\t * Input data for {@link Skeleton#boneTexture}.\n\t\t *\n\t\t * @type {?Float32Array}\n\t\t * @default null\n\t\t */\n\t\tthis.boneMatrices = null;\n\n\t\t/**\n\t\t * An array buffer holding the bone data of the previous frame.\n\t\t * Required for computing velocity. Maintained in {@link SkinningNode}.\n\t\t *\n\t\t * @type {?Float32Array}\n\t\t * @default null\n\t\t */\n\t\tthis.previousBoneMatrices = null;\n\n\t\t/**\n\t\t * A texture holding the bone data for use\n\t\t * in the vertex shader.\n\t\t *\n\t\t * @type {?DataTexture}\n\t\t * @default null\n\t\t */\n\t\tthis.boneTexture = null;\n\n\t\tthis.init();\n\n\t}\n\n\t/**\n\t * Initializes the skeleton. This method gets automatically called by the constructor\n\t * but depending on how the skeleton is created it might be necessary to call this method\n\t * manually.\n\t */\n\tinit() {\n\n\t\tconst bones = this.bones;\n\t\tconst boneInverses = this.boneInverses;\n\n\t\tthis.boneMatrices = new Float32Array( bones.length * 16 );\n\n\t\t// calculate inverse bone matrices if necessary\n\n\t\tif ( boneInverses.length === 0 ) {\n\n\t\t\tthis.calculateInverses();\n\n\t\t} else {\n\n\t\t\t// handle special case\n\n\t\t\tif ( bones.length !== boneInverses.length ) {\n\n\t\t\t\twarn( 'Skeleton: Number of inverse bone matrices does not match amount of bones.' );\n\n\t\t\t\tthis.boneInverses = [];\n\n\t\t\t\tfor ( let i = 0, il = this.bones.length; i < il; i ++ ) {\n\n\t\t\t\t\tthis.boneInverses.push( new Matrix4() );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Computes the bone inverse matrices. This method resets {@link Skeleton#boneInverses}\n\t * and fills it with new matrices.\n\t */\n\tcalculateInverses() {\n\n\t\tthis.boneInverses.length = 0;\n\n\t\tfor ( let i = 0, il = this.bones.length; i < il; i ++ ) {\n\n\t\t\tconst inverse = new Matrix4();\n\n\t\t\tif ( this.bones[ i ] ) {\n\n\t\t\t\tinverse.copy( this.bones[ i ].matrixWorld ).invert();\n\n\t\t\t}\n\n\t\t\tthis.boneInverses.push( inverse );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Resets the skeleton to the base pose.\n\t */\n\tpose() {\n\n\t\t// recover the bind-time world matrices\n\n\t\tfor ( let i = 0, il = this.bones.length; i < il; i ++ ) {\n\n\t\t\tconst bone = this.bones[ i ];\n\n\t\t\tif ( bone ) {\n\n\t\t\t\tbone.matrixWorld.copy( this.boneInverses[ i ] ).invert();\n\n\t\t\t}\n\n\t\t}\n\n\t\t// compute the local matrices, positions, rotations and scales\n\n\t\tfor ( let i = 0, il = this.bones.length; i < il; i ++ ) {\n\n\t\t\tconst bone = this.bones[ i ];\n\n\t\t\tif ( bone ) {\n\n\t\t\t\tif ( bone.parent && bone.parent.isBone ) {\n\n\t\t\t\t\tbone.matrix.copy( bone.parent.matrixWorld ).invert();\n\t\t\t\t\tbone.matrix.multiply( bone.matrixWorld );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tbone.matrix.copy( bone.matrixWorld );\n\n\t\t\t\t}\n\n\t\t\t\tbone.matrix.decompose( bone.position, bone.quaternion, bone.scale );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Resets the skeleton to the base pose.\n\t */\n\tupdate() {\n\n\t\tconst bones = this.bones;\n\t\tconst boneInverses = this.boneInverses;\n\t\tconst boneMatrices = this.boneMatrices;\n\t\tconst boneTexture = this.boneTexture;\n\n\t\t// flatten bone matrices to array\n\n\t\tfor ( let i = 0, il = bones.length; i < il; i ++ ) {\n\n\t\t\t// compute the offset between the current and the original transform\n\n\t\t\tconst matrix = bones[ i ] ? bones[ i ].matrixWorld : _identityMatrix;\n\n\t\t\t_offsetMatrix.multiplyMatrices( matrix, boneInverses[ i ] );\n\t\t\t_offsetMatrix.toArray( boneMatrices, i * 16 );\n\n\t\t}\n\n\t\tif ( boneTexture !== null ) {\n\n\t\t\tboneTexture.needsUpdate = true;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns a new skeleton with copied values from this instance.\n\t *\n\t * @return {Skeleton} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new Skeleton( this.bones, this.boneInverses );\n\n\t}\n\n\t/**\n\t * Computes a data texture for passing bone data to the vertex shader.\n\t *\n\t * @return {Skeleton} A reference of this instance.\n\t */\n\tcomputeBoneTexture() {\n\n\t\t// layout (1 matrix = 4 pixels)\n\t\t//      RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)\n\t\t//  with  8x8  pixel texture max   16 bones * 4 pixels =  (8 * 8)\n\t\t//       16x16 pixel texture max   64 bones * 4 pixels = (16 * 16)\n\t\t//       32x32 pixel texture max  256 bones * 4 pixels = (32 * 32)\n\t\t//       64x64 pixel texture max 1024 bones * 4 pixels = (64 * 64)\n\n\t\tlet size = Math.sqrt( this.bones.length * 4 ); // 4 pixels needed for 1 matrix\n\t\tsize = Math.ceil( size / 4 ) * 4;\n\t\tsize = Math.max( size, 4 );\n\n\t\tconst boneMatrices = new Float32Array( size * size * 4 ); // 4 floats per RGBA pixel\n\t\tboneMatrices.set( this.boneMatrices ); // copy current values\n\n\t\tconst boneTexture = new DataTexture( boneMatrices, size, size, RGBAFormat, FloatType );\n\t\tboneTexture.needsUpdate = true;\n\n\t\tthis.boneMatrices = boneMatrices;\n\t\tthis.boneTexture = boneTexture;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Searches through the skeleton's bone array and returns the first with a\n\t * matching name.\n\t *\n\t * @param {string} name - The name of the bone.\n\t * @return {Bone|undefined} The found bone. `undefined` if no bone has been found.\n\t */\n\tgetBoneByName( name ) {\n\n\t\tfor ( let i = 0, il = this.bones.length; i < il; i ++ ) {\n\n\t\t\tconst bone = this.bones[ i ];\n\n\t\t\tif ( bone.name === name ) {\n\n\t\t\t\treturn bone;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn undefined;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose( ) {\n\n\t\tif ( this.boneTexture !== null ) {\n\n\t\t\tthis.boneTexture.dispose();\n\n\t\t\tthis.boneTexture = null;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Setups the skeleton by the given JSON and bones.\n\t *\n\t * @param {Object} json - The skeleton as serialized JSON.\n\t * @param {Object<string, Bone>} bones - An array of bones.\n\t * @return {Skeleton} A reference of this instance.\n\t */\n\tfromJSON( json, bones ) {\n\n\t\tthis.uuid = json.uuid;\n\n\t\tfor ( let i = 0, l = json.bones.length; i < l; i ++ ) {\n\n\t\t\tconst uuid = json.bones[ i ];\n\t\t\tlet bone = bones[ uuid ];\n\n\t\t\tif ( bone === undefined ) {\n\n\t\t\t\twarn( 'Skeleton: No bone found with UUID:', uuid );\n\t\t\t\tbone = new Bone();\n\n\t\t\t}\n\n\t\t\tthis.bones.push( bone );\n\t\t\tthis.boneInverses.push( new Matrix4().fromArray( json.boneInverses[ i ] ) );\n\n\t\t}\n\n\t\tthis.init();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Serializes the skeleton into JSON.\n\t *\n\t * @return {Object} A JSON object representing the serialized skeleton.\n\t * @see {@link ObjectLoader#parse}\n\t */\n\ttoJSON() {\n\n\t\tconst data = {\n\t\t\tmetadata: {\n\t\t\t\tversion: 4.7,\n\t\t\t\ttype: 'Skeleton',\n\t\t\t\tgenerator: 'Skeleton.toJSON'\n\t\t\t},\n\t\t\tbones: [],\n\t\t\tboneInverses: []\n\t\t};\n\n\t\tdata.uuid = this.uuid;\n\n\t\tconst bones = this.bones;\n\t\tconst boneInverses = this.boneInverses;\n\n\t\tfor ( let i = 0, l = bones.length; i < l; i ++ ) {\n\n\t\t\tconst bone = bones[ i ];\n\t\t\tdata.bones.push( bone.uuid );\n\n\t\t\tconst boneInverse = boneInverses[ i ];\n\t\t\tdata.boneInverses.push( boneInverse.toArray() );\n\n\t\t}\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * An instanced version of a buffer attribute.\n *\n * @augments BufferAttribute\n */\nclass InstancedBufferAttribute extends BufferAttribute {\n\n\t/**\n\t * Constructs a new instanced buffer attribute.\n\t *\n\t * @param {TypedArray} array - The array holding the attribute data.\n\t * @param {number} itemSize - The item size.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t * @param {number} [meshPerAttribute=1] - How often a value of this buffer attribute should be repeated.\n\t */\n\tconstructor( array, itemSize, normalized, meshPerAttribute = 1 ) {\n\n\t\tsuper( array, itemSize, normalized );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isInstancedBufferAttribute = true;\n\n\t\t/**\n\t\t * Defines how often a value of this buffer attribute should be repeated. A\n\t\t * value of one means that each value of the instanced attribute is used for\n\t\t * a single instance. A value of two means that each value is used for two\n\t\t * consecutive instances (and so on).\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.meshPerAttribute = meshPerAttribute;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.meshPerAttribute = source.meshPerAttribute;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.meshPerAttribute = this.meshPerAttribute;\n\n\t\tdata.isInstancedBufferAttribute = true;\n\n\t\treturn data;\n\n\t}\n\n}\n\nconst _instanceLocalMatrix = /*@__PURE__*/ new Matrix4();\nconst _instanceWorldMatrix = /*@__PURE__*/ new Matrix4();\n\nconst _instanceIntersects = [];\n\nconst _box3 = /*@__PURE__*/ new Box3();\nconst _identity = /*@__PURE__*/ new Matrix4();\nconst _mesh$1 = /*@__PURE__*/ new Mesh();\nconst _sphere$4 = /*@__PURE__*/ new Sphere();\n\n/**\n * A special version of a mesh with instanced rendering support. Use\n * this class if you have to render a large number of objects with the same\n * geometry and material(s) but with different world transformations. The usage\n * of 'InstancedMesh' will help you to reduce the number of draw calls and thus\n * improve the overall rendering performance in your application.\n *\n * @augments Mesh\n */\nclass InstancedMesh extends Mesh {\n\n\t/**\n\t * Constructs a new instanced mesh.\n\t *\n\t * @param {BufferGeometry} [geometry] - The mesh geometry.\n\t * @param {Material|Array<Material>} [material] - The mesh material.\n\t * @param {number} count - The number of instances.\n\t */\n\tconstructor( geometry, material, count ) {\n\n\t\tsuper( geometry, material );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isInstancedMesh = true;\n\n\t\t/**\n\t\t * Represents the local transformation of all instances. You have to set its\n\t\t * {@link BufferAttribute#needsUpdate} flag to true if you modify instanced data\n\t\t * via {@link InstancedMesh#setMatrixAt}.\n\t\t *\n\t\t * @type {InstancedBufferAttribute}\n\t\t */\n\t\tthis.instanceMatrix = new InstancedBufferAttribute( new Float32Array( count * 16 ), 16 );\n\n\t\t/**\n\t\t * Represents the color of all instances. You have to set its\n\t\t * {@link BufferAttribute#needsUpdate} flag to true if you modify instanced data\n\t\t * via {@link InstancedMesh#setColorAt}.\n\t\t *\n\t\t * @type {?InstancedBufferAttribute}\n\t\t * @default null\n\t\t */\n\t\tthis.instanceColor = null;\n\n\t\t/**\n\t\t * Represents the morph target weights of all instances. You have to set its\n\t\t * {@link Texture#needsUpdate} flag to true if you modify instanced data\n\t\t * via {@link InstancedMesh#setMorphAt}.\n\t\t *\n\t\t * @type {?DataTexture}\n\t\t * @default null\n\t\t */\n\t\tthis.morphTexture = null;\n\n\t\t/**\n\t\t * The number of instances.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.count = count;\n\n\t\t/**\n\t\t * The bounding box of the instanced mesh. Can be computed via {@link InstancedMesh#computeBoundingBox}.\n\t\t *\n\t\t * @type {?Box3}\n\t\t * @default null\n\t\t */\n\t\tthis.boundingBox = null;\n\n\t\t/**\n\t\t * The bounding sphere of the instanced mesh. Can be computed via {@link InstancedMesh#computeBoundingSphere}.\n\t\t *\n\t\t * @type {?Sphere}\n\t\t * @default null\n\t\t */\n\t\tthis.boundingSphere = null;\n\n\t\tfor ( let i = 0; i < count; i ++ ) {\n\n\t\t\tthis.setMatrixAt( i, _identity );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Computes the bounding box of the instanced mesh, and updates {@link InstancedMesh#boundingBox}.\n\t * The bounding box is not automatically computed by the engine; this method must be called by your app.\n\t * You may need to recompute the bounding box if an instance is transformed via {@link InstancedMesh#setMatrixAt}.\n\t */\n\tcomputeBoundingBox() {\n\n\t\tconst geometry = this.geometry;\n\t\tconst count = this.count;\n\n\t\tif ( this.boundingBox === null ) {\n\n\t\t\tthis.boundingBox = new Box3();\n\n\t\t}\n\n\t\tif ( geometry.boundingBox === null ) {\n\n\t\t\tgeometry.computeBoundingBox();\n\n\t\t}\n\n\t\tthis.boundingBox.makeEmpty();\n\n\t\tfor ( let i = 0; i < count; i ++ ) {\n\n\t\t\tthis.getMatrixAt( i, _instanceLocalMatrix );\n\n\t\t\t_box3.copy( geometry.boundingBox ).applyMatrix4( _instanceLocalMatrix );\n\n\t\t\tthis.boundingBox.union( _box3 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Computes the bounding sphere of the instanced mesh, and updates {@link InstancedMesh#boundingSphere}\n\t * The engine automatically computes the bounding sphere when it is needed, e.g., for ray casting or view frustum culling.\n\t * You may need to recompute the bounding sphere if an instance is transformed via {@link InstancedMesh#setMatrixAt}.\n\t */\n\tcomputeBoundingSphere() {\n\n\t\tconst geometry = this.geometry;\n\t\tconst count = this.count;\n\n\t\tif ( this.boundingSphere === null ) {\n\n\t\t\tthis.boundingSphere = new Sphere();\n\n\t\t}\n\n\t\tif ( geometry.boundingSphere === null ) {\n\n\t\t\tgeometry.computeBoundingSphere();\n\n\t\t}\n\n\t\tthis.boundingSphere.makeEmpty();\n\n\t\tfor ( let i = 0; i < count; i ++ ) {\n\n\t\t\tthis.getMatrixAt( i, _instanceLocalMatrix );\n\n\t\t\t_sphere$4.copy( geometry.boundingSphere ).applyMatrix4( _instanceLocalMatrix );\n\n\t\t\tthis.boundingSphere.union( _sphere$4 );\n\n\t\t}\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.instanceMatrix.copy( source.instanceMatrix );\n\n\t\tif ( source.morphTexture !== null ) this.morphTexture = source.morphTexture.clone();\n\t\tif ( source.instanceColor !== null ) this.instanceColor = source.instanceColor.clone();\n\n\t\tthis.count = source.count;\n\n\t\tif ( source.boundingBox !== null ) this.boundingBox = source.boundingBox.clone();\n\t\tif ( source.boundingSphere !== null ) this.boundingSphere = source.boundingSphere.clone();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Gets the color of the defined instance.\n\t *\n\t * @param {number} index - The instance index.\n\t * @param {Color} color - The target object that is used to store the method's result.\n\t */\n\tgetColorAt( index, color ) {\n\n\t\tcolor.fromArray( this.instanceColor.array, index * 3 );\n\n\t}\n\n\t/**\n\t * Gets the local transformation matrix of the defined instance.\n\t *\n\t * @param {number} index - The instance index.\n\t * @param {Matrix4} matrix - The target object that is used to store the method's result.\n\t */\n\tgetMatrixAt( index, matrix ) {\n\n\t\tmatrix.fromArray( this.instanceMatrix.array, index * 16 );\n\n\t}\n\n\t/**\n\t * Gets the morph target weights of the defined instance.\n\t *\n\t * @param {number} index - The instance index.\n\t * @param {Mesh} object - The target object that is used to store the method's result.\n\t */\n\tgetMorphAt( index, object ) {\n\n\t\tconst objectInfluences = object.morphTargetInfluences;\n\n\t\tconst array = this.morphTexture.source.data.data;\n\n\t\tconst len = objectInfluences.length + 1; // All influences + the baseInfluenceSum\n\n\t\tconst dataIndex = index * len + 1; // Skip the baseInfluenceSum at the beginning\n\n\t\tfor ( let i = 0; i < objectInfluences.length; i ++ ) {\n\n\t\t\tobjectInfluences[ i ] = array[ dataIndex + i ];\n\n\t\t}\n\n\t}\n\n\traycast( raycaster, intersects ) {\n\n\t\tconst matrixWorld = this.matrixWorld;\n\t\tconst raycastTimes = this.count;\n\n\t\t_mesh$1.geometry = this.geometry;\n\t\t_mesh$1.material = this.material;\n\n\t\tif ( _mesh$1.material === undefined ) return;\n\n\t\t// test with bounding sphere first\n\n\t\tif ( this.boundingSphere === null ) this.computeBoundingSphere();\n\n\t\t_sphere$4.copy( this.boundingSphere );\n\t\t_sphere$4.applyMatrix4( matrixWorld );\n\n\t\tif ( raycaster.ray.intersectsSphere( _sphere$4 ) === false ) return;\n\n\t\t// now test each instance\n\n\t\tfor ( let instanceId = 0; instanceId < raycastTimes; instanceId ++ ) {\n\n\t\t\t// calculate the world matrix for each instance\n\n\t\t\tthis.getMatrixAt( instanceId, _instanceLocalMatrix );\n\n\t\t\t_instanceWorldMatrix.multiplyMatrices( matrixWorld, _instanceLocalMatrix );\n\n\t\t\t// the mesh represents this single instance\n\n\t\t\t_mesh$1.matrixWorld = _instanceWorldMatrix;\n\n\t\t\t_mesh$1.raycast( raycaster, _instanceIntersects );\n\n\t\t\t// process the result of raycast\n\n\t\t\tfor ( let i = 0, l = _instanceIntersects.length; i < l; i ++ ) {\n\n\t\t\t\tconst intersect = _instanceIntersects[ i ];\n\t\t\t\tintersect.instanceId = instanceId;\n\t\t\t\tintersect.object = this;\n\t\t\t\tintersects.push( intersect );\n\n\t\t\t}\n\n\t\t\t_instanceIntersects.length = 0;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sets the given color to the defined instance. Make sure you set the `needsUpdate` flag of\n\t * {@link InstancedMesh#instanceColor} to `true` after updating all the colors.\n\t *\n\t * @param {number} index - The instance index.\n\t * @param {Color} color - The instance color.\n\t */\n\tsetColorAt( index, color ) {\n\n\t\tif ( this.instanceColor === null ) {\n\n\t\t\tthis.instanceColor = new InstancedBufferAttribute( new Float32Array( this.instanceMatrix.count * 3 ).fill( 1 ), 3 );\n\n\t\t}\n\n\t\tcolor.toArray( this.instanceColor.array, index * 3 );\n\n\t}\n\n\t/**\n\t * Sets the given local transformation matrix to the defined instance. Make sure you set the `needsUpdate` flag of\n\t * {@link InstancedMesh#instanceMatrix} to `true` after updating all the colors.\n\t *\n\t * @param {number} index - The instance index.\n\t * @param {Matrix4} matrix - The local transformation.\n\t */\n\tsetMatrixAt( index, matrix ) {\n\n\t\tmatrix.toArray( this.instanceMatrix.array, index * 16 );\n\n\t}\n\n\t/**\n\t * Sets the morph target weights to the defined instance. Make sure you set the `needsUpdate` flag of\n\t * {@link InstancedMesh#morphTexture} to `true` after updating all the influences.\n\t *\n\t * @param {number} index - The instance index.\n\t * @param {Mesh} object -  A mesh which `morphTargetInfluences` property containing the morph target weights\n\t * of a single instance.\n\t */\n\tsetMorphAt( index, object ) {\n\n\t\tconst objectInfluences = object.morphTargetInfluences;\n\n\t\tconst len = objectInfluences.length + 1; // morphBaseInfluence + all influences\n\n\t\tif ( this.morphTexture === null ) {\n\n\t\t\tthis.morphTexture = new DataTexture( new Float32Array( len * this.count ), len, this.count, RedFormat, FloatType );\n\n\t\t}\n\n\t\tconst array = this.morphTexture.source.data.data;\n\n\t\tlet morphInfluencesSum = 0;\n\n\t\tfor ( let i = 0; i < objectInfluences.length; i ++ ) {\n\n\t\t\tmorphInfluencesSum += objectInfluences[ i ];\n\n\t\t}\n\n\t\tconst morphBaseInfluence = this.geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;\n\n\t\tconst dataIndex = len * index;\n\n\t\tarray[ dataIndex ] = morphBaseInfluence;\n\n\t\tarray.set( objectInfluences, dataIndex + 1 );\n\n\t}\n\n\tupdateMorphTargets() {\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.dispatchEvent( { type: 'dispose' } );\n\n\t\tif ( this.morphTexture !== null ) {\n\n\t\t\tthis.morphTexture.dispose();\n\t\t\tthis.morphTexture = null;\n\n\t\t}\n\n\t}\n\n}\n\nconst _vector1 = /*@__PURE__*/ new Vector3();\nconst _vector2 = /*@__PURE__*/ new Vector3();\nconst _normalMatrix = /*@__PURE__*/ new Matrix3();\n\n/**\n * A two dimensional surface that extends infinitely in 3D space, represented\n * in [Hessian normal form](http://mathworld.wolfram.com/HessianNormalForm.html)\n * by a unit length normal vector and a constant.\n */\nclass Plane {\n\n\t/**\n\t * Constructs a new plane.\n\t *\n\t * @param {Vector3} [normal=(1,0,0)] - A unit length vector defining the normal of the plane.\n\t * @param {number} [constant=0] - The signed distance from the origin to the plane.\n\t */\n\tconstructor( normal = new Vector3( 1, 0, 0 ), constant = 0 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isPlane = true;\n\n\t\t/**\n\t\t * A unit length vector defining the normal of the plane.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.normal = normal;\n\n\t\t/**\n\t\t * The signed distance from the origin to the plane.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.constant = constant;\n\n\t}\n\n\t/**\n\t * Sets the plane components by copying the given values.\n\t *\n\t * @param {Vector3} normal - The normal.\n\t * @param {number} constant - The constant.\n\t * @return {Plane} A reference to this plane.\n\t */\n\tset( normal, constant ) {\n\n\t\tthis.normal.copy( normal );\n\t\tthis.constant = constant;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the plane components by defining `x`, `y`, `z` as the\n\t * plane normal and `w` as the constant.\n\t *\n\t * @param {number} x - The value for the normal's x component.\n\t * @param {number} y - The value for the normal's y component.\n\t * @param {number} z - The value for the normal's z component.\n\t * @param {number} w - The constant value.\n\t * @return {Plane} A reference to this plane.\n\t */\n\tsetComponents( x, y, z, w ) {\n\n\t\tthis.normal.set( x, y, z );\n\t\tthis.constant = w;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the plane from the given normal and coplanar point (that is a point\n\t * that lies onto the plane).\n\t *\n\t * @param {Vector3} normal - The normal.\n\t * @param {Vector3} point - A coplanar point.\n\t * @return {Plane} A reference to this plane.\n\t */\n\tsetFromNormalAndCoplanarPoint( normal, point ) {\n\n\t\tthis.normal.copy( normal );\n\t\tthis.constant = - point.dot( this.normal );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the plane from three coplanar points. The winding order is\n\t * assumed to be counter-clockwise, and determines the direction of\n\t * the plane normal.\n\t *\n\t * @param {Vector3} a - The first coplanar point.\n\t * @param {Vector3} b - The second coplanar point.\n\t * @param {Vector3} c - The third coplanar point.\n\t * @return {Plane} A reference to this plane.\n\t */\n\tsetFromCoplanarPoints( a, b, c ) {\n\n\t\tconst normal = _vector1.subVectors( c, b ).cross( _vector2.subVectors( a, b ) ).normalize();\n\n\t\t// Q: should an error be thrown if normal is zero (e.g. degenerate plane)?\n\n\t\tthis.setFromNormalAndCoplanarPoint( normal, a );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the values of the given plane to this instance.\n\t *\n\t * @param {Plane} plane - The plane to copy.\n\t * @return {Plane} A reference to this plane.\n\t */\n\tcopy( plane ) {\n\n\t\tthis.normal.copy( plane.normal );\n\t\tthis.constant = plane.constant;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Normalizes the plane normal and adjusts the constant accordingly.\n\t *\n\t * @return {Plane} A reference to this plane.\n\t */\n\tnormalize() {\n\n\t\t// Note: will lead to a divide by zero if the plane is invalid.\n\n\t\tconst inverseNormalLength = 1.0 / this.normal.length();\n\t\tthis.normal.multiplyScalar( inverseNormalLength );\n\t\tthis.constant *= inverseNormalLength;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Negates both the plane normal and the constant.\n\t *\n\t * @return {Plane} A reference to this plane.\n\t */\n\tnegate() {\n\n\t\tthis.constant *= -1;\n\t\tthis.normal.negate();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the signed distance from the given point to this plane.\n\t *\n\t * @param {Vector3} point - The point to compute the distance for.\n\t * @return {number} The signed distance.\n\t */\n\tdistanceToPoint( point ) {\n\n\t\treturn this.normal.dot( point ) + this.constant;\n\n\t}\n\n\t/**\n\t * Returns the signed distance from the given sphere to this plane.\n\t *\n\t * @param {Sphere} sphere - The sphere to compute the distance for.\n\t * @return {number} The signed distance.\n\t */\n\tdistanceToSphere( sphere ) {\n\n\t\treturn this.distanceToPoint( sphere.center ) - sphere.radius;\n\n\t}\n\n\t/**\n\t * Projects a the given point onto the plane.\n\t *\n\t * @param {Vector3} point - The point to project.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The projected point on the plane.\n\t */\n\tprojectPoint( point, target ) {\n\n\t\treturn target.copy( point ).addScaledVector( this.normal, - this.distanceToPoint( point ) );\n\n\t}\n\n\t/**\n\t * Returns the intersection point of the passed line and the plane. Returns\n\t * `null` if the line does not intersect. Returns the line's starting point if\n\t * the line is coplanar with the plane.\n\t *\n\t * @param {Line3} line - The line to compute the intersection for.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {?Vector3} The intersection point.\n\t */\n\tintersectLine( line, target ) {\n\n\t\tconst direction = line.delta( _vector1 );\n\n\t\tconst denominator = this.normal.dot( direction );\n\n\t\tif ( denominator === 0 ) {\n\n\t\t\t// line is coplanar, return origin\n\t\t\tif ( this.distanceToPoint( line.start ) === 0 ) {\n\n\t\t\t\treturn target.copy( line.start );\n\n\t\t\t}\n\n\t\t\t// Unsure if this is the correct method to handle this case.\n\t\t\treturn null;\n\n\t\t}\n\n\t\tconst t = - ( line.start.dot( this.normal ) + this.constant ) / denominator;\n\n\t\tif ( t < 0 || t > 1 ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\treturn target.copy( line.start ).addScaledVector( direction, t );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given line segment intersects with (passes through) the plane.\n\t *\n\t * @param {Line3} line - The line to test.\n\t * @return {boolean} Whether the given line segment intersects with the plane or not.\n\t */\n\tintersectsLine( line ) {\n\n\t\t// Note: this tests if a line intersects the plane, not whether it (or its end-points) are coplanar with it.\n\n\t\tconst startSign = this.distanceToPoint( line.start );\n\t\tconst endSign = this.distanceToPoint( line.end );\n\n\t\treturn ( startSign < 0 && endSign > 0 ) || ( endSign < 0 && startSign > 0 );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given bounding box intersects with the plane.\n\t *\n\t * @param {Box3} box - The bounding box to test.\n\t * @return {boolean} Whether the given bounding box intersects with the plane or not.\n\t */\n\tintersectsBox( box ) {\n\n\t\treturn box.intersectsPlane( this );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given bounding sphere intersects with the plane.\n\t *\n\t * @param {Sphere} sphere - The bounding sphere to test.\n\t * @return {boolean} Whether the given bounding sphere intersects with the plane or not.\n\t */\n\tintersectsSphere( sphere ) {\n\n\t\treturn sphere.intersectsPlane( this );\n\n\t}\n\n\t/**\n\t * Returns a coplanar vector to the plane, by calculating the\n\t * projection of the normal at the origin onto the plane.\n\t *\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The coplanar point.\n\t */\n\tcoplanarPoint( target ) {\n\n\t\treturn target.copy( this.normal ).multiplyScalar( - this.constant );\n\n\t}\n\n\t/**\n\t * Apply a 4x4 matrix to the plane. The matrix must be an affine, homogeneous transform.\n\t *\n\t * The optional normal matrix can be pre-computed like so:\n\t * ```js\n\t * const optionalNormalMatrix = new THREE.Matrix3().getNormalMatrix( matrix );\n\t * ```\n\t *\n\t * @param {Matrix4} matrix - The transformation matrix.\n\t * @param {Matrix4} [optionalNormalMatrix] - A pre-computed normal matrix.\n\t * @return {Plane} A reference to this plane.\n\t */\n\tapplyMatrix4( matrix, optionalNormalMatrix ) {\n\n\t\tconst normalMatrix = optionalNormalMatrix || _normalMatrix.getNormalMatrix( matrix );\n\n\t\tconst referencePoint = this.coplanarPoint( _vector1 ).applyMatrix4( matrix );\n\n\t\tconst normal = this.normal.applyMatrix3( normalMatrix ).normalize();\n\n\t\tthis.constant = - referencePoint.dot( normal );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Translates the plane by the distance defined by the given offset vector.\n\t * Note that this only affects the plane constant and will not affect the normal vector.\n\t *\n\t * @param {Vector3} offset - The offset vector.\n\t * @return {Plane} A reference to this plane.\n\t */\n\ttranslate( offset ) {\n\n\t\tthis.constant -= offset.dot( this.normal );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this plane is equal with the given one.\n\t *\n\t * @param {Plane} plane - The plane to test for equality.\n\t * @return {boolean} Whether this plane is equal with the given one.\n\t */\n\tequals( plane ) {\n\n\t\treturn plane.normal.equals( this.normal ) && ( plane.constant === this.constant );\n\n\t}\n\n\t/**\n\t * Returns a new plane with copied values from this instance.\n\t *\n\t * @return {Plane} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n}\n\nconst _sphere$3 = /*@__PURE__*/ new Sphere();\nconst _defaultSpriteCenter = /*@__PURE__*/ new Vector2( 0.5, 0.5 );\nconst _vector$6 = /*@__PURE__*/ new Vector3();\n\n/**\n * Frustums are used to determine what is inside the camera's field of view.\n * They help speed up the rendering process - objects which lie outside a camera's\n * frustum can safely be excluded from rendering.\n *\n * This class is mainly intended for use internally by a renderer.\n */\nclass Frustum {\n\n\t/**\n\t * Constructs a new frustum.\n\t *\n\t * @param {Plane} [p0] - The first plane that encloses the frustum.\n\t * @param {Plane} [p1] - The second plane that encloses the frustum.\n\t * @param {Plane} [p2] - The third plane that encloses the frustum.\n\t * @param {Plane} [p3] - The fourth plane that encloses the frustum.\n\t * @param {Plane} [p4] - The fifth plane that encloses the frustum.\n\t * @param {Plane} [p5] - The sixth plane that encloses the frustum.\n\t */\n\tconstructor( p0 = new Plane(), p1 = new Plane(), p2 = new Plane(), p3 = new Plane(), p4 = new Plane(), p5 = new Plane() ) {\n\n\t\t/**\n\t\t * This array holds the planes that enclose the frustum.\n\t\t *\n\t\t * @type {Array<Plane>}\n\t\t */\n\t\tthis.planes = [ p0, p1, p2, p3, p4, p5 ];\n\n\t}\n\n\t/**\n\t * Sets the frustum planes by copying the given planes.\n\t *\n\t * @param {Plane} [p0] - The first plane that encloses the frustum.\n\t * @param {Plane} [p1] - The second plane that encloses the frustum.\n\t * @param {Plane} [p2] - The third plane that encloses the frustum.\n\t * @param {Plane} [p3] - The fourth plane that encloses the frustum.\n\t * @param {Plane} [p4] - The fifth plane that encloses the frustum.\n\t * @param {Plane} [p5] - The sixth plane that encloses the frustum.\n\t * @return {Frustum} A reference to this frustum.\n\t */\n\tset( p0, p1, p2, p3, p4, p5 ) {\n\n\t\tconst planes = this.planes;\n\n\t\tplanes[ 0 ].copy( p0 );\n\t\tplanes[ 1 ].copy( p1 );\n\t\tplanes[ 2 ].copy( p2 );\n\t\tplanes[ 3 ].copy( p3 );\n\t\tplanes[ 4 ].copy( p4 );\n\t\tplanes[ 5 ].copy( p5 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the values of the given frustum to this instance.\n\t *\n\t * @param {Frustum} frustum - The frustum to copy.\n\t * @return {Frustum} A reference to this frustum.\n\t */\n\tcopy( frustum ) {\n\n\t\tconst planes = this.planes;\n\n\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\tplanes[ i ].copy( frustum.planes[ i ] );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the frustum planes from the given projection matrix.\n\t *\n\t * @param {Matrix4} m - The projection matrix.\n\t * @param {(WebGLCoordinateSystem|WebGPUCoordinateSystem)} coordinateSystem - The coordinate system.\n\t * @param {boolean} [reversedDepth=false] - Whether to use a reversed depth.\n\t * @return {Frustum} A reference to this frustum.\n\t */\n\tsetFromProjectionMatrix( m, coordinateSystem = WebGLCoordinateSystem, reversedDepth = false ) {\n\n\t\tconst planes = this.planes;\n\t\tconst me = m.elements;\n\t\tconst me0 = me[ 0 ], me1 = me[ 1 ], me2 = me[ 2 ], me3 = me[ 3 ];\n\t\tconst me4 = me[ 4 ], me5 = me[ 5 ], me6 = me[ 6 ], me7 = me[ 7 ];\n\t\tconst me8 = me[ 8 ], me9 = me[ 9 ], me10 = me[ 10 ], me11 = me[ 11 ];\n\t\tconst me12 = me[ 12 ], me13 = me[ 13 ], me14 = me[ 14 ], me15 = me[ 15 ];\n\n\t\tplanes[ 0 ].setComponents( me3 - me0, me7 - me4, me11 - me8, me15 - me12 ).normalize();\n\t\tplanes[ 1 ].setComponents( me3 + me0, me7 + me4, me11 + me8, me15 + me12 ).normalize();\n\t\tplanes[ 2 ].setComponents( me3 + me1, me7 + me5, me11 + me9, me15 + me13 ).normalize();\n\t\tplanes[ 3 ].setComponents( me3 - me1, me7 - me5, me11 - me9, me15 - me13 ).normalize();\n\n\t\tif ( reversedDepth ) {\n\n\t\t\tplanes[ 4 ].setComponents( me2, me6, me10, me14 ).normalize(); // far\n\t\t\tplanes[ 5 ].setComponents( me3 - me2, me7 - me6, me11 - me10, me15 - me14 ).normalize(); // near\n\n\t\t} else {\n\n\t\t\tplanes[ 4 ].setComponents( me3 - me2, me7 - me6, me11 - me10, me15 - me14 ).normalize(); // far\n\n\t\t\tif ( coordinateSystem === WebGLCoordinateSystem ) {\n\n\t\t\t\tplanes[ 5 ].setComponents( me3 + me2, me7 + me6, me11 + me10, me15 + me14 ).normalize(); // near\n\n\t\t\t} else if ( coordinateSystem === WebGPUCoordinateSystem ) {\n\n\t\t\t\tplanes[ 5 ].setComponents( me2, me6, me10, me14 ).normalize(); // near\n\n\t\t\t} else {\n\n\t\t\t\tthrow new Error( 'THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: ' + coordinateSystem );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if the 3D object's bounding sphere is intersecting this frustum.\n\t *\n\t * Note that the 3D object must have a geometry so that the bounding sphere can be calculated.\n\t *\n\t * @param {Object3D} object - The 3D object to test.\n\t * @return {boolean} Whether the 3D object's bounding sphere is intersecting this frustum or not.\n\t */\n\tintersectsObject( object ) {\n\n\t\tif ( object.boundingSphere !== undefined ) {\n\n\t\t\tif ( object.boundingSphere === null ) object.computeBoundingSphere();\n\n\t\t\t_sphere$3.copy( object.boundingSphere ).applyMatrix4( object.matrixWorld );\n\n\t\t} else {\n\n\t\t\tconst geometry = object.geometry;\n\n\t\t\tif ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();\n\n\t\t\t_sphere$3.copy( geometry.boundingSphere ).applyMatrix4( object.matrixWorld );\n\n\t\t}\n\n\t\treturn this.intersectsSphere( _sphere$3 );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given sprite is intersecting this frustum.\n\t *\n\t * @param {Sprite} sprite - The sprite to test.\n\t * @return {boolean} Whether the sprite is intersecting this frustum or not.\n\t */\n\tintersectsSprite( sprite ) {\n\n\t\t_sphere$3.center.set( 0, 0, 0 );\n\n\t\tconst offset = _defaultSpriteCenter.distanceTo( sprite.center );\n\n\t\t_sphere$3.radius = 0.7071067811865476 + offset;\n\t\t_sphere$3.applyMatrix4( sprite.matrixWorld );\n\n\t\treturn this.intersectsSphere( _sphere$3 );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given bounding sphere is intersecting this frustum.\n\t *\n\t * @param {Sphere} sphere - The bounding sphere to test.\n\t * @return {boolean} Whether the bounding sphere is intersecting this frustum or not.\n\t */\n\tintersectsSphere( sphere ) {\n\n\t\tconst planes = this.planes;\n\t\tconst center = sphere.center;\n\t\tconst negRadius = - sphere.radius;\n\n\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\tconst distance = planes[ i ].distanceToPoint( center );\n\n\t\t\tif ( distance < negRadius ) {\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Returns `true` if the given bounding box is intersecting this frustum.\n\t *\n\t * @param {Box3} box - The bounding box to test.\n\t * @return {boolean} Whether the bounding box is intersecting this frustum or not.\n\t */\n\tintersectsBox( box ) {\n\n\t\tconst planes = this.planes;\n\n\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\tconst plane = planes[ i ];\n\n\t\t\t// corner at max distance\n\n\t\t\t_vector$6.x = plane.normal.x > 0 ? box.max.x : box.min.x;\n\t\t\t_vector$6.y = plane.normal.y > 0 ? box.max.y : box.min.y;\n\t\t\t_vector$6.z = plane.normal.z > 0 ? box.max.z : box.min.z;\n\n\t\t\tif ( plane.distanceToPoint( _vector$6 ) < 0 ) {\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Returns `true` if the given point lies within the frustum.\n\t *\n\t * @param {Vector3} point - The point to test.\n\t * @return {boolean} Whether the point lies within this frustum or not.\n\t */\n\tcontainsPoint( point ) {\n\n\t\tconst planes = this.planes;\n\n\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\tif ( planes[ i ].distanceToPoint( point ) < 0 ) {\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Returns a new frustum with copied values from this instance.\n\t *\n\t * @return {Frustum} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n}\n\nconst _projScreenMatrix$1 = /*@__PURE__*/ new Matrix4();\nconst _frustum$1 = /*@__PURE__*/ new Frustum();\n\n/**\n * FrustumArray is used to determine if an object is visible in at least one camera\n * from an array of cameras. This is particularly useful for multi-view renderers.\n*/\nclass FrustumArray {\n\n\t/**\n\t * Constructs a new frustum array.\n\t *\n\t */\n\tconstructor() {\n\n\t\t/**\n\t\t * The coordinate system to use.\n\t\t *\n\t\t * @type {WebGLCoordinateSystem|WebGPUCoordinateSystem}\n\t\t * @default WebGLCoordinateSystem\n\t\t */\n\t\tthis.coordinateSystem = WebGLCoordinateSystem;\n\n\t}\n\n\t/**\n\t * Returns `true` if the 3D object's bounding sphere is intersecting any frustum\n\t * from the camera array.\n\t *\n\t * @param {Object3D} object - The 3D object to test.\n\t * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.\n\t * @return {boolean} Whether the 3D object is visible in any camera.\n\t */\n\tintersectsObject( object, cameraArray ) {\n\n\t\tif ( ! cameraArray.isArrayCamera || cameraArray.cameras.length === 0 ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\tfor ( let i = 0; i < cameraArray.cameras.length; i ++ ) {\n\n\t\t\tconst camera = cameraArray.cameras[ i ];\n\n\t\t\t_projScreenMatrix$1.multiplyMatrices(\n\t\t\t\tcamera.projectionMatrix,\n\t\t\t\tcamera.matrixWorldInverse\n\t\t\t);\n\n\t\t\t_frustum$1.setFromProjectionMatrix(\n\t\t\t\t_projScreenMatrix$1,\n\t\t\t\tcamera.coordinateSystem,\n\t\t\t\tcamera.reversedDepth\n\t\t\t);\n\n\t\t\tif ( _frustum$1.intersectsObject( object ) ) {\n\n\t\t\t\treturn true; // Object is visible in at least one camera\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false; // Not visible in any camera\n\n\t}\n\n\t/**\n\t * Returns `true` if the given sprite is intersecting any frustum\n\t * from the camera array.\n\t *\n\t * @param {Sprite} sprite - The sprite to test.\n\t * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.\n\t * @return {boolean} Whether the sprite is visible in any camera.\n\t */\n\tintersectsSprite( sprite, cameraArray ) {\n\n\t\tif ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\tfor ( let i = 0; i < cameraArray.cameras.length; i ++ ) {\n\n\t\t\tconst camera = cameraArray.cameras[ i ];\n\n\t\t\t_projScreenMatrix$1.multiplyMatrices(\n\t\t\t\tcamera.projectionMatrix,\n\t\t\t\tcamera.matrixWorldInverse\n\t\t\t);\n\n\t\t\t_frustum$1.setFromProjectionMatrix(\n\t\t\t\t_projScreenMatrix$1,\n\t\t\t\tcamera.coordinateSystem,\n\t\t\t\tcamera.reversedDepth\n\t\t\t);\n\n\t\t\tif ( _frustum$1.intersectsSprite( sprite ) ) {\n\n\t\t\t\treturn true; // Sprite is visible in at least one camera\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false; // Not visible in any camera\n\n\t}\n\n\t/**\n\t * Returns `true` if the given bounding sphere is intersecting any frustum\n\t * from the camera array.\n\t *\n\t * @param {Sphere} sphere - The bounding sphere to test.\n\t * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.\n\t * @return {boolean} Whether the sphere is visible in any camera.\n\t */\n\tintersectsSphere( sphere, cameraArray ) {\n\n\t\tif ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\tfor ( let i = 0; i < cameraArray.cameras.length; i ++ ) {\n\n\t\t\tconst camera = cameraArray.cameras[ i ];\n\n\t\t\t_projScreenMatrix$1.multiplyMatrices(\n\t\t\t\tcamera.projectionMatrix,\n\t\t\t\tcamera.matrixWorldInverse\n\t\t\t);\n\n\t\t\t_frustum$1.setFromProjectionMatrix(\n\t\t\t\t_projScreenMatrix$1,\n\t\t\t\tcamera.coordinateSystem,\n\t\t\t\tcamera.reversedDepth\n\t\t\t);\n\n\t\t\tif ( _frustum$1.intersectsSphere( sphere ) ) {\n\n\t\t\t\treturn true; // Sphere is visible in at least one camera\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false; // Not visible in any camera\n\n\t}\n\n\t/**\n\t * Returns `true` if the given bounding box is intersecting any frustum\n\t * from the camera array.\n\t *\n\t * @param {Box3} box - The bounding box to test.\n\t * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.\n\t * @return {boolean} Whether the box is visible in any camera.\n\t */\n\tintersectsBox( box, cameraArray ) {\n\n\t\tif ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\tfor ( let i = 0; i < cameraArray.cameras.length; i ++ ) {\n\n\t\t\tconst camera = cameraArray.cameras[ i ];\n\n\t\t\t_projScreenMatrix$1.multiplyMatrices(\n\t\t\t\tcamera.projectionMatrix,\n\t\t\t\tcamera.matrixWorldInverse\n\t\t\t);\n\n\t\t\t_frustum$1.setFromProjectionMatrix(\n\t\t\t\t_projScreenMatrix$1,\n\t\t\t\tcamera.coordinateSystem,\n\t\t\t\tcamera.reversedDepth\n\t\t\t);\n\n\t\t\tif ( _frustum$1.intersectsBox( box ) ) {\n\n\t\t\t\treturn true; // Box is visible in at least one camera\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false; // Not visible in any camera\n\n\t}\n\n\t/**\n\t * Returns `true` if the given point lies within any frustum\n\t * from the camera array.\n\t *\n\t * @param {Vector3} point - The point to test.\n\t * @param {Object} cameraArray - An object with a cameras property containing an array of cameras.\n\t * @return {boolean} Whether the point is visible in any camera.\n\t */\n\tcontainsPoint( point, cameraArray ) {\n\n\t\tif ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\tfor ( let i = 0; i < cameraArray.cameras.length; i ++ ) {\n\n\t\t\tconst camera = cameraArray.cameras[ i ];\n\n\t\t\t_projScreenMatrix$1.multiplyMatrices(\n\t\t\t\tcamera.projectionMatrix,\n\t\t\t\tcamera.matrixWorldInverse\n\t\t\t);\n\n\t\t\t_frustum$1.setFromProjectionMatrix(\n\t\t\t\t_projScreenMatrix$1,\n\t\t\t\tcamera.coordinateSystem,\n\t\t\t\tcamera.reversedDepth\n\t\t\t);\n\n\t\t\tif ( _frustum$1.containsPoint( point ) ) {\n\n\t\t\t\treturn true; // Point is visible in at least one camera\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false; // Not visible in any camera\n\n\t}\n\n\t/**\n\t * Returns a new frustum array with copied values from this instance.\n\t *\n\t * @return {FrustumArray} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new FrustumArray();\n\n\t}\n\n}\n\nfunction ascIdSort( a, b ) {\n\n\treturn a - b;\n\n}\n\nfunction sortOpaque( a, b ) {\n\n\treturn a.z - b.z;\n\n}\n\nfunction sortTransparent( a, b ) {\n\n\treturn b.z - a.z;\n\n}\n\nclass MultiDrawRenderList {\n\n\tconstructor() {\n\n\t\tthis.index = 0;\n\t\tthis.pool = [];\n\t\tthis.list = [];\n\n\t}\n\n\tpush( start, count, z, index ) {\n\n\t\tconst pool = this.pool;\n\t\tconst list = this.list;\n\t\tif ( this.index >= pool.length ) {\n\n\t\t\tpool.push( {\n\n\t\t\t\tstart: -1,\n\t\t\t\tcount: -1,\n\t\t\t\tz: -1,\n\t\t\t\tindex: -1,\n\n\t\t\t} );\n\n\t\t}\n\n\t\tconst item = pool[ this.index ];\n\t\tlist.push( item );\n\t\tthis.index ++;\n\n\t\titem.start = start;\n\t\titem.count = count;\n\t\titem.z = z;\n\t\titem.index = index;\n\n\t}\n\n\treset() {\n\n\t\tthis.list.length = 0;\n\t\tthis.index = 0;\n\n\t}\n\n}\n\nconst _matrix$1 = /*@__PURE__*/ new Matrix4();\nconst _whiteColor = /*@__PURE__*/ new Color( 1, 1, 1 );\nconst _frustum = /*@__PURE__*/ new Frustum();\nconst _frustumArray = /*@__PURE__*/ new FrustumArray();\nconst _box$1 = /*@__PURE__*/ new Box3();\nconst _sphere$2 = /*@__PURE__*/ new Sphere();\nconst _vector$5 = /*@__PURE__*/ new Vector3();\nconst _forward$1 = /*@__PURE__*/ new Vector3();\nconst _temp = /*@__PURE__*/ new Vector3();\nconst _renderList = /*@__PURE__*/ new MultiDrawRenderList();\nconst _mesh = /*@__PURE__*/ new Mesh();\nconst _batchIntersects = [];\n\n// copies data from attribute \"src\" into \"target\" starting at \"targetOffset\"\nfunction copyAttributeData( src, target, targetOffset = 0 ) {\n\n\tconst itemSize = target.itemSize;\n\tif ( src.isInterleavedBufferAttribute || src.array.constructor !== target.array.constructor ) {\n\n\t\t// use the component getters and setters if the array data cannot\n\t\t// be copied directly\n\t\tconst vertexCount = src.count;\n\t\tfor ( let i = 0; i < vertexCount; i ++ ) {\n\n\t\t\tfor ( let c = 0; c < itemSize; c ++ ) {\n\n\t\t\t\ttarget.setComponent( i + targetOffset, c, src.getComponent( i, c ) );\n\n\t\t\t}\n\n\t\t}\n\n\t} else {\n\n\t\t// faster copy approach using typed array set function\n\t\ttarget.array.set( src.array, targetOffset * itemSize );\n\n\t}\n\n\ttarget.needsUpdate = true;\n\n}\n\n// safely copies array contents to a potentially smaller array\nfunction copyArrayContents( src, target ) {\n\n\tif ( src.constructor !== target.constructor ) {\n\n\t\t// if arrays are of a different type (eg due to index size increasing) then data must be per-element copied\n\t\tconst len = Math.min( src.length, target.length );\n\t\tfor ( let i = 0; i < len; i ++ ) {\n\n\t\t\ttarget[ i ] = src[ i ];\n\n\t\t}\n\n\t} else {\n\n\t\t// if the arrays use the same data layout we can use a fast block copy\n\t\tconst len = Math.min( src.length, target.length );\n\t\ttarget.set( new src.constructor( src.buffer, 0, len ) );\n\n\t}\n\n}\n\n/**\n * A special version of a mesh with multi draw batch rendering support. Use\n * this class if you have to render a large number of objects with the same\n * material but with different geometries or world transformations. The usage of\n * `BatchedMesh` will help you to reduce the number of draw calls and thus improve the overall\n * rendering performance in your application.\n *\n * ```js\n * const box = new THREE.BoxGeometry( 1, 1, 1 );\n * const sphere = new THREE.SphereGeometry( 1, 12, 12 );\n * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );\n *\n * // initialize and add geometries into the batched mesh\n * const batchedMesh = new BatchedMesh( 10, 5000, 10000, material );\n * const boxGeometryId = batchedMesh.addGeometry( box );\n * const sphereGeometryId = batchedMesh.addGeometry( sphere );\n *\n * // create instances of those geometries\n * const boxInstancedId1 = batchedMesh.addInstance( boxGeometryId );\n * const boxInstancedId2 = batchedMesh.addInstance( boxGeometryId );\n *\n * const sphereInstancedId1 = batchedMesh.addInstance( sphereGeometryId );\n * const sphereInstancedId2 = batchedMesh.addInstance( sphereGeometryId );\n *\n * // position the geometries\n * batchedMesh.setMatrixAt( boxInstancedId1, boxMatrix1 );\n * batchedMesh.setMatrixAt( boxInstancedId2, boxMatrix2 );\n *\n * batchedMesh.setMatrixAt( sphereInstancedId1, sphereMatrix1 );\n * batchedMesh.setMatrixAt( sphereInstancedId2, sphereMatrix2 );\n *\n * scene.add( batchedMesh );\n * ```\n *\n * @augments Mesh\n */\nclass BatchedMesh extends Mesh {\n\n\t/**\n\t * Constructs a new batched mesh.\n\t *\n\t * @param {number} maxInstanceCount - The maximum number of individual instances planned to be added and rendered.\n\t * @param {number} maxVertexCount - The maximum number of vertices to be used by all unique geometries.\n\t * @param {number} [maxIndexCount=maxVertexCount*2] - The maximum number of indices to be used by all unique geometries\n\t * @param {Material|Array<Material>} [material] - The mesh material.\n\t */\n\tconstructor( maxInstanceCount, maxVertexCount, maxIndexCount = maxVertexCount * 2, material ) {\n\n\t\tsuper( new BufferGeometry(), material );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isBatchedMesh = true;\n\n\t\t/**\n\t\t * When set ot `true`, the individual objects of a batch are frustum culled.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.perObjectFrustumCulled = true;\n\n\t\t/**\n\t\t * When set to `true`, the individual objects of a batch are sorted to improve overdraw-related artifacts.\n\t\t * If the material is marked as \"transparent\" objects are rendered back to front and if not then they are\n\t\t * rendered front to back.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.sortObjects = true;\n\n\t\t/**\n\t\t * The bounding box of the batched mesh. Can be computed via {@link BatchedMesh#computeBoundingBox}.\n\t\t *\n\t\t * @type {?Box3}\n\t\t * @default null\n\t\t */\n\t\tthis.boundingBox = null;\n\n\t\t/**\n\t\t * The bounding sphere of the batched mesh. Can be computed via {@link BatchedMesh#computeBoundingSphere}.\n\t\t *\n\t\t * @type {?Sphere}\n\t\t * @default null\n\t\t */\n\t\tthis.boundingSphere = null;\n\n\t\t/**\n\t\t * Takes a sort a function that is run before render. The function takes a list of instances to\n\t\t * sort and a camera. The objects in the list include a \"z\" field to perform a depth-ordered\n\t\t * sort with.\n\t\t *\n\t\t * @type {?Function}\n\t\t * @default null\n\t\t */\n\t\tthis.customSort = null;\n\n\t\t// stores visible, active, and geometry id per instance and reserved buffer ranges for geometries\n\t\tthis._instanceInfo = [];\n\t\tthis._geometryInfo = [];\n\n\t\t// instance, geometry ids that have been set as inactive, and are available to be overwritten\n\t\tthis._availableInstanceIds = [];\n\t\tthis._availableGeometryIds = [];\n\n\t\t// used to track where the next point is that geometry should be inserted\n\t\tthis._nextIndexStart = 0;\n\t\tthis._nextVertexStart = 0;\n\t\tthis._geometryCount = 0;\n\n\t\t// flags\n\t\tthis._visibilityChanged = true;\n\t\tthis._geometryInitialized = false;\n\n\t\t// cached user options\n\t\tthis._maxInstanceCount = maxInstanceCount;\n\t\tthis._maxVertexCount = maxVertexCount;\n\t\tthis._maxIndexCount = maxIndexCount;\n\n\t\t// buffers for multi draw\n\t\tthis._multiDrawCounts = new Int32Array( maxInstanceCount );\n\t\tthis._multiDrawStarts = new Int32Array( maxInstanceCount );\n\t\tthis._multiDrawCount = 0;\n\t\tthis._multiDrawInstances = null;\n\n\t\t// Local matrix per geometry by using data texture\n\t\tthis._matricesTexture = null;\n\t\tthis._indirectTexture = null;\n\t\tthis._colorsTexture = null;\n\n\t\tthis._initMatricesTexture();\n\t\tthis._initIndirectTexture();\n\n\t}\n\n\t/**\n\t * The maximum number of individual instances that can be stored in the batch.\n\t *\n\t * @type {number}\n\t * @readonly\n\t */\n\tget maxInstanceCount() {\n\n\t\treturn this._maxInstanceCount;\n\n\t}\n\n\t/**\n\t * The instance count.\n\t *\n\t * @type {number}\n\t * @readonly\n\t */\n\tget instanceCount() {\n\n\t\treturn this._instanceInfo.length - this._availableInstanceIds.length;\n\n\t}\n\n\t/**\n\t * The number of unused vertices.\n\t *\n\t * @type {number}\n\t * @readonly\n\t */\n\tget unusedVertexCount() {\n\n\t\treturn this._maxVertexCount - this._nextVertexStart;\n\n\t}\n\n\t/**\n\t * The number of unused indices.\n\t *\n\t * @type {number}\n\t * @readonly\n\t */\n\tget unusedIndexCount() {\n\n\t\treturn this._maxIndexCount - this._nextIndexStart;\n\n\t}\n\n\t_initMatricesTexture() {\n\n\t\t// layout (1 matrix = 4 pixels)\n\t\t//      RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)\n\t\t//  with  8x8  pixel texture max   16 matrices * 4 pixels =  (8 * 8)\n\t\t//       16x16 pixel texture max   64 matrices * 4 pixels = (16 * 16)\n\t\t//       32x32 pixel texture max  256 matrices * 4 pixels = (32 * 32)\n\t\t//       64x64 pixel texture max 1024 matrices * 4 pixels = (64 * 64)\n\n\t\tlet size = Math.sqrt( this._maxInstanceCount * 4 ); // 4 pixels needed for 1 matrix\n\t\tsize = Math.ceil( size / 4 ) * 4;\n\t\tsize = Math.max( size, 4 );\n\n\t\tconst matricesArray = new Float32Array( size * size * 4 ); // 4 floats per RGBA pixel\n\t\tconst matricesTexture = new DataTexture( matricesArray, size, size, RGBAFormat, FloatType );\n\n\t\tthis._matricesTexture = matricesTexture;\n\n\t}\n\n\t_initIndirectTexture() {\n\n\t\tlet size = Math.sqrt( this._maxInstanceCount );\n\t\tsize = Math.ceil( size );\n\n\t\tconst indirectArray = new Uint32Array( size * size );\n\t\tconst indirectTexture = new DataTexture( indirectArray, size, size, RedIntegerFormat, UnsignedIntType );\n\n\t\tthis._indirectTexture = indirectTexture;\n\n\t}\n\n\t_initColorsTexture() {\n\n\t\tlet size = Math.sqrt( this._maxInstanceCount );\n\t\tsize = Math.ceil( size );\n\n\t\t// 4 floats per RGBA pixel initialized to white\n\t\tconst colorsArray = new Float32Array( size * size * 4 ).fill( 1 );\n\t\tconst colorsTexture = new DataTexture( colorsArray, size, size, RGBAFormat, FloatType );\n\t\tcolorsTexture.colorSpace = ColorManagement.workingColorSpace;\n\n\t\tthis._colorsTexture = colorsTexture;\n\n\t}\n\n\t_initializeGeometry( reference ) {\n\n\t\tconst geometry = this.geometry;\n\t\tconst maxVertexCount = this._maxVertexCount;\n\t\tconst maxIndexCount = this._maxIndexCount;\n\t\tif ( this._geometryInitialized === false ) {\n\n\t\t\tfor ( const attributeName in reference.attributes ) {\n\n\t\t\t\tconst srcAttribute = reference.getAttribute( attributeName );\n\t\t\t\tconst { array, itemSize, normalized } = srcAttribute;\n\n\t\t\t\tconst dstArray = new array.constructor( maxVertexCount * itemSize );\n\t\t\t\tconst dstAttribute = new BufferAttribute( dstArray, itemSize, normalized );\n\n\t\t\t\tgeometry.setAttribute( attributeName, dstAttribute );\n\n\t\t\t}\n\n\t\t\tif ( reference.getIndex() !== null ) {\n\n\t\t\t\t// Reserve last u16 index for primitive restart.\n\t\t\t\tconst indexArray = maxVertexCount > 65535\n\t\t\t\t\t? new Uint32Array( maxIndexCount )\n\t\t\t\t\t: new Uint16Array( maxIndexCount );\n\n\t\t\t\tgeometry.setIndex( new BufferAttribute( indexArray, 1 ) );\n\n\t\t\t}\n\n\t\t\tthis._geometryInitialized = true;\n\n\t\t}\n\n\t}\n\n\t// Make sure the geometry is compatible with the existing combined geometry attributes\n\t_validateGeometry( geometry ) {\n\n\t\t// check to ensure the geometries are using consistent attributes and indices\n\t\tconst batchGeometry = this.geometry;\n\t\tif ( Boolean( geometry.getIndex() ) !== Boolean( batchGeometry.getIndex() ) ) {\n\n\t\t\tthrow new Error( 'THREE.BatchedMesh: All geometries must consistently have \"index\".' );\n\n\t\t}\n\n\t\tfor ( const attributeName in batchGeometry.attributes ) {\n\n\t\t\tif ( ! geometry.hasAttribute( attributeName ) ) {\n\n\t\t\t\tthrow new Error( `THREE.BatchedMesh: Added geometry missing \"${ attributeName }\". All geometries must have consistent attributes.` );\n\n\t\t\t}\n\n\t\t\tconst srcAttribute = geometry.getAttribute( attributeName );\n\t\t\tconst dstAttribute = batchGeometry.getAttribute( attributeName );\n\t\t\tif ( srcAttribute.itemSize !== dstAttribute.itemSize || srcAttribute.normalized !== dstAttribute.normalized ) {\n\n\t\t\t\tthrow new Error( 'THREE.BatchedMesh: All attributes must have a consistent itemSize and normalized value.' );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Validates the instance defined by the given ID.\n\t *\n\t * @param {number} instanceId - The instance to validate.\n\t */\n\tvalidateInstanceId( instanceId ) {\n\n\t\tconst instanceInfo = this._instanceInfo;\n\t\tif ( instanceId < 0 || instanceId >= instanceInfo.length || instanceInfo[ instanceId ].active === false ) {\n\n\t\t\tthrow new Error( `THREE.BatchedMesh: Invalid instanceId ${instanceId}. Instance is either out of range or has been deleted.` );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Validates the geometry defined by the given ID.\n\t *\n\t * @param {number} geometryId - The geometry to validate.\n\t */\n\tvalidateGeometryId( geometryId ) {\n\n\t\tconst geometryInfoList = this._geometryInfo;\n\t\tif ( geometryId < 0 || geometryId >= geometryInfoList.length || geometryInfoList[ geometryId ].active === false ) {\n\n\t\t\tthrow new Error( `THREE.BatchedMesh: Invalid geometryId ${geometryId}. Geometry is either out of range or has been deleted.` );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Takes a sort a function that is run before render. The function takes a list of instances to\n\t * sort and a camera. The objects in the list include a \"z\" field to perform a depth-ordered sort with.\n\t *\n\t * @param {Function} func - The custom sort function.\n\t * @return {BatchedMesh} A reference to this batched mesh.\n\t */\n\tsetCustomSort( func ) {\n\n\t\tthis.customSort = func;\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes the bounding box, updating {@link BatchedMesh#boundingBox}.\n\t * Bounding boxes aren't computed by default. They need to be explicitly computed,\n\t * otherwise they are `null`.\n\t */\n\tcomputeBoundingBox() {\n\n\t\tif ( this.boundingBox === null ) {\n\n\t\t\tthis.boundingBox = new Box3();\n\n\t\t}\n\n\t\tconst boundingBox = this.boundingBox;\n\t\tconst instanceInfo = this._instanceInfo;\n\n\t\tboundingBox.makeEmpty();\n\t\tfor ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {\n\n\t\t\tif ( instanceInfo[ i ].active === false ) continue;\n\n\t\t\tconst geometryId = instanceInfo[ i ].geometryIndex;\n\t\t\tthis.getMatrixAt( i, _matrix$1 );\n\t\t\tthis.getBoundingBoxAt( geometryId, _box$1 ).applyMatrix4( _matrix$1 );\n\t\t\tboundingBox.union( _box$1 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Computes the bounding sphere, updating {@link BatchedMesh#boundingSphere}.\n\t * Bounding spheres aren't computed by default. They need to be explicitly computed,\n\t * otherwise they are `null`.\n\t */\n\tcomputeBoundingSphere() {\n\n\t\tif ( this.boundingSphere === null ) {\n\n\t\t\tthis.boundingSphere = new Sphere();\n\n\t\t}\n\n\t\tconst boundingSphere = this.boundingSphere;\n\t\tconst instanceInfo = this._instanceInfo;\n\n\t\tboundingSphere.makeEmpty();\n\t\tfor ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {\n\n\t\t\tif ( instanceInfo[ i ].active === false ) continue;\n\n\t\t\tconst geometryId = instanceInfo[ i ].geometryIndex;\n\t\t\tthis.getMatrixAt( i, _matrix$1 );\n\t\t\tthis.getBoundingSphereAt( geometryId, _sphere$2 ).applyMatrix4( _matrix$1 );\n\t\t\tboundingSphere.union( _sphere$2 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Adds a new instance to the batch using the geometry of the given ID and returns\n\t * a new id referring to the new instance to be used by other functions.\n\t *\n\t * @param {number} geometryId - The ID of a previously added geometry via {@link BatchedMesh#addGeometry}.\n\t * @return {number} The instance ID.\n\t */\n\taddInstance( geometryId ) {\n\n\t\tconst atCapacity = this._instanceInfo.length >= this.maxInstanceCount;\n\n\t\t// ensure we're not over geometry\n\t\tif ( atCapacity && this._availableInstanceIds.length === 0 ) {\n\n\t\t\tthrow new Error( 'THREE.BatchedMesh: Maximum item count reached.' );\n\n\t\t}\n\n\t\tconst instanceInfo = {\n\t\t\tvisible: true,\n\t\t\tactive: true,\n\t\t\tgeometryIndex: geometryId,\n\t\t};\n\n\t\tlet drawId = null;\n\n\t\t// Prioritize using previously freed instance ids\n\t\tif ( this._availableInstanceIds.length > 0 ) {\n\n\t\t\tthis._availableInstanceIds.sort( ascIdSort );\n\n\t\t\tdrawId = this._availableInstanceIds.shift();\n\t\t\tthis._instanceInfo[ drawId ] = instanceInfo;\n\n\t\t} else {\n\n\t\t\tdrawId = this._instanceInfo.length;\n\t\t\tthis._instanceInfo.push( instanceInfo );\n\n\t\t}\n\n\t\tconst matricesTexture = this._matricesTexture;\n\t\t_matrix$1.identity().toArray( matricesTexture.image.data, drawId * 16 );\n\t\tmatricesTexture.needsUpdate = true;\n\n\t\tconst colorsTexture = this._colorsTexture;\n\t\tif ( colorsTexture ) {\n\n\t\t\t_whiteColor.toArray( colorsTexture.image.data, drawId * 4 );\n\t\t\tcolorsTexture.needsUpdate = true;\n\n\t\t}\n\n\t\tthis._visibilityChanged = true;\n\t\treturn drawId;\n\n\t}\n\n\t/**\n\t * Adds the given geometry to the batch and returns the associated\n\t * geometry id referring to it to be used in other functions.\n\t *\n\t * @param {BufferGeometry} geometry - The geometry to add.\n\t * @param {number} [reservedVertexCount=-1] - Optional parameter specifying the amount of\n\t * vertex buffer space to reserve for the added geometry. This is necessary if it is planned\n\t * to set a new geometry at this index at a later time that is larger than the original geometry.\n\t * Defaults to the length of the given geometry vertex buffer.\n\t * @param {number} [reservedIndexCount=-1] - Optional parameter specifying the amount of index\n\t * buffer space to reserve for the added geometry. This is necessary if it is planned to set a\n\t * new geometry at this index at a later time that is larger than the original geometry. Defaults to\n\t * the length of the given geometry index buffer.\n\t * @return {number} The geometry ID.\n\t */\n\taddGeometry( geometry, reservedVertexCount = -1, reservedIndexCount = -1 ) {\n\n\t\tthis._initializeGeometry( geometry );\n\n\t\tthis._validateGeometry( geometry );\n\n\t\tconst geometryInfo = {\n\t\t\t// geometry information\n\t\t\tvertexStart: -1,\n\t\t\tvertexCount: -1,\n\t\t\treservedVertexCount: -1,\n\n\t\t\tindexStart: -1,\n\t\t\tindexCount: -1,\n\t\t\treservedIndexCount: -1,\n\n\t\t\t// draw range information\n\t\t\tstart: -1,\n\t\t\tcount: -1,\n\n\t\t\t// state\n\t\t\tboundingBox: null,\n\t\t\tboundingSphere: null,\n\t\t\tactive: true,\n\t\t};\n\n\t\tconst geometryInfoList = this._geometryInfo;\n\t\tgeometryInfo.vertexStart = this._nextVertexStart;\n\t\tgeometryInfo.reservedVertexCount = reservedVertexCount === -1 ? geometry.getAttribute( 'position' ).count : reservedVertexCount;\n\n\t\tconst index = geometry.getIndex();\n\t\tconst hasIndex = index !== null;\n\t\tif ( hasIndex ) {\n\n\t\t\tgeometryInfo.indexStart = this._nextIndexStart;\n\t\t\tgeometryInfo.reservedIndexCount = reservedIndexCount === -1 ? index.count : reservedIndexCount;\n\n\t\t}\n\n\t\tif (\n\t\t\tgeometryInfo.indexStart !== -1 &&\n\t\t\tgeometryInfo.indexStart + geometryInfo.reservedIndexCount > this._maxIndexCount ||\n\t\t\tgeometryInfo.vertexStart + geometryInfo.reservedVertexCount > this._maxVertexCount\n\t\t) {\n\n\t\t\tthrow new Error( 'THREE.BatchedMesh: Reserved space request exceeds the maximum buffer size.' );\n\n\t\t}\n\n\t\t// update id\n\t\tlet geometryId;\n\t\tif ( this._availableGeometryIds.length > 0 ) {\n\n\t\t\tthis._availableGeometryIds.sort( ascIdSort );\n\n\t\t\tgeometryId = this._availableGeometryIds.shift();\n\t\t\tgeometryInfoList[ geometryId ] = geometryInfo;\n\n\n\t\t} else {\n\n\t\t\tgeometryId = this._geometryCount;\n\t\t\tthis._geometryCount ++;\n\t\t\tgeometryInfoList.push( geometryInfo );\n\n\t\t}\n\n\t\t// update the geometry\n\t\tthis.setGeometryAt( geometryId, geometry );\n\n\t\t// increment the next geometry position\n\t\tthis._nextIndexStart = geometryInfo.indexStart + geometryInfo.reservedIndexCount;\n\t\tthis._nextVertexStart = geometryInfo.vertexStart + geometryInfo.reservedVertexCount;\n\n\t\treturn geometryId;\n\n\t}\n\n\t/**\n\t * Replaces the geometry at the given ID with the provided geometry. Throws an error if there\n\t * is not enough space reserved for geometry. Calling this will change all instances that are\n\t * rendering that geometry.\n\t *\n\t * @param {number} geometryId - The ID of the geometry that should be replaced with the given geometry.\n\t * @param {BufferGeometry} geometry - The new geometry.\n\t * @return {number} The geometry ID.\n\t */\n\tsetGeometryAt( geometryId, geometry ) {\n\n\t\tif ( geometryId >= this._geometryCount ) {\n\n\t\t\tthrow new Error( 'THREE.BatchedMesh: Maximum geometry count reached.' );\n\n\t\t}\n\n\t\tthis._validateGeometry( geometry );\n\n\t\tconst batchGeometry = this.geometry;\n\t\tconst hasIndex = batchGeometry.getIndex() !== null;\n\t\tconst dstIndex = batchGeometry.getIndex();\n\t\tconst srcIndex = geometry.getIndex();\n\t\tconst geometryInfo = this._geometryInfo[ geometryId ];\n\t\tif (\n\t\t\thasIndex &&\n\t\t\tsrcIndex.count > geometryInfo.reservedIndexCount ||\n\t\t\tgeometry.attributes.position.count > geometryInfo.reservedVertexCount\n\t\t) {\n\n\t\t\tthrow new Error( 'THREE.BatchedMesh: Reserved space not large enough for provided geometry.' );\n\n\t\t}\n\n\t\t// copy geometry buffer data over\n\t\tconst vertexStart = geometryInfo.vertexStart;\n\t\tconst reservedVertexCount = geometryInfo.reservedVertexCount;\n\t\tgeometryInfo.vertexCount = geometry.getAttribute( 'position' ).count;\n\n\t\tfor ( const attributeName in batchGeometry.attributes ) {\n\n\t\t\t// copy attribute data\n\t\t\tconst srcAttribute = geometry.getAttribute( attributeName );\n\t\t\tconst dstAttribute = batchGeometry.getAttribute( attributeName );\n\t\t\tcopyAttributeData( srcAttribute, dstAttribute, vertexStart );\n\n\t\t\t// fill the rest in with zeroes\n\t\t\tconst itemSize = srcAttribute.itemSize;\n\t\t\tfor ( let i = srcAttribute.count, l = reservedVertexCount; i < l; i ++ ) {\n\n\t\t\t\tconst index = vertexStart + i;\n\t\t\t\tfor ( let c = 0; c < itemSize; c ++ ) {\n\n\t\t\t\t\tdstAttribute.setComponent( index, c, 0 );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tdstAttribute.needsUpdate = true;\n\t\t\tdstAttribute.addUpdateRange( vertexStart * itemSize, reservedVertexCount * itemSize );\n\n\t\t}\n\n\t\t// copy index\n\t\tif ( hasIndex ) {\n\n\t\t\tconst indexStart = geometryInfo.indexStart;\n\t\t\tconst reservedIndexCount = geometryInfo.reservedIndexCount;\n\t\t\tgeometryInfo.indexCount = geometry.getIndex().count;\n\n\t\t\t// copy index data over\n\t\t\tfor ( let i = 0; i < srcIndex.count; i ++ ) {\n\n\t\t\t\tdstIndex.setX( indexStart + i, vertexStart + srcIndex.getX( i ) );\n\n\t\t\t}\n\n\t\t\t// fill the rest in with zeroes\n\t\t\tfor ( let i = srcIndex.count, l = reservedIndexCount; i < l; i ++ ) {\n\n\t\t\t\tdstIndex.setX( indexStart + i, vertexStart );\n\n\t\t\t}\n\n\t\t\tdstIndex.needsUpdate = true;\n\t\t\tdstIndex.addUpdateRange( indexStart, geometryInfo.reservedIndexCount );\n\n\t\t}\n\n\t\t// update the draw range\n\t\tgeometryInfo.start = hasIndex ? geometryInfo.indexStart : geometryInfo.vertexStart;\n\t\tgeometryInfo.count = hasIndex ? geometryInfo.indexCount : geometryInfo.vertexCount;\n\n\t\t// store the bounding boxes\n\t\tgeometryInfo.boundingBox = null;\n\t\tif ( geometry.boundingBox !== null ) {\n\n\t\t\tgeometryInfo.boundingBox = geometry.boundingBox.clone();\n\n\t\t}\n\n\t\tgeometryInfo.boundingSphere = null;\n\t\tif ( geometry.boundingSphere !== null ) {\n\n\t\t\tgeometryInfo.boundingSphere = geometry.boundingSphere.clone();\n\n\t\t}\n\n\t\tthis._visibilityChanged = true;\n\t\treturn geometryId;\n\n\t}\n\n\t/**\n\t * Deletes the geometry defined by the given ID from this batch. Any instances referencing\n\t * this geometry will also be removed as a side effect.\n\t *\n\t * @param {number} geometryId - The ID of the geometry to remove from the batch.\n\t * @return {BatchedMesh} A reference to this batched mesh.\n\t */\n\tdeleteGeometry( geometryId ) {\n\n\t\tconst geometryInfoList = this._geometryInfo;\n\t\tif ( geometryId >= geometryInfoList.length || geometryInfoList[ geometryId ].active === false ) {\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\t// delete any instances associated with this geometry\n\t\tconst instanceInfo = this._instanceInfo;\n\t\tfor ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {\n\n\t\t\tif ( instanceInfo[ i ].active && instanceInfo[ i ].geometryIndex === geometryId ) {\n\n\t\t\t\tthis.deleteInstance( i );\n\n\t\t\t}\n\n\t\t}\n\n\t\tgeometryInfoList[ geometryId ].active = false;\n\t\tthis._availableGeometryIds.push( geometryId );\n\t\tthis._visibilityChanged = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Deletes an existing instance from the batch using the given ID.\n\t *\n\t * @param {number} instanceId - The ID of the instance to remove from the batch.\n\t * @return {BatchedMesh} A reference to this batched mesh.\n\t */\n\tdeleteInstance( instanceId ) {\n\n\t\tthis.validateInstanceId( instanceId );\n\n\t\tthis._instanceInfo[ instanceId ].active = false;\n\t\tthis._availableInstanceIds.push( instanceId );\n\t\tthis._visibilityChanged = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Repacks the sub geometries in BatchedMesh to remove any unused space remaining from\n\t * previously deleted geometry, freeing up space to add new geometry.\n\t *\n\t * @return {BatchedMesh} A reference to this batched mesh.\n\t */\n\toptimize() {\n\n\t\t// track the next indices to copy data to\n\t\tlet nextVertexStart = 0;\n\t\tlet nextIndexStart = 0;\n\n\t\t// Iterate over all geometry ranges in order sorted from earliest in the geometry buffer to latest\n\t\t// in the geometry buffer. Because draw range objects can be reused there is no guarantee of their order.\n\t\tconst geometryInfoList = this._geometryInfo;\n\t\tconst indices = geometryInfoList\n\t\t\t.map( ( e, i ) => i )\n\t\t\t.sort( ( a, b ) => {\n\n\t\t\t\treturn geometryInfoList[ a ].vertexStart - geometryInfoList[ b ].vertexStart;\n\n\t\t\t} );\n\n\t\tconst geometry = this.geometry;\n\t\tfor ( let i = 0, l = geometryInfoList.length; i < l; i ++ ) {\n\n\t\t\t// if a geometry range is inactive then don't copy anything\n\t\t\tconst index = indices[ i ];\n\t\t\tconst geometryInfo = geometryInfoList[ index ];\n\t\t\tif ( geometryInfo.active === false ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\t// if a geometry contains an index buffer then shift it, as well\n\t\t\tif ( geometry.index !== null ) {\n\n\t\t\t\tif ( geometryInfo.indexStart !== nextIndexStart ) {\n\n\t\t\t\t\tconst { indexStart, vertexStart, reservedIndexCount } = geometryInfo;\n\t\t\t\t\tconst index = geometry.index;\n\t\t\t\t\tconst array = index.array;\n\n\t\t\t\t\t// shift the index pointers based on how the vertex data will shift\n\t\t\t\t\t// adjusting the index must happen first so the original vertex start value is available\n\t\t\t\t\tconst elementDelta = nextVertexStart - vertexStart;\n\t\t\t\t\tfor ( let j = indexStart; j < indexStart + reservedIndexCount; j ++ ) {\n\n\t\t\t\t\t\tarray[ j ] = array[ j ] + elementDelta;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tindex.array.copyWithin( nextIndexStart, indexStart, indexStart + reservedIndexCount );\n\t\t\t\t\tindex.addUpdateRange( nextIndexStart, reservedIndexCount );\n\t\t\t\t\tindex.needsUpdate = true;\n\n\t\t\t\t\tgeometryInfo.indexStart = nextIndexStart;\n\n\t\t\t\t}\n\n\t\t\t\tnextIndexStart += geometryInfo.reservedIndexCount;\n\n\t\t\t}\n\n\t\t\t// if a geometry needs to be moved then copy attribute data to overwrite unused space\n\t\t\tif ( geometryInfo.vertexStart !== nextVertexStart ) {\n\n\t\t\t\tconst { vertexStart, reservedVertexCount } = geometryInfo;\n\t\t\t\tconst attributes = geometry.attributes;\n\t\t\t\tfor ( const key in attributes ) {\n\n\t\t\t\t\tconst attribute = attributes[ key ];\n\t\t\t\t\tconst { array, itemSize } = attribute;\n\t\t\t\t\tarray.copyWithin( nextVertexStart * itemSize, vertexStart * itemSize, ( vertexStart + reservedVertexCount ) * itemSize );\n\t\t\t\t\tattribute.addUpdateRange( nextVertexStart * itemSize, reservedVertexCount * itemSize );\n\t\t\t\t\tattribute.needsUpdate = true;\n\n\t\t\t\t}\n\n\t\t\t\tgeometryInfo.vertexStart = nextVertexStart;\n\n\t\t\t}\n\n\t\t\tnextVertexStart += geometryInfo.reservedVertexCount;\n\t\t\tgeometryInfo.start = geometry.index ? geometryInfo.indexStart : geometryInfo.vertexStart;\n\n\t\t\t// step the next geometry points to the shifted position\n\t\t\tthis._nextIndexStart = geometry.index ? geometryInfo.indexStart + geometryInfo.reservedIndexCount : 0;\n\t\t\tthis._nextVertexStart = geometryInfo.vertexStart + geometryInfo.reservedVertexCount;\n\n\t\t}\n\n\t\tthis._visibilityChanged = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the bounding box for the given geometry.\n\t *\n\t * @param {number} geometryId - The ID of the geometry to return the bounding box for.\n\t * @param {Box3} target - The target object that is used to store the method's result.\n\t * @return {?Box3} The geometry's bounding box. Returns `null` if no geometry has been found for the given ID.\n\t */\n\tgetBoundingBoxAt( geometryId, target ) {\n\n\t\tif ( geometryId >= this._geometryCount ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\t// compute bounding box\n\t\tconst geometry = this.geometry;\n\t\tconst geometryInfo = this._geometryInfo[ geometryId ];\n\t\tif ( geometryInfo.boundingBox === null ) {\n\n\t\t\tconst box = new Box3();\n\t\t\tconst index = geometry.index;\n\t\t\tconst position = geometry.attributes.position;\n\t\t\tfor ( let i = geometryInfo.start, l = geometryInfo.start + geometryInfo.count; i < l; i ++ ) {\n\n\t\t\t\tlet iv = i;\n\t\t\t\tif ( index ) {\n\n\t\t\t\t\tiv = index.getX( iv );\n\n\t\t\t\t}\n\n\t\t\t\tbox.expandByPoint( _vector$5.fromBufferAttribute( position, iv ) );\n\n\t\t\t}\n\n\t\t\tgeometryInfo.boundingBox = box;\n\n\t\t}\n\n\t\ttarget.copy( geometryInfo.boundingBox );\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Returns the bounding sphere for the given geometry.\n\t *\n\t * @param {number} geometryId - The ID of the geometry to return the bounding sphere for.\n\t * @param {Sphere} target - The target object that is used to store the method's result.\n\t * @return {?Sphere} The geometry's bounding sphere. Returns `null` if no geometry has been found for the given ID.\n\t */\n\tgetBoundingSphereAt( geometryId, target ) {\n\n\t\tif ( geometryId >= this._geometryCount ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\t// compute bounding sphere\n\t\tconst geometry = this.geometry;\n\t\tconst geometryInfo = this._geometryInfo[ geometryId ];\n\t\tif ( geometryInfo.boundingSphere === null ) {\n\n\t\t\tconst sphere = new Sphere();\n\t\t\tthis.getBoundingBoxAt( geometryId, _box$1 );\n\t\t\t_box$1.getCenter( sphere.center );\n\n\t\t\tconst index = geometry.index;\n\t\t\tconst position = geometry.attributes.position;\n\n\t\t\tlet maxRadiusSq = 0;\n\t\t\tfor ( let i = geometryInfo.start, l = geometryInfo.start + geometryInfo.count; i < l; i ++ ) {\n\n\t\t\t\tlet iv = i;\n\t\t\t\tif ( index ) {\n\n\t\t\t\t\tiv = index.getX( iv );\n\n\t\t\t\t}\n\n\t\t\t\t_vector$5.fromBufferAttribute( position, iv );\n\t\t\t\tmaxRadiusSq = Math.max( maxRadiusSq, sphere.center.distanceToSquared( _vector$5 ) );\n\n\t\t\t}\n\n\t\t\tsphere.radius = Math.sqrt( maxRadiusSq );\n\t\t\tgeometryInfo.boundingSphere = sphere;\n\n\t\t}\n\n\t\ttarget.copy( geometryInfo.boundingSphere );\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Sets the given local transformation matrix to the defined instance.\n\t * Negatively scaled matrices are not supported.\n\t *\n\t * @param {number} instanceId - The ID of an instance to set the matrix of.\n\t * @param {Matrix4} matrix - A 4x4 matrix representing the local transformation of a single instance.\n\t * @return {BatchedMesh} A reference to this batched mesh.\n\t */\n\tsetMatrixAt( instanceId, matrix ) {\n\n\t\tthis.validateInstanceId( instanceId );\n\n\t\tconst matricesTexture = this._matricesTexture;\n\t\tconst matricesArray = this._matricesTexture.image.data;\n\t\tmatrix.toArray( matricesArray, instanceId * 16 );\n\t\tmatricesTexture.needsUpdate = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the local transformation matrix of the defined instance.\n\t *\n\t * @param {number} instanceId - The ID of an instance to get the matrix of.\n\t * @param {Matrix4} matrix - The target object that is used to store the method's result.\n\t * @return {Matrix4} The instance's local transformation matrix.\n\t */\n\tgetMatrixAt( instanceId, matrix ) {\n\n\t\tthis.validateInstanceId( instanceId );\n\t\treturn matrix.fromArray( this._matricesTexture.image.data, instanceId * 16 );\n\n\t}\n\n\t/**\n\t * Sets the given color to the defined instance.\n\t *\n\t * @param {number} instanceId - The ID of an instance to set the color of.\n\t * @param {Color} color - The color to set the instance to.\n\t * @return {BatchedMesh} A reference to this batched mesh.\n\t */\n\tsetColorAt( instanceId, color ) {\n\n\t\tthis.validateInstanceId( instanceId );\n\n\t\tif ( this._colorsTexture === null ) {\n\n\t\t\tthis._initColorsTexture();\n\n\t\t}\n\n\t\tcolor.toArray( this._colorsTexture.image.data, instanceId * 4 );\n\t\tthis._colorsTexture.needsUpdate = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the color of the defined instance.\n\t *\n\t * @param {number} instanceId - The ID of an instance to get the color of.\n\t * @param {Color} color - The target object that is used to store the method's result.\n\t * @return {Color} The instance's color.\n\t */\n\tgetColorAt( instanceId, color ) {\n\n\t\tthis.validateInstanceId( instanceId );\n\t\treturn color.fromArray( this._colorsTexture.image.data, instanceId * 4 );\n\n\t}\n\n\t/**\n\t * Sets the visibility of the instance.\n\t *\n\t * @param {number} instanceId - The id of the instance to set the visibility of.\n\t * @param {boolean} visible - Whether the instance is visible or not.\n\t * @return {BatchedMesh} A reference to this batched mesh.\n\t */\n\tsetVisibleAt( instanceId, visible ) {\n\n\t\tthis.validateInstanceId( instanceId );\n\n\t\tif ( this._instanceInfo[ instanceId ].visible === visible ) {\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\tthis._instanceInfo[ instanceId ].visible = visible;\n\t\tthis._visibilityChanged = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the visibility state of the defined instance.\n\t *\n\t * @param {number} instanceId - The ID of an instance to get the visibility state of.\n\t * @return {boolean} Whether the instance is visible or not.\n\t */\n\tgetVisibleAt( instanceId ) {\n\n\t\tthis.validateInstanceId( instanceId );\n\n\t\treturn this._instanceInfo[ instanceId ].visible;\n\n\t}\n\n\t/**\n\t * Sets the geometry ID of the instance at the given index.\n\t *\n\t * @param {number} instanceId - The ID of the instance to set the geometry ID of.\n\t * @param {number} geometryId - The geometry ID to be use by the instance.\n\t * @return {BatchedMesh} A reference to this batched mesh.\n\t */\n\tsetGeometryIdAt( instanceId, geometryId ) {\n\n\t\tthis.validateInstanceId( instanceId );\n\t\tthis.validateGeometryId( geometryId );\n\n\t\tthis._instanceInfo[ instanceId ].geometryIndex = geometryId;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the geometry ID of the defined instance.\n\t *\n\t * @param {number} instanceId - The ID of an instance to get the geometry ID of.\n\t * @return {number} The instance's geometry ID.\n\t */\n\tgetGeometryIdAt( instanceId ) {\n\n\t\tthis.validateInstanceId( instanceId );\n\n\t\treturn this._instanceInfo[ instanceId ].geometryIndex;\n\n\t}\n\n\t/**\n\t * Get the range representing the subset of triangles related to the attached geometry,\n\t * indicating the starting offset and count, or `null` if invalid.\n\t *\n\t * @param {number} geometryId - The id of the geometry to get the range of.\n\t * @param {Object} [target] - The target object that is used to store the method's result.\n\t * @return {{\n\t * \tvertexStart:number,vertexCount:number,reservedVertexCount:number,\n\t * \tindexStart:number,indexCount:number,reservedIndexCount:number,\n\t * \tstart:number,count:number\n\t * }} The result object with range data.\n\t */\n\tgetGeometryRangeAt( geometryId, target = {} ) {\n\n\t\tthis.validateGeometryId( geometryId );\n\n\t\tconst geometryInfo = this._geometryInfo[ geometryId ];\n\t\ttarget.vertexStart = geometryInfo.vertexStart;\n\t\ttarget.vertexCount = geometryInfo.vertexCount;\n\t\ttarget.reservedVertexCount = geometryInfo.reservedVertexCount;\n\n\t\ttarget.indexStart = geometryInfo.indexStart;\n\t\ttarget.indexCount = geometryInfo.indexCount;\n\t\ttarget.reservedIndexCount = geometryInfo.reservedIndexCount;\n\n\t\ttarget.start = geometryInfo.start;\n\t\ttarget.count = geometryInfo.count;\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Resizes the necessary buffers to support the provided number of instances.\n\t * If the provided arguments shrink the number of instances but there are not enough\n\t * unused Ids at the end of the list then an error is thrown.\n\t *\n\t * @param {number} maxInstanceCount - The max number of individual instances that can be added and rendered by the batch.\n\t*/\n\tsetInstanceCount( maxInstanceCount ) {\n\n\t\t// shrink the available instances as much as possible\n\t\tconst availableInstanceIds = this._availableInstanceIds;\n\t\tconst instanceInfo = this._instanceInfo;\n\t\tavailableInstanceIds.sort( ascIdSort );\n\t\twhile ( availableInstanceIds[ availableInstanceIds.length - 1 ] === instanceInfo.length - 1 ) {\n\n\t\t\tinstanceInfo.pop();\n\t\t\tavailableInstanceIds.pop();\n\n\t\t}\n\n\t\t// throw an error if it can't be shrunk to the desired size\n\t\tif ( maxInstanceCount < instanceInfo.length ) {\n\n\t\t\tthrow new Error( `BatchedMesh: Instance ids outside the range ${ maxInstanceCount } are being used. Cannot shrink instance count.` );\n\n\t\t}\n\n\t\t// copy the multi draw counts\n\t\tconst multiDrawCounts = new Int32Array( maxInstanceCount );\n\t\tconst multiDrawStarts = new Int32Array( maxInstanceCount );\n\t\tcopyArrayContents( this._multiDrawCounts, multiDrawCounts );\n\t\tcopyArrayContents( this._multiDrawStarts, multiDrawStarts );\n\n\t\tthis._multiDrawCounts = multiDrawCounts;\n\t\tthis._multiDrawStarts = multiDrawStarts;\n\t\tthis._maxInstanceCount = maxInstanceCount;\n\n\t\t// update texture data for instance sampling\n\t\tconst indirectTexture = this._indirectTexture;\n\t\tconst matricesTexture = this._matricesTexture;\n\t\tconst colorsTexture = this._colorsTexture;\n\n\t\tindirectTexture.dispose();\n\t\tthis._initIndirectTexture();\n\t\tcopyArrayContents( indirectTexture.image.data, this._indirectTexture.image.data );\n\n\t\tmatricesTexture.dispose();\n\t\tthis._initMatricesTexture();\n\t\tcopyArrayContents( matricesTexture.image.data, this._matricesTexture.image.data );\n\n\t\tif ( colorsTexture ) {\n\n\t\t\tcolorsTexture.dispose();\n\t\t\tthis._initColorsTexture();\n\t\t\tcopyArrayContents( colorsTexture.image.data, this._colorsTexture.image.data );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Resizes the available space in the batch's vertex and index buffer attributes to the provided sizes.\n\t * If the provided arguments shrink the geometry buffers but there is not enough unused space at the\n\t * end of the geometry attributes then an error is thrown.\n\t *\n\t * @param {number} maxVertexCount - The maximum number of vertices to be used by all unique geometries to resize to.\n\t * @param {number} maxIndexCount - The maximum number of indices to be used by all unique geometries to resize to.\n\t*/\n\tsetGeometrySize( maxVertexCount, maxIndexCount ) {\n\n\t\t// Check if we can shrink to the requested vertex attribute size\n\t\tconst validRanges = [ ...this._geometryInfo ].filter( info => info.active );\n\t\tconst requiredVertexLength = Math.max( ...validRanges.map( range => range.vertexStart + range.reservedVertexCount ) );\n\t\tif ( requiredVertexLength > maxVertexCount ) {\n\n\t\t\tthrow new Error( `BatchedMesh: Geometry vertex values are being used outside the range ${ maxIndexCount }. Cannot shrink further.` );\n\n\t\t}\n\n\t\t// Check if we can shrink to the requested index attribute size\n\t\tif ( this.geometry.index ) {\n\n\t\t\tconst requiredIndexLength = Math.max( ...validRanges.map( range => range.indexStart + range.reservedIndexCount ) );\n\t\t\tif ( requiredIndexLength > maxIndexCount ) {\n\n\t\t\t\tthrow new Error( `BatchedMesh: Geometry index values are being used outside the range ${ maxIndexCount }. Cannot shrink further.` );\n\n\t\t\t}\n\n\t\t}\n\n\t\t//\n\n\t\t// dispose of the previous geometry\n\t\tconst oldGeometry = this.geometry;\n\t\toldGeometry.dispose();\n\n\t\t// recreate the geometry needed based on the previous variant\n\t\tthis._maxVertexCount = maxVertexCount;\n\t\tthis._maxIndexCount = maxIndexCount;\n\n\t\tif ( this._geometryInitialized ) {\n\n\t\t\tthis._geometryInitialized = false;\n\t\t\tthis.geometry = new BufferGeometry();\n\t\t\tthis._initializeGeometry( oldGeometry );\n\n\t\t}\n\n\t\t// copy data from the previous geometry\n\t\tconst geometry = this.geometry;\n\t\tif ( oldGeometry.index ) {\n\n\t\t\tcopyArrayContents( oldGeometry.index.array, geometry.index.array );\n\n\t\t}\n\n\t\tfor ( const key in oldGeometry.attributes ) {\n\n\t\t\tcopyArrayContents( oldGeometry.attributes[ key ].array, geometry.attributes[ key ].array );\n\n\t\t}\n\n\t}\n\n\traycast( raycaster, intersects ) {\n\n\t\tconst instanceInfo = this._instanceInfo;\n\t\tconst geometryInfoList = this._geometryInfo;\n\t\tconst matrixWorld = this.matrixWorld;\n\t\tconst batchGeometry = this.geometry;\n\n\t\t// iterate over each geometry\n\t\t_mesh.material = this.material;\n\t\t_mesh.geometry.index = batchGeometry.index;\n\t\t_mesh.geometry.attributes = batchGeometry.attributes;\n\t\tif ( _mesh.geometry.boundingBox === null ) {\n\n\t\t\t_mesh.geometry.boundingBox = new Box3();\n\n\t\t}\n\n\t\tif ( _mesh.geometry.boundingSphere === null ) {\n\n\t\t\t_mesh.geometry.boundingSphere = new Sphere();\n\n\t\t}\n\n\t\tfor ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {\n\n\t\t\tif ( ! instanceInfo[ i ].visible || ! instanceInfo[ i ].active ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\tconst geometryId = instanceInfo[ i ].geometryIndex;\n\t\t\tconst geometryInfo = geometryInfoList[ geometryId ];\n\t\t\t_mesh.geometry.setDrawRange( geometryInfo.start, geometryInfo.count );\n\n\t\t\t// get the intersects\n\t\t\tthis.getMatrixAt( i, _mesh.matrixWorld ).premultiply( matrixWorld );\n\t\t\tthis.getBoundingBoxAt( geometryId, _mesh.geometry.boundingBox );\n\t\t\tthis.getBoundingSphereAt( geometryId, _mesh.geometry.boundingSphere );\n\t\t\t_mesh.raycast( raycaster, _batchIntersects );\n\n\t\t\t// add batch id to the intersects\n\t\t\tfor ( let j = 0, l = _batchIntersects.length; j < l; j ++ ) {\n\n\t\t\t\tconst intersect = _batchIntersects[ j ];\n\t\t\t\tintersect.object = this;\n\t\t\t\tintersect.batchId = i;\n\t\t\t\tintersects.push( intersect );\n\n\t\t\t}\n\n\t\t\t_batchIntersects.length = 0;\n\n\t\t}\n\n\t\t_mesh.material = null;\n\t\t_mesh.geometry.index = null;\n\t\t_mesh.geometry.attributes = {};\n\t\t_mesh.geometry.setDrawRange( 0, Infinity );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.geometry = source.geometry.clone();\n\t\tthis.perObjectFrustumCulled = source.perObjectFrustumCulled;\n\t\tthis.sortObjects = source.sortObjects;\n\t\tthis.boundingBox = source.boundingBox !== null ? source.boundingBox.clone() : null;\n\t\tthis.boundingSphere = source.boundingSphere !== null ? source.boundingSphere.clone() : null;\n\n\t\tthis._geometryInfo = source._geometryInfo.map( info => ( {\n\t\t\t...info,\n\n\t\t\tboundingBox: info.boundingBox !== null ? info.boundingBox.clone() : null,\n\t\t\tboundingSphere: info.boundingSphere !== null ? info.boundingSphere.clone() : null,\n\t\t} ) );\n\t\tthis._instanceInfo = source._instanceInfo.map( info => ( { ...info } ) );\n\n\t\tthis._availableInstanceIds = source._availableInstanceIds.slice();\n\t\tthis._availableGeometryIds = source._availableGeometryIds.slice();\n\n\t\tthis._nextIndexStart = source._nextIndexStart;\n\t\tthis._nextVertexStart = source._nextVertexStart;\n\t\tthis._geometryCount = source._geometryCount;\n\n\t\tthis._maxInstanceCount = source._maxInstanceCount;\n\t\tthis._maxVertexCount = source._maxVertexCount;\n\t\tthis._maxIndexCount = source._maxIndexCount;\n\n\t\tthis._geometryInitialized = source._geometryInitialized;\n\t\tthis._multiDrawCounts = source._multiDrawCounts.slice();\n\t\tthis._multiDrawStarts = source._multiDrawStarts.slice();\n\n\t\tthis._indirectTexture = source._indirectTexture.clone();\n\t\tthis._indirectTexture.image.data = this._indirectTexture.image.data.slice();\n\n\t\tthis._matricesTexture = source._matricesTexture.clone();\n\t\tthis._matricesTexture.image.data = this._matricesTexture.image.data.slice();\n\n\t\tif ( this._colorsTexture !== null ) {\n\n\t\t\tthis._colorsTexture = source._colorsTexture.clone();\n\t\t\tthis._colorsTexture.image.data = this._colorsTexture.image.data.slice();\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\t// Assuming the geometry is not shared with other meshes\n\t\tthis.geometry.dispose();\n\n\t\tthis._matricesTexture.dispose();\n\t\tthis._matricesTexture = null;\n\n\t\tthis._indirectTexture.dispose();\n\t\tthis._indirectTexture = null;\n\n\t\tif ( this._colorsTexture !== null ) {\n\n\t\t\tthis._colorsTexture.dispose();\n\t\t\tthis._colorsTexture = null;\n\n\t\t}\n\n\t}\n\n\tonBeforeRender( renderer, scene, camera, geometry, material/*, _group*/ ) {\n\n\t\t// if visibility has not changed and frustum culling and object sorting is not required\n\t\t// then skip iterating over all items\n\t\tif ( ! this._visibilityChanged && ! this.perObjectFrustumCulled && ! this.sortObjects ) {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\t// the indexed version of the multi draw function requires specifying the start\n\t\t// offset in bytes.\n\t\tconst index = geometry.getIndex();\n\t\tconst bytesPerElement = index === null ? 1 : index.array.BYTES_PER_ELEMENT;\n\n\t\tconst instanceInfo = this._instanceInfo;\n\t\tconst multiDrawStarts = this._multiDrawStarts;\n\t\tconst multiDrawCounts = this._multiDrawCounts;\n\t\tconst geometryInfoList = this._geometryInfo;\n\t\tconst perObjectFrustumCulled = this.perObjectFrustumCulled;\n\t\tconst indirectTexture = this._indirectTexture;\n\t\tconst indirectArray = indirectTexture.image.data;\n\n\t\tconst frustum = camera.isArrayCamera ? _frustumArray : _frustum;\n\t\t// prepare the frustum in the local frame\n\t\tif ( perObjectFrustumCulled && ! camera.isArrayCamera ) {\n\n\t\t\t_matrix$1\n\t\t\t\t.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse )\n\t\t\t\t.multiply( this.matrixWorld );\n\n\t\t\t_frustum.setFromProjectionMatrix(\n\t\t\t\t_matrix$1,\n\t\t\t\tcamera.coordinateSystem,\n\t\t\t\tcamera.reversedDepth\n\t\t\t);\n\n\t\t}\n\n\t\tlet multiDrawCount = 0;\n\t\tif ( this.sortObjects ) {\n\n\t\t\t// get the camera position in the local frame\n\t\t\t_matrix$1.copy( this.matrixWorld ).invert();\n\t\t\t_vector$5.setFromMatrixPosition( camera.matrixWorld ).applyMatrix4( _matrix$1 );\n\t\t\t_forward$1.set( 0, 0, -1 ).transformDirection( camera.matrixWorld ).transformDirection( _matrix$1 );\n\n\t\t\tfor ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {\n\n\t\t\t\tif ( instanceInfo[ i ].visible && instanceInfo[ i ].active ) {\n\n\t\t\t\t\tconst geometryId = instanceInfo[ i ].geometryIndex;\n\n\t\t\t\t\t// get the bounds in world space\n\t\t\t\t\tthis.getMatrixAt( i, _matrix$1 );\n\t\t\t\t\tthis.getBoundingSphereAt( geometryId, _sphere$2 ).applyMatrix4( _matrix$1 );\n\n\t\t\t\t\t// determine whether the batched geometry is within the frustum\n\t\t\t\t\tlet culled = false;\n\t\t\t\t\tif ( perObjectFrustumCulled ) {\n\n\t\t\t\t\t\tculled = ! frustum.intersectsSphere( _sphere$2, camera );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! culled ) {\n\n\t\t\t\t\t\t// get the distance from camera used for sorting\n\t\t\t\t\t\tconst geometryInfo = geometryInfoList[ geometryId ];\n\t\t\t\t\t\tconst z = _temp.subVectors( _sphere$2.center, _vector$5 ).dot( _forward$1 );\n\t\t\t\t\t\t_renderList.push( geometryInfo.start, geometryInfo.count, z, i );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// Sort the draw ranges and prep for rendering\n\t\t\tconst list = _renderList.list;\n\t\t\tconst customSort = this.customSort;\n\t\t\tif ( customSort === null ) {\n\n\t\t\t\tlist.sort( material.transparent ? sortTransparent : sortOpaque );\n\n\t\t\t} else {\n\n\t\t\t\tcustomSort.call( this, list, camera );\n\n\t\t\t}\n\n\t\t\tfor ( let i = 0, l = list.length; i < l; i ++ ) {\n\n\t\t\t\tconst item = list[ i ];\n\t\t\t\tmultiDrawStarts[ multiDrawCount ] = item.start * bytesPerElement;\n\t\t\t\tmultiDrawCounts[ multiDrawCount ] = item.count;\n\t\t\t\tindirectArray[ multiDrawCount ] = item.index;\n\t\t\t\tmultiDrawCount ++;\n\n\t\t\t}\n\n\t\t\t_renderList.reset();\n\n\t\t} else {\n\n\t\t\tfor ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {\n\n\t\t\t\tif ( instanceInfo[ i ].visible && instanceInfo[ i ].active ) {\n\n\t\t\t\t\tconst geometryId = instanceInfo[ i ].geometryIndex;\n\n\t\t\t\t\t// determine whether the batched geometry is within the frustum\n\t\t\t\t\tlet culled = false;\n\t\t\t\t\tif ( perObjectFrustumCulled ) {\n\n\t\t\t\t\t\t// get the bounds in world space\n\t\t\t\t\t\tthis.getMatrixAt( i, _matrix$1 );\n\t\t\t\t\t\tthis.getBoundingSphereAt( geometryId, _sphere$2 ).applyMatrix4( _matrix$1 );\n\t\t\t\t\t\tculled = ! frustum.intersectsSphere( _sphere$2, camera );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! culled ) {\n\n\t\t\t\t\t\tconst geometryInfo = geometryInfoList[ geometryId ];\n\t\t\t\t\t\tmultiDrawStarts[ multiDrawCount ] = geometryInfo.start * bytesPerElement;\n\t\t\t\t\t\tmultiDrawCounts[ multiDrawCount ] = geometryInfo.count;\n\t\t\t\t\t\tindirectArray[ multiDrawCount ] = i;\n\t\t\t\t\t\tmultiDrawCount ++;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tindirectTexture.needsUpdate = true;\n\t\tthis._multiDrawCount = multiDrawCount;\n\t\tthis._visibilityChanged = false;\n\n\t}\n\n\tonBeforeShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial/* , group */ ) {\n\n\t\tthis.onBeforeRender( renderer, null, shadowCamera, geometry, depthMaterial );\n\n\t}\n\n}\n\n/**\n * A material for rendering line primitives.\n *\n * Materials define the appearance of renderable 3D objects.\n *\n * ```js\n * const material = new THREE.LineBasicMaterial( { color: 0xffffff } );\n * ```\n *\n * @augments Material\n */\nclass LineBasicMaterial extends Material {\n\n\t/**\n\t * Constructs a new line basic material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLineBasicMaterial = true;\n\n\t\tthis.type = 'LineBasicMaterial';\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.color = new Color( 0xffffff );\n\n\t\t/**\n\t\t * Sets the color of the lines using data from a texture. The texture map\n\t\t * color is modulated by the diffuse `color`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * Controls line thickness or lines.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}. WebGL and WebGPU\n\t\t * ignore this setting and always render line primitives with a\n\t\t * width of one pixel.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.linewidth = 1;\n\n\t\t/**\n\t\t * Defines appearance of line ends.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('butt'|'round'|'square')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.linecap = 'round';\n\n\t\t/**\n\t\t * Defines appearance of line joints.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.linejoin = 'round';\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.color.copy( source.color );\n\n\t\tthis.map = source.map;\n\n\t\tthis.linewidth = source.linewidth;\n\t\tthis.linecap = source.linecap;\n\t\tthis.linejoin = source.linejoin;\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\nconst _vStart = /*@__PURE__*/ new Vector3();\nconst _vEnd = /*@__PURE__*/ new Vector3();\n\nconst _inverseMatrix$1 = /*@__PURE__*/ new Matrix4();\nconst _ray$1 = /*@__PURE__*/ new Ray();\nconst _sphere$1 = /*@__PURE__*/ new Sphere();\n\nconst _intersectPointOnRay = /*@__PURE__*/ new Vector3();\nconst _intersectPointOnSegment = /*@__PURE__*/ new Vector3();\n\n/**\n * A continuous line. The line are rendered by connecting consecutive\n * vertices with straight lines.\n *\n * ```js\n * const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );\n *\n * const points = [];\n * points.push( new THREE.Vector3( - 10, 0, 0 ) );\n * points.push( new THREE.Vector3( 0, 10, 0 ) );\n * points.push( new THREE.Vector3( 10, 0, 0 ) );\n *\n * const geometry = new THREE.BufferGeometry().setFromPoints( points );\n *\n * const line = new THREE.Line( geometry, material );\n * scene.add( line );\n * ```\n *\n * @augments Object3D\n */\nclass Line extends Object3D {\n\n\t/**\n\t * Constructs a new line.\n\t *\n\t * @param {BufferGeometry} [geometry] - The line geometry.\n\t * @param {Material|Array<Material>} [material] - The line material.\n\t */\n\tconstructor( geometry = new BufferGeometry(), material = new LineBasicMaterial() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLine = true;\n\n\t\tthis.type = 'Line';\n\n\t\t/**\n\t\t * The line geometry.\n\t\t *\n\t\t * @type {BufferGeometry}\n\t\t */\n\t\tthis.geometry = geometry;\n\n\t\t/**\n\t\t * The line material.\n\t\t *\n\t\t * @type {Material|Array<Material>}\n\t\t * @default LineBasicMaterial\n\t\t */\n\t\tthis.material = material;\n\n\t\t/**\n\t\t * A dictionary representing the morph targets in the geometry. The key is the\n\t\t * morph targets name, the value its attribute index. This member is `undefined`\n\t\t * by default and only set when morph targets are detected in the geometry.\n\t\t *\n\t\t * @type {Object<string,number>|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.morphTargetDictionary = undefined;\n\n\t\t/**\n\t\t * An array of weights typically in the range `[0,1]` that specify how much of the morph\n\t\t * is applied. This member is `undefined` by default and only set when morph targets are\n\t\t * detected in the geometry.\n\t\t *\n\t\t * @type {Array<number>|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.morphTargetInfluences = undefined;\n\n\t\tthis.updateMorphTargets();\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.material = Array.isArray( source.material ) ? source.material.slice() : source.material;\n\t\tthis.geometry = source.geometry;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes an array of distance values which are necessary for rendering dashed lines.\n\t * For each vertex in the geometry, the method calculates the cumulative length from the\n\t * current point to the very beginning of the line.\n\t *\n\t * @return {Line} A reference to this line.\n\t */\n\tcomputeLineDistances() {\n\n\t\tconst geometry = this.geometry;\n\n\t\t// we assume non-indexed geometry\n\n\t\tif ( geometry.index === null ) {\n\n\t\t\tconst positionAttribute = geometry.attributes.position;\n\t\t\tconst lineDistances = [ 0 ];\n\n\t\t\tfor ( let i = 1, l = positionAttribute.count; i < l; i ++ ) {\n\n\t\t\t\t_vStart.fromBufferAttribute( positionAttribute, i - 1 );\n\t\t\t\t_vEnd.fromBufferAttribute( positionAttribute, i );\n\n\t\t\t\tlineDistances[ i ] = lineDistances[ i - 1 ];\n\t\t\t\tlineDistances[ i ] += _vStart.distanceTo( _vEnd );\n\n\t\t\t}\n\n\t\t\tgeometry.setAttribute( 'lineDistance', new Float32BufferAttribute( lineDistances, 1 ) );\n\n\t\t} else {\n\n\t\t\twarn( 'Line.computeLineDistances(): Computation only possible with non-indexed BufferGeometry.' );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes intersection points between a casted ray and this line.\n\t *\n\t * @param {Raycaster} raycaster - The raycaster.\n\t * @param {Array<Object>} intersects - The target array that holds the intersection points.\n\t */\n\traycast( raycaster, intersects ) {\n\n\t\tconst geometry = this.geometry;\n\t\tconst matrixWorld = this.matrixWorld;\n\t\tconst threshold = raycaster.params.Line.threshold;\n\t\tconst drawRange = geometry.drawRange;\n\n\t\t// Checking boundingSphere distance to ray\n\n\t\tif ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();\n\n\t\t_sphere$1.copy( geometry.boundingSphere );\n\t\t_sphere$1.applyMatrix4( matrixWorld );\n\t\t_sphere$1.radius += threshold;\n\n\t\tif ( raycaster.ray.intersectsSphere( _sphere$1 ) === false ) return;\n\n\t\t//\n\n\t\t_inverseMatrix$1.copy( matrixWorld ).invert();\n\t\t_ray$1.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$1 );\n\n\t\tconst localThreshold = threshold / ( ( this.scale.x + this.scale.y + this.scale.z ) / 3 );\n\t\tconst localThresholdSq = localThreshold * localThreshold;\n\n\t\tconst step = this.isLineSegments ? 2 : 1;\n\n\t\tconst index = geometry.index;\n\t\tconst attributes = geometry.attributes;\n\t\tconst positionAttribute = attributes.position;\n\n\t\tif ( index !== null ) {\n\n\t\t\tconst start = Math.max( 0, drawRange.start );\n\t\t\tconst end = Math.min( index.count, ( drawRange.start + drawRange.count ) );\n\n\t\t\tfor ( let i = start, l = end - 1; i < l; i += step ) {\n\n\t\t\t\tconst a = index.getX( i );\n\t\t\t\tconst b = index.getX( i + 1 );\n\n\t\t\t\tconst intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, a, b, i );\n\n\t\t\t\tif ( intersect ) {\n\n\t\t\t\t\tintersects.push( intersect );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( this.isLineLoop ) {\n\n\t\t\t\tconst a = index.getX( end - 1 );\n\t\t\t\tconst b = index.getX( start );\n\n\t\t\t\tconst intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, a, b, end - 1 );\n\n\t\t\t\tif ( intersect ) {\n\n\t\t\t\t\tintersects.push( intersect );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tconst start = Math.max( 0, drawRange.start );\n\t\t\tconst end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) );\n\n\t\t\tfor ( let i = start, l = end - 1; i < l; i += step ) {\n\n\t\t\t\tconst intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, i, i + 1, i );\n\n\t\t\t\tif ( intersect ) {\n\n\t\t\t\t\tintersects.push( intersect );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( this.isLineLoop ) {\n\n\t\t\t\tconst intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, end - 1, start, end - 1 );\n\n\t\t\t\tif ( intersect ) {\n\n\t\t\t\t\tintersects.push( intersect );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sets the values of {@link Line#morphTargetDictionary} and {@link Line#morphTargetInfluences}\n\t * to make sure existing morph targets can influence this 3D object.\n\t */\n\tupdateMorphTargets() {\n\n\t\tconst geometry = this.geometry;\n\n\t\tconst morphAttributes = geometry.morphAttributes;\n\t\tconst keys = Object.keys( morphAttributes );\n\n\t\tif ( keys.length > 0 ) {\n\n\t\t\tconst morphAttribute = morphAttributes[ keys[ 0 ] ];\n\n\t\t\tif ( morphAttribute !== undefined ) {\n\n\t\t\t\tthis.morphTargetInfluences = [];\n\t\t\t\tthis.morphTargetDictionary = {};\n\n\t\t\t\tfor ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {\n\n\t\t\t\t\tconst name = morphAttribute[ m ].name || String( m );\n\n\t\t\t\t\tthis.morphTargetInfluences.push( 0 );\n\t\t\t\t\tthis.morphTargetDictionary[ name ] = m;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}\n\nfunction checkIntersection( object, raycaster, ray, thresholdSq, a, b, i ) {\n\n\tconst positionAttribute = object.geometry.attributes.position;\n\n\t_vStart.fromBufferAttribute( positionAttribute, a );\n\t_vEnd.fromBufferAttribute( positionAttribute, b );\n\n\tconst distSq = ray.distanceSqToSegment( _vStart, _vEnd, _intersectPointOnRay, _intersectPointOnSegment );\n\n\tif ( distSq > thresholdSq ) return;\n\n\t_intersectPointOnRay.applyMatrix4( object.matrixWorld ); // Move back to world space for distance calculation\n\n\tconst distance = raycaster.ray.origin.distanceTo( _intersectPointOnRay );\n\n\tif ( distance < raycaster.near || distance > raycaster.far ) return;\n\n\treturn {\n\n\t\tdistance: distance,\n\t\t// What do we want? intersection point on the ray or on the segment??\n\t\t// point: raycaster.ray.at( distance ),\n\t\tpoint: _intersectPointOnSegment.clone().applyMatrix4( object.matrixWorld ),\n\t\tindex: i,\n\t\tface: null,\n\t\tfaceIndex: null,\n\t\tbarycoord: null,\n\t\tobject: object\n\n\t};\n\n}\n\nconst _start = /*@__PURE__*/ new Vector3();\nconst _end = /*@__PURE__*/ new Vector3();\n\n/**\n * A series of lines drawn between pairs of vertices.\n *\n * @augments Line\n */\nclass LineSegments extends Line {\n\n\t/**\n\t * Constructs a new line segments.\n\t *\n\t * @param {BufferGeometry} [geometry] - The line geometry.\n\t * @param {Material|Array<Material>} [material] - The line material.\n\t */\n\tconstructor( geometry, material ) {\n\n\t\tsuper( geometry, material );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLineSegments = true;\n\n\t\tthis.type = 'LineSegments';\n\n\t}\n\n\tcomputeLineDistances() {\n\n\t\tconst geometry = this.geometry;\n\n\t\t// we assume non-indexed geometry\n\n\t\tif ( geometry.index === null ) {\n\n\t\t\tconst positionAttribute = geometry.attributes.position;\n\t\t\tconst lineDistances = [];\n\n\t\t\tfor ( let i = 0, l = positionAttribute.count; i < l; i += 2 ) {\n\n\t\t\t\t_start.fromBufferAttribute( positionAttribute, i );\n\t\t\t\t_end.fromBufferAttribute( positionAttribute, i + 1 );\n\n\t\t\t\tlineDistances[ i ] = ( i === 0 ) ? 0 : lineDistances[ i - 1 ];\n\t\t\t\tlineDistances[ i + 1 ] = lineDistances[ i ] + _start.distanceTo( _end );\n\n\t\t\t}\n\n\t\t\tgeometry.setAttribute( 'lineDistance', new Float32BufferAttribute( lineDistances, 1 ) );\n\n\t\t} else {\n\n\t\t\twarn( 'LineSegments.computeLineDistances(): Computation only possible with non-indexed BufferGeometry.' );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A continuous line. This is nearly the same as {@link Line} the only difference\n * is that the last vertex is connected with the first vertex in order to close\n * the line to form a loop.\n *\n * @augments Line\n */\nclass LineLoop extends Line {\n\n\t/**\n\t * Constructs a new line loop.\n\t *\n\t * @param {BufferGeometry} [geometry] - The line geometry.\n\t * @param {Material|Array<Material>} [material] - The line material.\n\t */\n\tconstructor( geometry, material ) {\n\n\t\tsuper( geometry, material );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLineLoop = true;\n\n\t\tthis.type = 'LineLoop';\n\n\t}\n\n}\n\n/**\n * A material for rendering point primitives.\n *\n * Materials define the appearance of renderable 3D objects.\n *\n * ```js\n * const vertices = [];\n *\n * for ( let i = 0; i < 10000; i ++ ) {\n * \tconst x = THREE.MathUtils.randFloatSpread( 2000 );\n * \tconst y = THREE.MathUtils.randFloatSpread( 2000 );\n * \tconst z = THREE.MathUtils.randFloatSpread( 2000 );\n *\n * \tvertices.push( x, y, z );\n * }\n *\n * const geometry = new THREE.BufferGeometry();\n * geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );\n * const material = new THREE.PointsMaterial( { color: 0x888888 } );\n * const points = new THREE.Points( geometry, material );\n * scene.add( points );\n * ```\n *\n * @augments Material\n */\nclass PointsMaterial extends Material {\n\n\t/**\n\t * Constructs a new points material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isPointsMaterial = true;\n\n\t\tthis.type = 'PointsMaterial';\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.color = new Color( 0xffffff );\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map\n\t\t * color is modulated by the diffuse `color`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * Defines the size of the points in pixels.\n\t\t *\n\t\t * Might be capped if the value exceeds hardware dependent parameters like [gl.ALIASED_POINT_SIZE_RANGE](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getParamete).\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.size = 1;\n\n\t\t/**\n\t\t * Specifies whether size of individual points is attenuated by the camera depth (perspective camera only).\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.sizeAttenuation = true;\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.color.copy( source.color );\n\n\t\tthis.map = source.map;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.size = source.size;\n\t\tthis.sizeAttenuation = source.sizeAttenuation;\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\nconst _inverseMatrix = /*@__PURE__*/ new Matrix4();\nconst _ray = /*@__PURE__*/ new Ray();\nconst _sphere = /*@__PURE__*/ new Sphere();\nconst _position$2 = /*@__PURE__*/ new Vector3();\n\n/**\n * A class for displaying points or point clouds.\n *\n * @augments Object3D\n */\nclass Points extends Object3D {\n\n\t/**\n\t * Constructs a new point cloud.\n\t *\n\t * @param {BufferGeometry} [geometry] - The points geometry.\n\t * @param {Material|Array<Material>} [material] - The points material.\n\t */\n\tconstructor( geometry = new BufferGeometry(), material = new PointsMaterial() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isPoints = true;\n\n\t\tthis.type = 'Points';\n\n\t\t/**\n\t\t * The points geometry.\n\t\t *\n\t\t * @type {BufferGeometry}\n\t\t */\n\t\tthis.geometry = geometry;\n\n\t\t/**\n\t\t * The line material.\n\t\t *\n\t\t * @type {Material|Array<Material>}\n\t\t * @default PointsMaterial\n\t\t */\n\t\tthis.material = material;\n\n\t\t/**\n\t\t * A dictionary representing the morph targets in the geometry. The key is the\n\t\t * morph targets name, the value its attribute index. This member is `undefined`\n\t\t * by default and only set when morph targets are detected in the geometry.\n\t\t *\n\t\t * @type {Object<string,number>|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.morphTargetDictionary = undefined;\n\n\t\t/**\n\t\t * An array of weights typically in the range `[0,1]` that specify how much of the morph\n\t\t * is applied. This member is `undefined` by default and only set when morph targets are\n\t\t * detected in the geometry.\n\t\t *\n\t\t * @type {Array<number>|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.morphTargetInfluences = undefined;\n\n\t\tthis.updateMorphTargets();\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.material = Array.isArray( source.material ) ? source.material.slice() : source.material;\n\t\tthis.geometry = source.geometry;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes intersection points between a casted ray and this point cloud.\n\t *\n\t * @param {Raycaster} raycaster - The raycaster.\n\t * @param {Array<Object>} intersects - The target array that holds the intersection points.\n\t */\n\traycast( raycaster, intersects ) {\n\n\t\tconst geometry = this.geometry;\n\t\tconst matrixWorld = this.matrixWorld;\n\t\tconst threshold = raycaster.params.Points.threshold;\n\t\tconst drawRange = geometry.drawRange;\n\n\t\t// Checking boundingSphere distance to ray\n\n\t\tif ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();\n\n\t\t_sphere.copy( geometry.boundingSphere );\n\t\t_sphere.applyMatrix4( matrixWorld );\n\t\t_sphere.radius += threshold;\n\n\t\tif ( raycaster.ray.intersectsSphere( _sphere ) === false ) return;\n\n\t\t//\n\n\t\t_inverseMatrix.copy( matrixWorld ).invert();\n\t\t_ray.copy( raycaster.ray ).applyMatrix4( _inverseMatrix );\n\n\t\tconst localThreshold = threshold / ( ( this.scale.x + this.scale.y + this.scale.z ) / 3 );\n\t\tconst localThresholdSq = localThreshold * localThreshold;\n\n\t\tconst index = geometry.index;\n\t\tconst attributes = geometry.attributes;\n\t\tconst positionAttribute = attributes.position;\n\n\t\tif ( index !== null ) {\n\n\t\t\tconst start = Math.max( 0, drawRange.start );\n\t\t\tconst end = Math.min( index.count, ( drawRange.start + drawRange.count ) );\n\n\t\t\tfor ( let i = start, il = end; i < il; i ++ ) {\n\n\t\t\t\tconst a = index.getX( i );\n\n\t\t\t\t_position$2.fromBufferAttribute( positionAttribute, a );\n\n\t\t\t\ttestPoint( _position$2, a, localThresholdSq, matrixWorld, raycaster, intersects, this );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tconst start = Math.max( 0, drawRange.start );\n\t\t\tconst end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) );\n\n\t\t\tfor ( let i = start, l = end; i < l; i ++ ) {\n\n\t\t\t\t_position$2.fromBufferAttribute( positionAttribute, i );\n\n\t\t\t\ttestPoint( _position$2, i, localThresholdSq, matrixWorld, raycaster, intersects, this );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sets the values of {@link Points#morphTargetDictionary} and {@link Points#morphTargetInfluences}\n\t * to make sure existing morph targets can influence this 3D object.\n\t */\n\tupdateMorphTargets() {\n\n\t\tconst geometry = this.geometry;\n\n\t\tconst morphAttributes = geometry.morphAttributes;\n\t\tconst keys = Object.keys( morphAttributes );\n\n\t\tif ( keys.length > 0 ) {\n\n\t\t\tconst morphAttribute = morphAttributes[ keys[ 0 ] ];\n\n\t\t\tif ( morphAttribute !== undefined ) {\n\n\t\t\t\tthis.morphTargetInfluences = [];\n\t\t\t\tthis.morphTargetDictionary = {};\n\n\t\t\t\tfor ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {\n\n\t\t\t\t\tconst name = morphAttribute[ m ].name || String( m );\n\n\t\t\t\t\tthis.morphTargetInfluences.push( 0 );\n\t\t\t\t\tthis.morphTargetDictionary[ name ] = m;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}\n\nfunction testPoint( point, index, localThresholdSq, matrixWorld, raycaster, intersects, object ) {\n\n\tconst rayPointDistanceSq = _ray.distanceSqToPoint( point );\n\n\tif ( rayPointDistanceSq < localThresholdSq ) {\n\n\t\tconst intersectPoint = new Vector3();\n\n\t\t_ray.closestPointToPoint( point, intersectPoint );\n\t\tintersectPoint.applyMatrix4( matrixWorld );\n\n\t\tconst distance = raycaster.ray.origin.distanceTo( intersectPoint );\n\n\t\tif ( distance < raycaster.near || distance > raycaster.far ) return;\n\n\t\tintersects.push( {\n\n\t\t\tdistance: distance,\n\t\t\tdistanceToRay: Math.sqrt( rayPointDistanceSq ),\n\t\t\tpoint: intersectPoint,\n\t\t\tindex: index,\n\t\t\tface: null,\n\t\t\tfaceIndex: null,\n\t\t\tbarycoord: null,\n\t\t\tobject: object\n\n\t\t} );\n\n\t}\n\n}\n\n/**\n * A texture for use with a video.\n *\n * ```js\n * // assuming you have created a HTML video element with id=\"video\"\n * const video = document.getElementById( 'video' );\n * const texture = new THREE.VideoTexture( video );\n * ```\n *\n * Note: When using video textures with {@link WebGPURenderer}, {@link Texture#colorSpace} must be\n * set to THREE.SRGBColorSpace.\n *\n * Note: After the initial use of a texture, its dimensions, format, and type\n * cannot be changed. Instead, call {@link Texture#dispose} on the texture and instantiate a new one.\n *\n * @augments Texture\n */\nclass VideoTexture extends Texture {\n\n\t/**\n\t * Constructs a new video texture.\n\t *\n\t * @param {HTMLVideoElement} video - The video element to use as a data source for the texture.\n\t * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.\n\t * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.\n\t * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.\n\t * @param {number} [magFilter=LinearFilter] - The mag filter value.\n\t * @param {number} [minFilter=LinearFilter] - The min filter value.\n\t * @param {number} [format=RGBAFormat] - The texture format.\n\t * @param {number} [type=UnsignedByteType] - The texture type.\n\t * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.\n\t */\n\tconstructor( video, mapping, wrapS, wrapT, magFilter = LinearFilter, minFilter = LinearFilter, format, type, anisotropy ) {\n\n\t\tsuper( video, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isVideoTexture = true;\n\n\t\t/**\n\t\t * Whether to generate mipmaps (if possible) for a texture.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.generateMipmaps = false;\n\n\t\t/**\n\t\t * The video frame request callback identifier, which is a positive integer.\n\t\t *\n\t\t * Value of 0 represents no scheduled rVFC.\n\t\t *\n\t\t * @private\n\t\t * @type {number}\n\t\t */\n\t\tthis._requestVideoFrameCallbackId = 0;\n\n\t\tconst scope = this;\n\n\t\tfunction updateVideo() {\n\n\t\t\tscope.needsUpdate = true;\n\t\t\tscope._requestVideoFrameCallbackId = video.requestVideoFrameCallback( updateVideo );\n\n\t\t}\n\n\t\tif ( 'requestVideoFrameCallback' in video ) {\n\n\t\t\tthis._requestVideoFrameCallbackId = video.requestVideoFrameCallback( updateVideo );\n\n\t\t}\n\n\t}\n\n\tclone() {\n\n\t\treturn new this.constructor( this.image ).copy( this );\n\n\t}\n\n\t/**\n\t * This method is called automatically by the renderer and sets {@link Texture#needsUpdate}\n\t * to `true` every time a new frame is available.\n\t *\n\t * Only relevant if `requestVideoFrameCallback` is not supported in the browser.\n\t */\n\tupdate() {\n\n\t\tconst video = this.image;\n\t\tconst hasVideoFrameCallback = 'requestVideoFrameCallback' in video;\n\n\t\tif ( hasVideoFrameCallback === false && video.readyState >= video.HAVE_CURRENT_DATA ) {\n\n\t\t\tthis.needsUpdate = true;\n\n\t\t}\n\n\t}\n\n\tdispose() {\n\n\t\tif ( this._requestVideoFrameCallbackId !== 0 ) {\n\n\t\t\tthis.source.data.cancelVideoFrameCallback( this._requestVideoFrameCallbackId );\n\n\t\t\tthis._requestVideoFrameCallbackId = 0;\n\n\t\t}\n\n\t\tsuper.dispose();\n\n\t}\n\n}\n\n/**\n * This class can be used as an alternative way to define video data. Instead of using\n * an instance of `HTMLVideoElement` like with `VideoTexture`, `VideoFrameTexture` expects each frame is\n * defined manually via {@link VideoFrameTexture#setFrame}. A typical use case for this module is when\n * video frames are decoded with the WebCodecs API.\n *\n * ```js\n * const texture = new THREE.VideoFrameTexture();\n * texture.setFrame( frame );\n * ```\n *\n * @augments VideoTexture\n */\nclass VideoFrameTexture extends VideoTexture {\n\n\t/**\n\t * Constructs a new video frame texture.\n\t *\n\t * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.\n\t * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.\n\t * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.\n\t * @param {number} [magFilter=LinearFilter] - The mag filter value.\n\t * @param {number} [minFilter=LinearFilter] - The min filter value.\n\t * @param {number} [format=RGBAFormat] - The texture format.\n\t * @param {number} [type=UnsignedByteType] - The texture type.\n\t * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.\n\t */\n\tconstructor( mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {\n\n\t\tsuper( {}, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isVideoFrameTexture = true;\n\n\t}\n\n\t/**\n\t * This method overwritten with an empty implementation since\n\t * this type of texture is updated via `setFrame()`.\n\t */\n\tupdate() {}\n\n\tclone() {\n\n\t\treturn new this.constructor().copy( this ); // restoring Texture.clone()\n\n\t}\n\n\t/**\n\t * Sets the current frame of the video. This will automatically update the texture\n\t * so the data can be used for rendering.\n\t *\n\t * @param {VideoFrame} frame - The video frame.\n\t */\n\tsetFrame( frame ) {\n\n\t\tthis.image = frame;\n\t\tthis.needsUpdate = true;\n\n\t}\n\n}\n\n/**\n * This class can only be used in combination with `copyFramebufferToTexture()` methods\n * of renderers. It extracts the contents of the current bound framebuffer and provides it\n * as a texture for further usage.\n *\n * ```js\n * const pixelRatio = window.devicePixelRatio;\n * const textureSize = 128 * pixelRatio;\n *\n * const frameTexture = new FramebufferTexture( textureSize, textureSize );\n *\n * // calculate start position for copying part of the frame data\n * const vector = new Vector2();\n * vector.x = ( window.innerWidth * pixelRatio / 2 ) - ( textureSize / 2 );\n * vector.y = ( window.innerHeight * pixelRatio / 2 ) - ( textureSize / 2 );\n *\n * renderer.render( scene, camera );\n *\n * // copy part of the rendered frame into the framebuffer texture\n * renderer.copyFramebufferToTexture( frameTexture, vector );\n * ```\n *\n * @augments Texture\n */\nclass FramebufferTexture extends Texture {\n\n\t/**\n\t * Constructs a new framebuffer texture.\n\t *\n\t * @param {number} [width] - The width of the texture.\n\t * @param {number} [height] - The height of the texture.\n\t */\n\tconstructor( width, height ) {\n\n\t\tsuper( { width, height } );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isFramebufferTexture = true;\n\n\t\t/**\n\t\t * How the texture is sampled when a texel covers more than one pixel.\n\t\t *\n\t\t * Overwritten and set to `NearestFilter` by default to disable filtering.\n\t\t *\n\t\t * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}\n\t\t * @default NearestFilter\n\t\t */\n\t\tthis.magFilter = NearestFilter;\n\n\t\t/**\n\t\t * How the texture is sampled when a texel covers less than one pixel.\n\t\t *\n\t\t * Overwritten and set to `NearestFilter` by default to disable filtering.\n\t\t *\n\t\t * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}\n\t\t * @default NearestFilter\n\t\t */\n\t\tthis.minFilter = NearestFilter;\n\n\t\t/**\n\t\t * Whether to generate mipmaps (if possible) for a texture.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.generateMipmaps = false;\n\n\t\tthis.needsUpdate = true;\n\n\t}\n\n}\n\n/**\n * Creates a texture based on data in compressed form.\n *\n * These texture are usually loaded with {@link CompressedTextureLoader}.\n *\n * @augments Texture\n */\nclass CompressedTexture extends Texture {\n\n\t/**\n\t * Constructs a new compressed texture.\n\t *\n\t * @param {Array<Object>} mipmaps - This array holds for all mipmaps (including the bases mip)\n\t * the data and dimensions.\n\t * @param {number} width - The width of the texture.\n\t * @param {number} height - The height of the texture.\n\t * @param {number} [format=RGBAFormat] - The texture format.\n\t * @param {number} [type=UnsignedByteType] - The texture type.\n\t * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.\n\t * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.\n\t * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.\n\t * @param {number} [magFilter=LinearFilter] - The mag filter value.\n\t * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.\n\t * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.\n\t * @param {string} [colorSpace=NoColorSpace] - The color space.\n\t */\n\tconstructor( mipmaps, width, height, format, type, mapping, wrapS, wrapT, magFilter, minFilter, anisotropy, colorSpace ) {\n\n\t\tsuper( null, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCompressedTexture = true;\n\n\t\t/**\n\t\t * The image property of a compressed texture just defines its dimensions.\n\t\t *\n\t\t * @type {{width:number,height:number}}\n\t\t */\n\t\tthis.image = { width: width, height: height };\n\n\t\t/**\n\t\t * This array holds for all mipmaps (including the bases mip) the data and dimensions.\n\t\t *\n\t\t * @type {Array<Object>}\n\t\t */\n\t\tthis.mipmaps = mipmaps;\n\n\t\t/**\n\t\t * If set to `true`, the texture is flipped along the vertical axis when\n\t\t * uploaded to the GPU.\n\t\t *\n\t\t * Overwritten and set to `false` by default since it is not possible to\n\t\t * flip compressed textures.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t * @readonly\n\t\t */\n\t\tthis.flipY = false;\n\n\t\t/**\n\t\t * Whether to generate mipmaps (if possible) for a texture.\n\t\t *\n\t\t * Overwritten and set to `false` by default since it is not\n\t\t * possible to generate mipmaps for compressed data. Mipmaps\n\t\t * must be embedded in the compressed texture file.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t * @readonly\n\t\t */\n\t\tthis.generateMipmaps = false;\n\n\t}\n\n}\n\n/**\n * Creates a texture 2D array based on data in compressed form.\n *\n * These texture are usually loaded with {@link CompressedTextureLoader}.\n *\n * @augments CompressedTexture\n */\nclass CompressedArrayTexture extends CompressedTexture {\n\n\t/**\n\t * Constructs a new compressed array texture.\n\t *\n\t * @param {Array<Object>} mipmaps - This array holds for all mipmaps (including the bases mip)\n\t * the data and dimensions.\n\t * @param {number} width - The width of the texture.\n\t * @param {number} height - The height of the texture.\n\t * @param {number} depth - The depth of the texture.\n\t * @param {number} [format=RGBAFormat] - The min filter value.\n\t * @param {number} [type=UnsignedByteType] - The min filter value.\n\t */\n\tconstructor( mipmaps, width, height, depth, format, type ) {\n\n\t\tsuper( mipmaps, width, height, format, type );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCompressedArrayTexture = true;\n\n\t\t/**\n\t\t * The image property of a compressed texture just defines its dimensions.\n\t\t *\n\t\t * @name CompressedArrayTexture#image\n\t\t * @type {{width:number,height:number,depth:number}}\n\t\t */\n\t\tthis.image.depth = depth;\n\n\t\t/**\n\t\t * This defines how the texture is wrapped in the depth and corresponds to\n\t\t * *W* in UVW mapping.\n\t\t *\n\t\t * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}\n\t\t * @default ClampToEdgeWrapping\n\t\t */\n\t\tthis.wrapR = ClampToEdgeWrapping;\n\n\t\t/**\n\t\t * A set of all layers which need to be updated in the texture.\n\t\t *\n\t\t * @type {Set<number>}\n\t\t */\n\t\tthis.layerUpdates = new Set();\n\n\t}\n\n\t/**\n\t * Describes that a specific layer of the texture needs to be updated.\n\t * Normally when {@link Texture#needsUpdate} is set to `true`, the\n\t * entire compressed texture array is sent to the GPU. Marking specific\n\t * layers will only transmit subsets of all mipmaps associated with a\n\t * specific depth in the array which is often much more performant.\n\t *\n\t * @param {number} layerIndex - The layer index that should be updated.\n\t */\n\taddLayerUpdate( layerIndex ) {\n\n\t\tthis.layerUpdates.add( layerIndex );\n\n\t}\n\n\t/**\n\t * Resets the layer updates registry.\n\t */\n\tclearLayerUpdates() {\n\n\t\tthis.layerUpdates.clear();\n\n\t}\n\n}\n\n/**\n * Creates a cube texture based on data in compressed form.\n *\n * These texture are usually loaded with {@link CompressedTextureLoader}.\n *\n * @augments CompressedTexture\n */\nclass CompressedCubeTexture extends CompressedTexture {\n\n\t/**\n\t * Constructs a new compressed texture.\n\t *\n\t * @param {Array<CompressedTexture>} images - An array of compressed textures.\n\t * @param {number} [format=RGBAFormat] - The texture format.\n\t * @param {number} [type=UnsignedByteType] - The texture type.\n\t */\n\tconstructor( images, format, type ) {\n\n\t\tsuper( undefined, images[ 0 ].width, images[ 0 ].height, format, type, CubeReflectionMapping );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCompressedCubeTexture = true;\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCubeTexture = true;\n\n\t\tthis.image = images;\n\n\t}\n\n}\n\n/**\n * Creates a texture from a canvas element.\n *\n * This is almost the same as the base texture class, except that it sets {@link Texture#needsUpdate}\n * to `true` immediately since a canvas can directly be used for rendering.\n *\n * @augments Texture\n */\nclass CanvasTexture extends Texture {\n\n\t/**\n\t * Constructs a new texture.\n\t *\n\t * @param {HTMLCanvasElement} [canvas] - The HTML canvas element.\n\t * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.\n\t * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.\n\t * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.\n\t * @param {number} [magFilter=LinearFilter] - The mag filter value.\n\t * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.\n\t * @param {number} [format=RGBAFormat] - The texture format.\n\t * @param {number} [type=UnsignedByteType] - The texture type.\n\t * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.\n\t */\n\tconstructor( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {\n\n\t\tsuper( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCanvasTexture = true;\n\n\t\tthis.needsUpdate = true;\n\n\t}\n\n}\n\n/**\n * This class can be used to automatically save the depth information of a\n * rendering into a texture.\n *\n * @augments Texture\n */\nclass DepthTexture extends Texture {\n\n\t/**\n\t * Constructs a new depth texture.\n\t *\n\t * @param {number} width - The width of the texture.\n\t * @param {number} height - The height of the texture.\n\t * @param {number} [type=UnsignedIntType] - The texture type.\n\t * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.\n\t * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.\n\t * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.\n\t * @param {number} [magFilter=LinearFilter] - The mag filter value.\n\t * @param {number} [minFilter=LinearFilter] - The min filter value.\n\t * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.\n\t * @param {number} [format=DepthFormat] - The texture format.\n\t * @param {number} [depth=1] - The depth of the texture.\n\t */\n\tconstructor( width, height, type = UnsignedIntType, mapping, wrapS, wrapT, magFilter = NearestFilter, minFilter = NearestFilter, anisotropy, format = DepthFormat, depth = 1 ) {\n\n\t\tif ( format !== DepthFormat && format !== DepthStencilFormat ) {\n\n\t\t\tthrow new Error( 'DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat' );\n\n\t\t}\n\n\t\tconst image = { width: width, height: height, depth: depth };\n\n\t\tsuper( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isDepthTexture = true;\n\n\t\t/**\n\t\t * If set to `true`, the texture is flipped along the vertical axis when\n\t\t * uploaded to the GPU.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flipY = false;\n\n\t\t/**\n\t\t * Whether to generate mipmaps (if possible) for a texture.\n\t\t *\n\t\t * Overwritten and set to `false` by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.generateMipmaps = false;\n\n\t\t/**\n\t\t * Code corresponding to the depth compare function.\n\t\t *\n\t\t * @type {?(NeverCompare|LessCompare|EqualCompare|LessEqualCompare|GreaterCompare|NotEqualCompare|GreaterEqualCompare|AlwaysCompare)}\n\t\t * @default null\n\t\t */\n\t\tthis.compareFunction = null;\n\n\t}\n\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.source = new Source( Object.assign( {}, source.image ) ); // see #30540\n\t\tthis.compareFunction = source.compareFunction;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tif ( this.compareFunction !== null ) data.compareFunction = this.compareFunction;\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * This class can be used to automatically save the depth information of a\n * cube rendering into a cube texture with depth format. Used for PointLight shadows.\n *\n * @augments DepthTexture\n */\nclass CubeDepthTexture extends DepthTexture {\n\n\t/**\n\t * Constructs a new cube depth texture.\n\t *\n\t * @param {number} size - The size (width and height) of each cube face.\n\t * @param {number} [type=UnsignedIntType] - The texture type.\n\t * @param {number} [mapping=CubeReflectionMapping] - The texture mapping.\n\t * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.\n\t * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.\n\t * @param {number} [magFilter=NearestFilter] - The mag filter value.\n\t * @param {number} [minFilter=NearestFilter] - The min filter value.\n\t * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.\n\t * @param {number} [format=DepthFormat] - The texture format.\n\t */\n\tconstructor( size, type = UnsignedIntType, mapping = CubeReflectionMapping, wrapS, wrapT, magFilter = NearestFilter, minFilter = NearestFilter, anisotropy, format = DepthFormat ) {\n\n\t\t// Create 6 identical image descriptors for the cube faces\n\t\tconst image = { width: size, height: size, depth: 1 };\n\t\tconst images = [ image, image, image, image, image, image ];\n\n\t\t// Call DepthTexture constructor with width, height\n\t\tsuper( size, size, type, mapping, wrapS, wrapT, magFilter, minFilter, anisotropy, format );\n\n\t\t// Replace the single image with the array of 6 images\n\t\tthis.image = images;\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCubeDepthTexture = true;\n\n\t\t/**\n\t\t * Set to true for cube texture handling in WebGLTextures.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCubeTexture = true;\n\n\t}\n\n\t/**\n\t * Alias for {@link CubeDepthTexture#image}.\n\t *\n\t * @type {Array<Image>}\n\t */\n\tget images() {\n\n\t\treturn this.image;\n\n\t}\n\n\tset images( value ) {\n\n\t\tthis.image = value;\n\n\t}\n\n}\n\n/**\n * Represents a texture created externally with the same renderer context.\n *\n * This may be a texture from a protected media stream, device camera feed,\n * or other data feeds like a depth sensor.\n *\n * Note that this class is only supported in {@link WebGLRenderer}, and in\n * the {@link WebGPURenderer} WebGPU backend.\n *\n * @augments Texture\n */\nclass ExternalTexture extends Texture {\n\n\t/**\n\t * Creates a new raw texture.\n\t *\n\t * @param {?(WebGLTexture|GPUTexture)} [sourceTexture=null] - The external texture.\n\t */\n\tconstructor( sourceTexture = null ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * The external source texture.\n\t\t *\n\t\t * @type {?(WebGLTexture|GPUTexture)}\n\t\t * @default null\n\t\t */\n\t\tthis.sourceTexture = sourceTexture;\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isExternalTexture = true;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.sourceTexture = source.sourceTexture;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A geometry class for representing a capsule.\n *\n * ```js\n * const geometry = new THREE.CapsuleGeometry( 1, 1, 4, 8, 1 );\n * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );\n * const capsule = new THREE.Mesh( geometry, material );\n * scene.add( capsule );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#CapsuleGeometry\n */\nclass CapsuleGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new capsule geometry.\n\t *\n\t * @param {number} [radius=1] - Radius of the capsule.\n\t * @param {number} [height=1] - Height of the middle section.\n\t * @param {number} [capSegments=4] - Number of curve segments used to build each cap.\n\t * @param {number} [radialSegments=8] - Number of segmented faces around the circumference of the capsule. Must be an integer >= 3.\n\t * @param {number} [heightSegments=1] - Number of rows of faces along the height of the middle section. Must be an integer >= 1.\n\t */\n\tconstructor( radius = 1, height = 1, capSegments = 4, radialSegments = 8, heightSegments = 1 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'CapsuleGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\theight: height,\n\t\t\tcapSegments: capSegments,\n\t\t\tradialSegments: radialSegments,\n\t\t\theightSegments: heightSegments,\n\t\t};\n\n\t\theight = Math.max( 0, height );\n\t\tcapSegments = Math.max( 1, Math.floor( capSegments ) );\n\t\tradialSegments = Math.max( 3, Math.floor( radialSegments ) );\n\t\theightSegments = Math.max( 1, Math.floor( heightSegments ) );\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\t// helper variables\n\n\t\tconst halfHeight = height / 2;\n\t\tconst capArcLength = ( Math.PI / 2 ) * radius;\n\t\tconst cylinderPartLength = height;\n\t\tconst totalArcLength = 2 * capArcLength + cylinderPartLength;\n\n\t\tconst numVerticalSegments = capSegments * 2 + heightSegments;\n\t\tconst verticesPerRow = radialSegments + 1;\n\n\t\tconst normal = new Vector3();\n\t\tconst vertex = new Vector3();\n\n\t\t// generate vertices, normals, and uvs\n\n\t\tfor ( let iy = 0; iy <= numVerticalSegments; iy ++ ) {\n\n\t\t\tlet currentArcLength = 0;\n\t\t\tlet profileY = 0;\n\t\t\tlet profileRadius = 0;\n\t\t\tlet normalYComponent = 0;\n\n\t\t\tif ( iy <= capSegments ) {\n\n\t\t\t\t// bottom cap\n\t\t\t\tconst segmentProgress = iy / capSegments;\n\t\t\t\tconst angle = ( segmentProgress * Math.PI ) / 2;\n\t\t\t\tprofileY = - halfHeight - radius * Math.cos( angle );\n\t\t\t\tprofileRadius = radius * Math.sin( angle );\n\t\t\t\tnormalYComponent = - radius * Math.cos( angle );\n\t\t\t\tcurrentArcLength = segmentProgress * capArcLength;\n\n\t\t\t} else if ( iy <= capSegments + heightSegments ) {\n\n\t\t\t\t// middle section\n\t\t\t\tconst segmentProgress = ( iy - capSegments ) / heightSegments;\n\t\t\t\tprofileY = - halfHeight + segmentProgress * height;\n\t\t\t\tprofileRadius = radius;\n\t\t\t\tnormalYComponent = 0;\n\t\t\t\tcurrentArcLength = capArcLength + segmentProgress * cylinderPartLength;\n\n\t\t\t} else {\n\n\t\t\t\t// top cap\n\t\t\t\tconst segmentProgress =\n\t\t\t\t\t( iy - capSegments - heightSegments ) / capSegments;\n\t\t\t\tconst angle = ( segmentProgress * Math.PI ) / 2;\n\t\t\t\tprofileY = halfHeight + radius * Math.sin( angle );\n\t\t\t\tprofileRadius = radius * Math.cos( angle );\n\t\t\t\tnormalYComponent = radius * Math.sin( angle );\n\t\t\t\tcurrentArcLength =\n\t\t\t\t\tcapArcLength + cylinderPartLength + segmentProgress * capArcLength;\n\n\t\t\t}\n\n\t\t\tconst v = Math.max( 0, Math.min( 1, currentArcLength / totalArcLength ) );\n\n\n\t\t\t// special case for the poles\n\n\t\t\tlet uOffset = 0;\n\n\t\t\tif ( iy === 0 ) {\n\n\t\t\t\tuOffset = 0.5 / radialSegments;\n\n\t\t\t} else if ( iy === numVerticalSegments ) {\n\n\t\t\t\tuOffset = -0.5 / radialSegments;\n\n\t\t\t}\n\n\t\t\tfor ( let ix = 0; ix <= radialSegments; ix ++ ) {\n\n\t\t\t\tconst u = ix / radialSegments;\n\t\t\t\tconst theta = u * Math.PI * 2;\n\n\t\t\t\tconst sinTheta = Math.sin( theta );\n\t\t\t\tconst cosTheta = Math.cos( theta );\n\n\t\t\t\t// vertex\n\n\t\t\t\tvertex.x = - profileRadius * cosTheta;\n\t\t\t\tvertex.y = profileY;\n\t\t\t\tvertex.z = profileRadius * sinTheta;\n\t\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t\t// normal\n\n\t\t\t\tnormal.set(\n\t\t\t\t\t- profileRadius * cosTheta,\n\t\t\t\t\tnormalYComponent,\n\t\t\t\t\tprofileRadius * sinTheta\n\t\t\t\t);\n\t\t\t\tnormal.normalize();\n\t\t\t\tnormals.push( normal.x, normal.y, normal.z );\n\n\t\t\t\t// uv\n\n\t\t\t\tuvs.push( u + uOffset, v );\n\n\t\t\t}\n\n\t\t\tif ( iy > 0 ) {\n\n\t\t\t\tconst prevIndexRow = ( iy - 1 ) * verticesPerRow;\n\t\t\t\tfor ( let ix = 0; ix < radialSegments; ix ++ ) {\n\n\t\t\t\t\tconst i1 = prevIndexRow + ix;\n\t\t\t\t\tconst i2 = prevIndexRow + ix + 1;\n\t\t\t\t\tconst i3 = iy * verticesPerRow + ix;\n\t\t\t\t\tconst i4 = iy * verticesPerRow + ix + 1;\n\n\t\t\t\t\tindices.push( i1, i2, i3 );\n\t\t\t\t\tindices.push( i2, i4, i3 );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {CapsuleGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new CapsuleGeometry( data.radius, data.height, data.capSegments, data.radialSegments, data.heightSegments );\n\n\t}\n\n}\n\n/**\n * A simple shape of Euclidean geometry. It is constructed from a\n * number of triangular segments that are oriented around a central point and\n * extend as far out as a given radius. It is built counter-clockwise from a\n * start angle and a given central angle. It can also be used to create\n * regular polygons, where the number of segments determines the number of\n * sides.\n *\n * ```js\n * const geometry = new THREE.CircleGeometry( 5, 32 );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const circle = new THREE.Mesh( geometry, material );\n * scene.add( circle )\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#CircleGeometry\n */\nclass CircleGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new circle geometry.\n\t *\n\t * @param {number} [radius=1] - Radius of the circle.\n\t * @param {number} [segments=32] - Number of segments (triangles), minimum = `3`.\n\t * @param {number} [thetaStart=0] - Start angle for first segment in radians.\n\t * @param {number} [thetaLength=Math.PI*2] - The central angle, often called theta,\n\t * of the circular sector in radians. The default value results in a complete circle.\n\t */\n\tconstructor( radius = 1, segments = 32, thetaStart = 0, thetaLength = Math.PI * 2 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'CircleGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\tsegments: segments,\n\t\t\tthetaStart: thetaStart,\n\t\t\tthetaLength: thetaLength\n\t\t};\n\n\t\tsegments = Math.max( 3, segments );\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\t// helper variables\n\n\t\tconst vertex = new Vector3();\n\t\tconst uv = new Vector2();\n\n\t\t// center point\n\n\t\tvertices.push( 0, 0, 0 );\n\t\tnormals.push( 0, 0, 1 );\n\t\tuvs.push( 0.5, 0.5 );\n\n\t\tfor ( let s = 0, i = 3; s <= segments; s ++, i += 3 ) {\n\n\t\t\tconst segment = thetaStart + s / segments * thetaLength;\n\n\t\t\t// vertex\n\n\t\t\tvertex.x = radius * Math.cos( segment );\n\t\t\tvertex.y = radius * Math.sin( segment );\n\n\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t// normal\n\n\t\t\tnormals.push( 0, 0, 1 );\n\n\t\t\t// uvs\n\n\t\t\tuv.x = ( vertices[ i ] / radius + 1 ) / 2;\n\t\t\tuv.y = ( vertices[ i + 1 ] / radius + 1 ) / 2;\n\n\t\t\tuvs.push( uv.x, uv.y );\n\n\t\t}\n\n\t\t// indices\n\n\t\tfor ( let i = 1; i <= segments; i ++ ) {\n\n\t\t\tindices.push( i, i + 1, 0 );\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {CircleGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new CircleGeometry( data.radius, data.segments, data.thetaStart, data.thetaLength );\n\n\t}\n\n}\n\n/**\n * A geometry class for representing a cylinder.\n *\n * ```js\n * const geometry = new THREE.CylinderGeometry( 5, 5, 20, 32 );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const cylinder = new THREE.Mesh( geometry, material );\n * scene.add( cylinder );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#CylinderGeometry\n */\nclass CylinderGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new cylinder geometry.\n\t *\n\t * @param {number} [radiusTop=1] - Radius of the cylinder at the top.\n\t * @param {number} [radiusBottom=1] - Radius of the cylinder at the bottom.\n\t * @param {number} [height=1] - Height of the cylinder.\n\t * @param {number} [radialSegments=32] - Number of segmented faces around the circumference of the cylinder.\n\t * @param {number} [heightSegments=1] - Number of rows of faces along the height of the cylinder.\n\t * @param {boolean} [openEnded=false] - Whether the base of the cylinder is open or capped.\n\t * @param {number} [thetaStart=0] - Start angle for first segment, in radians.\n\t * @param {number} [thetaLength=Math.PI*2] - The central angle, often called theta, of the circular sector, in radians.\n\t * The default value results in a complete cylinder.\n\t */\n\tconstructor( radiusTop = 1, radiusBottom = 1, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'CylinderGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradiusTop: radiusTop,\n\t\t\tradiusBottom: radiusBottom,\n\t\t\theight: height,\n\t\t\tradialSegments: radialSegments,\n\t\t\theightSegments: heightSegments,\n\t\t\topenEnded: openEnded,\n\t\t\tthetaStart: thetaStart,\n\t\t\tthetaLength: thetaLength\n\t\t};\n\n\t\tconst scope = this;\n\n\t\tradialSegments = Math.floor( radialSegments );\n\t\theightSegments = Math.floor( heightSegments );\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\t// helper variables\n\n\t\tlet index = 0;\n\t\tconst indexArray = [];\n\t\tconst halfHeight = height / 2;\n\t\tlet groupStart = 0;\n\n\t\t// generate geometry\n\n\t\tgenerateTorso();\n\n\t\tif ( openEnded === false ) {\n\n\t\t\tif ( radiusTop > 0 ) generateCap( true );\n\t\t\tif ( radiusBottom > 0 ) generateCap( false );\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t\tfunction generateTorso() {\n\n\t\t\tconst normal = new Vector3();\n\t\t\tconst vertex = new Vector3();\n\n\t\t\tlet groupCount = 0;\n\n\t\t\t// this will be used to calculate the normal\n\t\t\tconst slope = ( radiusBottom - radiusTop ) / height;\n\n\t\t\t// generate vertices, normals and uvs\n\n\t\t\tfor ( let y = 0; y <= heightSegments; y ++ ) {\n\n\t\t\t\tconst indexRow = [];\n\n\t\t\t\tconst v = y / heightSegments;\n\n\t\t\t\t// calculate the radius of the current row\n\n\t\t\t\tconst radius = v * ( radiusBottom - radiusTop ) + radiusTop;\n\n\t\t\t\tfor ( let x = 0; x <= radialSegments; x ++ ) {\n\n\t\t\t\t\tconst u = x / radialSegments;\n\n\t\t\t\t\tconst theta = u * thetaLength + thetaStart;\n\n\t\t\t\t\tconst sinTheta = Math.sin( theta );\n\t\t\t\t\tconst cosTheta = Math.cos( theta );\n\n\t\t\t\t\t// vertex\n\n\t\t\t\t\tvertex.x = radius * sinTheta;\n\t\t\t\t\tvertex.y = - v * height + halfHeight;\n\t\t\t\t\tvertex.z = radius * cosTheta;\n\t\t\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t\t\t// normal\n\n\t\t\t\t\tnormal.set( sinTheta, slope, cosTheta ).normalize();\n\t\t\t\t\tnormals.push( normal.x, normal.y, normal.z );\n\n\t\t\t\t\t// uv\n\n\t\t\t\t\tuvs.push( u, 1 - v );\n\n\t\t\t\t\t// save index of vertex in respective row\n\n\t\t\t\t\tindexRow.push( index ++ );\n\n\t\t\t\t}\n\n\t\t\t\t// now save vertices of the row in our index array\n\n\t\t\t\tindexArray.push( indexRow );\n\n\t\t\t}\n\n\t\t\t// generate indices\n\n\t\t\tfor ( let x = 0; x < radialSegments; x ++ ) {\n\n\t\t\t\tfor ( let y = 0; y < heightSegments; y ++ ) {\n\n\t\t\t\t\t// we use the index array to access the correct indices\n\n\t\t\t\t\tconst a = indexArray[ y ][ x ];\n\t\t\t\t\tconst b = indexArray[ y + 1 ][ x ];\n\t\t\t\t\tconst c = indexArray[ y + 1 ][ x + 1 ];\n\t\t\t\t\tconst d = indexArray[ y ][ x + 1 ];\n\n\t\t\t\t\t// faces\n\n\t\t\t\t\tif ( radiusTop > 0 || y !== 0 ) {\n\n\t\t\t\t\t\tindices.push( a, b, d );\n\t\t\t\t\t\tgroupCount += 3;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( radiusBottom > 0 || y !== heightSegments - 1 ) {\n\n\t\t\t\t\t\tindices.push( b, c, d );\n\t\t\t\t\t\tgroupCount += 3;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// add a group to the geometry. this will ensure multi material support\n\n\t\t\tscope.addGroup( groupStart, groupCount, 0 );\n\n\t\t\t// calculate new start value for groups\n\n\t\t\tgroupStart += groupCount;\n\n\t\t}\n\n\t\tfunction generateCap( top ) {\n\n\t\t\t// save the index of the first center vertex\n\t\t\tconst centerIndexStart = index;\n\n\t\t\tconst uv = new Vector2();\n\t\t\tconst vertex = new Vector3();\n\n\t\t\tlet groupCount = 0;\n\n\t\t\tconst radius = ( top === true ) ? radiusTop : radiusBottom;\n\t\t\tconst sign = ( top === true ) ? 1 : -1;\n\n\t\t\t// first we generate the center vertex data of the cap.\n\t\t\t// because the geometry needs one set of uvs per face,\n\t\t\t// we must generate a center vertex per face/segment\n\n\t\t\tfor ( let x = 1; x <= radialSegments; x ++ ) {\n\n\t\t\t\t// vertex\n\n\t\t\t\tvertices.push( 0, halfHeight * sign, 0 );\n\n\t\t\t\t// normal\n\n\t\t\t\tnormals.push( 0, sign, 0 );\n\n\t\t\t\t// uv\n\n\t\t\t\tuvs.push( 0.5, 0.5 );\n\n\t\t\t\t// increase index\n\n\t\t\t\tindex ++;\n\n\t\t\t}\n\n\t\t\t// save the index of the last center vertex\n\t\t\tconst centerIndexEnd = index;\n\n\t\t\t// now we generate the surrounding vertices, normals and uvs\n\n\t\t\tfor ( let x = 0; x <= radialSegments; x ++ ) {\n\n\t\t\t\tconst u = x / radialSegments;\n\t\t\t\tconst theta = u * thetaLength + thetaStart;\n\n\t\t\t\tconst cosTheta = Math.cos( theta );\n\t\t\t\tconst sinTheta = Math.sin( theta );\n\n\t\t\t\t// vertex\n\n\t\t\t\tvertex.x = radius * sinTheta;\n\t\t\t\tvertex.y = halfHeight * sign;\n\t\t\t\tvertex.z = radius * cosTheta;\n\t\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t\t// normal\n\n\t\t\t\tnormals.push( 0, sign, 0 );\n\n\t\t\t\t// uv\n\n\t\t\t\tuv.x = ( cosTheta * 0.5 ) + 0.5;\n\t\t\t\tuv.y = ( sinTheta * 0.5 * sign ) + 0.5;\n\t\t\t\tuvs.push( uv.x, uv.y );\n\n\t\t\t\t// increase index\n\n\t\t\t\tindex ++;\n\n\t\t\t}\n\n\t\t\t// generate indices\n\n\t\t\tfor ( let x = 0; x < radialSegments; x ++ ) {\n\n\t\t\t\tconst c = centerIndexStart + x;\n\t\t\t\tconst i = centerIndexEnd + x;\n\n\t\t\t\tif ( top === true ) {\n\n\t\t\t\t\t// face top\n\n\t\t\t\t\tindices.push( i, i + 1, c );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// face bottom\n\n\t\t\t\t\tindices.push( i + 1, i, c );\n\n\t\t\t\t}\n\n\t\t\t\tgroupCount += 3;\n\n\t\t\t}\n\n\t\t\t// add a group to the geometry. this will ensure multi material support\n\n\t\t\tscope.addGroup( groupStart, groupCount, top === true ? 1 : 2 );\n\n\t\t\t// calculate new start value for groups\n\n\t\t\tgroupStart += groupCount;\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {CylinderGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new CylinderGeometry( data.radiusTop, data.radiusBottom, data.height, data.radialSegments, data.heightSegments, data.openEnded, data.thetaStart, data.thetaLength );\n\n\t}\n\n}\n\n/**\n * A geometry class for representing a cone.\n *\n * ```js\n * const geometry = new THREE.ConeGeometry( 5, 20, 32 );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const cone = new THREE.Mesh(geometry, material );\n * scene.add( cone );\n * ```\n *\n * @augments CylinderGeometry\n * @demo scenes/geometry-browser.html#ConeGeometry\n */\nclass ConeGeometry extends CylinderGeometry {\n\n\t/**\n\t * Constructs a new cone geometry.\n\t *\n\t * @param {number} [radius=1] - Radius of the cone base.\n\t * @param {number} [height=1] - Height of the cone.\n\t * @param {number} [radialSegments=32] - Number of segmented faces around the circumference of the cone.\n\t * @param {number} [heightSegments=1] - Number of rows of faces along the height of the cone.\n\t * @param {boolean} [openEnded=false] - Whether the base of the cone is open or capped.\n\t * @param {number} [thetaStart=0] - Start angle for first segment, in radians.\n\t * @param {number} [thetaLength=Math.PI*2] - The central angle, often called theta, of the circular sector, in radians.\n\t * The default value results in a complete cone.\n\t */\n\tconstructor( radius = 1, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2 ) {\n\n\t\tsuper( 0, radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength );\n\n\t\tthis.type = 'ConeGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\theight: height,\n\t\t\tradialSegments: radialSegments,\n\t\t\theightSegments: heightSegments,\n\t\t\topenEnded: openEnded,\n\t\t\tthetaStart: thetaStart,\n\t\t\tthetaLength: thetaLength\n\t\t};\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {ConeGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new ConeGeometry( data.radius, data.height, data.radialSegments, data.heightSegments, data.openEnded, data.thetaStart, data.thetaLength );\n\n\t}\n\n}\n\n/**\n * A polyhedron is a solid in three dimensions with flat faces. This class\n * will take an array of vertices, project them onto a sphere, and then\n * divide them up to the desired level of detail.\n *\n * @augments BufferGeometry\n */\nclass PolyhedronGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new polyhedron geometry.\n\t *\n\t * @param {Array<number>} [vertices] - A flat array of vertices describing the base shape.\n\t * @param {Array<number>} [indices] - A flat array of indices describing the base shape.\n\t * @param {number} [radius=1] - The radius of the shape.\n\t * @param {number} [detail=0] - How many levels to subdivide the geometry. The more detail, the smoother the shape.\n\t */\n\tconstructor( vertices = [], indices = [], radius = 1, detail = 0 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'PolyhedronGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tvertices: vertices,\n\t\t\tindices: indices,\n\t\t\tradius: radius,\n\t\t\tdetail: detail\n\t\t};\n\n\t\t// default buffer data\n\n\t\tconst vertexBuffer = [];\n\t\tconst uvBuffer = [];\n\n\t\t// the subdivision creates the vertex buffer data\n\n\t\tsubdivide( detail );\n\n\t\t// all vertices should lie on a conceptual sphere with a given radius\n\n\t\tapplyRadius( radius );\n\n\t\t// finally, create the uv data\n\n\t\tgenerateUVs();\n\n\t\t// build non-indexed geometry\n\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertexBuffer, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( vertexBuffer.slice(), 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvBuffer, 2 ) );\n\n\t\tif ( detail === 0 ) {\n\n\t\t\tthis.computeVertexNormals(); // flat normals\n\n\t\t} else {\n\n\t\t\tthis.normalizeNormals(); // smooth normals\n\n\t\t}\n\n\t\t// helper functions\n\n\t\tfunction subdivide( detail ) {\n\n\t\t\tconst a = new Vector3();\n\t\t\tconst b = new Vector3();\n\t\t\tconst c = new Vector3();\n\n\t\t\t// iterate over all faces and apply a subdivision with the given detail value\n\n\t\t\tfor ( let i = 0; i < indices.length; i += 3 ) {\n\n\t\t\t\t// get the vertices of the face\n\n\t\t\t\tgetVertexByIndex( indices[ i + 0 ], a );\n\t\t\t\tgetVertexByIndex( indices[ i + 1 ], b );\n\t\t\t\tgetVertexByIndex( indices[ i + 2 ], c );\n\n\t\t\t\t// perform subdivision\n\n\t\t\t\tsubdivideFace( a, b, c, detail );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction subdivideFace( a, b, c, detail ) {\n\n\t\t\tconst cols = detail + 1;\n\n\t\t\t// we use this multidimensional array as a data structure for creating the subdivision\n\n\t\t\tconst v = [];\n\n\t\t\t// construct all of the vertices for this subdivision\n\n\t\t\tfor ( let i = 0; i <= cols; i ++ ) {\n\n\t\t\t\tv[ i ] = [];\n\n\t\t\t\tconst aj = a.clone().lerp( c, i / cols );\n\t\t\t\tconst bj = b.clone().lerp( c, i / cols );\n\n\t\t\t\tconst rows = cols - i;\n\n\t\t\t\tfor ( let j = 0; j <= rows; j ++ ) {\n\n\t\t\t\t\tif ( j === 0 && i === cols ) {\n\n\t\t\t\t\t\tv[ i ][ j ] = aj;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tv[ i ][ j ] = aj.clone().lerp( bj, j / rows );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// construct all of the faces\n\n\t\t\tfor ( let i = 0; i < cols; i ++ ) {\n\n\t\t\t\tfor ( let j = 0; j < 2 * ( cols - i ) - 1; j ++ ) {\n\n\t\t\t\t\tconst k = Math.floor( j / 2 );\n\n\t\t\t\t\tif ( j % 2 === 0 ) {\n\n\t\t\t\t\t\tpushVertex( v[ i ][ k + 1 ] );\n\t\t\t\t\t\tpushVertex( v[ i + 1 ][ k ] );\n\t\t\t\t\t\tpushVertex( v[ i ][ k ] );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tpushVertex( v[ i ][ k + 1 ] );\n\t\t\t\t\t\tpushVertex( v[ i + 1 ][ k + 1 ] );\n\t\t\t\t\t\tpushVertex( v[ i + 1 ][ k ] );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction applyRadius( radius ) {\n\n\t\t\tconst vertex = new Vector3();\n\n\t\t\t// iterate over the entire buffer and apply the radius to each vertex\n\n\t\t\tfor ( let i = 0; i < vertexBuffer.length; i += 3 ) {\n\n\t\t\t\tvertex.x = vertexBuffer[ i + 0 ];\n\t\t\t\tvertex.y = vertexBuffer[ i + 1 ];\n\t\t\t\tvertex.z = vertexBuffer[ i + 2 ];\n\n\t\t\t\tvertex.normalize().multiplyScalar( radius );\n\n\t\t\t\tvertexBuffer[ i + 0 ] = vertex.x;\n\t\t\t\tvertexBuffer[ i + 1 ] = vertex.y;\n\t\t\t\tvertexBuffer[ i + 2 ] = vertex.z;\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction generateUVs() {\n\n\t\t\tconst vertex = new Vector3();\n\n\t\t\tfor ( let i = 0; i < vertexBuffer.length; i += 3 ) {\n\n\t\t\t\tvertex.x = vertexBuffer[ i + 0 ];\n\t\t\t\tvertex.y = vertexBuffer[ i + 1 ];\n\t\t\t\tvertex.z = vertexBuffer[ i + 2 ];\n\n\t\t\t\tconst u = azimuth( vertex ) / 2 / Math.PI + 0.5;\n\t\t\t\tconst v = inclination( vertex ) / Math.PI + 0.5;\n\t\t\t\tuvBuffer.push( u, 1 - v );\n\n\t\t\t}\n\n\t\t\tcorrectUVs();\n\n\t\t\tcorrectSeam();\n\n\t\t}\n\n\t\tfunction correctSeam() {\n\n\t\t\t// handle case when face straddles the seam, see #3269\n\n\t\t\tfor ( let i = 0; i < uvBuffer.length; i += 6 ) {\n\n\t\t\t\t// uv data of a single face\n\n\t\t\t\tconst x0 = uvBuffer[ i + 0 ];\n\t\t\t\tconst x1 = uvBuffer[ i + 2 ];\n\t\t\t\tconst x2 = uvBuffer[ i + 4 ];\n\n\t\t\t\tconst max = Math.max( x0, x1, x2 );\n\t\t\t\tconst min = Math.min( x0, x1, x2 );\n\n\t\t\t\t// 0.9 is somewhat arbitrary\n\n\t\t\t\tif ( max > 0.9 && min < 0.1 ) {\n\n\t\t\t\t\tif ( x0 < 0.2 ) uvBuffer[ i + 0 ] += 1;\n\t\t\t\t\tif ( x1 < 0.2 ) uvBuffer[ i + 2 ] += 1;\n\t\t\t\t\tif ( x2 < 0.2 ) uvBuffer[ i + 4 ] += 1;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction pushVertex( vertex ) {\n\n\t\t\tvertexBuffer.push( vertex.x, vertex.y, vertex.z );\n\n\t\t}\n\n\t\tfunction getVertexByIndex( index, vertex ) {\n\n\t\t\tconst stride = index * 3;\n\n\t\t\tvertex.x = vertices[ stride + 0 ];\n\t\t\tvertex.y = vertices[ stride + 1 ];\n\t\t\tvertex.z = vertices[ stride + 2 ];\n\n\t\t}\n\n\t\tfunction correctUVs() {\n\n\t\t\tconst a = new Vector3();\n\t\t\tconst b = new Vector3();\n\t\t\tconst c = new Vector3();\n\n\t\t\tconst centroid = new Vector3();\n\n\t\t\tconst uvA = new Vector2();\n\t\t\tconst uvB = new Vector2();\n\t\t\tconst uvC = new Vector2();\n\n\t\t\tfor ( let i = 0, j = 0; i < vertexBuffer.length; i += 9, j += 6 ) {\n\n\t\t\t\ta.set( vertexBuffer[ i + 0 ], vertexBuffer[ i + 1 ], vertexBuffer[ i + 2 ] );\n\t\t\t\tb.set( vertexBuffer[ i + 3 ], vertexBuffer[ i + 4 ], vertexBuffer[ i + 5 ] );\n\t\t\t\tc.set( vertexBuffer[ i + 6 ], vertexBuffer[ i + 7 ], vertexBuffer[ i + 8 ] );\n\n\t\t\t\tuvA.set( uvBuffer[ j + 0 ], uvBuffer[ j + 1 ] );\n\t\t\t\tuvB.set( uvBuffer[ j + 2 ], uvBuffer[ j + 3 ] );\n\t\t\t\tuvC.set( uvBuffer[ j + 4 ], uvBuffer[ j + 5 ] );\n\n\t\t\t\tcentroid.copy( a ).add( b ).add( c ).divideScalar( 3 );\n\n\t\t\t\tconst azi = azimuth( centroid );\n\n\t\t\t\tcorrectUV( uvA, j + 0, a, azi );\n\t\t\t\tcorrectUV( uvB, j + 2, b, azi );\n\t\t\t\tcorrectUV( uvC, j + 4, c, azi );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction correctUV( uv, stride, vector, azimuth ) {\n\n\t\t\tif ( ( azimuth < 0 ) && ( uv.x === 1 ) ) {\n\n\t\t\t\tuvBuffer[ stride ] = uv.x - 1;\n\n\t\t\t}\n\n\t\t\tif ( ( vector.x === 0 ) && ( vector.z === 0 ) ) {\n\n\t\t\t\tuvBuffer[ stride ] = azimuth / 2 / Math.PI + 0.5;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// Angle around the Y axis, counter-clockwise when looking from above.\n\n\t\tfunction azimuth( vector ) {\n\n\t\t\treturn Math.atan2( vector.z, - vector.x );\n\n\t\t}\n\n\n\t\t// Angle above the XZ plane.\n\n\t\tfunction inclination( vector ) {\n\n\t\t\treturn Math.atan2( - vector.y, Math.sqrt( ( vector.x * vector.x ) + ( vector.z * vector.z ) ) );\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {PolyhedronGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new PolyhedronGeometry( data.vertices, data.indices, data.radius, data.detail );\n\n\t}\n\n}\n\n/**\n * A geometry class for representing a dodecahedron.\n *\n * ```js\n * const geometry = new THREE.DodecahedronGeometry();\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const dodecahedron = new THREE.Mesh( geometry, material );\n * scene.add( dodecahedron );\n * ```\n *\n * @augments PolyhedronGeometry\n * @demo scenes/geometry-browser.html#DodecahedronGeometry\n */\nclass DodecahedronGeometry extends PolyhedronGeometry {\n\n\t/**\n\t * Constructs a new dodecahedron geometry.\n\t *\n\t * @param {number} [radius=1] - Radius of the dodecahedron.\n\t * @param {number} [detail=0] - Setting this to a value greater than `0` adds vertices making it no longer a dodecahedron.\n\t */\n\tconstructor( radius = 1, detail = 0 ) {\n\n\t\tconst t = ( 1 + Math.sqrt( 5 ) ) / 2;\n\t\tconst r = 1 / t;\n\n\t\tconst vertices = [\n\n\t\t\t// (±1, ±1, ±1)\n\t\t\t-1, -1, -1,\t-1, -1, 1,\n\t\t\t-1, 1, -1, -1, 1, 1,\n\t\t\t1, -1, -1, 1, -1, 1,\n\t\t\t1, 1, -1, 1, 1, 1,\n\n\t\t\t// (0, ±1/φ, ±φ)\n\t\t\t0, - r, - t, 0, - r, t,\n\t\t\t0, r, - t, 0, r, t,\n\n\t\t\t// (±1/φ, ±φ, 0)\n\t\t\t- r, - t, 0, - r, t, 0,\n\t\t\tr, - t, 0, r, t, 0,\n\n\t\t\t// (±φ, 0, ±1/φ)\n\t\t\t- t, 0, - r, t, 0, - r,\n\t\t\t- t, 0, r, t, 0, r\n\t\t];\n\n\t\tconst indices = [\n\t\t\t3, 11, 7, \t3, 7, 15, \t3, 15, 13,\n\t\t\t7, 19, 17, \t7, 17, 6, \t7, 6, 15,\n\t\t\t17, 4, 8, \t17, 8, 10, \t17, 10, 6,\n\t\t\t8, 0, 16, \t8, 16, 2, \t8, 2, 10,\n\t\t\t0, 12, 1, \t0, 1, 18, \t0, 18, 16,\n\t\t\t6, 10, 2, \t6, 2, 13, \t6, 13, 15,\n\t\t\t2, 16, 18, \t2, 18, 3, \t2, 3, 13,\n\t\t\t18, 1, 9, \t18, 9, 11, \t18, 11, 3,\n\t\t\t4, 14, 12, \t4, 12, 0, \t4, 0, 8,\n\t\t\t11, 9, 5, \t11, 5, 19, \t11, 19, 7,\n\t\t\t19, 5, 14, \t19, 14, 4, \t19, 4, 17,\n\t\t\t1, 12, 14, \t1, 14, 5, \t1, 5, 9\n\t\t];\n\n\t\tsuper( vertices, indices, radius, detail );\n\n\t\tthis.type = 'DodecahedronGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\tdetail: detail\n\t\t};\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {DodecahedronGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new DodecahedronGeometry( data.radius, data.detail );\n\n\t}\n\n}\n\nconst _v0 = /*@__PURE__*/ new Vector3();\nconst _v1$1 = /*@__PURE__*/ new Vector3();\nconst _normal = /*@__PURE__*/ new Vector3();\nconst _triangle = /*@__PURE__*/ new Triangle();\n\n/**\n * Can be used as a helper object to view the edges of a geometry.\n *\n * ```js\n * const geometry = new THREE.BoxGeometry();\n * const edges = new THREE.EdgesGeometry( geometry );\n * const line = new THREE.LineSegments( edges );\n * scene.add( line );\n * ```\n *\n * Note: It is not yet possible to serialize/deserialize instances of this class.\n *\n * @augments BufferGeometry\n */\nclass EdgesGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new edges geometry.\n\t *\n\t * @param {?BufferGeometry} [geometry=null] - The geometry.\n\t * @param {number} [thresholdAngle=1] - An edge is only rendered if the angle (in degrees)\n\t * between the face normals of the adjoining faces exceeds this value.\n\t */\n\tconstructor( geometry = null, thresholdAngle = 1 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'EdgesGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tgeometry: geometry,\n\t\t\tthresholdAngle: thresholdAngle\n\t\t};\n\n\t\tif ( geometry !== null ) {\n\n\t\t\tconst precisionPoints = 4;\n\t\t\tconst precision = Math.pow( 10, precisionPoints );\n\t\t\tconst thresholdDot = Math.cos( DEG2RAD * thresholdAngle );\n\n\t\t\tconst indexAttr = geometry.getIndex();\n\t\t\tconst positionAttr = geometry.getAttribute( 'position' );\n\t\t\tconst indexCount = indexAttr ? indexAttr.count : positionAttr.count;\n\n\t\t\tconst indexArr = [ 0, 0, 0 ];\n\t\t\tconst vertKeys = [ 'a', 'b', 'c' ];\n\t\t\tconst hashes = new Array( 3 );\n\n\t\t\tconst edgeData = {};\n\t\t\tconst vertices = [];\n\t\t\tfor ( let i = 0; i < indexCount; i += 3 ) {\n\n\t\t\t\tif ( indexAttr ) {\n\n\t\t\t\t\tindexArr[ 0 ] = indexAttr.getX( i );\n\t\t\t\t\tindexArr[ 1 ] = indexAttr.getX( i + 1 );\n\t\t\t\t\tindexArr[ 2 ] = indexAttr.getX( i + 2 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tindexArr[ 0 ] = i;\n\t\t\t\t\tindexArr[ 1 ] = i + 1;\n\t\t\t\t\tindexArr[ 2 ] = i + 2;\n\n\t\t\t\t}\n\n\t\t\t\tconst { a, b, c } = _triangle;\n\t\t\t\ta.fromBufferAttribute( positionAttr, indexArr[ 0 ] );\n\t\t\t\tb.fromBufferAttribute( positionAttr, indexArr[ 1 ] );\n\t\t\t\tc.fromBufferAttribute( positionAttr, indexArr[ 2 ] );\n\t\t\t\t_triangle.getNormal( _normal );\n\n\t\t\t\t// create hashes for the edge from the vertices\n\t\t\t\thashes[ 0 ] = `${ Math.round( a.x * precision ) },${ Math.round( a.y * precision ) },${ Math.round( a.z * precision ) }`;\n\t\t\t\thashes[ 1 ] = `${ Math.round( b.x * precision ) },${ Math.round( b.y * precision ) },${ Math.round( b.z * precision ) }`;\n\t\t\t\thashes[ 2 ] = `${ Math.round( c.x * precision ) },${ Math.round( c.y * precision ) },${ Math.round( c.z * precision ) }`;\n\n\t\t\t\t// skip degenerate triangles\n\t\t\t\tif ( hashes[ 0 ] === hashes[ 1 ] || hashes[ 1 ] === hashes[ 2 ] || hashes[ 2 ] === hashes[ 0 ] ) {\n\n\t\t\t\t\tcontinue;\n\n\t\t\t\t}\n\n\t\t\t\t// iterate over every edge\n\t\t\t\tfor ( let j = 0; j < 3; j ++ ) {\n\n\t\t\t\t\t// get the first and next vertex making up the edge\n\t\t\t\t\tconst jNext = ( j + 1 ) % 3;\n\t\t\t\t\tconst vecHash0 = hashes[ j ];\n\t\t\t\t\tconst vecHash1 = hashes[ jNext ];\n\t\t\t\t\tconst v0 = _triangle[ vertKeys[ j ] ];\n\t\t\t\t\tconst v1 = _triangle[ vertKeys[ jNext ] ];\n\n\t\t\t\t\tconst hash = `${ vecHash0 }_${ vecHash1 }`;\n\t\t\t\t\tconst reverseHash = `${ vecHash1 }_${ vecHash0 }`;\n\n\t\t\t\t\tif ( reverseHash in edgeData && edgeData[ reverseHash ] ) {\n\n\t\t\t\t\t\t// if we found a sibling edge add it into the vertex array if\n\t\t\t\t\t\t// it meets the angle threshold and delete the edge from the map.\n\t\t\t\t\t\tif ( _normal.dot( edgeData[ reverseHash ].normal ) <= thresholdDot ) {\n\n\t\t\t\t\t\t\tvertices.push( v0.x, v0.y, v0.z );\n\t\t\t\t\t\t\tvertices.push( v1.x, v1.y, v1.z );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tedgeData[ reverseHash ] = null;\n\n\t\t\t\t\t} else if ( ! ( hash in edgeData ) ) {\n\n\t\t\t\t\t\t// if we've already got an edge here then skip adding a new one\n\t\t\t\t\t\tedgeData[ hash ] = {\n\n\t\t\t\t\t\t\tindex0: indexArr[ j ],\n\t\t\t\t\t\t\tindex1: indexArr[ jNext ],\n\t\t\t\t\t\t\tnormal: _normal.clone(),\n\n\t\t\t\t\t\t};\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// iterate over all remaining, unmatched edges and add them to the vertex array\n\t\t\tfor ( const key in edgeData ) {\n\n\t\t\t\tif ( edgeData[ key ] ) {\n\n\t\t\t\t\tconst { index0, index1 } = edgeData[ key ];\n\t\t\t\t\t_v0.fromBufferAttribute( positionAttr, index0 );\n\t\t\t\t\t_v1$1.fromBufferAttribute( positionAttr, index1 );\n\n\t\t\t\t\tvertices.push( _v0.x, _v0.y, _v0.z );\n\t\t\t\t\tvertices.push( _v1$1.x, _v1$1.y, _v1$1.z );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * An abstract base class for creating an analytic curve object that contains methods\n * for interpolation.\n *\n * @abstract\n */\nclass Curve {\n\n\t/**\n\t * Constructs a new curve.\n\t */\n\tconstructor() {\n\n\t\t/**\n\t\t * The type property is used for detecting the object type\n\t\t * in context of serialization/deserialization.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.type = 'Curve';\n\n\t\t/**\n\t\t * This value determines the amount of divisions when calculating the\n\t\t * cumulative segment lengths of a curve via {@link Curve#getLengths}. To ensure\n\t\t * precision when using methods like {@link Curve#getSpacedPoints}, it is\n\t\t * recommended to increase the value of this property if the curve is very large.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 200\n\t\t */\n\t\tthis.arcLengthDivisions = 200;\n\n\t\t/**\n\t\t * Must be set to `true` if the curve parameters have changed.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.needsUpdate = false;\n\n\t\t/**\n\t\t * An internal cache that holds precomputed curve length values.\n\t\t *\n\t\t * @private\n\t\t * @type {?Array<number>}\n\t\t * @default null\n\t\t */\n\t\tthis.cacheArcLengths = null;\n\n\t}\n\n\t/**\n\t * This method returns a vector in 2D or 3D space (depending on the curve definition)\n\t * for the given interpolation factor.\n\t *\n\t * @abstract\n\t * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {(Vector2|Vector3)} The position on the curve. It can be a 2D or 3D vector depending on the curve definition.\n\t */\n\tgetPoint( /* t, optionalTarget */ ) {\n\n\t\twarn( 'Curve: .getPoint() not implemented.' );\n\n\t}\n\n\t/**\n\t * This method returns a vector in 2D or 3D space (depending on the curve definition)\n\t * for the given interpolation factor. Unlike {@link Curve#getPoint}, this method honors the length\n\t * of the curve which equidistant samples.\n\t *\n\t * @param {number} u - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {(Vector2|Vector3)} The position on the curve. It can be a 2D or 3D vector depending on the curve definition.\n\t */\n\tgetPointAt( u, optionalTarget ) {\n\n\t\tconst t = this.getUtoTmapping( u );\n\t\treturn this.getPoint( t, optionalTarget );\n\n\t}\n\n\t/**\n\t * This method samples the curve via {@link Curve#getPoint} and returns an array of points representing\n\t * the curve shape.\n\t *\n\t * @param {number} [divisions=5] - The number of divisions.\n\t * @return {Array<(Vector2|Vector3)>} An array holding the sampled curve values. The number of points is `divisions + 1`.\n\t */\n\tgetPoints( divisions = 5 ) {\n\n\t\tconst points = [];\n\n\t\tfor ( let d = 0; d <= divisions; d ++ ) {\n\n\t\t\tpoints.push( this.getPoint( d / divisions ) );\n\n\t\t}\n\n\t\treturn points;\n\n\t}\n\n\t// Get sequence of points using getPointAt( u )\n\n\t/**\n\t * This method samples the curve via {@link Curve#getPointAt} and returns an array of points representing\n\t * the curve shape. Unlike {@link Curve#getPoints}, this method returns equi-spaced points across the entire\n\t * curve.\n\t *\n\t * @param {number} [divisions=5] - The number of divisions.\n\t * @return {Array<(Vector2|Vector3)>} An array holding the sampled curve values. The number of points is `divisions + 1`.\n\t */\n\tgetSpacedPoints( divisions = 5 ) {\n\n\t\tconst points = [];\n\n\t\tfor ( let d = 0; d <= divisions; d ++ ) {\n\n\t\t\tpoints.push( this.getPointAt( d / divisions ) );\n\n\t\t}\n\n\t\treturn points;\n\n\t}\n\n\t/**\n\t * Returns the total arc length of the curve.\n\t *\n\t * @return {number} The length of the curve.\n\t */\n\tgetLength() {\n\n\t\tconst lengths = this.getLengths();\n\t\treturn lengths[ lengths.length - 1 ];\n\n\t}\n\n\t/**\n\t * Returns an array of cumulative segment lengths of the curve.\n\t *\n\t * @param {number} [divisions=this.arcLengthDivisions] - The number of divisions.\n\t * @return {Array<number>} An array holding the cumulative segment lengths.\n\t */\n\tgetLengths( divisions = this.arcLengthDivisions ) {\n\n\t\tif ( this.cacheArcLengths &&\n\t\t\t( this.cacheArcLengths.length === divisions + 1 ) &&\n\t\t\t! this.needsUpdate ) {\n\n\t\t\treturn this.cacheArcLengths;\n\n\t\t}\n\n\t\tthis.needsUpdate = false;\n\n\t\tconst cache = [];\n\t\tlet current, last = this.getPoint( 0 );\n\t\tlet sum = 0;\n\n\t\tcache.push( 0 );\n\n\t\tfor ( let p = 1; p <= divisions; p ++ ) {\n\n\t\t\tcurrent = this.getPoint( p / divisions );\n\t\t\tsum += current.distanceTo( last );\n\t\t\tcache.push( sum );\n\t\t\tlast = current;\n\n\t\t}\n\n\t\tthis.cacheArcLengths = cache;\n\n\t\treturn cache; // { sums: cache, sum: sum }; Sum is in the last element.\n\n\t}\n\n\t/**\n\t * Update the cumulative segment distance cache. The method must be called\n\t * every time curve parameters are changed. If an updated curve is part of a\n\t * composed curve like {@link CurvePath}, this method must be called on the\n\t * composed curve, too.\n\t */\n\tupdateArcLengths() {\n\n\t\tthis.needsUpdate = true;\n\t\tthis.getLengths();\n\n\t}\n\n\t/**\n\t * Given an interpolation factor in the range `[0,1]`, this method returns an updated\n\t * interpolation factor in the same range that can be ued to sample equidistant points\n\t * from a curve.\n\t *\n\t * @param {number} u - The interpolation factor.\n\t * @param {?number} distance - An optional distance on the curve.\n\t * @return {number} The updated interpolation factor.\n\t */\n\tgetUtoTmapping( u, distance = null ) {\n\n\t\tconst arcLengths = this.getLengths();\n\n\t\tlet i = 0;\n\t\tconst il = arcLengths.length;\n\n\t\tlet targetArcLength; // The targeted u distance value to get\n\n\t\tif ( distance ) {\n\n\t\t\ttargetArcLength = distance;\n\n\t\t} else {\n\n\t\t\ttargetArcLength = u * arcLengths[ il - 1 ];\n\n\t\t}\n\n\t\t// binary search for the index with largest value smaller than target u distance\n\n\t\tlet low = 0, high = il - 1, comparison;\n\n\t\twhile ( low <= high ) {\n\n\t\t\ti = Math.floor( low + ( high - low ) / 2 ); // less likely to overflow, though probably not issue here, JS doesn't really have integers, all numbers are floats\n\n\t\t\tcomparison = arcLengths[ i ] - targetArcLength;\n\n\t\t\tif ( comparison < 0 ) {\n\n\t\t\t\tlow = i + 1;\n\n\t\t\t} else if ( comparison > 0 ) {\n\n\t\t\t\thigh = i - 1;\n\n\t\t\t} else {\n\n\t\t\t\thigh = i;\n\t\t\t\tbreak;\n\n\t\t\t\t// DONE\n\n\t\t\t}\n\n\t\t}\n\n\t\ti = high;\n\n\t\tif ( arcLengths[ i ] === targetArcLength ) {\n\n\t\t\treturn i / ( il - 1 );\n\n\t\t}\n\n\t\t// we could get finer grain at lengths, or use simple interpolation between two points\n\n\t\tconst lengthBefore = arcLengths[ i ];\n\t\tconst lengthAfter = arcLengths[ i + 1 ];\n\n\t\tconst segmentLength = lengthAfter - lengthBefore;\n\n\t\t// determine where we are between the 'before' and 'after' points\n\n\t\tconst segmentFraction = ( targetArcLength - lengthBefore ) / segmentLength;\n\n\t\t// add that fractional amount to t\n\n\t\tconst t = ( i + segmentFraction ) / ( il - 1 );\n\n\t\treturn t;\n\n\t}\n\n\t/**\n\t * Returns a unit vector tangent for the given interpolation factor.\n\t * If the derived curve does not implement its tangent derivation,\n\t * two points a small delta apart will be used to find its gradient\n\t * which seems to give a reasonable approximation.\n\t *\n\t * @param {number} t - The interpolation factor.\n\t * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {(Vector2|Vector3)} The tangent vector.\n\t */\n\tgetTangent( t, optionalTarget ) {\n\n\t\tconst delta = 0.0001;\n\t\tlet t1 = t - delta;\n\t\tlet t2 = t + delta;\n\n\t\t// Capping in case of danger\n\n\t\tif ( t1 < 0 ) t1 = 0;\n\t\tif ( t2 > 1 ) t2 = 1;\n\n\t\tconst pt1 = this.getPoint( t1 );\n\t\tconst pt2 = this.getPoint( t2 );\n\n\t\tconst tangent = optionalTarget || ( ( pt1.isVector2 ) ? new Vector2() : new Vector3() );\n\n\t\ttangent.copy( pt2 ).sub( pt1 ).normalize();\n\n\t\treturn tangent;\n\n\t}\n\n\t/**\n\t * Same as {@link Curve#getTangent} but with equidistant samples.\n\t *\n\t * @param {number} u - The interpolation factor.\n\t * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {(Vector2|Vector3)} The tangent vector.\n\t * @see {@link Curve#getPointAt}\n\t */\n\tgetTangentAt( u, optionalTarget ) {\n\n\t\tconst t = this.getUtoTmapping( u );\n\t\treturn this.getTangent( t, optionalTarget );\n\n\t}\n\n\t/**\n\t * Generates the Frenet Frames. Requires a curve definition in 3D space. Used\n\t * in geometries like {@link TubeGeometry} or {@link ExtrudeGeometry}.\n\t *\n\t * @param {number} segments - The number of segments.\n\t * @param {boolean} [closed=false] - Whether the curve is closed or not.\n\t * @return {{tangents: Array<Vector3>, normals: Array<Vector3>, binormals: Array<Vector3>}} The Frenet Frames.\n\t */\n\tcomputeFrenetFrames( segments, closed = false ) {\n\n\t\t// see http://www.cs.indiana.edu/pub/techreports/TR425.pdf\n\n\t\tconst normal = new Vector3();\n\n\t\tconst tangents = [];\n\t\tconst normals = [];\n\t\tconst binormals = [];\n\n\t\tconst vec = new Vector3();\n\t\tconst mat = new Matrix4();\n\n\t\t// compute the tangent vectors for each segment on the curve\n\n\t\tfor ( let i = 0; i <= segments; i ++ ) {\n\n\t\t\tconst u = i / segments;\n\n\t\t\ttangents[ i ] = this.getTangentAt( u, new Vector3() );\n\n\t\t}\n\n\t\t// select an initial normal vector perpendicular to the first tangent vector,\n\t\t// and in the direction of the minimum tangent xyz component\n\n\t\tnormals[ 0 ] = new Vector3();\n\t\tbinormals[ 0 ] = new Vector3();\n\t\tlet min = Number.MAX_VALUE;\n\t\tconst tx = Math.abs( tangents[ 0 ].x );\n\t\tconst ty = Math.abs( tangents[ 0 ].y );\n\t\tconst tz = Math.abs( tangents[ 0 ].z );\n\n\t\tif ( tx <= min ) {\n\n\t\t\tmin = tx;\n\t\t\tnormal.set( 1, 0, 0 );\n\n\t\t}\n\n\t\tif ( ty <= min ) {\n\n\t\t\tmin = ty;\n\t\t\tnormal.set( 0, 1, 0 );\n\n\t\t}\n\n\t\tif ( tz <= min ) {\n\n\t\t\tnormal.set( 0, 0, 1 );\n\n\t\t}\n\n\t\tvec.crossVectors( tangents[ 0 ], normal ).normalize();\n\n\t\tnormals[ 0 ].crossVectors( tangents[ 0 ], vec );\n\t\tbinormals[ 0 ].crossVectors( tangents[ 0 ], normals[ 0 ] );\n\n\n\t\t// compute the slowly-varying normal and binormal vectors for each segment on the curve\n\n\t\tfor ( let i = 1; i <= segments; i ++ ) {\n\n\t\t\tnormals[ i ] = normals[ i - 1 ].clone();\n\n\t\t\tbinormals[ i ] = binormals[ i - 1 ].clone();\n\n\t\t\tvec.crossVectors( tangents[ i - 1 ], tangents[ i ] );\n\n\t\t\tif ( vec.length() > Number.EPSILON ) {\n\n\t\t\t\tvec.normalize();\n\n\t\t\t\tconst theta = Math.acos( clamp( tangents[ i - 1 ].dot( tangents[ i ] ), -1, 1 ) ); // clamp for floating pt errors\n\n\t\t\t\tnormals[ i ].applyMatrix4( mat.makeRotationAxis( vec, theta ) );\n\n\t\t\t}\n\n\t\t\tbinormals[ i ].crossVectors( tangents[ i ], normals[ i ] );\n\n\t\t}\n\n\t\t// if the curve is closed, postprocess the vectors so the first and last normal vectors are the same\n\n\t\tif ( closed === true ) {\n\n\t\t\tlet theta = Math.acos( clamp( normals[ 0 ].dot( normals[ segments ] ), -1, 1 ) );\n\t\t\ttheta /= segments;\n\n\t\t\tif ( tangents[ 0 ].dot( vec.crossVectors( normals[ 0 ], normals[ segments ] ) ) > 0 ) {\n\n\t\t\t\ttheta = - theta;\n\n\t\t\t}\n\n\t\t\tfor ( let i = 1; i <= segments; i ++ ) {\n\n\t\t\t\t// twist a little...\n\t\t\t\tnormals[ i ].applyMatrix4( mat.makeRotationAxis( tangents[ i ], theta * i ) );\n\t\t\t\tbinormals[ i ].crossVectors( tangents[ i ], normals[ i ] );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn {\n\t\t\ttangents: tangents,\n\t\t\tnormals: normals,\n\t\t\tbinormals: binormals\n\t\t};\n\n\t}\n\n\t/**\n\t * Returns a new curve with copied values from this instance.\n\t *\n\t * @return {Curve} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Copies the values of the given curve to this instance.\n\t *\n\t * @param {Curve} source - The curve to copy.\n\t * @return {Curve} A reference to this curve.\n\t */\n\tcopy( source ) {\n\n\t\tthis.arcLengthDivisions = source.arcLengthDivisions;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Serializes the curve into JSON.\n\t *\n\t * @return {Object} A JSON object representing the serialized curve.\n\t * @see {@link ObjectLoader#parse}\n\t */\n\ttoJSON() {\n\n\t\tconst data = {\n\t\t\tmetadata: {\n\t\t\t\tversion: 4.7,\n\t\t\t\ttype: 'Curve',\n\t\t\t\tgenerator: 'Curve.toJSON'\n\t\t\t}\n\t\t};\n\n\t\tdata.arcLengthDivisions = this.arcLengthDivisions;\n\t\tdata.type = this.type;\n\n\t\treturn data;\n\n\t}\n\n\t/**\n\t * Deserializes the curve from the given JSON.\n\t *\n\t * @param {Object} json - The JSON holding the serialized curve.\n\t * @return {Curve} A reference to this curve.\n\t */\n\tfromJSON( json ) {\n\n\t\tthis.arcLengthDivisions = json.arcLengthDivisions;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A curve representing an ellipse.\n *\n * ```js\n * const curve = new THREE.EllipseCurve(\n * \t0, 0,\n * \t10, 10,\n * \t0, 2 * Math.PI,\n * \tfalse,\n * \t0\n * );\n *\n * const points = curve.getPoints( 50 );\n * const geometry = new THREE.BufferGeometry().setFromPoints( points );\n *\n * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );\n *\n * // Create the final object to add to the scene\n * const ellipse = new THREE.Line( geometry, material );\n * ```\n *\n * @augments Curve\n */\nclass EllipseCurve extends Curve {\n\n\t/**\n\t * Constructs a new ellipse curve.\n\t *\n\t * @param {number} [aX=0] - The X center of the ellipse.\n\t * @param {number} [aY=0] - The Y center of the ellipse.\n\t * @param {number} [xRadius=1] - The radius of the ellipse in the x direction.\n\t * @param {number} [yRadius=1] - The radius of the ellipse in the y direction.\n\t * @param {number} [aStartAngle=0] - The start angle of the curve in radians starting from the positive X axis.\n\t * @param {number} [aEndAngle=Math.PI*2] - The end angle of the curve in radians starting from the positive X axis.\n\t * @param {boolean} [aClockwise=false] - Whether the ellipse is drawn clockwise or not.\n\t * @param {number} [aRotation=0] - The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.\n\t */\n\tconstructor( aX = 0, aY = 0, xRadius = 1, yRadius = 1, aStartAngle = 0, aEndAngle = Math.PI * 2, aClockwise = false, aRotation = 0 ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isEllipseCurve = true;\n\n\t\tthis.type = 'EllipseCurve';\n\n\t\t/**\n\t\t * The X center of the ellipse.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.aX = aX;\n\n\t\t/**\n\t\t * The Y center of the ellipse.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.aY = aY;\n\n\t\t/**\n\t\t * The radius of the ellipse in the x direction.\n\t\t * Setting the this value equal to the {@link EllipseCurve#yRadius} will result in a circle.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.xRadius = xRadius;\n\n\t\t/**\n\t\t * The radius of the ellipse in the y direction.\n\t\t * Setting the this value equal to the {@link EllipseCurve#xRadius} will result in a circle.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.yRadius = yRadius;\n\n\t\t/**\n\t\t * The start angle of the curve in radians starting from the positive X axis.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.aStartAngle = aStartAngle;\n\n\t\t/**\n\t\t * The end angle of the curve in radians starting from the positive X axis.\n\t\t *\n\t\t * @type {number}\n\t\t * @default Math.PI*2\n\t\t */\n\t\tthis.aEndAngle = aEndAngle;\n\n\t\t/**\n\t\t * Whether the ellipse is drawn clockwise or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.aClockwise = aClockwise;\n\n\t\t/**\n\t\t * The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.aRotation = aRotation;\n\n\t}\n\n\t/**\n\t * Returns a point on the curve.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {Vector2} The position on the curve.\n\t */\n\tgetPoint( t, optionalTarget = new Vector2() ) {\n\n\t\tconst point = optionalTarget;\n\n\t\tconst twoPi = Math.PI * 2;\n\t\tlet deltaAngle = this.aEndAngle - this.aStartAngle;\n\t\tconst samePoints = Math.abs( deltaAngle ) < Number.EPSILON;\n\n\t\t// ensures that deltaAngle is 0 .. 2 PI\n\t\twhile ( deltaAngle < 0 ) deltaAngle += twoPi;\n\t\twhile ( deltaAngle > twoPi ) deltaAngle -= twoPi;\n\n\t\tif ( deltaAngle < Number.EPSILON ) {\n\n\t\t\tif ( samePoints ) {\n\n\t\t\t\tdeltaAngle = 0;\n\n\t\t\t} else {\n\n\t\t\t\tdeltaAngle = twoPi;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( this.aClockwise === true && ! samePoints ) {\n\n\t\t\tif ( deltaAngle === twoPi ) {\n\n\t\t\t\tdeltaAngle = - twoPi;\n\n\t\t\t} else {\n\n\t\t\t\tdeltaAngle = deltaAngle - twoPi;\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst angle = this.aStartAngle + t * deltaAngle;\n\t\tlet x = this.aX + this.xRadius * Math.cos( angle );\n\t\tlet y = this.aY + this.yRadius * Math.sin( angle );\n\n\t\tif ( this.aRotation !== 0 ) {\n\n\t\t\tconst cos = Math.cos( this.aRotation );\n\t\t\tconst sin = Math.sin( this.aRotation );\n\n\t\t\tconst tx = x - this.aX;\n\t\t\tconst ty = y - this.aY;\n\n\t\t\t// Rotate the point about the center of the ellipse.\n\t\t\tx = tx * cos - ty * sin + this.aX;\n\t\t\ty = tx * sin + ty * cos + this.aY;\n\n\t\t}\n\n\t\treturn point.set( x, y );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.aX = source.aX;\n\t\tthis.aY = source.aY;\n\n\t\tthis.xRadius = source.xRadius;\n\t\tthis.yRadius = source.yRadius;\n\n\t\tthis.aStartAngle = source.aStartAngle;\n\t\tthis.aEndAngle = source.aEndAngle;\n\n\t\tthis.aClockwise = source.aClockwise;\n\n\t\tthis.aRotation = source.aRotation;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.aX = this.aX;\n\t\tdata.aY = this.aY;\n\n\t\tdata.xRadius = this.xRadius;\n\t\tdata.yRadius = this.yRadius;\n\n\t\tdata.aStartAngle = this.aStartAngle;\n\t\tdata.aEndAngle = this.aEndAngle;\n\n\t\tdata.aClockwise = this.aClockwise;\n\n\t\tdata.aRotation = this.aRotation;\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.aX = json.aX;\n\t\tthis.aY = json.aY;\n\n\t\tthis.xRadius = json.xRadius;\n\t\tthis.yRadius = json.yRadius;\n\n\t\tthis.aStartAngle = json.aStartAngle;\n\t\tthis.aEndAngle = json.aEndAngle;\n\n\t\tthis.aClockwise = json.aClockwise;\n\n\t\tthis.aRotation = json.aRotation;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A curve representing an arc.\n *\n * @augments EllipseCurve\n */\nclass ArcCurve extends EllipseCurve {\n\n\t/**\n\t * Constructs a new arc curve.\n\t *\n\t * @param {number} [aX=0] - The X center of the ellipse.\n\t * @param {number} [aY=0] - The Y center of the ellipse.\n\t * @param {number} [aRadius=1] - The radius of the ellipse in the x direction.\n\t * @param {number} [aStartAngle=0] - The start angle of the curve in radians starting from the positive X axis.\n\t * @param {number} [aEndAngle=Math.PI*2] - The end angle of the curve in radians starting from the positive X axis.\n\t * @param {boolean} [aClockwise=false] - Whether the ellipse is drawn clockwise or not.\n\t */\n\tconstructor( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {\n\n\t\tsuper( aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isArcCurve = true;\n\n\t\tthis.type = 'ArcCurve';\n\n\t}\n\n}\n\nfunction CubicPoly() {\n\n\t/**\n\t * Centripetal CatmullRom Curve - which is useful for avoiding\n\t* cusps and self-intersections in non-uniform catmull rom curves.\n\t* http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf\n\t*\n\t* curve.type accepts centripetal(default), chordal and catmullrom\n\t* curve.tension is used for catmullrom which defaults to 0.5\n\t*/\n\n\t/*\n\tBased on an optimized c++ solution in\n\t- http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections/\n\t- http://ideone.com/NoEbVM\n\n\tThis CubicPoly class could be used for reusing some variables and calculations,\n\tbut for three.js curve use, it could be possible inlined and flatten into a single function call\n\twhich can be placed in CurveUtils.\n\t*/\n\n\tlet c0 = 0, c1 = 0, c2 = 0, c3 = 0;\n\n\t/*\n\t * Compute coefficients for a cubic polynomial\n\t *   p(s) = c0 + c1*s + c2*s^2 + c3*s^3\n\t * such that\n\t *   p(0) = x0, p(1) = x1\n\t *  and\n\t *   p'(0) = t0, p'(1) = t1.\n\t */\n\tfunction init( x0, x1, t0, t1 ) {\n\n\t\tc0 = x0;\n\t\tc1 = t0;\n\t\tc2 = -3 * x0 + 3 * x1 - 2 * t0 - t1;\n\t\tc3 = 2 * x0 - 2 * x1 + t0 + t1;\n\n\t}\n\n\treturn {\n\n\t\tinitCatmullRom: function ( x0, x1, x2, x3, tension ) {\n\n\t\t\tinit( x1, x2, tension * ( x2 - x0 ), tension * ( x3 - x1 ) );\n\n\t\t},\n\n\t\tinitNonuniformCatmullRom: function ( x0, x1, x2, x3, dt0, dt1, dt2 ) {\n\n\t\t\t// compute tangents when parameterized in [t1,t2]\n\t\t\tlet t1 = ( x1 - x0 ) / dt0 - ( x2 - x0 ) / ( dt0 + dt1 ) + ( x2 - x1 ) / dt1;\n\t\t\tlet t2 = ( x2 - x1 ) / dt1 - ( x3 - x1 ) / ( dt1 + dt2 ) + ( x3 - x2 ) / dt2;\n\n\t\t\t// rescale tangents for parametrization in [0,1]\n\t\t\tt1 *= dt1;\n\t\t\tt2 *= dt1;\n\n\t\t\tinit( x1, x2, t1, t2 );\n\n\t\t},\n\n\t\tcalc: function ( t ) {\n\n\t\t\tconst t2 = t * t;\n\t\t\tconst t3 = t2 * t;\n\t\t\treturn c0 + c1 * t + c2 * t2 + c3 * t3;\n\n\t\t}\n\n\t};\n\n}\n\n//\n\nconst tmp = /*@__PURE__*/ new Vector3();\nconst px = /*@__PURE__*/ new CubicPoly();\nconst py = /*@__PURE__*/ new CubicPoly();\nconst pz = /*@__PURE__*/ new CubicPoly();\n\n/**\n * A curve representing a Catmull-Rom spline.\n *\n * ```js\n * //Create a closed wavey loop\n * const curve = new THREE.CatmullRomCurve3( [\n * \tnew THREE.Vector3( -10, 0, 10 ),\n * \tnew THREE.Vector3( -5, 5, 5 ),\n * \tnew THREE.Vector3( 0, 0, 0 ),\n * \tnew THREE.Vector3( 5, -5, 5 ),\n * \tnew THREE.Vector3( 10, 0, 10 )\n * ] );\n *\n * const points = curve.getPoints( 50 );\n * const geometry = new THREE.BufferGeometry().setFromPoints( points );\n *\n * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );\n *\n * // Create the final object to add to the scene\n * const curveObject = new THREE.Line( geometry, material );\n * ```\n *\n * @augments Curve\n */\nclass CatmullRomCurve3 extends Curve {\n\n\t/**\n\t * Constructs a new Catmull-Rom curve.\n\t *\n\t * @param {Array<Vector3>} [points] - An array of 3D points defining the curve.\n\t * @param {boolean} [closed=false] - Whether the curve is closed or not.\n\t * @param {('centripetal'|'chordal'|'catmullrom')} [curveType='centripetal'] - The curve type.\n\t * @param {number} [tension=0.5] - Tension of the curve.\n\t */\n\tconstructor( points = [], closed = false, curveType = 'centripetal', tension = 0.5 ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCatmullRomCurve3 = true;\n\n\t\tthis.type = 'CatmullRomCurve3';\n\n\t\t/**\n\t\t * An array of 3D points defining the curve.\n\t\t *\n\t\t * @type {Array<Vector3>}\n\t\t */\n\t\tthis.points = points;\n\n\t\t/**\n\t\t * Whether the curve is closed or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.closed = closed;\n\n\t\t/**\n\t\t * The curve type.\n\t\t *\n\t\t * @type {('centripetal'|'chordal'|'catmullrom')}\n\t\t * @default 'centripetal'\n\t\t */\n\t\tthis.curveType = curveType;\n\n\t\t/**\n\t\t * Tension of the curve.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0.5\n\t\t */\n\t\tthis.tension = tension;\n\n\t}\n\n\t/**\n\t * Returns a point on the curve.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {Vector3} The position on the curve.\n\t */\n\tgetPoint( t, optionalTarget = new Vector3() ) {\n\n\t\tconst point = optionalTarget;\n\n\t\tconst points = this.points;\n\t\tconst l = points.length;\n\n\t\tconst p = ( l - ( this.closed ? 0 : 1 ) ) * t;\n\t\tlet intPoint = Math.floor( p );\n\t\tlet weight = p - intPoint;\n\n\t\tif ( this.closed ) {\n\n\t\t\tintPoint += intPoint > 0 ? 0 : ( Math.floor( Math.abs( intPoint ) / l ) + 1 ) * l;\n\n\t\t} else if ( weight === 0 && intPoint === l - 1 ) {\n\n\t\t\tintPoint = l - 2;\n\t\t\tweight = 1;\n\n\t\t}\n\n\t\tlet p0, p3; // 4 points (p1 & p2 defined below)\n\n\t\tif ( this.closed || intPoint > 0 ) {\n\n\t\t\tp0 = points[ ( intPoint - 1 ) % l ];\n\n\t\t} else {\n\n\t\t\t// extrapolate first point\n\t\t\ttmp.subVectors( points[ 0 ], points[ 1 ] ).add( points[ 0 ] );\n\t\t\tp0 = tmp;\n\n\t\t}\n\n\t\tconst p1 = points[ intPoint % l ];\n\t\tconst p2 = points[ ( intPoint + 1 ) % l ];\n\n\t\tif ( this.closed || intPoint + 2 < l ) {\n\n\t\t\tp3 = points[ ( intPoint + 2 ) % l ];\n\n\t\t} else {\n\n\t\t\t// extrapolate last point\n\t\t\ttmp.subVectors( points[ l - 1 ], points[ l - 2 ] ).add( points[ l - 1 ] );\n\t\t\tp3 = tmp;\n\n\t\t}\n\n\t\tif ( this.curveType === 'centripetal' || this.curveType === 'chordal' ) {\n\n\t\t\t// init Centripetal / Chordal Catmull-Rom\n\t\t\tconst pow = this.curveType === 'chordal' ? 0.5 : 0.25;\n\t\t\tlet dt0 = Math.pow( p0.distanceToSquared( p1 ), pow );\n\t\t\tlet dt1 = Math.pow( p1.distanceToSquared( p2 ), pow );\n\t\t\tlet dt2 = Math.pow( p2.distanceToSquared( p3 ), pow );\n\n\t\t\t// safety check for repeated points\n\t\t\tif ( dt1 < 1e-4 ) dt1 = 1.0;\n\t\t\tif ( dt0 < 1e-4 ) dt0 = dt1;\n\t\t\tif ( dt2 < 1e-4 ) dt2 = dt1;\n\n\t\t\tpx.initNonuniformCatmullRom( p0.x, p1.x, p2.x, p3.x, dt0, dt1, dt2 );\n\t\t\tpy.initNonuniformCatmullRom( p0.y, p1.y, p2.y, p3.y, dt0, dt1, dt2 );\n\t\t\tpz.initNonuniformCatmullRom( p0.z, p1.z, p2.z, p3.z, dt0, dt1, dt2 );\n\n\t\t} else if ( this.curveType === 'catmullrom' ) {\n\n\t\t\tpx.initCatmullRom( p0.x, p1.x, p2.x, p3.x, this.tension );\n\t\t\tpy.initCatmullRom( p0.y, p1.y, p2.y, p3.y, this.tension );\n\t\t\tpz.initCatmullRom( p0.z, p1.z, p2.z, p3.z, this.tension );\n\n\t\t}\n\n\t\tpoint.set(\n\t\t\tpx.calc( weight ),\n\t\t\tpy.calc( weight ),\n\t\t\tpz.calc( weight )\n\t\t);\n\n\t\treturn point;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.points = [];\n\n\t\tfor ( let i = 0, l = source.points.length; i < l; i ++ ) {\n\n\t\t\tconst point = source.points[ i ];\n\n\t\t\tthis.points.push( point.clone() );\n\n\t\t}\n\n\t\tthis.closed = source.closed;\n\t\tthis.curveType = source.curveType;\n\t\tthis.tension = source.tension;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.points = [];\n\n\t\tfor ( let i = 0, l = this.points.length; i < l; i ++ ) {\n\n\t\t\tconst point = this.points[ i ];\n\t\t\tdata.points.push( point.toArray() );\n\n\t\t}\n\n\t\tdata.closed = this.closed;\n\t\tdata.curveType = this.curveType;\n\t\tdata.tension = this.tension;\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.points = [];\n\n\t\tfor ( let i = 0, l = json.points.length; i < l; i ++ ) {\n\n\t\t\tconst point = json.points[ i ];\n\t\t\tthis.points.push( new Vector3().fromArray( point ) );\n\n\t\t}\n\n\t\tthis.closed = json.closed;\n\t\tthis.curveType = json.curveType;\n\t\tthis.tension = json.tension;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * Interpolations contains spline and Bézier functions internally used by concrete curve classes.\n *\n * Bezier Curves formulas obtained from: https://en.wikipedia.org/wiki/B%C3%A9zier_curve\n *\n * @module Interpolations\n */\n\n/**\n * Computes a point on a Catmull-Rom spline.\n *\n * @param {number} t - The interpolation factor.\n * @param {number} p0 - The first control point.\n * @param {number} p1 - The second control point.\n * @param {number} p2 - The third control point.\n * @param {number} p3 - The fourth control point.\n * @return {number} The calculated point on a Catmull-Rom spline.\n */\nfunction CatmullRom( t, p0, p1, p2, p3 ) {\n\n\tconst v0 = ( p2 - p0 ) * 0.5;\n\tconst v1 = ( p3 - p1 ) * 0.5;\n\tconst t2 = t * t;\n\tconst t3 = t * t2;\n\treturn ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3 + ( -3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2 + v0 * t + p1;\n\n}\n\n//\n\nfunction QuadraticBezierP0( t, p ) {\n\n\tconst k = 1 - t;\n\treturn k * k * p;\n\n}\n\nfunction QuadraticBezierP1( t, p ) {\n\n\treturn 2 * ( 1 - t ) * t * p;\n\n}\n\nfunction QuadraticBezierP2( t, p ) {\n\n\treturn t * t * p;\n\n}\n\n/**\n * Computes a point on a Quadratic Bezier curve.\n *\n * @param {number} t - The interpolation factor.\n * @param {number} p0 - The first control point.\n * @param {number} p1 - The second control point.\n * @param {number} p2 - The third control point.\n * @return {number} The calculated point on a Quadratic Bezier curve.\n */\nfunction QuadraticBezier( t, p0, p1, p2 ) {\n\n\treturn QuadraticBezierP0( t, p0 ) + QuadraticBezierP1( t, p1 ) +\n\t\tQuadraticBezierP2( t, p2 );\n\n}\n\n//\n\nfunction CubicBezierP0( t, p ) {\n\n\tconst k = 1 - t;\n\treturn k * k * k * p;\n\n}\n\nfunction CubicBezierP1( t, p ) {\n\n\tconst k = 1 - t;\n\treturn 3 * k * k * t * p;\n\n}\n\nfunction CubicBezierP2( t, p ) {\n\n\treturn 3 * ( 1 - t ) * t * t * p;\n\n}\n\nfunction CubicBezierP3( t, p ) {\n\n\treturn t * t * t * p;\n\n}\n\n/**\n * Computes a point on a Cubic Bezier curve.\n *\n * @param {number} t - The interpolation factor.\n * @param {number} p0 - The first control point.\n * @param {number} p1 - The second control point.\n * @param {number} p2 - The third control point.\n * @param {number} p3 - The fourth control point.\n * @return {number} The calculated point on a Cubic Bezier curve.\n */\nfunction CubicBezier( t, p0, p1, p2, p3 ) {\n\n\treturn CubicBezierP0( t, p0 ) + CubicBezierP1( t, p1 ) + CubicBezierP2( t, p2 ) +\n\t\tCubicBezierP3( t, p3 );\n\n}\n\n/**\n * A curve representing a 2D Cubic Bezier curve.\n *\n * ```js\n * const curve = new THREE.CubicBezierCurve(\n * \tnew THREE.Vector2( - 0, 0 ),\n * \tnew THREE.Vector2( - 5, 15 ),\n * \tnew THREE.Vector2( 20, 15 ),\n * \tnew THREE.Vector2( 10, 0 )\n * );\n *\n * const points = curve.getPoints( 50 );\n * const geometry = new THREE.BufferGeometry().setFromPoints( points );\n *\n * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );\n *\n * // Create the final object to add to the scene\n * const curveObject = new THREE.Line( geometry, material );\n * ```\n *\n * @augments Curve\n */\nclass CubicBezierCurve extends Curve {\n\n\t/**\n\t * Constructs a new Cubic Bezier curve.\n\t *\n\t * @param {Vector2} [v0] - The start point.\n\t * @param {Vector2} [v1] - The first control point.\n\t * @param {Vector2} [v2] - The second control point.\n\t * @param {Vector2} [v3] - The end point.\n\t */\n\tconstructor( v0 = new Vector2(), v1 = new Vector2(), v2 = new Vector2(), v3 = new Vector2() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCubicBezierCurve = true;\n\n\t\tthis.type = 'CubicBezierCurve';\n\n\t\t/**\n\t\t * The start point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v0 = v0;\n\n\t\t/**\n\t\t * The first control point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v1 = v1;\n\n\t\t/**\n\t\t * The second control point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v2 = v2;\n\n\t\t/**\n\t\t * The end point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v3 = v3;\n\n\t}\n\n\t/**\n\t * Returns a point on the curve.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {Vector2} The position on the curve.\n\t */\n\tgetPoint( t, optionalTarget = new Vector2() ) {\n\n\t\tconst point = optionalTarget;\n\n\t\tconst v0 = this.v0, v1 = this.v1, v2 = this.v2, v3 = this.v3;\n\n\t\tpoint.set(\n\t\t\tCubicBezier( t, v0.x, v1.x, v2.x, v3.x ),\n\t\t\tCubicBezier( t, v0.y, v1.y, v2.y, v3.y )\n\t\t);\n\n\t\treturn point;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.v0.copy( source.v0 );\n\t\tthis.v1.copy( source.v1 );\n\t\tthis.v2.copy( source.v2 );\n\t\tthis.v3.copy( source.v3 );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.v0 = this.v0.toArray();\n\t\tdata.v1 = this.v1.toArray();\n\t\tdata.v2 = this.v2.toArray();\n\t\tdata.v3 = this.v3.toArray();\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.v0.fromArray( json.v0 );\n\t\tthis.v1.fromArray( json.v1 );\n\t\tthis.v2.fromArray( json.v2 );\n\t\tthis.v3.fromArray( json.v3 );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A curve representing a 3D Cubic Bezier curve.\n *\n * @augments Curve\n */\nclass CubicBezierCurve3 extends Curve {\n\n\t/**\n\t * Constructs a new Cubic Bezier curve.\n\t *\n\t * @param {Vector3} [v0] - The start point.\n\t * @param {Vector3} [v1] - The first control point.\n\t * @param {Vector3} [v2] - The second control point.\n\t * @param {Vector3} [v3] - The end point.\n\t */\n\tconstructor( v0 = new Vector3(), v1 = new Vector3(), v2 = new Vector3(), v3 = new Vector3() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCubicBezierCurve3 = true;\n\n\t\tthis.type = 'CubicBezierCurve3';\n\n\t\t/**\n\t\t * The start point.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.v0 = v0;\n\n\t\t/**\n\t\t * The first control point.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.v1 = v1;\n\n\t\t/**\n\t\t * The second control point.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.v2 = v2;\n\n\t\t/**\n\t\t * The end point.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.v3 = v3;\n\n\t}\n\n\t/**\n\t * Returns a point on the curve.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {Vector3} The position on the curve.\n\t */\n\tgetPoint( t, optionalTarget = new Vector3() ) {\n\n\t\tconst point = optionalTarget;\n\n\t\tconst v0 = this.v0, v1 = this.v1, v2 = this.v2, v3 = this.v3;\n\n\t\tpoint.set(\n\t\t\tCubicBezier( t, v0.x, v1.x, v2.x, v3.x ),\n\t\t\tCubicBezier( t, v0.y, v1.y, v2.y, v3.y ),\n\t\t\tCubicBezier( t, v0.z, v1.z, v2.z, v3.z )\n\t\t);\n\n\t\treturn point;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.v0.copy( source.v0 );\n\t\tthis.v1.copy( source.v1 );\n\t\tthis.v2.copy( source.v2 );\n\t\tthis.v3.copy( source.v3 );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.v0 = this.v0.toArray();\n\t\tdata.v1 = this.v1.toArray();\n\t\tdata.v2 = this.v2.toArray();\n\t\tdata.v3 = this.v3.toArray();\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.v0.fromArray( json.v0 );\n\t\tthis.v1.fromArray( json.v1 );\n\t\tthis.v2.fromArray( json.v2 );\n\t\tthis.v3.fromArray( json.v3 );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A curve representing a 2D line segment.\n *\n * @augments Curve\n */\nclass LineCurve extends Curve {\n\n\t/**\n\t * Constructs a new line curve.\n\t *\n\t * @param {Vector2} [v1] - The start point.\n\t * @param {Vector2} [v2] - The end point.\n\t */\n\tconstructor( v1 = new Vector2(), v2 = new Vector2() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLineCurve = true;\n\n\t\tthis.type = 'LineCurve';\n\n\t\t/**\n\t\t * The start point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v1 = v1;\n\n\t\t/**\n\t\t * The end point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v2 = v2;\n\n\t}\n\n\t/**\n\t * Returns a point on the line.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the line. Must be in the range `[0,1]`.\n\t * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {Vector2} The position on the line.\n\t */\n\tgetPoint( t, optionalTarget = new Vector2() ) {\n\n\t\tconst point = optionalTarget;\n\n\t\tif ( t === 1 ) {\n\n\t\t\tpoint.copy( this.v2 );\n\n\t\t} else {\n\n\t\t\tpoint.copy( this.v2 ).sub( this.v1 );\n\t\t\tpoint.multiplyScalar( t ).add( this.v1 );\n\n\t\t}\n\n\t\treturn point;\n\n\t}\n\n\t// Line curve is linear, so we can overwrite default getPointAt\n\tgetPointAt( u, optionalTarget ) {\n\n\t\treturn this.getPoint( u, optionalTarget );\n\n\t}\n\n\tgetTangent( t, optionalTarget = new Vector2() ) {\n\n\t\treturn optionalTarget.subVectors( this.v2, this.v1 ).normalize();\n\n\t}\n\n\tgetTangentAt( u, optionalTarget ) {\n\n\t\treturn this.getTangent( u, optionalTarget );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.v1.copy( source.v1 );\n\t\tthis.v2.copy( source.v2 );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.v1 = this.v1.toArray();\n\t\tdata.v2 = this.v2.toArray();\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.v1.fromArray( json.v1 );\n\t\tthis.v2.fromArray( json.v2 );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A curve representing a 3D line segment.\n *\n * @augments Curve\n */\nclass LineCurve3 extends Curve {\n\n\t/**\n\t * Constructs a new line curve.\n\t *\n\t * @param {Vector3} [v1] - The start point.\n\t * @param {Vector3} [v2] - The end point.\n\t */\n\tconstructor( v1 = new Vector3(), v2 = new Vector3() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLineCurve3 = true;\n\n\t\tthis.type = 'LineCurve3';\n\n\t\t/**\n\t\t * The start point.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.v1 = v1;\n\n\t\t/**\n\t\t * The end point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v2 = v2;\n\n\t}\n\n\t/**\n\t * Returns a point on the line.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the line. Must be in the range `[0,1]`.\n\t * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {Vector3} The position on the line.\n\t */\n\tgetPoint( t, optionalTarget = new Vector3() ) {\n\n\t\tconst point = optionalTarget;\n\n\t\tif ( t === 1 ) {\n\n\t\t\tpoint.copy( this.v2 );\n\n\t\t} else {\n\n\t\t\tpoint.copy( this.v2 ).sub( this.v1 );\n\t\t\tpoint.multiplyScalar( t ).add( this.v1 );\n\n\t\t}\n\n\t\treturn point;\n\n\t}\n\n\t// Line curve is linear, so we can overwrite default getPointAt\n\tgetPointAt( u, optionalTarget ) {\n\n\t\treturn this.getPoint( u, optionalTarget );\n\n\t}\n\n\tgetTangent( t, optionalTarget = new Vector3() ) {\n\n\t\treturn optionalTarget.subVectors( this.v2, this.v1 ).normalize();\n\n\t}\n\n\tgetTangentAt( u, optionalTarget ) {\n\n\t\treturn this.getTangent( u, optionalTarget );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.v1.copy( source.v1 );\n\t\tthis.v2.copy( source.v2 );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.v1 = this.v1.toArray();\n\t\tdata.v2 = this.v2.toArray();\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.v1.fromArray( json.v1 );\n\t\tthis.v2.fromArray( json.v2 );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A curve representing a 2D Quadratic Bezier curve.\n *\n * ```js\n * const curve = new THREE.QuadraticBezierCurve(\n * \tnew THREE.Vector2( - 10, 0 ),\n * \tnew THREE.Vector2( 20, 15 ),\n * \tnew THREE.Vector2( 10, 0 )\n * )\n *\n * const points = curve.getPoints( 50 );\n * const geometry = new THREE.BufferGeometry().setFromPoints( points );\n *\n * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );\n *\n * // Create the final object to add to the scene\n * const curveObject = new THREE.Line( geometry, material );\n * ```\n *\n * @augments Curve\n */\nclass QuadraticBezierCurve extends Curve {\n\n\t/**\n\t * Constructs a new Quadratic Bezier curve.\n\t *\n\t * @param {Vector2} [v0] - The start point.\n\t * @param {Vector2} [v1] - The control point.\n\t * @param {Vector2} [v2] - The end point.\n\t */\n\tconstructor( v0 = new Vector2(), v1 = new Vector2(), v2 = new Vector2() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isQuadraticBezierCurve = true;\n\n\t\tthis.type = 'QuadraticBezierCurve';\n\n\t\t/**\n\t\t * The start point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v0 = v0;\n\n\t\t/**\n\t\t * The control point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v1 = v1;\n\n\t\t/**\n\t\t * The end point.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.v2 = v2;\n\n\t}\n\n\t/**\n\t * Returns a point on the curve.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {Vector2} The position on the curve.\n\t */\n\tgetPoint( t, optionalTarget = new Vector2() ) {\n\n\t\tconst point = optionalTarget;\n\n\t\tconst v0 = this.v0, v1 = this.v1, v2 = this.v2;\n\n\t\tpoint.set(\n\t\t\tQuadraticBezier( t, v0.x, v1.x, v2.x ),\n\t\t\tQuadraticBezier( t, v0.y, v1.y, v2.y )\n\t\t);\n\n\t\treturn point;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.v0.copy( source.v0 );\n\t\tthis.v1.copy( source.v1 );\n\t\tthis.v2.copy( source.v2 );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.v0 = this.v0.toArray();\n\t\tdata.v1 = this.v1.toArray();\n\t\tdata.v2 = this.v2.toArray();\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.v0.fromArray( json.v0 );\n\t\tthis.v1.fromArray( json.v1 );\n\t\tthis.v2.fromArray( json.v2 );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A curve representing a 3D Quadratic Bezier curve.\n *\n * @augments Curve\n */\nclass QuadraticBezierCurve3 extends Curve {\n\n\t/**\n\t * Constructs a new Quadratic Bezier curve.\n\t *\n\t * @param {Vector3} [v0] - The start point.\n\t * @param {Vector3} [v1] - The control point.\n\t * @param {Vector3} [v2] - The end point.\n\t */\n\tconstructor( v0 = new Vector3(), v1 = new Vector3(), v2 = new Vector3() ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isQuadraticBezierCurve3 = true;\n\n\t\tthis.type = 'QuadraticBezierCurve3';\n\n\t\t/**\n\t\t * The start point.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.v0 = v0;\n\n\t\t/**\n\t\t * The control point.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.v1 = v1;\n\n\t\t/**\n\t\t * The end point.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.v2 = v2;\n\n\t}\n\n\t/**\n\t * Returns a point on the curve.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {Vector3} The position on the curve.\n\t */\n\tgetPoint( t, optionalTarget = new Vector3() ) {\n\n\t\tconst point = optionalTarget;\n\n\t\tconst v0 = this.v0, v1 = this.v1, v2 = this.v2;\n\n\t\tpoint.set(\n\t\t\tQuadraticBezier( t, v0.x, v1.x, v2.x ),\n\t\t\tQuadraticBezier( t, v0.y, v1.y, v2.y ),\n\t\t\tQuadraticBezier( t, v0.z, v1.z, v2.z )\n\t\t);\n\n\t\treturn point;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.v0.copy( source.v0 );\n\t\tthis.v1.copy( source.v1 );\n\t\tthis.v2.copy( source.v2 );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.v0 = this.v0.toArray();\n\t\tdata.v1 = this.v1.toArray();\n\t\tdata.v2 = this.v2.toArray();\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.v0.fromArray( json.v0 );\n\t\tthis.v1.fromArray( json.v1 );\n\t\tthis.v2.fromArray( json.v2 );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A curve representing a 2D spline curve.\n *\n * ```js\n * // Create a sine-like wave\n * const curve = new THREE.SplineCurve( [\n * \tnew THREE.Vector2( -10, 0 ),\n * \tnew THREE.Vector2( -5, 5 ),\n * \tnew THREE.Vector2( 0, 0 ),\n * \tnew THREE.Vector2( 5, -5 ),\n * \tnew THREE.Vector2( 10, 0 )\n * ] );\n *\n * const points = curve.getPoints( 50 );\n * const geometry = new THREE.BufferGeometry().setFromPoints( points );\n *\n * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );\n *\n * // Create the final object to add to the scene\n * const splineObject = new THREE.Line( geometry, material );\n * ```\n *\n * @augments Curve\n */\nclass SplineCurve extends Curve {\n\n\t/**\n\t * Constructs a new 2D spline curve.\n\t *\n\t * @param {Array<Vector2>} [points] -  An array of 2D points defining the curve.\n\t */\n\tconstructor( points = [] ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSplineCurve = true;\n\n\t\tthis.type = 'SplineCurve';\n\n\t\t/**\n\t\t * An array of 2D points defining the curve.\n\t\t *\n\t\t * @type {Array<Vector2>}\n\t\t */\n\t\tthis.points = points;\n\n\t}\n\n\t/**\n\t * Returns a point on the curve.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {Vector2} The position on the curve.\n\t */\n\tgetPoint( t, optionalTarget = new Vector2() ) {\n\n\t\tconst point = optionalTarget;\n\n\t\tconst points = this.points;\n\t\tconst p = ( points.length - 1 ) * t;\n\n\t\tconst intPoint = Math.floor( p );\n\t\tconst weight = p - intPoint;\n\n\t\tconst p0 = points[ intPoint === 0 ? intPoint : intPoint - 1 ];\n\t\tconst p1 = points[ intPoint ];\n\t\tconst p2 = points[ intPoint > points.length - 2 ? points.length - 1 : intPoint + 1 ];\n\t\tconst p3 = points[ intPoint > points.length - 3 ? points.length - 1 : intPoint + 2 ];\n\n\t\tpoint.set(\n\t\t\tCatmullRom( weight, p0.x, p1.x, p2.x, p3.x ),\n\t\t\tCatmullRom( weight, p0.y, p1.y, p2.y, p3.y )\n\t\t);\n\n\t\treturn point;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.points = [];\n\n\t\tfor ( let i = 0, l = source.points.length; i < l; i ++ ) {\n\n\t\t\tconst point = source.points[ i ];\n\n\t\t\tthis.points.push( point.clone() );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.points = [];\n\n\t\tfor ( let i = 0, l = this.points.length; i < l; i ++ ) {\n\n\t\t\tconst point = this.points[ i ];\n\t\t\tdata.points.push( point.toArray() );\n\n\t\t}\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.points = [];\n\n\t\tfor ( let i = 0, l = json.points.length; i < l; i ++ ) {\n\n\t\t\tconst point = json.points[ i ];\n\t\t\tthis.points.push( new Vector2().fromArray( point ) );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n}\n\nvar Curves = /*#__PURE__*/Object.freeze({\n\t__proto__: null,\n\tArcCurve: ArcCurve,\n\tCatmullRomCurve3: CatmullRomCurve3,\n\tCubicBezierCurve: CubicBezierCurve,\n\tCubicBezierCurve3: CubicBezierCurve3,\n\tEllipseCurve: EllipseCurve,\n\tLineCurve: LineCurve,\n\tLineCurve3: LineCurve3,\n\tQuadraticBezierCurve: QuadraticBezierCurve,\n\tQuadraticBezierCurve3: QuadraticBezierCurve3,\n\tSplineCurve: SplineCurve\n});\n\n/**\n * A base class extending {@link Curve}. `CurvePath` is simply an\n * array of connected curves, but retains the API of a curve.\n *\n * @augments Curve\n */\nclass CurvePath extends Curve {\n\n\t/**\n\t * Constructs a new curve path.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\tthis.type = 'CurvePath';\n\n\t\t/**\n\t\t * An array of curves defining the\n\t\t * path.\n\t\t *\n\t\t * @type {Array<Curve>}\n\t\t */\n\t\tthis.curves = [];\n\n\t\t/**\n\t\t * Whether the path should automatically be closed\n\t\t * by a line curve.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.autoClose = false;\n\n\t}\n\n\t/**\n\t * Adds a curve to this curve path.\n\t *\n\t * @param {Curve} curve - The curve to add.\n\t */\n\tadd( curve ) {\n\n\t\tthis.curves.push( curve );\n\n\t}\n\n\t/**\n\t * Adds a line curve to close the path.\n\t *\n\t * @return {CurvePath} A reference to this curve path.\n\t */\n\tclosePath() {\n\n\t\t// Add a line curve if start and end of lines are not connected\n\t\tconst startPoint = this.curves[ 0 ].getPoint( 0 );\n\t\tconst endPoint = this.curves[ this.curves.length - 1 ].getPoint( 1 );\n\n\t\tif ( ! startPoint.equals( endPoint ) ) {\n\n\t\t\tconst lineType = ( startPoint.isVector2 === true ) ? 'LineCurve' : 'LineCurve3';\n\t\t\tthis.curves.push( new Curves[ lineType ]( endPoint, startPoint ) );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * This method returns a vector in 2D or 3D space (depending on the curve definitions)\n\t * for the given interpolation factor.\n\t *\n\t * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.\n\t * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.\n\t * @return {?(Vector2|Vector3)} The position on the curve. It can be a 2D or 3D vector depending on the curve definition.\n\t */\n\tgetPoint( t, optionalTarget ) {\n\n\t\t// To get accurate point with reference to\n\t\t// entire path distance at time t,\n\t\t// following has to be done:\n\n\t\t// 1. Length of each sub path have to be known\n\t\t// 2. Locate and identify type of curve\n\t\t// 3. Get t for the curve\n\t\t// 4. Return curve.getPointAt(t')\n\n\t\tconst d = t * this.getLength();\n\t\tconst curveLengths = this.getCurveLengths();\n\t\tlet i = 0;\n\n\t\t// To think about boundaries points.\n\n\t\twhile ( i < curveLengths.length ) {\n\n\t\t\tif ( curveLengths[ i ] >= d ) {\n\n\t\t\t\tconst diff = curveLengths[ i ] - d;\n\t\t\t\tconst curve = this.curves[ i ];\n\n\t\t\t\tconst segmentLength = curve.getLength();\n\t\t\t\tconst u = segmentLength === 0 ? 0 : 1 - diff / segmentLength;\n\n\t\t\t\treturn curve.getPointAt( u, optionalTarget );\n\n\t\t\t}\n\n\t\t\ti ++;\n\n\t\t}\n\n\t\treturn null;\n\n\t\t// loop where sum != 0, sum > d , sum+1 <d\n\n\t}\n\n\tgetLength() {\n\n\t\t// We cannot use the default THREE.Curve getPoint() with getLength() because in\n\t\t// THREE.Curve, getLength() depends on getPoint() but in THREE.CurvePath\n\t\t// getPoint() depends on getLength\n\n\t\tconst lens = this.getCurveLengths();\n\t\treturn lens[ lens.length - 1 ];\n\n\t}\n\n\tupdateArcLengths() {\n\n\t\t// cacheLengths must be recalculated.\n\n\t\tthis.needsUpdate = true;\n\t\tthis.cacheLengths = null;\n\t\tthis.getCurveLengths();\n\n\t}\n\n\t/**\n\t * Returns list of cumulative curve lengths of the defined curves.\n\t *\n\t * @return {Array<number>} The curve lengths.\n\t */\n\tgetCurveLengths() {\n\n\t\t// Compute lengths and cache them\n\t\t// We cannot overwrite getLengths() because UtoT mapping uses it.\n\t\t// We use cache values if curves and cache array are same length\n\n\t\tif ( this.cacheLengths && this.cacheLengths.length === this.curves.length ) {\n\n\t\t\treturn this.cacheLengths;\n\n\t\t}\n\n\t\t// Get length of sub-curve\n\t\t// Push sums into cached array\n\n\t\tconst lengths = [];\n\t\tlet sums = 0;\n\n\t\tfor ( let i = 0, l = this.curves.length; i < l; i ++ ) {\n\n\t\t\tsums += this.curves[ i ].getLength();\n\t\t\tlengths.push( sums );\n\n\t\t}\n\n\t\tthis.cacheLengths = lengths;\n\n\t\treturn lengths;\n\n\t}\n\n\tgetSpacedPoints( divisions = 40 ) {\n\n\t\tconst points = [];\n\n\t\tfor ( let i = 0; i <= divisions; i ++ ) {\n\n\t\t\tpoints.push( this.getPoint( i / divisions ) );\n\n\t\t}\n\n\t\tif ( this.autoClose ) {\n\n\t\t\tpoints.push( points[ 0 ] );\n\n\t\t}\n\n\t\treturn points;\n\n\t}\n\n\tgetPoints( divisions = 12 ) {\n\n\t\tconst points = [];\n\t\tlet last;\n\n\t\tfor ( let i = 0, curves = this.curves; i < curves.length; i ++ ) {\n\n\t\t\tconst curve = curves[ i ];\n\t\t\tconst resolution = curve.isEllipseCurve ? divisions * 2\n\t\t\t\t: ( curve.isLineCurve || curve.isLineCurve3 ) ? 1\n\t\t\t\t\t: curve.isSplineCurve ? divisions * curve.points.length\n\t\t\t\t\t\t: divisions;\n\n\t\t\tconst pts = curve.getPoints( resolution );\n\n\t\t\tfor ( let j = 0; j < pts.length; j ++ ) {\n\n\t\t\t\tconst point = pts[ j ];\n\n\t\t\t\tif ( last && last.equals( point ) ) continue; // ensures no consecutive points are duplicates\n\n\t\t\t\tpoints.push( point );\n\t\t\t\tlast = point;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( this.autoClose && points.length > 1 && ! points[ points.length - 1 ].equals( points[ 0 ] ) ) {\n\n\t\t\tpoints.push( points[ 0 ] );\n\n\t\t}\n\n\t\treturn points;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.curves = [];\n\n\t\tfor ( let i = 0, l = source.curves.length; i < l; i ++ ) {\n\n\t\t\tconst curve = source.curves[ i ];\n\n\t\t\tthis.curves.push( curve.clone() );\n\n\t\t}\n\n\t\tthis.autoClose = source.autoClose;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.autoClose = this.autoClose;\n\t\tdata.curves = [];\n\n\t\tfor ( let i = 0, l = this.curves.length; i < l; i ++ ) {\n\n\t\t\tconst curve = this.curves[ i ];\n\t\t\tdata.curves.push( curve.toJSON() );\n\n\t\t}\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.autoClose = json.autoClose;\n\t\tthis.curves = [];\n\n\t\tfor ( let i = 0, l = json.curves.length; i < l; i ++ ) {\n\n\t\t\tconst curve = json.curves[ i ];\n\t\t\tthis.curves.push( new Curves[ curve.type ]().fromJSON( curve ) );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A 2D path representation. The class provides methods for creating paths\n * and contours of 2D shapes similar to the 2D Canvas API.\n *\n * ```js\n * const path = new THREE.Path();\n *\n * path.lineTo( 0, 0.8 );\n * path.quadraticCurveTo( 0, 1, 0.2, 1 );\n * path.lineTo( 1, 1 );\n *\n * const points = path.getPoints();\n *\n * const geometry = new THREE.BufferGeometry().setFromPoints( points );\n * const material = new THREE.LineBasicMaterial( { color: 0xffffff } );\n *\n * const line = new THREE.Line( geometry, material );\n * scene.add( line );\n * ```\n *\n * @augments CurvePath\n */\nclass Path extends CurvePath {\n\n\t/**\n\t * Constructs a new path.\n\t *\n\t * @param {Array<Vector2>} [points] - An array of 2D points defining the path.\n\t */\n\tconstructor( points ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'Path';\n\n\t\t/**\n\t\t * The current offset of the path. Any new curve added will start here.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.currentPoint = new Vector2();\n\n\t\tif ( points ) {\n\n\t\t\tthis.setFromPoints( points );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Creates a path from the given list of points. The points are added\n\t * to the path as instances of {@link LineCurve}.\n\t *\n\t * @param {Array<Vector2>} points - An array of 2D points.\n\t * @return {Path} A reference to this path.\n\t */\n\tsetFromPoints( points ) {\n\n\t\tthis.moveTo( points[ 0 ].x, points[ 0 ].y );\n\n\t\tfor ( let i = 1, l = points.length; i < l; i ++ ) {\n\n\t\t\tthis.lineTo( points[ i ].x, points[ i ].y );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Moves {@link Path#currentPoint} to the given point.\n\t *\n\t * @param {number} x - The x coordinate.\n\t * @param {number} y - The y coordinate.\n\t * @return {Path} A reference to this path.\n\t */\n\tmoveTo( x, y ) {\n\n\t\tthis.currentPoint.set( x, y ); // TODO consider referencing vectors instead of copying?\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an instance of {@link LineCurve} to the path by connecting\n\t * the current point with the given one.\n\t *\n\t * @param {number} x - The x coordinate of the end point.\n\t * @param {number} y - The y coordinate of the end point.\n\t * @return {Path} A reference to this path.\n\t */\n\tlineTo( x, y ) {\n\n\t\tconst curve = new LineCurve( this.currentPoint.clone(), new Vector2( x, y ) );\n\t\tthis.curves.push( curve );\n\n\t\tthis.currentPoint.set( x, y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an instance of {@link QuadraticBezierCurve} to the path by connecting\n\t * the current point with the given one.\n\t *\n\t * @param {number} aCPx - The x coordinate of the control point.\n\t * @param {number} aCPy - The y coordinate of the control point.\n\t * @param {number} aX - The x coordinate of the end point.\n\t * @param {number} aY - The y coordinate of the end point.\n\t * @return {Path} A reference to this path.\n\t */\n\tquadraticCurveTo( aCPx, aCPy, aX, aY ) {\n\n\t\tconst curve = new QuadraticBezierCurve(\n\t\t\tthis.currentPoint.clone(),\n\t\t\tnew Vector2( aCPx, aCPy ),\n\t\t\tnew Vector2( aX, aY )\n\t\t);\n\n\t\tthis.curves.push( curve );\n\n\t\tthis.currentPoint.set( aX, aY );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an instance of {@link CubicBezierCurve} to the path by connecting\n\t * the current point with the given one.\n\t *\n\t * @param {number} aCP1x - The x coordinate of the first control point.\n\t * @param {number} aCP1y - The y coordinate of the first control point.\n\t * @param {number} aCP2x - The x coordinate of the second control point.\n\t * @param {number} aCP2y - The y coordinate of the second control point.\n\t * @param {number} aX - The x coordinate of the end point.\n\t * @param {number} aY - The y coordinate of the end point.\n\t * @return {Path} A reference to this path.\n\t */\n\tbezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY ) {\n\n\t\tconst curve = new CubicBezierCurve(\n\t\t\tthis.currentPoint.clone(),\n\t\t\tnew Vector2( aCP1x, aCP1y ),\n\t\t\tnew Vector2( aCP2x, aCP2y ),\n\t\t\tnew Vector2( aX, aY )\n\t\t);\n\n\t\tthis.curves.push( curve );\n\n\t\tthis.currentPoint.set( aX, aY );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an instance of {@link SplineCurve} to the path by connecting\n\t * the current point with the given list of points.\n\t *\n\t * @param {Array<Vector2>} pts - An array of points in 2D space.\n\t * @return {Path} A reference to this path.\n\t */\n\tsplineThru( pts ) {\n\n\t\tconst npts = [ this.currentPoint.clone() ].concat( pts );\n\n\t\tconst curve = new SplineCurve( npts );\n\t\tthis.curves.push( curve );\n\n\t\tthis.currentPoint.copy( pts[ pts.length - 1 ] );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an arc as an instance of {@link EllipseCurve} to the path, positioned relative\n\t * to the current point.\n\t *\n\t * @param {number} [aX=0] - The x coordinate of the center of the arc offsetted from the previous curve.\n\t * @param {number} [aY=0] - The y coordinate of the center of the arc offsetted from the previous curve.\n\t * @param {number} [aRadius=1] - The radius of the arc.\n\t * @param {number} [aStartAngle=0] - The start angle in radians.\n\t * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.\n\t * @param {boolean} [aClockwise=false] - Whether to sweep the arc clockwise or not.\n\t * @return {Path} A reference to this path.\n\t */\n\tarc( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {\n\n\t\tconst x0 = this.currentPoint.x;\n\t\tconst y0 = this.currentPoint.y;\n\n\t\tthis.absarc( aX + x0, aY + y0, aRadius,\n\t\t\taStartAngle, aEndAngle, aClockwise );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an absolutely positioned arc as an instance of {@link EllipseCurve} to the path.\n\t *\n\t * @param {number} [aX=0] - The x coordinate of the center of the arc.\n\t * @param {number} [aY=0] - The y coordinate of the center of the arc.\n\t * @param {number} [aRadius=1] - The radius of the arc.\n\t * @param {number} [aStartAngle=0] - The start angle in radians.\n\t * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.\n\t * @param {boolean} [aClockwise=false] - Whether to sweep the arc clockwise or not.\n\t * @return {Path} A reference to this path.\n\t */\n\tabsarc( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {\n\n\t\tthis.absellipse( aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an ellipse as an instance of {@link EllipseCurve} to the path, positioned relative\n\t * to the current point\n\t *\n\t * @param {number} [aX=0] - The x coordinate of the center of the ellipse offsetted from the previous curve.\n\t * @param {number} [aY=0] - The y coordinate of the center of the ellipse offsetted from the previous curve.\n\t * @param {number} [xRadius=1] - The radius of the ellipse in the x axis.\n\t * @param {number} [yRadius=1] - The radius of the ellipse in the y axis.\n\t * @param {number} [aStartAngle=0] - The start angle in radians.\n\t * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.\n\t * @param {boolean} [aClockwise=false] - Whether to sweep the ellipse clockwise or not.\n\t * @param {number} [aRotation=0] - The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.\n\t * @return {Path} A reference to this path.\n\t */\n\tellipse( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation ) {\n\n\t\tconst x0 = this.currentPoint.x;\n\t\tconst y0 = this.currentPoint.y;\n\n\t\tthis.absellipse( aX + x0, aY + y0, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an absolutely positioned ellipse as an instance of {@link EllipseCurve} to the path.\n\t *\n\t * @param {number} [aX=0] - The x coordinate of the absolute center of the ellipse.\n\t * @param {number} [aY=0] - The y coordinate of the absolute center of the ellipse.\n\t * @param {number} [xRadius=1] - The radius of the ellipse in the x axis.\n\t * @param {number} [yRadius=1] - The radius of the ellipse in the y axis.\n\t * @param {number} [aStartAngle=0] - The start angle in radians.\n\t * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.\n\t * @param {boolean} [aClockwise=false] - Whether to sweep the ellipse clockwise or not.\n\t * @param {number} [aRotation=0] - The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.\n\t * @return {Path} A reference to this path.\n\t */\n\tabsellipse( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation ) {\n\n\t\tconst curve = new EllipseCurve( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation );\n\n\t\tif ( this.curves.length > 0 ) {\n\n\t\t\t// if a previous curve is present, attempt to join\n\t\t\tconst firstPoint = curve.getPoint( 0 );\n\n\t\t\tif ( ! firstPoint.equals( this.currentPoint ) ) {\n\n\t\t\t\tthis.lineTo( firstPoint.x, firstPoint.y );\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis.curves.push( curve );\n\n\t\tconst lastPoint = curve.getPoint( 1 );\n\t\tthis.currentPoint.copy( lastPoint );\n\n\t\treturn this;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.currentPoint.copy( source.currentPoint );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.currentPoint = this.currentPoint.toArray();\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.currentPoint.fromArray( json.currentPoint );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * Defines an arbitrary 2d shape plane using paths with optional holes. It\n * can be used with {@link ExtrudeGeometry}, {@link ShapeGeometry}, to get\n * points, or to get triangulated faces.\n *\n * ```js\n * const heartShape = new THREE.Shape();\n *\n * heartShape.moveTo( 25, 25 );\n * heartShape.bezierCurveTo( 25, 25, 20, 0, 0, 0 );\n * heartShape.bezierCurveTo( - 30, 0, - 30, 35, - 30, 35 );\n * heartShape.bezierCurveTo( - 30, 55, - 10, 77, 25, 95 );\n * heartShape.bezierCurveTo( 60, 77, 80, 55, 80, 35 );\n * heartShape.bezierCurveTo( 80, 35, 80, 0, 50, 0 );\n * heartShape.bezierCurveTo( 35, 0, 25, 25, 25, 25 );\n *\n * const extrudeSettings = {\n * \tdepth: 8,\n * \tbevelEnabled: true,\n * \tbevelSegments: 2,\n * \tsteps: 2,\n * \tbevelSize: 1,\n * \tbevelThickness: 1\n * };\n *\n * const geometry = new THREE.ExtrudeGeometry( heartShape, extrudeSettings );\n * const mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial() );\n * ```\n *\n * @augments Path\n */\nclass Shape extends Path {\n\n\t/**\n\t * Constructs a new shape.\n\t *\n\t * @param {Array<Vector2>} [points] - An array of 2D points defining the shape.\n\t */\n\tconstructor( points ) {\n\n\t\tsuper( points );\n\n\t\t/**\n\t\t * The UUID of the shape.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.uuid = generateUUID();\n\n\t\tthis.type = 'Shape';\n\n\t\t/**\n\t\t * Defines the holes in the shape. Hole definitions must use the\n\t\t * opposite winding order (CW/CCW) than the outer shape.\n\t\t *\n\t\t * @type {Array<Path>}\n\t\t * @readonly\n\t\t */\n\t\tthis.holes = [];\n\n\t}\n\n\t/**\n\t * Returns an array representing each contour of the holes\n\t * as a list of 2D points.\n\t *\n\t * @param {number} divisions - The fineness of the result.\n\t * @return {Array<Array<Vector2>>} The holes as a series of 2D points.\n\t */\n\tgetPointsHoles( divisions ) {\n\n\t\tconst holesPts = [];\n\n\t\tfor ( let i = 0, l = this.holes.length; i < l; i ++ ) {\n\n\t\t\tholesPts[ i ] = this.holes[ i ].getPoints( divisions );\n\n\t\t}\n\n\t\treturn holesPts;\n\n\t}\n\n\t// get points of shape and holes (keypoints based on segments parameter)\n\n\t/**\n\t * Returns an object that holds contour data for the shape and its holes as\n\t * arrays of 2D points.\n\t *\n\t * @param {number} divisions - The fineness of the result.\n\t * @return {{shape:Array<Vector2>,holes:Array<Array<Vector2>>}} An object with contour data.\n\t */\n\textractPoints( divisions ) {\n\n\t\treturn {\n\n\t\t\tshape: this.getPoints( divisions ),\n\t\t\tholes: this.getPointsHoles( divisions )\n\n\t\t};\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.holes = [];\n\n\t\tfor ( let i = 0, l = source.holes.length; i < l; i ++ ) {\n\n\t\t\tconst hole = source.holes[ i ];\n\n\t\t\tthis.holes.push( hole.clone() );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.uuid = this.uuid;\n\t\tdata.holes = [];\n\n\t\tfor ( let i = 0, l = this.holes.length; i < l; i ++ ) {\n\n\t\t\tconst hole = this.holes[ i ];\n\t\t\tdata.holes.push( hole.toJSON() );\n\n\t\t}\n\n\t\treturn data;\n\n\t}\n\n\tfromJSON( json ) {\n\n\t\tsuper.fromJSON( json );\n\n\t\tthis.uuid = json.uuid;\n\t\tthis.holes = [];\n\n\t\tfor ( let i = 0, l = json.holes.length; i < l; i ++ ) {\n\n\t\t\tconst hole = json.holes[ i ];\n\t\t\tthis.holes.push( new Path().fromJSON( hole ) );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n}\n\n/* eslint-disable */\n// copy of mapbox/earcut version 3.0.2\n// https://github.com/mapbox/earcut/tree/v3.0.2\n\nfunction earcut(data, holeIndices, dim = 2) {\n\n    const hasHoles = holeIndices && holeIndices.length;\n    const outerLen = hasHoles ? holeIndices[0] * dim : data.length;\n    let outerNode = linkedList(data, 0, outerLen, dim, true);\n    const triangles = [];\n\n    if (!outerNode || outerNode.next === outerNode.prev) return triangles;\n\n    let minX, minY, invSize;\n\n    if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);\n\n    // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox\n    if (data.length > 80 * dim) {\n        minX = data[0];\n        minY = data[1];\n        let maxX = minX;\n        let maxY = minY;\n\n        for (let i = dim; i < outerLen; i += dim) {\n            const x = data[i];\n            const y = data[i + 1];\n            if (x < minX) minX = x;\n            if (y < minY) minY = y;\n            if (x > maxX) maxX = x;\n            if (y > maxY) maxY = y;\n        }\n\n        // minX, minY and invSize are later used to transform coords into integers for z-order calculation\n        invSize = Math.max(maxX - minX, maxY - minY);\n        invSize = invSize !== 0 ? 32767 / invSize : 0;\n    }\n\n    earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0);\n\n    return triangles;\n}\n\n// create a circular doubly linked list from polygon points in the specified winding order\nfunction linkedList(data, start, end, dim, clockwise) {\n    let last;\n\n    if (clockwise === (signedArea(data, start, end, dim) > 0)) {\n        for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);\n    } else {\n        for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);\n    }\n\n    if (last && equals(last, last.next)) {\n        removeNode(last);\n        last = last.next;\n    }\n\n    return last;\n}\n\n// eliminate colinear or duplicate points\nfunction filterPoints(start, end) {\n    if (!start) return start;\n    if (!end) end = start;\n\n    let p = start,\n        again;\n    do {\n        again = false;\n\n        if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {\n            removeNode(p);\n            p = end = p.prev;\n            if (p === p.next) break;\n            again = true;\n\n        } else {\n            p = p.next;\n        }\n    } while (again || p !== end);\n\n    return end;\n}\n\n// main ear slicing loop which triangulates a polygon (given as a linked list)\nfunction earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {\n    if (!ear) return;\n\n    // interlink polygon nodes in z-order\n    if (!pass && invSize) indexCurve(ear, minX, minY, invSize);\n\n    let stop = ear;\n\n    // iterate through ears, slicing them one by one\n    while (ear.prev !== ear.next) {\n        const prev = ear.prev;\n        const next = ear.next;\n\n        if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {\n            triangles.push(prev.i, ear.i, next.i); // cut off the triangle\n\n            removeNode(ear);\n\n            // skipping the next vertex leads to less sliver triangles\n            ear = next.next;\n            stop = next.next;\n\n            continue;\n        }\n\n        ear = next;\n\n        // if we looped through the whole remaining polygon and can't find any more ears\n        if (ear === stop) {\n            // try filtering points and slicing again\n            if (!pass) {\n                earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);\n\n            // if this didn't work, try curing all small self-intersections locally\n            } else if (pass === 1) {\n                ear = cureLocalIntersections(filterPoints(ear), triangles);\n                earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);\n\n            // as a last resort, try splitting the remaining polygon into two\n            } else if (pass === 2) {\n                splitEarcut(ear, triangles, dim, minX, minY, invSize);\n            }\n\n            break;\n        }\n    }\n}\n\n// check whether a polygon node forms a valid ear with adjacent nodes\nfunction isEar(ear) {\n    const a = ear.prev,\n        b = ear,\n        c = ear.next;\n\n    if (area(a, b, c) >= 0) return false; // reflex, can't be an ear\n\n    // now make sure we don't have other points inside the potential ear\n    const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;\n\n    // triangle bbox\n    const x0 = Math.min(ax, bx, cx),\n        y0 = Math.min(ay, by, cy),\n        x1 = Math.max(ax, bx, cx),\n        y1 = Math.max(ay, by, cy);\n\n    let p = c.next;\n    while (p !== a) {\n        if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&\n            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&\n            area(p.prev, p, p.next) >= 0) return false;\n        p = p.next;\n    }\n\n    return true;\n}\n\nfunction isEarHashed(ear, minX, minY, invSize) {\n    const a = ear.prev,\n        b = ear,\n        c = ear.next;\n\n    if (area(a, b, c) >= 0) return false; // reflex, can't be an ear\n\n    const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;\n\n    // triangle bbox\n    const x0 = Math.min(ax, bx, cx),\n        y0 = Math.min(ay, by, cy),\n        x1 = Math.max(ax, bx, cx),\n        y1 = Math.max(ay, by, cy);\n\n    // z-order range for the current triangle bbox;\n    const minZ = zOrder(x0, y0, minX, minY, invSize),\n        maxZ = zOrder(x1, y1, minX, minY, invSize);\n\n    let p = ear.prevZ,\n        n = ear.nextZ;\n\n    // look for points inside the triangle in both directions\n    while (p && p.z >= minZ && n && n.z <= maxZ) {\n        if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&\n            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;\n        p = p.prevZ;\n\n        if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&\n            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;\n        n = n.nextZ;\n    }\n\n    // look for remaining points in decreasing z-order\n    while (p && p.z >= minZ) {\n        if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&\n            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;\n        p = p.prevZ;\n    }\n\n    // look for remaining points in increasing z-order\n    while (n && n.z <= maxZ) {\n        if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&\n            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;\n        n = n.nextZ;\n    }\n\n    return true;\n}\n\n// go through all polygon nodes and cure small local self-intersections\nfunction cureLocalIntersections(start, triangles) {\n    let p = start;\n    do {\n        const a = p.prev,\n            b = p.next.next;\n\n        if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {\n\n            triangles.push(a.i, p.i, b.i);\n\n            // remove two nodes involved\n            removeNode(p);\n            removeNode(p.next);\n\n            p = start = b;\n        }\n        p = p.next;\n    } while (p !== start);\n\n    return filterPoints(p);\n}\n\n// try splitting polygon into two and triangulate them independently\nfunction splitEarcut(start, triangles, dim, minX, minY, invSize) {\n    // look for a valid diagonal that divides the polygon into two\n    let a = start;\n    do {\n        let b = a.next.next;\n        while (b !== a.prev) {\n            if (a.i !== b.i && isValidDiagonal(a, b)) {\n                // split the polygon in two by the diagonal\n                let c = splitPolygon(a, b);\n\n                // filter colinear points around the cuts\n                a = filterPoints(a, a.next);\n                c = filterPoints(c, c.next);\n\n                // run earcut on each half\n                earcutLinked(a, triangles, dim, minX, minY, invSize, 0);\n                earcutLinked(c, triangles, dim, minX, minY, invSize, 0);\n                return;\n            }\n            b = b.next;\n        }\n        a = a.next;\n    } while (a !== start);\n}\n\n// link every hole into the outer loop, producing a single-ring polygon without holes\nfunction eliminateHoles(data, holeIndices, outerNode, dim) {\n    const queue = [];\n\n    for (let i = 0, len = holeIndices.length; i < len; i++) {\n        const start = holeIndices[i] * dim;\n        const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;\n        const list = linkedList(data, start, end, dim, false);\n        if (list === list.next) list.steiner = true;\n        queue.push(getLeftmost(list));\n    }\n\n    queue.sort(compareXYSlope);\n\n    // process holes from left to right\n    for (let i = 0; i < queue.length; i++) {\n        outerNode = eliminateHole(queue[i], outerNode);\n    }\n\n    return outerNode;\n}\n\nfunction compareXYSlope(a, b) {\n    let result = a.x - b.x;\n    // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find\n    // the bridge to the outer shell is always the point that they meet at.\n    if (result === 0) {\n        result = a.y - b.y;\n        if (result === 0) {\n            const aSlope = (a.next.y - a.y) / (a.next.x - a.x);\n            const bSlope = (b.next.y - b.y) / (b.next.x - b.x);\n            result = aSlope - bSlope;\n        }\n    }\n    return result;\n}\n\n// find a bridge between vertices that connects hole with an outer ring and link it\nfunction eliminateHole(hole, outerNode) {\n    const bridge = findHoleBridge(hole, outerNode);\n    if (!bridge) {\n        return outerNode;\n    }\n\n    const bridgeReverse = splitPolygon(bridge, hole);\n\n    // filter collinear points around the cuts\n    filterPoints(bridgeReverse, bridgeReverse.next);\n    return filterPoints(bridge, bridge.next);\n}\n\n// David Eberly's algorithm for finding a bridge between hole and outer polygon\nfunction findHoleBridge(hole, outerNode) {\n    let p = outerNode;\n    const hx = hole.x;\n    const hy = hole.y;\n    let qx = -Infinity;\n    let m;\n\n    // find a segment intersected by a ray from the hole's leftmost point to the left;\n    // segment's endpoint with lesser x will be potential connection point\n    // unless they intersect at a vertex, then choose the vertex\n    if (equals(hole, p)) return p;\n    do {\n        if (equals(hole, p.next)) return p.next;\n        else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {\n            const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);\n            if (x <= hx && x > qx) {\n                qx = x;\n                m = p.x < p.next.x ? p : p.next;\n                if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint\n            }\n        }\n        p = p.next;\n    } while (p !== outerNode);\n\n    if (!m) return null;\n\n    // look for points inside the triangle of hole point, segment intersection and endpoint;\n    // if there are no points found, we have a valid connection;\n    // otherwise choose the point of the minimum angle with the ray as connection point\n\n    const stop = m;\n    const mx = m.x;\n    const my = m.y;\n    let tanMin = Infinity;\n\n    p = m;\n\n    do {\n        if (hx >= p.x && p.x >= mx && hx !== p.x &&\n                pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {\n\n            const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential\n\n            if (locallyInside(p, hole) &&\n                (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {\n                m = p;\n                tanMin = tan;\n            }\n        }\n\n        p = p.next;\n    } while (p !== stop);\n\n    return m;\n}\n\n// whether sector in vertex m contains sector in vertex p in the same coordinates\nfunction sectorContainsSector(m, p) {\n    return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;\n}\n\n// interlink polygon nodes in z-order\nfunction indexCurve(start, minX, minY, invSize) {\n    let p = start;\n    do {\n        if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);\n        p.prevZ = p.prev;\n        p.nextZ = p.next;\n        p = p.next;\n    } while (p !== start);\n\n    p.prevZ.nextZ = null;\n    p.prevZ = null;\n\n    sortLinked(p);\n}\n\n// Simon Tatham's linked list merge sort algorithm\n// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html\nfunction sortLinked(list) {\n    let numMerges;\n    let inSize = 1;\n\n    do {\n        let p = list;\n        let e;\n        list = null;\n        let tail = null;\n        numMerges = 0;\n\n        while (p) {\n            numMerges++;\n            let q = p;\n            let pSize = 0;\n            for (let i = 0; i < inSize; i++) {\n                pSize++;\n                q = q.nextZ;\n                if (!q) break;\n            }\n            let qSize = inSize;\n\n            while (pSize > 0 || (qSize > 0 && q)) {\n\n                if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {\n                    e = p;\n                    p = p.nextZ;\n                    pSize--;\n                } else {\n                    e = q;\n                    q = q.nextZ;\n                    qSize--;\n                }\n\n                if (tail) tail.nextZ = e;\n                else list = e;\n\n                e.prevZ = tail;\n                tail = e;\n            }\n\n            p = q;\n        }\n\n        tail.nextZ = null;\n        inSize *= 2;\n\n    } while (numMerges > 1);\n\n    return list;\n}\n\n// z-order of a point given coords and inverse of the longer side of data bbox\nfunction zOrder(x, y, minX, minY, invSize) {\n    // coords are transformed into non-negative 15-bit integer range\n    x = (x - minX) * invSize | 0;\n    y = (y - minY) * invSize | 0;\n\n    x = (x | (x << 8)) & 0x00FF00FF;\n    x = (x | (x << 4)) & 0x0F0F0F0F;\n    x = (x | (x << 2)) & 0x33333333;\n    x = (x | (x << 1)) & 0x55555555;\n\n    y = (y | (y << 8)) & 0x00FF00FF;\n    y = (y | (y << 4)) & 0x0F0F0F0F;\n    y = (y | (y << 2)) & 0x33333333;\n    y = (y | (y << 1)) & 0x55555555;\n\n    return x | (y << 1);\n}\n\n// find the leftmost node of a polygon ring\nfunction getLeftmost(start) {\n    let p = start,\n        leftmost = start;\n    do {\n        if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;\n        p = p.next;\n    } while (p !== start);\n\n    return leftmost;\n}\n\n// check if a point lies within a convex triangle\nfunction pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {\n    return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&\n           (ax - px) * (by - py) >= (bx - px) * (ay - py) &&\n           (bx - px) * (cy - py) >= (cx - px) * (by - py);\n}\n\n// check if a point lies within a convex triangle but false if its equal to the first point of the triangle\nfunction pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {\n    return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);\n}\n\n// check if a diagonal between two polygon nodes is valid (lies in polygon interior)\nfunction isValidDiagonal(a, b) {\n    return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges\n           (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible\n            (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors\n            equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case\n}\n\n// signed area of a triangle\nfunction area(p, q, r) {\n    return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);\n}\n\n// check if two points are equal\nfunction equals(p1, p2) {\n    return p1.x === p2.x && p1.y === p2.y;\n}\n\n// check if two segments intersect\nfunction intersects(p1, q1, p2, q2) {\n    const o1 = sign(area(p1, q1, p2));\n    const o2 = sign(area(p1, q1, q2));\n    const o3 = sign(area(p2, q2, p1));\n    const o4 = sign(area(p2, q2, q1));\n\n    if (o1 !== o2 && o3 !== o4) return true; // general case\n\n    if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1\n    if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1\n    if (o3 === 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2\n    if (o4 === 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2\n\n    return false;\n}\n\n// for collinear points p, q, r, check if point q lies on segment pr\nfunction onSegment(p, q, r) {\n    return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);\n}\n\nfunction sign(num) {\n    return num > 0 ? 1 : num < 0 ? -1 : 0;\n}\n\n// check if a polygon diagonal intersects any polygon segments\nfunction intersectsPolygon(a, b) {\n    let p = a;\n    do {\n        if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&\n                intersects(p, p.next, a, b)) return true;\n        p = p.next;\n    } while (p !== a);\n\n    return false;\n}\n\n// check if a polygon diagonal is locally inside the polygon\nfunction locallyInside(a, b) {\n    return area(a.prev, a, a.next) < 0 ?\n        area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :\n        area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;\n}\n\n// check if the middle point of a polygon diagonal is inside the polygon\nfunction middleInside(a, b) {\n    let p = a;\n    let inside = false;\n    const px = (a.x + b.x) / 2;\n    const py = (a.y + b.y) / 2;\n    do {\n        if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&\n                (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))\n            inside = !inside;\n        p = p.next;\n    } while (p !== a);\n\n    return inside;\n}\n\n// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;\n// if one belongs to the outer ring and another to a hole, it merges it into a single ring\nfunction splitPolygon(a, b) {\n    const a2 = createNode(a.i, a.x, a.y),\n        b2 = createNode(b.i, b.x, b.y),\n        an = a.next,\n        bp = b.prev;\n\n    a.next = b;\n    b.prev = a;\n\n    a2.next = an;\n    an.prev = a2;\n\n    b2.next = a2;\n    a2.prev = b2;\n\n    bp.next = b2;\n    b2.prev = bp;\n\n    return b2;\n}\n\n// create a node and optionally link it with previous one (in a circular doubly linked list)\nfunction insertNode(i, x, y, last) {\n    const p = createNode(i, x, y);\n\n    if (!last) {\n        p.prev = p;\n        p.next = p;\n\n    } else {\n        p.next = last.next;\n        p.prev = last;\n        last.next.prev = p;\n        last.next = p;\n    }\n    return p;\n}\n\nfunction removeNode(p) {\n    p.next.prev = p.prev;\n    p.prev.next = p.next;\n\n    if (p.prevZ) p.prevZ.nextZ = p.nextZ;\n    if (p.nextZ) p.nextZ.prevZ = p.prevZ;\n}\n\nfunction createNode(i, x, y) {\n    return {\n        i, // vertex index in coordinates array\n        x, y, // vertex coordinates\n        prev: null, // previous and next vertex nodes in a polygon ring\n        next: null,\n        z: 0, // z-order curve value\n        prevZ: null, // previous and next nodes in z-order\n        nextZ: null,\n        steiner: false // indicates whether this is a steiner point\n    };\n}\n\nfunction signedArea(data, start, end, dim) {\n    let sum = 0;\n    for (let i = start, j = end - dim; i < end; i += dim) {\n        sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);\n        j = i;\n    }\n    return sum;\n}\n\n/**\n * An implementation of the earcut polygon triangulation algorithm.\n * The code is a port of [mapbox/earcut](https://github.com/mapbox/earcut).\n *\n * @see https://github.com/mapbox/earcut\n */\nclass Earcut {\n\n\t/**\n\t * Triangulates the given shape definition by returning an array of triangles.\n\t *\n\t * @param {Array<number>} data - An array with 2D points.\n\t * @param {Array<number>} holeIndices - An array with indices defining holes.\n\t * @param {number} [dim=2] - The number of coordinates per vertex in the input array.\n\t * @return {Array<number>} An array representing the triangulated faces. Each face is defined by three consecutive numbers\n\t * representing vertex indices.\n\t */\n\tstatic triangulate( data, holeIndices, dim = 2 ) {\n\n\t\treturn earcut( data, holeIndices, dim );\n\n\t}\n\n}\n\n/**\n * A class containing utility functions for shapes.\n *\n * @hideconstructor\n */\nclass ShapeUtils {\n\n\t/**\n\t * Calculate area of a ( 2D ) contour polygon.\n\t *\n\t * @param {Array<Vector2>} contour - An array of 2D points.\n\t * @return {number} The area.\n\t */\n\tstatic area( contour ) {\n\n\t\tconst n = contour.length;\n\t\tlet a = 0.0;\n\n\t\tfor ( let p = n - 1, q = 0; q < n; p = q ++ ) {\n\n\t\t\ta += contour[ p ].x * contour[ q ].y - contour[ q ].x * contour[ p ].y;\n\n\t\t}\n\n\t\treturn a * 0.5;\n\n\t}\n\n\t/**\n\t * Returns `true` if the given contour uses a clockwise winding order.\n\t *\n\t * @param {Array<Vector2>} pts - An array of 2D points defining a polygon.\n\t * @return {boolean} Whether the given contour uses a clockwise winding order or not.\n\t */\n\tstatic isClockWise( pts ) {\n\n\t\treturn ShapeUtils.area( pts ) < 0;\n\n\t}\n\n\t/**\n\t * Triangulates the given shape definition.\n\t *\n\t * @param {Array<Vector2>} contour - An array of 2D points defining the contour.\n\t * @param {Array<Array<Vector2>>} holes - An array that holds arrays of 2D points defining the holes.\n\t * @return {Array<Array<number>>} An array that holds for each face definition an array with three indices.\n\t */\n\tstatic triangulateShape( contour, holes ) {\n\n\t\tconst vertices = []; // flat array of vertices like [ x0,y0, x1,y1, x2,y2, ... ]\n\t\tconst holeIndices = []; // array of hole indices\n\t\tconst faces = []; // final array of vertex indices like [ [ a,b,d ], [ b,c,d ] ]\n\n\t\tremoveDupEndPts( contour );\n\t\taddContour( vertices, contour );\n\n\t\t//\n\n\t\tlet holeIndex = contour.length;\n\n\t\tholes.forEach( removeDupEndPts );\n\n\t\tfor ( let i = 0; i < holes.length; i ++ ) {\n\n\t\t\tholeIndices.push( holeIndex );\n\t\t\tholeIndex += holes[ i ].length;\n\t\t\taddContour( vertices, holes[ i ] );\n\n\t\t}\n\n\t\t//\n\n\t\tconst triangles = Earcut.triangulate( vertices, holeIndices );\n\n\t\t//\n\n\t\tfor ( let i = 0; i < triangles.length; i += 3 ) {\n\n\t\t\tfaces.push( triangles.slice( i, i + 3 ) );\n\n\t\t}\n\n\t\treturn faces;\n\n\t}\n\n}\n\nfunction removeDupEndPts( points ) {\n\n\tconst l = points.length;\n\n\tif ( l > 2 && points[ l - 1 ].equals( points[ 0 ] ) ) {\n\n\t\tpoints.pop();\n\n\t}\n\n}\n\nfunction addContour( vertices, contour ) {\n\n\tfor ( let i = 0; i < contour.length; i ++ ) {\n\n\t\tvertices.push( contour[ i ].x );\n\t\tvertices.push( contour[ i ].y );\n\n\t}\n\n}\n\n/**\n * Creates extruded geometry from a path shape.\n *\n * ```js\n * const length = 12, width = 8;\n *\n * const shape = new THREE.Shape();\n * shape.moveTo( 0,0 );\n * shape.lineTo( 0, width );\n * shape.lineTo( length, width );\n * shape.lineTo( length, 0 );\n * shape.lineTo( 0, 0 );\n *\n * const geometry = new THREE.ExtrudeGeometry( shape );\n * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );\n * const mesh = new THREE.Mesh( geometry, material ) ;\n * scene.add( mesh );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#ExtrudeGeometry\n */\nclass ExtrudeGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new extrude geometry.\n\t *\n\t * @param {Shape|Array<Shape>} [shapes] - A shape or an array of shapes.\n\t * @param {ExtrudeGeometry~Options} [options] - The extrude settings.\n\t */\n\tconstructor( shapes = new Shape( [ new Vector2( 0.5, 0.5 ), new Vector2( -0.5, 0.5 ), new Vector2( -0.5, -0.5 ), new Vector2( 0.5, -0.5 ) ] ), options = {} ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'ExtrudeGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tshapes: shapes,\n\t\t\toptions: options\n\t\t};\n\n\t\tshapes = Array.isArray( shapes ) ? shapes : [ shapes ];\n\n\t\tconst scope = this;\n\n\t\tconst verticesArray = [];\n\t\tconst uvArray = [];\n\n\t\tfor ( let i = 0, l = shapes.length; i < l; i ++ ) {\n\n\t\t\tconst shape = shapes[ i ];\n\t\t\taddShape( shape );\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( verticesArray, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvArray, 2 ) );\n\n\t\tthis.computeVertexNormals();\n\n\t\t// functions\n\n\t\tfunction addShape( shape ) {\n\n\t\t\tconst placeholder = [];\n\n\t\t\t// options\n\n\t\t\tconst curveSegments = options.curveSegments !== undefined ? options.curveSegments : 12;\n\t\t\tconst steps = options.steps !== undefined ? options.steps : 1;\n\t\t\tconst depth = options.depth !== undefined ? options.depth : 1;\n\n\t\t\tlet bevelEnabled = options.bevelEnabled !== undefined ? options.bevelEnabled : true;\n\t\t\tlet bevelThickness = options.bevelThickness !== undefined ? options.bevelThickness : 0.2;\n\t\t\tlet bevelSize = options.bevelSize !== undefined ? options.bevelSize : bevelThickness - 0.1;\n\t\t\tlet bevelOffset = options.bevelOffset !== undefined ? options.bevelOffset : 0;\n\t\t\tlet bevelSegments = options.bevelSegments !== undefined ? options.bevelSegments : 3;\n\n\t\t\tconst extrudePath = options.extrudePath;\n\n\t\t\tconst uvgen = options.UVGenerator !== undefined ? options.UVGenerator : WorldUVGenerator;\n\n\t\t\t//\n\n\t\t\tlet extrudePts, extrudeByPath = false;\n\t\t\tlet splineTube, binormal, normal, position2;\n\n\t\t\tif ( extrudePath ) {\n\n\t\t\t\textrudePts = extrudePath.getSpacedPoints( steps );\n\n\t\t\t\textrudeByPath = true;\n\t\t\t\tbevelEnabled = false; // bevels not supported for path extrusion\n\n\t\t\t\t// SETUP TNB variables\n\n\t\t\t\tconst isClosed = extrudePath.isCatmullRomCurve3 ? extrudePath.closed : false;\n\n\t\t\t\tsplineTube = extrudePath.computeFrenetFrames( steps, isClosed );\n\n\t\t\t\t// log(splineTube, 'splineTube', splineTube.normals.length, 'steps', steps, 'extrudePts', extrudePts.length);\n\n\t\t\t\tbinormal = new Vector3();\n\t\t\t\tnormal = new Vector3();\n\t\t\t\tposition2 = new Vector3();\n\n\t\t\t}\n\n\t\t\t// Safeguards if bevels are not enabled\n\n\t\t\tif ( ! bevelEnabled ) {\n\n\t\t\t\tbevelSegments = 0;\n\t\t\t\tbevelThickness = 0;\n\t\t\t\tbevelSize = 0;\n\t\t\t\tbevelOffset = 0;\n\n\t\t\t}\n\n\t\t\t// Variables initialization\n\n\t\t\tconst shapePoints = shape.extractPoints( curveSegments );\n\n\t\t\tlet vertices = shapePoints.shape;\n\t\t\tconst holes = shapePoints.holes;\n\n\t\t\tconst reverse = ! ShapeUtils.isClockWise( vertices );\n\n\t\t\tif ( reverse ) {\n\n\t\t\t\tvertices = vertices.reverse();\n\n\t\t\t\t// Maybe we should also check if holes are in the opposite direction, just to be safe ...\n\n\t\t\t\tfor ( let h = 0, hl = holes.length; h < hl; h ++ ) {\n\n\t\t\t\t\tconst ahole = holes[ h ];\n\n\t\t\t\t\tif ( ShapeUtils.isClockWise( ahole ) ) {\n\n\t\t\t\t\t\tholes[ h ] = ahole.reverse();\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t/**Merges index-adjacent points that are within a threshold distance of each other. Array is modified in-place. Threshold distance is empirical, and scaled based on the magnitude of point coordinates.\n\t\t\t * @param {Array<Vector2>} points\n\t\t\t*/\n\t\t\tfunction mergeOverlappingPoints( points ) {\n\n\t\t\t\tconst THRESHOLD = 1e-10;\n\t\t\t\tconst THRESHOLD_SQ = THRESHOLD * THRESHOLD;\n\t\t\t\tlet prevPos = points[ 0 ];\n\t\t\t\tfor ( let i = 1; i <= points.length; i ++ ) {\n\n\t\t\t\t\tconst currentIndex = i % points.length;\n\t\t\t\t\tconst currentPos = points[ currentIndex ];\n\t\t\t\t\tconst dx = currentPos.x - prevPos.x;\n\t\t\t\t\tconst dy = currentPos.y - prevPos.y;\n\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\n\t\t\t\t\tconst scalingFactorSqrt = Math.max(\n\t\t\t\t\t\tMath.abs( currentPos.x ),\n\t\t\t\t\t\tMath.abs( currentPos.y ),\n\t\t\t\t\t\tMath.abs( prevPos.x ),\n\t\t\t\t\t\tMath.abs( prevPos.y )\n\t\t\t\t\t);\n\t\t\t\t\tconst thresholdSqScaled = THRESHOLD_SQ * scalingFactorSqrt * scalingFactorSqrt;\n\t\t\t\t\tif ( distSq <= thresholdSqScaled ) {\n\n\t\t\t\t\t\tpoints.splice( currentIndex, 1 );\n\t\t\t\t\t\ti --;\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tprevPos = currentPos;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tmergeOverlappingPoints( vertices );\n\t\t\tholes.forEach( mergeOverlappingPoints );\n\n\t\t\tconst numHoles = holes.length;\n\n\t\t\t/* Vertices */\n\n\t\t\tconst contour = vertices; // vertices has all points but contour has only points of circumference\n\n\t\t\tfor ( let h = 0; h < numHoles; h ++ ) {\n\n\t\t\t\tconst ahole = holes[ h ];\n\n\t\t\t\tvertices = vertices.concat( ahole );\n\n\t\t\t}\n\n\n\t\t\tfunction scalePt2( pt, vec, size ) {\n\n\t\t\t\tif ( ! vec ) error( 'ExtrudeGeometry: vec does not exist' );\n\n\t\t\t\treturn pt.clone().addScaledVector( vec, size );\n\n\t\t\t}\n\n\t\t\tconst vlen = vertices.length;\n\n\n\t\t\t// Find directions for point movement\n\n\n\t\t\tfunction getBevelVec( inPt, inPrev, inNext ) {\n\n\t\t\t\t// computes for inPt the corresponding point inPt' on a new contour\n\t\t\t\t//   shifted by 1 unit (length of normalized vector) to the left\n\t\t\t\t// if we walk along contour clockwise, this new contour is outside the old one\n\t\t\t\t//\n\t\t\t\t// inPt' is the intersection of the two lines parallel to the two\n\t\t\t\t//  adjacent edges of inPt at a distance of 1 unit on the left side.\n\n\t\t\t\tlet v_trans_x, v_trans_y, shrink_by; // resulting translation vector for inPt\n\n\t\t\t\t// good reading for geometry algorithms (here: line-line intersection)\n\t\t\t\t// http://geomalgorithms.com/a05-_intersect-1.html\n\n\t\t\t\tconst v_prev_x = inPt.x - inPrev.x,\n\t\t\t\t\tv_prev_y = inPt.y - inPrev.y;\n\t\t\t\tconst v_next_x = inNext.x - inPt.x,\n\t\t\t\t\tv_next_y = inNext.y - inPt.y;\n\n\t\t\t\tconst v_prev_lensq = ( v_prev_x * v_prev_x + v_prev_y * v_prev_y );\n\n\t\t\t\t// check for collinear edges\n\t\t\t\tconst collinear0 = ( v_prev_x * v_next_y - v_prev_y * v_next_x );\n\n\t\t\t\tif ( Math.abs( collinear0 ) > Number.EPSILON ) {\n\n\t\t\t\t\t// not collinear\n\n\t\t\t\t\t// length of vectors for normalizing\n\n\t\t\t\t\tconst v_prev_len = Math.sqrt( v_prev_lensq );\n\t\t\t\t\tconst v_next_len = Math.sqrt( v_next_x * v_next_x + v_next_y * v_next_y );\n\n\t\t\t\t\t// shift adjacent points by unit vectors to the left\n\n\t\t\t\t\tconst ptPrevShift_x = ( inPrev.x - v_prev_y / v_prev_len );\n\t\t\t\t\tconst ptPrevShift_y = ( inPrev.y + v_prev_x / v_prev_len );\n\n\t\t\t\t\tconst ptNextShift_x = ( inNext.x - v_next_y / v_next_len );\n\t\t\t\t\tconst ptNextShift_y = ( inNext.y + v_next_x / v_next_len );\n\n\t\t\t\t\t// scaling factor for v_prev to intersection point\n\n\t\t\t\t\tconst sf = ( ( ptNextShift_x - ptPrevShift_x ) * v_next_y -\n\t\t\t\t\t\t\t( ptNextShift_y - ptPrevShift_y ) * v_next_x ) /\n\t\t\t\t\t\t( v_prev_x * v_next_y - v_prev_y * v_next_x );\n\n\t\t\t\t\t// vector from inPt to intersection point\n\n\t\t\t\t\tv_trans_x = ( ptPrevShift_x + v_prev_x * sf - inPt.x );\n\t\t\t\t\tv_trans_y = ( ptPrevShift_y + v_prev_y * sf - inPt.y );\n\n\t\t\t\t\t// Don't normalize!, otherwise sharp corners become ugly\n\t\t\t\t\t//  but prevent crazy spikes\n\t\t\t\t\tconst v_trans_lensq = ( v_trans_x * v_trans_x + v_trans_y * v_trans_y );\n\t\t\t\t\tif ( v_trans_lensq <= 2 ) {\n\n\t\t\t\t\t\treturn new Vector2( v_trans_x, v_trans_y );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tshrink_by = Math.sqrt( v_trans_lensq / 2 );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// handle special case of collinear edges\n\n\t\t\t\t\tlet direction_eq = false; // assumes: opposite\n\n\t\t\t\t\tif ( v_prev_x > Number.EPSILON ) {\n\n\t\t\t\t\t\tif ( v_next_x > Number.EPSILON ) {\n\n\t\t\t\t\t\t\tdirection_eq = true;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tif ( v_prev_x < - Number.EPSILON ) {\n\n\t\t\t\t\t\t\tif ( v_next_x < - Number.EPSILON ) {\n\n\t\t\t\t\t\t\t\tdirection_eq = true;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tif ( Math.sign( v_prev_y ) === Math.sign( v_next_y ) ) {\n\n\t\t\t\t\t\t\t\tdirection_eq = true;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( direction_eq ) {\n\n\t\t\t\t\t\t// log(\"Warning: lines are a straight sequence\");\n\t\t\t\t\t\tv_trans_x = - v_prev_y;\n\t\t\t\t\t\tv_trans_y = v_prev_x;\n\t\t\t\t\t\tshrink_by = Math.sqrt( v_prev_lensq );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// log(\"Warning: lines are a straight spike\");\n\t\t\t\t\t\tv_trans_x = v_prev_x;\n\t\t\t\t\t\tv_trans_y = v_prev_y;\n\t\t\t\t\t\tshrink_by = Math.sqrt( v_prev_lensq / 2 );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\treturn new Vector2( v_trans_x / shrink_by, v_trans_y / shrink_by );\n\n\t\t\t}\n\n\n\t\t\tconst contourMovements = [];\n\n\t\t\tfor ( let i = 0, il = contour.length, j = il - 1, k = i + 1; i < il; i ++, j ++, k ++ ) {\n\n\t\t\t\tif ( j === il ) j = 0;\n\t\t\t\tif ( k === il ) k = 0;\n\n\t\t\t\t//  (j)---(i)---(k)\n\t\t\t\t// log('i,j,k', i, j , k)\n\n\t\t\t\tcontourMovements[ i ] = getBevelVec( contour[ i ], contour[ j ], contour[ k ] );\n\n\t\t\t}\n\n\t\t\tconst holesMovements = [];\n\t\t\tlet oneHoleMovements, verticesMovements = contourMovements.concat();\n\n\t\t\tfor ( let h = 0, hl = numHoles; h < hl; h ++ ) {\n\n\t\t\t\tconst ahole = holes[ h ];\n\n\t\t\t\toneHoleMovements = [];\n\n\t\t\t\tfor ( let i = 0, il = ahole.length, j = il - 1, k = i + 1; i < il; i ++, j ++, k ++ ) {\n\n\t\t\t\t\tif ( j === il ) j = 0;\n\t\t\t\t\tif ( k === il ) k = 0;\n\n\t\t\t\t\t//  (j)---(i)---(k)\n\t\t\t\t\toneHoleMovements[ i ] = getBevelVec( ahole[ i ], ahole[ j ], ahole[ k ] );\n\n\t\t\t\t}\n\n\t\t\t\tholesMovements.push( oneHoleMovements );\n\t\t\t\tverticesMovements = verticesMovements.concat( oneHoleMovements );\n\n\t\t\t}\n\n\t\t\tlet faces;\n\n\t\t\tif ( bevelSegments === 0 ) {\n\n\t\t\t\tfaces = ShapeUtils.triangulateShape( contour, holes );\n\n\t\t\t} else {\n\n\t\t\t\tconst contractedContourVertices = [];\n\t\t\t\tconst expandedHoleVertices = [];\n\n\t\t\t\t// Loop bevelSegments, 1 for the front, 1 for the back\n\n\t\t\t\tfor ( let b = 0; b < bevelSegments; b ++ ) {\n\n\t\t\t\t\t//for ( b = bevelSegments; b > 0; b -- ) {\n\n\t\t\t\t\tconst t = b / bevelSegments;\n\t\t\t\t\tconst z = bevelThickness * Math.cos( t * Math.PI / 2 );\n\t\t\t\t\tconst bs = bevelSize * Math.sin( t * Math.PI / 2 ) + bevelOffset;\n\n\t\t\t\t\t// contract shape\n\n\t\t\t\t\tfor ( let i = 0, il = contour.length; i < il; i ++ ) {\n\n\t\t\t\t\t\tconst vert = scalePt2( contour[ i ], contourMovements[ i ], bs );\n\n\t\t\t\t\t\tv( vert.x, vert.y, - z );\n\t\t\t\t\t\tif ( t === 0 ) contractedContourVertices.push( vert );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// expand holes\n\n\t\t\t\t\tfor ( let h = 0, hl = numHoles; h < hl; h ++ ) {\n\n\t\t\t\t\t\tconst ahole = holes[ h ];\n\t\t\t\t\t\toneHoleMovements = holesMovements[ h ];\n\t\t\t\t\t\tconst oneHoleVertices = [];\n\t\t\t\t\t\tfor ( let i = 0, il = ahole.length; i < il; i ++ ) {\n\n\t\t\t\t\t\t\tconst vert = scalePt2( ahole[ i ], oneHoleMovements[ i ], bs );\n\n\t\t\t\t\t\t\tv( vert.x, vert.y, - z );\n\t\t\t\t\t\t\tif ( t === 0 ) oneHoleVertices.push( vert );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( t === 0 ) expandedHoleVertices.push( oneHoleVertices );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tfaces = ShapeUtils.triangulateShape( contractedContourVertices, expandedHoleVertices );\n\n\t\t\t}\n\n\t\t\tconst flen = faces.length;\n\n\t\t\tconst bs = bevelSize + bevelOffset;\n\n\t\t\t// Back facing vertices\n\n\t\t\tfor ( let i = 0; i < vlen; i ++ ) {\n\n\t\t\t\tconst vert = bevelEnabled ? scalePt2( vertices[ i ], verticesMovements[ i ], bs ) : vertices[ i ];\n\n\t\t\t\tif ( ! extrudeByPath ) {\n\n\t\t\t\t\tv( vert.x, vert.y, 0 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// v( vert.x, vert.y + extrudePts[ 0 ].y, extrudePts[ 0 ].x );\n\n\t\t\t\t\tnormal.copy( splineTube.normals[ 0 ] ).multiplyScalar( vert.x );\n\t\t\t\t\tbinormal.copy( splineTube.binormals[ 0 ] ).multiplyScalar( vert.y );\n\n\t\t\t\t\tposition2.copy( extrudePts[ 0 ] ).add( normal ).add( binormal );\n\n\t\t\t\t\tv( position2.x, position2.y, position2.z );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// Add stepped vertices...\n\t\t\t// Including front facing vertices\n\n\t\t\tfor ( let s = 1; s <= steps; s ++ ) {\n\n\t\t\t\tfor ( let i = 0; i < vlen; i ++ ) {\n\n\t\t\t\t\tconst vert = bevelEnabled ? scalePt2( vertices[ i ], verticesMovements[ i ], bs ) : vertices[ i ];\n\n\t\t\t\t\tif ( ! extrudeByPath ) {\n\n\t\t\t\t\t\tv( vert.x, vert.y, depth / steps * s );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// v( vert.x, vert.y + extrudePts[ s - 1 ].y, extrudePts[ s - 1 ].x );\n\n\t\t\t\t\t\tnormal.copy( splineTube.normals[ s ] ).multiplyScalar( vert.x );\n\t\t\t\t\t\tbinormal.copy( splineTube.binormals[ s ] ).multiplyScalar( vert.y );\n\n\t\t\t\t\t\tposition2.copy( extrudePts[ s ] ).add( normal ).add( binormal );\n\n\t\t\t\t\t\tv( position2.x, position2.y, position2.z );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\n\t\t\t// Add bevel segments planes\n\n\t\t\t//for ( b = 1; b <= bevelSegments; b ++ ) {\n\t\t\tfor ( let b = bevelSegments - 1; b >= 0; b -- ) {\n\n\t\t\t\tconst t = b / bevelSegments;\n\t\t\t\tconst z = bevelThickness * Math.cos( t * Math.PI / 2 );\n\t\t\t\tconst bs = bevelSize * Math.sin( t * Math.PI / 2 ) + bevelOffset;\n\n\t\t\t\t// contract shape\n\n\t\t\t\tfor ( let i = 0, il = contour.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst vert = scalePt2( contour[ i ], contourMovements[ i ], bs );\n\t\t\t\t\tv( vert.x, vert.y, depth + z );\n\n\t\t\t\t}\n\n\t\t\t\t// expand holes\n\n\t\t\t\tfor ( let h = 0, hl = holes.length; h < hl; h ++ ) {\n\n\t\t\t\t\tconst ahole = holes[ h ];\n\t\t\t\t\toneHoleMovements = holesMovements[ h ];\n\n\t\t\t\t\tfor ( let i = 0, il = ahole.length; i < il; i ++ ) {\n\n\t\t\t\t\t\tconst vert = scalePt2( ahole[ i ], oneHoleMovements[ i ], bs );\n\n\t\t\t\t\t\tif ( ! extrudeByPath ) {\n\n\t\t\t\t\t\t\tv( vert.x, vert.y, depth + z );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tv( vert.x, vert.y + extrudePts[ steps - 1 ].y, extrudePts[ steps - 1 ].x + z );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t/* Faces */\n\n\t\t\t// Top and bottom faces\n\n\t\t\tbuildLidFaces();\n\n\t\t\t// Sides faces\n\n\t\t\tbuildSideFaces();\n\n\n\t\t\t/////  Internal functions\n\n\t\t\tfunction buildLidFaces() {\n\n\t\t\t\tconst start = verticesArray.length / 3;\n\n\t\t\t\tif ( bevelEnabled ) {\n\n\t\t\t\t\tlet layer = 0; // steps + 1\n\t\t\t\t\tlet offset = vlen * layer;\n\n\t\t\t\t\t// Bottom faces\n\n\t\t\t\t\tfor ( let i = 0; i < flen; i ++ ) {\n\n\t\t\t\t\t\tconst face = faces[ i ];\n\t\t\t\t\t\tf3( face[ 2 ] + offset, face[ 1 ] + offset, face[ 0 ] + offset );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tlayer = steps + bevelSegments * 2;\n\t\t\t\t\toffset = vlen * layer;\n\n\t\t\t\t\t// Top faces\n\n\t\t\t\t\tfor ( let i = 0; i < flen; i ++ ) {\n\n\t\t\t\t\t\tconst face = faces[ i ];\n\t\t\t\t\t\tf3( face[ 0 ] + offset, face[ 1 ] + offset, face[ 2 ] + offset );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// Bottom faces\n\n\t\t\t\t\tfor ( let i = 0; i < flen; i ++ ) {\n\n\t\t\t\t\t\tconst face = faces[ i ];\n\t\t\t\t\t\tf3( face[ 2 ], face[ 1 ], face[ 0 ] );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// Top faces\n\n\t\t\t\t\tfor ( let i = 0; i < flen; i ++ ) {\n\n\t\t\t\t\t\tconst face = faces[ i ];\n\t\t\t\t\t\tf3( face[ 0 ] + vlen * steps, face[ 1 ] + vlen * steps, face[ 2 ] + vlen * steps );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tscope.addGroup( start, verticesArray.length / 3 - start, 0 );\n\n\t\t\t}\n\n\t\t\t// Create faces for the z-sides of the shape\n\n\t\t\tfunction buildSideFaces() {\n\n\t\t\t\tconst start = verticesArray.length / 3;\n\t\t\t\tlet layeroffset = 0;\n\t\t\t\tsidewalls( contour, layeroffset );\n\t\t\t\tlayeroffset += contour.length;\n\n\t\t\t\tfor ( let h = 0, hl = holes.length; h < hl; h ++ ) {\n\n\t\t\t\t\tconst ahole = holes[ h ];\n\t\t\t\t\tsidewalls( ahole, layeroffset );\n\n\t\t\t\t\t//, true\n\t\t\t\t\tlayeroffset += ahole.length;\n\n\t\t\t\t}\n\n\n\t\t\t\tscope.addGroup( start, verticesArray.length / 3 - start, 1 );\n\n\n\t\t\t}\n\n\t\t\tfunction sidewalls( contour, layeroffset ) {\n\n\t\t\t\tlet i = contour.length;\n\n\t\t\t\twhile ( -- i >= 0 ) {\n\n\t\t\t\t\tconst j = i;\n\t\t\t\t\tlet k = i - 1;\n\t\t\t\t\tif ( k < 0 ) k = contour.length - 1;\n\n\t\t\t\t\t//log('b', i,j, i-1, k,vertices.length);\n\n\t\t\t\t\tfor ( let s = 0, sl = ( steps + bevelSegments * 2 ); s < sl; s ++ ) {\n\n\t\t\t\t\t\tconst slen1 = vlen * s;\n\t\t\t\t\t\tconst slen2 = vlen * ( s + 1 );\n\n\t\t\t\t\t\tconst a = layeroffset + j + slen1,\n\t\t\t\t\t\t\tb = layeroffset + k + slen1,\n\t\t\t\t\t\t\tc = layeroffset + k + slen2,\n\t\t\t\t\t\t\td = layeroffset + j + slen2;\n\n\t\t\t\t\t\tf4( a, b, c, d );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tfunction v( x, y, z ) {\n\n\t\t\t\tplaceholder.push( x );\n\t\t\t\tplaceholder.push( y );\n\t\t\t\tplaceholder.push( z );\n\n\t\t\t}\n\n\n\t\t\tfunction f3( a, b, c ) {\n\n\t\t\t\taddVertex( a );\n\t\t\t\taddVertex( b );\n\t\t\t\taddVertex( c );\n\n\t\t\t\tconst nextIndex = verticesArray.length / 3;\n\t\t\t\tconst uvs = uvgen.generateTopUV( scope, verticesArray, nextIndex - 3, nextIndex - 2, nextIndex - 1 );\n\n\t\t\t\taddUV( uvs[ 0 ] );\n\t\t\t\taddUV( uvs[ 1 ] );\n\t\t\t\taddUV( uvs[ 2 ] );\n\n\t\t\t}\n\n\t\t\tfunction f4( a, b, c, d ) {\n\n\t\t\t\taddVertex( a );\n\t\t\t\taddVertex( b );\n\t\t\t\taddVertex( d );\n\n\t\t\t\taddVertex( b );\n\t\t\t\taddVertex( c );\n\t\t\t\taddVertex( d );\n\n\n\t\t\t\tconst nextIndex = verticesArray.length / 3;\n\t\t\t\tconst uvs = uvgen.generateSideWallUV( scope, verticesArray, nextIndex - 6, nextIndex - 3, nextIndex - 2, nextIndex - 1 );\n\n\t\t\t\taddUV( uvs[ 0 ] );\n\t\t\t\taddUV( uvs[ 1 ] );\n\t\t\t\taddUV( uvs[ 3 ] );\n\n\t\t\t\taddUV( uvs[ 1 ] );\n\t\t\t\taddUV( uvs[ 2 ] );\n\t\t\t\taddUV( uvs[ 3 ] );\n\n\t\t\t}\n\n\t\t\tfunction addVertex( index ) {\n\n\t\t\t\tverticesArray.push( placeholder[ index * 3 + 0 ] );\n\t\t\t\tverticesArray.push( placeholder[ index * 3 + 1 ] );\n\t\t\t\tverticesArray.push( placeholder[ index * 3 + 2 ] );\n\n\t\t\t}\n\n\n\t\t\tfunction addUV( vector2 ) {\n\n\t\t\t\tuvArray.push( vector2.x );\n\t\t\t\tuvArray.push( vector2.y );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tconst shapes = this.parameters.shapes;\n\t\tconst options = this.parameters.options;\n\n\t\treturn toJSON$1( shapes, options, data );\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @param {Array<Shape>} shapes - An array of shapes.\n\t * @return {ExtrudeGeometry} A new instance.\n\t */\n\tstatic fromJSON( data, shapes ) {\n\n\t\tconst geometryShapes = [];\n\n\t\tfor ( let j = 0, jl = data.shapes.length; j < jl; j ++ ) {\n\n\t\t\tconst shape = shapes[ data.shapes[ j ] ];\n\n\t\t\tgeometryShapes.push( shape );\n\n\t\t}\n\n\t\tconst extrudePath = data.options.extrudePath;\n\n\t\tif ( extrudePath !== undefined ) {\n\n\t\t\tdata.options.extrudePath = new Curves[ extrudePath.type ]().fromJSON( extrudePath );\n\n\t\t}\n\n\t\treturn new ExtrudeGeometry( geometryShapes, data.options );\n\n\t}\n\n}\n\nconst WorldUVGenerator = {\n\n\tgenerateTopUV: function ( geometry, vertices, indexA, indexB, indexC ) {\n\n\t\tconst a_x = vertices[ indexA * 3 ];\n\t\tconst a_y = vertices[ indexA * 3 + 1 ];\n\t\tconst b_x = vertices[ indexB * 3 ];\n\t\tconst b_y = vertices[ indexB * 3 + 1 ];\n\t\tconst c_x = vertices[ indexC * 3 ];\n\t\tconst c_y = vertices[ indexC * 3 + 1 ];\n\n\t\treturn [\n\t\t\tnew Vector2( a_x, a_y ),\n\t\t\tnew Vector2( b_x, b_y ),\n\t\t\tnew Vector2( c_x, c_y )\n\t\t];\n\n\t},\n\n\tgenerateSideWallUV: function ( geometry, vertices, indexA, indexB, indexC, indexD ) {\n\n\t\tconst a_x = vertices[ indexA * 3 ];\n\t\tconst a_y = vertices[ indexA * 3 + 1 ];\n\t\tconst a_z = vertices[ indexA * 3 + 2 ];\n\t\tconst b_x = vertices[ indexB * 3 ];\n\t\tconst b_y = vertices[ indexB * 3 + 1 ];\n\t\tconst b_z = vertices[ indexB * 3 + 2 ];\n\t\tconst c_x = vertices[ indexC * 3 ];\n\t\tconst c_y = vertices[ indexC * 3 + 1 ];\n\t\tconst c_z = vertices[ indexC * 3 + 2 ];\n\t\tconst d_x = vertices[ indexD * 3 ];\n\t\tconst d_y = vertices[ indexD * 3 + 1 ];\n\t\tconst d_z = vertices[ indexD * 3 + 2 ];\n\n\t\tif ( Math.abs( a_y - b_y ) < Math.abs( a_x - b_x ) ) {\n\n\t\t\treturn [\n\t\t\t\tnew Vector2( a_x, 1 - a_z ),\n\t\t\t\tnew Vector2( b_x, 1 - b_z ),\n\t\t\t\tnew Vector2( c_x, 1 - c_z ),\n\t\t\t\tnew Vector2( d_x, 1 - d_z )\n\t\t\t];\n\n\t\t} else {\n\n\t\t\treturn [\n\t\t\t\tnew Vector2( a_y, 1 - a_z ),\n\t\t\t\tnew Vector2( b_y, 1 - b_z ),\n\t\t\t\tnew Vector2( c_y, 1 - c_z ),\n\t\t\t\tnew Vector2( d_y, 1 - d_z )\n\t\t\t];\n\n\t\t}\n\n\t}\n\n};\n\nfunction toJSON$1( shapes, options, data ) {\n\n\tdata.shapes = [];\n\n\tif ( Array.isArray( shapes ) ) {\n\n\t\tfor ( let i = 0, l = shapes.length; i < l; i ++ ) {\n\n\t\t\tconst shape = shapes[ i ];\n\n\t\t\tdata.shapes.push( shape.uuid );\n\n\t\t}\n\n\t} else {\n\n\t\tdata.shapes.push( shapes.uuid );\n\n\t}\n\n\tdata.options = Object.assign( {}, options );\n\n\tif ( options.extrudePath !== undefined ) data.options.extrudePath = options.extrudePath.toJSON();\n\n\treturn data;\n\n}\n\n/**\n * A geometry class for representing an icosahedron.\n *\n * ```js\n * const geometry = new THREE.IcosahedronGeometry();\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const icosahedron = new THREE.Mesh( geometry, material );\n * scene.add( icosahedron );\n * ```\n *\n * @augments PolyhedronGeometry\n * @demo scenes/geometry-browser.html#IcosahedronGeometry\n */\nclass IcosahedronGeometry extends PolyhedronGeometry {\n\n\t/**\n\t * Constructs a new icosahedron geometry.\n\t *\n\t * @param {number} [radius=1] - Radius of the icosahedron.\n\t * @param {number} [detail=0] - Setting this to a value greater than `0` adds vertices making it no longer a icosahedron.\n\t */\n\tconstructor( radius = 1, detail = 0 ) {\n\n\t\tconst t = ( 1 + Math.sqrt( 5 ) ) / 2;\n\n\t\tconst vertices = [\n\t\t\t-1, t, 0, \t1, t, 0, \t-1, - t, 0, \t1, - t, 0,\n\t\t\t0, -1, t, \t0, 1, t,\t0, -1, - t, \t0, 1, - t,\n\t\t\tt, 0, -1, \tt, 0, 1, \t- t, 0, -1, \t- t, 0, 1\n\t\t];\n\n\t\tconst indices = [\n\t\t\t0, 11, 5, \t0, 5, 1, \t0, 1, 7, \t0, 7, 10, \t0, 10, 11,\n\t\t\t1, 5, 9, \t5, 11, 4,\t11, 10, 2,\t10, 7, 6,\t7, 1, 8,\n\t\t\t3, 9, 4, \t3, 4, 2,\t3, 2, 6,\t3, 6, 8,\t3, 8, 9,\n\t\t\t4, 9, 5, \t2, 4, 11,\t6, 2, 10,\t8, 6, 7,\t9, 8, 1\n\t\t];\n\n\t\tsuper( vertices, indices, radius, detail );\n\n\t\tthis.type = 'IcosahedronGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\tdetail: detail\n\t\t};\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {IcosahedronGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new IcosahedronGeometry( data.radius, data.detail );\n\n\t}\n\n}\n\n/**\n * Creates meshes with axial symmetry like vases. The lathe rotates around the Y axis.\n *\n * ```js\n * const points = [];\n * for ( let i = 0; i < 10; i ++ ) {\n * \tpoints.push( new THREE.Vector2( Math.sin( i * 0.2 ) * 10 + 5, ( i - 5 ) * 2 ) );\n * }\n * const geometry = new THREE.LatheGeometry( points );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const lathe = new THREE.Mesh( geometry, material );\n * scene.add( lathe );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#LatheGeometry\n */\nclass LatheGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new lathe geometry.\n\t *\n\t * @param {Array<Vector2|Vector3>} [points] - An array of points in 2D space. The x-coordinate of each point\n\t * must be greater than zero.\n\t * @param {number} [segments=12] - The number of circumference segments to generate.\n\t * @param {number} [phiStart=0] - The starting angle in radians.\n\t * @param {number} [phiLength=Math.PI*2] - The radian (0 to 2PI) range of the lathed section 2PI is a\n\t * closed lathe, less than 2PI is a portion.\n\t */\n\tconstructor( points = [ new Vector2( 0, -0.5 ), new Vector2( 0.5, 0 ), new Vector2( 0, 0.5 ) ], segments = 12, phiStart = 0, phiLength = Math.PI * 2 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'LatheGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tpoints: points,\n\t\t\tsegments: segments,\n\t\t\tphiStart: phiStart,\n\t\t\tphiLength: phiLength\n\t\t};\n\n\t\tsegments = Math.floor( segments );\n\n\t\t// clamp phiLength so it's in range of [ 0, 2PI ]\n\n\t\tphiLength = clamp( phiLength, 0, Math.PI * 2 );\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst uvs = [];\n\t\tconst initNormals = [];\n\t\tconst normals = [];\n\n\t\t// helper variables\n\n\t\tconst inverseSegments = 1.0 / segments;\n\t\tconst vertex = new Vector3();\n\t\tconst uv = new Vector2();\n\t\tconst normal = new Vector3();\n\t\tconst curNormal = new Vector3();\n\t\tconst prevNormal = new Vector3();\n\t\tlet dx = 0;\n\t\tlet dy = 0;\n\n\t\t// pre-compute normals for initial \"meridian\"\n\n\t\tfor ( let j = 0; j <= ( points.length - 1 ); j ++ ) {\n\n\t\t\tswitch ( j ) {\n\n\t\t\t\tcase 0:\t\t\t\t// special handling for 1st vertex on path\n\n\t\t\t\t\tdx = points[ j + 1 ].x - points[ j ].x;\n\t\t\t\t\tdy = points[ j + 1 ].y - points[ j ].y;\n\n\t\t\t\t\tnormal.x = dy * 1.0;\n\t\t\t\t\tnormal.y = - dx;\n\t\t\t\t\tnormal.z = dy * 0.0;\n\n\t\t\t\t\tprevNormal.copy( normal );\n\n\t\t\t\t\tnormal.normalize();\n\n\t\t\t\t\tinitNormals.push( normal.x, normal.y, normal.z );\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase ( points.length - 1 ):\t// special handling for last Vertex on path\n\n\t\t\t\t\tinitNormals.push( prevNormal.x, prevNormal.y, prevNormal.z );\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\t\t\t// default handling for all vertices in between\n\n\t\t\t\t\tdx = points[ j + 1 ].x - points[ j ].x;\n\t\t\t\t\tdy = points[ j + 1 ].y - points[ j ].y;\n\n\t\t\t\t\tnormal.x = dy * 1.0;\n\t\t\t\t\tnormal.y = - dx;\n\t\t\t\t\tnormal.z = dy * 0.0;\n\n\t\t\t\t\tcurNormal.copy( normal );\n\n\t\t\t\t\tnormal.x += prevNormal.x;\n\t\t\t\t\tnormal.y += prevNormal.y;\n\t\t\t\t\tnormal.z += prevNormal.z;\n\n\t\t\t\t\tnormal.normalize();\n\n\t\t\t\t\tinitNormals.push( normal.x, normal.y, normal.z );\n\n\t\t\t\t\tprevNormal.copy( curNormal );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// generate vertices, uvs and normals\n\n\t\tfor ( let i = 0; i <= segments; i ++ ) {\n\n\t\t\tconst phi = phiStart + i * inverseSegments * phiLength;\n\n\t\t\tconst sin = Math.sin( phi );\n\t\t\tconst cos = Math.cos( phi );\n\n\t\t\tfor ( let j = 0; j <= ( points.length - 1 ); j ++ ) {\n\n\t\t\t\t// vertex\n\n\t\t\t\tvertex.x = points[ j ].x * sin;\n\t\t\t\tvertex.y = points[ j ].y;\n\t\t\t\tvertex.z = points[ j ].x * cos;\n\n\t\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t\t// uv\n\n\t\t\t\tuv.x = i / segments;\n\t\t\t\tuv.y = j / ( points.length - 1 );\n\n\t\t\t\tuvs.push( uv.x, uv.y );\n\n\t\t\t\t// normal\n\n\t\t\t\tconst x = initNormals[ 3 * j + 0 ] * sin;\n\t\t\t\tconst y = initNormals[ 3 * j + 1 ];\n\t\t\t\tconst z = initNormals[ 3 * j + 0 ] * cos;\n\n\t\t\t\tnormals.push( x, y, z );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// indices\n\n\t\tfor ( let i = 0; i < segments; i ++ ) {\n\n\t\t\tfor ( let j = 0; j < ( points.length - 1 ); j ++ ) {\n\n\t\t\t\tconst base = j + i * points.length;\n\n\t\t\t\tconst a = base;\n\t\t\t\tconst b = base + points.length;\n\t\t\t\tconst c = base + points.length + 1;\n\t\t\t\tconst d = base + 1;\n\n\t\t\t\t// faces\n\n\t\t\t\tindices.push( a, b, d );\n\t\t\t\tindices.push( c, d, b );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {LatheGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new LatheGeometry( data.points, data.segments, data.phiStart, data.phiLength );\n\n\t}\n\n}\n\n/**\n * A geometry class for representing an octahedron.\n *\n * ```js\n * const geometry = new THREE.OctahedronGeometry();\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const octahedron = new THREE.Mesh( geometry, material );\n * scene.add( octahedron );\n * ```\n *\n * @augments PolyhedronGeometry\n * @demo scenes/geometry-browser.html#OctahedronGeometry\n */\nclass OctahedronGeometry extends PolyhedronGeometry {\n\n\t/**\n\t * Constructs a new octahedron geometry.\n\t *\n\t * @param {number} [radius=1] - Radius of the octahedron.\n\t * @param {number} [detail=0] - Setting this to a value greater than `0` adds vertices making it no longer a octahedron.\n\t */\n\tconstructor( radius = 1, detail = 0 ) {\n\n\t\tconst vertices = [\n\t\t\t1, 0, 0, \t-1, 0, 0,\t0, 1, 0,\n\t\t\t0, -1, 0, \t0, 0, 1,\t0, 0, -1\n\t\t];\n\n\t\tconst indices = [\n\t\t\t0, 2, 4,\t0, 4, 3,\t0, 3, 5,\n\t\t\t0, 5, 2,\t1, 2, 5,\t1, 5, 3,\n\t\t\t1, 3, 4,\t1, 4, 2\n\t\t];\n\n\t\tsuper( vertices, indices, radius, detail );\n\n\t\tthis.type = 'OctahedronGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\tdetail: detail\n\t\t};\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {OctahedronGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new OctahedronGeometry( data.radius, data.detail );\n\n\t}\n\n}\n\n/**\n * A geometry class for representing a plane.\n *\n * ```js\n * const geometry = new THREE.PlaneGeometry( 1, 1 );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00, side: THREE.DoubleSide } );\n * const plane = new THREE.Mesh( geometry, material );\n * scene.add( plane );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#PlaneGeometry\n */\nclass PlaneGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new plane geometry.\n\t *\n\t * @param {number} [width=1] - The width along the X axis.\n\t * @param {number} [height=1] - The height along the Y axis\n\t * @param {number} [widthSegments=1] - The number of segments along the X axis.\n\t * @param {number} [heightSegments=1] - The number of segments along the Y axis.\n\t */\n\tconstructor( width = 1, height = 1, widthSegments = 1, heightSegments = 1 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'PlaneGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\twidth: width,\n\t\t\theight: height,\n\t\t\twidthSegments: widthSegments,\n\t\t\theightSegments: heightSegments\n\t\t};\n\n\t\tconst width_half = width / 2;\n\t\tconst height_half = height / 2;\n\n\t\tconst gridX = Math.floor( widthSegments );\n\t\tconst gridY = Math.floor( heightSegments );\n\n\t\tconst gridX1 = gridX + 1;\n\t\tconst gridY1 = gridY + 1;\n\n\t\tconst segment_width = width / gridX;\n\t\tconst segment_height = height / gridY;\n\n\t\t//\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\tfor ( let iy = 0; iy < gridY1; iy ++ ) {\n\n\t\t\tconst y = iy * segment_height - height_half;\n\n\t\t\tfor ( let ix = 0; ix < gridX1; ix ++ ) {\n\n\t\t\t\tconst x = ix * segment_width - width_half;\n\n\t\t\t\tvertices.push( x, - y, 0 );\n\n\t\t\t\tnormals.push( 0, 0, 1 );\n\n\t\t\t\tuvs.push( ix / gridX );\n\t\t\t\tuvs.push( 1 - ( iy / gridY ) );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfor ( let iy = 0; iy < gridY; iy ++ ) {\n\n\t\t\tfor ( let ix = 0; ix < gridX; ix ++ ) {\n\n\t\t\t\tconst a = ix + gridX1 * iy;\n\t\t\t\tconst b = ix + gridX1 * ( iy + 1 );\n\t\t\t\tconst c = ( ix + 1 ) + gridX1 * ( iy + 1 );\n\t\t\t\tconst d = ( ix + 1 ) + gridX1 * iy;\n\n\t\t\t\tindices.push( a, b, d );\n\t\t\t\tindices.push( b, c, d );\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {PlaneGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new PlaneGeometry( data.width, data.height, data.widthSegments, data.heightSegments );\n\n\t}\n\n}\n\n/**\n * A class for generating a two-dimensional ring geometry.\n *\n * ```js\n * const geometry = new THREE.RingGeometry( 1, 5, 32 );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00, side: THREE.DoubleSide } );\n * const mesh = new THREE.Mesh( geometry, material );\n * scene.add( mesh );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#RingGeometry\n */\nclass RingGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new ring geometry.\n\t *\n\t * @param {number} [innerRadius=0.5] - The inner radius of the ring.\n\t * @param {number} [outerRadius=1] - The outer radius of the ring.\n\t * @param {number} [thetaSegments=32] - Number of segments. A higher number means the ring will be more round. Minimum is `3`.\n\t * @param {number} [phiSegments=1] - Number of segments per ring segment. Minimum is `1`.\n\t * @param {number} [thetaStart=0] - Starting angle in radians.\n\t * @param {number} [thetaLength=Math.PI*2] - Central angle in radians.\n\t */\n\tconstructor( innerRadius = 0.5, outerRadius = 1, thetaSegments = 32, phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'RingGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tinnerRadius: innerRadius,\n\t\t\touterRadius: outerRadius,\n\t\t\tthetaSegments: thetaSegments,\n\t\t\tphiSegments: phiSegments,\n\t\t\tthetaStart: thetaStart,\n\t\t\tthetaLength: thetaLength\n\t\t};\n\n\t\tthetaSegments = Math.max( 3, thetaSegments );\n\t\tphiSegments = Math.max( 1, phiSegments );\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\t// some helper variables\n\n\t\tlet radius = innerRadius;\n\t\tconst radiusStep = ( ( outerRadius - innerRadius ) / phiSegments );\n\t\tconst vertex = new Vector3();\n\t\tconst uv = new Vector2();\n\n\t\t// generate vertices, normals and uvs\n\n\t\tfor ( let j = 0; j <= phiSegments; j ++ ) {\n\n\t\t\tfor ( let i = 0; i <= thetaSegments; i ++ ) {\n\n\t\t\t\t// values are generate from the inside of the ring to the outside\n\n\t\t\t\tconst segment = thetaStart + i / thetaSegments * thetaLength;\n\n\t\t\t\t// vertex\n\n\t\t\t\tvertex.x = radius * Math.cos( segment );\n\t\t\t\tvertex.y = radius * Math.sin( segment );\n\n\t\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t\t// normal\n\n\t\t\t\tnormals.push( 0, 0, 1 );\n\n\t\t\t\t// uv\n\n\t\t\t\tuv.x = ( vertex.x / outerRadius + 1 ) / 2;\n\t\t\t\tuv.y = ( vertex.y / outerRadius + 1 ) / 2;\n\n\t\t\t\tuvs.push( uv.x, uv.y );\n\n\t\t\t}\n\n\t\t\t// increase the radius for next row of vertices\n\n\t\t\tradius += radiusStep;\n\n\t\t}\n\n\t\t// indices\n\n\t\tfor ( let j = 0; j < phiSegments; j ++ ) {\n\n\t\t\tconst thetaSegmentLevel = j * ( thetaSegments + 1 );\n\n\t\t\tfor ( let i = 0; i < thetaSegments; i ++ ) {\n\n\t\t\t\tconst segment = i + thetaSegmentLevel;\n\n\t\t\t\tconst a = segment;\n\t\t\t\tconst b = segment + thetaSegments + 1;\n\t\t\t\tconst c = segment + thetaSegments + 2;\n\t\t\t\tconst d = segment + 1;\n\n\t\t\t\t// faces\n\n\t\t\t\tindices.push( a, b, d );\n\t\t\t\tindices.push( b, c, d );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {RingGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new RingGeometry( data.innerRadius, data.outerRadius, data.thetaSegments, data.phiSegments, data.thetaStart, data.thetaLength );\n\n\t}\n\n}\n\n/**\n * Creates an one-sided polygonal geometry from one or more path shapes.\n *\n * ```js\n * const arcShape = new THREE.Shape()\n *\t.moveTo( 5, 1 )\n *\t.absarc( 1, 1, 4, 0, Math.PI * 2, false );\n *\n * const geometry = new THREE.ShapeGeometry( arcShape );\n * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00, side: THREE.DoubleSide } );\n * const mesh = new THREE.Mesh( geometry, material ) ;\n * scene.add( mesh );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#ShapeGeometry\n */\nclass ShapeGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new shape geometry.\n\t *\n\t * @param {Shape|Array<Shape>} [shapes] - A shape or an array of shapes.\n\t * @param {number} [curveSegments=12] - Number of segments per shape.\n\t */\n\tconstructor( shapes = new Shape( [ new Vector2( 0, 0.5 ), new Vector2( -0.5, -0.5 ), new Vector2( 0.5, -0.5 ) ] ), curveSegments = 12 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'ShapeGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tshapes: shapes,\n\t\t\tcurveSegments: curveSegments\n\t\t};\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\t// helper variables\n\n\t\tlet groupStart = 0;\n\t\tlet groupCount = 0;\n\n\t\t// allow single and array values for \"shapes\" parameter\n\n\t\tif ( Array.isArray( shapes ) === false ) {\n\n\t\t\taddShape( shapes );\n\n\t\t} else {\n\n\t\t\tfor ( let i = 0; i < shapes.length; i ++ ) {\n\n\t\t\t\taddShape( shapes[ i ] );\n\n\t\t\t\tthis.addGroup( groupStart, groupCount, i ); // enables MultiMaterial support\n\n\t\t\t\tgroupStart += groupCount;\n\t\t\t\tgroupCount = 0;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\n\t\t// helper functions\n\n\t\tfunction addShape( shape ) {\n\n\t\t\tconst indexOffset = vertices.length / 3;\n\t\t\tconst points = shape.extractPoints( curveSegments );\n\n\t\t\tlet shapeVertices = points.shape;\n\t\t\tconst shapeHoles = points.holes;\n\n\t\t\t// check direction of vertices\n\n\t\t\tif ( ShapeUtils.isClockWise( shapeVertices ) === false ) {\n\n\t\t\t\tshapeVertices = shapeVertices.reverse();\n\n\t\t\t}\n\n\t\t\tfor ( let i = 0, l = shapeHoles.length; i < l; i ++ ) {\n\n\t\t\t\tconst shapeHole = shapeHoles[ i ];\n\n\t\t\t\tif ( ShapeUtils.isClockWise( shapeHole ) === true ) {\n\n\t\t\t\t\tshapeHoles[ i ] = shapeHole.reverse();\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tconst faces = ShapeUtils.triangulateShape( shapeVertices, shapeHoles );\n\n\t\t\t// join vertices of inner and outer paths to a single array\n\n\t\t\tfor ( let i = 0, l = shapeHoles.length; i < l; i ++ ) {\n\n\t\t\t\tconst shapeHole = shapeHoles[ i ];\n\t\t\t\tshapeVertices = shapeVertices.concat( shapeHole );\n\n\t\t\t}\n\n\t\t\t// vertices, normals, uvs\n\n\t\t\tfor ( let i = 0, l = shapeVertices.length; i < l; i ++ ) {\n\n\t\t\t\tconst vertex = shapeVertices[ i ];\n\n\t\t\t\tvertices.push( vertex.x, vertex.y, 0 );\n\t\t\t\tnormals.push( 0, 0, 1 );\n\t\t\t\tuvs.push( vertex.x, vertex.y ); // world uvs\n\n\t\t\t}\n\n\t\t\t// indices\n\n\t\t\tfor ( let i = 0, l = faces.length; i < l; i ++ ) {\n\n\t\t\t\tconst face = faces[ i ];\n\n\t\t\t\tconst a = face[ 0 ] + indexOffset;\n\t\t\t\tconst b = face[ 1 ] + indexOffset;\n\t\t\t\tconst c = face[ 2 ] + indexOffset;\n\n\t\t\t\tindices.push( a, b, c );\n\t\t\t\tgroupCount += 3;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tconst shapes = this.parameters.shapes;\n\n\t\treturn toJSON( shapes, data );\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @param {Array<Shape>} shapes - An array of shapes.\n\t * @return {ShapeGeometry} A new instance.\n\t */\n\tstatic fromJSON( data, shapes ) {\n\n\t\tconst geometryShapes = [];\n\n\t\tfor ( let j = 0, jl = data.shapes.length; j < jl; j ++ ) {\n\n\t\t\tconst shape = shapes[ data.shapes[ j ] ];\n\n\t\t\tgeometryShapes.push( shape );\n\n\t\t}\n\n\t\treturn new ShapeGeometry( geometryShapes, data.curveSegments );\n\n\t}\n\n}\n\nfunction toJSON( shapes, data ) {\n\n\tdata.shapes = [];\n\n\tif ( Array.isArray( shapes ) ) {\n\n\t\tfor ( let i = 0, l = shapes.length; i < l; i ++ ) {\n\n\t\t\tconst shape = shapes[ i ];\n\n\t\t\tdata.shapes.push( shape.uuid );\n\n\t\t}\n\n\t} else {\n\n\t\tdata.shapes.push( shapes.uuid );\n\n\t}\n\n\treturn data;\n\n}\n\n/**\n * A class for generating a sphere geometry.\n *\n * ```js\n * const geometry = new THREE.SphereGeometry( 15, 32, 16 );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const sphere = new THREE.Mesh( geometry, material );\n * scene.add( sphere );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#SphereGeometry\n */\nclass SphereGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new sphere geometry.\n\t *\n\t * @param {number} [radius=1] - The sphere radius.\n\t * @param {number} [widthSegments=32] - The number of horizontal segments. Minimum value is `3`.\n\t * @param {number} [heightSegments=16] - The number of vertical segments. Minimum value is `2`.\n\t * @param {number} [phiStart=0] - The horizontal starting angle in radians.\n\t * @param {number} [phiLength=Math.PI*2] - The horizontal sweep angle size.\n\t * @param {number} [thetaStart=0] - The vertical starting angle in radians.\n\t * @param {number} [thetaLength=Math.PI] - The vertical sweep angle size.\n\t */\n\tconstructor( radius = 1, widthSegments = 32, heightSegments = 16, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'SphereGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\twidthSegments: widthSegments,\n\t\t\theightSegments: heightSegments,\n\t\t\tphiStart: phiStart,\n\t\t\tphiLength: phiLength,\n\t\t\tthetaStart: thetaStart,\n\t\t\tthetaLength: thetaLength\n\t\t};\n\n\t\twidthSegments = Math.max( 3, Math.floor( widthSegments ) );\n\t\theightSegments = Math.max( 2, Math.floor( heightSegments ) );\n\n\t\tconst thetaEnd = Math.min( thetaStart + thetaLength, Math.PI );\n\n\t\tlet index = 0;\n\t\tconst grid = [];\n\n\t\tconst vertex = new Vector3();\n\t\tconst normal = new Vector3();\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\t// generate vertices, normals and uvs\n\n\t\tfor ( let iy = 0; iy <= heightSegments; iy ++ ) {\n\n\t\t\tconst verticesRow = [];\n\n\t\t\tconst v = iy / heightSegments;\n\n\t\t\t// special case for the poles\n\n\t\t\tlet uOffset = 0;\n\n\t\t\tif ( iy === 0 && thetaStart === 0 ) {\n\n\t\t\t\tuOffset = 0.5 / widthSegments;\n\n\t\t\t} else if ( iy === heightSegments && thetaEnd === Math.PI ) {\n\n\t\t\t\tuOffset = -0.5 / widthSegments;\n\n\t\t\t}\n\n\t\t\tfor ( let ix = 0; ix <= widthSegments; ix ++ ) {\n\n\t\t\t\tconst u = ix / widthSegments;\n\n\t\t\t\t// vertex\n\n\t\t\t\tvertex.x = - radius * Math.cos( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );\n\t\t\t\tvertex.y = radius * Math.cos( thetaStart + v * thetaLength );\n\t\t\t\tvertex.z = radius * Math.sin( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );\n\n\t\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t\t// normal\n\n\t\t\t\tnormal.copy( vertex ).normalize();\n\t\t\t\tnormals.push( normal.x, normal.y, normal.z );\n\n\t\t\t\t// uv\n\n\t\t\t\tuvs.push( u + uOffset, 1 - v );\n\n\t\t\t\tverticesRow.push( index ++ );\n\n\t\t\t}\n\n\t\t\tgrid.push( verticesRow );\n\n\t\t}\n\n\t\t// indices\n\n\t\tfor ( let iy = 0; iy < heightSegments; iy ++ ) {\n\n\t\t\tfor ( let ix = 0; ix < widthSegments; ix ++ ) {\n\n\t\t\t\tconst a = grid[ iy ][ ix + 1 ];\n\t\t\t\tconst b = grid[ iy ][ ix ];\n\t\t\t\tconst c = grid[ iy + 1 ][ ix ];\n\t\t\t\tconst d = grid[ iy + 1 ][ ix + 1 ];\n\n\t\t\t\tif ( iy !== 0 || thetaStart > 0 ) indices.push( a, b, d );\n\t\t\t\tif ( iy !== heightSegments - 1 || thetaEnd < Math.PI ) indices.push( b, c, d );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {SphereGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new SphereGeometry( data.radius, data.widthSegments, data.heightSegments, data.phiStart, data.phiLength, data.thetaStart, data.thetaLength );\n\n\t}\n\n}\n\n/**\n * A geometry class for representing an tetrahedron.\n *\n * ```js\n * const geometry = new THREE.TetrahedronGeometry();\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const tetrahedron = new THREE.Mesh( geometry, material );\n * scene.add( tetrahedron );\n * ```\n *\n * @augments PolyhedronGeometry\n * @demo scenes/geometry-browser.html#TetrahedronGeometry\n */\nclass TetrahedronGeometry extends PolyhedronGeometry {\n\n\t/**\n\t * Constructs a new tetrahedron geometry.\n\t *\n\t * @param {number} [radius=1] - Radius of the tetrahedron.\n\t * @param {number} [detail=0] - Setting this to a value greater than `0` adds vertices making it no longer a tetrahedron.\n\t */\n\tconstructor( radius = 1, detail = 0 ) {\n\n\t\tconst vertices = [\n\t\t\t1, 1, 1, \t-1, -1, 1, \t-1, 1, -1, \t1, -1, -1\n\t\t];\n\n\t\tconst indices = [\n\t\t\t2, 1, 0, \t0, 3, 2,\t1, 3, 0,\t2, 3, 1\n\t\t];\n\n\t\tsuper( vertices, indices, radius, detail );\n\n\t\tthis.type = 'TetrahedronGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\tdetail: detail\n\t\t};\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {TetrahedronGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new TetrahedronGeometry( data.radius, data.detail );\n\n\t}\n\n}\n\n/**\n * A geometry class for representing an torus.\n *\n * ```js\n * const geometry = new THREE.TorusGeometry( 10, 3, 16, 100 );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const torus = new THREE.Mesh( geometry, material );\n * scene.add( torus );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#TorusGeometry\n */\nclass TorusGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new torus geometry.\n\t *\n\t * @param {number} [radius=1] - Radius of the torus, from the center of the torus to the center of the tube.\n\t * @param {number} [tube=0.4] - Radius of the tube. Must be smaller than `radius`.\n\t * @param {number} [radialSegments=12] - The number of radial segments.\n\t * @param {number} [tubularSegments=48] - The number of tubular segments.\n\t * @param {number} [arc=Math.PI*2] - Central angle in radians.\n\t */\n\tconstructor( radius = 1, tube = 0.4, radialSegments = 12, tubularSegments = 48, arc = Math.PI * 2 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'TorusGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\ttube: tube,\n\t\t\tradialSegments: radialSegments,\n\t\t\ttubularSegments: tubularSegments,\n\t\t\tarc: arc\n\t\t};\n\n\t\tradialSegments = Math.floor( radialSegments );\n\t\ttubularSegments = Math.floor( tubularSegments );\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\t// helper variables\n\n\t\tconst center = new Vector3();\n\t\tconst vertex = new Vector3();\n\t\tconst normal = new Vector3();\n\n\t\t// generate vertices, normals and uvs\n\n\t\tfor ( let j = 0; j <= radialSegments; j ++ ) {\n\n\t\t\tfor ( let i = 0; i <= tubularSegments; i ++ ) {\n\n\t\t\t\tconst u = i / tubularSegments * arc;\n\t\t\t\tconst v = j / radialSegments * Math.PI * 2;\n\n\t\t\t\t// vertex\n\n\t\t\t\tvertex.x = ( radius + tube * Math.cos( v ) ) * Math.cos( u );\n\t\t\t\tvertex.y = ( radius + tube * Math.cos( v ) ) * Math.sin( u );\n\t\t\t\tvertex.z = tube * Math.sin( v );\n\n\t\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t\t// normal\n\n\t\t\t\tcenter.x = radius * Math.cos( u );\n\t\t\t\tcenter.y = radius * Math.sin( u );\n\t\t\t\tnormal.subVectors( vertex, center ).normalize();\n\n\t\t\t\tnormals.push( normal.x, normal.y, normal.z );\n\n\t\t\t\t// uv\n\n\t\t\t\tuvs.push( i / tubularSegments );\n\t\t\t\tuvs.push( j / radialSegments );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// generate indices\n\n\t\tfor ( let j = 1; j <= radialSegments; j ++ ) {\n\n\t\t\tfor ( let i = 1; i <= tubularSegments; i ++ ) {\n\n\t\t\t\t// indices\n\n\t\t\t\tconst a = ( tubularSegments + 1 ) * j + i - 1;\n\t\t\t\tconst b = ( tubularSegments + 1 ) * ( j - 1 ) + i - 1;\n\t\t\t\tconst c = ( tubularSegments + 1 ) * ( j - 1 ) + i;\n\t\t\t\tconst d = ( tubularSegments + 1 ) * j + i;\n\n\t\t\t\t// faces\n\n\t\t\t\tindices.push( a, b, d );\n\t\t\t\tindices.push( b, c, d );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {TorusGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new TorusGeometry( data.radius, data.tube, data.radialSegments, data.tubularSegments, data.arc );\n\n\t}\n\n}\n\n/**\n * Creates a torus knot, the particular shape of which is defined by a pair\n * of coprime integers, p and q. If p and q are not coprime, the result will\n * be a torus link.\n *\n * ```js\n * const geometry = new THREE.TorusKnotGeometry( 10, 3, 100, 16 );\n * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );\n * const torusKnot = new THREE.Mesh( geometry, material );\n * scene.add( torusKnot );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#TorusKnotGeometry\n */\nclass TorusKnotGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new torus knot geometry.\n\t *\n\t * @param {number} [radius=1] - Radius of the torus knot.\n\t * @param {number} [tube=0.4] - Radius of the tube.\n\t * @param {number} [tubularSegments=64] - The number of tubular segments.\n\t * @param {number} [radialSegments=8] - The number of radial segments.\n\t * @param {number} [p=2] - This value determines, how many times the geometry winds around its axis of rotational symmetry.\n\t * @param {number} [q=3] - This value determines, how many times the geometry winds around a circle in the interior of the torus.\n\t */\n\tconstructor( radius = 1, tube = 0.4, tubularSegments = 64, radialSegments = 8, p = 2, q = 3 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'TorusKnotGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tradius: radius,\n\t\t\ttube: tube,\n\t\t\ttubularSegments: tubularSegments,\n\t\t\tradialSegments: radialSegments,\n\t\t\tp: p,\n\t\t\tq: q\n\t\t};\n\n\t\ttubularSegments = Math.floor( tubularSegments );\n\t\tradialSegments = Math.floor( radialSegments );\n\n\t\t// buffers\n\n\t\tconst indices = [];\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\n\t\t// helper variables\n\n\t\tconst vertex = new Vector3();\n\t\tconst normal = new Vector3();\n\n\t\tconst P1 = new Vector3();\n\t\tconst P2 = new Vector3();\n\n\t\tconst B = new Vector3();\n\t\tconst T = new Vector3();\n\t\tconst N = new Vector3();\n\n\t\t// generate vertices, normals and uvs\n\n\t\tfor ( let i = 0; i <= tubularSegments; ++ i ) {\n\n\t\t\t// the radian \"u\" is used to calculate the position on the torus curve of the current tubular segment\n\n\t\t\tconst u = i / tubularSegments * p * Math.PI * 2;\n\n\t\t\t// now we calculate two points. P1 is our current position on the curve, P2 is a little farther ahead.\n\t\t\t// these points are used to create a special \"coordinate space\", which is necessary to calculate the correct vertex positions\n\n\t\t\tcalculatePositionOnCurve( u, p, q, radius, P1 );\n\t\t\tcalculatePositionOnCurve( u + 0.01, p, q, radius, P2 );\n\n\t\t\t// calculate orthonormal basis\n\n\t\t\tT.subVectors( P2, P1 );\n\t\t\tN.addVectors( P2, P1 );\n\t\t\tB.crossVectors( T, N );\n\t\t\tN.crossVectors( B, T );\n\n\t\t\t// normalize B, N. T can be ignored, we don't use it\n\n\t\t\tB.normalize();\n\t\t\tN.normalize();\n\n\t\t\tfor ( let j = 0; j <= radialSegments; ++ j ) {\n\n\t\t\t\t// now calculate the vertices. they are nothing more than an extrusion of the torus curve.\n\t\t\t\t// because we extrude a shape in the xy-plane, there is no need to calculate a z-value.\n\n\t\t\t\tconst v = j / radialSegments * Math.PI * 2;\n\t\t\t\tconst cx = - tube * Math.cos( v );\n\t\t\t\tconst cy = tube * Math.sin( v );\n\n\t\t\t\t// now calculate the final vertex position.\n\t\t\t\t// first we orient the extrusion with our basis vectors, then we add it to the current position on the curve\n\n\t\t\t\tvertex.x = P1.x + ( cx * N.x + cy * B.x );\n\t\t\t\tvertex.y = P1.y + ( cx * N.y + cy * B.y );\n\t\t\t\tvertex.z = P1.z + ( cx * N.z + cy * B.z );\n\n\t\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t\t// normal (P1 is always the center/origin of the extrusion, thus we can use it to calculate the normal)\n\n\t\t\t\tnormal.subVectors( vertex, P1 ).normalize();\n\n\t\t\t\tnormals.push( normal.x, normal.y, normal.z );\n\n\t\t\t\t// uv\n\n\t\t\t\tuvs.push( i / tubularSegments );\n\t\t\t\tuvs.push( j / radialSegments );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// generate indices\n\n\t\tfor ( let j = 1; j <= tubularSegments; j ++ ) {\n\n\t\t\tfor ( let i = 1; i <= radialSegments; i ++ ) {\n\n\t\t\t\t// indices\n\n\t\t\t\tconst a = ( radialSegments + 1 ) * ( j - 1 ) + ( i - 1 );\n\t\t\t\tconst b = ( radialSegments + 1 ) * j + ( i - 1 );\n\t\t\t\tconst c = ( radialSegments + 1 ) * j + i;\n\t\t\t\tconst d = ( radialSegments + 1 ) * ( j - 1 ) + i;\n\n\t\t\t\t// faces\n\n\t\t\t\tindices.push( a, b, d );\n\t\t\t\tindices.push( b, c, d );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t\t// this function calculates the current position on the torus curve\n\n\t\tfunction calculatePositionOnCurve( u, p, q, radius, position ) {\n\n\t\t\tconst cu = Math.cos( u );\n\t\t\tconst su = Math.sin( u );\n\t\t\tconst quOverP = q / p * u;\n\t\t\tconst cs = Math.cos( quOverP );\n\n\t\t\tposition.x = radius * ( 2 + cs ) * 0.5 * cu;\n\t\t\tposition.y = radius * ( 2 + cs ) * su * 0.5;\n\t\t\tposition.z = radius * Math.sin( quOverP ) * 0.5;\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {TorusKnotGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\treturn new TorusKnotGeometry( data.radius, data.tube, data.tubularSegments, data.radialSegments, data.p, data.q );\n\n\t}\n\n}\n\n/**\n * Creates a tube that extrudes along a 3D curve.\n *\n * ```js\n * class CustomSinCurve extends THREE.Curve {\n *\n * \tgetPoint( t, optionalTarget = new THREE.Vector3() ) {\n *\n * \t\tconst tx = t * 3 - 1.5;\n * \t\tconst ty = Math.sin( 2 * Math.PI * t );\n * \t\tconst tz = 0;\n *\n * \t\treturn optionalTarget.set( tx, ty, tz );\n * \t}\n *\n * }\n *\n * const path = new CustomSinCurve( 10 );\n * const geometry = new THREE.TubeGeometry( path, 20, 2, 8, false );\n * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );\n * const mesh = new THREE.Mesh( geometry, material );\n * scene.add( mesh );\n * ```\n *\n * @augments BufferGeometry\n * @demo scenes/geometry-browser.html#TubeGeometry\n */\nclass TubeGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new tube geometry.\n\t *\n\t * @param {Curve} [path=QuadraticBezierCurve3] - A 3D curve defining the path of the tube.\n\t * @param {number} [tubularSegments=64] - The number of segments that make up the tube.\n\t * @param {number} [radius=1] -The radius of the tube.\n\t * @param {number} [radialSegments=8] - The number of segments that make up the cross-section.\n\t * @param {boolean} [closed=false] - Whether the tube is closed or not.\n\t */\n\tconstructor( path = new QuadraticBezierCurve3( new Vector3( -1, -1, 0 ), new Vector3( -1, 1, 0 ), new Vector3( 1, 1, 0 ) ), tubularSegments = 64, radius = 1, radialSegments = 8, closed = false ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'TubeGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tpath: path,\n\t\t\ttubularSegments: tubularSegments,\n\t\t\tradius: radius,\n\t\t\tradialSegments: radialSegments,\n\t\t\tclosed: closed\n\t\t};\n\n\t\tconst frames = path.computeFrenetFrames( tubularSegments, closed );\n\n\t\t// expose internals\n\n\t\tthis.tangents = frames.tangents;\n\t\tthis.normals = frames.normals;\n\t\tthis.binormals = frames.binormals;\n\n\t\t// helper variables\n\n\t\tconst vertex = new Vector3();\n\t\tconst normal = new Vector3();\n\t\tconst uv = new Vector2();\n\t\tlet P = new Vector3();\n\n\t\t// buffer\n\n\t\tconst vertices = [];\n\t\tconst normals = [];\n\t\tconst uvs = [];\n\t\tconst indices = [];\n\n\t\t// create buffer data\n\n\t\tgenerateBufferData();\n\n\t\t// build geometry\n\n\t\tthis.setIndex( indices );\n\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tthis.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );\n\t\tthis.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );\n\n\t\t// functions\n\n\t\tfunction generateBufferData() {\n\n\t\t\tfor ( let i = 0; i < tubularSegments; i ++ ) {\n\n\t\t\t\tgenerateSegment( i );\n\n\t\t\t}\n\n\t\t\t// if the geometry is not closed, generate the last row of vertices and normals\n\t\t\t// at the regular position on the given path\n\t\t\t//\n\t\t\t// if the geometry is closed, duplicate the first row of vertices and normals (uvs will differ)\n\n\t\t\tgenerateSegment( ( closed === false ) ? tubularSegments : 0 );\n\n\t\t\t// uvs are generated in a separate function.\n\t\t\t// this makes it easy compute correct values for closed geometries\n\n\t\t\tgenerateUVs();\n\n\t\t\t// finally create faces\n\n\t\t\tgenerateIndices();\n\n\t\t}\n\n\t\tfunction generateSegment( i ) {\n\n\t\t\t// we use getPointAt to sample evenly distributed points from the given path\n\n\t\t\tP = path.getPointAt( i / tubularSegments, P );\n\n\t\t\t// retrieve corresponding normal and binormal\n\n\t\t\tconst N = frames.normals[ i ];\n\t\t\tconst B = frames.binormals[ i ];\n\n\t\t\t// generate normals and vertices for the current segment\n\n\t\t\tfor ( let j = 0; j <= radialSegments; j ++ ) {\n\n\t\t\t\tconst v = j / radialSegments * Math.PI * 2;\n\n\t\t\t\tconst sin = Math.sin( v );\n\t\t\t\tconst cos = - Math.cos( v );\n\n\t\t\t\t// normal\n\n\t\t\t\tnormal.x = ( cos * N.x + sin * B.x );\n\t\t\t\tnormal.y = ( cos * N.y + sin * B.y );\n\t\t\t\tnormal.z = ( cos * N.z + sin * B.z );\n\t\t\t\tnormal.normalize();\n\n\t\t\t\tnormals.push( normal.x, normal.y, normal.z );\n\n\t\t\t\t// vertex\n\n\t\t\t\tvertex.x = P.x + radius * normal.x;\n\t\t\t\tvertex.y = P.y + radius * normal.y;\n\t\t\t\tvertex.z = P.z + radius * normal.z;\n\n\t\t\t\tvertices.push( vertex.x, vertex.y, vertex.z );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction generateIndices() {\n\n\t\t\tfor ( let j = 1; j <= tubularSegments; j ++ ) {\n\n\t\t\t\tfor ( let i = 1; i <= radialSegments; i ++ ) {\n\n\t\t\t\t\tconst a = ( radialSegments + 1 ) * ( j - 1 ) + ( i - 1 );\n\t\t\t\t\tconst b = ( radialSegments + 1 ) * j + ( i - 1 );\n\t\t\t\t\tconst c = ( radialSegments + 1 ) * j + i;\n\t\t\t\t\tconst d = ( radialSegments + 1 ) * ( j - 1 ) + i;\n\n\t\t\t\t\t// faces\n\n\t\t\t\t\tindices.push( a, b, d );\n\t\t\t\t\tindices.push( b, c, d );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction generateUVs() {\n\n\t\t\tfor ( let i = 0; i <= tubularSegments; i ++ ) {\n\n\t\t\t\tfor ( let j = 0; j <= radialSegments; j ++ ) {\n\n\t\t\t\t\tuv.x = i / tubularSegments;\n\t\t\t\t\tuv.y = j / radialSegments;\n\n\t\t\t\t\tuvs.push( uv.x, uv.y );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.path = this.parameters.path.toJSON();\n\n\t\treturn data;\n\n\t}\n\n\t/**\n\t * Factory method for creating an instance of this class from the given\n\t * JSON object.\n\t *\n\t * @param {Object} data - A JSON object representing the serialized geometry.\n\t * @return {TubeGeometry} A new instance.\n\t */\n\tstatic fromJSON( data ) {\n\n\t\t// This only works for built-in curves (e.g. CatmullRomCurve3).\n\t\t// User defined curves or instances of CurvePath will not be deserialized.\n\t\treturn new TubeGeometry(\n\t\t\tnew Curves[ data.path.type ]().fromJSON( data.path ),\n\t\t\tdata.tubularSegments,\n\t\t\tdata.radius,\n\t\t\tdata.radialSegments,\n\t\t\tdata.closed\n\t\t);\n\n\t}\n\n}\n\n/**\n * Can be used as a helper object to visualize a geometry as a wireframe.\n *\n * ```js\n * const geometry = new THREE.SphereGeometry();\n *\n * const wireframe = new THREE.WireframeGeometry( geometry );\n *\n * const line = new THREE.LineSegments( wireframe );\n * line.material.depthWrite = false;\n * line.material.opacity = 0.25;\n * line.material.transparent = true;\n *\n * scene.add( line );\n * ```\n *\n * Note: It is not yet possible to serialize/deserialize instances of this class.\n *\n * @augments BufferGeometry\n */\nclass WireframeGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new wireframe geometry.\n\t *\n\t * @param {?BufferGeometry} [geometry=null] - The geometry.\n\t */\n\tconstructor( geometry = null ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'WireframeGeometry';\n\n\t\t/**\n\t\t * Holds the constructor parameters that have been\n\t\t * used to generate the geometry. Any modification\n\t\t * after instantiation does not change the geometry.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parameters = {\n\t\t\tgeometry: geometry\n\t\t};\n\n\t\tif ( geometry !== null ) {\n\n\t\t\t// buffer\n\n\t\t\tconst vertices = [];\n\t\t\tconst edges = new Set();\n\n\t\t\t// helper variables\n\n\t\t\tconst start = new Vector3();\n\t\t\tconst end = new Vector3();\n\n\t\t\tif ( geometry.index !== null ) {\n\n\t\t\t\t// indexed BufferGeometry\n\n\t\t\t\tconst position = geometry.attributes.position;\n\t\t\t\tconst indices = geometry.index;\n\t\t\t\tlet groups = geometry.groups;\n\n\t\t\t\tif ( groups.length === 0 ) {\n\n\t\t\t\t\tgroups = [ { start: 0, count: indices.count, materialIndex: 0 } ];\n\n\t\t\t\t}\n\n\t\t\t\t// create a data structure that contains all edges without duplicates\n\n\t\t\t\tfor ( let o = 0, ol = groups.length; o < ol; ++ o ) {\n\n\t\t\t\t\tconst group = groups[ o ];\n\n\t\t\t\t\tconst groupStart = group.start;\n\t\t\t\t\tconst groupCount = group.count;\n\n\t\t\t\t\tfor ( let i = groupStart, l = ( groupStart + groupCount ); i < l; i += 3 ) {\n\n\t\t\t\t\t\tfor ( let j = 0; j < 3; j ++ ) {\n\n\t\t\t\t\t\t\tconst index1 = indices.getX( i + j );\n\t\t\t\t\t\t\tconst index2 = indices.getX( i + ( j + 1 ) % 3 );\n\n\t\t\t\t\t\t\tstart.fromBufferAttribute( position, index1 );\n\t\t\t\t\t\t\tend.fromBufferAttribute( position, index2 );\n\n\t\t\t\t\t\t\tif ( isUniqueEdge( start, end, edges ) === true ) {\n\n\t\t\t\t\t\t\t\tvertices.push( start.x, start.y, start.z );\n\t\t\t\t\t\t\t\tvertices.push( end.x, end.y, end.z );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\t// non-indexed BufferGeometry\n\n\t\t\t\tconst position = geometry.attributes.position;\n\n\t\t\t\tfor ( let i = 0, l = ( position.count / 3 ); i < l; i ++ ) {\n\n\t\t\t\t\tfor ( let j = 0; j < 3; j ++ ) {\n\n\t\t\t\t\t\t// three edges per triangle, an edge is represented as (index1, index2)\n\t\t\t\t\t\t// e.g. the first triangle has the following edges: (0,1),(1,2),(2,0)\n\n\t\t\t\t\t\tconst index1 = 3 * i + j;\n\t\t\t\t\t\tconst index2 = 3 * i + ( ( j + 1 ) % 3 );\n\n\t\t\t\t\t\tstart.fromBufferAttribute( position, index1 );\n\t\t\t\t\t\tend.fromBufferAttribute( position, index2 );\n\n\t\t\t\t\t\tif ( isUniqueEdge( start, end, edges ) === true ) {\n\n\t\t\t\t\t\t\tvertices.push( start.x, start.y, start.z );\n\t\t\t\t\t\t\tvertices.push( end.x, end.y, end.z );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// build geometry\n\n\t\t\tthis.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\n\t\t}\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.parameters = Object.assign( {}, source.parameters );\n\n\t\treturn this;\n\n\t}\n\n}\n\nfunction isUniqueEdge( start, end, edges ) {\n\n\tconst hash1 = `${start.x},${start.y},${start.z}-${end.x},${end.y},${end.z}`;\n\tconst hash2 = `${end.x},${end.y},${end.z}-${start.x},${start.y},${start.z}`; // coincident edge\n\n\tif ( edges.has( hash1 ) === true || edges.has( hash2 ) === true ) {\n\n\t\treturn false;\n\n\t} else {\n\n\t\tedges.add( hash1 );\n\t\tedges.add( hash2 );\n\t\treturn true;\n\n\t}\n\n}\n\nvar Geometries = /*#__PURE__*/Object.freeze({\n\t__proto__: null,\n\tBoxGeometry: BoxGeometry,\n\tCapsuleGeometry: CapsuleGeometry,\n\tCircleGeometry: CircleGeometry,\n\tConeGeometry: ConeGeometry,\n\tCylinderGeometry: CylinderGeometry,\n\tDodecahedronGeometry: DodecahedronGeometry,\n\tEdgesGeometry: EdgesGeometry,\n\tExtrudeGeometry: ExtrudeGeometry,\n\tIcosahedronGeometry: IcosahedronGeometry,\n\tLatheGeometry: LatheGeometry,\n\tOctahedronGeometry: OctahedronGeometry,\n\tPlaneGeometry: PlaneGeometry,\n\tPolyhedronGeometry: PolyhedronGeometry,\n\tRingGeometry: RingGeometry,\n\tShapeGeometry: ShapeGeometry,\n\tSphereGeometry: SphereGeometry,\n\tTetrahedronGeometry: TetrahedronGeometry,\n\tTorusGeometry: TorusGeometry,\n\tTorusKnotGeometry: TorusKnotGeometry,\n\tTubeGeometry: TubeGeometry,\n\tWireframeGeometry: WireframeGeometry\n});\n\n/**\n * This material can receive shadows, but otherwise is completely transparent.\n *\n * ```js\n * const geometry = new THREE.PlaneGeometry( 2000, 2000 );\n * geometry.rotateX( - Math.PI / 2 );\n *\n * const material = new THREE.ShadowMaterial();\n * material.opacity = 0.2;\n *\n * const plane = new THREE.Mesh( geometry, material );\n * plane.position.y = -200;\n * plane.receiveShadow = true;\n * scene.add( plane );\n * ```\n *\n * @augments Material\n */\nclass ShadowMaterial extends Material {\n\n\t/**\n\t * Constructs a new shadow material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isShadowMaterial = true;\n\n\t\tthis.type = 'ShadowMaterial';\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.color = new Color( 0x000000 );\n\n\t\t/**\n\t\t * Overwritten since shadow materials are transparent\n\t\t * by default.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.transparent = true;\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.color.copy( source.color );\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * This class works just like {@link ShaderMaterial}, except that definitions\n * of built-in uniforms and attributes are not automatically prepended to the\n * GLSL shader code.\n *\n * `RawShaderMaterial` can only be used with {@link WebGLRenderer}.\n *\n * @augments ShaderMaterial\n */\nclass RawShaderMaterial extends ShaderMaterial {\n\n\t/**\n\t * Constructs a new raw shader material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper( parameters );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isRawShaderMaterial = true;\n\n\t\tthis.type = 'RawShaderMaterial';\n\n\t}\n\n}\n\n/**\n * A standard physically based material, using Metallic-Roughness workflow.\n *\n * Physically based rendering (PBR) has recently become the standard in many\n * 3D applications, such as [Unity](https://blogs.unity3d.com/2014/10/29/physically-based-shading-in-unity-5-a-primer/),\n * [Unreal](https://docs.unrealengine.com/latest/INT/Engine/Rendering/Materials/PhysicallyBased/) and\n * [3D Studio Max](http://area.autodesk.com/blogs/the-3ds-max-blog/what039s-new-for-rendering-in-3ds-max-2017).\n *\n * This approach differs from older approaches in that instead of using\n * approximations for the way in which light interacts with a surface, a\n * physically correct model is used. The idea is that, instead of tweaking\n * materials to look good under specific lighting, a material can be created\n * that will react 'correctly' under all lighting scenarios.\n *\n * In practice this gives a more accurate and realistic looking result than\n * the {@link MeshLambertMaterial} or {@link MeshPhongMaterial}, at the cost of\n * being somewhat more computationally expensive. `MeshStandardMaterial` uses per-fragment\n * shading.\n *\n * Note that for best results you should always specify an environment map when using this material.\n *\n * For a non-technical introduction to the concept of PBR and how to set up a\n * PBR material, check out these articles by the people at [marmoset](https://www.marmoset.co):\n *\n * - [Basic Theory of Physically Based Rendering](https://www.marmoset.co/posts/basic-theory-of-physically-based-rendering/)\n * - [Physically Based Rendering and You Can Too](https://www.marmoset.co/posts/physically-based-rendering-and-you-can-too/)\n *\n * Technical details of the approach used in three.js (and most other PBR systems) can be found is this\n * [paper from Disney](https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf)\n * (pdf), by Brent Burley.\n *\n * @augments Material\n * @demo scenes/material-browser.html#MeshStandardMaterial\n */\nclass MeshStandardMaterial extends Material {\n\n\t/**\n\t * Constructs a new mesh standard material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshStandardMaterial = true;\n\n\t\tthis.type = 'MeshStandardMaterial';\n\n\t\tthis.defines = { 'STANDARD': '' };\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.color = new Color( 0xffffff ); // diffuse\n\n\t\t/**\n\t\t * How rough the material appears. `0.0` means a smooth mirror reflection, `1.0`\n\t\t * means fully diffuse. If `roughnessMap` is also provided,\n\t\t * both values are multiplied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.roughness = 1.0;\n\n\t\t/**\n\t\t * How much the material is like a metal. Non-metallic materials such as wood\n\t\t * or stone use `0.0`, metallic use `1.0`, with nothing (usually) in between.\n\t\t * A value between `0.0` and `1.0` could be used for a rusty metal look.\n\t\t * If `metalnessMap` is also provided, both values are multiplied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.metalness = 0.0;\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map\n\t\t * color is modulated by the diffuse `color`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The light map. Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.lightMap = null;\n\n\t\t/**\n\t\t * Intensity of the baked light.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.lightMapIntensity = 1.0;\n\n\t\t/**\n\t\t * The red channel of this texture is used as the ambient occlusion map.\n\t\t * Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.aoMap = null;\n\n\t\t/**\n\t\t * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`\n\t\t * disables ambient occlusion. Where intensity is `1` and the AO map's\n\t\t * red channel is also `1`, ambient light is fully occluded on a surface.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.aoMapIntensity = 1.0;\n\n\t\t/**\n\t\t * Emissive (light) color of the material, essentially a solid color\n\t\t * unaffected by other lighting.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.emissive = new Color( 0x000000 );\n\n\t\t/**\n\t\t * Intensity of the emissive light. Modulates the emissive color.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.emissiveIntensity = 1.0;\n\n\t\t/**\n\t\t * Set emissive (glow) map. The emissive map color is modulated by the\n\t\t * emissive color and the emissive intensity. If you have an emissive map,\n\t\t * be sure to set the emissive color to something other than black.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.emissiveMap = null;\n\n\t\t/**\n\t\t * The texture to create a bump map. The black and white values map to the\n\t\t * perceived depth in relation to the lights. Bump doesn't actually affect\n\t\t * the geometry of the object, only the lighting. If a normal map is defined\n\t\t * this will be ignored.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.bumpMap = null;\n\n\t\t/**\n\t\t * How much the bump map affects the material. Typical range is `[0,1]`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.bumpScale = 1;\n\n\t\t/**\n\t\t * The texture to create a normal map. The RGB values affect the surface\n\t\t * normal for each pixel fragment and change the way the color is lit. Normal\n\t\t * maps do not change the actual shape of the surface, only the lighting. In\n\t\t * case the material has a normal map authored using the left handed\n\t\t * convention, the `y` component of `normalScale` should be negated to compensate\n\t\t * for the different handedness.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.normalMap = null;\n\n\t\t/**\n\t\t * The type of normal map.\n\t\t *\n\t\t * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}\n\t\t * @default TangentSpaceNormalMap\n\t\t */\n\t\tthis.normalMapType = TangentSpaceNormalMap;\n\n\t\t/**\n\t\t * How much the normal map affects the material. Typical value range is `[0,1]`.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (1,1)\n\t\t */\n\t\tthis.normalScale = new Vector2( 1, 1 );\n\n\t\t/**\n\t\t * The displacement map affects the position of the mesh's vertices. Unlike\n\t\t * other maps which only affect the light and shade of the material the\n\t\t * displaced vertices can cast shadows, block other objects, and otherwise\n\t\t * act as real geometry. The displacement texture is an image where the value\n\t\t * of each pixel (white being the highest) is mapped against, and\n\t\t * repositions, the vertices of the mesh.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.displacementMap = null;\n\n\t\t/**\n\t\t * How much the displacement map affects the mesh (where black is no\n\t\t * displacement, and white is maximum displacement). Without a displacement\n\t\t * map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementScale = 1;\n\n\t\t/**\n\t\t * The offset of the displacement map's values on the mesh's vertices.\n\t\t * The bias is added to the scaled sample of the displacement map.\n\t\t * Without a displacement map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementBias = 0;\n\n\t\t/**\n\t\t * The green channel of this texture is used to alter the roughness of the\n\t\t * material.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.roughnessMap = null;\n\n\t\t/**\n\t\t * The blue channel of this texture is used to alter the metalness of the\n\t\t * material.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.metalnessMap = null;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * The environment map. To ensure a physically correct rendering, environment maps\n\t\t * are internally pre-processed with {@link PMREMGenerator}.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.envMap = null;\n\n\t\t/**\n\t\t * The rotation of the environment map in radians.\n\t\t *\n\t\t * @type {Euler}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.envMapRotation = new Euler();\n\n\t\t/**\n\t\t * Scales the effect of the environment map by multiplying its color.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.envMapIntensity = 1.0;\n\n\t\t/**\n\t\t * Renders the geometry as a wireframe.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.wireframe = false;\n\n\t\t/**\n\t\t * Controls the thickness of the wireframe.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.wireframeLinewidth = 1;\n\n\t\t/**\n\t\t * Defines appearance of wireframe ends.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinecap = 'round';\n\n\t\t/**\n\t\t * Defines appearance of wireframe joints.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinejoin = 'round';\n\n\t\t/**\n\t\t * Whether the material is rendered with flat shading or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flatShading = false;\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.defines = { 'STANDARD': '' };\n\n\t\tthis.color.copy( source.color );\n\t\tthis.roughness = source.roughness;\n\t\tthis.metalness = source.metalness;\n\n\t\tthis.map = source.map;\n\n\t\tthis.lightMap = source.lightMap;\n\t\tthis.lightMapIntensity = source.lightMapIntensity;\n\n\t\tthis.aoMap = source.aoMap;\n\t\tthis.aoMapIntensity = source.aoMapIntensity;\n\n\t\tthis.emissive.copy( source.emissive );\n\t\tthis.emissiveMap = source.emissiveMap;\n\t\tthis.emissiveIntensity = source.emissiveIntensity;\n\n\t\tthis.bumpMap = source.bumpMap;\n\t\tthis.bumpScale = source.bumpScale;\n\n\t\tthis.normalMap = source.normalMap;\n\t\tthis.normalMapType = source.normalMapType;\n\t\tthis.normalScale.copy( source.normalScale );\n\n\t\tthis.displacementMap = source.displacementMap;\n\t\tthis.displacementScale = source.displacementScale;\n\t\tthis.displacementBias = source.displacementBias;\n\n\t\tthis.roughnessMap = source.roughnessMap;\n\n\t\tthis.metalnessMap = source.metalnessMap;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.envMap = source.envMap;\n\t\tthis.envMapRotation.copy( source.envMapRotation );\n\t\tthis.envMapIntensity = source.envMapIntensity;\n\n\t\tthis.wireframe = source.wireframe;\n\t\tthis.wireframeLinewidth = source.wireframeLinewidth;\n\t\tthis.wireframeLinecap = source.wireframeLinecap;\n\t\tthis.wireframeLinejoin = source.wireframeLinejoin;\n\n\t\tthis.flatShading = source.flatShading;\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * An extension of the {@link MeshStandardMaterial}, providing more advanced\n * physically-based rendering properties:\n *\n * - Anisotropy: Ability to represent the anisotropic property of materials\n * as observable with brushed metals.\n * - Clearcoat: Some materials — like car paints, carbon fiber, and wet surfaces — require\n * a clear, reflective layer on top of another layer that may be irregular or rough.\n * Clearcoat approximates this effect, without the need for a separate transparent surface.\n * - Iridescence: Allows to render the effect where hue varies  depending on the viewing\n * angle and illumination angle. This can be seen on soap bubbles, oil films, or on the\n * wings of many insects.\n * - Physically-based transparency: One limitation of {@link Material#opacity} is that highly\n * transparent materials are less reflective. Physically-based transmission provides a more\n * realistic option for thin, transparent surfaces like glass.\n * - Advanced reflectivity: More flexible reflectivity for non-metallic materials.\n * - Sheen: Can be used for representing cloth and fabric materials.\n *\n * As a result of these complex shading features, `MeshPhysicalMaterial` has a\n * higher performance cost, per pixel, than other three.js materials. Most\n * effects are disabled by default, and add cost as they are enabled. For\n * best results, always specify an environment map when using this material.\n *\n * @augments MeshStandardMaterial\n * @demo scenes/material-browser.html#MeshPhysicalMaterial\n */\nclass MeshPhysicalMaterial extends MeshStandardMaterial {\n\n\t/**\n\t * Constructs a new mesh physical material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshPhysicalMaterial = true;\n\n\t\tthis.defines = {\n\n\t\t\t'STANDARD': '',\n\t\t\t'PHYSICAL': ''\n\n\t\t};\n\n\t\tthis.type = 'MeshPhysicalMaterial';\n\n\t\t/**\n\t\t * The rotation of the anisotropy in tangent, bitangent space, measured in radians\n\t\t * counter-clockwise from the tangent. When `anisotropyMap` is present, this\n\t\t * property provides additional rotation to the vectors in the texture.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.anisotropyRotation = 0;\n\n\t\t/**\n\t\t * Red and green channels represent the anisotropy direction in `[-1, 1]` tangent,\n\t\t * bitangent space, to be rotated by `anisotropyRotation`. The blue channel\n\t\t * contains strength as `[0, 1]` to be multiplied by `anisotropy`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.anisotropyMap = null;\n\n\t\t/**\n\t\t * The red channel of this texture is multiplied against `clearcoat`,\n\t\t * for per-pixel control over a coating's intensity.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.clearcoatMap = null;\n\n\t\t/**\n\t\t * Roughness of the clear coat layer, from `0.0` to `1.0`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.clearcoatRoughness = 0.0;\n\n\t\t/**\n\t\t * The green channel of this texture is multiplied against\n\t\t * `clearcoatRoughness`, for per-pixel control over a coating's roughness.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.clearcoatRoughnessMap = null;\n\n\t\t/**\n\t\t * How much `clearcoatNormalMap` affects the clear coat layer, from\n\t\t * `(0,0)` to `(1,1)`.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (1,1)\n\t\t */\n\t\tthis.clearcoatNormalScale = new Vector2( 1, 1 );\n\n\t\t/**\n\t\t * Can be used to enable independent normals for the clear coat layer.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.clearcoatNormalMap = null;\n\n\t\t/**\n\t\t * Index-of-refraction for non-metallic materials, from `1.0` to `2.333`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1.5\n\t\t */\n\t\tthis.ior = 1.5;\n\n\t\t/**\n\t\t * Degree of reflectivity, from `0.0` to `1.0`. Default is `0.5`, which\n\t\t * corresponds to an index-of-refraction of `1.5`.\n\t\t *\n\t\t * This models the reflectivity of non-metallic materials. It has no effect\n\t\t * when `metalness` is `1.0`\n\t\t *\n\t\t * @name MeshPhysicalMaterial#reflectivity\n\t\t * @type {number}\n\t\t * @default 0.5\n\t\t */\n\t\tObject.defineProperty( this, 'reflectivity', {\n\t\t\tget: function () {\n\n\t\t\t\treturn ( clamp( 2.5 * ( this.ior - 1 ) / ( this.ior + 1 ), 0, 1 ) );\n\n\t\t\t},\n\t\t\tset: function ( reflectivity ) {\n\n\t\t\t\tthis.ior = ( 1 + 0.4 * reflectivity ) / ( 1 - 0.4 * reflectivity );\n\n\t\t\t}\n\t\t} );\n\n\t\t/**\n\t\t * The red channel of this texture is multiplied against `iridescence`, for per-pixel\n\t\t * control over iridescence.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.iridescenceMap = null;\n\n\t\t/**\n\t\t * Strength of the iridescence RGB color shift effect, represented by an index-of-refraction.\n\t\t * Between `1.0` to `2.333`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1.3\n\t\t */\n\t\tthis.iridescenceIOR = 1.3;\n\n\t\t/**\n\t\t *Array of exactly 2 elements, specifying minimum and maximum thickness of the iridescence layer.\n\t\t Thickness of iridescence layer has an equivalent effect of the one `thickness` has on `ior`.\n\t\t *\n\t\t * @type {Array<number,number>}\n\t\t * @default [100,400]\n\t\t */\n\t\tthis.iridescenceThicknessRange = [ 100, 400 ];\n\n\t\t/**\n\t\t * A texture that defines the thickness of the iridescence layer, stored in the green channel.\n\t\t * Minimum and maximum values of thickness are defined by `iridescenceThicknessRange` array:\n\t\t * - `0.0` in the green channel will result in thickness equal to first element of the array.\n\t\t * - `1.0` in the green channel will result in thickness equal to second element of the array.\n\t\t * - Values in-between will linearly interpolate between the elements of the array.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.iridescenceThicknessMap = null;\n\n\t\t/**\n\t\t * The sheen tint.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.sheenColor = new Color( 0x000000 );\n\n\t\t/**\n\t\t * The RGB channels of this texture are multiplied against  `sheenColor`, for per-pixel control\n\t\t * over sheen tint.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.sheenColorMap = null;\n\n\t\t/**\n\t\t * Roughness of the sheen layer, from `0.0` to `1.0`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.sheenRoughness = 1.0;\n\n\t\t/**\n\t\t * The alpha channel of this texture is multiplied against `sheenRoughness`, for per-pixel control\n\t\t * over sheen roughness.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.sheenRoughnessMap = null;\n\n\t\t/**\n\t\t * The red channel of this texture is multiplied against `transmission`, for per-pixel control over\n\t\t * optical transparency.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.transmissionMap = null;\n\n\t\t/**\n\t\t * The thickness of the volume beneath the surface. The value is given in the\n\t\t * coordinate space of the mesh. If the value is `0` the material is\n\t\t * thin-walled. Otherwise the material is a volume boundary.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.thickness = 0;\n\n\t\t/**\n\t\t * A texture that defines the thickness, stored in the green channel. This will\n\t\t * be multiplied by `thickness`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.thicknessMap = null;\n\n\t\t/**\n\t\t * Density of the medium given as the average distance that light travels in\n\t\t * the medium before interacting with a particle. The value is given in world\n\t\t * space units, and must be greater than zero.\n\t\t *\n\t\t * @type {number}\n\t\t * @default Infinity\n\t\t */\n\t\tthis.attenuationDistance = Infinity;\n\n\t\t/**\n\t\t * The color that white light turns into due to absorption when reaching the\n\t\t * attenuation distance.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.attenuationColor = new Color( 1, 1, 1 );\n\n\t\t/**\n\t\t * A float that scales the amount of specular reflection for non-metals only.\n\t\t * When set to zero, the model is effectively Lambertian. From `0.0` to `1.0`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.specularIntensity = 1.0;\n\n\t\t/**\n\t\t * The alpha channel of this texture is multiplied against `specularIntensity`,\n\t\t * for per-pixel control over specular intensity.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.specularIntensityMap = null;\n\n\t\t/**\n\t\t * Tints the specular reflection at normal incidence for non-metals only.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.specularColor = new Color( 1, 1, 1 );\n\n\t\t/**\n\t\t * The RGB channels of this texture are multiplied against `specularColor`,\n\t\t * for per-pixel control over specular color.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.specularColorMap = null;\n\n\t\tthis._anisotropy = 0;\n\t\tthis._clearcoat = 0;\n\t\tthis._dispersion = 0;\n\t\tthis._iridescence = 0;\n\t\tthis._sheen = 0.0;\n\t\tthis._transmission = 0;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\t/**\n\t * The anisotropy strength, from `0.0` to `1.0`.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget anisotropy() {\n\n\t\treturn this._anisotropy;\n\n\t}\n\n\tset anisotropy( value ) {\n\n\t\tif ( this._anisotropy > 0 !== value > 0 ) {\n\n\t\t\tthis.version ++;\n\n\t\t}\n\n\t\tthis._anisotropy = value;\n\n\t}\n\n\t/**\n\t * Represents the intensity of the clear coat layer, from `0.0` to `1.0`. Use\n\t * clear coat related properties to enable multilayer materials that have a\n\t * thin translucent layer over the base layer.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget clearcoat() {\n\n\t\treturn this._clearcoat;\n\n\t}\n\n\tset clearcoat( value ) {\n\n\t\tif ( this._clearcoat > 0 !== value > 0 ) {\n\n\t\t\tthis.version ++;\n\n\t\t}\n\n\t\tthis._clearcoat = value;\n\n\t}\n\t/**\n\t * The intensity of the iridescence layer, simulating RGB color shift based on the angle between\n\t * the surface and the viewer, from `0.0` to `1.0`.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget iridescence() {\n\n\t\treturn this._iridescence;\n\n\t}\n\n\tset iridescence( value ) {\n\n\t\tif ( this._iridescence > 0 !== value > 0 ) {\n\n\t\t\tthis.version ++;\n\n\t\t}\n\n\t\tthis._iridescence = value;\n\n\t}\n\n\t/**\n\t * Defines the strength of the angular separation of colors (chromatic aberration) transmitting\n\t * through a relatively clear volume. Any value zero or larger is valid, the typical range of\n\t * realistic values is `[0, 1]`. This property can be only be used with transmissive objects.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget dispersion() {\n\n\t\treturn this._dispersion;\n\n\t}\n\n\tset dispersion( value ) {\n\n\t\tif ( this._dispersion > 0 !== value > 0 ) {\n\n\t\t\tthis.version ++;\n\n\t\t}\n\n\t\tthis._dispersion = value;\n\n\t}\n\n\t/**\n\t * The intensity of the sheen layer, from `0.0` to `1.0`.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget sheen() {\n\n\t\treturn this._sheen;\n\n\t}\n\n\tset sheen( value ) {\n\n\t\tif ( this._sheen > 0 !== value > 0 ) {\n\n\t\t\tthis.version ++;\n\n\t\t}\n\n\t\tthis._sheen = value;\n\n\t}\n\n\t/**\n\t * Degree of transmission (or optical transparency), from `0.0` to `1.0`.\n\t *\n\t * Thin, transparent or semitransparent, plastic or glass materials remain\n\t * largely reflective even if they are fully transmissive. The transmission\n\t * property can be used to model these materials.\n\t *\n\t * When transmission is non-zero, `opacity` should be  set to `1`.\n\t *\n\t * @type {number}\n\t * @default 0\n\t */\n\tget transmission() {\n\n\t\treturn this._transmission;\n\n\t}\n\n\tset transmission( value ) {\n\n\t\tif ( this._transmission > 0 !== value > 0 ) {\n\n\t\t\tthis.version ++;\n\n\t\t}\n\n\t\tthis._transmission = value;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.defines = {\n\n\t\t\t'STANDARD': '',\n\t\t\t'PHYSICAL': ''\n\n\t\t};\n\n\t\tthis.anisotropy = source.anisotropy;\n\t\tthis.anisotropyRotation = source.anisotropyRotation;\n\t\tthis.anisotropyMap = source.anisotropyMap;\n\n\t\tthis.clearcoat = source.clearcoat;\n\t\tthis.clearcoatMap = source.clearcoatMap;\n\t\tthis.clearcoatRoughness = source.clearcoatRoughness;\n\t\tthis.clearcoatRoughnessMap = source.clearcoatRoughnessMap;\n\t\tthis.clearcoatNormalMap = source.clearcoatNormalMap;\n\t\tthis.clearcoatNormalScale.copy( source.clearcoatNormalScale );\n\n\t\tthis.dispersion = source.dispersion;\n\t\tthis.ior = source.ior;\n\n\t\tthis.iridescence = source.iridescence;\n\t\tthis.iridescenceMap = source.iridescenceMap;\n\t\tthis.iridescenceIOR = source.iridescenceIOR;\n\t\tthis.iridescenceThicknessRange = [ ...source.iridescenceThicknessRange ];\n\t\tthis.iridescenceThicknessMap = source.iridescenceThicknessMap;\n\n\t\tthis.sheen = source.sheen;\n\t\tthis.sheenColor.copy( source.sheenColor );\n\t\tthis.sheenColorMap = source.sheenColorMap;\n\t\tthis.sheenRoughness = source.sheenRoughness;\n\t\tthis.sheenRoughnessMap = source.sheenRoughnessMap;\n\n\t\tthis.transmission = source.transmission;\n\t\tthis.transmissionMap = source.transmissionMap;\n\n\t\tthis.thickness = source.thickness;\n\t\tthis.thicknessMap = source.thicknessMap;\n\t\tthis.attenuationDistance = source.attenuationDistance;\n\t\tthis.attenuationColor.copy( source.attenuationColor );\n\n\t\tthis.specularIntensity = source.specularIntensity;\n\t\tthis.specularIntensityMap = source.specularIntensityMap;\n\t\tthis.specularColor.copy( source.specularColor );\n\t\tthis.specularColorMap = source.specularColorMap;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A material for shiny surfaces with specular highlights.\n *\n * The material uses a non-physically based [Blinn-Phong](https://en.wikipedia.org/wiki/Blinn-Phong_shading_model)\n * model for calculating reflectance. Unlike the Lambertian model used in the\n * {@link MeshLambertMaterial} this can simulate shiny surfaces with specular\n * highlights (such as varnished wood). `MeshPhongMaterial` uses per-fragment shading.\n *\n * Performance will generally be greater when using this material over the\n * {@link MeshStandardMaterial} or {@link MeshPhysicalMaterial}, at the cost of\n * some graphical accuracy.\n *\n * @augments Material\n * @demo scenes/material-browser.html#MeshPhongMaterial\n */\nclass MeshPhongMaterial extends Material {\n\n\t/**\n\t * Constructs a new mesh phong material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshPhongMaterial = true;\n\n\t\tthis.type = 'MeshPhongMaterial';\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.color = new Color( 0xffffff ); // diffuse\n\n\t\t/**\n\t\t * Specular color of the material. The default color is set to `0x111111` (very dark grey)\n\t\t *\n\t\t * This defines how shiny the material is and the color of its shine.\n\t\t *\n\t\t * @type {Color}\n\t\t */\n\t\tthis.specular = new Color( 0x111111 );\n\n\t\t/**\n\t\t * How shiny the specular highlight is; a higher value gives a sharper highlight.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 30\n\t\t */\n\t\tthis.shininess = 30;\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map\n\t\t * color is modulated by the diffuse `color`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The light map. Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.lightMap = null;\n\n\t\t/**\n\t\t * Intensity of the baked light.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.lightMapIntensity = 1.0;\n\n\t\t/**\n\t\t * The red channel of this texture is used as the ambient occlusion map.\n\t\t * Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.aoMap = null;\n\n\t\t/**\n\t\t * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`\n\t\t * disables ambient occlusion. Where intensity is `1` and the AO map's\n\t\t * red channel is also `1`, ambient light is fully occluded on a surface.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.aoMapIntensity = 1.0;\n\n\t\t/**\n\t\t * Emissive (light) color of the material, essentially a solid color\n\t\t * unaffected by other lighting.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.emissive = new Color( 0x000000 );\n\n\t\t/**\n\t\t * Intensity of the emissive light. Modulates the emissive color.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.emissiveIntensity = 1.0;\n\n\t\t/**\n\t\t * Set emissive (glow) map. The emissive map color is modulated by the\n\t\t * emissive color and the emissive intensity. If you have an emissive map,\n\t\t * be sure to set the emissive color to something other than black.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.emissiveMap = null;\n\n\t\t/**\n\t\t * The texture to create a bump map. The black and white values map to the\n\t\t * perceived depth in relation to the lights. Bump doesn't actually affect\n\t\t * the geometry of the object, only the lighting. If a normal map is defined\n\t\t * this will be ignored.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.bumpMap = null;\n\n\t\t/**\n\t\t * How much the bump map affects the material. Typical range is `[0,1]`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.bumpScale = 1;\n\n\t\t/**\n\t\t * The texture to create a normal map. The RGB values affect the surface\n\t\t * normal for each pixel fragment and change the way the color is lit. Normal\n\t\t * maps do not change the actual shape of the surface, only the lighting. In\n\t\t * case the material has a normal map authored using the left handed\n\t\t * convention, the `y` component of `normalScale` should be negated to compensate\n\t\t * for the different handedness.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.normalMap = null;\n\n\t\t/**\n\t\t * The type of normal map.\n\t\t *\n\t\t * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}\n\t\t * @default TangentSpaceNormalMap\n\t\t */\n\t\tthis.normalMapType = TangentSpaceNormalMap;\n\n\t\t/**\n\t\t * How much the normal map affects the material. Typical value range is `[0,1]`.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (1,1)\n\t\t */\n\t\tthis.normalScale = new Vector2( 1, 1 );\n\n\t\t/**\n\t\t * The displacement map affects the position of the mesh's vertices. Unlike\n\t\t * other maps which only affect the light and shade of the material the\n\t\t * displaced vertices can cast shadows, block other objects, and otherwise\n\t\t * act as real geometry. The displacement texture is an image where the value\n\t\t * of each pixel (white being the highest) is mapped against, and\n\t\t * repositions, the vertices of the mesh.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.displacementMap = null;\n\n\t\t/**\n\t\t * How much the displacement map affects the mesh (where black is no\n\t\t * displacement, and white is maximum displacement). Without a displacement\n\t\t * map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementScale = 1;\n\n\t\t/**\n\t\t * The offset of the displacement map's values on the mesh's vertices.\n\t\t * The bias is added to the scaled sample of the displacement map.\n\t\t * Without a displacement map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementBias = 0;\n\n\t\t/**\n\t\t * The specular map value affects both how much the specular surface\n\t\t * highlight contributes and how much of the environment map affects the\n\t\t * surface.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.specularMap = null;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * The environment map.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.envMap = null;\n\n\t\t/**\n\t\t * The rotation of the environment map in radians.\n\t\t *\n\t\t * @type {Euler}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.envMapRotation = new Euler();\n\n\t\t/**\n\t\t * How to combine the result of the surface's color with the environment map, if any.\n\t\t *\n\t\t * When set to `MixOperation`, the {@link MeshBasicMaterial#reflectivity} is used to\n\t\t * blend between the two colors.\n\t\t *\n\t\t * @type {(MultiplyOperation|MixOperation|AddOperation)}\n\t\t * @default MultiplyOperation\n\t\t */\n\t\tthis.combine = MultiplyOperation;\n\n\t\t/**\n\t\t * How much the environment map affects the surface.\n\t\t * The valid range is between `0` (no reflections) and `1` (full reflections).\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.reflectivity = 1;\n\n\t\t/**\n\t\t * The index of refraction (IOR) of air (approximately 1) divided by the\n\t\t * index of refraction of the material. It is used with environment mapping\n\t\t * modes {@link CubeRefractionMapping} and {@link EquirectangularRefractionMapping}.\n\t\t * The refraction ratio should not exceed `1`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0.98\n\t\t */\n\t\tthis.refractionRatio = 0.98;\n\n\t\t/**\n\t\t * Renders the geometry as a wireframe.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.wireframe = false;\n\n\t\t/**\n\t\t * Controls the thickness of the wireframe.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.wireframeLinewidth = 1;\n\n\t\t/**\n\t\t * Defines appearance of wireframe ends.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinecap = 'round';\n\n\t\t/**\n\t\t * Defines appearance of wireframe joints.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinejoin = 'round';\n\n\t\t/**\n\t\t * Whether the material is rendered with flat shading or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flatShading = false;\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.color.copy( source.color );\n\t\tthis.specular.copy( source.specular );\n\t\tthis.shininess = source.shininess;\n\n\t\tthis.map = source.map;\n\n\t\tthis.lightMap = source.lightMap;\n\t\tthis.lightMapIntensity = source.lightMapIntensity;\n\n\t\tthis.aoMap = source.aoMap;\n\t\tthis.aoMapIntensity = source.aoMapIntensity;\n\n\t\tthis.emissive.copy( source.emissive );\n\t\tthis.emissiveMap = source.emissiveMap;\n\t\tthis.emissiveIntensity = source.emissiveIntensity;\n\n\t\tthis.bumpMap = source.bumpMap;\n\t\tthis.bumpScale = source.bumpScale;\n\n\t\tthis.normalMap = source.normalMap;\n\t\tthis.normalMapType = source.normalMapType;\n\t\tthis.normalScale.copy( source.normalScale );\n\n\t\tthis.displacementMap = source.displacementMap;\n\t\tthis.displacementScale = source.displacementScale;\n\t\tthis.displacementBias = source.displacementBias;\n\n\t\tthis.specularMap = source.specularMap;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.envMap = source.envMap;\n\t\tthis.envMapRotation.copy( source.envMapRotation );\n\t\tthis.combine = source.combine;\n\t\tthis.reflectivity = source.reflectivity;\n\t\tthis.refractionRatio = source.refractionRatio;\n\n\t\tthis.wireframe = source.wireframe;\n\t\tthis.wireframeLinewidth = source.wireframeLinewidth;\n\t\tthis.wireframeLinecap = source.wireframeLinecap;\n\t\tthis.wireframeLinejoin = source.wireframeLinejoin;\n\n\t\tthis.flatShading = source.flatShading;\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A material implementing toon shading.\n *\n * @augments Material\n * @demo scenes/material-browser.html#MeshToonMaterial\n */\nclass MeshToonMaterial extends Material {\n\n\t/**\n\t * Constructs a new mesh toon material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshToonMaterial = true;\n\n\t\tthis.defines = { 'TOON': '' };\n\n\t\tthis.type = 'MeshToonMaterial';\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.color = new Color( 0xffffff );\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map\n\t\t * color is modulated by the diffuse `color`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * Gradient map for toon shading. It's required to set\n\t\t * {@link Texture#minFilter} and {@link Texture#magFilter} to {@linkNearestFilter}\n\t\t * when using this type of texture.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.gradientMap = null;\n\n\t\t/**\n\t\t * The light map. Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.lightMap = null;\n\n\t\t/**\n\t\t * Intensity of the baked light.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.lightMapIntensity = 1.0;\n\n\t\t/**\n\t\t * The red channel of this texture is used as the ambient occlusion map.\n\t\t * Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.aoMap = null;\n\n\t\t/**\n\t\t * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`\n\t\t * disables ambient occlusion. Where intensity is `1` and the AO map's\n\t\t * red channel is also `1`, ambient light is fully occluded on a surface.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.aoMapIntensity = 1.0;\n\n\t\t/**\n\t\t * Emissive (light) color of the material, essentially a solid color\n\t\t * unaffected by other lighting.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.emissive = new Color( 0x000000 );\n\n\t\t/**\n\t\t * Intensity of the emissive light. Modulates the emissive color.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.emissiveIntensity = 1.0;\n\n\t\t/**\n\t\t * Set emissive (glow) map. The emissive map color is modulated by the\n\t\t * emissive color and the emissive intensity. If you have an emissive map,\n\t\t * be sure to set the emissive color to something other than black.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.emissiveMap = null;\n\n\t\t/**\n\t\t * The texture to create a bump map. The black and white values map to the\n\t\t * perceived depth in relation to the lights. Bump doesn't actually affect\n\t\t * the geometry of the object, only the lighting. If a normal map is defined\n\t\t * this will be ignored.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.bumpMap = null;\n\n\t\t/**\n\t\t * How much the bump map affects the material. Typical range is `[0,1]`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.bumpScale = 1;\n\n\t\t/**\n\t\t * The texture to create a normal map. The RGB values affect the surface\n\t\t * normal for each pixel fragment and change the way the color is lit. Normal\n\t\t * maps do not change the actual shape of the surface, only the lighting. In\n\t\t * case the material has a normal map authored using the left handed\n\t\t * convention, the `y` component of `normalScale` should be negated to compensate\n\t\t * for the different handedness.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.normalMap = null;\n\n\t\t/**\n\t\t * The type of normal map.\n\t\t *\n\t\t * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}\n\t\t * @default TangentSpaceNormalMap\n\t\t */\n\t\tthis.normalMapType = TangentSpaceNormalMap;\n\n\t\t/**\n\t\t * How much the normal map affects the material. Typical value range is `[0,1]`.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (1,1)\n\t\t */\n\t\tthis.normalScale = new Vector2( 1, 1 );\n\n\t\t/**\n\t\t * The displacement map affects the position of the mesh's vertices. Unlike\n\t\t * other maps which only affect the light and shade of the material the\n\t\t * displaced vertices can cast shadows, block other objects, and otherwise\n\t\t * act as real geometry. The displacement texture is an image where the value\n\t\t * of each pixel (white being the highest) is mapped against, and\n\t\t * repositions, the vertices of the mesh.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.displacementMap = null;\n\n\t\t/**\n\t\t * How much the displacement map affects the mesh (where black is no\n\t\t * displacement, and white is maximum displacement). Without a displacement\n\t\t * map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementScale = 1;\n\n\t\t/**\n\t\t * The offset of the displacement map's values on the mesh's vertices.\n\t\t * The bias is added to the scaled sample of the displacement map.\n\t\t * Without a displacement map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementBias = 0;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * Renders the geometry as a wireframe.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.wireframe = false;\n\n\t\t/**\n\t\t * Controls the thickness of the wireframe.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.wireframeLinewidth = 1;\n\n\t\t/**\n\t\t * Defines appearance of wireframe ends.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinecap = 'round';\n\n\t\t/**\n\t\t * Defines appearance of wireframe joints.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinejoin = 'round';\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.color.copy( source.color );\n\n\t\tthis.map = source.map;\n\t\tthis.gradientMap = source.gradientMap;\n\n\t\tthis.lightMap = source.lightMap;\n\t\tthis.lightMapIntensity = source.lightMapIntensity;\n\n\t\tthis.aoMap = source.aoMap;\n\t\tthis.aoMapIntensity = source.aoMapIntensity;\n\n\t\tthis.emissive.copy( source.emissive );\n\t\tthis.emissiveMap = source.emissiveMap;\n\t\tthis.emissiveIntensity = source.emissiveIntensity;\n\n\t\tthis.bumpMap = source.bumpMap;\n\t\tthis.bumpScale = source.bumpScale;\n\n\t\tthis.normalMap = source.normalMap;\n\t\tthis.normalMapType = source.normalMapType;\n\t\tthis.normalScale.copy( source.normalScale );\n\n\t\tthis.displacementMap = source.displacementMap;\n\t\tthis.displacementScale = source.displacementScale;\n\t\tthis.displacementBias = source.displacementBias;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.wireframe = source.wireframe;\n\t\tthis.wireframeLinewidth = source.wireframeLinewidth;\n\t\tthis.wireframeLinecap = source.wireframeLinecap;\n\t\tthis.wireframeLinejoin = source.wireframeLinejoin;\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A material that maps the normal vectors to RGB colors.\n *\n * @augments Material\n * @demo scenes/material-browser.html#MeshNormalMaterial\n */\nclass MeshNormalMaterial extends Material {\n\n\t/**\n\t * Constructs a new mesh normal material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshNormalMaterial = true;\n\n\t\tthis.type = 'MeshNormalMaterial';\n\n\t\t/**\n\t\t * The texture to create a bump map. The black and white values map to the\n\t\t * perceived depth in relation to the lights. Bump doesn't actually affect\n\t\t * the geometry of the object, only the lighting. If a normal map is defined\n\t\t * this will be ignored.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.bumpMap = null;\n\n\t\t/**\n\t\t * How much the bump map affects the material. Typical range is `[0,1]`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.bumpScale = 1;\n\n\t\t/**\n\t\t * The texture to create a normal map. The RGB values affect the surface\n\t\t * normal for each pixel fragment and change the way the color is lit. Normal\n\t\t * maps do not change the actual shape of the surface, only the lighting. In\n\t\t * case the material has a normal map authored using the left handed\n\t\t * convention, the `y` component of `normalScale` should be negated to compensate\n\t\t * for the different handedness.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.normalMap = null;\n\n\t\t/**\n\t\t * The type of normal map.\n\t\t *\n\t\t * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}\n\t\t * @default TangentSpaceNormalMap\n\t\t */\n\t\tthis.normalMapType = TangentSpaceNormalMap;\n\n\t\t/**\n\t\t * How much the normal map affects the material. Typical value range is `[0,1]`.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (1,1)\n\t\t */\n\t\tthis.normalScale = new Vector2( 1, 1 );\n\n\t\t/**\n\t\t * The displacement map affects the position of the mesh's vertices. Unlike\n\t\t * other maps which only affect the light and shade of the material the\n\t\t * displaced vertices can cast shadows, block other objects, and otherwise\n\t\t * act as real geometry. The displacement texture is an image where the value\n\t\t * of each pixel (white being the highest) is mapped against, and\n\t\t * repositions, the vertices of the mesh.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.displacementMap = null;\n\n\t\t/**\n\t\t * How much the displacement map affects the mesh (where black is no\n\t\t * displacement, and white is maximum displacement). Without a displacement\n\t\t * map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementScale = 1;\n\n\t\t/**\n\t\t * The offset of the displacement map's values on the mesh's vertices.\n\t\t * The bias is added to the scaled sample of the displacement map.\n\t\t * Without a displacement map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementBias = 0;\n\n\t\t/**\n\t\t * Renders the geometry as a wireframe.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.wireframe = false;\n\n\t\t/**\n\t\t * Controls the thickness of the wireframe.\n\t\t *\n\t\t * WebGL and WebGPU ignore this property and always render\n\t\t * 1 pixel wide lines.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.wireframeLinewidth = 1;\n\n\t\t/**\n\t\t * Whether the material is rendered with flat shading or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flatShading = false;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.bumpMap = source.bumpMap;\n\t\tthis.bumpScale = source.bumpScale;\n\n\t\tthis.normalMap = source.normalMap;\n\t\tthis.normalMapType = source.normalMapType;\n\t\tthis.normalScale.copy( source.normalScale );\n\n\t\tthis.displacementMap = source.displacementMap;\n\t\tthis.displacementScale = source.displacementScale;\n\t\tthis.displacementBias = source.displacementBias;\n\n\t\tthis.wireframe = source.wireframe;\n\t\tthis.wireframeLinewidth = source.wireframeLinewidth;\n\n\t\tthis.flatShading = source.flatShading;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A material for non-shiny surfaces, without specular highlights.\n *\n * The material uses a non-physically based [Lambertian](https://en.wikipedia.org/wiki/Lambertian_reflectance)\n * model for calculating reflectance. This can simulate some surfaces (such\n * as untreated wood or stone) well, but cannot simulate shiny surfaces with\n * specular highlights (such as varnished wood). `MeshLambertMaterial` uses per-fragment\n * shading.\n *\n * Due to the simplicity of the reflectance and illumination models,\n * performance will be greater when using this material over the\n * {@link MeshPhongMaterial}, {@link MeshStandardMaterial} or\n * {@link MeshPhysicalMaterial}, at the cost of some graphical accuracy.\n *\n * @augments Material\n * @demo scenes/material-browser.html#MeshLambertMaterial\n */\nclass MeshLambertMaterial extends Material {\n\n\t/**\n\t * Constructs a new mesh lambert material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshLambertMaterial = true;\n\n\t\tthis.type = 'MeshLambertMaterial';\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.color = new Color( 0xffffff ); // diffuse\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map\n\t\t * color is modulated by the diffuse `color`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The light map. Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.lightMap = null;\n\n\t\t/**\n\t\t * Intensity of the baked light.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.lightMapIntensity = 1.0;\n\n\t\t/**\n\t\t * The red channel of this texture is used as the ambient occlusion map.\n\t\t * Requires a second set of UVs.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.aoMap = null;\n\n\t\t/**\n\t\t * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`\n\t\t * disables ambient occlusion. Where intensity is `1` and the AO map's\n\t\t * red channel is also `1`, ambient light is fully occluded on a surface.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.aoMapIntensity = 1.0;\n\n\t\t/**\n\t\t * Emissive (light) color of the material, essentially a solid color\n\t\t * unaffected by other lighting.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.emissive = new Color( 0x000000 );\n\n\t\t/**\n\t\t * Intensity of the emissive light. Modulates the emissive color.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.emissiveIntensity = 1.0;\n\n\t\t/**\n\t\t * Set emissive (glow) map. The emissive map color is modulated by the\n\t\t * emissive color and the emissive intensity. If you have an emissive map,\n\t\t * be sure to set the emissive color to something other than black.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.emissiveMap = null;\n\n\t\t/**\n\t\t * The texture to create a bump map. The black and white values map to the\n\t\t * perceived depth in relation to the lights. Bump doesn't actually affect\n\t\t * the geometry of the object, only the lighting. If a normal map is defined\n\t\t * this will be ignored.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.bumpMap = null;\n\n\t\t/**\n\t\t * How much the bump map affects the material. Typical range is `[0,1]`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.bumpScale = 1;\n\n\t\t/**\n\t\t * The texture to create a normal map. The RGB values affect the surface\n\t\t * normal for each pixel fragment and change the way the color is lit. Normal\n\t\t * maps do not change the actual shape of the surface, only the lighting. In\n\t\t * case the material has a normal map authored using the left handed\n\t\t * convention, the `y` component of `normalScale` should be negated to compensate\n\t\t * for the different handedness.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.normalMap = null;\n\n\t\t/**\n\t\t * The type of normal map.\n\t\t *\n\t\t * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}\n\t\t * @default TangentSpaceNormalMap\n\t\t */\n\t\tthis.normalMapType = TangentSpaceNormalMap;\n\n\t\t/**\n\t\t * How much the normal map affects the material. Typical value range is `[0,1]`.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (1,1)\n\t\t */\n\t\tthis.normalScale = new Vector2( 1, 1 );\n\n\t\t/**\n\t\t * The displacement map affects the position of the mesh's vertices. Unlike\n\t\t * other maps which only affect the light and shade of the material the\n\t\t * displaced vertices can cast shadows, block other objects, and otherwise\n\t\t * act as real geometry. The displacement texture is an image where the value\n\t\t * of each pixel (white being the highest) is mapped against, and\n\t\t * repositions, the vertices of the mesh.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.displacementMap = null;\n\n\t\t/**\n\t\t * How much the displacement map affects the mesh (where black is no\n\t\t * displacement, and white is maximum displacement). Without a displacement\n\t\t * map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementScale = 1;\n\n\t\t/**\n\t\t * The offset of the displacement map's values on the mesh's vertices.\n\t\t * The bias is added to the scaled sample of the displacement map.\n\t\t * Without a displacement map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementBias = 0;\n\n\t\t/**\n\t\t * Specular map used by the material.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.specularMap = null;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * The environment map.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.envMap = null;\n\n\t\t/**\n\t\t * The rotation of the environment map in radians.\n\t\t *\n\t\t * @type {Euler}\n\t\t * @default (0,0,0)\n\t\t */\n\t\tthis.envMapRotation = new Euler();\n\n\t\t/**\n\t\t * How to combine the result of the surface's color with the environment map, if any.\n\t\t *\n\t\t * When set to `MixOperation`, the {@link MeshBasicMaterial#reflectivity} is used to\n\t\t * blend between the two colors.\n\t\t *\n\t\t * @type {(MultiplyOperation|MixOperation|AddOperation)}\n\t\t * @default MultiplyOperation\n\t\t */\n\t\tthis.combine = MultiplyOperation;\n\n\t\t/**\n\t\t * How much the environment map affects the surface.\n\t\t * The valid range is between `0` (no reflections) and `1` (full reflections).\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.reflectivity = 1;\n\n\t\t/**\n\t\t * The index of refraction (IOR) of air (approximately 1) divided by the\n\t\t * index of refraction of the material. It is used with environment mapping\n\t\t * modes {@link CubeRefractionMapping} and {@link EquirectangularRefractionMapping}.\n\t\t * The refraction ratio should not exceed `1`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0.98\n\t\t */\n\t\tthis.refractionRatio = 0.98;\n\n\t\t/**\n\t\t * Renders the geometry as a wireframe.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.wireframe = false;\n\n\t\t/**\n\t\t * Controls the thickness of the wireframe.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.wireframeLinewidth = 1;\n\n\t\t/**\n\t\t * Defines appearance of wireframe ends.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinecap = 'round';\n\n\t\t/**\n\t\t * Defines appearance of wireframe joints.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {('round'|'bevel'|'miter')}\n\t\t * @default 'round'\n\t\t */\n\t\tthis.wireframeLinejoin = 'round';\n\n\t\t/**\n\t\t * Whether the material is rendered with flat shading or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flatShading = false;\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.color.copy( source.color );\n\n\t\tthis.map = source.map;\n\n\t\tthis.lightMap = source.lightMap;\n\t\tthis.lightMapIntensity = source.lightMapIntensity;\n\n\t\tthis.aoMap = source.aoMap;\n\t\tthis.aoMapIntensity = source.aoMapIntensity;\n\n\t\tthis.emissive.copy( source.emissive );\n\t\tthis.emissiveMap = source.emissiveMap;\n\t\tthis.emissiveIntensity = source.emissiveIntensity;\n\n\t\tthis.bumpMap = source.bumpMap;\n\t\tthis.bumpScale = source.bumpScale;\n\n\t\tthis.normalMap = source.normalMap;\n\t\tthis.normalMapType = source.normalMapType;\n\t\tthis.normalScale.copy( source.normalScale );\n\n\t\tthis.displacementMap = source.displacementMap;\n\t\tthis.displacementScale = source.displacementScale;\n\t\tthis.displacementBias = source.displacementBias;\n\n\t\tthis.specularMap = source.specularMap;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.envMap = source.envMap;\n\t\tthis.envMapRotation.copy( source.envMapRotation );\n\t\tthis.combine = source.combine;\n\t\tthis.reflectivity = source.reflectivity;\n\t\tthis.refractionRatio = source.refractionRatio;\n\n\t\tthis.wireframe = source.wireframe;\n\t\tthis.wireframeLinewidth = source.wireframeLinewidth;\n\t\tthis.wireframeLinecap = source.wireframeLinecap;\n\t\tthis.wireframeLinejoin = source.wireframeLinejoin;\n\n\t\tthis.flatShading = source.flatShading;\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A material for drawing geometry by depth. Depth is based off of the camera\n * near and far plane. White is nearest, black is farthest.\n *\n * @augments Material\n * @demo scenes/material-browser.html#MeshDepthMaterial\n */\nclass MeshDepthMaterial extends Material {\n\n\t/**\n\t * Constructs a new mesh depth material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshDepthMaterial = true;\n\n\t\tthis.type = 'MeshDepthMaterial';\n\n\t\t/**\n\t\t * Type for depth packing.\n\t\t *\n\t\t * @type {(BasicDepthPacking|RGBADepthPacking|RGBDepthPacking|RGDepthPacking)}\n\t\t * @default BasicDepthPacking\n\t\t */\n\t\tthis.depthPacking = BasicDepthPacking;\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * The displacement map affects the position of the mesh's vertices. Unlike\n\t\t * other maps which only affect the light and shade of the material the\n\t\t * displaced vertices can cast shadows, block other objects, and otherwise\n\t\t * act as real geometry. The displacement texture is an image where the value\n\t\t * of each pixel (white being the highest) is mapped against, and\n\t\t * repositions, the vertices of the mesh.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.displacementMap = null;\n\n\t\t/**\n\t\t * How much the displacement map affects the mesh (where black is no\n\t\t * displacement, and white is maximum displacement). Without a displacement\n\t\t * map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementScale = 1;\n\n\t\t/**\n\t\t * The offset of the displacement map's values on the mesh's vertices.\n\t\t * The bias is added to the scaled sample of the displacement map.\n\t\t * Without a displacement map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementBias = 0;\n\n\t\t/**\n\t\t * Renders the geometry as a wireframe.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.wireframe = false;\n\n\t\t/**\n\t\t * Controls the thickness of the wireframe.\n\t\t *\n\t\t * WebGL and WebGPU ignore this property and always render\n\t\t * 1 pixel wide lines.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.wireframeLinewidth = 1;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.depthPacking = source.depthPacking;\n\n\t\tthis.map = source.map;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.displacementMap = source.displacementMap;\n\t\tthis.displacementScale = source.displacementScale;\n\t\tthis.displacementBias = source.displacementBias;\n\n\t\tthis.wireframe = source.wireframe;\n\t\tthis.wireframeLinewidth = source.wireframeLinewidth;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A material used internally for implementing shadow mapping with\n * point lights.\n *\n * Can also be used to customize the shadow casting of an object by assigning\n * an instance of `MeshDistanceMaterial` to {@link Object3D#customDistanceMaterial}.\n * The following examples demonstrates this approach in order to ensure\n * transparent parts of objects do not cast shadows.\n *\n * @augments Material\n */\nclass MeshDistanceMaterial extends Material {\n\n\t/**\n\t * Constructs a new mesh distance material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshDistanceMaterial = true;\n\n\t\tthis.type = 'MeshDistanceMaterial';\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * The displacement map affects the position of the mesh's vertices. Unlike\n\t\t * other maps which only affect the light and shade of the material the\n\t\t * displaced vertices can cast shadows, block other objects, and otherwise\n\t\t * act as real geometry. The displacement texture is an image where the value\n\t\t * of each pixel (white being the highest) is mapped against, and\n\t\t * repositions, the vertices of the mesh.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.displacementMap = null;\n\n\t\t/**\n\t\t * How much the displacement map affects the mesh (where black is no\n\t\t * displacement, and white is maximum displacement). Without a displacement\n\t\t * map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementScale = 1;\n\n\t\t/**\n\t\t * The offset of the displacement map's values on the mesh's vertices.\n\t\t * The bias is added to the scaled sample of the displacement map.\n\t\t * Without a displacement map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementBias = 0;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.map = source.map;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.displacementMap = source.displacementMap;\n\t\tthis.displacementScale = source.displacementScale;\n\t\tthis.displacementBias = source.displacementBias;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * This material is defined by a MatCap (or Lit Sphere) texture, which encodes the\n * material color and shading.\n *\n * `MeshMatcapMaterial` does not respond to lights since the matcap image file encodes\n * baked lighting. It will cast a shadow onto an object that receives shadows\n * (and shadow clipping works), but it will not self-shadow or receive\n * shadows.\n *\n * @augments Material\n * @demo scenes/material-browser.html#MeshMatcapMaterial\n */\nclass MeshMatcapMaterial extends Material {\n\n\t/**\n\t * Constructs a new mesh matcap material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isMeshMatcapMaterial = true;\n\n\t\tthis.defines = { 'MATCAP': '' };\n\n\t\tthis.type = 'MeshMatcapMaterial';\n\n\t\t/**\n\t\t * Color of the material.\n\t\t *\n\t\t * @type {Color}\n\t\t * @default (1,1,1)\n\t\t */\n\t\tthis.color = new Color( 0xffffff ); // diffuse\n\n\t\t/**\n\t\t * The matcap map.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.matcap = null;\n\n\t\t/**\n\t\t * The color map. May optionally include an alpha channel, typically combined\n\t\t * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map\n\t\t * color is modulated by the diffuse `color`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The texture to create a bump map. The black and white values map to the\n\t\t * perceived depth in relation to the lights. Bump doesn't actually affect\n\t\t * the geometry of the object, only the lighting. If a normal map is defined\n\t\t * this will be ignored.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.bumpMap = null;\n\n\t\t/**\n\t\t * How much the bump map affects the material. Typical range is `[0,1]`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.bumpScale = 1;\n\n\t\t/**\n\t\t * The texture to create a normal map. The RGB values affect the surface\n\t\t * normal for each pixel fragment and change the way the color is lit. Normal\n\t\t * maps do not change the actual shape of the surface, only the lighting. In\n\t\t * case the material has a normal map authored using the left handed\n\t\t * convention, the `y` component of `normalScale` should be negated to compensate\n\t\t * for the different handedness.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.normalMap = null;\n\n\t\t/**\n\t\t * The type of normal map.\n\t\t *\n\t\t * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}\n\t\t * @default TangentSpaceNormalMap\n\t\t */\n\t\tthis.normalMapType = TangentSpaceNormalMap;\n\n\t\t/**\n\t\t * How much the normal map affects the material. Typical value range is `[0,1]`.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (1,1)\n\t\t */\n\t\tthis.normalScale = new Vector2( 1, 1 );\n\n\t\t/**\n\t\t * The displacement map affects the position of the mesh's vertices. Unlike\n\t\t * other maps which only affect the light and shade of the material the\n\t\t * displaced vertices can cast shadows, block other objects, and otherwise\n\t\t * act as real geometry. The displacement texture is an image where the value\n\t\t * of each pixel (white being the highest) is mapped against, and\n\t\t * repositions, the vertices of the mesh.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.displacementMap = null;\n\n\t\t/**\n\t\t * How much the displacement map affects the mesh (where black is no\n\t\t * displacement, and white is maximum displacement). Without a displacement\n\t\t * map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementScale = 1;\n\n\t\t/**\n\t\t * The offset of the displacement map's values on the mesh's vertices.\n\t\t * The bias is added to the scaled sample of the displacement map.\n\t\t * Without a displacement map set, this value is not applied.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.displacementBias = 0;\n\n\t\t/**\n\t\t * The alpha map is a grayscale texture that controls the opacity across the\n\t\t * surface (black: fully transparent; white: fully opaque).\n\t\t *\n\t\t * Only the color of the texture is used, ignoring the alpha channel if one\n\t\t * exists. For RGB and RGBA textures, the renderer will use the green channel\n\t\t * when sampling this texture due to the extra bit of precision provided for\n\t\t * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and\n\t\t * luminance/alpha textures will also still work as expected.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.alphaMap = null;\n\n\t\t/**\n\t\t * Renders the geometry as a wireframe.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.wireframe = false;\n\n\t\t/**\n\t\t * Controls the thickness of the wireframe.\n\t\t *\n\t\t * Can only be used with {@link SVGRenderer}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.wireframeLinewidth = 1;\n\n\t\t/**\n\t\t * Whether the material is rendered with flat shading or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.flatShading = false;\n\n\t\t/**\n\t\t * Whether the material is affected by fog or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.fog = true;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.defines = { 'MATCAP': '' };\n\n\t\tthis.color.copy( source.color );\n\n\t\tthis.matcap = source.matcap;\n\n\t\tthis.map = source.map;\n\n\t\tthis.bumpMap = source.bumpMap;\n\t\tthis.bumpScale = source.bumpScale;\n\n\t\tthis.normalMap = source.normalMap;\n\t\tthis.normalMapType = source.normalMapType;\n\t\tthis.normalScale.copy( source.normalScale );\n\n\t\tthis.displacementMap = source.displacementMap;\n\t\tthis.displacementScale = source.displacementScale;\n\t\tthis.displacementBias = source.displacementBias;\n\n\t\tthis.alphaMap = source.alphaMap;\n\n\t\tthis.wireframe = source.wireframe;\n\t\tthis.wireframeLinewidth = source.wireframeLinewidth;\n\n\t\tthis.flatShading = source.flatShading;\n\n\t\tthis.fog = source.fog;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A material for rendering line primitives.\n *\n * Materials define the appearance of renderable 3D objects.\n *\n * ```js\n * const material = new THREE.LineDashedMaterial( {\n * \tcolor: 0xffffff,\n * \tscale: 1,\n * \tdashSize: 3,\n * \tgapSize: 1,\n * } );\n * ```\n *\n * @augments LineBasicMaterial\n */\nclass LineDashedMaterial extends LineBasicMaterial {\n\n\t/**\n\t * Constructs a new line dashed material.\n\t *\n\t * @param {Object} [parameters] - An object with one or more properties\n\t * defining the material's appearance. Any property of the material\n\t * (including any property from inherited materials) can be passed\n\t * in here. Color values can be passed any type of value accepted\n\t * by {@link Color#set}.\n\t */\n\tconstructor( parameters ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLineDashedMaterial = true;\n\t\tthis.type = 'LineDashedMaterial';\n\n\t\t/**\n\t\t * The scale of the dashed part of a line.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.scale = 1;\n\n\t\t/**\n\t\t * The size of the dash. This is both the gap with the stroke.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 3\n\t\t */\n\t\tthis.dashSize = 3;\n\n\t\t/**\n\t\t * The size of the gap.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.gapSize = 1;\n\n\t\tthis.setValues( parameters );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.scale = source.scale;\n\t\tthis.dashSize = source.dashSize;\n\t\tthis.gapSize = source.gapSize;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * Converts an array to a specific type.\n *\n * @param {TypedArray|Array} array - The array to convert.\n * @param {TypedArray.constructor} type - The constructor of a typed array that defines the new type.\n * @return {TypedArray} The converted array.\n */\nfunction convertArray( array, type ) {\n\n\tif ( ! array || array.constructor === type ) return array;\n\n\tif ( typeof type.BYTES_PER_ELEMENT === 'number' ) {\n\n\t\treturn new type( array ); // create typed array\n\n\t}\n\n\treturn Array.prototype.slice.call( array ); // create Array\n\n}\n\n/**\n * Returns an array by which times and values can be sorted.\n *\n * @param {Array<number>} times - The keyframe time values.\n * @return {Array<number>} The array.\n */\nfunction getKeyframeOrder( times ) {\n\n\tfunction compareTime( i, j ) {\n\n\t\treturn times[ i ] - times[ j ];\n\n\t}\n\n\tconst n = times.length;\n\tconst result = new Array( n );\n\tfor ( let i = 0; i !== n; ++ i ) result[ i ] = i;\n\n\tresult.sort( compareTime );\n\n\treturn result;\n\n}\n\n/**\n * Sorts the given array by the previously computed order via `getKeyframeOrder()`.\n *\n * @param {Array<number>} values - The values to sort.\n * @param {number} stride - The stride.\n * @param {Array<number>} order - The sort order.\n * @return {Array<number>} The sorted values.\n */\nfunction sortedArray( values, stride, order ) {\n\n\tconst nValues = values.length;\n\tconst result = new values.constructor( nValues );\n\n\tfor ( let i = 0, dstOffset = 0; dstOffset !== nValues; ++ i ) {\n\n\t\tconst srcOffset = order[ i ] * stride;\n\n\t\tfor ( let j = 0; j !== stride; ++ j ) {\n\n\t\t\tresult[ dstOffset ++ ] = values[ srcOffset + j ];\n\n\t\t}\n\n\t}\n\n\treturn result;\n\n}\n\n/**\n * Used for parsing AOS keyframe formats.\n *\n * @param {Array<number>} jsonKeys - A list of JSON keyframes.\n * @param {Array<number>} times - This array will be filled with keyframe times by this function.\n * @param {Array<number>} values - This array will be filled with keyframe values by this function.\n * @param {string} valuePropertyName - The name of the property to use.\n */\nfunction flattenJSON( jsonKeys, times, values, valuePropertyName ) {\n\n\tlet i = 1, key = jsonKeys[ 0 ];\n\n\twhile ( key !== undefined && key[ valuePropertyName ] === undefined ) {\n\n\t\tkey = jsonKeys[ i ++ ];\n\n\t}\n\n\tif ( key === undefined ) return; // no data\n\n\tlet value = key[ valuePropertyName ];\n\tif ( value === undefined ) return; // no data\n\n\tif ( Array.isArray( value ) ) {\n\n\t\tdo {\n\n\t\t\tvalue = key[ valuePropertyName ];\n\n\t\t\tif ( value !== undefined ) {\n\n\t\t\t\ttimes.push( key.time );\n\t\t\t\tvalues.push( ...value ); // push all elements\n\n\t\t\t}\n\n\t\t\tkey = jsonKeys[ i ++ ];\n\n\t\t} while ( key !== undefined );\n\n\t} else if ( value.toArray !== undefined ) {\n\n\t\t// ...assume THREE.Math-ish\n\n\t\tdo {\n\n\t\t\tvalue = key[ valuePropertyName ];\n\n\t\t\tif ( value !== undefined ) {\n\n\t\t\t\ttimes.push( key.time );\n\t\t\t\tvalue.toArray( values, values.length );\n\n\t\t\t}\n\n\t\t\tkey = jsonKeys[ i ++ ];\n\n\t\t} while ( key !== undefined );\n\n\t} else {\n\n\t\t// otherwise push as-is\n\n\t\tdo {\n\n\t\t\tvalue = key[ valuePropertyName ];\n\n\t\t\tif ( value !== undefined ) {\n\n\t\t\t\ttimes.push( key.time );\n\t\t\t\tvalues.push( value );\n\n\t\t\t}\n\n\t\t\tkey = jsonKeys[ i ++ ];\n\n\t\t} while ( key !== undefined );\n\n\t}\n\n}\n\n/**\n * Creates a new clip, containing only the segment of the original clip between the given frames.\n *\n * @param {AnimationClip} sourceClip - The values to sort.\n * @param {string} name - The name of the clip.\n * @param {number} startFrame - The start frame.\n * @param {number} endFrame - The end frame.\n * @param {number} [fps=30] - The FPS.\n * @return {AnimationClip} The new sub clip.\n */\nfunction subclip( sourceClip, name, startFrame, endFrame, fps = 30 ) {\n\n\tconst clip = sourceClip.clone();\n\n\tclip.name = name;\n\n\tconst tracks = [];\n\n\tfor ( let i = 0; i < clip.tracks.length; ++ i ) {\n\n\t\tconst track = clip.tracks[ i ];\n\t\tconst valueSize = track.getValueSize();\n\n\t\tconst times = [];\n\t\tconst values = [];\n\n\t\tfor ( let j = 0; j < track.times.length; ++ j ) {\n\n\t\t\tconst frame = track.times[ j ] * fps;\n\n\t\t\tif ( frame < startFrame || frame >= endFrame ) continue;\n\n\t\t\ttimes.push( track.times[ j ] );\n\n\t\t\tfor ( let k = 0; k < valueSize; ++ k ) {\n\n\t\t\t\tvalues.push( track.values[ j * valueSize + k ] );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( times.length === 0 ) continue;\n\n\t\ttrack.times = convertArray( times, track.times.constructor );\n\t\ttrack.values = convertArray( values, track.values.constructor );\n\n\t\ttracks.push( track );\n\n\t}\n\n\tclip.tracks = tracks;\n\n\t// find minimum .times value across all tracks in the trimmed clip\n\n\tlet minStartTime = Infinity;\n\n\tfor ( let i = 0; i < clip.tracks.length; ++ i ) {\n\n\t\tif ( minStartTime > clip.tracks[ i ].times[ 0 ] ) {\n\n\t\t\tminStartTime = clip.tracks[ i ].times[ 0 ];\n\n\t\t}\n\n\t}\n\n\t// shift all tracks such that clip begins at t=0\n\n\tfor ( let i = 0; i < clip.tracks.length; ++ i ) {\n\n\t\tclip.tracks[ i ].shift( -1 * minStartTime );\n\n\t}\n\n\tclip.resetDuration();\n\n\treturn clip;\n\n}\n\n/**\n * Converts the keyframes of the given animation clip to an additive format.\n *\n * @param {AnimationClip} targetClip - The clip to make additive.\n * @param {number} [referenceFrame=0] - The reference frame.\n * @param {AnimationClip} [referenceClip=targetClip] - The reference clip.\n * @param {number} [fps=30] - The FPS.\n * @return {AnimationClip} The updated clip which is now additive.\n */\nfunction makeClipAdditive( targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30 ) {\n\n\tif ( fps <= 0 ) fps = 30;\n\n\tconst numTracks = referenceClip.tracks.length;\n\tconst referenceTime = referenceFrame / fps;\n\n\t// Make each track's values relative to the values at the reference frame\n\tfor ( let i = 0; i < numTracks; ++ i ) {\n\n\t\tconst referenceTrack = referenceClip.tracks[ i ];\n\t\tconst referenceTrackType = referenceTrack.ValueTypeName;\n\n\t\t// Skip this track if it's non-numeric\n\t\tif ( referenceTrackType === 'bool' || referenceTrackType === 'string' ) continue;\n\n\t\t// Find the track in the target clip whose name and type matches the reference track\n\t\tconst targetTrack = targetClip.tracks.find( function ( track ) {\n\n\t\t\treturn track.name === referenceTrack.name\n\t\t\t\t&& track.ValueTypeName === referenceTrackType;\n\n\t\t} );\n\n\t\tif ( targetTrack === undefined ) continue;\n\n\t\tlet referenceOffset = 0;\n\t\tconst referenceValueSize = referenceTrack.getValueSize();\n\n\t\tif ( referenceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {\n\n\t\t\treferenceOffset = referenceValueSize / 3;\n\n\t\t}\n\n\t\tlet targetOffset = 0;\n\t\tconst targetValueSize = targetTrack.getValueSize();\n\n\t\tif ( targetTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {\n\n\t\t\ttargetOffset = targetValueSize / 3;\n\n\t\t}\n\n\t\tconst lastIndex = referenceTrack.times.length - 1;\n\t\tlet referenceValue;\n\n\t\t// Find the value to subtract out of the track\n\t\tif ( referenceTime <= referenceTrack.times[ 0 ] ) {\n\n\t\t\t// Reference frame is earlier than the first keyframe, so just use the first keyframe\n\t\t\tconst startIndex = referenceOffset;\n\t\t\tconst endIndex = referenceValueSize - referenceOffset;\n\t\t\treferenceValue = referenceTrack.values.slice( startIndex, endIndex );\n\n\t\t} else if ( referenceTime >= referenceTrack.times[ lastIndex ] ) {\n\n\t\t\t// Reference frame is after the last keyframe, so just use the last keyframe\n\t\t\tconst startIndex = lastIndex * referenceValueSize + referenceOffset;\n\t\t\tconst endIndex = startIndex + referenceValueSize - referenceOffset;\n\t\t\treferenceValue = referenceTrack.values.slice( startIndex, endIndex );\n\n\t\t} else {\n\n\t\t\t// Interpolate to the reference value\n\t\t\tconst interpolant = referenceTrack.createInterpolant();\n\t\t\tconst startIndex = referenceOffset;\n\t\t\tconst endIndex = referenceValueSize - referenceOffset;\n\t\t\tinterpolant.evaluate( referenceTime );\n\t\t\treferenceValue = interpolant.resultBuffer.slice( startIndex, endIndex );\n\n\t\t}\n\n\t\t// Conjugate the quaternion\n\t\tif ( referenceTrackType === 'quaternion' ) {\n\n\t\t\tconst referenceQuat = new Quaternion().fromArray( referenceValue ).normalize().conjugate();\n\t\t\treferenceQuat.toArray( referenceValue );\n\n\t\t}\n\n\t\t// Subtract the reference value from all of the track values\n\n\t\tconst numTimes = targetTrack.times.length;\n\t\tfor ( let j = 0; j < numTimes; ++ j ) {\n\n\t\t\tconst valueStart = j * targetValueSize + targetOffset;\n\n\t\t\tif ( referenceTrackType === 'quaternion' ) {\n\n\t\t\t\t// Multiply the conjugate for quaternion track types\n\t\t\t\tQuaternion.multiplyQuaternionsFlat(\n\t\t\t\t\ttargetTrack.values,\n\t\t\t\t\tvalueStart,\n\t\t\t\t\treferenceValue,\n\t\t\t\t\t0,\n\t\t\t\t\ttargetTrack.values,\n\t\t\t\t\tvalueStart\n\t\t\t\t);\n\n\t\t\t} else {\n\n\t\t\t\tconst valueEnd = targetValueSize - targetOffset * 2;\n\n\t\t\t\t// Subtract each value for all other numeric track types\n\t\t\t\tfor ( let k = 0; k < valueEnd; ++ k ) {\n\n\t\t\t\t\ttargetTrack.values[ valueStart + k ] -= referenceValue[ k ];\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\ttargetClip.blendMode = AdditiveAnimationBlendMode;\n\n\treturn targetClip;\n\n}\n\n/**\n * A class with various methods to assist with animations.\n *\n * @hideconstructor\n */\nclass AnimationUtils {\n\n\t/**\n\t * Converts an array to a specific type\n\t *\n\t * @static\n\t * @param {TypedArray|Array} array - The array to convert.\n\t * @param {TypedArray.constructor} type - The constructor of a type array.\n\t * @return {TypedArray} The converted array\n\t */\n\tstatic convertArray( array, type ) {\n\n\t\treturn convertArray( array, type );\n\n\t}\n\n\t/**\n\t * Returns `true` if the given object is a typed array.\n\t *\n\t * @static\n\t * @param {any} object - The object to check.\n\t * @return {boolean} Whether the given object is a typed array.\n\t */\n\tstatic isTypedArray( object ) {\n\n\t\treturn isTypedArray( object );\n\n\t}\n\n\t/**\n\t * Returns an array by which times and values can be sorted.\n\t *\n\t * @static\n\t * @param {Array<number>} times - The keyframe time values.\n\t * @return {Array<number>} The array.\n\t */\n\tstatic getKeyframeOrder( times ) {\n\n\t\treturn getKeyframeOrder( times );\n\n\t}\n\n\t/**\n\t * Sorts the given array by the previously computed order via `getKeyframeOrder()`.\n\t *\n\t * @static\n\t * @param {Array<number>} values - The values to sort.\n\t * @param {number} stride - The stride.\n\t * @param {Array<number>} order - The sort order.\n\t * @return {Array<number>} The sorted values.\n\t */\n\tstatic sortedArray( values, stride, order ) {\n\n\t\treturn sortedArray( values, stride, order );\n\n\t}\n\n\t/**\n\t * Used for parsing AOS keyframe formats.\n\t *\n\t * @static\n\t * @param {Array<number>} jsonKeys - A list of JSON keyframes.\n\t * @param {Array<number>} times - This array will be filled with keyframe times by this method.\n\t * @param {Array<number>} values - This array will be filled with keyframe values by this method.\n\t * @param {string} valuePropertyName - The name of the property to use.\n\t */\n\tstatic flattenJSON( jsonKeys, times, values, valuePropertyName ) {\n\n\t\tflattenJSON( jsonKeys, times, values, valuePropertyName );\n\n\t}\n\n\t/**\n\t * Creates a new clip, containing only the segment of the original clip between the given frames.\n\t *\n\t * @static\n\t * @param {AnimationClip} sourceClip - The values to sort.\n\t * @param {string} name - The name of the clip.\n\t * @param {number} startFrame - The start frame.\n\t * @param {number} endFrame - The end frame.\n\t * @param {number} [fps=30] - The FPS.\n\t * @return {AnimationClip} The new sub clip.\n\t */\n\tstatic subclip( sourceClip, name, startFrame, endFrame, fps = 30 ) {\n\n\t\treturn subclip( sourceClip, name, startFrame, endFrame, fps );\n\n\t}\n\n\t/**\n\t * Converts the keyframes of the given animation clip to an additive format.\n\t *\n\t * @static\n\t * @param {AnimationClip} targetClip - The clip to make additive.\n\t * @param {number} [referenceFrame=0] - The reference frame.\n\t * @param {AnimationClip} [referenceClip=targetClip] - The reference clip.\n\t * @param {number} [fps=30] - The FPS.\n\t * @return {AnimationClip} The updated clip which is now additive.\n\t */\n\tstatic makeClipAdditive( targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30 ) {\n\n\t\treturn makeClipAdditive( targetClip, referenceFrame, referenceClip, fps );\n\n\t}\n\n}\n\n/**\n * Abstract base class of interpolants over parametric samples.\n *\n * The parameter domain is one dimensional, typically the time or a path\n * along a curve defined by the data.\n *\n * The sample values can have any dimensionality and derived classes may\n * apply special interpretations to the data.\n *\n * This class provides the interval seek in a Template Method, deferring\n * the actual interpolation to derived classes.\n *\n * Time complexity is O(1) for linear access crossing at most two points\n * and O(log N) for random access, where N is the number of positions.\n *\n * References: {@link http://www.oodesign.com/template-method-pattern.html}\n *\n * @abstract\n */\nclass Interpolant {\n\n\t/**\n\t * Constructs a new interpolant.\n\t *\n\t * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.\n\t * @param {TypedArray} sampleValues - The sample values.\n\t * @param {number} sampleSize - The sample size\n\t * @param {TypedArray} [resultBuffer] - The result buffer.\n\t */\n\tconstructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {\n\n\t\t/**\n\t\t * The parameter positions.\n\t\t *\n\t\t * @type {TypedArray}\n\t\t */\n\t\tthis.parameterPositions = parameterPositions;\n\n\t\t/**\n\t\t * A cache index.\n\t\t *\n\t\t * @private\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis._cachedIndex = 0;\n\n\t\t/**\n\t\t * The result buffer.\n\t\t *\n\t\t * @type {TypedArray}\n\t\t */\n\t\tthis.resultBuffer = resultBuffer !== undefined ? resultBuffer : new sampleValues.constructor( sampleSize );\n\n\t\t/**\n\t\t * The sample values.\n\t\t *\n\t\t * @type {TypedArray}\n\t\t */\n\t\tthis.sampleValues = sampleValues;\n\n\t\t/**\n\t\t * The value size.\n\t\t *\n\t\t * @type {TypedArray}\n\t\t */\n\t\tthis.valueSize = sampleSize;\n\n\t\t/**\n\t\t * The interpolation settings.\n\t\t *\n\t\t * @type {?Object}\n\t\t * @default null\n\t\t */\n\t\tthis.settings = null;\n\n\t\t/**\n\t\t * The default settings object.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.DefaultSettings_ = {};\n\n\t}\n\n\t/**\n\t * Evaluate the interpolant at position `t`.\n\t *\n\t * @param {number} t - The interpolation factor.\n\t * @return {TypedArray} The result buffer.\n\t */\n\tevaluate( t ) {\n\n\t\tconst pp = this.parameterPositions;\n\t\tlet i1 = this._cachedIndex,\n\t\t\tt1 = pp[ i1 ],\n\t\t\tt0 = pp[ i1 - 1 ];\n\n\t\tvalidate_interval: {\n\n\t\t\tseek: {\n\n\t\t\t\tlet right;\n\n\t\t\t\tlinear_scan: {\n\n\t\t\t\t\t//- See http://jsperf.com/comparison-to-undefined/3\n\t\t\t\t\t//- slower code:\n\t\t\t\t\t//-\n\t\t\t\t\t//- \t\t\t\tif ( t >= t1 || t1 === undefined ) {\n\t\t\t\t\tforward_scan: if ( ! ( t < t1 ) ) {\n\n\t\t\t\t\t\tfor ( let giveUpAt = i1 + 2; ; ) {\n\n\t\t\t\t\t\t\tif ( t1 === undefined ) {\n\n\t\t\t\t\t\t\t\tif ( t < t0 ) break forward_scan;\n\n\t\t\t\t\t\t\t\t// after end\n\n\t\t\t\t\t\t\t\ti1 = pp.length;\n\t\t\t\t\t\t\t\tthis._cachedIndex = i1;\n\t\t\t\t\t\t\t\treturn this.copySampleValue_( i1 - 1 );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif ( i1 === giveUpAt ) break; // this loop\n\n\t\t\t\t\t\t\tt0 = t1;\n\t\t\t\t\t\t\tt1 = pp[ ++ i1 ];\n\n\t\t\t\t\t\t\tif ( t < t1 ) {\n\n\t\t\t\t\t\t\t\t// we have arrived at the sought interval\n\t\t\t\t\t\t\t\tbreak seek;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// prepare binary search on the right side of the index\n\t\t\t\t\t\tright = pp.length;\n\t\t\t\t\t\tbreak linear_scan;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t//- slower code:\n\t\t\t\t\t//-\t\t\t\t\tif ( t < t0 || t0 === undefined ) {\n\t\t\t\t\tif ( ! ( t >= t0 ) ) {\n\n\t\t\t\t\t\t// looping?\n\n\t\t\t\t\t\tconst t1global = pp[ 1 ];\n\n\t\t\t\t\t\tif ( t < t1global ) {\n\n\t\t\t\t\t\t\ti1 = 2; // + 1, using the scan for the details\n\t\t\t\t\t\t\tt0 = t1global;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// linear reverse scan\n\n\t\t\t\t\t\tfor ( let giveUpAt = i1 - 2; ; ) {\n\n\t\t\t\t\t\t\tif ( t0 === undefined ) {\n\n\t\t\t\t\t\t\t\t// before start\n\n\t\t\t\t\t\t\t\tthis._cachedIndex = 0;\n\t\t\t\t\t\t\t\treturn this.copySampleValue_( 0 );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif ( i1 === giveUpAt ) break; // this loop\n\n\t\t\t\t\t\t\tt1 = t0;\n\t\t\t\t\t\t\tt0 = pp[ -- i1 - 1 ];\n\n\t\t\t\t\t\t\tif ( t >= t0 ) {\n\n\t\t\t\t\t\t\t\t// we have arrived at the sought interval\n\t\t\t\t\t\t\t\tbreak seek;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// prepare binary search on the left side of the index\n\t\t\t\t\t\tright = i1;\n\t\t\t\t\t\ti1 = 0;\n\t\t\t\t\t\tbreak linear_scan;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// the interval is valid\n\n\t\t\t\t\tbreak validate_interval;\n\n\t\t\t\t} // linear scan\n\n\t\t\t\t// binary search\n\n\t\t\t\twhile ( i1 < right ) {\n\n\t\t\t\t\tconst mid = ( i1 + right ) >>> 1;\n\n\t\t\t\t\tif ( t < pp[ mid ] ) {\n\n\t\t\t\t\t\tright = mid;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\ti1 = mid + 1;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tt1 = pp[ i1 ];\n\t\t\t\tt0 = pp[ i1 - 1 ];\n\n\t\t\t\t// check boundary cases, again\n\n\t\t\t\tif ( t0 === undefined ) {\n\n\t\t\t\t\tthis._cachedIndex = 0;\n\t\t\t\t\treturn this.copySampleValue_( 0 );\n\n\t\t\t\t}\n\n\t\t\t\tif ( t1 === undefined ) {\n\n\t\t\t\t\ti1 = pp.length;\n\t\t\t\t\tthis._cachedIndex = i1;\n\t\t\t\t\treturn this.copySampleValue_( i1 - 1 );\n\n\t\t\t\t}\n\n\t\t\t} // seek\n\n\t\t\tthis._cachedIndex = i1;\n\n\t\t\tthis.intervalChanged_( i1, t0, t1 );\n\n\t\t} // validate_interval\n\n\t\treturn this.interpolate_( i1, t0, t, t1 );\n\n\t}\n\n\t/**\n\t * Returns the interpolation settings.\n\t *\n\t * @return {Object} The interpolation settings.\n\t */\n\tgetSettings_() {\n\n\t\treturn this.settings || this.DefaultSettings_;\n\n\t}\n\n\t/**\n\t * Copies a sample value to the result buffer.\n\t *\n\t * @param {number} index - An index into the sample value buffer.\n\t * @return {TypedArray} The result buffer.\n\t */\n\tcopySampleValue_( index ) {\n\n\t\t// copies a sample value to the result buffer\n\n\t\tconst result = this.resultBuffer,\n\t\t\tvalues = this.sampleValues,\n\t\t\tstride = this.valueSize,\n\t\t\toffset = index * stride;\n\n\t\tfor ( let i = 0; i !== stride; ++ i ) {\n\n\t\t\tresult[ i ] = values[ offset + i ];\n\n\t\t}\n\n\t\treturn result;\n\n\t}\n\n\t/**\n\t * Copies a sample value to the result buffer.\n\t *\n\t * @abstract\n\t * @param {number} i1 - An index into the sample value buffer.\n\t * @param {number} t0 - The previous interpolation factor.\n\t * @param {number} t - The current interpolation factor.\n\t * @param {number} t1 - The next interpolation factor.\n\t * @return {TypedArray} The result buffer.\n\t */\n\tinterpolate_( /* i1, t0, t, t1 */ ) {\n\n\t\tthrow new Error( 'call to abstract method' );\n\t\t// implementations shall return this.resultBuffer\n\n\t}\n\n\t/**\n\t * Optional method that is executed when the interval has changed.\n\t *\n\t * @param {number} i1 - An index into the sample value buffer.\n\t * @param {number} t0 - The previous interpolation factor.\n\t * @param {number} t - The current interpolation factor.\n\t */\n\tintervalChanged_( /* i1, t0, t1 */ ) {\n\n\t\t// empty\n\n\t}\n\n}\n\n/**\n * Fast and simple cubic spline interpolant.\n *\n * It was derived from a Hermitian construction setting the first derivative\n * at each sample position to the linear slope between neighboring positions\n * over their parameter interval.\n *\n * @augments Interpolant\n */\nclass CubicInterpolant extends Interpolant {\n\n\t/**\n\t * Constructs a new cubic interpolant.\n\t *\n\t * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.\n\t * @param {TypedArray} sampleValues - The sample values.\n\t * @param {number} sampleSize - The sample size\n\t * @param {TypedArray} [resultBuffer] - The result buffer.\n\t */\n\tconstructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {\n\n\t\tsuper( parameterPositions, sampleValues, sampleSize, resultBuffer );\n\n\t\tthis._weightPrev = -0;\n\t\tthis._offsetPrev = -0;\n\t\tthis._weightNext = -0;\n\t\tthis._offsetNext = -0;\n\n\t\tthis.DefaultSettings_ = {\n\n\t\t\tendingStart: ZeroCurvatureEnding,\n\t\t\tendingEnd: ZeroCurvatureEnding\n\n\t\t};\n\n\t}\n\n\tintervalChanged_( i1, t0, t1 ) {\n\n\t\tconst pp = this.parameterPositions;\n\t\tlet iPrev = i1 - 2,\n\t\t\tiNext = i1 + 1,\n\n\t\t\ttPrev = pp[ iPrev ],\n\t\t\ttNext = pp[ iNext ];\n\n\t\tif ( tPrev === undefined ) {\n\n\t\t\tswitch ( this.getSettings_().endingStart ) {\n\n\t\t\t\tcase ZeroSlopeEnding:\n\n\t\t\t\t\t// f'(t0) = 0\n\t\t\t\t\tiPrev = i1;\n\t\t\t\t\ttPrev = 2 * t0 - t1;\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase WrapAroundEnding:\n\n\t\t\t\t\t// use the other end of the curve\n\t\t\t\t\tiPrev = pp.length - 2;\n\t\t\t\t\ttPrev = t0 + pp[ iPrev ] - pp[ iPrev + 1 ];\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault: // ZeroCurvatureEnding\n\n\t\t\t\t\t// f''(t0) = 0 a.k.a. Natural Spline\n\t\t\t\t\tiPrev = i1;\n\t\t\t\t\ttPrev = t1;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( tNext === undefined ) {\n\n\t\t\tswitch ( this.getSettings_().endingEnd ) {\n\n\t\t\t\tcase ZeroSlopeEnding:\n\n\t\t\t\t\t// f'(tN) = 0\n\t\t\t\t\tiNext = i1;\n\t\t\t\t\ttNext = 2 * t1 - t0;\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase WrapAroundEnding:\n\n\t\t\t\t\t// use the other end of the curve\n\t\t\t\t\tiNext = 1;\n\t\t\t\t\ttNext = t1 + pp[ 1 ] - pp[ 0 ];\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault: // ZeroCurvatureEnding\n\n\t\t\t\t\t// f''(tN) = 0, a.k.a. Natural Spline\n\t\t\t\t\tiNext = i1 - 1;\n\t\t\t\t\ttNext = t0;\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst halfDt = ( t1 - t0 ) * 0.5,\n\t\t\tstride = this.valueSize;\n\n\t\tthis._weightPrev = halfDt / ( t0 - tPrev );\n\t\tthis._weightNext = halfDt / ( tNext - t1 );\n\t\tthis._offsetPrev = iPrev * stride;\n\t\tthis._offsetNext = iNext * stride;\n\n\t}\n\n\tinterpolate_( i1, t0, t, t1 ) {\n\n\t\tconst result = this.resultBuffer,\n\t\t\tvalues = this.sampleValues,\n\t\t\tstride = this.valueSize,\n\n\t\t\to1 = i1 * stride,\t\to0 = o1 - stride,\n\t\t\toP = this._offsetPrev, \toN = this._offsetNext,\n\t\t\twP = this._weightPrev,\twN = this._weightNext,\n\n\t\t\tp = ( t - t0 ) / ( t1 - t0 ),\n\t\t\tpp = p * p,\n\t\t\tppp = pp * p;\n\n\t\t// evaluate polynomials\n\n\t\tconst sP = - wP * ppp + 2 * wP * pp - wP * p;\n\t\tconst s0 = ( 1 + wP ) * ppp + ( -1.5 - 2 * wP ) * pp + ( -0.5 + wP ) * p + 1;\n\t\tconst s1 = ( -1 - wN ) * ppp + ( 1.5 + wN ) * pp + 0.5 * p;\n\t\tconst sN = wN * ppp - wN * pp;\n\n\t\t// combine data linearly\n\n\t\tfor ( let i = 0; i !== stride; ++ i ) {\n\n\t\t\tresult[ i ] =\n\t\t\t\t\tsP * values[ oP + i ] +\n\t\t\t\t\ts0 * values[ o0 + i ] +\n\t\t\t\t\ts1 * values[ o1 + i ] +\n\t\t\t\t\tsN * values[ oN + i ];\n\n\t\t}\n\n\t\treturn result;\n\n\t}\n\n}\n\n/**\n * A basic linear interpolant.\n *\n * @augments Interpolant\n */\nclass LinearInterpolant extends Interpolant {\n\n\t/**\n\t * Constructs a new linear interpolant.\n\t *\n\t * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.\n\t * @param {TypedArray} sampleValues - The sample values.\n\t * @param {number} sampleSize - The sample size\n\t * @param {TypedArray} [resultBuffer] - The result buffer.\n\t */\n\tconstructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {\n\n\t\tsuper( parameterPositions, sampleValues, sampleSize, resultBuffer );\n\n\t}\n\n\tinterpolate_( i1, t0, t, t1 ) {\n\n\t\tconst result = this.resultBuffer,\n\t\t\tvalues = this.sampleValues,\n\t\t\tstride = this.valueSize,\n\n\t\t\toffset1 = i1 * stride,\n\t\t\toffset0 = offset1 - stride,\n\n\t\t\tweight1 = ( t - t0 ) / ( t1 - t0 ),\n\t\t\tweight0 = 1 - weight1;\n\n\t\tfor ( let i = 0; i !== stride; ++ i ) {\n\n\t\t\tresult[ i ] =\n\t\t\t\t\tvalues[ offset0 + i ] * weight0 +\n\t\t\t\t\tvalues[ offset1 + i ] * weight1;\n\n\t\t}\n\n\t\treturn result;\n\n\t}\n\n}\n\n/**\n * Interpolant that evaluates to the sample value at the position preceding\n * the parameter.\n *\n * @augments Interpolant\n */\nclass DiscreteInterpolant extends Interpolant {\n\n\t/**\n\t * Constructs a new discrete interpolant.\n\t *\n\t * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.\n\t * @param {TypedArray} sampleValues - The sample values.\n\t * @param {number} sampleSize - The sample size\n\t * @param {TypedArray} [resultBuffer] - The result buffer.\n\t */\n\tconstructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {\n\n\t\tsuper( parameterPositions, sampleValues, sampleSize, resultBuffer );\n\n\t}\n\n\tinterpolate_( i1 /*, t0, t, t1 */ ) {\n\n\t\treturn this.copySampleValue_( i1 - 1 );\n\n\t}\n\n}\n\n/**\n * Represents s a timed sequence of keyframes, which are composed of lists of\n * times and related values, and which are used to animate a specific property\n * of an object.\n */\nclass KeyframeTrack {\n\n\t/**\n\t * Constructs a new keyframe track.\n\t *\n\t * @param {string} name - The keyframe track's name.\n\t * @param {Array<number>} times - A list of keyframe times.\n\t * @param {Array<number|string|boolean>} values - A list of keyframe values.\n\t * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} [interpolation] - The interpolation type.\n\t */\n\tconstructor( name, times, values, interpolation ) {\n\n\t\tif ( name === undefined ) throw new Error( 'THREE.KeyframeTrack: track name is undefined' );\n\t\tif ( times === undefined || times.length === 0 ) throw new Error( 'THREE.KeyframeTrack: no keyframes in track named ' + name );\n\n\t\t/**\n\t\t * The track's name can refer to morph targets or bones or\n\t\t * possibly other values within an animated object. See {@link PropertyBinding#parseTrackName}\n\t\t * for the forms of strings that can be parsed for property binding.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = name;\n\n\t\t/**\n\t\t * The keyframe times.\n\t\t *\n\t\t * @type {Float32Array}\n\t\t */\n\t\tthis.times = convertArray( times, this.TimeBufferType );\n\n\t\t/**\n\t\t * The keyframe values.\n\t\t *\n\t\t * @type {Float32Array}\n\t\t */\n\t\tthis.values = convertArray( values, this.ValueBufferType );\n\n\t\tthis.setInterpolation( interpolation || this.DefaultInterpolation );\n\n\t}\n\n\t/**\n\t * Converts the keyframe track to JSON.\n\t *\n\t * @static\n\t * @param {KeyframeTrack} track - The keyframe track to serialize.\n\t * @return {Object} The serialized keyframe track as JSON.\n\t */\n\tstatic toJSON( track ) {\n\n\t\tconst trackType = track.constructor;\n\n\t\tlet json;\n\n\t\t// derived classes can define a static toJSON method\n\t\tif ( trackType.toJSON !== this.toJSON ) {\n\n\t\t\tjson = trackType.toJSON( track );\n\n\t\t} else {\n\n\t\t\t// by default, we assume the data can be serialized as-is\n\t\t\tjson = {\n\n\t\t\t\t'name': track.name,\n\t\t\t\t'times': convertArray( track.times, Array ),\n\t\t\t\t'values': convertArray( track.values, Array )\n\n\t\t\t};\n\n\t\t\tconst interpolation = track.getInterpolation();\n\n\t\t\tif ( interpolation !== track.DefaultInterpolation ) {\n\n\t\t\t\tjson.interpolation = interpolation;\n\n\t\t\t}\n\n\t\t}\n\n\t\tjson.type = track.ValueTypeName; // mandatory\n\n\t\treturn json;\n\n\t}\n\n\t/**\n\t * Factory method for creating a new discrete interpolant.\n\t *\n\t * @static\n\t * @param {TypedArray} [result] - The result buffer.\n\t * @return {DiscreteInterpolant} The new interpolant.\n\t */\n\tInterpolantFactoryMethodDiscrete( result ) {\n\n\t\treturn new DiscreteInterpolant( this.times, this.values, this.getValueSize(), result );\n\n\t}\n\n\t/**\n\t * Factory method for creating a new linear interpolant.\n\t *\n\t * @static\n\t * @param {TypedArray} [result] - The result buffer.\n\t * @return {LinearInterpolant} The new interpolant.\n\t */\n\tInterpolantFactoryMethodLinear( result ) {\n\n\t\treturn new LinearInterpolant( this.times, this.values, this.getValueSize(), result );\n\n\t}\n\n\t/**\n\t * Factory method for creating a new smooth interpolant.\n\t *\n\t * @static\n\t * @param {TypedArray} [result] - The result buffer.\n\t * @return {CubicInterpolant} The new interpolant.\n\t */\n\tInterpolantFactoryMethodSmooth( result ) {\n\n\t\treturn new CubicInterpolant( this.times, this.values, this.getValueSize(), result );\n\n\t}\n\n\t/**\n\t * Defines the interpolation factor method for this keyframe track.\n\t *\n\t * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} interpolation - The interpolation type.\n\t * @return {KeyframeTrack} A reference to this keyframe track.\n\t */\n\tsetInterpolation( interpolation ) {\n\n\t\tlet factoryMethod;\n\n\t\tswitch ( interpolation ) {\n\n\t\t\tcase InterpolateDiscrete:\n\n\t\t\t\tfactoryMethod = this.InterpolantFactoryMethodDiscrete;\n\n\t\t\t\tbreak;\n\n\t\t\tcase InterpolateLinear:\n\n\t\t\t\tfactoryMethod = this.InterpolantFactoryMethodLinear;\n\n\t\t\t\tbreak;\n\n\t\t\tcase InterpolateSmooth:\n\n\t\t\t\tfactoryMethod = this.InterpolantFactoryMethodSmooth;\n\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\tif ( factoryMethod === undefined ) {\n\n\t\t\tconst message = 'unsupported interpolation for ' +\n\t\t\t\tthis.ValueTypeName + ' keyframe track named ' + this.name;\n\n\t\t\tif ( this.createInterpolant === undefined ) {\n\n\t\t\t\t// fall back to default, unless the default itself is messed up\n\t\t\t\tif ( interpolation !== this.DefaultInterpolation ) {\n\n\t\t\t\t\tthis.setInterpolation( this.DefaultInterpolation );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthrow new Error( message ); // fatal, in this case\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\twarn( 'KeyframeTrack:', message );\n\t\t\treturn this;\n\n\t\t}\n\n\t\tthis.createInterpolant = factoryMethod;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the current interpolation type.\n\t *\n\t * @return {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} The interpolation type.\n\t */\n\tgetInterpolation() {\n\n\t\tswitch ( this.createInterpolant ) {\n\n\t\t\tcase this.InterpolantFactoryMethodDiscrete:\n\n\t\t\t\treturn InterpolateDiscrete;\n\n\t\t\tcase this.InterpolantFactoryMethodLinear:\n\n\t\t\t\treturn InterpolateLinear;\n\n\t\t\tcase this.InterpolantFactoryMethodSmooth:\n\n\t\t\t\treturn InterpolateSmooth;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns the value size.\n\t *\n\t * @return {number} The value size.\n\t */\n\tgetValueSize() {\n\n\t\treturn this.values.length / this.times.length;\n\n\t}\n\n\t/**\n\t * Moves all keyframes either forward or backward in time.\n\t *\n\t * @param {number} timeOffset - The offset to move the time values.\n\t * @return {KeyframeTrack} A reference to this keyframe track.\n\t */\n\tshift( timeOffset ) {\n\n\t\tif ( timeOffset !== 0.0 ) {\n\n\t\t\tconst times = this.times;\n\n\t\t\tfor ( let i = 0, n = times.length; i !== n; ++ i ) {\n\n\t\t\t\ttimes[ i ] += timeOffset;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Scale all keyframe times by a factor (useful for frame - seconds conversions).\n\t *\n\t * @param {number} timeScale - The time scale.\n\t * @return {KeyframeTrack} A reference to this keyframe track.\n\t */\n\tscale( timeScale ) {\n\n\t\tif ( timeScale !== 1.0 ) {\n\n\t\t\tconst times = this.times;\n\n\t\t\tfor ( let i = 0, n = times.length; i !== n; ++ i ) {\n\n\t\t\t\ttimes[ i ] *= timeScale;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Removes keyframes before and after animation without changing any values within the defined time range.\n\t *\n\t * Note: The method does not shift around keys to the start of the track time, because for interpolated\n\t * keys this will change their values\n\t *\n\t * @param {number} startTime - The start time.\n\t * @param {number} endTime - The end time.\n\t * @return {KeyframeTrack} A reference to this keyframe track.\n\t */\n\ttrim( startTime, endTime ) {\n\n\t\tconst times = this.times,\n\t\t\tnKeys = times.length;\n\n\t\tlet from = 0,\n\t\t\tto = nKeys - 1;\n\n\t\twhile ( from !== nKeys && times[ from ] < startTime ) {\n\n\t\t\t++ from;\n\n\t\t}\n\n\t\twhile ( to !== -1 && times[ to ] > endTime ) {\n\n\t\t\t-- to;\n\n\t\t}\n\n\t\t++ to; // inclusive -> exclusive bound\n\n\t\tif ( from !== 0 || to !== nKeys ) {\n\n\t\t\t// empty tracks are forbidden, so keep at least one keyframe\n\t\t\tif ( from >= to ) {\n\n\t\t\t\tto = Math.max( to, 1 );\n\t\t\t\tfrom = to - 1;\n\n\t\t\t}\n\n\t\t\tconst stride = this.getValueSize();\n\t\t\tthis.times = times.slice( from, to );\n\t\t\tthis.values = this.values.slice( from * stride, to * stride );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Performs minimal validation on the keyframe track. Returns `true` if the values\n\t * are valid.\n\t *\n\t * @return {boolean} Whether the keyframes are valid or not.\n\t */\n\tvalidate() {\n\n\t\tlet valid = true;\n\n\t\tconst valueSize = this.getValueSize();\n\t\tif ( valueSize - Math.floor( valueSize ) !== 0 ) {\n\n\t\t\terror( 'KeyframeTrack: Invalid value size in track.', this );\n\t\t\tvalid = false;\n\n\t\t}\n\n\t\tconst times = this.times,\n\t\t\tvalues = this.values,\n\n\t\t\tnKeys = times.length;\n\n\t\tif ( nKeys === 0 ) {\n\n\t\t\terror( 'KeyframeTrack: Track is empty.', this );\n\t\t\tvalid = false;\n\n\t\t}\n\n\t\tlet prevTime = null;\n\n\t\tfor ( let i = 0; i !== nKeys; i ++ ) {\n\n\t\t\tconst currTime = times[ i ];\n\n\t\t\tif ( typeof currTime === 'number' && isNaN( currTime ) ) {\n\n\t\t\t\terror( 'KeyframeTrack: Time is not a valid number.', this, i, currTime );\n\t\t\t\tvalid = false;\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t\tif ( prevTime !== null && prevTime > currTime ) {\n\n\t\t\t\terror( 'KeyframeTrack: Out of order keys.', this, i, currTime, prevTime );\n\t\t\t\tvalid = false;\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t\tprevTime = currTime;\n\n\t\t}\n\n\t\tif ( values !== undefined ) {\n\n\t\t\tif ( isTypedArray( values ) ) {\n\n\t\t\t\tfor ( let i = 0, n = values.length; i !== n; ++ i ) {\n\n\t\t\t\t\tconst value = values[ i ];\n\n\t\t\t\t\tif ( isNaN( value ) ) {\n\n\t\t\t\t\t\terror( 'KeyframeTrack: Value is not a valid number.', this, i, value );\n\t\t\t\t\t\tvalid = false;\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn valid;\n\n\t}\n\n\t/**\n\t * Optimizes this keyframe track by removing equivalent sequential keys (which are\n\t * common in morph target sequences).\n\t *\n\t * @return {AnimationClip} A reference to this animation clip.\n\t */\n\toptimize() {\n\n\t\t// (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)\n\n\t\t// times or values may be shared with other tracks, so overwriting is unsafe\n\t\tconst times = this.times.slice(),\n\t\t\tvalues = this.values.slice(),\n\t\t\tstride = this.getValueSize(),\n\n\t\t\tsmoothInterpolation = this.getInterpolation() === InterpolateSmooth,\n\n\t\t\tlastIndex = times.length - 1;\n\n\t\tlet writeIndex = 1;\n\n\t\tfor ( let i = 1; i < lastIndex; ++ i ) {\n\n\t\t\tlet keep = false;\n\n\t\t\tconst time = times[ i ];\n\t\t\tconst timeNext = times[ i + 1 ];\n\n\t\t\t// remove adjacent keyframes scheduled at the same time\n\n\t\t\tif ( time !== timeNext && ( i !== 1 || time !== times[ 0 ] ) ) {\n\n\t\t\t\tif ( ! smoothInterpolation ) {\n\n\t\t\t\t\t// remove unnecessary keyframes same as their neighbors\n\n\t\t\t\t\tconst offset = i * stride,\n\t\t\t\t\t\toffsetP = offset - stride,\n\t\t\t\t\t\toffsetN = offset + stride;\n\n\t\t\t\t\tfor ( let j = 0; j !== stride; ++ j ) {\n\n\t\t\t\t\t\tconst value = values[ offset + j ];\n\n\t\t\t\t\t\tif ( value !== values[ offsetP + j ] ||\n\t\t\t\t\t\t\tvalue !== values[ offsetN + j ] ) {\n\n\t\t\t\t\t\t\tkeep = true;\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tkeep = true;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// in-place compaction\n\n\t\t\tif ( keep ) {\n\n\t\t\t\tif ( i !== writeIndex ) {\n\n\t\t\t\t\ttimes[ writeIndex ] = times[ i ];\n\n\t\t\t\t\tconst readOffset = i * stride,\n\t\t\t\t\t\twriteOffset = writeIndex * stride;\n\n\t\t\t\t\tfor ( let j = 0; j !== stride; ++ j ) {\n\n\t\t\t\t\t\tvalues[ writeOffset + j ] = values[ readOffset + j ];\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\t++ writeIndex;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// flush last keyframe (compaction looks ahead)\n\n\t\tif ( lastIndex > 0 ) {\n\n\t\t\ttimes[ writeIndex ] = times[ lastIndex ];\n\n\t\t\tfor ( let readOffset = lastIndex * stride, writeOffset = writeIndex * stride, j = 0; j !== stride; ++ j ) {\n\n\t\t\t\tvalues[ writeOffset + j ] = values[ readOffset + j ];\n\n\t\t\t}\n\n\t\t\t++ writeIndex;\n\n\t\t}\n\n\t\tif ( writeIndex !== times.length ) {\n\n\t\t\tthis.times = times.slice( 0, writeIndex );\n\t\t\tthis.values = values.slice( 0, writeIndex * stride );\n\n\t\t} else {\n\n\t\t\tthis.times = times;\n\t\t\tthis.values = values;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new keyframe track with copied values from this instance.\n\t *\n\t * @return {KeyframeTrack} A clone of this instance.\n\t */\n\tclone() {\n\n\t\tconst times = this.times.slice();\n\t\tconst values = this.values.slice();\n\n\t\tconst TypedKeyframeTrack = this.constructor;\n\t\tconst track = new TypedKeyframeTrack( this.name, times, values );\n\n\t\t// Interpolant argument to constructor is not saved, so copy the factory method directly.\n\t\ttrack.createInterpolant = this.createInterpolant;\n\n\t\treturn track;\n\n\t}\n\n}\n\n/**\n * The value type name.\n *\n * @type {string}\n * @default ''\n */\nKeyframeTrack.prototype.ValueTypeName = '';\n\n/**\n * The time buffer type of this keyframe track.\n *\n * @type {TypedArray|Array}\n * @default Float32Array.constructor\n */\nKeyframeTrack.prototype.TimeBufferType = Float32Array;\n\n/**\n * The value buffer type of this keyframe track.\n *\n * @type {TypedArray|Array}\n * @default Float32Array.constructor\n */\nKeyframeTrack.prototype.ValueBufferType = Float32Array;\n\n/**\n * The default interpolation type of this keyframe track.\n *\n * @type {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)}\n * @default InterpolateLinear\n */\nKeyframeTrack.prototype.DefaultInterpolation = InterpolateLinear;\n\n/**\n * A track for boolean keyframe values.\n *\n * @augments KeyframeTrack\n */\nclass BooleanKeyframeTrack extends KeyframeTrack {\n\n\t/**\n\t * Constructs a new boolean keyframe track.\n\t *\n\t * This keyframe track type has no `interpolation` parameter because the\n\t * interpolation is always discrete.\n\t *\n\t * @param {string} name - The keyframe track's name.\n\t * @param {Array<number>} times - A list of keyframe times.\n\t * @param {Array<boolean>} values - A list of keyframe values.\n\t */\n\tconstructor( name, times, values ) {\n\n\t\tsuper( name, times, values );\n\n\t}\n\n}\n\n/**\n * The value type name.\n *\n * @type {string}\n * @default 'bool'\n */\nBooleanKeyframeTrack.prototype.ValueTypeName = 'bool';\n\n/**\n * The value buffer type of this keyframe track.\n *\n * @type {TypedArray|Array}\n * @default Array.constructor\n */\nBooleanKeyframeTrack.prototype.ValueBufferType = Array;\n\n/**\n * The default interpolation type of this keyframe track.\n *\n * @type {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)}\n * @default InterpolateDiscrete\n */\nBooleanKeyframeTrack.prototype.DefaultInterpolation = InterpolateDiscrete;\nBooleanKeyframeTrack.prototype.InterpolantFactoryMethodLinear = undefined;\nBooleanKeyframeTrack.prototype.InterpolantFactoryMethodSmooth = undefined;\n\n/**\n * A track for color keyframe values.\n *\n * @augments KeyframeTrack\n */\nclass ColorKeyframeTrack extends KeyframeTrack {\n\n\t/**\n\t * Constructs a new color keyframe track.\n\t *\n\t * @param {string} name - The keyframe track's name.\n\t * @param {Array<number>} times - A list of keyframe times.\n\t * @param {Array<number>} values - A list of keyframe values.\n\t * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} [interpolation] - The interpolation type.\n\t */\n\tconstructor( name, times, values, interpolation ) {\n\n\t\tsuper( name, times, values, interpolation );\n\n\t}\n\n}\n\n/**\n * The value type name.\n *\n * @type {string}\n * @default 'color'\n */\nColorKeyframeTrack.prototype.ValueTypeName = 'color';\n\n/**\n * A track for numeric keyframe values.\n *\n * @augments KeyframeTrack\n */\nclass NumberKeyframeTrack extends KeyframeTrack {\n\n\t/**\n\t * Constructs a new number keyframe track.\n\t *\n\t * @param {string} name - The keyframe track's name.\n\t * @param {Array<number>} times - A list of keyframe times.\n\t * @param {Array<number>} values - A list of keyframe values.\n\t * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} [interpolation] - The interpolation type.\n\t */\n\tconstructor( name, times, values, interpolation ) {\n\n\t\tsuper( name, times, values, interpolation );\n\n\t}\n\n}\n\n/**\n * The value type name.\n *\n * @type {string}\n * @default 'number'\n */\nNumberKeyframeTrack.prototype.ValueTypeName = 'number';\n\n/**\n * Spherical linear unit quaternion interpolant.\n *\n * @augments Interpolant\n */\nclass QuaternionLinearInterpolant extends Interpolant {\n\n\t/**\n\t * Constructs a new SLERP interpolant.\n\t *\n\t * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.\n\t * @param {TypedArray} sampleValues - The sample values.\n\t * @param {number} sampleSize - The sample size\n\t * @param {TypedArray} [resultBuffer] - The result buffer.\n\t */\n\tconstructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {\n\n\t\tsuper( parameterPositions, sampleValues, sampleSize, resultBuffer );\n\n\t}\n\n\tinterpolate_( i1, t0, t, t1 ) {\n\n\t\tconst result = this.resultBuffer,\n\t\t\tvalues = this.sampleValues,\n\t\t\tstride = this.valueSize,\n\n\t\t\talpha = ( t - t0 ) / ( t1 - t0 );\n\n\t\tlet offset = i1 * stride;\n\n\t\tfor ( let end = offset + stride; offset !== end; offset += 4 ) {\n\n\t\t\tQuaternion.slerpFlat( result, 0, values, offset - stride, values, offset, alpha );\n\n\t\t}\n\n\t\treturn result;\n\n\t}\n\n}\n\n/**\n * A track for Quaternion keyframe values.\n *\n * @augments KeyframeTrack\n */\nclass QuaternionKeyframeTrack extends KeyframeTrack {\n\n\t/**\n\t * Constructs a new Quaternion keyframe track.\n\t *\n\t * @param {string} name - The keyframe track's name.\n\t * @param {Array<number>} times - A list of keyframe times.\n\t * @param {Array<number>} values - A list of keyframe values.\n\t * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} [interpolation] - The interpolation type.\n\t */\n\tconstructor( name, times, values, interpolation ) {\n\n\t\tsuper( name, times, values, interpolation );\n\n\t}\n\n\t/**\n\t * Overwritten so the method returns Quaternion based interpolant.\n\t *\n\t * @static\n\t * @param {TypedArray} [result] - The result buffer.\n\t * @return {QuaternionLinearInterpolant} The new interpolant.\n\t */\n\tInterpolantFactoryMethodLinear( result ) {\n\n\t\treturn new QuaternionLinearInterpolant( this.times, this.values, this.getValueSize(), result );\n\n\t}\n\n}\n\n/**\n * The value type name.\n *\n * @type {string}\n * @default 'quaternion'\n */\nQuaternionKeyframeTrack.prototype.ValueTypeName = 'quaternion';\n// ValueBufferType is inherited\n// DefaultInterpolation is inherited;\nQuaternionKeyframeTrack.prototype.InterpolantFactoryMethodSmooth = undefined;\n\n/**\n * A track for string keyframe values.\n *\n * @augments KeyframeTrack\n */\nclass StringKeyframeTrack extends KeyframeTrack {\n\n\t/**\n\t * Constructs a new string keyframe track.\n\t *\n\t * This keyframe track type has no `interpolation` parameter because the\n\t * interpolation is always discrete.\n\t *\n\t * @param {string} name - The keyframe track's name.\n\t * @param {Array<number>} times - A list of keyframe times.\n\t * @param {Array<string>} values - A list of keyframe values.\n\t */\n\tconstructor( name, times, values ) {\n\n\t\tsuper( name, times, values );\n\n\t}\n\n}\n\n/**\n * The value type name.\n *\n * @type {string}\n * @default 'string'\n */\nStringKeyframeTrack.prototype.ValueTypeName = 'string';\n\n/**\n * The value buffer type of this keyframe track.\n *\n * @type {TypedArray|Array}\n * @default Array.constructor\n */\nStringKeyframeTrack.prototype.ValueBufferType = Array;\n\n/**\n * The default interpolation type of this keyframe track.\n *\n * @type {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)}\n * @default InterpolateDiscrete\n */\nStringKeyframeTrack.prototype.DefaultInterpolation = InterpolateDiscrete;\nStringKeyframeTrack.prototype.InterpolantFactoryMethodLinear = undefined;\nStringKeyframeTrack.prototype.InterpolantFactoryMethodSmooth = undefined;\n\n/**\n * A track for vector keyframe values.\n *\n * @augments KeyframeTrack\n */\nclass VectorKeyframeTrack extends KeyframeTrack {\n\n\t/**\n\t * Constructs a new vector keyframe track.\n\t *\n\t * @param {string} name - The keyframe track's name.\n\t * @param {Array<number>} times - A list of keyframe times.\n\t * @param {Array<number>} values - A list of keyframe values.\n\t * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} [interpolation] - The interpolation type.\n\t */\n\tconstructor( name, times, values, interpolation ) {\n\n\t\tsuper( name, times, values, interpolation );\n\n\t}\n\n}\n\n/**\n * The value type name.\n *\n * @type {string}\n * @default 'vector'\n */\nVectorKeyframeTrack.prototype.ValueTypeName = 'vector';\n\n/**\n * A reusable set of keyframe tracks which represent an animation.\n */\nclass AnimationClip {\n\n\t/**\n\t * Constructs a new animation clip.\n\t *\n\t * Note: Instead of instantiating an AnimationClip directly with the constructor, you can\n\t * use the static interface of this class for creating clips. In most cases though, animation clips\n\t * will automatically be created by loaders when importing animated 3D assets.\n\t *\n\t * @param {string} [name=''] - The clip's name.\n\t * @param {number} [duration=-1] - The clip's duration in seconds. If a negative value is passed,\n\t * the duration will be calculated from the passed keyframes.\n\t * @param {Array<KeyframeTrack>} tracks - An array of keyframe tracks.\n\t * @param {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)} [blendMode=NormalAnimationBlendMode] - Defines how the animation\n\t * is blended/combined when two or more animations are simultaneously played.\n\t */\n\tconstructor( name = '', duration = -1, tracks = [], blendMode = NormalAnimationBlendMode ) {\n\n\t\t/**\n\t\t * The clip's name.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = name;\n\n\t\t/**\n\t\t *  An array of keyframe tracks.\n\t\t *\n\t\t * @type {Array<KeyframeTrack>}\n\t\t */\n\t\tthis.tracks = tracks;\n\n\t\t/**\n\t\t * The clip's duration in seconds.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.duration = duration;\n\n\t\t/**\n\t\t * Defines how the animation is blended/combined when two or more animations\n\t\t * are simultaneously played.\n\t\t *\n\t\t * @type {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)}\n\t\t */\n\t\tthis.blendMode = blendMode;\n\n\t\t/**\n\t\t * The UUID of the animation clip.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.uuid = generateUUID();\n\n\t\t/**\n\t\t * An object that can be used to store custom data about the animation clip.\n\t\t * It should not hold references to functions as these will not be cloned.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.userData = {};\n\n\t\t// this means it should figure out its duration by scanning the tracks\n\t\tif ( this.duration < 0 ) {\n\n\t\t\tthis.resetDuration();\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Factory method for creating an animation clip from the given JSON.\n\t *\n\t * @static\n\t * @param {Object} json - The serialized animation clip.\n\t * @return {AnimationClip} The new animation clip.\n\t */\n\tstatic parse( json ) {\n\n\t\tconst tracks = [],\n\t\t\tjsonTracks = json.tracks,\n\t\t\tframeTime = 1.0 / ( json.fps || 1.0 );\n\n\t\tfor ( let i = 0, n = jsonTracks.length; i !== n; ++ i ) {\n\n\t\t\ttracks.push( parseKeyframeTrack( jsonTracks[ i ] ).scale( frameTime ) );\n\n\t\t}\n\n\t\tconst clip = new this( json.name, json.duration, tracks, json.blendMode );\n\t\tclip.uuid = json.uuid;\n\n\t\tclip.userData = JSON.parse( json.userData || '{}' );\n\n\t\treturn clip;\n\n\t}\n\n\t/**\n\t * Serializes the given animation clip into JSON.\n\t *\n\t * @static\n\t * @param {AnimationClip} clip - The animation clip to serialize.\n\t * @return {Object} The JSON object.\n\t */\n\tstatic toJSON( clip ) {\n\n\t\tconst tracks = [],\n\t\t\tclipTracks = clip.tracks;\n\n\t\tconst json = {\n\n\t\t\t'name': clip.name,\n\t\t\t'duration': clip.duration,\n\t\t\t'tracks': tracks,\n\t\t\t'uuid': clip.uuid,\n\t\t\t'blendMode': clip.blendMode,\n\t\t\t'userData': JSON.stringify( clip.userData ),\n\n\t\t};\n\n\t\tfor ( let i = 0, n = clipTracks.length; i !== n; ++ i ) {\n\n\t\t\ttracks.push( KeyframeTrack.toJSON( clipTracks[ i ] ) );\n\n\t\t}\n\n\t\treturn json;\n\n\t}\n\n\t/**\n\t * Returns a new animation clip from the passed morph targets array of a\n\t * geometry, taking a name and the number of frames per second.\n\t *\n\t * Note: The fps parameter is required, but the animation speed can be\n\t * overridden via {@link AnimationAction#setDuration}.\n\t *\n\t * @static\n\t * @param {string} name - The name of the animation clip.\n\t * @param {Array<Object>} morphTargetSequence - A sequence of morph targets.\n\t * @param {number} fps - The Frames-Per-Second value.\n\t * @param {boolean} noLoop - Whether the clip should be no loop or not.\n\t * @return {AnimationClip} The new animation clip.\n\t */\n\tstatic CreateFromMorphTargetSequence( name, morphTargetSequence, fps, noLoop ) {\n\n\t\tconst numMorphTargets = morphTargetSequence.length;\n\t\tconst tracks = [];\n\n\t\tfor ( let i = 0; i < numMorphTargets; i ++ ) {\n\n\t\t\tlet times = [];\n\t\t\tlet values = [];\n\n\t\t\ttimes.push(\n\t\t\t\t( i + numMorphTargets - 1 ) % numMorphTargets,\n\t\t\t\ti,\n\t\t\t\t( i + 1 ) % numMorphTargets );\n\n\t\t\tvalues.push( 0, 1, 0 );\n\n\t\t\tconst order = getKeyframeOrder( times );\n\t\t\ttimes = sortedArray( times, 1, order );\n\t\t\tvalues = sortedArray( values, 1, order );\n\n\t\t\t// if there is a key at the first frame, duplicate it as the\n\t\t\t// last frame as well for perfect loop.\n\t\t\tif ( ! noLoop && times[ 0 ] === 0 ) {\n\n\t\t\t\ttimes.push( numMorphTargets );\n\t\t\t\tvalues.push( values[ 0 ] );\n\n\t\t\t}\n\n\t\t\ttracks.push(\n\t\t\t\tnew NumberKeyframeTrack(\n\t\t\t\t\t'.morphTargetInfluences[' + morphTargetSequence[ i ].name + ']',\n\t\t\t\t\ttimes, values\n\t\t\t\t).scale( 1.0 / fps ) );\n\n\t\t}\n\n\t\treturn new this( name, -1, tracks );\n\n\t}\n\n\t/**\n\t * Searches for an animation clip by name, taking as its first parameter\n\t * either an array of clips, or a mesh or geometry that contains an\n\t * array named \"animations\" property.\n\t *\n\t * @static\n\t * @param {(Array<AnimationClip>|Object3D)} objectOrClipArray - The array or object to search through.\n\t * @param {string} name - The name to search for.\n\t * @return {?AnimationClip} The found animation clip. Returns `null` if no clip has been found.\n\t */\n\tstatic findByName( objectOrClipArray, name ) {\n\n\t\tlet clipArray = objectOrClipArray;\n\n\t\tif ( ! Array.isArray( objectOrClipArray ) ) {\n\n\t\t\tconst o = objectOrClipArray;\n\t\t\tclipArray = o.geometry && o.geometry.animations || o.animations;\n\n\t\t}\n\n\t\tfor ( let i = 0; i < clipArray.length; i ++ ) {\n\n\t\t\tif ( clipArray[ i ].name === name ) {\n\n\t\t\t\treturn clipArray[ i ];\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn null;\n\n\t}\n\n\t/**\n\t * Returns an array of new AnimationClips created from the morph target\n\t * sequences of a geometry, trying to sort morph target names into\n\t * animation-group-based patterns like \"Walk_001, Walk_002, Run_001, Run_002...\".\n\t *\n\t * See {@link MD2Loader#parse} as an example for how the method should be used.\n\t *\n\t * @static\n\t * @param {Array<Object>} morphTargets - A sequence of morph targets.\n\t * @param {number} fps - The Frames-Per-Second value.\n\t * @param {boolean} noLoop - Whether the clip should be no loop or not.\n\t * @return {Array<AnimationClip>} An array of new animation clips.\n\t */\n\tstatic CreateClipsFromMorphTargetSequences( morphTargets, fps, noLoop ) {\n\n\t\tconst animationToMorphTargets = {};\n\n\t\t// tested with https://regex101.com/ on trick sequences\n\t\t// such flamingo_flyA_003, flamingo_run1_003, crdeath0059\n\t\tconst pattern = /^([\\w-]*?)([\\d]+)$/;\n\n\t\t// sort morph target names into animation groups based\n\t\t// patterns like Walk_001, Walk_002, Run_001, Run_002\n\t\tfor ( let i = 0, il = morphTargets.length; i < il; i ++ ) {\n\n\t\t\tconst morphTarget = morphTargets[ i ];\n\t\t\tconst parts = morphTarget.name.match( pattern );\n\n\t\t\tif ( parts && parts.length > 1 ) {\n\n\t\t\t\tconst name = parts[ 1 ];\n\n\t\t\t\tlet animationMorphTargets = animationToMorphTargets[ name ];\n\n\t\t\t\tif ( ! animationMorphTargets ) {\n\n\t\t\t\t\tanimationToMorphTargets[ name ] = animationMorphTargets = [];\n\n\t\t\t\t}\n\n\t\t\t\tanimationMorphTargets.push( morphTarget );\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst clips = [];\n\n\t\tfor ( const name in animationToMorphTargets ) {\n\n\t\t\tclips.push( this.CreateFromMorphTargetSequence( name, animationToMorphTargets[ name ], fps, noLoop ) );\n\n\t\t}\n\n\t\treturn clips;\n\n\t}\n\n\t/**\n\t * Parses the `animation.hierarchy` format and returns a new animation clip.\n\t *\n\t * @static\n\t * @deprecated since r175.\n\t * @param {Object} animation - A serialized animation clip as JSON.\n\t * @param {Array<Bones>} bones - An array of bones.\n\t * @return {?AnimationClip} The new animation clip.\n\t */\n\tstatic parseAnimation( animation, bones ) {\n\n\t\twarn( 'AnimationClip: parseAnimation() is deprecated and will be removed with r185' );\n\n\t\tif ( ! animation ) {\n\n\t\t\terror( 'AnimationClip: No animation in JSONLoader data.' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\tconst addNonemptyTrack = function ( trackType, trackName, animationKeys, propertyName, destTracks ) {\n\n\t\t\t// only return track if there are actually keys.\n\t\t\tif ( animationKeys.length !== 0 ) {\n\n\t\t\t\tconst times = [];\n\t\t\t\tconst values = [];\n\n\t\t\t\tflattenJSON( animationKeys, times, values, propertyName );\n\n\t\t\t\t// empty keys are filtered out, so check again\n\t\t\t\tif ( times.length !== 0 ) {\n\n\t\t\t\t\tdestTracks.push( new trackType( trackName, times, values ) );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t};\n\n\t\tconst tracks = [];\n\n\t\tconst clipName = animation.name || 'default';\n\t\tconst fps = animation.fps || 30;\n\t\tconst blendMode = animation.blendMode;\n\n\t\t// automatic length determination in AnimationClip.\n\t\tlet duration = animation.length || -1;\n\n\t\tconst hierarchyTracks = animation.hierarchy || [];\n\n\t\tfor ( let h = 0; h < hierarchyTracks.length; h ++ ) {\n\n\t\t\tconst animationKeys = hierarchyTracks[ h ].keys;\n\n\t\t\t// skip empty tracks\n\t\t\tif ( ! animationKeys || animationKeys.length === 0 ) continue;\n\n\t\t\t// process morph targets\n\t\t\tif ( animationKeys[ 0 ].morphTargets ) {\n\n\t\t\t\t// figure out all morph targets used in this track\n\t\t\t\tconst morphTargetNames = {};\n\n\t\t\t\tlet k;\n\n\t\t\t\tfor ( k = 0; k < animationKeys.length; k ++ ) {\n\n\t\t\t\t\tif ( animationKeys[ k ].morphTargets ) {\n\n\t\t\t\t\t\tfor ( let m = 0; m < animationKeys[ k ].morphTargets.length; m ++ ) {\n\n\t\t\t\t\t\t\tmorphTargetNames[ animationKeys[ k ].morphTargets[ m ] ] = -1;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\t// create a track for each morph target with all zero\n\t\t\t\t// morphTargetInfluences except for the keys in which\n\t\t\t\t// the morphTarget is named.\n\t\t\t\tfor ( const morphTargetName in morphTargetNames ) {\n\n\t\t\t\t\tconst times = [];\n\t\t\t\t\tconst values = [];\n\n\t\t\t\t\tfor ( let m = 0; m !== animationKeys[ k ].morphTargets.length; ++ m ) {\n\n\t\t\t\t\t\tconst animationKey = animationKeys[ k ];\n\n\t\t\t\t\t\ttimes.push( animationKey.time );\n\t\t\t\t\t\tvalues.push( ( animationKey.morphTarget === morphTargetName ) ? 1 : 0 );\n\n\t\t\t\t\t}\n\n\t\t\t\t\ttracks.push( new NumberKeyframeTrack( '.morphTargetInfluence[' + morphTargetName + ']', times, values ) );\n\n\t\t\t\t}\n\n\t\t\t\tduration = morphTargetNames.length * fps;\n\n\t\t\t} else {\n\n\t\t\t\t// ...assume skeletal animation\n\n\t\t\t\tconst boneName = '.bones[' + bones[ h ].name + ']';\n\n\t\t\t\taddNonemptyTrack(\n\t\t\t\t\tVectorKeyframeTrack, boneName + '.position',\n\t\t\t\t\tanimationKeys, 'pos', tracks );\n\n\t\t\t\taddNonemptyTrack(\n\t\t\t\t\tQuaternionKeyframeTrack, boneName + '.quaternion',\n\t\t\t\t\tanimationKeys, 'rot', tracks );\n\n\t\t\t\taddNonemptyTrack(\n\t\t\t\t\tVectorKeyframeTrack, boneName + '.scale',\n\t\t\t\t\tanimationKeys, 'scl', tracks );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( tracks.length === 0 ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\tconst clip = new this( clipName, duration, tracks, blendMode );\n\n\t\treturn clip;\n\n\t}\n\n\t/**\n\t * Sets the duration of this clip to the duration of its longest keyframe track.\n\t *\n\t * @return {AnimationClip} A reference to this animation clip.\n\t */\n\tresetDuration() {\n\n\t\tconst tracks = this.tracks;\n\t\tlet duration = 0;\n\n\t\tfor ( let i = 0, n = tracks.length; i !== n; ++ i ) {\n\n\t\t\tconst track = this.tracks[ i ];\n\n\t\t\tduration = Math.max( duration, track.times[ track.times.length - 1 ] );\n\n\t\t}\n\n\t\tthis.duration = duration;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Trims all tracks to the clip's duration.\n\t *\n\t * @return {AnimationClip} A reference to this animation clip.\n\t */\n\ttrim() {\n\n\t\tfor ( let i = 0; i < this.tracks.length; i ++ ) {\n\n\t\t\tthis.tracks[ i ].trim( 0, this.duration );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Performs minimal validation on each track in the clip. Returns `true` if all\n\t * tracks are valid.\n\t *\n\t * @return {boolean} Whether the clip's keyframes are valid or not.\n\t */\n\tvalidate() {\n\n\t\tlet valid = true;\n\n\t\tfor ( let i = 0; i < this.tracks.length; i ++ ) {\n\n\t\t\tvalid = valid && this.tracks[ i ].validate();\n\n\t\t}\n\n\t\treturn valid;\n\n\t}\n\n\t/**\n\t * Optimizes each track by removing equivalent sequential keys (which are\n\t * common in morph target sequences).\n\t *\n\t * @return {AnimationClip} A reference to this animation clip.\n\t */\n\toptimize() {\n\n\t\tfor ( let i = 0; i < this.tracks.length; i ++ ) {\n\n\t\t\tthis.tracks[ i ].optimize();\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new animation clip with copied values from this instance.\n\t *\n\t * @return {AnimationClip} A clone of this instance.\n\t */\n\tclone() {\n\n\t\tconst tracks = [];\n\n\t\tfor ( let i = 0; i < this.tracks.length; i ++ ) {\n\n\t\t\ttracks.push( this.tracks[ i ].clone() );\n\n\t\t}\n\n\t\tconst clip = new this.constructor( this.name, this.duration, tracks, this.blendMode );\n\n\t\tclip.userData = JSON.parse( JSON.stringify( this.userData ) );\n\n\t\treturn clip;\n\n\t}\n\n\t/**\n\t * Serializes this animation clip into JSON.\n\t *\n\t * @return {Object} The JSON object.\n\t */\n\ttoJSON() {\n\n\t\treturn this.constructor.toJSON( this );\n\n\t}\n\n}\n\nfunction getTrackTypeForValueTypeName( typeName ) {\n\n\tswitch ( typeName.toLowerCase() ) {\n\n\t\tcase 'scalar':\n\t\tcase 'double':\n\t\tcase 'float':\n\t\tcase 'number':\n\t\tcase 'integer':\n\n\t\t\treturn NumberKeyframeTrack;\n\n\t\tcase 'vector':\n\t\tcase 'vector2':\n\t\tcase 'vector3':\n\t\tcase 'vector4':\n\n\t\t\treturn VectorKeyframeTrack;\n\n\t\tcase 'color':\n\n\t\t\treturn ColorKeyframeTrack;\n\n\t\tcase 'quaternion':\n\n\t\t\treturn QuaternionKeyframeTrack;\n\n\t\tcase 'bool':\n\t\tcase 'boolean':\n\n\t\t\treturn BooleanKeyframeTrack;\n\n\t\tcase 'string':\n\n\t\t\treturn StringKeyframeTrack;\n\n\t}\n\n\tthrow new Error( 'THREE.KeyframeTrack: Unsupported typeName: ' + typeName );\n\n}\n\nfunction parseKeyframeTrack( json ) {\n\n\tif ( json.type === undefined ) {\n\n\t\tthrow new Error( 'THREE.KeyframeTrack: track type undefined, can not parse' );\n\n\t}\n\n\tconst trackType = getTrackTypeForValueTypeName( json.type );\n\n\tif ( json.times === undefined ) {\n\n\t\tconst times = [], values = [];\n\n\t\tflattenJSON( json.keys, times, values, 'value' );\n\n\t\tjson.times = times;\n\t\tjson.values = values;\n\n\t}\n\n\t// derived classes can define a static parse method\n\tif ( trackType.parse !== undefined ) {\n\n\t\treturn trackType.parse( json );\n\n\t} else {\n\n\t\t// by default, we assume a constructor compatible with the base\n\t\treturn new trackType( json.name, json.times, json.values, json.interpolation );\n\n\t}\n\n}\n\n/**\n * @class\n * @classdesc A simple caching system, used internally by {@link FileLoader}.\n * To enable caching across all loaders that use {@link FileLoader}, add `THREE.Cache.enabled = true.` once in your app.\n * @hideconstructor\n */\nconst Cache = {\n\n\t/**\n\t * Whether caching is enabled or not.\n\t *\n\t * @static\n\t * @type {boolean}\n\t * @default false\n\t */\n\tenabled: false,\n\n\t/**\n\t * A dictionary that holds cached files.\n\t *\n\t * @static\n\t * @type {Object<string,Object>}\n\t */\n\tfiles: {},\n\n\t/**\n\t * Adds a cache entry with a key to reference the file. If this key already\n\t * holds a file, it is overwritten.\n\t *\n\t * @static\n\t * @param {string} key - The key to reference the cached file.\n\t * @param {Object} file -  The file to be cached.\n\t */\n\tadd: function ( key, file ) {\n\n\t\tif ( this.enabled === false ) return;\n\n\t\t// log( 'Cache', 'Adding key:', key );\n\n\t\tthis.files[ key ] = file;\n\n\t},\n\n\t/**\n\t * Gets the cached value for the given key.\n\t *\n\t * @static\n\t * @param {string} key - The key to reference the cached file.\n\t * @return {Object|undefined} The cached file. If the key does not exist `undefined` is returned.\n\t */\n\tget: function ( key ) {\n\n\t\tif ( this.enabled === false ) return;\n\n\t\t// log( 'Cache', 'Checking key:', key );\n\n\t\treturn this.files[ key ];\n\n\t},\n\n\t/**\n\t * Removes the cached file associated with the given key.\n\t *\n\t * @static\n\t * @param {string} key - The key to reference the cached file.\n\t */\n\tremove: function ( key ) {\n\n\t\tdelete this.files[ key ];\n\n\t},\n\n\t/**\n\t * Remove all values from the cache.\n\t *\n\t * @static\n\t */\n\tclear: function () {\n\n\t\tthis.files = {};\n\n\t}\n\n};\n\n/**\n * Handles and keeps track of loaded and pending data. A default global\n * instance of this class is created and used by loaders if not supplied\n * manually.\n *\n * In general that should be sufficient, however there are times when it can\n * be useful to have separate loaders - for example if you want to show\n * separate loading bars for objects and textures.\n *\n * ```js\n * const manager = new THREE.LoadingManager();\n * manager.onLoad = () => console.log( 'Loading complete!' );\n *\n * const loader1 = new OBJLoader( manager );\n * const loader2 = new ColladaLoader( manager );\n * ```\n */\nclass LoadingManager {\n\n\t/**\n\t * Constructs a new loading manager.\n\t *\n\t * @param {Function} [onLoad] - Executes when all items have been loaded.\n\t * @param {Function} [onProgress] - Executes when single items have been loaded.\n\t * @param {Function} [onError] - Executes when an error occurs.\n\t */\n\tconstructor( onLoad, onProgress, onError ) {\n\n\t\tconst scope = this;\n\n\t\tlet isLoading = false;\n\t\tlet itemsLoaded = 0;\n\t\tlet itemsTotal = 0;\n\t\tlet urlModifier = undefined;\n\t\tconst handlers = [];\n\n\t\t// Refer to #5689 for the reason why we don't set .onStart\n\t\t// in the constructor\n\n\t\t/**\n\t\t * Executes when an item starts loading.\n\t\t *\n\t\t * @type {Function|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.onStart = undefined;\n\n\t\t/**\n\t\t * Executes when all items have been loaded.\n\t\t *\n\t\t * @type {Function|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.onLoad = onLoad;\n\n\t\t/**\n\t\t * Executes when single items have been loaded.\n\t\t *\n\t\t * @type {Function|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.onProgress = onProgress;\n\n\t\t/**\n\t\t * Executes when an error occurs.\n\t\t *\n\t\t * @type {Function|undefined}\n\t\t * @default undefined\n\t\t */\n\t\tthis.onError = onError;\n\n\t\t/**\n\t\t * Used for aborting ongoing requests in loaders using this manager.\n\t\t *\n\t\t * @private\n\t\t * @type {AbortController | null}\n\t\t */\n\t\tthis._abortController = null;\n\n\t\t/**\n\t\t * This should be called by any loader using the manager when the loader\n\t\t * starts loading an item.\n\t\t *\n\t\t * @param {string} url - The URL to load.\n\t\t */\n\t\tthis.itemStart = function ( url ) {\n\n\t\t\titemsTotal ++;\n\n\t\t\tif ( isLoading === false ) {\n\n\t\t\t\tif ( scope.onStart !== undefined ) {\n\n\t\t\t\t\tscope.onStart( url, itemsLoaded, itemsTotal );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tisLoading = true;\n\n\t\t};\n\n\t\t/**\n\t\t * This should be called by any loader using the manager when the loader\n\t\t * ended loading an item.\n\t\t *\n\t\t * @param {string} url - The URL of the loaded item.\n\t\t */\n\t\tthis.itemEnd = function ( url ) {\n\n\t\t\titemsLoaded ++;\n\n\t\t\tif ( scope.onProgress !== undefined ) {\n\n\t\t\t\tscope.onProgress( url, itemsLoaded, itemsTotal );\n\n\t\t\t}\n\n\t\t\tif ( itemsLoaded === itemsTotal ) {\n\n\t\t\t\tisLoading = false;\n\n\t\t\t\tif ( scope.onLoad !== undefined ) {\n\n\t\t\t\t\tscope.onLoad();\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * This should be called by any loader using the manager when the loader\n\t\t * encounters an error when loading an item.\n\t\t *\n\t\t * @param {string} url - The URL of the item that produces an error.\n\t\t */\n\t\tthis.itemError = function ( url ) {\n\n\t\t\tif ( scope.onError !== undefined ) {\n\n\t\t\t\tscope.onError( url );\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Given a URL, uses the URL modifier callback (if any) and returns a\n\t\t * resolved URL. If no URL modifier is set, returns the original URL.\n\t\t *\n\t\t * @param {string} url - The URL to load.\n\t\t * @return {string} The resolved URL.\n\t\t */\n\t\tthis.resolveURL = function ( url ) {\n\n\t\t\tif ( urlModifier ) {\n\n\t\t\t\treturn urlModifier( url );\n\n\t\t\t}\n\n\t\t\treturn url;\n\n\t\t};\n\n\t\t/**\n\t\t * If provided, the callback will be passed each resource URL before a\n\t\t * request is sent. The callback may return the original URL, or a new URL to\n\t\t * override loading behavior. This behavior can be used to load assets from\n\t\t * .ZIP files, drag-and-drop APIs, and Data URIs.\n\t\t *\n\t\t * ```js\n\t\t * const blobs = {'fish.gltf': blob1, 'diffuse.png': blob2, 'normal.png': blob3};\n\t\t *\n\t\t * const manager = new THREE.LoadingManager();\n\t\t *\n\t\t * // Initialize loading manager with URL callback.\n\t\t * const objectURLs = [];\n\t\t * manager.setURLModifier( ( url ) => {\n\t\t *\n\t\t * \turl = URL.createObjectURL( blobs[ url ] );\n\t\t * \tobjectURLs.push( url );\n\t\t * \treturn url;\n\t\t *\n\t\t * } );\n\t\t *\n\t\t * // Load as usual, then revoke the blob URLs.\n\t\t * const loader = new GLTFLoader( manager );\n\t\t * loader.load( 'fish.gltf', (gltf) => {\n\t\t *\n\t\t * \tscene.add( gltf.scene );\n\t\t * \tobjectURLs.forEach( ( url ) => URL.revokeObjectURL( url ) );\n\t\t *\n\t\t * } );\n\t\t * ```\n\t\t *\n\t\t * @param {function(string):string} transform - URL modifier callback. Called with an URL and must return a resolved URL.\n\t\t * @return {LoadingManager} A reference to this loading manager.\n\t\t */\n\t\tthis.setURLModifier = function ( transform ) {\n\n\t\t\turlModifier = transform;\n\n\t\t\treturn this;\n\n\t\t};\n\n\t\t/**\n\t\t * Registers a loader with the given regular expression. Can be used to\n\t\t * define what loader should be used in order to load specific files. A\n\t\t * typical use case is to overwrite the default loader for textures.\n\t\t *\n\t\t * ```js\n\t\t * // add handler for TGA textures\n\t\t * manager.addHandler( /\\.tga$/i, new TGALoader() );\n\t\t * ```\n\t\t *\n\t\t * @param {string} regex - A regular expression.\n\t\t * @param {Loader} loader - A loader that should handle matched cases.\n\t\t * @return {LoadingManager} A reference to this loading manager.\n\t\t */\n\t\tthis.addHandler = function ( regex, loader ) {\n\n\t\t\thandlers.push( regex, loader );\n\n\t\t\treturn this;\n\n\t\t};\n\n\t\t/**\n\t\t * Removes the loader for the given regular expression.\n\t\t *\n\t\t * @param {string} regex - A regular expression.\n\t\t * @return {LoadingManager} A reference to this loading manager.\n\t\t */\n\t\tthis.removeHandler = function ( regex ) {\n\n\t\t\tconst index = handlers.indexOf( regex );\n\n\t\t\tif ( index !== -1 ) {\n\n\t\t\t\thandlers.splice( index, 2 );\n\n\t\t\t}\n\n\t\t\treturn this;\n\n\t\t};\n\n\t\t/**\n\t\t * Can be used to retrieve the registered loader for the given file path.\n\t\t *\n\t\t * @param {string} file - The file path.\n\t\t * @return {?Loader} The registered loader. Returns `null` if no loader was found.\n\t\t */\n\t\tthis.getHandler = function ( file ) {\n\n\t\t\tfor ( let i = 0, l = handlers.length; i < l; i += 2 ) {\n\n\t\t\t\tconst regex = handlers[ i ];\n\t\t\t\tconst loader = handlers[ i + 1 ];\n\n\t\t\t\tif ( regex.global ) regex.lastIndex = 0; // see #17920\n\n\t\t\t\tif ( regex.test( file ) ) {\n\n\t\t\t\t\treturn loader;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn null;\n\n\t\t};\n\n\t\t/**\n\t\t * Can be used to abort ongoing loading requests in loaders using this manager.\n\t\t * The abort only works if the loaders implement {@link Loader#abort} and `AbortSignal.any()`\n\t\t * is supported in the browser.\n\t\t *\n\t\t * @return {LoadingManager} A reference to this loading manager.\n\t\t */\n\t\tthis.abort = function () {\n\n\n\t\t\tthis.abortController.abort();\n\t\t\tthis._abortController = null;\n\n\t\t\treturn this;\n\n\t\t};\n\n\t}\n\n\t// TODO: Revert this back to a single member variable once this issue has been fixed\n\t// https://github.com/cloudflare/workerd/issues/3657\n\n\t/**\n\t * Used for aborting ongoing requests in loaders using this manager.\n\t *\n\t * @type {AbortController}\n\t */\n\tget abortController() {\n\n\t\tif ( ! this._abortController ) {\n\n\t\t\tthis._abortController = new AbortController();\n\n\t\t}\n\n\t\treturn this._abortController;\n\n\t}\n\n}\n\n/**\n * The global default loading manager.\n *\n * @constant\n * @type {LoadingManager}\n */\nconst DefaultLoadingManager = /*@__PURE__*/ new LoadingManager();\n\n/**\n * Abstract base class for loaders.\n *\n * @abstract\n */\nclass Loader {\n\n\t/**\n\t * Constructs a new loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\t/**\n\t\t * The loading manager.\n\t\t *\n\t\t * @type {LoadingManager}\n\t\t * @default DefaultLoadingManager\n\t\t */\n\t\tthis.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager;\n\n\t\t/**\n\t\t * The crossOrigin string to implement CORS for loading the url from a\n\t\t * different domain that allows CORS.\n\t\t *\n\t\t * @type {string}\n\t\t * @default 'anonymous'\n\t\t */\n\t\tthis.crossOrigin = 'anonymous';\n\n\t\t/**\n\t\t * Whether the XMLHttpRequest uses credentials.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.withCredentials = false;\n\n\t\t/**\n\t\t * The base path from which the asset will be loaded.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.path = '';\n\n\t\t/**\n\t\t * The base path from which additional resources like textures will be loaded.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.resourcePath = '';\n\n\t\t/**\n\t\t * The [request header](https://developer.mozilla.org/en-US/docs/Glossary/Request_header)\n\t\t * used in HTTP request.\n\t\t *\n\t\t * @type {Object<string, any>}\n\t\t */\n\t\tthis.requestHeader = {};\n\n\t}\n\n\t/**\n\t * This method needs to be implemented by all concrete loaders. It holds the\n\t * logic for loading assets from the backend.\n\t *\n\t * @abstract\n\t * @param {string} url - The path/URL of the file to be loaded.\n\t * @param {Function} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} [onProgress] - Executed while the loading is in progress.\n\t * @param {onErrorCallback} [onError] - Executed when errors occur.\n\t */\n\tload( /* url, onLoad, onProgress, onError */ ) {}\n\n\t/**\n\t * A async version of {@link Loader#load}.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded.\n\t * @param {onProgressCallback} [onProgress] - Executed while the loading is in progress.\n\t * @return {Promise} A Promise that resolves when the asset has been loaded.\n\t */\n\tloadAsync( url, onProgress ) {\n\n\t\tconst scope = this;\n\n\t\treturn new Promise( function ( resolve, reject ) {\n\n\t\t\tscope.load( url, resolve, onProgress, reject );\n\n\t\t} );\n\n\t}\n\n\t/**\n\t * This method needs to be implemented by all concrete loaders. It holds the\n\t * logic for parsing the asset into three.js entities.\n\t *\n\t * @abstract\n\t * @param {any} data - The data to parse.\n\t */\n\tparse( /* data */ ) {}\n\n\t/**\n\t * Sets the `crossOrigin` String to implement CORS for loading the URL\n\t * from a different domain that allows CORS.\n\t *\n\t * @param {string} crossOrigin - The `crossOrigin` value.\n\t * @return {Loader} A reference to this instance.\n\t */\n\tsetCrossOrigin( crossOrigin ) {\n\n\t\tthis.crossOrigin = crossOrigin;\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Whether the XMLHttpRequest uses credentials such as cookies, authorization\n\t * headers or TLS client certificates, see [XMLHttpRequest.withCredentials](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials).\n\t *\n\t * Note: This setting has no effect if you are loading files locally or from the same domain.\n\t *\n\t * @param {boolean} value - The `withCredentials` value.\n\t * @return {Loader} A reference to this instance.\n\t */\n\tsetWithCredentials( value ) {\n\n\t\tthis.withCredentials = value;\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the base path for the asset.\n\t *\n\t * @param {string} path - The base path.\n\t * @return {Loader} A reference to this instance.\n\t */\n\tsetPath( path ) {\n\n\t\tthis.path = path;\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the base path for dependent resources like textures.\n\t *\n\t * @param {string} resourcePath - The resource path.\n\t * @return {Loader} A reference to this instance.\n\t */\n\tsetResourcePath( resourcePath ) {\n\n\t\tthis.resourcePath = resourcePath;\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given request header.\n\t *\n\t * @param {Object} requestHeader - A [request header](https://developer.mozilla.org/en-US/docs/Glossary/Request_header)\n\t * for configuring the HTTP request.\n\t * @return {Loader} A reference to this instance.\n\t */\n\tsetRequestHeader( requestHeader ) {\n\n\t\tthis.requestHeader = requestHeader;\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * This method can be implemented in loaders for aborting ongoing requests.\n\t *\n\t * @abstract\n\t * @return {Loader} A reference to this instance.\n\t */\n\tabort() {\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * Callback for onProgress in loaders.\n *\n * @callback onProgressCallback\n * @param {ProgressEvent} event - An instance of `ProgressEvent` that represents the current loading status.\n */\n\n/**\n * Callback for onError in loaders.\n *\n * @callback onErrorCallback\n * @param {Error} error - The error which occurred during the loading process.\n */\n\n/**\n * The default material name that is used by loaders\n * when creating materials for loaded 3D objects.\n *\n * Note: Not all loaders might honor this setting.\n *\n * @static\n * @type {string}\n * @default '__DEFAULT'\n */\nLoader.DEFAULT_MATERIAL_NAME = '__DEFAULT';\n\nconst loading = {};\n\nclass HttpError extends Error {\n\n\tconstructor( message, response ) {\n\n\t\tsuper( message );\n\t\tthis.response = response;\n\n\t}\n\n}\n\n/**\n * A low level class for loading resources with the Fetch API, used internally by\n * most loaders. It can also be used directly to load any file type that does\n * not have a loader.\n *\n * This loader supports caching. If you want to use it, add `THREE.Cache.enabled = true;`\n * once to your application.\n *\n * ```js\n * const loader = new THREE.FileLoader();\n * const data = await loader.loadAsync( 'example.txt' );\n * ```\n *\n * @augments Loader\n */\nclass FileLoader extends Loader {\n\n\t/**\n\t * Constructs a new file loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t\t/**\n\t\t * The expected mime type. Valid values can be found\n\t\t * [here](hhttps://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#mimetype)\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.mimeType = '';\n\n\t\t/**\n\t\t * The expected response type.\n\t\t *\n\t\t * @type {('arraybuffer'|'blob'|'document'|'json'|'')}\n\t\t * @default ''\n\t\t */\n\t\tthis.responseType = '';\n\n\t\t/**\n\t\t * Used for aborting requests.\n\t\t *\n\t\t * @private\n\t\t * @type {AbortController}\n\t\t */\n\t\tthis._abortController = new AbortController();\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and pass the loaded response to the `onLoad()` callback.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(any)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} [onProgress] - Executed while the loading is in progress.\n\t * @param {onErrorCallback} [onError] - Executed when errors occur.\n\t * @return {any|undefined} The cached resource if available.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tif ( url === undefined ) url = '';\n\n\t\tif ( this.path !== undefined ) url = this.path + url;\n\n\t\turl = this.manager.resolveURL( url );\n\n\t\tconst cached = Cache.get( `file:${url}` );\n\n\t\tif ( cached !== undefined ) {\n\n\t\t\tthis.manager.itemStart( url );\n\n\t\t\tsetTimeout( () => {\n\n\t\t\t\tif ( onLoad ) onLoad( cached );\n\n\t\t\t\tthis.manager.itemEnd( url );\n\n\t\t\t}, 0 );\n\n\t\t\treturn cached;\n\n\t\t}\n\n\t\t// Check if request is duplicate\n\n\t\tif ( loading[ url ] !== undefined ) {\n\n\t\t\tloading[ url ].push( {\n\n\t\t\t\tonLoad: onLoad,\n\t\t\t\tonProgress: onProgress,\n\t\t\t\tonError: onError\n\n\t\t\t} );\n\n\t\t\treturn;\n\n\t\t}\n\n\t\t// Initialise array for duplicate requests\n\t\tloading[ url ] = [];\n\n\t\tloading[ url ].push( {\n\t\t\tonLoad: onLoad,\n\t\t\tonProgress: onProgress,\n\t\t\tonError: onError,\n\t\t} );\n\n\t\t// create request\n\t\tconst req = new Request( url, {\n\t\t\theaders: new Headers( this.requestHeader ),\n\t\t\tcredentials: this.withCredentials ? 'include' : 'same-origin',\n\t\t\tsignal: ( typeof AbortSignal.any === 'function' ) ? AbortSignal.any( [ this._abortController.signal, this.manager.abortController.signal ] ) : this._abortController.signal\n\t\t} );\n\n\t\t// record states ( avoid data race )\n\t\tconst mimeType = this.mimeType;\n\t\tconst responseType = this.responseType;\n\n\t\t// start the fetch\n\t\tfetch( req )\n\t\t\t.then( response => {\n\n\t\t\t\tif ( response.status === 200 || response.status === 0 ) {\n\n\t\t\t\t\t// Some browsers return HTTP Status 0 when using non-http protocol\n\t\t\t\t\t// e.g. 'file://' or 'data://'. Handle as success.\n\n\t\t\t\t\tif ( response.status === 0 ) {\n\n\t\t\t\t\t\twarn( 'FileLoader: HTTP Status 0 received.' );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// Workaround: Checking if response.body === undefined for Alipay browser #23548\n\n\t\t\t\t\tif ( typeof ReadableStream === 'undefined' || response.body === undefined || response.body.getReader === undefined ) {\n\n\t\t\t\t\t\treturn response;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tconst callbacks = loading[ url ];\n\t\t\t\t\tconst reader = response.body.getReader();\n\n\t\t\t\t\t// Nginx needs X-File-Size check\n\t\t\t\t\t// https://serverfault.com/questions/482875/why-does-nginx-remove-content-length-header-for-chunked-content\n\t\t\t\t\tconst contentLength = response.headers.get( 'X-File-Size' ) || response.headers.get( 'Content-Length' );\n\t\t\t\t\tconst total = contentLength ? parseInt( contentLength ) : 0;\n\t\t\t\t\tconst lengthComputable = total !== 0;\n\t\t\t\t\tlet loaded = 0;\n\n\t\t\t\t\t// periodically read data into the new stream tracking while download progress\n\t\t\t\t\tconst stream = new ReadableStream( {\n\t\t\t\t\t\tstart( controller ) {\n\n\t\t\t\t\t\t\treadData();\n\n\t\t\t\t\t\t\tfunction readData() {\n\n\t\t\t\t\t\t\t\treader.read().then( ( { done, value } ) => {\n\n\t\t\t\t\t\t\t\t\tif ( done ) {\n\n\t\t\t\t\t\t\t\t\t\tcontroller.close();\n\n\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\tloaded += value.byteLength;\n\n\t\t\t\t\t\t\t\t\t\tconst event = new ProgressEvent( 'progress', { lengthComputable, loaded, total } );\n\t\t\t\t\t\t\t\t\t\tfor ( let i = 0, il = callbacks.length; i < il; i ++ ) {\n\n\t\t\t\t\t\t\t\t\t\t\tconst callback = callbacks[ i ];\n\t\t\t\t\t\t\t\t\t\t\tif ( callback.onProgress ) callback.onProgress( event );\n\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tcontroller.enqueue( value );\n\t\t\t\t\t\t\t\t\t\treadData();\n\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t}, ( e ) => {\n\n\t\t\t\t\t\t\t\t\tcontroller.error( e );\n\n\t\t\t\t\t\t\t\t} );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} );\n\n\t\t\t\t\treturn new Response( stream );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthrow new HttpError( `fetch for \"${response.url}\" responded with ${response.status}: ${response.statusText}`, response );\n\n\t\t\t\t}\n\n\t\t\t} )\n\t\t\t.then( response => {\n\n\t\t\t\tswitch ( responseType ) {\n\n\t\t\t\t\tcase 'arraybuffer':\n\n\t\t\t\t\t\treturn response.arrayBuffer();\n\n\t\t\t\t\tcase 'blob':\n\n\t\t\t\t\t\treturn response.blob();\n\n\t\t\t\t\tcase 'document':\n\n\t\t\t\t\t\treturn response.text()\n\t\t\t\t\t\t\t.then( text => {\n\n\t\t\t\t\t\t\t\tconst parser = new DOMParser();\n\t\t\t\t\t\t\t\treturn parser.parseFromString( text, mimeType );\n\n\t\t\t\t\t\t\t} );\n\n\t\t\t\t\tcase 'json':\n\n\t\t\t\t\t\treturn response.json();\n\n\t\t\t\t\tdefault:\n\n\t\t\t\t\t\tif ( mimeType === '' ) {\n\n\t\t\t\t\t\t\treturn response.text();\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// sniff encoding\n\t\t\t\t\t\t\tconst re = /charset=\"?([^;\"\\s]*)\"?/i;\n\t\t\t\t\t\t\tconst exec = re.exec( mimeType );\n\t\t\t\t\t\t\tconst label = exec && exec[ 1 ] ? exec[ 1 ].toLowerCase() : undefined;\n\t\t\t\t\t\t\tconst decoder = new TextDecoder( label );\n\t\t\t\t\t\t\treturn response.arrayBuffer().then( ab => decoder.decode( ab ) );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} )\n\t\t\t.then( data => {\n\n\t\t\t\t// Add to cache only on HTTP success, so that we do not cache\n\t\t\t\t// error response bodies as proper responses to requests.\n\t\t\t\tCache.add( `file:${url}`, data );\n\n\t\t\t\tconst callbacks = loading[ url ];\n\t\t\t\tdelete loading[ url ];\n\n\t\t\t\tfor ( let i = 0, il = callbacks.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst callback = callbacks[ i ];\n\t\t\t\t\tif ( callback.onLoad ) callback.onLoad( data );\n\n\t\t\t\t}\n\n\t\t\t} )\n\t\t\t.catch( err => {\n\n\t\t\t\t// Abort errors and other errors are handled the same\n\n\t\t\t\tconst callbacks = loading[ url ];\n\n\t\t\t\tif ( callbacks === undefined ) {\n\n\t\t\t\t\t// When onLoad was called and url was deleted in `loading`\n\t\t\t\t\tthis.manager.itemError( url );\n\t\t\t\t\tthrow err;\n\n\t\t\t\t}\n\n\t\t\t\tdelete loading[ url ];\n\n\t\t\t\tfor ( let i = 0, il = callbacks.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst callback = callbacks[ i ];\n\t\t\t\t\tif ( callback.onError ) callback.onError( err );\n\n\t\t\t\t}\n\n\t\t\t\tthis.manager.itemError( url );\n\n\t\t\t} )\n\t\t\t.finally( () => {\n\n\t\t\t\tthis.manager.itemEnd( url );\n\n\t\t\t} );\n\n\t\tthis.manager.itemStart( url );\n\n\t}\n\n\t/**\n\t * Sets the expected response type.\n\t *\n\t * @param {('arraybuffer'|'blob'|'document'|'json'|'')} value - The response type.\n\t * @return {FileLoader} A reference to this file loader.\n\t */\n\tsetResponseType( value ) {\n\n\t\tthis.responseType = value;\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the expected mime type of the loaded file.\n\t *\n\t * @param {string} value - The mime type.\n\t * @return {FileLoader} A reference to this file loader.\n\t */\n\tsetMimeType( value ) {\n\n\t\tthis.mimeType = value;\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Aborts ongoing fetch requests.\n\t *\n\t * @return {FileLoader} A reference to this instance.\n\t */\n\tabort() {\n\n\t\tthis._abortController.abort();\n\t\tthis._abortController = new AbortController();\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * Class for loading animation clips in the JSON format. The files are internally\n * loaded via {@link FileLoader}.\n *\n * ```js\n * const loader = new THREE.AnimationLoader();\n * const animations = await loader.loadAsync( 'animations/animation.js' );\n * ```\n *\n * @augments Loader\n */\nclass AnimationLoader extends Loader {\n\n\t/**\n\t * Constructs a new animation loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and pass the loaded animations as an array\n\t * holding instances of {@link AnimationClip} to the `onLoad()` callback.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(Array<AnimationClip>)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Executed while the loading is in progress.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tconst scope = this;\n\n\t\tconst loader = new FileLoader( this.manager );\n\t\tloader.setPath( this.path );\n\t\tloader.setRequestHeader( this.requestHeader );\n\t\tloader.setWithCredentials( this.withCredentials );\n\t\tloader.load( url, function ( text ) {\n\n\t\t\ttry {\n\n\t\t\t\tonLoad( scope.parse( JSON.parse( text ) ) );\n\n\t\t\t} catch ( e ) {\n\n\t\t\t\tif ( onError ) {\n\n\t\t\t\t\tonError( e );\n\n\t\t\t\t} else {\n\n\t\t\t\t\terror( e );\n\n\t\t\t\t}\n\n\t\t\t\tscope.manager.itemError( url );\n\n\t\t\t}\n\n\t\t}, onProgress, onError );\n\n\t}\n\n\t/**\n\t * Parses the given JSON object and returns an array of animation clips.\n\t *\n\t * @param {Object} json - The serialized animation clips.\n\t * @return {Array<AnimationClip>} The parsed animation clips.\n\t */\n\tparse( json ) {\n\n\t\tconst animations = [];\n\n\t\tfor ( let i = 0; i < json.length; i ++ ) {\n\n\t\t\tconst clip = AnimationClip.parse( json[ i ] );\n\n\t\t\tanimations.push( clip );\n\n\t\t}\n\n\t\treturn animations;\n\n\t}\n\n}\n\n/**\n * Abstract base class for loading compressed texture formats S3TC, ASTC or ETC.\n * Textures are internally loaded via {@link FileLoader}.\n *\n * Derived classes have to implement the `parse()` method which holds the parsing\n * for the respective format.\n *\n * @abstract\n * @augments Loader\n */\nclass CompressedTextureLoader extends Loader {\n\n\t/**\n\t * Constructs a new compressed texture loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and passes the loaded compressed texture\n\t * to the `onLoad()` callback. The method also returns a new texture object which can\n\t * directly be used for material creation. If you do it this way, the texture\n\t * may pop up in your scene once the respective loading process is finished.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(CompressedTexture)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Executed while the loading is in progress.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t * @return {CompressedTexture} The compressed texture.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tconst scope = this;\n\n\t\tconst images = [];\n\n\t\tconst texture = new CompressedTexture();\n\n\t\tconst loader = new FileLoader( this.manager );\n\t\tloader.setPath( this.path );\n\t\tloader.setResponseType( 'arraybuffer' );\n\t\tloader.setRequestHeader( this.requestHeader );\n\t\tloader.setWithCredentials( scope.withCredentials );\n\n\t\tlet loaded = 0;\n\n\t\tfunction loadTexture( i ) {\n\n\t\t\tloader.load( url[ i ], function ( buffer ) {\n\n\t\t\t\tconst texDatas = scope.parse( buffer, true );\n\n\t\t\t\timages[ i ] = {\n\t\t\t\t\twidth: texDatas.width,\n\t\t\t\t\theight: texDatas.height,\n\t\t\t\t\tformat: texDatas.format,\n\t\t\t\t\tmipmaps: texDatas.mipmaps\n\t\t\t\t};\n\n\t\t\t\tloaded += 1;\n\n\t\t\t\tif ( loaded === 6 ) {\n\n\t\t\t\t\tif ( texDatas.mipmapCount === 1 ) texture.minFilter = LinearFilter;\n\n\t\t\t\t\ttexture.image = images;\n\t\t\t\t\ttexture.format = texDatas.format;\n\t\t\t\t\ttexture.needsUpdate = true;\n\n\t\t\t\t\tif ( onLoad ) onLoad( texture );\n\n\t\t\t\t}\n\n\t\t\t}, onProgress, onError );\n\n\t\t}\n\n\t\tif ( Array.isArray( url ) ) {\n\n\t\t\tfor ( let i = 0, il = url.length; i < il; ++ i ) {\n\n\t\t\t\tloadTexture( i );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// compressed cubemap texture stored in a single DDS file\n\n\t\t\tloader.load( url, function ( buffer ) {\n\n\t\t\t\tconst texDatas = scope.parse( buffer, true );\n\n\t\t\t\tif ( texDatas.isCubemap ) {\n\n\t\t\t\t\tconst faces = texDatas.mipmaps.length / texDatas.mipmapCount;\n\n\t\t\t\t\tfor ( let f = 0; f < faces; f ++ ) {\n\n\t\t\t\t\t\timages[ f ] = { mipmaps: [] };\n\n\t\t\t\t\t\tfor ( let i = 0; i < texDatas.mipmapCount; i ++ ) {\n\n\t\t\t\t\t\t\timages[ f ].mipmaps.push( texDatas.mipmaps[ f * texDatas.mipmapCount + i ] );\n\t\t\t\t\t\t\timages[ f ].format = texDatas.format;\n\t\t\t\t\t\t\timages[ f ].width = texDatas.width;\n\t\t\t\t\t\t\timages[ f ].height = texDatas.height;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\ttexture.image = images;\n\n\t\t\t\t} else {\n\n\t\t\t\t\ttexture.image.width = texDatas.width;\n\t\t\t\t\ttexture.image.height = texDatas.height;\n\t\t\t\t\ttexture.mipmaps = texDatas.mipmaps;\n\n\t\t\t\t}\n\n\t\t\t\tif ( texDatas.mipmapCount === 1 ) {\n\n\t\t\t\t\ttexture.minFilter = LinearFilter;\n\n\t\t\t\t}\n\n\t\t\t\ttexture.format = texDatas.format;\n\t\t\t\ttexture.needsUpdate = true;\n\n\t\t\t\tif ( onLoad ) onLoad( texture );\n\n\t\t\t}, onProgress, onError );\n\n\t\t}\n\n\t\treturn texture;\n\n\t}\n\n}\n\nconst _loading = new WeakMap();\n\n/**\n * A loader for loading images. The class loads images with the HTML `Image` API.\n *\n * ```js\n * const loader = new THREE.ImageLoader();\n * const image = await loader.loadAsync( 'image.png' );\n * ```\n * Please note that `ImageLoader` has dropped support for progress\n * events in `r84`. For an `ImageLoader` that supports progress events, see\n * [this thread](https://github.com/mrdoob/three.js/issues/10439#issuecomment-275785639).\n *\n * @augments Loader\n */\nclass ImageLoader extends Loader {\n\n\t/**\n\t * Constructs a new image loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and passes the loaded image\n\t * to the `onLoad()` callback. The method also returns a new `Image` object which can\n\t * directly be used for texture creation. If you do it this way, the texture\n\t * may pop up in your scene once the respective loading process is finished.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(Image)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Unsupported in this loader.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t * @return {Image} The image.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tif ( this.path !== undefined ) url = this.path + url;\n\n\t\turl = this.manager.resolveURL( url );\n\n\t\tconst scope = this;\n\n\t\tconst cached = Cache.get( `image:${url}` );\n\n\t\tif ( cached !== undefined ) {\n\n\t\t\tif ( cached.complete === true ) {\n\n\t\t\t\tscope.manager.itemStart( url );\n\n\t\t\t\tsetTimeout( function () {\n\n\t\t\t\t\tif ( onLoad ) onLoad( cached );\n\n\t\t\t\t\tscope.manager.itemEnd( url );\n\n\t\t\t\t}, 0 );\n\n\t\t\t} else {\n\n\t\t\t\tlet arr = _loading.get( cached );\n\n\t\t\t\tif ( arr === undefined ) {\n\n\t\t\t\t\tarr = [];\n\t\t\t\t\t_loading.set( cached, arr );\n\n\t\t\t\t}\n\n\t\t\t\tarr.push( { onLoad, onError } );\n\n\t\t\t}\n\n\t\t\treturn cached;\n\n\t\t}\n\n\t\tconst image = createElementNS( 'img' );\n\n\t\tfunction onImageLoad() {\n\n\t\t\tremoveEventListeners();\n\n\t\t\tif ( onLoad ) onLoad( this );\n\n\t\t\t//\n\n\t\t\tconst callbacks = _loading.get( this ) || [];\n\n\t\t\tfor ( let i = 0; i < callbacks.length; i ++ ) {\n\n\t\t\t\tconst callback = callbacks[ i ];\n\t\t\t\tif ( callback.onLoad ) callback.onLoad( this );\n\n\t\t\t}\n\n\t\t\t_loading.delete( this );\n\n\t\t\tscope.manager.itemEnd( url );\n\n\t\t}\n\n\t\tfunction onImageError( event ) {\n\n\t\t\tremoveEventListeners();\n\n\t\t\tif ( onError ) onError( event );\n\n\t\t\tCache.remove( `image:${url}` );\n\n\t\t\t//\n\n\t\t\tconst callbacks = _loading.get( this ) || [];\n\n\t\t\tfor ( let i = 0; i < callbacks.length; i ++ ) {\n\n\t\t\t\tconst callback = callbacks[ i ];\n\t\t\t\tif ( callback.onError ) callback.onError( event );\n\n\t\t\t}\n\n\t\t\t_loading.delete( this );\n\n\n\t\t\tscope.manager.itemError( url );\n\t\t\tscope.manager.itemEnd( url );\n\n\t\t}\n\n\t\tfunction removeEventListeners() {\n\n\t\t\timage.removeEventListener( 'load', onImageLoad, false );\n\t\t\timage.removeEventListener( 'error', onImageError, false );\n\n\t\t}\n\n\t\timage.addEventListener( 'load', onImageLoad, false );\n\t\timage.addEventListener( 'error', onImageError, false );\n\n\t\tif ( url.slice( 0, 5 ) !== 'data:' ) {\n\n\t\t\tif ( this.crossOrigin !== undefined ) image.crossOrigin = this.crossOrigin;\n\n\t\t}\n\n\t\tCache.add( `image:${url}`, image );\n\t\tscope.manager.itemStart( url );\n\n\t\timage.src = url;\n\n\t\treturn image;\n\n\t}\n\n}\n\n/**\n * Class for loading cube textures. Images are internally loaded via {@link ImageLoader}.\n *\n * The loader returns an instance of {@link CubeTexture} and expects the cube map to\n * be defined as six separate images representing the sides of a cube. Other cube map definitions\n * like vertical and horizontal cross, column and row layouts are not supported.\n *\n * Note that, by convention, cube maps are specified in a coordinate system\n * in which positive-x is to the right when looking up the positive-z axis --\n * in other words, using a left-handed coordinate system. Since three.js uses\n * a right-handed coordinate system, environment maps used in three.js will\n * have pos-x and neg-x swapped.\n *\n * The loaded cube texture is in sRGB color space. Meaning {@link Texture#colorSpace}\n * is set to `SRGBColorSpace` by default.\n *\n * ```js\n * const loader = new THREE.CubeTextureLoader().setPath( 'textures/cubeMaps/' );\n * const cubeTexture = await loader.loadAsync( [\n * \t'px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png'\n * ] );\n * scene.background = cubeTexture;\n * ```\n *\n * @augments Loader\n */\nclass CubeTextureLoader extends Loader {\n\n\t/**\n\t * Constructs a new cube texture loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and pass the fully loaded cube texture\n\t * to the `onLoad()` callback. The method also returns a new cube texture object which can\n\t * directly be used for material creation. If you do it this way, the cube texture\n\t * may pop up in your scene once the respective loading process is finished.\n\t *\n\t * @param {Array<string>} urls - Array of 6 URLs to images, one for each side of the\n\t * cube texture. The urls should be specified in the following order: pos-x,\n\t * neg-x, pos-y, neg-y, pos-z, neg-z. An array of data URIs are allowed as well.\n\t * @param {function(CubeTexture)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Unsupported in this loader.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t * @return {CubeTexture} The cube texture.\n\t */\n\tload( urls, onLoad, onProgress, onError ) {\n\n\t\tconst texture = new CubeTexture();\n\t\ttexture.colorSpace = SRGBColorSpace;\n\n\t\tconst loader = new ImageLoader( this.manager );\n\t\tloader.setCrossOrigin( this.crossOrigin );\n\t\tloader.setPath( this.path );\n\n\t\tlet loaded = 0;\n\n\t\tfunction loadTexture( i ) {\n\n\t\t\tloader.load( urls[ i ], function ( image ) {\n\n\t\t\t\ttexture.images[ i ] = image;\n\n\t\t\t\tloaded ++;\n\n\t\t\t\tif ( loaded === 6 ) {\n\n\t\t\t\t\ttexture.needsUpdate = true;\n\n\t\t\t\t\tif ( onLoad ) onLoad( texture );\n\n\t\t\t\t}\n\n\t\t\t}, undefined, onError );\n\n\t\t}\n\n\t\tfor ( let i = 0; i < urls.length; ++ i ) {\n\n\t\t\tloadTexture( i );\n\n\t\t}\n\n\t\treturn texture;\n\n\t}\n\n}\n\n/**\n * Abstract base class for loading binary texture formats RGBE, EXR or TGA.\n * Textures are internally loaded via {@link FileLoader}.\n *\n * Derived classes have to implement the `parse()` method which holds the parsing\n * for the respective format.\n *\n * @abstract\n * @augments Loader\n */\nclass DataTextureLoader extends Loader {\n\n\t/**\n\t * Constructs a new data texture loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and passes the loaded data texture\n\t * to the `onLoad()` callback. The method also returns a new texture object which can\n\t * directly be used for material creation. If you do it this way, the texture\n\t * may pop up in your scene once the respective loading process is finished.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(DataTexture)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Executed while the loading is in progress.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t * @return {DataTexture} The data texture.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tconst scope = this;\n\n\t\tconst texture = new DataTexture();\n\n\t\tconst loader = new FileLoader( this.manager );\n\t\tloader.setResponseType( 'arraybuffer' );\n\t\tloader.setRequestHeader( this.requestHeader );\n\t\tloader.setPath( this.path );\n\t\tloader.setWithCredentials( scope.withCredentials );\n\t\tloader.load( url, function ( buffer ) {\n\n\t\t\tlet texData;\n\n\t\t\ttry {\n\n\t\t\t\ttexData = scope.parse( buffer );\n\n\t\t\t} catch ( error ) {\n\n\t\t\t\tif ( onError !== undefined ) {\n\n\t\t\t\t\tonError( error );\n\n\t\t\t\t} else {\n\n\t\t\t\t\terror( error );\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( texData.image !== undefined ) {\n\n\t\t\t\ttexture.image = texData.image;\n\n\t\t\t} else if ( texData.data !== undefined ) {\n\n\t\t\t\ttexture.image.width = texData.width;\n\t\t\t\ttexture.image.height = texData.height;\n\t\t\t\ttexture.image.data = texData.data;\n\n\t\t\t}\n\n\t\t\ttexture.wrapS = texData.wrapS !== undefined ? texData.wrapS : ClampToEdgeWrapping;\n\t\t\ttexture.wrapT = texData.wrapT !== undefined ? texData.wrapT : ClampToEdgeWrapping;\n\n\t\t\ttexture.magFilter = texData.magFilter !== undefined ? texData.magFilter : LinearFilter;\n\t\t\ttexture.minFilter = texData.minFilter !== undefined ? texData.minFilter : LinearFilter;\n\n\t\t\ttexture.anisotropy = texData.anisotropy !== undefined ? texData.anisotropy : 1;\n\n\t\t\tif ( texData.colorSpace !== undefined ) {\n\n\t\t\t\ttexture.colorSpace = texData.colorSpace;\n\n\t\t\t}\n\n\t\t\tif ( texData.flipY !== undefined ) {\n\n\t\t\t\ttexture.flipY = texData.flipY;\n\n\t\t\t}\n\n\t\t\tif ( texData.format !== undefined ) {\n\n\t\t\t\ttexture.format = texData.format;\n\n\t\t\t}\n\n\t\t\tif ( texData.type !== undefined ) {\n\n\t\t\t\ttexture.type = texData.type;\n\n\t\t\t}\n\n\t\t\tif ( texData.mipmaps !== undefined ) {\n\n\t\t\t\ttexture.mipmaps = texData.mipmaps;\n\t\t\t\ttexture.minFilter = LinearMipmapLinearFilter; // presumably...\n\n\t\t\t}\n\n\t\t\tif ( texData.mipmapCount === 1 ) {\n\n\t\t\t\ttexture.minFilter = LinearFilter;\n\n\t\t\t}\n\n\t\t\tif ( texData.generateMipmaps !== undefined ) {\n\n\t\t\t\ttexture.generateMipmaps = texData.generateMipmaps;\n\n\t\t\t}\n\n\t\t\ttexture.needsUpdate = true;\n\n\t\t\tif ( onLoad ) onLoad( texture, texData );\n\n\t\t}, onProgress, onError );\n\n\n\t\treturn texture;\n\n\t}\n\n}\n\n/**\n * Class for loading textures. Images are internally\n * loaded via {@link ImageLoader}.\n *\n * ```js\n * const loader = new THREE.TextureLoader();\n * const texture = await loader.loadAsync( 'textures/land_ocean_ice_cloud_2048.jpg' );\n *\n * const material = new THREE.MeshBasicMaterial( { map:texture } );\n * ```\n * Please note that `TextureLoader` has dropped support for progress\n * events in `r84`. For a `TextureLoader` that supports progress events, see\n * [this thread](https://github.com/mrdoob/three.js/issues/10439#issuecomment-293260145).\n *\n * @augments Loader\n */\nclass TextureLoader extends Loader {\n\n\t/**\n\t * Constructs a new texture loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and pass the fully loaded texture\n\t * to the `onLoad()` callback. The method also returns a new texture object which can\n\t * directly be used for material creation. If you do it this way, the texture\n\t * may pop up in your scene once the respective loading process is finished.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(Texture)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Unsupported in this loader.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t * @return {Texture} The texture.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tconst texture = new Texture();\n\n\t\tconst loader = new ImageLoader( this.manager );\n\t\tloader.setCrossOrigin( this.crossOrigin );\n\t\tloader.setPath( this.path );\n\n\t\tloader.load( url, function ( image ) {\n\n\t\t\ttexture.image = image;\n\t\t\ttexture.needsUpdate = true;\n\n\t\t\tif ( onLoad !== undefined ) {\n\n\t\t\t\tonLoad( texture );\n\n\t\t\t}\n\n\t\t}, onProgress, onError );\n\n\t\treturn texture;\n\n\t}\n\n}\n\n/**\n * Abstract base class for lights - all other light types inherit the\n * properties and methods described here.\n *\n * @abstract\n * @augments Object3D\n */\nclass Light extends Object3D {\n\n\t/**\n\t * Constructs a new light.\n\t *\n\t * @param {(number|Color|string)} [color=0xffffff] - The light's color.\n\t * @param {number} [intensity=1] - The light's strength/intensity.\n\t */\n\tconstructor( color, intensity = 1 ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLight = true;\n\n\t\tthis.type = 'Light';\n\n\t\t/**\n\t\t * The light's color.\n\t\t *\n\t\t * @type {Color}\n\t\t */\n\t\tthis.color = new Color( color );\n\n\t\t/**\n\t\t * The light's intensity.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.intensity = intensity;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.dispatchEvent( { type: 'dispose' } );\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.color.copy( source.color );\n\t\tthis.intensity = source.intensity;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.object.color = this.color.getHex();\n\t\tdata.object.intensity = this.intensity;\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * A light source positioned directly above the scene, with color fading from\n * the sky color to the ground color.\n *\n * This light cannot be used to cast shadows.\n *\n * ```js\n * const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );\n * scene.add( light );\n * ```\n *\n * @augments Light\n */\nclass HemisphereLight extends Light {\n\n\t/**\n\t * Constructs a new hemisphere light.\n\t *\n\t * @param {(number|Color|string)} [skyColor=0xffffff] - The light's sky color.\n\t * @param {(number|Color|string)} [groundColor=0xffffff] - The light's ground color.\n\t * @param {number} [intensity=1] - The light's strength/intensity.\n\t */\n\tconstructor( skyColor, groundColor, intensity ) {\n\n\t\tsuper( skyColor, intensity );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isHemisphereLight = true;\n\n\t\tthis.type = 'HemisphereLight';\n\n\t\tthis.position.copy( Object3D.DEFAULT_UP );\n\t\tthis.updateMatrix();\n\n\t\t/**\n\t\t * The light's ground color.\n\t\t *\n\t\t * @type {Color}\n\t\t */\n\t\tthis.groundColor = new Color( groundColor );\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.groundColor.copy( source.groundColor );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.object.groundColor = this.groundColor.getHex();\n\n\t\treturn data;\n\n\t}\n\n}\n\nconst _projScreenMatrix = /*@__PURE__*/ new Matrix4();\nconst _lightPositionWorld = /*@__PURE__*/ new Vector3();\nconst _lookTarget = /*@__PURE__*/ new Vector3();\n\n/**\n * Abstract base class for light shadow classes. These classes\n * represent the shadow configuration for different light types.\n *\n * @abstract\n */\nclass LightShadow {\n\n\t/**\n\t * Constructs a new light shadow.\n\t *\n\t * @param {Camera} camera - The light's view of the world.\n\t */\n\tconstructor( camera ) {\n\n\t\t/**\n\t\t * The light's view of the world.\n\t\t *\n\t\t * @type {Camera}\n\t\t */\n\t\tthis.camera = camera;\n\n\t\t/**\n\t\t * The intensity of the shadow. The default is `1`.\n\t\t * Valid values are in the range `[0, 1]`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.intensity = 1;\n\n\t\t/**\n\t\t * Shadow map bias, how much to add or subtract from the normalized depth\n\t\t * when deciding whether a surface is in shadow.\n\t\t *\n\t\t * The default is `0`. Very tiny adjustments here (in the order of `0.0001`)\n\t\t * may help reduce artifacts in shadows.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.bias = 0;\n\n\t\t/**\n\t\t * Defines how much the position used to query the shadow map is offset along\n\t\t * the object normal. The default is `0`. Increasing this value can be used to\n\t\t * reduce shadow acne especially in large scenes where light shines onto\n\t\t * geometry at a shallow angle. The cost is that shadows may appear distorted.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.normalBias = 0;\n\n\t\t/**\n\t\t * Setting this to values greater than 1 will blur the edges of the shadow.\n\t\t * High values will cause unwanted banding effects in the shadows - a greater\n\t\t * map size will allow for a higher value to be used here before these effects\n\t\t * become visible.\n\t\t *\n\t\t * The property has no effect when the shadow map type is `BasicShadowMap`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.radius = 1;\n\n\t\t/**\n\t\t * The amount of samples to use when blurring a VSM shadow map.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 8\n\t\t */\n\t\tthis.blurSamples = 8;\n\n\t\t/**\n\t\t * Defines the width and height of the shadow map. Higher values give better quality\n\t\t * shadows at the cost of computation time. Values must be powers of two.\n\t\t *\n\t\t * @type {Vector2}\n\t\t * @default (512,512)\n\t\t */\n\t\tthis.mapSize = new Vector2( 512, 512 );\n\n\t\t/**\n\t\t * The type of shadow texture. The default is `UnsignedByteType`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default UnsignedByteType\n\t\t */\n\t\tthis.mapType = UnsignedByteType;\n\n\t\t/**\n\t\t * The depth map generated using the internal camera; a location beyond a\n\t\t * pixel's depth is in shadow. Computed internally during rendering.\n\t\t *\n\t\t * @type {?RenderTarget}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * The distribution map generated using the internal camera; an occlusion is\n\t\t * calculated based on the distribution of depths. Computed internally during\n\t\t * rendering.\n\t\t *\n\t\t * @type {?RenderTarget}\n\t\t * @default null\n\t\t */\n\t\tthis.mapPass = null;\n\n\t\t/**\n\t\t * Model to shadow camera space, to compute location and depth in shadow map.\n\t\t * This is computed internally during rendering.\n\t\t *\n\t\t * @type {Matrix4}\n\t\t */\n\t\tthis.matrix = new Matrix4();\n\n\t\t/**\n\t\t * Enables automatic updates of the light's shadow. If you do not require dynamic\n\t\t * lighting / shadows, you may set this to `false`.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.autoUpdate = true;\n\n\t\t/**\n\t\t * When set to `true`, shadow maps will be updated in the next `render` call.\n\t\t * If you have set {@link LightShadow#autoUpdate} to `false`, you will need to\n\t\t * set this property to `true` and then make a render call to update the light's shadow.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.needsUpdate = false;\n\n\t\tthis._frustum = new Frustum();\n\t\tthis._frameExtents = new Vector2( 1, 1 );\n\n\t\tthis._viewportCount = 1;\n\n\t\tthis._viewports = [\n\n\t\t\tnew Vector4( 0, 0, 1, 1 )\n\n\t\t];\n\n\t}\n\n\t/**\n\t * Used internally by the renderer to get the number of viewports that need\n\t * to be rendered for this shadow.\n\t *\n\t * @return {number} The viewport count.\n\t */\n\tgetViewportCount() {\n\n\t\treturn this._viewportCount;\n\n\t}\n\n\t/**\n\t * Gets the shadow cameras frustum. Used internally by the renderer to cull objects.\n\t *\n\t * @return {Frustum} The shadow camera frustum.\n\t */\n\tgetFrustum() {\n\n\t\treturn this._frustum;\n\n\t}\n\n\t/**\n\t * Update the matrices for the camera and shadow, used internally by the renderer.\n\t *\n\t * @param {Light} light - The light for which the shadow is being rendered.\n\t */\n\tupdateMatrices( light ) {\n\n\t\tconst shadowCamera = this.camera;\n\t\tconst shadowMatrix = this.matrix;\n\n\t\t_lightPositionWorld.setFromMatrixPosition( light.matrixWorld );\n\t\tshadowCamera.position.copy( _lightPositionWorld );\n\n\t\t_lookTarget.setFromMatrixPosition( light.target.matrixWorld );\n\t\tshadowCamera.lookAt( _lookTarget );\n\t\tshadowCamera.updateMatrixWorld();\n\n\t\t_projScreenMatrix.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );\n\t\tthis._frustum.setFromProjectionMatrix( _projScreenMatrix, shadowCamera.coordinateSystem, shadowCamera.reversedDepth );\n\n\t\tif ( shadowCamera.reversedDepth ) {\n\n\t\t\tshadowMatrix.set(\n\t\t\t\t0.5, 0.0, 0.0, 0.5,\n\t\t\t\t0.0, 0.5, 0.0, 0.5,\n\t\t\t\t0.0, 0.0, 1.0, 0.0,\n\t\t\t\t0.0, 0.0, 0.0, 1.0\n\t\t\t);\n\n\t\t} else {\n\n\t\t\tshadowMatrix.set(\n\t\t\t\t0.5, 0.0, 0.0, 0.5,\n\t\t\t\t0.0, 0.5, 0.0, 0.5,\n\t\t\t\t0.0, 0.0, 0.5, 0.5,\n\t\t\t\t0.0, 0.0, 0.0, 1.0\n\t\t\t);\n\n\t\t}\n\n\t\tshadowMatrix.multiply( _projScreenMatrix );\n\n\t}\n\n\t/**\n\t * Returns a viewport definition for the given viewport index.\n\t *\n\t * @param {number} viewportIndex - The viewport index.\n\t * @return {Vector4} The viewport.\n\t */\n\tgetViewport( viewportIndex ) {\n\n\t\treturn this._viewports[ viewportIndex ];\n\n\t}\n\n\t/**\n\t * Returns the frame extends.\n\t *\n\t * @return {Vector2} The frame extends.\n\t */\n\tgetFrameExtents() {\n\n\t\treturn this._frameExtents;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tif ( this.map ) {\n\n\t\t\tthis.map.dispose();\n\n\t\t}\n\n\t\tif ( this.mapPass ) {\n\n\t\t\tthis.mapPass.dispose();\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Copies the values of the given light shadow instance to this instance.\n\t *\n\t * @param {LightShadow} source - The light shadow to copy.\n\t * @return {LightShadow} A reference to this light shadow instance.\n\t */\n\tcopy( source ) {\n\n\t\tthis.camera = source.camera.clone();\n\n\t\tthis.intensity = source.intensity;\n\n\t\tthis.bias = source.bias;\n\t\tthis.radius = source.radius;\n\n\t\tthis.autoUpdate = source.autoUpdate;\n\t\tthis.needsUpdate = source.needsUpdate;\n\t\tthis.normalBias = source.normalBias;\n\t\tthis.blurSamples = source.blurSamples;\n\n\t\tthis.mapSize.copy( source.mapSize );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new light shadow instance with copied values from this instance.\n\t *\n\t * @return {LightShadow} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Serializes the light shadow into JSON.\n\t *\n\t * @return {Object} A JSON object representing the serialized light shadow.\n\t * @see {@link ObjectLoader#parse}\n\t */\n\ttoJSON() {\n\n\t\tconst object = {};\n\n\t\tif ( this.intensity !== 1 ) object.intensity = this.intensity;\n\t\tif ( this.bias !== 0 ) object.bias = this.bias;\n\t\tif ( this.normalBias !== 0 ) object.normalBias = this.normalBias;\n\t\tif ( this.radius !== 1 ) object.radius = this.radius;\n\t\tif ( this.mapSize.x !== 512 || this.mapSize.y !== 512 ) object.mapSize = this.mapSize.toArray();\n\n\t\tobject.camera = this.camera.toJSON( false ).object;\n\t\tdelete object.camera.matrix;\n\n\t\treturn object;\n\n\t}\n\n}\n\n/**\n * Represents the shadow configuration of directional lights.\n *\n * @augments LightShadow\n */\nclass SpotLightShadow extends LightShadow {\n\n\t/**\n\t * Constructs a new spot light shadow.\n\t */\n\tconstructor() {\n\n\t\tsuper( new PerspectiveCamera( 50, 1, 0.5, 500 ) );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSpotLightShadow = true;\n\n\t\t/**\n\t\t * Used to focus the shadow camera. The camera's field of view is set as a\n\t\t * percentage of the spotlight's field-of-view. Range is `[0, 1]`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.focus = 1;\n\n\t\t/**\n\t\t * Texture aspect ratio.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.aspect = 1;\n\n\t}\n\n\tupdateMatrices( light ) {\n\n\t\tconst camera = this.camera;\n\n\t\tconst fov = RAD2DEG * 2 * light.angle * this.focus;\n\t\tconst aspect = ( this.mapSize.width / this.mapSize.height ) * this.aspect;\n\t\tconst far = light.distance || camera.far;\n\n\t\tif ( fov !== camera.fov || aspect !== camera.aspect || far !== camera.far ) {\n\n\t\t\tcamera.fov = fov;\n\t\t\tcamera.aspect = aspect;\n\t\t\tcamera.far = far;\n\t\t\tcamera.updateProjectionMatrix();\n\n\t\t}\n\n\t\tsuper.updateMatrices( light );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.focus = source.focus;\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * This light gets emitted from a single point in one direction, along a cone\n * that increases in size the further from the light it gets.\n *\n * This light can cast shadows - see the {@link SpotLightShadow} for details.\n *\n * ```js\n * // white spotlight shining from the side, modulated by a texture\n * const spotLight = new THREE.SpotLight( 0xffffff );\n * spotLight.position.set( 100, 1000, 100 );\n * spotLight.map = new THREE.TextureLoader().load( url );\n *\n * spotLight.castShadow = true;\n * spotLight.shadow.mapSize.width = 1024;\n * spotLight.shadow.mapSize.height = 1024;\n * spotLight.shadow.camera.near = 500;\n * spotLight.shadow.camera.far = 4000;\n * spotLight.shadow.camera.fov = 30;s\n * ```\n *\n * @augments Light\n */\nclass SpotLight extends Light {\n\n\t/**\n\t * Constructs a new spot light.\n\t *\n\t * @param {(number|Color|string)} [color=0xffffff] - The light's color.\n\t * @param {number} [intensity=1] - The light's strength/intensity measured in candela (cd).\n\t * @param {number} [distance=0] - Maximum range of the light. `0` means no limit.\n\t * @param {number} [angle=Math.PI/3] - Maximum angle of light dispersion from its direction whose upper bound is `Math.PI/2`.\n\t * @param {number} [penumbra=0] - Percent of the spotlight cone that is attenuated due to penumbra. Value range is `[0,1]`.\n\t * @param {number} [decay=2] - The amount the light dims along the distance of the light.\n\t */\n\tconstructor( color, intensity, distance = 0, angle = Math.PI / 3, penumbra = 0, decay = 2 ) {\n\n\t\tsuper( color, intensity );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSpotLight = true;\n\n\t\tthis.type = 'SpotLight';\n\n\t\tthis.position.copy( Object3D.DEFAULT_UP );\n\t\tthis.updateMatrix();\n\n\t\t/**\n\t\t * The spot light points from its position to the\n\t\t * target's position.\n\t\t *\n\t\t * For the target's position to be changed to anything other\n\t\t * than the default, it must be added to the scene.\n\t\t *\n\t\t * It is also possible to set the target to be another 3D object\n\t\t * in the scene. The light will now track the target object.\n\t\t *\n\t\t * @type {Object3D}\n\t\t */\n\t\tthis.target = new Object3D();\n\n\t\t/**\n\t\t * Maximum range of the light. `0` means no limit.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.distance = distance;\n\n\t\t/**\n\t\t * Maximum angle of light dispersion from its direction whose upper bound is `Math.PI/2`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default Math.PI/3\n\t\t */\n\t\tthis.angle = angle;\n\n\t\t/**\n\t\t * Percent of the spotlight cone that is attenuated due to penumbra.\n\t\t * Value range is `[0,1]`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.penumbra = penumbra;\n\n\t\t/**\n\t\t * The amount the light dims along the distance of the light. In context of\n\t\t * physically-correct rendering the default value should not be changed.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 2\n\t\t */\n\t\tthis.decay = decay;\n\n\t\t/**\n\t\t * A texture used to modulate the color of the light. The spot light\n\t\t * color is mixed with the RGB value of this texture, with a ratio\n\t\t * corresponding to its alpha value. The cookie-like masking effect is\n\t\t * reproduced using pixel values (0, 0, 0, 1-cookie_value).\n\t\t *\n\t\t * *Warning*: This property is disabled if {@link Object3D#castShadow} is set to `false`.\n\t\t *\n\t\t * @type {?Texture}\n\t\t * @default null\n\t\t */\n\t\tthis.map = null;\n\n\t\t/**\n\t\t * This property holds the light's shadow configuration.\n\t\t *\n\t\t * @type {SpotLightShadow}\n\t\t */\n\t\tthis.shadow = new SpotLightShadow();\n\n\t}\n\n\t/**\n\t * The light's power. Power is the luminous power of the light measured in lumens (lm).\n\t *  Changing the power will also change the light's intensity.\n\t *\n\t * @type {number}\n\t */\n\tget power() {\n\n\t\t// compute the light's luminous power (in lumens) from its intensity (in candela)\n\t\t// by convention for a spotlight, luminous power (lm) = π * luminous intensity (cd)\n\t\treturn this.intensity * Math.PI;\n\n\t}\n\n\tset power( power ) {\n\n\t\t// set the light's intensity (in candela) from the desired luminous power (in lumens)\n\t\tthis.intensity = power / Math.PI;\n\n\t}\n\n\tdispose() {\n\n\t\tsuper.dispose();\n\n\t\tthis.shadow.dispose();\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.distance = source.distance;\n\t\tthis.angle = source.angle;\n\t\tthis.penumbra = source.penumbra;\n\t\tthis.decay = source.decay;\n\n\t\tthis.target = source.target.clone();\n\t\tthis.map = source.map;\n\t\tthis.shadow = source.shadow.clone();\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.object.distance = this.distance;\n\t\tdata.object.angle = this.angle;\n\t\tdata.object.decay = this.decay;\n\t\tdata.object.penumbra = this.penumbra;\n\n\t\tdata.object.target = this.target.uuid;\n\n\t\tif ( this.map && this.map.isTexture ) data.object.map = this.map.toJSON( meta ).uuid;\n\n\t\tdata.object.shadow = this.shadow.toJSON();\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * Represents the shadow configuration of point lights.\n *\n * @augments LightShadow\n */\nclass PointLightShadow extends LightShadow {\n\n\t/**\n\t * Constructs a new point light shadow.\n\t */\n\tconstructor() {\n\n\t\tsuper( new PerspectiveCamera( 90, 1, 0.5, 500 ) );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isPointLightShadow = true;\n\n\t}\n\n}\n\n/**\n * A light that gets emitted from a single point in all directions. A common\n * use case for this is to replicate the light emitted from a bare\n * lightbulb.\n *\n * This light can cast shadows - see the {@link PointLightShadow} for details.\n *\n * ```js\n * const light = new THREE.PointLight( 0xff0000, 1, 100 );\n * light.position.set( 50, 50, 50 );\n * scene.add( light );\n * ```\n *\n * @augments Light\n */\nclass PointLight extends Light {\n\n\t/**\n\t * Constructs a new point light.\n\t *\n\t * @param {(number|Color|string)} [color=0xffffff] - The light's color.\n\t * @param {number} [intensity=1] - The light's strength/intensity measured in candela (cd).\n\t * @param {number} [distance=0] - Maximum range of the light. `0` means no limit.\n\t * @param {number} [decay=2] - The amount the light dims along the distance of the light.\n\t */\n\tconstructor( color, intensity, distance = 0, decay = 2 ) {\n\n\t\tsuper( color, intensity );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isPointLight = true;\n\n\t\tthis.type = 'PointLight';\n\n\t\t/**\n\t\t * When distance is zero, light will attenuate according to inverse-square\n\t\t * law to infinite distance. When distance is non-zero, light will attenuate\n\t\t * according to inverse-square law until near the distance cutoff, where it\n\t\t * will then attenuate quickly and smoothly to 0. Inherently, cutoffs are not\n\t\t * physically correct.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.distance = distance;\n\n\t\t/**\n\t\t * The amount the light dims along the distance of the light. In context of\n\t\t * physically-correct rendering the default value should not be changed.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 2\n\t\t */\n\t\tthis.decay = decay;\n\n\t\t/**\n\t\t * This property holds the light's shadow configuration.\n\t\t *\n\t\t * @type {PointLightShadow}\n\t\t */\n\t\tthis.shadow = new PointLightShadow();\n\n\t}\n\n\t/**\n\t * The light's power. Power is the luminous power of the light measured in lumens (lm).\n\t * Changing the power will also change the light's intensity.\n\t *\n\t * @type {number}\n\t */\n\tget power() {\n\n\t\t// compute the light's luminous power (in lumens) from its intensity (in candela)\n\t\t// for an isotropic light source, luminous power (lm) = 4 π luminous intensity (cd)\n\t\treturn this.intensity * 4 * Math.PI;\n\n\t}\n\n\tset power( power ) {\n\n\t\t// set the light's intensity (in candela) from the desired luminous power (in lumens)\n\t\tthis.intensity = power / ( 4 * Math.PI );\n\n\t}\n\n\tdispose() {\n\n\t\tsuper.dispose();\n\n\t\tthis.shadow.dispose();\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.distance = source.distance;\n\t\tthis.decay = source.decay;\n\n\t\tthis.shadow = source.shadow.clone();\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.object.distance = this.distance;\n\t\tdata.object.decay = this.decay;\n\n\t\tdata.object.shadow = this.shadow.toJSON();\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * Camera that uses [orthographic projection](https://en.wikipedia.org/wiki/Orthographic_projection).\n *\n * In this projection mode, an object's size in the rendered image stays\n * constant regardless of its distance from the camera. This can be useful\n * for rendering 2D scenes and UI elements, amongst other things.\n *\n * ```js\n * const camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );\n * scene.add( camera );\n * ```\n *\n * @augments Camera\n */\nclass OrthographicCamera extends Camera {\n\n\t/**\n\t * Constructs a new orthographic camera.\n\t *\n\t * @param {number} [left=-1] - The left plane of the camera's frustum.\n\t * @param {number} [right=1] - The right plane of the camera's frustum.\n\t * @param {number} [top=1] - The top plane of the camera's frustum.\n\t * @param {number} [bottom=-1] - The bottom plane of the camera's frustum.\n\t * @param {number} [near=0.1] - The camera's near plane.\n\t * @param {number} [far=2000] - The camera's far plane.\n\t */\n\tconstructor( left = -1, right = 1, top = 1, bottom = -1, near = 0.1, far = 2000 ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isOrthographicCamera = true;\n\n\t\tthis.type = 'OrthographicCamera';\n\n\t\t/**\n\t\t * The zoom factor of the camera.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.zoom = 1;\n\n\t\t/**\n\t\t * Represents the frustum window specification. This property should not be edited\n\t\t * directly but via {@link PerspectiveCamera#setViewOffset} and {@link PerspectiveCamera#clearViewOffset}.\n\t\t *\n\t\t * @type {?Object}\n\t\t * @default null\n\t\t */\n\t\tthis.view = null;\n\n\t\t/**\n\t\t * The left plane of the camera's frustum.\n\t\t *\n\t\t * @type {number}\n\t\t * @default -1\n\t\t */\n\t\tthis.left = left;\n\n\t\t/**\n\t\t * The right plane of the camera's frustum.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.right = right;\n\n\t\t/**\n\t\t * The top plane of the camera's frustum.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.top = top;\n\n\t\t/**\n\t\t * The bottom plane of the camera's frustum.\n\t\t *\n\t\t * @type {number}\n\t\t * @default -1\n\t\t */\n\t\tthis.bottom = bottom;\n\n\t\t/**\n\t\t * The camera's near plane. The valid range is greater than `0`\n\t\t * and less than the current value of {@link OrthographicCamera#far}.\n\t\t *\n\t\t * Note that, unlike for the {@link PerspectiveCamera}, `0` is a\n\t\t * valid value for an orthographic camera's near plane.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0.1\n\t\t */\n\t\tthis.near = near;\n\n\t\t/**\n\t\t * The camera's far plane. Must be greater than the\n\t\t * current value of {@link OrthographicCamera#near}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 2000\n\t\t */\n\t\tthis.far = far;\n\n\t\tthis.updateProjectionMatrix();\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.left = source.left;\n\t\tthis.right = source.right;\n\t\tthis.top = source.top;\n\t\tthis.bottom = source.bottom;\n\t\tthis.near = source.near;\n\t\tthis.far = source.far;\n\n\t\tthis.zoom = source.zoom;\n\t\tthis.view = source.view === null ? null : Object.assign( {}, source.view );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets an offset in a larger frustum. This is useful for multi-window or\n\t * multi-monitor/multi-machine setups.\n\t *\n\t * @param {number} fullWidth - The full width of multiview setup.\n\t * @param {number} fullHeight - The full height of multiview setup.\n\t * @param {number} x - The horizontal offset of the subcamera.\n\t * @param {number} y - The vertical offset of the subcamera.\n\t * @param {number} width - The width of subcamera.\n\t * @param {number} height - The height of subcamera.\n\t * @see {@link PerspectiveCamera#setViewOffset}\n\t */\n\tsetViewOffset( fullWidth, fullHeight, x, y, width, height ) {\n\n\t\tif ( this.view === null ) {\n\n\t\t\tthis.view = {\n\t\t\t\tenabled: true,\n\t\t\t\tfullWidth: 1,\n\t\t\t\tfullHeight: 1,\n\t\t\t\toffsetX: 0,\n\t\t\t\toffsetY: 0,\n\t\t\t\twidth: 1,\n\t\t\t\theight: 1\n\t\t\t};\n\n\t\t}\n\n\t\tthis.view.enabled = true;\n\t\tthis.view.fullWidth = fullWidth;\n\t\tthis.view.fullHeight = fullHeight;\n\t\tthis.view.offsetX = x;\n\t\tthis.view.offsetY = y;\n\t\tthis.view.width = width;\n\t\tthis.view.height = height;\n\n\t\tthis.updateProjectionMatrix();\n\n\t}\n\n\t/**\n\t * Removes the view offset from the projection matrix.\n\t */\n\tclearViewOffset() {\n\n\t\tif ( this.view !== null ) {\n\n\t\t\tthis.view.enabled = false;\n\n\t\t}\n\n\t\tthis.updateProjectionMatrix();\n\n\t}\n\n\t/**\n\t * Updates the camera's projection matrix. Must be called after any change of\n\t * camera properties.\n\t */\n\tupdateProjectionMatrix() {\n\n\t\tconst dx = ( this.right - this.left ) / ( 2 * this.zoom );\n\t\tconst dy = ( this.top - this.bottom ) / ( 2 * this.zoom );\n\t\tconst cx = ( this.right + this.left ) / 2;\n\t\tconst cy = ( this.top + this.bottom ) / 2;\n\n\t\tlet left = cx - dx;\n\t\tlet right = cx + dx;\n\t\tlet top = cy + dy;\n\t\tlet bottom = cy - dy;\n\n\t\tif ( this.view !== null && this.view.enabled ) {\n\n\t\t\tconst scaleW = ( this.right - this.left ) / this.view.fullWidth / this.zoom;\n\t\t\tconst scaleH = ( this.top - this.bottom ) / this.view.fullHeight / this.zoom;\n\n\t\t\tleft += scaleW * this.view.offsetX;\n\t\t\tright = left + scaleW * this.view.width;\n\t\t\ttop -= scaleH * this.view.offsetY;\n\t\t\tbottom = top - scaleH * this.view.height;\n\n\t\t}\n\n\t\tthis.projectionMatrix.makeOrthographic( left, right, top, bottom, this.near, this.far, this.coordinateSystem, this.reversedDepth );\n\n\t\tthis.projectionMatrixInverse.copy( this.projectionMatrix ).invert();\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.object.zoom = this.zoom;\n\t\tdata.object.left = this.left;\n\t\tdata.object.right = this.right;\n\t\tdata.object.top = this.top;\n\t\tdata.object.bottom = this.bottom;\n\t\tdata.object.near = this.near;\n\t\tdata.object.far = this.far;\n\n\t\tif ( this.view !== null ) data.object.view = Object.assign( {}, this.view );\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * Represents the shadow configuration of directional lights.\n *\n * @augments LightShadow\n */\nclass DirectionalLightShadow extends LightShadow {\n\n\t/**\n\t * Constructs a new directional light shadow.\n\t */\n\tconstructor() {\n\n\t\tsuper( new OrthographicCamera( -5, 5, 5, -5, 0.5, 500 ) );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isDirectionalLightShadow = true;\n\n\t}\n\n}\n\n/**\n * A light that gets emitted in a specific direction. This light will behave\n * as though it is infinitely far away and the rays produced from it are all\n * parallel. The common use case for this is to simulate daylight; the sun is\n * far enough away that its position can be considered to be infinite, and\n * all light rays coming from it are parallel.\n *\n * A common point of confusion for directional lights is that setting the\n * rotation has no effect. This is because three.js's DirectionalLight is the\n * equivalent to what is often called a 'Target Direct Light' in other\n * applications.\n *\n * This means that its direction is calculated as pointing from the light's\n * {@link Object3D#position} to the {@link DirectionalLight#target} position\n * (as opposed to a 'Free Direct Light' that just has a rotation\n * component).\n *\n * This light can cast shadows - see the {@link DirectionalLightShadow} for details.\n *\n * ```js\n * // White directional light at half intensity shining from the top.\n * const directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );\n * scene.add( directionalLight );\n * ```\n *\n * @augments Light\n */\nclass DirectionalLight extends Light {\n\n\t/**\n\t * Constructs a new directional light.\n\t *\n\t * @param {(number|Color|string)} [color=0xffffff] - The light's color.\n\t * @param {number} [intensity=1] - The light's strength/intensity.\n\t */\n\tconstructor( color, intensity ) {\n\n\t\tsuper( color, intensity );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isDirectionalLight = true;\n\n\t\tthis.type = 'DirectionalLight';\n\n\t\tthis.position.copy( Object3D.DEFAULT_UP );\n\t\tthis.updateMatrix();\n\n\t\t/**\n\t\t * The directional light points from its position to the\n\t\t * target's position.\n\t\t *\n\t\t * For the target's position to be changed to anything other\n\t\t * than the default, it must be added to the scene.\n\t\t *\n\t\t * It is also possible to set the target to be another 3D object\n\t\t * in the scene. The light will now track the target object.\n\t\t *\n\t\t * @type {Object3D}\n\t\t */\n\t\tthis.target = new Object3D();\n\n\t\t/**\n\t\t * This property holds the light's shadow configuration.\n\t\t *\n\t\t * @type {DirectionalLightShadow}\n\t\t */\n\t\tthis.shadow = new DirectionalLightShadow();\n\n\t}\n\n\tdispose() {\n\n\t\tsuper.dispose();\n\n\t\tthis.shadow.dispose();\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.target = source.target.clone();\n\t\tthis.shadow = source.shadow.clone();\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.object.shadow = this.shadow.toJSON();\n\t\tdata.object.target = this.target.uuid;\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * This light globally illuminates all objects in the scene equally.\n *\n * It cannot be used to cast shadows as it does not have a direction.\n *\n * ```js\n * const light = new THREE.AmbientLight( 0x404040 ); // soft white light\n * scene.add( light );\n * ```\n *\n * @augments Light\n */\nclass AmbientLight extends Light {\n\n\t/**\n\t * Constructs a new ambient light.\n\t *\n\t * @param {(number|Color|string)} [color=0xffffff] - The light's color.\n\t * @param {number} [intensity=1] - The light's strength/intensity.\n\t */\n\tconstructor( color, intensity ) {\n\n\t\tsuper( color, intensity );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isAmbientLight = true;\n\n\t\tthis.type = 'AmbientLight';\n\n\t}\n\n}\n\n/**\n * This class emits light uniformly across the face a rectangular plane.\n * This light type can be used to simulate light sources such as bright\n * windows or strip lighting.\n *\n * Important Notes:\n *\n * - There is no shadow support.\n * - Only PBR materials are supported.\n * - You have to include `RectAreaLightUniformsLib` (`WebGLRenderer`) or `RectAreaLightTexturesLib` (`WebGPURenderer`)\n * into your app and init the uniforms/textures.\n *\n * ```js\n * RectAreaLightUniformsLib.init(); // only relevant for WebGLRenderer\n * THREE.RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() ); //  only relevant for WebGPURenderer\n *\n * const intensity = 1; const width = 10; const height = 10;\n * const rectLight = new THREE.RectAreaLight( 0xffffff, intensity, width, height );\n * rectLight.position.set( 5, 5, 0 );\n * rectLight.lookAt( 0, 0, 0 );\n * scene.add( rectLight )\n * ```\n *\n * @augments Light\n */\nclass RectAreaLight extends Light {\n\n\t/**\n\t * Constructs a new area light.\n\t *\n\t * @param {(number|Color|string)} [color=0xffffff] - The light's color.\n\t * @param {number} [intensity=1] - The light's strength/intensity.\n\t * @param {number} [width=10] - The width of the light.\n\t * @param {number} [height=10] - The height of the light.\n\t */\n\tconstructor( color, intensity, width = 10, height = 10 ) {\n\n\t\tsuper( color, intensity );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isRectAreaLight = true;\n\n\t\tthis.type = 'RectAreaLight';\n\n\t\t/**\n\t\t * The width of the light.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 10\n\t\t */\n\t\tthis.width = width;\n\n\t\t/**\n\t\t * The height of the light.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 10\n\t\t */\n\t\tthis.height = height;\n\n\t}\n\n\t/**\n\t * The light's power. Power is the luminous power of the light measured in lumens (lm).\n\t * Changing the power will also change the light's intensity.\n\t *\n\t * @type {number}\n\t */\n\tget power() {\n\n\t\t// compute the light's luminous power (in lumens) from its intensity (in nits)\n\t\treturn this.intensity * this.width * this.height * Math.PI;\n\n\t}\n\n\tset power( power ) {\n\n\t\t// set the light's intensity (in nits) from the desired luminous power (in lumens)\n\t\tthis.intensity = power / ( this.width * this.height * Math.PI );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.width = source.width;\n\t\tthis.height = source.height;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.object.width = this.width;\n\t\tdata.object.height = this.height;\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * Represents a third-order spherical harmonics (SH). Light probes use this class\n * to encode lighting information.\n *\n * - Primary reference: {@link https://graphics.stanford.edu/papers/envmap/envmap.pdf}\n * - Secondary reference: {@link https://www.ppsloan.org/publications/StupidSH36.pdf}\n */\nclass SphericalHarmonics3 {\n\n\t/**\n\t * Constructs a new spherical harmonics.\n\t */\n\tconstructor() {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSphericalHarmonics3 = true;\n\n\t\t/**\n\t\t * An array holding the (9) SH coefficients.\n\t\t *\n\t\t * @type {Array<Vector3>}\n\t\t */\n\t\tthis.coefficients = [];\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tthis.coefficients.push( new Vector3() );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sets the given SH coefficients to this instance by copying\n\t * the values.\n\t *\n\t * @param {Array<Vector3>} coefficients - The SH coefficients.\n\t * @return {SphericalHarmonics3} A reference to this spherical harmonics.\n\t */\n\tset( coefficients ) {\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tthis.coefficients[ i ].copy( coefficients[ i ] );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets all SH coefficients to `0`.\n\t *\n\t * @return {SphericalHarmonics3} A reference to this spherical harmonics.\n\t */\n\tzero() {\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tthis.coefficients[ i ].set( 0, 0, 0 );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the radiance in the direction of the given normal.\n\t *\n\t * @param {Vector3} normal - The normal vector (assumed to be unit length)\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The radiance.\n\t */\n\tgetAt( normal, target ) {\n\n\t\t// normal is assumed to be unit length\n\n\t\tconst x = normal.x, y = normal.y, z = normal.z;\n\n\t\tconst coeff = this.coefficients;\n\n\t\t// band 0\n\t\ttarget.copy( coeff[ 0 ] ).multiplyScalar( 0.282095 );\n\n\t\t// band 1\n\t\ttarget.addScaledVector( coeff[ 1 ], 0.488603 * y );\n\t\ttarget.addScaledVector( coeff[ 2 ], 0.488603 * z );\n\t\ttarget.addScaledVector( coeff[ 3 ], 0.488603 * x );\n\n\t\t// band 2\n\t\ttarget.addScaledVector( coeff[ 4 ], 1.092548 * ( x * y ) );\n\t\ttarget.addScaledVector( coeff[ 5 ], 1.092548 * ( y * z ) );\n\t\ttarget.addScaledVector( coeff[ 6 ], 0.315392 * ( 3.0 * z * z - 1.0 ) );\n\t\ttarget.addScaledVector( coeff[ 7 ], 1.092548 * ( x * z ) );\n\t\ttarget.addScaledVector( coeff[ 8 ], 0.546274 * ( x * x - y * y ) );\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Returns the irradiance (radiance convolved with cosine lobe) in the\n\t * direction of the given normal.\n\t *\n\t * @param {Vector3} normal - The normal vector (assumed to be unit length)\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The irradiance.\n\t */\n\tgetIrradianceAt( normal, target ) {\n\n\t\t// normal is assumed to be unit length\n\n\t\tconst x = normal.x, y = normal.y, z = normal.z;\n\n\t\tconst coeff = this.coefficients;\n\n\t\t// band 0\n\t\ttarget.copy( coeff[ 0 ] ).multiplyScalar( 0.886227 ); // π * 0.282095\n\n\t\t// band 1\n\t\ttarget.addScaledVector( coeff[ 1 ], 2.0 * 0.511664 * y ); // ( 2 * π / 3 ) * 0.488603\n\t\ttarget.addScaledVector( coeff[ 2 ], 2.0 * 0.511664 * z );\n\t\ttarget.addScaledVector( coeff[ 3 ], 2.0 * 0.511664 * x );\n\n\t\t// band 2\n\t\ttarget.addScaledVector( coeff[ 4 ], 2.0 * 0.429043 * x * y ); // ( π / 4 ) * 1.092548\n\t\ttarget.addScaledVector( coeff[ 5 ], 2.0 * 0.429043 * y * z );\n\t\ttarget.addScaledVector( coeff[ 6 ], 0.743125 * z * z - 0.247708 ); // ( π / 4 ) * 0.315392 * 3\n\t\ttarget.addScaledVector( coeff[ 7 ], 2.0 * 0.429043 * x * z );\n\t\ttarget.addScaledVector( coeff[ 8 ], 0.429043 * ( x * x - y * y ) ); // ( π / 4 ) * 0.546274\n\n\t\treturn target;\n\n\t}\n\n\t/**\n\t * Adds the given SH to this instance.\n\t *\n\t * @param {SphericalHarmonics3} sh - The SH to add.\n\t * @return {SphericalHarmonics3} A reference to this spherical harmonics.\n\t */\n\tadd( sh ) {\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tthis.coefficients[ i ].add( sh.coefficients[ i ] );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * A convenience method for performing {@link SphericalHarmonics3#add} and\n\t * {@link SphericalHarmonics3#scale} at once.\n\t *\n\t * @param {SphericalHarmonics3} sh - The SH to add.\n\t * @param {number} s - The scale factor.\n\t * @return {SphericalHarmonics3} A reference to this spherical harmonics.\n\t */\n\taddScaledSH( sh, s ) {\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tthis.coefficients[ i ].addScaledVector( sh.coefficients[ i ], s );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Scales this SH by the given scale factor.\n\t *\n\t * @param {number} s - The scale factor.\n\t * @return {SphericalHarmonics3} A reference to this spherical harmonics.\n\t */\n\tscale( s ) {\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tthis.coefficients[ i ].multiplyScalar( s );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Linear interpolates between the given SH and this instance by the given\n\t * alpha factor.\n\t *\n\t * @param {SphericalHarmonics3} sh - The SH to interpolate with.\n\t * @param {number} alpha - The alpha factor.\n\t * @return {SphericalHarmonics3} A reference to this spherical harmonics.\n\t */\n\tlerp( sh, alpha ) {\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tthis.coefficients[ i ].lerp( sh.coefficients[ i ], alpha );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this spherical harmonics is equal with the given one.\n\t *\n\t * @param {SphericalHarmonics3} sh - The spherical harmonics to test for equality.\n\t * @return {boolean} Whether this spherical harmonics is equal with the given one.\n\t */\n\tequals( sh ) {\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tif ( ! this.coefficients[ i ].equals( sh.coefficients[ i ] ) ) {\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Copies the values of the given spherical harmonics to this instance.\n\t *\n\t * @param {SphericalHarmonics3} sh - The spherical harmonics to copy.\n\t * @return {SphericalHarmonics3} A reference to this spherical harmonics.\n\t */\n\tcopy( sh ) {\n\n\t\treturn this.set( sh.coefficients );\n\n\t}\n\n\t/**\n\t * Returns a new spherical harmonics with copied values from this instance.\n\t *\n\t * @return {SphericalHarmonics3} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Sets the SH coefficients of this instance from the given array.\n\t *\n\t * @param {Array<number>} array - An array holding the SH coefficients.\n\t * @param {number} [offset=0] - The array offset where to start copying.\n\t * @return {SphericalHarmonics3} A clone of this instance.\n\t */\n\tfromArray( array, offset = 0 ) {\n\n\t\tconst coefficients = this.coefficients;\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tcoefficients[ i ].fromArray( array, offset + ( i * 3 ) );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns an array with the SH coefficients, or copies them into the provided\n\t * array. The coefficients are represented as numbers.\n\t *\n\t * @param {Array<number>} [array=[]] - The target array.\n\t * @param {number} [offset=0] - The array offset where to start copying.\n\t * @return {Array<number>} An array with flat SH coefficients.\n\t */\n\ttoArray( array = [], offset = 0 ) {\n\n\t\tconst coefficients = this.coefficients;\n\n\t\tfor ( let i = 0; i < 9; i ++ ) {\n\n\t\t\tcoefficients[ i ].toArray( array, offset + ( i * 3 ) );\n\n\t\t}\n\n\t\treturn array;\n\n\t}\n\n\t/**\n\t * Computes the SH basis for the given normal vector.\n\t *\n\t * @param {Vector3} normal - The normal.\n\t * @param {Array<number>} shBasis - The target array holding the SH basis.\n\t */\n\tstatic getBasisAt( normal, shBasis ) {\n\n\t\t// normal is assumed to be unit length\n\n\t\tconst x = normal.x, y = normal.y, z = normal.z;\n\n\t\t// band 0\n\t\tshBasis[ 0 ] = 0.282095;\n\n\t\t// band 1\n\t\tshBasis[ 1 ] = 0.488603 * y;\n\t\tshBasis[ 2 ] = 0.488603 * z;\n\t\tshBasis[ 3 ] = 0.488603 * x;\n\n\t\t// band 2\n\t\tshBasis[ 4 ] = 1.092548 * x * y;\n\t\tshBasis[ 5 ] = 1.092548 * y * z;\n\t\tshBasis[ 6 ] = 0.315392 * ( 3 * z * z - 1 );\n\t\tshBasis[ 7 ] = 1.092548 * x * z;\n\t\tshBasis[ 8 ] = 0.546274 * ( x * x - y * y );\n\n\t}\n\n}\n\n/**\n * Light probes are an alternative way of adding light to a 3D scene. Unlike\n * classical light sources (e.g. directional, point or spot lights), light\n * probes do not emit light. Instead they store information about light\n * passing through 3D space. During rendering, the light that hits a 3D\n * object is approximated by using the data from the light probe.\n *\n * Light probes are usually created from (radiance) environment maps. The\n * class {@link LightProbeGenerator} can be used to create light probes from\n * cube textures or render targets. However, light estimation data could also\n * be provided in other forms e.g. by WebXR. This enables the rendering of\n * augmented reality content that reacts to real world lighting.\n *\n * The current probe implementation in three.js supports so-called diffuse\n * light probes. This type of light probe is functionally equivalent to an\n * irradiance environment map.\n *\n * @augments Light\n */\nclass LightProbe extends Light {\n\n\t/**\n\t * Constructs a new light probe.\n\t *\n\t * @param {SphericalHarmonics3} sh - The spherical harmonics which represents encoded lighting information.\n\t * @param {number} [intensity=1] - The light's strength/intensity.\n\t */\n\tconstructor( sh = new SphericalHarmonics3(), intensity = 1 ) {\n\n\t\tsuper( undefined, intensity );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isLightProbe = true;\n\n\t\t/**\n\t\t * A light probe uses spherical harmonics to encode lighting information.\n\t\t *\n\t\t * @type {SphericalHarmonics3}\n\t\t */\n\t\tthis.sh = sh;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.sh.copy( source.sh );\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON( meta ) {\n\n\t\tconst data = super.toJSON( meta );\n\n\t\tdata.object.sh = this.sh.toArray();\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * Class for loading materials. The files are internally\n * loaded via {@link FileLoader}.\n *\n * ```js\n * const loader = new THREE.MaterialLoader();\n * const material = await loader.loadAsync( 'material.json' );\n * ```\n * This loader does not support node materials. Use {@link NodeMaterialLoader} instead.\n *\n * @augments Loader\n */\nclass MaterialLoader extends Loader {\n\n\t/**\n\t * Constructs a new material loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t\t/**\n\t\t * A dictionary holding textures used by the material.\n\t\t *\n\t\t * @type {Object<string,Texture>}\n\t\t */\n\t\tthis.textures = {};\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and pass the loaded material to the `onLoad()` callback.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(Material)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Executed while the loading is in progress.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tconst scope = this;\n\n\t\tconst loader = new FileLoader( scope.manager );\n\t\tloader.setPath( scope.path );\n\t\tloader.setRequestHeader( scope.requestHeader );\n\t\tloader.setWithCredentials( scope.withCredentials );\n\t\tloader.load( url, function ( text ) {\n\n\t\t\ttry {\n\n\t\t\t\tonLoad( scope.parse( JSON.parse( text ) ) );\n\n\t\t\t} catch ( e ) {\n\n\t\t\t\tif ( onError ) {\n\n\t\t\t\t\tonError( e );\n\n\t\t\t\t} else {\n\n\t\t\t\t\terror( e );\n\n\t\t\t\t}\n\n\t\t\t\tscope.manager.itemError( url );\n\n\t\t\t}\n\n\t\t}, onProgress, onError );\n\n\t}\n\n\t/**\n\t * Parses the given JSON object and returns a material.\n\t *\n\t * @param {Object} json - The serialized material.\n\t * @return {Material} The parsed material.\n\t */\n\tparse( json ) {\n\n\t\tconst textures = this.textures;\n\n\t\tfunction getTexture( name ) {\n\n\t\t\tif ( textures[ name ] === undefined ) {\n\n\t\t\t\twarn( 'MaterialLoader: Undefined texture', name );\n\n\t\t\t}\n\n\t\t\treturn textures[ name ];\n\n\t\t}\n\n\t\tconst material = this.createMaterialFromType( json.type );\n\n\t\tif ( json.uuid !== undefined ) material.uuid = json.uuid;\n\t\tif ( json.name !== undefined ) material.name = json.name;\n\t\tif ( json.color !== undefined && material.color !== undefined ) material.color.setHex( json.color );\n\t\tif ( json.roughness !== undefined ) material.roughness = json.roughness;\n\t\tif ( json.metalness !== undefined ) material.metalness = json.metalness;\n\t\tif ( json.sheen !== undefined ) material.sheen = json.sheen;\n\t\tif ( json.sheenColor !== undefined ) material.sheenColor = new Color().setHex( json.sheenColor );\n\t\tif ( json.sheenRoughness !== undefined ) material.sheenRoughness = json.sheenRoughness;\n\t\tif ( json.emissive !== undefined && material.emissive !== undefined ) material.emissive.setHex( json.emissive );\n\t\tif ( json.specular !== undefined && material.specular !== undefined ) material.specular.setHex( json.specular );\n\t\tif ( json.specularIntensity !== undefined ) material.specularIntensity = json.specularIntensity;\n\t\tif ( json.specularColor !== undefined && material.specularColor !== undefined ) material.specularColor.setHex( json.specularColor );\n\t\tif ( json.shininess !== undefined ) material.shininess = json.shininess;\n\t\tif ( json.clearcoat !== undefined ) material.clearcoat = json.clearcoat;\n\t\tif ( json.clearcoatRoughness !== undefined ) material.clearcoatRoughness = json.clearcoatRoughness;\n\t\tif ( json.dispersion !== undefined ) material.dispersion = json.dispersion;\n\t\tif ( json.iridescence !== undefined ) material.iridescence = json.iridescence;\n\t\tif ( json.iridescenceIOR !== undefined ) material.iridescenceIOR = json.iridescenceIOR;\n\t\tif ( json.iridescenceThicknessRange !== undefined ) material.iridescenceThicknessRange = json.iridescenceThicknessRange;\n\t\tif ( json.transmission !== undefined ) material.transmission = json.transmission;\n\t\tif ( json.thickness !== undefined ) material.thickness = json.thickness;\n\t\tif ( json.attenuationDistance !== undefined ) material.attenuationDistance = json.attenuationDistance;\n\t\tif ( json.attenuationColor !== undefined && material.attenuationColor !== undefined ) material.attenuationColor.setHex( json.attenuationColor );\n\t\tif ( json.anisotropy !== undefined ) material.anisotropy = json.anisotropy;\n\t\tif ( json.anisotropyRotation !== undefined ) material.anisotropyRotation = json.anisotropyRotation;\n\t\tif ( json.fog !== undefined ) material.fog = json.fog;\n\t\tif ( json.flatShading !== undefined ) material.flatShading = json.flatShading;\n\t\tif ( json.blending !== undefined ) material.blending = json.blending;\n\t\tif ( json.combine !== undefined ) material.combine = json.combine;\n\t\tif ( json.side !== undefined ) material.side = json.side;\n\t\tif ( json.shadowSide !== undefined ) material.shadowSide = json.shadowSide;\n\t\tif ( json.opacity !== undefined ) material.opacity = json.opacity;\n\t\tif ( json.transparent !== undefined ) material.transparent = json.transparent;\n\t\tif ( json.alphaTest !== undefined ) material.alphaTest = json.alphaTest;\n\t\tif ( json.alphaHash !== undefined ) material.alphaHash = json.alphaHash;\n\t\tif ( json.depthFunc !== undefined ) material.depthFunc = json.depthFunc;\n\t\tif ( json.depthTest !== undefined ) material.depthTest = json.depthTest;\n\t\tif ( json.depthWrite !== undefined ) material.depthWrite = json.depthWrite;\n\t\tif ( json.colorWrite !== undefined ) material.colorWrite = json.colorWrite;\n\t\tif ( json.blendSrc !== undefined ) material.blendSrc = json.blendSrc;\n\t\tif ( json.blendDst !== undefined ) material.blendDst = json.blendDst;\n\t\tif ( json.blendEquation !== undefined ) material.blendEquation = json.blendEquation;\n\t\tif ( json.blendSrcAlpha !== undefined ) material.blendSrcAlpha = json.blendSrcAlpha;\n\t\tif ( json.blendDstAlpha !== undefined ) material.blendDstAlpha = json.blendDstAlpha;\n\t\tif ( json.blendEquationAlpha !== undefined ) material.blendEquationAlpha = json.blendEquationAlpha;\n\t\tif ( json.blendColor !== undefined && material.blendColor !== undefined ) material.blendColor.setHex( json.blendColor );\n\t\tif ( json.blendAlpha !== undefined ) material.blendAlpha = json.blendAlpha;\n\t\tif ( json.stencilWriteMask !== undefined ) material.stencilWriteMask = json.stencilWriteMask;\n\t\tif ( json.stencilFunc !== undefined ) material.stencilFunc = json.stencilFunc;\n\t\tif ( json.stencilRef !== undefined ) material.stencilRef = json.stencilRef;\n\t\tif ( json.stencilFuncMask !== undefined ) material.stencilFuncMask = json.stencilFuncMask;\n\t\tif ( json.stencilFail !== undefined ) material.stencilFail = json.stencilFail;\n\t\tif ( json.stencilZFail !== undefined ) material.stencilZFail = json.stencilZFail;\n\t\tif ( json.stencilZPass !== undefined ) material.stencilZPass = json.stencilZPass;\n\t\tif ( json.stencilWrite !== undefined ) material.stencilWrite = json.stencilWrite;\n\n\t\tif ( json.wireframe !== undefined ) material.wireframe = json.wireframe;\n\t\tif ( json.wireframeLinewidth !== undefined ) material.wireframeLinewidth = json.wireframeLinewidth;\n\t\tif ( json.wireframeLinecap !== undefined ) material.wireframeLinecap = json.wireframeLinecap;\n\t\tif ( json.wireframeLinejoin !== undefined ) material.wireframeLinejoin = json.wireframeLinejoin;\n\n\t\tif ( json.rotation !== undefined ) material.rotation = json.rotation;\n\n\t\tif ( json.linewidth !== undefined ) material.linewidth = json.linewidth;\n\t\tif ( json.dashSize !== undefined ) material.dashSize = json.dashSize;\n\t\tif ( json.gapSize !== undefined ) material.gapSize = json.gapSize;\n\t\tif ( json.scale !== undefined ) material.scale = json.scale;\n\n\t\tif ( json.polygonOffset !== undefined ) material.polygonOffset = json.polygonOffset;\n\t\tif ( json.polygonOffsetFactor !== undefined ) material.polygonOffsetFactor = json.polygonOffsetFactor;\n\t\tif ( json.polygonOffsetUnits !== undefined ) material.polygonOffsetUnits = json.polygonOffsetUnits;\n\n\t\tif ( json.dithering !== undefined ) material.dithering = json.dithering;\n\n\t\tif ( json.alphaToCoverage !== undefined ) material.alphaToCoverage = json.alphaToCoverage;\n\t\tif ( json.premultipliedAlpha !== undefined ) material.premultipliedAlpha = json.premultipliedAlpha;\n\t\tif ( json.forceSinglePass !== undefined ) material.forceSinglePass = json.forceSinglePass;\n\t\tif ( json.allowOverride !== undefined ) material.allowOverride = json.allowOverride;\n\n\t\tif ( json.visible !== undefined ) material.visible = json.visible;\n\n\t\tif ( json.toneMapped !== undefined ) material.toneMapped = json.toneMapped;\n\n\t\tif ( json.userData !== undefined ) material.userData = json.userData;\n\n\t\tif ( json.vertexColors !== undefined ) {\n\n\t\t\tif ( typeof json.vertexColors === 'number' ) {\n\n\t\t\t\tmaterial.vertexColors = ( json.vertexColors > 0 ) ? true : false;\n\n\t\t\t} else {\n\n\t\t\t\tmaterial.vertexColors = json.vertexColors;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// Shader Material\n\n\t\tif ( json.uniforms !== undefined ) {\n\n\t\t\tfor ( const name in json.uniforms ) {\n\n\t\t\t\tconst uniform = json.uniforms[ name ];\n\n\t\t\t\tmaterial.uniforms[ name ] = {};\n\n\t\t\t\tswitch ( uniform.type ) {\n\n\t\t\t\t\tcase 't':\n\t\t\t\t\t\tmaterial.uniforms[ name ].value = getTexture( uniform.value );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'c':\n\t\t\t\t\t\tmaterial.uniforms[ name ].value = new Color().setHex( uniform.value );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'v2':\n\t\t\t\t\t\tmaterial.uniforms[ name ].value = new Vector2().fromArray( uniform.value );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'v3':\n\t\t\t\t\t\tmaterial.uniforms[ name ].value = new Vector3().fromArray( uniform.value );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'v4':\n\t\t\t\t\t\tmaterial.uniforms[ name ].value = new Vector4().fromArray( uniform.value );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'm3':\n\t\t\t\t\t\tmaterial.uniforms[ name ].value = new Matrix3().fromArray( uniform.value );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'm4':\n\t\t\t\t\t\tmaterial.uniforms[ name ].value = new Matrix4().fromArray( uniform.value );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tmaterial.uniforms[ name ].value = uniform.value;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( json.defines !== undefined ) material.defines = json.defines;\n\t\tif ( json.vertexShader !== undefined ) material.vertexShader = json.vertexShader;\n\t\tif ( json.fragmentShader !== undefined ) material.fragmentShader = json.fragmentShader;\n\t\tif ( json.glslVersion !== undefined ) material.glslVersion = json.glslVersion;\n\n\t\tif ( json.extensions !== undefined ) {\n\n\t\t\tfor ( const key in json.extensions ) {\n\n\t\t\t\tmaterial.extensions[ key ] = json.extensions[ key ];\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( json.lights !== undefined ) material.lights = json.lights;\n\t\tif ( json.clipping !== undefined ) material.clipping = json.clipping;\n\n\t\t// for PointsMaterial\n\n\t\tif ( json.size !== undefined ) material.size = json.size;\n\t\tif ( json.sizeAttenuation !== undefined ) material.sizeAttenuation = json.sizeAttenuation;\n\n\t\t// maps\n\n\t\tif ( json.map !== undefined ) material.map = getTexture( json.map );\n\t\tif ( json.matcap !== undefined ) material.matcap = getTexture( json.matcap );\n\n\t\tif ( json.alphaMap !== undefined ) material.alphaMap = getTexture( json.alphaMap );\n\n\t\tif ( json.bumpMap !== undefined ) material.bumpMap = getTexture( json.bumpMap );\n\t\tif ( json.bumpScale !== undefined ) material.bumpScale = json.bumpScale;\n\n\t\tif ( json.normalMap !== undefined ) material.normalMap = getTexture( json.normalMap );\n\t\tif ( json.normalMapType !== undefined ) material.normalMapType = json.normalMapType;\n\t\tif ( json.normalScale !== undefined ) {\n\n\t\t\tlet normalScale = json.normalScale;\n\n\t\t\tif ( Array.isArray( normalScale ) === false ) {\n\n\t\t\t\t// Blender exporter used to export a scalar. See #7459\n\n\t\t\t\tnormalScale = [ normalScale, normalScale ];\n\n\t\t\t}\n\n\t\t\tmaterial.normalScale = new Vector2().fromArray( normalScale );\n\n\t\t}\n\n\t\tif ( json.displacementMap !== undefined ) material.displacementMap = getTexture( json.displacementMap );\n\t\tif ( json.displacementScale !== undefined ) material.displacementScale = json.displacementScale;\n\t\tif ( json.displacementBias !== undefined ) material.displacementBias = json.displacementBias;\n\n\t\tif ( json.roughnessMap !== undefined ) material.roughnessMap = getTexture( json.roughnessMap );\n\t\tif ( json.metalnessMap !== undefined ) material.metalnessMap = getTexture( json.metalnessMap );\n\n\t\tif ( json.emissiveMap !== undefined ) material.emissiveMap = getTexture( json.emissiveMap );\n\t\tif ( json.emissiveIntensity !== undefined ) material.emissiveIntensity = json.emissiveIntensity;\n\n\t\tif ( json.specularMap !== undefined ) material.specularMap = getTexture( json.specularMap );\n\t\tif ( json.specularIntensityMap !== undefined ) material.specularIntensityMap = getTexture( json.specularIntensityMap );\n\t\tif ( json.specularColorMap !== undefined ) material.specularColorMap = getTexture( json.specularColorMap );\n\n\t\tif ( json.envMap !== undefined ) material.envMap = getTexture( json.envMap );\n\t\tif ( json.envMapRotation !== undefined ) material.envMapRotation.fromArray( json.envMapRotation );\n\t\tif ( json.envMapIntensity !== undefined ) material.envMapIntensity = json.envMapIntensity;\n\n\t\tif ( json.reflectivity !== undefined ) material.reflectivity = json.reflectivity;\n\t\tif ( json.refractionRatio !== undefined ) material.refractionRatio = json.refractionRatio;\n\n\t\tif ( json.lightMap !== undefined ) material.lightMap = getTexture( json.lightMap );\n\t\tif ( json.lightMapIntensity !== undefined ) material.lightMapIntensity = json.lightMapIntensity;\n\n\t\tif ( json.aoMap !== undefined ) material.aoMap = getTexture( json.aoMap );\n\t\tif ( json.aoMapIntensity !== undefined ) material.aoMapIntensity = json.aoMapIntensity;\n\n\t\tif ( json.gradientMap !== undefined ) material.gradientMap = getTexture( json.gradientMap );\n\n\t\tif ( json.clearcoatMap !== undefined ) material.clearcoatMap = getTexture( json.clearcoatMap );\n\t\tif ( json.clearcoatRoughnessMap !== undefined ) material.clearcoatRoughnessMap = getTexture( json.clearcoatRoughnessMap );\n\t\tif ( json.clearcoatNormalMap !== undefined ) material.clearcoatNormalMap = getTexture( json.clearcoatNormalMap );\n\t\tif ( json.clearcoatNormalScale !== undefined ) material.clearcoatNormalScale = new Vector2().fromArray( json.clearcoatNormalScale );\n\n\t\tif ( json.iridescenceMap !== undefined ) material.iridescenceMap = getTexture( json.iridescenceMap );\n\t\tif ( json.iridescenceThicknessMap !== undefined ) material.iridescenceThicknessMap = getTexture( json.iridescenceThicknessMap );\n\n\t\tif ( json.transmissionMap !== undefined ) material.transmissionMap = getTexture( json.transmissionMap );\n\t\tif ( json.thicknessMap !== undefined ) material.thicknessMap = getTexture( json.thicknessMap );\n\n\t\tif ( json.anisotropyMap !== undefined ) material.anisotropyMap = getTexture( json.anisotropyMap );\n\n\t\tif ( json.sheenColorMap !== undefined ) material.sheenColorMap = getTexture( json.sheenColorMap );\n\t\tif ( json.sheenRoughnessMap !== undefined ) material.sheenRoughnessMap = getTexture( json.sheenRoughnessMap );\n\n\t\treturn material;\n\n\t}\n\n\t/**\n\t * Textures are not embedded in the material JSON so they have\n\t * to be injected before the loading process starts.\n\t *\n\t * @param {Object} value - A dictionary holding textures for material properties.\n\t * @return {MaterialLoader} A reference to this material loader.\n\t */\n\tsetTextures( value ) {\n\n\t\tthis.textures = value;\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Creates a material for the given type.\n\t *\n\t * @param {string} type - The material type.\n\t * @return {Material} The new material.\n\t */\n\tcreateMaterialFromType( type ) {\n\n\t\treturn MaterialLoader.createMaterialFromType( type );\n\n\t}\n\n\t/**\n\t * Creates a material for the given type.\n\t *\n\t * @static\n\t * @param {string} type - The material type.\n\t * @return {Material} The new material.\n\t */\n\tstatic createMaterialFromType( type ) {\n\n\t\tconst materialLib = {\n\t\t\tShadowMaterial,\n\t\t\tSpriteMaterial,\n\t\t\tRawShaderMaterial,\n\t\t\tShaderMaterial,\n\t\t\tPointsMaterial,\n\t\t\tMeshPhysicalMaterial,\n\t\t\tMeshStandardMaterial,\n\t\t\tMeshPhongMaterial,\n\t\t\tMeshToonMaterial,\n\t\t\tMeshNormalMaterial,\n\t\t\tMeshLambertMaterial,\n\t\t\tMeshDepthMaterial,\n\t\t\tMeshDistanceMaterial,\n\t\t\tMeshBasicMaterial,\n\t\t\tMeshMatcapMaterial,\n\t\t\tLineDashedMaterial,\n\t\t\tLineBasicMaterial,\n\t\t\tMaterial\n\t\t};\n\n\t\treturn new materialLib[ type ]();\n\n\t}\n\n}\n\n/**\n * A class with loader utility functions.\n */\nclass LoaderUtils {\n\n\t/**\n\t * Extracts the base URL from the given URL.\n\t *\n\t * @param {string} url -The URL to extract the base URL from.\n\t * @return {string} The extracted base URL.\n\t */\n\tstatic extractUrlBase( url ) {\n\n\t\tconst index = url.lastIndexOf( '/' );\n\n\t\tif ( index === -1 ) return './';\n\n\t\treturn url.slice( 0, index + 1 );\n\n\t}\n\n\t/**\n\t * Resolves relative URLs against the given path. Absolute paths, data urls,\n\t * and blob URLs will be returned as is. Invalid URLs will return an empty\n\t * string.\n\t *\n\t * @param {string} url -The URL to resolve.\n\t * @param {string} path - The base path for relative URLs to be resolved against.\n\t * @return {string} The resolved URL.\n\t */\n\tstatic resolveURL( url, path ) {\n\n\t\t// Invalid URL\n\t\tif ( typeof url !== 'string' || url === '' ) return '';\n\n\t\t// Host Relative URL\n\t\tif ( /^https?:\\/\\//i.test( path ) && /^\\//.test( url ) ) {\n\n\t\t\tpath = path.replace( /(^https?:\\/\\/[^\\/]+).*/i, '$1' );\n\n\t\t}\n\n\t\t// Absolute URL http://,https://,//\n\t\tif ( /^(https?:)?\\/\\//i.test( url ) ) return url;\n\n\t\t// Data URI\n\t\tif ( /^data:.*,.*$/i.test( url ) ) return url;\n\n\t\t// Blob URL\n\t\tif ( /^blob:.*$/i.test( url ) ) return url;\n\n\t\t// Relative URL\n\t\treturn path + url;\n\n\t}\n\n}\n\n/**\n * An instanced version of a geometry.\n */\nclass InstancedBufferGeometry extends BufferGeometry {\n\n\t/**\n\t * Constructs a new instanced buffer geometry.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isInstancedBufferGeometry = true;\n\n\t\tthis.type = 'InstancedBufferGeometry';\n\n\t\t/**\n\t\t * The instance count.\n\t\t *\n\t\t * @type {number}\n\t\t * @default Infinity\n\t\t */\n\t\tthis.instanceCount = Infinity;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.instanceCount = source.instanceCount;\n\n\t\treturn this;\n\n\t}\n\n\ttoJSON() {\n\n\t\tconst data = super.toJSON();\n\n\t\tdata.instanceCount = this.instanceCount;\n\n\t\tdata.isInstancedBufferGeometry = true;\n\n\t\treturn data;\n\n\t}\n\n}\n\n/**\n * Class for loading geometries. The files are internally\n * loaded via {@link FileLoader}.\n *\n * ```js\n * const loader = new THREE.BufferGeometryLoader();\n * const geometry = await loader.loadAsync( 'models/json/pressure.json' );\n *\n * const material = new THREE.MeshBasicMaterial( { color: 0xF5F5F5 } );\n * const object = new THREE.Mesh( geometry, material );\n * scene.add( object );\n * ```\n *\n * @augments Loader\n */\nclass BufferGeometryLoader extends Loader {\n\n\t/**\n\t * Constructs a new geometry loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and pass the loaded geometry to the `onLoad()` callback.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(BufferGeometry)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Executed while the loading is in progress.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tconst scope = this;\n\n\t\tconst loader = new FileLoader( scope.manager );\n\t\tloader.setPath( scope.path );\n\t\tloader.setRequestHeader( scope.requestHeader );\n\t\tloader.setWithCredentials( scope.withCredentials );\n\t\tloader.load( url, function ( text ) {\n\n\t\t\ttry {\n\n\t\t\t\tonLoad( scope.parse( JSON.parse( text ) ) );\n\n\t\t\t} catch ( e ) {\n\n\t\t\t\tif ( onError ) {\n\n\t\t\t\t\tonError( e );\n\n\t\t\t\t} else {\n\n\t\t\t\t\terror( e );\n\n\t\t\t\t}\n\n\t\t\t\tscope.manager.itemError( url );\n\n\t\t\t}\n\n\t\t}, onProgress, onError );\n\n\t}\n\n\t/**\n\t * Parses the given JSON object and returns a geometry.\n\t *\n\t * @param {Object} json - The serialized geometry.\n\t * @return {BufferGeometry} The parsed geometry.\n\t */\n\tparse( json ) {\n\n\t\tconst interleavedBufferMap = {};\n\t\tconst arrayBufferMap = {};\n\n\t\tfunction getInterleavedBuffer( json, uuid ) {\n\n\t\t\tif ( interleavedBufferMap[ uuid ] !== undefined ) return interleavedBufferMap[ uuid ];\n\n\t\t\tconst interleavedBuffers = json.interleavedBuffers;\n\t\t\tconst interleavedBuffer = interleavedBuffers[ uuid ];\n\n\t\t\tconst buffer = getArrayBuffer( json, interleavedBuffer.buffer );\n\n\t\t\tconst array = getTypedArray( interleavedBuffer.type, buffer );\n\t\t\tconst ib = new InterleavedBuffer( array, interleavedBuffer.stride );\n\t\t\tib.uuid = interleavedBuffer.uuid;\n\n\t\t\tinterleavedBufferMap[ uuid ] = ib;\n\n\t\t\treturn ib;\n\n\t\t}\n\n\t\tfunction getArrayBuffer( json, uuid ) {\n\n\t\t\tif ( arrayBufferMap[ uuid ] !== undefined ) return arrayBufferMap[ uuid ];\n\n\t\t\tconst arrayBuffers = json.arrayBuffers;\n\t\t\tconst arrayBuffer = arrayBuffers[ uuid ];\n\n\t\t\tconst ab = new Uint32Array( arrayBuffer ).buffer;\n\n\t\t\tarrayBufferMap[ uuid ] = ab;\n\n\t\t\treturn ab;\n\n\t\t}\n\n\t\tconst geometry = json.isInstancedBufferGeometry ? new InstancedBufferGeometry() : new BufferGeometry();\n\n\t\tconst index = json.data.index;\n\n\t\tif ( index !== undefined ) {\n\n\t\t\tconst typedArray = getTypedArray( index.type, index.array );\n\t\t\tgeometry.setIndex( new BufferAttribute( typedArray, 1 ) );\n\n\t\t}\n\n\t\tconst attributes = json.data.attributes;\n\n\t\tfor ( const key in attributes ) {\n\n\t\t\tconst attribute = attributes[ key ];\n\t\t\tlet bufferAttribute;\n\n\t\t\tif ( attribute.isInterleavedBufferAttribute ) {\n\n\t\t\t\tconst interleavedBuffer = getInterleavedBuffer( json.data, attribute.data );\n\t\t\t\tbufferAttribute = new InterleavedBufferAttribute( interleavedBuffer, attribute.itemSize, attribute.offset, attribute.normalized );\n\n\t\t\t} else {\n\n\t\t\t\tconst typedArray = getTypedArray( attribute.type, attribute.array );\n\t\t\t\tconst bufferAttributeConstr = attribute.isInstancedBufferAttribute ? InstancedBufferAttribute : BufferAttribute;\n\t\t\t\tbufferAttribute = new bufferAttributeConstr( typedArray, attribute.itemSize, attribute.normalized );\n\n\t\t\t}\n\n\t\t\tif ( attribute.name !== undefined ) bufferAttribute.name = attribute.name;\n\t\t\tif ( attribute.usage !== undefined ) bufferAttribute.setUsage( attribute.usage );\n\n\t\t\tgeometry.setAttribute( key, bufferAttribute );\n\n\t\t}\n\n\t\tconst morphAttributes = json.data.morphAttributes;\n\n\t\tif ( morphAttributes ) {\n\n\t\t\tfor ( const key in morphAttributes ) {\n\n\t\t\t\tconst attributeArray = morphAttributes[ key ];\n\n\t\t\t\tconst array = [];\n\n\t\t\t\tfor ( let i = 0, il = attributeArray.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst attribute = attributeArray[ i ];\n\t\t\t\t\tlet bufferAttribute;\n\n\t\t\t\t\tif ( attribute.isInterleavedBufferAttribute ) {\n\n\t\t\t\t\t\tconst interleavedBuffer = getInterleavedBuffer( json.data, attribute.data );\n\t\t\t\t\t\tbufferAttribute = new InterleavedBufferAttribute( interleavedBuffer, attribute.itemSize, attribute.offset, attribute.normalized );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tconst typedArray = getTypedArray( attribute.type, attribute.array );\n\t\t\t\t\t\tbufferAttribute = new BufferAttribute( typedArray, attribute.itemSize, attribute.normalized );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( attribute.name !== undefined ) bufferAttribute.name = attribute.name;\n\t\t\t\t\tarray.push( bufferAttribute );\n\n\t\t\t\t}\n\n\t\t\t\tgeometry.morphAttributes[ key ] = array;\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst morphTargetsRelative = json.data.morphTargetsRelative;\n\n\t\tif ( morphTargetsRelative ) {\n\n\t\t\tgeometry.morphTargetsRelative = true;\n\n\t\t}\n\n\t\tconst groups = json.data.groups || json.data.drawcalls || json.data.offsets;\n\n\t\tif ( groups !== undefined ) {\n\n\t\t\tfor ( let i = 0, n = groups.length; i !== n; ++ i ) {\n\n\t\t\t\tconst group = groups[ i ];\n\n\t\t\t\tgeometry.addGroup( group.start, group.count, group.materialIndex );\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst boundingSphere = json.data.boundingSphere;\n\n\t\tif ( boundingSphere !== undefined ) {\n\n\t\t\tgeometry.boundingSphere = new Sphere().fromJSON( boundingSphere );\n\n\t\t}\n\n\t\tif ( json.name ) geometry.name = json.name;\n\t\tif ( json.userData ) geometry.userData = json.userData;\n\n\t\treturn geometry;\n\n\t}\n\n}\n\n/**\n * A loader for loading a JSON resource in the [JSON Object/Scene format](https://github.com/mrdoob/three.js/wiki/JSON-Object-Scene-format-4).\n * The files are internally loaded via {@link FileLoader}.\n *\n * ```js\n * const loader = new THREE.ObjectLoader();\n * const obj = await loader.loadAsync( 'models/json/example.json' );\n * scene.add( obj );\n *\n * // Alternatively, to parse a previously loaded JSON structure\n * const object = await loader.parseAsync( a_json_object );\n * scene.add( object );\n * ```\n *\n * @augments Loader\n */\nclass ObjectLoader extends Loader {\n\n\t/**\n\t * Constructs a new object loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and pass the loaded 3D object to the `onLoad()` callback.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(Object3D)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Executed while the loading is in progress.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tconst scope = this;\n\n\t\tconst path = ( this.path === '' ) ? LoaderUtils.extractUrlBase( url ) : this.path;\n\t\tthis.resourcePath = this.resourcePath || path;\n\n\t\tconst loader = new FileLoader( this.manager );\n\t\tloader.setPath( this.path );\n\t\tloader.setRequestHeader( this.requestHeader );\n\t\tloader.setWithCredentials( this.withCredentials );\n\t\tloader.load( url, function ( text ) {\n\n\t\t\tlet json = null;\n\n\t\t\ttry {\n\n\t\t\t\tjson = JSON.parse( text );\n\n\t\t\t} catch ( error ) {\n\n\t\t\t\tif ( onError !== undefined ) onError( error );\n\n\t\t\t\terror( 'ObjectLoader: Can\\'t parse ' + url + '.', error.message );\n\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tconst metadata = json.metadata;\n\n\t\t\tif ( metadata === undefined || metadata.type === undefined || metadata.type.toLowerCase() === 'geometry' ) {\n\n\t\t\t\tif ( onError !== undefined ) onError( new Error( 'THREE.ObjectLoader: Can\\'t load ' + url ) );\n\n\t\t\t\terror( 'ObjectLoader: Can\\'t load ' + url );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tscope.parse( json, onLoad );\n\n\t\t}, onProgress, onError );\n\n\t}\n\n\t/**\n\t * Async version of {@link ObjectLoader#load}.\n\t *\n\t * @async\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {onProgressCallback} onProgress - Executed while the loading is in progress.\n\t * @return {Promise<Object3D>} A Promise that resolves with the loaded 3D object.\n\t */\n\tasync loadAsync( url, onProgress ) {\n\n\t\tconst scope = this;\n\n\t\tconst path = ( this.path === '' ) ? LoaderUtils.extractUrlBase( url ) : this.path;\n\t\tthis.resourcePath = this.resourcePath || path;\n\n\t\tconst loader = new FileLoader( this.manager );\n\t\tloader.setPath( this.path );\n\t\tloader.setRequestHeader( this.requestHeader );\n\t\tloader.setWithCredentials( this.withCredentials );\n\n\t\tconst text = await loader.loadAsync( url, onProgress );\n\n\t\tconst json = JSON.parse( text );\n\n\t\tconst metadata = json.metadata;\n\n\t\tif ( metadata === undefined || metadata.type === undefined || metadata.type.toLowerCase() === 'geometry' ) {\n\n\t\t\tthrow new Error( 'THREE.ObjectLoader: Can\\'t load ' + url );\n\n\t\t}\n\n\t\treturn await scope.parseAsync( json );\n\n\t}\n\n\t/**\n\t * Parses the given JSON. This is used internally by {@link ObjectLoader#load}\n\t * but can also be used directly to parse a previously loaded JSON structure.\n\t *\n\t * @param {Object} json - The serialized 3D object.\n\t * @param {onLoad} onLoad - Executed when all resources (e.g. textures) have been fully loaded.\n\t * @return {Object3D} The parsed 3D object.\n\t */\n\tparse( json, onLoad ) {\n\n\t\tconst animations = this.parseAnimations( json.animations );\n\t\tconst shapes = this.parseShapes( json.shapes );\n\t\tconst geometries = this.parseGeometries( json.geometries, shapes );\n\n\t\tconst images = this.parseImages( json.images, function () {\n\n\t\t\tif ( onLoad !== undefined ) onLoad( object );\n\n\t\t} );\n\n\t\tconst textures = this.parseTextures( json.textures, images );\n\t\tconst materials = this.parseMaterials( json.materials, textures );\n\n\t\tconst object = this.parseObject( json.object, geometries, materials, textures, animations );\n\t\tconst skeletons = this.parseSkeletons( json.skeletons, object );\n\n\t\tthis.bindSkeletons( object, skeletons );\n\t\tthis.bindLightTargets( object );\n\n\t\t//\n\n\t\tif ( onLoad !== undefined ) {\n\n\t\t\tlet hasImages = false;\n\n\t\t\tfor ( const uuid in images ) {\n\n\t\t\t\tif ( images[ uuid ].data instanceof HTMLImageElement ) {\n\n\t\t\t\t\thasImages = true;\n\t\t\t\t\tbreak;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( hasImages === false ) onLoad( object );\n\n\t\t}\n\n\t\treturn object;\n\n\t}\n\n\t/**\n\t * Async version of {@link ObjectLoader#parse}.\n\t *\n\t * @param {Object} json - The serialized 3D object.\n\t * @return {Promise<Object3D>} A Promise that resolves with the parsed 3D object.\n\t */\n\tasync parseAsync( json ) {\n\n\t\tconst animations = this.parseAnimations( json.animations );\n\t\tconst shapes = this.parseShapes( json.shapes );\n\t\tconst geometries = this.parseGeometries( json.geometries, shapes );\n\n\t\tconst images = await this.parseImagesAsync( json.images );\n\n\t\tconst textures = this.parseTextures( json.textures, images );\n\t\tconst materials = this.parseMaterials( json.materials, textures );\n\n\t\tconst object = this.parseObject( json.object, geometries, materials, textures, animations );\n\t\tconst skeletons = this.parseSkeletons( json.skeletons, object );\n\n\t\tthis.bindSkeletons( object, skeletons );\n\t\tthis.bindLightTargets( object );\n\n\t\treturn object;\n\n\t}\n\n\t// internals\n\n\tparseShapes( json ) {\n\n\t\tconst shapes = {};\n\n\t\tif ( json !== undefined ) {\n\n\t\t\tfor ( let i = 0, l = json.length; i < l; i ++ ) {\n\n\t\t\t\tconst shape = new Shape().fromJSON( json[ i ] );\n\n\t\t\t\tshapes[ shape.uuid ] = shape;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn shapes;\n\n\t}\n\n\tparseSkeletons( json, object ) {\n\n\t\tconst skeletons = {};\n\t\tconst bones = {};\n\n\t\t// generate bone lookup table\n\n\t\tobject.traverse( function ( child ) {\n\n\t\t\tif ( child.isBone ) bones[ child.uuid ] = child;\n\n\t\t} );\n\n\t\t// create skeletons\n\n\t\tif ( json !== undefined ) {\n\n\t\t\tfor ( let i = 0, l = json.length; i < l; i ++ ) {\n\n\t\t\t\tconst skeleton = new Skeleton().fromJSON( json[ i ], bones );\n\n\t\t\t\tskeletons[ skeleton.uuid ] = skeleton;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn skeletons;\n\n\t}\n\n\tparseGeometries( json, shapes ) {\n\n\t\tconst geometries = {};\n\n\t\tif ( json !== undefined ) {\n\n\t\t\tconst bufferGeometryLoader = new BufferGeometryLoader();\n\n\t\t\tfor ( let i = 0, l = json.length; i < l; i ++ ) {\n\n\t\t\t\tlet geometry;\n\t\t\t\tconst data = json[ i ];\n\n\t\t\t\tswitch ( data.type ) {\n\n\t\t\t\t\tcase 'BufferGeometry':\n\t\t\t\t\tcase 'InstancedBufferGeometry':\n\n\t\t\t\t\t\tgeometry = bufferGeometryLoader.parse( data );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tdefault:\n\n\t\t\t\t\t\tif ( data.type in Geometries ) {\n\n\t\t\t\t\t\t\tgeometry = Geometries[ data.type ].fromJSON( data, shapes );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\twarn( `ObjectLoader: Unsupported geometry type \"${ data.type }\"` );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tgeometry.uuid = data.uuid;\n\n\t\t\t\tif ( data.name !== undefined ) geometry.name = data.name;\n\t\t\t\tif ( data.userData !== undefined ) geometry.userData = data.userData;\n\n\t\t\t\tgeometries[ data.uuid ] = geometry;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn geometries;\n\n\t}\n\n\tparseMaterials( json, textures ) {\n\n\t\tconst cache = {}; // MultiMaterial\n\t\tconst materials = {};\n\n\t\tif ( json !== undefined ) {\n\n\t\t\tconst loader = new MaterialLoader();\n\t\t\tloader.setTextures( textures );\n\n\t\t\tfor ( let i = 0, l = json.length; i < l; i ++ ) {\n\n\t\t\t\tconst data = json[ i ];\n\n\t\t\t\tif ( cache[ data.uuid ] === undefined ) {\n\n\t\t\t\t\tcache[ data.uuid ] = loader.parse( data );\n\n\t\t\t\t}\n\n\t\t\t\tmaterials[ data.uuid ] = cache[ data.uuid ];\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn materials;\n\n\t}\n\n\tparseAnimations( json ) {\n\n\t\tconst animations = {};\n\n\t\tif ( json !== undefined ) {\n\n\t\t\tfor ( let i = 0; i < json.length; i ++ ) {\n\n\t\t\t\tconst data = json[ i ];\n\n\t\t\t\tconst clip = AnimationClip.parse( data );\n\n\t\t\t\tanimations[ clip.uuid ] = clip;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn animations;\n\n\t}\n\n\tparseImages( json, onLoad ) {\n\n\t\tconst scope = this;\n\t\tconst images = {};\n\n\t\tlet loader;\n\n\t\tfunction loadImage( url ) {\n\n\t\t\tscope.manager.itemStart( url );\n\n\t\t\treturn loader.load( url, function () {\n\n\t\t\t\tscope.manager.itemEnd( url );\n\n\t\t\t}, undefined, function () {\n\n\t\t\t\tscope.manager.itemError( url );\n\t\t\t\tscope.manager.itemEnd( url );\n\n\t\t\t} );\n\n\t\t}\n\n\t\tfunction deserializeImage( image ) {\n\n\t\t\tif ( typeof image === 'string' ) {\n\n\t\t\t\tconst url = image;\n\n\t\t\t\tconst path = /^(\\/\\/)|([a-z]+:(\\/\\/)?)/i.test( url ) ? url : scope.resourcePath + url;\n\n\t\t\t\treturn loadImage( path );\n\n\t\t\t} else {\n\n\t\t\t\tif ( image.data ) {\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: getTypedArray( image.type, image.data ),\n\t\t\t\t\t\twidth: image.width,\n\t\t\t\t\t\theight: image.height\n\t\t\t\t\t};\n\n\t\t\t\t} else {\n\n\t\t\t\t\treturn null;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( json !== undefined && json.length > 0 ) {\n\n\t\t\tconst manager = new LoadingManager( onLoad );\n\n\t\t\tloader = new ImageLoader( manager );\n\t\t\tloader.setCrossOrigin( this.crossOrigin );\n\n\t\t\tfor ( let i = 0, il = json.length; i < il; i ++ ) {\n\n\t\t\t\tconst image = json[ i ];\n\t\t\t\tconst url = image.url;\n\n\t\t\t\tif ( Array.isArray( url ) ) {\n\n\t\t\t\t\t// load array of images e.g CubeTexture\n\n\t\t\t\t\tconst imageArray = [];\n\n\t\t\t\t\tfor ( let j = 0, jl = url.length; j < jl; j ++ ) {\n\n\t\t\t\t\t\tconst currentUrl = url[ j ];\n\n\t\t\t\t\t\tconst deserializedImage = deserializeImage( currentUrl );\n\n\t\t\t\t\t\tif ( deserializedImage !== null ) {\n\n\t\t\t\t\t\t\tif ( deserializedImage instanceof HTMLImageElement ) {\n\n\t\t\t\t\t\t\t\timageArray.push( deserializedImage );\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t// special case: handle array of data textures for cube textures\n\n\t\t\t\t\t\t\t\timageArray.push( new DataTexture( deserializedImage.data, deserializedImage.width, deserializedImage.height ) );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\timages[ image.uuid ] = new Source( imageArray );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// load single image\n\n\t\t\t\t\tconst deserializedImage = deserializeImage( image.url );\n\t\t\t\t\timages[ image.uuid ] = new Source( deserializedImage );\n\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn images;\n\n\t}\n\n\tasync parseImagesAsync( json ) {\n\n\t\tconst scope = this;\n\t\tconst images = {};\n\n\t\tlet loader;\n\n\t\tasync function deserializeImage( image ) {\n\n\t\t\tif ( typeof image === 'string' ) {\n\n\t\t\t\tconst url = image;\n\n\t\t\t\tconst path = /^(\\/\\/)|([a-z]+:(\\/\\/)?)/i.test( url ) ? url : scope.resourcePath + url;\n\n\t\t\t\treturn await loader.loadAsync( path );\n\n\t\t\t} else {\n\n\t\t\t\tif ( image.data ) {\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: getTypedArray( image.type, image.data ),\n\t\t\t\t\t\twidth: image.width,\n\t\t\t\t\t\theight: image.height\n\t\t\t\t\t};\n\n\t\t\t\t} else {\n\n\t\t\t\t\treturn null;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( json !== undefined && json.length > 0 ) {\n\n\t\t\tloader = new ImageLoader( this.manager );\n\t\t\tloader.setCrossOrigin( this.crossOrigin );\n\n\t\t\tfor ( let i = 0, il = json.length; i < il; i ++ ) {\n\n\t\t\t\tconst image = json[ i ];\n\t\t\t\tconst url = image.url;\n\n\t\t\t\tif ( Array.isArray( url ) ) {\n\n\t\t\t\t\t// load array of images e.g CubeTexture\n\n\t\t\t\t\tconst imageArray = [];\n\n\t\t\t\t\tfor ( let j = 0, jl = url.length; j < jl; j ++ ) {\n\n\t\t\t\t\t\tconst currentUrl = url[ j ];\n\n\t\t\t\t\t\tconst deserializedImage = await deserializeImage( currentUrl );\n\n\t\t\t\t\t\tif ( deserializedImage !== null ) {\n\n\t\t\t\t\t\t\tif ( deserializedImage instanceof HTMLImageElement ) {\n\n\t\t\t\t\t\t\t\timageArray.push( deserializedImage );\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t// special case: handle array of data textures for cube textures\n\n\t\t\t\t\t\t\t\timageArray.push( new DataTexture( deserializedImage.data, deserializedImage.width, deserializedImage.height ) );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\timages[ image.uuid ] = new Source( imageArray );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// load single image\n\n\t\t\t\t\tconst deserializedImage = await deserializeImage( image.url );\n\t\t\t\t\timages[ image.uuid ] = new Source( deserializedImage );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn images;\n\n\t}\n\n\tparseTextures( json, images ) {\n\n\t\tfunction parseConstant( value, type ) {\n\n\t\t\tif ( typeof value === 'number' ) return value;\n\n\t\t\twarn( 'ObjectLoader.parseTexture: Constant should be in numeric form.', value );\n\n\t\t\treturn type[ value ];\n\n\t\t}\n\n\t\tconst textures = {};\n\n\t\tif ( json !== undefined ) {\n\n\t\t\tfor ( let i = 0, l = json.length; i < l; i ++ ) {\n\n\t\t\t\tconst data = json[ i ];\n\n\t\t\t\tif ( data.image === undefined ) {\n\n\t\t\t\t\twarn( 'ObjectLoader: No \"image\" specified for', data.uuid );\n\n\t\t\t\t}\n\n\t\t\t\tif ( images[ data.image ] === undefined ) {\n\n\t\t\t\t\twarn( 'ObjectLoader: Undefined image', data.image );\n\n\t\t\t\t}\n\n\t\t\t\tconst source = images[ data.image ];\n\t\t\t\tconst image = source.data;\n\n\t\t\t\tlet texture;\n\n\t\t\t\tif ( Array.isArray( image ) ) {\n\n\t\t\t\t\ttexture = new CubeTexture();\n\n\t\t\t\t\tif ( image.length === 6 ) texture.needsUpdate = true;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( image && image.data ) {\n\n\t\t\t\t\t\ttexture = new DataTexture();\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\ttexture = new Texture();\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( image ) texture.needsUpdate = true; // textures can have undefined image data\n\n\t\t\t\t}\n\n\t\t\t\ttexture.source = source;\n\n\t\t\t\ttexture.uuid = data.uuid;\n\n\t\t\t\tif ( data.name !== undefined ) texture.name = data.name;\n\n\t\t\t\tif ( data.mapping !== undefined ) texture.mapping = parseConstant( data.mapping, TEXTURE_MAPPING );\n\t\t\t\tif ( data.channel !== undefined ) texture.channel = data.channel;\n\n\t\t\t\tif ( data.offset !== undefined ) texture.offset.fromArray( data.offset );\n\t\t\t\tif ( data.repeat !== undefined ) texture.repeat.fromArray( data.repeat );\n\t\t\t\tif ( data.center !== undefined ) texture.center.fromArray( data.center );\n\t\t\t\tif ( data.rotation !== undefined ) texture.rotation = data.rotation;\n\n\t\t\t\tif ( data.wrap !== undefined ) {\n\n\t\t\t\t\ttexture.wrapS = parseConstant( data.wrap[ 0 ], TEXTURE_WRAPPING );\n\t\t\t\t\ttexture.wrapT = parseConstant( data.wrap[ 1 ], TEXTURE_WRAPPING );\n\n\t\t\t\t}\n\n\t\t\t\tif ( data.format !== undefined ) texture.format = data.format;\n\t\t\t\tif ( data.internalFormat !== undefined ) texture.internalFormat = data.internalFormat;\n\t\t\t\tif ( data.type !== undefined ) texture.type = data.type;\n\t\t\t\tif ( data.colorSpace !== undefined ) texture.colorSpace = data.colorSpace;\n\n\t\t\t\tif ( data.minFilter !== undefined ) texture.minFilter = parseConstant( data.minFilter, TEXTURE_FILTER );\n\t\t\t\tif ( data.magFilter !== undefined ) texture.magFilter = parseConstant( data.magFilter, TEXTURE_FILTER );\n\t\t\t\tif ( data.anisotropy !== undefined ) texture.anisotropy = data.anisotropy;\n\n\t\t\t\tif ( data.flipY !== undefined ) texture.flipY = data.flipY;\n\n\t\t\t\tif ( data.generateMipmaps !== undefined ) texture.generateMipmaps = data.generateMipmaps;\n\t\t\t\tif ( data.premultiplyAlpha !== undefined ) texture.premultiplyAlpha = data.premultiplyAlpha;\n\t\t\t\tif ( data.unpackAlignment !== undefined ) texture.unpackAlignment = data.unpackAlignment;\n\t\t\t\tif ( data.compareFunction !== undefined ) texture.compareFunction = data.compareFunction;\n\n\t\t\t\tif ( data.userData !== undefined ) texture.userData = data.userData;\n\n\t\t\t\ttextures[ data.uuid ] = texture;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn textures;\n\n\t}\n\n\tparseObject( data, geometries, materials, textures, animations ) {\n\n\t\tlet object;\n\n\t\tfunction getGeometry( name ) {\n\n\t\t\tif ( geometries[ name ] === undefined ) {\n\n\t\t\t\twarn( 'ObjectLoader: Undefined geometry', name );\n\n\t\t\t}\n\n\t\t\treturn geometries[ name ];\n\n\t\t}\n\n\t\tfunction getMaterial( name ) {\n\n\t\t\tif ( name === undefined ) return undefined;\n\n\t\t\tif ( Array.isArray( name ) ) {\n\n\t\t\t\tconst array = [];\n\n\t\t\t\tfor ( let i = 0, l = name.length; i < l; i ++ ) {\n\n\t\t\t\t\tconst uuid = name[ i ];\n\n\t\t\t\t\tif ( materials[ uuid ] === undefined ) {\n\n\t\t\t\t\t\twarn( 'ObjectLoader: Undefined material', uuid );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tarray.push( materials[ uuid ] );\n\n\t\t\t\t}\n\n\t\t\t\treturn array;\n\n\t\t\t}\n\n\t\t\tif ( materials[ name ] === undefined ) {\n\n\t\t\t\twarn( 'ObjectLoader: Undefined material', name );\n\n\t\t\t}\n\n\t\t\treturn materials[ name ];\n\n\t\t}\n\n\t\tfunction getTexture( uuid ) {\n\n\t\t\tif ( textures[ uuid ] === undefined ) {\n\n\t\t\t\twarn( 'ObjectLoader: Undefined texture', uuid );\n\n\t\t\t}\n\n\t\t\treturn textures[ uuid ];\n\n\t\t}\n\n\t\tlet geometry, material;\n\n\t\tswitch ( data.type ) {\n\n\t\t\tcase 'Scene':\n\n\t\t\t\tobject = new Scene();\n\n\t\t\t\tif ( data.background !== undefined ) {\n\n\t\t\t\t\tif ( Number.isInteger( data.background ) ) {\n\n\t\t\t\t\t\tobject.background = new Color( data.background );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tobject.background = getTexture( data.background );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tif ( data.environment !== undefined ) {\n\n\t\t\t\t\tobject.environment = getTexture( data.environment );\n\n\t\t\t\t}\n\n\t\t\t\tif ( data.fog !== undefined ) {\n\n\t\t\t\t\tif ( data.fog.type === 'Fog' ) {\n\n\t\t\t\t\t\tobject.fog = new Fog( data.fog.color, data.fog.near, data.fog.far );\n\n\t\t\t\t\t} else if ( data.fog.type === 'FogExp2' ) {\n\n\t\t\t\t\t\tobject.fog = new FogExp2( data.fog.color, data.fog.density );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( data.fog.name !== '' ) {\n\n\t\t\t\t\t\tobject.fog.name = data.fog.name;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tif ( data.backgroundBlurriness !== undefined ) object.backgroundBlurriness = data.backgroundBlurriness;\n\t\t\t\tif ( data.backgroundIntensity !== undefined ) object.backgroundIntensity = data.backgroundIntensity;\n\t\t\t\tif ( data.backgroundRotation !== undefined ) object.backgroundRotation.fromArray( data.backgroundRotation );\n\n\t\t\t\tif ( data.environmentIntensity !== undefined ) object.environmentIntensity = data.environmentIntensity;\n\t\t\t\tif ( data.environmentRotation !== undefined ) object.environmentRotation.fromArray( data.environmentRotation );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'PerspectiveCamera':\n\n\t\t\t\tobject = new PerspectiveCamera( data.fov, data.aspect, data.near, data.far );\n\n\t\t\t\tif ( data.focus !== undefined ) object.focus = data.focus;\n\t\t\t\tif ( data.zoom !== undefined ) object.zoom = data.zoom;\n\t\t\t\tif ( data.filmGauge !== undefined ) object.filmGauge = data.filmGauge;\n\t\t\t\tif ( data.filmOffset !== undefined ) object.filmOffset = data.filmOffset;\n\t\t\t\tif ( data.view !== undefined ) object.view = Object.assign( {}, data.view );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'OrthographicCamera':\n\n\t\t\t\tobject = new OrthographicCamera( data.left, data.right, data.top, data.bottom, data.near, data.far );\n\n\t\t\t\tif ( data.zoom !== undefined ) object.zoom = data.zoom;\n\t\t\t\tif ( data.view !== undefined ) object.view = Object.assign( {}, data.view );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'AmbientLight':\n\n\t\t\t\tobject = new AmbientLight( data.color, data.intensity );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'DirectionalLight':\n\n\t\t\t\tobject = new DirectionalLight( data.color, data.intensity );\n\t\t\t\tobject.target = data.target || '';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'PointLight':\n\n\t\t\t\tobject = new PointLight( data.color, data.intensity, data.distance, data.decay );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'RectAreaLight':\n\n\t\t\t\tobject = new RectAreaLight( data.color, data.intensity, data.width, data.height );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'SpotLight':\n\n\t\t\t\tobject = new SpotLight( data.color, data.intensity, data.distance, data.angle, data.penumbra, data.decay );\n\t\t\t\tobject.target = data.target || '';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'HemisphereLight':\n\n\t\t\t\tobject = new HemisphereLight( data.color, data.groundColor, data.intensity );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'LightProbe':\n\n\t\t\t\tconst sh = new SphericalHarmonics3().fromArray( data.sh );\n\t\t\t\tobject = new LightProbe( sh, data.intensity );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'SkinnedMesh':\n\n\t\t\t\tgeometry = getGeometry( data.geometry );\n\t\t\t \tmaterial = getMaterial( data.material );\n\n\t\t\t\tobject = new SkinnedMesh( geometry, material );\n\n\t\t\t\tif ( data.bindMode !== undefined ) object.bindMode = data.bindMode;\n\t\t\t\tif ( data.bindMatrix !== undefined ) object.bindMatrix.fromArray( data.bindMatrix );\n\t\t\t\tif ( data.skeleton !== undefined ) object.skeleton = data.skeleton;\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'Mesh':\n\n\t\t\t\tgeometry = getGeometry( data.geometry );\n\t\t\t\tmaterial = getMaterial( data.material );\n\n\t\t\t\tobject = new Mesh( geometry, material );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'InstancedMesh':\n\n\t\t\t\tgeometry = getGeometry( data.geometry );\n\t\t\t\tmaterial = getMaterial( data.material );\n\t\t\t\tconst count = data.count;\n\t\t\t\tconst instanceMatrix = data.instanceMatrix;\n\t\t\t\tconst instanceColor = data.instanceColor;\n\n\t\t\t\tobject = new InstancedMesh( geometry, material, count );\n\t\t\t\tobject.instanceMatrix = new InstancedBufferAttribute( new Float32Array( instanceMatrix.array ), 16 );\n\t\t\t\tif ( instanceColor !== undefined ) object.instanceColor = new InstancedBufferAttribute( new Float32Array( instanceColor.array ), instanceColor.itemSize );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'BatchedMesh':\n\n\t\t\t\tgeometry = getGeometry( data.geometry );\n\t\t\t\tmaterial = getMaterial( data.material );\n\n\t\t\t\tobject = new BatchedMesh( data.maxInstanceCount, data.maxVertexCount, data.maxIndexCount, material );\n\t\t\t\tobject.geometry = geometry;\n\t\t\t\tobject.perObjectFrustumCulled = data.perObjectFrustumCulled;\n\t\t\t\tobject.sortObjects = data.sortObjects;\n\n\t\t\t\tobject._drawRanges = data.drawRanges;\n\t\t\t\tobject._reservedRanges = data.reservedRanges;\n\n\t\t\t\tobject._geometryInfo = data.geometryInfo.map( info => {\n\n\t\t\t\t\tlet box = null;\n\t\t\t\t\tlet sphere = null;\n\t\t\t\t\tif ( info.boundingBox !== undefined ) {\n\n\t\t\t\t\t\tbox = new Box3().fromJSON( info.boundingBox );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( info.boundingSphere !== undefined ) {\n\n\t\t\t\t\t\tsphere = new Sphere().fromJSON( info.boundingSphere );\n\n\t\t\t\t\t}\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...info,\n\t\t\t\t\t\tboundingBox: box,\n\t\t\t\t\t\tboundingSphere: sphere\n\t\t\t\t\t};\n\n\t\t\t\t} );\n\t\t\t\tobject._instanceInfo = data.instanceInfo;\n\n\t\t\t\tobject._availableInstanceIds = data._availableInstanceIds;\n\t\t\t\tobject._availableGeometryIds = data._availableGeometryIds;\n\n\t\t\t\tobject._nextIndexStart = data.nextIndexStart;\n\t\t\t\tobject._nextVertexStart = data.nextVertexStart;\n\t\t\t\tobject._geometryCount = data.geometryCount;\n\n\t\t\t\tobject._maxInstanceCount = data.maxInstanceCount;\n\t\t\t\tobject._maxVertexCount = data.maxVertexCount;\n\t\t\t\tobject._maxIndexCount = data.maxIndexCount;\n\n\t\t\t\tobject._geometryInitialized = data.geometryInitialized;\n\n\t\t\t\tobject._matricesTexture = getTexture( data.matricesTexture.uuid );\n\n\t\t\t\tobject._indirectTexture = getTexture( data.indirectTexture.uuid );\n\n\t\t\t\tif ( data.colorsTexture !== undefined ) {\n\n\t\t\t\t\tobject._colorsTexture = getTexture( data.colorsTexture.uuid );\n\n\t\t\t\t}\n\n\t\t\t\tif ( data.boundingSphere !== undefined ) {\n\n\t\t\t\t\tobject.boundingSphere = new Sphere().fromJSON( data.boundingSphere );\n\n\t\t\t\t}\n\n\t\t\t\tif ( data.boundingBox !== undefined ) {\n\n\t\t\t\t\tobject.boundingBox = new Box3().fromJSON( data.boundingBox );\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'LOD':\n\n\t\t\t\tobject = new LOD();\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'Line':\n\n\t\t\t\tobject = new Line( getGeometry( data.geometry ), getMaterial( data.material ) );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'LineLoop':\n\n\t\t\t\tobject = new LineLoop( getGeometry( data.geometry ), getMaterial( data.material ) );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'LineSegments':\n\n\t\t\t\tobject = new LineSegments( getGeometry( data.geometry ), getMaterial( data.material ) );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'PointCloud':\n\t\t\tcase 'Points':\n\n\t\t\t\tobject = new Points( getGeometry( data.geometry ), getMaterial( data.material ) );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'Sprite':\n\n\t\t\t\tobject = new Sprite( getMaterial( data.material ) );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'Group':\n\n\t\t\t\tobject = new Group();\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'Bone':\n\n\t\t\t\tobject = new Bone();\n\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\n\t\t\t\tobject = new Object3D();\n\n\t\t}\n\n\t\tobject.uuid = data.uuid;\n\n\t\tif ( data.name !== undefined ) object.name = data.name;\n\n\t\tif ( data.matrix !== undefined ) {\n\n\t\t\tobject.matrix.fromArray( data.matrix );\n\n\t\t\tif ( data.matrixAutoUpdate !== undefined ) object.matrixAutoUpdate = data.matrixAutoUpdate;\n\t\t\tif ( object.matrixAutoUpdate ) object.matrix.decompose( object.position, object.quaternion, object.scale );\n\n\t\t} else {\n\n\t\t\tif ( data.position !== undefined ) object.position.fromArray( data.position );\n\t\t\tif ( data.rotation !== undefined ) object.rotation.fromArray( data.rotation );\n\t\t\tif ( data.quaternion !== undefined ) object.quaternion.fromArray( data.quaternion );\n\t\t\tif ( data.scale !== undefined ) object.scale.fromArray( data.scale );\n\n\t\t}\n\n\t\tif ( data.up !== undefined ) object.up.fromArray( data.up );\n\n\t\tif ( data.castShadow !== undefined ) object.castShadow = data.castShadow;\n\t\tif ( data.receiveShadow !== undefined ) object.receiveShadow = data.receiveShadow;\n\n\t\tif ( data.shadow ) {\n\n\t\t\tif ( data.shadow.intensity !== undefined ) object.shadow.intensity = data.shadow.intensity;\n\t\t\tif ( data.shadow.bias !== undefined ) object.shadow.bias = data.shadow.bias;\n\t\t\tif ( data.shadow.normalBias !== undefined ) object.shadow.normalBias = data.shadow.normalBias;\n\t\t\tif ( data.shadow.radius !== undefined ) object.shadow.radius = data.shadow.radius;\n\t\t\tif ( data.shadow.mapSize !== undefined ) object.shadow.mapSize.fromArray( data.shadow.mapSize );\n\t\t\tif ( data.shadow.camera !== undefined ) object.shadow.camera = this.parseObject( data.shadow.camera );\n\n\t\t}\n\n\t\tif ( data.visible !== undefined ) object.visible = data.visible;\n\t\tif ( data.frustumCulled !== undefined ) object.frustumCulled = data.frustumCulled;\n\t\tif ( data.renderOrder !== undefined ) object.renderOrder = data.renderOrder;\n\t\tif ( data.userData !== undefined ) object.userData = data.userData;\n\t\tif ( data.layers !== undefined ) object.layers.mask = data.layers;\n\n\t\tif ( data.children !== undefined ) {\n\n\t\t\tconst children = data.children;\n\n\t\t\tfor ( let i = 0; i < children.length; i ++ ) {\n\n\t\t\t\tobject.add( this.parseObject( children[ i ], geometries, materials, textures, animations ) );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( data.animations !== undefined ) {\n\n\t\t\tconst objectAnimations = data.animations;\n\n\t\t\tfor ( let i = 0; i < objectAnimations.length; i ++ ) {\n\n\t\t\t\tconst uuid = objectAnimations[ i ];\n\n\t\t\t\tobject.animations.push( animations[ uuid ] );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( data.type === 'LOD' ) {\n\n\t\t\tif ( data.autoUpdate !== undefined ) object.autoUpdate = data.autoUpdate;\n\n\t\t\tconst levels = data.levels;\n\n\t\t\tfor ( let l = 0; l < levels.length; l ++ ) {\n\n\t\t\t\tconst level = levels[ l ];\n\t\t\t\tconst child = object.getObjectByProperty( 'uuid', level.object );\n\n\t\t\t\tif ( child !== undefined ) {\n\n\t\t\t\t\tobject.addLevel( child, level.distance, level.hysteresis );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn object;\n\n\t}\n\n\tbindSkeletons( object, skeletons ) {\n\n\t\tif ( Object.keys( skeletons ).length === 0 ) return;\n\n\t\tobject.traverse( function ( child ) {\n\n\t\t\tif ( child.isSkinnedMesh === true && child.skeleton !== undefined ) {\n\n\t\t\t\tconst skeleton = skeletons[ child.skeleton ];\n\n\t\t\t\tif ( skeleton === undefined ) {\n\n\t\t\t\t\twarn( 'ObjectLoader: No skeleton found with UUID:', child.skeleton );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tchild.bind( skeleton, child.bindMatrix );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} );\n\n\t}\n\n\tbindLightTargets( object ) {\n\n\t\tobject.traverse( function ( child ) {\n\n\t\t\tif ( child.isDirectionalLight || child.isSpotLight ) {\n\n\t\t\t\tconst uuid = child.target;\n\n\t\t\t\tconst target = object.getObjectByProperty( 'uuid', uuid );\n\n\t\t\t\tif ( target !== undefined ) {\n\n\t\t\t\t\tchild.target = target;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tchild.target = new Object3D();\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} );\n\n\t}\n\n}\n\nconst TEXTURE_MAPPING = {\n\tUVMapping: UVMapping,\n\tCubeReflectionMapping: CubeReflectionMapping,\n\tCubeRefractionMapping: CubeRefractionMapping,\n\tEquirectangularReflectionMapping: EquirectangularReflectionMapping,\n\tEquirectangularRefractionMapping: EquirectangularRefractionMapping,\n\tCubeUVReflectionMapping: CubeUVReflectionMapping\n};\n\nconst TEXTURE_WRAPPING = {\n\tRepeatWrapping: RepeatWrapping,\n\tClampToEdgeWrapping: ClampToEdgeWrapping,\n\tMirroredRepeatWrapping: MirroredRepeatWrapping\n};\n\nconst TEXTURE_FILTER = {\n\tNearestFilter: NearestFilter,\n\tNearestMipmapNearestFilter: NearestMipmapNearestFilter,\n\tNearestMipmapLinearFilter: NearestMipmapLinearFilter,\n\tLinearFilter: LinearFilter,\n\tLinearMipmapNearestFilter: LinearMipmapNearestFilter,\n\tLinearMipmapLinearFilter: LinearMipmapLinearFilter\n};\n\nconst _errorMap = new WeakMap();\n\n/**\n * A loader for loading images as an [ImageBitmap](https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap).\n * An `ImageBitmap` provides an asynchronous and resource efficient pathway to prepare\n * textures for rendering.\n *\n * Note that {@link Texture#flipY} and {@link Texture#premultiplyAlpha} are ignored with image bitmaps.\n * They needs these configuration on bitmap creation unlike regular images need them on uploading to GPU.\n *\n * You need to set the equivalent options via {@link ImageBitmapLoader#setOptions} instead.\n *\n * Also note that unlike {@link FileLoader}, this loader avoids multiple concurrent requests to the same URL only if `Cache` is enabled.\n *\n * ```js\n * const loader = new THREE.ImageBitmapLoader();\n * loader.setOptions( { imageOrientation: 'flipY' } ); // set options if needed\n * const imageBitmap = await loader.loadAsync( 'image.png' );\n *\n * const texture = new THREE.Texture( imageBitmap );\n * texture.needsUpdate = true;\n * ```\n *\n * @augments Loader\n */\nclass ImageBitmapLoader extends Loader {\n\n\t/**\n\t * Constructs a new image bitmap loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isImageBitmapLoader = true;\n\n\t\tif ( typeof createImageBitmap === 'undefined' ) {\n\n\t\t\twarn( 'ImageBitmapLoader: createImageBitmap() not supported.' );\n\n\t\t}\n\n\t\tif ( typeof fetch === 'undefined' ) {\n\n\t\t\twarn( 'ImageBitmapLoader: fetch() not supported.' );\n\n\t\t}\n\n\t\t/**\n\t\t * Represents the loader options.\n\t\t *\n\t\t * @type {Object}\n\t\t * @default {premultiplyAlpha:'none'}\n\t\t */\n\t\tthis.options = { premultiplyAlpha: 'none' };\n\n\t\t/**\n\t\t * Used for aborting requests.\n\t\t *\n\t\t * @private\n\t\t * @type {AbortController}\n\t\t */\n\t\tthis._abortController = new AbortController();\n\n\t}\n\n\t/**\n\t * Sets the given loader options. The structure of the object must match the `options` parameter of\n\t * [createImageBitmap](https://developer.mozilla.org/en-US/docs/Web/API/Window/createImageBitmap).\n\t *\n\t * @param {Object} options - The loader options to set.\n\t * @return {ImageBitmapLoader} A reference to this image bitmap loader.\n\t */\n\tsetOptions( options ) {\n\n\t\tthis.options = options;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and pass the loaded image bitmap to the `onLoad()` callback.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(ImageBitmap)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Unsupported in this loader.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t * @return {ImageBitmap|undefined} The image bitmap.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tif ( url === undefined ) url = '';\n\n\t\tif ( this.path !== undefined ) url = this.path + url;\n\n\t\turl = this.manager.resolveURL( url );\n\n\t\tconst scope = this;\n\n\t\tconst cached = Cache.get( `image-bitmap:${url}` );\n\n\t\tif ( cached !== undefined ) {\n\n\t\t\tscope.manager.itemStart( url );\n\n\t\t\t// If cached is a promise, wait for it to resolve\n\t\t\tif ( cached.then ) {\n\n\t\t\t\tcached.then( imageBitmap => {\n\n\t\t\t\t\t// check if there is an error for the cached promise\n\n\t\t\t\t\tif ( _errorMap.has( cached ) === true ) {\n\n\t\t\t\t\t\tif ( onError ) onError( _errorMap.get( cached ) );\n\n\t\t\t\t\t\tscope.manager.itemError( url );\n\t\t\t\t\t\tscope.manager.itemEnd( url );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tif ( onLoad ) onLoad( imageBitmap );\n\n\t\t\t\t\t\tscope.manager.itemEnd( url );\n\n\t\t\t\t\t\treturn imageBitmap;\n\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\t// If cached is not a promise (i.e., it's already an imageBitmap)\n\t\t\tsetTimeout( function () {\n\n\t\t\t\tif ( onLoad ) onLoad( cached );\n\n\t\t\t\tscope.manager.itemEnd( url );\n\n\t\t\t}, 0 );\n\n\t\t\treturn cached;\n\n\t\t}\n\n\t\tconst fetchOptions = {};\n\t\tfetchOptions.credentials = ( this.crossOrigin === 'anonymous' ) ? 'same-origin' : 'include';\n\t\tfetchOptions.headers = this.requestHeader;\n\t\tfetchOptions.signal = ( typeof AbortSignal.any === 'function' ) ? AbortSignal.any( [ this._abortController.signal, this.manager.abortController.signal ] ) : this._abortController.signal;\n\n\t\tconst promise = fetch( url, fetchOptions ).then( function ( res ) {\n\n\t\t\treturn res.blob();\n\n\t\t} ).then( function ( blob ) {\n\n\t\t\treturn createImageBitmap( blob, Object.assign( scope.options, { colorSpaceConversion: 'none' } ) );\n\n\t\t} ).then( function ( imageBitmap ) {\n\n\t\t\tCache.add( `image-bitmap:${url}`, imageBitmap );\n\n\t\t\tif ( onLoad ) onLoad( imageBitmap );\n\n\t\t\tscope.manager.itemEnd( url );\n\n\t\t\treturn imageBitmap;\n\n\t\t} ).catch( function ( e ) {\n\n\t\t\tif ( onError ) onError( e );\n\n\t\t\t_errorMap.set( promise, e );\n\n\t\t\tCache.remove( `image-bitmap:${url}` );\n\n\t\t\tscope.manager.itemError( url );\n\t\t\tscope.manager.itemEnd( url );\n\n\t\t} );\n\n\t\tCache.add( `image-bitmap:${url}`, promise );\n\t\tscope.manager.itemStart( url );\n\n\t}\n\n\t/**\n\t * Aborts ongoing fetch requests.\n\t *\n\t * @return {ImageBitmapLoader} A reference to this instance.\n\t */\n\tabort() {\n\n\t\tthis._abortController.abort();\n\t\tthis._abortController = new AbortController();\n\n\t\treturn this;\n\n\t}\n\n}\n\nlet _context;\n\n/**\n * Manages the global audio context in the engine.\n *\n * @hideconstructor\n */\nclass AudioContext {\n\n\t/**\n\t * Returns the global native audio context.\n\t *\n\t * @return {AudioContext} The native audio context.\n\t */\n\tstatic getContext() {\n\n\t\tif ( _context === undefined ) {\n\n\t\t\t_context = new ( window.AudioContext || window.webkitAudioContext )();\n\n\t\t}\n\n\t\treturn _context;\n\n\t}\n\n\t/**\n\t * Allows to set the global native audio context from outside.\n\t *\n\t * @param {AudioContext} value - The native context to set.\n\t */\n\tstatic setContext( value ) {\n\n\t\t_context = value;\n\n\t}\n\n}\n\n/**\n * Class for loading audio buffers. Audios are internally\n * loaded via {@link FileLoader}.\n *\n * ```js\n * const audioListener = new THREE.AudioListener();\n * const ambientSound = new THREE.Audio( audioListener );\n *\n * const loader = new THREE.AudioLoader();\n * const audioBuffer = await loader.loadAsync( 'audio/ambient_ocean.ogg' );\n *\n * ambientSound.setBuffer( audioBuffer );\n * ambientSound.play();\n * ```\n *\n * @augments Loader\n */\nclass AudioLoader extends Loader {\n\n\t/**\n\t * Constructs a new audio loader.\n\t *\n\t * @param {LoadingManager} [manager] - The loading manager.\n\t */\n\tconstructor( manager ) {\n\n\t\tsuper( manager );\n\n\t}\n\n\t/**\n\t * Starts loading from the given URL and passes the loaded audio buffer\n\t * to the `onLoad()` callback.\n\t *\n\t * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.\n\t * @param {function(AudioBuffer)} onLoad - Executed when the loading process has been finished.\n\t * @param {onProgressCallback} onProgress - Executed while the loading is in progress.\n\t * @param {onErrorCallback} onError - Executed when errors occur.\n\t */\n\tload( url, onLoad, onProgress, onError ) {\n\n\t\tconst scope = this;\n\n\t\tconst loader = new FileLoader( this.manager );\n\t\tloader.setResponseType( 'arraybuffer' );\n\t\tloader.setPath( this.path );\n\t\tloader.setRequestHeader( this.requestHeader );\n\t\tloader.setWithCredentials( this.withCredentials );\n\t\tloader.load( url, function ( buffer ) {\n\n\t\t\ttry {\n\n\t\t\t\t// Create a copy of the buffer. The `decodeAudioData` method\n\t\t\t\t// detaches the buffer when complete, preventing reuse.\n\t\t\t\tconst bufferCopy = buffer.slice( 0 );\n\n\t\t\t\tconst context = AudioContext.getContext();\n\t\t\t\tcontext.decodeAudioData( bufferCopy, function ( audioBuffer ) {\n\n\t\t\t\t\tonLoad( audioBuffer );\n\n\t\t\t\t} ).catch( handleError );\n\n\t\t\t} catch ( e ) {\n\n\t\t\t\thandleError( e );\n\n\t\t\t}\n\n\t\t}, onProgress, onError );\n\n\t\tfunction handleError( e ) {\n\n\t\t\tif ( onError ) {\n\n\t\t\t\tonError( e );\n\n\t\t\t} else {\n\n\t\t\t\terror( e );\n\n\t\t\t}\n\n\t\t\tscope.manager.itemError( url );\n\n\t\t}\n\n\t}\n\n}\n\nconst _eyeRight = /*@__PURE__*/ new Matrix4();\nconst _eyeLeft = /*@__PURE__*/ new Matrix4();\nconst _projectionMatrix = /*@__PURE__*/ new Matrix4();\n\n/**\n * A special type of camera that uses two perspective cameras with\n * stereoscopic projection. Can be used for rendering stereo effects\n * like [3D Anaglyph](https://en.wikipedia.org/wiki/Anaglyph_3D) or\n * [Parallax Barrier](https://en.wikipedia.org/wiki/parallax_barrier).\n */\nclass StereoCamera {\n\n\t/**\n\t * Constructs a new stereo camera.\n\t */\n\tconstructor() {\n\n\t\t/**\n\t\t * The type property is used for detecting the object type\n\t\t * in context of serialization/deserialization.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.type = 'StereoCamera';\n\n\t\t/**\n\t\t * The aspect.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.aspect = 1;\n\n\t\t/**\n\t\t * The eye separation which represents the distance\n\t\t * between the left and right camera.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0.064\n\t\t */\n\t\tthis.eyeSep = 0.064;\n\n\t\t/**\n\t\t * The camera representing the left eye. This is added to layer `1` so objects to be\n\t\t * rendered by the left camera must also be added to this layer.\n\t\t *\n\t\t * @type {PerspectiveCamera}\n\t\t */\n\t\tthis.cameraL = new PerspectiveCamera();\n\t\tthis.cameraL.layers.enable( 1 );\n\t\tthis.cameraL.matrixAutoUpdate = false;\n\n\t\t/**\n\t\t * The camera representing the right eye. This is added to layer `2` so objects to be\n\t\t * rendered by the right camera must also be added to this layer.\n\t\t *\n\t\t * @type {PerspectiveCamera}\n\t\t */\n\t\tthis.cameraR = new PerspectiveCamera();\n\t\tthis.cameraR.layers.enable( 2 );\n\t\tthis.cameraR.matrixAutoUpdate = false;\n\n\t\tthis._cache = {\n\t\t\tfocus: null,\n\t\t\tfov: null,\n\t\t\taspect: null,\n\t\t\tnear: null,\n\t\t\tfar: null,\n\t\t\tzoom: null,\n\t\t\teyeSep: null\n\t\t};\n\n\t}\n\n\t/**\n\t * Updates the stereo camera based on the given perspective camera.\n\t *\n\t * @param {PerspectiveCamera} camera - The perspective camera.\n\t */\n\tupdate( camera ) {\n\n\t\tconst cache = this._cache;\n\n\t\tconst needsUpdate = cache.focus !== camera.focus || cache.fov !== camera.fov ||\n\t\t\tcache.aspect !== camera.aspect * this.aspect || cache.near !== camera.near ||\n\t\t\tcache.far !== camera.far || cache.zoom !== camera.zoom || cache.eyeSep !== this.eyeSep;\n\n\t\tif ( needsUpdate ) {\n\n\t\t\tcache.focus = camera.focus;\n\t\t\tcache.fov = camera.fov;\n\t\t\tcache.aspect = camera.aspect * this.aspect;\n\t\t\tcache.near = camera.near;\n\t\t\tcache.far = camera.far;\n\t\t\tcache.zoom = camera.zoom;\n\t\t\tcache.eyeSep = this.eyeSep;\n\n\t\t\t// Off-axis stereoscopic effect based on\n\t\t\t// http://paulbourke.net/stereographics/stereorender/\n\n\t\t\t_projectionMatrix.copy( camera.projectionMatrix );\n\t\t\tconst eyeSepHalf = cache.eyeSep / 2;\n\t\t\tconst eyeSepOnProjection = eyeSepHalf * cache.near / cache.focus;\n\t\t\tconst ymax = ( cache.near * Math.tan( DEG2RAD * cache.fov * 0.5 ) ) / cache.zoom;\n\t\t\tlet xmin, xmax;\n\n\t\t\t// translate xOffset\n\n\t\t\t_eyeLeft.elements[ 12 ] = - eyeSepHalf;\n\t\t\t_eyeRight.elements[ 12 ] = eyeSepHalf;\n\n\t\t\t// for left eye\n\n\t\t\txmin = - ymax * cache.aspect + eyeSepOnProjection;\n\t\t\txmax = ymax * cache.aspect + eyeSepOnProjection;\n\n\t\t\t_projectionMatrix.elements[ 0 ] = 2 * cache.near / ( xmax - xmin );\n\t\t\t_projectionMatrix.elements[ 8 ] = ( xmax + xmin ) / ( xmax - xmin );\n\n\t\t\tthis.cameraL.projectionMatrix.copy( _projectionMatrix );\n\n\t\t\t// for right eye\n\n\t\t\txmin = - ymax * cache.aspect - eyeSepOnProjection;\n\t\t\txmax = ymax * cache.aspect - eyeSepOnProjection;\n\n\t\t\t_projectionMatrix.elements[ 0 ] = 2 * cache.near / ( xmax - xmin );\n\t\t\t_projectionMatrix.elements[ 8 ] = ( xmax + xmin ) / ( xmax - xmin );\n\n\t\t\tthis.cameraR.projectionMatrix.copy( _projectionMatrix );\n\n\t\t}\n\n\t\tthis.cameraL.matrixWorld.copy( camera.matrixWorld ).multiply( _eyeLeft );\n\t\tthis.cameraR.matrixWorld.copy( camera.matrixWorld ).multiply( _eyeRight );\n\n\t}\n\n}\n\n/**\n * This type of camera can be used in order to efficiently render a scene with a\n * predefined set of cameras. This is an important performance aspect for\n * rendering VR scenes.\n *\n * An instance of `ArrayCamera` always has an array of sub cameras. It's mandatory\n * to define for each sub camera the `viewport` property which determines the\n * part of the viewport that is rendered with this camera.\n *\n * @augments PerspectiveCamera\n */\nclass ArrayCamera extends PerspectiveCamera {\n\n\t/**\n\t * Constructs a new array camera.\n\t *\n\t * @param {Array<PerspectiveCamera>} [array=[]] - An array of perspective sub cameras.\n\t */\n\tconstructor( array = [] ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isArrayCamera = true;\n\n\t\t/**\n\t\t * Whether this camera is used with multiview rendering or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default false\n\t\t */\n\t\tthis.isMultiViewCamera = false;\n\n\t\t/**\n\t\t * An array of perspective sub cameras.\n\t\t *\n\t\t * @type {Array<PerspectiveCamera>}\n\t\t */\n\t\tthis.cameras = array;\n\n\t}\n\n}\n\n/**\n * Class for keeping track of time.\n */\nclass Clock {\n\n\t/**\n\t * Constructs a new clock.\n\t *\n\t * @param {boolean} [autoStart=true] - Whether to automatically start the clock when\n\t * `getDelta()` is called for the first time.\n\t */\n\tconstructor( autoStart = true ) {\n\n\t\t/**\n\t\t * If set to `true`, the clock starts automatically when `getDelta()` is called\n\t\t * for the first time.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.autoStart = autoStart;\n\n\t\t/**\n\t\t * Holds the time at which the clock's `start()` method was last called.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.startTime = 0;\n\n\t\t/**\n\t\t * Holds the time at which the clock's `start()`, `getElapsedTime()` or\n\t\t * `getDelta()` methods were last called.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.oldTime = 0;\n\n\t\t/**\n\t\t * Keeps track of the total time that the clock has been running.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.elapsedTime = 0;\n\n\t\t/**\n\t\t * Whether the clock is running or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.running = false;\n\n\t}\n\n\t/**\n\t * Starts the clock. When `autoStart` is set to `true`, the method is automatically\n\t * called by the class.\n\t */\n\tstart() {\n\n\t\tthis.startTime = performance.now();\n\n\t\tthis.oldTime = this.startTime;\n\t\tthis.elapsedTime = 0;\n\t\tthis.running = true;\n\n\t}\n\n\t/**\n\t * Stops the clock.\n\t */\n\tstop() {\n\n\t\tthis.getElapsedTime();\n\t\tthis.running = false;\n\t\tthis.autoStart = false;\n\n\t}\n\n\t/**\n\t * Returns the elapsed time in seconds.\n\t *\n\t * @return {number} The elapsed time.\n\t */\n\tgetElapsedTime() {\n\n\t\tthis.getDelta();\n\t\treturn this.elapsedTime;\n\n\t}\n\n\t/**\n\t * Returns the delta time in seconds.\n\t *\n\t * @return {number} The delta time.\n\t */\n\tgetDelta() {\n\n\t\tlet diff = 0;\n\n\t\tif ( this.autoStart && ! this.running ) {\n\n\t\t\tthis.start();\n\t\t\treturn 0;\n\n\t\t}\n\n\t\tif ( this.running ) {\n\n\t\t\tconst newTime = performance.now();\n\n\t\t\tdiff = ( newTime - this.oldTime ) / 1000;\n\t\t\tthis.oldTime = newTime;\n\n\t\t\tthis.elapsedTime += diff;\n\n\t\t}\n\n\t\treturn diff;\n\n\t}\n\n}\n\nconst _position$1 = /*@__PURE__*/ new Vector3();\nconst _quaternion$1 = /*@__PURE__*/ new Quaternion();\nconst _scale$1 = /*@__PURE__*/ new Vector3();\n\nconst _forward = /*@__PURE__*/ new Vector3();\nconst _up = /*@__PURE__*/ new Vector3();\n\n/**\n * The class represents a virtual listener of the all positional and non-positional audio effects\n * in the scene. A three.js application usually creates a single listener. It is a mandatory\n * constructor parameter for audios entities like {@link Audio} and {@link PositionalAudio}.\n *\n * In most cases, the listener object is a child of the camera. So the 3D transformation of the\n * camera represents the 3D transformation of the listener.\n *\n * @augments Object3D\n */\nclass AudioListener extends Object3D {\n\n\t/**\n\t * Constructs a new audio listener.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\tthis.type = 'AudioListener';\n\n\t\t/**\n\t\t * The native audio context.\n\t\t *\n\t\t * @type {AudioContext}\n\t\t * @readonly\n\t\t */\n\t\tthis.context = AudioContext.getContext();\n\n\t\t/**\n\t\t * The gain node used for volume control.\n\t\t *\n\t\t * @type {GainNode}\n\t\t * @readonly\n\t\t */\n\t\tthis.gain = this.context.createGain();\n\t\tthis.gain.connect( this.context.destination );\n\n\t\t/**\n\t\t * An optional filter.\n\t\t *\n\t\t * Defined via {@link AudioListener#setFilter}.\n\t\t *\n\t\t * @type {?AudioNode}\n\t\t * @default null\n\t\t * @readonly\n\t\t */\n\t\tthis.filter = null;\n\n\t\t/**\n\t\t * Time delta values required for `linearRampToValueAtTime()` usage.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t * @readonly\n\t\t */\n\t\tthis.timeDelta = 0;\n\n\t\t// private\n\n\t\tthis._clock = new Clock();\n\n\t}\n\n\t/**\n\t * Returns the listener's input node.\n\t *\n\t * This method is used by other audio nodes to connect to this listener.\n\t *\n\t * @return {GainNode} The input node.\n\t */\n\tgetInput() {\n\n\t\treturn this.gain;\n\n\t}\n\n\t/**\n\t * Removes the current filter from this listener.\n\t *\n\t * @return {AudioListener} A reference to this listener.\n\t */\n\tremoveFilter() {\n\n\t\tif ( this.filter !== null ) {\n\n\t\t\tthis.gain.disconnect( this.filter );\n\t\t\tthis.filter.disconnect( this.context.destination );\n\t\t\tthis.gain.connect( this.context.destination );\n\t\t\tthis.filter = null;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the current set filter.\n\t *\n\t * @return {?AudioNode} The filter.\n\t */\n\tgetFilter() {\n\n\t\treturn this.filter;\n\n\t}\n\n\t/**\n\t * Sets the given filter to this listener.\n\t *\n\t * @param {AudioNode} value - The filter to set.\n\t * @return {AudioListener} A reference to this listener.\n\t */\n\tsetFilter( value ) {\n\n\t\tif ( this.filter !== null ) {\n\n\t\t\tthis.gain.disconnect( this.filter );\n\t\t\tthis.filter.disconnect( this.context.destination );\n\n\t\t} else {\n\n\t\t\tthis.gain.disconnect( this.context.destination );\n\n\t\t}\n\n\t\tthis.filter = value;\n\t\tthis.gain.connect( this.filter );\n\t\tthis.filter.connect( this.context.destination );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the applications master volume.\n\t *\n\t * @return {number} The master volume.\n\t */\n\tgetMasterVolume() {\n\n\t\treturn this.gain.gain.value;\n\n\t}\n\n\t/**\n\t * Sets the applications master volume. This volume setting affects\n\t * all audio nodes in the scene.\n\t *\n\t * @param {number} value - The master volume to set.\n\t * @return {AudioListener} A reference to this listener.\n\t */\n\tsetMasterVolume( value ) {\n\n\t\tthis.gain.gain.setTargetAtTime( value, this.context.currentTime, 0.01 );\n\n\t\treturn this;\n\n\t}\n\n\tupdateMatrixWorld( force ) {\n\n\t\tsuper.updateMatrixWorld( force );\n\n\t\tconst listener = this.context.listener;\n\n\t\tthis.timeDelta = this._clock.getDelta();\n\n\t\tthis.matrixWorld.decompose( _position$1, _quaternion$1, _scale$1 );\n\n\t\t// the initial forward and up directions must be orthogonal\n\t\t_forward.set( 0, 0, -1 ).applyQuaternion( _quaternion$1 );\n\t\t_up.set( 0, 1, 0 ).applyQuaternion( _quaternion$1 );\n\n\t\tif ( listener.positionX ) {\n\n\t\t\t// code path for Chrome (see #14393)\n\n\t\t\tconst endTime = this.context.currentTime + this.timeDelta;\n\n\t\t\tlistener.positionX.linearRampToValueAtTime( _position$1.x, endTime );\n\t\t\tlistener.positionY.linearRampToValueAtTime( _position$1.y, endTime );\n\t\t\tlistener.positionZ.linearRampToValueAtTime( _position$1.z, endTime );\n\t\t\tlistener.forwardX.linearRampToValueAtTime( _forward.x, endTime );\n\t\t\tlistener.forwardY.linearRampToValueAtTime( _forward.y, endTime );\n\t\t\tlistener.forwardZ.linearRampToValueAtTime( _forward.z, endTime );\n\t\t\tlistener.upX.linearRampToValueAtTime( _up.x, endTime );\n\t\t\tlistener.upY.linearRampToValueAtTime( _up.y, endTime );\n\t\t\tlistener.upZ.linearRampToValueAtTime( _up.z, endTime );\n\n\t\t} else {\n\n\t\t\tlistener.setPosition( _position$1.x, _position$1.y, _position$1.z );\n\t\t\tlistener.setOrientation( _forward.x, _forward.y, _forward.z, _up.x, _up.y, _up.z );\n\n\t\t}\n\n\t}\n\n}\n\n/**\n * Represents a non-positional ( global ) audio object.\n *\n * This and related audio modules make use of the [Web Audio API](https://www.w3.org/TR/webaudio-1.1/).\n *\n * ```js\n * // create an AudioListener and add it to the camera\n * const listener = new THREE.AudioListener();\n * camera.add( listener );\n *\n * // create a global audio source\n * const sound = new THREE.Audio( listener );\n *\n * // load a sound and set it as the Audio object's buffer\n * const audioLoader = new THREE.AudioLoader();\n * audioLoader.load( 'sounds/ambient.ogg', function( buffer ) {\n * \tsound.setBuffer( buffer );\n * \tsound.setLoop( true );\n * \tsound.setVolume( 0.5 );\n * \tsound.play();\n * });\n * ```\n *\n * @augments Object3D\n */\nclass Audio extends Object3D {\n\n\t/**\n\t * Constructs a new audio.\n\t *\n\t * @param {AudioListener} listener - The global audio listener.\n\t */\n\tconstructor( listener ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'Audio';\n\n\t\t/**\n\t\t * The global audio listener.\n\t\t *\n\t\t * @type {AudioListener}\n\t\t * @readonly\n\t\t */\n\t\tthis.listener = listener;\n\n\t\t/**\n\t\t * The audio context.\n\t\t *\n\t\t * @type {AudioContext}\n\t\t * @readonly\n\t\t */\n\t\tthis.context = listener.context;\n\n\t\t/**\n\t\t * The gain node used for volume control.\n\t\t *\n\t\t * @type {GainNode}\n\t\t * @readonly\n\t\t */\n\t\tthis.gain = this.context.createGain();\n\t\tthis.gain.connect( listener.getInput() );\n\n\t\t/**\n\t\t * Whether to start playback automatically or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.autoplay = false;\n\n\t\t/**\n\t\t * A reference to an audio buffer.\n\t\t *\n\t\t * Defined via {@link Audio#setBuffer}.\n\t\t *\n\t\t * @type {?AudioBuffer}\n\t\t * @default null\n\t\t * @readonly\n\t\t */\n\t\tthis.buffer = null;\n\n\t\t/**\n\t\t * Modify pitch, measured in cents. +/- 100 is a semitone.\n\t\t * +/- 1200 is an octave.\n\t\t *\n\t\t * Defined via {@link Audio#setDetune}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t * @readonly\n\t\t */\n\t\tthis.detune = 0;\n\n\t\t/**\n\t\t * Whether the audio should loop or not.\n\t\t *\n\t\t * Defined via {@link Audio#setLoop}.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t * @readonly\n\t\t */\n\t\tthis.loop = false;\n\n\t\t/**\n\t\t * Defines where in the audio buffer the replay should\n\t\t * start, in seconds.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.loopStart = 0;\n\n\t\t/**\n\t\t * Defines where in the audio buffer the replay should\n\t\t * stop, in seconds.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.loopEnd = 0;\n\n\t\t/**\n\t\t * An offset to the time within the audio buffer the playback\n\t\t * should begin, in seconds.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.offset = 0;\n\n\t\t/**\n\t\t * Overrides the default duration of the audio.\n\t\t *\n\t\t * @type {undefined|number}\n\t\t * @default undefined\n\t\t */\n\t\tthis.duration = undefined;\n\n\t\t/**\n\t\t * The playback speed.\n\t\t *\n\t\t * Defined via {@link Audio#setPlaybackRate}.\n\t\t *\n\t\t * @type {number}\n\t\t * @readonly\n\t\t * @default 1\n\t\t */\n\t\tthis.playbackRate = 1;\n\n\t\t/**\n\t\t * Indicates whether the audio is playing or not.\n\t\t *\n\t\t * This flag will be automatically set when using {@link Audio#play},\n\t\t * {@link Audio#pause}, {@link Audio#stop}.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default false\n\t\t */\n\t\tthis.isPlaying = false;\n\n\t\t/**\n\t\t * Indicates whether the audio playback can be controlled\n\t\t * with method like {@link Audio#play} or {@link Audio#pause}.\n\t\t *\n\t\t * This flag will be automatically set when audio sources are\n\t\t * defined.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.hasPlaybackControl = true;\n\n\t\t/**\n\t\t * Holds a reference to the current audio source.\n\t\t *\n\t\t * The property is automatically by one of the `set*()` methods.\n\t\t *\n\t\t * @type {?AudioNode}\n\t\t * @readonly\n\t\t * @default null\n\t\t */\n\t\tthis.source = null;\n\n\t\t/**\n\t\t * Defines the source type.\n\t\t *\n\t\t * The property is automatically by one of the `set*()` methods.\n\t\t *\n\t\t * @type {('empty'|'audioNode'|'mediaNode'|'mediaStreamNode'|'buffer')}\n\t\t * @readonly\n\t\t * @default 'empty'\n\t\t */\n\t\tthis.sourceType = 'empty';\n\n\t\tthis._startedAt = 0;\n\t\tthis._progress = 0;\n\t\tthis._connected = false;\n\n\t\t/**\n\t\t * Can be used to apply a variety of low-order filters to create\n\t\t * more complex sound effects e.g. via `BiquadFilterNode`.\n\t\t *\n\t\t * The property is automatically set by {@link Audio#setFilters}.\n\t\t *\n\t\t * @type {Array<AudioNode>}\n\t\t * @readonly\n\t\t */\n\t\tthis.filters = [];\n\n\t}\n\n\t/**\n\t * Returns the output audio node.\n\t *\n\t * @return {GainNode} The output node.\n\t */\n\tgetOutput() {\n\n\t\treturn this.gain;\n\n\t}\n\n\t/**\n\t * Sets the given audio node as the source of this instance.\n\t *\n\t * {@link Audio#sourceType} is set to `audioNode` and {@link Audio#hasPlaybackControl} to `false`.\n\t *\n\t * @param {AudioNode} audioNode - The audio node like an instance of `OscillatorNode`.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetNodeSource( audioNode ) {\n\n\t\tthis.hasPlaybackControl = false;\n\t\tthis.sourceType = 'audioNode';\n\t\tthis.source = audioNode;\n\t\tthis.connect();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given media element as the source of this instance.\n\t *\n\t * {@link Audio#sourceType} is set to `mediaNode` and {@link Audio#hasPlaybackControl} to `false`.\n\t *\n\t * @param {HTMLMediaElement} mediaElement - The media element.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetMediaElementSource( mediaElement ) {\n\n\t\tthis.hasPlaybackControl = false;\n\t\tthis.sourceType = 'mediaNode';\n\t\tthis.source = this.context.createMediaElementSource( mediaElement );\n\t\tthis.connect();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given media stream as the source of this instance.\n\t *\n\t * {@link Audio#sourceType} is set to `mediaStreamNode` and {@link Audio#hasPlaybackControl} to `false`.\n\t *\n\t * @param {MediaStream} mediaStream - The media stream.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetMediaStreamSource( mediaStream ) {\n\n\t\tthis.hasPlaybackControl = false;\n\t\tthis.sourceType = 'mediaStreamNode';\n\t\tthis.source = this.context.createMediaStreamSource( mediaStream );\n\t\tthis.connect();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given audio buffer as the source of this instance.\n\t *\n\t * {@link Audio#sourceType} is set to `buffer` and {@link Audio#hasPlaybackControl} to `true`.\n\t *\n\t * @param {AudioBuffer} audioBuffer - The audio buffer.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetBuffer( audioBuffer ) {\n\n\t\tthis.buffer = audioBuffer;\n\t\tthis.sourceType = 'buffer';\n\n\t\tif ( this.autoplay ) this.play();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Starts the playback of the audio.\n\t *\n\t * Can only be used with compatible audio sources that allow playback control.\n\t *\n\t * @param {number} [delay=0] - The delay, in seconds, at which the audio should start playing.\n\t * @return {Audio|undefined} A reference to this instance.\n\t */\n\tplay( delay = 0 ) {\n\n\t\tif ( this.isPlaying === true ) {\n\n\t\t\twarn( 'Audio: Audio is already playing.' );\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( this.hasPlaybackControl === false ) {\n\n\t\t\twarn( 'Audio: this Audio has no playback control.' );\n\t\t\treturn;\n\n\t\t}\n\n\t\tthis._startedAt = this.context.currentTime + delay;\n\n\t\tconst source = this.context.createBufferSource();\n\t\tsource.buffer = this.buffer;\n\t\tsource.loop = this.loop;\n\t\tsource.loopStart = this.loopStart;\n\t\tsource.loopEnd = this.loopEnd;\n\t\tsource.onended = this.onEnded.bind( this );\n\t\tsource.start( this._startedAt, this._progress + this.offset, this.duration );\n\n\t\tthis.isPlaying = true;\n\n\t\tthis.source = source;\n\n\t\tthis.setDetune( this.detune );\n\t\tthis.setPlaybackRate( this.playbackRate );\n\n\t\treturn this.connect();\n\n\t}\n\n\t/**\n\t * Pauses the playback of the audio.\n\t *\n\t * Can only be used with compatible audio sources that allow playback control.\n\t *\n\t * @return {Audio|undefined} A reference to this instance.\n\t */\n\tpause() {\n\n\t\tif ( this.hasPlaybackControl === false ) {\n\n\t\t\twarn( 'Audio: this Audio has no playback control.' );\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( this.isPlaying === true ) {\n\n\t\t\t// update current progress\n\n\t\t\tthis._progress += Math.max( this.context.currentTime - this._startedAt, 0 ) * this.playbackRate;\n\n\t\t\tif ( this.loop === true ) {\n\n\t\t\t\t// ensure _progress does not exceed duration with looped audios\n\n\t\t\t\tthis._progress = this._progress % ( this.duration || this.buffer.duration );\n\n\t\t\t}\n\n\t\t\tthis.source.stop();\n\t\t\tthis.source.onended = null;\n\n\t\t\tthis.isPlaying = false;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Stops the playback of the audio.\n\t *\n\t * Can only be used with compatible audio sources that allow playback control.\n\t *\n\t * @param {number} [delay=0] - The delay, in seconds, at which the audio should stop playing.\n\t * @return {Audio|undefined} A reference to this instance.\n\t */\n\tstop( delay = 0 ) {\n\n\t\tif ( this.hasPlaybackControl === false ) {\n\n\t\t\twarn( 'Audio: this Audio has no playback control.' );\n\t\t\treturn;\n\n\t\t}\n\n\t\tthis._progress = 0;\n\n\t\tif ( this.source !== null ) {\n\n\t\t\tthis.source.stop( this.context.currentTime + delay );\n\t\t\tthis.source.onended = null;\n\n\t\t}\n\n\t\tthis.isPlaying = false;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Connects to the audio source. This is used internally on\n\t * initialisation and when setting / removing filters.\n\t *\n\t * @return {Audio} A reference to this instance.\n\t */\n\tconnect() {\n\n\t\tif ( this.filters.length > 0 ) {\n\n\t\t\tthis.source.connect( this.filters[ 0 ] );\n\n\t\t\tfor ( let i = 1, l = this.filters.length; i < l; i ++ ) {\n\n\t\t\t\tthis.filters[ i - 1 ].connect( this.filters[ i ] );\n\n\t\t\t}\n\n\t\t\tthis.filters[ this.filters.length - 1 ].connect( this.getOutput() );\n\n\t\t} else {\n\n\t\t\tthis.source.connect( this.getOutput() );\n\n\t\t}\n\n\t\tthis._connected = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Disconnects to the audio source. This is used internally on\n\t * initialisation and when setting / removing filters.\n\t *\n\t * @return {Audio|undefined} A reference to this instance.\n\t */\n\tdisconnect() {\n\n\t\tif ( this._connected === false ) {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( this.filters.length > 0 ) {\n\n\t\t\tthis.source.disconnect( this.filters[ 0 ] );\n\n\t\t\tfor ( let i = 1, l = this.filters.length; i < l; i ++ ) {\n\n\t\t\t\tthis.filters[ i - 1 ].disconnect( this.filters[ i ] );\n\n\t\t\t}\n\n\t\t\tthis.filters[ this.filters.length - 1 ].disconnect( this.getOutput() );\n\n\t\t} else {\n\n\t\t\tthis.source.disconnect( this.getOutput() );\n\n\t\t}\n\n\t\tthis._connected = false;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the current set filters.\n\t *\n\t * @return {Array<AudioNode>} The list of filters.\n\t */\n\tgetFilters() {\n\n\t\treturn this.filters;\n\n\t}\n\n\t/**\n\t * Sets an array of filters and connects them with the audio source.\n\t *\n\t * @param {Array<AudioNode>} [value] - A list of filters.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetFilters( value ) {\n\n\t\tif ( ! value ) value = [];\n\n\t\tif ( this._connected === true ) {\n\n\t\t\tthis.disconnect();\n\t\t\tthis.filters = value.slice();\n\t\t\tthis.connect();\n\n\t\t} else {\n\n\t\t\tthis.filters = value.slice();\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Defines the detuning of oscillation in cents.\n\t *\n\t * @param {number} value - The detuning of oscillation in cents.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetDetune( value ) {\n\n\t\tthis.detune = value;\n\n\t\tif ( this.isPlaying === true && this.source.detune !== undefined ) {\n\n\t\t\tthis.source.detune.setTargetAtTime( this.detune, this.context.currentTime, 0.01 );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the detuning of oscillation in cents.\n\t *\n\t * @return {number} The detuning of oscillation in cents.\n\t */\n\tgetDetune() {\n\n\t\treturn this.detune;\n\n\t}\n\n\t/**\n\t * Returns the first filter in the list of filters.\n\t *\n\t * @return {AudioNode|undefined} The first filter in the list of filters.\n\t */\n\tgetFilter() {\n\n\t\treturn this.getFilters()[ 0 ];\n\n\t}\n\n\t/**\n\t * Applies a single filter node to the audio.\n\t *\n\t * @param {AudioNode} [filter] - The filter to set.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetFilter( filter ) {\n\n\t\treturn this.setFilters( filter ? [ filter ] : [] );\n\n\t}\n\n\t/**\n\t * Sets the playback rate.\n\t *\n\t * Can only be used with compatible audio sources that allow playback control.\n\t *\n\t * @param {number} [value] - The playback rate to set.\n\t * @return {Audio|undefined} A reference to this instance.\n\t */\n\tsetPlaybackRate( value ) {\n\n\t\tif ( this.hasPlaybackControl === false ) {\n\n\t\t\twarn( 'Audio: this Audio has no playback control.' );\n\t\t\treturn;\n\n\t\t}\n\n\t\tthis.playbackRate = value;\n\n\t\tif ( this.isPlaying === true ) {\n\n\t\t\tthis.source.playbackRate.setTargetAtTime( this.playbackRate, this.context.currentTime, 0.01 );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the current playback rate.\n\n\t * @return {number} The playback rate.\n\t */\n\tgetPlaybackRate() {\n\n\t\treturn this.playbackRate;\n\n\t}\n\n\t/**\n\t * Automatically called when playback finished.\n\t */\n\tonEnded() {\n\n\t\tthis.isPlaying = false;\n\t\tthis._progress = 0;\n\n\t}\n\n\t/**\n\t * Returns the loop flag.\n\t *\n\t * Can only be used with compatible audio sources that allow playback control.\n\t *\n\t * @return {boolean} Whether the audio should loop or not.\n\t */\n\tgetLoop() {\n\n\t\tif ( this.hasPlaybackControl === false ) {\n\n\t\t\twarn( 'Audio: this Audio has no playback control.' );\n\t\t\treturn false;\n\n\t\t}\n\n\t\treturn this.loop;\n\n\t}\n\n\t/**\n\t * Sets the loop flag.\n\t *\n\t * Can only be used with compatible audio sources that allow playback control.\n\t *\n\t * @param {boolean} value - Whether the audio should loop or not.\n\t * @return {Audio|undefined} A reference to this instance.\n\t */\n\tsetLoop( value ) {\n\n\t\tif ( this.hasPlaybackControl === false ) {\n\n\t\t\twarn( 'Audio: this Audio has no playback control.' );\n\t\t\treturn;\n\n\t\t}\n\n\t\tthis.loop = value;\n\n\t\tif ( this.isPlaying === true ) {\n\n\t\t\tthis.source.loop = this.loop;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the loop start value which defines where in the audio buffer the replay should\n\t * start, in seconds.\n\t *\n\t * @param {number} value - The loop start value.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetLoopStart( value ) {\n\n\t\tthis.loopStart = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the loop end value which defines where in the audio buffer the replay should\n\t * stop, in seconds.\n\t *\n\t * @param {number} value - The loop end value.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetLoopEnd( value ) {\n\n\t\tthis.loopEnd = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the volume.\n\t *\n\t * @return {number} The volume.\n\t */\n\tgetVolume() {\n\n\t\treturn this.gain.gain.value;\n\n\t}\n\n\t/**\n\t * Sets the volume.\n\t *\n\t * @param {number} value - The volume to set.\n\t * @return {Audio} A reference to this instance.\n\t */\n\tsetVolume( value ) {\n\n\t\tthis.gain.gain.setTargetAtTime( value, this.context.currentTime, 0.01 );\n\n\t\treturn this;\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tif ( source.sourceType !== 'buffer' ) {\n\n\t\t\twarn( 'Audio: Audio source type cannot be copied.' );\n\n\t\t\treturn this;\n\n\t\t}\n\n\t\tthis.autoplay = source.autoplay;\n\n\t\tthis.buffer = source.buffer;\n\t\tthis.detune = source.detune;\n\t\tthis.loop = source.loop;\n\t\tthis.loopStart = source.loopStart;\n\t\tthis.loopEnd = source.loopEnd;\n\t\tthis.offset = source.offset;\n\t\tthis.duration = source.duration;\n\t\tthis.playbackRate = source.playbackRate;\n\t\tthis.hasPlaybackControl = source.hasPlaybackControl;\n\t\tthis.sourceType = source.sourceType;\n\n\t\tthis.filters = source.filters.slice();\n\n\t\treturn this;\n\n\t}\n\n\tclone( recursive ) {\n\n\t\treturn new this.constructor( this.listener ).copy( this, recursive );\n\n\t}\n\n}\n\nconst _position = /*@__PURE__*/ new Vector3();\nconst _quaternion = /*@__PURE__*/ new Quaternion();\nconst _scale = /*@__PURE__*/ new Vector3();\nconst _orientation = /*@__PURE__*/ new Vector3();\n\n/**\n * Represents a positional audio object.\n *\n * ```js\n * // create an AudioListener and add it to the camera\n * const listener = new THREE.AudioListener();\n * camera.add( listener );\n *\n * // create the PositionalAudio object (passing in the listener)\n * const sound = new THREE.PositionalAudio( listener );\n *\n * // load a sound and set it as the PositionalAudio object's buffer\n * const audioLoader = new THREE.AudioLoader();\n * audioLoader.load( 'sounds/song.ogg', function( buffer ) {\n * \tsound.setBuffer( buffer );\n * \tsound.setRefDistance( 20 );\n * \tsound.play();\n * });\n *\n * // create an object for the sound to play from\n * const sphere = new THREE.SphereGeometry( 20, 32, 16 );\n * const material = new THREE.MeshPhongMaterial( { color: 0xff2200 } );\n * const mesh = new THREE.Mesh( sphere, material );\n * scene.add( mesh );\n *\n * // finally add the sound to the mesh\n * mesh.add( sound );\n *\n * @augments Audio\n */\nclass PositionalAudio extends Audio {\n\n\t/**\n\t * Constructs a positional audio.\n\t *\n\t * @param {AudioListener} listener - The global audio listener.\n\t */\n\tconstructor( listener ) {\n\n\t\tsuper( listener );\n\n\t\t/**\n\t\t * The panner node represents the location, direction, and behavior of an audio\n\t\t * source in 3D space.\n\t\t *\n\t\t * @type {PannerNode}\n\t\t * @readonly\n\t\t */\n\t\tthis.panner = this.context.createPanner();\n\t\tthis.panner.panningModel = 'HRTF';\n\t\tthis.panner.connect( this.gain );\n\n\t}\n\n\tconnect() {\n\n\t\tsuper.connect();\n\n\t\tthis.panner.connect( this.gain );\n\n\t\treturn this;\n\n\t}\n\n\tdisconnect() {\n\n\t\tsuper.disconnect();\n\n\t\tthis.panner.disconnect( this.gain );\n\n\t\treturn this;\n\n\t}\n\n\tgetOutput() {\n\n\t\treturn this.panner;\n\n\t}\n\n\t/**\n\t * Returns the current reference distance.\n\t *\n\t * @return {number} The reference distance.\n\t */\n\tgetRefDistance() {\n\n\t\treturn this.panner.refDistance;\n\n\t}\n\n\t/**\n\t * Defines the reference distance for reducing volume as the audio source moves\n\t * further from the listener – i.e. the distance at which the volume reduction\n\t * starts taking effect.\n\t *\n\t * @param {number} value - The reference distance to set.\n\t * @return {PositionalAudio} A reference to this instance.\n\t */\n\tsetRefDistance( value ) {\n\n\t\tthis.panner.refDistance = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the current rolloff factor.\n\t *\n\t * @return {number} The rolloff factor.\n\t */\n\tgetRolloffFactor() {\n\n\t\treturn this.panner.rolloffFactor;\n\n\t}\n\n\t/**\n\t * Defines how quickly the volume is reduced as the source moves away from the listener.\n\t *\n\t * @param {number} value - The rolloff factor.\n\t * @return {PositionalAudio} A reference to this instance.\n\t */\n\tsetRolloffFactor( value ) {\n\n\t\tthis.panner.rolloffFactor = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the current distance model.\n\t *\n\t * @return {('linear'|'inverse'|'exponential')} The distance model.\n\t */\n\tgetDistanceModel() {\n\n\t\treturn this.panner.distanceModel;\n\n\t}\n\n\t/**\n\t * Defines which algorithm to use to reduce the volume of the audio source\n\t * as it moves away from the listener.\n\t *\n\t * Read [the spec](https://www.w3.org/TR/webaudio-1.1/#enumdef-distancemodeltype)\n\t * for more details.\n\t *\n\t * @param {('linear'|'inverse'|'exponential')} value - The distance model to set.\n\t * @return {PositionalAudio} A reference to this instance.\n\t */\n\tsetDistanceModel( value ) {\n\n\t\tthis.panner.distanceModel = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the current max distance.\n\t *\n\t * @return {number} The max distance.\n\t */\n\tgetMaxDistance() {\n\n\t\treturn this.panner.maxDistance;\n\n\t}\n\n\t/**\n\t * Defines the maximum distance between the audio source and the listener,\n\t * after which the volume is not reduced any further.\n\t *\n\t * This value is used only by the `linear` distance model.\n\t *\n\t * @param {number} value - The max distance.\n\t * @return {PositionalAudio} A reference to this instance.\n\t */\n\tsetMaxDistance( value ) {\n\n\t\tthis.panner.maxDistance = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the directional cone in which the audio can be listened.\n\t *\n\t * @param {number} coneInnerAngle - An angle, in degrees, of a cone inside of which there will be no volume reduction.\n\t * @param {number} coneOuterAngle - An angle, in degrees, of a cone outside of which the volume will be reduced by a constant value, defined by the `coneOuterGain` parameter.\n\t * @param {number} coneOuterGain - The amount of volume reduction outside the cone defined by the `coneOuterAngle`. When set to `0`, no sound can be heard.\n\t * @return {PositionalAudio} A reference to this instance.\n\t */\n\tsetDirectionalCone( coneInnerAngle, coneOuterAngle, coneOuterGain ) {\n\n\t\tthis.panner.coneInnerAngle = coneInnerAngle;\n\t\tthis.panner.coneOuterAngle = coneOuterAngle;\n\t\tthis.panner.coneOuterGain = coneOuterGain;\n\n\t\treturn this;\n\n\t}\n\n\tupdateMatrixWorld( force ) {\n\n\t\tsuper.updateMatrixWorld( force );\n\n\t\tif ( this.hasPlaybackControl === true && this.isPlaying === false ) return;\n\n\t\tthis.matrixWorld.decompose( _position, _quaternion, _scale );\n\n\t\t_orientation.set( 0, 0, 1 ).applyQuaternion( _quaternion );\n\n\t\tconst panner = this.panner;\n\n\t\tif ( panner.positionX ) {\n\n\t\t\t// code path for Chrome and Firefox (see #14393)\n\n\t\t\tconst endTime = this.context.currentTime + this.listener.timeDelta;\n\n\t\t\tpanner.positionX.linearRampToValueAtTime( _position.x, endTime );\n\t\t\tpanner.positionY.linearRampToValueAtTime( _position.y, endTime );\n\t\t\tpanner.positionZ.linearRampToValueAtTime( _position.z, endTime );\n\t\t\tpanner.orientationX.linearRampToValueAtTime( _orientation.x, endTime );\n\t\t\tpanner.orientationY.linearRampToValueAtTime( _orientation.y, endTime );\n\t\t\tpanner.orientationZ.linearRampToValueAtTime( _orientation.z, endTime );\n\n\t\t} else {\n\n\t\t\tpanner.setPosition( _position.x, _position.y, _position.z );\n\t\t\tpanner.setOrientation( _orientation.x, _orientation.y, _orientation.z );\n\n\t\t}\n\n\t}\n\n}\n\n/**\n * This class can be used to analyse audio data.\n *\n * ```js\n * // create an AudioListener and add it to the camera\n * const listener = new THREE.AudioListener();\n * camera.add( listener );\n *\n * // create an Audio source\n * const sound = new THREE.Audio( listener );\n *\n * // load a sound and set it as the Audio object's buffer\n * const audioLoader = new THREE.AudioLoader();\n * audioLoader.load( 'sounds/ambient.ogg', function( buffer ) {\n * \tsound.setBuffer( buffer );\n * \tsound.setLoop(true);\n * \tsound.setVolume(0.5);\n * \tsound.play();\n * });\n *\n * // create an AudioAnalyser, passing in the sound and desired fftSize\n * const analyser = new THREE.AudioAnalyser( sound, 32 );\n *\n * // get the average frequency of the sound\n * const data = analyser.getAverageFrequency();\n * ```\n */\nclass AudioAnalyser {\n\n\t/**\n\t * Constructs a new audio analyzer.\n\t *\n\t * @param {Audio} audio - The audio to analyze.\n\t * @param {number} [fftSize=2048] - The window size in samples that is used when performing a Fast Fourier Transform (FFT) to get frequency domain data.\n\t */\n\tconstructor( audio, fftSize = 2048 ) {\n\n\t\t/**\n\t\t * The global audio listener.\n\t\t *\n\t\t * @type {AnalyserNode}\n\t\t */\n\t\tthis.analyser = audio.context.createAnalyser();\n\t\tthis.analyser.fftSize = fftSize;\n\n\t\t/**\n\t\t * Holds the analyzed data.\n\t\t *\n\t\t * @type {Uint8Array}\n\t\t */\n\t\tthis.data = new Uint8Array( this.analyser.frequencyBinCount );\n\n\t\taudio.getOutput().connect( this.analyser );\n\n\t}\n\n\t/**\n\t * Returns an array with frequency data of the audio.\n\t *\n\t * Each item in the array represents the decibel value for a specific frequency.\n\t * The frequencies are spread linearly from 0 to 1/2 of the sample rate.\n\t * For example, for 48000 sample rate, the last item of the array will represent\n\t * the decibel value for 24000 Hz.\n\t *\n\t * @return {Uint8Array} The frequency data.\n\t */\n\tgetFrequencyData() {\n\n\t\tthis.analyser.getByteFrequencyData( this.data );\n\n\t\treturn this.data;\n\n\t}\n\n\t/**\n\t * Returns the average of the frequencies returned by {@link AudioAnalyser#getFrequencyData}.\n\t *\n\t * @return {number} The average frequency.\n\t */\n\tgetAverageFrequency() {\n\n\t\tlet value = 0;\n\t\tconst data = this.getFrequencyData();\n\n\t\tfor ( let i = 0; i < data.length; i ++ ) {\n\n\t\t\tvalue += data[ i ];\n\n\t\t}\n\n\t\treturn value / data.length;\n\n\t}\n\n}\n\n/**\n * Buffered scene graph property that allows weighted accumulation; used internally.\n */\nclass PropertyMixer {\n\n\t/**\n\t * Constructs a new property mixer.\n\t *\n\t * @param {PropertyBinding} binding - The property binding.\n\t * @param {string} typeName - The keyframe track type name.\n\t * @param {number} valueSize - The keyframe track value size.\n\t */\n\tconstructor( binding, typeName, valueSize ) {\n\n\t\t/**\n\t\t * The property binding.\n\t\t *\n\t\t * @type {PropertyBinding}\n\t\t */\n\t\tthis.binding = binding;\n\n\t\t/**\n\t\t * The keyframe track value size.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.valueSize = valueSize;\n\n\t\tlet mixFunction,\n\t\t\tmixFunctionAdditive,\n\t\t\tsetIdentity;\n\n\t\t// buffer layout: [ incoming | accu0 | accu1 | orig | addAccu | (optional work) ]\n\t\t//\n\t\t// interpolators can use .buffer as their .result\n\t\t// the data then goes to 'incoming'\n\t\t//\n\t\t// 'accu0' and 'accu1' are used frame-interleaved for\n\t\t// the cumulative result and are compared to detect\n\t\t// changes\n\t\t//\n\t\t// 'orig' stores the original state of the property\n\t\t//\n\t\t// 'add' is used for additive cumulative results\n\t\t//\n\t\t// 'work' is optional and is only present for quaternion types. It is used\n\t\t// to store intermediate quaternion multiplication results\n\n\t\tswitch ( typeName ) {\n\n\t\t\tcase 'quaternion':\n\t\t\t\tmixFunction = this._slerp;\n\t\t\t\tmixFunctionAdditive = this._slerpAdditive;\n\t\t\t\tsetIdentity = this._setAdditiveIdentityQuaternion;\n\n\t\t\t\tthis.buffer = new Float64Array( valueSize * 6 );\n\t\t\t\tthis._workIndex = 5;\n\t\t\t\tbreak;\n\n\t\t\tcase 'string':\n\t\t\tcase 'bool':\n\t\t\t\tmixFunction = this._select;\n\n\t\t\t\t// Use the regular mix function and for additive on these types,\n\t\t\t\t// additive is not relevant for non-numeric types\n\t\t\t\tmixFunctionAdditive = this._select;\n\n\t\t\t\tsetIdentity = this._setAdditiveIdentityOther;\n\n\t\t\t\tthis.buffer = new Array( valueSize * 5 );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tmixFunction = this._lerp;\n\t\t\t\tmixFunctionAdditive = this._lerpAdditive;\n\t\t\t\tsetIdentity = this._setAdditiveIdentityNumeric;\n\n\t\t\t\tthis.buffer = new Float64Array( valueSize * 5 );\n\n\t\t}\n\n\t\tthis._mixBufferRegion = mixFunction;\n\t\tthis._mixBufferRegionAdditive = mixFunctionAdditive;\n\t\tthis._setIdentity = setIdentity;\n\t\tthis._origIndex = 3;\n\t\tthis._addIndex = 4;\n\n\t\t/**\n\t\t * TODO\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.cumulativeWeight = 0;\n\n\t\t/**\n\t\t * TODO\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.cumulativeWeightAdditive = 0;\n\n\t\t/**\n\t\t * TODO\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.useCount = 0;\n\n\t\t/**\n\t\t * TODO\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.referenceCount = 0;\n\n\t}\n\n\t/**\n\t * Accumulates data in the `incoming` region into `accu<i>`.\n\t *\n\t * @param {number} accuIndex - The accumulation index.\n\t * @param {number} weight - The weight.\n\t */\n\taccumulate( accuIndex, weight ) {\n\n\t\t// note: happily accumulating nothing when weight = 0, the caller knows\n\t\t// the weight and shouldn't have made the call in the first place\n\n\t\tconst buffer = this.buffer,\n\t\t\tstride = this.valueSize,\n\t\t\toffset = accuIndex * stride + stride;\n\n\t\tlet currentWeight = this.cumulativeWeight;\n\n\t\tif ( currentWeight === 0 ) {\n\n\t\t\t// accuN := incoming * weight\n\n\t\t\tfor ( let i = 0; i !== stride; ++ i ) {\n\n\t\t\t\tbuffer[ offset + i ] = buffer[ i ];\n\n\t\t\t}\n\n\t\t\tcurrentWeight = weight;\n\n\t\t} else {\n\n\t\t\t// accuN := accuN + incoming * weight\n\n\t\t\tcurrentWeight += weight;\n\t\t\tconst mix = weight / currentWeight;\n\t\t\tthis._mixBufferRegion( buffer, offset, 0, mix, stride );\n\n\t\t}\n\n\t\tthis.cumulativeWeight = currentWeight;\n\n\t}\n\n\t/**\n\t * Accumulates data in the `incoming` region into `add`.\n\t *\n\t * @param {number} weight - The weight.\n\t */\n\taccumulateAdditive( weight ) {\n\n\t\tconst buffer = this.buffer,\n\t\t\tstride = this.valueSize,\n\t\t\toffset = stride * this._addIndex;\n\n\t\tif ( this.cumulativeWeightAdditive === 0 ) {\n\n\t\t\t// add = identity\n\n\t\t\tthis._setIdentity();\n\n\t\t}\n\n\t\t// add := add + incoming * weight\n\n\t\tthis._mixBufferRegionAdditive( buffer, offset, 0, weight, stride );\n\t\tthis.cumulativeWeightAdditive += weight;\n\n\t}\n\n\t/**\n\t * Applies the state of `accu<i>` to the binding when accus differ.\n\t *\n\t * @param {number} accuIndex - The accumulation index.\n\t */\n\tapply( accuIndex ) {\n\n\t\tconst stride = this.valueSize,\n\t\t\tbuffer = this.buffer,\n\t\t\toffset = accuIndex * stride + stride,\n\n\t\t\tweight = this.cumulativeWeight,\n\t\t\tweightAdditive = this.cumulativeWeightAdditive,\n\n\t\t\tbinding = this.binding;\n\n\t\tthis.cumulativeWeight = 0;\n\t\tthis.cumulativeWeightAdditive = 0;\n\n\t\tif ( weight < 1 ) {\n\n\t\t\t// accuN := accuN + original * ( 1 - cumulativeWeight )\n\n\t\t\tconst originalValueOffset = stride * this._origIndex;\n\n\t\t\tthis._mixBufferRegion(\n\t\t\t\tbuffer, offset, originalValueOffset, 1 - weight, stride );\n\n\t\t}\n\n\t\tif ( weightAdditive > 0 ) {\n\n\t\t\t// accuN := accuN + additive accuN\n\n\t\t\tthis._mixBufferRegionAdditive( buffer, offset, this._addIndex * stride, 1, stride );\n\n\t\t}\n\n\t\tfor ( let i = stride, e = stride + stride; i !== e; ++ i ) {\n\n\t\t\tif ( buffer[ i ] !== buffer[ i + stride ] ) {\n\n\t\t\t\t// value has changed -> update scene graph\n\n\t\t\t\tbinding.setValue( buffer, offset );\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Remembers the state of the bound property and copy it to both accus.\n\t */\n\tsaveOriginalState() {\n\n\t\tconst binding = this.binding;\n\n\t\tconst buffer = this.buffer,\n\t\t\tstride = this.valueSize,\n\n\t\t\toriginalValueOffset = stride * this._origIndex;\n\n\t\tbinding.getValue( buffer, originalValueOffset );\n\n\t\t// accu[0..1] := orig -- initially detect changes against the original\n\t\tfor ( let i = stride, e = originalValueOffset; i !== e; ++ i ) {\n\n\t\t\tbuffer[ i ] = buffer[ originalValueOffset + ( i % stride ) ];\n\n\t\t}\n\n\t\t// Add to identity for additive\n\t\tthis._setIdentity();\n\n\t\tthis.cumulativeWeight = 0;\n\t\tthis.cumulativeWeightAdditive = 0;\n\n\t}\n\n\t/**\n\t * Applies the state previously taken via {@link PropertyMixer#saveOriginalState} to the binding.\n\t */\n\trestoreOriginalState() {\n\n\t\tconst originalValueOffset = this.valueSize * 3;\n\t\tthis.binding.setValue( this.buffer, originalValueOffset );\n\n\t}\n\n\t// internals\n\n\t_setAdditiveIdentityNumeric() {\n\n\t\tconst startIndex = this._addIndex * this.valueSize;\n\t\tconst endIndex = startIndex + this.valueSize;\n\n\t\tfor ( let i = startIndex; i < endIndex; i ++ ) {\n\n\t\t\tthis.buffer[ i ] = 0;\n\n\t\t}\n\n\t}\n\n\t_setAdditiveIdentityQuaternion() {\n\n\t\tthis._setAdditiveIdentityNumeric();\n\t\tthis.buffer[ this._addIndex * this.valueSize + 3 ] = 1;\n\n\t}\n\n\t_setAdditiveIdentityOther() {\n\n\t\tconst startIndex = this._origIndex * this.valueSize;\n\t\tconst targetIndex = this._addIndex * this.valueSize;\n\n\t\tfor ( let i = 0; i < this.valueSize; i ++ ) {\n\n\t\t\tthis.buffer[ targetIndex + i ] = this.buffer[ startIndex + i ];\n\n\t\t}\n\n\t}\n\n\n\t// mix functions\n\n\t_select( buffer, dstOffset, srcOffset, t, stride ) {\n\n\t\tif ( t >= 0.5 ) {\n\n\t\t\tfor ( let i = 0; i !== stride; ++ i ) {\n\n\t\t\t\tbuffer[ dstOffset + i ] = buffer[ srcOffset + i ];\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t_slerp( buffer, dstOffset, srcOffset, t ) {\n\n\t\tQuaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, srcOffset, t );\n\n\t}\n\n\t_slerpAdditive( buffer, dstOffset, srcOffset, t, stride ) {\n\n\t\tconst workOffset = this._workIndex * stride;\n\n\t\t// Store result in intermediate buffer offset\n\t\tQuaternion.multiplyQuaternionsFlat( buffer, workOffset, buffer, dstOffset, buffer, srcOffset );\n\n\t\t// Slerp to the intermediate result\n\t\tQuaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, workOffset, t );\n\n\t}\n\n\t_lerp( buffer, dstOffset, srcOffset, t, stride ) {\n\n\t\tconst s = 1 - t;\n\n\t\tfor ( let i = 0; i !== stride; ++ i ) {\n\n\t\t\tconst j = dstOffset + i;\n\n\t\t\tbuffer[ j ] = buffer[ j ] * s + buffer[ srcOffset + i ] * t;\n\n\t\t}\n\n\t}\n\n\t_lerpAdditive( buffer, dstOffset, srcOffset, t, stride ) {\n\n\t\tfor ( let i = 0; i !== stride; ++ i ) {\n\n\t\t\tconst j = dstOffset + i;\n\n\t\t\tbuffer[ j ] = buffer[ j ] + buffer[ srcOffset + i ] * t;\n\n\t\t}\n\n\t}\n\n}\n\n// Characters [].:/ are reserved for track binding syntax.\nconst _RESERVED_CHARS_RE = '\\\\[\\\\]\\\\.:\\\\/';\nconst _reservedRe = new RegExp( '[' + _RESERVED_CHARS_RE + ']', 'g' );\n\n// Attempts to allow node names from any language. ES5's `\\w` regexp matches\n// only latin characters, and the unicode \\p{L} is not yet supported. So\n// instead, we exclude reserved characters and match everything else.\nconst _wordChar = '[^' + _RESERVED_CHARS_RE + ']';\nconst _wordCharOrDot = '[^' + _RESERVED_CHARS_RE.replace( '\\\\.', '' ) + ']';\n\n// Parent directories, delimited by '/' or ':'. Currently unused, but must\n// be matched to parse the rest of the track name.\nconst _directoryRe = /*@__PURE__*/ /((?:WC+[\\/:])*)/.source.replace( 'WC', _wordChar );\n\n// Target node. May contain word characters (a-zA-Z0-9_) and '.' or '-'.\nconst _nodeRe = /*@__PURE__*/ /(WCOD+)?/.source.replace( 'WCOD', _wordCharOrDot );\n\n// Object on target node, and accessor. May not contain reserved\n// characters. Accessor may contain any character except closing bracket.\nconst _objectRe = /*@__PURE__*/ /(?:\\.(WC+)(?:\\[(.+)\\])?)?/.source.replace( 'WC', _wordChar );\n\n// Property and accessor. May not contain reserved characters. Accessor may\n// contain any non-bracket characters.\nconst _propertyRe = /*@__PURE__*/ /\\.(WC+)(?:\\[(.+)\\])?/.source.replace( 'WC', _wordChar );\n\nconst _trackRe = new RegExp( ''\n\t+ '^'\n\t+ _directoryRe\n\t+ _nodeRe\n\t+ _objectRe\n\t+ _propertyRe\n\t+ '$'\n);\n\nconst _supportedObjectNames = [ 'material', 'materials', 'bones', 'map' ];\n\nclass Composite {\n\n\tconstructor( targetGroup, path, optionalParsedPath ) {\n\n\t\tconst parsedPath = optionalParsedPath || PropertyBinding.parseTrackName( path );\n\n\t\tthis._targetGroup = targetGroup;\n\t\tthis._bindings = targetGroup.subscribe_( path, parsedPath );\n\n\t}\n\n\tgetValue( array, offset ) {\n\n\t\tthis.bind(); // bind all binding\n\n\t\tconst firstValidIndex = this._targetGroup.nCachedObjects_,\n\t\t\tbinding = this._bindings[ firstValidIndex ];\n\n\t\t// and only call .getValue on the first\n\t\tif ( binding !== undefined ) binding.getValue( array, offset );\n\n\t}\n\n\tsetValue( array, offset ) {\n\n\t\tconst bindings = this._bindings;\n\n\t\tfor ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {\n\n\t\t\tbindings[ i ].setValue( array, offset );\n\n\t\t}\n\n\t}\n\n\tbind() {\n\n\t\tconst bindings = this._bindings;\n\n\t\tfor ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {\n\n\t\t\tbindings[ i ].bind();\n\n\t\t}\n\n\t}\n\n\tunbind() {\n\n\t\tconst bindings = this._bindings;\n\n\t\tfor ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {\n\n\t\t\tbindings[ i ].unbind();\n\n\t\t}\n\n\t}\n\n}\n\n// Note: This class uses a State pattern on a per-method basis:\n// 'bind' sets 'this.getValue' / 'setValue' and shadows the\n// prototype version of these methods with one that represents\n// the bound state. When the property is not found, the methods\n// become no-ops.\n\n\n/**\n * This holds a reference to a real property in the scene graph; used internally.\n */\nclass PropertyBinding {\n\n\t/**\n\t * Constructs a new property binding.\n\t *\n\t * @param {Object} rootNode - The root node.\n\t * @param {string} path - The path.\n\t * @param {?Object} [parsedPath] - The parsed path.\n\t */\n\tconstructor( rootNode, path, parsedPath ) {\n\n\t\t/**\n\t\t * The object path to the animated property.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.path = path;\n\n\t\t/**\n\t\t * An object holding information about the path.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.parsedPath = parsedPath || PropertyBinding.parseTrackName( path );\n\n\t\t/**\n\t\t * The object owns the animated property.\n\t\t *\n\t\t * @type {?Object}\n\t\t */\n\t\tthis.node = PropertyBinding.findNode( rootNode, this.parsedPath.nodeName );\n\n\t\t/**\n\t\t * The root node.\n\t\t *\n\t\t * @type {Object3D|Skeleton}\n\t\t */\n\t\tthis.rootNode = rootNode;\n\n\t\t// initial state of these methods that calls 'bind'\n\t\tthis.getValue = this._getValue_unbound;\n\t\tthis.setValue = this._setValue_unbound;\n\n\t}\n\n\n\t/**\n\t * Factory method for creating a property binding from the given parameters.\n\t *\n\t * @static\n\t * @param {Object} root - The root node.\n\t * @param {string} path - The path.\n\t * @param {?Object} [parsedPath] - The parsed path.\n\t * @return {PropertyBinding|Composite} The created property binding or composite.\n\t */\n\tstatic create( root, path, parsedPath ) {\n\n\t\tif ( ! ( root && root.isAnimationObjectGroup ) ) {\n\n\t\t\treturn new PropertyBinding( root, path, parsedPath );\n\n\t\t} else {\n\n\t\t\treturn new PropertyBinding.Composite( root, path, parsedPath );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Replaces spaces with underscores and removes unsupported characters from\n\t * node names, to ensure compatibility with parseTrackName().\n\t *\n\t * @param {string} name - Node name to be sanitized.\n\t * @return {string} The sanitized node name.\n\t */\n\tstatic sanitizeNodeName( name ) {\n\n\t\treturn name.replace( /\\s/g, '_' ).replace( _reservedRe, '' );\n\n\t}\n\n\t/**\n\t * Parses the given track name (an object path to an animated property) and\n\t * returns an object with information about the path. Matches strings in the following forms:\n\t *\n\t * - nodeName.property\n\t * - nodeName.property[accessor]\n\t * - nodeName.material.property[accessor]\n\t * - uuid.property[accessor]\n\t * - uuid.objectName[objectIndex].propertyName[propertyIndex]\n\t * - parentName/nodeName.property\n\t * - parentName/parentName/nodeName.property[index]\n\t * - .bone[Armature.DEF_cog].position\n\t * - scene:helium_balloon_model:helium_balloon_model.position\n\t *\n\t * @static\n\t * @param {string} trackName - The track name to parse.\n\t * @return {Object} The parsed track name as an object.\n\t */\n\tstatic parseTrackName( trackName ) {\n\n\t\tconst matches = _trackRe.exec( trackName );\n\n\t\tif ( matches === null ) {\n\n\t\t\tthrow new Error( 'PropertyBinding: Cannot parse trackName: ' + trackName );\n\n\t\t}\n\n\t\tconst results = {\n\t\t\t// directoryName: matches[ 1 ], // (tschw) currently unused\n\t\t\tnodeName: matches[ 2 ],\n\t\t\tobjectName: matches[ 3 ],\n\t\t\tobjectIndex: matches[ 4 ],\n\t\t\tpropertyName: matches[ 5 ], // required\n\t\t\tpropertyIndex: matches[ 6 ]\n\t\t};\n\n\t\tconst lastDot = results.nodeName && results.nodeName.lastIndexOf( '.' );\n\n\t\tif ( lastDot !== undefined && lastDot !== -1 ) {\n\n\t\t\tconst objectName = results.nodeName.substring( lastDot + 1 );\n\n\t\t\t// Object names must be checked against an allowlist. Otherwise, there\n\t\t\t// is no way to parse 'foo.bar.baz': 'baz' must be a property, but\n\t\t\t// 'bar' could be the objectName, or part of a nodeName (which can\n\t\t\t// include '.' characters).\n\t\t\tif ( _supportedObjectNames.indexOf( objectName ) !== -1 ) {\n\n\t\t\t\tresults.nodeName = results.nodeName.substring( 0, lastDot );\n\t\t\t\tresults.objectName = objectName;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( results.propertyName === null || results.propertyName.length === 0 ) {\n\n\t\t\tthrow new Error( 'PropertyBinding: can not parse propertyName from trackName: ' + trackName );\n\n\t\t}\n\n\t\treturn results;\n\n\t}\n\n\t/**\n\t * Searches for a node in the hierarchy of the given root object by the given\n\t * node name.\n\t *\n\t * @static\n\t * @param {Object} root - The root object.\n\t * @param {string|number} nodeName - The name of the node.\n\t * @return {?Object} The found node. Returns `null` if no object was found.\n\t */\n\tstatic findNode( root, nodeName ) {\n\n\t\tif ( nodeName === undefined || nodeName === '' || nodeName === '.' || nodeName === -1 || nodeName === root.name || nodeName === root.uuid ) {\n\n\t\t\treturn root;\n\n\t\t}\n\n\t\t// search into skeleton bones.\n\t\tif ( root.skeleton ) {\n\n\t\t\tconst bone = root.skeleton.getBoneByName( nodeName );\n\n\t\t\tif ( bone !== undefined ) {\n\n\t\t\t\treturn bone;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// search into node subtree.\n\t\tif ( root.children ) {\n\n\t\t\tconst searchNodeSubtree = function ( children ) {\n\n\t\t\t\tfor ( let i = 0; i < children.length; i ++ ) {\n\n\t\t\t\t\tconst childNode = children[ i ];\n\n\t\t\t\t\tif ( childNode.name === nodeName || childNode.uuid === nodeName ) {\n\n\t\t\t\t\t\treturn childNode;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tconst result = searchNodeSubtree( childNode.children );\n\n\t\t\t\t\tif ( result ) return result;\n\n\t\t\t\t}\n\n\t\t\t\treturn null;\n\n\t\t\t};\n\n\t\t\tconst subTreeNode = searchNodeSubtree( root.children );\n\n\t\t\tif ( subTreeNode ) {\n\n\t\t\t\treturn subTreeNode;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn null;\n\n\t}\n\n\t// these are used to \"bind\" a nonexistent property\n\t_getValue_unavailable() {}\n\t_setValue_unavailable() {}\n\n\t// Getters\n\n\t_getValue_direct( buffer, offset ) {\n\n\t\tbuffer[ offset ] = this.targetObject[ this.propertyName ];\n\n\t}\n\n\t_getValue_array( buffer, offset ) {\n\n\t\tconst source = this.resolvedProperty;\n\n\t\tfor ( let i = 0, n = source.length; i !== n; ++ i ) {\n\n\t\t\tbuffer[ offset ++ ] = source[ i ];\n\n\t\t}\n\n\t}\n\n\t_getValue_arrayElement( buffer, offset ) {\n\n\t\tbuffer[ offset ] = this.resolvedProperty[ this.propertyIndex ];\n\n\t}\n\n\t_getValue_toArray( buffer, offset ) {\n\n\t\tthis.resolvedProperty.toArray( buffer, offset );\n\n\t}\n\n\t// Direct\n\n\t_setValue_direct( buffer, offset ) {\n\n\t\tthis.targetObject[ this.propertyName ] = buffer[ offset ];\n\n\t}\n\n\t_setValue_direct_setNeedsUpdate( buffer, offset ) {\n\n\t\tthis.targetObject[ this.propertyName ] = buffer[ offset ];\n\t\tthis.targetObject.needsUpdate = true;\n\n\t}\n\n\t_setValue_direct_setMatrixWorldNeedsUpdate( buffer, offset ) {\n\n\t\tthis.targetObject[ this.propertyName ] = buffer[ offset ];\n\t\tthis.targetObject.matrixWorldNeedsUpdate = true;\n\n\t}\n\n\t// EntireArray\n\n\t_setValue_array( buffer, offset ) {\n\n\t\tconst dest = this.resolvedProperty;\n\n\t\tfor ( let i = 0, n = dest.length; i !== n; ++ i ) {\n\n\t\t\tdest[ i ] = buffer[ offset ++ ];\n\n\t\t}\n\n\t}\n\n\t_setValue_array_setNeedsUpdate( buffer, offset ) {\n\n\t\tconst dest = this.resolvedProperty;\n\n\t\tfor ( let i = 0, n = dest.length; i !== n; ++ i ) {\n\n\t\t\tdest[ i ] = buffer[ offset ++ ];\n\n\t\t}\n\n\t\tthis.targetObject.needsUpdate = true;\n\n\t}\n\n\t_setValue_array_setMatrixWorldNeedsUpdate( buffer, offset ) {\n\n\t\tconst dest = this.resolvedProperty;\n\n\t\tfor ( let i = 0, n = dest.length; i !== n; ++ i ) {\n\n\t\t\tdest[ i ] = buffer[ offset ++ ];\n\n\t\t}\n\n\t\tthis.targetObject.matrixWorldNeedsUpdate = true;\n\n\t}\n\n\t// ArrayElement\n\n\t_setValue_arrayElement( buffer, offset ) {\n\n\t\tthis.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];\n\n\t}\n\n\t_setValue_arrayElement_setNeedsUpdate( buffer, offset ) {\n\n\t\tthis.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];\n\t\tthis.targetObject.needsUpdate = true;\n\n\t}\n\n\t_setValue_arrayElement_setMatrixWorldNeedsUpdate( buffer, offset ) {\n\n\t\tthis.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];\n\t\tthis.targetObject.matrixWorldNeedsUpdate = true;\n\n\t}\n\n\t// HasToFromArray\n\n\t_setValue_fromArray( buffer, offset ) {\n\n\t\tthis.resolvedProperty.fromArray( buffer, offset );\n\n\t}\n\n\t_setValue_fromArray_setNeedsUpdate( buffer, offset ) {\n\n\t\tthis.resolvedProperty.fromArray( buffer, offset );\n\t\tthis.targetObject.needsUpdate = true;\n\n\t}\n\n\t_setValue_fromArray_setMatrixWorldNeedsUpdate( buffer, offset ) {\n\n\t\tthis.resolvedProperty.fromArray( buffer, offset );\n\t\tthis.targetObject.matrixWorldNeedsUpdate = true;\n\n\t}\n\n\t_getValue_unbound( targetArray, offset ) {\n\n\t\tthis.bind();\n\t\tthis.getValue( targetArray, offset );\n\n\t}\n\n\t_setValue_unbound( sourceArray, offset ) {\n\n\t\tthis.bind();\n\t\tthis.setValue( sourceArray, offset );\n\n\t}\n\n\t/**\n\t * Creates a getter / setter pair for the property tracked by this binding.\n\t */\n\tbind() {\n\n\t\tlet targetObject = this.node;\n\t\tconst parsedPath = this.parsedPath;\n\n\t\tconst objectName = parsedPath.objectName;\n\t\tconst propertyName = parsedPath.propertyName;\n\t\tlet propertyIndex = parsedPath.propertyIndex;\n\n\t\tif ( ! targetObject ) {\n\n\t\t\ttargetObject = PropertyBinding.findNode( this.rootNode, parsedPath.nodeName );\n\n\t\t\tthis.node = targetObject;\n\n\t\t}\n\n\t\t// set fail state so we can just 'return' on error\n\t\tthis.getValue = this._getValue_unavailable;\n\t\tthis.setValue = this._setValue_unavailable;\n\n\t\t// ensure there is a value node\n\t\tif ( ! targetObject ) {\n\n\t\t\twarn( 'PropertyBinding: No target node found for track: ' + this.path + '.' );\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( objectName ) {\n\n\t\t\tlet objectIndex = parsedPath.objectIndex;\n\n\t\t\t// special cases were we need to reach deeper into the hierarchy to get the face materials....\n\t\t\tswitch ( objectName ) {\n\n\t\t\t\tcase 'materials':\n\n\t\t\t\t\tif ( ! targetObject.material ) {\n\n\t\t\t\t\t\terror( 'PropertyBinding: Can not bind to material as node does not have a material.', this );\n\t\t\t\t\t\treturn;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! targetObject.material.materials ) {\n\n\t\t\t\t\t\terror( 'PropertyBinding: Can not bind to material.materials as node.material does not have a materials array.', this );\n\t\t\t\t\t\treturn;\n\n\t\t\t\t\t}\n\n\t\t\t\t\ttargetObject = targetObject.material.materials;\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'bones':\n\n\t\t\t\t\tif ( ! targetObject.skeleton ) {\n\n\t\t\t\t\t\terror( 'PropertyBinding: Can not bind to bones as node does not have a skeleton.', this );\n\t\t\t\t\t\treturn;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// potential future optimization: skip this if propertyIndex is already an integer\n\t\t\t\t\t// and convert the integer string to a true integer.\n\n\t\t\t\t\ttargetObject = targetObject.skeleton.bones;\n\n\t\t\t\t\t// support resolving morphTarget names into indices.\n\t\t\t\t\tfor ( let i = 0; i < targetObject.length; i ++ ) {\n\n\t\t\t\t\t\tif ( targetObject[ i ].name === objectIndex ) {\n\n\t\t\t\t\t\t\tobjectIndex = i;\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'map':\n\n\t\t\t\t\tif ( 'map' in targetObject ) {\n\n\t\t\t\t\t\ttargetObject = targetObject.map;\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! targetObject.material ) {\n\n\t\t\t\t\t\terror( 'PropertyBinding: Can not bind to material as node does not have a material.', this );\n\t\t\t\t\t\treturn;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! targetObject.material.map ) {\n\n\t\t\t\t\t\terror( 'PropertyBinding: Can not bind to material.map as node.material does not have a map.', this );\n\t\t\t\t\t\treturn;\n\n\t\t\t\t\t}\n\n\t\t\t\t\ttargetObject = targetObject.material.map;\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\n\t\t\t\t\tif ( targetObject[ objectName ] === undefined ) {\n\n\t\t\t\t\t\terror( 'PropertyBinding: Can not bind to objectName of node undefined.', this );\n\t\t\t\t\t\treturn;\n\n\t\t\t\t\t}\n\n\t\t\t\t\ttargetObject = targetObject[ objectName ];\n\n\t\t\t}\n\n\n\t\t\tif ( objectIndex !== undefined ) {\n\n\t\t\t\tif ( targetObject[ objectIndex ] === undefined ) {\n\n\t\t\t\t\terror( 'PropertyBinding: Trying to bind to objectIndex of objectName, but is undefined.', this, targetObject );\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t\ttargetObject = targetObject[ objectIndex ];\n\n\t\t\t}\n\n\t\t}\n\n\t\t// resolve property\n\t\tconst nodeProperty = targetObject[ propertyName ];\n\n\t\tif ( nodeProperty === undefined ) {\n\n\t\t\tconst nodeName = parsedPath.nodeName;\n\n\t\t\terror( 'PropertyBinding: Trying to update property for track: ' + nodeName +\n\t\t\t\t'.' + propertyName + ' but it wasn\\'t found.', targetObject );\n\t\t\treturn;\n\n\t\t}\n\n\t\t// determine versioning scheme\n\t\tlet versioning = this.Versioning.None;\n\n\t\tthis.targetObject = targetObject;\n\n\t\tif ( targetObject.isMaterial === true ) {\n\n\t\t\tversioning = this.Versioning.NeedsUpdate;\n\n\t\t} else if ( targetObject.isObject3D === true ) {\n\n\t\t\tversioning = this.Versioning.MatrixWorldNeedsUpdate;\n\n\t\t}\n\n\t\t// determine how the property gets bound\n\t\tlet bindingType = this.BindingType.Direct;\n\n\t\tif ( propertyIndex !== undefined ) {\n\n\t\t\t// access a sub element of the property array (only primitives are supported right now)\n\n\t\t\tif ( propertyName === 'morphTargetInfluences' ) {\n\n\t\t\t\t// potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.\n\n\t\t\t\t// support resolving morphTarget names into indices.\n\t\t\t\tif ( ! targetObject.geometry ) {\n\n\t\t\t\t\terror( 'PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.', this );\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t\tif ( ! targetObject.geometry.morphAttributes ) {\n\n\t\t\t\t\terror( 'PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.morphAttributes.', this );\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t\tif ( targetObject.morphTargetDictionary[ propertyIndex ] !== undefined ) {\n\n\t\t\t\t\tpropertyIndex = targetObject.morphTargetDictionary[ propertyIndex ];\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tbindingType = this.BindingType.ArrayElement;\n\n\t\t\tthis.resolvedProperty = nodeProperty;\n\t\t\tthis.propertyIndex = propertyIndex;\n\n\t\t} else if ( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {\n\n\t\t\t// must use copy for Object3D.Euler/Quaternion\n\n\t\t\tbindingType = this.BindingType.HasFromToArray;\n\n\t\t\tthis.resolvedProperty = nodeProperty;\n\n\t\t} else if ( Array.isArray( nodeProperty ) ) {\n\n\t\t\tbindingType = this.BindingType.EntireArray;\n\n\t\t\tthis.resolvedProperty = nodeProperty;\n\n\t\t} else {\n\n\t\t\tthis.propertyName = propertyName;\n\n\t\t}\n\n\t\t// select getter / setter\n\t\tthis.getValue = this.GetterByBindingType[ bindingType ];\n\t\tthis.setValue = this.SetterByBindingTypeAndVersioning[ bindingType ][ versioning ];\n\n\t}\n\n\t/**\n\t * Unbinds the property.\n\t */\n\tunbind() {\n\n\t\tthis.node = null;\n\n\t\t// back to the prototype version of getValue / setValue\n\t\t// note: avoiding to mutate the shape of 'this' via 'delete'\n\t\tthis.getValue = this._getValue_unbound;\n\t\tthis.setValue = this._setValue_unbound;\n\n\t}\n\n}\n\nPropertyBinding.Composite = Composite;\n\nPropertyBinding.prototype.BindingType = {\n\tDirect: 0,\n\tEntireArray: 1,\n\tArrayElement: 2,\n\tHasFromToArray: 3\n};\n\nPropertyBinding.prototype.Versioning = {\n\tNone: 0,\n\tNeedsUpdate: 1,\n\tMatrixWorldNeedsUpdate: 2\n};\n\nPropertyBinding.prototype.GetterByBindingType = [\n\n\tPropertyBinding.prototype._getValue_direct,\n\tPropertyBinding.prototype._getValue_array,\n\tPropertyBinding.prototype._getValue_arrayElement,\n\tPropertyBinding.prototype._getValue_toArray,\n\n];\n\nPropertyBinding.prototype.SetterByBindingTypeAndVersioning = [\n\n\t[\n\t\t// Direct\n\t\tPropertyBinding.prototype._setValue_direct,\n\t\tPropertyBinding.prototype._setValue_direct_setNeedsUpdate,\n\t\tPropertyBinding.prototype._setValue_direct_setMatrixWorldNeedsUpdate,\n\n\t], [\n\n\t\t// EntireArray\n\n\t\tPropertyBinding.prototype._setValue_array,\n\t\tPropertyBinding.prototype._setValue_array_setNeedsUpdate,\n\t\tPropertyBinding.prototype._setValue_array_setMatrixWorldNeedsUpdate,\n\n\t], [\n\n\t\t// ArrayElement\n\t\tPropertyBinding.prototype._setValue_arrayElement,\n\t\tPropertyBinding.prototype._setValue_arrayElement_setNeedsUpdate,\n\t\tPropertyBinding.prototype._setValue_arrayElement_setMatrixWorldNeedsUpdate,\n\n\t], [\n\n\t\t// HasToFromArray\n\t\tPropertyBinding.prototype._setValue_fromArray,\n\t\tPropertyBinding.prototype._setValue_fromArray_setNeedsUpdate,\n\t\tPropertyBinding.prototype._setValue_fromArray_setMatrixWorldNeedsUpdate,\n\n\t]\n\n];\n\n/**\n * A group of objects that receives a shared animation state.\n *\n * Usage:\n *\n * - Add objects you would otherwise pass as 'root' to the\n * constructor or the .clipAction method of AnimationMixer.\n * - Instead pass this object as 'root'.\n * - You can also add and remove objects later when the mixer is running.\n *\n * Note:\n *\n * - Objects of this class appear as one object to the mixer,\n * so cache control of the individual objects must be done on the group.\n *\n * Limitation:\n *\n * - The animated properties must be compatible among the all objects in the group.\n * - A single property can either be controlled through a target group or directly, but not both.\n */\nclass AnimationObjectGroup {\n\n\t/**\n\t * Constructs a new animation group.\n\t *\n\t * @param {...Object3D} arguments - An arbitrary number of 3D objects that share the same animation state.\n\t */\n\tconstructor() {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isAnimationObjectGroup = true;\n\n\t\t/**\n\t\t * The UUID of the 3D object.\n\t\t *\n\t\t * @type {string}\n\t\t * @readonly\n\t\t */\n\t\tthis.uuid = generateUUID();\n\n\t\t// cached objects followed by the active ones\n\t\tthis._objects = Array.prototype.slice.call( arguments );\n\n\t\tthis.nCachedObjects_ = 0; // threshold\n\t\t// note: read by PropertyBinding.Composite\n\n\t\tconst indices = {};\n\t\tthis._indicesByUUID = indices; // for bookkeeping\n\n\t\tfor ( let i = 0, n = arguments.length; i !== n; ++ i ) {\n\n\t\t\tindices[ arguments[ i ].uuid ] = i;\n\n\t\t}\n\n\t\tthis._paths = []; // inside: string\n\t\tthis._parsedPaths = []; // inside: { we don't care, here }\n\t\tthis._bindings = []; // inside: Array< PropertyBinding >\n\t\tthis._bindingsIndicesByPath = {}; // inside: indices in these arrays\n\n\t\tconst scope = this;\n\n\t\tthis.stats = {\n\n\t\t\tobjects: {\n\t\t\t\tget total() {\n\n\t\t\t\t\treturn scope._objects.length;\n\n\t\t\t\t},\n\t\t\t\tget inUse() {\n\n\t\t\t\t\treturn this.total - scope.nCachedObjects_;\n\n\t\t\t\t}\n\t\t\t},\n\t\t\tget bindingsPerObject() {\n\n\t\t\t\treturn scope._bindings.length;\n\n\t\t\t}\n\n\t\t};\n\n\t}\n\n\t/**\n\t * Adds an arbitrary number of objects to this animation group.\n\t *\n\t * @param {...Object3D} arguments - The 3D objects to add.\n\t */\n\tadd() {\n\n\t\tconst objects = this._objects,\n\t\t\tindicesByUUID = this._indicesByUUID,\n\t\t\tpaths = this._paths,\n\t\t\tparsedPaths = this._parsedPaths,\n\t\t\tbindings = this._bindings,\n\t\t\tnBindings = bindings.length;\n\n\t\tlet knownObject = undefined,\n\t\t\tnObjects = objects.length,\n\t\t\tnCachedObjects = this.nCachedObjects_;\n\n\t\tfor ( let i = 0, n = arguments.length; i !== n; ++ i ) {\n\n\t\t\tconst object = arguments[ i ],\n\t\t\t\tuuid = object.uuid;\n\t\t\tlet index = indicesByUUID[ uuid ];\n\n\t\t\tif ( index === undefined ) {\n\n\t\t\t\t// unknown object -> add it to the ACTIVE region\n\n\t\t\t\tindex = nObjects ++;\n\t\t\t\tindicesByUUID[ uuid ] = index;\n\t\t\t\tobjects.push( object );\n\n\t\t\t\t// accounting is done, now do the same for all bindings\n\n\t\t\t\tfor ( let j = 0, m = nBindings; j !== m; ++ j ) {\n\n\t\t\t\t\tbindings[ j ].push( new PropertyBinding( object, paths[ j ], parsedPaths[ j ] ) );\n\n\t\t\t\t}\n\n\t\t\t} else if ( index < nCachedObjects ) {\n\n\t\t\t\tknownObject = objects[ index ];\n\n\t\t\t\t// move existing object to the ACTIVE region\n\n\t\t\t\tconst firstActiveIndex = -- nCachedObjects,\n\t\t\t\t\tlastCachedObject = objects[ firstActiveIndex ];\n\n\t\t\t\tindicesByUUID[ lastCachedObject.uuid ] = index;\n\t\t\t\tobjects[ index ] = lastCachedObject;\n\n\t\t\t\tindicesByUUID[ uuid ] = firstActiveIndex;\n\t\t\t\tobjects[ firstActiveIndex ] = object;\n\n\t\t\t\t// accounting is done, now do the same for all bindings\n\n\t\t\t\tfor ( let j = 0, m = nBindings; j !== m; ++ j ) {\n\n\t\t\t\t\tconst bindingsForPath = bindings[ j ],\n\t\t\t\t\t\tlastCached = bindingsForPath[ firstActiveIndex ];\n\n\t\t\t\t\tlet binding = bindingsForPath[ index ];\n\n\t\t\t\t\tbindingsForPath[ index ] = lastCached;\n\n\t\t\t\t\tif ( binding === undefined ) {\n\n\t\t\t\t\t\t// since we do not bother to create new bindings\n\t\t\t\t\t\t// for objects that are cached, the binding may\n\t\t\t\t\t\t// or may not exist\n\n\t\t\t\t\t\tbinding = new PropertyBinding( object, paths[ j ], parsedPaths[ j ] );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbindingsForPath[ firstActiveIndex ] = binding;\n\n\t\t\t\t}\n\n\t\t\t} else if ( objects[ index ] !== knownObject ) {\n\n\t\t\t\terror( 'AnimationObjectGroup: Different objects with the same UUID ' +\n\t\t\t\t\t'detected. Clean the caches or recreate your infrastructure when reloading scenes.' );\n\n\t\t\t} // else the object is already where we want it to be\n\n\t\t} // for arguments\n\n\t\tthis.nCachedObjects_ = nCachedObjects;\n\n\t}\n\n\t/**\n\t * Removes an arbitrary number of objects to this animation group\n\t *\n\t * @param {...Object3D} arguments - The 3D objects to remove.\n\t */\n\tremove() {\n\n\t\tconst objects = this._objects,\n\t\t\tindicesByUUID = this._indicesByUUID,\n\t\t\tbindings = this._bindings,\n\t\t\tnBindings = bindings.length;\n\n\t\tlet nCachedObjects = this.nCachedObjects_;\n\n\t\tfor ( let i = 0, n = arguments.length; i !== n; ++ i ) {\n\n\t\t\tconst object = arguments[ i ],\n\t\t\t\tuuid = object.uuid,\n\t\t\t\tindex = indicesByUUID[ uuid ];\n\n\t\t\tif ( index !== undefined && index >= nCachedObjects ) {\n\n\t\t\t\t// move existing object into the CACHED region\n\n\t\t\t\tconst lastCachedIndex = nCachedObjects ++,\n\t\t\t\t\tfirstActiveObject = objects[ lastCachedIndex ];\n\n\t\t\t\tindicesByUUID[ firstActiveObject.uuid ] = index;\n\t\t\t\tobjects[ index ] = firstActiveObject;\n\n\t\t\t\tindicesByUUID[ uuid ] = lastCachedIndex;\n\t\t\t\tobjects[ lastCachedIndex ] = object;\n\n\t\t\t\t// accounting is done, now do the same for all bindings\n\n\t\t\t\tfor ( let j = 0, m = nBindings; j !== m; ++ j ) {\n\n\t\t\t\t\tconst bindingsForPath = bindings[ j ],\n\t\t\t\t\t\tfirstActive = bindingsForPath[ lastCachedIndex ],\n\t\t\t\t\t\tbinding = bindingsForPath[ index ];\n\n\t\t\t\t\tbindingsForPath[ index ] = firstActive;\n\t\t\t\t\tbindingsForPath[ lastCachedIndex ] = binding;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} // for arguments\n\n\t\tthis.nCachedObjects_ = nCachedObjects;\n\n\t}\n\n\t/**\n\t * Deallocates all memory resources for the passed 3D objects of this animation group.\n\t *\n\t * @param {...Object3D} arguments - The 3D objects to uncache.\n\t */\n\tuncache() {\n\n\t\tconst objects = this._objects,\n\t\t\tindicesByUUID = this._indicesByUUID,\n\t\t\tbindings = this._bindings,\n\t\t\tnBindings = bindings.length;\n\n\t\tlet nCachedObjects = this.nCachedObjects_,\n\t\t\tnObjects = objects.length;\n\n\t\tfor ( let i = 0, n = arguments.length; i !== n; ++ i ) {\n\n\t\t\tconst object = arguments[ i ],\n\t\t\t\tuuid = object.uuid,\n\t\t\t\tindex = indicesByUUID[ uuid ];\n\n\t\t\tif ( index !== undefined ) {\n\n\t\t\t\tdelete indicesByUUID[ uuid ];\n\n\t\t\t\tif ( index < nCachedObjects ) {\n\n\t\t\t\t\t// object is cached, shrink the CACHED region\n\n\t\t\t\t\tconst firstActiveIndex = -- nCachedObjects,\n\t\t\t\t\t\tlastCachedObject = objects[ firstActiveIndex ],\n\t\t\t\t\t\tlastIndex = -- nObjects,\n\t\t\t\t\t\tlastObject = objects[ lastIndex ];\n\n\t\t\t\t\t// last cached object takes this object's place\n\t\t\t\t\tindicesByUUID[ lastCachedObject.uuid ] = index;\n\t\t\t\t\tobjects[ index ] = lastCachedObject;\n\n\t\t\t\t\t// last object goes to the activated slot and pop\n\t\t\t\t\tindicesByUUID[ lastObject.uuid ] = firstActiveIndex;\n\t\t\t\t\tobjects[ firstActiveIndex ] = lastObject;\n\t\t\t\t\tobjects.pop();\n\n\t\t\t\t\t// accounting is done, now do the same for all bindings\n\n\t\t\t\t\tfor ( let j = 0, m = nBindings; j !== m; ++ j ) {\n\n\t\t\t\t\t\tconst bindingsForPath = bindings[ j ],\n\t\t\t\t\t\t\tlastCached = bindingsForPath[ firstActiveIndex ],\n\t\t\t\t\t\t\tlast = bindingsForPath[ lastIndex ];\n\n\t\t\t\t\t\tbindingsForPath[ index ] = lastCached;\n\t\t\t\t\t\tbindingsForPath[ firstActiveIndex ] = last;\n\t\t\t\t\t\tbindingsForPath.pop();\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// object is active, just swap with the last and pop\n\n\t\t\t\t\tconst lastIndex = -- nObjects,\n\t\t\t\t\t\tlastObject = objects[ lastIndex ];\n\n\t\t\t\t\tif ( lastIndex > 0 ) {\n\n\t\t\t\t\t\tindicesByUUID[ lastObject.uuid ] = index;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tobjects[ index ] = lastObject;\n\t\t\t\t\tobjects.pop();\n\n\t\t\t\t\t// accounting is done, now do the same for all bindings\n\n\t\t\t\t\tfor ( let j = 0, m = nBindings; j !== m; ++ j ) {\n\n\t\t\t\t\t\tconst bindingsForPath = bindings[ j ];\n\n\t\t\t\t\t\tbindingsForPath[ index ] = bindingsForPath[ lastIndex ];\n\t\t\t\t\t\tbindingsForPath.pop();\n\n\t\t\t\t\t}\n\n\t\t\t\t} // cached or active\n\n\t\t\t} // if object is known\n\n\t\t} // for arguments\n\n\t\tthis.nCachedObjects_ = nCachedObjects;\n\n\t}\n\n\t// Internal interface used by befriended PropertyBinding.Composite:\n\n\tsubscribe_( path, parsedPath ) {\n\n\t\t// returns an array of bindings for the given path that is changed\n\t\t// according to the contained objects in the group\n\n\t\tconst indicesByPath = this._bindingsIndicesByPath;\n\t\tlet index = indicesByPath[ path ];\n\t\tconst bindings = this._bindings;\n\n\t\tif ( index !== undefined ) return bindings[ index ];\n\n\t\tconst paths = this._paths,\n\t\t\tparsedPaths = this._parsedPaths,\n\t\t\tobjects = this._objects,\n\t\t\tnObjects = objects.length,\n\t\t\tnCachedObjects = this.nCachedObjects_,\n\t\t\tbindingsForPath = new Array( nObjects );\n\n\t\tindex = bindings.length;\n\n\t\tindicesByPath[ path ] = index;\n\n\t\tpaths.push( path );\n\t\tparsedPaths.push( parsedPath );\n\t\tbindings.push( bindingsForPath );\n\n\t\tfor ( let i = nCachedObjects, n = objects.length; i !== n; ++ i ) {\n\n\t\t\tconst object = objects[ i ];\n\t\t\tbindingsForPath[ i ] = new PropertyBinding( object, path, parsedPath );\n\n\t\t}\n\n\t\treturn bindingsForPath;\n\n\t}\n\n\tunsubscribe_( path ) {\n\n\t\t// tells the group to forget about a property path and no longer\n\t\t// update the array previously obtained with 'subscribe_'\n\n\t\tconst indicesByPath = this._bindingsIndicesByPath,\n\t\t\tindex = indicesByPath[ path ];\n\n\t\tif ( index !== undefined ) {\n\n\t\t\tconst paths = this._paths,\n\t\t\t\tparsedPaths = this._parsedPaths,\n\t\t\t\tbindings = this._bindings,\n\t\t\t\tlastBindingsIndex = bindings.length - 1,\n\t\t\t\tlastBindings = bindings[ lastBindingsIndex ],\n\t\t\t\tlastBindingsPath = path[ lastBindingsIndex ];\n\n\t\t\tindicesByPath[ lastBindingsPath ] = index;\n\n\t\t\tbindings[ index ] = lastBindings;\n\t\t\tbindings.pop();\n\n\t\t\tparsedPaths[ index ] = parsedPaths[ lastBindingsIndex ];\n\t\t\tparsedPaths.pop();\n\n\t\t\tpaths[ index ] = paths[ lastBindingsIndex ];\n\t\t\tpaths.pop();\n\n\t\t}\n\n\t}\n\n}\n\n/**\n * An instance of `AnimationAction` schedules the playback of an animation which is\n * stored in {@link AnimationClip}.\n */\nclass AnimationAction {\n\n\t/**\n\t * Constructs a new animation action.\n\t *\n\t * @param {AnimationMixer} mixer - The mixer that is controlled by this action.\n\t * @param {AnimationClip} clip - The animation clip that holds the actual keyframes.\n\t * @param {?Object3D} [localRoot=null] - The root object on which this action is performed.\n\t * @param {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)} [blendMode] - The blend mode.\n\t */\n\tconstructor( mixer, clip, localRoot = null, blendMode = clip.blendMode ) {\n\n\t\tthis._mixer = mixer;\n\t\tthis._clip = clip;\n\t\tthis._localRoot = localRoot;\n\n\t\t/**\n\t\t * Defines how the animation is blended/combined when two or more animations\n\t\t * are simultaneously played.\n\t\t *\n\t\t * @type {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)}\n\t\t */\n\t\tthis.blendMode = blendMode;\n\n\t\tconst tracks = clip.tracks,\n\t\t\tnTracks = tracks.length,\n\t\t\tinterpolants = new Array( nTracks );\n\n\t\tconst interpolantSettings = {\n\t\t\tendingStart: ZeroCurvatureEnding,\n\t\t\tendingEnd: ZeroCurvatureEnding\n\t\t};\n\n\t\tfor ( let i = 0; i !== nTracks; ++ i ) {\n\n\t\t\tconst interpolant = tracks[ i ].createInterpolant( null );\n\t\t\tinterpolants[ i ] = interpolant;\n\t\t\tinterpolant.settings = interpolantSettings;\n\n\t\t}\n\n\t\tthis._interpolantSettings = interpolantSettings;\n\n\t\tthis._interpolants = interpolants; // bound by the mixer\n\n\t\t// inside: PropertyMixer (managed by the mixer)\n\t\tthis._propertyBindings = new Array( nTracks );\n\n\t\tthis._cacheIndex = null; // for the memory manager\n\t\tthis._byClipCacheIndex = null; // for the memory manager\n\n\t\tthis._timeScaleInterpolant = null;\n\t\tthis._weightInterpolant = null;\n\n\t\t/**\n\t\t * The loop mode, set via {@link AnimationAction#setLoop}.\n\t\t *\n\t\t * @type {(LoopRepeat|LoopOnce|LoopPingPong)}\n\t\t * @default LoopRepeat\n\t\t */\n\t\tthis.loop = LoopRepeat;\n\t\tthis._loopCount = -1;\n\n\t\t// global mixer time when the action is to be started\n\t\t// it's set back to 'null' upon start of the action\n\t\tthis._startTime = null;\n\n\t\t/**\n\t\t * The local time of this action (in seconds, starting with `0`).\n\t\t *\n\t\t * The value gets clamped or wrapped to `[0,clip.duration]` (according to the\n\t\t * loop state).\n\t\t *\n\t\t * @type {number}\n\t\t * @default Infinity\n\t\t */\n\t\tthis.time = 0;\n\n\t\t/**\n\t\t * Scaling factor for the {@link AnimationAction#time}. A value of `0` causes the\n\t\t * animation to pause. Negative values cause the animation to play backwards.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.timeScale = 1;\n\t\tthis._effectiveTimeScale = 1;\n\n\t\t/**\n\t\t * The degree of influence of this action (in the interval `[0, 1]`). Values\n\t\t * between `0` (no impact) and `1` (full impact) can be used to blend between\n\t\t * several actions.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.weight = 1;\n\t\tthis._effectiveWeight = 1;\n\n\t\t/**\n\t\t * The number of repetitions of the performed clip over the course of this action.\n\t\t * Can be set via {@link AnimationAction#setLoop}.\n\t\t *\n\t\t * Setting this number has no effect if {@link AnimationAction#loop} is set to\n\t\t * `THREE:LoopOnce`.\n\t\t *\n\t\t * @type {number}\n\t\t * @default Infinity\n\t\t */\n\t\tthis.repetitions = Infinity;\n\n\t\t/**\n\t\t * If set to `true`, the playback of the action is paused.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.paused = false;\n\n\t\t/**\n\t\t * If set to `false`, the action is disabled so it has no impact.\n\t\t *\n\t\t * When the action is re-enabled, the animation continues from its current\n\t\t * time (setting `enabled` to `false` doesn't reset the action).\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.enabled = true;\n\n\t\t/**\n\t\t * If set to true the animation will automatically be paused on its last frame.\n\t\t *\n\t\t * If set to false, {@link AnimationAction#enabled} will automatically be switched\n\t\t * to `false` when the last loop of the action has finished, so that this action has\n\t\t * no further impact.\n\t\t *\n\t\t * Note: This member has no impact if the action is interrupted (it\n\t\t * has only an effect if its last loop has really finished).\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.clampWhenFinished = false;\n\n\t\t/**\n\t\t * Enables smooth interpolation without separate clips for start, loop and end.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.zeroSlopeAtStart = true;\n\n\t\t/**\n\t\t * Enables smooth interpolation without separate clips for start, loop and end.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.zeroSlopeAtEnd = true;\n\n\t}\n\n\t/**\n\t * Starts the playback of the animation.\n\t *\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tplay() {\n\n\t\tthis._mixer._activateAction( this );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Stops the playback of the animation.\n\t *\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tstop() {\n\n\t\tthis._mixer._deactivateAction( this );\n\n\t\treturn this.reset();\n\n\t}\n\n\t/**\n\t * Resets the playback of the animation.\n\t *\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\treset() {\n\n\t\tthis.paused = false;\n\t\tthis.enabled = true;\n\n\t\tthis.time = 0; // restart clip\n\t\tthis._loopCount = -1;// forget previous loops\n\t\tthis._startTime = null;// forget scheduling\n\n\t\treturn this.stopFading().stopWarping();\n\n\t}\n\n\t/**\n\t * Returns `true` if the animation is running.\n\t *\n\t * @return {boolean} Whether the animation is running or not.\n\t */\n\tisRunning() {\n\n\t\treturn this.enabled && ! this.paused && this.timeScale !== 0 &&\n\t\t\tthis._startTime === null && this._mixer._isActiveAction( this );\n\n\t}\n\n\t/**\n\t * Returns `true` when {@link AnimationAction#play} has been called.\n\t *\n\t * @return {boolean} Whether the animation is scheduled or not.\n\t */\n\tisScheduled() {\n\n\t\treturn this._mixer._isActiveAction( this );\n\n\t}\n\n\t/**\n\t * Defines the time when the animation should start.\n\t *\n\t * @param {number} time - The start time in seconds.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tstartAt( time ) {\n\n\t\tthis._startTime = time;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Configures the loop settings for this action.\n\t *\n\t * @param {(LoopRepeat|LoopOnce|LoopPingPong)} mode - The loop mode.\n\t * @param {number} repetitions - The number of repetitions.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tsetLoop( mode, repetitions ) {\n\n\t\tthis.loop = mode;\n\t\tthis.repetitions = repetitions;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the effective weight of this action.\n\t *\n\t * An action has no effect and thus an effective weight of zero when the\n\t * action is disabled.\n\t *\n\t * @param {number} weight - The weight to set.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tsetEffectiveWeight( weight ) {\n\n\t\tthis.weight = weight;\n\n\t\t// note: same logic as when updated at runtime\n\t\tthis._effectiveWeight = this.enabled ? weight : 0;\n\n\t\treturn this.stopFading();\n\n\t}\n\n\t/**\n\t * Returns the effective weight of this action.\n\t *\n\t * @return {number} The effective weight.\n\t */\n\tgetEffectiveWeight() {\n\n\t\treturn this._effectiveWeight;\n\n\t}\n\n\t/**\n\t * Fades the animation in by increasing its weight gradually from `0` to `1`,\n\t * within the passed time interval.\n\t *\n\t * @param {number} duration - The duration of the fade.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tfadeIn( duration ) {\n\n\t\treturn this._scheduleFading( duration, 0, 1 );\n\n\t}\n\n\t/**\n\t * Fades the animation out by decreasing its weight gradually from `1` to `0`,\n\t * within the passed time interval.\n\t *\n\t * @param {number} duration - The duration of the fade.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tfadeOut( duration ) {\n\n\t\treturn this._scheduleFading( duration, 1, 0 );\n\n\t}\n\n\t/**\n\t * Causes this action to fade in and the given action to fade out,\n\t * within the passed time interval.\n\t *\n\t * @param {AnimationAction} fadeOutAction - The animation action to fade out.\n\t * @param {number} duration - The duration of the fade.\n\t * @param {boolean} [warp=false] - Whether warping should be used or not.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tcrossFadeFrom( fadeOutAction, duration, warp = false ) {\n\n\t\tfadeOutAction.fadeOut( duration );\n\t\tthis.fadeIn( duration );\n\n\t\tif ( warp === true ) {\n\n\t\t\tconst fadeInDuration = this._clip.duration,\n\t\t\t\tfadeOutDuration = fadeOutAction._clip.duration,\n\n\t\t\t\tstartEndRatio = fadeOutDuration / fadeInDuration,\n\t\t\t\tendStartRatio = fadeInDuration / fadeOutDuration;\n\n\t\t\tfadeOutAction.warp( 1.0, startEndRatio, duration );\n\t\t\tthis.warp( endStartRatio, 1.0, duration );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Causes this action to fade out and the given action to fade in,\n\t * within the passed time interval.\n\t *\n\t * @param {AnimationAction} fadeInAction - The animation action to fade in.\n\t * @param {number} duration - The duration of the fade.\n\t * @param {boolean} [warp=false] - Whether warping should be used or not.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tcrossFadeTo( fadeInAction, duration, warp = false ) {\n\n\t\treturn fadeInAction.crossFadeFrom( this, duration, warp );\n\n\t}\n\n\t/**\n\t * Stops any fading which is applied to this action.\n\t *\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tstopFading() {\n\n\t\tconst weightInterpolant = this._weightInterpolant;\n\n\t\tif ( weightInterpolant !== null ) {\n\n\t\t\tthis._weightInterpolant = null;\n\t\t\tthis._mixer._takeBackControlInterpolant( weightInterpolant );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the effective time scale of this action.\n\t *\n\t * An action has no effect and thus an effective time scale of zero when the\n\t * action is paused.\n\t *\n\t * @param {number} timeScale - The time scale to set.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tsetEffectiveTimeScale( timeScale ) {\n\n\t\tthis.timeScale = timeScale;\n\t\tthis._effectiveTimeScale = this.paused ? 0 : timeScale;\n\n\t\treturn this.stopWarping();\n\n\t}\n\n\t/**\n\t * Returns the effective time scale of this action.\n\t *\n\t * @return {number} The effective time scale.\n\t */\n\tgetEffectiveTimeScale() {\n\n\t\treturn this._effectiveTimeScale;\n\n\t}\n\n\t/**\n\t * Sets the duration for a single loop of this action.\n\t *\n\t * @param {number} duration - The duration to set.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tsetDuration( duration ) {\n\n\t\tthis.timeScale = this._clip.duration / duration;\n\n\t\treturn this.stopWarping();\n\n\t}\n\n\t/**\n\t * Synchronizes this action with the passed other action.\n\t *\n\t * @param {AnimationAction} action - The action to sync with.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tsyncWith( action ) {\n\n\t\tthis.time = action.time;\n\t\tthis.timeScale = action.timeScale;\n\n\t\treturn this.stopWarping();\n\n\t}\n\n\t/**\n\t * Decelerates this animation's speed to `0` within the passed time interval.\n\t *\n\t * @param {number} duration - The duration.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\thalt( duration ) {\n\n\t\treturn this.warp( this._effectiveTimeScale, 0, duration );\n\n\t}\n\n\t/**\n\t * Changes the playback speed, within the passed time interval, by modifying\n\t * {@link AnimationAction#timeScale} gradually from `startTimeScale` to\n\t * `endTimeScale`.\n\t *\n\t * @param {number} startTimeScale - The start time scale.\n\t * @param {number} endTimeScale - The end time scale.\n\t * @param {number} duration - The duration.\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\twarp( startTimeScale, endTimeScale, duration ) {\n\n\t\tconst mixer = this._mixer,\n\t\t\tnow = mixer.time,\n\t\t\ttimeScale = this.timeScale;\n\n\t\tlet interpolant = this._timeScaleInterpolant;\n\n\t\tif ( interpolant === null ) {\n\n\t\t\tinterpolant = mixer._lendControlInterpolant();\n\t\t\tthis._timeScaleInterpolant = interpolant;\n\n\t\t}\n\n\t\tconst times = interpolant.parameterPositions,\n\t\t\tvalues = interpolant.sampleValues;\n\n\t\ttimes[ 0 ] = now;\n\t\ttimes[ 1 ] = now + duration;\n\n\t\tvalues[ 0 ] = startTimeScale / timeScale;\n\t\tvalues[ 1 ] = endTimeScale / timeScale;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Stops any scheduled warping which is applied to this action.\n\t *\n\t * @return {AnimationAction} A reference to this animation action.\n\t */\n\tstopWarping() {\n\n\t\tconst timeScaleInterpolant = this._timeScaleInterpolant;\n\n\t\tif ( timeScaleInterpolant !== null ) {\n\n\t\t\tthis._timeScaleInterpolant = null;\n\t\t\tthis._mixer._takeBackControlInterpolant( timeScaleInterpolant );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the animation mixer of this animation action.\n\t *\n\t * @return {AnimationMixer} The animation mixer.\n\t */\n\tgetMixer() {\n\n\t\treturn this._mixer;\n\n\t}\n\n\t/**\n\t * Returns the animation clip of this animation action.\n\t *\n\t * @return {AnimationClip} The animation clip.\n\t */\n\tgetClip() {\n\n\t\treturn this._clip;\n\n\t}\n\n\t/**\n\t * Returns the root object of this animation action.\n\t *\n\t * @return {Object3D} The root object.\n\t */\n\tgetRoot() {\n\n\t\treturn this._localRoot || this._mixer._root;\n\n\t}\n\n\t// Interna\n\n\t_update( time, deltaTime, timeDirection, accuIndex ) {\n\n\t\t// called by the mixer\n\n\t\tif ( ! this.enabled ) {\n\n\t\t\t// call ._updateWeight() to update ._effectiveWeight\n\n\t\t\tthis._updateWeight( time );\n\t\t\treturn;\n\n\t\t}\n\n\t\tconst startTime = this._startTime;\n\n\t\tif ( startTime !== null ) {\n\n\t\t\t// check for scheduled start of action\n\n\t\t\tconst timeRunning = ( time - startTime ) * timeDirection;\n\t\t\tif ( timeRunning < 0 || timeDirection === 0 ) {\n\n\t\t\t\tdeltaTime = 0;\n\n\t\t\t} else {\n\n\n\t\t\t\tthis._startTime = null; // unschedule\n\t\t\t\tdeltaTime = timeDirection * timeRunning;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// apply time scale and advance time\n\n\t\tdeltaTime *= this._updateTimeScale( time );\n\t\tconst clipTime = this._updateTime( deltaTime );\n\n\t\t// note: _updateTime may disable the action resulting in\n\t\t// an effective weight of 0\n\n\t\tconst weight = this._updateWeight( time );\n\n\t\tif ( weight > 0 ) {\n\n\t\t\tconst interpolants = this._interpolants;\n\t\t\tconst propertyMixers = this._propertyBindings;\n\n\t\t\tswitch ( this.blendMode ) {\n\n\t\t\t\tcase AdditiveAnimationBlendMode:\n\n\t\t\t\t\tfor ( let j = 0, m = interpolants.length; j !== m; ++ j ) {\n\n\t\t\t\t\t\tinterpolants[ j ].evaluate( clipTime );\n\t\t\t\t\t\tpropertyMixers[ j ].accumulateAdditive( weight );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase NormalAnimationBlendMode:\n\t\t\t\tdefault:\n\n\t\t\t\t\tfor ( let j = 0, m = interpolants.length; j !== m; ++ j ) {\n\n\t\t\t\t\t\tinterpolants[ j ].evaluate( clipTime );\n\t\t\t\t\t\tpropertyMixers[ j ].accumulate( accuIndex, weight );\n\n\t\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t_updateWeight( time ) {\n\n\t\tlet weight = 0;\n\n\t\tif ( this.enabled ) {\n\n\t\t\tweight = this.weight;\n\t\t\tconst interpolant = this._weightInterpolant;\n\n\t\t\tif ( interpolant !== null ) {\n\n\t\t\t\tconst interpolantValue = interpolant.evaluate( time )[ 0 ];\n\n\t\t\t\tweight *= interpolantValue;\n\n\t\t\t\tif ( time > interpolant.parameterPositions[ 1 ] ) {\n\n\t\t\t\t\tthis.stopFading();\n\n\t\t\t\t\tif ( interpolantValue === 0 ) {\n\n\t\t\t\t\t\t// faded out, disable\n\t\t\t\t\t\tthis.enabled = false;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis._effectiveWeight = weight;\n\t\treturn weight;\n\n\t}\n\n\t_updateTimeScale( time ) {\n\n\t\tlet timeScale = 0;\n\n\t\tif ( ! this.paused ) {\n\n\t\t\ttimeScale = this.timeScale;\n\n\t\t\tconst interpolant = this._timeScaleInterpolant;\n\n\t\t\tif ( interpolant !== null ) {\n\n\t\t\t\tconst interpolantValue = interpolant.evaluate( time )[ 0 ];\n\n\t\t\t\ttimeScale *= interpolantValue;\n\n\t\t\t\tif ( time > interpolant.parameterPositions[ 1 ] ) {\n\n\t\t\t\t\tthis.stopWarping();\n\n\t\t\t\t\tif ( timeScale === 0 ) {\n\n\t\t\t\t\t\t// motion has halted, pause\n\t\t\t\t\t\tthis.paused = true;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// warp done - apply final time scale\n\t\t\t\t\t\tthis.timeScale = timeScale;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis._effectiveTimeScale = timeScale;\n\t\treturn timeScale;\n\n\t}\n\n\t_updateTime( deltaTime ) {\n\n\t\tconst duration = this._clip.duration;\n\t\tconst loop = this.loop;\n\n\t\tlet time = this.time + deltaTime;\n\t\tlet loopCount = this._loopCount;\n\n\t\tconst pingPong = ( loop === LoopPingPong );\n\n\t\tif ( deltaTime === 0 ) {\n\n\t\t\tif ( loopCount === -1 ) return time;\n\n\t\t\treturn ( pingPong && ( loopCount & 1 ) === 1 ) ? duration - time : time;\n\n\t\t}\n\n\t\tif ( loop === LoopOnce ) {\n\n\t\t\tif ( loopCount === -1 ) {\n\n\t\t\t\t// just started\n\n\t\t\t\tthis._loopCount = 0;\n\t\t\t\tthis._setEndings( true, true, false );\n\n\t\t\t}\n\n\t\t\thandle_stop: {\n\n\t\t\t\tif ( time >= duration ) {\n\n\t\t\t\t\ttime = duration;\n\n\t\t\t\t} else if ( time < 0 ) {\n\n\t\t\t\t\ttime = 0;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis.time = time;\n\n\t\t\t\t\tbreak handle_stop;\n\n\t\t\t\t}\n\n\t\t\t\tif ( this.clampWhenFinished ) this.paused = true;\n\t\t\t\telse this.enabled = false;\n\n\t\t\t\tthis.time = time;\n\n\t\t\t\tthis._mixer.dispatchEvent( {\n\t\t\t\t\ttype: 'finished', action: this,\n\t\t\t\t\tdirection: deltaTime < 0 ? -1 : 1\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t} else { // repetitive Repeat or PingPong\n\n\t\t\tif ( loopCount === -1 ) {\n\n\t\t\t\t// just started\n\n\t\t\t\tif ( deltaTime >= 0 ) {\n\n\t\t\t\t\tloopCount = 0;\n\n\t\t\t\t\tthis._setEndings( true, this.repetitions === 0, pingPong );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// when looping in reverse direction, the initial\n\t\t\t\t\t// transition through zero counts as a repetition,\n\t\t\t\t\t// so leave loopCount at -1\n\n\t\t\t\t\tthis._setEndings( this.repetitions === 0, true, pingPong );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( time >= duration || time < 0 ) {\n\n\t\t\t\t// wrap around\n\n\t\t\t\tconst loopDelta = Math.floor( time / duration ); // signed\n\t\t\t\ttime -= duration * loopDelta;\n\n\t\t\t\tloopCount += Math.abs( loopDelta );\n\n\t\t\t\tconst pending = this.repetitions - loopCount;\n\n\t\t\t\tif ( pending <= 0 ) {\n\n\t\t\t\t\t// have to stop (switch state, clamp time, fire event)\n\n\t\t\t\t\tif ( this.clampWhenFinished ) this.paused = true;\n\t\t\t\t\telse this.enabled = false;\n\n\t\t\t\t\ttime = deltaTime > 0 ? duration : 0;\n\n\t\t\t\t\tthis.time = time;\n\n\t\t\t\t\tthis._mixer.dispatchEvent( {\n\t\t\t\t\t\ttype: 'finished', action: this,\n\t\t\t\t\t\tdirection: deltaTime > 0 ? 1 : -1\n\t\t\t\t\t} );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// keep running\n\n\t\t\t\t\tif ( pending === 1 ) {\n\n\t\t\t\t\t\t// entering the last round\n\n\t\t\t\t\t\tconst atStart = deltaTime < 0;\n\t\t\t\t\t\tthis._setEndings( atStart, ! atStart, pingPong );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tthis._setEndings( false, false, pingPong );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tthis._loopCount = loopCount;\n\n\t\t\t\t\tthis.time = time;\n\n\t\t\t\t\tthis._mixer.dispatchEvent( {\n\t\t\t\t\t\ttype: 'loop', action: this, loopDelta: loopDelta\n\t\t\t\t\t} );\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tthis.time = time;\n\n\t\t\t}\n\n\t\t\tif ( pingPong && ( loopCount & 1 ) === 1 ) {\n\n\t\t\t\t// invert time for the \"pong round\"\n\n\t\t\t\treturn duration - time;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn time;\n\n\t}\n\n\t_setEndings( atStart, atEnd, pingPong ) {\n\n\t\tconst settings = this._interpolantSettings;\n\n\t\tif ( pingPong ) {\n\n\t\t\tsettings.endingStart = ZeroSlopeEnding;\n\t\t\tsettings.endingEnd = ZeroSlopeEnding;\n\n\t\t} else {\n\n\t\t\t// assuming for LoopOnce atStart == atEnd == true\n\n\t\t\tif ( atStart ) {\n\n\t\t\t\tsettings.endingStart = this.zeroSlopeAtStart ? ZeroSlopeEnding : ZeroCurvatureEnding;\n\n\t\t\t} else {\n\n\t\t\t\tsettings.endingStart = WrapAroundEnding;\n\n\t\t\t}\n\n\t\t\tif ( atEnd ) {\n\n\t\t\t\tsettings.endingEnd = this.zeroSlopeAtEnd ? ZeroSlopeEnding : ZeroCurvatureEnding;\n\n\t\t\t} else {\n\n\t\t\t\tsettings.endingEnd \t = WrapAroundEnding;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t_scheduleFading( duration, weightNow, weightThen ) {\n\n\t\tconst mixer = this._mixer, now = mixer.time;\n\t\tlet interpolant = this._weightInterpolant;\n\n\t\tif ( interpolant === null ) {\n\n\t\t\tinterpolant = mixer._lendControlInterpolant();\n\t\t\tthis._weightInterpolant = interpolant;\n\n\t\t}\n\n\t\tconst times = interpolant.parameterPositions,\n\t\t\tvalues = interpolant.sampleValues;\n\n\t\ttimes[ 0 ] = now;\n\t\tvalues[ 0 ] = weightNow;\n\t\ttimes[ 1 ] = now + duration;\n\t\tvalues[ 1 ] = weightThen;\n\n\t\treturn this;\n\n\t}\n\n}\n\nconst _controlInterpolantsResultBuffer = new Float32Array( 1 );\n\n/**\n * `AnimationMixer` is a player for animations on a particular object in\n * the scene. When multiple objects in the scene are animated independently,\n * one `AnimationMixer` may be used for each object.\n */\nclass AnimationMixer extends EventDispatcher {\n\n\t/**\n\t * Constructs a new animation mixer.\n\t *\n\t * @param {Object3D} root - The object whose animations shall be played by this mixer.\n\t */\n\tconstructor( root ) {\n\n\t\tsuper();\n\n\t\tthis._root = root;\n\t\tthis._initMemoryManager();\n\t\tthis._accuIndex = 0;\n\n\t\t/**\n\t\t * The global mixer time (in seconds; starting with `0` on the mixer's creation).\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.time = 0;\n\n\t\t/**\n\t\t * A scaling factor for the global time.\n\t\t *\n\t\t * Note: Setting this member to `0` and later back to `1` is a\n\t\t * possibility to pause/unpause all actions that are controlled by this\n\t\t * mixer.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.timeScale = 1.0;\n\n\t}\n\n\t_bindAction( action, prototypeAction ) {\n\n\t\tconst root = action._localRoot || this._root,\n\t\t\ttracks = action._clip.tracks,\n\t\t\tnTracks = tracks.length,\n\t\t\tbindings = action._propertyBindings,\n\t\t\tinterpolants = action._interpolants,\n\t\t\trootUuid = root.uuid,\n\t\t\tbindingsByRoot = this._bindingsByRootAndName;\n\n\t\tlet bindingsByName = bindingsByRoot[ rootUuid ];\n\n\t\tif ( bindingsByName === undefined ) {\n\n\t\t\tbindingsByName = {};\n\t\t\tbindingsByRoot[ rootUuid ] = bindingsByName;\n\n\t\t}\n\n\t\tfor ( let i = 0; i !== nTracks; ++ i ) {\n\n\t\t\tconst track = tracks[ i ],\n\t\t\t\ttrackName = track.name;\n\n\t\t\tlet binding = bindingsByName[ trackName ];\n\n\t\t\tif ( binding !== undefined ) {\n\n\t\t\t\t++ binding.referenceCount;\n\t\t\t\tbindings[ i ] = binding;\n\n\t\t\t} else {\n\n\t\t\t\tbinding = bindings[ i ];\n\n\t\t\t\tif ( binding !== undefined ) {\n\n\t\t\t\t\t// existing binding, make sure the cache knows\n\n\t\t\t\t\tif ( binding._cacheIndex === null ) {\n\n\t\t\t\t\t\t++ binding.referenceCount;\n\t\t\t\t\t\tthis._addInactiveBinding( binding, rootUuid, trackName );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tcontinue;\n\n\t\t\t\t}\n\n\t\t\t\tconst path = prototypeAction && prototypeAction.\n\t\t\t\t\t_propertyBindings[ i ].binding.parsedPath;\n\n\t\t\t\tbinding = new PropertyMixer(\n\t\t\t\t\tPropertyBinding.create( root, trackName, path ),\n\t\t\t\t\ttrack.ValueTypeName, track.getValueSize() );\n\n\t\t\t\t++ binding.referenceCount;\n\t\t\t\tthis._addInactiveBinding( binding, rootUuid, trackName );\n\n\t\t\t\tbindings[ i ] = binding;\n\n\t\t\t}\n\n\t\t\tinterpolants[ i ].resultBuffer = binding.buffer;\n\n\t\t}\n\n\t}\n\n\t_activateAction( action ) {\n\n\t\tif ( ! this._isActiveAction( action ) ) {\n\n\t\t\tif ( action._cacheIndex === null ) {\n\n\t\t\t\t// this action has been forgotten by the cache, but the user\n\t\t\t\t// appears to be still using it -> rebind\n\n\t\t\t\tconst rootUuid = ( action._localRoot || this._root ).uuid,\n\t\t\t\t\tclipUuid = action._clip.uuid,\n\t\t\t\t\tactionsForClip = this._actionsByClip[ clipUuid ];\n\n\t\t\t\tthis._bindAction( action,\n\t\t\t\t\tactionsForClip && actionsForClip.knownActions[ 0 ] );\n\n\t\t\t\tthis._addInactiveAction( action, clipUuid, rootUuid );\n\n\t\t\t}\n\n\t\t\tconst bindings = action._propertyBindings;\n\n\t\t\t// increment reference counts / sort out state\n\t\t\tfor ( let i = 0, n = bindings.length; i !== n; ++ i ) {\n\n\t\t\t\tconst binding = bindings[ i ];\n\n\t\t\t\tif ( binding.useCount ++ === 0 ) {\n\n\t\t\t\t\tthis._lendBinding( binding );\n\t\t\t\t\tbinding.saveOriginalState();\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis._lendAction( action );\n\n\t\t}\n\n\t}\n\n\t_deactivateAction( action ) {\n\n\t\tif ( this._isActiveAction( action ) ) {\n\n\t\t\tconst bindings = action._propertyBindings;\n\n\t\t\t// decrement reference counts / sort out state\n\t\t\tfor ( let i = 0, n = bindings.length; i !== n; ++ i ) {\n\n\t\t\t\tconst binding = bindings[ i ];\n\n\t\t\t\tif ( -- binding.useCount === 0 ) {\n\n\t\t\t\t\tbinding.restoreOriginalState();\n\t\t\t\t\tthis._takeBackBinding( binding );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis._takeBackAction( action );\n\n\t\t}\n\n\t}\n\n\t// Memory manager\n\n\t_initMemoryManager() {\n\n\t\tthis._actions = []; // 'nActiveActions' followed by inactive ones\n\t\tthis._nActiveActions = 0;\n\n\t\tthis._actionsByClip = {};\n\t\t// inside:\n\t\t// {\n\t\t// \tknownActions: Array< AnimationAction > - used as prototypes\n\t\t// \tactionByRoot: AnimationAction - lookup\n\t\t// }\n\n\n\t\tthis._bindings = []; // 'nActiveBindings' followed by inactive ones\n\t\tthis._nActiveBindings = 0;\n\n\t\tthis._bindingsByRootAndName = {}; // inside: Map< name, PropertyMixer >\n\n\n\t\tthis._controlInterpolants = []; // same game as above\n\t\tthis._nActiveControlInterpolants = 0;\n\n\t\tconst scope = this;\n\n\t\tthis.stats = {\n\n\t\t\tactions: {\n\t\t\t\tget total() {\n\n\t\t\t\t\treturn scope._actions.length;\n\n\t\t\t\t},\n\t\t\t\tget inUse() {\n\n\t\t\t\t\treturn scope._nActiveActions;\n\n\t\t\t\t}\n\t\t\t},\n\t\t\tbindings: {\n\t\t\t\tget total() {\n\n\t\t\t\t\treturn scope._bindings.length;\n\n\t\t\t\t},\n\t\t\t\tget inUse() {\n\n\t\t\t\t\treturn scope._nActiveBindings;\n\n\t\t\t\t}\n\t\t\t},\n\t\t\tcontrolInterpolants: {\n\t\t\t\tget total() {\n\n\t\t\t\t\treturn scope._controlInterpolants.length;\n\n\t\t\t\t},\n\t\t\t\tget inUse() {\n\n\t\t\t\t\treturn scope._nActiveControlInterpolants;\n\n\t\t\t\t}\n\t\t\t}\n\n\t\t};\n\n\t}\n\n\t// Memory management for AnimationAction objects\n\n\t_isActiveAction( action ) {\n\n\t\tconst index = action._cacheIndex;\n\t\treturn index !== null && index < this._nActiveActions;\n\n\t}\n\n\t_addInactiveAction( action, clipUuid, rootUuid ) {\n\n\t\tconst actions = this._actions,\n\t\t\tactionsByClip = this._actionsByClip;\n\n\t\tlet actionsForClip = actionsByClip[ clipUuid ];\n\n\t\tif ( actionsForClip === undefined ) {\n\n\t\t\tactionsForClip = {\n\n\t\t\t\tknownActions: [ action ],\n\t\t\t\tactionByRoot: {}\n\n\t\t\t};\n\n\t\t\taction._byClipCacheIndex = 0;\n\n\t\t\tactionsByClip[ clipUuid ] = actionsForClip;\n\n\t\t} else {\n\n\t\t\tconst knownActions = actionsForClip.knownActions;\n\n\t\t\taction._byClipCacheIndex = knownActions.length;\n\t\t\tknownActions.push( action );\n\n\t\t}\n\n\t\taction._cacheIndex = actions.length;\n\t\tactions.push( action );\n\n\t\tactionsForClip.actionByRoot[ rootUuid ] = action;\n\n\t}\n\n\t_removeInactiveAction( action ) {\n\n\t\tconst actions = this._actions,\n\t\t\tlastInactiveAction = actions[ actions.length - 1 ],\n\t\t\tcacheIndex = action._cacheIndex;\n\n\t\tlastInactiveAction._cacheIndex = cacheIndex;\n\t\tactions[ cacheIndex ] = lastInactiveAction;\n\t\tactions.pop();\n\n\t\taction._cacheIndex = null;\n\n\n\t\tconst clipUuid = action._clip.uuid,\n\t\t\tactionsByClip = this._actionsByClip,\n\t\t\tactionsForClip = actionsByClip[ clipUuid ],\n\t\t\tknownActionsForClip = actionsForClip.knownActions,\n\n\t\t\tlastKnownAction =\n\t\t\t\tknownActionsForClip[ knownActionsForClip.length - 1 ],\n\n\t\t\tbyClipCacheIndex = action._byClipCacheIndex;\n\n\t\tlastKnownAction._byClipCacheIndex = byClipCacheIndex;\n\t\tknownActionsForClip[ byClipCacheIndex ] = lastKnownAction;\n\t\tknownActionsForClip.pop();\n\n\t\taction._byClipCacheIndex = null;\n\n\n\t\tconst actionByRoot = actionsForClip.actionByRoot,\n\t\t\trootUuid = ( action._localRoot || this._root ).uuid;\n\n\t\tdelete actionByRoot[ rootUuid ];\n\n\t\tif ( knownActionsForClip.length === 0 ) {\n\n\t\t\tdelete actionsByClip[ clipUuid ];\n\n\t\t}\n\n\t\tthis._removeInactiveBindingsForAction( action );\n\n\t}\n\n\t_removeInactiveBindingsForAction( action ) {\n\n\t\tconst bindings = action._propertyBindings;\n\n\t\tfor ( let i = 0, n = bindings.length; i !== n; ++ i ) {\n\n\t\t\tconst binding = bindings[ i ];\n\n\t\t\tif ( -- binding.referenceCount === 0 ) {\n\n\t\t\t\tthis._removeInactiveBinding( binding );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t_lendAction( action ) {\n\n\t\t// [ active actions |  inactive actions  ]\n\t\t// [  active actions >| inactive actions ]\n\t\t//                 s        a\n\t\t//                  <-swap->\n\t\t//                 a        s\n\n\t\tconst actions = this._actions,\n\t\t\tprevIndex = action._cacheIndex,\n\n\t\t\tlastActiveIndex = this._nActiveActions ++,\n\n\t\t\tfirstInactiveAction = actions[ lastActiveIndex ];\n\n\t\taction._cacheIndex = lastActiveIndex;\n\t\tactions[ lastActiveIndex ] = action;\n\n\t\tfirstInactiveAction._cacheIndex = prevIndex;\n\t\tactions[ prevIndex ] = firstInactiveAction;\n\n\t}\n\n\t_takeBackAction( action ) {\n\n\t\t// [  active actions  | inactive actions ]\n\t\t// [ active actions |< inactive actions  ]\n\t\t//        a        s\n\t\t//         <-swap->\n\t\t//        s        a\n\n\t\tconst actions = this._actions,\n\t\t\tprevIndex = action._cacheIndex,\n\n\t\t\tfirstInactiveIndex = -- this._nActiveActions,\n\n\t\t\tlastActiveAction = actions[ firstInactiveIndex ];\n\n\t\taction._cacheIndex = firstInactiveIndex;\n\t\tactions[ firstInactiveIndex ] = action;\n\n\t\tlastActiveAction._cacheIndex = prevIndex;\n\t\tactions[ prevIndex ] = lastActiveAction;\n\n\t}\n\n\t// Memory management for PropertyMixer objects\n\n\t_addInactiveBinding( binding, rootUuid, trackName ) {\n\n\t\tconst bindingsByRoot = this._bindingsByRootAndName,\n\t\t\tbindings = this._bindings;\n\n\t\tlet bindingByName = bindingsByRoot[ rootUuid ];\n\n\t\tif ( bindingByName === undefined ) {\n\n\t\t\tbindingByName = {};\n\t\t\tbindingsByRoot[ rootUuid ] = bindingByName;\n\n\t\t}\n\n\t\tbindingByName[ trackName ] = binding;\n\n\t\tbinding._cacheIndex = bindings.length;\n\t\tbindings.push( binding );\n\n\t}\n\n\t_removeInactiveBinding( binding ) {\n\n\t\tconst bindings = this._bindings,\n\t\t\tpropBinding = binding.binding,\n\t\t\trootUuid = propBinding.rootNode.uuid,\n\t\t\ttrackName = propBinding.path,\n\t\t\tbindingsByRoot = this._bindingsByRootAndName,\n\t\t\tbindingByName = bindingsByRoot[ rootUuid ],\n\n\t\t\tlastInactiveBinding = bindings[ bindings.length - 1 ],\n\t\t\tcacheIndex = binding._cacheIndex;\n\n\t\tlastInactiveBinding._cacheIndex = cacheIndex;\n\t\tbindings[ cacheIndex ] = lastInactiveBinding;\n\t\tbindings.pop();\n\n\t\tdelete bindingByName[ trackName ];\n\n\t\tif ( Object.keys( bindingByName ).length === 0 ) {\n\n\t\t\tdelete bindingsByRoot[ rootUuid ];\n\n\t\t}\n\n\t}\n\n\t_lendBinding( binding ) {\n\n\t\tconst bindings = this._bindings,\n\t\t\tprevIndex = binding._cacheIndex,\n\n\t\t\tlastActiveIndex = this._nActiveBindings ++,\n\n\t\t\tfirstInactiveBinding = bindings[ lastActiveIndex ];\n\n\t\tbinding._cacheIndex = lastActiveIndex;\n\t\tbindings[ lastActiveIndex ] = binding;\n\n\t\tfirstInactiveBinding._cacheIndex = prevIndex;\n\t\tbindings[ prevIndex ] = firstInactiveBinding;\n\n\t}\n\n\t_takeBackBinding( binding ) {\n\n\t\tconst bindings = this._bindings,\n\t\t\tprevIndex = binding._cacheIndex,\n\n\t\t\tfirstInactiveIndex = -- this._nActiveBindings,\n\n\t\t\tlastActiveBinding = bindings[ firstInactiveIndex ];\n\n\t\tbinding._cacheIndex = firstInactiveIndex;\n\t\tbindings[ firstInactiveIndex ] = binding;\n\n\t\tlastActiveBinding._cacheIndex = prevIndex;\n\t\tbindings[ prevIndex ] = lastActiveBinding;\n\n\t}\n\n\n\t// Memory management of Interpolants for weight and time scale\n\n\t_lendControlInterpolant() {\n\n\t\tconst interpolants = this._controlInterpolants,\n\t\t\tlastActiveIndex = this._nActiveControlInterpolants ++;\n\n\t\tlet interpolant = interpolants[ lastActiveIndex ];\n\n\t\tif ( interpolant === undefined ) {\n\n\t\t\tinterpolant = new LinearInterpolant(\n\t\t\t\tnew Float32Array( 2 ), new Float32Array( 2 ),\n\t\t\t\t1, _controlInterpolantsResultBuffer );\n\n\t\t\tinterpolant.__cacheIndex = lastActiveIndex;\n\t\t\tinterpolants[ lastActiveIndex ] = interpolant;\n\n\t\t}\n\n\t\treturn interpolant;\n\n\t}\n\n\t_takeBackControlInterpolant( interpolant ) {\n\n\t\tconst interpolants = this._controlInterpolants,\n\t\t\tprevIndex = interpolant.__cacheIndex,\n\n\t\t\tfirstInactiveIndex = -- this._nActiveControlInterpolants,\n\n\t\t\tlastActiveInterpolant = interpolants[ firstInactiveIndex ];\n\n\t\tinterpolant.__cacheIndex = firstInactiveIndex;\n\t\tinterpolants[ firstInactiveIndex ] = interpolant;\n\n\t\tlastActiveInterpolant.__cacheIndex = prevIndex;\n\t\tinterpolants[ prevIndex ] = lastActiveInterpolant;\n\n\t}\n\n\t/**\n\t * Returns an instance of {@link AnimationAction} for the passed clip.\n\t *\n\t * If an action fitting the clip and root parameters doesn't yet exist, it\n\t * will be created by this method. Calling this method several times with the\n\t * same clip and root parameters always returns the same action.\n\t *\n\t * @param {AnimationClip|string} clip - An animation clip or alternatively the name of the animation clip.\n\t * @param {Object3D} [optionalRoot] - An alternative root object.\n\t * @param {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)} [blendMode] - The blend mode.\n\t * @return {?AnimationAction} The animation action.\n\t */\n\tclipAction( clip, optionalRoot, blendMode ) {\n\n\t\tconst root = optionalRoot || this._root,\n\t\t\trootUuid = root.uuid;\n\n\t\tlet clipObject = typeof clip === 'string' ? AnimationClip.findByName( root, clip ) : clip;\n\n\t\tconst clipUuid = clipObject !== null ? clipObject.uuid : clip;\n\n\t\tconst actionsForClip = this._actionsByClip[ clipUuid ];\n\t\tlet prototypeAction = null;\n\n\t\tif ( blendMode === undefined ) {\n\n\t\t\tif ( clipObject !== null ) {\n\n\t\t\t\tblendMode = clipObject.blendMode;\n\n\t\t\t} else {\n\n\t\t\t\tblendMode = NormalAnimationBlendMode;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( actionsForClip !== undefined ) {\n\n\t\t\tconst existingAction = actionsForClip.actionByRoot[ rootUuid ];\n\n\t\t\tif ( existingAction !== undefined && existingAction.blendMode === blendMode ) {\n\n\t\t\t\treturn existingAction;\n\n\t\t\t}\n\n\t\t\t// we know the clip, so we don't have to parse all\n\t\t\t// the bindings again but can just copy\n\t\t\tprototypeAction = actionsForClip.knownActions[ 0 ];\n\n\t\t\t// also, take the clip from the prototype action\n\t\t\tif ( clipObject === null )\n\t\t\t\tclipObject = prototypeAction._clip;\n\n\t\t}\n\n\t\t// clip must be known when specified via string\n\t\tif ( clipObject === null ) return null;\n\n\t\t// allocate all resources required to run it\n\t\tconst newAction = new AnimationAction( this, clipObject, optionalRoot, blendMode );\n\n\t\tthis._bindAction( newAction, prototypeAction );\n\n\t\t// and make the action known to the memory manager\n\t\tthis._addInactiveAction( newAction, clipUuid, rootUuid );\n\n\t\treturn newAction;\n\n\t}\n\n\t/**\n\t * Returns an existing animation action for the passed clip.\n\t *\n\t * @param {AnimationClip|string} clip - An animation clip or alternatively the name of the animation clip.\n\t * @param {Object3D} [optionalRoot] - An alternative root object.\n\t * @return {?AnimationAction} The animation action. Returns `null` if no action was found.\n\t */\n\texistingAction( clip, optionalRoot ) {\n\n\t\tconst root = optionalRoot || this._root,\n\t\t\trootUuid = root.uuid,\n\n\t\t\tclipObject = typeof clip === 'string' ?\n\t\t\t\tAnimationClip.findByName( root, clip ) : clip,\n\n\t\t\tclipUuid = clipObject ? clipObject.uuid : clip,\n\n\t\t\tactionsForClip = this._actionsByClip[ clipUuid ];\n\n\t\tif ( actionsForClip !== undefined ) {\n\n\t\t\treturn actionsForClip.actionByRoot[ rootUuid ] || null;\n\n\t\t}\n\n\t\treturn null;\n\n\t}\n\n\t/**\n\t * Deactivates all previously scheduled actions on this mixer.\n\t *\n\t * @return {AnimationMixer} A reference to this animation mixer.\n\t */\n\tstopAllAction() {\n\n\t\tconst actions = this._actions,\n\t\t\tnActions = this._nActiveActions;\n\n\t\tfor ( let i = nActions - 1; i >= 0; -- i ) {\n\n\t\t\tactions[ i ].stop();\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Advances the global mixer time and updates the animation.\n\t *\n\t * This is usually done in the render loop by passing the delta\n\t * time from {@link Clock} or {@link Timer}.\n\t *\n\t * @param {number} deltaTime - The delta time in seconds.\n\t * @return {AnimationMixer} A reference to this animation mixer.\n\t */\n\tupdate( deltaTime ) {\n\n\t\tdeltaTime *= this.timeScale;\n\n\t\tconst actions = this._actions,\n\t\t\tnActions = this._nActiveActions,\n\n\t\t\ttime = this.time += deltaTime,\n\t\t\ttimeDirection = Math.sign( deltaTime ),\n\n\t\t\taccuIndex = this._accuIndex ^= 1;\n\n\t\t// run active actions\n\n\t\tfor ( let i = 0; i !== nActions; ++ i ) {\n\n\t\t\tconst action = actions[ i ];\n\n\t\t\taction._update( time, deltaTime, timeDirection, accuIndex );\n\n\t\t}\n\n\t\t// update scene graph\n\n\t\tconst bindings = this._bindings,\n\t\t\tnBindings = this._nActiveBindings;\n\n\t\tfor ( let i = 0; i !== nBindings; ++ i ) {\n\n\t\t\tbindings[ i ].apply( accuIndex );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the global mixer to a specific time and updates the animation accordingly.\n\t *\n\t * This is useful when you need to jump to an exact time in an animation. The\n\t * input parameter will be scaled by {@link AnimationMixer#timeScale}\n\t *\n\t * @param {number} time - The time to set in seconds.\n\t * @return {AnimationMixer} A reference to this animation mixer.\n\t */\n\tsetTime( time ) {\n\n\t\tthis.time = 0; // Zero out time attribute for AnimationMixer object;\n\t\tfor ( let i = 0; i < this._actions.length; i ++ ) {\n\n\t\t\tthis._actions[ i ].time = 0; // Zero out time attribute for all associated AnimationAction objects.\n\n\t\t}\n\n\t\treturn this.update( time ); // Update used to set exact time. Returns \"this\" AnimationMixer object.\n\n\t}\n\n\t/**\n\t * Returns this mixer's root object.\n\t *\n\t * @return {Object3D} The mixer's root object.\n\t */\n\tgetRoot() {\n\n\t\treturn this._root;\n\n\t}\n\n\t/**\n\t * Deallocates all memory resources for a clip. Before using this method make\n\t * sure to call {@link AnimationAction#stop} for all related actions.\n\t *\n\t * @param {AnimationClip} clip - The clip to uncache.\n\t */\n\tuncacheClip( clip ) {\n\n\t\tconst actions = this._actions,\n\t\t\tclipUuid = clip.uuid,\n\t\t\tactionsByClip = this._actionsByClip,\n\t\t\tactionsForClip = actionsByClip[ clipUuid ];\n\n\t\tif ( actionsForClip !== undefined ) {\n\n\t\t\t// note: just calling _removeInactiveAction would mess up the\n\t\t\t// iteration state and also require updating the state we can\n\t\t\t// just throw away\n\n\t\t\tconst actionsToRemove = actionsForClip.knownActions;\n\n\t\t\tfor ( let i = 0, n = actionsToRemove.length; i !== n; ++ i ) {\n\n\t\t\t\tconst action = actionsToRemove[ i ];\n\n\t\t\t\tthis._deactivateAction( action );\n\n\t\t\t\tconst cacheIndex = action._cacheIndex,\n\t\t\t\t\tlastInactiveAction = actions[ actions.length - 1 ];\n\n\t\t\t\taction._cacheIndex = null;\n\t\t\t\taction._byClipCacheIndex = null;\n\n\t\t\t\tlastInactiveAction._cacheIndex = cacheIndex;\n\t\t\t\tactions[ cacheIndex ] = lastInactiveAction;\n\t\t\t\tactions.pop();\n\n\t\t\t\tthis._removeInactiveBindingsForAction( action );\n\n\t\t\t}\n\n\t\t\tdelete actionsByClip[ clipUuid ];\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Deallocates all memory resources for a root object. Before using this\n\t * method make sure to call {@link AnimationAction#stop} for all related\n\t * actions or alternatively {@link AnimationMixer#stopAllAction} when the\n\t * mixer operates on a single root.\n\t *\n\t * @param {Object3D} root - The root object to uncache.\n\t */\n\tuncacheRoot( root ) {\n\n\t\tconst rootUuid = root.uuid,\n\t\t\tactionsByClip = this._actionsByClip;\n\n\t\tfor ( const clipUuid in actionsByClip ) {\n\n\t\t\tconst actionByRoot = actionsByClip[ clipUuid ].actionByRoot,\n\t\t\t\taction = actionByRoot[ rootUuid ];\n\n\t\t\tif ( action !== undefined ) {\n\n\t\t\t\tthis._deactivateAction( action );\n\t\t\t\tthis._removeInactiveAction( action );\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst bindingsByRoot = this._bindingsByRootAndName,\n\t\t\tbindingByName = bindingsByRoot[ rootUuid ];\n\n\t\tif ( bindingByName !== undefined ) {\n\n\t\t\tfor ( const trackName in bindingByName ) {\n\n\t\t\t\tconst binding = bindingByName[ trackName ];\n\t\t\t\tbinding.restoreOriginalState();\n\t\t\t\tthis._removeInactiveBinding( binding );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Deallocates all memory resources for an action. The action is identified by the\n\t * given clip and an optional root object. Before using this method make\n\t * sure to call {@link AnimationAction#stop} to deactivate the action.\n\t *\n\t * @param {AnimationClip|string} clip - An animation clip or alternatively the name of the animation clip.\n\t * @param {Object3D} [optionalRoot] - An alternative root object.\n\t */\n\tuncacheAction( clip, optionalRoot ) {\n\n\t\tconst action = this.existingAction( clip, optionalRoot );\n\n\t\tif ( action !== null ) {\n\n\t\t\tthis._deactivateAction( action );\n\t\t\tthis._removeInactiveAction( action );\n\n\t\t}\n\n\t}\n\n}\n\n/**\n * Represents a 3D render target.\n *\n * @augments RenderTarget\n */\nclass RenderTarget3D extends RenderTarget {\n\n\t/**\n\t * Constructs a new 3D render target.\n\t *\n\t * @param {number} [width=1] - The width of the render target.\n\t * @param {number} [height=1] - The height of the render target.\n\t * @param {number} [depth=1] - The height of the render target.\n\t * @param {RenderTarget~Options} [options] - The configuration object.\n\t */\n\tconstructor( width = 1, height = 1, depth = 1, options = {} ) {\n\n\t\tsuper( width, height, options );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isRenderTarget3D = true;\n\n\t\tthis.depth = depth;\n\n\t\t/**\n\t\t * Overwritten with a different texture type.\n\t\t *\n\t\t * @type {Data3DTexture}\n\t\t */\n\t\tthis.texture = new Data3DTexture( null, width, height, depth );\n\t\tthis._setTextureOptions( options );\n\n\t\tthis.texture.isRenderTargetTexture = true;\n\n\t}\n\n}\n\n/**\n * Represents a uniform which is a global shader variable. They are passed to shader programs.\n *\n * When declaring a uniform of a {@link ShaderMaterial}, it is declared by value or by object.\n * ```js\n * uniforms: {\n * \ttime: { value: 1.0 },\n * \tresolution: new Uniform( new Vector2() )\n * };\n * ```\n * Since this class can only be used in context of {@link ShaderMaterial}, it is only supported\n * in {@link WebGLRenderer}.\n */\nclass Uniform {\n\n\t/**\n\t * Constructs a new uniform.\n\t *\n\t * @param {any} value - The uniform value.\n\t */\n\tconstructor( value ) {\n\n\t\t/**\n\t\t * The uniform value.\n\t\t *\n\t\t * @type {any}\n\t\t */\n\t\tthis.value = value;\n\n\t}\n\n\t/**\n\t * Returns a new uniform with copied values from this instance.\n\t * If the value has a `clone()` method, the value is cloned as well.\n\t *\n\t * @return {Uniform} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new Uniform( this.value.clone === undefined ? this.value : this.value.clone() );\n\n\t}\n\n}\n\nlet _id = 0;\n\n/**\n * A class for managing multiple uniforms in a single group. The renderer will process\n * such a definition as a single UBO.\n *\n * Since this class can only be used in context of {@link ShaderMaterial}, it is only supported\n * in {@link WebGLRenderer}.\n *\n * @augments EventDispatcher\n */\nclass UniformsGroup extends EventDispatcher {\n\n\t/**\n\t * Constructs a new uniforms group.\n\t */\n\tconstructor() {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isUniformsGroup = true;\n\n\t\t/**\n\t\t * The ID of the 3D object.\n\t\t *\n\t\t * @name UniformsGroup#id\n\t\t * @type {number}\n\t\t * @readonly\n\t\t */\n\t\tObject.defineProperty( this, 'id', { value: _id ++ } );\n\n\t\t/**\n\t\t * The name of the uniforms group.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\n\t\t/**\n\t\t * The buffer usage.\n\t\t *\n\t\t * @type {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)}\n\t\t * @default StaticDrawUsage\n\t\t */\n\t\tthis.usage = StaticDrawUsage;\n\n\t\t/**\n\t\t * An array holding the uniforms.\n\t\t *\n\t\t * @type {Array<Uniform>}\n\t\t */\n\t\tthis.uniforms = [];\n\n\t}\n\n\t/**\n\t * Adds the given uniform to this uniforms group.\n\t *\n\t * @param {Uniform} uniform - The uniform to add.\n\t * @return {UniformsGroup} A reference to this uniforms group.\n\t */\n\tadd( uniform ) {\n\n\t\tthis.uniforms.push( uniform );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Removes the given uniform from this uniforms group.\n\t *\n\t * @param {Uniform} uniform - The uniform to remove.\n\t * @return {UniformsGroup} A reference to this uniforms group.\n\t */\n\tremove( uniform ) {\n\n\t\tconst index = this.uniforms.indexOf( uniform );\n\n\t\tif ( index !== -1 ) this.uniforms.splice( index, 1 );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the name of this uniforms group.\n\t *\n\t * @param {string} name - The name to set.\n\t * @return {UniformsGroup} A reference to this uniforms group.\n\t */\n\tsetName( name ) {\n\n\t\tthis.name = name;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the usage of this uniforms group.\n\t *\n\t * @param {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)} value - The usage to set.\n\t * @return {UniformsGroup} A reference to this uniforms group.\n\t */\n\tsetUsage( value ) {\n\n\t\tthis.usage = value;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t *\n\t * @fires Texture#dispose\n\t */\n\tdispose() {\n\n\t\tthis.dispatchEvent( { type: 'dispose' } );\n\n\t}\n\n\t/**\n\t * Copies the values of the given uniforms group to this instance.\n\t *\n\t * @param {UniformsGroup} source - The uniforms group to copy.\n\t * @return {UniformsGroup} A reference to this uniforms group.\n\t */\n\tcopy( source ) {\n\n\t\tthis.name = source.name;\n\t\tthis.usage = source.usage;\n\n\t\tconst uniformsSource = source.uniforms;\n\n\t\tthis.uniforms.length = 0;\n\n\t\tfor ( let i = 0, l = uniformsSource.length; i < l; i ++ ) {\n\n\t\t\tconst uniforms = Array.isArray( uniformsSource[ i ] ) ? uniformsSource[ i ] : [ uniformsSource[ i ] ];\n\n\t\t\tfor ( let j = 0; j < uniforms.length; j ++ ) {\n\n\t\t\t\tthis.uniforms.push( uniforms[ j ].clone() );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new uniforms group with copied values from this instance.\n\t *\n\t * @return {UniformsGroup} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n}\n\n/**\n * An instanced version of an interleaved buffer.\n *\n * @augments InterleavedBuffer\n */\nclass InstancedInterleavedBuffer extends InterleavedBuffer {\n\n\t/**\n\t * Constructs a new instanced interleaved buffer.\n\t *\n\t * @param {TypedArray} array - A typed array with a shared buffer storing attribute data.\n\t * @param {number} stride - The number of typed-array elements per vertex.\n\t * @param {number} [meshPerAttribute=1] - Defines how often a value of this interleaved buffer should be repeated.\n\t */\n\tconstructor( array, stride, meshPerAttribute = 1 ) {\n\n\t\tsuper( array, stride );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isInstancedInterleavedBuffer = true;\n\n\t\t/**\n\t\t * Defines how often a value of this buffer attribute should be repeated,\n\t\t * see {@link InstancedBufferAttribute#meshPerAttribute}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.meshPerAttribute = meshPerAttribute;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\n\t\tthis.meshPerAttribute = source.meshPerAttribute;\n\n\t\treturn this;\n\n\t}\n\n\tclone( data ) {\n\n\t\tconst ib = super.clone( data );\n\n\t\tib.meshPerAttribute = this.meshPerAttribute;\n\n\t\treturn ib;\n\n\t}\n\n\ttoJSON( data ) {\n\n\t\tconst json = super.toJSON( data );\n\n\t\tjson.isInstancedInterleavedBuffer = true;\n\t\tjson.meshPerAttribute = this.meshPerAttribute;\n\n\t\treturn json;\n\n\t}\n\n}\n\n/**\n * An alternative version of a buffer attribute with more control over the VBO.\n *\n * The renderer does not construct a VBO for this kind of attribute. Instead, it uses\n * whatever VBO is passed in constructor and can later be altered via the `buffer` property.\n *\n * The most common use case for this class is when some kind of GPGPU calculation interferes\n * or even produces the VBOs in question.\n *\n * Notice that this class can only be used with {@link WebGLRenderer}.\n */\nclass GLBufferAttribute {\n\n\t/**\n\t * Constructs a new GL buffer attribute.\n\t *\n\t * @param {WebGLBuffer} buffer - The native WebGL buffer.\n\t * @param {number} type - The native data type (e.g. `gl.FLOAT`).\n\t * @param {number} itemSize - The item size.\n\t * @param {number} elementSize - The corresponding size (in bytes) for the given `type` parameter.\n\t * @param {number} count - The expected number of vertices in VBO.\n\t * @param {boolean} [normalized=false] - Whether the data are normalized or not.\n\t */\n\tconstructor( buffer, type, itemSize, elementSize, count, normalized = false ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isGLBufferAttribute = true;\n\n\t\t/**\n\t\t * The name of the buffer attribute.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tthis.name = '';\n\n\t\t/**\n\t\t * The native WebGL buffer.\n\t\t *\n\t\t * @type {WebGLBuffer}\n\t\t */\n\t\tthis.buffer = buffer;\n\n\t\t/**\n\t\t * The native data type.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.type = type;\n\n\t\t/**\n\t\t * The item size, see {@link BufferAttribute#itemSize}.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.itemSize = itemSize;\n\n\t\t/**\n\t\t * The corresponding size (in bytes) for the given `type` parameter.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.elementSize = elementSize;\n\n\t\t/**\n\t\t * The expected number of vertices in VBO.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.count = count;\n\n\t\t/**\n\t\t * Applies to integer data only. Indicates how the underlying data in the buffer maps to\n\t\t * the values in the GLSL code. For instance, if `buffer` contains data of `gl.UNSIGNED_SHORT`,\n\t\t * and `normalized` is `true`, the values `0 - +65535` in the buffer data will be mapped to\n\t\t * `0.0f - +1.0f` in the GLSL attribute. If `normalized` is `false`, the values will be converted\n\t\t * to floats unmodified, i.e. `65535` becomes `65535.0f`.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tthis.normalized = normalized;\n\n\t\t/**\n\t\t * A version number, incremented every time the `needsUpdate` is set to `true`.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.version = 0;\n\n\t}\n\n\t/**\n\t * Flag to indicate that this attribute has changed and should be re-sent to\n\t * the GPU. Set this to `true` when you modify the value of the array.\n\t *\n\t * @type {number}\n\t * @default false\n\t * @param {boolean} value\n\t */\n\tset needsUpdate( value ) {\n\n\t\tif ( value === true ) this.version ++;\n\n\t}\n\n\t/**\n\t * Sets the given native WebGL buffer.\n\t *\n\t * @param {WebGLBuffer} buffer - The buffer to set.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetBuffer( buffer ) {\n\n\t\tthis.buffer = buffer;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the given native data type and element size.\n\t *\n\t * @param {number} type - The native data type (e.g. `gl.FLOAT`).\n\t * @param {number} elementSize - The corresponding size (in bytes) for the given `type` parameter.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetType( type, elementSize ) {\n\n\t\tthis.type = type;\n\t\tthis.elementSize = elementSize;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the item size.\n\t *\n\t * @param {number} itemSize - The item size.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetItemSize( itemSize ) {\n\n\t\tthis.itemSize = itemSize;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the count (the expected number of vertices in VBO).\n\t *\n\t * @param {number} count - The count.\n\t * @return {BufferAttribute} A reference to this instance.\n\t */\n\tsetCount( count ) {\n\n\t\tthis.count = count;\n\n\t\treturn this;\n\n\t}\n\n}\n\nconst _matrix = /*@__PURE__*/ new Matrix4();\n\n/**\n * This class is designed to assist with raycasting. Raycasting is used for\n * mouse picking (working out what objects in the 3d space the mouse is over)\n * amongst other things.\n */\nclass Raycaster {\n\n\t/**\n\t * Constructs a new raycaster.\n\t *\n\t * @param {Vector3} origin - The origin vector where the ray casts from.\n\t * @param {Vector3} direction - The (normalized) direction vector that gives direction to the ray.\n\t * @param {number} [near=0] - All results returned are further away than near. Near can't be negative.\n\t * @param {number} [far=Infinity] - All results returned are closer than far. Far can't be lower than near.\n\t */\n\tconstructor( origin, direction, near = 0, far = Infinity ) {\n\n\t\t/**\n\t\t * The ray used for raycasting.\n\t\t *\n\t\t * @type {Ray}\n\t\t */\n\t\tthis.ray = new Ray( origin, direction );\n\n\t\t/**\n\t\t * All results returned are further away than near. Near can't be negative.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.near = near;\n\n\t\t/**\n\t\t * All results returned are closer than far. Far can't be lower than near.\n\t\t *\n\t\t * @type {number}\n\t\t * @default Infinity\n\t\t */\n\t\tthis.far = far;\n\n\t\t/**\n\t\t * The camera to use when raycasting against view-dependent objects such as\n\t\t * billboarded objects like sprites. This field can be set manually or\n\t\t * is set when calling `setFromCamera()`.\n\t\t *\n\t\t * @type {?Camera}\n\t\t * @default null\n\t\t */\n\t\tthis.camera = null;\n\n\t\t/**\n\t\t * Allows to selectively ignore 3D objects when performing intersection tests.\n\t\t * The following code example ensures that only 3D objects on layer `1` will be\n\t\t * honored by raycaster.\n\t\t * ```js\n\t\t * raycaster.layers.set( 1 );\n\t\t * object.layers.enable( 1 );\n\t\t * ```\n\t\t *\n\t\t * @type {Layers}\n\t\t */\n\t\tthis.layers = new Layers();\n\n\n\t\t/**\n\t\t * A parameter object that configures the raycasting. It has the structure:\n\t\t *\n\t\t * ```\n\t\t * {\n\t\t * \tMesh: {},\n\t\t * \tLine: { threshold: 1 },\n\t\t * \tLOD: {},\n\t\t * \tPoints: { threshold: 1 },\n\t\t * \tSprite: {}\n\t\t * }\n\t\t * ```\n\t\t * Where `threshold` is the precision of the raycaster when intersecting objects, in world units.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.params = {\n\t\t\tMesh: {},\n\t\t\tLine: { threshold: 1 },\n\t\t\tLOD: {},\n\t\t\tPoints: { threshold: 1 },\n\t\t\tSprite: {}\n\t\t};\n\n\t}\n\n\t/**\n\t * Updates the ray with a new origin and direction by copying the values from the arguments.\n\t *\n\t * @param {Vector3} origin - The origin vector where the ray casts from.\n\t * @param {Vector3} direction - The (normalized) direction vector that gives direction to the ray.\n\t */\n\tset( origin, direction ) {\n\n\t\t// direction is assumed to be normalized (for accurate distance calculations)\n\n\t\tthis.ray.set( origin, direction );\n\n\t}\n\n\t/**\n\t * Uses the given coordinates and camera to compute a new origin and direction for the internal ray.\n\t *\n\t * @param {Vector2} coords - 2D coordinates of the mouse, in normalized device coordinates (NDC).\n\t * X and Y components should be between `-1` and `1`.\n\t * @param {Camera} camera - The camera from which the ray should originate.\n\t */\n\tsetFromCamera( coords, camera ) {\n\n\t\tif ( camera.isPerspectiveCamera ) {\n\n\t\t\tthis.ray.origin.setFromMatrixPosition( camera.matrixWorld );\n\t\t\tthis.ray.direction.set( coords.x, coords.y, 0.5 ).unproject( camera ).sub( this.ray.origin ).normalize();\n\t\t\tthis.camera = camera;\n\n\t\t} else if ( camera.isOrthographicCamera ) {\n\n\t\t\tthis.ray.origin.set( coords.x, coords.y, ( camera.near + camera.far ) / ( camera.near - camera.far ) ).unproject( camera ); // set origin in plane of camera\n\t\t\tthis.ray.direction.set( 0, 0, -1 ).transformDirection( camera.matrixWorld );\n\t\t\tthis.camera = camera;\n\n\t\t} else {\n\n\t\t\terror( 'Raycaster: Unsupported camera type: ' + camera.type );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Uses the given WebXR controller to compute a new origin and direction for the internal ray.\n\t *\n\t * @param {WebXRController} controller - The controller to copy the position and direction from.\n\t * @return {Raycaster} A reference to this raycaster.\n\t */\n\tsetFromXRController( controller ) {\n\n\t\t_matrix.identity().extractRotation( controller.matrixWorld );\n\n\t\tthis.ray.origin.setFromMatrixPosition( controller.matrixWorld );\n\t\tthis.ray.direction.set( 0, 0, -1 ).applyMatrix4( _matrix );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * The intersection point of a raycaster intersection test.\n\t * @typedef {Object} Raycaster~Intersection\n\t * @property {number} distance - The distance from the ray's origin to the intersection point.\n\t * @property {number} distanceToRay -  Some 3D objects e.g. {@link Points} provide the distance of the\n\t * intersection to the nearest point on the ray. For other objects it will be `undefined`.\n\t * @property {Vector3} point - The intersection point, in world coordinates.\n\t * @property {Object} face - The face that has been intersected.\n\t * @property {number} faceIndex - The face index.\n\t * @property {Object3D} object - The 3D object that has been intersected.\n\t * @property {Vector2} uv - U,V coordinates at point of intersection.\n\t * @property {Vector2} uv1 - Second set of U,V coordinates at point of intersection.\n\t * @property {Vector3} normal - Interpolated normal vector at point of intersection.\n\t * @property {number} instanceId - The index number of the instance where the ray\n\t * intersects the {@link InstancedMesh}.\n\t */\n\n\t/**\n\t * Checks all intersection between the ray and the object with or without the\n\t * descendants. Intersections are returned sorted by distance, closest first.\n\t *\n\t * `Raycaster` delegates to the `raycast()` method of the passed 3D object, when\n\t * evaluating whether the ray intersects the object or not. This allows meshes to respond\n\t * differently to ray casting than lines or points.\n\t *\n\t * Note that for meshes, faces must be pointed towards the origin of the ray in order\n\t * to be detected; intersections of the ray passing through the back of a face will not\n\t * be detected. To raycast against both faces of an object, you'll want to set  {@link Material#side}\n\t * to `THREE.DoubleSide`.\n\t *\n\t * @param {Object3D} object - The 3D object to check for intersection with the ray.\n\t * @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants.\n\t * Otherwise it only checks intersection with the object.\n\t * @param {Array<Raycaster~Intersection>} [intersects=[]] The target array that holds the result of the method.\n\t * @return {Array<Raycaster~Intersection>} An array holding the intersection points.\n\t */\n\tintersectObject( object, recursive = true, intersects = [] ) {\n\n\t\tintersect( object, this, intersects, recursive );\n\n\t\tintersects.sort( ascSort );\n\n\t\treturn intersects;\n\n\t}\n\n\t/**\n\t * Checks all intersection between the ray and the objects with or without\n\t * the descendants. Intersections are returned sorted by distance, closest first.\n\t *\n\t * @param {Array<Object3D>} objects - The 3D objects to check for intersection with the ray.\n\t * @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants.\n\t * Otherwise it only checks intersection with the object.\n\t * @param {Array<Raycaster~Intersection>} [intersects=[]] The target array that holds the result of the method.\n\t * @return {Array<Raycaster~Intersection>} An array holding the intersection points.\n\t */\n\tintersectObjects( objects, recursive = true, intersects = [] ) {\n\n\t\tfor ( let i = 0, l = objects.length; i < l; i ++ ) {\n\n\t\t\tintersect( objects[ i ], this, intersects, recursive );\n\n\t\t}\n\n\t\tintersects.sort( ascSort );\n\n\t\treturn intersects;\n\n\t}\n\n}\n\nfunction ascSort( a, b ) {\n\n\treturn a.distance - b.distance;\n\n}\n\nfunction intersect( object, raycaster, intersects, recursive ) {\n\n\tlet propagate = true;\n\n\tif ( object.layers.test( raycaster.layers ) ) {\n\n\t\tconst result = object.raycast( raycaster, intersects );\n\n\t\tif ( result === false ) propagate = false;\n\n\t}\n\n\tif ( propagate === true && recursive === true ) {\n\n\t\tconst children = object.children;\n\n\t\tfor ( let i = 0, l = children.length; i < l; i ++ ) {\n\n\t\t\tintersect( children[ i ], raycaster, intersects, true );\n\n\t\t}\n\n\t}\n\n}\n\n/**\n * This class is an alternative to {@link Clock} with a different API design and behavior.\n * The goal is to avoid the conceptual flaws that became apparent in `Clock` over time.\n *\n * - `Timer` has an `update()` method that updates its internal state. That makes it possible to\n * call `getDelta()` and `getElapsed()` multiple times per simulation step without getting different values.\n * - The class can make use of the Page Visibility API to avoid large time delta values when the app\n * is inactive (e.g. tab switched or browser hidden).\n *\n * ```js\n * const timer = new Timer();\n * timer.connect( document ); // use Page Visibility API\n * ```\n */\nclass Timer {\n\n\t/**\n\t * Constructs a new timer.\n\t */\n\tconstructor() {\n\n\t\tthis._previousTime = 0;\n\t\tthis._currentTime = 0;\n\t\tthis._startTime = performance.now();\n\n\t\tthis._delta = 0;\n\t\tthis._elapsed = 0;\n\n\t\tthis._timescale = 1;\n\n\t\tthis._document = null;\n\t\tthis._pageVisibilityHandler = null;\n\n\t}\n\n\t/**\n\t * Connect the timer to the given document.Calling this method is not mandatory to\n\t * use the timer but enables the usage of the Page Visibility API to avoid large time\n\t * delta values.\n\t *\n\t * @param {Document} document - The document.\n\t */\n\tconnect( document ) {\n\n\t\tthis._document = document;\n\n\t\t// use Page Visibility API to avoid large time delta values\n\n\t\tif ( document.hidden !== undefined ) {\n\n\t\t\tthis._pageVisibilityHandler = handleVisibilityChange.bind( this );\n\n\t\t\tdocument.addEventListener( 'visibilitychange', this._pageVisibilityHandler, false );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Disconnects the timer from the DOM and also disables the usage of the Page Visibility API.\n\t */\n\tdisconnect() {\n\n\t\tif ( this._pageVisibilityHandler !== null ) {\n\n\t\t\tthis._document.removeEventListener( 'visibilitychange', this._pageVisibilityHandler );\n\t\t\tthis._pageVisibilityHandler = null;\n\n\t\t}\n\n\t\tthis._document = null;\n\n\t}\n\n\t/**\n\t * Returns the time delta in seconds.\n\t *\n\t * @return {number} The time delta in second.\n\t */\n\tgetDelta() {\n\n\t\treturn this._delta / 1000;\n\n\t}\n\n\t/**\n\t * Returns the elapsed time in seconds.\n\t *\n\t * @return {number} The elapsed time in second.\n\t */\n\tgetElapsed() {\n\n\t\treturn this._elapsed / 1000;\n\n\t}\n\n\t/**\n\t * Returns the timescale.\n\t *\n\t * @return {number} The timescale.\n\t */\n\tgetTimescale() {\n\n\t\treturn this._timescale;\n\n\t}\n\n\t/**\n\t * Sets the given timescale which scale the time delta computation\n\t * in `update()`.\n\t *\n\t * @param {number} timescale - The timescale to set.\n\t * @return {Timer} A reference to this timer.\n\t */\n\tsetTimescale( timescale ) {\n\n\t\tthis._timescale = timescale;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Resets the time computation for the current simulation step.\n\t *\n\t * @return {Timer} A reference to this timer.\n\t */\n\treset() {\n\n\t\tthis._currentTime = performance.now() - this._startTime;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Can be used to free all internal resources. Usually called when\n\t * the timer instance isn't required anymore.\n\t */\n\tdispose() {\n\n\t\tthis.disconnect();\n\n\t}\n\n\t/**\n\t * Updates the internal state of the timer. This method should be called\n\t * once per simulation step and before you perform queries against the timer\n\t * (e.g. via `getDelta()`).\n\t *\n\t * @param {number} timestamp - The current time in milliseconds. Can be obtained\n\t * from the `requestAnimationFrame` callback argument. If not provided, the current\n\t * time will be determined with `performance.now`.\n\t * @return {Timer} A reference to this timer.\n\t */\n\tupdate( timestamp ) {\n\n\t\tif ( this._pageVisibilityHandler !== null && this._document.hidden === true ) {\n\n\t\t\tthis._delta = 0;\n\n\t\t} else {\n\n\t\t\tthis._previousTime = this._currentTime;\n\t\t\tthis._currentTime = ( timestamp !== undefined ? timestamp : performance.now() ) - this._startTime;\n\n\t\t\tthis._delta = ( this._currentTime - this._previousTime ) * this._timescale;\n\t\t\tthis._elapsed += this._delta; // _elapsed is the accumulation of all previous deltas\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n}\n\nfunction handleVisibilityChange() {\n\n\tif ( this._document.hidden === false ) this.reset();\n\n}\n\n/**\n * This class can be used to represent points in 3D space as\n * [Spherical coordinates](https://en.wikipedia.org/wiki/Spherical_coordinate_system).\n */\nclass Spherical {\n\n\t/**\n\t * Constructs a new spherical.\n\t *\n\t * @param {number} [radius=1] - The radius, or the Euclidean distance (straight-line distance) from the point to the origin.\n\t * @param {number} [phi=0] - The polar angle in radians from the y (up) axis.\n\t * @param {number} [theta=0] - The equator/azimuthal angle in radians around the y (up) axis.\n\t */\n\tconstructor( radius = 1, phi = 0, theta = 0 ) {\n\n\t\t/**\n\t\t * The radius, or the Euclidean distance (straight-line distance) from the point to the origin.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.radius = radius;\n\n\t\t/**\n\t\t * The polar angle in radians from the y (up) axis.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.phi = phi;\n\n\t\t/**\n\t\t * The equator/azimuthal angle in radians around the y (up) axis.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.theta = theta;\n\n\t}\n\n\t/**\n\t * Sets the spherical components by copying the given values.\n\t *\n\t * @param {number} radius - The radius.\n\t * @param {number} phi - The polar angle.\n\t * @param {number} theta - The azimuthal angle.\n\t * @return {Spherical} A reference to this spherical.\n\t */\n\tset( radius, phi, theta ) {\n\n\t\tthis.radius = radius;\n\t\tthis.phi = phi;\n\t\tthis.theta = theta;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the values of the given spherical to this instance.\n\t *\n\t * @param {Spherical} other - The spherical to copy.\n\t * @return {Spherical} A reference to this spherical.\n\t */\n\tcopy( other ) {\n\n\t\tthis.radius = other.radius;\n\t\tthis.phi = other.phi;\n\t\tthis.theta = other.theta;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Restricts the polar angle [page:.phi phi] to be between `0.000001` and pi -\n\t * `0.000001`.\n\t *\n\t * @return {Spherical} A reference to this spherical.\n\t */\n\tmakeSafe() {\n\n\t\tconst EPS = 0.000001;\n\t\tthis.phi = clamp( this.phi, EPS, Math.PI - EPS );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the spherical components from the given vector which is assumed to hold\n\t * Cartesian coordinates.\n\t *\n\t * @param {Vector3} v - The vector to set.\n\t * @return {Spherical} A reference to this spherical.\n\t */\n\tsetFromVector3( v ) {\n\n\t\treturn this.setFromCartesianCoords( v.x, v.y, v.z );\n\n\t}\n\n\t/**\n\t * Sets the spherical components from the given Cartesian coordinates.\n\t *\n\t * @param {number} x - The x value.\n\t * @param {number} y - The y value.\n\t * @param {number} z - The z value.\n\t * @return {Spherical} A reference to this spherical.\n\t */\n\tsetFromCartesianCoords( x, y, z ) {\n\n\t\tthis.radius = Math.sqrt( x * x + y * y + z * z );\n\n\t\tif ( this.radius === 0 ) {\n\n\t\t\tthis.theta = 0;\n\t\t\tthis.phi = 0;\n\n\t\t} else {\n\n\t\t\tthis.theta = Math.atan2( x, z );\n\t\t\tthis.phi = Math.acos( clamp( y / this.radius, -1, 1 ) );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new spherical with copied values from this instance.\n\t *\n\t * @return {Spherical} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n}\n\n/**\n * This class can be used to represent points in 3D space as\n * [Cylindrical coordinates](https://en.wikipedia.org/wiki/Cylindrical_coordinate_system).\n */\nclass Cylindrical {\n\n\t/**\n\t * Constructs a new cylindrical.\n\t *\n\t * @param {number} [radius=1] - The distance from the origin to a point in the x-z plane.\n\t * @param {number} [theta=0] - A counterclockwise angle in the x-z plane measured in radians from the positive z-axis.\n\t * @param {number} [y=0] - The height above the x-z plane.\n\t */\n\tconstructor( radius = 1, theta = 0, y = 0 ) {\n\n\t\t/**\n\t\t * The distance from the origin to a point in the x-z plane.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.radius = radius;\n\n\t\t/**\n\t\t * A counterclockwise angle in the x-z plane measured in radians from the positive z-axis.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.theta = theta;\n\n\t\t/**\n\t\t * The height above the x-z plane.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.y = y;\n\n\t}\n\n\t/**\n\t * Sets the cylindrical components by copying the given values.\n\t *\n\t * @param {number} radius - The radius.\n\t * @param {number} theta - The theta angle.\n\t * @param {number} y - The height value.\n\t * @return {Cylindrical} A reference to this cylindrical.\n\t */\n\tset( radius, theta, y ) {\n\n\t\tthis.radius = radius;\n\t\tthis.theta = theta;\n\t\tthis.y = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the values of the given cylindrical to this instance.\n\t *\n\t * @param {Cylindrical} other - The cylindrical to copy.\n\t * @return {Cylindrical} A reference to this cylindrical.\n\t */\n\tcopy( other ) {\n\n\t\tthis.radius = other.radius;\n\t\tthis.theta = other.theta;\n\t\tthis.y = other.y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the cylindrical components from the given vector which is assumed to hold\n\t * Cartesian coordinates.\n\t *\n\t * @param {Vector3} v - The vector to set.\n\t * @return {Cylindrical} A reference to this cylindrical.\n\t */\n\tsetFromVector3( v ) {\n\n\t\treturn this.setFromCartesianCoords( v.x, v.y, v.z );\n\n\t}\n\n\t/**\n\t * Sets the cylindrical components from the given Cartesian coordinates.\n\t *\n\t * @param {number} x - The x value.\n\t * @param {number} y - The x value.\n\t * @param {number} z - The x value.\n\t * @return {Cylindrical} A reference to this cylindrical.\n\t */\n\tsetFromCartesianCoords( x, y, z ) {\n\n\t\tthis.radius = Math.sqrt( x * x + z * z );\n\t\tthis.theta = Math.atan2( x, z );\n\t\tthis.y = y;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new cylindrical with copied values from this instance.\n\t *\n\t * @return {Cylindrical} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n}\n\n/**\n * Represents a 2x2 matrix.\n *\n * A Note on Row-Major and Column-Major Ordering:\n *\n * The constructor and {@link Matrix2#set} method take arguments in\n * [row-major](https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order)\n * order, while internally they are stored in the {@link Matrix2#elements} array in column-major order.\n * This means that calling:\n * ```js\n * const m = new THREE.Matrix2();\n * m.set( 11, 12,\n *        21, 22 );\n * ```\n * will result in the elements array containing:\n * ```js\n * m.elements = [ 11, 21,\n *                12, 22 ];\n * ```\n * and internally all calculations are performed using column-major ordering.\n * However, as the actual ordering makes no difference mathematically and\n * most people are used to thinking about matrices in row-major order, the\n * three.js documentation shows matrices in row-major order. Just bear in\n * mind that if you are reading the source code, you'll have to take the\n * transpose of any matrices outlined here to make sense of the calculations.\n */\nclass Matrix2 {\n\n\t/**\n\t * Constructs a new 2x2 matrix. The arguments are supposed to be\n\t * in row-major order. If no arguments are provided, the constructor\n\t * initializes the matrix as an identity matrix.\n\t *\n\t * @param {number} [n11] - 1-1 matrix element.\n\t * @param {number} [n12] - 1-2 matrix element.\n\t * @param {number} [n21] - 2-1 matrix element.\n\t * @param {number} [n22] - 2-2 matrix element.\n\t */\n\tconstructor( n11, n12, n21, n22 ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tMatrix2.prototype.isMatrix2 = true;\n\n\t\t/**\n\t\t * A column-major list of matrix values.\n\t\t *\n\t\t * @type {Array<number>}\n\t\t */\n\t\tthis.elements = [\n\t\t\t1, 0,\n\t\t\t0, 1,\n\t\t];\n\n\t\tif ( n11 !== undefined ) {\n\n\t\t\tthis.set( n11, n12, n21, n22 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sets this matrix to the 2x2 identity matrix.\n\t *\n\t * @return {Matrix2} A reference to this matrix.\n\t */\n\tidentity() {\n\n\t\tthis.set(\n\t\t\t1, 0,\n\t\t\t0, 1,\n\t\t);\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the elements of the matrix from the given array.\n\t *\n\t * @param {Array<number>} array - The matrix elements in column-major order.\n\t * @param {number} [offset=0] - Index of the first element in the array.\n\t * @return {Matrix2} A reference to this matrix.\n\t */\n\tfromArray( array, offset = 0 ) {\n\n\t\tfor ( let i = 0; i < 4; i ++ ) {\n\n\t\t\tthis.elements[ i ] = array[ i + offset ];\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the elements of the matrix.The arguments are supposed to be\n\t * in row-major order.\n\t *\n\t * @param {number} n11 - 1-1 matrix element.\n\t * @param {number} n12 - 1-2 matrix element.\n\t * @param {number} n21 - 2-1 matrix element.\n\t * @param {number} n22 - 2-2 matrix element.\n\t * @return {Matrix2} A reference to this matrix.\n\t */\n\tset( n11, n12, n21, n22 ) {\n\n\t\tconst te = this.elements;\n\n\t\tte[ 0 ] = n11; te[ 2 ] = n12;\n\t\tte[ 1 ] = n21; te[ 3 ] = n22;\n\n\t\treturn this;\n\n\t}\n\n}\n\nconst _vector$4 = /*@__PURE__*/ new Vector2();\n\n/**\n * Represents an axis-aligned bounding box (AABB) in 2D space.\n */\nclass Box2 {\n\n\t/**\n\t * Constructs a new bounding box.\n\t *\n\t * @param {Vector2} [min=(Infinity,Infinity)] - A vector representing the lower boundary of the box.\n\t * @param {Vector2} [max=(-Infinity,-Infinity)] - A vector representing the upper boundary of the box.\n\t */\n\tconstructor( min = new Vector2( + Infinity, + Infinity ), max = new Vector2( - Infinity, - Infinity ) ) {\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isBox2 = true;\n\n\t\t/**\n\t\t * The lower boundary of the box.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.min = min;\n\n\t\t/**\n\t\t * The upper boundary of the box.\n\t\t *\n\t\t * @type {Vector2}\n\t\t */\n\t\tthis.max = max;\n\n\t}\n\n\t/**\n\t * Sets the lower and upper boundaries of this box.\n\t * Please note that this method only copies the values from the given objects.\n\t *\n\t * @param {Vector2} min - The lower boundary of the box.\n\t * @param {Vector2} max - The upper boundary of the box.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\tset( min, max ) {\n\n\t\tthis.min.copy( min );\n\t\tthis.max.copy( max );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Sets the upper and lower bounds of this box so it encloses the position data\n\t * in the given array.\n\t *\n\t * @param {Array<Vector2>} points - An array holding 2D position data as instances of {@link Vector2}.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\tsetFromPoints( points ) {\n\n\t\tthis.makeEmpty();\n\n\t\tfor ( let i = 0, il = points.length; i < il; i ++ ) {\n\n\t\t\tthis.expandByPoint( points[ i ] );\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Centers this box on the given center vector and sets this box's width, height and\n\t * depth to the given size values.\n\t *\n\t * @param {Vector2} center - The center of the box.\n\t * @param {Vector2} size - The x and y dimensions of the box.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\tsetFromCenterAndSize( center, size ) {\n\n\t\tconst halfSize = _vector$4.copy( size ).multiplyScalar( 0.5 );\n\t\tthis.min.copy( center ).sub( halfSize );\n\t\tthis.max.copy( center ).add( halfSize );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns a new box with copied values from this instance.\n\t *\n\t * @return {Box2} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n\t/**\n\t * Copies the values of the given box to this instance.\n\t *\n\t * @param {Box2} box - The box to copy.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\tcopy( box ) {\n\n\t\tthis.min.copy( box.min );\n\t\tthis.max.copy( box.max );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Makes this box empty which means in encloses a zero space in 2D.\n\t *\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\tmakeEmpty() {\n\n\t\tthis.min.x = this.min.y = + Infinity;\n\t\tthis.max.x = this.max.y = - Infinity;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns true if this box includes zero points within its bounds.\n\t * Note that a box with equal lower and upper bounds still includes one\n\t * point, the one both bounds share.\n\t *\n\t * @return {boolean} Whether this box is empty or not.\n\t */\n\tisEmpty() {\n\n\t\t// this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes\n\n\t\treturn ( this.max.x < this.min.x ) || ( this.max.y < this.min.y );\n\n\t}\n\n\t/**\n\t * Returns the center point of this box.\n\t *\n\t * @param {Vector2} target - The target vector that is used to store the method's result.\n\t * @return {Vector2} The center point.\n\t */\n\tgetCenter( target ) {\n\n\t\treturn this.isEmpty() ? target.set( 0, 0 ) : target.addVectors( this.min, this.max ).multiplyScalar( 0.5 );\n\n\t}\n\n\t/**\n\t * Returns the dimensions of this box.\n\t *\n\t * @param {Vector2} target - The target vector that is used to store the method's result.\n\t * @return {Vector2} The size.\n\t */\n\tgetSize( target ) {\n\n\t\treturn this.isEmpty() ? target.set( 0, 0 ) : target.subVectors( this.max, this.min );\n\n\t}\n\n\t/**\n\t * Expands the boundaries of this box to include the given point.\n\t *\n\t * @param {Vector2} point - The point that should be included by the bounding box.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\texpandByPoint( point ) {\n\n\t\tthis.min.min( point );\n\t\tthis.max.max( point );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Expands this box equilaterally by the given vector. The width of this\n\t * box will be expanded by the x component of the vector in both\n\t * directions. The height of this box will be expanded by the y component of\n\t * the vector in both directions.\n\t *\n\t * @param {Vector2} vector - The vector that should expand the bounding box.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\texpandByVector( vector ) {\n\n\t\tthis.min.sub( vector );\n\t\tthis.max.add( vector );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Expands each dimension of the box by the given scalar. If negative, the\n\t * dimensions of the box will be contracted.\n\t *\n\t * @param {number} scalar - The scalar value that should expand the bounding box.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\texpandByScalar( scalar ) {\n\n\t\tthis.min.addScalar( - scalar );\n\t\tthis.max.addScalar( scalar );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if the given point lies within or on the boundaries of this box.\n\t *\n\t * @param {Vector2} point - The point to test.\n\t * @return {boolean} Whether the bounding box contains the given point or not.\n\t */\n\tcontainsPoint( point ) {\n\n\t\treturn point.x >= this.min.x && point.x <= this.max.x &&\n\t\t\tpoint.y >= this.min.y && point.y <= this.max.y;\n\n\t}\n\n\t/**\n\t * Returns `true` if this bounding box includes the entirety of the given bounding box.\n\t * If this box and the given one are identical, this function also returns `true`.\n\t *\n\t * @param {Box2} box - The bounding box to test.\n\t * @return {boolean} Whether the bounding box contains the given bounding box or not.\n\t */\n\tcontainsBox( box ) {\n\n\t\treturn this.min.x <= box.min.x && box.max.x <= this.max.x &&\n\t\t\tthis.min.y <= box.min.y && box.max.y <= this.max.y;\n\n\t}\n\n\t/**\n\t * Returns a point as a proportion of this box's width and height.\n\t *\n\t * @param {Vector2} point - A point in 2D space.\n\t * @param {Vector2} target - The target vector that is used to store the method's result.\n\t * @return {Vector2} A point as a proportion of this box's width and height.\n\t */\n\tgetParameter( point, target ) {\n\n\t\t// This can potentially have a divide by zero if the box\n\t\t// has a size dimension of 0.\n\n\t\treturn target.set(\n\t\t\t( point.x - this.min.x ) / ( this.max.x - this.min.x ),\n\t\t\t( point.y - this.min.y ) / ( this.max.y - this.min.y )\n\t\t);\n\n\t}\n\n\t/**\n\t * Returns `true` if the given bounding box intersects with this bounding box.\n\t *\n\t * @param {Box2} box - The bounding box to test.\n\t * @return {boolean} Whether the given bounding box intersects with this bounding box.\n\t */\n\tintersectsBox( box ) {\n\n\t\t// using 4 splitting planes to rule out intersections\n\n\t\treturn box.max.x >= this.min.x && box.min.x <= this.max.x &&\n\t\t\tbox.max.y >= this.min.y && box.min.y <= this.max.y;\n\n\t}\n\n\t/**\n\t * Clamps the given point within the bounds of this box.\n\t *\n\t * @param {Vector2} point - The point to clamp.\n\t * @param {Vector2} target - The target vector that is used to store the method's result.\n\t * @return {Vector2} The clamped point.\n\t */\n\tclampPoint( point, target ) {\n\n\t\treturn target.copy( point ).clamp( this.min, this.max );\n\n\t}\n\n\t/**\n\t * Returns the euclidean distance from any edge of this box to the specified point. If\n\t * the given point lies inside of this box, the distance will be `0`.\n\t *\n\t * @param {Vector2} point - The point to compute the distance to.\n\t * @return {number} The euclidean distance.\n\t */\n\tdistanceToPoint( point ) {\n\n\t\treturn this.clampPoint( point, _vector$4 ).distanceTo( point );\n\n\t}\n\n\t/**\n\t * Computes the intersection of this bounding box and the given one, setting the upper\n\t * bound of this box to the lesser of the two boxes' upper bounds and the\n\t * lower bound of this box to the greater of the two boxes' lower bounds. If\n\t * there's no overlap, makes this box empty.\n\t *\n\t * @param {Box2} box - The bounding box to intersect with.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\tintersect( box ) {\n\n\t\tthis.min.max( box.min );\n\t\tthis.max.min( box.max );\n\n\t\tif ( this.isEmpty() ) this.makeEmpty();\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Computes the union of this box and another and the given one, setting the upper\n\t * bound of this box to the greater of the two boxes' upper bounds and the\n\t * lower bound of this box to the lesser of the two boxes' lower bounds.\n\t *\n\t * @param {Box2} box - The bounding box that will be unioned with this instance.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\tunion( box ) {\n\n\t\tthis.min.min( box.min );\n\t\tthis.max.max( box.max );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds the given offset to both the upper and lower bounds of this bounding box,\n\t * effectively moving it in 2D space.\n\t *\n\t * @param {Vector2} offset - The offset that should be used to translate the bounding box.\n\t * @return {Box2} A reference to this bounding box.\n\t */\n\ttranslate( offset ) {\n\n\t\tthis.min.add( offset );\n\t\tthis.max.add( offset );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this bounding box is equal with the given one.\n\t *\n\t * @param {Box2} box - The box to test for equality.\n\t * @return {boolean} Whether this bounding box is equal with the given one.\n\t */\n\tequals( box ) {\n\n\t\treturn box.min.equals( this.min ) && box.max.equals( this.max );\n\n\t}\n\n}\n\nconst _startP = /*@__PURE__*/ new Vector3();\nconst _startEnd = /*@__PURE__*/ new Vector3();\n\nconst _d1 = /*@__PURE__*/ new Vector3();\nconst _d2 = /*@__PURE__*/ new Vector3();\nconst _r = /*@__PURE__*/ new Vector3();\nconst _c1 = /*@__PURE__*/ new Vector3();\nconst _c2 = /*@__PURE__*/ new Vector3();\n\n/**\n * An analytical line segment in 3D space represented by a start and end point.\n */\nclass Line3 {\n\n\t/**\n\t * Constructs a new line segment.\n\t *\n\t * @param {Vector3} [start=(0,0,0)] - Start of the line segment.\n\t * @param {Vector3} [end=(0,0,0)] - End of the line segment.\n\t */\n\tconstructor( start = new Vector3(), end = new Vector3() ) {\n\n\t\t/**\n\t\t * Start of the line segment.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.start = start;\n\n\t\t/**\n\t\t * End of the line segment.\n\t\t *\n\t\t * @type {Vector3}\n\t\t */\n\t\tthis.end = end;\n\n\t}\n\n\t/**\n\t * Sets the start and end values by copying the given vectors.\n\t *\n\t * @param {Vector3} start - The start point.\n\t * @param {Vector3} end - The end point.\n\t * @return {Line3} A reference to this line segment.\n\t */\n\tset( start, end ) {\n\n\t\tthis.start.copy( start );\n\t\tthis.end.copy( end );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Copies the values of the given line segment to this instance.\n\t *\n\t * @param {Line3} line - The line segment to copy.\n\t * @return {Line3} A reference to this line segment.\n\t */\n\tcopy( line ) {\n\n\t\tthis.start.copy( line.start );\n\t\tthis.end.copy( line.end );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns the center of the line segment.\n\t *\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The center point.\n\t */\n\tgetCenter( target ) {\n\n\t\treturn target.addVectors( this.start, this.end ).multiplyScalar( 0.5 );\n\n\t}\n\n\t/**\n\t * Returns the delta vector of the line segment's start and end point.\n\t *\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The delta vector.\n\t */\n\tdelta( target ) {\n\n\t\treturn target.subVectors( this.end, this.start );\n\n\t}\n\n\t/**\n\t * Returns the squared Euclidean distance between the line' start and end point.\n\t *\n\t * @return {number} The squared Euclidean distance.\n\t */\n\tdistanceSq() {\n\n\t\treturn this.start.distanceToSquared( this.end );\n\n\t}\n\n\t/**\n\t * Returns the Euclidean distance between the line' start and end point.\n\t *\n\t * @return {number} The Euclidean distance.\n\t */\n\tdistance() {\n\n\t\treturn this.start.distanceTo( this.end );\n\n\t}\n\n\t/**\n\t * Returns a vector at a certain position along the line segment.\n\t *\n\t * @param {number} t - A value between `[0,1]` to represent a position along the line segment.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The delta vector.\n\t */\n\tat( t, target ) {\n\n\t\treturn this.delta( target ).multiplyScalar( t ).add( this.start );\n\n\t}\n\n\t/**\n\t * Returns a point parameter based on the closest point as projected on the line segment.\n\t *\n\t * @param {Vector3} point - The point for which to return a point parameter.\n\t * @param {boolean} clampToLine - Whether to clamp the result to the range `[0,1]` or not.\n\t * @return {number} The point parameter.\n\t */\n\tclosestPointToPointParameter( point, clampToLine ) {\n\n\t\t_startP.subVectors( point, this.start );\n\t\t_startEnd.subVectors( this.end, this.start );\n\n\t\tconst startEnd2 = _startEnd.dot( _startEnd );\n\t\tconst startEnd_startP = _startEnd.dot( _startP );\n\n\t\tlet t = startEnd_startP / startEnd2;\n\n\t\tif ( clampToLine ) {\n\n\t\t\tt = clamp( t, 0, 1 );\n\n\t\t}\n\n\t\treturn t;\n\n\t}\n\n\t/**\n\t * Returns the closest point on the line for a given point.\n\t *\n\t * @param {Vector3} point - The point to compute the closest point on the line for.\n\t * @param {boolean} clampToLine - Whether to clamp the result to the range `[0,1]` or not.\n\t * @param {Vector3} target - The target vector that is used to store the method's result.\n\t * @return {Vector3} The closest point on the line.\n\t */\n\tclosestPointToPoint( point, clampToLine, target ) {\n\n\t\tconst t = this.closestPointToPointParameter( point, clampToLine );\n\n\t\treturn this.delta( target ).multiplyScalar( t ).add( this.start );\n\n\t}\n\n\t/**\n\t * Returns the closest squared distance between this line segment and the given one.\n\t *\n\t * @param {Line3} line - The line segment to compute the closest squared distance to.\n\t * @param {Vector3} [c1] - The closest point on this line segment.\n\t * @param {Vector3} [c2] - The closest point on the given line segment.\n\t * @return {number} The squared distance between this line segment and the given one.\n\t */\n\tdistanceSqToLine3( line, c1 = _c1, c2 = _c2 ) {\n\n\t\t// from Real-Time Collision Detection by Christer Ericson, chapter 5.1.9\n\n\t\t// Computes closest points C1 and C2 of S1(s)=P1+s*(Q1-P1) and\n\t\t// S2(t)=P2+t*(Q2-P2), returning s and t. Function result is squared\n\t\t// distance between between S1(s) and S2(t)\n\n\t\tconst EPSILON = 1e-8 * 1e-8; // must be squared since we compare squared length\n\t\tlet s, t;\n\n\t\tconst p1 = this.start;\n\t\tconst p2 = line.start;\n\t\tconst q1 = this.end;\n\t\tconst q2 = line.end;\n\n\t\t_d1.subVectors( q1, p1 ); // Direction vector of segment S1\n\t\t_d2.subVectors( q2, p2 ); // Direction vector of segment S2\n\t\t_r.subVectors( p1, p2 );\n\n\t\tconst a = _d1.dot( _d1 ); // Squared length of segment S1, always nonnegative\n\t\tconst e = _d2.dot( _d2 ); // Squared length of segment S2, always nonnegative\n\t\tconst f = _d2.dot( _r );\n\n\t\t// Check if either or both segments degenerate into points\n\n\t\tif ( a <= EPSILON && e <= EPSILON ) {\n\n\t\t\t// Both segments degenerate into points\n\n\t\t\tc1.copy( p1 );\n\t\t\tc2.copy( p2 );\n\n\t\t\tc1.sub( c2 );\n\n\t\t\treturn c1.dot( c1 );\n\n\t\t}\n\n\t\tif ( a <= EPSILON ) {\n\n\t\t\t// First segment degenerates into a point\n\n\t\t\ts = 0;\n\t\t\tt = f / e; // s = 0 => t = (b*s + f) / e = f / e\n\t\t\tt = clamp( t, 0, 1 );\n\n\n\t\t} else {\n\n\t\t\tconst c = _d1.dot( _r );\n\n\t\t\tif ( e <= EPSILON ) {\n\n\t\t\t\t// Second segment degenerates into a point\n\n\t\t\t\tt = 0;\n\t\t\t\ts = clamp( - c / a, 0, 1 ); // t = 0 => s = (b*t - c) / a = -c / a\n\n\t\t\t} else {\n\n\t\t\t\t// The general nondegenerate case starts here\n\n\t\t\t\tconst b = _d1.dot( _d2 );\n\t\t\t\tconst denom = a * e - b * b; // Always nonnegative\n\n\t\t\t\t// If segments not parallel, compute closest point on L1 to L2 and\n\t\t\t\t// clamp to segment S1. Else pick arbitrary s (here 0)\n\n\t\t\t\tif ( denom !== 0 ) {\n\n\t\t\t\t\ts = clamp( ( b * f - c * e ) / denom, 0, 1 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\ts = 0;\n\n\t\t\t\t}\n\n\t\t\t\t// Compute point on L2 closest to S1(s) using\n\t\t\t\t// t = Dot((P1 + D1*s) - P2,D2) / Dot(D2,D2) = (b*s + f) / e\n\n\t\t\t\tt = ( b * s + f ) / e;\n\n\t\t\t\t// If t in [0,1] done. Else clamp t, recompute s for the new value\n\t\t\t\t// of t using s = Dot((P2 + D2*t) - P1,D1) / Dot(D1,D1)= (t*b - c) / a\n\t\t\t\t// and clamp s to [0, 1]\n\n\t\t\t\tif ( t < 0 ) {\n\n\t\t\t\t\tt = 0.;\n\t\t\t\t\ts = clamp( - c / a, 0, 1 );\n\n\t\t\t\t} else if ( t > 1 ) {\n\n\t\t\t\t\tt = 1;\n\t\t\t\t\ts = clamp( ( b - c ) / a, 0, 1 );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tc1.copy( p1 ).add( _d1.multiplyScalar( s ) );\n\t\tc2.copy( p2 ).add( _d2.multiplyScalar( t ) );\n\n\t\tc1.sub( c2 );\n\n\t\treturn c1.dot( c1 );\n\n\t}\n\n\t/**\n\t * Applies a 4x4 transformation matrix to this line segment.\n\t *\n\t * @param {Matrix4} matrix - The transformation matrix.\n\t * @return {Line3} A reference to this line segment.\n\t */\n\tapplyMatrix4( matrix ) {\n\n\t\tthis.start.applyMatrix4( matrix );\n\t\tthis.end.applyMatrix4( matrix );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Returns `true` if this line segment is equal with the given one.\n\t *\n\t * @param {Line3} line - The line segment to test for equality.\n\t * @return {boolean} Whether this line segment is equal with the given one.\n\t */\n\tequals( line ) {\n\n\t\treturn line.start.equals( this.start ) && line.end.equals( this.end );\n\n\t}\n\n\t/**\n\t * Returns a new line segment with copied values from this instance.\n\t *\n\t * @return {Line3} A clone of this instance.\n\t */\n\tclone() {\n\n\t\treturn new this.constructor().copy( this );\n\n\t}\n\n}\n\nconst _vector$3 = /*@__PURE__*/ new Vector3();\n\n/**\n * This displays a cone shaped helper object for a {@link SpotLight}.\n *\n * ```js\n * const spotLight = new THREE.SpotLight( 0xffffff );\n * spotLight.position.set( 10, 10, 10 );\n * scene.add( spotLight );\n *\n * const spotLightHelper = new THREE.SpotLightHelper( spotLight );\n * scene.add( spotLightHelper );\n * ```\n *\n * @augments Object3D\n */\nclass SpotLightHelper extends Object3D {\n\n\t/**\n\t * Constructs a new spot light helper.\n\t *\n\t * @param {HemisphereLight} light - The light to be visualized.\n\t * @param {number|Color|string} [color] - The helper's color. If not set, the helper will take\n\t * the color of the light.\n\t */\n\tconstructor( light, color ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * The light being visualized.\n\t\t *\n\t\t * @type {SpotLight}\n\t\t */\n\t\tthis.light = light;\n\n\t\tthis.matrixAutoUpdate = false;\n\n\t\t/**\n\t\t * The color parameter passed in the constructor.\n\t\t * If not set, the helper will take the color of the light.\n\t\t *\n\t\t * @type {number|Color|string}\n\t\t */\n\t\tthis.color = color;\n\n\t\tthis.type = 'SpotLightHelper';\n\n\t\tconst geometry = new BufferGeometry();\n\n\t\tconst positions = [\n\t\t\t0, 0, 0, \t0, 0, 1,\n\t\t\t0, 0, 0, \t1, 0, 1,\n\t\t\t0, 0, 0,\t-1, 0, 1,\n\t\t\t0, 0, 0, \t0, 1, 1,\n\t\t\t0, 0, 0, \t0, -1, 1\n\t\t];\n\n\t\tfor ( let i = 0, j = 1, l = 32; i < l; i ++, j ++ ) {\n\n\t\t\tconst p1 = ( i / l ) * Math.PI * 2;\n\t\t\tconst p2 = ( j / l ) * Math.PI * 2;\n\n\t\t\tpositions.push(\n\t\t\t\tMath.cos( p1 ), Math.sin( p1 ), 1,\n\t\t\t\tMath.cos( p2 ), Math.sin( p2 ), 1\n\t\t\t);\n\n\t\t}\n\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );\n\n\t\tconst material = new LineBasicMaterial( { fog: false, toneMapped: false } );\n\n\t\tthis.cone = new LineSegments( geometry, material );\n\t\tthis.add( this.cone );\n\n\t\tthis.update();\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.cone.geometry.dispose();\n\t\tthis.cone.material.dispose();\n\n\t}\n\n\t/**\n\t * Updates the helper to match the position and direction of the\n\t * light being visualized.\n\t */\n\tupdate() {\n\n\t\tthis.light.updateWorldMatrix( true, false );\n\t\tthis.light.target.updateWorldMatrix( true, false );\n\n\t\t// update the local matrix based on the parent and light target transforms\n\t\tif ( this.parent ) {\n\n\t\t\tthis.parent.updateWorldMatrix( true );\n\n\t\t\tthis.matrix\n\t\t\t\t.copy( this.parent.matrixWorld )\n\t\t\t\t.invert()\n\t\t\t\t.multiply( this.light.matrixWorld );\n\n\t\t} else {\n\n\t\t\tthis.matrix.copy( this.light.matrixWorld );\n\n\t\t}\n\n\t\tthis.matrixWorld.copy( this.light.matrixWorld );\n\n\t\tconst coneLength = this.light.distance ? this.light.distance : 1000;\n\t\tconst coneWidth = coneLength * Math.tan( this.light.angle );\n\n\t\tthis.cone.scale.set( coneWidth, coneWidth, coneLength );\n\n\t\t_vector$3.setFromMatrixPosition( this.light.target.matrixWorld );\n\n\t\tthis.cone.lookAt( _vector$3 );\n\n\t\tif ( this.color !== undefined ) {\n\n\t\t\tthis.cone.material.color.set( this.color );\n\n\t\t} else {\n\n\t\t\tthis.cone.material.color.copy( this.light.color );\n\n\t\t}\n\n\t}\n\n}\n\nconst _vector$2 = /*@__PURE__*/ new Vector3();\nconst _boneMatrix = /*@__PURE__*/ new Matrix4();\nconst _matrixWorldInv = /*@__PURE__*/ new Matrix4();\n\n/**\n * A helper object to assist with visualizing a {@link Skeleton}.\n *\n * ```js\n * const helper = new THREE.SkeletonHelper( skinnedMesh );\n * scene.add( helper );\n * ```\n *\n * @augments LineSegments\n */\nclass SkeletonHelper extends LineSegments {\n\n\t/**\n\t * Constructs a new skeleton helper.\n\t *\n\t * @param {Object3D} object -  Usually an instance of {@link SkinnedMesh}. However, any 3D object\n\t * can be used if it represents a hierarchy of bones (see {@link Bone}).\n\t */\n\tconstructor( object ) {\n\n\t\tconst bones = getBoneList( object );\n\n\t\tconst geometry = new BufferGeometry();\n\n\t\tconst vertices = [];\n\t\tconst colors = [];\n\n\t\tfor ( let i = 0; i < bones.length; i ++ ) {\n\n\t\t\tconst bone = bones[ i ];\n\n\t\t\tif ( bone.parent && bone.parent.isBone ) {\n\n\t\t\t\tvertices.push( 0, 0, 0 );\n\t\t\t\tvertices.push( 0, 0, 0 );\n\t\t\t\tcolors.push( 0, 0, 0 );\n\t\t\t\tcolors.push( 0, 0, 0 );\n\n\t\t\t}\n\n\t\t}\n\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tgeometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );\n\n\t\tconst material = new LineBasicMaterial( { vertexColors: true, depthTest: false, depthWrite: false, toneMapped: false, transparent: true } );\n\n\t\tsuper( geometry, material );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isSkeletonHelper = true;\n\n\t\tthis.type = 'SkeletonHelper';\n\n\t\t/**\n\t\t * The object being visualized.\n\t\t *\n\t\t * @type {Object3D}\n\t\t */\n\t\tthis.root = object;\n\n\t\t/**\n\t\t * The list of bones that the helper visualizes.\n\t\t *\n\t\t * @type {Array<Bone>}\n\t\t */\n\t\tthis.bones = bones;\n\n\t\tthis.matrix = object.matrixWorld;\n\t\tthis.matrixAutoUpdate = false;\n\n\t\t// colors\n\n\t\tconst color1 = new Color( 0x0000ff );\n\t\tconst color2 = new Color( 0x00ff00 );\n\n\t\tthis.setColors( color1, color2 );\n\n\t}\n\n\tupdateMatrixWorld( force ) {\n\n\t\tconst bones = this.bones;\n\n\t\tconst geometry = this.geometry;\n\t\tconst position = geometry.getAttribute( 'position' );\n\n\t\t_matrixWorldInv.copy( this.root.matrixWorld ).invert();\n\n\t\tfor ( let i = 0, j = 0; i < bones.length; i ++ ) {\n\n\t\t\tconst bone = bones[ i ];\n\n\t\t\tif ( bone.parent && bone.parent.isBone ) {\n\n\t\t\t\t_boneMatrix.multiplyMatrices( _matrixWorldInv, bone.matrixWorld );\n\t\t\t\t_vector$2.setFromMatrixPosition( _boneMatrix );\n\t\t\t\tposition.setXYZ( j, _vector$2.x, _vector$2.y, _vector$2.z );\n\n\t\t\t\t_boneMatrix.multiplyMatrices( _matrixWorldInv, bone.parent.matrixWorld );\n\t\t\t\t_vector$2.setFromMatrixPosition( _boneMatrix );\n\t\t\t\tposition.setXYZ( j + 1, _vector$2.x, _vector$2.y, _vector$2.z );\n\n\t\t\t\tj += 2;\n\n\t\t\t}\n\n\t\t}\n\n\t\tgeometry.getAttribute( 'position' ).needsUpdate = true;\n\n\t\tsuper.updateMatrixWorld( force );\n\n\t}\n\n\t/**\n\t * Defines the colors of the helper.\n\t *\n\t * @param {Color} color1 - The first line color for each bone.\n\t * @param {Color} color2 - The second line color for each bone.\n\t * @return {SkeletonHelper} A reference to this helper.\n\t */\n\tsetColors( color1, color2 ) {\n\n\t\tconst geometry = this.geometry;\n\t\tconst colorAttribute = geometry.getAttribute( 'color' );\n\n\t\tfor ( let i = 0; i < colorAttribute.count; i += 2 ) {\n\n\t\t\tcolorAttribute.setXYZ( i, color1.r, color1.g, color1.b );\n\t\t\tcolorAttribute.setXYZ( i + 1, color2.r, color2.g, color2.b );\n\n\t\t}\n\n\t\tcolorAttribute.needsUpdate = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.geometry.dispose();\n\t\tthis.material.dispose();\n\n\t}\n\n}\n\n\nfunction getBoneList( object ) {\n\n\tconst boneList = [];\n\n\tif ( object.isBone === true ) {\n\n\t\tboneList.push( object );\n\n\t}\n\n\tfor ( let i = 0; i < object.children.length; i ++ ) {\n\n\t\tboneList.push( ...getBoneList( object.children[ i ] ) );\n\n\t}\n\n\treturn boneList;\n\n}\n\n/**\n * This displays a helper object consisting of a spherical mesh for\n * visualizing an instance of {@link PointLight}.\n *\n * ```js\n * const pointLight = new THREE.PointLight( 0xff0000, 1, 100 );\n * pointLight.position.set( 10, 10, 10 );\n * scene.add( pointLight );\n *\n * const sphereSize = 1;\n * const pointLightHelper = new THREE.PointLightHelper( pointLight, sphereSize );\n * scene.add( pointLightHelper );\n * ```\n *\n * @augments Mesh\n */\nclass PointLightHelper extends Mesh {\n\n\t/**\n\t * Constructs a new point light helper.\n\t *\n\t * @param {PointLight} light - The light to be visualized.\n\t * @param {number} [sphereSize=1] - The size of the sphere helper.\n\t * @param {number|Color|string} [color] - The helper's color. If not set, the helper will take\n\t * the color of the light.\n\t */\n\tconstructor( light, sphereSize, color ) {\n\n\t\tconst geometry = new SphereGeometry( sphereSize, 4, 2 );\n\t\tconst material = new MeshBasicMaterial( { wireframe: true, fog: false, toneMapped: false } );\n\n\t\tsuper( geometry, material );\n\n\t\t/**\n\t\t * The light being visualized.\n\t\t *\n\t\t * @type {PointLight}\n\t\t */\n\t\tthis.light = light;\n\n\t\t/**\n\t\t * The color parameter passed in the constructor.\n\t\t * If not set, the helper will take the color of the light.\n\t\t *\n\t\t * @type {number|Color|string}\n\t\t */\n\t\tthis.color = color;\n\n\t\tthis.type = 'PointLightHelper';\n\n\t\tthis.matrix = this.light.matrixWorld;\n\t\tthis.matrixAutoUpdate = false;\n\n\t\tthis.update();\n\n\n\t\t/*\n\t// TODO: delete this comment?\n\tconst distanceGeometry = new THREE.IcosahedronGeometry( 1, 2 );\n\tconst distanceMaterial = new THREE.MeshBasicMaterial( { color: hexColor, fog: false, wireframe: true, opacity: 0.1, transparent: true } );\n\n\tthis.lightSphere = new THREE.Mesh( bulbGeometry, bulbMaterial );\n\tthis.lightDistance = new THREE.Mesh( distanceGeometry, distanceMaterial );\n\n\tconst d = light.distance;\n\n\tif ( d === 0.0 ) {\n\n\t\tthis.lightDistance.visible = false;\n\n\t} else {\n\n\t\tthis.lightDistance.scale.set( d, d, d );\n\n\t}\n\n\tthis.add( this.lightDistance );\n\t*/\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.geometry.dispose();\n\t\tthis.material.dispose();\n\n\t}\n\n\t/**\n\t * Updates the helper to match the position of the\n\t * light being visualized.\n\t */\n\tupdate() {\n\n\t\tthis.light.updateWorldMatrix( true, false );\n\n\t\tif ( this.color !== undefined ) {\n\n\t\t\tthis.material.color.set( this.color );\n\n\t\t} else {\n\n\t\t\tthis.material.color.copy( this.light.color );\n\n\t\t}\n\n\t\t/*\n\t\tconst d = this.light.distance;\n\n\t\tif ( d === 0.0 ) {\n\n\t\t\tthis.lightDistance.visible = false;\n\n\t\t} else {\n\n\t\t\tthis.lightDistance.visible = true;\n\t\t\tthis.lightDistance.scale.set( d, d, d );\n\n\t\t}\n\t\t*/\n\n\t}\n\n}\n\nconst _vector$1 = /*@__PURE__*/ new Vector3();\nconst _color1 = /*@__PURE__*/ new Color();\nconst _color2 = /*@__PURE__*/ new Color();\n\n/**\n * Creates a visual aid consisting of a spherical mesh for a\n * given {@link HemisphereLight}.\n *\n * ```js\n * const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );\n * const helper = new THREE.HemisphereLightHelper( light, 5 );\n * scene.add( helper );\n * ```\n *\n * @augments Object3D\n */\nclass HemisphereLightHelper extends Object3D {\n\n\t/**\n\t * Constructs a new hemisphere light helper.\n\t *\n\t * @param {HemisphereLight} light - The light to be visualized.\n\t * @param {number} [size=1] - The size of the mesh used to visualize the light.\n\t * @param {number|Color|string} [color] - The helper's color. If not set, the helper will take\n\t * the color of the light.\n\t */\n\tconstructor( light, size, color ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * The light being visualized.\n\t\t *\n\t\t * @type {HemisphereLight}\n\t\t */\n\t\tthis.light = light;\n\n\t\tthis.matrix = light.matrixWorld;\n\t\tthis.matrixAutoUpdate = false;\n\n\t\t/**\n\t\t * The color parameter passed in the constructor.\n\t\t * If not set, the helper will take the color of the light.\n\t\t *\n\t\t * @type {number|Color|string}\n\t\t */\n\t\tthis.color = color;\n\n\t\tthis.type = 'HemisphereLightHelper';\n\n\t\tconst geometry = new OctahedronGeometry( size );\n\t\tgeometry.rotateY( Math.PI * 0.5 );\n\n\t\tthis.material = new MeshBasicMaterial( { wireframe: true, fog: false, toneMapped: false } );\n\t\tif ( this.color === undefined ) this.material.vertexColors = true;\n\n\t\tconst position = geometry.getAttribute( 'position' );\n\t\tconst colors = new Float32Array( position.count * 3 );\n\n\t\tgeometry.setAttribute( 'color', new BufferAttribute( colors, 3 ) );\n\n\t\tthis.add( new Mesh( geometry, this.material ) );\n\n\t\tthis.update();\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.children[ 0 ].geometry.dispose();\n\t\tthis.children[ 0 ].material.dispose();\n\n\t}\n\n\t/**\n\t * Updates the helper to match the position and direction of the\n\t * light being visualized.\n\t */\n\tupdate() {\n\n\t\tconst mesh = this.children[ 0 ];\n\n\t\tif ( this.color !== undefined ) {\n\n\t\t\tthis.material.color.set( this.color );\n\n\t\t} else {\n\n\t\t\tconst colors = mesh.geometry.getAttribute( 'color' );\n\n\t\t\t_color1.copy( this.light.color );\n\t\t\t_color2.copy( this.light.groundColor );\n\n\t\t\tfor ( let i = 0, l = colors.count; i < l; i ++ ) {\n\n\t\t\t\tconst color = ( i < ( l / 2 ) ) ? _color1 : _color2;\n\n\t\t\t\tcolors.setXYZ( i, color.r, color.g, color.b );\n\n\t\t\t}\n\n\t\t\tcolors.needsUpdate = true;\n\n\t\t}\n\n\t\tthis.light.updateWorldMatrix( true, false );\n\n\t\tmesh.lookAt( _vector$1.setFromMatrixPosition( this.light.matrixWorld ).negate() );\n\n\t}\n\n}\n\n/**\n * The helper is an object to define grids. Grids are two-dimensional\n * arrays of lines.\n *\n * ```js\n * const size = 10;\n * const divisions = 10;\n *\n * const gridHelper = new THREE.GridHelper( size, divisions );\n * scene.add( gridHelper );\n * ```\n *\n * @augments LineSegments\n */\nclass GridHelper extends LineSegments {\n\n\t/**\n\t * Constructs a new grid helper.\n\t *\n\t * @param {number} [size=10] - The size of the grid.\n\t * @param {number} [divisions=10] - The number of divisions across the grid.\n\t * @param {number|Color|string} [color1=0x444444] - The color of the center line.\n\t * @param {number|Color|string} [color2=0x888888] - The color of the lines of the grid.\n\t */\n\tconstructor( size = 10, divisions = 10, color1 = 0x444444, color2 = 0x888888 ) {\n\n\t\tcolor1 = new Color( color1 );\n\t\tcolor2 = new Color( color2 );\n\n\t\tconst center = divisions / 2;\n\t\tconst step = size / divisions;\n\t\tconst halfSize = size / 2;\n\n\t\tconst vertices = [], colors = [];\n\n\t\tfor ( let i = 0, j = 0, k = - halfSize; i <= divisions; i ++, k += step ) {\n\n\t\t\tvertices.push( - halfSize, 0, k, halfSize, 0, k );\n\t\t\tvertices.push( k, 0, - halfSize, k, 0, halfSize );\n\n\t\t\tconst color = i === center ? color1 : color2;\n\n\t\t\tcolor.toArray( colors, j ); j += 3;\n\t\t\tcolor.toArray( colors, j ); j += 3;\n\t\t\tcolor.toArray( colors, j ); j += 3;\n\t\t\tcolor.toArray( colors, j ); j += 3;\n\n\t\t}\n\n\t\tconst geometry = new BufferGeometry();\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tgeometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );\n\n\t\tconst material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } );\n\n\t\tsuper( geometry, material );\n\n\t\tthis.type = 'GridHelper';\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.geometry.dispose();\n\t\tthis.material.dispose();\n\n\t}\n\n}\n\n/**\n * This helper is an object to define polar grids. Grids are\n * two-dimensional arrays of lines.\n *\n * ```js\n * const radius = 10;\n * const sectors = 16;\n * const rings = 8;\n * const divisions = 64;\n *\n * const helper = new THREE.PolarGridHelper( radius, sectors, rings, divisions );\n * scene.add( helper );\n * ```\n *\n * @augments LineSegments\n */\nclass PolarGridHelper extends LineSegments {\n\n\t/**\n\t * Constructs a new polar grid helper.\n\t *\n\t * @param {number} [radius=10] - The radius of the polar grid. This can be any positive number.\n\t * @param {number} [sectors=16] - The number of sectors the grid will be divided into. This can be any positive integer.\n\t * @param {number} [rings=16] - The number of rings. This can be any positive integer.\n\t * @param {number} [divisions=64] - The number of line segments used for each circle. This can be any positive integer.\n\t * @param {number|Color|string} [color1=0x444444] - The first color used for grid elements.\n\t * @param {number|Color|string} [color2=0x888888] -  The second color used for grid elements.\n\t */\n\tconstructor( radius = 10, sectors = 16, rings = 8, divisions = 64, color1 = 0x444444, color2 = 0x888888 ) {\n\n\t\tcolor1 = new Color( color1 );\n\t\tcolor2 = new Color( color2 );\n\n\t\tconst vertices = [];\n\t\tconst colors = [];\n\n\t\t// create the sectors\n\n\t\tif ( sectors > 1 ) {\n\n\t\t\tfor ( let i = 0; i < sectors; i ++ ) {\n\n\t\t\t\tconst v = ( i / sectors ) * ( Math.PI * 2 );\n\n\t\t\t\tconst x = Math.sin( v ) * radius;\n\t\t\t\tconst z = Math.cos( v ) * radius;\n\n\t\t\t\tvertices.push( 0, 0, 0 );\n\t\t\t\tvertices.push( x, 0, z );\n\n\t\t\t\tconst color = ( i & 1 ) ? color1 : color2;\n\n\t\t\t\tcolors.push( color.r, color.g, color.b );\n\t\t\t\tcolors.push( color.r, color.g, color.b );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// create the rings\n\n\t\tfor ( let i = 0; i < rings; i ++ ) {\n\n\t\t\tconst color = ( i & 1 ) ? color1 : color2;\n\n\t\t\tconst r = radius - ( radius / rings * i );\n\n\t\t\tfor ( let j = 0; j < divisions; j ++ ) {\n\n\t\t\t\t// first vertex\n\n\t\t\t\tlet v = ( j / divisions ) * ( Math.PI * 2 );\n\n\t\t\t\tlet x = Math.sin( v ) * r;\n\t\t\t\tlet z = Math.cos( v ) * r;\n\n\t\t\t\tvertices.push( x, 0, z );\n\t\t\t\tcolors.push( color.r, color.g, color.b );\n\n\t\t\t\t// second vertex\n\n\t\t\t\tv = ( ( j + 1 ) / divisions ) * ( Math.PI * 2 );\n\n\t\t\t\tx = Math.sin( v ) * r;\n\t\t\t\tz = Math.cos( v ) * r;\n\n\t\t\t\tvertices.push( x, 0, z );\n\t\t\t\tcolors.push( color.r, color.g, color.b );\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst geometry = new BufferGeometry();\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tgeometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );\n\n\t\tconst material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } );\n\n\t\tsuper( geometry, material );\n\n\t\tthis.type = 'PolarGridHelper';\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.geometry.dispose();\n\t\tthis.material.dispose();\n\n\t}\n\n}\n\nconst _v1 = /*@__PURE__*/ new Vector3();\nconst _v2 = /*@__PURE__*/ new Vector3();\nconst _v3 = /*@__PURE__*/ new Vector3();\n\n/**\n * Helper object to assist with visualizing a {@link DirectionalLight}'s\n * effect on the scene. This consists of plane and a line representing the\n * light's position and direction.\n *\n * ```js\n * const light = new THREE.DirectionalLight( 0xFFFFFF );\n * scene.add( light );\n *\n * const helper = new THREE.DirectionalLightHelper( light, 5 );\n * scene.add( helper );\n * ```\n *\n * @augments Object3D\n */\nclass DirectionalLightHelper extends Object3D {\n\n\t/**\n\t * Constructs a new directional light helper.\n\t *\n\t * @param {DirectionalLight} light - The light to be visualized.\n\t * @param {number} [size=1] - The dimensions of the plane.\n\t * @param {number|Color|string} [color] - The helper's color. If not set, the helper will take\n\t * the color of the light.\n\t */\n\tconstructor( light, size, color ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * The light being visualized.\n\t\t *\n\t\t * @type {DirectionalLight}\n\t\t */\n\t\tthis.light = light;\n\n\t\tthis.matrix = light.matrixWorld;\n\t\tthis.matrixAutoUpdate = false;\n\n\t\t/**\n\t\t * The color parameter passed in the constructor.\n\t\t * If not set, the helper will take the color of the light.\n\t\t *\n\t\t * @type {number|Color|string}\n\t\t */\n\t\tthis.color = color;\n\n\t\tthis.type = 'DirectionalLightHelper';\n\n\t\tif ( size === undefined ) size = 1;\n\n\t\tlet geometry = new BufferGeometry();\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( [\n\t\t\t- size, size, 0,\n\t\t\tsize, size, 0,\n\t\t\tsize, - size, 0,\n\t\t\t- size, - size, 0,\n\t\t\t- size, size, 0\n\t\t], 3 ) );\n\n\t\tconst material = new LineBasicMaterial( { fog: false, toneMapped: false } );\n\n\t\t/**\n\t\t * Contains the line showing the location of the directional light.\n\t\t *\n\t\t * @type {Line}\n\t\t */\n\t\tthis.lightPlane = new Line( geometry, material );\n\t\tthis.add( this.lightPlane );\n\n\t\tgeometry = new BufferGeometry();\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 0, 0, 1 ], 3 ) );\n\n\t\t/**\n\t\t * Represents the target line of the directional light.\n\t\t *\n\t\t * @type {Line}\n\t\t */\n\t\tthis.targetLine = new Line( geometry, material );\n\t\tthis.add( this.targetLine );\n\n\t\tthis.update();\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.lightPlane.geometry.dispose();\n\t\tthis.lightPlane.material.dispose();\n\t\tthis.targetLine.geometry.dispose();\n\t\tthis.targetLine.material.dispose();\n\n\t}\n\n\t/**\n\t * Updates the helper to match the position and direction of the\n\t * light being visualized.\n\t */\n\tupdate() {\n\n\t\tthis.light.updateWorldMatrix( true, false );\n\t\tthis.light.target.updateWorldMatrix( true, false );\n\n\t\t_v1.setFromMatrixPosition( this.light.matrixWorld );\n\t\t_v2.setFromMatrixPosition( this.light.target.matrixWorld );\n\t\t_v3.subVectors( _v2, _v1 );\n\n\t\tthis.lightPlane.lookAt( _v2 );\n\n\t\tif ( this.color !== undefined ) {\n\n\t\t\tthis.lightPlane.material.color.set( this.color );\n\t\t\tthis.targetLine.material.color.set( this.color );\n\n\t\t} else {\n\n\t\t\tthis.lightPlane.material.color.copy( this.light.color );\n\t\t\tthis.targetLine.material.color.copy( this.light.color );\n\n\t\t}\n\n\t\tthis.targetLine.lookAt( _v2 );\n\t\tthis.targetLine.scale.z = _v3.length();\n\n\t}\n\n}\n\nconst _vector = /*@__PURE__*/ new Vector3();\nconst _camera = /*@__PURE__*/ new Camera();\n\n/**\n * This helps with visualizing what a camera contains in its frustum. It\n * visualizes the frustum of a camera using a line segments.\n *\n * Based on frustum visualization in [lightgl.js shadowmap example](https://github.com/evanw/lightgl.js/blob/master/tests/shadowmap.html).\n *\n * `CameraHelper` must be a child of the scene.\n *\n * ```js\n * const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );\n * const helper = new THREE.CameraHelper( camera );\n * scene.add( helper );\n * ```\n *\n * @augments LineSegments\n */\nclass CameraHelper extends LineSegments {\n\n\t/**\n\t * Constructs a new arrow helper.\n\t *\n\t * @param {Camera} camera - The camera to visualize.\n\t */\n\tconstructor( camera ) {\n\n\t\tconst geometry = new BufferGeometry();\n\t\tconst material = new LineBasicMaterial( { color: 0xffffff, vertexColors: true, toneMapped: false } );\n\n\t\tconst vertices = [];\n\t\tconst colors = [];\n\n\t\tconst pointMap = {};\n\n\t\t// near\n\n\t\taddLine( 'n1', 'n2' );\n\t\taddLine( 'n2', 'n4' );\n\t\taddLine( 'n4', 'n3' );\n\t\taddLine( 'n3', 'n1' );\n\n\t\t// far\n\n\t\taddLine( 'f1', 'f2' );\n\t\taddLine( 'f2', 'f4' );\n\t\taddLine( 'f4', 'f3' );\n\t\taddLine( 'f3', 'f1' );\n\n\t\t// sides\n\n\t\taddLine( 'n1', 'f1' );\n\t\taddLine( 'n2', 'f2' );\n\t\taddLine( 'n3', 'f3' );\n\t\taddLine( 'n4', 'f4' );\n\n\t\t// cone\n\n\t\taddLine( 'p', 'n1' );\n\t\taddLine( 'p', 'n2' );\n\t\taddLine( 'p', 'n3' );\n\t\taddLine( 'p', 'n4' );\n\n\t\t// up\n\n\t\taddLine( 'u1', 'u2' );\n\t\taddLine( 'u2', 'u3' );\n\t\taddLine( 'u3', 'u1' );\n\n\t\t// target\n\n\t\taddLine( 'c', 't' );\n\t\taddLine( 'p', 'c' );\n\n\t\t// cross\n\n\t\taddLine( 'cn1', 'cn2' );\n\t\taddLine( 'cn3', 'cn4' );\n\n\t\taddLine( 'cf1', 'cf2' );\n\t\taddLine( 'cf3', 'cf4' );\n\n\t\tfunction addLine( a, b ) {\n\n\t\t\taddPoint( a );\n\t\t\taddPoint( b );\n\n\t\t}\n\n\t\tfunction addPoint( id ) {\n\n\t\t\tvertices.push( 0, 0, 0 );\n\t\t\tcolors.push( 0, 0, 0 );\n\n\t\t\tif ( pointMap[ id ] === undefined ) {\n\n\t\t\t\tpointMap[ id ] = [];\n\n\t\t\t}\n\n\t\t\tpointMap[ id ].push( ( vertices.length / 3 ) - 1 );\n\n\t\t}\n\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tgeometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );\n\n\t\tsuper( geometry, material );\n\n\t\tthis.type = 'CameraHelper';\n\n\t\t/**\n\t\t * The camera being visualized.\n\t\t *\n\t\t * @type {Camera}\n\t\t */\n\t\tthis.camera = camera;\n\t\tif ( this.camera.updateProjectionMatrix ) this.camera.updateProjectionMatrix();\n\n\t\tthis.matrix = camera.matrixWorld;\n\t\tthis.matrixAutoUpdate = false;\n\n\t\t/**\n\t\t * This contains the points used to visualize the camera.\n\t\t *\n\t\t * @type {Object<string,Array<number>>}\n\t\t */\n\t\tthis.pointMap = pointMap;\n\n\t\tthis.update();\n\n\t\t// colors\n\n\t\tconst colorFrustum = new Color( 0xffaa00 );\n\t\tconst colorCone = new Color( 0xff0000 );\n\t\tconst colorUp = new Color( 0x00aaff );\n\t\tconst colorTarget = new Color( 0xffffff );\n\t\tconst colorCross = new Color( 0x333333 );\n\n\t\tthis.setColors( colorFrustum, colorCone, colorUp, colorTarget, colorCross );\n\n\t}\n\n\t/**\n\t * Defines the colors of the helper.\n\t *\n\t * @param {Color} frustum - The frustum line color.\n\t * @param {Color} cone - The cone line color.\n\t * @param {Color} up - The up line color.\n\t * @param {Color} target - The target line color.\n\t * @param {Color} cross - The cross line color.\n\t * @return {CameraHelper} A reference to this helper.\n\t */\n\tsetColors( frustum, cone, up, target, cross ) {\n\n\t\tconst geometry = this.geometry;\n\n\t\tconst colorAttribute = geometry.getAttribute( 'color' );\n\n\t\t// near\n\n\t\tcolorAttribute.setXYZ( 0, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 1, frustum.r, frustum.g, frustum.b ); // n1, n2\n\t\tcolorAttribute.setXYZ( 2, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 3, frustum.r, frustum.g, frustum.b ); // n2, n4\n\t\tcolorAttribute.setXYZ( 4, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 5, frustum.r, frustum.g, frustum.b ); // n4, n3\n\t\tcolorAttribute.setXYZ( 6, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 7, frustum.r, frustum.g, frustum.b ); // n3, n1\n\n\t\t// far\n\n\t\tcolorAttribute.setXYZ( 8, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 9, frustum.r, frustum.g, frustum.b ); // f1, f2\n\t\tcolorAttribute.setXYZ( 10, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 11, frustum.r, frustum.g, frustum.b ); // f2, f4\n\t\tcolorAttribute.setXYZ( 12, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 13, frustum.r, frustum.g, frustum.b ); // f4, f3\n\t\tcolorAttribute.setXYZ( 14, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 15, frustum.r, frustum.g, frustum.b ); // f3, f1\n\n\t\t// sides\n\n\t\tcolorAttribute.setXYZ( 16, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 17, frustum.r, frustum.g, frustum.b ); // n1, f1\n\t\tcolorAttribute.setXYZ( 18, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 19, frustum.r, frustum.g, frustum.b ); // n2, f2\n\t\tcolorAttribute.setXYZ( 20, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 21, frustum.r, frustum.g, frustum.b ); // n3, f3\n\t\tcolorAttribute.setXYZ( 22, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 23, frustum.r, frustum.g, frustum.b ); // n4, f4\n\n\t\t// cone\n\n\t\tcolorAttribute.setXYZ( 24, cone.r, cone.g, cone.b ); colorAttribute.setXYZ( 25, cone.r, cone.g, cone.b ); // p, n1\n\t\tcolorAttribute.setXYZ( 26, cone.r, cone.g, cone.b ); colorAttribute.setXYZ( 27, cone.r, cone.g, cone.b ); // p, n2\n\t\tcolorAttribute.setXYZ( 28, cone.r, cone.g, cone.b ); colorAttribute.setXYZ( 29, cone.r, cone.g, cone.b ); // p, n3\n\t\tcolorAttribute.setXYZ( 30, cone.r, cone.g, cone.b ); colorAttribute.setXYZ( 31, cone.r, cone.g, cone.b ); // p, n4\n\n\t\t// up\n\n\t\tcolorAttribute.setXYZ( 32, up.r, up.g, up.b ); colorAttribute.setXYZ( 33, up.r, up.g, up.b ); // u1, u2\n\t\tcolorAttribute.setXYZ( 34, up.r, up.g, up.b ); colorAttribute.setXYZ( 35, up.r, up.g, up.b ); // u2, u3\n\t\tcolorAttribute.setXYZ( 36, up.r, up.g, up.b ); colorAttribute.setXYZ( 37, up.r, up.g, up.b ); // u3, u1\n\n\t\t// target\n\n\t\tcolorAttribute.setXYZ( 38, target.r, target.g, target.b ); colorAttribute.setXYZ( 39, target.r, target.g, target.b ); // c, t\n\t\tcolorAttribute.setXYZ( 40, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 41, cross.r, cross.g, cross.b ); // p, c\n\n\t\t// cross\n\n\t\tcolorAttribute.setXYZ( 42, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 43, cross.r, cross.g, cross.b ); // cn1, cn2\n\t\tcolorAttribute.setXYZ( 44, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 45, cross.r, cross.g, cross.b ); // cn3, cn4\n\n\t\tcolorAttribute.setXYZ( 46, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 47, cross.r, cross.g, cross.b ); // cf1, cf2\n\t\tcolorAttribute.setXYZ( 48, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 49, cross.r, cross.g, cross.b ); // cf3, cf4\n\n\t\tcolorAttribute.needsUpdate = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Updates the helper based on the projection matrix of the camera.\n\t */\n\tupdate() {\n\n\t\tconst geometry = this.geometry;\n\t\tconst pointMap = this.pointMap;\n\n\t\tconst w = 1, h = 1;\n\n\t\tlet nearZ, farZ;\n\n\t\t// we need just camera projection matrix inverse\n\t\t// world matrix must be identity\n\n\t\t_camera.projectionMatrixInverse.copy( this.camera.projectionMatrixInverse );\n\n\t\t// Adjust z values based on coordinate system\n\n\t\tif ( this.camera.reversedDepth === true ) {\n\n\t\t\tnearZ = 1;\n\t\t\tfarZ = 0;\n\n\t\t} else {\n\n\t\t\tif ( this.camera.coordinateSystem === WebGLCoordinateSystem ) {\n\n\t\t\t\tnearZ = -1;\n\t\t\t\tfarZ = 1;\n\n\t\t\t} else if ( this.camera.coordinateSystem === WebGPUCoordinateSystem ) {\n\n\t\t\t\tnearZ = 0;\n\t\t\t\tfarZ = 1;\n\n\t\t\t} else {\n\n\t\t\t\tthrow new Error( 'THREE.CameraHelper.update(): Invalid coordinate system: ' + this.camera.coordinateSystem );\n\n\t\t\t}\n\n\t\t}\n\n\n\t\t// center / target\n\t\tsetPoint( 'c', pointMap, geometry, _camera, 0, 0, nearZ );\n\t\tsetPoint( 't', pointMap, geometry, _camera, 0, 0, farZ );\n\n\t\t// near\n\n\t\tsetPoint( 'n1', pointMap, geometry, _camera, - w, - h, nearZ );\n\t\tsetPoint( 'n2', pointMap, geometry, _camera, w, - h, nearZ );\n\t\tsetPoint( 'n3', pointMap, geometry, _camera, - w, h, nearZ );\n\t\tsetPoint( 'n4', pointMap, geometry, _camera, w, h, nearZ );\n\n\t\t// far\n\n\t\tsetPoint( 'f1', pointMap, geometry, _camera, - w, - h, farZ );\n\t\tsetPoint( 'f2', pointMap, geometry, _camera, w, - h, farZ );\n\t\tsetPoint( 'f3', pointMap, geometry, _camera, - w, h, farZ );\n\t\tsetPoint( 'f4', pointMap, geometry, _camera, w, h, farZ );\n\n\t\t// up\n\n\t\tsetPoint( 'u1', pointMap, geometry, _camera, w * 0.7, h * 1.1, nearZ );\n\t\tsetPoint( 'u2', pointMap, geometry, _camera, - w * 0.7, h * 1.1, nearZ );\n\t\tsetPoint( 'u3', pointMap, geometry, _camera, 0, h * 2, nearZ );\n\n\t\t// cross\n\n\t\tsetPoint( 'cf1', pointMap, geometry, _camera, - w, 0, farZ );\n\t\tsetPoint( 'cf2', pointMap, geometry, _camera, w, 0, farZ );\n\t\tsetPoint( 'cf3', pointMap, geometry, _camera, 0, - h, farZ );\n\t\tsetPoint( 'cf4', pointMap, geometry, _camera, 0, h, farZ );\n\n\t\tsetPoint( 'cn1', pointMap, geometry, _camera, - w, 0, nearZ );\n\t\tsetPoint( 'cn2', pointMap, geometry, _camera, w, 0, nearZ );\n\t\tsetPoint( 'cn3', pointMap, geometry, _camera, 0, - h, nearZ );\n\t\tsetPoint( 'cn4', pointMap, geometry, _camera, 0, h, nearZ );\n\n\t\tgeometry.getAttribute( 'position' ).needsUpdate = true;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.geometry.dispose();\n\t\tthis.material.dispose();\n\n\t}\n\n}\n\n\nfunction setPoint( point, pointMap, geometry, camera, x, y, z ) {\n\n\t_vector.set( x, y, z ).unproject( camera );\n\n\tconst points = pointMap[ point ];\n\n\tif ( points !== undefined ) {\n\n\t\tconst position = geometry.getAttribute( 'position' );\n\n\t\tfor ( let i = 0, l = points.length; i < l; i ++ ) {\n\n\t\t\tposition.setXYZ( points[ i ], _vector.x, _vector.y, _vector.z );\n\n\t\t}\n\n\t}\n\n}\n\nconst _box = /*@__PURE__*/ new Box3();\n\n/**\n * Helper object to graphically show the world-axis-aligned bounding box\n * around an object. The actual bounding box is handled with {@link Box3},\n * this is just a visual helper for debugging. It can be automatically\n * resized with {@link BoxHelper#update} when the object it's created from\n * is transformed. Note that the object must have a geometry for this to work,\n * so it won't work with sprites.\n *\n * ```js\n * const sphere = new THREE.SphereGeometry();\n * const object = new THREE.Mesh( sphere, new THREE.MeshBasicMaterial( 0xff0000 ) );\n * const box = new THREE.BoxHelper( object, 0xffff00 );\n * scene.add( box );\n * ```\n *\n * @augments LineSegments\n */\nclass BoxHelper extends LineSegments {\n\n\t/**\n\t * Constructs a new box helper.\n\t *\n\t * @param {Object3D} [object] - The 3D object to show the world-axis-aligned bounding box.\n\t * @param {number|Color|string} [color=0xffff00] - The box's color.\n\t */\n\tconstructor( object, color = 0xffff00 ) {\n\n\t\tconst indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] );\n\t\tconst positions = new Float32Array( 8 * 3 );\n\n\t\tconst geometry = new BufferGeometry();\n\t\tgeometry.setIndex( new BufferAttribute( indices, 1 ) );\n\t\tgeometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );\n\n\t\tsuper( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );\n\n\t\t/**\n\t\t * The 3D object being visualized.\n\t\t *\n\t\t * @type {Object3D}\n\t\t */\n\t\tthis.object = object;\n\t\tthis.type = 'BoxHelper';\n\n\t\tthis.matrixAutoUpdate = false;\n\n\t\tthis.update();\n\n\t}\n\n\t/**\n\t * Updates the helper's geometry to match the dimensions of the object,\n\t * including any children.\n\t */\n\tupdate() {\n\n\t\tif ( this.object !== undefined ) {\n\n\t\t\t_box.setFromObject( this.object );\n\n\t\t}\n\n\t\tif ( _box.isEmpty() ) return;\n\n\t\tconst min = _box.min;\n\t\tconst max = _box.max;\n\n\t\t/*\n\t\t\t5____4\n\t\t1/___0/|\n\t\t| 6__|_7\n\t\t2/___3/\n\n\t\t0: max.x, max.y, max.z\n\t\t1: min.x, max.y, max.z\n\t\t2: min.x, min.y, max.z\n\t\t3: max.x, min.y, max.z\n\t\t4: max.x, max.y, min.z\n\t\t5: min.x, max.y, min.z\n\t\t6: min.x, min.y, min.z\n\t\t7: max.x, min.y, min.z\n\t\t*/\n\n\t\tconst position = this.geometry.attributes.position;\n\t\tconst array = position.array;\n\n\t\tarray[ 0 ] = max.x; array[ 1 ] = max.y; array[ 2 ] = max.z;\n\t\tarray[ 3 ] = min.x; array[ 4 ] = max.y; array[ 5 ] = max.z;\n\t\tarray[ 6 ] = min.x; array[ 7 ] = min.y; array[ 8 ] = max.z;\n\t\tarray[ 9 ] = max.x; array[ 10 ] = min.y; array[ 11 ] = max.z;\n\t\tarray[ 12 ] = max.x; array[ 13 ] = max.y; array[ 14 ] = min.z;\n\t\tarray[ 15 ] = min.x; array[ 16 ] = max.y; array[ 17 ] = min.z;\n\t\tarray[ 18 ] = min.x; array[ 19 ] = min.y; array[ 20 ] = min.z;\n\t\tarray[ 21 ] = max.x; array[ 22 ] = min.y; array[ 23 ] = min.z;\n\n\t\tposition.needsUpdate = true;\n\n\t\tthis.geometry.computeBoundingSphere();\n\n\t}\n\n\t/**\n\t * Updates the wireframe box for the passed object.\n\t *\n\t * @param {Object3D} object - The 3D object to create the helper for.\n\t * @return {BoxHelper} A reference to this instance.\n\t */\n\tsetFromObject( object ) {\n\n\t\tthis.object = object;\n\t\tthis.update();\n\n\t\treturn this;\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.object = source.object;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.geometry.dispose();\n\t\tthis.material.dispose();\n\n\t}\n\n}\n\n/**\n * A helper object to visualize an instance of {@link Box3}.\n *\n * ```js\n * const box = new THREE.Box3();\n * box.setFromCenterAndSize( new THREE.Vector3( 1, 1, 1 ), new THREE.Vector3( 2, 1, 3 ) );\n *\n * const helper = new THREE.Box3Helper( box, 0xffff00 );\n * scene.add( helper )\n * ```\n *\n * @augments LineSegments\n */\nclass Box3Helper extends LineSegments {\n\n\t/**\n\t * Constructs a new box3 helper.\n\t *\n\t * @param {Box3} box - The box to visualize.\n\t * @param {number|Color|string} [color=0xffff00] - The box's color.\n\t */\n\tconstructor( box, color = 0xffff00 ) {\n\n\t\tconst indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] );\n\n\t\tconst positions = [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1 ];\n\n\t\tconst geometry = new BufferGeometry();\n\n\t\tgeometry.setIndex( new BufferAttribute( indices, 1 ) );\n\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );\n\n\t\tsuper( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );\n\n\t\t/**\n\t\t * The box being visualized.\n\t\t *\n\t\t * @type {Box3}\n\t\t */\n\t\tthis.box = box;\n\n\t\tthis.type = 'Box3Helper';\n\n\t\tthis.geometry.computeBoundingSphere();\n\n\t}\n\n\tupdateMatrixWorld( force ) {\n\n\t\tconst box = this.box;\n\n\t\tif ( box.isEmpty() ) return;\n\n\t\tbox.getCenter( this.position );\n\n\t\tbox.getSize( this.scale );\n\n\t\tthis.scale.multiplyScalar( 0.5 );\n\n\t\tsuper.updateMatrixWorld( force );\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.geometry.dispose();\n\t\tthis.material.dispose();\n\n\t}\n\n}\n\n/**\n * A helper object to visualize an instance of {@link Plane}.\n *\n * ```js\n * const plane = new THREE.Plane( new THREE.Vector3( 1, 1, 0.2 ), 3 );\n * const helper = new THREE.PlaneHelper( plane, 1, 0xffff00 );\n * scene.add( helper );\n * ```\n *\n * @augments Line\n */\nclass PlaneHelper extends Line {\n\n\t/**\n\t * Constructs a new plane helper.\n\t *\n\t * @param {Plane} plane - The plane to be visualized.\n\t * @param {number} [size=1] - The side length of plane helper.\n\t * @param {number|Color|string} [hex=0xffff00] - The helper's color.\n\t */\n\tconstructor( plane, size = 1, hex = 0xffff00 ) {\n\n\t\tconst color = hex;\n\n\t\tconst positions = [ 1, -1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0, 1, 1, 0 ];\n\n\t\tconst geometry = new BufferGeometry();\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );\n\t\tgeometry.computeBoundingSphere();\n\n\t\tsuper( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );\n\n\t\tthis.type = 'PlaneHelper';\n\n\t\t/**\n\t\t * The plane being visualized.\n\t\t *\n\t\t * @type {Plane}\n\t\t */\n\t\tthis.plane = plane;\n\n\t\t/**\n\t\t * The side length of plane helper.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.size = size;\n\n\t\tconst positions2 = [ 1, 1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0 ];\n\n\t\tconst geometry2 = new BufferGeometry();\n\t\tgeometry2.setAttribute( 'position', new Float32BufferAttribute( positions2, 3 ) );\n\t\tgeometry2.computeBoundingSphere();\n\n\t\tthis.add( new Mesh( geometry2, new MeshBasicMaterial( { color: color, opacity: 0.2, transparent: true, depthWrite: false, toneMapped: false } ) ) );\n\n\t}\n\n\tupdateMatrixWorld( force ) {\n\n\t\tthis.position.set( 0, 0, 0 );\n\n\t\tthis.scale.set( 0.5 * this.size, 0.5 * this.size, 1 );\n\n\t\tthis.lookAt( this.plane.normal );\n\n\t\tthis.translateZ( - this.plane.constant );\n\n\t\tsuper.updateMatrixWorld( force );\n\n\t}\n\n\t/**\n\t * Updates the helper to match the position and direction of the\n\t * light being visualized.\n\t */\n\tdispose() {\n\n\t\tthis.geometry.dispose();\n\t\tthis.material.dispose();\n\t\tthis.children[ 0 ].geometry.dispose();\n\t\tthis.children[ 0 ].material.dispose();\n\n\t}\n\n}\n\nconst _axis = /*@__PURE__*/ new Vector3();\nlet _lineGeometry, _coneGeometry;\n\n/**\n * An 3D arrow object for visualizing directions.\n *\n * ```js\n * const dir = new THREE.Vector3( 1, 2, 0 );\n *\n * //normalize the direction vector (convert to vector of length 1)\n * dir.normalize();\n *\n * const origin = new THREE.Vector3( 0, 0, 0 );\n * const length = 1;\n * const hex = 0xffff00;\n *\n * const arrowHelper = new THREE.ArrowHelper( dir, origin, length, hex );\n * scene.add( arrowHelper );\n * ```\n *\n * @augments Object3D\n */\nclass ArrowHelper extends Object3D {\n\n\t/**\n\t * Constructs a new arrow helper.\n\t *\n\t * @param {Vector3} [dir=(0, 0, 1)] - The (normalized) direction vector.\n\t * @param {Vector3} [origin=(0, 0, 0)] - Point at which the arrow starts.\n\t * @param {number} [length=1] - Length of the arrow in world units.\n\t * @param {(number|Color|string)} [color=0xffff00] - Color of the arrow.\n\t * @param {number} [headLength=length*0.2] - The length of the head of the arrow.\n\t * @param {number} [headWidth=headLength*0.2] - The width of the head of the arrow.\n\t */\n\tconstructor( dir = new Vector3( 0, 0, 1 ), origin = new Vector3( 0, 0, 0 ), length = 1, color = 0xffff00, headLength = length * 0.2, headWidth = headLength * 0.2 ) {\n\n\t\tsuper();\n\n\t\tthis.type = 'ArrowHelper';\n\n\t\tif ( _lineGeometry === undefined ) {\n\n\t\t\t_lineGeometry = new BufferGeometry();\n\t\t\t_lineGeometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 0, 1, 0 ], 3 ) );\n\n\t\t\t_coneGeometry = new ConeGeometry( 0.5, 1, 5, 1 );\n\t\t\t_coneGeometry.translate( 0, -0.5, 0 );\n\n\t\t}\n\n\t\tthis.position.copy( origin );\n\n\t\t/**\n\t\t * The line part of the arrow helper.\n\t\t *\n\t\t * @type {Line}\n\t\t */\n\t\tthis.line = new Line( _lineGeometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );\n\t\tthis.line.matrixAutoUpdate = false;\n\t\tthis.add( this.line );\n\n\t\t/**\n\t\t * The cone part of the arrow helper.\n\t\t *\n\t\t * @type {Mesh}\n\t\t */\n\t\tthis.cone = new Mesh( _coneGeometry, new MeshBasicMaterial( { color: color, toneMapped: false } ) );\n\t\tthis.cone.matrixAutoUpdate = false;\n\t\tthis.add( this.cone );\n\n\t\tthis.setDirection( dir );\n\t\tthis.setLength( length, headLength, headWidth );\n\n\t}\n\n\t/**\n\t * Sets the direction of the helper.\n\t *\n\t * @param {Vector3} dir - The normalized direction vector.\n\t */\n\tsetDirection( dir ) {\n\n\t\t// dir is assumed to be normalized\n\n\t\tif ( dir.y > 0.99999 ) {\n\n\t\t\tthis.quaternion.set( 0, 0, 0, 1 );\n\n\t\t} else if ( dir.y < -0.99999 ) {\n\n\t\t\tthis.quaternion.set( 1, 0, 0, 0 );\n\n\t\t} else {\n\n\t\t\t_axis.set( dir.z, 0, - dir.x ).normalize();\n\n\t\t\tconst radians = Math.acos( dir.y );\n\n\t\t\tthis.quaternion.setFromAxisAngle( _axis, radians );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sets the length of the helper.\n\t *\n\t * @param {number} length - Length of the arrow in world units.\n\t * @param {number} [headLength=length*0.2] - The length of the head of the arrow.\n\t * @param {number} [headWidth=headLength*0.2] - The width of the head of the arrow.\n\t */\n\tsetLength( length, headLength = length * 0.2, headWidth = headLength * 0.2 ) {\n\n\t\tthis.line.scale.set( 1, Math.max( 0.0001, length - headLength ), 1 ); // see #17458\n\t\tthis.line.updateMatrix();\n\n\t\tthis.cone.scale.set( headWidth, headLength, headWidth );\n\t\tthis.cone.position.y = length;\n\t\tthis.cone.updateMatrix();\n\n\t}\n\n\t/**\n\t * Sets the color of the helper.\n\t *\n\t * @param {number|Color|string} color - The color to set.\n\t */\n\tsetColor( color ) {\n\n\t\tthis.line.material.color.set( color );\n\t\tthis.cone.material.color.set( color );\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source, false );\n\n\t\tthis.line.copy( source.line );\n\t\tthis.cone.copy( source.cone );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.line.geometry.dispose();\n\t\tthis.line.material.dispose();\n\t\tthis.cone.geometry.dispose();\n\t\tthis.cone.material.dispose();\n\n\t}\n\n}\n\n/**\n * An axis object to visualize the 3 axes in a simple way.\n * The X axis is red. The Y axis is green. The Z axis is blue.\n *\n * ```js\n * const axesHelper = new THREE.AxesHelper( 5 );\n * scene.add( axesHelper );\n * ```\n *\n * @augments LineSegments\n */\nclass AxesHelper extends LineSegments {\n\n\t/**\n\t * Constructs a new axes helper.\n\t *\n\t * @param {number} [size=1] - Size of the lines representing the axes.\n\t */\n\tconstructor( size = 1 ) {\n\n\t\tconst vertices = [\n\t\t\t0, 0, 0,\tsize, 0, 0,\n\t\t\t0, 0, 0,\t0, size, 0,\n\t\t\t0, 0, 0,\t0, 0, size\n\t\t];\n\n\t\tconst colors = [\n\t\t\t1, 0, 0,\t1, 0.6, 0,\n\t\t\t0, 1, 0,\t0.6, 1, 0,\n\t\t\t0, 0, 1,\t0, 0.6, 1\n\t\t];\n\n\t\tconst geometry = new BufferGeometry();\n\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );\n\t\tgeometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );\n\n\t\tconst material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } );\n\n\t\tsuper( geometry, material );\n\n\t\tthis.type = 'AxesHelper';\n\n\t}\n\n\t/**\n\t * Defines the colors of the axes helper.\n\t *\n\t * @param {number|Color|string} xAxisColor - The color for the x axis.\n\t * @param {number|Color|string} yAxisColor - The color for the y axis.\n\t * @param {number|Color|string} zAxisColor - The color for the z axis.\n\t * @return {AxesHelper} A reference to this axes helper.\n\t */\n\tsetColors( xAxisColor, yAxisColor, zAxisColor ) {\n\n\t\tconst color = new Color();\n\t\tconst array = this.geometry.attributes.color.array;\n\n\t\tcolor.set( xAxisColor );\n\t\tcolor.toArray( array, 0 );\n\t\tcolor.toArray( array, 3 );\n\n\t\tcolor.set( yAxisColor );\n\t\tcolor.toArray( array, 6 );\n\t\tcolor.toArray( array, 9 );\n\n\t\tcolor.set( zAxisColor );\n\t\tcolor.toArray( array, 12 );\n\t\tcolor.toArray( array, 15 );\n\n\t\tthis.geometry.attributes.color.needsUpdate = true;\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Frees the GPU-related resources allocated by this instance. Call this\n\t * method whenever this instance is no longer used in your app.\n\t */\n\tdispose() {\n\n\t\tthis.geometry.dispose();\n\t\tthis.material.dispose();\n\n\t}\n\n}\n\n/**\n * This class is used to convert a series of paths to an array of\n * shapes. It is specifically used in context of fonts and SVG.\n */\nclass ShapePath {\n\n\t/**\n\t * Constructs a new shape path.\n\t */\n\tconstructor() {\n\n\t\tthis.type = 'ShapePath';\n\n\t\t/**\n\t\t * The color of the shape.\n\t\t *\n\t\t * @type {Color}\n\t\t */\n\t\tthis.color = new Color();\n\n\t\t/**\n\t\t * The paths that have been generated for this shape.\n\t\t *\n\t\t * @type {Array<Path>}\n\t\t * @default null\n\t\t */\n\t\tthis.subPaths = [];\n\n\t\t/**\n\t\t * The current path that is being generated.\n\t\t *\n\t\t * @type {?Path}\n\t\t * @default null\n\t\t */\n\t\tthis.currentPath = null;\n\n\t}\n\n\t/**\n\t * Creates a new path and moves it current point to the given one.\n\t *\n\t * @param {number} x - The x coordinate.\n\t * @param {number} y - The y coordinate.\n\t * @return {ShapePath} A reference to this shape path.\n\t */\n\tmoveTo( x, y ) {\n\n\t\tthis.currentPath = new Path();\n\t\tthis.subPaths.push( this.currentPath );\n\t\tthis.currentPath.moveTo( x, y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an instance of {@link LineCurve} to the path by connecting\n\t * the current point with the given one.\n\t *\n\t * @param {number} x - The x coordinate of the end point.\n\t * @param {number} y - The y coordinate of the end point.\n\t * @return {ShapePath} A reference to this shape path.\n\t */\n\tlineTo( x, y ) {\n\n\t\tthis.currentPath.lineTo( x, y );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an instance of {@link QuadraticBezierCurve} to the path by connecting\n\t * the current point with the given one.\n\t *\n\t * @param {number} aCPx - The x coordinate of the control point.\n\t * @param {number} aCPy - The y coordinate of the control point.\n\t * @param {number} aX - The x coordinate of the end point.\n\t * @param {number} aY - The y coordinate of the end point.\n\t * @return {ShapePath} A reference to this shape path.\n\t */\n\tquadraticCurveTo( aCPx, aCPy, aX, aY ) {\n\n\t\tthis.currentPath.quadraticCurveTo( aCPx, aCPy, aX, aY );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an instance of {@link CubicBezierCurve} to the path by connecting\n\t * the current point with the given one.\n\t *\n\t * @param {number} aCP1x - The x coordinate of the first control point.\n\t * @param {number} aCP1y - The y coordinate of the first control point.\n\t * @param {number} aCP2x - The x coordinate of the second control point.\n\t * @param {number} aCP2y - The y coordinate of the second control point.\n\t * @param {number} aX - The x coordinate of the end point.\n\t * @param {number} aY - The y coordinate of the end point.\n\t * @return {ShapePath} A reference to this shape path.\n\t */\n\tbezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY ) {\n\n\t\tthis.currentPath.bezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Adds an instance of {@link SplineCurve} to the path by connecting\n\t * the current point with the given list of points.\n\t *\n\t * @param {Array<Vector2>} pts - An array of points in 2D space.\n\t * @return {ShapePath} A reference to this shape path.\n\t */\n\tsplineThru( pts ) {\n\n\t\tthis.currentPath.splineThru( pts );\n\n\t\treturn this;\n\n\t}\n\n\t/**\n\t * Converts the paths into an array of shapes.\n\t *\n\t * @param {boolean} isCCW - By default solid shapes are  defined clockwise (CW) and holes are defined counterclockwise (CCW).\n\t * If this flag is set to `true`, then those are flipped.\n\t * @return {Array<Shape>} An array of shapes.\n\t */\n\ttoShapes( isCCW ) {\n\n\t\tfunction toShapesNoHoles( inSubpaths ) {\n\n\t\t\tconst shapes = [];\n\n\t\t\tfor ( let i = 0, l = inSubpaths.length; i < l; i ++ ) {\n\n\t\t\t\tconst tmpPath = inSubpaths[ i ];\n\n\t\t\t\tconst tmpShape = new Shape();\n\t\t\t\ttmpShape.curves = tmpPath.curves;\n\n\t\t\t\tshapes.push( tmpShape );\n\n\t\t\t}\n\n\t\t\treturn shapes;\n\n\t\t}\n\n\t\tfunction isPointInsidePolygon( inPt, inPolygon ) {\n\n\t\t\tconst polyLen = inPolygon.length;\n\n\t\t\t// inPt on polygon contour => immediate success    or\n\t\t\t// toggling of inside/outside at every single! intersection point of an edge\n\t\t\t//  with the horizontal line through inPt, left of inPt\n\t\t\t//  not counting lowerY endpoints of edges and whole edges on that line\n\t\t\tlet inside = false;\n\t\t\tfor ( let p = polyLen - 1, q = 0; q < polyLen; p = q ++ ) {\n\n\t\t\t\tlet edgeLowPt = inPolygon[ p ];\n\t\t\t\tlet edgeHighPt = inPolygon[ q ];\n\n\t\t\t\tlet edgeDx = edgeHighPt.x - edgeLowPt.x;\n\t\t\t\tlet edgeDy = edgeHighPt.y - edgeLowPt.y;\n\n\t\t\t\tif ( Math.abs( edgeDy ) > Number.EPSILON ) {\n\n\t\t\t\t\t// not parallel\n\t\t\t\t\tif ( edgeDy < 0 ) {\n\n\t\t\t\t\t\tedgeLowPt = inPolygon[ q ]; edgeDx = - edgeDx;\n\t\t\t\t\t\tedgeHighPt = inPolygon[ p ]; edgeDy = - edgeDy;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ( inPt.y < edgeLowPt.y ) || ( inPt.y > edgeHighPt.y ) ) \t\tcontinue;\n\n\t\t\t\t\tif ( inPt.y === edgeLowPt.y ) {\n\n\t\t\t\t\t\tif ( inPt.x === edgeLowPt.x )\t\treturn\ttrue;\t\t// inPt is on contour ?\n\t\t\t\t\t\t// continue;\t\t\t\t// no intersection or edgeLowPt => doesn't count !!!\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tconst perpEdge = edgeDy * ( inPt.x - edgeLowPt.x ) - edgeDx * ( inPt.y - edgeLowPt.y );\n\t\t\t\t\t\tif ( perpEdge === 0 )\t\t\t\treturn\ttrue;\t\t// inPt is on contour ?\n\t\t\t\t\t\tif ( perpEdge < 0 ) \t\t\t\tcontinue;\n\t\t\t\t\t\tinside = ! inside;\t\t// true intersection left of inPt\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// parallel or collinear\n\t\t\t\t\tif ( inPt.y !== edgeLowPt.y ) \t\tcontinue;\t\t\t// parallel\n\t\t\t\t\t// edge lies on the same horizontal line as inPt\n\t\t\t\t\tif ( ( ( edgeHighPt.x <= inPt.x ) && ( inPt.x <= edgeLowPt.x ) ) ||\n\t\t\t\t\t\t ( ( edgeLowPt.x <= inPt.x ) && ( inPt.x <= edgeHighPt.x ) ) )\t\treturn\ttrue;\t// inPt: Point on contour !\n\t\t\t\t\t// continue;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn\tinside;\n\n\t\t}\n\n\t\tconst isClockWise = ShapeUtils.isClockWise;\n\n\t\tconst subPaths = this.subPaths;\n\t\tif ( subPaths.length === 0 ) return [];\n\n\t\tlet solid, tmpPath, tmpShape;\n\t\tconst shapes = [];\n\n\t\tif ( subPaths.length === 1 ) {\n\n\t\t\ttmpPath = subPaths[ 0 ];\n\t\t\ttmpShape = new Shape();\n\t\t\ttmpShape.curves = tmpPath.curves;\n\t\t\tshapes.push( tmpShape );\n\t\t\treturn shapes;\n\n\t\t}\n\n\t\tlet holesFirst = ! isClockWise( subPaths[ 0 ].getPoints() );\n\t\tholesFirst = isCCW ? ! holesFirst : holesFirst;\n\n\t\t// log(\"Holes first\", holesFirst);\n\n\t\tconst betterShapeHoles = [];\n\t\tconst newShapes = [];\n\t\tlet newShapeHoles = [];\n\t\tlet mainIdx = 0;\n\t\tlet tmpPoints;\n\n\t\tnewShapes[ mainIdx ] = undefined;\n\t\tnewShapeHoles[ mainIdx ] = [];\n\n\t\tfor ( let i = 0, l = subPaths.length; i < l; i ++ ) {\n\n\t\t\ttmpPath = subPaths[ i ];\n\t\t\ttmpPoints = tmpPath.getPoints();\n\t\t\tsolid = isClockWise( tmpPoints );\n\t\t\tsolid = isCCW ? ! solid : solid;\n\n\t\t\tif ( solid ) {\n\n\t\t\t\tif ( ( ! holesFirst ) && ( newShapes[ mainIdx ] ) )\tmainIdx ++;\n\n\t\t\t\tnewShapes[ mainIdx ] = { s: new Shape(), p: tmpPoints };\n\t\t\t\tnewShapes[ mainIdx ].s.curves = tmpPath.curves;\n\n\t\t\t\tif ( holesFirst )\tmainIdx ++;\n\t\t\t\tnewShapeHoles[ mainIdx ] = [];\n\n\t\t\t\t//log('cw', i);\n\n\t\t\t} else {\n\n\t\t\t\tnewShapeHoles[ mainIdx ].push( { h: tmpPath, p: tmpPoints[ 0 ] } );\n\n\t\t\t\t//log('ccw', i);\n\n\t\t\t}\n\n\t\t}\n\n\t\t// only Holes? -> probably all Shapes with wrong orientation\n\t\tif ( ! newShapes[ 0 ] )\treturn\ttoShapesNoHoles( subPaths );\n\n\n\t\tif ( newShapes.length > 1 ) {\n\n\t\t\tlet ambiguous = false;\n\t\t\tlet toChange = 0;\n\n\t\t\tfor ( let sIdx = 0, sLen = newShapes.length; sIdx < sLen; sIdx ++ ) {\n\n\t\t\t\tbetterShapeHoles[ sIdx ] = [];\n\n\t\t\t}\n\n\t\t\tfor ( let sIdx = 0, sLen = newShapes.length; sIdx < sLen; sIdx ++ ) {\n\n\t\t\t\tconst sho = newShapeHoles[ sIdx ];\n\n\t\t\t\tfor ( let hIdx = 0; hIdx < sho.length; hIdx ++ ) {\n\n\t\t\t\t\tconst ho = sho[ hIdx ];\n\t\t\t\t\tlet hole_unassigned = true;\n\n\t\t\t\t\tfor ( let s2Idx = 0; s2Idx < newShapes.length; s2Idx ++ ) {\n\n\t\t\t\t\t\tif ( isPointInsidePolygon( ho.p, newShapes[ s2Idx ].p ) ) {\n\n\t\t\t\t\t\t\tif ( sIdx !== s2Idx )\ttoChange ++;\n\n\t\t\t\t\t\t\tif ( hole_unassigned ) {\n\n\t\t\t\t\t\t\t\thole_unassigned = false;\n\t\t\t\t\t\t\t\tbetterShapeHoles[ s2Idx ].push( ho );\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\tambiguous = true;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( hole_unassigned ) {\n\n\t\t\t\t\t\tbetterShapeHoles[ sIdx ].push( ho );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( toChange > 0 && ambiguous === false ) {\n\n\t\t\t\tnewShapeHoles = betterShapeHoles;\n\n\t\t\t}\n\n\t\t}\n\n\t\tlet tmpHoles;\n\n\t\tfor ( let i = 0, il = newShapes.length; i < il; i ++ ) {\n\n\t\t\ttmpShape = newShapes[ i ].s;\n\t\t\tshapes.push( tmpShape );\n\t\t\ttmpHoles = newShapeHoles[ i ];\n\n\t\t\tfor ( let j = 0, jl = tmpHoles.length; j < jl; j ++ ) {\n\n\t\t\t\ttmpShape.holes.push( tmpHoles[ j ].h );\n\n\t\t\t}\n\n\t\t}\n\n\t\t//log(\"shape\", shapes);\n\n\t\treturn shapes;\n\n\t}\n\n}\n\n/**\n * Abstract base class for controls.\n *\n * @abstract\n * @augments EventDispatcher\n */\nclass Controls extends EventDispatcher {\n\n\t/**\n\t * Constructs a new controls instance.\n\t *\n\t * @param {Object3D} object - The object that is managed by the controls.\n\t * @param {?HTMLElement} domElement - The HTML element used for event listeners.\n\t */\n\tconstructor( object, domElement = null ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * The object that is managed by the controls.\n\t\t *\n\t\t * @type {Object3D}\n\t\t */\n\t\tthis.object = object;\n\n\t\t/**\n\t\t * The HTML element used for event listeners.\n\t\t *\n\t\t * @type {?HTMLElement}\n\t\t * @default null\n\t\t */\n\t\tthis.domElement = domElement;\n\n\t\t/**\n\t\t * Whether the controls responds to user input or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.enabled = true;\n\n\t\t/**\n\t\t * The internal state of the controls.\n\t\t *\n\t\t * @type {number}\n\t\t * @default -1\n\t\t */\n\t\tthis.state = -1;\n\n\t\t/**\n\t\t * This object defines the keyboard input of the controls.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.keys = {};\n\n\t\t/**\n\t\t * This object defines what type of actions are assigned to the available mouse buttons.\n\t\t * It depends on the control implementation what kind of mouse buttons and actions are supported.\n\t\t *\n\t\t * @type {{LEFT: ?number, MIDDLE: ?number, RIGHT: ?number}}\n\t\t */\n\t\tthis.mouseButtons = { LEFT: null, MIDDLE: null, RIGHT: null };\n\n\t\t/**\n\t\t * This object defines what type of actions are assigned to what kind of touch interaction.\n\t\t * It depends on the control implementation what kind of touch interaction and actions are supported.\n\t\t *\n\t\t * @type {{ONE: ?number, TWO: ?number}}\n\t\t */\n\t\tthis.touches = { ONE: null, TWO: null };\n\n\t}\n\n\t/**\n\t * Connects the controls to the DOM. This method has so called \"side effects\" since\n\t * it adds the module's event listeners to the DOM.\n\t *\n\t * @param {HTMLElement} element - The DOM element to connect to.\n\t */\n\tconnect( element ) {\n\n\t\tif ( element === undefined ) {\n\n\t\t\twarn( 'Controls: connect() now requires an element.' ); // @deprecated, the warning can be removed with r185\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( this.domElement !== null ) this.disconnect();\n\n\t\tthis.domElement = element;\n\n\t}\n\n\t/**\n\t * Disconnects the controls from the DOM.\n\t */\n\tdisconnect() {}\n\n\t/**\n\t * Call this method if you no longer want use to the controls. It frees all internal\n\t * resources and removes all event listeners.\n\t */\n\tdispose() {}\n\n\t/**\n\t * Controls should implement this method if they have to update their internal state\n\t * per simulation step.\n\t *\n\t * @param {number} [delta] - The time delta in seconds.\n\t */\n\tupdate( /* delta */ ) {}\n\n}\n\n/**\n * Scales the texture as large as possible within its surface without cropping\n * or stretching the texture. The method preserves the original aspect ratio of\n * the texture. Akin to CSS `object-fit: contain`\n *\n * @param {Texture} texture - The texture.\n * @param {number} aspect - The texture's aspect ratio.\n * @return {Texture} The updated texture.\n */\nfunction contain( texture, aspect ) {\n\n\tconst imageAspect = ( texture.image && texture.image.width ) ? texture.image.width / texture.image.height : 1;\n\n\tif ( imageAspect > aspect ) {\n\n\t\ttexture.repeat.x = 1;\n\t\ttexture.repeat.y = imageAspect / aspect;\n\n\t\ttexture.offset.x = 0;\n\t\ttexture.offset.y = ( 1 - texture.repeat.y ) / 2;\n\n\t} else {\n\n\t\ttexture.repeat.x = aspect / imageAspect;\n\t\ttexture.repeat.y = 1;\n\n\t\ttexture.offset.x = ( 1 - texture.repeat.x ) / 2;\n\t\ttexture.offset.y = 0;\n\n\t}\n\n\treturn texture;\n\n}\n\n/**\n * Scales the texture to the smallest possible size to fill the surface, leaving\n * no empty space. The method preserves the original aspect ratio of the texture.\n * Akin to CSS `object-fit: cover`.\n *\n * @param {Texture} texture - The texture.\n * @param {number} aspect - The texture's aspect ratio.\n * @return {Texture} The updated texture.\n */\nfunction cover( texture, aspect ) {\n\n\tconst imageAspect = ( texture.image && texture.image.width ) ? texture.image.width / texture.image.height : 1;\n\n\tif ( imageAspect > aspect ) {\n\n\t\ttexture.repeat.x = aspect / imageAspect;\n\t\ttexture.repeat.y = 1;\n\n\t\ttexture.offset.x = ( 1 - texture.repeat.x ) / 2;\n\t\ttexture.offset.y = 0;\n\n\t} else {\n\n\t\ttexture.repeat.x = 1;\n\t\ttexture.repeat.y = imageAspect / aspect;\n\n\t\ttexture.offset.x = 0;\n\t\ttexture.offset.y = ( 1 - texture.repeat.y ) / 2;\n\n\t}\n\n\treturn texture;\n\n}\n\n/**\n * Configures the texture to the default transformation. Akin to CSS `object-fit: fill`.\n *\n * @param {Texture} texture - The texture.\n * @return {Texture} The updated texture.\n */\nfunction fill( texture ) {\n\n\ttexture.repeat.x = 1;\n\ttexture.repeat.y = 1;\n\n\ttexture.offset.x = 0;\n\ttexture.offset.y = 0;\n\n\treturn texture;\n\n}\n\n/**\n * Determines how many bytes must be used to represent the texture.\n *\n * @param {number} width - The width of the texture.\n * @param {number} height - The height of the texture.\n * @param {number} format - The texture's format.\n * @param {number} type - The texture's type.\n * @return {number} The byte length.\n */\nfunction getByteLength( width, height, format, type ) {\n\n\tconst typeByteLength = getTextureTypeByteLength( type );\n\n\tswitch ( format ) {\n\n\t\t// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glTexImage2D.xhtml\n\t\tcase AlphaFormat:\n\t\t\treturn width * height;\n\t\tcase RedFormat:\n\t\t\treturn ( ( width * height ) / typeByteLength.components ) * typeByteLength.byteLength;\n\t\tcase RedIntegerFormat:\n\t\t\treturn ( ( width * height ) / typeByteLength.components ) * typeByteLength.byteLength;\n\t\tcase RGFormat:\n\t\t\treturn ( ( width * height * 2 ) / typeByteLength.components ) * typeByteLength.byteLength;\n\t\tcase RGIntegerFormat:\n\t\t\treturn ( ( width * height * 2 ) / typeByteLength.components ) * typeByteLength.byteLength;\n\t\tcase RGBFormat:\n\t\t\treturn ( ( width * height * 3 ) / typeByteLength.components ) * typeByteLength.byteLength;\n\t\tcase RGBAFormat:\n\t\t\treturn ( ( width * height * 4 ) / typeByteLength.components ) * typeByteLength.byteLength;\n\t\tcase RGBAIntegerFormat:\n\t\t\treturn ( ( width * height * 4 ) / typeByteLength.components ) * typeByteLength.byteLength;\n\n\t\t// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc_srgb/\n\t\tcase RGB_S3TC_DXT1_Format:\n\t\tcase RGBA_S3TC_DXT1_Format:\n\t\t\treturn Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 8;\n\t\tcase RGBA_S3TC_DXT3_Format:\n\t\tcase RGBA_S3TC_DXT5_Format:\n\t\t\treturn Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;\n\n\t\t// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_pvrtc/\n\t\tcase RGB_PVRTC_2BPPV1_Format:\n\t\tcase RGBA_PVRTC_2BPPV1_Format:\n\t\t\treturn ( Math.max( width, 16 ) * Math.max( height, 8 ) ) / 4;\n\t\tcase RGB_PVRTC_4BPPV1_Format:\n\t\tcase RGBA_PVRTC_4BPPV1_Format:\n\t\t\treturn ( Math.max( width, 8 ) * Math.max( height, 8 ) ) / 2;\n\n\t\t// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc/\n\t\tcase RGB_ETC1_Format:\n\t\tcase RGB_ETC2_Format:\n\t\tcase R11_EAC_Format:\n\t\tcase SIGNED_R11_EAC_Format:\n\t\t\treturn Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 8;\n\t\tcase RGBA_ETC2_EAC_Format:\n\t\tcase RG11_EAC_Format:\n\t\tcase SIGNED_RG11_EAC_Format:\n\t\t\treturn Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;\n\n\t\t// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/\n\t\tcase RGBA_ASTC_4x4_Format:\n\t\t\treturn Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;\n\t\tcase RGBA_ASTC_5x4_Format:\n\t\t\treturn Math.floor( ( width + 4 ) / 5 ) * Math.floor( ( height + 3 ) / 4 ) * 16;\n\t\tcase RGBA_ASTC_5x5_Format:\n\t\t\treturn Math.floor( ( width + 4 ) / 5 ) * Math.floor( ( height + 4 ) / 5 ) * 16;\n\t\tcase RGBA_ASTC_6x5_Format:\n\t\t\treturn Math.floor( ( width + 5 ) / 6 ) * Math.floor( ( height + 4 ) / 5 ) * 16;\n\t\tcase RGBA_ASTC_6x6_Format:\n\t\t\treturn Math.floor( ( width + 5 ) / 6 ) * Math.floor( ( height + 5 ) / 6 ) * 16;\n\t\tcase RGBA_ASTC_8x5_Format:\n\t\t\treturn Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 4 ) / 5 ) * 16;\n\t\tcase RGBA_ASTC_8x6_Format:\n\t\t\treturn Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 5 ) / 6 ) * 16;\n\t\tcase RGBA_ASTC_8x8_Format:\n\t\t\treturn Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 7 ) / 8 ) * 16;\n\t\tcase RGBA_ASTC_10x5_Format:\n\t\t\treturn Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 4 ) / 5 ) * 16;\n\t\tcase RGBA_ASTC_10x6_Format:\n\t\t\treturn Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 5 ) / 6 ) * 16;\n\t\tcase RGBA_ASTC_10x8_Format:\n\t\t\treturn Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 7 ) / 8 ) * 16;\n\t\tcase RGBA_ASTC_10x10_Format:\n\t\t\treturn Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 9 ) / 10 ) * 16;\n\t\tcase RGBA_ASTC_12x10_Format:\n\t\t\treturn Math.floor( ( width + 11 ) / 12 ) * Math.floor( ( height + 9 ) / 10 ) * 16;\n\t\tcase RGBA_ASTC_12x12_Format:\n\t\t\treturn Math.floor( ( width + 11 ) / 12 ) * Math.floor( ( height + 11 ) / 12 ) * 16;\n\n\t\t// https://registry.khronos.org/webgl/extensions/EXT_texture_compression_bptc/\n\t\tcase RGBA_BPTC_Format:\n\t\tcase RGB_BPTC_SIGNED_Format:\n\t\tcase RGB_BPTC_UNSIGNED_Format:\n\t\t\treturn Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 16;\n\n\t\t// https://registry.khronos.org/webgl/extensions/EXT_texture_compression_rgtc/\n\t\tcase RED_RGTC1_Format:\n\t\tcase SIGNED_RED_RGTC1_Format:\n\t\t\treturn Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 8;\n\t\tcase RED_GREEN_RGTC2_Format:\n\t\tcase SIGNED_RED_GREEN_RGTC2_Format:\n\t\t\treturn Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 16;\n\n\t}\n\n\tthrow new Error(\n\t\t`Unable to determine texture byte length for ${format} format.`,\n\t);\n\n}\n\nfunction getTextureTypeByteLength( type ) {\n\n\tswitch ( type ) {\n\n\t\tcase UnsignedByteType:\n\t\tcase ByteType:\n\t\t\treturn { byteLength: 1, components: 1 };\n\t\tcase UnsignedShortType:\n\t\tcase ShortType:\n\t\tcase HalfFloatType:\n\t\t\treturn { byteLength: 2, components: 1 };\n\t\tcase UnsignedShort4444Type:\n\t\tcase UnsignedShort5551Type:\n\t\t\treturn { byteLength: 2, components: 4 };\n\t\tcase UnsignedIntType:\n\t\tcase IntType:\n\t\tcase FloatType:\n\t\t\treturn { byteLength: 4, components: 1 };\n\t\tcase UnsignedInt5999Type:\n\t\tcase UnsignedInt101111Type:\n\t\t\treturn { byteLength: 4, components: 3 };\n\n\t}\n\n\tthrow new Error( `Unknown texture type ${type}.` );\n\n}\n\n/**\n * A class containing utility functions for textures.\n *\n * @hideconstructor\n */\nclass TextureUtils {\n\n\t/**\n\t * Scales the texture as large as possible within its surface without cropping\n\t * or stretching the texture. The method preserves the original aspect ratio of\n\t * the texture. Akin to CSS `object-fit: contain`\n\t *\n\t * @param {Texture} texture - The texture.\n\t * @param {number} aspect - The texture's aspect ratio.\n\t * @return {Texture} The updated texture.\n\t */\n\tstatic contain( texture, aspect ) {\n\n\t\treturn contain( texture, aspect );\n\n\t}\n\n\t/**\n\t * Scales the texture to the smallest possible size to fill the surface, leaving\n\t * no empty space. The method preserves the original aspect ratio of the texture.\n\t * Akin to CSS `object-fit: cover`.\n\t *\n\t * @param {Texture} texture - The texture.\n\t * @param {number} aspect - The texture's aspect ratio.\n\t * @return {Texture} The updated texture.\n\t */\n\tstatic cover( texture, aspect ) {\n\n\t\treturn cover( texture, aspect );\n\n\t}\n\n\t/**\n\t * Configures the texture to the default transformation. Akin to CSS `object-fit: fill`.\n\t *\n\t * @param {Texture} texture - The texture.\n\t * @return {Texture} The updated texture.\n\t */\n\tstatic fill( texture ) {\n\n\t\treturn fill( texture );\n\n\t}\n\n\t/**\n\t * Determines how many bytes must be used to represent the texture.\n\t *\n\t * @param {number} width - The width of the texture.\n\t * @param {number} height - The height of the texture.\n\t * @param {number} format - The texture's format.\n\t * @param {number} type - The texture's type.\n\t * @return {number} The byte length.\n\t */\n\tstatic getByteLength( width, height, format, type ) {\n\n\t\treturn getByteLength( width, height, format, type );\n\n\t}\n\n}\n\nif ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {\n\n\t__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'register', { detail: {\n\t\trevision: REVISION,\n\t} } ) );\n\n}\n\nif ( typeof window !== 'undefined' ) {\n\n\tif ( window.__THREE__ ) {\n\n\t\twarn( 'WARNING: Multiple instances of Three.js being imported.' );\n\n\t} else {\n\n\t\twindow.__THREE__ = REVISION;\n\n\t}\n\n}\n\nexport { ACESFilmicToneMapping, AddEquation, AddOperation, AdditiveAnimationBlendMode, AdditiveBlending, AgXToneMapping, AlphaFormat, AlwaysCompare, AlwaysDepth, AlwaysStencilFunc, AmbientLight, AnimationAction, AnimationClip, AnimationLoader, AnimationMixer, AnimationObjectGroup, AnimationUtils, ArcCurve, ArrayCamera, ArrowHelper, AttachedBindMode, Audio, AudioAnalyser, AudioContext, AudioListener, AudioLoader, AxesHelper, BackSide, BasicDepthPacking, BasicShadowMap, BatchedMesh, Bone, BooleanKeyframeTrack, Box2, Box3, Box3Helper, BoxGeometry, BoxHelper, BufferAttribute, BufferGeometry, BufferGeometryLoader, ByteType, Cache, Camera, CameraHelper, CanvasTexture, CapsuleGeometry, CatmullRomCurve3, CineonToneMapping, CircleGeometry, ClampToEdgeWrapping, Clock, Color, ColorKeyframeTrack, ColorManagement, CompressedArrayTexture, CompressedCubeTexture, CompressedTexture, CompressedTextureLoader, ConeGeometry, ConstantAlphaFactor, ConstantColorFactor, Controls, CubeCamera, CubeDepthTexture, CubeReflectionMapping, CubeRefractionMapping, CubeTexture, CubeTextureLoader, CubeUVReflectionMapping, CubicBezierCurve, CubicBezierCurve3, CubicInterpolant, CullFaceBack, CullFaceFront, CullFaceFrontBack, CullFaceNone, Curve, CurvePath, CustomBlending, CustomToneMapping, CylinderGeometry, Cylindrical, Data3DTexture, DataArrayTexture, DataTexture, DataTextureLoader, DataUtils, DecrementStencilOp, DecrementWrapStencilOp, DefaultLoadingManager, DepthFormat, DepthStencilFormat, DepthTexture, DetachedBindMode, DirectionalLight, DirectionalLightHelper, DiscreteInterpolant, DodecahedronGeometry, DoubleSide, DstAlphaFactor, DstColorFactor, DynamicCopyUsage, DynamicDrawUsage, DynamicReadUsage, EdgesGeometry, EllipseCurve, EqualCompare, EqualDepth, EqualStencilFunc, EquirectangularReflectionMapping, EquirectangularRefractionMapping, Euler, EventDispatcher, ExternalTexture, ExtrudeGeometry, FileLoader, Float16BufferAttribute, Float32BufferAttribute, FloatType, Fog, FogExp2, FramebufferTexture, FrontSide, Frustum, FrustumArray, GLBufferAttribute, GLSL1, GLSL3, GreaterCompare, GreaterDepth, GreaterEqualCompare, GreaterEqualDepth, GreaterEqualStencilFunc, GreaterStencilFunc, GridHelper, Group, HalfFloatType, HemisphereLight, HemisphereLightHelper, IcosahedronGeometry, ImageBitmapLoader, ImageLoader, ImageUtils, IncrementStencilOp, IncrementWrapStencilOp, InstancedBufferAttribute, InstancedBufferGeometry, InstancedInterleavedBuffer, InstancedMesh, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, IntType, InterleavedBuffer, InterleavedBufferAttribute, Interpolant, InterpolateDiscrete, InterpolateLinear, InterpolateSmooth, InterpolationSamplingMode, InterpolationSamplingType, InvertStencilOp, KeepStencilOp, KeyframeTrack, LOD, LatheGeometry, Layers, LessCompare, LessDepth, LessEqualCompare, LessEqualDepth, LessEqualStencilFunc, LessStencilFunc, Light, LightProbe, Line, Line3, LineBasicMaterial, LineCurve, LineCurve3, LineDashedMaterial, LineLoop, LineSegments, LinearFilter, LinearInterpolant, LinearMipMapLinearFilter, LinearMipMapNearestFilter, LinearMipmapLinearFilter, LinearMipmapNearestFilter, LinearSRGBColorSpace, LinearToneMapping, LinearTransfer, Loader, LoaderUtils, LoadingManager, LoopOnce, LoopPingPong, LoopRepeat, MOUSE, Material, MaterialLoader, MathUtils, Matrix2, Matrix3, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshDistanceMaterial, MeshLambertMaterial, MeshMatcapMaterial, MeshNormalMaterial, MeshPhongMaterial, MeshPhysicalMaterial, MeshStandardMaterial, MeshToonMaterial, MinEquation, MirroredRepeatWrapping, MixOperation, MultiplyBlending, MultiplyOperation, NearestFilter, NearestMipMapLinearFilter, NearestMipMapNearestFilter, NearestMipmapLinearFilter, NearestMipmapNearestFilter, NeutralToneMapping, NeverCompare, NeverDepth, NeverStencilFunc, NoBlending, NoColorSpace, NoNormalPacking, NoToneMapping, NormalAnimationBlendMode, NormalBlending, NormalGAPacking, NormalRGPacking, NotEqualCompare, NotEqualDepth, NotEqualStencilFunc, NumberKeyframeTrack, Object3D, ObjectLoader, ObjectSpaceNormalMap, OctahedronGeometry, OneFactor, OneMinusConstantAlphaFactor, OneMinusConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcAlphaFactor, OneMinusSrcColorFactor, OrthographicCamera, PCFShadowMap, PCFSoftShadowMap, Path, PerspectiveCamera, Plane, PlaneGeometry, PlaneHelper, PointLight, PointLightHelper, Points, PointsMaterial, PolarGridHelper, PolyhedronGeometry, PositionalAudio, PropertyBinding, PropertyMixer, QuadraticBezierCurve, QuadraticBezierCurve3, Quaternion, QuaternionKeyframeTrack, QuaternionLinearInterpolant, R11_EAC_Format, RAD2DEG, RED_GREEN_RGTC2_Format, RED_RGTC1_Format, REVISION, RG11_EAC_Format, RGBADepthPacking, RGBAFormat, RGBAIntegerFormat, RGBA_ASTC_10x10_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_BPTC_Format, RGBA_ETC2_EAC_Format, RGBA_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGBDepthPacking, RGBFormat, RGBIntegerFormat, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGB_PVRTC_2BPPV1_Format, RGB_PVRTC_4BPPV1_Format, RGB_S3TC_DXT1_Format, RGDepthPacking, RGFormat, RGIntegerFormat, RawShaderMaterial, Ray, Raycaster, RectAreaLight, RedFormat, RedIntegerFormat, ReinhardToneMapping, RenderTarget, RenderTarget3D, RepeatWrapping, ReplaceStencilOp, ReverseSubtractEquation, RingGeometry, SIGNED_R11_EAC_Format, SIGNED_RED_GREEN_RGTC2_Format, SIGNED_RED_RGTC1_Format, SIGNED_RG11_EAC_Format, SRGBColorSpace, SRGBTransfer, Scene, ShaderMaterial, ShadowMaterial, Shape, ShapeGeometry, ShapePath, ShapeUtils, ShortType, Skeleton, SkeletonHelper, SkinnedMesh, Source, Sphere, SphereGeometry, Spherical, SphericalHarmonics3, SplineCurve, SpotLight, SpotLightHelper, Sprite, SpriteMaterial, SrcAlphaFactor, SrcAlphaSaturateFactor, SrcColorFactor, StaticCopyUsage, StaticDrawUsage, StaticReadUsage, StereoCamera, StreamCopyUsage, StreamDrawUsage, StreamReadUsage, StringKeyframeTrack, SubtractEquation, SubtractiveBlending, TOUCH, TangentSpaceNormalMap, TetrahedronGeometry, Texture, TextureLoader, TextureUtils, Timer, TimestampQuery, TorusGeometry, TorusKnotGeometry, Triangle, TriangleFanDrawMode, TriangleStripDrawMode, TrianglesDrawMode, TubeGeometry, UVMapping, Uint16BufferAttribute, Uint32BufferAttribute, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Uniform, UniformsGroup, UniformsUtils, UnsignedByteType, UnsignedInt101111Type, UnsignedInt248Type, UnsignedInt5999Type, UnsignedIntType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedShortType, VSMShadowMap, Vector2, Vector3, Vector4, VectorKeyframeTrack, VideoFrameTexture, VideoTexture, WebGL3DRenderTarget, WebGLArrayRenderTarget, WebGLCoordinateSystem, WebGLCubeRenderTarget, WebGLRenderTarget, WebGPUCoordinateSystem, WebXRController, WireframeGeometry, WrapAroundEnding, ZeroCurvatureEnding, ZeroFactor, ZeroSlopeEnding, ZeroStencilOp, arrayNeedsUint32, cloneUniforms, createCanvasElement, createElementNS, error, getByteLength, getConsoleFunction, getUnlitUniformColorSpace, isTypedArray, log, mergeUniforms, probeAsync, setConsoleFunction, warn, warnOnce };\n"
  },
  {
    "path": "web/src/scripts/vendor-other/three-r182.module.js",
    "content": "/**\n * @license\n * Copyright 2010-2025 Three.js Authors\n * SPDX-License-Identifier: MIT\n */\nimport { Matrix3, Vector2, Color, mergeUniforms, Vector3, CubeUVReflectionMapping, Mesh, BoxGeometry, ShaderMaterial, BackSide, cloneUniforms, Euler, Matrix4, ColorManagement, SRGBTransfer, PlaneGeometry, FrontSide, getUnlitUniformColorSpace, IntType, warn, HalfFloatType, UnsignedByteType, FloatType, RGBAFormat, Plane, EquirectangularReflectionMapping, EquirectangularRefractionMapping, WebGLCubeRenderTarget, CubeReflectionMapping, CubeRefractionMapping, BufferGeometry, OrthographicCamera, PerspectiveCamera, NoToneMapping, MeshBasicMaterial, error, NoBlending, WebGLRenderTarget, BufferAttribute, LinearSRGBColorSpace, LinearFilter, warnOnce, Uint32BufferAttribute, Uint16BufferAttribute, arrayNeedsUint32, Vector4, DataArrayTexture, Float32BufferAttribute, RawShaderMaterial, CustomToneMapping, NeutralToneMapping, AgXToneMapping, ACESFilmicToneMapping, CineonToneMapping, ReinhardToneMapping, LinearToneMapping, CubeTexture, Data3DTexture, GreaterEqualCompare, LessEqualCompare, DepthTexture, Texture, GLSL3, VSMShadowMap, PCFShadowMap, AddOperation, MixOperation, MultiplyOperation, LinearTransfer, UniformsUtils, DoubleSide, NormalBlending, TangentSpaceNormalMap, ObjectSpaceNormalMap, Layers, RGFormat, Frustum, MeshDepthMaterial, MeshDistanceMaterial, PCFSoftShadowMap, DepthFormat, NearestFilter, CubeDepthTexture, UnsignedIntType, LessEqualDepth, ReverseSubtractEquation, SubtractEquation, AddEquation, OneMinusConstantAlphaFactor, ConstantAlphaFactor, OneMinusConstantColorFactor, ConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcAlphaFactor, OneMinusSrcColorFactor, DstAlphaFactor, DstColorFactor, SrcAlphaSaturateFactor, SrcAlphaFactor, SrcColorFactor, OneFactor, ZeroFactor, NotEqualDepth, GreaterDepth, GreaterEqualDepth, EqualDepth, LessDepth, AlwaysDepth, NeverDepth, CullFaceNone, CullFaceBack, CullFaceFront, CustomBlending, MultiplyBlending, SubtractiveBlending, AdditiveBlending, MinEquation, MaxEquation, MirroredRepeatWrapping, ClampToEdgeWrapping, RepeatWrapping, LinearMipmapLinearFilter, LinearMipmapNearestFilter, NearestMipmapLinearFilter, NearestMipmapNearestFilter, NotEqualCompare, GreaterCompare, EqualCompare, LessCompare, AlwaysCompare, NeverCompare, NoColorSpace, DepthStencilFormat, getByteLength, UnsignedInt248Type, UnsignedShortType, createElementNS, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedInt5999Type, UnsignedInt101111Type, ByteType, ShortType, AlphaFormat, RGBFormat, RedFormat, RedIntegerFormat, RGIntegerFormat, RGBAIntegerFormat, RGB_S3TC_DXT1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGB_PVRTC_4BPPV1_Format, RGB_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_PVRTC_2BPPV1_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGBA_ETC2_EAC_Format, R11_EAC_Format, SIGNED_R11_EAC_Format, RG11_EAC_Format, SIGNED_RG11_EAC_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_10x10_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_BPTC_Format, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, RED_GREEN_RGTC2_Format, SIGNED_RED_GREEN_RGTC2_Format, ExternalTexture, EventDispatcher, ArrayCamera, WebXRController, RAD2DEG, DataTexture, createCanvasElement, SRGBColorSpace, REVISION, log, WebGLCoordinateSystem, probeAsync } from './three-r182.core.js';\nexport { AdditiveAnimationBlendMode, AlwaysStencilFunc, AmbientLight, AnimationAction, AnimationClip, AnimationLoader, AnimationMixer, AnimationObjectGroup, AnimationUtils, ArcCurve, ArrowHelper, AttachedBindMode, Audio, AudioAnalyser, AudioContext, AudioListener, AudioLoader, AxesHelper, BasicDepthPacking, BasicShadowMap, BatchedMesh, Bone, BooleanKeyframeTrack, Box2, Box3, Box3Helper, BoxHelper, BufferGeometryLoader, Cache, Camera, CameraHelper, CanvasTexture, CapsuleGeometry, CatmullRomCurve3, CircleGeometry, Clock, ColorKeyframeTrack, CompressedArrayTexture, CompressedCubeTexture, CompressedTexture, CompressedTextureLoader, ConeGeometry, Controls, CubeCamera, CubeTextureLoader, CubicBezierCurve, CubicBezierCurve3, CubicInterpolant, CullFaceFrontBack, Curve, CurvePath, CylinderGeometry, Cylindrical, DataTextureLoader, DataUtils, DecrementStencilOp, DecrementWrapStencilOp, DefaultLoadingManager, DetachedBindMode, DirectionalLight, DirectionalLightHelper, DiscreteInterpolant, DodecahedronGeometry, DynamicCopyUsage, DynamicDrawUsage, DynamicReadUsage, EdgesGeometry, EllipseCurve, EqualStencilFunc, ExtrudeGeometry, FileLoader, Float16BufferAttribute, Fog, FogExp2, FramebufferTexture, FrustumArray, GLBufferAttribute, GLSL1, GreaterEqualStencilFunc, GreaterStencilFunc, GridHelper, Group, HemisphereLight, HemisphereLightHelper, IcosahedronGeometry, ImageBitmapLoader, ImageLoader, ImageUtils, IncrementStencilOp, IncrementWrapStencilOp, InstancedBufferAttribute, InstancedBufferGeometry, InstancedInterleavedBuffer, InstancedMesh, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, InterleavedBuffer, InterleavedBufferAttribute, Interpolant, InterpolateDiscrete, InterpolateLinear, InterpolateSmooth, InterpolationSamplingMode, InterpolationSamplingType, InvertStencilOp, KeepStencilOp, KeyframeTrack, LOD, LatheGeometry, LessEqualStencilFunc, LessStencilFunc, Light, LightProbe, Line, Line3, LineBasicMaterial, LineCurve, LineCurve3, LineDashedMaterial, LineLoop, LineSegments, LinearInterpolant, LinearMipMapLinearFilter, LinearMipMapNearestFilter, Loader, LoaderUtils, LoadingManager, LoopOnce, LoopPingPong, LoopRepeat, MOUSE, Material, MaterialLoader, MathUtils, Matrix2, MeshLambertMaterial, MeshMatcapMaterial, MeshNormalMaterial, MeshPhongMaterial, MeshPhysicalMaterial, MeshStandardMaterial, MeshToonMaterial, NearestMipMapLinearFilter, NearestMipMapNearestFilter, NeverStencilFunc, NoNormalPacking, NormalAnimationBlendMode, NormalGAPacking, NormalRGPacking, NotEqualStencilFunc, NumberKeyframeTrack, Object3D, ObjectLoader, OctahedronGeometry, Path, PlaneHelper, PointLight, PointLightHelper, Points, PointsMaterial, PolarGridHelper, PolyhedronGeometry, PositionalAudio, PropertyBinding, PropertyMixer, QuadraticBezierCurve, QuadraticBezierCurve3, Quaternion, QuaternionKeyframeTrack, QuaternionLinearInterpolant, RGBADepthPacking, RGBDepthPacking, RGBIntegerFormat, RGDepthPacking, Ray, Raycaster, RectAreaLight, RenderTarget, RenderTarget3D, ReplaceStencilOp, RingGeometry, Scene, ShadowMaterial, Shape, ShapeGeometry, ShapePath, ShapeUtils, Skeleton, SkeletonHelper, SkinnedMesh, Source, Sphere, SphereGeometry, Spherical, SphericalHarmonics3, SplineCurve, SpotLight, SpotLightHelper, Sprite, SpriteMaterial, StaticCopyUsage, StaticDrawUsage, StaticReadUsage, StereoCamera, StreamCopyUsage, StreamDrawUsage, StreamReadUsage, StringKeyframeTrack, TOUCH, TetrahedronGeometry, TextureLoader, TextureUtils, Timer, TimestampQuery, TorusGeometry, TorusKnotGeometry, Triangle, TriangleFanDrawMode, TriangleStripDrawMode, TrianglesDrawMode, TubeGeometry, UVMapping, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Uniform, UniformsGroup, VectorKeyframeTrack, VideoFrameTexture, VideoTexture, WebGL3DRenderTarget, WebGLArrayRenderTarget, WebGPUCoordinateSystem, WireframeGeometry, WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, ZeroStencilOp, getConsoleFunction, setConsoleFunction } from './three-r182.core.js';\n\nfunction WebGLAnimation() {\n\n\tlet context = null;\n\tlet isAnimating = false;\n\tlet animationLoop = null;\n\tlet requestId = null;\n\n\tfunction onAnimationFrame( time, frame ) {\n\n\t\tanimationLoop( time, frame );\n\n\t\trequestId = context.requestAnimationFrame( onAnimationFrame );\n\n\t}\n\n\treturn {\n\n\t\tstart: function () {\n\n\t\t\tif ( isAnimating === true ) return;\n\t\t\tif ( animationLoop === null ) return;\n\n\t\t\trequestId = context.requestAnimationFrame( onAnimationFrame );\n\n\t\t\tisAnimating = true;\n\n\t\t},\n\n\t\tstop: function () {\n\n\t\t\tcontext.cancelAnimationFrame( requestId );\n\n\t\t\tisAnimating = false;\n\n\t\t},\n\n\t\tsetAnimationLoop: function ( callback ) {\n\n\t\t\tanimationLoop = callback;\n\n\t\t},\n\n\t\tsetContext: function ( value ) {\n\n\t\t\tcontext = value;\n\n\t\t}\n\n\t};\n\n}\n\nfunction WebGLAttributes( gl ) {\n\n\tconst buffers = new WeakMap();\n\n\tfunction createBuffer( attribute, bufferType ) {\n\n\t\tconst array = attribute.array;\n\t\tconst usage = attribute.usage;\n\t\tconst size = array.byteLength;\n\n\t\tconst buffer = gl.createBuffer();\n\n\t\tgl.bindBuffer( bufferType, buffer );\n\t\tgl.bufferData( bufferType, array, usage );\n\n\t\tattribute.onUploadCallback();\n\n\t\tlet type;\n\n\t\tif ( array instanceof Float32Array ) {\n\n\t\t\ttype = gl.FLOAT;\n\n\t\t} else if ( typeof Float16Array !== 'undefined' && array instanceof Float16Array ) {\n\n\t\t\ttype = gl.HALF_FLOAT;\n\n\t\t} else if ( array instanceof Uint16Array ) {\n\n\t\t\tif ( attribute.isFloat16BufferAttribute ) {\n\n\t\t\t\ttype = gl.HALF_FLOAT;\n\n\t\t\t} else {\n\n\t\t\t\ttype = gl.UNSIGNED_SHORT;\n\n\t\t\t}\n\n\t\t} else if ( array instanceof Int16Array ) {\n\n\t\t\ttype = gl.SHORT;\n\n\t\t} else if ( array instanceof Uint32Array ) {\n\n\t\t\ttype = gl.UNSIGNED_INT;\n\n\t\t} else if ( array instanceof Int32Array ) {\n\n\t\t\ttype = gl.INT;\n\n\t\t} else if ( array instanceof Int8Array ) {\n\n\t\t\ttype = gl.BYTE;\n\n\t\t} else if ( array instanceof Uint8Array ) {\n\n\t\t\ttype = gl.UNSIGNED_BYTE;\n\n\t\t} else if ( array instanceof Uint8ClampedArray ) {\n\n\t\t\ttype = gl.UNSIGNED_BYTE;\n\n\t\t} else {\n\n\t\t\tthrow new Error( 'THREE.WebGLAttributes: Unsupported buffer data format: ' + array );\n\n\t\t}\n\n\t\treturn {\n\t\t\tbuffer: buffer,\n\t\t\ttype: type,\n\t\t\tbytesPerElement: array.BYTES_PER_ELEMENT,\n\t\t\tversion: attribute.version,\n\t\t\tsize: size\n\t\t};\n\n\t}\n\n\tfunction updateBuffer( buffer, attribute, bufferType ) {\n\n\t\tconst array = attribute.array;\n\t\tconst updateRanges = attribute.updateRanges;\n\n\t\tgl.bindBuffer( bufferType, buffer );\n\n\t\tif ( updateRanges.length === 0 ) {\n\n\t\t\t// Not using update ranges\n\t\t\tgl.bufferSubData( bufferType, 0, array );\n\n\t\t} else {\n\n\t\t\t// Before applying update ranges, we merge any adjacent / overlapping\n\t\t\t// ranges to reduce load on `gl.bufferSubData`. Empirically, this has led\n\t\t\t// to performance improvements for applications which make heavy use of\n\t\t\t// update ranges. Likely due to GPU command overhead.\n\t\t\t//\n\t\t\t// Note that to reduce garbage collection between frames, we merge the\n\t\t\t// update ranges in-place. This is safe because this method will clear the\n\t\t\t// update ranges once updated.\n\n\t\t\tupdateRanges.sort( ( a, b ) => a.start - b.start );\n\n\t\t\t// To merge the update ranges in-place, we work from left to right in the\n\t\t\t// existing updateRanges array, merging ranges. This may result in a final\n\t\t\t// array which is smaller than the original. This index tracks the last\n\t\t\t// index representing a merged range, any data after this index can be\n\t\t\t// trimmed once the merge algorithm is completed.\n\t\t\tlet mergeIndex = 0;\n\n\t\t\tfor ( let i = 1; i < updateRanges.length; i ++ ) {\n\n\t\t\t\tconst previousRange = updateRanges[ mergeIndex ];\n\t\t\t\tconst range = updateRanges[ i ];\n\n\t\t\t\t// We add one here to merge adjacent ranges. This is safe because ranges\n\t\t\t\t// operate over positive integers.\n\t\t\t\tif ( range.start <= previousRange.start + previousRange.count + 1 ) {\n\n\t\t\t\t\tpreviousRange.count = Math.max(\n\t\t\t\t\t\tpreviousRange.count,\n\t\t\t\t\t\trange.start + range.count - previousRange.start\n\t\t\t\t\t);\n\n\t\t\t\t} else {\n\n\t\t\t\t\t++ mergeIndex;\n\t\t\t\t\tupdateRanges[ mergeIndex ] = range;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// Trim the array to only contain the merged ranges.\n\t\t\tupdateRanges.length = mergeIndex + 1;\n\n\t\t\tfor ( let i = 0, l = updateRanges.length; i < l; i ++ ) {\n\n\t\t\t\tconst range = updateRanges[ i ];\n\n\t\t\t\tgl.bufferSubData( bufferType, range.start * array.BYTES_PER_ELEMENT,\n\t\t\t\t\tarray, range.start, range.count );\n\n\t\t\t}\n\n\t\t\tattribute.clearUpdateRanges();\n\n\t\t}\n\n\t\tattribute.onUploadCallback();\n\n\t}\n\n\t//\n\n\tfunction get( attribute ) {\n\n\t\tif ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;\n\n\t\treturn buffers.get( attribute );\n\n\t}\n\n\tfunction remove( attribute ) {\n\n\t\tif ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;\n\n\t\tconst data = buffers.get( attribute );\n\n\t\tif ( data ) {\n\n\t\t\tgl.deleteBuffer( data.buffer );\n\n\t\t\tbuffers.delete( attribute );\n\n\t\t}\n\n\t}\n\n\tfunction update( attribute, bufferType ) {\n\n\t\tif ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;\n\n\t\tif ( attribute.isGLBufferAttribute ) {\n\n\t\t\tconst cached = buffers.get( attribute );\n\n\t\t\tif ( ! cached || cached.version < attribute.version ) {\n\n\t\t\t\tbuffers.set( attribute, {\n\t\t\t\t\tbuffer: attribute.buffer,\n\t\t\t\t\ttype: attribute.type,\n\t\t\t\t\tbytesPerElement: attribute.elementSize,\n\t\t\t\t\tversion: attribute.version\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tconst data = buffers.get( attribute );\n\n\t\tif ( data === undefined ) {\n\n\t\t\tbuffers.set( attribute, createBuffer( attribute, bufferType ) );\n\n\t\t} else if ( data.version < attribute.version ) {\n\n\t\t\tif ( data.size !== attribute.array.byteLength ) {\n\n\t\t\t\tthrow new Error( 'THREE.WebGLAttributes: The size of the buffer attribute\\'s array buffer does not match the original size. Resizing buffer attributes is not supported.' );\n\n\t\t\t}\n\n\t\t\tupdateBuffer( data.buffer, attribute, bufferType );\n\n\t\t\tdata.version = attribute.version;\n\n\t\t}\n\n\t}\n\n\treturn {\n\n\t\tget: get,\n\t\tremove: remove,\n\t\tupdate: update\n\n\t};\n\n}\n\nvar alphahash_fragment = \"#ifdef USE_ALPHAHASH\\n\\tif ( diffuseColor.a < getAlphaHashThreshold( vPosition ) ) discard;\\n#endif\";\n\nvar alphahash_pars_fragment = \"#ifdef USE_ALPHAHASH\\n\\tconst float ALPHA_HASH_SCALE = 0.05;\\n\\tfloat hash2D( vec2 value ) {\\n\\t\\treturn fract( 1.0e4 * sin( 17.0 * value.x + 0.1 * value.y ) * ( 0.1 + abs( sin( 13.0 * value.y + value.x ) ) ) );\\n\\t}\\n\\tfloat hash3D( vec3 value ) {\\n\\t\\treturn hash2D( vec2( hash2D( value.xy ), value.z ) );\\n\\t}\\n\\tfloat getAlphaHashThreshold( vec3 position ) {\\n\\t\\tfloat maxDeriv = max(\\n\\t\\t\\tlength( dFdx( position.xyz ) ),\\n\\t\\t\\tlength( dFdy( position.xyz ) )\\n\\t\\t);\\n\\t\\tfloat pixScale = 1.0 / ( ALPHA_HASH_SCALE * maxDeriv );\\n\\t\\tvec2 pixScales = vec2(\\n\\t\\t\\texp2( floor( log2( pixScale ) ) ),\\n\\t\\t\\texp2( ceil( log2( pixScale ) ) )\\n\\t\\t);\\n\\t\\tvec2 alpha = vec2(\\n\\t\\t\\thash3D( floor( pixScales.x * position.xyz ) ),\\n\\t\\t\\thash3D( floor( pixScales.y * position.xyz ) )\\n\\t\\t);\\n\\t\\tfloat lerpFactor = fract( log2( pixScale ) );\\n\\t\\tfloat x = ( 1.0 - lerpFactor ) * alpha.x + lerpFactor * alpha.y;\\n\\t\\tfloat a = min( lerpFactor, 1.0 - lerpFactor );\\n\\t\\tvec3 cases = vec3(\\n\\t\\t\\tx * x / ( 2.0 * a * ( 1.0 - a ) ),\\n\\t\\t\\t( x - 0.5 * a ) / ( 1.0 - a ),\\n\\t\\t\\t1.0 - ( ( 1.0 - x ) * ( 1.0 - x ) / ( 2.0 * a * ( 1.0 - a ) ) )\\n\\t\\t);\\n\\t\\tfloat threshold = ( x < ( 1.0 - a ) )\\n\\t\\t\\t? ( ( x < a ) ? cases.x : cases.y )\\n\\t\\t\\t: cases.z;\\n\\t\\treturn clamp( threshold , 1.0e-6, 1.0 );\\n\\t}\\n#endif\";\n\nvar alphamap_fragment = \"#ifdef USE_ALPHAMAP\\n\\tdiffuseColor.a *= texture2D( alphaMap, vAlphaMapUv ).g;\\n#endif\";\n\nvar alphamap_pars_fragment = \"#ifdef USE_ALPHAMAP\\n\\tuniform sampler2D alphaMap;\\n#endif\";\n\nvar alphatest_fragment = \"#ifdef USE_ALPHATEST\\n\\t#ifdef ALPHA_TO_COVERAGE\\n\\tdiffuseColor.a = smoothstep( alphaTest, alphaTest + fwidth( diffuseColor.a ), diffuseColor.a );\\n\\tif ( diffuseColor.a == 0.0 ) discard;\\n\\t#else\\n\\tif ( diffuseColor.a < alphaTest ) discard;\\n\\t#endif\\n#endif\";\n\nvar alphatest_pars_fragment = \"#ifdef USE_ALPHATEST\\n\\tuniform float alphaTest;\\n#endif\";\n\nvar aomap_fragment = \"#ifdef USE_AOMAP\\n\\tfloat ambientOcclusion = ( texture2D( aoMap, vAoMapUv ).r - 1.0 ) * aoMapIntensity + 1.0;\\n\\treflectedLight.indirectDiffuse *= ambientOcclusion;\\n\\t#if defined( USE_CLEARCOAT ) \\n\\t\\tclearcoatSpecularIndirect *= ambientOcclusion;\\n\\t#endif\\n\\t#if defined( USE_SHEEN ) \\n\\t\\tsheenSpecularIndirect *= ambientOcclusion;\\n\\t#endif\\n\\t#if defined( USE_ENVMAP ) && defined( STANDARD )\\n\\t\\tfloat dotNV = saturate( dot( geometryNormal, geometryViewDir ) );\\n\\t\\treflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );\\n\\t#endif\\n#endif\";\n\nvar aomap_pars_fragment = \"#ifdef USE_AOMAP\\n\\tuniform sampler2D aoMap;\\n\\tuniform float aoMapIntensity;\\n#endif\";\n\nvar batching_pars_vertex = \"#ifdef USE_BATCHING\\n\\t#if ! defined( GL_ANGLE_multi_draw )\\n\\t#define gl_DrawID _gl_DrawID\\n\\tuniform int _gl_DrawID;\\n\\t#endif\\n\\tuniform highp sampler2D batchingTexture;\\n\\tuniform highp usampler2D batchingIdTexture;\\n\\tmat4 getBatchingMatrix( const in float i ) {\\n\\t\\tint size = textureSize( batchingTexture, 0 ).x;\\n\\t\\tint j = int( i ) * 4;\\n\\t\\tint x = j % size;\\n\\t\\tint y = j / size;\\n\\t\\tvec4 v1 = texelFetch( batchingTexture, ivec2( x, y ), 0 );\\n\\t\\tvec4 v2 = texelFetch( batchingTexture, ivec2( x + 1, y ), 0 );\\n\\t\\tvec4 v3 = texelFetch( batchingTexture, ivec2( x + 2, y ), 0 );\\n\\t\\tvec4 v4 = texelFetch( batchingTexture, ivec2( x + 3, y ), 0 );\\n\\t\\treturn mat4( v1, v2, v3, v4 );\\n\\t}\\n\\tfloat getIndirectIndex( const in int i ) {\\n\\t\\tint size = textureSize( batchingIdTexture, 0 ).x;\\n\\t\\tint x = i % size;\\n\\t\\tint y = i / size;\\n\\t\\treturn float( texelFetch( batchingIdTexture, ivec2( x, y ), 0 ).r );\\n\\t}\\n#endif\\n#ifdef USE_BATCHING_COLOR\\n\\tuniform sampler2D batchingColorTexture;\\n\\tvec3 getBatchingColor( const in float i ) {\\n\\t\\tint size = textureSize( batchingColorTexture, 0 ).x;\\n\\t\\tint j = int( i );\\n\\t\\tint x = j % size;\\n\\t\\tint y = j / size;\\n\\t\\treturn texelFetch( batchingColorTexture, ivec2( x, y ), 0 ).rgb;\\n\\t}\\n#endif\";\n\nvar batching_vertex = \"#ifdef USE_BATCHING\\n\\tmat4 batchingMatrix = getBatchingMatrix( getIndirectIndex( gl_DrawID ) );\\n#endif\";\n\nvar begin_vertex = \"vec3 transformed = vec3( position );\\n#ifdef USE_ALPHAHASH\\n\\tvPosition = vec3( position );\\n#endif\";\n\nvar beginnormal_vertex = \"vec3 objectNormal = vec3( normal );\\n#ifdef USE_TANGENT\\n\\tvec3 objectTangent = vec3( tangent.xyz );\\n#endif\";\n\nvar bsdfs = \"float G_BlinnPhong_Implicit( ) {\\n\\treturn 0.25;\\n}\\nfloat D_BlinnPhong( const in float shininess, const in float dotNH ) {\\n\\treturn RECIPROCAL_PI * ( shininess * 0.5 + 1.0 ) * pow( dotNH, shininess );\\n}\\nvec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in vec3 specularColor, const in float shininess ) {\\n\\tvec3 halfDir = normalize( lightDir + viewDir );\\n\\tfloat dotNH = saturate( dot( normal, halfDir ) );\\n\\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\\n\\tvec3 F = F_Schlick( specularColor, 1.0, dotVH );\\n\\tfloat G = G_BlinnPhong_Implicit( );\\n\\tfloat D = D_BlinnPhong( shininess, dotNH );\\n\\treturn F * ( G * D );\\n} // validated\";\n\nvar iridescence_fragment = \"#ifdef USE_IRIDESCENCE\\n\\tconst mat3 XYZ_TO_REC709 = mat3(\\n\\t\\t 3.2404542, -0.9692660,  0.0556434,\\n\\t\\t-1.5371385,  1.8760108, -0.2040259,\\n\\t\\t-0.4985314,  0.0415560,  1.0572252\\n\\t);\\n\\tvec3 Fresnel0ToIor( vec3 fresnel0 ) {\\n\\t\\tvec3 sqrtF0 = sqrt( fresnel0 );\\n\\t\\treturn ( vec3( 1.0 ) + sqrtF0 ) / ( vec3( 1.0 ) - sqrtF0 );\\n\\t}\\n\\tvec3 IorToFresnel0( vec3 transmittedIor, float incidentIor ) {\\n\\t\\treturn pow2( ( transmittedIor - vec3( incidentIor ) ) / ( transmittedIor + vec3( incidentIor ) ) );\\n\\t}\\n\\tfloat IorToFresnel0( float transmittedIor, float incidentIor ) {\\n\\t\\treturn pow2( ( transmittedIor - incidentIor ) / ( transmittedIor + incidentIor ));\\n\\t}\\n\\tvec3 evalSensitivity( float OPD, vec3 shift ) {\\n\\t\\tfloat phase = 2.0 * PI * OPD * 1.0e-9;\\n\\t\\tvec3 val = vec3( 5.4856e-13, 4.4201e-13, 5.2481e-13 );\\n\\t\\tvec3 pos = vec3( 1.6810e+06, 1.7953e+06, 2.2084e+06 );\\n\\t\\tvec3 var = vec3( 4.3278e+09, 9.3046e+09, 6.6121e+09 );\\n\\t\\tvec3 xyz = val * sqrt( 2.0 * PI * var ) * cos( pos * phase + shift ) * exp( - pow2( phase ) * var );\\n\\t\\txyz.x += 9.7470e-14 * sqrt( 2.0 * PI * 4.5282e+09 ) * cos( 2.2399e+06 * phase + shift[ 0 ] ) * exp( - 4.5282e+09 * pow2( phase ) );\\n\\t\\txyz /= 1.0685e-7;\\n\\t\\tvec3 rgb = XYZ_TO_REC709 * xyz;\\n\\t\\treturn rgb;\\n\\t}\\n\\tvec3 evalIridescence( float outsideIOR, float eta2, float cosTheta1, float thinFilmThickness, vec3 baseF0 ) {\\n\\t\\tvec3 I;\\n\\t\\tfloat iridescenceIOR = mix( outsideIOR, eta2, smoothstep( 0.0, 0.03, thinFilmThickness ) );\\n\\t\\tfloat sinTheta2Sq = pow2( outsideIOR / iridescenceIOR ) * ( 1.0 - pow2( cosTheta1 ) );\\n\\t\\tfloat cosTheta2Sq = 1.0 - sinTheta2Sq;\\n\\t\\tif ( cosTheta2Sq < 0.0 ) {\\n\\t\\t\\treturn vec3( 1.0 );\\n\\t\\t}\\n\\t\\tfloat cosTheta2 = sqrt( cosTheta2Sq );\\n\\t\\tfloat R0 = IorToFresnel0( iridescenceIOR, outsideIOR );\\n\\t\\tfloat R12 = F_Schlick( R0, 1.0, cosTheta1 );\\n\\t\\tfloat T121 = 1.0 - R12;\\n\\t\\tfloat phi12 = 0.0;\\n\\t\\tif ( iridescenceIOR < outsideIOR ) phi12 = PI;\\n\\t\\tfloat phi21 = PI - phi12;\\n\\t\\tvec3 baseIOR = Fresnel0ToIor( clamp( baseF0, 0.0, 0.9999 ) );\\t\\tvec3 R1 = IorToFresnel0( baseIOR, iridescenceIOR );\\n\\t\\tvec3 R23 = F_Schlick( R1, 1.0, cosTheta2 );\\n\\t\\tvec3 phi23 = vec3( 0.0 );\\n\\t\\tif ( baseIOR[ 0 ] < iridescenceIOR ) phi23[ 0 ] = PI;\\n\\t\\tif ( baseIOR[ 1 ] < iridescenceIOR ) phi23[ 1 ] = PI;\\n\\t\\tif ( baseIOR[ 2 ] < iridescenceIOR ) phi23[ 2 ] = PI;\\n\\t\\tfloat OPD = 2.0 * iridescenceIOR * thinFilmThickness * cosTheta2;\\n\\t\\tvec3 phi = vec3( phi21 ) + phi23;\\n\\t\\tvec3 R123 = clamp( R12 * R23, 1e-5, 0.9999 );\\n\\t\\tvec3 r123 = sqrt( R123 );\\n\\t\\tvec3 Rs = pow2( T121 ) * R23 / ( vec3( 1.0 ) - R123 );\\n\\t\\tvec3 C0 = R12 + Rs;\\n\\t\\tI = C0;\\n\\t\\tvec3 Cm = Rs - T121;\\n\\t\\tfor ( int m = 1; m <= 2; ++ m ) {\\n\\t\\t\\tCm *= r123;\\n\\t\\t\\tvec3 Sm = 2.0 * evalSensitivity( float( m ) * OPD, float( m ) * phi );\\n\\t\\t\\tI += Cm * Sm;\\n\\t\\t}\\n\\t\\treturn max( I, vec3( 0.0 ) );\\n\\t}\\n#endif\";\n\nvar bumpmap_pars_fragment = \"#ifdef USE_BUMPMAP\\n\\tuniform sampler2D bumpMap;\\n\\tuniform float bumpScale;\\n\\tvec2 dHdxy_fwd() {\\n\\t\\tvec2 dSTdx = dFdx( vBumpMapUv );\\n\\t\\tvec2 dSTdy = dFdy( vBumpMapUv );\\n\\t\\tfloat Hll = bumpScale * texture2D( bumpMap, vBumpMapUv ).x;\\n\\t\\tfloat dBx = bumpScale * texture2D( bumpMap, vBumpMapUv + dSTdx ).x - Hll;\\n\\t\\tfloat dBy = bumpScale * texture2D( bumpMap, vBumpMapUv + dSTdy ).x - Hll;\\n\\t\\treturn vec2( dBx, dBy );\\n\\t}\\n\\tvec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) {\\n\\t\\tvec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) );\\n\\t\\tvec3 vSigmaY = normalize( dFdy( surf_pos.xyz ) );\\n\\t\\tvec3 vN = surf_norm;\\n\\t\\tvec3 R1 = cross( vSigmaY, vN );\\n\\t\\tvec3 R2 = cross( vN, vSigmaX );\\n\\t\\tfloat fDet = dot( vSigmaX, R1 ) * faceDirection;\\n\\t\\tvec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );\\n\\t\\treturn normalize( abs( fDet ) * surf_norm - vGrad );\\n\\t}\\n#endif\";\n\nvar clipping_planes_fragment = \"#if NUM_CLIPPING_PLANES > 0\\n\\tvec4 plane;\\n\\t#ifdef ALPHA_TO_COVERAGE\\n\\t\\tfloat distanceToPlane, distanceGradient;\\n\\t\\tfloat clipOpacity = 1.0;\\n\\t\\t#pragma unroll_loop_start\\n\\t\\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\\n\\t\\t\\tplane = clippingPlanes[ i ];\\n\\t\\t\\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\\n\\t\\t\\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\\n\\t\\t\\tclipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\\n\\t\\t\\tif ( clipOpacity == 0.0 ) discard;\\n\\t\\t}\\n\\t\\t#pragma unroll_loop_end\\n\\t\\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\\n\\t\\t\\tfloat unionClipOpacity = 1.0;\\n\\t\\t\\t#pragma unroll_loop_start\\n\\t\\t\\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\\n\\t\\t\\t\\tplane = clippingPlanes[ i ];\\n\\t\\t\\t\\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\\n\\t\\t\\t\\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\\n\\t\\t\\t\\tunionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\\n\\t\\t\\t}\\n\\t\\t\\t#pragma unroll_loop_end\\n\\t\\t\\tclipOpacity *= 1.0 - unionClipOpacity;\\n\\t\\t#endif\\n\\t\\tdiffuseColor.a *= clipOpacity;\\n\\t\\tif ( diffuseColor.a == 0.0 ) discard;\\n\\t#else\\n\\t\\t#pragma unroll_loop_start\\n\\t\\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\\n\\t\\t\\tplane = clippingPlanes[ i ];\\n\\t\\t\\tif ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;\\n\\t\\t}\\n\\t\\t#pragma unroll_loop_end\\n\\t\\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\\n\\t\\t\\tbool clipped = true;\\n\\t\\t\\t#pragma unroll_loop_start\\n\\t\\t\\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\\n\\t\\t\\t\\tplane = clippingPlanes[ i ];\\n\\t\\t\\t\\tclipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped;\\n\\t\\t\\t}\\n\\t\\t\\t#pragma unroll_loop_end\\n\\t\\t\\tif ( clipped ) discard;\\n\\t\\t#endif\\n\\t#endif\\n#endif\";\n\nvar clipping_planes_pars_fragment = \"#if NUM_CLIPPING_PLANES > 0\\n\\tvarying vec3 vClipPosition;\\n\\tuniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];\\n#endif\";\n\nvar clipping_planes_pars_vertex = \"#if NUM_CLIPPING_PLANES > 0\\n\\tvarying vec3 vClipPosition;\\n#endif\";\n\nvar clipping_planes_vertex = \"#if NUM_CLIPPING_PLANES > 0\\n\\tvClipPosition = - mvPosition.xyz;\\n#endif\";\n\nvar color_fragment = \"#if defined( USE_COLOR_ALPHA )\\n\\tdiffuseColor *= vColor;\\n#elif defined( USE_COLOR )\\n\\tdiffuseColor.rgb *= vColor;\\n#endif\";\n\nvar color_pars_fragment = \"#if defined( USE_COLOR_ALPHA )\\n\\tvarying vec4 vColor;\\n#elif defined( USE_COLOR )\\n\\tvarying vec3 vColor;\\n#endif\";\n\nvar color_pars_vertex = \"#if defined( USE_COLOR_ALPHA )\\n\\tvarying vec4 vColor;\\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\\n\\tvarying vec3 vColor;\\n#endif\";\n\nvar color_vertex = \"#if defined( USE_COLOR_ALPHA )\\n\\tvColor = vec4( 1.0 );\\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\\n\\tvColor = vec3( 1.0 );\\n#endif\\n#ifdef USE_COLOR\\n\\tvColor *= color;\\n#endif\\n#ifdef USE_INSTANCING_COLOR\\n\\tvColor.xyz *= instanceColor.xyz;\\n#endif\\n#ifdef USE_BATCHING_COLOR\\n\\tvec3 batchingColor = getBatchingColor( getIndirectIndex( gl_DrawID ) );\\n\\tvColor.xyz *= batchingColor.xyz;\\n#endif\";\n\nvar common = \"#define PI 3.141592653589793\\n#define PI2 6.283185307179586\\n#define PI_HALF 1.5707963267948966\\n#define RECIPROCAL_PI 0.3183098861837907\\n#define RECIPROCAL_PI2 0.15915494309189535\\n#define EPSILON 1e-6\\n#ifndef saturate\\n#define saturate( a ) clamp( a, 0.0, 1.0 )\\n#endif\\n#define whiteComplement( a ) ( 1.0 - saturate( a ) )\\nfloat pow2( const in float x ) { return x*x; }\\nvec3 pow2( const in vec3 x ) { return x*x; }\\nfloat pow3( const in float x ) { return x*x*x; }\\nfloat pow4( const in float x ) { float x2 = x*x; return x2*x2; }\\nfloat max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); }\\nfloat average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); }\\nhighp float rand( const in vec2 uv ) {\\n\\tconst highp float a = 12.9898, b = 78.233, c = 43758.5453;\\n\\thighp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );\\n\\treturn fract( sin( sn ) * c );\\n}\\n#ifdef HIGH_PRECISION\\n\\tfloat precisionSafeLength( vec3 v ) { return length( v ); }\\n#else\\n\\tfloat precisionSafeLength( vec3 v ) {\\n\\t\\tfloat maxComponent = max3( abs( v ) );\\n\\t\\treturn length( v / maxComponent ) * maxComponent;\\n\\t}\\n#endif\\nstruct IncidentLight {\\n\\tvec3 color;\\n\\tvec3 direction;\\n\\tbool visible;\\n};\\nstruct ReflectedLight {\\n\\tvec3 directDiffuse;\\n\\tvec3 directSpecular;\\n\\tvec3 indirectDiffuse;\\n\\tvec3 indirectSpecular;\\n};\\n#ifdef USE_ALPHAHASH\\n\\tvarying vec3 vPosition;\\n#endif\\nvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\\n\\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\\n}\\nvec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {\\n\\treturn normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );\\n}\\nbool isPerspectiveMatrix( mat4 m ) {\\n\\treturn m[ 2 ][ 3 ] == - 1.0;\\n}\\nvec2 equirectUv( in vec3 dir ) {\\n\\tfloat u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5;\\n\\tfloat v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\\n\\treturn vec2( u, v );\\n}\\nvec3 BRDF_Lambert( const in vec3 diffuseColor ) {\\n\\treturn RECIPROCAL_PI * diffuseColor;\\n}\\nvec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) {\\n\\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\\n\\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\\n}\\nfloat F_Schlick( const in float f0, const in float f90, const in float dotVH ) {\\n\\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\\n\\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\\n} // validated\";\n\nvar cube_uv_reflection_fragment = \"#ifdef ENVMAP_TYPE_CUBE_UV\\n\\t#define cubeUV_minMipLevel 4.0\\n\\t#define cubeUV_minTileSize 16.0\\n\\tfloat getFace( vec3 direction ) {\\n\\t\\tvec3 absDirection = abs( direction );\\n\\t\\tfloat face = - 1.0;\\n\\t\\tif ( absDirection.x > absDirection.z ) {\\n\\t\\t\\tif ( absDirection.x > absDirection.y )\\n\\t\\t\\t\\tface = direction.x > 0.0 ? 0.0 : 3.0;\\n\\t\\t\\telse\\n\\t\\t\\t\\tface = direction.y > 0.0 ? 1.0 : 4.0;\\n\\t\\t} else {\\n\\t\\t\\tif ( absDirection.z > absDirection.y )\\n\\t\\t\\t\\tface = direction.z > 0.0 ? 2.0 : 5.0;\\n\\t\\t\\telse\\n\\t\\t\\t\\tface = direction.y > 0.0 ? 1.0 : 4.0;\\n\\t\\t}\\n\\t\\treturn face;\\n\\t}\\n\\tvec2 getUV( vec3 direction, float face ) {\\n\\t\\tvec2 uv;\\n\\t\\tif ( face == 0.0 ) {\\n\\t\\t\\tuv = vec2( direction.z, direction.y ) / abs( direction.x );\\n\\t\\t} else if ( face == 1.0 ) {\\n\\t\\t\\tuv = vec2( - direction.x, - direction.z ) / abs( direction.y );\\n\\t\\t} else if ( face == 2.0 ) {\\n\\t\\t\\tuv = vec2( - direction.x, direction.y ) / abs( direction.z );\\n\\t\\t} else if ( face == 3.0 ) {\\n\\t\\t\\tuv = vec2( - direction.z, direction.y ) / abs( direction.x );\\n\\t\\t} else if ( face == 4.0 ) {\\n\\t\\t\\tuv = vec2( - direction.x, direction.z ) / abs( direction.y );\\n\\t\\t} else {\\n\\t\\t\\tuv = vec2( direction.x, direction.y ) / abs( direction.z );\\n\\t\\t}\\n\\t\\treturn 0.5 * ( uv + 1.0 );\\n\\t}\\n\\tvec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) {\\n\\t\\tfloat face = getFace( direction );\\n\\t\\tfloat filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 );\\n\\t\\tmipInt = max( mipInt, cubeUV_minMipLevel );\\n\\t\\tfloat faceSize = exp2( mipInt );\\n\\t\\thighp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0;\\n\\t\\tif ( face > 2.0 ) {\\n\\t\\t\\tuv.y += faceSize;\\n\\t\\t\\tface -= 3.0;\\n\\t\\t}\\n\\t\\tuv.x += face * faceSize;\\n\\t\\tuv.x += filterInt * 3.0 * cubeUV_minTileSize;\\n\\t\\tuv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize );\\n\\t\\tuv.x *= CUBEUV_TEXEL_WIDTH;\\n\\t\\tuv.y *= CUBEUV_TEXEL_HEIGHT;\\n\\t\\t#ifdef texture2DGradEXT\\n\\t\\t\\treturn texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb;\\n\\t\\t#else\\n\\t\\t\\treturn texture2D( envMap, uv ).rgb;\\n\\t\\t#endif\\n\\t}\\n\\t#define cubeUV_r0 1.0\\n\\t#define cubeUV_m0 - 2.0\\n\\t#define cubeUV_r1 0.8\\n\\t#define cubeUV_m1 - 1.0\\n\\t#define cubeUV_r4 0.4\\n\\t#define cubeUV_m4 2.0\\n\\t#define cubeUV_r5 0.305\\n\\t#define cubeUV_m5 3.0\\n\\t#define cubeUV_r6 0.21\\n\\t#define cubeUV_m6 4.0\\n\\tfloat roughnessToMip( float roughness ) {\\n\\t\\tfloat mip = 0.0;\\n\\t\\tif ( roughness >= cubeUV_r1 ) {\\n\\t\\t\\tmip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0;\\n\\t\\t} else if ( roughness >= cubeUV_r4 ) {\\n\\t\\t\\tmip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1;\\n\\t\\t} else if ( roughness >= cubeUV_r5 ) {\\n\\t\\t\\tmip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4;\\n\\t\\t} else if ( roughness >= cubeUV_r6 ) {\\n\\t\\t\\tmip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5;\\n\\t\\t} else {\\n\\t\\t\\tmip = - 2.0 * log2( 1.16 * roughness );\\t\\t}\\n\\t\\treturn mip;\\n\\t}\\n\\tvec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) {\\n\\t\\tfloat mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP );\\n\\t\\tfloat mipF = fract( mip );\\n\\t\\tfloat mipInt = floor( mip );\\n\\t\\tvec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt );\\n\\t\\tif ( mipF == 0.0 ) {\\n\\t\\t\\treturn vec4( color0, 1.0 );\\n\\t\\t} else {\\n\\t\\t\\tvec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 );\\n\\t\\t\\treturn vec4( mix( color0, color1, mipF ), 1.0 );\\n\\t\\t}\\n\\t}\\n#endif\";\n\nvar defaultnormal_vertex = \"vec3 transformedNormal = objectNormal;\\n#ifdef USE_TANGENT\\n\\tvec3 transformedTangent = objectTangent;\\n#endif\\n#ifdef USE_BATCHING\\n\\tmat3 bm = mat3( batchingMatrix );\\n\\ttransformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) );\\n\\ttransformedNormal = bm * transformedNormal;\\n\\t#ifdef USE_TANGENT\\n\\t\\ttransformedTangent = bm * transformedTangent;\\n\\t#endif\\n#endif\\n#ifdef USE_INSTANCING\\n\\tmat3 im = mat3( instanceMatrix );\\n\\ttransformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) );\\n\\ttransformedNormal = im * transformedNormal;\\n\\t#ifdef USE_TANGENT\\n\\t\\ttransformedTangent = im * transformedTangent;\\n\\t#endif\\n#endif\\ntransformedNormal = normalMatrix * transformedNormal;\\n#ifdef FLIP_SIDED\\n\\ttransformedNormal = - transformedNormal;\\n#endif\\n#ifdef USE_TANGENT\\n\\ttransformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz;\\n\\t#ifdef FLIP_SIDED\\n\\t\\ttransformedTangent = - transformedTangent;\\n\\t#endif\\n#endif\";\n\nvar displacementmap_pars_vertex = \"#ifdef USE_DISPLACEMENTMAP\\n\\tuniform sampler2D displacementMap;\\n\\tuniform float displacementScale;\\n\\tuniform float displacementBias;\\n#endif\";\n\nvar displacementmap_vertex = \"#ifdef USE_DISPLACEMENTMAP\\n\\ttransformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias );\\n#endif\";\n\nvar emissivemap_fragment = \"#ifdef USE_EMISSIVEMAP\\n\\tvec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv );\\n\\t#ifdef DECODE_VIDEO_TEXTURE_EMISSIVE\\n\\t\\temissiveColor = sRGBTransferEOTF( emissiveColor );\\n\\t#endif\\n\\ttotalEmissiveRadiance *= emissiveColor.rgb;\\n#endif\";\n\nvar emissivemap_pars_fragment = \"#ifdef USE_EMISSIVEMAP\\n\\tuniform sampler2D emissiveMap;\\n#endif\";\n\nvar colorspace_fragment = \"gl_FragColor = linearToOutputTexel( gl_FragColor );\";\n\nvar colorspace_pars_fragment = \"vec4 LinearTransferOETF( in vec4 value ) {\\n\\treturn value;\\n}\\nvec4 sRGBTransferEOTF( in vec4 value ) {\\n\\treturn vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );\\n}\\nvec4 sRGBTransferOETF( in vec4 value ) {\\n\\treturn vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );\\n}\";\n\nvar envmap_fragment = \"#ifdef USE_ENVMAP\\n\\t#ifdef ENV_WORLDPOS\\n\\t\\tvec3 cameraToFrag;\\n\\t\\tif ( isOrthographic ) {\\n\\t\\t\\tcameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\\n\\t\\t} else {\\n\\t\\t\\tcameraToFrag = normalize( vWorldPosition - cameraPosition );\\n\\t\\t}\\n\\t\\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\\n\\t\\t#ifdef ENVMAP_MODE_REFLECTION\\n\\t\\t\\tvec3 reflectVec = reflect( cameraToFrag, worldNormal );\\n\\t\\t#else\\n\\t\\t\\tvec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio );\\n\\t\\t#endif\\n\\t#else\\n\\t\\tvec3 reflectVec = vReflect;\\n\\t#endif\\n\\t#ifdef ENVMAP_TYPE_CUBE\\n\\t\\tvec4 envColor = textureCube( envMap, envMapRotation * vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) );\\n\\t#else\\n\\t\\tvec4 envColor = vec4( 0.0 );\\n\\t#endif\\n\\t#ifdef ENVMAP_BLENDING_MULTIPLY\\n\\t\\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\\n\\t#elif defined( ENVMAP_BLENDING_MIX )\\n\\t\\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\\n\\t#elif defined( ENVMAP_BLENDING_ADD )\\n\\t\\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\\n\\t#endif\\n#endif\";\n\nvar envmap_common_pars_fragment = \"#ifdef USE_ENVMAP\\n\\tuniform float envMapIntensity;\\n\\tuniform float flipEnvMap;\\n\\tuniform mat3 envMapRotation;\\n\\t#ifdef ENVMAP_TYPE_CUBE\\n\\t\\tuniform samplerCube envMap;\\n\\t#else\\n\\t\\tuniform sampler2D envMap;\\n\\t#endif\\n#endif\";\n\nvar envmap_pars_fragment = \"#ifdef USE_ENVMAP\\n\\tuniform float reflectivity;\\n\\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\\n\\t\\t#define ENV_WORLDPOS\\n\\t#endif\\n\\t#ifdef ENV_WORLDPOS\\n\\t\\tvarying vec3 vWorldPosition;\\n\\t\\tuniform float refractionRatio;\\n\\t#else\\n\\t\\tvarying vec3 vReflect;\\n\\t#endif\\n#endif\";\n\nvar envmap_pars_vertex = \"#ifdef USE_ENVMAP\\n\\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\\n\\t\\t#define ENV_WORLDPOS\\n\\t#endif\\n\\t#ifdef ENV_WORLDPOS\\n\\t\\t\\n\\t\\tvarying vec3 vWorldPosition;\\n\\t#else\\n\\t\\tvarying vec3 vReflect;\\n\\t\\tuniform float refractionRatio;\\n\\t#endif\\n#endif\";\n\nvar envmap_vertex = \"#ifdef USE_ENVMAP\\n\\t#ifdef ENV_WORLDPOS\\n\\t\\tvWorldPosition = worldPosition.xyz;\\n\\t#else\\n\\t\\tvec3 cameraToVertex;\\n\\t\\tif ( isOrthographic ) {\\n\\t\\t\\tcameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\\n\\t\\t} else {\\n\\t\\t\\tcameraToVertex = normalize( worldPosition.xyz - cameraPosition );\\n\\t\\t}\\n\\t\\tvec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\\n\\t\\t#ifdef ENVMAP_MODE_REFLECTION\\n\\t\\t\\tvReflect = reflect( cameraToVertex, worldNormal );\\n\\t\\t#else\\n\\t\\t\\tvReflect = refract( cameraToVertex, worldNormal, refractionRatio );\\n\\t\\t#endif\\n\\t#endif\\n#endif\";\n\nvar fog_vertex = \"#ifdef USE_FOG\\n\\tvFogDepth = - mvPosition.z;\\n#endif\";\n\nvar fog_pars_vertex = \"#ifdef USE_FOG\\n\\tvarying float vFogDepth;\\n#endif\";\n\nvar fog_fragment = \"#ifdef USE_FOG\\n\\t#ifdef FOG_EXP2\\n\\t\\tfloat fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth );\\n\\t#else\\n\\t\\tfloat fogFactor = smoothstep( fogNear, fogFar, vFogDepth );\\n\\t#endif\\n\\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\\n#endif\";\n\nvar fog_pars_fragment = \"#ifdef USE_FOG\\n\\tuniform vec3 fogColor;\\n\\tvarying float vFogDepth;\\n\\t#ifdef FOG_EXP2\\n\\t\\tuniform float fogDensity;\\n\\t#else\\n\\t\\tuniform float fogNear;\\n\\t\\tuniform float fogFar;\\n\\t#endif\\n#endif\";\n\nvar gradientmap_pars_fragment = \"#ifdef USE_GRADIENTMAP\\n\\tuniform sampler2D gradientMap;\\n#endif\\nvec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) {\\n\\tfloat dotNL = dot( normal, lightDirection );\\n\\tvec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 );\\n\\t#ifdef USE_GRADIENTMAP\\n\\t\\treturn vec3( texture2D( gradientMap, coord ).r );\\n\\t#else\\n\\t\\tvec2 fw = fwidth( coord ) * 0.5;\\n\\t\\treturn mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) );\\n\\t#endif\\n}\";\n\nvar lightmap_pars_fragment = \"#ifdef USE_LIGHTMAP\\n\\tuniform sampler2D lightMap;\\n\\tuniform float lightMapIntensity;\\n#endif\";\n\nvar lights_lambert_fragment = \"LambertMaterial material;\\nmaterial.diffuseColor = diffuseColor.rgb;\\nmaterial.specularStrength = specularStrength;\";\n\nvar lights_lambert_pars_fragment = \"varying vec3 vViewPosition;\\nstruct LambertMaterial {\\n\\tvec3 diffuseColor;\\n\\tfloat specularStrength;\\n};\\nvoid RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\\n\\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\\n\\tvec3 irradiance = dotNL * directLight.color;\\n\\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\\n}\\nvoid RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\\n\\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\\n}\\n#define RE_Direct\\t\\t\\t\\tRE_Direct_Lambert\\n#define RE_IndirectDiffuse\\t\\tRE_IndirectDiffuse_Lambert\";\n\nvar lights_pars_begin = \"uniform bool receiveShadow;\\nuniform vec3 ambientLightColor;\\n#if defined( USE_LIGHT_PROBES )\\n\\tuniform vec3 lightProbe[ 9 ];\\n#endif\\nvec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {\\n\\tfloat x = normal.x, y = normal.y, z = normal.z;\\n\\tvec3 result = shCoefficients[ 0 ] * 0.886227;\\n\\tresult += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;\\n\\tresult += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;\\n\\tresult += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;\\n\\tresult += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;\\n\\tresult += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;\\n\\tresult += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );\\n\\tresult += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;\\n\\tresult += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );\\n\\treturn result;\\n}\\nvec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) {\\n\\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\\n\\tvec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe );\\n\\treturn irradiance;\\n}\\nvec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {\\n\\tvec3 irradiance = ambientLightColor;\\n\\treturn irradiance;\\n}\\nfloat getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {\\n\\tfloat distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );\\n\\tif ( cutoffDistance > 0.0 ) {\\n\\t\\tdistanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\\n\\t}\\n\\treturn distanceFalloff;\\n}\\nfloat getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) {\\n\\treturn smoothstep( coneCosine, penumbraCosine, angleCosine );\\n}\\n#if NUM_DIR_LIGHTS > 0\\n\\tstruct DirectionalLight {\\n\\t\\tvec3 direction;\\n\\t\\tvec3 color;\\n\\t};\\n\\tuniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];\\n\\tvoid getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) {\\n\\t\\tlight.color = directionalLight.color;\\n\\t\\tlight.direction = directionalLight.direction;\\n\\t\\tlight.visible = true;\\n\\t}\\n#endif\\n#if NUM_POINT_LIGHTS > 0\\n\\tstruct PointLight {\\n\\t\\tvec3 position;\\n\\t\\tvec3 color;\\n\\t\\tfloat distance;\\n\\t\\tfloat decay;\\n\\t};\\n\\tuniform PointLight pointLights[ NUM_POINT_LIGHTS ];\\n\\tvoid getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) {\\n\\t\\tvec3 lVector = pointLight.position - geometryPosition;\\n\\t\\tlight.direction = normalize( lVector );\\n\\t\\tfloat lightDistance = length( lVector );\\n\\t\\tlight.color = pointLight.color;\\n\\t\\tlight.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay );\\n\\t\\tlight.visible = ( light.color != vec3( 0.0 ) );\\n\\t}\\n#endif\\n#if NUM_SPOT_LIGHTS > 0\\n\\tstruct SpotLight {\\n\\t\\tvec3 position;\\n\\t\\tvec3 direction;\\n\\t\\tvec3 color;\\n\\t\\tfloat distance;\\n\\t\\tfloat decay;\\n\\t\\tfloat coneCos;\\n\\t\\tfloat penumbraCos;\\n\\t};\\n\\tuniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];\\n\\tvoid getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) {\\n\\t\\tvec3 lVector = spotLight.position - geometryPosition;\\n\\t\\tlight.direction = normalize( lVector );\\n\\t\\tfloat angleCos = dot( light.direction, spotLight.direction );\\n\\t\\tfloat spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos );\\n\\t\\tif ( spotAttenuation > 0.0 ) {\\n\\t\\t\\tfloat lightDistance = length( lVector );\\n\\t\\t\\tlight.color = spotLight.color * spotAttenuation;\\n\\t\\t\\tlight.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay );\\n\\t\\t\\tlight.visible = ( light.color != vec3( 0.0 ) );\\n\\t\\t} else {\\n\\t\\t\\tlight.color = vec3( 0.0 );\\n\\t\\t\\tlight.visible = false;\\n\\t\\t}\\n\\t}\\n#endif\\n#if NUM_RECT_AREA_LIGHTS > 0\\n\\tstruct RectAreaLight {\\n\\t\\tvec3 color;\\n\\t\\tvec3 position;\\n\\t\\tvec3 halfWidth;\\n\\t\\tvec3 halfHeight;\\n\\t};\\n\\tuniform sampler2D ltc_1;\\tuniform sampler2D ltc_2;\\n\\tuniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];\\n#endif\\n#if NUM_HEMI_LIGHTS > 0\\n\\tstruct HemisphereLight {\\n\\t\\tvec3 direction;\\n\\t\\tvec3 skyColor;\\n\\t\\tvec3 groundColor;\\n\\t};\\n\\tuniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ];\\n\\tvec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) {\\n\\t\\tfloat dotNL = dot( normal, hemiLight.direction );\\n\\t\\tfloat hemiDiffuseWeight = 0.5 * dotNL + 0.5;\\n\\t\\tvec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );\\n\\t\\treturn irradiance;\\n\\t}\\n#endif\";\n\nvar envmap_physical_pars_fragment = \"#ifdef USE_ENVMAP\\n\\tvec3 getIBLIrradiance( const in vec3 normal ) {\\n\\t\\t#ifdef ENVMAP_TYPE_CUBE_UV\\n\\t\\t\\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\\n\\t\\t\\tvec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 );\\n\\t\\t\\treturn PI * envMapColor.rgb * envMapIntensity;\\n\\t\\t#else\\n\\t\\t\\treturn vec3( 0.0 );\\n\\t\\t#endif\\n\\t}\\n\\tvec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) {\\n\\t\\t#ifdef ENVMAP_TYPE_CUBE_UV\\n\\t\\t\\tvec3 reflectVec = reflect( - viewDir, normal );\\n\\t\\t\\treflectVec = normalize( mix( reflectVec, normal, pow4( roughness ) ) );\\n\\t\\t\\treflectVec = inverseTransformDirection( reflectVec, viewMatrix );\\n\\t\\t\\tvec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness );\\n\\t\\t\\treturn envMapColor.rgb * envMapIntensity;\\n\\t\\t#else\\n\\t\\t\\treturn vec3( 0.0 );\\n\\t\\t#endif\\n\\t}\\n\\t#ifdef USE_ANISOTROPY\\n\\t\\tvec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) {\\n\\t\\t\\t#ifdef ENVMAP_TYPE_CUBE_UV\\n\\t\\t\\t\\tvec3 bentNormal = cross( bitangent, viewDir );\\n\\t\\t\\t\\tbentNormal = normalize( cross( bentNormal, bitangent ) );\\n\\t\\t\\t\\tbentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) );\\n\\t\\t\\t\\treturn getIBLRadiance( viewDir, bentNormal, roughness );\\n\\t\\t\\t#else\\n\\t\\t\\t\\treturn vec3( 0.0 );\\n\\t\\t\\t#endif\\n\\t\\t}\\n\\t#endif\\n#endif\";\n\nvar lights_toon_fragment = \"ToonMaterial material;\\nmaterial.diffuseColor = diffuseColor.rgb;\";\n\nvar lights_toon_pars_fragment = \"varying vec3 vViewPosition;\\nstruct ToonMaterial {\\n\\tvec3 diffuseColor;\\n};\\nvoid RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\\n\\tvec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color;\\n\\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\\n}\\nvoid RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\\n\\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\\n}\\n#define RE_Direct\\t\\t\\t\\tRE_Direct_Toon\\n#define RE_IndirectDiffuse\\t\\tRE_IndirectDiffuse_Toon\";\n\nvar lights_phong_fragment = \"BlinnPhongMaterial material;\\nmaterial.diffuseColor = diffuseColor.rgb;\\nmaterial.specularColor = specular;\\nmaterial.specularShininess = shininess;\\nmaterial.specularStrength = specularStrength;\";\n\nvar lights_phong_pars_fragment = \"varying vec3 vViewPosition;\\nstruct BlinnPhongMaterial {\\n\\tvec3 diffuseColor;\\n\\tvec3 specularColor;\\n\\tfloat specularShininess;\\n\\tfloat specularStrength;\\n};\\nvoid RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\\n\\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\\n\\tvec3 irradiance = dotNL * directLight.color;\\n\\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\\n\\treflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength;\\n}\\nvoid RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\\n\\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\\n}\\n#define RE_Direct\\t\\t\\t\\tRE_Direct_BlinnPhong\\n#define RE_IndirectDiffuse\\t\\tRE_IndirectDiffuse_BlinnPhong\";\n\nvar lights_physical_fragment = \"PhysicalMaterial material;\\nmaterial.diffuseColor = diffuseColor.rgb;\\nmaterial.diffuseContribution = diffuseColor.rgb * ( 1.0 - metalnessFactor );\\nmaterial.metalness = metalnessFactor;\\nvec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) );\\nfloat geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );\\nmaterial.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness;\\nmaterial.roughness = min( material.roughness, 1.0 );\\n#ifdef IOR\\n\\tmaterial.ior = ior;\\n\\t#ifdef USE_SPECULAR\\n\\t\\tfloat specularIntensityFactor = specularIntensity;\\n\\t\\tvec3 specularColorFactor = specularColor;\\n\\t\\t#ifdef USE_SPECULAR_COLORMAP\\n\\t\\t\\tspecularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb;\\n\\t\\t#endif\\n\\t\\t#ifdef USE_SPECULAR_INTENSITYMAP\\n\\t\\t\\tspecularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a;\\n\\t\\t#endif\\n\\t\\tmaterial.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor );\\n\\t#else\\n\\t\\tfloat specularIntensityFactor = 1.0;\\n\\t\\tvec3 specularColorFactor = vec3( 1.0 );\\n\\t\\tmaterial.specularF90 = 1.0;\\n\\t#endif\\n\\tmaterial.specularColor = min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor;\\n\\tmaterial.specularColorBlended = mix( material.specularColor, diffuseColor.rgb, metalnessFactor );\\n#else\\n\\tmaterial.specularColor = vec3( 0.04 );\\n\\tmaterial.specularColorBlended = mix( material.specularColor, diffuseColor.rgb, metalnessFactor );\\n\\tmaterial.specularF90 = 1.0;\\n#endif\\n#ifdef USE_CLEARCOAT\\n\\tmaterial.clearcoat = clearcoat;\\n\\tmaterial.clearcoatRoughness = clearcoatRoughness;\\n\\tmaterial.clearcoatF0 = vec3( 0.04 );\\n\\tmaterial.clearcoatF90 = 1.0;\\n\\t#ifdef USE_CLEARCOATMAP\\n\\t\\tmaterial.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x;\\n\\t#endif\\n\\t#ifdef USE_CLEARCOAT_ROUGHNESSMAP\\n\\t\\tmaterial.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y;\\n\\t#endif\\n\\tmaterial.clearcoat = saturate( material.clearcoat );\\tmaterial.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 );\\n\\tmaterial.clearcoatRoughness += geometryRoughness;\\n\\tmaterial.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 );\\n#endif\\n#ifdef USE_DISPERSION\\n\\tmaterial.dispersion = dispersion;\\n#endif\\n#ifdef USE_IRIDESCENCE\\n\\tmaterial.iridescence = iridescence;\\n\\tmaterial.iridescenceIOR = iridescenceIOR;\\n\\t#ifdef USE_IRIDESCENCEMAP\\n\\t\\tmaterial.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r;\\n\\t#endif\\n\\t#ifdef USE_IRIDESCENCE_THICKNESSMAP\\n\\t\\tmaterial.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum;\\n\\t#else\\n\\t\\tmaterial.iridescenceThickness = iridescenceThicknessMaximum;\\n\\t#endif\\n#endif\\n#ifdef USE_SHEEN\\n\\tmaterial.sheenColor = sheenColor;\\n\\t#ifdef USE_SHEEN_COLORMAP\\n\\t\\tmaterial.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb;\\n\\t#endif\\n\\tmaterial.sheenRoughness = clamp( sheenRoughness, 0.0001, 1.0 );\\n\\t#ifdef USE_SHEEN_ROUGHNESSMAP\\n\\t\\tmaterial.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a;\\n\\t#endif\\n#endif\\n#ifdef USE_ANISOTROPY\\n\\t#ifdef USE_ANISOTROPYMAP\\n\\t\\tmat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x );\\n\\t\\tvec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb;\\n\\t\\tvec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b;\\n\\t#else\\n\\t\\tvec2 anisotropyV = anisotropyVector;\\n\\t#endif\\n\\tmaterial.anisotropy = length( anisotropyV );\\n\\tif( material.anisotropy == 0.0 ) {\\n\\t\\tanisotropyV = vec2( 1.0, 0.0 );\\n\\t} else {\\n\\t\\tanisotropyV /= material.anisotropy;\\n\\t\\tmaterial.anisotropy = saturate( material.anisotropy );\\n\\t}\\n\\tmaterial.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) );\\n\\tmaterial.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y;\\n\\tmaterial.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y;\\n#endif\";\n\nvar lights_physical_pars_fragment = \"uniform sampler2D dfgLUT;\\nstruct PhysicalMaterial {\\n\\tvec3 diffuseColor;\\n\\tvec3 diffuseContribution;\\n\\tvec3 specularColor;\\n\\tvec3 specularColorBlended;\\n\\tfloat roughness;\\n\\tfloat metalness;\\n\\tfloat specularF90;\\n\\tfloat dispersion;\\n\\t#ifdef USE_CLEARCOAT\\n\\t\\tfloat clearcoat;\\n\\t\\tfloat clearcoatRoughness;\\n\\t\\tvec3 clearcoatF0;\\n\\t\\tfloat clearcoatF90;\\n\\t#endif\\n\\t#ifdef USE_IRIDESCENCE\\n\\t\\tfloat iridescence;\\n\\t\\tfloat iridescenceIOR;\\n\\t\\tfloat iridescenceThickness;\\n\\t\\tvec3 iridescenceFresnel;\\n\\t\\tvec3 iridescenceF0;\\n\\t\\tvec3 iridescenceFresnelDielectric;\\n\\t\\tvec3 iridescenceFresnelMetallic;\\n\\t#endif\\n\\t#ifdef USE_SHEEN\\n\\t\\tvec3 sheenColor;\\n\\t\\tfloat sheenRoughness;\\n\\t#endif\\n\\t#ifdef IOR\\n\\t\\tfloat ior;\\n\\t#endif\\n\\t#ifdef USE_TRANSMISSION\\n\\t\\tfloat transmission;\\n\\t\\tfloat transmissionAlpha;\\n\\t\\tfloat thickness;\\n\\t\\tfloat attenuationDistance;\\n\\t\\tvec3 attenuationColor;\\n\\t#endif\\n\\t#ifdef USE_ANISOTROPY\\n\\t\\tfloat anisotropy;\\n\\t\\tfloat alphaT;\\n\\t\\tvec3 anisotropyT;\\n\\t\\tvec3 anisotropyB;\\n\\t#endif\\n};\\nvec3 clearcoatSpecularDirect = vec3( 0.0 );\\nvec3 clearcoatSpecularIndirect = vec3( 0.0 );\\nvec3 sheenSpecularDirect = vec3( 0.0 );\\nvec3 sheenSpecularIndirect = vec3(0.0 );\\nvec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) {\\n    float x = clamp( 1.0 - dotVH, 0.0, 1.0 );\\n    float x2 = x * x;\\n    float x5 = clamp( x * x2 * x2, 0.0, 0.9999 );\\n    return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 );\\n}\\nfloat V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) {\\n\\tfloat a2 = pow2( alpha );\\n\\tfloat gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\\n\\tfloat gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\\n\\treturn 0.5 / max( gv + gl, EPSILON );\\n}\\nfloat D_GGX( const in float alpha, const in float dotNH ) {\\n\\tfloat a2 = pow2( alpha );\\n\\tfloat denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0;\\n\\treturn RECIPROCAL_PI * a2 / pow2( denom );\\n}\\n#ifdef USE_ANISOTROPY\\n\\tfloat V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) {\\n\\t\\tfloat gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) );\\n\\t\\tfloat gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) );\\n\\t\\tfloat v = 0.5 / ( gv + gl );\\n\\t\\treturn v;\\n\\t}\\n\\tfloat D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) {\\n\\t\\tfloat a2 = alphaT * alphaB;\\n\\t\\thighp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH );\\n\\t\\thighp float v2 = dot( v, v );\\n\\t\\tfloat w2 = a2 / v2;\\n\\t\\treturn RECIPROCAL_PI * a2 * pow2 ( w2 );\\n\\t}\\n#endif\\n#ifdef USE_CLEARCOAT\\n\\tvec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) {\\n\\t\\tvec3 f0 = material.clearcoatF0;\\n\\t\\tfloat f90 = material.clearcoatF90;\\n\\t\\tfloat roughness = material.clearcoatRoughness;\\n\\t\\tfloat alpha = pow2( roughness );\\n\\t\\tvec3 halfDir = normalize( lightDir + viewDir );\\n\\t\\tfloat dotNL = saturate( dot( normal, lightDir ) );\\n\\t\\tfloat dotNV = saturate( dot( normal, viewDir ) );\\n\\t\\tfloat dotNH = saturate( dot( normal, halfDir ) );\\n\\t\\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\\n\\t\\tvec3 F = F_Schlick( f0, f90, dotVH );\\n\\t\\tfloat V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );\\n\\t\\tfloat D = D_GGX( alpha, dotNH );\\n\\t\\treturn F * ( V * D );\\n\\t}\\n#endif\\nvec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) {\\n\\tvec3 f0 = material.specularColorBlended;\\n\\tfloat f90 = material.specularF90;\\n\\tfloat roughness = material.roughness;\\n\\tfloat alpha = pow2( roughness );\\n\\tvec3 halfDir = normalize( lightDir + viewDir );\\n\\tfloat dotNL = saturate( dot( normal, lightDir ) );\\n\\tfloat dotNV = saturate( dot( normal, viewDir ) );\\n\\tfloat dotNH = saturate( dot( normal, halfDir ) );\\n\\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\\n\\tvec3 F = F_Schlick( f0, f90, dotVH );\\n\\t#ifdef USE_IRIDESCENCE\\n\\t\\tF = mix( F, material.iridescenceFresnel, material.iridescence );\\n\\t#endif\\n\\t#ifdef USE_ANISOTROPY\\n\\t\\tfloat dotTL = dot( material.anisotropyT, lightDir );\\n\\t\\tfloat dotTV = dot( material.anisotropyT, viewDir );\\n\\t\\tfloat dotTH = dot( material.anisotropyT, halfDir );\\n\\t\\tfloat dotBL = dot( material.anisotropyB, lightDir );\\n\\t\\tfloat dotBV = dot( material.anisotropyB, viewDir );\\n\\t\\tfloat dotBH = dot( material.anisotropyB, halfDir );\\n\\t\\tfloat V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL );\\n\\t\\tfloat D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH );\\n\\t#else\\n\\t\\tfloat V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );\\n\\t\\tfloat D = D_GGX( alpha, dotNH );\\n\\t#endif\\n\\treturn F * ( V * D );\\n}\\nvec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) {\\n\\tconst float LUT_SIZE = 64.0;\\n\\tconst float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;\\n\\tconst float LUT_BIAS = 0.5 / LUT_SIZE;\\n\\tfloat dotNV = saturate( dot( N, V ) );\\n\\tvec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) );\\n\\tuv = uv * LUT_SCALE + LUT_BIAS;\\n\\treturn uv;\\n}\\nfloat LTC_ClippedSphereFormFactor( const in vec3 f ) {\\n\\tfloat l = length( f );\\n\\treturn max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 );\\n}\\nvec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) {\\n\\tfloat x = dot( v1, v2 );\\n\\tfloat y = abs( x );\\n\\tfloat a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y;\\n\\tfloat b = 3.4175940 + ( 4.1616724 + y ) * y;\\n\\tfloat v = a / b;\\n\\tfloat theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v;\\n\\treturn cross( v1, v2 ) * theta_sintheta;\\n}\\nvec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) {\\n\\tvec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ];\\n\\tvec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ];\\n\\tvec3 lightNormal = cross( v1, v2 );\\n\\tif( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 );\\n\\tvec3 T1, T2;\\n\\tT1 = normalize( V - N * dot( V, N ) );\\n\\tT2 = - cross( N, T1 );\\n\\tmat3 mat = mInv * transpose( mat3( T1, T2, N ) );\\n\\tvec3 coords[ 4 ];\\n\\tcoords[ 0 ] = mat * ( rectCoords[ 0 ] - P );\\n\\tcoords[ 1 ] = mat * ( rectCoords[ 1 ] - P );\\n\\tcoords[ 2 ] = mat * ( rectCoords[ 2 ] - P );\\n\\tcoords[ 3 ] = mat * ( rectCoords[ 3 ] - P );\\n\\tcoords[ 0 ] = normalize( coords[ 0 ] );\\n\\tcoords[ 1 ] = normalize( coords[ 1 ] );\\n\\tcoords[ 2 ] = normalize( coords[ 2 ] );\\n\\tcoords[ 3 ] = normalize( coords[ 3 ] );\\n\\tvec3 vectorFormFactor = vec3( 0.0 );\\n\\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] );\\n\\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] );\\n\\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] );\\n\\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] );\\n\\tfloat result = LTC_ClippedSphereFormFactor( vectorFormFactor );\\n\\treturn vec3( result );\\n}\\n#if defined( USE_SHEEN )\\nfloat D_Charlie( float roughness, float dotNH ) {\\n\\tfloat alpha = pow2( roughness );\\n\\tfloat invAlpha = 1.0 / alpha;\\n\\tfloat cos2h = dotNH * dotNH;\\n\\tfloat sin2h = max( 1.0 - cos2h, 0.0078125 );\\n\\treturn ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI );\\n}\\nfloat V_Neubelt( float dotNV, float dotNL ) {\\n\\treturn saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) );\\n}\\nvec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) {\\n\\tvec3 halfDir = normalize( lightDir + viewDir );\\n\\tfloat dotNL = saturate( dot( normal, lightDir ) );\\n\\tfloat dotNV = saturate( dot( normal, viewDir ) );\\n\\tfloat dotNH = saturate( dot( normal, halfDir ) );\\n\\tfloat D = D_Charlie( sheenRoughness, dotNH );\\n\\tfloat V = V_Neubelt( dotNV, dotNL );\\n\\treturn sheenColor * ( D * V );\\n}\\n#endif\\nfloat IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) {\\n\\tfloat dotNV = saturate( dot( normal, viewDir ) );\\n\\tfloat r2 = roughness * roughness;\\n\\tfloat rInv = 1.0 / ( roughness + 0.1 );\\n\\tfloat a = -1.9362 + 1.0678 * roughness + 0.4573 * r2 - 0.8469 * rInv;\\n\\tfloat b = -0.6014 + 0.5538 * roughness - 0.4670 * r2 - 0.1255 * rInv;\\n\\tfloat DG = exp( a * dotNV + b );\\n\\treturn saturate( DG );\\n}\\nvec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) {\\n\\tfloat dotNV = saturate( dot( normal, viewDir ) );\\n\\tvec2 fab = texture2D( dfgLUT, vec2( roughness, dotNV ) ).rg;\\n\\treturn specularColor * fab.x + specularF90 * fab.y;\\n}\\n#ifdef USE_IRIDESCENCE\\nvoid computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\\n#else\\nvoid computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\\n#endif\\n\\tfloat dotNV = saturate( dot( normal, viewDir ) );\\n\\tvec2 fab = texture2D( dfgLUT, vec2( roughness, dotNV ) ).rg;\\n\\t#ifdef USE_IRIDESCENCE\\n\\t\\tvec3 Fr = mix( specularColor, iridescenceF0, iridescence );\\n\\t#else\\n\\t\\tvec3 Fr = specularColor;\\n\\t#endif\\n\\tvec3 FssEss = Fr * fab.x + specularF90 * fab.y;\\n\\tfloat Ess = fab.x + fab.y;\\n\\tfloat Ems = 1.0 - Ess;\\n\\tvec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619;\\tvec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );\\n\\tsingleScatter += FssEss;\\n\\tmultiScatter += Fms * Ems;\\n}\\nvec3 BRDF_GGX_Multiscatter( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) {\\n\\tvec3 singleScatter = BRDF_GGX( lightDir, viewDir, normal, material );\\n\\tfloat dotNL = saturate( dot( normal, lightDir ) );\\n\\tfloat dotNV = saturate( dot( normal, viewDir ) );\\n\\tvec2 dfgV = texture2D( dfgLUT, vec2( material.roughness, dotNV ) ).rg;\\n\\tvec2 dfgL = texture2D( dfgLUT, vec2( material.roughness, dotNL ) ).rg;\\n\\tvec3 FssEss_V = material.specularColorBlended * dfgV.x + material.specularF90 * dfgV.y;\\n\\tvec3 FssEss_L = material.specularColorBlended * dfgL.x + material.specularF90 * dfgL.y;\\n\\tfloat Ess_V = dfgV.x + dfgV.y;\\n\\tfloat Ess_L = dfgL.x + dfgL.y;\\n\\tfloat Ems_V = 1.0 - Ess_V;\\n\\tfloat Ems_L = 1.0 - Ess_L;\\n\\tvec3 Favg = material.specularColorBlended + ( 1.0 - material.specularColorBlended ) * 0.047619;\\n\\tvec3 Fms = FssEss_V * FssEss_L * Favg / ( 1.0 - Ems_V * Ems_L * Favg + EPSILON );\\n\\tfloat compensationFactor = Ems_V * Ems_L;\\n\\tvec3 multiScatter = Fms * compensationFactor;\\n\\treturn singleScatter + multiScatter;\\n}\\n#if NUM_RECT_AREA_LIGHTS > 0\\n\\tvoid RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\\n\\t\\tvec3 normal = geometryNormal;\\n\\t\\tvec3 viewDir = geometryViewDir;\\n\\t\\tvec3 position = geometryPosition;\\n\\t\\tvec3 lightPos = rectAreaLight.position;\\n\\t\\tvec3 halfWidth = rectAreaLight.halfWidth;\\n\\t\\tvec3 halfHeight = rectAreaLight.halfHeight;\\n\\t\\tvec3 lightColor = rectAreaLight.color;\\n\\t\\tfloat roughness = material.roughness;\\n\\t\\tvec3 rectCoords[ 4 ];\\n\\t\\trectCoords[ 0 ] = lightPos + halfWidth - halfHeight;\\t\\trectCoords[ 1 ] = lightPos - halfWidth - halfHeight;\\n\\t\\trectCoords[ 2 ] = lightPos - halfWidth + halfHeight;\\n\\t\\trectCoords[ 3 ] = lightPos + halfWidth + halfHeight;\\n\\t\\tvec2 uv = LTC_Uv( normal, viewDir, roughness );\\n\\t\\tvec4 t1 = texture2D( ltc_1, uv );\\n\\t\\tvec4 t2 = texture2D( ltc_2, uv );\\n\\t\\tmat3 mInv = mat3(\\n\\t\\t\\tvec3( t1.x, 0, t1.y ),\\n\\t\\t\\tvec3(    0, 1,    0 ),\\n\\t\\t\\tvec3( t1.z, 0, t1.w )\\n\\t\\t);\\n\\t\\tvec3 fresnel = ( material.specularColorBlended * t2.x + ( vec3( 1.0 ) - material.specularColorBlended ) * t2.y );\\n\\t\\treflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords );\\n\\t\\treflectedLight.directDiffuse += lightColor * material.diffuseContribution * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords );\\n\\t}\\n#endif\\nvoid RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\\n\\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\\n\\tvec3 irradiance = dotNL * directLight.color;\\n\\t#ifdef USE_CLEARCOAT\\n\\t\\tfloat dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) );\\n\\t\\tvec3 ccIrradiance = dotNLcc * directLight.color;\\n\\t\\tclearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material );\\n\\t#endif\\n\\t#ifdef USE_SHEEN\\n \\n \\t\\tsheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness );\\n \\n \\t\\tfloat sheenAlbedoV = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );\\n \\t\\tfloat sheenAlbedoL = IBLSheenBRDF( geometryNormal, directLight.direction, material.sheenRoughness );\\n \\n \\t\\tfloat sheenEnergyComp = 1.0 - max3( material.sheenColor ) * max( sheenAlbedoV, sheenAlbedoL );\\n \\n \\t\\tirradiance *= sheenEnergyComp;\\n \\n \\t#endif\\n\\treflectedLight.directSpecular += irradiance * BRDF_GGX_Multiscatter( directLight.direction, geometryViewDir, geometryNormal, material );\\n\\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseContribution );\\n}\\nvoid RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\\n\\tvec3 diffuse = irradiance * BRDF_Lambert( material.diffuseContribution );\\n\\t#ifdef USE_SHEEN\\n\\t\\tfloat sheenAlbedo = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );\\n\\t\\tfloat sheenEnergyComp = 1.0 - max3( material.sheenColor ) * sheenAlbedo;\\n\\t\\tdiffuse *= sheenEnergyComp;\\n\\t#endif\\n\\treflectedLight.indirectDiffuse += diffuse;\\n}\\nvoid RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {\\n\\t#ifdef USE_CLEARCOAT\\n\\t\\tclearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness );\\n\\t#endif\\n\\t#ifdef USE_SHEEN\\n\\t\\tsheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ) * RECIPROCAL_PI;\\n \\t#endif\\n\\tvec3 singleScatteringDielectric = vec3( 0.0 );\\n\\tvec3 multiScatteringDielectric = vec3( 0.0 );\\n\\tvec3 singleScatteringMetallic = vec3( 0.0 );\\n\\tvec3 multiScatteringMetallic = vec3( 0.0 );\\n\\t#ifdef USE_IRIDESCENCE\\n\\t\\tcomputeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnelDielectric, material.roughness, singleScatteringDielectric, multiScatteringDielectric );\\n\\t\\tcomputeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.diffuseColor, material.specularF90, material.iridescence, material.iridescenceFresnelMetallic, material.roughness, singleScatteringMetallic, multiScatteringMetallic );\\n\\t#else\\n\\t\\tcomputeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScatteringDielectric, multiScatteringDielectric );\\n\\t\\tcomputeMultiscattering( geometryNormal, geometryViewDir, material.diffuseColor, material.specularF90, material.roughness, singleScatteringMetallic, multiScatteringMetallic );\\n\\t#endif\\n\\tvec3 singleScattering = mix( singleScatteringDielectric, singleScatteringMetallic, material.metalness );\\n\\tvec3 multiScattering = mix( multiScatteringDielectric, multiScatteringMetallic, material.metalness );\\n\\tvec3 totalScatteringDielectric = singleScatteringDielectric + multiScatteringDielectric;\\n\\tvec3 diffuse = material.diffuseContribution * ( 1.0 - totalScatteringDielectric );\\n\\tvec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;\\n\\tvec3 indirectSpecular = radiance * singleScattering;\\n\\tindirectSpecular += multiScattering * cosineWeightedIrradiance;\\n\\tvec3 indirectDiffuse = diffuse * cosineWeightedIrradiance;\\n\\t#ifdef USE_SHEEN\\n\\t\\tfloat sheenAlbedo = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );\\n\\t\\tfloat sheenEnergyComp = 1.0 - max3( material.sheenColor ) * sheenAlbedo;\\n\\t\\tindirectSpecular *= sheenEnergyComp;\\n\\t\\tindirectDiffuse *= sheenEnergyComp;\\n\\t#endif\\n\\treflectedLight.indirectSpecular += indirectSpecular;\\n\\treflectedLight.indirectDiffuse += indirectDiffuse;\\n}\\n#define RE_Direct\\t\\t\\t\\tRE_Direct_Physical\\n#define RE_Direct_RectArea\\t\\tRE_Direct_RectArea_Physical\\n#define RE_IndirectDiffuse\\t\\tRE_IndirectDiffuse_Physical\\n#define RE_IndirectSpecular\\t\\tRE_IndirectSpecular_Physical\\nfloat computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) {\\n\\treturn saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion );\\n}\";\n\nvar lights_fragment_begin = \"\\nvec3 geometryPosition = - vViewPosition;\\nvec3 geometryNormal = normal;\\nvec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );\\nvec3 geometryClearcoatNormal = vec3( 0.0 );\\n#ifdef USE_CLEARCOAT\\n\\tgeometryClearcoatNormal = clearcoatNormal;\\n#endif\\n#ifdef USE_IRIDESCENCE\\n\\tfloat dotNVi = saturate( dot( normal, geometryViewDir ) );\\n\\tif ( material.iridescenceThickness == 0.0 ) {\\n\\t\\tmaterial.iridescence = 0.0;\\n\\t} else {\\n\\t\\tmaterial.iridescence = saturate( material.iridescence );\\n\\t}\\n\\tif ( material.iridescence > 0.0 ) {\\n\\t\\tmaterial.iridescenceFresnelDielectric = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor );\\n\\t\\tmaterial.iridescenceFresnelMetallic = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.diffuseColor );\\n\\t\\tmaterial.iridescenceFresnel = mix( material.iridescenceFresnelDielectric, material.iridescenceFresnelMetallic, material.metalness );\\n\\t\\tmaterial.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi );\\n\\t}\\n#endif\\nIncidentLight directLight;\\n#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )\\n\\tPointLight pointLight;\\n\\t#if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0\\n\\tPointLightShadow pointLightShadow;\\n\\t#endif\\n\\t#pragma unroll_loop_start\\n\\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\\n\\t\\tpointLight = pointLights[ i ];\\n\\t\\tgetPointLightInfo( pointLight, geometryPosition, directLight );\\n\\t\\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) && ( defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_BASIC ) )\\n\\t\\tpointLightShadow = pointLightShadows[ i ];\\n\\t\\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0;\\n\\t\\t#endif\\n\\t\\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n\\t}\\n\\t#pragma unroll_loop_end\\n#endif\\n#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )\\n\\tSpotLight spotLight;\\n\\tvec4 spotColor;\\n\\tvec3 spotLightCoord;\\n\\tbool inSpotLightMap;\\n\\t#if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0\\n\\tSpotLightShadow spotLightShadow;\\n\\t#endif\\n\\t#pragma unroll_loop_start\\n\\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\\n\\t\\tspotLight = spotLights[ i ];\\n\\t\\tgetSpotLightInfo( spotLight, geometryPosition, directLight );\\n\\t\\t#if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\\n\\t\\t#define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX\\n\\t\\t#elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\\n\\t\\t#define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS\\n\\t\\t#else\\n\\t\\t#define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\\n\\t\\t#endif\\n\\t\\t#if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS )\\n\\t\\t\\tspotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w;\\n\\t\\t\\tinSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) );\\n\\t\\t\\tspotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy );\\n\\t\\t\\tdirectLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color;\\n\\t\\t#endif\\n\\t\\t#undef SPOT_LIGHT_MAP_INDEX\\n\\t\\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\\n\\t\\tspotLightShadow = spotLightShadows[ i ];\\n\\t\\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\\n\\t\\t#endif\\n\\t\\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n\\t}\\n\\t#pragma unroll_loop_end\\n#endif\\n#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )\\n\\tDirectionalLight directionalLight;\\n\\t#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0\\n\\tDirectionalLightShadow directionalLightShadow;\\n\\t#endif\\n\\t#pragma unroll_loop_start\\n\\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\\n\\t\\tdirectionalLight = directionalLights[ i ];\\n\\t\\tgetDirectionalLightInfo( directionalLight, directLight );\\n\\t\\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )\\n\\t\\tdirectionalLightShadow = directionalLightShadows[ i ];\\n\\t\\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\\n\\t\\t#endif\\n\\t\\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n\\t}\\n\\t#pragma unroll_loop_end\\n#endif\\n#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )\\n\\tRectAreaLight rectAreaLight;\\n\\t#pragma unroll_loop_start\\n\\tfor ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {\\n\\t\\trectAreaLight = rectAreaLights[ i ];\\n\\t\\tRE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n\\t}\\n\\t#pragma unroll_loop_end\\n#endif\\n#if defined( RE_IndirectDiffuse )\\n\\tvec3 iblIrradiance = vec3( 0.0 );\\n\\tvec3 irradiance = getAmbientLightIrradiance( ambientLightColor );\\n\\t#if defined( USE_LIGHT_PROBES )\\n\\t\\tirradiance += getLightProbeIrradiance( lightProbe, geometryNormal );\\n\\t#endif\\n\\t#if ( NUM_HEMI_LIGHTS > 0 )\\n\\t\\t#pragma unroll_loop_start\\n\\t\\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\\n\\t\\t\\tirradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal );\\n\\t\\t}\\n\\t\\t#pragma unroll_loop_end\\n\\t#endif\\n#endif\\n#if defined( RE_IndirectSpecular )\\n\\tvec3 radiance = vec3( 0.0 );\\n\\tvec3 clearcoatRadiance = vec3( 0.0 );\\n#endif\";\n\nvar lights_fragment_maps = \"#if defined( RE_IndirectDiffuse )\\n\\t#ifdef USE_LIGHTMAP\\n\\t\\tvec4 lightMapTexel = texture2D( lightMap, vLightMapUv );\\n\\t\\tvec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity;\\n\\t\\tirradiance += lightMapIrradiance;\\n\\t#endif\\n\\t#if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV )\\n\\t\\tiblIrradiance += getIBLIrradiance( geometryNormal );\\n\\t#endif\\n#endif\\n#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular )\\n\\t#ifdef USE_ANISOTROPY\\n\\t\\tradiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy );\\n\\t#else\\n\\t\\tradiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness );\\n\\t#endif\\n\\t#ifdef USE_CLEARCOAT\\n\\t\\tclearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness );\\n\\t#endif\\n#endif\";\n\nvar lights_fragment_end = \"#if defined( RE_IndirectDiffuse )\\n\\tRE_IndirectDiffuse( irradiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n#endif\\n#if defined( RE_IndirectSpecular )\\n\\tRE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n#endif\";\n\nvar logdepthbuf_fragment = \"#if defined( USE_LOGARITHMIC_DEPTH_BUFFER )\\n\\tgl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;\\n#endif\";\n\nvar logdepthbuf_pars_fragment = \"#if defined( USE_LOGARITHMIC_DEPTH_BUFFER )\\n\\tuniform float logDepthBufFC;\\n\\tvarying float vFragDepth;\\n\\tvarying float vIsPerspective;\\n#endif\";\n\nvar logdepthbuf_pars_vertex = \"#ifdef USE_LOGARITHMIC_DEPTH_BUFFER\\n\\tvarying float vFragDepth;\\n\\tvarying float vIsPerspective;\\n#endif\";\n\nvar logdepthbuf_vertex = \"#ifdef USE_LOGARITHMIC_DEPTH_BUFFER\\n\\tvFragDepth = 1.0 + gl_Position.w;\\n\\tvIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) );\\n#endif\";\n\nvar map_fragment = \"#ifdef USE_MAP\\n\\tvec4 sampledDiffuseColor = texture2D( map, vMapUv );\\n\\t#ifdef DECODE_VIDEO_TEXTURE\\n\\t\\tsampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor );\\n\\t#endif\\n\\tdiffuseColor *= sampledDiffuseColor;\\n#endif\";\n\nvar map_pars_fragment = \"#ifdef USE_MAP\\n\\tuniform sampler2D map;\\n#endif\";\n\nvar map_particle_fragment = \"#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\\n\\t#if defined( USE_POINTS_UV )\\n\\t\\tvec2 uv = vUv;\\n\\t#else\\n\\t\\tvec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy;\\n\\t#endif\\n#endif\\n#ifdef USE_MAP\\n\\tdiffuseColor *= texture2D( map, uv );\\n#endif\\n#ifdef USE_ALPHAMAP\\n\\tdiffuseColor.a *= texture2D( alphaMap, uv ).g;\\n#endif\";\n\nvar map_particle_pars_fragment = \"#if defined( USE_POINTS_UV )\\n\\tvarying vec2 vUv;\\n#else\\n\\t#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\\n\\t\\tuniform mat3 uvTransform;\\n\\t#endif\\n#endif\\n#ifdef USE_MAP\\n\\tuniform sampler2D map;\\n#endif\\n#ifdef USE_ALPHAMAP\\n\\tuniform sampler2D alphaMap;\\n#endif\";\n\nvar metalnessmap_fragment = \"float metalnessFactor = metalness;\\n#ifdef USE_METALNESSMAP\\n\\tvec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv );\\n\\tmetalnessFactor *= texelMetalness.b;\\n#endif\";\n\nvar metalnessmap_pars_fragment = \"#ifdef USE_METALNESSMAP\\n\\tuniform sampler2D metalnessMap;\\n#endif\";\n\nvar morphinstance_vertex = \"#ifdef USE_INSTANCING_MORPH\\n\\tfloat morphTargetInfluences[ MORPHTARGETS_COUNT ];\\n\\tfloat morphTargetBaseInfluence = texelFetch( morphTexture, ivec2( 0, gl_InstanceID ), 0 ).r;\\n\\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\\n\\t\\tmorphTargetInfluences[i] =  texelFetch( morphTexture, ivec2( i + 1, gl_InstanceID ), 0 ).r;\\n\\t}\\n#endif\";\n\nvar morphcolor_vertex = \"#if defined( USE_MORPHCOLORS )\\n\\tvColor *= morphTargetBaseInfluence;\\n\\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\\n\\t\\t#if defined( USE_COLOR_ALPHA )\\n\\t\\t\\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ];\\n\\t\\t#elif defined( USE_COLOR )\\n\\t\\t\\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ];\\n\\t\\t#endif\\n\\t}\\n#endif\";\n\nvar morphnormal_vertex = \"#ifdef USE_MORPHNORMALS\\n\\tobjectNormal *= morphTargetBaseInfluence;\\n\\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\\n\\t\\tif ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ];\\n\\t}\\n#endif\";\n\nvar morphtarget_pars_vertex = \"#ifdef USE_MORPHTARGETS\\n\\t#ifndef USE_INSTANCING_MORPH\\n\\t\\tuniform float morphTargetBaseInfluence;\\n\\t\\tuniform float morphTargetInfluences[ MORPHTARGETS_COUNT ];\\n\\t#endif\\n\\tuniform sampler2DArray morphTargetsTexture;\\n\\tuniform ivec2 morphTargetsTextureSize;\\n\\tvec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) {\\n\\t\\tint texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset;\\n\\t\\tint y = texelIndex / morphTargetsTextureSize.x;\\n\\t\\tint x = texelIndex - y * morphTargetsTextureSize.x;\\n\\t\\tivec3 morphUV = ivec3( x, y, morphTargetIndex );\\n\\t\\treturn texelFetch( morphTargetsTexture, morphUV, 0 );\\n\\t}\\n#endif\";\n\nvar morphtarget_vertex = \"#ifdef USE_MORPHTARGETS\\n\\ttransformed *= morphTargetBaseInfluence;\\n\\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\\n\\t\\tif ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ];\\n\\t}\\n#endif\";\n\nvar normal_fragment_begin = \"float faceDirection = gl_FrontFacing ? 1.0 : - 1.0;\\n#ifdef FLAT_SHADED\\n\\tvec3 fdx = dFdx( vViewPosition );\\n\\tvec3 fdy = dFdy( vViewPosition );\\n\\tvec3 normal = normalize( cross( fdx, fdy ) );\\n#else\\n\\tvec3 normal = normalize( vNormal );\\n\\t#ifdef DOUBLE_SIDED\\n\\t\\tnormal *= faceDirection;\\n\\t#endif\\n#endif\\n#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY )\\n\\t#ifdef USE_TANGENT\\n\\t\\tmat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal );\\n\\t#else\\n\\t\\tmat3 tbn = getTangentFrame( - vViewPosition, normal,\\n\\t\\t#if defined( USE_NORMALMAP )\\n\\t\\t\\tvNormalMapUv\\n\\t\\t#elif defined( USE_CLEARCOAT_NORMALMAP )\\n\\t\\t\\tvClearcoatNormalMapUv\\n\\t\\t#else\\n\\t\\t\\tvUv\\n\\t\\t#endif\\n\\t\\t);\\n\\t#endif\\n\\t#if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED )\\n\\t\\ttbn[0] *= faceDirection;\\n\\t\\ttbn[1] *= faceDirection;\\n\\t#endif\\n#endif\\n#ifdef USE_CLEARCOAT_NORMALMAP\\n\\t#ifdef USE_TANGENT\\n\\t\\tmat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal );\\n\\t#else\\n\\t\\tmat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv );\\n\\t#endif\\n\\t#if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED )\\n\\t\\ttbn2[0] *= faceDirection;\\n\\t\\ttbn2[1] *= faceDirection;\\n\\t#endif\\n#endif\\nvec3 nonPerturbedNormal = normal;\";\n\nvar normal_fragment_maps = \"#ifdef USE_NORMALMAP_OBJECTSPACE\\n\\tnormal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;\\n\\t#ifdef FLIP_SIDED\\n\\t\\tnormal = - normal;\\n\\t#endif\\n\\t#ifdef DOUBLE_SIDED\\n\\t\\tnormal = normal * faceDirection;\\n\\t#endif\\n\\tnormal = normalize( normalMatrix * normal );\\n#elif defined( USE_NORMALMAP_TANGENTSPACE )\\n\\tvec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;\\n\\tmapN.xy *= normalScale;\\n\\tnormal = normalize( tbn * mapN );\\n#elif defined( USE_BUMPMAP )\\n\\tnormal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection );\\n#endif\";\n\nvar normal_pars_fragment = \"#ifndef FLAT_SHADED\\n\\tvarying vec3 vNormal;\\n\\t#ifdef USE_TANGENT\\n\\t\\tvarying vec3 vTangent;\\n\\t\\tvarying vec3 vBitangent;\\n\\t#endif\\n#endif\";\n\nvar normal_pars_vertex = \"#ifndef FLAT_SHADED\\n\\tvarying vec3 vNormal;\\n\\t#ifdef USE_TANGENT\\n\\t\\tvarying vec3 vTangent;\\n\\t\\tvarying vec3 vBitangent;\\n\\t#endif\\n#endif\";\n\nvar normal_vertex = \"#ifndef FLAT_SHADED\\n\\tvNormal = normalize( transformedNormal );\\n\\t#ifdef USE_TANGENT\\n\\t\\tvTangent = normalize( transformedTangent );\\n\\t\\tvBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );\\n\\t#endif\\n#endif\";\n\nvar normalmap_pars_fragment = \"#ifdef USE_NORMALMAP\\n\\tuniform sampler2D normalMap;\\n\\tuniform vec2 normalScale;\\n#endif\\n#ifdef USE_NORMALMAP_OBJECTSPACE\\n\\tuniform mat3 normalMatrix;\\n#endif\\n#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) )\\n\\tmat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {\\n\\t\\tvec3 q0 = dFdx( eye_pos.xyz );\\n\\t\\tvec3 q1 = dFdy( eye_pos.xyz );\\n\\t\\tvec2 st0 = dFdx( uv.st );\\n\\t\\tvec2 st1 = dFdy( uv.st );\\n\\t\\tvec3 N = surf_norm;\\n\\t\\tvec3 q1perp = cross( q1, N );\\n\\t\\tvec3 q0perp = cross( N, q0 );\\n\\t\\tvec3 T = q1perp * st0.x + q0perp * st1.x;\\n\\t\\tvec3 B = q1perp * st0.y + q0perp * st1.y;\\n\\t\\tfloat det = max( dot( T, T ), dot( B, B ) );\\n\\t\\tfloat scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det );\\n\\t\\treturn mat3( T * scale, B * scale, N );\\n\\t}\\n#endif\";\n\nvar clearcoat_normal_fragment_begin = \"#ifdef USE_CLEARCOAT\\n\\tvec3 clearcoatNormal = nonPerturbedNormal;\\n#endif\";\n\nvar clearcoat_normal_fragment_maps = \"#ifdef USE_CLEARCOAT_NORMALMAP\\n\\tvec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0;\\n\\tclearcoatMapN.xy *= clearcoatNormalScale;\\n\\tclearcoatNormal = normalize( tbn2 * clearcoatMapN );\\n#endif\";\n\nvar clearcoat_pars_fragment = \"#ifdef USE_CLEARCOATMAP\\n\\tuniform sampler2D clearcoatMap;\\n#endif\\n#ifdef USE_CLEARCOAT_NORMALMAP\\n\\tuniform sampler2D clearcoatNormalMap;\\n\\tuniform vec2 clearcoatNormalScale;\\n#endif\\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\\n\\tuniform sampler2D clearcoatRoughnessMap;\\n#endif\";\n\nvar iridescence_pars_fragment = \"#ifdef USE_IRIDESCENCEMAP\\n\\tuniform sampler2D iridescenceMap;\\n#endif\\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\\n\\tuniform sampler2D iridescenceThicknessMap;\\n#endif\";\n\nvar opaque_fragment = \"#ifdef OPAQUE\\ndiffuseColor.a = 1.0;\\n#endif\\n#ifdef USE_TRANSMISSION\\ndiffuseColor.a *= material.transmissionAlpha;\\n#endif\\ngl_FragColor = vec4( outgoingLight, diffuseColor.a );\";\n\nvar packing = \"vec3 packNormalToRGB( const in vec3 normal ) {\\n\\treturn normalize( normal ) * 0.5 + 0.5;\\n}\\nvec3 unpackRGBToNormal( const in vec3 rgb ) {\\n\\treturn 2.0 * rgb.xyz - 1.0;\\n}\\nconst float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;const float ShiftRight8 = 1. / 256.;\\nconst float Inv255 = 1. / 255.;\\nconst vec4 PackFactors = vec4( 1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0 );\\nconst vec2 UnpackFactors2 = vec2( UnpackDownscale, 1.0 / PackFactors.g );\\nconst vec3 UnpackFactors3 = vec3( UnpackDownscale / PackFactors.rg, 1.0 / PackFactors.b );\\nconst vec4 UnpackFactors4 = vec4( UnpackDownscale / PackFactors.rgb, 1.0 / PackFactors.a );\\nvec4 packDepthToRGBA( const in float v ) {\\n\\tif( v <= 0.0 )\\n\\t\\treturn vec4( 0., 0., 0., 0. );\\n\\tif( v >= 1.0 )\\n\\t\\treturn vec4( 1., 1., 1., 1. );\\n\\tfloat vuf;\\n\\tfloat af = modf( v * PackFactors.a, vuf );\\n\\tfloat bf = modf( vuf * ShiftRight8, vuf );\\n\\tfloat gf = modf( vuf * ShiftRight8, vuf );\\n\\treturn vec4( vuf * Inv255, gf * PackUpscale, bf * PackUpscale, af );\\n}\\nvec3 packDepthToRGB( const in float v ) {\\n\\tif( v <= 0.0 )\\n\\t\\treturn vec3( 0., 0., 0. );\\n\\tif( v >= 1.0 )\\n\\t\\treturn vec3( 1., 1., 1. );\\n\\tfloat vuf;\\n\\tfloat bf = modf( v * PackFactors.b, vuf );\\n\\tfloat gf = modf( vuf * ShiftRight8, vuf );\\n\\treturn vec3( vuf * Inv255, gf * PackUpscale, bf );\\n}\\nvec2 packDepthToRG( const in float v ) {\\n\\tif( v <= 0.0 )\\n\\t\\treturn vec2( 0., 0. );\\n\\tif( v >= 1.0 )\\n\\t\\treturn vec2( 1., 1. );\\n\\tfloat vuf;\\n\\tfloat gf = modf( v * 256., vuf );\\n\\treturn vec2( vuf * Inv255, gf );\\n}\\nfloat unpackRGBAToDepth( const in vec4 v ) {\\n\\treturn dot( v, UnpackFactors4 );\\n}\\nfloat unpackRGBToDepth( const in vec3 v ) {\\n\\treturn dot( v, UnpackFactors3 );\\n}\\nfloat unpackRGToDepth( const in vec2 v ) {\\n\\treturn v.r * UnpackFactors2.r + v.g * UnpackFactors2.g;\\n}\\nvec4 pack2HalfToRGBA( const in vec2 v ) {\\n\\tvec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) );\\n\\treturn vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w );\\n}\\nvec2 unpackRGBATo2Half( const in vec4 v ) {\\n\\treturn vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) );\\n}\\nfloat viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) {\\n\\treturn ( viewZ + near ) / ( near - far );\\n}\\nfloat orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) {\\n\\treturn depth * ( near - far ) - near;\\n}\\nfloat viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) {\\n\\treturn ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ );\\n}\\nfloat perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) {\\n\\treturn ( near * far ) / ( ( far - near ) * depth - far );\\n}\";\n\nvar premultiplied_alpha_fragment = \"#ifdef PREMULTIPLIED_ALPHA\\n\\tgl_FragColor.rgb *= gl_FragColor.a;\\n#endif\";\n\nvar project_vertex = \"vec4 mvPosition = vec4( transformed, 1.0 );\\n#ifdef USE_BATCHING\\n\\tmvPosition = batchingMatrix * mvPosition;\\n#endif\\n#ifdef USE_INSTANCING\\n\\tmvPosition = instanceMatrix * mvPosition;\\n#endif\\nmvPosition = modelViewMatrix * mvPosition;\\ngl_Position = projectionMatrix * mvPosition;\";\n\nvar dithering_fragment = \"#ifdef DITHERING\\n\\tgl_FragColor.rgb = dithering( gl_FragColor.rgb );\\n#endif\";\n\nvar dithering_pars_fragment = \"#ifdef DITHERING\\n\\tvec3 dithering( vec3 color ) {\\n\\t\\tfloat grid_position = rand( gl_FragCoord.xy );\\n\\t\\tvec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 );\\n\\t\\tdither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position );\\n\\t\\treturn color + dither_shift_RGB;\\n\\t}\\n#endif\";\n\nvar roughnessmap_fragment = \"float roughnessFactor = roughness;\\n#ifdef USE_ROUGHNESSMAP\\n\\tvec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv );\\n\\troughnessFactor *= texelRoughness.g;\\n#endif\";\n\nvar roughnessmap_pars_fragment = \"#ifdef USE_ROUGHNESSMAP\\n\\tuniform sampler2D roughnessMap;\\n#endif\";\n\nvar shadowmap_pars_fragment = \"#if NUM_SPOT_LIGHT_COORDS > 0\\n\\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\\n#endif\\n#if NUM_SPOT_LIGHT_MAPS > 0\\n\\tuniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ];\\n#endif\\n#ifdef USE_SHADOWMAP\\n\\t#if NUM_DIR_LIGHT_SHADOWS > 0\\n\\t\\t#if defined( SHADOWMAP_TYPE_PCF )\\n\\t\\t\\tuniform sampler2DShadow directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\\n\\t\\t#else\\n\\t\\t\\tuniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\\n\\t\\t#endif\\n\\t\\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\\n\\t\\tstruct DirectionalLightShadow {\\n\\t\\t\\tfloat shadowIntensity;\\n\\t\\t\\tfloat shadowBias;\\n\\t\\t\\tfloat shadowNormalBias;\\n\\t\\t\\tfloat shadowRadius;\\n\\t\\t\\tvec2 shadowMapSize;\\n\\t\\t};\\n\\t\\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\\n\\t#endif\\n\\t#if NUM_SPOT_LIGHT_SHADOWS > 0\\n\\t\\t#if defined( SHADOWMAP_TYPE_PCF )\\n\\t\\t\\tuniform sampler2DShadow spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\\n\\t\\t#else\\n\\t\\t\\tuniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\\n\\t\\t#endif\\n\\t\\tstruct SpotLightShadow {\\n\\t\\t\\tfloat shadowIntensity;\\n\\t\\t\\tfloat shadowBias;\\n\\t\\t\\tfloat shadowNormalBias;\\n\\t\\t\\tfloat shadowRadius;\\n\\t\\t\\tvec2 shadowMapSize;\\n\\t\\t};\\n\\t\\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\\n\\t#endif\\n\\t#if NUM_POINT_LIGHT_SHADOWS > 0\\n\\t\\t#if defined( SHADOWMAP_TYPE_PCF )\\n\\t\\t\\tuniform samplerCubeShadow pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\\n\\t\\t#elif defined( SHADOWMAP_TYPE_BASIC )\\n\\t\\t\\tuniform samplerCube pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\\n\\t\\t#endif\\n\\t\\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\\n\\t\\tstruct PointLightShadow {\\n\\t\\t\\tfloat shadowIntensity;\\n\\t\\t\\tfloat shadowBias;\\n\\t\\t\\tfloat shadowNormalBias;\\n\\t\\t\\tfloat shadowRadius;\\n\\t\\t\\tvec2 shadowMapSize;\\n\\t\\t\\tfloat shadowCameraNear;\\n\\t\\t\\tfloat shadowCameraFar;\\n\\t\\t};\\n\\t\\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\\n\\t#endif\\n\\t#if defined( SHADOWMAP_TYPE_PCF )\\n\\t\\tfloat interleavedGradientNoise( vec2 position ) {\\n\\t\\t\\treturn fract( 52.9829189 * fract( dot( position, vec2( 0.06711056, 0.00583715 ) ) ) );\\n\\t\\t}\\n\\t\\tvec2 vogelDiskSample( int sampleIndex, int samplesCount, float phi ) {\\n\\t\\t\\tconst float goldenAngle = 2.399963229728653;\\n\\t\\t\\tfloat r = sqrt( ( float( sampleIndex ) + 0.5 ) / float( samplesCount ) );\\n\\t\\t\\tfloat theta = float( sampleIndex ) * goldenAngle + phi;\\n\\t\\t\\treturn vec2( cos( theta ), sin( theta ) ) * r;\\n\\t\\t}\\n\\t#endif\\n\\t#if defined( SHADOWMAP_TYPE_PCF )\\n\\t\\tfloat getShadow( sampler2DShadow shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\\n\\t\\t\\tfloat shadow = 1.0;\\n\\t\\t\\tshadowCoord.xyz /= shadowCoord.w;\\n\\t\\t\\tshadowCoord.z += shadowBias;\\n\\t\\t\\tbool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;\\n\\t\\t\\tbool frustumTest = inFrustum && shadowCoord.z <= 1.0;\\n\\t\\t\\tif ( frustumTest ) {\\n\\t\\t\\t\\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\\n\\t\\t\\t\\tfloat radius = shadowRadius * texelSize.x;\\n\\t\\t\\t\\tfloat phi = interleavedGradientNoise( gl_FragCoord.xy ) * 6.28318530718;\\n\\t\\t\\t\\tshadow = (\\n\\t\\t\\t\\t\\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 0, 5, phi ) * radius, shadowCoord.z ) ) +\\n\\t\\t\\t\\t\\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 1, 5, phi ) * radius, shadowCoord.z ) ) +\\n\\t\\t\\t\\t\\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 2, 5, phi ) * radius, shadowCoord.z ) ) +\\n\\t\\t\\t\\t\\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 3, 5, phi ) * radius, shadowCoord.z ) ) +\\n\\t\\t\\t\\t\\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 4, 5, phi ) * radius, shadowCoord.z ) )\\n\\t\\t\\t\\t) * 0.2;\\n\\t\\t\\t}\\n\\t\\t\\treturn mix( 1.0, shadow, shadowIntensity );\\n\\t\\t}\\n\\t#elif defined( SHADOWMAP_TYPE_VSM )\\n\\t\\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\\n\\t\\t\\tfloat shadow = 1.0;\\n\\t\\t\\tshadowCoord.xyz /= shadowCoord.w;\\n\\t\\t\\tshadowCoord.z += shadowBias;\\n\\t\\t\\tbool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;\\n\\t\\t\\tbool frustumTest = inFrustum && shadowCoord.z <= 1.0;\\n\\t\\t\\tif ( frustumTest ) {\\n\\t\\t\\t\\tvec2 distribution = texture2D( shadowMap, shadowCoord.xy ).rg;\\n\\t\\t\\t\\tfloat mean = distribution.x;\\n\\t\\t\\t\\tfloat variance = distribution.y * distribution.y;\\n\\t\\t\\t\\t#ifdef USE_REVERSED_DEPTH_BUFFER\\n\\t\\t\\t\\t\\tfloat hard_shadow = step( mean, shadowCoord.z );\\n\\t\\t\\t\\t#else\\n\\t\\t\\t\\t\\tfloat hard_shadow = step( shadowCoord.z, mean );\\n\\t\\t\\t\\t#endif\\n\\t\\t\\t\\tif ( hard_shadow == 1.0 ) {\\n\\t\\t\\t\\t\\tshadow = 1.0;\\n\\t\\t\\t\\t} else {\\n\\t\\t\\t\\t\\tvariance = max( variance, 0.0000001 );\\n\\t\\t\\t\\t\\tfloat d = shadowCoord.z - mean;\\n\\t\\t\\t\\t\\tfloat p_max = variance / ( variance + d * d );\\n\\t\\t\\t\\t\\tp_max = clamp( ( p_max - 0.3 ) / 0.65, 0.0, 1.0 );\\n\\t\\t\\t\\t\\tshadow = max( hard_shadow, p_max );\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\t\\t\\treturn mix( 1.0, shadow, shadowIntensity );\\n\\t\\t}\\n\\t#else\\n\\t\\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\\n\\t\\t\\tfloat shadow = 1.0;\\n\\t\\t\\tshadowCoord.xyz /= shadowCoord.w;\\n\\t\\t\\tshadowCoord.z += shadowBias;\\n\\t\\t\\tbool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;\\n\\t\\t\\tbool frustumTest = inFrustum && shadowCoord.z <= 1.0;\\n\\t\\t\\tif ( frustumTest ) {\\n\\t\\t\\t\\tfloat depth = texture2D( shadowMap, shadowCoord.xy ).r;\\n\\t\\t\\t\\t#ifdef USE_REVERSED_DEPTH_BUFFER\\n\\t\\t\\t\\t\\tshadow = step( depth, shadowCoord.z );\\n\\t\\t\\t\\t#else\\n\\t\\t\\t\\t\\tshadow = step( shadowCoord.z, depth );\\n\\t\\t\\t\\t#endif\\n\\t\\t\\t}\\n\\t\\t\\treturn mix( 1.0, shadow, shadowIntensity );\\n\\t\\t}\\n\\t#endif\\n\\t#if NUM_POINT_LIGHT_SHADOWS > 0\\n\\t#if defined( SHADOWMAP_TYPE_PCF )\\n\\tfloat getPointShadow( samplerCubeShadow shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\\n\\t\\tfloat shadow = 1.0;\\n\\t\\tvec3 lightToPosition = shadowCoord.xyz;\\n\\t\\tvec3 bd3D = normalize( lightToPosition );\\n\\t\\tvec3 absVec = abs( lightToPosition );\\n\\t\\tfloat viewSpaceZ = max( max( absVec.x, absVec.y ), absVec.z );\\n\\t\\tif ( viewSpaceZ - shadowCameraFar <= 0.0 && viewSpaceZ - shadowCameraNear >= 0.0 ) {\\n\\t\\t\\tfloat dp = ( shadowCameraFar * ( viewSpaceZ - shadowCameraNear ) ) / ( viewSpaceZ * ( shadowCameraFar - shadowCameraNear ) );\\n\\t\\t\\tdp += shadowBias;\\n\\t\\t\\tfloat texelSize = shadowRadius / shadowMapSize.x;\\n\\t\\t\\tvec3 absDir = abs( bd3D );\\n\\t\\t\\tvec3 tangent = absDir.x > absDir.z ? vec3( 0.0, 1.0, 0.0 ) : vec3( 1.0, 0.0, 0.0 );\\n\\t\\t\\ttangent = normalize( cross( bd3D, tangent ) );\\n\\t\\t\\tvec3 bitangent = cross( bd3D, tangent );\\n\\t\\t\\tfloat phi = interleavedGradientNoise( gl_FragCoord.xy ) * 6.28318530718;\\n\\t\\t\\tshadow = (\\n\\t\\t\\t\\ttexture( shadowMap, vec4( bd3D + ( tangent * vogelDiskSample( 0, 5, phi ).x + bitangent * vogelDiskSample( 0, 5, phi ).y ) * texelSize, dp ) ) +\\n\\t\\t\\t\\ttexture( shadowMap, vec4( bd3D + ( tangent * vogelDiskSample( 1, 5, phi ).x + bitangent * vogelDiskSample( 1, 5, phi ).y ) * texelSize, dp ) ) +\\n\\t\\t\\t\\ttexture( shadowMap, vec4( bd3D + ( tangent * vogelDiskSample( 2, 5, phi ).x + bitangent * vogelDiskSample( 2, 5, phi ).y ) * texelSize, dp ) ) +\\n\\t\\t\\t\\ttexture( shadowMap, vec4( bd3D + ( tangent * vogelDiskSample( 3, 5, phi ).x + bitangent * vogelDiskSample( 3, 5, phi ).y ) * texelSize, dp ) ) +\\n\\t\\t\\t\\ttexture( shadowMap, vec4( bd3D + ( tangent * vogelDiskSample( 4, 5, phi ).x + bitangent * vogelDiskSample( 4, 5, phi ).y ) * texelSize, dp ) )\\n\\t\\t\\t) * 0.2;\\n\\t\\t}\\n\\t\\treturn mix( 1.0, shadow, shadowIntensity );\\n\\t}\\n\\t#elif defined( SHADOWMAP_TYPE_BASIC )\\n\\tfloat getPointShadow( samplerCube shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\\n\\t\\tfloat shadow = 1.0;\\n\\t\\tvec3 lightToPosition = shadowCoord.xyz;\\n\\t\\tvec3 bd3D = normalize( lightToPosition );\\n\\t\\tvec3 absVec = abs( lightToPosition );\\n\\t\\tfloat viewSpaceZ = max( max( absVec.x, absVec.y ), absVec.z );\\n\\t\\tif ( viewSpaceZ - shadowCameraFar <= 0.0 && viewSpaceZ - shadowCameraNear >= 0.0 ) {\\n\\t\\t\\tfloat dp = ( shadowCameraFar * ( viewSpaceZ - shadowCameraNear ) ) / ( viewSpaceZ * ( shadowCameraFar - shadowCameraNear ) );\\n\\t\\t\\tdp += shadowBias;\\n\\t\\t\\tfloat depth = textureCube( shadowMap, bd3D ).r;\\n\\t\\t\\t#ifdef USE_REVERSED_DEPTH_BUFFER\\n\\t\\t\\t\\tshadow = step( depth, dp );\\n\\t\\t\\t#else\\n\\t\\t\\t\\tshadow = step( dp, depth );\\n\\t\\t\\t#endif\\n\\t\\t}\\n\\t\\treturn mix( 1.0, shadow, shadowIntensity );\\n\\t}\\n\\t#endif\\n\\t#endif\\n#endif\";\n\nvar shadowmap_pars_vertex = \"#if NUM_SPOT_LIGHT_COORDS > 0\\n\\tuniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ];\\n\\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\\n#endif\\n#ifdef USE_SHADOWMAP\\n\\t#if NUM_DIR_LIGHT_SHADOWS > 0\\n\\t\\tuniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ];\\n\\t\\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\\n\\t\\tstruct DirectionalLightShadow {\\n\\t\\t\\tfloat shadowIntensity;\\n\\t\\t\\tfloat shadowBias;\\n\\t\\t\\tfloat shadowNormalBias;\\n\\t\\t\\tfloat shadowRadius;\\n\\t\\t\\tvec2 shadowMapSize;\\n\\t\\t};\\n\\t\\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\\n\\t#endif\\n\\t#if NUM_SPOT_LIGHT_SHADOWS > 0\\n\\t\\tstruct SpotLightShadow {\\n\\t\\t\\tfloat shadowIntensity;\\n\\t\\t\\tfloat shadowBias;\\n\\t\\t\\tfloat shadowNormalBias;\\n\\t\\t\\tfloat shadowRadius;\\n\\t\\t\\tvec2 shadowMapSize;\\n\\t\\t};\\n\\t\\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\\n\\t#endif\\n\\t#if NUM_POINT_LIGHT_SHADOWS > 0\\n\\t\\tuniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ];\\n\\t\\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\\n\\t\\tstruct PointLightShadow {\\n\\t\\t\\tfloat shadowIntensity;\\n\\t\\t\\tfloat shadowBias;\\n\\t\\t\\tfloat shadowNormalBias;\\n\\t\\t\\tfloat shadowRadius;\\n\\t\\t\\tvec2 shadowMapSize;\\n\\t\\t\\tfloat shadowCameraNear;\\n\\t\\t\\tfloat shadowCameraFar;\\n\\t\\t};\\n\\t\\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\\n\\t#endif\\n#endif\";\n\nvar shadowmap_vertex = \"#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 )\\n\\tvec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\\n\\tvec4 shadowWorldPosition;\\n#endif\\n#if defined( USE_SHADOWMAP )\\n\\t#if NUM_DIR_LIGHT_SHADOWS > 0\\n\\t\\t#pragma unroll_loop_start\\n\\t\\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\\n\\t\\t\\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );\\n\\t\\t\\tvDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;\\n\\t\\t}\\n\\t\\t#pragma unroll_loop_end\\n\\t#endif\\n\\t#if NUM_POINT_LIGHT_SHADOWS > 0\\n\\t\\t#pragma unroll_loop_start\\n\\t\\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\\n\\t\\t\\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 );\\n\\t\\t\\tvPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition;\\n\\t\\t}\\n\\t\\t#pragma unroll_loop_end\\n\\t#endif\\n#endif\\n#if NUM_SPOT_LIGHT_COORDS > 0\\n\\t#pragma unroll_loop_start\\n\\tfor ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) {\\n\\t\\tshadowWorldPosition = worldPosition;\\n\\t\\t#if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\\n\\t\\t\\tshadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias;\\n\\t\\t#endif\\n\\t\\tvSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition;\\n\\t}\\n\\t#pragma unroll_loop_end\\n#endif\";\n\nvar shadowmask_pars_fragment = \"float getShadowMask() {\\n\\tfloat shadow = 1.0;\\n\\t#ifdef USE_SHADOWMAP\\n\\t#if NUM_DIR_LIGHT_SHADOWS > 0\\n\\tDirectionalLightShadow directionalLight;\\n\\t#pragma unroll_loop_start\\n\\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\\n\\t\\tdirectionalLight = directionalLightShadows[ i ];\\n\\t\\tshadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowIntensity, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\\n\\t}\\n\\t#pragma unroll_loop_end\\n\\t#endif\\n\\t#if NUM_SPOT_LIGHT_SHADOWS > 0\\n\\tSpotLightShadow spotLight;\\n\\t#pragma unroll_loop_start\\n\\tfor ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) {\\n\\t\\tspotLight = spotLightShadows[ i ];\\n\\t\\tshadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowIntensity, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\\n\\t}\\n\\t#pragma unroll_loop_end\\n\\t#endif\\n\\t#if NUM_POINT_LIGHT_SHADOWS > 0 && ( defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_BASIC ) )\\n\\tPointLightShadow pointLight;\\n\\t#pragma unroll_loop_start\\n\\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\\n\\t\\tpointLight = pointLightShadows[ i ];\\n\\t\\tshadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowIntensity, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0;\\n\\t}\\n\\t#pragma unroll_loop_end\\n\\t#endif\\n\\t#endif\\n\\treturn shadow;\\n}\";\n\nvar skinbase_vertex = \"#ifdef USE_SKINNING\\n\\tmat4 boneMatX = getBoneMatrix( skinIndex.x );\\n\\tmat4 boneMatY = getBoneMatrix( skinIndex.y );\\n\\tmat4 boneMatZ = getBoneMatrix( skinIndex.z );\\n\\tmat4 boneMatW = getBoneMatrix( skinIndex.w );\\n#endif\";\n\nvar skinning_pars_vertex = \"#ifdef USE_SKINNING\\n\\tuniform mat4 bindMatrix;\\n\\tuniform mat4 bindMatrixInverse;\\n\\tuniform highp sampler2D boneTexture;\\n\\tmat4 getBoneMatrix( const in float i ) {\\n\\t\\tint size = textureSize( boneTexture, 0 ).x;\\n\\t\\tint j = int( i ) * 4;\\n\\t\\tint x = j % size;\\n\\t\\tint y = j / size;\\n\\t\\tvec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 );\\n\\t\\tvec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 );\\n\\t\\tvec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 );\\n\\t\\tvec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 );\\n\\t\\treturn mat4( v1, v2, v3, v4 );\\n\\t}\\n#endif\";\n\nvar skinning_vertex = \"#ifdef USE_SKINNING\\n\\tvec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );\\n\\tvec4 skinned = vec4( 0.0 );\\n\\tskinned += boneMatX * skinVertex * skinWeight.x;\\n\\tskinned += boneMatY * skinVertex * skinWeight.y;\\n\\tskinned += boneMatZ * skinVertex * skinWeight.z;\\n\\tskinned += boneMatW * skinVertex * skinWeight.w;\\n\\ttransformed = ( bindMatrixInverse * skinned ).xyz;\\n#endif\";\n\nvar skinnormal_vertex = \"#ifdef USE_SKINNING\\n\\tmat4 skinMatrix = mat4( 0.0 );\\n\\tskinMatrix += skinWeight.x * boneMatX;\\n\\tskinMatrix += skinWeight.y * boneMatY;\\n\\tskinMatrix += skinWeight.z * boneMatZ;\\n\\tskinMatrix += skinWeight.w * boneMatW;\\n\\tskinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;\\n\\tobjectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz;\\n\\t#ifdef USE_TANGENT\\n\\t\\tobjectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz;\\n\\t#endif\\n#endif\";\n\nvar specularmap_fragment = \"float specularStrength;\\n#ifdef USE_SPECULARMAP\\n\\tvec4 texelSpecular = texture2D( specularMap, vSpecularMapUv );\\n\\tspecularStrength = texelSpecular.r;\\n#else\\n\\tspecularStrength = 1.0;\\n#endif\";\n\nvar specularmap_pars_fragment = \"#ifdef USE_SPECULARMAP\\n\\tuniform sampler2D specularMap;\\n#endif\";\n\nvar tonemapping_fragment = \"#if defined( TONE_MAPPING )\\n\\tgl_FragColor.rgb = toneMapping( gl_FragColor.rgb );\\n#endif\";\n\nvar tonemapping_pars_fragment = \"#ifndef saturate\\n#define saturate( a ) clamp( a, 0.0, 1.0 )\\n#endif\\nuniform float toneMappingExposure;\\nvec3 LinearToneMapping( vec3 color ) {\\n\\treturn saturate( toneMappingExposure * color );\\n}\\nvec3 ReinhardToneMapping( vec3 color ) {\\n\\tcolor *= toneMappingExposure;\\n\\treturn saturate( color / ( vec3( 1.0 ) + color ) );\\n}\\nvec3 CineonToneMapping( vec3 color ) {\\n\\tcolor *= toneMappingExposure;\\n\\tcolor = max( vec3( 0.0 ), color - 0.004 );\\n\\treturn pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );\\n}\\nvec3 RRTAndODTFit( vec3 v ) {\\n\\tvec3 a = v * ( v + 0.0245786 ) - 0.000090537;\\n\\tvec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;\\n\\treturn a / b;\\n}\\nvec3 ACESFilmicToneMapping( vec3 color ) {\\n\\tconst mat3 ACESInputMat = mat3(\\n\\t\\tvec3( 0.59719, 0.07600, 0.02840 ),\\t\\tvec3( 0.35458, 0.90834, 0.13383 ),\\n\\t\\tvec3( 0.04823, 0.01566, 0.83777 )\\n\\t);\\n\\tconst mat3 ACESOutputMat = mat3(\\n\\t\\tvec3(  1.60475, -0.10208, -0.00327 ),\\t\\tvec3( -0.53108,  1.10813, -0.07276 ),\\n\\t\\tvec3( -0.07367, -0.00605,  1.07602 )\\n\\t);\\n\\tcolor *= toneMappingExposure / 0.6;\\n\\tcolor = ACESInputMat * color;\\n\\tcolor = RRTAndODTFit( color );\\n\\tcolor = ACESOutputMat * color;\\n\\treturn saturate( color );\\n}\\nconst mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(\\n\\tvec3( 1.6605, - 0.1246, - 0.0182 ),\\n\\tvec3( - 0.5876, 1.1329, - 0.1006 ),\\n\\tvec3( - 0.0728, - 0.0083, 1.1187 )\\n);\\nconst mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(\\n\\tvec3( 0.6274, 0.0691, 0.0164 ),\\n\\tvec3( 0.3293, 0.9195, 0.0880 ),\\n\\tvec3( 0.0433, 0.0113, 0.8956 )\\n);\\nvec3 agxDefaultContrastApprox( vec3 x ) {\\n\\tvec3 x2 = x * x;\\n\\tvec3 x4 = x2 * x2;\\n\\treturn + 15.5 * x4 * x2\\n\\t\\t- 40.14 * x4 * x\\n\\t\\t+ 31.96 * x4\\n\\t\\t- 6.868 * x2 * x\\n\\t\\t+ 0.4298 * x2\\n\\t\\t+ 0.1191 * x\\n\\t\\t- 0.00232;\\n}\\nvec3 AgXToneMapping( vec3 color ) {\\n\\tconst mat3 AgXInsetMatrix = mat3(\\n\\t\\tvec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ),\\n\\t\\tvec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ),\\n\\t\\tvec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 )\\n\\t);\\n\\tconst mat3 AgXOutsetMatrix = mat3(\\n\\t\\tvec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ),\\n\\t\\tvec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ),\\n\\t\\tvec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 )\\n\\t);\\n\\tconst float AgxMinEv = - 12.47393;\\tconst float AgxMaxEv = 4.026069;\\n\\tcolor *= toneMappingExposure;\\n\\tcolor = LINEAR_SRGB_TO_LINEAR_REC2020 * color;\\n\\tcolor = AgXInsetMatrix * color;\\n\\tcolor = max( color, 1e-10 );\\tcolor = log2( color );\\n\\tcolor = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv );\\n\\tcolor = clamp( color, 0.0, 1.0 );\\n\\tcolor = agxDefaultContrastApprox( color );\\n\\tcolor = AgXOutsetMatrix * color;\\n\\tcolor = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) );\\n\\tcolor = LINEAR_REC2020_TO_LINEAR_SRGB * color;\\n\\tcolor = clamp( color, 0.0, 1.0 );\\n\\treturn color;\\n}\\nvec3 NeutralToneMapping( vec3 color ) {\\n\\tconst float StartCompression = 0.8 - 0.04;\\n\\tconst float Desaturation = 0.15;\\n\\tcolor *= toneMappingExposure;\\n\\tfloat x = min( color.r, min( color.g, color.b ) );\\n\\tfloat offset = x < 0.08 ? x - 6.25 * x * x : 0.04;\\n\\tcolor -= offset;\\n\\tfloat peak = max( color.r, max( color.g, color.b ) );\\n\\tif ( peak < StartCompression ) return color;\\n\\tfloat d = 1. - StartCompression;\\n\\tfloat newPeak = 1. - d * d / ( peak + d - StartCompression );\\n\\tcolor *= newPeak / peak;\\n\\tfloat g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. );\\n\\treturn mix( color, vec3( newPeak ), g );\\n}\\nvec3 CustomToneMapping( vec3 color ) { return color; }\";\n\nvar transmission_fragment = \"#ifdef USE_TRANSMISSION\\n\\tmaterial.transmission = transmission;\\n\\tmaterial.transmissionAlpha = 1.0;\\n\\tmaterial.thickness = thickness;\\n\\tmaterial.attenuationDistance = attenuationDistance;\\n\\tmaterial.attenuationColor = attenuationColor;\\n\\t#ifdef USE_TRANSMISSIONMAP\\n\\t\\tmaterial.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r;\\n\\t#endif\\n\\t#ifdef USE_THICKNESSMAP\\n\\t\\tmaterial.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g;\\n\\t#endif\\n\\tvec3 pos = vWorldPosition;\\n\\tvec3 v = normalize( cameraPosition - pos );\\n\\tvec3 n = inverseTransformDirection( normal, viewMatrix );\\n\\tvec4 transmitted = getIBLVolumeRefraction(\\n\\t\\tn, v, material.roughness, material.diffuseContribution, material.specularColorBlended, material.specularF90,\\n\\t\\tpos, modelMatrix, viewMatrix, projectionMatrix, material.dispersion, material.ior, material.thickness,\\n\\t\\tmaterial.attenuationColor, material.attenuationDistance );\\n\\tmaterial.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission );\\n\\ttotalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission );\\n#endif\";\n\nvar transmission_pars_fragment = \"#ifdef USE_TRANSMISSION\\n\\tuniform float transmission;\\n\\tuniform float thickness;\\n\\tuniform float attenuationDistance;\\n\\tuniform vec3 attenuationColor;\\n\\t#ifdef USE_TRANSMISSIONMAP\\n\\t\\tuniform sampler2D transmissionMap;\\n\\t#endif\\n\\t#ifdef USE_THICKNESSMAP\\n\\t\\tuniform sampler2D thicknessMap;\\n\\t#endif\\n\\tuniform vec2 transmissionSamplerSize;\\n\\tuniform sampler2D transmissionSamplerMap;\\n\\tuniform mat4 modelMatrix;\\n\\tuniform mat4 projectionMatrix;\\n\\tvarying vec3 vWorldPosition;\\n\\tfloat w0( float a ) {\\n\\t\\treturn ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 );\\n\\t}\\n\\tfloat w1( float a ) {\\n\\t\\treturn ( 1.0 / 6.0 ) * ( a *  a * ( 3.0 * a - 6.0 ) + 4.0 );\\n\\t}\\n\\tfloat w2( float a ){\\n\\t\\treturn ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 );\\n\\t}\\n\\tfloat w3( float a ) {\\n\\t\\treturn ( 1.0 / 6.0 ) * ( a * a * a );\\n\\t}\\n\\tfloat g0( float a ) {\\n\\t\\treturn w0( a ) + w1( a );\\n\\t}\\n\\tfloat g1( float a ) {\\n\\t\\treturn w2( a ) + w3( a );\\n\\t}\\n\\tfloat h0( float a ) {\\n\\t\\treturn - 1.0 + w1( a ) / ( w0( a ) + w1( a ) );\\n\\t}\\n\\tfloat h1( float a ) {\\n\\t\\treturn 1.0 + w3( a ) / ( w2( a ) + w3( a ) );\\n\\t}\\n\\tvec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) {\\n\\t\\tuv = uv * texelSize.zw + 0.5;\\n\\t\\tvec2 iuv = floor( uv );\\n\\t\\tvec2 fuv = fract( uv );\\n\\t\\tfloat g0x = g0( fuv.x );\\n\\t\\tfloat g1x = g1( fuv.x );\\n\\t\\tfloat h0x = h0( fuv.x );\\n\\t\\tfloat h1x = h1( fuv.x );\\n\\t\\tfloat h0y = h0( fuv.y );\\n\\t\\tfloat h1y = h1( fuv.y );\\n\\t\\tvec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy;\\n\\t\\tvec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy;\\n\\t\\tvec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy;\\n\\t\\tvec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy;\\n\\t\\treturn g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) +\\n\\t\\t\\tg1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) );\\n\\t}\\n\\tvec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) {\\n\\t\\tvec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) );\\n\\t\\tvec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) );\\n\\t\\tvec2 fLodSizeInv = 1.0 / fLodSize;\\n\\t\\tvec2 cLodSizeInv = 1.0 / cLodSize;\\n\\t\\tvec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) );\\n\\t\\tvec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) );\\n\\t\\treturn mix( fSample, cSample, fract( lod ) );\\n\\t}\\n\\tvec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) {\\n\\t\\tvec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior );\\n\\t\\tvec3 modelScale;\\n\\t\\tmodelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) );\\n\\t\\tmodelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) );\\n\\t\\tmodelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) );\\n\\t\\treturn normalize( refractionVector ) * thickness * modelScale;\\n\\t}\\n\\tfloat applyIorToRoughness( const in float roughness, const in float ior ) {\\n\\t\\treturn roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 );\\n\\t}\\n\\tvec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) {\\n\\t\\tfloat lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior );\\n\\t\\treturn textureBicubic( transmissionSamplerMap, fragCoord.xy, lod );\\n\\t}\\n\\tvec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) {\\n\\t\\tif ( isinf( attenuationDistance ) ) {\\n\\t\\t\\treturn vec3( 1.0 );\\n\\t\\t} else {\\n\\t\\t\\tvec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance;\\n\\t\\t\\tvec3 transmittance = exp( - attenuationCoefficient * transmissionDistance );\\t\\t\\treturn transmittance;\\n\\t\\t}\\n\\t}\\n\\tvec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor,\\n\\t\\tconst in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix,\\n\\t\\tconst in mat4 viewMatrix, const in mat4 projMatrix, const in float dispersion, const in float ior, const in float thickness,\\n\\t\\tconst in vec3 attenuationColor, const in float attenuationDistance ) {\\n\\t\\tvec4 transmittedLight;\\n\\t\\tvec3 transmittance;\\n\\t\\t#ifdef USE_DISPERSION\\n\\t\\t\\tfloat halfSpread = ( ior - 1.0 ) * 0.025 * dispersion;\\n\\t\\t\\tvec3 iors = vec3( ior - halfSpread, ior, ior + halfSpread );\\n\\t\\t\\tfor ( int i = 0; i < 3; i ++ ) {\\n\\t\\t\\t\\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, iors[ i ], modelMatrix );\\n\\t\\t\\t\\tvec3 refractedRayExit = position + transmissionRay;\\n\\t\\t\\t\\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\\n\\t\\t\\t\\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\\n\\t\\t\\t\\trefractionCoords += 1.0;\\n\\t\\t\\t\\trefractionCoords /= 2.0;\\n\\t\\t\\t\\tvec4 transmissionSample = getTransmissionSample( refractionCoords, roughness, iors[ i ] );\\n\\t\\t\\t\\ttransmittedLight[ i ] = transmissionSample[ i ];\\n\\t\\t\\t\\ttransmittedLight.a += transmissionSample.a;\\n\\t\\t\\t\\ttransmittance[ i ] = diffuseColor[ i ] * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance )[ i ];\\n\\t\\t\\t}\\n\\t\\t\\ttransmittedLight.a /= 3.0;\\n\\t\\t#else\\n\\t\\t\\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix );\\n\\t\\t\\tvec3 refractedRayExit = position + transmissionRay;\\n\\t\\t\\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\\n\\t\\t\\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\\n\\t\\t\\trefractionCoords += 1.0;\\n\\t\\t\\trefractionCoords /= 2.0;\\n\\t\\t\\ttransmittedLight = getTransmissionSample( refractionCoords, roughness, ior );\\n\\t\\t\\ttransmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance );\\n\\t\\t#endif\\n\\t\\tvec3 attenuatedColor = transmittance * transmittedLight.rgb;\\n\\t\\tvec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness );\\n\\t\\tfloat transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0;\\n\\t\\treturn vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor );\\n\\t}\\n#endif\";\n\nvar uv_pars_fragment = \"#if defined( USE_UV ) || defined( USE_ANISOTROPY )\\n\\tvarying vec2 vUv;\\n#endif\\n#ifdef USE_MAP\\n\\tvarying vec2 vMapUv;\\n#endif\\n#ifdef USE_ALPHAMAP\\n\\tvarying vec2 vAlphaMapUv;\\n#endif\\n#ifdef USE_LIGHTMAP\\n\\tvarying vec2 vLightMapUv;\\n#endif\\n#ifdef USE_AOMAP\\n\\tvarying vec2 vAoMapUv;\\n#endif\\n#ifdef USE_BUMPMAP\\n\\tvarying vec2 vBumpMapUv;\\n#endif\\n#ifdef USE_NORMALMAP\\n\\tvarying vec2 vNormalMapUv;\\n#endif\\n#ifdef USE_EMISSIVEMAP\\n\\tvarying vec2 vEmissiveMapUv;\\n#endif\\n#ifdef USE_METALNESSMAP\\n\\tvarying vec2 vMetalnessMapUv;\\n#endif\\n#ifdef USE_ROUGHNESSMAP\\n\\tvarying vec2 vRoughnessMapUv;\\n#endif\\n#ifdef USE_ANISOTROPYMAP\\n\\tvarying vec2 vAnisotropyMapUv;\\n#endif\\n#ifdef USE_CLEARCOATMAP\\n\\tvarying vec2 vClearcoatMapUv;\\n#endif\\n#ifdef USE_CLEARCOAT_NORMALMAP\\n\\tvarying vec2 vClearcoatNormalMapUv;\\n#endif\\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\\n\\tvarying vec2 vClearcoatRoughnessMapUv;\\n#endif\\n#ifdef USE_IRIDESCENCEMAP\\n\\tvarying vec2 vIridescenceMapUv;\\n#endif\\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\\n\\tvarying vec2 vIridescenceThicknessMapUv;\\n#endif\\n#ifdef USE_SHEEN_COLORMAP\\n\\tvarying vec2 vSheenColorMapUv;\\n#endif\\n#ifdef USE_SHEEN_ROUGHNESSMAP\\n\\tvarying vec2 vSheenRoughnessMapUv;\\n#endif\\n#ifdef USE_SPECULARMAP\\n\\tvarying vec2 vSpecularMapUv;\\n#endif\\n#ifdef USE_SPECULAR_COLORMAP\\n\\tvarying vec2 vSpecularColorMapUv;\\n#endif\\n#ifdef USE_SPECULAR_INTENSITYMAP\\n\\tvarying vec2 vSpecularIntensityMapUv;\\n#endif\\n#ifdef USE_TRANSMISSIONMAP\\n\\tuniform mat3 transmissionMapTransform;\\n\\tvarying vec2 vTransmissionMapUv;\\n#endif\\n#ifdef USE_THICKNESSMAP\\n\\tuniform mat3 thicknessMapTransform;\\n\\tvarying vec2 vThicknessMapUv;\\n#endif\";\n\nvar uv_pars_vertex = \"#if defined( USE_UV ) || defined( USE_ANISOTROPY )\\n\\tvarying vec2 vUv;\\n#endif\\n#ifdef USE_MAP\\n\\tuniform mat3 mapTransform;\\n\\tvarying vec2 vMapUv;\\n#endif\\n#ifdef USE_ALPHAMAP\\n\\tuniform mat3 alphaMapTransform;\\n\\tvarying vec2 vAlphaMapUv;\\n#endif\\n#ifdef USE_LIGHTMAP\\n\\tuniform mat3 lightMapTransform;\\n\\tvarying vec2 vLightMapUv;\\n#endif\\n#ifdef USE_AOMAP\\n\\tuniform mat3 aoMapTransform;\\n\\tvarying vec2 vAoMapUv;\\n#endif\\n#ifdef USE_BUMPMAP\\n\\tuniform mat3 bumpMapTransform;\\n\\tvarying vec2 vBumpMapUv;\\n#endif\\n#ifdef USE_NORMALMAP\\n\\tuniform mat3 normalMapTransform;\\n\\tvarying vec2 vNormalMapUv;\\n#endif\\n#ifdef USE_DISPLACEMENTMAP\\n\\tuniform mat3 displacementMapTransform;\\n\\tvarying vec2 vDisplacementMapUv;\\n#endif\\n#ifdef USE_EMISSIVEMAP\\n\\tuniform mat3 emissiveMapTransform;\\n\\tvarying vec2 vEmissiveMapUv;\\n#endif\\n#ifdef USE_METALNESSMAP\\n\\tuniform mat3 metalnessMapTransform;\\n\\tvarying vec2 vMetalnessMapUv;\\n#endif\\n#ifdef USE_ROUGHNESSMAP\\n\\tuniform mat3 roughnessMapTransform;\\n\\tvarying vec2 vRoughnessMapUv;\\n#endif\\n#ifdef USE_ANISOTROPYMAP\\n\\tuniform mat3 anisotropyMapTransform;\\n\\tvarying vec2 vAnisotropyMapUv;\\n#endif\\n#ifdef USE_CLEARCOATMAP\\n\\tuniform mat3 clearcoatMapTransform;\\n\\tvarying vec2 vClearcoatMapUv;\\n#endif\\n#ifdef USE_CLEARCOAT_NORMALMAP\\n\\tuniform mat3 clearcoatNormalMapTransform;\\n\\tvarying vec2 vClearcoatNormalMapUv;\\n#endif\\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\\n\\tuniform mat3 clearcoatRoughnessMapTransform;\\n\\tvarying vec2 vClearcoatRoughnessMapUv;\\n#endif\\n#ifdef USE_SHEEN_COLORMAP\\n\\tuniform mat3 sheenColorMapTransform;\\n\\tvarying vec2 vSheenColorMapUv;\\n#endif\\n#ifdef USE_SHEEN_ROUGHNESSMAP\\n\\tuniform mat3 sheenRoughnessMapTransform;\\n\\tvarying vec2 vSheenRoughnessMapUv;\\n#endif\\n#ifdef USE_IRIDESCENCEMAP\\n\\tuniform mat3 iridescenceMapTransform;\\n\\tvarying vec2 vIridescenceMapUv;\\n#endif\\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\\n\\tuniform mat3 iridescenceThicknessMapTransform;\\n\\tvarying vec2 vIridescenceThicknessMapUv;\\n#endif\\n#ifdef USE_SPECULARMAP\\n\\tuniform mat3 specularMapTransform;\\n\\tvarying vec2 vSpecularMapUv;\\n#endif\\n#ifdef USE_SPECULAR_COLORMAP\\n\\tuniform mat3 specularColorMapTransform;\\n\\tvarying vec2 vSpecularColorMapUv;\\n#endif\\n#ifdef USE_SPECULAR_INTENSITYMAP\\n\\tuniform mat3 specularIntensityMapTransform;\\n\\tvarying vec2 vSpecularIntensityMapUv;\\n#endif\\n#ifdef USE_TRANSMISSIONMAP\\n\\tuniform mat3 transmissionMapTransform;\\n\\tvarying vec2 vTransmissionMapUv;\\n#endif\\n#ifdef USE_THICKNESSMAP\\n\\tuniform mat3 thicknessMapTransform;\\n\\tvarying vec2 vThicknessMapUv;\\n#endif\";\n\nvar uv_vertex = \"#if defined( USE_UV ) || defined( USE_ANISOTROPY )\\n\\tvUv = vec3( uv, 1 ).xy;\\n#endif\\n#ifdef USE_MAP\\n\\tvMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_ALPHAMAP\\n\\tvAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_LIGHTMAP\\n\\tvLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_AOMAP\\n\\tvAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_BUMPMAP\\n\\tvBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_NORMALMAP\\n\\tvNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_DISPLACEMENTMAP\\n\\tvDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_EMISSIVEMAP\\n\\tvEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_METALNESSMAP\\n\\tvMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_ROUGHNESSMAP\\n\\tvRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_ANISOTROPYMAP\\n\\tvAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_CLEARCOATMAP\\n\\tvClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_CLEARCOAT_NORMALMAP\\n\\tvClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\\n\\tvClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_IRIDESCENCEMAP\\n\\tvIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\\n\\tvIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_SHEEN_COLORMAP\\n\\tvSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_SHEEN_ROUGHNESSMAP\\n\\tvSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_SPECULARMAP\\n\\tvSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_SPECULAR_COLORMAP\\n\\tvSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_SPECULAR_INTENSITYMAP\\n\\tvSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_TRANSMISSIONMAP\\n\\tvTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy;\\n#endif\\n#ifdef USE_THICKNESSMAP\\n\\tvThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy;\\n#endif\";\n\nvar worldpos_vertex = \"#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0\\n\\tvec4 worldPosition = vec4( transformed, 1.0 );\\n\\t#ifdef USE_BATCHING\\n\\t\\tworldPosition = batchingMatrix * worldPosition;\\n\\t#endif\\n\\t#ifdef USE_INSTANCING\\n\\t\\tworldPosition = instanceMatrix * worldPosition;\\n\\t#endif\\n\\tworldPosition = modelMatrix * worldPosition;\\n#endif\";\n\nconst vertex$h = \"varying vec2 vUv;\\nuniform mat3 uvTransform;\\nvoid main() {\\n\\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\\n\\tgl_Position = vec4( position.xy, 1.0, 1.0 );\\n}\";\n\nconst fragment$h = \"uniform sampler2D t2D;\\nuniform float backgroundIntensity;\\nvarying vec2 vUv;\\nvoid main() {\\n\\tvec4 texColor = texture2D( t2D, vUv );\\n\\t#ifdef DECODE_VIDEO_TEXTURE\\n\\t\\ttexColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w );\\n\\t#endif\\n\\ttexColor.rgb *= backgroundIntensity;\\n\\tgl_FragColor = texColor;\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n}\";\n\nconst vertex$g = \"varying vec3 vWorldDirection;\\n#include <common>\\nvoid main() {\\n\\tvWorldDirection = transformDirection( position, modelMatrix );\\n\\t#include <begin_vertex>\\n\\t#include <project_vertex>\\n\\tgl_Position.z = gl_Position.w;\\n}\";\n\nconst fragment$g = \"#ifdef ENVMAP_TYPE_CUBE\\n\\tuniform samplerCube envMap;\\n#elif defined( ENVMAP_TYPE_CUBE_UV )\\n\\tuniform sampler2D envMap;\\n#endif\\nuniform float flipEnvMap;\\nuniform float backgroundBlurriness;\\nuniform float backgroundIntensity;\\nuniform mat3 backgroundRotation;\\nvarying vec3 vWorldDirection;\\n#include <cube_uv_reflection_fragment>\\nvoid main() {\\n\\t#ifdef ENVMAP_TYPE_CUBE\\n\\t\\tvec4 texColor = textureCube( envMap, backgroundRotation * vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) );\\n\\t#elif defined( ENVMAP_TYPE_CUBE_UV )\\n\\t\\tvec4 texColor = textureCubeUV( envMap, backgroundRotation * vWorldDirection, backgroundBlurriness );\\n\\t#else\\n\\t\\tvec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 );\\n\\t#endif\\n\\ttexColor.rgb *= backgroundIntensity;\\n\\tgl_FragColor = texColor;\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n}\";\n\nconst vertex$f = \"varying vec3 vWorldDirection;\\n#include <common>\\nvoid main() {\\n\\tvWorldDirection = transformDirection( position, modelMatrix );\\n\\t#include <begin_vertex>\\n\\t#include <project_vertex>\\n\\tgl_Position.z = gl_Position.w;\\n}\";\n\nconst fragment$f = \"uniform samplerCube tCube;\\nuniform float tFlip;\\nuniform float opacity;\\nvarying vec3 vWorldDirection;\\nvoid main() {\\n\\tvec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) );\\n\\tgl_FragColor = texColor;\\n\\tgl_FragColor.a *= opacity;\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n}\";\n\nconst vertex$e = \"#include <common>\\n#include <batching_pars_vertex>\\n#include <uv_pars_vertex>\\n#include <displacementmap_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvarying vec2 vHighPrecisionZW;\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\t#include <batching_vertex>\\n\\t#include <skinbase_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#ifdef USE_DISPLACEMENTMAP\\n\\t\\t#include <beginnormal_vertex>\\n\\t\\t#include <morphnormal_vertex>\\n\\t\\t#include <skinnormal_vertex>\\n\\t#endif\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <displacementmap_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\tvHighPrecisionZW = gl_Position.zw;\\n}\";\n\nconst fragment$e = \"#if DEPTH_PACKING == 3200\\n\\tuniform float opacity;\\n#endif\\n#include <common>\\n#include <packing>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <alphamap_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvarying vec2 vHighPrecisionZW;\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( 1.0 );\\n\\t#include <clipping_planes_fragment>\\n\\t#if DEPTH_PACKING == 3200\\n\\t\\tdiffuseColor.a = opacity;\\n\\t#endif\\n\\t#include <map_fragment>\\n\\t#include <alphamap_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\t#include <logdepthbuf_fragment>\\n\\t#ifdef USE_REVERSED_DEPTH_BUFFER\\n\\t\\tfloat fragCoordZ = vHighPrecisionZW[ 0 ] / vHighPrecisionZW[ 1 ];\\n\\t#else\\n\\t\\tfloat fragCoordZ = 0.5 * vHighPrecisionZW[ 0 ] / vHighPrecisionZW[ 1 ] + 0.5;\\n\\t#endif\\n\\t#if DEPTH_PACKING == 3200\\n\\t\\tgl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );\\n\\t#elif DEPTH_PACKING == 3201\\n\\t\\tgl_FragColor = packDepthToRGBA( fragCoordZ );\\n\\t#elif DEPTH_PACKING == 3202\\n\\t\\tgl_FragColor = vec4( packDepthToRGB( fragCoordZ ), 1.0 );\\n\\t#elif DEPTH_PACKING == 3203\\n\\t\\tgl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 );\\n\\t#endif\\n}\";\n\nconst vertex$d = \"#define DISTANCE\\nvarying vec3 vWorldPosition;\\n#include <common>\\n#include <batching_pars_vertex>\\n#include <uv_pars_vertex>\\n#include <displacementmap_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\t#include <batching_vertex>\\n\\t#include <skinbase_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#ifdef USE_DISPLACEMENTMAP\\n\\t\\t#include <beginnormal_vertex>\\n\\t\\t#include <morphnormal_vertex>\\n\\t\\t#include <skinnormal_vertex>\\n\\t#endif\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <displacementmap_vertex>\\n\\t#include <project_vertex>\\n\\t#include <worldpos_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\tvWorldPosition = worldPosition.xyz;\\n}\";\n\nconst fragment$d = \"#define DISTANCE\\nuniform vec3 referencePosition;\\nuniform float nearDistance;\\nuniform float farDistance;\\nvarying vec3 vWorldPosition;\\n#include <common>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <alphamap_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main () {\\n\\tvec4 diffuseColor = vec4( 1.0 );\\n\\t#include <clipping_planes_fragment>\\n\\t#include <map_fragment>\\n\\t#include <alphamap_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\tfloat dist = length( vWorldPosition - referencePosition );\\n\\tdist = ( dist - nearDistance ) / ( farDistance - nearDistance );\\n\\tdist = saturate( dist );\\n\\tgl_FragColor = vec4( dist, 0.0, 0.0, 1.0 );\\n}\";\n\nconst vertex$c = \"varying vec3 vWorldDirection;\\n#include <common>\\nvoid main() {\\n\\tvWorldDirection = transformDirection( position, modelMatrix );\\n\\t#include <begin_vertex>\\n\\t#include <project_vertex>\\n}\";\n\nconst fragment$c = \"uniform sampler2D tEquirect;\\nvarying vec3 vWorldDirection;\\n#include <common>\\nvoid main() {\\n\\tvec3 direction = normalize( vWorldDirection );\\n\\tvec2 sampleUV = equirectUv( direction );\\n\\tgl_FragColor = texture2D( tEquirect, sampleUV );\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n}\";\n\nconst vertex$b = \"uniform float scale;\\nattribute float lineDistance;\\nvarying float vLineDistance;\\n#include <common>\\n#include <uv_pars_vertex>\\n#include <color_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\tvLineDistance = scale * lineDistance;\\n\\t#include <uv_vertex>\\n\\t#include <color_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphcolor_vertex>\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\t#include <fog_vertex>\\n}\";\n\nconst fragment$b = \"uniform vec3 diffuse;\\nuniform float opacity;\\nuniform float dashSize;\\nuniform float totalSize;\\nvarying float vLineDistance;\\n#include <common>\\n#include <color_pars_fragment>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <fog_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( diffuse, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\tif ( mod( vLineDistance, totalSize ) > dashSize ) {\\n\\t\\tdiscard;\\n\\t}\\n\\tvec3 outgoingLight = vec3( 0.0 );\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <map_fragment>\\n\\t#include <color_fragment>\\n\\toutgoingLight = diffuseColor.rgb;\\n\\t#include <opaque_fragment>\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n\\t#include <premultiplied_alpha_fragment>\\n}\";\n\nconst vertex$a = \"#include <common>\\n#include <batching_pars_vertex>\\n#include <uv_pars_vertex>\\n#include <envmap_pars_vertex>\\n#include <color_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\t#include <color_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphcolor_vertex>\\n\\t#include <batching_vertex>\\n\\t#if defined ( USE_ENVMAP ) || defined ( USE_SKINNING )\\n\\t\\t#include <beginnormal_vertex>\\n\\t\\t#include <morphnormal_vertex>\\n\\t\\t#include <skinbase_vertex>\\n\\t\\t#include <skinnormal_vertex>\\n\\t\\t#include <defaultnormal_vertex>\\n\\t#endif\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\t#include <worldpos_vertex>\\n\\t#include <envmap_vertex>\\n\\t#include <fog_vertex>\\n}\";\n\nconst fragment$a = \"uniform vec3 diffuse;\\nuniform float opacity;\\n#ifndef FLAT_SHADED\\n\\tvarying vec3 vNormal;\\n#endif\\n#include <common>\\n#include <dithering_pars_fragment>\\n#include <color_pars_fragment>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <alphamap_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <aomap_pars_fragment>\\n#include <lightmap_pars_fragment>\\n#include <envmap_common_pars_fragment>\\n#include <envmap_pars_fragment>\\n#include <fog_pars_fragment>\\n#include <specularmap_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( diffuse, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <map_fragment>\\n\\t#include <color_fragment>\\n\\t#include <alphamap_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\t#include <specularmap_fragment>\\n\\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\\n\\t#ifdef USE_LIGHTMAP\\n\\t\\tvec4 lightMapTexel = texture2D( lightMap, vLightMapUv );\\n\\t\\treflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI;\\n\\t#else\\n\\t\\treflectedLight.indirectDiffuse += vec3( 1.0 );\\n\\t#endif\\n\\t#include <aomap_fragment>\\n\\treflectedLight.indirectDiffuse *= diffuseColor.rgb;\\n\\tvec3 outgoingLight = reflectedLight.indirectDiffuse;\\n\\t#include <envmap_fragment>\\n\\t#include <opaque_fragment>\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n\\t#include <premultiplied_alpha_fragment>\\n\\t#include <dithering_fragment>\\n}\";\n\nconst vertex$9 = \"#define LAMBERT\\nvarying vec3 vViewPosition;\\n#include <common>\\n#include <batching_pars_vertex>\\n#include <uv_pars_vertex>\\n#include <displacementmap_pars_vertex>\\n#include <envmap_pars_vertex>\\n#include <color_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <normal_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <shadowmap_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\t#include <color_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphcolor_vertex>\\n\\t#include <batching_vertex>\\n\\t#include <beginnormal_vertex>\\n\\t#include <morphnormal_vertex>\\n\\t#include <skinbase_vertex>\\n\\t#include <skinnormal_vertex>\\n\\t#include <defaultnormal_vertex>\\n\\t#include <normal_vertex>\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <displacementmap_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\tvViewPosition = - mvPosition.xyz;\\n\\t#include <worldpos_vertex>\\n\\t#include <envmap_vertex>\\n\\t#include <shadowmap_vertex>\\n\\t#include <fog_vertex>\\n}\";\n\nconst fragment$9 = \"#define LAMBERT\\nuniform vec3 diffuse;\\nuniform vec3 emissive;\\nuniform float opacity;\\n#include <common>\\n#include <dithering_pars_fragment>\\n#include <color_pars_fragment>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <alphamap_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <aomap_pars_fragment>\\n#include <lightmap_pars_fragment>\\n#include <emissivemap_pars_fragment>\\n#include <envmap_common_pars_fragment>\\n#include <envmap_pars_fragment>\\n#include <fog_pars_fragment>\\n#include <bsdfs>\\n#include <lights_pars_begin>\\n#include <normal_pars_fragment>\\n#include <lights_lambert_pars_fragment>\\n#include <shadowmap_pars_fragment>\\n#include <bumpmap_pars_fragment>\\n#include <normalmap_pars_fragment>\\n#include <specularmap_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( diffuse, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\\n\\tvec3 totalEmissiveRadiance = emissive;\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <map_fragment>\\n\\t#include <color_fragment>\\n\\t#include <alphamap_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\t#include <specularmap_fragment>\\n\\t#include <normal_fragment_begin>\\n\\t#include <normal_fragment_maps>\\n\\t#include <emissivemap_fragment>\\n\\t#include <lights_lambert_fragment>\\n\\t#include <lights_fragment_begin>\\n\\t#include <lights_fragment_maps>\\n\\t#include <lights_fragment_end>\\n\\t#include <aomap_fragment>\\n\\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\\n\\t#include <envmap_fragment>\\n\\t#include <opaque_fragment>\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n\\t#include <premultiplied_alpha_fragment>\\n\\t#include <dithering_fragment>\\n}\";\n\nconst vertex$8 = \"#define MATCAP\\nvarying vec3 vViewPosition;\\n#include <common>\\n#include <batching_pars_vertex>\\n#include <uv_pars_vertex>\\n#include <color_pars_vertex>\\n#include <displacementmap_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <normal_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\t#include <color_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphcolor_vertex>\\n\\t#include <batching_vertex>\\n\\t#include <beginnormal_vertex>\\n\\t#include <morphnormal_vertex>\\n\\t#include <skinbase_vertex>\\n\\t#include <skinnormal_vertex>\\n\\t#include <defaultnormal_vertex>\\n\\t#include <normal_vertex>\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <displacementmap_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\t#include <fog_vertex>\\n\\tvViewPosition = - mvPosition.xyz;\\n}\";\n\nconst fragment$8 = \"#define MATCAP\\nuniform vec3 diffuse;\\nuniform float opacity;\\nuniform sampler2D matcap;\\nvarying vec3 vViewPosition;\\n#include <common>\\n#include <dithering_pars_fragment>\\n#include <color_pars_fragment>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <alphamap_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <fog_pars_fragment>\\n#include <normal_pars_fragment>\\n#include <bumpmap_pars_fragment>\\n#include <normalmap_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( diffuse, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <map_fragment>\\n\\t#include <color_fragment>\\n\\t#include <alphamap_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\t#include <normal_fragment_begin>\\n\\t#include <normal_fragment_maps>\\n\\tvec3 viewDir = normalize( vViewPosition );\\n\\tvec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) );\\n\\tvec3 y = cross( viewDir, x );\\n\\tvec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5;\\n\\t#ifdef USE_MATCAP\\n\\t\\tvec4 matcapColor = texture2D( matcap, uv );\\n\\t#else\\n\\t\\tvec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 );\\n\\t#endif\\n\\tvec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb;\\n\\t#include <opaque_fragment>\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n\\t#include <premultiplied_alpha_fragment>\\n\\t#include <dithering_fragment>\\n}\";\n\nconst vertex$7 = \"#define NORMAL\\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\\n\\tvarying vec3 vViewPosition;\\n#endif\\n#include <common>\\n#include <batching_pars_vertex>\\n#include <uv_pars_vertex>\\n#include <displacementmap_pars_vertex>\\n#include <normal_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\t#include <batching_vertex>\\n\\t#include <beginnormal_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphnormal_vertex>\\n\\t#include <skinbase_vertex>\\n\\t#include <skinnormal_vertex>\\n\\t#include <defaultnormal_vertex>\\n\\t#include <normal_vertex>\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <displacementmap_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\\n\\tvViewPosition = - mvPosition.xyz;\\n#endif\\n}\";\n\nconst fragment$7 = \"#define NORMAL\\nuniform float opacity;\\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\\n\\tvarying vec3 vViewPosition;\\n#endif\\n#include <uv_pars_fragment>\\n#include <normal_pars_fragment>\\n#include <bumpmap_pars_fragment>\\n#include <normalmap_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <normal_fragment_begin>\\n\\t#include <normal_fragment_maps>\\n\\tgl_FragColor = vec4( normalize( normal ) * 0.5 + 0.5, diffuseColor.a );\\n\\t#ifdef OPAQUE\\n\\t\\tgl_FragColor.a = 1.0;\\n\\t#endif\\n}\";\n\nconst vertex$6 = \"#define PHONG\\nvarying vec3 vViewPosition;\\n#include <common>\\n#include <batching_pars_vertex>\\n#include <uv_pars_vertex>\\n#include <displacementmap_pars_vertex>\\n#include <envmap_pars_vertex>\\n#include <color_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <normal_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <shadowmap_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\t#include <color_vertex>\\n\\t#include <morphcolor_vertex>\\n\\t#include <batching_vertex>\\n\\t#include <beginnormal_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphnormal_vertex>\\n\\t#include <skinbase_vertex>\\n\\t#include <skinnormal_vertex>\\n\\t#include <defaultnormal_vertex>\\n\\t#include <normal_vertex>\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <displacementmap_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\tvViewPosition = - mvPosition.xyz;\\n\\t#include <worldpos_vertex>\\n\\t#include <envmap_vertex>\\n\\t#include <shadowmap_vertex>\\n\\t#include <fog_vertex>\\n}\";\n\nconst fragment$6 = \"#define PHONG\\nuniform vec3 diffuse;\\nuniform vec3 emissive;\\nuniform vec3 specular;\\nuniform float shininess;\\nuniform float opacity;\\n#include <common>\\n#include <dithering_pars_fragment>\\n#include <color_pars_fragment>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <alphamap_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <aomap_pars_fragment>\\n#include <lightmap_pars_fragment>\\n#include <emissivemap_pars_fragment>\\n#include <envmap_common_pars_fragment>\\n#include <envmap_pars_fragment>\\n#include <fog_pars_fragment>\\n#include <bsdfs>\\n#include <lights_pars_begin>\\n#include <normal_pars_fragment>\\n#include <lights_phong_pars_fragment>\\n#include <shadowmap_pars_fragment>\\n#include <bumpmap_pars_fragment>\\n#include <normalmap_pars_fragment>\\n#include <specularmap_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( diffuse, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\\n\\tvec3 totalEmissiveRadiance = emissive;\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <map_fragment>\\n\\t#include <color_fragment>\\n\\t#include <alphamap_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\t#include <specularmap_fragment>\\n\\t#include <normal_fragment_begin>\\n\\t#include <normal_fragment_maps>\\n\\t#include <emissivemap_fragment>\\n\\t#include <lights_phong_fragment>\\n\\t#include <lights_fragment_begin>\\n\\t#include <lights_fragment_maps>\\n\\t#include <lights_fragment_end>\\n\\t#include <aomap_fragment>\\n\\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\\n\\t#include <envmap_fragment>\\n\\t#include <opaque_fragment>\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n\\t#include <premultiplied_alpha_fragment>\\n\\t#include <dithering_fragment>\\n}\";\n\nconst vertex$5 = \"#define STANDARD\\nvarying vec3 vViewPosition;\\n#ifdef USE_TRANSMISSION\\n\\tvarying vec3 vWorldPosition;\\n#endif\\n#include <common>\\n#include <batching_pars_vertex>\\n#include <uv_pars_vertex>\\n#include <displacementmap_pars_vertex>\\n#include <color_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <normal_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <shadowmap_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\t#include <color_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphcolor_vertex>\\n\\t#include <batching_vertex>\\n\\t#include <beginnormal_vertex>\\n\\t#include <morphnormal_vertex>\\n\\t#include <skinbase_vertex>\\n\\t#include <skinnormal_vertex>\\n\\t#include <defaultnormal_vertex>\\n\\t#include <normal_vertex>\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <displacementmap_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\tvViewPosition = - mvPosition.xyz;\\n\\t#include <worldpos_vertex>\\n\\t#include <shadowmap_vertex>\\n\\t#include <fog_vertex>\\n#ifdef USE_TRANSMISSION\\n\\tvWorldPosition = worldPosition.xyz;\\n#endif\\n}\";\n\nconst fragment$5 = \"#define STANDARD\\n#ifdef PHYSICAL\\n\\t#define IOR\\n\\t#define USE_SPECULAR\\n#endif\\nuniform vec3 diffuse;\\nuniform vec3 emissive;\\nuniform float roughness;\\nuniform float metalness;\\nuniform float opacity;\\n#ifdef IOR\\n\\tuniform float ior;\\n#endif\\n#ifdef USE_SPECULAR\\n\\tuniform float specularIntensity;\\n\\tuniform vec3 specularColor;\\n\\t#ifdef USE_SPECULAR_COLORMAP\\n\\t\\tuniform sampler2D specularColorMap;\\n\\t#endif\\n\\t#ifdef USE_SPECULAR_INTENSITYMAP\\n\\t\\tuniform sampler2D specularIntensityMap;\\n\\t#endif\\n#endif\\n#ifdef USE_CLEARCOAT\\n\\tuniform float clearcoat;\\n\\tuniform float clearcoatRoughness;\\n#endif\\n#ifdef USE_DISPERSION\\n\\tuniform float dispersion;\\n#endif\\n#ifdef USE_IRIDESCENCE\\n\\tuniform float iridescence;\\n\\tuniform float iridescenceIOR;\\n\\tuniform float iridescenceThicknessMinimum;\\n\\tuniform float iridescenceThicknessMaximum;\\n#endif\\n#ifdef USE_SHEEN\\n\\tuniform vec3 sheenColor;\\n\\tuniform float sheenRoughness;\\n\\t#ifdef USE_SHEEN_COLORMAP\\n\\t\\tuniform sampler2D sheenColorMap;\\n\\t#endif\\n\\t#ifdef USE_SHEEN_ROUGHNESSMAP\\n\\t\\tuniform sampler2D sheenRoughnessMap;\\n\\t#endif\\n#endif\\n#ifdef USE_ANISOTROPY\\n\\tuniform vec2 anisotropyVector;\\n\\t#ifdef USE_ANISOTROPYMAP\\n\\t\\tuniform sampler2D anisotropyMap;\\n\\t#endif\\n#endif\\nvarying vec3 vViewPosition;\\n#include <common>\\n#include <dithering_pars_fragment>\\n#include <color_pars_fragment>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <alphamap_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <aomap_pars_fragment>\\n#include <lightmap_pars_fragment>\\n#include <emissivemap_pars_fragment>\\n#include <iridescence_fragment>\\n#include <cube_uv_reflection_fragment>\\n#include <envmap_common_pars_fragment>\\n#include <envmap_physical_pars_fragment>\\n#include <fog_pars_fragment>\\n#include <lights_pars_begin>\\n#include <normal_pars_fragment>\\n#include <lights_physical_pars_fragment>\\n#include <transmission_pars_fragment>\\n#include <shadowmap_pars_fragment>\\n#include <bumpmap_pars_fragment>\\n#include <normalmap_pars_fragment>\\n#include <clearcoat_pars_fragment>\\n#include <iridescence_pars_fragment>\\n#include <roughnessmap_pars_fragment>\\n#include <metalnessmap_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( diffuse, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\\n\\tvec3 totalEmissiveRadiance = emissive;\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <map_fragment>\\n\\t#include <color_fragment>\\n\\t#include <alphamap_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\t#include <roughnessmap_fragment>\\n\\t#include <metalnessmap_fragment>\\n\\t#include <normal_fragment_begin>\\n\\t#include <normal_fragment_maps>\\n\\t#include <clearcoat_normal_fragment_begin>\\n\\t#include <clearcoat_normal_fragment_maps>\\n\\t#include <emissivemap_fragment>\\n\\t#include <lights_physical_fragment>\\n\\t#include <lights_fragment_begin>\\n\\t#include <lights_fragment_maps>\\n\\t#include <lights_fragment_end>\\n\\t#include <aomap_fragment>\\n\\tvec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;\\n\\tvec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;\\n\\t#include <transmission_fragment>\\n\\tvec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;\\n\\t#ifdef USE_SHEEN\\n \\n\\t\\toutgoingLight = outgoingLight + sheenSpecularDirect + sheenSpecularIndirect;\\n \\n \\t#endif\\n\\t#ifdef USE_CLEARCOAT\\n\\t\\tfloat dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) );\\n\\t\\tvec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc );\\n\\t\\toutgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + ( clearcoatSpecularDirect + clearcoatSpecularIndirect ) * material.clearcoat;\\n\\t#endif\\n\\t#include <opaque_fragment>\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n\\t#include <premultiplied_alpha_fragment>\\n\\t#include <dithering_fragment>\\n}\";\n\nconst vertex$4 = \"#define TOON\\nvarying vec3 vViewPosition;\\n#include <common>\\n#include <batching_pars_vertex>\\n#include <uv_pars_vertex>\\n#include <displacementmap_pars_vertex>\\n#include <color_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <normal_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <shadowmap_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\t#include <color_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphcolor_vertex>\\n\\t#include <batching_vertex>\\n\\t#include <beginnormal_vertex>\\n\\t#include <morphnormal_vertex>\\n\\t#include <skinbase_vertex>\\n\\t#include <skinnormal_vertex>\\n\\t#include <defaultnormal_vertex>\\n\\t#include <normal_vertex>\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <displacementmap_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\tvViewPosition = - mvPosition.xyz;\\n\\t#include <worldpos_vertex>\\n\\t#include <shadowmap_vertex>\\n\\t#include <fog_vertex>\\n}\";\n\nconst fragment$4 = \"#define TOON\\nuniform vec3 diffuse;\\nuniform vec3 emissive;\\nuniform float opacity;\\n#include <common>\\n#include <dithering_pars_fragment>\\n#include <color_pars_fragment>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <alphamap_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <aomap_pars_fragment>\\n#include <lightmap_pars_fragment>\\n#include <emissivemap_pars_fragment>\\n#include <gradientmap_pars_fragment>\\n#include <fog_pars_fragment>\\n#include <bsdfs>\\n#include <lights_pars_begin>\\n#include <normal_pars_fragment>\\n#include <lights_toon_pars_fragment>\\n#include <shadowmap_pars_fragment>\\n#include <bumpmap_pars_fragment>\\n#include <normalmap_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( diffuse, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\\n\\tvec3 totalEmissiveRadiance = emissive;\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <map_fragment>\\n\\t#include <color_fragment>\\n\\t#include <alphamap_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\t#include <normal_fragment_begin>\\n\\t#include <normal_fragment_maps>\\n\\t#include <emissivemap_fragment>\\n\\t#include <lights_toon_fragment>\\n\\t#include <lights_fragment_begin>\\n\\t#include <lights_fragment_maps>\\n\\t#include <lights_fragment_end>\\n\\t#include <aomap_fragment>\\n\\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\\n\\t#include <opaque_fragment>\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n\\t#include <premultiplied_alpha_fragment>\\n\\t#include <dithering_fragment>\\n}\";\n\nconst vertex$3 = \"uniform float size;\\nuniform float scale;\\n#include <common>\\n#include <color_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\n#ifdef USE_POINTS_UV\\n\\tvarying vec2 vUv;\\n\\tuniform mat3 uvTransform;\\n#endif\\nvoid main() {\\n\\t#ifdef USE_POINTS_UV\\n\\t\\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\\n\\t#endif\\n\\t#include <color_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphcolor_vertex>\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <project_vertex>\\n\\tgl_PointSize = size;\\n\\t#ifdef USE_SIZEATTENUATION\\n\\t\\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\\n\\t\\tif ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );\\n\\t#endif\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\t#include <worldpos_vertex>\\n\\t#include <fog_vertex>\\n}\";\n\nconst fragment$3 = \"uniform vec3 diffuse;\\nuniform float opacity;\\n#include <common>\\n#include <color_pars_fragment>\\n#include <map_particle_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <fog_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( diffuse, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\tvec3 outgoingLight = vec3( 0.0 );\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <map_particle_fragment>\\n\\t#include <color_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\toutgoingLight = diffuseColor.rgb;\\n\\t#include <opaque_fragment>\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n\\t#include <premultiplied_alpha_fragment>\\n}\";\n\nconst vertex$2 = \"#include <common>\\n#include <batching_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <morphtarget_pars_vertex>\\n#include <skinning_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <shadowmap_pars_vertex>\\nvoid main() {\\n\\t#include <batching_vertex>\\n\\t#include <beginnormal_vertex>\\n\\t#include <morphinstance_vertex>\\n\\t#include <morphnormal_vertex>\\n\\t#include <skinbase_vertex>\\n\\t#include <skinnormal_vertex>\\n\\t#include <defaultnormal_vertex>\\n\\t#include <begin_vertex>\\n\\t#include <morphtarget_vertex>\\n\\t#include <skinning_vertex>\\n\\t#include <project_vertex>\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <worldpos_vertex>\\n\\t#include <shadowmap_vertex>\\n\\t#include <fog_vertex>\\n}\";\n\nconst fragment$2 = \"uniform vec3 color;\\nuniform float opacity;\\n#include <common>\\n#include <fog_pars_fragment>\\n#include <bsdfs>\\n#include <lights_pars_begin>\\n#include <logdepthbuf_pars_fragment>\\n#include <shadowmap_pars_fragment>\\n#include <shadowmask_pars_fragment>\\nvoid main() {\\n\\t#include <logdepthbuf_fragment>\\n\\tgl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n}\";\n\nconst vertex$1 = \"uniform float rotation;\\nuniform vec2 center;\\n#include <common>\\n#include <uv_pars_vertex>\\n#include <fog_pars_vertex>\\n#include <logdepthbuf_pars_vertex>\\n#include <clipping_planes_pars_vertex>\\nvoid main() {\\n\\t#include <uv_vertex>\\n\\tvec4 mvPosition = modelViewMatrix[ 3 ];\\n\\tvec2 scale = vec2( length( modelMatrix[ 0 ].xyz ), length( modelMatrix[ 1 ].xyz ) );\\n\\t#ifndef USE_SIZEATTENUATION\\n\\t\\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\\n\\t\\tif ( isPerspective ) scale *= - mvPosition.z;\\n\\t#endif\\n\\tvec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;\\n\\tvec2 rotatedPosition;\\n\\trotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;\\n\\trotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;\\n\\tmvPosition.xy += rotatedPosition;\\n\\tgl_Position = projectionMatrix * mvPosition;\\n\\t#include <logdepthbuf_vertex>\\n\\t#include <clipping_planes_vertex>\\n\\t#include <fog_vertex>\\n}\";\n\nconst fragment$1 = \"uniform vec3 diffuse;\\nuniform float opacity;\\n#include <common>\\n#include <uv_pars_fragment>\\n#include <map_pars_fragment>\\n#include <alphamap_pars_fragment>\\n#include <alphatest_pars_fragment>\\n#include <alphahash_pars_fragment>\\n#include <fog_pars_fragment>\\n#include <logdepthbuf_pars_fragment>\\n#include <clipping_planes_pars_fragment>\\nvoid main() {\\n\\tvec4 diffuseColor = vec4( diffuse, opacity );\\n\\t#include <clipping_planes_fragment>\\n\\tvec3 outgoingLight = vec3( 0.0 );\\n\\t#include <logdepthbuf_fragment>\\n\\t#include <map_fragment>\\n\\t#include <alphamap_fragment>\\n\\t#include <alphatest_fragment>\\n\\t#include <alphahash_fragment>\\n\\toutgoingLight = diffuseColor.rgb;\\n\\t#include <opaque_fragment>\\n\\t#include <tonemapping_fragment>\\n\\t#include <colorspace_fragment>\\n\\t#include <fog_fragment>\\n}\";\n\nconst ShaderChunk = {\n\talphahash_fragment: alphahash_fragment,\n\talphahash_pars_fragment: alphahash_pars_fragment,\n\talphamap_fragment: alphamap_fragment,\n\talphamap_pars_fragment: alphamap_pars_fragment,\n\talphatest_fragment: alphatest_fragment,\n\talphatest_pars_fragment: alphatest_pars_fragment,\n\taomap_fragment: aomap_fragment,\n\taomap_pars_fragment: aomap_pars_fragment,\n\tbatching_pars_vertex: batching_pars_vertex,\n\tbatching_vertex: batching_vertex,\n\tbegin_vertex: begin_vertex,\n\tbeginnormal_vertex: beginnormal_vertex,\n\tbsdfs: bsdfs,\n\tiridescence_fragment: iridescence_fragment,\n\tbumpmap_pars_fragment: bumpmap_pars_fragment,\n\tclipping_planes_fragment: clipping_planes_fragment,\n\tclipping_planes_pars_fragment: clipping_planes_pars_fragment,\n\tclipping_planes_pars_vertex: clipping_planes_pars_vertex,\n\tclipping_planes_vertex: clipping_planes_vertex,\n\tcolor_fragment: color_fragment,\n\tcolor_pars_fragment: color_pars_fragment,\n\tcolor_pars_vertex: color_pars_vertex,\n\tcolor_vertex: color_vertex,\n\tcommon: common,\n\tcube_uv_reflection_fragment: cube_uv_reflection_fragment,\n\tdefaultnormal_vertex: defaultnormal_vertex,\n\tdisplacementmap_pars_vertex: displacementmap_pars_vertex,\n\tdisplacementmap_vertex: displacementmap_vertex,\n\temissivemap_fragment: emissivemap_fragment,\n\temissivemap_pars_fragment: emissivemap_pars_fragment,\n\tcolorspace_fragment: colorspace_fragment,\n\tcolorspace_pars_fragment: colorspace_pars_fragment,\n\tenvmap_fragment: envmap_fragment,\n\tenvmap_common_pars_fragment: envmap_common_pars_fragment,\n\tenvmap_pars_fragment: envmap_pars_fragment,\n\tenvmap_pars_vertex: envmap_pars_vertex,\n\tenvmap_physical_pars_fragment: envmap_physical_pars_fragment,\n\tenvmap_vertex: envmap_vertex,\n\tfog_vertex: fog_vertex,\n\tfog_pars_vertex: fog_pars_vertex,\n\tfog_fragment: fog_fragment,\n\tfog_pars_fragment: fog_pars_fragment,\n\tgradientmap_pars_fragment: gradientmap_pars_fragment,\n\tlightmap_pars_fragment: lightmap_pars_fragment,\n\tlights_lambert_fragment: lights_lambert_fragment,\n\tlights_lambert_pars_fragment: lights_lambert_pars_fragment,\n\tlights_pars_begin: lights_pars_begin,\n\tlights_toon_fragment: lights_toon_fragment,\n\tlights_toon_pars_fragment: lights_toon_pars_fragment,\n\tlights_phong_fragment: lights_phong_fragment,\n\tlights_phong_pars_fragment: lights_phong_pars_fragment,\n\tlights_physical_fragment: lights_physical_fragment,\n\tlights_physical_pars_fragment: lights_physical_pars_fragment,\n\tlights_fragment_begin: lights_fragment_begin,\n\tlights_fragment_maps: lights_fragment_maps,\n\tlights_fragment_end: lights_fragment_end,\n\tlogdepthbuf_fragment: logdepthbuf_fragment,\n\tlogdepthbuf_pars_fragment: logdepthbuf_pars_fragment,\n\tlogdepthbuf_pars_vertex: logdepthbuf_pars_vertex,\n\tlogdepthbuf_vertex: logdepthbuf_vertex,\n\tmap_fragment: map_fragment,\n\tmap_pars_fragment: map_pars_fragment,\n\tmap_particle_fragment: map_particle_fragment,\n\tmap_particle_pars_fragment: map_particle_pars_fragment,\n\tmetalnessmap_fragment: metalnessmap_fragment,\n\tmetalnessmap_pars_fragment: metalnessmap_pars_fragment,\n\tmorphinstance_vertex: morphinstance_vertex,\n\tmorphcolor_vertex: morphcolor_vertex,\n\tmorphnormal_vertex: morphnormal_vertex,\n\tmorphtarget_pars_vertex: morphtarget_pars_vertex,\n\tmorphtarget_vertex: morphtarget_vertex,\n\tnormal_fragment_begin: normal_fragment_begin,\n\tnormal_fragment_maps: normal_fragment_maps,\n\tnormal_pars_fragment: normal_pars_fragment,\n\tnormal_pars_vertex: normal_pars_vertex,\n\tnormal_vertex: normal_vertex,\n\tnormalmap_pars_fragment: normalmap_pars_fragment,\n\tclearcoat_normal_fragment_begin: clearcoat_normal_fragment_begin,\n\tclearcoat_normal_fragment_maps: clearcoat_normal_fragment_maps,\n\tclearcoat_pars_fragment: clearcoat_pars_fragment,\n\tiridescence_pars_fragment: iridescence_pars_fragment,\n\topaque_fragment: opaque_fragment,\n\tpacking: packing,\n\tpremultiplied_alpha_fragment: premultiplied_alpha_fragment,\n\tproject_vertex: project_vertex,\n\tdithering_fragment: dithering_fragment,\n\tdithering_pars_fragment: dithering_pars_fragment,\n\troughnessmap_fragment: roughnessmap_fragment,\n\troughnessmap_pars_fragment: roughnessmap_pars_fragment,\n\tshadowmap_pars_fragment: shadowmap_pars_fragment,\n\tshadowmap_pars_vertex: shadowmap_pars_vertex,\n\tshadowmap_vertex: shadowmap_vertex,\n\tshadowmask_pars_fragment: shadowmask_pars_fragment,\n\tskinbase_vertex: skinbase_vertex,\n\tskinning_pars_vertex: skinning_pars_vertex,\n\tskinning_vertex: skinning_vertex,\n\tskinnormal_vertex: skinnormal_vertex,\n\tspecularmap_fragment: specularmap_fragment,\n\tspecularmap_pars_fragment: specularmap_pars_fragment,\n\ttonemapping_fragment: tonemapping_fragment,\n\ttonemapping_pars_fragment: tonemapping_pars_fragment,\n\ttransmission_fragment: transmission_fragment,\n\ttransmission_pars_fragment: transmission_pars_fragment,\n\tuv_pars_fragment: uv_pars_fragment,\n\tuv_pars_vertex: uv_pars_vertex,\n\tuv_vertex: uv_vertex,\n\tworldpos_vertex: worldpos_vertex,\n\n\tbackground_vert: vertex$h,\n\tbackground_frag: fragment$h,\n\tbackgroundCube_vert: vertex$g,\n\tbackgroundCube_frag: fragment$g,\n\tcube_vert: vertex$f,\n\tcube_frag: fragment$f,\n\tdepth_vert: vertex$e,\n\tdepth_frag: fragment$e,\n\tdistance_vert: vertex$d,\n\tdistance_frag: fragment$d,\n\tequirect_vert: vertex$c,\n\tequirect_frag: fragment$c,\n\tlinedashed_vert: vertex$b,\n\tlinedashed_frag: fragment$b,\n\tmeshbasic_vert: vertex$a,\n\tmeshbasic_frag: fragment$a,\n\tmeshlambert_vert: vertex$9,\n\tmeshlambert_frag: fragment$9,\n\tmeshmatcap_vert: vertex$8,\n\tmeshmatcap_frag: fragment$8,\n\tmeshnormal_vert: vertex$7,\n\tmeshnormal_frag: fragment$7,\n\tmeshphong_vert: vertex$6,\n\tmeshphong_frag: fragment$6,\n\tmeshphysical_vert: vertex$5,\n\tmeshphysical_frag: fragment$5,\n\tmeshtoon_vert: vertex$4,\n\tmeshtoon_frag: fragment$4,\n\tpoints_vert: vertex$3,\n\tpoints_frag: fragment$3,\n\tshadow_vert: vertex$2,\n\tshadow_frag: fragment$2,\n\tsprite_vert: vertex$1,\n\tsprite_frag: fragment$1\n};\n\n// Uniforms library for shared webgl shaders\nconst UniformsLib = {\n\n\tcommon: {\n\n\t\tdiffuse: { value: /*@__PURE__*/ new Color( 0xffffff ) },\n\t\topacity: { value: 1.0 },\n\n\t\tmap: { value: null },\n\t\tmapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\n\t\talphaMap: { value: null },\n\t\talphaMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\n\t\talphaTest: { value: 0 }\n\n\t},\n\n\tspecularmap: {\n\n\t\tspecularMap: { value: null },\n\t\tspecularMapTransform: { value: /*@__PURE__*/ new Matrix3() }\n\n\t},\n\n\tenvmap: {\n\n\t\tenvMap: { value: null },\n\t\tenvMapRotation: { value: /*@__PURE__*/ new Matrix3() },\n\t\tflipEnvMap: { value: -1 },\n\t\treflectivity: { value: 1.0 }, // basic, lambert, phong\n\t\tior: { value: 1.5 }, // physical\n\t\trefractionRatio: { value: 0.98 }, // basic, lambert, phong\n\t\tdfgLUT: { value: null } // DFG LUT for physically-based rendering\n\n\t},\n\n\taomap: {\n\n\t\taoMap: { value: null },\n\t\taoMapIntensity: { value: 1 },\n\t\taoMapTransform: { value: /*@__PURE__*/ new Matrix3() }\n\n\t},\n\n\tlightmap: {\n\n\t\tlightMap: { value: null },\n\t\tlightMapIntensity: { value: 1 },\n\t\tlightMapTransform: { value: /*@__PURE__*/ new Matrix3() }\n\n\t},\n\n\tbumpmap: {\n\n\t\tbumpMap: { value: null },\n\t\tbumpMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\tbumpScale: { value: 1 }\n\n\t},\n\n\tnormalmap: {\n\n\t\tnormalMap: { value: null },\n\t\tnormalMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\tnormalScale: { value: /*@__PURE__*/ new Vector2( 1, 1 ) }\n\n\t},\n\n\tdisplacementmap: {\n\n\t\tdisplacementMap: { value: null },\n\t\tdisplacementMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\tdisplacementScale: { value: 1 },\n\t\tdisplacementBias: { value: 0 }\n\n\t},\n\n\temissivemap: {\n\n\t\temissiveMap: { value: null },\n\t\temissiveMapTransform: { value: /*@__PURE__*/ new Matrix3() }\n\n\t},\n\n\tmetalnessmap: {\n\n\t\tmetalnessMap: { value: null },\n\t\tmetalnessMapTransform: { value: /*@__PURE__*/ new Matrix3() }\n\n\t},\n\n\troughnessmap: {\n\n\t\troughnessMap: { value: null },\n\t\troughnessMapTransform: { value: /*@__PURE__*/ new Matrix3() }\n\n\t},\n\n\tgradientmap: {\n\n\t\tgradientMap: { value: null }\n\n\t},\n\n\tfog: {\n\n\t\tfogDensity: { value: 0.00025 },\n\t\tfogNear: { value: 1 },\n\t\tfogFar: { value: 2000 },\n\t\tfogColor: { value: /*@__PURE__*/ new Color( 0xffffff ) }\n\n\t},\n\n\tlights: {\n\n\t\tambientLightColor: { value: [] },\n\n\t\tlightProbe: { value: [] },\n\n\t\tdirectionalLights: { value: [], properties: {\n\t\t\tdirection: {},\n\t\t\tcolor: {}\n\t\t} },\n\n\t\tdirectionalLightShadows: { value: [], properties: {\n\t\t\tshadowIntensity: 1,\n\t\t\tshadowBias: {},\n\t\t\tshadowNormalBias: {},\n\t\t\tshadowRadius: {},\n\t\t\tshadowMapSize: {}\n\t\t} },\n\n\t\tdirectionalShadowMap: { value: [] },\n\t\tdirectionalShadowMatrix: { value: [] },\n\n\t\tspotLights: { value: [], properties: {\n\t\t\tcolor: {},\n\t\t\tposition: {},\n\t\t\tdirection: {},\n\t\t\tdistance: {},\n\t\t\tconeCos: {},\n\t\t\tpenumbraCos: {},\n\t\t\tdecay: {}\n\t\t} },\n\n\t\tspotLightShadows: { value: [], properties: {\n\t\t\tshadowIntensity: 1,\n\t\t\tshadowBias: {},\n\t\t\tshadowNormalBias: {},\n\t\t\tshadowRadius: {},\n\t\t\tshadowMapSize: {}\n\t\t} },\n\n\t\tspotLightMap: { value: [] },\n\t\tspotShadowMap: { value: [] },\n\t\tspotLightMatrix: { value: [] },\n\n\t\tpointLights: { value: [], properties: {\n\t\t\tcolor: {},\n\t\t\tposition: {},\n\t\t\tdecay: {},\n\t\t\tdistance: {}\n\t\t} },\n\n\t\tpointLightShadows: { value: [], properties: {\n\t\t\tshadowIntensity: 1,\n\t\t\tshadowBias: {},\n\t\t\tshadowNormalBias: {},\n\t\t\tshadowRadius: {},\n\t\t\tshadowMapSize: {},\n\t\t\tshadowCameraNear: {},\n\t\t\tshadowCameraFar: {}\n\t\t} },\n\n\t\tpointShadowMap: { value: [] },\n\t\tpointShadowMatrix: { value: [] },\n\n\t\themisphereLights: { value: [], properties: {\n\t\t\tdirection: {},\n\t\t\tskyColor: {},\n\t\t\tgroundColor: {}\n\t\t} },\n\n\t\t// TODO (abelnation): RectAreaLight BRDF data needs to be moved from example to main src\n\t\trectAreaLights: { value: [], properties: {\n\t\t\tcolor: {},\n\t\t\tposition: {},\n\t\t\twidth: {},\n\t\t\theight: {}\n\t\t} },\n\n\t\tltc_1: { value: null },\n\t\tltc_2: { value: null }\n\n\t},\n\n\tpoints: {\n\n\t\tdiffuse: { value: /*@__PURE__*/ new Color( 0xffffff ) },\n\t\topacity: { value: 1.0 },\n\t\tsize: { value: 1.0 },\n\t\tscale: { value: 1.0 },\n\t\tmap: { value: null },\n\t\talphaMap: { value: null },\n\t\talphaMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\talphaTest: { value: 0 },\n\t\tuvTransform: { value: /*@__PURE__*/ new Matrix3() }\n\n\t},\n\n\tsprite: {\n\n\t\tdiffuse: { value: /*@__PURE__*/ new Color( 0xffffff ) },\n\t\topacity: { value: 1.0 },\n\t\tcenter: { value: /*@__PURE__*/ new Vector2( 0.5, 0.5 ) },\n\t\trotation: { value: 0.0 },\n\t\tmap: { value: null },\n\t\tmapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\talphaMap: { value: null },\n\t\talphaMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\talphaTest: { value: 0 }\n\n\t}\n\n};\n\nconst ShaderLib = {\n\n\tbasic: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.specularmap,\n\t\t\tUniformsLib.envmap,\n\t\t\tUniformsLib.aomap,\n\t\t\tUniformsLib.lightmap,\n\t\t\tUniformsLib.fog\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.meshbasic_vert,\n\t\tfragmentShader: ShaderChunk.meshbasic_frag\n\n\t},\n\n\tlambert: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.specularmap,\n\t\t\tUniformsLib.envmap,\n\t\t\tUniformsLib.aomap,\n\t\t\tUniformsLib.lightmap,\n\t\t\tUniformsLib.emissivemap,\n\t\t\tUniformsLib.bumpmap,\n\t\t\tUniformsLib.normalmap,\n\t\t\tUniformsLib.displacementmap,\n\t\t\tUniformsLib.fog,\n\t\t\tUniformsLib.lights,\n\t\t\t{\n\t\t\t\temissive: { value: /*@__PURE__*/ new Color( 0x000000 ) }\n\t\t\t}\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.meshlambert_vert,\n\t\tfragmentShader: ShaderChunk.meshlambert_frag\n\n\t},\n\n\tphong: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.specularmap,\n\t\t\tUniformsLib.envmap,\n\t\t\tUniformsLib.aomap,\n\t\t\tUniformsLib.lightmap,\n\t\t\tUniformsLib.emissivemap,\n\t\t\tUniformsLib.bumpmap,\n\t\t\tUniformsLib.normalmap,\n\t\t\tUniformsLib.displacementmap,\n\t\t\tUniformsLib.fog,\n\t\t\tUniformsLib.lights,\n\t\t\t{\n\t\t\t\temissive: { value: /*@__PURE__*/ new Color( 0x000000 ) },\n\t\t\t\tspecular: { value: /*@__PURE__*/ new Color( 0x111111 ) },\n\t\t\t\tshininess: { value: 30 }\n\t\t\t}\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.meshphong_vert,\n\t\tfragmentShader: ShaderChunk.meshphong_frag\n\n\t},\n\n\tstandard: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.envmap,\n\t\t\tUniformsLib.aomap,\n\t\t\tUniformsLib.lightmap,\n\t\t\tUniformsLib.emissivemap,\n\t\t\tUniformsLib.bumpmap,\n\t\t\tUniformsLib.normalmap,\n\t\t\tUniformsLib.displacementmap,\n\t\t\tUniformsLib.roughnessmap,\n\t\t\tUniformsLib.metalnessmap,\n\t\t\tUniformsLib.fog,\n\t\t\tUniformsLib.lights,\n\t\t\t{\n\t\t\t\temissive: { value: /*@__PURE__*/ new Color( 0x000000 ) },\n\t\t\t\troughness: { value: 1.0 },\n\t\t\t\tmetalness: { value: 0.0 },\n\t\t\t\tenvMapIntensity: { value: 1 }\n\t\t\t}\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.meshphysical_vert,\n\t\tfragmentShader: ShaderChunk.meshphysical_frag\n\n\t},\n\n\ttoon: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.aomap,\n\t\t\tUniformsLib.lightmap,\n\t\t\tUniformsLib.emissivemap,\n\t\t\tUniformsLib.bumpmap,\n\t\t\tUniformsLib.normalmap,\n\t\t\tUniformsLib.displacementmap,\n\t\t\tUniformsLib.gradientmap,\n\t\t\tUniformsLib.fog,\n\t\t\tUniformsLib.lights,\n\t\t\t{\n\t\t\t\temissive: { value: /*@__PURE__*/ new Color( 0x000000 ) }\n\t\t\t}\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.meshtoon_vert,\n\t\tfragmentShader: ShaderChunk.meshtoon_frag\n\n\t},\n\n\tmatcap: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.bumpmap,\n\t\t\tUniformsLib.normalmap,\n\t\t\tUniformsLib.displacementmap,\n\t\t\tUniformsLib.fog,\n\t\t\t{\n\t\t\t\tmatcap: { value: null }\n\t\t\t}\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.meshmatcap_vert,\n\t\tfragmentShader: ShaderChunk.meshmatcap_frag\n\n\t},\n\n\tpoints: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.points,\n\t\t\tUniformsLib.fog\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.points_vert,\n\t\tfragmentShader: ShaderChunk.points_frag\n\n\t},\n\n\tdashed: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.fog,\n\t\t\t{\n\t\t\t\tscale: { value: 1 },\n\t\t\t\tdashSize: { value: 1 },\n\t\t\t\ttotalSize: { value: 2 }\n\t\t\t}\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.linedashed_vert,\n\t\tfragmentShader: ShaderChunk.linedashed_frag\n\n\t},\n\n\tdepth: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.displacementmap\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.depth_vert,\n\t\tfragmentShader: ShaderChunk.depth_frag\n\n\t},\n\n\tnormal: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.bumpmap,\n\t\t\tUniformsLib.normalmap,\n\t\t\tUniformsLib.displacementmap,\n\t\t\t{\n\t\t\t\topacity: { value: 1.0 }\n\t\t\t}\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.meshnormal_vert,\n\t\tfragmentShader: ShaderChunk.meshnormal_frag\n\n\t},\n\n\tsprite: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.sprite,\n\t\t\tUniformsLib.fog\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.sprite_vert,\n\t\tfragmentShader: ShaderChunk.sprite_frag\n\n\t},\n\n\tbackground: {\n\n\t\tuniforms: {\n\t\t\tuvTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tt2D: { value: null },\n\t\t\tbackgroundIntensity: { value: 1 }\n\t\t},\n\n\t\tvertexShader: ShaderChunk.background_vert,\n\t\tfragmentShader: ShaderChunk.background_frag\n\n\t},\n\n\tbackgroundCube: {\n\n\t\tuniforms: {\n\t\t\tenvMap: { value: null },\n\t\t\tflipEnvMap: { value: -1 },\n\t\t\tbackgroundBlurriness: { value: 0 },\n\t\t\tbackgroundIntensity: { value: 1 },\n\t\t\tbackgroundRotation: { value: /*@__PURE__*/ new Matrix3() }\n\t\t},\n\n\t\tvertexShader: ShaderChunk.backgroundCube_vert,\n\t\tfragmentShader: ShaderChunk.backgroundCube_frag\n\n\t},\n\n\tcube: {\n\n\t\tuniforms: {\n\t\t\ttCube: { value: null },\n\t\t\ttFlip: { value: -1 },\n\t\t\topacity: { value: 1.0 }\n\t\t},\n\n\t\tvertexShader: ShaderChunk.cube_vert,\n\t\tfragmentShader: ShaderChunk.cube_frag\n\n\t},\n\n\tequirect: {\n\n\t\tuniforms: {\n\t\t\ttEquirect: { value: null },\n\t\t},\n\n\t\tvertexShader: ShaderChunk.equirect_vert,\n\t\tfragmentShader: ShaderChunk.equirect_frag\n\n\t},\n\n\tdistance: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.common,\n\t\t\tUniformsLib.displacementmap,\n\t\t\t{\n\t\t\t\treferencePosition: { value: /*@__PURE__*/ new Vector3() },\n\t\t\t\tnearDistance: { value: 1 },\n\t\t\t\tfarDistance: { value: 1000 }\n\t\t\t}\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.distance_vert,\n\t\tfragmentShader: ShaderChunk.distance_frag\n\n\t},\n\n\tshadow: {\n\n\t\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\t\tUniformsLib.lights,\n\t\t\tUniformsLib.fog,\n\t\t\t{\n\t\t\t\tcolor: { value: /*@__PURE__*/ new Color( 0x00000 ) },\n\t\t\t\topacity: { value: 1.0 }\n\t\t\t},\n\t\t] ),\n\n\t\tvertexShader: ShaderChunk.shadow_vert,\n\t\tfragmentShader: ShaderChunk.shadow_frag\n\n\t}\n\n};\n\nShaderLib.physical = {\n\n\tuniforms: /*@__PURE__*/ mergeUniforms( [\n\t\tShaderLib.standard.uniforms,\n\t\t{\n\t\t\tclearcoat: { value: 0 },\n\t\t\tclearcoatMap: { value: null },\n\t\t\tclearcoatMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tclearcoatNormalMap: { value: null },\n\t\t\tclearcoatNormalMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tclearcoatNormalScale: { value: /*@__PURE__*/ new Vector2( 1, 1 ) },\n\t\t\tclearcoatRoughness: { value: 0 },\n\t\t\tclearcoatRoughnessMap: { value: null },\n\t\t\tclearcoatRoughnessMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tdispersion: { value: 0 },\n\t\t\tiridescence: { value: 0 },\n\t\t\tiridescenceMap: { value: null },\n\t\t\tiridescenceMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tiridescenceIOR: { value: 1.3 },\n\t\t\tiridescenceThicknessMinimum: { value: 100 },\n\t\t\tiridescenceThicknessMaximum: { value: 400 },\n\t\t\tiridescenceThicknessMap: { value: null },\n\t\t\tiridescenceThicknessMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tsheen: { value: 0 },\n\t\t\tsheenColor: { value: /*@__PURE__*/ new Color( 0x000000 ) },\n\t\t\tsheenColorMap: { value: null },\n\t\t\tsheenColorMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tsheenRoughness: { value: 1 },\n\t\t\tsheenRoughnessMap: { value: null },\n\t\t\tsheenRoughnessMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\ttransmission: { value: 0 },\n\t\t\ttransmissionMap: { value: null },\n\t\t\ttransmissionMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\ttransmissionSamplerSize: { value: /*@__PURE__*/ new Vector2() },\n\t\t\ttransmissionSamplerMap: { value: null },\n\t\t\tthickness: { value: 0 },\n\t\t\tthicknessMap: { value: null },\n\t\t\tthicknessMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tattenuationDistance: { value: 0 },\n\t\t\tattenuationColor: { value: /*@__PURE__*/ new Color( 0x000000 ) },\n\t\t\tspecularColor: { value: /*@__PURE__*/ new Color( 1, 1, 1 ) },\n\t\t\tspecularColorMap: { value: null },\n\t\t\tspecularColorMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tspecularIntensity: { value: 1 },\n\t\t\tspecularIntensityMap: { value: null },\n\t\t\tspecularIntensityMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t\tanisotropyVector: { value: /*@__PURE__*/ new Vector2() },\n\t\t\tanisotropyMap: { value: null },\n\t\t\tanisotropyMapTransform: { value: /*@__PURE__*/ new Matrix3() },\n\t\t}\n\t] ),\n\n\tvertexShader: ShaderChunk.meshphysical_vert,\n\tfragmentShader: ShaderChunk.meshphysical_frag\n\n};\n\nconst _rgb = { r: 0, b: 0, g: 0 };\nconst _e1$1 = /*@__PURE__*/ new Euler();\nconst _m1$1 = /*@__PURE__*/ new Matrix4();\n\nfunction WebGLBackground( renderer, cubemaps, cubeuvmaps, state, objects, alpha, premultipliedAlpha ) {\n\n\tconst clearColor = new Color( 0x000000 );\n\tlet clearAlpha = alpha === true ? 0 : 1;\n\n\tlet planeMesh;\n\tlet boxMesh;\n\n\tlet currentBackground = null;\n\tlet currentBackgroundVersion = 0;\n\tlet currentTonemapping = null;\n\n\tfunction getBackground( scene ) {\n\n\t\tlet background = scene.isScene === true ? scene.background : null;\n\n\t\tif ( background && background.isTexture ) {\n\n\t\t\tconst usePMREM = scene.backgroundBlurriness > 0; // use PMREM if the user wants to blur the background\n\t\t\tbackground = ( usePMREM ? cubeuvmaps : cubemaps ).get( background );\n\n\t\t}\n\n\t\treturn background;\n\n\t}\n\n\tfunction render( scene ) {\n\n\t\tlet forceClear = false;\n\t\tconst background = getBackground( scene );\n\n\t\tif ( background === null ) {\n\n\t\t\tsetClear( clearColor, clearAlpha );\n\n\t\t} else if ( background && background.isColor ) {\n\n\t\t\tsetClear( background, 1 );\n\t\t\tforceClear = true;\n\n\t\t}\n\n\t\tconst environmentBlendMode = renderer.xr.getEnvironmentBlendMode();\n\n\t\tif ( environmentBlendMode === 'additive' ) {\n\n\t\t\tstate.buffers.color.setClear( 0, 0, 0, 1, premultipliedAlpha );\n\n\t\t} else if ( environmentBlendMode === 'alpha-blend' ) {\n\n\t\t\tstate.buffers.color.setClear( 0, 0, 0, 0, premultipliedAlpha );\n\n\t\t}\n\n\t\tif ( renderer.autoClear || forceClear ) {\n\n\t\t\t// buffers might not be writable which is required to ensure a correct clear\n\n\t\t\tstate.buffers.depth.setTest( true );\n\t\t\tstate.buffers.depth.setMask( true );\n\t\t\tstate.buffers.color.setMask( true );\n\n\t\t\trenderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );\n\n\t\t}\n\n\t}\n\n\tfunction addToRenderList( renderList, scene ) {\n\n\t\tconst background = getBackground( scene );\n\n\t\tif ( background && ( background.isCubeTexture || background.mapping === CubeUVReflectionMapping ) ) {\n\n\t\t\tif ( boxMesh === undefined ) {\n\n\t\t\t\tboxMesh = new Mesh(\n\t\t\t\t\tnew BoxGeometry( 1, 1, 1 ),\n\t\t\t\t\tnew ShaderMaterial( {\n\t\t\t\t\t\tname: 'BackgroundCubeMaterial',\n\t\t\t\t\t\tuniforms: cloneUniforms( ShaderLib.backgroundCube.uniforms ),\n\t\t\t\t\t\tvertexShader: ShaderLib.backgroundCube.vertexShader,\n\t\t\t\t\t\tfragmentShader: ShaderLib.backgroundCube.fragmentShader,\n\t\t\t\t\t\tside: BackSide,\n\t\t\t\t\t\tdepthTest: false,\n\t\t\t\t\t\tdepthWrite: false,\n\t\t\t\t\t\tfog: false,\n\t\t\t\t\t\tallowOverride: false\n\t\t\t\t\t} )\n\t\t\t\t);\n\n\t\t\t\tboxMesh.geometry.deleteAttribute( 'normal' );\n\t\t\t\tboxMesh.geometry.deleteAttribute( 'uv' );\n\n\t\t\t\tboxMesh.onBeforeRender = function ( renderer, scene, camera ) {\n\n\t\t\t\t\tthis.matrixWorld.copyPosition( camera.matrixWorld );\n\n\t\t\t\t};\n\n\t\t\t\t// add \"envMap\" material property so the renderer can evaluate it like for built-in materials\n\t\t\t\tObject.defineProperty( boxMesh.material, 'envMap', {\n\n\t\t\t\t\tget: function () {\n\n\t\t\t\t\t\treturn this.uniforms.envMap.value;\n\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t\tobjects.update( boxMesh );\n\n\t\t\t}\n\n\t\t\t_e1$1.copy( scene.backgroundRotation );\n\n\t\t\t// accommodate left-handed frame\n\t\t\t_e1$1.x *= -1; _e1$1.y *= -1; _e1$1.z *= -1;\n\n\t\t\tif ( background.isCubeTexture && background.isRenderTargetTexture === false ) {\n\n\t\t\t\t// environment maps which are not cube render targets or PMREMs follow a different convention\n\t\t\t\t_e1$1.y *= -1;\n\t\t\t\t_e1$1.z *= -1;\n\n\t\t\t}\n\n\t\t\tboxMesh.material.uniforms.envMap.value = background;\n\t\t\tboxMesh.material.uniforms.flipEnvMap.value = ( background.isCubeTexture && background.isRenderTargetTexture === false ) ? -1 : 1;\n\t\t\tboxMesh.material.uniforms.backgroundBlurriness.value = scene.backgroundBlurriness;\n\t\t\tboxMesh.material.uniforms.backgroundIntensity.value = scene.backgroundIntensity;\n\t\t\tboxMesh.material.uniforms.backgroundRotation.value.setFromMatrix4( _m1$1.makeRotationFromEuler( _e1$1 ) );\n\t\t\tboxMesh.material.toneMapped = ColorManagement.getTransfer( background.colorSpace ) !== SRGBTransfer;\n\n\t\t\tif ( currentBackground !== background ||\n\t\t\t\tcurrentBackgroundVersion !== background.version ||\n\t\t\t\tcurrentTonemapping !== renderer.toneMapping ) {\n\n\t\t\t\tboxMesh.material.needsUpdate = true;\n\n\t\t\t\tcurrentBackground = background;\n\t\t\t\tcurrentBackgroundVersion = background.version;\n\t\t\t\tcurrentTonemapping = renderer.toneMapping;\n\n\t\t\t}\n\n\t\t\tboxMesh.layers.enableAll();\n\n\t\t\t// push to the pre-sorted opaque render list\n\t\t\trenderList.unshift( boxMesh, boxMesh.geometry, boxMesh.material, 0, 0, null );\n\n\t\t} else if ( background && background.isTexture ) {\n\n\t\t\tif ( planeMesh === undefined ) {\n\n\t\t\t\tplaneMesh = new Mesh(\n\t\t\t\t\tnew PlaneGeometry( 2, 2 ),\n\t\t\t\t\tnew ShaderMaterial( {\n\t\t\t\t\t\tname: 'BackgroundMaterial',\n\t\t\t\t\t\tuniforms: cloneUniforms( ShaderLib.background.uniforms ),\n\t\t\t\t\t\tvertexShader: ShaderLib.background.vertexShader,\n\t\t\t\t\t\tfragmentShader: ShaderLib.background.fragmentShader,\n\t\t\t\t\t\tside: FrontSide,\n\t\t\t\t\t\tdepthTest: false,\n\t\t\t\t\t\tdepthWrite: false,\n\t\t\t\t\t\tfog: false,\n\t\t\t\t\t\tallowOverride: false\n\t\t\t\t\t} )\n\t\t\t\t);\n\n\t\t\t\tplaneMesh.geometry.deleteAttribute( 'normal' );\n\n\t\t\t\t// add \"map\" material property so the renderer can evaluate it like for built-in materials\n\t\t\t\tObject.defineProperty( planeMesh.material, 'map', {\n\n\t\t\t\t\tget: function () {\n\n\t\t\t\t\t\treturn this.uniforms.t2D.value;\n\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t\tobjects.update( planeMesh );\n\n\t\t\t}\n\n\t\t\tplaneMesh.material.uniforms.t2D.value = background;\n\t\t\tplaneMesh.material.uniforms.backgroundIntensity.value = scene.backgroundIntensity;\n\t\t\tplaneMesh.material.toneMapped = ColorManagement.getTransfer( background.colorSpace ) !== SRGBTransfer;\n\n\t\t\tif ( background.matrixAutoUpdate === true ) {\n\n\t\t\t\tbackground.updateMatrix();\n\n\t\t\t}\n\n\t\t\tplaneMesh.material.uniforms.uvTransform.value.copy( background.matrix );\n\n\t\t\tif ( currentBackground !== background ||\n\t\t\t\tcurrentBackgroundVersion !== background.version ||\n\t\t\t\tcurrentTonemapping !== renderer.toneMapping ) {\n\n\t\t\t\tplaneMesh.material.needsUpdate = true;\n\n\t\t\t\tcurrentBackground = background;\n\t\t\t\tcurrentBackgroundVersion = background.version;\n\t\t\t\tcurrentTonemapping = renderer.toneMapping;\n\n\t\t\t}\n\n\t\t\tplaneMesh.layers.enableAll();\n\n\t\t\t// push to the pre-sorted opaque render list\n\t\t\trenderList.unshift( planeMesh, planeMesh.geometry, planeMesh.material, 0, 0, null );\n\n\t\t}\n\n\t}\n\n\tfunction setClear( color, alpha ) {\n\n\t\tcolor.getRGB( _rgb, getUnlitUniformColorSpace( renderer ) );\n\n\t\tstate.buffers.color.setClear( _rgb.r, _rgb.g, _rgb.b, alpha, premultipliedAlpha );\n\n\t}\n\n\tfunction dispose() {\n\n\t\tif ( boxMesh !== undefined ) {\n\n\t\t\tboxMesh.geometry.dispose();\n\t\t\tboxMesh.material.dispose();\n\n\t\t\tboxMesh = undefined;\n\n\t\t}\n\n\t\tif ( planeMesh !== undefined ) {\n\n\t\t\tplaneMesh.geometry.dispose();\n\t\t\tplaneMesh.material.dispose();\n\n\t\t\tplaneMesh = undefined;\n\n\t\t}\n\n\t}\n\n\treturn {\n\n\t\tgetClearColor: function () {\n\n\t\t\treturn clearColor;\n\n\t\t},\n\t\tsetClearColor: function ( color, alpha = 1 ) {\n\n\t\t\tclearColor.set( color );\n\t\t\tclearAlpha = alpha;\n\t\t\tsetClear( clearColor, clearAlpha );\n\n\t\t},\n\t\tgetClearAlpha: function () {\n\n\t\t\treturn clearAlpha;\n\n\t\t},\n\t\tsetClearAlpha: function ( alpha ) {\n\n\t\t\tclearAlpha = alpha;\n\t\t\tsetClear( clearColor, clearAlpha );\n\n\t\t},\n\t\trender: render,\n\t\taddToRenderList: addToRenderList,\n\t\tdispose: dispose\n\n\t};\n\n}\n\nfunction WebGLBindingStates( gl, attributes ) {\n\n\tconst maxVertexAttributes = gl.getParameter( gl.MAX_VERTEX_ATTRIBS );\n\n\tconst bindingStates = {};\n\n\tconst defaultState = createBindingState( null );\n\tlet currentState = defaultState;\n\tlet forceUpdate = false;\n\n\tfunction setup( object, material, program, geometry, index ) {\n\n\t\tlet updateBuffers = false;\n\n\t\tconst state = getBindingState( geometry, program, material );\n\n\t\tif ( currentState !== state ) {\n\n\t\t\tcurrentState = state;\n\t\t\tbindVertexArrayObject( currentState.object );\n\n\t\t}\n\n\t\tupdateBuffers = needsUpdate( object, geometry, program, index );\n\n\t\tif ( updateBuffers ) saveCache( object, geometry, program, index );\n\n\t\tif ( index !== null ) {\n\n\t\t\tattributes.update( index, gl.ELEMENT_ARRAY_BUFFER );\n\n\t\t}\n\n\t\tif ( updateBuffers || forceUpdate ) {\n\n\t\t\tforceUpdate = false;\n\n\t\t\tsetupVertexAttributes( object, material, program, geometry );\n\n\t\t\tif ( index !== null ) {\n\n\t\t\t\tgl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, attributes.get( index ).buffer );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tfunction createVertexArrayObject() {\n\n\t\treturn gl.createVertexArray();\n\n\t}\n\n\tfunction bindVertexArrayObject( vao ) {\n\n\t\treturn gl.bindVertexArray( vao );\n\n\t}\n\n\tfunction deleteVertexArrayObject( vao ) {\n\n\t\treturn gl.deleteVertexArray( vao );\n\n\t}\n\n\tfunction getBindingState( geometry, program, material ) {\n\n\t\tconst wireframe = ( material.wireframe === true );\n\n\t\tlet programMap = bindingStates[ geometry.id ];\n\n\t\tif ( programMap === undefined ) {\n\n\t\t\tprogramMap = {};\n\t\t\tbindingStates[ geometry.id ] = programMap;\n\n\t\t}\n\n\t\tlet stateMap = programMap[ program.id ];\n\n\t\tif ( stateMap === undefined ) {\n\n\t\t\tstateMap = {};\n\t\t\tprogramMap[ program.id ] = stateMap;\n\n\t\t}\n\n\t\tlet state = stateMap[ wireframe ];\n\n\t\tif ( state === undefined ) {\n\n\t\t\tstate = createBindingState( createVertexArrayObject() );\n\t\t\tstateMap[ wireframe ] = state;\n\n\t\t}\n\n\t\treturn state;\n\n\t}\n\n\tfunction createBindingState( vao ) {\n\n\t\tconst newAttributes = [];\n\t\tconst enabledAttributes = [];\n\t\tconst attributeDivisors = [];\n\n\t\tfor ( let i = 0; i < maxVertexAttributes; i ++ ) {\n\n\t\t\tnewAttributes[ i ] = 0;\n\t\t\tenabledAttributes[ i ] = 0;\n\t\t\tattributeDivisors[ i ] = 0;\n\n\t\t}\n\n\t\treturn {\n\n\t\t\t// for backward compatibility on non-VAO support browser\n\t\t\tgeometry: null,\n\t\t\tprogram: null,\n\t\t\twireframe: false,\n\n\t\t\tnewAttributes: newAttributes,\n\t\t\tenabledAttributes: enabledAttributes,\n\t\t\tattributeDivisors: attributeDivisors,\n\t\t\tobject: vao,\n\t\t\tattributes: {},\n\t\t\tindex: null\n\n\t\t};\n\n\t}\n\n\tfunction needsUpdate( object, geometry, program, index ) {\n\n\t\tconst cachedAttributes = currentState.attributes;\n\t\tconst geometryAttributes = geometry.attributes;\n\n\t\tlet attributesNum = 0;\n\n\t\tconst programAttributes = program.getAttributes();\n\n\t\tfor ( const name in programAttributes ) {\n\n\t\t\tconst programAttribute = programAttributes[ name ];\n\n\t\t\tif ( programAttribute.location >= 0 ) {\n\n\t\t\t\tconst cachedAttribute = cachedAttributes[ name ];\n\t\t\t\tlet geometryAttribute = geometryAttributes[ name ];\n\n\t\t\t\tif ( geometryAttribute === undefined ) {\n\n\t\t\t\t\tif ( name === 'instanceMatrix' && object.instanceMatrix ) geometryAttribute = object.instanceMatrix;\n\t\t\t\t\tif ( name === 'instanceColor' && object.instanceColor ) geometryAttribute = object.instanceColor;\n\n\t\t\t\t}\n\n\t\t\t\tif ( cachedAttribute === undefined ) return true;\n\n\t\t\t\tif ( cachedAttribute.attribute !== geometryAttribute ) return true;\n\n\t\t\t\tif ( geometryAttribute && cachedAttribute.data !== geometryAttribute.data ) return true;\n\n\t\t\t\tattributesNum ++;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( currentState.attributesNum !== attributesNum ) return true;\n\n\t\tif ( currentState.index !== index ) return true;\n\n\t\treturn false;\n\n\t}\n\n\tfunction saveCache( object, geometry, program, index ) {\n\n\t\tconst cache = {};\n\t\tconst attributes = geometry.attributes;\n\t\tlet attributesNum = 0;\n\n\t\tconst programAttributes = program.getAttributes();\n\n\t\tfor ( const name in programAttributes ) {\n\n\t\t\tconst programAttribute = programAttributes[ name ];\n\n\t\t\tif ( programAttribute.location >= 0 ) {\n\n\t\t\t\tlet attribute = attributes[ name ];\n\n\t\t\t\tif ( attribute === undefined ) {\n\n\t\t\t\t\tif ( name === 'instanceMatrix' && object.instanceMatrix ) attribute = object.instanceMatrix;\n\t\t\t\t\tif ( name === 'instanceColor' && object.instanceColor ) attribute = object.instanceColor;\n\n\t\t\t\t}\n\n\t\t\t\tconst data = {};\n\t\t\t\tdata.attribute = attribute;\n\n\t\t\t\tif ( attribute && attribute.data ) {\n\n\t\t\t\t\tdata.data = attribute.data;\n\n\t\t\t\t}\n\n\t\t\t\tcache[ name ] = data;\n\n\t\t\t\tattributesNum ++;\n\n\t\t\t}\n\n\t\t}\n\n\t\tcurrentState.attributes = cache;\n\t\tcurrentState.attributesNum = attributesNum;\n\n\t\tcurrentState.index = index;\n\n\t}\n\n\tfunction initAttributes() {\n\n\t\tconst newAttributes = currentState.newAttributes;\n\n\t\tfor ( let i = 0, il = newAttributes.length; i < il; i ++ ) {\n\n\t\t\tnewAttributes[ i ] = 0;\n\n\t\t}\n\n\t}\n\n\tfunction enableAttribute( attribute ) {\n\n\t\tenableAttributeAndDivisor( attribute, 0 );\n\n\t}\n\n\tfunction enableAttributeAndDivisor( attribute, meshPerAttribute ) {\n\n\t\tconst newAttributes = currentState.newAttributes;\n\t\tconst enabledAttributes = currentState.enabledAttributes;\n\t\tconst attributeDivisors = currentState.attributeDivisors;\n\n\t\tnewAttributes[ attribute ] = 1;\n\n\t\tif ( enabledAttributes[ attribute ] === 0 ) {\n\n\t\t\tgl.enableVertexAttribArray( attribute );\n\t\t\tenabledAttributes[ attribute ] = 1;\n\n\t\t}\n\n\t\tif ( attributeDivisors[ attribute ] !== meshPerAttribute ) {\n\n\t\t\tgl.vertexAttribDivisor( attribute, meshPerAttribute );\n\t\t\tattributeDivisors[ attribute ] = meshPerAttribute;\n\n\t\t}\n\n\t}\n\n\tfunction disableUnusedAttributes() {\n\n\t\tconst newAttributes = currentState.newAttributes;\n\t\tconst enabledAttributes = currentState.enabledAttributes;\n\n\t\tfor ( let i = 0, il = enabledAttributes.length; i < il; i ++ ) {\n\n\t\t\tif ( enabledAttributes[ i ] !== newAttributes[ i ] ) {\n\n\t\t\t\tgl.disableVertexAttribArray( i );\n\t\t\t\tenabledAttributes[ i ] = 0;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tfunction vertexAttribPointer( index, size, type, normalized, stride, offset, integer ) {\n\n\t\tif ( integer === true ) {\n\n\t\t\tgl.vertexAttribIPointer( index, size, type, stride, offset );\n\n\t\t} else {\n\n\t\t\tgl.vertexAttribPointer( index, size, type, normalized, stride, offset );\n\n\t\t}\n\n\t}\n\n\tfunction setupVertexAttributes( object, material, program, geometry ) {\n\n\t\tinitAttributes();\n\n\t\tconst geometryAttributes = geometry.attributes;\n\n\t\tconst programAttributes = program.getAttributes();\n\n\t\tconst materialDefaultAttributeValues = material.defaultAttributeValues;\n\n\t\tfor ( const name in programAttributes ) {\n\n\t\t\tconst programAttribute = programAttributes[ name ];\n\n\t\t\tif ( programAttribute.location >= 0 ) {\n\n\t\t\t\tlet geometryAttribute = geometryAttributes[ name ];\n\n\t\t\t\tif ( geometryAttribute === undefined ) {\n\n\t\t\t\t\tif ( name === 'instanceMatrix' && object.instanceMatrix ) geometryAttribute = object.instanceMatrix;\n\t\t\t\t\tif ( name === 'instanceColor' && object.instanceColor ) geometryAttribute = object.instanceColor;\n\n\t\t\t\t}\n\n\t\t\t\tif ( geometryAttribute !== undefined ) {\n\n\t\t\t\t\tconst normalized = geometryAttribute.normalized;\n\t\t\t\t\tconst size = geometryAttribute.itemSize;\n\n\t\t\t\t\tconst attribute = attributes.get( geometryAttribute );\n\n\t\t\t\t\t// TODO Attribute may not be available on context restore\n\n\t\t\t\t\tif ( attribute === undefined ) continue;\n\n\t\t\t\t\tconst buffer = attribute.buffer;\n\t\t\t\t\tconst type = attribute.type;\n\t\t\t\t\tconst bytesPerElement = attribute.bytesPerElement;\n\n\t\t\t\t\t// check for integer attributes\n\n\t\t\t\t\tconst integer = ( type === gl.INT || type === gl.UNSIGNED_INT || geometryAttribute.gpuType === IntType );\n\n\t\t\t\t\tif ( geometryAttribute.isInterleavedBufferAttribute ) {\n\n\t\t\t\t\t\tconst data = geometryAttribute.data;\n\t\t\t\t\t\tconst stride = data.stride;\n\t\t\t\t\t\tconst offset = geometryAttribute.offset;\n\n\t\t\t\t\t\tif ( data.isInstancedInterleavedBuffer ) {\n\n\t\t\t\t\t\t\tfor ( let i = 0; i < programAttribute.locationSize; i ++ ) {\n\n\t\t\t\t\t\t\t\tenableAttributeAndDivisor( programAttribute.location + i, data.meshPerAttribute );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif ( object.isInstancedMesh !== true && geometry._maxInstanceCount === undefined ) {\n\n\t\t\t\t\t\t\t\tgeometry._maxInstanceCount = data.meshPerAttribute * data.count;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tfor ( let i = 0; i < programAttribute.locationSize; i ++ ) {\n\n\t\t\t\t\t\t\t\tenableAttribute( programAttribute.location + i );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgl.bindBuffer( gl.ARRAY_BUFFER, buffer );\n\n\t\t\t\t\t\tfor ( let i = 0; i < programAttribute.locationSize; i ++ ) {\n\n\t\t\t\t\t\t\tvertexAttribPointer(\n\t\t\t\t\t\t\t\tprogramAttribute.location + i,\n\t\t\t\t\t\t\t\tsize / programAttribute.locationSize,\n\t\t\t\t\t\t\t\ttype,\n\t\t\t\t\t\t\t\tnormalized,\n\t\t\t\t\t\t\t\tstride * bytesPerElement,\n\t\t\t\t\t\t\t\t( offset + ( size / programAttribute.locationSize ) * i ) * bytesPerElement,\n\t\t\t\t\t\t\t\tinteger\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tif ( geometryAttribute.isInstancedBufferAttribute ) {\n\n\t\t\t\t\t\t\tfor ( let i = 0; i < programAttribute.locationSize; i ++ ) {\n\n\t\t\t\t\t\t\t\tenableAttributeAndDivisor( programAttribute.location + i, geometryAttribute.meshPerAttribute );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif ( object.isInstancedMesh !== true && geometry._maxInstanceCount === undefined ) {\n\n\t\t\t\t\t\t\t\tgeometry._maxInstanceCount = geometryAttribute.meshPerAttribute * geometryAttribute.count;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tfor ( let i = 0; i < programAttribute.locationSize; i ++ ) {\n\n\t\t\t\t\t\t\t\tenableAttribute( programAttribute.location + i );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgl.bindBuffer( gl.ARRAY_BUFFER, buffer );\n\n\t\t\t\t\t\tfor ( let i = 0; i < programAttribute.locationSize; i ++ ) {\n\n\t\t\t\t\t\t\tvertexAttribPointer(\n\t\t\t\t\t\t\t\tprogramAttribute.location + i,\n\t\t\t\t\t\t\t\tsize / programAttribute.locationSize,\n\t\t\t\t\t\t\t\ttype,\n\t\t\t\t\t\t\t\tnormalized,\n\t\t\t\t\t\t\t\tsize * bytesPerElement,\n\t\t\t\t\t\t\t\t( size / programAttribute.locationSize ) * i * bytesPerElement,\n\t\t\t\t\t\t\t\tinteger\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t} else if ( materialDefaultAttributeValues !== undefined ) {\n\n\t\t\t\t\tconst value = materialDefaultAttributeValues[ name ];\n\n\t\t\t\t\tif ( value !== undefined ) {\n\n\t\t\t\t\t\tswitch ( value.length ) {\n\n\t\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\t\tgl.vertexAttrib2fv( programAttribute.location, value );\n\t\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t\tcase 3:\n\t\t\t\t\t\t\t\tgl.vertexAttrib3fv( programAttribute.location, value );\n\t\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t\tcase 4:\n\t\t\t\t\t\t\t\tgl.vertexAttrib4fv( programAttribute.location, value );\n\t\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tgl.vertexAttrib1fv( programAttribute.location, value );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tdisableUnusedAttributes();\n\n\t}\n\n\tfunction dispose() {\n\n\t\treset();\n\n\t\tfor ( const geometryId in bindingStates ) {\n\n\t\t\tconst programMap = bindingStates[ geometryId ];\n\n\t\t\tfor ( const programId in programMap ) {\n\n\t\t\t\tconst stateMap = programMap[ programId ];\n\n\t\t\t\tfor ( const wireframe in stateMap ) {\n\n\t\t\t\t\tdeleteVertexArrayObject( stateMap[ wireframe ].object );\n\n\t\t\t\t\tdelete stateMap[ wireframe ];\n\n\t\t\t\t}\n\n\t\t\t\tdelete programMap[ programId ];\n\n\t\t\t}\n\n\t\t\tdelete bindingStates[ geometryId ];\n\n\t\t}\n\n\t}\n\n\tfunction releaseStatesOfGeometry( geometry ) {\n\n\t\tif ( bindingStates[ geometry.id ] === undefined ) return;\n\n\t\tconst programMap = bindingStates[ geometry.id ];\n\n\t\tfor ( const programId in programMap ) {\n\n\t\t\tconst stateMap = programMap[ programId ];\n\n\t\t\tfor ( const wireframe in stateMap ) {\n\n\t\t\t\tdeleteVertexArrayObject( stateMap[ wireframe ].object );\n\n\t\t\t\tdelete stateMap[ wireframe ];\n\n\t\t\t}\n\n\t\t\tdelete programMap[ programId ];\n\n\t\t}\n\n\t\tdelete bindingStates[ geometry.id ];\n\n\t}\n\n\tfunction releaseStatesOfProgram( program ) {\n\n\t\tfor ( const geometryId in bindingStates ) {\n\n\t\t\tconst programMap = bindingStates[ geometryId ];\n\n\t\t\tif ( programMap[ program.id ] === undefined ) continue;\n\n\t\t\tconst stateMap = programMap[ program.id ];\n\n\t\t\tfor ( const wireframe in stateMap ) {\n\n\t\t\t\tdeleteVertexArrayObject( stateMap[ wireframe ].object );\n\n\t\t\t\tdelete stateMap[ wireframe ];\n\n\t\t\t}\n\n\t\t\tdelete programMap[ program.id ];\n\n\t\t}\n\n\t}\n\n\tfunction reset() {\n\n\t\tresetDefaultState();\n\t\tforceUpdate = true;\n\n\t\tif ( currentState === defaultState ) return;\n\n\t\tcurrentState = defaultState;\n\t\tbindVertexArrayObject( currentState.object );\n\n\t}\n\n\t// for backward-compatibility\n\n\tfunction resetDefaultState() {\n\n\t\tdefaultState.geometry = null;\n\t\tdefaultState.program = null;\n\t\tdefaultState.wireframe = false;\n\n\t}\n\n\treturn {\n\n\t\tsetup: setup,\n\t\treset: reset,\n\t\tresetDefaultState: resetDefaultState,\n\t\tdispose: dispose,\n\t\treleaseStatesOfGeometry: releaseStatesOfGeometry,\n\t\treleaseStatesOfProgram: releaseStatesOfProgram,\n\n\t\tinitAttributes: initAttributes,\n\t\tenableAttribute: enableAttribute,\n\t\tdisableUnusedAttributes: disableUnusedAttributes\n\n\t};\n\n}\n\nfunction WebGLBufferRenderer( gl, extensions, info ) {\n\n\tlet mode;\n\n\tfunction setMode( value ) {\n\n\t\tmode = value;\n\n\t}\n\n\tfunction render( start, count ) {\n\n\t\tgl.drawArrays( mode, start, count );\n\n\t\tinfo.update( count, mode, 1 );\n\n\t}\n\n\tfunction renderInstances( start, count, primcount ) {\n\n\t\tif ( primcount === 0 ) return;\n\n\t\tgl.drawArraysInstanced( mode, start, count, primcount );\n\n\t\tinfo.update( count, mode, primcount );\n\n\t}\n\n\tfunction renderMultiDraw( starts, counts, drawCount ) {\n\n\t\tif ( drawCount === 0 ) return;\n\n\t\tconst extension = extensions.get( 'WEBGL_multi_draw' );\n\t\textension.multiDrawArraysWEBGL( mode, starts, 0, counts, 0, drawCount );\n\n\t\tlet elementCount = 0;\n\t\tfor ( let i = 0; i < drawCount; i ++ ) {\n\n\t\t\telementCount += counts[ i ];\n\n\t\t}\n\n\t\tinfo.update( elementCount, mode, 1 );\n\n\t}\n\n\tfunction renderMultiDrawInstances( starts, counts, drawCount, primcount ) {\n\n\t\tif ( drawCount === 0 ) return;\n\n\t\tconst extension = extensions.get( 'WEBGL_multi_draw' );\n\n\t\tif ( extension === null ) {\n\n\t\t\tfor ( let i = 0; i < starts.length; i ++ ) {\n\n\t\t\t\trenderInstances( starts[ i ], counts[ i ], primcount[ i ] );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\textension.multiDrawArraysInstancedWEBGL( mode, starts, 0, counts, 0, primcount, 0, drawCount );\n\n\t\t\tlet elementCount = 0;\n\t\t\tfor ( let i = 0; i < drawCount; i ++ ) {\n\n\t\t\t\telementCount += counts[ i ] * primcount[ i ];\n\n\t\t\t}\n\n\t\t\tinfo.update( elementCount, mode, 1 );\n\n\t\t}\n\n\t}\n\n\t//\n\n\tthis.setMode = setMode;\n\tthis.render = render;\n\tthis.renderInstances = renderInstances;\n\tthis.renderMultiDraw = renderMultiDraw;\n\tthis.renderMultiDrawInstances = renderMultiDrawInstances;\n\n}\n\nfunction WebGLCapabilities( gl, extensions, parameters, utils ) {\n\n\tlet maxAnisotropy;\n\n\tfunction getMaxAnisotropy() {\n\n\t\tif ( maxAnisotropy !== undefined ) return maxAnisotropy;\n\n\t\tif ( extensions.has( 'EXT_texture_filter_anisotropic' ) === true ) {\n\n\t\t\tconst extension = extensions.get( 'EXT_texture_filter_anisotropic' );\n\n\t\t\tmaxAnisotropy = gl.getParameter( extension.MAX_TEXTURE_MAX_ANISOTROPY_EXT );\n\n\t\t} else {\n\n\t\t\tmaxAnisotropy = 0;\n\n\t\t}\n\n\t\treturn maxAnisotropy;\n\n\t}\n\n\tfunction textureFormatReadable( textureFormat ) {\n\n\t\tif ( textureFormat !== RGBAFormat && utils.convert( textureFormat ) !== gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_FORMAT ) ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\tfunction textureTypeReadable( textureType ) {\n\n\t\tconst halfFloatSupportedByExt = ( textureType === HalfFloatType ) && ( extensions.has( 'EXT_color_buffer_half_float' ) || extensions.has( 'EXT_color_buffer_float' ) );\n\n\t\tif ( textureType !== UnsignedByteType && utils.convert( textureType ) !== gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_TYPE ) && // Edge and Chrome Mac < 52 (#9513)\n\t\t\ttextureType !== FloatType && ! halfFloatSupportedByExt ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\tfunction getMaxPrecision( precision ) {\n\n\t\tif ( precision === 'highp' ) {\n\n\t\t\tif ( gl.getShaderPrecisionFormat( gl.VERTEX_SHADER, gl.HIGH_FLOAT ).precision > 0 &&\n\t\t\t\tgl.getShaderPrecisionFormat( gl.FRAGMENT_SHADER, gl.HIGH_FLOAT ).precision > 0 ) {\n\n\t\t\t\treturn 'highp';\n\n\t\t\t}\n\n\t\t\tprecision = 'mediump';\n\n\t\t}\n\n\t\tif ( precision === 'mediump' ) {\n\n\t\t\tif ( gl.getShaderPrecisionFormat( gl.VERTEX_SHADER, gl.MEDIUM_FLOAT ).precision > 0 &&\n\t\t\t\tgl.getShaderPrecisionFormat( gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT ).precision > 0 ) {\n\n\t\t\t\treturn 'mediump';\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn 'lowp';\n\n\t}\n\n\tlet precision = parameters.precision !== undefined ? parameters.precision : 'highp';\n\tconst maxPrecision = getMaxPrecision( precision );\n\n\tif ( maxPrecision !== precision ) {\n\n\t\twarn( 'WebGLRenderer:', precision, 'not supported, using', maxPrecision, 'instead.' );\n\t\tprecision = maxPrecision;\n\n\t}\n\n\tconst logarithmicDepthBuffer = parameters.logarithmicDepthBuffer === true;\n\tconst reversedDepthBuffer = parameters.reversedDepthBuffer === true && extensions.has( 'EXT_clip_control' );\n\n\tconst maxTextures = gl.getParameter( gl.MAX_TEXTURE_IMAGE_UNITS );\n\tconst maxVertexTextures = gl.getParameter( gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS );\n\tconst maxTextureSize = gl.getParameter( gl.MAX_TEXTURE_SIZE );\n\tconst maxCubemapSize = gl.getParameter( gl.MAX_CUBE_MAP_TEXTURE_SIZE );\n\n\tconst maxAttributes = gl.getParameter( gl.MAX_VERTEX_ATTRIBS );\n\tconst maxVertexUniforms = gl.getParameter( gl.MAX_VERTEX_UNIFORM_VECTORS );\n\tconst maxVaryings = gl.getParameter( gl.MAX_VARYING_VECTORS );\n\tconst maxFragmentUniforms = gl.getParameter( gl.MAX_FRAGMENT_UNIFORM_VECTORS );\n\n\tconst maxSamples = gl.getParameter( gl.MAX_SAMPLES );\n\tconst samples = gl.getParameter( gl.SAMPLES );\n\n\treturn {\n\n\t\tisWebGL2: true, // keeping this for backwards compatibility\n\n\t\tgetMaxAnisotropy: getMaxAnisotropy,\n\t\tgetMaxPrecision: getMaxPrecision,\n\n\t\ttextureFormatReadable: textureFormatReadable,\n\t\ttextureTypeReadable: textureTypeReadable,\n\n\t\tprecision: precision,\n\t\tlogarithmicDepthBuffer: logarithmicDepthBuffer,\n\t\treversedDepthBuffer: reversedDepthBuffer,\n\n\t\tmaxTextures: maxTextures,\n\t\tmaxVertexTextures: maxVertexTextures,\n\t\tmaxTextureSize: maxTextureSize,\n\t\tmaxCubemapSize: maxCubemapSize,\n\n\t\tmaxAttributes: maxAttributes,\n\t\tmaxVertexUniforms: maxVertexUniforms,\n\t\tmaxVaryings: maxVaryings,\n\t\tmaxFragmentUniforms: maxFragmentUniforms,\n\n\t\tmaxSamples: maxSamples,\n\n\t\tsamples: samples\n\n\t};\n\n}\n\nfunction WebGLClipping( properties ) {\n\n\tconst scope = this;\n\n\tlet globalState = null,\n\t\tnumGlobalPlanes = 0,\n\t\tlocalClippingEnabled = false,\n\t\trenderingShadows = false;\n\n\tconst plane = new Plane(),\n\t\tviewNormalMatrix = new Matrix3(),\n\n\t\tuniform = { value: null, needsUpdate: false };\n\n\tthis.uniform = uniform;\n\tthis.numPlanes = 0;\n\tthis.numIntersection = 0;\n\n\tthis.init = function ( planes, enableLocalClipping ) {\n\n\t\tconst enabled =\n\t\t\tplanes.length !== 0 ||\n\t\t\tenableLocalClipping ||\n\t\t\t// enable state of previous frame - the clipping code has to\n\t\t\t// run another frame in order to reset the state:\n\t\t\tnumGlobalPlanes !== 0 ||\n\t\t\tlocalClippingEnabled;\n\n\t\tlocalClippingEnabled = enableLocalClipping;\n\n\t\tnumGlobalPlanes = planes.length;\n\n\t\treturn enabled;\n\n\t};\n\n\tthis.beginShadows = function () {\n\n\t\trenderingShadows = true;\n\t\tprojectPlanes( null );\n\n\t};\n\n\tthis.endShadows = function () {\n\n\t\trenderingShadows = false;\n\n\t};\n\n\tthis.setGlobalState = function ( planes, camera ) {\n\n\t\tglobalState = projectPlanes( planes, camera, 0 );\n\n\t};\n\n\tthis.setState = function ( material, camera, useCache ) {\n\n\t\tconst planes = material.clippingPlanes,\n\t\t\tclipIntersection = material.clipIntersection,\n\t\t\tclipShadows = material.clipShadows;\n\n\t\tconst materialProperties = properties.get( material );\n\n\t\tif ( ! localClippingEnabled || planes === null || planes.length === 0 || renderingShadows && ! clipShadows ) {\n\n\t\t\t// there's no local clipping\n\n\t\t\tif ( renderingShadows ) {\n\n\t\t\t\t// there's no global clipping\n\n\t\t\t\tprojectPlanes( null );\n\n\t\t\t} else {\n\n\t\t\t\tresetGlobalState();\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tconst nGlobal = renderingShadows ? 0 : numGlobalPlanes,\n\t\t\t\tlGlobal = nGlobal * 4;\n\n\t\t\tlet dstArray = materialProperties.clippingState || null;\n\n\t\t\tuniform.value = dstArray; // ensure unique state\n\n\t\t\tdstArray = projectPlanes( planes, camera, lGlobal, useCache );\n\n\t\t\tfor ( let i = 0; i !== lGlobal; ++ i ) {\n\n\t\t\t\tdstArray[ i ] = globalState[ i ];\n\n\t\t\t}\n\n\t\t\tmaterialProperties.clippingState = dstArray;\n\t\t\tthis.numIntersection = clipIntersection ? this.numPlanes : 0;\n\t\t\tthis.numPlanes += nGlobal;\n\n\t\t}\n\n\n\t};\n\n\tfunction resetGlobalState() {\n\n\t\tif ( uniform.value !== globalState ) {\n\n\t\t\tuniform.value = globalState;\n\t\t\tuniform.needsUpdate = numGlobalPlanes > 0;\n\n\t\t}\n\n\t\tscope.numPlanes = numGlobalPlanes;\n\t\tscope.numIntersection = 0;\n\n\t}\n\n\tfunction projectPlanes( planes, camera, dstOffset, skipTransform ) {\n\n\t\tconst nPlanes = planes !== null ? planes.length : 0;\n\t\tlet dstArray = null;\n\n\t\tif ( nPlanes !== 0 ) {\n\n\t\t\tdstArray = uniform.value;\n\n\t\t\tif ( skipTransform !== true || dstArray === null ) {\n\n\t\t\t\tconst flatSize = dstOffset + nPlanes * 4,\n\t\t\t\t\tviewMatrix = camera.matrixWorldInverse;\n\n\t\t\t\tviewNormalMatrix.getNormalMatrix( viewMatrix );\n\n\t\t\t\tif ( dstArray === null || dstArray.length < flatSize ) {\n\n\t\t\t\t\tdstArray = new Float32Array( flatSize );\n\n\t\t\t\t}\n\n\t\t\t\tfor ( let i = 0, i4 = dstOffset; i !== nPlanes; ++ i, i4 += 4 ) {\n\n\t\t\t\t\tplane.copy( planes[ i ] ).applyMatrix4( viewMatrix, viewNormalMatrix );\n\n\t\t\t\t\tplane.normal.toArray( dstArray, i4 );\n\t\t\t\t\tdstArray[ i4 + 3 ] = plane.constant;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tuniform.value = dstArray;\n\t\t\tuniform.needsUpdate = true;\n\n\t\t}\n\n\t\tscope.numPlanes = nPlanes;\n\t\tscope.numIntersection = 0;\n\n\t\treturn dstArray;\n\n\t}\n\n}\n\nfunction WebGLCubeMaps( renderer ) {\n\n\tlet cubemaps = new WeakMap();\n\n\tfunction mapTextureMapping( texture, mapping ) {\n\n\t\tif ( mapping === EquirectangularReflectionMapping ) {\n\n\t\t\ttexture.mapping = CubeReflectionMapping;\n\n\t\t} else if ( mapping === EquirectangularRefractionMapping ) {\n\n\t\t\ttexture.mapping = CubeRefractionMapping;\n\n\t\t}\n\n\t\treturn texture;\n\n\t}\n\n\tfunction get( texture ) {\n\n\t\tif ( texture && texture.isTexture ) {\n\n\t\t\tconst mapping = texture.mapping;\n\n\t\t\tif ( mapping === EquirectangularReflectionMapping || mapping === EquirectangularRefractionMapping ) {\n\n\t\t\t\tif ( cubemaps.has( texture ) ) {\n\n\t\t\t\t\tconst cubemap = cubemaps.get( texture ).texture;\n\t\t\t\t\treturn mapTextureMapping( cubemap, texture.mapping );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tconst image = texture.image;\n\n\t\t\t\t\tif ( image && image.height > 0 ) {\n\n\t\t\t\t\t\tconst renderTarget = new WebGLCubeRenderTarget( image.height );\n\t\t\t\t\t\trenderTarget.fromEquirectangularTexture( renderer, texture );\n\t\t\t\t\t\tcubemaps.set( texture, renderTarget );\n\n\t\t\t\t\t\ttexture.addEventListener( 'dispose', onTextureDispose );\n\n\t\t\t\t\t\treturn mapTextureMapping( renderTarget.texture, texture.mapping );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// image not yet ready. try the conversion next frame\n\n\t\t\t\t\t\treturn null;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn texture;\n\n\t}\n\n\tfunction onTextureDispose( event ) {\n\n\t\tconst texture = event.target;\n\n\t\ttexture.removeEventListener( 'dispose', onTextureDispose );\n\n\t\tconst cubemap = cubemaps.get( texture );\n\n\t\tif ( cubemap !== undefined ) {\n\n\t\t\tcubemaps.delete( texture );\n\t\t\tcubemap.dispose();\n\n\t\t}\n\n\t}\n\n\tfunction dispose() {\n\n\t\tcubemaps = new WeakMap();\n\n\t}\n\n\treturn {\n\t\tget: get,\n\t\tdispose: dispose\n\t};\n\n}\n\nconst LOD_MIN = 4;\n\n// The standard deviations (radians) associated with the extra mips.\n// Used for scene blur in fromScene() method.\nconst EXTRA_LOD_SIGMA = [ 0.125, 0.215, 0.35, 0.446, 0.526, 0.582 ];\n\n// The maximum length of the blur for loop. Smaller sigmas will use fewer\n// samples and exit early, but not recompile the shader.\n// Used for scene blur in fromScene() method.\nconst MAX_SAMPLES = 20;\n\n// GGX VNDF importance sampling configuration\nconst GGX_SAMPLES = 256;\n\nconst _flatCamera = /*@__PURE__*/ new OrthographicCamera();\nconst _clearColor = /*@__PURE__*/ new Color();\nlet _oldTarget = null;\nlet _oldActiveCubeFace = 0;\nlet _oldActiveMipmapLevel = 0;\nlet _oldXrEnabled = false;\n\nconst _origin = /*@__PURE__*/ new Vector3();\n\n/**\n * This class generates a Prefiltered, Mipmapped Radiance Environment Map\n * (PMREM) from a cubeMap environment texture. This allows different levels of\n * blur to be quickly accessed based on material roughness. It is packed into a\n * special CubeUV format that allows us to perform custom interpolation so that\n * we can support nonlinear formats such as RGBE. Unlike a traditional mipmap\n * chain, it only goes down to the LOD_MIN level (above), and then creates extra\n * even more filtered 'mips' at the same LOD_MIN resolution, associated with\n * higher roughness levels. In this way we maintain resolution to smoothly\n * interpolate diffuse lighting while limiting sampling computation.\n *\n * The prefiltering uses GGX VNDF (Visible Normal Distribution Function)\n * importance sampling based on \"Sampling the GGX Distribution of Visible Normals\"\n * (Heitz, 2018) to generate environment maps that accurately match the GGX BRDF\n * used in material rendering for physically-based image-based lighting.\n */\nclass PMREMGenerator {\n\n\t/**\n\t * Constructs a new PMREM generator.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t */\n\tconstructor( renderer ) {\n\n\t\tthis._renderer = renderer;\n\t\tthis._pingPongRenderTarget = null;\n\n\t\tthis._lodMax = 0;\n\t\tthis._cubeSize = 0;\n\t\tthis._sizeLods = [];\n\t\tthis._sigmas = [];\n\t\tthis._lodMeshes = [];\n\n\t\tthis._backgroundBox = null;\n\n\t\tthis._cubemapMaterial = null;\n\t\tthis._equirectMaterial = null;\n\n\t\tthis._blurMaterial = null;\n\t\tthis._ggxMaterial = null;\n\n\t}\n\n\t/**\n\t * Generates a PMREM from a supplied Scene, which can be faster than using an\n\t * image if networking bandwidth is low. Optional sigma specifies a blur radius\n\t * in radians to be applied to the scene before PMREM generation. Optional near\n\t * and far planes ensure the scene is rendered in its entirety.\n\t *\n\t * @param {Scene} scene - The scene to be captured.\n\t * @param {number} [sigma=0] - The blur radius in radians.\n\t * @param {number} [near=0.1] - The near plane distance.\n\t * @param {number} [far=100] - The far plane distance.\n\t * @param {Object} [options={}] - The configuration options.\n\t * @param {number} [options.size=256] - The texture size of the PMREM.\n\t * @param {Vector3} [options.position=origin] - The position of the internal cube camera that renders the scene.\n\t * @return {WebGLRenderTarget} The resulting PMREM.\n\t */\n\tfromScene( scene, sigma = 0, near = 0.1, far = 100, options = {} ) {\n\n\t\tconst {\n\t\t\tsize = 256,\n\t\t\tposition = _origin,\n\t\t} = options;\n\n\t\t_oldTarget = this._renderer.getRenderTarget();\n\t\t_oldActiveCubeFace = this._renderer.getActiveCubeFace();\n\t\t_oldActiveMipmapLevel = this._renderer.getActiveMipmapLevel();\n\t\t_oldXrEnabled = this._renderer.xr.enabled;\n\n\t\tthis._renderer.xr.enabled = false;\n\n\t\tthis._setSize( size );\n\n\t\tconst cubeUVRenderTarget = this._allocateTargets();\n\t\tcubeUVRenderTarget.depthBuffer = true;\n\n\t\tthis._sceneToCubeUV( scene, near, far, cubeUVRenderTarget, position );\n\n\t\tif ( sigma > 0 ) {\n\n\t\t\tthis._blur( cubeUVRenderTarget, 0, 0, sigma );\n\n\t\t}\n\n\t\tthis._applyPMREM( cubeUVRenderTarget );\n\t\tthis._cleanup( cubeUVRenderTarget );\n\n\t\treturn cubeUVRenderTarget;\n\n\t}\n\n\t/**\n\t * Generates a PMREM from an equirectangular texture, which can be either LDR\n\t * or HDR. The ideal input image size is 1k (1024 x 512),\n\t * as this matches best with the 256 x 256 cubemap output.\n\t *\n\t * @param {Texture} equirectangular - The equirectangular texture to be converted.\n\t * @param {?WebGLRenderTarget} [renderTarget=null] - The render target to use.\n\t * @return {WebGLRenderTarget} The resulting PMREM.\n\t */\n\tfromEquirectangular( equirectangular, renderTarget = null ) {\n\n\t\treturn this._fromTexture( equirectangular, renderTarget );\n\n\t}\n\n\t/**\n\t * Generates a PMREM from an cubemap texture, which can be either LDR\n\t * or HDR. The ideal input cube size is 256 x 256,\n\t * as this matches best with the 256 x 256 cubemap output.\n\t *\n\t * @param {Texture} cubemap - The cubemap texture to be converted.\n\t * @param {?WebGLRenderTarget} [renderTarget=null] - The render target to use.\n\t * @return {WebGLRenderTarget} The resulting PMREM.\n\t */\n\tfromCubemap( cubemap, renderTarget = null ) {\n\n\t\treturn this._fromTexture( cubemap, renderTarget );\n\n\t}\n\n\t/**\n\t * Pre-compiles the cubemap shader. You can get faster start-up by invoking this method during\n\t * your texture's network fetch for increased concurrency.\n\t */\n\tcompileCubemapShader() {\n\n\t\tif ( this._cubemapMaterial === null ) {\n\n\t\t\tthis._cubemapMaterial = _getCubemapMaterial();\n\t\t\tthis._compileMaterial( this._cubemapMaterial );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Pre-compiles the equirectangular shader. You can get faster start-up by invoking this method during\n\t * your texture's network fetch for increased concurrency.\n\t */\n\tcompileEquirectangularShader() {\n\n\t\tif ( this._equirectMaterial === null ) {\n\n\t\t\tthis._equirectMaterial = _getEquirectMaterial();\n\t\t\tthis._compileMaterial( this._equirectMaterial );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Disposes of the PMREMGenerator's internal memory. Note that PMREMGenerator is a static class,\n\t * so you should not need more than one PMREMGenerator object. If you do, calling dispose() on\n\t * one of them will cause any others to also become unusable.\n\t */\n\tdispose() {\n\n\t\tthis._dispose();\n\n\t\tif ( this._cubemapMaterial !== null ) this._cubemapMaterial.dispose();\n\t\tif ( this._equirectMaterial !== null ) this._equirectMaterial.dispose();\n\n\t\tif ( this._backgroundBox !== null ) {\n\n\t\t\tthis._backgroundBox.geometry.dispose();\n\t\t\tthis._backgroundBox.material.dispose();\n\n\t\t}\n\n\t}\n\n\t// private interface\n\n\t_setSize( cubeSize ) {\n\n\t\tthis._lodMax = Math.floor( Math.log2( cubeSize ) );\n\t\tthis._cubeSize = Math.pow( 2, this._lodMax );\n\n\t}\n\n\t_dispose() {\n\n\t\tif ( this._blurMaterial !== null ) this._blurMaterial.dispose();\n\t\tif ( this._ggxMaterial !== null ) this._ggxMaterial.dispose();\n\n\t\tif ( this._pingPongRenderTarget !== null ) this._pingPongRenderTarget.dispose();\n\n\t\tfor ( let i = 0; i < this._lodMeshes.length; i ++ ) {\n\n\t\t\tthis._lodMeshes[ i ].geometry.dispose();\n\n\t\t}\n\n\t}\n\n\t_cleanup( outputTarget ) {\n\n\t\tthis._renderer.setRenderTarget( _oldTarget, _oldActiveCubeFace, _oldActiveMipmapLevel );\n\t\tthis._renderer.xr.enabled = _oldXrEnabled;\n\n\t\toutputTarget.scissorTest = false;\n\t\t_setViewport( outputTarget, 0, 0, outputTarget.width, outputTarget.height );\n\n\t}\n\n\t_fromTexture( texture, renderTarget ) {\n\n\t\tif ( texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping ) {\n\n\t\t\tthis._setSize( texture.image.length === 0 ? 16 : ( texture.image[ 0 ].width || texture.image[ 0 ].image.width ) );\n\n\t\t} else { // Equirectangular\n\n\t\t\tthis._setSize( texture.image.width / 4 );\n\n\t\t}\n\n\t\t_oldTarget = this._renderer.getRenderTarget();\n\t\t_oldActiveCubeFace = this._renderer.getActiveCubeFace();\n\t\t_oldActiveMipmapLevel = this._renderer.getActiveMipmapLevel();\n\t\t_oldXrEnabled = this._renderer.xr.enabled;\n\n\t\tthis._renderer.xr.enabled = false;\n\n\t\tconst cubeUVRenderTarget = renderTarget || this._allocateTargets();\n\t\tthis._textureToCubeUV( texture, cubeUVRenderTarget );\n\t\tthis._applyPMREM( cubeUVRenderTarget );\n\t\tthis._cleanup( cubeUVRenderTarget );\n\n\t\treturn cubeUVRenderTarget;\n\n\t}\n\n\t_allocateTargets() {\n\n\t\tconst width = 3 * Math.max( this._cubeSize, 16 * 7 );\n\t\tconst height = 4 * this._cubeSize;\n\n\t\tconst params = {\n\t\t\tmagFilter: LinearFilter,\n\t\t\tminFilter: LinearFilter,\n\t\t\tgenerateMipmaps: false,\n\t\t\ttype: HalfFloatType,\n\t\t\tformat: RGBAFormat,\n\t\t\tcolorSpace: LinearSRGBColorSpace,\n\t\t\tdepthBuffer: false\n\t\t};\n\n\t\tconst cubeUVRenderTarget = _createRenderTarget( width, height, params );\n\n\t\tif ( this._pingPongRenderTarget === null || this._pingPongRenderTarget.width !== width || this._pingPongRenderTarget.height !== height ) {\n\n\t\t\tif ( this._pingPongRenderTarget !== null ) {\n\n\t\t\t\tthis._dispose();\n\n\t\t\t}\n\n\t\t\tthis._pingPongRenderTarget = _createRenderTarget( width, height, params );\n\n\t\t\tconst { _lodMax } = this;\n\t\t\t( { lodMeshes: this._lodMeshes, sizeLods: this._sizeLods, sigmas: this._sigmas } = _createPlanes( _lodMax ) );\n\n\t\t\tthis._blurMaterial = _getBlurShader( _lodMax, width, height );\n\t\t\tthis._ggxMaterial = _getGGXShader( _lodMax, width, height );\n\n\t\t}\n\n\t\treturn cubeUVRenderTarget;\n\n\t}\n\n\t_compileMaterial( material ) {\n\n\t\tconst mesh = new Mesh( new BufferGeometry(), material );\n\t\tthis._renderer.compile( mesh, _flatCamera );\n\n\t}\n\n\t_sceneToCubeUV( scene, near, far, cubeUVRenderTarget, position ) {\n\n\t\tconst fov = 90;\n\t\tconst aspect = 1;\n\t\tconst cubeCamera = new PerspectiveCamera( fov, aspect, near, far );\n\t\tconst upSign = [ 1, -1, 1, 1, 1, 1 ];\n\t\tconst forwardSign = [ 1, 1, 1, -1, -1, -1 ];\n\t\tconst renderer = this._renderer;\n\n\t\tconst originalAutoClear = renderer.autoClear;\n\t\tconst toneMapping = renderer.toneMapping;\n\t\trenderer.getClearColor( _clearColor );\n\n\t\trenderer.toneMapping = NoToneMapping;\n\t\trenderer.autoClear = false;\n\n\t\t// https://github.com/mrdoob/three.js/issues/31413#issuecomment-3095966812\n\t\tconst reversedDepthBuffer = renderer.state.buffers.depth.getReversed();\n\n\t\tif ( reversedDepthBuffer ) {\n\n\t\t\trenderer.setRenderTarget( cubeUVRenderTarget );\n\t\t\trenderer.clearDepth();\n\t\t\trenderer.setRenderTarget( null );\n\n\t\t}\n\n\t\tif ( this._backgroundBox === null ) {\n\n\t\t\tthis._backgroundBox = new Mesh(\n\t\t\t\tnew BoxGeometry(),\n\t\t\t\tnew MeshBasicMaterial( {\n\t\t\t\t\tname: 'PMREM.Background',\n\t\t\t\t\tside: BackSide,\n\t\t\t\t\tdepthWrite: false,\n\t\t\t\t\tdepthTest: false,\n\t\t\t\t} )\n\t\t\t);\n\n\t\t}\n\n\t\tconst backgroundBox = this._backgroundBox;\n\t\tconst backgroundMaterial = backgroundBox.material;\n\n\t\tlet useSolidColor = false;\n\n\t\tconst background = scene.background;\n\n\t\tif ( background ) {\n\n\t\t\tif ( background.isColor ) {\n\n\t\t\t\tbackgroundMaterial.color.copy( background );\n\t\t\t\tscene.background = null;\n\t\t\t\tuseSolidColor = true;\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tbackgroundMaterial.color.copy( _clearColor );\n\t\t\tuseSolidColor = true;\n\n\t\t}\n\n\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\tconst col = i % 3;\n\n\t\t\tif ( col === 0 ) {\n\n\t\t\t\tcubeCamera.up.set( 0, upSign[ i ], 0 );\n\t\t\t\tcubeCamera.position.set( position.x, position.y, position.z );\n\t\t\t\tcubeCamera.lookAt( position.x + forwardSign[ i ], position.y, position.z );\n\n\t\t\t} else if ( col === 1 ) {\n\n\t\t\t\tcubeCamera.up.set( 0, 0, upSign[ i ] );\n\t\t\t\tcubeCamera.position.set( position.x, position.y, position.z );\n\t\t\t\tcubeCamera.lookAt( position.x, position.y + forwardSign[ i ], position.z );\n\n\n\t\t\t} else {\n\n\t\t\t\tcubeCamera.up.set( 0, upSign[ i ], 0 );\n\t\t\t\tcubeCamera.position.set( position.x, position.y, position.z );\n\t\t\t\tcubeCamera.lookAt( position.x, position.y, position.z + forwardSign[ i ] );\n\n\t\t\t}\n\n\t\t\tconst size = this._cubeSize;\n\n\t\t\t_setViewport( cubeUVRenderTarget, col * size, i > 2 ? size : 0, size, size );\n\n\t\t\trenderer.setRenderTarget( cubeUVRenderTarget );\n\n\t\t\tif ( useSolidColor ) {\n\n\t\t\t\trenderer.render( backgroundBox, cubeCamera );\n\n\t\t\t}\n\n\t\t\trenderer.render( scene, cubeCamera );\n\n\t\t}\n\n\t\trenderer.toneMapping = toneMapping;\n\t\trenderer.autoClear = originalAutoClear;\n\t\tscene.background = background;\n\n\t}\n\n\t_textureToCubeUV( texture, cubeUVRenderTarget ) {\n\n\t\tconst renderer = this._renderer;\n\n\t\tconst isCubeTexture = ( texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping );\n\n\t\tif ( isCubeTexture ) {\n\n\t\t\tif ( this._cubemapMaterial === null ) {\n\n\t\t\t\tthis._cubemapMaterial = _getCubemapMaterial();\n\n\t\t\t}\n\n\t\t\tthis._cubemapMaterial.uniforms.flipEnvMap.value = ( texture.isRenderTargetTexture === false ) ? -1 : 1;\n\n\t\t} else {\n\n\t\t\tif ( this._equirectMaterial === null ) {\n\n\t\t\t\tthis._equirectMaterial = _getEquirectMaterial();\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst material = isCubeTexture ? this._cubemapMaterial : this._equirectMaterial;\n\n\t\tconst mesh = this._lodMeshes[ 0 ];\n\t\tmesh.material = material;\n\n\t\tconst uniforms = material.uniforms;\n\n\t\tuniforms[ 'envMap' ].value = texture;\n\n\t\tconst size = this._cubeSize;\n\n\t\t_setViewport( cubeUVRenderTarget, 0, 0, 3 * size, 2 * size );\n\n\t\trenderer.setRenderTarget( cubeUVRenderTarget );\n\t\trenderer.render( mesh, _flatCamera );\n\n\t}\n\n\t_applyPMREM( cubeUVRenderTarget ) {\n\n\t\tconst renderer = this._renderer;\n\t\tconst autoClear = renderer.autoClear;\n\t\trenderer.autoClear = false;\n\n\t\tconst n = this._lodMeshes.length;\n\n\t\t// Use GGX VNDF importance sampling\n\t\tfor ( let i = 1; i < n; i ++ ) {\n\n\t\t\tthis._applyGGXFilter( cubeUVRenderTarget, i - 1, i );\n\n\t\t}\n\n\t\trenderer.autoClear = autoClear;\n\n\t}\n\n\t/**\n\t * Applies GGX VNDF importance sampling filter to generate a prefiltered environment map.\n\t * Uses Monte Carlo integration with VNDF importance sampling to accurately represent the\n\t * GGX BRDF for physically-based rendering. Reads from the previous LOD level and\n\t * applies incremental roughness filtering to avoid over-blurring.\n\t *\n\t * @private\n\t * @param {WebGLRenderTarget} cubeUVRenderTarget\n\t * @param {number} lodIn - Source LOD level to read from\n\t * @param {number} lodOut - Target LOD level to write to\n\t */\n\t_applyGGXFilter( cubeUVRenderTarget, lodIn, lodOut ) {\n\n\t\tconst renderer = this._renderer;\n\t\tconst pingPongRenderTarget = this._pingPongRenderTarget;\n\n\t\tconst ggxMaterial = this._ggxMaterial;\n\t\tconst ggxMesh = this._lodMeshes[ lodOut ];\n\t\tggxMesh.material = ggxMaterial;\n\n\t\tconst ggxUniforms = ggxMaterial.uniforms;\n\n\t\t// Calculate incremental roughness between LOD levels\n\t\tconst targetRoughness = lodOut / ( this._lodMeshes.length - 1 );\n\t\tconst sourceRoughness = lodIn / ( this._lodMeshes.length - 1 );\n\t\tconst incrementalRoughness = Math.sqrt( targetRoughness * targetRoughness - sourceRoughness * sourceRoughness );\n\n\t\t// Apply blur strength mapping for better quality across the roughness range\n\t\tconst blurStrength = 0.0 + targetRoughness * 1.25;\n\t\tconst adjustedRoughness = incrementalRoughness * blurStrength;\n\n\t\t// Calculate viewport position based on output LOD level\n\t\tconst { _lodMax } = this;\n\t\tconst outputSize = this._sizeLods[ lodOut ];\n\t\tconst x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 );\n\t\tconst y = 4 * ( this._cubeSize - outputSize );\n\n\t\t// Read from previous LOD with incremental roughness\n\t\tggxUniforms[ 'envMap' ].value = cubeUVRenderTarget.texture;\n\t\tggxUniforms[ 'roughness' ].value = adjustedRoughness;\n\t\tggxUniforms[ 'mipInt' ].value = _lodMax - lodIn; // Sample from input LOD\n\n\t\t_setViewport( pingPongRenderTarget, x, y, 3 * outputSize, 2 * outputSize );\n\t\trenderer.setRenderTarget( pingPongRenderTarget );\n\t\trenderer.render( ggxMesh, _flatCamera );\n\n\t\t// Copy from pingPong back to cubeUV (simple direct copy)\n\t\tggxUniforms[ 'envMap' ].value = pingPongRenderTarget.texture;\n\t\tggxUniforms[ 'roughness' ].value = 0.0; // Direct copy\n\t\tggxUniforms[ 'mipInt' ].value = _lodMax - lodOut; // Read from the level we just wrote\n\n\t\t_setViewport( cubeUVRenderTarget, x, y, 3 * outputSize, 2 * outputSize );\n\t\trenderer.setRenderTarget( cubeUVRenderTarget );\n\t\trenderer.render( ggxMesh, _flatCamera );\n\n\t}\n\n\t/**\n\t * This is a two-pass Gaussian blur for a cubemap. Normally this is done\n\t * vertically and horizontally, but this breaks down on a cube. Here we apply\n\t * the blur latitudinally (around the poles), and then longitudinally (towards\n\t * the poles) to approximate the orthogonally-separable blur. It is least\n\t * accurate at the poles, but still does a decent job.\n\t *\n\t * Used for initial scene blur in fromScene() method when sigma > 0.\n\t *\n\t * @private\n\t * @param {WebGLRenderTarget} cubeUVRenderTarget\n\t * @param {number} lodIn\n\t * @param {number} lodOut\n\t * @param {number} sigma\n\t * @param {Vector3} [poleAxis]\n\t */\n\t_blur( cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis ) {\n\n\t\tconst pingPongRenderTarget = this._pingPongRenderTarget;\n\n\t\tthis._halfBlur(\n\t\t\tcubeUVRenderTarget,\n\t\t\tpingPongRenderTarget,\n\t\t\tlodIn,\n\t\t\tlodOut,\n\t\t\tsigma,\n\t\t\t'latitudinal',\n\t\t\tpoleAxis );\n\n\t\tthis._halfBlur(\n\t\t\tpingPongRenderTarget,\n\t\t\tcubeUVRenderTarget,\n\t\t\tlodOut,\n\t\t\tlodOut,\n\t\t\tsigma,\n\t\t\t'longitudinal',\n\t\t\tpoleAxis );\n\n\t}\n\n\t_halfBlur( targetIn, targetOut, lodIn, lodOut, sigmaRadians, direction, poleAxis ) {\n\n\t\tconst renderer = this._renderer;\n\t\tconst blurMaterial = this._blurMaterial;\n\n\t\tif ( direction !== 'latitudinal' && direction !== 'longitudinal' ) {\n\n\t\t\terror(\n\t\t\t\t'blur direction must be either latitudinal or longitudinal!' );\n\n\t\t}\n\n\t\t// Number of standard deviations at which to cut off the discrete approximation.\n\t\tconst STANDARD_DEVIATIONS = 3;\n\n\t\tconst blurMesh = this._lodMeshes[ lodOut ];\n\t\tblurMesh.material = blurMaterial;\n\n\t\tconst blurUniforms = blurMaterial.uniforms;\n\n\t\tconst pixels = this._sizeLods[ lodIn ] - 1;\n\t\tconst radiansPerPixel = isFinite( sigmaRadians ) ? Math.PI / ( 2 * pixels ) : 2 * Math.PI / ( 2 * MAX_SAMPLES - 1 );\n\t\tconst sigmaPixels = sigmaRadians / radiansPerPixel;\n\t\tconst samples = isFinite( sigmaRadians ) ? 1 + Math.floor( STANDARD_DEVIATIONS * sigmaPixels ) : MAX_SAMPLES;\n\n\t\tif ( samples > MAX_SAMPLES ) {\n\n\t\t\twarn( `sigmaRadians, ${\n\t\t\t\tsigmaRadians}, is too large and will clip, as it requested ${\n\t\t\t\tsamples} samples when the maximum is set to ${MAX_SAMPLES}` );\n\n\t\t}\n\n\t\tconst weights = [];\n\t\tlet sum = 0;\n\n\t\tfor ( let i = 0; i < MAX_SAMPLES; ++ i ) {\n\n\t\t\tconst x = i / sigmaPixels;\n\t\t\tconst weight = Math.exp( - x * x / 2 );\n\t\t\tweights.push( weight );\n\n\t\t\tif ( i === 0 ) {\n\n\t\t\t\tsum += weight;\n\n\t\t\t} else if ( i < samples ) {\n\n\t\t\t\tsum += 2 * weight;\n\n\t\t\t}\n\n\t\t}\n\n\t\tfor ( let i = 0; i < weights.length; i ++ ) {\n\n\t\t\tweights[ i ] = weights[ i ] / sum;\n\n\t\t}\n\n\t\tblurUniforms[ 'envMap' ].value = targetIn.texture;\n\t\tblurUniforms[ 'samples' ].value = samples;\n\t\tblurUniforms[ 'weights' ].value = weights;\n\t\tblurUniforms[ 'latitudinal' ].value = direction === 'latitudinal';\n\n\t\tif ( poleAxis ) {\n\n\t\t\tblurUniforms[ 'poleAxis' ].value = poleAxis;\n\n\t\t}\n\n\t\tconst { _lodMax } = this;\n\t\tblurUniforms[ 'dTheta' ].value = radiansPerPixel;\n\t\tblurUniforms[ 'mipInt' ].value = _lodMax - lodIn;\n\n\t\tconst outputSize = this._sizeLods[ lodOut ];\n\t\tconst x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 );\n\t\tconst y = 4 * ( this._cubeSize - outputSize );\n\n\t\t_setViewport( targetOut, x, y, 3 * outputSize, 2 * outputSize );\n\t\trenderer.setRenderTarget( targetOut );\n\t\trenderer.render( blurMesh, _flatCamera );\n\n\t}\n\n}\n\n\n\nfunction _createPlanes( lodMax ) {\n\n\tconst sizeLods = [];\n\tconst sigmas = [];\n\tconst lodMeshes = [];\n\n\tlet lod = lodMax;\n\n\tconst totalLods = lodMax - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length;\n\n\tfor ( let i = 0; i < totalLods; i ++ ) {\n\n\t\tconst sizeLod = Math.pow( 2, lod );\n\t\tsizeLods.push( sizeLod );\n\t\tlet sigma = 1.0 / sizeLod;\n\n\t\tif ( i > lodMax - LOD_MIN ) {\n\n\t\t\tsigma = EXTRA_LOD_SIGMA[ i - lodMax + LOD_MIN - 1 ];\n\n\t\t} else if ( i === 0 ) {\n\n\t\t\tsigma = 0;\n\n\t\t}\n\n\t\tsigmas.push( sigma );\n\n\t\tconst texelSize = 1.0 / ( sizeLod - 2 );\n\t\tconst min = - texelSize;\n\t\tconst max = 1 + texelSize;\n\t\tconst uv1 = [ min, min, max, min, max, max, min, min, max, max, min, max ];\n\n\t\tconst cubeFaces = 6;\n\t\tconst vertices = 6;\n\t\tconst positionSize = 3;\n\t\tconst uvSize = 2;\n\t\tconst faceIndexSize = 1;\n\n\t\tconst position = new Float32Array( positionSize * vertices * cubeFaces );\n\t\tconst uv = new Float32Array( uvSize * vertices * cubeFaces );\n\t\tconst faceIndex = new Float32Array( faceIndexSize * vertices * cubeFaces );\n\n\t\tfor ( let face = 0; face < cubeFaces; face ++ ) {\n\n\t\t\tconst x = ( face % 3 ) * 2 / 3 - 1;\n\t\t\tconst y = face > 2 ? 0 : -1;\n\t\t\tconst coordinates = [\n\t\t\t\tx, y, 0,\n\t\t\t\tx + 2 / 3, y, 0,\n\t\t\t\tx + 2 / 3, y + 1, 0,\n\t\t\t\tx, y, 0,\n\t\t\t\tx + 2 / 3, y + 1, 0,\n\t\t\t\tx, y + 1, 0\n\t\t\t];\n\t\t\tposition.set( coordinates, positionSize * vertices * face );\n\t\t\tuv.set( uv1, uvSize * vertices * face );\n\t\t\tconst fill = [ face, face, face, face, face, face ];\n\t\t\tfaceIndex.set( fill, faceIndexSize * vertices * face );\n\n\t\t}\n\n\t\tconst planes = new BufferGeometry();\n\t\tplanes.setAttribute( 'position', new BufferAttribute( position, positionSize ) );\n\t\tplanes.setAttribute( 'uv', new BufferAttribute( uv, uvSize ) );\n\t\tplanes.setAttribute( 'faceIndex', new BufferAttribute( faceIndex, faceIndexSize ) );\n\t\tlodMeshes.push( new Mesh( planes, null ) );\n\n\t\tif ( lod > LOD_MIN ) {\n\n\t\t\tlod --;\n\n\t\t}\n\n\t}\n\n\treturn { lodMeshes, sizeLods, sigmas };\n\n}\n\nfunction _createRenderTarget( width, height, params ) {\n\n\tconst cubeUVRenderTarget = new WebGLRenderTarget( width, height, params );\n\tcubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping;\n\tcubeUVRenderTarget.texture.name = 'PMREM.cubeUv';\n\tcubeUVRenderTarget.scissorTest = true;\n\treturn cubeUVRenderTarget;\n\n}\n\nfunction _setViewport( target, x, y, width, height ) {\n\n\ttarget.viewport.set( x, y, width, height );\n\ttarget.scissor.set( x, y, width, height );\n\n}\n\nfunction _getGGXShader( lodMax, width, height ) {\n\n\tconst shaderMaterial = new ShaderMaterial( {\n\n\t\tname: 'PMREMGGXConvolution',\n\n\t\tdefines: {\n\t\t\t'GGX_SAMPLES': GGX_SAMPLES,\n\t\t\t'CUBEUV_TEXEL_WIDTH': 1.0 / width,\n\t\t\t'CUBEUV_TEXEL_HEIGHT': 1.0 / height,\n\t\t\t'CUBEUV_MAX_MIP': `${lodMax}.0`,\n\t\t},\n\n\t\tuniforms: {\n\t\t\t'envMap': { value: null },\n\t\t\t'roughness': { value: 0.0 },\n\t\t\t'mipInt': { value: 0 }\n\t\t},\n\n\t\tvertexShader: _getCommonVertexShader(),\n\n\t\tfragmentShader: /* glsl */`\n\n\t\t\tprecision highp float;\n\t\t\tprecision highp int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\t\t\tuniform float roughness;\n\t\t\tuniform float mipInt;\n\n\t\t\t#define ENVMAP_TYPE_CUBE_UV\n\t\t\t#include <cube_uv_reflection_fragment>\n\n\t\t\t#define PI 3.14159265359\n\n\t\t\t// Van der Corput radical inverse\n\t\t\tfloat radicalInverse_VdC(uint bits) {\n\t\t\t\tbits = (bits << 16u) | (bits >> 16u);\n\t\t\t\tbits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);\n\t\t\t\tbits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);\n\t\t\t\tbits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);\n\t\t\t\tbits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);\n\t\t\t\treturn float(bits) * 2.3283064365386963e-10; // / 0x100000000\n\t\t\t}\n\n\t\t\t// Hammersley sequence\n\t\t\tvec2 hammersley(uint i, uint N) {\n\t\t\t\treturn vec2(float(i) / float(N), radicalInverse_VdC(i));\n\t\t\t}\n\n\t\t\t// GGX VNDF importance sampling (Eric Heitz 2018)\n\t\t\t// \"Sampling the GGX Distribution of Visible Normals\"\n\t\t\t// https://jcgt.org/published/0007/04/01/\n\t\t\tvec3 importanceSampleGGX_VNDF(vec2 Xi, vec3 V, float roughness) {\n\t\t\t\tfloat alpha = roughness * roughness;\n\n\t\t\t\t// Section 3.2: Transform view direction to hemisphere configuration\n\t\t\t\tvec3 Vh = normalize(vec3(alpha * V.x, alpha * V.y, V.z));\n\n\t\t\t\t// Section 4.1: Orthonormal basis\n\t\t\t\tfloat lensq = Vh.x * Vh.x + Vh.y * Vh.y;\n\t\t\t\tvec3 T1 = lensq > 0.0 ? vec3(-Vh.y, Vh.x, 0.0) / sqrt(lensq) : vec3(1.0, 0.0, 0.0);\n\t\t\t\tvec3 T2 = cross(Vh, T1);\n\n\t\t\t\t// Section 4.2: Parameterization of projected area\n\t\t\t\tfloat r = sqrt(Xi.x);\n\t\t\t\tfloat phi = 2.0 * PI * Xi.y;\n\t\t\t\tfloat t1 = r * cos(phi);\n\t\t\t\tfloat t2 = r * sin(phi);\n\t\t\t\tfloat s = 0.5 * (1.0 + Vh.z);\n\t\t\t\tt2 = (1.0 - s) * sqrt(1.0 - t1 * t1) + s * t2;\n\n\t\t\t\t// Section 4.3: Reprojection onto hemisphere\n\t\t\t\tvec3 Nh = t1 * T1 + t2 * T2 + sqrt(max(0.0, 1.0 - t1 * t1 - t2 * t2)) * Vh;\n\n\t\t\t\t// Section 3.4: Transform back to ellipsoid configuration\n\t\t\t\treturn normalize(vec3(alpha * Nh.x, alpha * Nh.y, max(0.0, Nh.z)));\n\t\t\t}\n\n\t\t\tvoid main() {\n\t\t\t\tvec3 N = normalize(vOutputDirection);\n\t\t\t\tvec3 V = N; // Assume view direction equals normal for pre-filtering\n\n\t\t\t\tvec3 prefilteredColor = vec3(0.0);\n\t\t\t\tfloat totalWeight = 0.0;\n\n\t\t\t\t// For very low roughness, just sample the environment directly\n\t\t\t\tif (roughness < 0.001) {\n\t\t\t\t\tgl_FragColor = vec4(bilinearCubeUV(envMap, N, mipInt), 1.0);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Tangent space basis for VNDF sampling\n\t\t\t\tvec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);\n\t\t\t\tvec3 tangent = normalize(cross(up, N));\n\t\t\t\tvec3 bitangent = cross(N, tangent);\n\n\t\t\t\tfor(uint i = 0u; i < uint(GGX_SAMPLES); i++) {\n\t\t\t\t\tvec2 Xi = hammersley(i, uint(GGX_SAMPLES));\n\n\t\t\t\t\t// For PMREM, V = N, so in tangent space V is always (0, 0, 1)\n\t\t\t\t\tvec3 H_tangent = importanceSampleGGX_VNDF(Xi, vec3(0.0, 0.0, 1.0), roughness);\n\n\t\t\t\t\t// Transform H back to world space\n\t\t\t\t\tvec3 H = normalize(tangent * H_tangent.x + bitangent * H_tangent.y + N * H_tangent.z);\n\t\t\t\t\tvec3 L = normalize(2.0 * dot(V, H) * H - V);\n\n\t\t\t\t\tfloat NdotL = max(dot(N, L), 0.0);\n\n\t\t\t\t\tif(NdotL > 0.0) {\n\t\t\t\t\t\t// Sample environment at fixed mip level\n\t\t\t\t\t\t// VNDF importance sampling handles the distribution filtering\n\t\t\t\t\t\tvec3 sampleColor = bilinearCubeUV(envMap, L, mipInt);\n\n\t\t\t\t\t\t// Weight by NdotL for the split-sum approximation\n\t\t\t\t\t\t// VNDF PDF naturally accounts for the visible microfacet distribution\n\t\t\t\t\t\tprefilteredColor += sampleColor * NdotL;\n\t\t\t\t\t\ttotalWeight += NdotL;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (totalWeight > 0.0) {\n\t\t\t\t\tprefilteredColor = prefilteredColor / totalWeight;\n\t\t\t\t}\n\n\t\t\t\tgl_FragColor = vec4(prefilteredColor, 1.0);\n\t\t\t}\n\t\t`,\n\n\t\tblending: NoBlending,\n\t\tdepthTest: false,\n\t\tdepthWrite: false\n\n\t} );\n\n\treturn shaderMaterial;\n\n}\n\nfunction _getBlurShader( lodMax, width, height ) {\n\n\tconst weights = new Float32Array( MAX_SAMPLES );\n\tconst poleAxis = new Vector3( 0, 1, 0 );\n\tconst shaderMaterial = new ShaderMaterial( {\n\n\t\tname: 'SphericalGaussianBlur',\n\n\t\tdefines: {\n\t\t\t'n': MAX_SAMPLES,\n\t\t\t'CUBEUV_TEXEL_WIDTH': 1.0 / width,\n\t\t\t'CUBEUV_TEXEL_HEIGHT': 1.0 / height,\n\t\t\t'CUBEUV_MAX_MIP': `${lodMax}.0`,\n\t\t},\n\n\t\tuniforms: {\n\t\t\t'envMap': { value: null },\n\t\t\t'samples': { value: 1 },\n\t\t\t'weights': { value: weights },\n\t\t\t'latitudinal': { value: false },\n\t\t\t'dTheta': { value: 0 },\n\t\t\t'mipInt': { value: 0 },\n\t\t\t'poleAxis': { value: poleAxis }\n\t\t},\n\n\t\tvertexShader: _getCommonVertexShader(),\n\n\t\tfragmentShader: /* glsl */`\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\t\t\tuniform int samples;\n\t\t\tuniform float weights[ n ];\n\t\t\tuniform bool latitudinal;\n\t\t\tuniform float dTheta;\n\t\t\tuniform float mipInt;\n\t\t\tuniform vec3 poleAxis;\n\n\t\t\t#define ENVMAP_TYPE_CUBE_UV\n\t\t\t#include <cube_uv_reflection_fragment>\n\n\t\t\tvec3 getSample( float theta, vec3 axis ) {\n\n\t\t\t\tfloat cosTheta = cos( theta );\n\t\t\t\t// Rodrigues' axis-angle rotation\n\t\t\t\tvec3 sampleDirection = vOutputDirection * cosTheta\n\t\t\t\t\t+ cross( axis, vOutputDirection ) * sin( theta )\n\t\t\t\t\t+ axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta );\n\n\t\t\t\treturn bilinearCubeUV( envMap, sampleDirection, mipInt );\n\n\t\t\t}\n\n\t\t\tvoid main() {\n\n\t\t\t\tvec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection );\n\n\t\t\t\tif ( all( equal( axis, vec3( 0.0 ) ) ) ) {\n\n\t\t\t\t\taxis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x );\n\n\t\t\t\t}\n\n\t\t\t\taxis = normalize( axis );\n\n\t\t\t\tgl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t\t\t\tgl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis );\n\n\t\t\t\tfor ( int i = 1; i < n; i++ ) {\n\n\t\t\t\t\tif ( i >= samples ) {\n\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfloat theta = dTheta * float( i );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( theta, axis );\n\n\t\t\t\t}\n\n\t\t\t}\n\t\t`,\n\n\t\tblending: NoBlending,\n\t\tdepthTest: false,\n\t\tdepthWrite: false\n\n\t} );\n\n\treturn shaderMaterial;\n\n}\n\nfunction _getEquirectMaterial() {\n\n\treturn new ShaderMaterial( {\n\n\t\tname: 'EquirectangularToCubeUV',\n\n\t\tuniforms: {\n\t\t\t'envMap': { value: null }\n\t\t},\n\n\t\tvertexShader: _getCommonVertexShader(),\n\n\t\tfragmentShader: /* glsl */`\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\n\t\t\t#include <common>\n\n\t\t\tvoid main() {\n\n\t\t\t\tvec3 outputDirection = normalize( vOutputDirection );\n\t\t\t\tvec2 uv = equirectUv( outputDirection );\n\n\t\t\t\tgl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 );\n\n\t\t\t}\n\t\t`,\n\n\t\tblending: NoBlending,\n\t\tdepthTest: false,\n\t\tdepthWrite: false\n\n\t} );\n\n}\n\nfunction _getCubemapMaterial() {\n\n\treturn new ShaderMaterial( {\n\n\t\tname: 'CubemapToCubeUV',\n\n\t\tuniforms: {\n\t\t\t'envMap': { value: null },\n\t\t\t'flipEnvMap': { value: -1 }\n\t\t},\n\n\t\tvertexShader: _getCommonVertexShader(),\n\n\t\tfragmentShader: /* glsl */`\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tuniform float flipEnvMap;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform samplerCube envMap;\n\n\t\t\tvoid main() {\n\n\t\t\t\tgl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) );\n\n\t\t\t}\n\t\t`,\n\n\t\tblending: NoBlending,\n\t\tdepthTest: false,\n\t\tdepthWrite: false\n\n\t} );\n\n}\n\nfunction _getCommonVertexShader() {\n\n\treturn /* glsl */`\n\n\t\tprecision mediump float;\n\t\tprecision mediump int;\n\n\t\tattribute float faceIndex;\n\n\t\tvarying vec3 vOutputDirection;\n\n\t\t// RH coordinate system; PMREM face-indexing convention\n\t\tvec3 getDirection( vec2 uv, float face ) {\n\n\t\t\tuv = 2.0 * uv - 1.0;\n\n\t\t\tvec3 direction = vec3( uv, 1.0 );\n\n\t\t\tif ( face == 0.0 ) {\n\n\t\t\t\tdirection = direction.zyx; // ( 1, v, u ) pos x\n\n\t\t\t} else if ( face == 1.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xz *= -1.0; // ( -u, 1, -v ) pos y\n\n\t\t\t} else if ( face == 2.0 ) {\n\n\t\t\t\tdirection.x *= -1.0; // ( -u, v, 1 ) pos z\n\n\t\t\t} else if ( face == 3.0 ) {\n\n\t\t\t\tdirection = direction.zyx;\n\t\t\t\tdirection.xz *= -1.0; // ( -1, v, -u ) neg x\n\n\t\t\t} else if ( face == 4.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xy *= -1.0; // ( -u, -1, v ) neg y\n\n\t\t\t} else if ( face == 5.0 ) {\n\n\t\t\t\tdirection.z *= -1.0; // ( u, v, -1 ) neg z\n\n\t\t\t}\n\n\t\t\treturn direction;\n\n\t\t}\n\n\t\tvoid main() {\n\n\t\t\tvOutputDirection = getDirection( uv, faceIndex );\n\t\t\tgl_Position = vec4( position, 1.0 );\n\n\t\t}\n\t`;\n\n}\n\nfunction WebGLCubeUVMaps( renderer ) {\n\n\tlet cubeUVmaps = new WeakMap();\n\n\tlet pmremGenerator = null;\n\n\tfunction get( texture ) {\n\n\t\tif ( texture && texture.isTexture ) {\n\n\t\t\tconst mapping = texture.mapping;\n\n\t\t\tconst isEquirectMap = ( mapping === EquirectangularReflectionMapping || mapping === EquirectangularRefractionMapping );\n\t\t\tconst isCubeMap = ( mapping === CubeReflectionMapping || mapping === CubeRefractionMapping );\n\n\t\t\t// equirect/cube map to cubeUV conversion\n\n\t\t\tif ( isEquirectMap || isCubeMap ) {\n\n\t\t\t\tlet renderTarget = cubeUVmaps.get( texture );\n\n\t\t\t\tconst currentPMREMVersion = renderTarget !== undefined ? renderTarget.texture.pmremVersion : 0;\n\n\t\t\t\tif ( texture.isRenderTargetTexture && texture.pmremVersion !== currentPMREMVersion ) {\n\n\t\t\t\t\tif ( pmremGenerator === null ) pmremGenerator = new PMREMGenerator( renderer );\n\n\t\t\t\t\trenderTarget = isEquirectMap ? pmremGenerator.fromEquirectangular( texture, renderTarget ) : pmremGenerator.fromCubemap( texture, renderTarget );\n\t\t\t\t\trenderTarget.texture.pmremVersion = texture.pmremVersion;\n\n\t\t\t\t\tcubeUVmaps.set( texture, renderTarget );\n\n\t\t\t\t\treturn renderTarget.texture;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( renderTarget !== undefined ) {\n\n\t\t\t\t\t\treturn renderTarget.texture;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tconst image = texture.image;\n\n\t\t\t\t\t\tif ( ( isEquirectMap && image && image.height > 0 ) || ( isCubeMap && image && isCubeTextureComplete( image ) ) ) {\n\n\t\t\t\t\t\t\tif ( pmremGenerator === null ) pmremGenerator = new PMREMGenerator( renderer );\n\n\t\t\t\t\t\t\trenderTarget = isEquirectMap ? pmremGenerator.fromEquirectangular( texture ) : pmremGenerator.fromCubemap( texture );\n\t\t\t\t\t\t\trenderTarget.texture.pmremVersion = texture.pmremVersion;\n\n\t\t\t\t\t\t\tcubeUVmaps.set( texture, renderTarget );\n\n\t\t\t\t\t\t\ttexture.addEventListener( 'dispose', onTextureDispose );\n\n\t\t\t\t\t\t\treturn renderTarget.texture;\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// image not yet ready. try the conversion next frame\n\n\t\t\t\t\t\t\treturn null;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn texture;\n\n\t}\n\n\tfunction isCubeTextureComplete( image ) {\n\n\t\tlet count = 0;\n\t\tconst length = 6;\n\n\t\tfor ( let i = 0; i < length; i ++ ) {\n\n\t\t\tif ( image[ i ] !== undefined ) count ++;\n\n\t\t}\n\n\t\treturn count === length;\n\n\n\t}\n\n\tfunction onTextureDispose( event ) {\n\n\t\tconst texture = event.target;\n\n\t\ttexture.removeEventListener( 'dispose', onTextureDispose );\n\n\t\tconst cubemapUV = cubeUVmaps.get( texture );\n\n\t\tif ( cubemapUV !== undefined ) {\n\n\t\t\tcubeUVmaps.delete( texture );\n\t\t\tcubemapUV.dispose();\n\n\t\t}\n\n\t}\n\n\tfunction dispose() {\n\n\t\tcubeUVmaps = new WeakMap();\n\n\t\tif ( pmremGenerator !== null ) {\n\n\t\t\tpmremGenerator.dispose();\n\t\t\tpmremGenerator = null;\n\n\t\t}\n\n\t}\n\n\treturn {\n\t\tget: get,\n\t\tdispose: dispose\n\t};\n\n}\n\nfunction WebGLExtensions( gl ) {\n\n\tconst extensions = {};\n\n\tfunction getExtension( name ) {\n\n\t\tif ( extensions[ name ] !== undefined ) {\n\n\t\t\treturn extensions[ name ];\n\n\t\t}\n\n\t\tconst extension = gl.getExtension( name );\n\n\t\textensions[ name ] = extension;\n\n\t\treturn extension;\n\n\t}\n\n\treturn {\n\n\t\thas: function ( name ) {\n\n\t\t\treturn getExtension( name ) !== null;\n\n\t\t},\n\n\t\tinit: function () {\n\n\t\t\tgetExtension( 'EXT_color_buffer_float' );\n\t\t\tgetExtension( 'WEBGL_clip_cull_distance' );\n\t\t\tgetExtension( 'OES_texture_float_linear' );\n\t\t\tgetExtension( 'EXT_color_buffer_half_float' );\n\t\t\tgetExtension( 'WEBGL_multisampled_render_to_texture' );\n\t\t\tgetExtension( 'WEBGL_render_shared_exponent' );\n\n\t\t},\n\n\t\tget: function ( name ) {\n\n\t\t\tconst extension = getExtension( name );\n\n\t\t\tif ( extension === null ) {\n\n\t\t\t\twarnOnce( 'WebGLRenderer: ' + name + ' extension not supported.' );\n\n\t\t\t}\n\n\t\t\treturn extension;\n\n\t\t}\n\n\t};\n\n}\n\nfunction WebGLGeometries( gl, attributes, info, bindingStates ) {\n\n\tconst geometries = {};\n\tconst wireframeAttributes = new WeakMap();\n\n\tfunction onGeometryDispose( event ) {\n\n\t\tconst geometry = event.target;\n\n\t\tif ( geometry.index !== null ) {\n\n\t\t\tattributes.remove( geometry.index );\n\n\t\t}\n\n\t\tfor ( const name in geometry.attributes ) {\n\n\t\t\tattributes.remove( geometry.attributes[ name ] );\n\n\t\t}\n\n\t\tgeometry.removeEventListener( 'dispose', onGeometryDispose );\n\n\t\tdelete geometries[ geometry.id ];\n\n\t\tconst attribute = wireframeAttributes.get( geometry );\n\n\t\tif ( attribute ) {\n\n\t\t\tattributes.remove( attribute );\n\t\t\twireframeAttributes.delete( geometry );\n\n\t\t}\n\n\t\tbindingStates.releaseStatesOfGeometry( geometry );\n\n\t\tif ( geometry.isInstancedBufferGeometry === true ) {\n\n\t\t\tdelete geometry._maxInstanceCount;\n\n\t\t}\n\n\t\t//\n\n\t\tinfo.memory.geometries --;\n\n\t}\n\n\tfunction get( object, geometry ) {\n\n\t\tif ( geometries[ geometry.id ] === true ) return geometry;\n\n\t\tgeometry.addEventListener( 'dispose', onGeometryDispose );\n\n\t\tgeometries[ geometry.id ] = true;\n\n\t\tinfo.memory.geometries ++;\n\n\t\treturn geometry;\n\n\t}\n\n\tfunction update( geometry ) {\n\n\t\tconst geometryAttributes = geometry.attributes;\n\n\t\t// Updating index buffer in VAO now. See WebGLBindingStates.\n\n\t\tfor ( const name in geometryAttributes ) {\n\n\t\t\tattributes.update( geometryAttributes[ name ], gl.ARRAY_BUFFER );\n\n\t\t}\n\n\t}\n\n\tfunction updateWireframeAttribute( geometry ) {\n\n\t\tconst indices = [];\n\n\t\tconst geometryIndex = geometry.index;\n\t\tconst geometryPosition = geometry.attributes.position;\n\t\tlet version = 0;\n\n\t\tif ( geometryIndex !== null ) {\n\n\t\t\tconst array = geometryIndex.array;\n\t\t\tversion = geometryIndex.version;\n\n\t\t\tfor ( let i = 0, l = array.length; i < l; i += 3 ) {\n\n\t\t\t\tconst a = array[ i + 0 ];\n\t\t\t\tconst b = array[ i + 1 ];\n\t\t\t\tconst c = array[ i + 2 ];\n\n\t\t\t\tindices.push( a, b, b, c, c, a );\n\n\t\t\t}\n\n\t\t} else if ( geometryPosition !== undefined ) {\n\n\t\t\tconst array = geometryPosition.array;\n\t\t\tversion = geometryPosition.version;\n\n\t\t\tfor ( let i = 0, l = ( array.length / 3 ) - 1; i < l; i += 3 ) {\n\n\t\t\t\tconst a = i + 0;\n\t\t\t\tconst b = i + 1;\n\t\t\t\tconst c = i + 2;\n\n\t\t\t\tindices.push( a, b, b, c, c, a );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tconst attribute = new ( arrayNeedsUint32( indices ) ? Uint32BufferAttribute : Uint16BufferAttribute )( indices, 1 );\n\t\tattribute.version = version;\n\n\t\t// Updating index buffer in VAO now. See WebGLBindingStates\n\n\t\t//\n\n\t\tconst previousAttribute = wireframeAttributes.get( geometry );\n\n\t\tif ( previousAttribute ) attributes.remove( previousAttribute );\n\n\t\t//\n\n\t\twireframeAttributes.set( geometry, attribute );\n\n\t}\n\n\tfunction getWireframeAttribute( geometry ) {\n\n\t\tconst currentAttribute = wireframeAttributes.get( geometry );\n\n\t\tif ( currentAttribute ) {\n\n\t\t\tconst geometryIndex = geometry.index;\n\n\t\t\tif ( geometryIndex !== null ) {\n\n\t\t\t\t// if the attribute is obsolete, create a new one\n\n\t\t\t\tif ( currentAttribute.version < geometryIndex.version ) {\n\n\t\t\t\t\tupdateWireframeAttribute( geometry );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tupdateWireframeAttribute( geometry );\n\n\t\t}\n\n\t\treturn wireframeAttributes.get( geometry );\n\n\t}\n\n\treturn {\n\n\t\tget: get,\n\t\tupdate: update,\n\n\t\tgetWireframeAttribute: getWireframeAttribute\n\n\t};\n\n}\n\nfunction WebGLIndexedBufferRenderer( gl, extensions, info ) {\n\n\tlet mode;\n\n\tfunction setMode( value ) {\n\n\t\tmode = value;\n\n\t}\n\n\tlet type, bytesPerElement;\n\n\tfunction setIndex( value ) {\n\n\t\ttype = value.type;\n\t\tbytesPerElement = value.bytesPerElement;\n\n\t}\n\n\tfunction render( start, count ) {\n\n\t\tgl.drawElements( mode, count, type, start * bytesPerElement );\n\n\t\tinfo.update( count, mode, 1 );\n\n\t}\n\n\tfunction renderInstances( start, count, primcount ) {\n\n\t\tif ( primcount === 0 ) return;\n\n\t\tgl.drawElementsInstanced( mode, count, type, start * bytesPerElement, primcount );\n\n\t\tinfo.update( count, mode, primcount );\n\n\t}\n\n\tfunction renderMultiDraw( starts, counts, drawCount ) {\n\n\t\tif ( drawCount === 0 ) return;\n\n\t\tconst extension = extensions.get( 'WEBGL_multi_draw' );\n\t\textension.multiDrawElementsWEBGL( mode, counts, 0, type, starts, 0, drawCount );\n\n\t\tlet elementCount = 0;\n\t\tfor ( let i = 0; i < drawCount; i ++ ) {\n\n\t\t\telementCount += counts[ i ];\n\n\t\t}\n\n\t\tinfo.update( elementCount, mode, 1 );\n\n\n\t}\n\n\tfunction renderMultiDrawInstances( starts, counts, drawCount, primcount ) {\n\n\t\tif ( drawCount === 0 ) return;\n\n\t\tconst extension = extensions.get( 'WEBGL_multi_draw' );\n\n\t\tif ( extension === null ) {\n\n\t\t\tfor ( let i = 0; i < starts.length; i ++ ) {\n\n\t\t\t\trenderInstances( starts[ i ] / bytesPerElement, counts[ i ], primcount[ i ] );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\textension.multiDrawElementsInstancedWEBGL( mode, counts, 0, type, starts, 0, primcount, 0, drawCount );\n\n\t\t\tlet elementCount = 0;\n\t\t\tfor ( let i = 0; i < drawCount; i ++ ) {\n\n\t\t\t\telementCount += counts[ i ] * primcount[ i ];\n\n\t\t\t}\n\n\t\t\tinfo.update( elementCount, mode, 1 );\n\n\t\t}\n\n\t}\n\n\t//\n\n\tthis.setMode = setMode;\n\tthis.setIndex = setIndex;\n\tthis.render = render;\n\tthis.renderInstances = renderInstances;\n\tthis.renderMultiDraw = renderMultiDraw;\n\tthis.renderMultiDrawInstances = renderMultiDrawInstances;\n\n}\n\nfunction WebGLInfo( gl ) {\n\n\tconst memory = {\n\t\tgeometries: 0,\n\t\ttextures: 0\n\t};\n\n\tconst render = {\n\t\tframe: 0,\n\t\tcalls: 0,\n\t\ttriangles: 0,\n\t\tpoints: 0,\n\t\tlines: 0\n\t};\n\n\tfunction update( count, mode, instanceCount ) {\n\n\t\trender.calls ++;\n\n\t\tswitch ( mode ) {\n\n\t\t\tcase gl.TRIANGLES:\n\t\t\t\trender.triangles += instanceCount * ( count / 3 );\n\t\t\t\tbreak;\n\n\t\t\tcase gl.LINES:\n\t\t\t\trender.lines += instanceCount * ( count / 2 );\n\t\t\t\tbreak;\n\n\t\t\tcase gl.LINE_STRIP:\n\t\t\t\trender.lines += instanceCount * ( count - 1 );\n\t\t\t\tbreak;\n\n\t\t\tcase gl.LINE_LOOP:\n\t\t\t\trender.lines += instanceCount * count;\n\t\t\t\tbreak;\n\n\t\t\tcase gl.POINTS:\n\t\t\t\trender.points += instanceCount * count;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\terror( 'WebGLInfo: Unknown draw mode:', mode );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t}\n\n\tfunction reset() {\n\n\t\trender.calls = 0;\n\t\trender.triangles = 0;\n\t\trender.points = 0;\n\t\trender.lines = 0;\n\n\t}\n\n\treturn {\n\t\tmemory: memory,\n\t\trender: render,\n\t\tprograms: null,\n\t\tautoReset: true,\n\t\treset: reset,\n\t\tupdate: update\n\t};\n\n}\n\nfunction WebGLMorphtargets( gl, capabilities, textures ) {\n\n\tconst morphTextures = new WeakMap();\n\tconst morph = new Vector4();\n\n\tfunction update( object, geometry, program ) {\n\n\t\tconst objectInfluences = object.morphTargetInfluences;\n\n\t\t// the following encodes morph targets into an array of data textures. Each layer represents a single morph target.\n\n\t\tconst morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;\n\t\tconst morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;\n\n\t\tlet entry = morphTextures.get( geometry );\n\n\t\tif ( entry === undefined || entry.count !== morphTargetsCount ) {\n\n\t\t\tif ( entry !== undefined ) entry.texture.dispose();\n\n\t\t\tconst hasMorphPosition = geometry.morphAttributes.position !== undefined;\n\t\t\tconst hasMorphNormals = geometry.morphAttributes.normal !== undefined;\n\t\t\tconst hasMorphColors = geometry.morphAttributes.color !== undefined;\n\n\t\t\tconst morphTargets = geometry.morphAttributes.position || [];\n\t\t\tconst morphNormals = geometry.morphAttributes.normal || [];\n\t\t\tconst morphColors = geometry.morphAttributes.color || [];\n\n\t\t\tlet vertexDataCount = 0;\n\n\t\t\tif ( hasMorphPosition === true ) vertexDataCount = 1;\n\t\t\tif ( hasMorphNormals === true ) vertexDataCount = 2;\n\t\t\tif ( hasMorphColors === true ) vertexDataCount = 3;\n\n\t\t\tlet width = geometry.attributes.position.count * vertexDataCount;\n\t\t\tlet height = 1;\n\n\t\t\tif ( width > capabilities.maxTextureSize ) {\n\n\t\t\t\theight = Math.ceil( width / capabilities.maxTextureSize );\n\t\t\t\twidth = capabilities.maxTextureSize;\n\n\t\t\t}\n\n\t\t\tconst buffer = new Float32Array( width * height * 4 * morphTargetsCount );\n\n\t\t\tconst texture = new DataArrayTexture( buffer, width, height, morphTargetsCount );\n\t\t\ttexture.type = FloatType;\n\t\t\ttexture.needsUpdate = true;\n\n\t\t\t// fill buffer\n\n\t\t\tconst vertexDataStride = vertexDataCount * 4;\n\n\t\t\tfor ( let i = 0; i < morphTargetsCount; i ++ ) {\n\n\t\t\t\tconst morphTarget = morphTargets[ i ];\n\t\t\t\tconst morphNormal = morphNormals[ i ];\n\t\t\t\tconst morphColor = morphColors[ i ];\n\n\t\t\t\tconst offset = width * height * 4 * i;\n\n\t\t\t\tfor ( let j = 0; j < morphTarget.count; j ++ ) {\n\n\t\t\t\t\tconst stride = j * vertexDataStride;\n\n\t\t\t\t\tif ( hasMorphPosition === true ) {\n\n\t\t\t\t\t\tmorph.fromBufferAttribute( morphTarget, j );\n\n\t\t\t\t\t\tbuffer[ offset + stride + 0 ] = morph.x;\n\t\t\t\t\t\tbuffer[ offset + stride + 1 ] = morph.y;\n\t\t\t\t\t\tbuffer[ offset + stride + 2 ] = morph.z;\n\t\t\t\t\t\tbuffer[ offset + stride + 3 ] = 0;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( hasMorphNormals === true ) {\n\n\t\t\t\t\t\tmorph.fromBufferAttribute( morphNormal, j );\n\n\t\t\t\t\t\tbuffer[ offset + stride + 4 ] = morph.x;\n\t\t\t\t\t\tbuffer[ offset + stride + 5 ] = morph.y;\n\t\t\t\t\t\tbuffer[ offset + stride + 6 ] = morph.z;\n\t\t\t\t\t\tbuffer[ offset + stride + 7 ] = 0;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( hasMorphColors === true ) {\n\n\t\t\t\t\t\tmorph.fromBufferAttribute( morphColor, j );\n\n\t\t\t\t\t\tbuffer[ offset + stride + 8 ] = morph.x;\n\t\t\t\t\t\tbuffer[ offset + stride + 9 ] = morph.y;\n\t\t\t\t\t\tbuffer[ offset + stride + 10 ] = morph.z;\n\t\t\t\t\t\tbuffer[ offset + stride + 11 ] = ( morphColor.itemSize === 4 ) ? morph.w : 1;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tentry = {\n\t\t\t\tcount: morphTargetsCount,\n\t\t\t\ttexture: texture,\n\t\t\t\tsize: new Vector2( width, height )\n\t\t\t};\n\n\t\t\tmorphTextures.set( geometry, entry );\n\n\t\t\tfunction disposeTexture() {\n\n\t\t\t\ttexture.dispose();\n\n\t\t\t\tmorphTextures.delete( geometry );\n\n\t\t\t\tgeometry.removeEventListener( 'dispose', disposeTexture );\n\n\t\t\t}\n\n\t\t\tgeometry.addEventListener( 'dispose', disposeTexture );\n\n\t\t}\n\n\t\t//\n\t\tif ( object.isInstancedMesh === true && object.morphTexture !== null ) {\n\n\t\t\tprogram.getUniforms().setValue( gl, 'morphTexture', object.morphTexture, textures );\n\n\t\t} else {\n\n\t\t\tlet morphInfluencesSum = 0;\n\n\t\t\tfor ( let i = 0; i < objectInfluences.length; i ++ ) {\n\n\t\t\t\tmorphInfluencesSum += objectInfluences[ i ];\n\n\t\t\t}\n\n\t\t\tconst morphBaseInfluence = geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;\n\n\n\t\t\tprogram.getUniforms().setValue( gl, 'morphTargetBaseInfluence', morphBaseInfluence );\n\t\t\tprogram.getUniforms().setValue( gl, 'morphTargetInfluences', objectInfluences );\n\n\t\t}\n\n\t\tprogram.getUniforms().setValue( gl, 'morphTargetsTexture', entry.texture, textures );\n\t\tprogram.getUniforms().setValue( gl, 'morphTargetsTextureSize', entry.size );\n\n\t}\n\n\treturn {\n\n\t\tupdate: update\n\n\t};\n\n}\n\nfunction WebGLObjects( gl, geometries, attributes, info ) {\n\n\tlet updateMap = new WeakMap();\n\n\tfunction update( object ) {\n\n\t\tconst frame = info.render.frame;\n\n\t\tconst geometry = object.geometry;\n\t\tconst buffergeometry = geometries.get( object, geometry );\n\n\t\t// Update once per frame\n\n\t\tif ( updateMap.get( buffergeometry ) !== frame ) {\n\n\t\t\tgeometries.update( buffergeometry );\n\n\t\t\tupdateMap.set( buffergeometry, frame );\n\n\t\t}\n\n\t\tif ( object.isInstancedMesh ) {\n\n\t\t\tif ( object.hasEventListener( 'dispose', onInstancedMeshDispose ) === false ) {\n\n\t\t\t\tobject.addEventListener( 'dispose', onInstancedMeshDispose );\n\n\t\t\t}\n\n\t\t\tif ( updateMap.get( object ) !== frame ) {\n\n\t\t\t\tattributes.update( object.instanceMatrix, gl.ARRAY_BUFFER );\n\n\t\t\t\tif ( object.instanceColor !== null ) {\n\n\t\t\t\t\tattributes.update( object.instanceColor, gl.ARRAY_BUFFER );\n\n\t\t\t\t}\n\n\t\t\t\tupdateMap.set( object, frame );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( object.isSkinnedMesh ) {\n\n\t\t\tconst skeleton = object.skeleton;\n\n\t\t\tif ( updateMap.get( skeleton ) !== frame ) {\n\n\t\t\t\tskeleton.update();\n\n\t\t\t\tupdateMap.set( skeleton, frame );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn buffergeometry;\n\n\t}\n\n\tfunction dispose() {\n\n\t\tupdateMap = new WeakMap();\n\n\t}\n\n\tfunction onInstancedMeshDispose( event ) {\n\n\t\tconst instancedMesh = event.target;\n\n\t\tinstancedMesh.removeEventListener( 'dispose', onInstancedMeshDispose );\n\n\t\tattributes.remove( instancedMesh.instanceMatrix );\n\n\t\tif ( instancedMesh.instanceColor !== null ) attributes.remove( instancedMesh.instanceColor );\n\n\t}\n\n\treturn {\n\n\t\tupdate: update,\n\t\tdispose: dispose\n\n\t};\n\n}\n\nconst toneMappingMap = {\n\t[ LinearToneMapping ]: 'LINEAR_TONE_MAPPING',\n\t[ ReinhardToneMapping ]: 'REINHARD_TONE_MAPPING',\n\t[ CineonToneMapping ]: 'CINEON_TONE_MAPPING',\n\t[ ACESFilmicToneMapping ]: 'ACES_FILMIC_TONE_MAPPING',\n\t[ AgXToneMapping ]: 'AGX_TONE_MAPPING',\n\t[ NeutralToneMapping ]: 'NEUTRAL_TONE_MAPPING',\n\t[ CustomToneMapping ]: 'CUSTOM_TONE_MAPPING'\n};\n\nfunction WebGLOutput( type, width, height, depth, stencil ) {\n\n\t// render targets for scene and post-processing\n\tconst targetA = new WebGLRenderTarget( width, height, {\n\t\ttype: type,\n\t\tdepthBuffer: depth,\n\t\tstencilBuffer: stencil\n\t} );\n\n\tconst targetB = new WebGLRenderTarget( width, height, {\n\t\ttype: HalfFloatType,\n\t\tdepthBuffer: false,\n\t\tstencilBuffer: false\n\t} );\n\n\t// create fullscreen triangle geometry\n\tconst geometry = new BufferGeometry();\n\tgeometry.setAttribute( 'position', new Float32BufferAttribute( [ -1, 3, 0, -1, -1, 0, 3, -1, 0 ], 3 ) );\n\tgeometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );\n\n\t// create output material with tone mapping support\n\tconst material = new RawShaderMaterial( {\n\t\tuniforms: {\n\t\t\ttDiffuse: { value: null }\n\t\t},\n\t\tvertexShader: /* glsl */`\n\t\t\tprecision highp float;\n\n\t\t\tuniform mat4 modelViewMatrix;\n\t\t\tuniform mat4 projectionMatrix;\n\n\t\t\tattribute vec3 position;\n\t\t\tattribute vec2 uv;\n\n\t\t\tvarying vec2 vUv;\n\n\t\t\tvoid main() {\n\t\t\t\tvUv = uv;\n\t\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\t\t\t}`,\n\t\tfragmentShader: /* glsl */`\n\t\t\tprecision highp float;\n\n\t\t\tuniform sampler2D tDiffuse;\n\n\t\t\tvarying vec2 vUv;\n\n\t\t\t#include <tonemapping_pars_fragment>\n\t\t\t#include <colorspace_pars_fragment>\n\n\t\t\tvoid main() {\n\t\t\t\tgl_FragColor = texture2D( tDiffuse, vUv );\n\n\t\t\t\t#ifdef LINEAR_TONE_MAPPING\n\t\t\t\t\tgl_FragColor.rgb = LinearToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( REINHARD_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = ReinhardToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( CINEON_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( ACES_FILMIC_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( AGX_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( NEUTRAL_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = NeutralToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( CUSTOM_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = CustomToneMapping( gl_FragColor.rgb );\n\t\t\t\t#endif\n\n\t\t\t\t#ifdef SRGB_TRANSFER\n\t\t\t\t\tgl_FragColor = sRGBTransferOETF( gl_FragColor );\n\t\t\t\t#endif\n\t\t\t}`,\n\t\tdepthTest: false,\n\t\tdepthWrite: false\n\t} );\n\n\tconst mesh = new Mesh( geometry, material );\n\tconst camera = new OrthographicCamera( -1, 1, 1, -1, 0, 1 );\n\n\tlet _outputColorSpace = null;\n\tlet _outputToneMapping = null;\n\tlet _isCompositing = false;\n\tlet _savedToneMapping;\n\tlet _savedRenderTarget = null;\n\tlet _effects = [];\n\tlet _hasRenderPass = false;\n\n\tthis.setSize = function ( width, height ) {\n\n\t\ttargetA.setSize( width, height );\n\t\ttargetB.setSize( width, height );\n\n\t\tfor ( let i = 0; i < _effects.length; i ++ ) {\n\n\t\t\tconst effect = _effects[ i ];\n\t\t\tif ( effect.setSize ) effect.setSize( width, height );\n\n\t\t}\n\n\t};\n\n\tthis.setEffects = function ( effects ) {\n\n\t\t_effects = effects;\n\t\t_hasRenderPass = _effects.length > 0 && _effects[ 0 ].isRenderPass === true;\n\n\t\tconst width = targetA.width;\n\t\tconst height = targetA.height;\n\n\t\tfor ( let i = 0; i < _effects.length; i ++ ) {\n\n\t\t\tconst effect = _effects[ i ];\n\t\t\tif ( effect.setSize ) effect.setSize( width, height );\n\n\t\t}\n\n\t};\n\n\tthis.begin = function ( renderer, renderTarget ) {\n\n\t\t// Don't begin during compositing phase (post-processing effects call render())\n\t\tif ( _isCompositing ) return false;\n\n\t\tif ( renderer.toneMapping === NoToneMapping && _effects.length === 0 ) return false;\n\n\t\t_savedRenderTarget = renderTarget;\n\n\t\t// resize internal buffers to match render target (e.g. XR resolution)\n\t\tif ( renderTarget !== null ) {\n\n\t\t\tconst width = renderTarget.width;\n\t\t\tconst height = renderTarget.height;\n\n\t\t\tif ( targetA.width !== width || targetA.height !== height ) {\n\n\t\t\t\tthis.setSize( width, height );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// if first effect is a RenderPass, it will set its own render target\n\t\tif ( _hasRenderPass === false ) {\n\n\t\t\trenderer.setRenderTarget( targetA );\n\n\t\t}\n\n\t\t// disable tone mapping during render - it will be applied in end()\n\t\t_savedToneMapping = renderer.toneMapping;\n\t\trenderer.toneMapping = NoToneMapping;\n\n\t\treturn true;\n\n\t};\n\n\tthis.hasRenderPass = function () {\n\n\t\treturn _hasRenderPass;\n\n\t};\n\n\tthis.end = function ( renderer, deltaTime ) {\n\n\t\t// restore tone mapping\n\t\trenderer.toneMapping = _savedToneMapping;\n\n\t\t_isCompositing = true;\n\n\t\t// run post-processing effects\n\t\tlet readBuffer = targetA;\n\t\tlet writeBuffer = targetB;\n\n\t\tfor ( let i = 0; i < _effects.length; i ++ ) {\n\n\t\t\tconst effect = _effects[ i ];\n\n\t\t\tif ( effect.enabled === false ) continue;\n\n\t\t\teffect.render( renderer, writeBuffer, readBuffer, deltaTime );\n\n\t\t\tif ( effect.needsSwap !== false ) {\n\n\t\t\t\tconst temp = readBuffer;\n\t\t\t\treadBuffer = writeBuffer;\n\t\t\t\twriteBuffer = temp;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// update output material defines if settings changed\n\t\tif ( _outputColorSpace !== renderer.outputColorSpace || _outputToneMapping !== renderer.toneMapping ) {\n\n\t\t\t_outputColorSpace = renderer.outputColorSpace;\n\t\t\t_outputToneMapping = renderer.toneMapping;\n\n\t\t\tmaterial.defines = {};\n\n\t\t\tif ( ColorManagement.getTransfer( _outputColorSpace ) === SRGBTransfer ) material.defines.SRGB_TRANSFER = '';\n\n\t\t\tconst toneMapping = toneMappingMap[ _outputToneMapping ];\n\t\t\tif ( toneMapping ) material.defines[ toneMapping ] = '';\n\n\t\t\tmaterial.needsUpdate = true;\n\n\t\t}\n\n\t\t// final output to canvas (or XR render target)\n\t\tmaterial.uniforms.tDiffuse.value = readBuffer.texture;\n\t\trenderer.setRenderTarget( _savedRenderTarget );\n\t\trenderer.render( mesh, camera );\n\n\t\t_savedRenderTarget = null;\n\t\t_isCompositing = false;\n\n\t};\n\n\tthis.isCompositing = function () {\n\n\t\treturn _isCompositing;\n\n\t};\n\n\tthis.dispose = function () {\n\n\t\ttargetA.dispose();\n\t\ttargetB.dispose();\n\t\tgeometry.dispose();\n\t\tmaterial.dispose();\n\n\t};\n\n}\n\n/**\n * Uniforms of a program.\n * Those form a tree structure with a special top-level container for the root,\n * which you get by calling 'new WebGLUniforms( gl, program )'.\n *\n *\n * Properties of inner nodes including the top-level container:\n *\n * .seq - array of nested uniforms\n * .map - nested uniforms by name\n *\n *\n * Methods of all nodes except the top-level container:\n *\n * .setValue( gl, value, [textures] )\n *\n * \t\tuploads a uniform value(s)\n *  \tthe 'textures' parameter is needed for sampler uniforms\n *\n *\n * Static methods of the top-level container (textures factorizations):\n *\n * .upload( gl, seq, values, textures )\n *\n * \t\tsets uniforms in 'seq' to 'values[id].value'\n *\n * .seqWithValue( seq, values ) : filteredSeq\n *\n * \t\tfilters 'seq' entries with corresponding entry in values\n *\n *\n * Methods of the top-level container (textures factorizations):\n *\n * .setValue( gl, name, value, textures )\n *\n * \t\tsets uniform with  name 'name' to 'value'\n *\n * .setOptional( gl, obj, prop )\n *\n * \t\tlike .set for an optional property of the object\n *\n */\n\n\nconst emptyTexture = /*@__PURE__*/ new Texture();\n\nconst emptyShadowTexture = /*@__PURE__*/ new DepthTexture( 1, 1 );\n\nconst emptyArrayTexture = /*@__PURE__*/ new DataArrayTexture();\nconst empty3dTexture = /*@__PURE__*/ new Data3DTexture();\nconst emptyCubeTexture = /*@__PURE__*/ new CubeTexture();\n\n// --- Utilities ---\n\n// Array Caches (provide typed arrays for temporary by size)\n\nconst arrayCacheF32 = [];\nconst arrayCacheI32 = [];\n\n// Float32Array caches used for uploading Matrix uniforms\n\nconst mat4array = new Float32Array( 16 );\nconst mat3array = new Float32Array( 9 );\nconst mat2array = new Float32Array( 4 );\n\n// Flattening for arrays of vectors and matrices\n\nfunction flatten( array, nBlocks, blockSize ) {\n\n\tconst firstElem = array[ 0 ];\n\n\tif ( firstElem <= 0 || firstElem > 0 ) return array;\n\t// unoptimized: ! isNaN( firstElem )\n\t// see http://jacksondunstan.com/articles/983\n\n\tconst n = nBlocks * blockSize;\n\tlet r = arrayCacheF32[ n ];\n\n\tif ( r === undefined ) {\n\n\t\tr = new Float32Array( n );\n\t\tarrayCacheF32[ n ] = r;\n\n\t}\n\n\tif ( nBlocks !== 0 ) {\n\n\t\tfirstElem.toArray( r, 0 );\n\n\t\tfor ( let i = 1, offset = 0; i !== nBlocks; ++ i ) {\n\n\t\t\toffset += blockSize;\n\t\t\tarray[ i ].toArray( r, offset );\n\n\t\t}\n\n\t}\n\n\treturn r;\n\n}\n\nfunction arraysEqual( a, b ) {\n\n\tif ( a.length !== b.length ) return false;\n\n\tfor ( let i = 0, l = a.length; i < l; i ++ ) {\n\n\t\tif ( a[ i ] !== b[ i ] ) return false;\n\n\t}\n\n\treturn true;\n\n}\n\nfunction copyArray( a, b ) {\n\n\tfor ( let i = 0, l = b.length; i < l; i ++ ) {\n\n\t\ta[ i ] = b[ i ];\n\n\t}\n\n}\n\n// Texture unit allocation\n\nfunction allocTexUnits( textures, n ) {\n\n\tlet r = arrayCacheI32[ n ];\n\n\tif ( r === undefined ) {\n\n\t\tr = new Int32Array( n );\n\t\tarrayCacheI32[ n ] = r;\n\n\t}\n\n\tfor ( let i = 0; i !== n; ++ i ) {\n\n\t\tr[ i ] = textures.allocateTextureUnit();\n\n\t}\n\n\treturn r;\n\n}\n\n// --- Setters ---\n\n// Note: Defining these methods externally, because they come in a bunch\n// and this way their names minify.\n\n// Single scalar\n\nfunction setValueV1f( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( cache[ 0 ] === v ) return;\n\n\tgl.uniform1f( this.addr, v );\n\n\tcache[ 0 ] = v;\n\n}\n\n// Single float vector (from flat array or THREE.VectorN)\n\nfunction setValueV2f( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( v.x !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y ) {\n\n\t\t\tgl.uniform2f( this.addr, v.x, v.y );\n\n\t\t\tcache[ 0 ] = v.x;\n\t\t\tcache[ 1 ] = v.y;\n\n\t\t}\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniform2fv( this.addr, v );\n\n\t\tcopyArray( cache, v );\n\n\t}\n\n}\n\nfunction setValueV3f( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( v.x !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z ) {\n\n\t\t\tgl.uniform3f( this.addr, v.x, v.y, v.z );\n\n\t\t\tcache[ 0 ] = v.x;\n\t\t\tcache[ 1 ] = v.y;\n\t\t\tcache[ 2 ] = v.z;\n\n\t\t}\n\n\t} else if ( v.r !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.r || cache[ 1 ] !== v.g || cache[ 2 ] !== v.b ) {\n\n\t\t\tgl.uniform3f( this.addr, v.r, v.g, v.b );\n\n\t\t\tcache[ 0 ] = v.r;\n\t\t\tcache[ 1 ] = v.g;\n\t\t\tcache[ 2 ] = v.b;\n\n\t\t}\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniform3fv( this.addr, v );\n\n\t\tcopyArray( cache, v );\n\n\t}\n\n}\n\nfunction setValueV4f( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( v.x !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z || cache[ 3 ] !== v.w ) {\n\n\t\t\tgl.uniform4f( this.addr, v.x, v.y, v.z, v.w );\n\n\t\t\tcache[ 0 ] = v.x;\n\t\t\tcache[ 1 ] = v.y;\n\t\t\tcache[ 2 ] = v.z;\n\t\t\tcache[ 3 ] = v.w;\n\n\t\t}\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniform4fv( this.addr, v );\n\n\t\tcopyArray( cache, v );\n\n\t}\n\n}\n\n// Single matrix (from flat array or THREE.MatrixN)\n\nfunction setValueM2( gl, v ) {\n\n\tconst cache = this.cache;\n\tconst elements = v.elements;\n\n\tif ( elements === undefined ) {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniformMatrix2fv( this.addr, false, v );\n\n\t\tcopyArray( cache, v );\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, elements ) ) return;\n\n\t\tmat2array.set( elements );\n\n\t\tgl.uniformMatrix2fv( this.addr, false, mat2array );\n\n\t\tcopyArray( cache, elements );\n\n\t}\n\n}\n\nfunction setValueM3( gl, v ) {\n\n\tconst cache = this.cache;\n\tconst elements = v.elements;\n\n\tif ( elements === undefined ) {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniformMatrix3fv( this.addr, false, v );\n\n\t\tcopyArray( cache, v );\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, elements ) ) return;\n\n\t\tmat3array.set( elements );\n\n\t\tgl.uniformMatrix3fv( this.addr, false, mat3array );\n\n\t\tcopyArray( cache, elements );\n\n\t}\n\n}\n\nfunction setValueM4( gl, v ) {\n\n\tconst cache = this.cache;\n\tconst elements = v.elements;\n\n\tif ( elements === undefined ) {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniformMatrix4fv( this.addr, false, v );\n\n\t\tcopyArray( cache, v );\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, elements ) ) return;\n\n\t\tmat4array.set( elements );\n\n\t\tgl.uniformMatrix4fv( this.addr, false, mat4array );\n\n\t\tcopyArray( cache, elements );\n\n\t}\n\n}\n\n// Single integer / boolean\n\nfunction setValueV1i( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( cache[ 0 ] === v ) return;\n\n\tgl.uniform1i( this.addr, v );\n\n\tcache[ 0 ] = v;\n\n}\n\n// Single integer / boolean vector (from flat array or THREE.VectorN)\n\nfunction setValueV2i( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( v.x !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y ) {\n\n\t\t\tgl.uniform2i( this.addr, v.x, v.y );\n\n\t\t\tcache[ 0 ] = v.x;\n\t\t\tcache[ 1 ] = v.y;\n\n\t\t}\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniform2iv( this.addr, v );\n\n\t\tcopyArray( cache, v );\n\n\t}\n\n}\n\nfunction setValueV3i( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( v.x !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z ) {\n\n\t\t\tgl.uniform3i( this.addr, v.x, v.y, v.z );\n\n\t\t\tcache[ 0 ] = v.x;\n\t\t\tcache[ 1 ] = v.y;\n\t\t\tcache[ 2 ] = v.z;\n\n\t\t}\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniform3iv( this.addr, v );\n\n\t\tcopyArray( cache, v );\n\n\t}\n\n}\n\nfunction setValueV4i( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( v.x !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z || cache[ 3 ] !== v.w ) {\n\n\t\t\tgl.uniform4i( this.addr, v.x, v.y, v.z, v.w );\n\n\t\t\tcache[ 0 ] = v.x;\n\t\t\tcache[ 1 ] = v.y;\n\t\t\tcache[ 2 ] = v.z;\n\t\t\tcache[ 3 ] = v.w;\n\n\t\t}\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniform4iv( this.addr, v );\n\n\t\tcopyArray( cache, v );\n\n\t}\n\n}\n\n// Single unsigned integer\n\nfunction setValueV1ui( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( cache[ 0 ] === v ) return;\n\n\tgl.uniform1ui( this.addr, v );\n\n\tcache[ 0 ] = v;\n\n}\n\n// Single unsigned integer vector (from flat array or THREE.VectorN)\n\nfunction setValueV2ui( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( v.x !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y ) {\n\n\t\t\tgl.uniform2ui( this.addr, v.x, v.y );\n\n\t\t\tcache[ 0 ] = v.x;\n\t\t\tcache[ 1 ] = v.y;\n\n\t\t}\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniform2uiv( this.addr, v );\n\n\t\tcopyArray( cache, v );\n\n\t}\n\n}\n\nfunction setValueV3ui( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( v.x !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z ) {\n\n\t\t\tgl.uniform3ui( this.addr, v.x, v.y, v.z );\n\n\t\t\tcache[ 0 ] = v.x;\n\t\t\tcache[ 1 ] = v.y;\n\t\t\tcache[ 2 ] = v.z;\n\n\t\t}\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniform3uiv( this.addr, v );\n\n\t\tcopyArray( cache, v );\n\n\t}\n\n}\n\nfunction setValueV4ui( gl, v ) {\n\n\tconst cache = this.cache;\n\n\tif ( v.x !== undefined ) {\n\n\t\tif ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z || cache[ 3 ] !== v.w ) {\n\n\t\t\tgl.uniform4ui( this.addr, v.x, v.y, v.z, v.w );\n\n\t\t\tcache[ 0 ] = v.x;\n\t\t\tcache[ 1 ] = v.y;\n\t\t\tcache[ 2 ] = v.z;\n\t\t\tcache[ 3 ] = v.w;\n\n\t\t}\n\n\t} else {\n\n\t\tif ( arraysEqual( cache, v ) ) return;\n\n\t\tgl.uniform4uiv( this.addr, v );\n\n\t\tcopyArray( cache, v );\n\n\t}\n\n}\n\n\n// Single texture (2D / Cube)\n\nfunction setValueT1( gl, v, textures ) {\n\n\tconst cache = this.cache;\n\tconst unit = textures.allocateTextureUnit();\n\n\tif ( cache[ 0 ] !== unit ) {\n\n\t\tgl.uniform1i( this.addr, unit );\n\t\tcache[ 0 ] = unit;\n\n\t}\n\n\tlet emptyTexture2D;\n\n\tif ( this.type === gl.SAMPLER_2D_SHADOW ) {\n\n\t\temptyShadowTexture.compareFunction = textures.isReversedDepthBuffer() ? GreaterEqualCompare : LessEqualCompare;\n\t\temptyTexture2D = emptyShadowTexture;\n\n\t} else {\n\n\t\temptyTexture2D = emptyTexture;\n\n\t}\n\n\ttextures.setTexture2D( v || emptyTexture2D, unit );\n\n}\n\nfunction setValueT3D1( gl, v, textures ) {\n\n\tconst cache = this.cache;\n\tconst unit = textures.allocateTextureUnit();\n\n\tif ( cache[ 0 ] !== unit ) {\n\n\t\tgl.uniform1i( this.addr, unit );\n\t\tcache[ 0 ] = unit;\n\n\t}\n\n\ttextures.setTexture3D( v || empty3dTexture, unit );\n\n}\n\nfunction setValueT6( gl, v, textures ) {\n\n\tconst cache = this.cache;\n\tconst unit = textures.allocateTextureUnit();\n\n\tif ( cache[ 0 ] !== unit ) {\n\n\t\tgl.uniform1i( this.addr, unit );\n\t\tcache[ 0 ] = unit;\n\n\t}\n\n\ttextures.setTextureCube( v || emptyCubeTexture, unit );\n\n}\n\nfunction setValueT2DArray1( gl, v, textures ) {\n\n\tconst cache = this.cache;\n\tconst unit = textures.allocateTextureUnit();\n\n\tif ( cache[ 0 ] !== unit ) {\n\n\t\tgl.uniform1i( this.addr, unit );\n\t\tcache[ 0 ] = unit;\n\n\t}\n\n\ttextures.setTexture2DArray( v || emptyArrayTexture, unit );\n\n}\n\n// Helper to pick the right setter for the singular case\n\nfunction getSingularSetter( type ) {\n\n\tswitch ( type ) {\n\n\t\tcase 0x1406: return setValueV1f; // FLOAT\n\t\tcase 0x8b50: return setValueV2f; // _VEC2\n\t\tcase 0x8b51: return setValueV3f; // _VEC3\n\t\tcase 0x8b52: return setValueV4f; // _VEC4\n\n\t\tcase 0x8b5a: return setValueM2; // _MAT2\n\t\tcase 0x8b5b: return setValueM3; // _MAT3\n\t\tcase 0x8b5c: return setValueM4; // _MAT4\n\n\t\tcase 0x1404: case 0x8b56: return setValueV1i; // INT, BOOL\n\t\tcase 0x8b53: case 0x8b57: return setValueV2i; // _VEC2\n\t\tcase 0x8b54: case 0x8b58: return setValueV3i; // _VEC3\n\t\tcase 0x8b55: case 0x8b59: return setValueV4i; // _VEC4\n\n\t\tcase 0x1405: return setValueV1ui; // UINT\n\t\tcase 0x8dc6: return setValueV2ui; // _VEC2\n\t\tcase 0x8dc7: return setValueV3ui; // _VEC3\n\t\tcase 0x8dc8: return setValueV4ui; // _VEC4\n\n\t\tcase 0x8b5e: // SAMPLER_2D\n\t\tcase 0x8d66: // SAMPLER_EXTERNAL_OES\n\t\tcase 0x8dca: // INT_SAMPLER_2D\n\t\tcase 0x8dd2: // UNSIGNED_INT_SAMPLER_2D\n\t\tcase 0x8b62: // SAMPLER_2D_SHADOW\n\t\t\treturn setValueT1;\n\n\t\tcase 0x8b5f: // SAMPLER_3D\n\t\tcase 0x8dcb: // INT_SAMPLER_3D\n\t\tcase 0x8dd3: // UNSIGNED_INT_SAMPLER_3D\n\t\t\treturn setValueT3D1;\n\n\t\tcase 0x8b60: // SAMPLER_CUBE\n\t\tcase 0x8dcc: // INT_SAMPLER_CUBE\n\t\tcase 0x8dd4: // UNSIGNED_INT_SAMPLER_CUBE\n\t\tcase 0x8dc5: // SAMPLER_CUBE_SHADOW\n\t\t\treturn setValueT6;\n\n\t\tcase 0x8dc1: // SAMPLER_2D_ARRAY\n\t\tcase 0x8dcf: // INT_SAMPLER_2D_ARRAY\n\t\tcase 0x8dd7: // UNSIGNED_INT_SAMPLER_2D_ARRAY\n\t\tcase 0x8dc4: // SAMPLER_2D_ARRAY_SHADOW\n\t\t\treturn setValueT2DArray1;\n\n\t}\n\n}\n\n\n// Array of scalars\n\nfunction setValueV1fArray( gl, v ) {\n\n\tgl.uniform1fv( this.addr, v );\n\n}\n\n// Array of vectors (from flat array or array of THREE.VectorN)\n\nfunction setValueV2fArray( gl, v ) {\n\n\tconst data = flatten( v, this.size, 2 );\n\n\tgl.uniform2fv( this.addr, data );\n\n}\n\nfunction setValueV3fArray( gl, v ) {\n\n\tconst data = flatten( v, this.size, 3 );\n\n\tgl.uniform3fv( this.addr, data );\n\n}\n\nfunction setValueV4fArray( gl, v ) {\n\n\tconst data = flatten( v, this.size, 4 );\n\n\tgl.uniform4fv( this.addr, data );\n\n}\n\n// Array of matrices (from flat array or array of THREE.MatrixN)\n\nfunction setValueM2Array( gl, v ) {\n\n\tconst data = flatten( v, this.size, 4 );\n\n\tgl.uniformMatrix2fv( this.addr, false, data );\n\n}\n\nfunction setValueM3Array( gl, v ) {\n\n\tconst data = flatten( v, this.size, 9 );\n\n\tgl.uniformMatrix3fv( this.addr, false, data );\n\n}\n\nfunction setValueM4Array( gl, v ) {\n\n\tconst data = flatten( v, this.size, 16 );\n\n\tgl.uniformMatrix4fv( this.addr, false, data );\n\n}\n\n// Array of integer / boolean\n\nfunction setValueV1iArray( gl, v ) {\n\n\tgl.uniform1iv( this.addr, v );\n\n}\n\n// Array of integer / boolean vectors (from flat array)\n\nfunction setValueV2iArray( gl, v ) {\n\n\tgl.uniform2iv( this.addr, v );\n\n}\n\nfunction setValueV3iArray( gl, v ) {\n\n\tgl.uniform3iv( this.addr, v );\n\n}\n\nfunction setValueV4iArray( gl, v ) {\n\n\tgl.uniform4iv( this.addr, v );\n\n}\n\n// Array of unsigned integer\n\nfunction setValueV1uiArray( gl, v ) {\n\n\tgl.uniform1uiv( this.addr, v );\n\n}\n\n// Array of unsigned integer vectors (from flat array)\n\nfunction setValueV2uiArray( gl, v ) {\n\n\tgl.uniform2uiv( this.addr, v );\n\n}\n\nfunction setValueV3uiArray( gl, v ) {\n\n\tgl.uniform3uiv( this.addr, v );\n\n}\n\nfunction setValueV4uiArray( gl, v ) {\n\n\tgl.uniform4uiv( this.addr, v );\n\n}\n\n\n// Array of textures (2D / 3D / Cube / 2DArray)\n\nfunction setValueT1Array( gl, v, textures ) {\n\n\tconst cache = this.cache;\n\n\tconst n = v.length;\n\n\tconst units = allocTexUnits( textures, n );\n\n\tif ( ! arraysEqual( cache, units ) ) {\n\n\t\tgl.uniform1iv( this.addr, units );\n\n\t\tcopyArray( cache, units );\n\n\t}\n\n\tlet emptyTexture2D;\n\n\tif ( this.type === gl.SAMPLER_2D_SHADOW ) {\n\n\t\temptyTexture2D = emptyShadowTexture;\n\n\t} else {\n\n\t\temptyTexture2D = emptyTexture;\n\n\t}\n\n\tfor ( let i = 0; i !== n; ++ i ) {\n\n\t\ttextures.setTexture2D( v[ i ] || emptyTexture2D, units[ i ] );\n\n\t}\n\n}\n\nfunction setValueT3DArray( gl, v, textures ) {\n\n\tconst cache = this.cache;\n\n\tconst n = v.length;\n\n\tconst units = allocTexUnits( textures, n );\n\n\tif ( ! arraysEqual( cache, units ) ) {\n\n\t\tgl.uniform1iv( this.addr, units );\n\n\t\tcopyArray( cache, units );\n\n\t}\n\n\tfor ( let i = 0; i !== n; ++ i ) {\n\n\t\ttextures.setTexture3D( v[ i ] || empty3dTexture, units[ i ] );\n\n\t}\n\n}\n\nfunction setValueT6Array( gl, v, textures ) {\n\n\tconst cache = this.cache;\n\n\tconst n = v.length;\n\n\tconst units = allocTexUnits( textures, n );\n\n\tif ( ! arraysEqual( cache, units ) ) {\n\n\t\tgl.uniform1iv( this.addr, units );\n\n\t\tcopyArray( cache, units );\n\n\t}\n\n\tfor ( let i = 0; i !== n; ++ i ) {\n\n\t\ttextures.setTextureCube( v[ i ] || emptyCubeTexture, units[ i ] );\n\n\t}\n\n}\n\nfunction setValueT2DArrayArray( gl, v, textures ) {\n\n\tconst cache = this.cache;\n\n\tconst n = v.length;\n\n\tconst units = allocTexUnits( textures, n );\n\n\tif ( ! arraysEqual( cache, units ) ) {\n\n\t\tgl.uniform1iv( this.addr, units );\n\n\t\tcopyArray( cache, units );\n\n\t}\n\n\tfor ( let i = 0; i !== n; ++ i ) {\n\n\t\ttextures.setTexture2DArray( v[ i ] || emptyArrayTexture, units[ i ] );\n\n\t}\n\n}\n\n\n// Helper to pick the right setter for a pure (bottom-level) array\n\nfunction getPureArraySetter( type ) {\n\n\tswitch ( type ) {\n\n\t\tcase 0x1406: return setValueV1fArray; // FLOAT\n\t\tcase 0x8b50: return setValueV2fArray; // _VEC2\n\t\tcase 0x8b51: return setValueV3fArray; // _VEC3\n\t\tcase 0x8b52: return setValueV4fArray; // _VEC4\n\n\t\tcase 0x8b5a: return setValueM2Array; // _MAT2\n\t\tcase 0x8b5b: return setValueM3Array; // _MAT3\n\t\tcase 0x8b5c: return setValueM4Array; // _MAT4\n\n\t\tcase 0x1404: case 0x8b56: return setValueV1iArray; // INT, BOOL\n\t\tcase 0x8b53: case 0x8b57: return setValueV2iArray; // _VEC2\n\t\tcase 0x8b54: case 0x8b58: return setValueV3iArray; // _VEC3\n\t\tcase 0x8b55: case 0x8b59: return setValueV4iArray; // _VEC4\n\n\t\tcase 0x1405: return setValueV1uiArray; // UINT\n\t\tcase 0x8dc6: return setValueV2uiArray; // _VEC2\n\t\tcase 0x8dc7: return setValueV3uiArray; // _VEC3\n\t\tcase 0x8dc8: return setValueV4uiArray; // _VEC4\n\n\t\tcase 0x8b5e: // SAMPLER_2D\n\t\tcase 0x8d66: // SAMPLER_EXTERNAL_OES\n\t\tcase 0x8dca: // INT_SAMPLER_2D\n\t\tcase 0x8dd2: // UNSIGNED_INT_SAMPLER_2D\n\t\tcase 0x8b62: // SAMPLER_2D_SHADOW\n\t\t\treturn setValueT1Array;\n\n\t\tcase 0x8b5f: // SAMPLER_3D\n\t\tcase 0x8dcb: // INT_SAMPLER_3D\n\t\tcase 0x8dd3: // UNSIGNED_INT_SAMPLER_3D\n\t\t\treturn setValueT3DArray;\n\n\t\tcase 0x8b60: // SAMPLER_CUBE\n\t\tcase 0x8dcc: // INT_SAMPLER_CUBE\n\t\tcase 0x8dd4: // UNSIGNED_INT_SAMPLER_CUBE\n\t\tcase 0x8dc5: // SAMPLER_CUBE_SHADOW\n\t\t\treturn setValueT6Array;\n\n\t\tcase 0x8dc1: // SAMPLER_2D_ARRAY\n\t\tcase 0x8dcf: // INT_SAMPLER_2D_ARRAY\n\t\tcase 0x8dd7: // UNSIGNED_INT_SAMPLER_2D_ARRAY\n\t\tcase 0x8dc4: // SAMPLER_2D_ARRAY_SHADOW\n\t\t\treturn setValueT2DArrayArray;\n\n\t}\n\n}\n\n// --- Uniform Classes ---\n\nclass SingleUniform {\n\n\tconstructor( id, activeInfo, addr ) {\n\n\t\tthis.id = id;\n\t\tthis.addr = addr;\n\t\tthis.cache = [];\n\t\tthis.type = activeInfo.type;\n\t\tthis.setValue = getSingularSetter( activeInfo.type );\n\n\t\t// this.path = activeInfo.name; // DEBUG\n\n\t}\n\n}\n\nclass PureArrayUniform {\n\n\tconstructor( id, activeInfo, addr ) {\n\n\t\tthis.id = id;\n\t\tthis.addr = addr;\n\t\tthis.cache = [];\n\t\tthis.type = activeInfo.type;\n\t\tthis.size = activeInfo.size;\n\t\tthis.setValue = getPureArraySetter( activeInfo.type );\n\n\t\t// this.path = activeInfo.name; // DEBUG\n\n\t}\n\n}\n\nclass StructuredUniform {\n\n\tconstructor( id ) {\n\n\t\tthis.id = id;\n\n\t\tthis.seq = [];\n\t\tthis.map = {};\n\n\t}\n\n\tsetValue( gl, value, textures ) {\n\n\t\tconst seq = this.seq;\n\n\t\tfor ( let i = 0, n = seq.length; i !== n; ++ i ) {\n\n\t\t\tconst u = seq[ i ];\n\t\t\tu.setValue( gl, value[ u.id ], textures );\n\n\t\t}\n\n\t}\n\n}\n\n// --- Top-level ---\n\n// Parser - builds up the property tree from the path strings\n\nconst RePathPart = /(\\w+)(\\])?(\\[|\\.)?/g;\n\n// extracts\n// \t- the identifier (member name or array index)\n//  - followed by an optional right bracket (found when array index)\n//  - followed by an optional left bracket or dot (type of subscript)\n//\n// Note: These portions can be read in a non-overlapping fashion and\n// allow straightforward parsing of the hierarchy that WebGL encodes\n// in the uniform names.\n\nfunction addUniform( container, uniformObject ) {\n\n\tcontainer.seq.push( uniformObject );\n\tcontainer.map[ uniformObject.id ] = uniformObject;\n\n}\n\nfunction parseUniform( activeInfo, addr, container ) {\n\n\tconst path = activeInfo.name,\n\t\tpathLength = path.length;\n\n\t// reset RegExp object, because of the early exit of a previous run\n\tRePathPart.lastIndex = 0;\n\n\twhile ( true ) {\n\n\t\tconst match = RePathPart.exec( path ),\n\t\t\tmatchEnd = RePathPart.lastIndex;\n\n\t\tlet id = match[ 1 ];\n\t\tconst idIsIndex = match[ 2 ] === ']',\n\t\t\tsubscript = match[ 3 ];\n\n\t\tif ( idIsIndex ) id = id | 0; // convert to integer\n\n\t\tif ( subscript === undefined || subscript === '[' && matchEnd + 2 === pathLength ) {\n\n\t\t\t// bare name or \"pure\" bottom-level array \"[0]\" suffix\n\n\t\t\taddUniform( container, subscript === undefined ?\n\t\t\t\tnew SingleUniform( id, activeInfo, addr ) :\n\t\t\t\tnew PureArrayUniform( id, activeInfo, addr ) );\n\n\t\t\tbreak;\n\n\t\t} else {\n\n\t\t\t// step into inner node / create it in case it doesn't exist\n\n\t\t\tconst map = container.map;\n\t\t\tlet next = map[ id ];\n\n\t\t\tif ( next === undefined ) {\n\n\t\t\t\tnext = new StructuredUniform( id );\n\t\t\t\taddUniform( container, next );\n\n\t\t\t}\n\n\t\t\tcontainer = next;\n\n\t\t}\n\n\t}\n\n}\n\n// Root Container\n\nclass WebGLUniforms {\n\n\tconstructor( gl, program ) {\n\n\t\tthis.seq = [];\n\t\tthis.map = {};\n\n\t\tconst n = gl.getProgramParameter( program, gl.ACTIVE_UNIFORMS );\n\n\t\tfor ( let i = 0; i < n; ++ i ) {\n\n\t\t\tconst info = gl.getActiveUniform( program, i ),\n\t\t\t\taddr = gl.getUniformLocation( program, info.name );\n\n\t\t\tparseUniform( info, addr, this );\n\n\t\t}\n\n\t\t// Sort uniforms to prioritize shadow samplers first (for optimal texture unit allocation)\n\n\t\tconst shadowSamplers = [];\n\t\tconst otherUniforms = [];\n\n\t\tfor ( const u of this.seq ) {\n\n\t\t\tif ( u.type === gl.SAMPLER_2D_SHADOW || u.type === gl.SAMPLER_CUBE_SHADOW || u.type === gl.SAMPLER_2D_ARRAY_SHADOW ) {\n\n\t\t\t\tshadowSamplers.push( u );\n\n\t\t\t} else {\n\n\t\t\t\totherUniforms.push( u );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( shadowSamplers.length > 0 ) {\n\n\t\t\tthis.seq = shadowSamplers.concat( otherUniforms );\n\n\t\t}\n\n\t}\n\n\tsetValue( gl, name, value, textures ) {\n\n\t\tconst u = this.map[ name ];\n\n\t\tif ( u !== undefined ) u.setValue( gl, value, textures );\n\n\t}\n\n\tsetOptional( gl, object, name ) {\n\n\t\tconst v = object[ name ];\n\n\t\tif ( v !== undefined ) this.setValue( gl, name, v );\n\n\t}\n\n\tstatic upload( gl, seq, values, textures ) {\n\n\t\tfor ( let i = 0, n = seq.length; i !== n; ++ i ) {\n\n\t\t\tconst u = seq[ i ],\n\t\t\t\tv = values[ u.id ];\n\n\t\t\tif ( v.needsUpdate !== false ) {\n\n\t\t\t\t// note: always updating when .needsUpdate is undefined\n\t\t\t\tu.setValue( gl, v.value, textures );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tstatic seqWithValue( seq, values ) {\n\n\t\tconst r = [];\n\n\t\tfor ( let i = 0, n = seq.length; i !== n; ++ i ) {\n\n\t\t\tconst u = seq[ i ];\n\t\t\tif ( u.id in values ) r.push( u );\n\n\t\t}\n\n\t\treturn r;\n\n\t}\n\n}\n\nfunction WebGLShader( gl, type, string ) {\n\n\tconst shader = gl.createShader( type );\n\n\tgl.shaderSource( shader, string );\n\tgl.compileShader( shader );\n\n\treturn shader;\n\n}\n\n// From https://www.khronos.org/registry/webgl/extensions/KHR_parallel_shader_compile/\nconst COMPLETION_STATUS_KHR = 0x91B1;\n\nlet programIdCount = 0;\n\nfunction handleSource( string, errorLine ) {\n\n\tconst lines = string.split( '\\n' );\n\tconst lines2 = [];\n\n\tconst from = Math.max( errorLine - 6, 0 );\n\tconst to = Math.min( errorLine + 6, lines.length );\n\n\tfor ( let i = from; i < to; i ++ ) {\n\n\t\tconst line = i + 1;\n\t\tlines2.push( `${line === errorLine ? '>' : ' '} ${line}: ${lines[ i ]}` );\n\n\t}\n\n\treturn lines2.join( '\\n' );\n\n}\n\nconst _m0 = /*@__PURE__*/ new Matrix3();\n\nfunction getEncodingComponents( colorSpace ) {\n\n\tColorManagement._getMatrix( _m0, ColorManagement.workingColorSpace, colorSpace );\n\n\tconst encodingMatrix = `mat3( ${ _m0.elements.map( ( v ) => v.toFixed( 4 ) ) } )`;\n\n\tswitch ( ColorManagement.getTransfer( colorSpace ) ) {\n\n\t\tcase LinearTransfer:\n\t\t\treturn [ encodingMatrix, 'LinearTransferOETF' ];\n\n\t\tcase SRGBTransfer:\n\t\t\treturn [ encodingMatrix, 'sRGBTransferOETF' ];\n\n\t\tdefault:\n\t\t\twarn( 'WebGLProgram: Unsupported color space: ', colorSpace );\n\t\t\treturn [ encodingMatrix, 'LinearTransferOETF' ];\n\n\t}\n\n}\n\nfunction getShaderErrors( gl, shader, type ) {\n\n\tconst status = gl.getShaderParameter( shader, gl.COMPILE_STATUS );\n\n\tconst shaderInfoLog = gl.getShaderInfoLog( shader ) || '';\n\tconst errors = shaderInfoLog.trim();\n\n\tif ( status && errors === '' ) return '';\n\n\tconst errorMatches = /ERROR: 0:(\\d+)/.exec( errors );\n\tif ( errorMatches ) {\n\n\t\t// --enable-privileged-webgl-extension\n\t\t// log( '**' + type + '**', gl.getExtension( 'WEBGL_debug_shaders' ).getTranslatedShaderSource( shader ) );\n\n\t\tconst errorLine = parseInt( errorMatches[ 1 ] );\n\t\treturn type.toUpperCase() + '\\n\\n' + errors + '\\n\\n' + handleSource( gl.getShaderSource( shader ), errorLine );\n\n\t} else {\n\n\t\treturn errors;\n\n\t}\n\n}\n\nfunction getTexelEncodingFunction( functionName, colorSpace ) {\n\n\tconst components = getEncodingComponents( colorSpace );\n\n\treturn [\n\n\t\t`vec4 ${functionName}( vec4 value ) {`,\n\n\t\t`\treturn ${components[ 1 ]}( vec4( value.rgb * ${components[ 0 ]}, value.a ) );`,\n\n\t\t'}',\n\n\t].join( '\\n' );\n\n}\n\nconst toneMappingFunctions = {\n\t[ LinearToneMapping ]: 'Linear',\n\t[ ReinhardToneMapping ]: 'Reinhard',\n\t[ CineonToneMapping ]: 'Cineon',\n\t[ ACESFilmicToneMapping ]: 'ACESFilmic',\n\t[ AgXToneMapping ]: 'AgX',\n\t[ NeutralToneMapping ]: 'Neutral',\n\t[ CustomToneMapping ]: 'Custom'\n};\n\nfunction getToneMappingFunction( functionName, toneMapping ) {\n\n\tconst toneMappingName = toneMappingFunctions[ toneMapping ];\n\n\tif ( toneMappingName === undefined ) {\n\n\t\twarn( 'WebGLProgram: Unsupported toneMapping:', toneMapping );\n\t\treturn 'vec3 ' + functionName + '( vec3 color ) { return LinearToneMapping( color ); }';\n\n\t}\n\n\treturn 'vec3 ' + functionName + '( vec3 color ) { return ' + toneMappingName + 'ToneMapping( color ); }';\n\n}\n\nconst _v0 = /*@__PURE__*/ new Vector3();\n\nfunction getLuminanceFunction() {\n\n\tColorManagement.getLuminanceCoefficients( _v0 );\n\n\tconst r = _v0.x.toFixed( 4 );\n\tconst g = _v0.y.toFixed( 4 );\n\tconst b = _v0.z.toFixed( 4 );\n\n\treturn [\n\n\t\t'float luminance( const in vec3 rgb ) {',\n\n\t\t`\tconst vec3 weights = vec3( ${ r }, ${ g }, ${ b } );`,\n\n\t\t'\treturn dot( weights, rgb );',\n\n\t\t'}'\n\n\t].join( '\\n' );\n\n}\n\nfunction generateVertexExtensions( parameters ) {\n\n\tconst chunks = [\n\t\tparameters.extensionClipCullDistance ? '#extension GL_ANGLE_clip_cull_distance : require' : '',\n\t\tparameters.extensionMultiDraw ? '#extension GL_ANGLE_multi_draw : require' : '',\n\t];\n\n\treturn chunks.filter( filterEmptyLine ).join( '\\n' );\n\n}\n\nfunction generateDefines( defines ) {\n\n\tconst chunks = [];\n\n\tfor ( const name in defines ) {\n\n\t\tconst value = defines[ name ];\n\n\t\tif ( value === false ) continue;\n\n\t\tchunks.push( '#define ' + name + ' ' + value );\n\n\t}\n\n\treturn chunks.join( '\\n' );\n\n}\n\nfunction fetchAttributeLocations( gl, program ) {\n\n\tconst attributes = {};\n\n\tconst n = gl.getProgramParameter( program, gl.ACTIVE_ATTRIBUTES );\n\n\tfor ( let i = 0; i < n; i ++ ) {\n\n\t\tconst info = gl.getActiveAttrib( program, i );\n\t\tconst name = info.name;\n\n\t\tlet locationSize = 1;\n\t\tif ( info.type === gl.FLOAT_MAT2 ) locationSize = 2;\n\t\tif ( info.type === gl.FLOAT_MAT3 ) locationSize = 3;\n\t\tif ( info.type === gl.FLOAT_MAT4 ) locationSize = 4;\n\n\t\t// log( 'WebGLProgram: ACTIVE VERTEX ATTRIBUTE:', name, i );\n\n\t\tattributes[ name ] = {\n\t\t\ttype: info.type,\n\t\t\tlocation: gl.getAttribLocation( program, name ),\n\t\t\tlocationSize: locationSize\n\t\t};\n\n\t}\n\n\treturn attributes;\n\n}\n\nfunction filterEmptyLine( string ) {\n\n\treturn string !== '';\n\n}\n\nfunction replaceLightNums( string, parameters ) {\n\n\tconst numSpotLightCoords = parameters.numSpotLightShadows + parameters.numSpotLightMaps - parameters.numSpotLightShadowsWithMaps;\n\n\treturn string\n\t\t.replace( /NUM_DIR_LIGHTS/g, parameters.numDirLights )\n\t\t.replace( /NUM_SPOT_LIGHTS/g, parameters.numSpotLights )\n\t\t.replace( /NUM_SPOT_LIGHT_MAPS/g, parameters.numSpotLightMaps )\n\t\t.replace( /NUM_SPOT_LIGHT_COORDS/g, numSpotLightCoords )\n\t\t.replace( /NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights )\n\t\t.replace( /NUM_POINT_LIGHTS/g, parameters.numPointLights )\n\t\t.replace( /NUM_HEMI_LIGHTS/g, parameters.numHemiLights )\n\t\t.replace( /NUM_DIR_LIGHT_SHADOWS/g, parameters.numDirLightShadows )\n\t\t.replace( /NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS/g, parameters.numSpotLightShadowsWithMaps )\n\t\t.replace( /NUM_SPOT_LIGHT_SHADOWS/g, parameters.numSpotLightShadows )\n\t\t.replace( /NUM_POINT_LIGHT_SHADOWS/g, parameters.numPointLightShadows );\n\n}\n\nfunction replaceClippingPlaneNums( string, parameters ) {\n\n\treturn string\n\t\t.replace( /NUM_CLIPPING_PLANES/g, parameters.numClippingPlanes )\n\t\t.replace( /UNION_CLIPPING_PLANES/g, ( parameters.numClippingPlanes - parameters.numClipIntersection ) );\n\n}\n\n// Resolve Includes\n\nconst includePattern = /^[ \\t]*#include +<([\\w\\d./]+)>/gm;\n\nfunction resolveIncludes( string ) {\n\n\treturn string.replace( includePattern, includeReplacer );\n\n}\n\nconst shaderChunkMap = new Map();\n\nfunction includeReplacer( match, include ) {\n\n\tlet string = ShaderChunk[ include ];\n\n\tif ( string === undefined ) {\n\n\t\tconst newInclude = shaderChunkMap.get( include );\n\n\t\tif ( newInclude !== undefined ) {\n\n\t\t\tstring = ShaderChunk[ newInclude ];\n\t\t\twarn( 'WebGLRenderer: Shader chunk \"%s\" has been deprecated. Use \"%s\" instead.', include, newInclude );\n\n\t\t} else {\n\n\t\t\tthrow new Error( 'Can not resolve #include <' + include + '>' );\n\n\t\t}\n\n\t}\n\n\treturn resolveIncludes( string );\n\n}\n\n// Unroll Loops\n\nconst unrollLoopPattern = /#pragma unroll_loop_start\\s+for\\s*\\(\\s*int\\s+i\\s*=\\s*(\\d+)\\s*;\\s*i\\s*<\\s*(\\d+)\\s*;\\s*i\\s*\\+\\+\\s*\\)\\s*{([\\s\\S]+?)}\\s+#pragma unroll_loop_end/g;\n\nfunction unrollLoops( string ) {\n\n\treturn string.replace( unrollLoopPattern, loopReplacer );\n\n}\n\nfunction loopReplacer( match, start, end, snippet ) {\n\n\tlet string = '';\n\n\tfor ( let i = parseInt( start ); i < parseInt( end ); i ++ ) {\n\n\t\tstring += snippet\n\t\t\t.replace( /\\[\\s*i\\s*\\]/g, '[ ' + i + ' ]' )\n\t\t\t.replace( /UNROLLED_LOOP_INDEX/g, i );\n\n\t}\n\n\treturn string;\n\n}\n\n//\n\nfunction generatePrecision( parameters ) {\n\n\tlet precisionstring = `precision ${parameters.precision} float;\n\tprecision ${parameters.precision} int;\n\tprecision ${parameters.precision} sampler2D;\n\tprecision ${parameters.precision} samplerCube;\n\tprecision ${parameters.precision} sampler3D;\n\tprecision ${parameters.precision} sampler2DArray;\n\tprecision ${parameters.precision} sampler2DShadow;\n\tprecision ${parameters.precision} samplerCubeShadow;\n\tprecision ${parameters.precision} sampler2DArrayShadow;\n\tprecision ${parameters.precision} isampler2D;\n\tprecision ${parameters.precision} isampler3D;\n\tprecision ${parameters.precision} isamplerCube;\n\tprecision ${parameters.precision} isampler2DArray;\n\tprecision ${parameters.precision} usampler2D;\n\tprecision ${parameters.precision} usampler3D;\n\tprecision ${parameters.precision} usamplerCube;\n\tprecision ${parameters.precision} usampler2DArray;\n\t`;\n\n\tif ( parameters.precision === 'highp' ) {\n\n\t\tprecisionstring += '\\n#define HIGH_PRECISION';\n\n\t} else if ( parameters.precision === 'mediump' ) {\n\n\t\tprecisionstring += '\\n#define MEDIUM_PRECISION';\n\n\t} else if ( parameters.precision === 'lowp' ) {\n\n\t\tprecisionstring += '\\n#define LOW_PRECISION';\n\n\t}\n\n\treturn precisionstring;\n\n}\n\nconst shadowMapTypeDefines = {\n\t[ PCFShadowMap ]: 'SHADOWMAP_TYPE_PCF',\n\t[ VSMShadowMap ]: 'SHADOWMAP_TYPE_VSM'\n};\n\nfunction generateShadowMapTypeDefine( parameters ) {\n\n\treturn shadowMapTypeDefines[ parameters.shadowMapType ] || 'SHADOWMAP_TYPE_BASIC';\n\n}\n\nconst envMapTypeDefines = {\n\t[ CubeReflectionMapping ]: 'ENVMAP_TYPE_CUBE',\n\t[ CubeRefractionMapping ]: 'ENVMAP_TYPE_CUBE',\n\t[ CubeUVReflectionMapping ]: 'ENVMAP_TYPE_CUBE_UV'\n};\n\nfunction generateEnvMapTypeDefine( parameters ) {\n\n\tif ( parameters.envMap === false ) return 'ENVMAP_TYPE_CUBE';\n\n\treturn envMapTypeDefines[ parameters.envMapMode ] || 'ENVMAP_TYPE_CUBE';\n\n}\n\nconst envMapModeDefines = {\n\t[ CubeRefractionMapping ]: 'ENVMAP_MODE_REFRACTION'\n};\n\nfunction generateEnvMapModeDefine( parameters ) {\n\n\tif ( parameters.envMap === false ) return 'ENVMAP_MODE_REFLECTION';\n\n\treturn envMapModeDefines[ parameters.envMapMode ] || 'ENVMAP_MODE_REFLECTION';\n\n}\n\nconst envMapBlendingDefines = {\n\t[ MultiplyOperation ]: 'ENVMAP_BLENDING_MULTIPLY',\n\t[ MixOperation ]: 'ENVMAP_BLENDING_MIX',\n\t[ AddOperation ]: 'ENVMAP_BLENDING_ADD'\n};\n\nfunction generateEnvMapBlendingDefine( parameters ) {\n\n\tif ( parameters.envMap === false ) return 'ENVMAP_BLENDING_NONE';\n\n\treturn envMapBlendingDefines[ parameters.combine ] || 'ENVMAP_BLENDING_NONE';\n\n}\n\nfunction generateCubeUVSize( parameters ) {\n\n\tconst imageHeight = parameters.envMapCubeUVHeight;\n\n\tif ( imageHeight === null ) return null;\n\n\tconst maxMip = Math.log2( imageHeight ) - 2;\n\n\tconst texelHeight = 1.0 / imageHeight;\n\n\tconst texelWidth = 1.0 / ( 3 * Math.max( Math.pow( 2, maxMip ), 7 * 16 ) );\n\n\treturn { texelWidth, texelHeight, maxMip };\n\n}\n\nfunction WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {\n\n\t// TODO Send this event to Three.js DevTools\n\t// log( 'WebGLProgram', cacheKey );\n\n\tconst gl = renderer.getContext();\n\n\tconst defines = parameters.defines;\n\n\tlet vertexShader = parameters.vertexShader;\n\tlet fragmentShader = parameters.fragmentShader;\n\n\tconst shadowMapTypeDefine = generateShadowMapTypeDefine( parameters );\n\tconst envMapTypeDefine = generateEnvMapTypeDefine( parameters );\n\tconst envMapModeDefine = generateEnvMapModeDefine( parameters );\n\tconst envMapBlendingDefine = generateEnvMapBlendingDefine( parameters );\n\tconst envMapCubeUVSize = generateCubeUVSize( parameters );\n\n\tconst customVertexExtensions = generateVertexExtensions( parameters );\n\n\tconst customDefines = generateDefines( defines );\n\n\tconst program = gl.createProgram();\n\n\tlet prefixVertex, prefixFragment;\n\tlet versionString = parameters.glslVersion ? '#version ' + parameters.glslVersion + '\\n' : '';\n\n\tif ( parameters.isRawShaderMaterial ) {\n\n\t\tprefixVertex = [\n\n\t\t\t'#define SHADER_TYPE ' + parameters.shaderType,\n\t\t\t'#define SHADER_NAME ' + parameters.shaderName,\n\n\t\t\tcustomDefines\n\n\t\t].filter( filterEmptyLine ).join( '\\n' );\n\n\t\tif ( prefixVertex.length > 0 ) {\n\n\t\t\tprefixVertex += '\\n';\n\n\t\t}\n\n\t\tprefixFragment = [\n\n\t\t\t'#define SHADER_TYPE ' + parameters.shaderType,\n\t\t\t'#define SHADER_NAME ' + parameters.shaderName,\n\n\t\t\tcustomDefines\n\n\t\t].filter( filterEmptyLine ).join( '\\n' );\n\n\t\tif ( prefixFragment.length > 0 ) {\n\n\t\t\tprefixFragment += '\\n';\n\n\t\t}\n\n\t} else {\n\n\t\tprefixVertex = [\n\n\t\t\tgeneratePrecision( parameters ),\n\n\t\t\t'#define SHADER_TYPE ' + parameters.shaderType,\n\t\t\t'#define SHADER_NAME ' + parameters.shaderName,\n\n\t\t\tcustomDefines,\n\n\t\t\tparameters.extensionClipCullDistance ? '#define USE_CLIP_DISTANCE' : '',\n\t\t\tparameters.batching ? '#define USE_BATCHING' : '',\n\t\t\tparameters.batchingColor ? '#define USE_BATCHING_COLOR' : '',\n\t\t\tparameters.instancing ? '#define USE_INSTANCING' : '',\n\t\t\tparameters.instancingColor ? '#define USE_INSTANCING_COLOR' : '',\n\t\t\tparameters.instancingMorph ? '#define USE_INSTANCING_MORPH' : '',\n\n\t\t\tparameters.useFog && parameters.fog ? '#define USE_FOG' : '',\n\t\t\tparameters.useFog && parameters.fogExp2 ? '#define FOG_EXP2' : '',\n\n\t\t\tparameters.map ? '#define USE_MAP' : '',\n\t\t\tparameters.envMap ? '#define USE_ENVMAP' : '',\n\t\t\tparameters.envMap ? '#define ' + envMapModeDefine : '',\n\t\t\tparameters.lightMap ? '#define USE_LIGHTMAP' : '',\n\t\t\tparameters.aoMap ? '#define USE_AOMAP' : '',\n\t\t\tparameters.bumpMap ? '#define USE_BUMPMAP' : '',\n\t\t\tparameters.normalMap ? '#define USE_NORMALMAP' : '',\n\t\t\tparameters.normalMapObjectSpace ? '#define USE_NORMALMAP_OBJECTSPACE' : '',\n\t\t\tparameters.normalMapTangentSpace ? '#define USE_NORMALMAP_TANGENTSPACE' : '',\n\t\t\tparameters.displacementMap ? '#define USE_DISPLACEMENTMAP' : '',\n\t\t\tparameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',\n\n\t\t\tparameters.anisotropy ? '#define USE_ANISOTROPY' : '',\n\t\t\tparameters.anisotropyMap ? '#define USE_ANISOTROPYMAP' : '',\n\n\t\t\tparameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',\n\t\t\tparameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',\n\t\t\tparameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',\n\n\t\t\tparameters.iridescenceMap ? '#define USE_IRIDESCENCEMAP' : '',\n\t\t\tparameters.iridescenceThicknessMap ? '#define USE_IRIDESCENCE_THICKNESSMAP' : '',\n\n\t\t\tparameters.specularMap ? '#define USE_SPECULARMAP' : '',\n\t\t\tparameters.specularColorMap ? '#define USE_SPECULAR_COLORMAP' : '',\n\t\t\tparameters.specularIntensityMap ? '#define USE_SPECULAR_INTENSITYMAP' : '',\n\n\t\t\tparameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',\n\t\t\tparameters.metalnessMap ? '#define USE_METALNESSMAP' : '',\n\t\t\tparameters.alphaMap ? '#define USE_ALPHAMAP' : '',\n\t\t\tparameters.alphaHash ? '#define USE_ALPHAHASH' : '',\n\n\t\t\tparameters.transmission ? '#define USE_TRANSMISSION' : '',\n\t\t\tparameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',\n\t\t\tparameters.thicknessMap ? '#define USE_THICKNESSMAP' : '',\n\n\t\t\tparameters.sheenColorMap ? '#define USE_SHEEN_COLORMAP' : '',\n\t\t\tparameters.sheenRoughnessMap ? '#define USE_SHEEN_ROUGHNESSMAP' : '',\n\n\t\t\t//\n\n\t\t\tparameters.mapUv ? '#define MAP_UV ' + parameters.mapUv : '',\n\t\t\tparameters.alphaMapUv ? '#define ALPHAMAP_UV ' + parameters.alphaMapUv : '',\n\t\t\tparameters.lightMapUv ? '#define LIGHTMAP_UV ' + parameters.lightMapUv : '',\n\t\t\tparameters.aoMapUv ? '#define AOMAP_UV ' + parameters.aoMapUv : '',\n\t\t\tparameters.emissiveMapUv ? '#define EMISSIVEMAP_UV ' + parameters.emissiveMapUv : '',\n\t\t\tparameters.bumpMapUv ? '#define BUMPMAP_UV ' + parameters.bumpMapUv : '',\n\t\t\tparameters.normalMapUv ? '#define NORMALMAP_UV ' + parameters.normalMapUv : '',\n\t\t\tparameters.displacementMapUv ? '#define DISPLACEMENTMAP_UV ' + parameters.displacementMapUv : '',\n\n\t\t\tparameters.metalnessMapUv ? '#define METALNESSMAP_UV ' + parameters.metalnessMapUv : '',\n\t\t\tparameters.roughnessMapUv ? '#define ROUGHNESSMAP_UV ' + parameters.roughnessMapUv : '',\n\n\t\t\tparameters.anisotropyMapUv ? '#define ANISOTROPYMAP_UV ' + parameters.anisotropyMapUv : '',\n\n\t\t\tparameters.clearcoatMapUv ? '#define CLEARCOATMAP_UV ' + parameters.clearcoatMapUv : '',\n\t\t\tparameters.clearcoatNormalMapUv ? '#define CLEARCOAT_NORMALMAP_UV ' + parameters.clearcoatNormalMapUv : '',\n\t\t\tparameters.clearcoatRoughnessMapUv ? '#define CLEARCOAT_ROUGHNESSMAP_UV ' + parameters.clearcoatRoughnessMapUv : '',\n\n\t\t\tparameters.iridescenceMapUv ? '#define IRIDESCENCEMAP_UV ' + parameters.iridescenceMapUv : '',\n\t\t\tparameters.iridescenceThicknessMapUv ? '#define IRIDESCENCE_THICKNESSMAP_UV ' + parameters.iridescenceThicknessMapUv : '',\n\n\t\t\tparameters.sheenColorMapUv ? '#define SHEEN_COLORMAP_UV ' + parameters.sheenColorMapUv : '',\n\t\t\tparameters.sheenRoughnessMapUv ? '#define SHEEN_ROUGHNESSMAP_UV ' + parameters.sheenRoughnessMapUv : '',\n\n\t\t\tparameters.specularMapUv ? '#define SPECULARMAP_UV ' + parameters.specularMapUv : '',\n\t\t\tparameters.specularColorMapUv ? '#define SPECULAR_COLORMAP_UV ' + parameters.specularColorMapUv : '',\n\t\t\tparameters.specularIntensityMapUv ? '#define SPECULAR_INTENSITYMAP_UV ' + parameters.specularIntensityMapUv : '',\n\n\t\t\tparameters.transmissionMapUv ? '#define TRANSMISSIONMAP_UV ' + parameters.transmissionMapUv : '',\n\t\t\tparameters.thicknessMapUv ? '#define THICKNESSMAP_UV ' + parameters.thicknessMapUv : '',\n\n\t\t\t//\n\n\t\t\tparameters.vertexTangents && parameters.flatShading === false ? '#define USE_TANGENT' : '',\n\t\t\tparameters.vertexColors ? '#define USE_COLOR' : '',\n\t\t\tparameters.vertexAlphas ? '#define USE_COLOR_ALPHA' : '',\n\t\t\tparameters.vertexUv1s ? '#define USE_UV1' : '',\n\t\t\tparameters.vertexUv2s ? '#define USE_UV2' : '',\n\t\t\tparameters.vertexUv3s ? '#define USE_UV3' : '',\n\n\t\t\tparameters.pointsUvs ? '#define USE_POINTS_UV' : '',\n\n\t\t\tparameters.flatShading ? '#define FLAT_SHADED' : '',\n\n\t\t\tparameters.skinning ? '#define USE_SKINNING' : '',\n\n\t\t\tparameters.morphTargets ? '#define USE_MORPHTARGETS' : '',\n\t\t\tparameters.morphNormals && parameters.flatShading === false ? '#define USE_MORPHNORMALS' : '',\n\t\t\t( parameters.morphColors ) ? '#define USE_MORPHCOLORS' : '',\n\t\t\t( parameters.morphTargetsCount > 0 ) ? '#define MORPHTARGETS_TEXTURE_STRIDE ' + parameters.morphTextureStride : '',\n\t\t\t( parameters.morphTargetsCount > 0 ) ? '#define MORPHTARGETS_COUNT ' + parameters.morphTargetsCount : '',\n\t\t\tparameters.doubleSided ? '#define DOUBLE_SIDED' : '',\n\t\t\tparameters.flipSided ? '#define FLIP_SIDED' : '',\n\n\t\t\tparameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',\n\t\t\tparameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',\n\n\t\t\tparameters.sizeAttenuation ? '#define USE_SIZEATTENUATION' : '',\n\n\t\t\tparameters.numLightProbes > 0 ? '#define USE_LIGHT_PROBES' : '',\n\n\t\t\tparameters.logarithmicDepthBuffer ? '#define USE_LOGARITHMIC_DEPTH_BUFFER' : '',\n\t\t\tparameters.reversedDepthBuffer ? '#define USE_REVERSED_DEPTH_BUFFER' : '',\n\n\t\t\t'uniform mat4 modelMatrix;',\n\t\t\t'uniform mat4 modelViewMatrix;',\n\t\t\t'uniform mat4 projectionMatrix;',\n\t\t\t'uniform mat4 viewMatrix;',\n\t\t\t'uniform mat3 normalMatrix;',\n\t\t\t'uniform vec3 cameraPosition;',\n\t\t\t'uniform bool isOrthographic;',\n\n\t\t\t'#ifdef USE_INSTANCING',\n\n\t\t\t'\tattribute mat4 instanceMatrix;',\n\n\t\t\t'#endif',\n\n\t\t\t'#ifdef USE_INSTANCING_COLOR',\n\n\t\t\t'\tattribute vec3 instanceColor;',\n\n\t\t\t'#endif',\n\n\t\t\t'#ifdef USE_INSTANCING_MORPH',\n\n\t\t\t'\tuniform sampler2D morphTexture;',\n\n\t\t\t'#endif',\n\n\t\t\t'attribute vec3 position;',\n\t\t\t'attribute vec3 normal;',\n\t\t\t'attribute vec2 uv;',\n\n\t\t\t'#ifdef USE_UV1',\n\n\t\t\t'\tattribute vec2 uv1;',\n\n\t\t\t'#endif',\n\n\t\t\t'#ifdef USE_UV2',\n\n\t\t\t'\tattribute vec2 uv2;',\n\n\t\t\t'#endif',\n\n\t\t\t'#ifdef USE_UV3',\n\n\t\t\t'\tattribute vec2 uv3;',\n\n\t\t\t'#endif',\n\n\t\t\t'#ifdef USE_TANGENT',\n\n\t\t\t'\tattribute vec4 tangent;',\n\n\t\t\t'#endif',\n\n\t\t\t'#if defined( USE_COLOR_ALPHA )',\n\n\t\t\t'\tattribute vec4 color;',\n\n\t\t\t'#elif defined( USE_COLOR )',\n\n\t\t\t'\tattribute vec3 color;',\n\n\t\t\t'#endif',\n\n\t\t\t'#ifdef USE_SKINNING',\n\n\t\t\t'\tattribute vec4 skinIndex;',\n\t\t\t'\tattribute vec4 skinWeight;',\n\n\t\t\t'#endif',\n\n\t\t\t'\\n'\n\n\t\t].filter( filterEmptyLine ).join( '\\n' );\n\n\t\tprefixFragment = [\n\n\t\t\tgeneratePrecision( parameters ),\n\n\t\t\t'#define SHADER_TYPE ' + parameters.shaderType,\n\t\t\t'#define SHADER_NAME ' + parameters.shaderName,\n\n\t\t\tcustomDefines,\n\n\t\t\tparameters.useFog && parameters.fog ? '#define USE_FOG' : '',\n\t\t\tparameters.useFog && parameters.fogExp2 ? '#define FOG_EXP2' : '',\n\n\t\t\tparameters.alphaToCoverage ? '#define ALPHA_TO_COVERAGE' : '',\n\t\t\tparameters.map ? '#define USE_MAP' : '',\n\t\t\tparameters.matcap ? '#define USE_MATCAP' : '',\n\t\t\tparameters.envMap ? '#define USE_ENVMAP' : '',\n\t\t\tparameters.envMap ? '#define ' + envMapTypeDefine : '',\n\t\t\tparameters.envMap ? '#define ' + envMapModeDefine : '',\n\t\t\tparameters.envMap ? '#define ' + envMapBlendingDefine : '',\n\t\t\tenvMapCubeUVSize ? '#define CUBEUV_TEXEL_WIDTH ' + envMapCubeUVSize.texelWidth : '',\n\t\t\tenvMapCubeUVSize ? '#define CUBEUV_TEXEL_HEIGHT ' + envMapCubeUVSize.texelHeight : '',\n\t\t\tenvMapCubeUVSize ? '#define CUBEUV_MAX_MIP ' + envMapCubeUVSize.maxMip + '.0' : '',\n\t\t\tparameters.lightMap ? '#define USE_LIGHTMAP' : '',\n\t\t\tparameters.aoMap ? '#define USE_AOMAP' : '',\n\t\t\tparameters.bumpMap ? '#define USE_BUMPMAP' : '',\n\t\t\tparameters.normalMap ? '#define USE_NORMALMAP' : '',\n\t\t\tparameters.normalMapObjectSpace ? '#define USE_NORMALMAP_OBJECTSPACE' : '',\n\t\t\tparameters.normalMapTangentSpace ? '#define USE_NORMALMAP_TANGENTSPACE' : '',\n\t\t\tparameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',\n\n\t\t\tparameters.anisotropy ? '#define USE_ANISOTROPY' : '',\n\t\t\tparameters.anisotropyMap ? '#define USE_ANISOTROPYMAP' : '',\n\n\t\t\tparameters.clearcoat ? '#define USE_CLEARCOAT' : '',\n\t\t\tparameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',\n\t\t\tparameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',\n\t\t\tparameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',\n\n\t\t\tparameters.dispersion ? '#define USE_DISPERSION' : '',\n\n\t\t\tparameters.iridescence ? '#define USE_IRIDESCENCE' : '',\n\t\t\tparameters.iridescenceMap ? '#define USE_IRIDESCENCEMAP' : '',\n\t\t\tparameters.iridescenceThicknessMap ? '#define USE_IRIDESCENCE_THICKNESSMAP' : '',\n\n\t\t\tparameters.specularMap ? '#define USE_SPECULARMAP' : '',\n\t\t\tparameters.specularColorMap ? '#define USE_SPECULAR_COLORMAP' : '',\n\t\t\tparameters.specularIntensityMap ? '#define USE_SPECULAR_INTENSITYMAP' : '',\n\n\t\t\tparameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',\n\t\t\tparameters.metalnessMap ? '#define USE_METALNESSMAP' : '',\n\n\t\t\tparameters.alphaMap ? '#define USE_ALPHAMAP' : '',\n\t\t\tparameters.alphaTest ? '#define USE_ALPHATEST' : '',\n\t\t\tparameters.alphaHash ? '#define USE_ALPHAHASH' : '',\n\n\t\t\tparameters.sheen ? '#define USE_SHEEN' : '',\n\t\t\tparameters.sheenColorMap ? '#define USE_SHEEN_COLORMAP' : '',\n\t\t\tparameters.sheenRoughnessMap ? '#define USE_SHEEN_ROUGHNESSMAP' : '',\n\n\t\t\tparameters.transmission ? '#define USE_TRANSMISSION' : '',\n\t\t\tparameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',\n\t\t\tparameters.thicknessMap ? '#define USE_THICKNESSMAP' : '',\n\n\t\t\tparameters.vertexTangents && parameters.flatShading === false ? '#define USE_TANGENT' : '',\n\t\t\tparameters.vertexColors || parameters.instancingColor || parameters.batchingColor ? '#define USE_COLOR' : '',\n\t\t\tparameters.vertexAlphas ? '#define USE_COLOR_ALPHA' : '',\n\t\t\tparameters.vertexUv1s ? '#define USE_UV1' : '',\n\t\t\tparameters.vertexUv2s ? '#define USE_UV2' : '',\n\t\t\tparameters.vertexUv3s ? '#define USE_UV3' : '',\n\n\t\t\tparameters.pointsUvs ? '#define USE_POINTS_UV' : '',\n\n\t\t\tparameters.gradientMap ? '#define USE_GRADIENTMAP' : '',\n\n\t\t\tparameters.flatShading ? '#define FLAT_SHADED' : '',\n\n\t\t\tparameters.doubleSided ? '#define DOUBLE_SIDED' : '',\n\t\t\tparameters.flipSided ? '#define FLIP_SIDED' : '',\n\n\t\t\tparameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',\n\t\t\tparameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',\n\n\t\t\tparameters.premultipliedAlpha ? '#define PREMULTIPLIED_ALPHA' : '',\n\n\t\t\tparameters.numLightProbes > 0 ? '#define USE_LIGHT_PROBES' : '',\n\n\t\t\tparameters.decodeVideoTexture ? '#define DECODE_VIDEO_TEXTURE' : '',\n\t\t\tparameters.decodeVideoTextureEmissive ? '#define DECODE_VIDEO_TEXTURE_EMISSIVE' : '',\n\n\t\t\tparameters.logarithmicDepthBuffer ? '#define USE_LOGARITHMIC_DEPTH_BUFFER' : '',\n\t\t\tparameters.reversedDepthBuffer ? '#define USE_REVERSED_DEPTH_BUFFER' : '',\n\n\t\t\t'uniform mat4 viewMatrix;',\n\t\t\t'uniform vec3 cameraPosition;',\n\t\t\t'uniform bool isOrthographic;',\n\n\t\t\t( parameters.toneMapping !== NoToneMapping ) ? '#define TONE_MAPPING' : '',\n\t\t\t( parameters.toneMapping !== NoToneMapping ) ? ShaderChunk[ 'tonemapping_pars_fragment' ] : '', // this code is required here because it is used by the toneMapping() function defined below\n\t\t\t( parameters.toneMapping !== NoToneMapping ) ? getToneMappingFunction( 'toneMapping', parameters.toneMapping ) : '',\n\n\t\t\tparameters.dithering ? '#define DITHERING' : '',\n\t\t\tparameters.opaque ? '#define OPAQUE' : '',\n\n\t\t\tShaderChunk[ 'colorspace_pars_fragment' ], // this code is required here because it is used by the various encoding/decoding function defined below\n\t\t\tgetTexelEncodingFunction( 'linearToOutputTexel', parameters.outputColorSpace ),\n\t\t\tgetLuminanceFunction(),\n\n\t\t\tparameters.useDepthPacking ? '#define DEPTH_PACKING ' + parameters.depthPacking : '',\n\n\t\t\t'\\n'\n\n\t\t].filter( filterEmptyLine ).join( '\\n' );\n\n\t}\n\n\tvertexShader = resolveIncludes( vertexShader );\n\tvertexShader = replaceLightNums( vertexShader, parameters );\n\tvertexShader = replaceClippingPlaneNums( vertexShader, parameters );\n\n\tfragmentShader = resolveIncludes( fragmentShader );\n\tfragmentShader = replaceLightNums( fragmentShader, parameters );\n\tfragmentShader = replaceClippingPlaneNums( fragmentShader, parameters );\n\n\tvertexShader = unrollLoops( vertexShader );\n\tfragmentShader = unrollLoops( fragmentShader );\n\n\tif ( parameters.isRawShaderMaterial !== true ) {\n\n\t\t// GLSL 3.0 conversion for built-in materials and ShaderMaterial\n\n\t\tversionString = '#version 300 es\\n';\n\n\t\tprefixVertex = [\n\t\t\tcustomVertexExtensions,\n\t\t\t'#define attribute in',\n\t\t\t'#define varying out',\n\t\t\t'#define texture2D texture'\n\t\t].join( '\\n' ) + '\\n' + prefixVertex;\n\n\t\tprefixFragment = [\n\t\t\t'#define varying in',\n\t\t\t( parameters.glslVersion === GLSL3 ) ? '' : 'layout(location = 0) out highp vec4 pc_fragColor;',\n\t\t\t( parameters.glslVersion === GLSL3 ) ? '' : '#define gl_FragColor pc_fragColor',\n\t\t\t'#define gl_FragDepthEXT gl_FragDepth',\n\t\t\t'#define texture2D texture',\n\t\t\t'#define textureCube texture',\n\t\t\t'#define texture2DProj textureProj',\n\t\t\t'#define texture2DLodEXT textureLod',\n\t\t\t'#define texture2DProjLodEXT textureProjLod',\n\t\t\t'#define textureCubeLodEXT textureLod',\n\t\t\t'#define texture2DGradEXT textureGrad',\n\t\t\t'#define texture2DProjGradEXT textureProjGrad',\n\t\t\t'#define textureCubeGradEXT textureGrad'\n\t\t].join( '\\n' ) + '\\n' + prefixFragment;\n\n\t}\n\n\tconst vertexGlsl = versionString + prefixVertex + vertexShader;\n\tconst fragmentGlsl = versionString + prefixFragment + fragmentShader;\n\n\t// log( '*VERTEX*', vertexGlsl );\n\t// log( '*FRAGMENT*', fragmentGlsl );\n\n\tconst glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );\n\tconst glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );\n\n\tgl.attachShader( program, glVertexShader );\n\tgl.attachShader( program, glFragmentShader );\n\n\t// Force a particular attribute to index 0.\n\n\tif ( parameters.index0AttributeName !== undefined ) {\n\n\t\tgl.bindAttribLocation( program, 0, parameters.index0AttributeName );\n\n\t} else if ( parameters.morphTargets === true ) {\n\n\t\t// programs with morphTargets displace position out of attribute 0\n\t\tgl.bindAttribLocation( program, 0, 'position' );\n\n\t}\n\n\tgl.linkProgram( program );\n\n\tfunction onFirstUse( self ) {\n\n\t\t// check for link errors\n\t\tif ( renderer.debug.checkShaderErrors ) {\n\n\t\t\tconst programInfoLog = gl.getProgramInfoLog( program ) || '';\n\t\t\tconst vertexShaderInfoLog = gl.getShaderInfoLog( glVertexShader ) || '';\n\t\t\tconst fragmentShaderInfoLog = gl.getShaderInfoLog( glFragmentShader ) || '';\n\n\t\t\tconst programLog = programInfoLog.trim();\n\t\t\tconst vertexLog = vertexShaderInfoLog.trim();\n\t\t\tconst fragmentLog = fragmentShaderInfoLog.trim();\n\n\t\t\tlet runnable = true;\n\t\t\tlet haveDiagnostics = true;\n\n\t\t\tif ( gl.getProgramParameter( program, gl.LINK_STATUS ) === false ) {\n\n\t\t\t\trunnable = false;\n\n\t\t\t\tif ( typeof renderer.debug.onShaderError === 'function' ) {\n\n\t\t\t\t\trenderer.debug.onShaderError( gl, program, glVertexShader, glFragmentShader );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// default error reporting\n\n\t\t\t\t\tconst vertexErrors = getShaderErrors( gl, glVertexShader, 'vertex' );\n\t\t\t\t\tconst fragmentErrors = getShaderErrors( gl, glFragmentShader, 'fragment' );\n\n\t\t\t\t\terror(\n\t\t\t\t\t\t'THREE.WebGLProgram: Shader Error ' + gl.getError() + ' - ' +\n\t\t\t\t\t\t'VALIDATE_STATUS ' + gl.getProgramParameter( program, gl.VALIDATE_STATUS ) + '\\n\\n' +\n\t\t\t\t\t\t'Material Name: ' + self.name + '\\n' +\n\t\t\t\t\t\t'Material Type: ' + self.type + '\\n\\n' +\n\t\t\t\t\t\t'Program Info Log: ' + programLog + '\\n' +\n\t\t\t\t\t\tvertexErrors + '\\n' +\n\t\t\t\t\t\tfragmentErrors\n\t\t\t\t\t);\n\n\t\t\t\t}\n\n\t\t\t} else if ( programLog !== '' ) {\n\n\t\t\t\twarn( 'WebGLProgram: Program Info Log:', programLog );\n\n\t\t\t} else if ( vertexLog === '' || fragmentLog === '' ) {\n\n\t\t\t\thaveDiagnostics = false;\n\n\t\t\t}\n\n\t\t\tif ( haveDiagnostics ) {\n\n\t\t\t\tself.diagnostics = {\n\n\t\t\t\t\trunnable: runnable,\n\n\t\t\t\t\tprogramLog: programLog,\n\n\t\t\t\t\tvertexShader: {\n\n\t\t\t\t\t\tlog: vertexLog,\n\t\t\t\t\t\tprefix: prefixVertex\n\n\t\t\t\t\t},\n\n\t\t\t\t\tfragmentShader: {\n\n\t\t\t\t\t\tlog: fragmentLog,\n\t\t\t\t\t\tprefix: prefixFragment\n\n\t\t\t\t\t}\n\n\t\t\t\t};\n\n\t\t\t}\n\n\t\t}\n\n\t\t// Clean up\n\n\t\t// Crashes in iOS9 and iOS10. #18402\n\t\t// gl.detachShader( program, glVertexShader );\n\t\t// gl.detachShader( program, glFragmentShader );\n\n\t\tgl.deleteShader( glVertexShader );\n\t\tgl.deleteShader( glFragmentShader );\n\n\t\tcachedUniforms = new WebGLUniforms( gl, program );\n\t\tcachedAttributes = fetchAttributeLocations( gl, program );\n\n\t}\n\n\t// set up caching for uniform locations\n\n\tlet cachedUniforms;\n\n\tthis.getUniforms = function () {\n\n\t\tif ( cachedUniforms === undefined ) {\n\n\t\t\t// Populates cachedUniforms and cachedAttributes\n\t\t\tonFirstUse( this );\n\n\t\t}\n\n\t\treturn cachedUniforms;\n\n\t};\n\n\t// set up caching for attribute locations\n\n\tlet cachedAttributes;\n\n\tthis.getAttributes = function () {\n\n\t\tif ( cachedAttributes === undefined ) {\n\n\t\t\t// Populates cachedAttributes and cachedUniforms\n\t\t\tonFirstUse( this );\n\n\t\t}\n\n\t\treturn cachedAttributes;\n\n\t};\n\n\t// indicate when the program is ready to be used. if the KHR_parallel_shader_compile extension isn't supported,\n\t// flag the program as ready immediately. It may cause a stall when it's first used.\n\n\tlet programReady = ( parameters.rendererExtensionParallelShaderCompile === false );\n\n\tthis.isReady = function () {\n\n\t\tif ( programReady === false ) {\n\n\t\t\tprogramReady = gl.getProgramParameter( program, COMPLETION_STATUS_KHR );\n\n\t\t}\n\n\t\treturn programReady;\n\n\t};\n\n\t// free resource\n\n\tthis.destroy = function () {\n\n\t\tbindingStates.releaseStatesOfProgram( this );\n\n\t\tgl.deleteProgram( program );\n\t\tthis.program = undefined;\n\n\t};\n\n\t//\n\n\tthis.type = parameters.shaderType;\n\tthis.name = parameters.shaderName;\n\tthis.id = programIdCount ++;\n\tthis.cacheKey = cacheKey;\n\tthis.usedTimes = 1;\n\tthis.program = program;\n\tthis.vertexShader = glVertexShader;\n\tthis.fragmentShader = glFragmentShader;\n\n\treturn this;\n\n}\n\nlet _id = 0;\n\nclass WebGLShaderCache {\n\n\tconstructor() {\n\n\t\tthis.shaderCache = new Map();\n\t\tthis.materialCache = new Map();\n\n\t}\n\n\tupdate( material ) {\n\n\t\tconst vertexShader = material.vertexShader;\n\t\tconst fragmentShader = material.fragmentShader;\n\n\t\tconst vertexShaderStage = this._getShaderStage( vertexShader );\n\t\tconst fragmentShaderStage = this._getShaderStage( fragmentShader );\n\n\t\tconst materialShaders = this._getShaderCacheForMaterial( material );\n\n\t\tif ( materialShaders.has( vertexShaderStage ) === false ) {\n\n\t\t\tmaterialShaders.add( vertexShaderStage );\n\t\t\tvertexShaderStage.usedTimes ++;\n\n\t\t}\n\n\t\tif ( materialShaders.has( fragmentShaderStage ) === false ) {\n\n\t\t\tmaterialShaders.add( fragmentShaderStage );\n\t\t\tfragmentShaderStage.usedTimes ++;\n\n\t\t}\n\n\t\treturn this;\n\n\t}\n\n\tremove( material ) {\n\n\t\tconst materialShaders = this.materialCache.get( material );\n\n\t\tfor ( const shaderStage of materialShaders ) {\n\n\t\t\tshaderStage.usedTimes --;\n\n\t\t\tif ( shaderStage.usedTimes === 0 ) this.shaderCache.delete( shaderStage.code );\n\n\t\t}\n\n\t\tthis.materialCache.delete( material );\n\n\t\treturn this;\n\n\t}\n\n\tgetVertexShaderID( material ) {\n\n\t\treturn this._getShaderStage( material.vertexShader ).id;\n\n\t}\n\n\tgetFragmentShaderID( material ) {\n\n\t\treturn this._getShaderStage( material.fragmentShader ).id;\n\n\t}\n\n\tdispose() {\n\n\t\tthis.shaderCache.clear();\n\t\tthis.materialCache.clear();\n\n\t}\n\n\t_getShaderCacheForMaterial( material ) {\n\n\t\tconst cache = this.materialCache;\n\t\tlet set = cache.get( material );\n\n\t\tif ( set === undefined ) {\n\n\t\t\tset = new Set();\n\t\t\tcache.set( material, set );\n\n\t\t}\n\n\t\treturn set;\n\n\t}\n\n\t_getShaderStage( code ) {\n\n\t\tconst cache = this.shaderCache;\n\t\tlet stage = cache.get( code );\n\n\t\tif ( stage === undefined ) {\n\n\t\t\tstage = new WebGLShaderStage( code );\n\t\t\tcache.set( code, stage );\n\n\t\t}\n\n\t\treturn stage;\n\n\t}\n\n}\n\nclass WebGLShaderStage {\n\n\tconstructor( code ) {\n\n\t\tthis.id = _id ++;\n\n\t\tthis.code = code;\n\t\tthis.usedTimes = 0;\n\n\t}\n\n}\n\nfunction WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities, bindingStates, clipping ) {\n\n\tconst _programLayers = new Layers();\n\tconst _customShaders = new WebGLShaderCache();\n\tconst _activeChannels = new Set();\n\tconst programs = [];\n\tconst programsMap = new Map();\n\n\tconst logarithmicDepthBuffer = capabilities.logarithmicDepthBuffer;\n\n\tlet precision = capabilities.precision;\n\n\tconst shaderIDs = {\n\t\tMeshDepthMaterial: 'depth',\n\t\tMeshDistanceMaterial: 'distance',\n\t\tMeshNormalMaterial: 'normal',\n\t\tMeshBasicMaterial: 'basic',\n\t\tMeshLambertMaterial: 'lambert',\n\t\tMeshPhongMaterial: 'phong',\n\t\tMeshToonMaterial: 'toon',\n\t\tMeshStandardMaterial: 'physical',\n\t\tMeshPhysicalMaterial: 'physical',\n\t\tMeshMatcapMaterial: 'matcap',\n\t\tLineBasicMaterial: 'basic',\n\t\tLineDashedMaterial: 'dashed',\n\t\tPointsMaterial: 'points',\n\t\tShadowMaterial: 'shadow',\n\t\tSpriteMaterial: 'sprite'\n\t};\n\n\tfunction getChannel( value ) {\n\n\t\t_activeChannels.add( value );\n\n\t\tif ( value === 0 ) return 'uv';\n\n\t\treturn `uv${ value }`;\n\n\t}\n\n\tfunction getParameters( material, lights, shadows, scene, object ) {\n\n\t\tconst fog = scene.fog;\n\t\tconst geometry = object.geometry;\n\t\tconst environment = material.isMeshStandardMaterial ? scene.environment : null;\n\n\t\tconst envMap = ( material.isMeshStandardMaterial ? cubeuvmaps : cubemaps ).get( material.envMap || environment );\n\t\tconst envMapCubeUVHeight = ( !! envMap ) && ( envMap.mapping === CubeUVReflectionMapping ) ? envMap.image.height : null;\n\n\t\tconst shaderID = shaderIDs[ material.type ];\n\n\t\t// heuristics to create shader parameters according to lights in the scene\n\t\t// (not to blow over maxLights budget)\n\n\t\tif ( material.precision !== null ) {\n\n\t\t\tprecision = capabilities.getMaxPrecision( material.precision );\n\n\t\t\tif ( precision !== material.precision ) {\n\n\t\t\t\twarn( 'WebGLProgram.getParameters:', material.precision, 'not supported, using', precision, 'instead.' );\n\n\t\t\t}\n\n\t\t}\n\n\t\t//\n\n\t\tconst morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;\n\t\tconst morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;\n\n\t\tlet morphTextureStride = 0;\n\n\t\tif ( geometry.morphAttributes.position !== undefined ) morphTextureStride = 1;\n\t\tif ( geometry.morphAttributes.normal !== undefined ) morphTextureStride = 2;\n\t\tif ( geometry.morphAttributes.color !== undefined ) morphTextureStride = 3;\n\n\t\t//\n\n\t\tlet vertexShader, fragmentShader;\n\t\tlet customVertexShaderID, customFragmentShaderID;\n\n\t\tif ( shaderID ) {\n\n\t\t\tconst shader = ShaderLib[ shaderID ];\n\n\t\t\tvertexShader = shader.vertexShader;\n\t\t\tfragmentShader = shader.fragmentShader;\n\n\t\t} else {\n\n\t\t\tvertexShader = material.vertexShader;\n\t\t\tfragmentShader = material.fragmentShader;\n\n\t\t\t_customShaders.update( material );\n\n\t\t\tcustomVertexShaderID = _customShaders.getVertexShaderID( material );\n\t\t\tcustomFragmentShaderID = _customShaders.getFragmentShaderID( material );\n\n\t\t}\n\n\t\tconst currentRenderTarget = renderer.getRenderTarget();\n\t\tconst reversedDepthBuffer = renderer.state.buffers.depth.getReversed();\n\n\t\tconst IS_INSTANCEDMESH = object.isInstancedMesh === true;\n\t\tconst IS_BATCHEDMESH = object.isBatchedMesh === true;\n\n\t\tconst HAS_MAP = !! material.map;\n\t\tconst HAS_MATCAP = !! material.matcap;\n\t\tconst HAS_ENVMAP = !! envMap;\n\t\tconst HAS_AOMAP = !! material.aoMap;\n\t\tconst HAS_LIGHTMAP = !! material.lightMap;\n\t\tconst HAS_BUMPMAP = !! material.bumpMap;\n\t\tconst HAS_NORMALMAP = !! material.normalMap;\n\t\tconst HAS_DISPLACEMENTMAP = !! material.displacementMap;\n\t\tconst HAS_EMISSIVEMAP = !! material.emissiveMap;\n\n\t\tconst HAS_METALNESSMAP = !! material.metalnessMap;\n\t\tconst HAS_ROUGHNESSMAP = !! material.roughnessMap;\n\n\t\tconst HAS_ANISOTROPY = material.anisotropy > 0;\n\t\tconst HAS_CLEARCOAT = material.clearcoat > 0;\n\t\tconst HAS_DISPERSION = material.dispersion > 0;\n\t\tconst HAS_IRIDESCENCE = material.iridescence > 0;\n\t\tconst HAS_SHEEN = material.sheen > 0;\n\t\tconst HAS_TRANSMISSION = material.transmission > 0;\n\n\t\tconst HAS_ANISOTROPYMAP = HAS_ANISOTROPY && !! material.anisotropyMap;\n\n\t\tconst HAS_CLEARCOATMAP = HAS_CLEARCOAT && !! material.clearcoatMap;\n\t\tconst HAS_CLEARCOAT_NORMALMAP = HAS_CLEARCOAT && !! material.clearcoatNormalMap;\n\t\tconst HAS_CLEARCOAT_ROUGHNESSMAP = HAS_CLEARCOAT && !! material.clearcoatRoughnessMap;\n\n\t\tconst HAS_IRIDESCENCEMAP = HAS_IRIDESCENCE && !! material.iridescenceMap;\n\t\tconst HAS_IRIDESCENCE_THICKNESSMAP = HAS_IRIDESCENCE && !! material.iridescenceThicknessMap;\n\n\t\tconst HAS_SHEEN_COLORMAP = HAS_SHEEN && !! material.sheenColorMap;\n\t\tconst HAS_SHEEN_ROUGHNESSMAP = HAS_SHEEN && !! material.sheenRoughnessMap;\n\n\t\tconst HAS_SPECULARMAP = !! material.specularMap;\n\t\tconst HAS_SPECULAR_COLORMAP = !! material.specularColorMap;\n\t\tconst HAS_SPECULAR_INTENSITYMAP = !! material.specularIntensityMap;\n\n\t\tconst HAS_TRANSMISSIONMAP = HAS_TRANSMISSION && !! material.transmissionMap;\n\t\tconst HAS_THICKNESSMAP = HAS_TRANSMISSION && !! material.thicknessMap;\n\n\t\tconst HAS_GRADIENTMAP = !! material.gradientMap;\n\n\t\tconst HAS_ALPHAMAP = !! material.alphaMap;\n\n\t\tconst HAS_ALPHATEST = material.alphaTest > 0;\n\n\t\tconst HAS_ALPHAHASH = !! material.alphaHash;\n\n\t\tconst HAS_EXTENSIONS = !! material.extensions;\n\n\t\tlet toneMapping = NoToneMapping;\n\n\t\tif ( material.toneMapped ) {\n\n\t\t\tif ( currentRenderTarget === null || currentRenderTarget.isXRRenderTarget === true ) {\n\n\t\t\t\ttoneMapping = renderer.toneMapping;\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst parameters = {\n\n\t\t\tshaderID: shaderID,\n\t\t\tshaderType: material.type,\n\t\t\tshaderName: material.name,\n\n\t\t\tvertexShader: vertexShader,\n\t\t\tfragmentShader: fragmentShader,\n\t\t\tdefines: material.defines,\n\n\t\t\tcustomVertexShaderID: customVertexShaderID,\n\t\t\tcustomFragmentShaderID: customFragmentShaderID,\n\n\t\t\tisRawShaderMaterial: material.isRawShaderMaterial === true,\n\t\t\tglslVersion: material.glslVersion,\n\n\t\t\tprecision: precision,\n\n\t\t\tbatching: IS_BATCHEDMESH,\n\t\t\tbatchingColor: IS_BATCHEDMESH && object._colorsTexture !== null,\n\t\t\tinstancing: IS_INSTANCEDMESH,\n\t\t\tinstancingColor: IS_INSTANCEDMESH && object.instanceColor !== null,\n\t\t\tinstancingMorph: IS_INSTANCEDMESH && object.morphTexture !== null,\n\n\t\t\toutputColorSpace: ( currentRenderTarget === null ) ? renderer.outputColorSpace : ( currentRenderTarget.isXRRenderTarget === true ? currentRenderTarget.texture.colorSpace : LinearSRGBColorSpace ),\n\t\t\talphaToCoverage: !! material.alphaToCoverage,\n\n\t\t\tmap: HAS_MAP,\n\t\t\tmatcap: HAS_MATCAP,\n\t\t\tenvMap: HAS_ENVMAP,\n\t\t\tenvMapMode: HAS_ENVMAP && envMap.mapping,\n\t\t\tenvMapCubeUVHeight: envMapCubeUVHeight,\n\t\t\taoMap: HAS_AOMAP,\n\t\t\tlightMap: HAS_LIGHTMAP,\n\t\t\tbumpMap: HAS_BUMPMAP,\n\t\t\tnormalMap: HAS_NORMALMAP,\n\t\t\tdisplacementMap: HAS_DISPLACEMENTMAP,\n\t\t\temissiveMap: HAS_EMISSIVEMAP,\n\n\t\t\tnormalMapObjectSpace: HAS_NORMALMAP && material.normalMapType === ObjectSpaceNormalMap,\n\t\t\tnormalMapTangentSpace: HAS_NORMALMAP && material.normalMapType === TangentSpaceNormalMap,\n\n\t\t\tmetalnessMap: HAS_METALNESSMAP,\n\t\t\troughnessMap: HAS_ROUGHNESSMAP,\n\n\t\t\tanisotropy: HAS_ANISOTROPY,\n\t\t\tanisotropyMap: HAS_ANISOTROPYMAP,\n\n\t\t\tclearcoat: HAS_CLEARCOAT,\n\t\t\tclearcoatMap: HAS_CLEARCOATMAP,\n\t\t\tclearcoatNormalMap: HAS_CLEARCOAT_NORMALMAP,\n\t\t\tclearcoatRoughnessMap: HAS_CLEARCOAT_ROUGHNESSMAP,\n\n\t\t\tdispersion: HAS_DISPERSION,\n\n\t\t\tiridescence: HAS_IRIDESCENCE,\n\t\t\tiridescenceMap: HAS_IRIDESCENCEMAP,\n\t\t\tiridescenceThicknessMap: HAS_IRIDESCENCE_THICKNESSMAP,\n\n\t\t\tsheen: HAS_SHEEN,\n\t\t\tsheenColorMap: HAS_SHEEN_COLORMAP,\n\t\t\tsheenRoughnessMap: HAS_SHEEN_ROUGHNESSMAP,\n\n\t\t\tspecularMap: HAS_SPECULARMAP,\n\t\t\tspecularColorMap: HAS_SPECULAR_COLORMAP,\n\t\t\tspecularIntensityMap: HAS_SPECULAR_INTENSITYMAP,\n\n\t\t\ttransmission: HAS_TRANSMISSION,\n\t\t\ttransmissionMap: HAS_TRANSMISSIONMAP,\n\t\t\tthicknessMap: HAS_THICKNESSMAP,\n\n\t\t\tgradientMap: HAS_GRADIENTMAP,\n\n\t\t\topaque: material.transparent === false && material.blending === NormalBlending && material.alphaToCoverage === false,\n\n\t\t\talphaMap: HAS_ALPHAMAP,\n\t\t\talphaTest: HAS_ALPHATEST,\n\t\t\talphaHash: HAS_ALPHAHASH,\n\n\t\t\tcombine: material.combine,\n\n\t\t\t//\n\n\t\t\tmapUv: HAS_MAP && getChannel( material.map.channel ),\n\t\t\taoMapUv: HAS_AOMAP && getChannel( material.aoMap.channel ),\n\t\t\tlightMapUv: HAS_LIGHTMAP && getChannel( material.lightMap.channel ),\n\t\t\tbumpMapUv: HAS_BUMPMAP && getChannel( material.bumpMap.channel ),\n\t\t\tnormalMapUv: HAS_NORMALMAP && getChannel( material.normalMap.channel ),\n\t\t\tdisplacementMapUv: HAS_DISPLACEMENTMAP && getChannel( material.displacementMap.channel ),\n\t\t\temissiveMapUv: HAS_EMISSIVEMAP && getChannel( material.emissiveMap.channel ),\n\n\t\t\tmetalnessMapUv: HAS_METALNESSMAP && getChannel( material.metalnessMap.channel ),\n\t\t\troughnessMapUv: HAS_ROUGHNESSMAP && getChannel( material.roughnessMap.channel ),\n\n\t\t\tanisotropyMapUv: HAS_ANISOTROPYMAP && getChannel( material.anisotropyMap.channel ),\n\n\t\t\tclearcoatMapUv: HAS_CLEARCOATMAP && getChannel( material.clearcoatMap.channel ),\n\t\t\tclearcoatNormalMapUv: HAS_CLEARCOAT_NORMALMAP && getChannel( material.clearcoatNormalMap.channel ),\n\t\t\tclearcoatRoughnessMapUv: HAS_CLEARCOAT_ROUGHNESSMAP && getChannel( material.clearcoatRoughnessMap.channel ),\n\n\t\t\tiridescenceMapUv: HAS_IRIDESCENCEMAP && getChannel( material.iridescenceMap.channel ),\n\t\t\tiridescenceThicknessMapUv: HAS_IRIDESCENCE_THICKNESSMAP && getChannel( material.iridescenceThicknessMap.channel ),\n\n\t\t\tsheenColorMapUv: HAS_SHEEN_COLORMAP && getChannel( material.sheenColorMap.channel ),\n\t\t\tsheenRoughnessMapUv: HAS_SHEEN_ROUGHNESSMAP && getChannel( material.sheenRoughnessMap.channel ),\n\n\t\t\tspecularMapUv: HAS_SPECULARMAP && getChannel( material.specularMap.channel ),\n\t\t\tspecularColorMapUv: HAS_SPECULAR_COLORMAP && getChannel( material.specularColorMap.channel ),\n\t\t\tspecularIntensityMapUv: HAS_SPECULAR_INTENSITYMAP && getChannel( material.specularIntensityMap.channel ),\n\n\t\t\ttransmissionMapUv: HAS_TRANSMISSIONMAP && getChannel( material.transmissionMap.channel ),\n\t\t\tthicknessMapUv: HAS_THICKNESSMAP && getChannel( material.thicknessMap.channel ),\n\n\t\t\talphaMapUv: HAS_ALPHAMAP && getChannel( material.alphaMap.channel ),\n\n\t\t\t//\n\n\t\t\tvertexTangents: !! geometry.attributes.tangent && ( HAS_NORMALMAP || HAS_ANISOTROPY ),\n\t\t\tvertexColors: material.vertexColors,\n\t\t\tvertexAlphas: material.vertexColors === true && !! geometry.attributes.color && geometry.attributes.color.itemSize === 4,\n\n\t\t\tpointsUvs: object.isPoints === true && !! geometry.attributes.uv && ( HAS_MAP || HAS_ALPHAMAP ),\n\n\t\t\tfog: !! fog,\n\t\t\tuseFog: material.fog === true,\n\t\t\tfogExp2: ( !! fog && fog.isFogExp2 ),\n\n\t\t\tflatShading: ( material.flatShading === true && material.wireframe === false ),\n\n\t\t\tsizeAttenuation: material.sizeAttenuation === true,\n\t\t\tlogarithmicDepthBuffer: logarithmicDepthBuffer,\n\t\t\treversedDepthBuffer: reversedDepthBuffer,\n\n\t\t\tskinning: object.isSkinnedMesh === true,\n\n\t\t\tmorphTargets: geometry.morphAttributes.position !== undefined,\n\t\t\tmorphNormals: geometry.morphAttributes.normal !== undefined,\n\t\t\tmorphColors: geometry.morphAttributes.color !== undefined,\n\t\t\tmorphTargetsCount: morphTargetsCount,\n\t\t\tmorphTextureStride: morphTextureStride,\n\n\t\t\tnumDirLights: lights.directional.length,\n\t\t\tnumPointLights: lights.point.length,\n\t\t\tnumSpotLights: lights.spot.length,\n\t\t\tnumSpotLightMaps: lights.spotLightMap.length,\n\t\t\tnumRectAreaLights: lights.rectArea.length,\n\t\t\tnumHemiLights: lights.hemi.length,\n\n\t\t\tnumDirLightShadows: lights.directionalShadowMap.length,\n\t\t\tnumPointLightShadows: lights.pointShadowMap.length,\n\t\t\tnumSpotLightShadows: lights.spotShadowMap.length,\n\t\t\tnumSpotLightShadowsWithMaps: lights.numSpotLightShadowsWithMaps,\n\n\t\t\tnumLightProbes: lights.numLightProbes,\n\n\t\t\tnumClippingPlanes: clipping.numPlanes,\n\t\t\tnumClipIntersection: clipping.numIntersection,\n\n\t\t\tdithering: material.dithering,\n\n\t\t\tshadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,\n\t\t\tshadowMapType: renderer.shadowMap.type,\n\n\t\t\ttoneMapping: toneMapping,\n\n\t\t\tdecodeVideoTexture: HAS_MAP && ( material.map.isVideoTexture === true ) && ( ColorManagement.getTransfer( material.map.colorSpace ) === SRGBTransfer ),\n\t\t\tdecodeVideoTextureEmissive: HAS_EMISSIVEMAP && ( material.emissiveMap.isVideoTexture === true ) && ( ColorManagement.getTransfer( material.emissiveMap.colorSpace ) === SRGBTransfer ),\n\n\t\t\tpremultipliedAlpha: material.premultipliedAlpha,\n\n\t\t\tdoubleSided: material.side === DoubleSide,\n\t\t\tflipSided: material.side === BackSide,\n\n\t\t\tuseDepthPacking: material.depthPacking >= 0,\n\t\t\tdepthPacking: material.depthPacking || 0,\n\n\t\t\tindex0AttributeName: material.index0AttributeName,\n\n\t\t\textensionClipCullDistance: HAS_EXTENSIONS && material.extensions.clipCullDistance === true && extensions.has( 'WEBGL_clip_cull_distance' ),\n\t\t\textensionMultiDraw: ( HAS_EXTENSIONS && material.extensions.multiDraw === true || IS_BATCHEDMESH ) && extensions.has( 'WEBGL_multi_draw' ),\n\n\t\t\trendererExtensionParallelShaderCompile: extensions.has( 'KHR_parallel_shader_compile' ),\n\n\t\t\tcustomProgramCacheKey: material.customProgramCacheKey()\n\n\t\t};\n\n\t\t// the usage of getChannel() determines the active texture channels for this shader\n\n\t\tparameters.vertexUv1s = _activeChannels.has( 1 );\n\t\tparameters.vertexUv2s = _activeChannels.has( 2 );\n\t\tparameters.vertexUv3s = _activeChannels.has( 3 );\n\n\t\t_activeChannels.clear();\n\n\t\treturn parameters;\n\n\t}\n\n\tfunction getProgramCacheKey( parameters ) {\n\n\t\tconst array = [];\n\n\t\tif ( parameters.shaderID ) {\n\n\t\t\tarray.push( parameters.shaderID );\n\n\t\t} else {\n\n\t\t\tarray.push( parameters.customVertexShaderID );\n\t\t\tarray.push( parameters.customFragmentShaderID );\n\n\t\t}\n\n\t\tif ( parameters.defines !== undefined ) {\n\n\t\t\tfor ( const name in parameters.defines ) {\n\n\t\t\t\tarray.push( name );\n\t\t\t\tarray.push( parameters.defines[ name ] );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( parameters.isRawShaderMaterial === false ) {\n\n\t\t\tgetProgramCacheKeyParameters( array, parameters );\n\t\t\tgetProgramCacheKeyBooleans( array, parameters );\n\t\t\tarray.push( renderer.outputColorSpace );\n\n\t\t}\n\n\t\tarray.push( parameters.customProgramCacheKey );\n\n\t\treturn array.join();\n\n\t}\n\n\tfunction getProgramCacheKeyParameters( array, parameters ) {\n\n\t\tarray.push( parameters.precision );\n\t\tarray.push( parameters.outputColorSpace );\n\t\tarray.push( parameters.envMapMode );\n\t\tarray.push( parameters.envMapCubeUVHeight );\n\t\tarray.push( parameters.mapUv );\n\t\tarray.push( parameters.alphaMapUv );\n\t\tarray.push( parameters.lightMapUv );\n\t\tarray.push( parameters.aoMapUv );\n\t\tarray.push( parameters.bumpMapUv );\n\t\tarray.push( parameters.normalMapUv );\n\t\tarray.push( parameters.displacementMapUv );\n\t\tarray.push( parameters.emissiveMapUv );\n\t\tarray.push( parameters.metalnessMapUv );\n\t\tarray.push( parameters.roughnessMapUv );\n\t\tarray.push( parameters.anisotropyMapUv );\n\t\tarray.push( parameters.clearcoatMapUv );\n\t\tarray.push( parameters.clearcoatNormalMapUv );\n\t\tarray.push( parameters.clearcoatRoughnessMapUv );\n\t\tarray.push( parameters.iridescenceMapUv );\n\t\tarray.push( parameters.iridescenceThicknessMapUv );\n\t\tarray.push( parameters.sheenColorMapUv );\n\t\tarray.push( parameters.sheenRoughnessMapUv );\n\t\tarray.push( parameters.specularMapUv );\n\t\tarray.push( parameters.specularColorMapUv );\n\t\tarray.push( parameters.specularIntensityMapUv );\n\t\tarray.push( parameters.transmissionMapUv );\n\t\tarray.push( parameters.thicknessMapUv );\n\t\tarray.push( parameters.combine );\n\t\tarray.push( parameters.fogExp2 );\n\t\tarray.push( parameters.sizeAttenuation );\n\t\tarray.push( parameters.morphTargetsCount );\n\t\tarray.push( parameters.morphAttributeCount );\n\t\tarray.push( parameters.numDirLights );\n\t\tarray.push( parameters.numPointLights );\n\t\tarray.push( parameters.numSpotLights );\n\t\tarray.push( parameters.numSpotLightMaps );\n\t\tarray.push( parameters.numHemiLights );\n\t\tarray.push( parameters.numRectAreaLights );\n\t\tarray.push( parameters.numDirLightShadows );\n\t\tarray.push( parameters.numPointLightShadows );\n\t\tarray.push( parameters.numSpotLightShadows );\n\t\tarray.push( parameters.numSpotLightShadowsWithMaps );\n\t\tarray.push( parameters.numLightProbes );\n\t\tarray.push( parameters.shadowMapType );\n\t\tarray.push( parameters.toneMapping );\n\t\tarray.push( parameters.numClippingPlanes );\n\t\tarray.push( parameters.numClipIntersection );\n\t\tarray.push( parameters.depthPacking );\n\n\t}\n\n\tfunction getProgramCacheKeyBooleans( array, parameters ) {\n\n\t\t_programLayers.disableAll();\n\n\t\tif ( parameters.instancing )\n\t\t\t_programLayers.enable( 0 );\n\t\tif ( parameters.instancingColor )\n\t\t\t_programLayers.enable( 1 );\n\t\tif ( parameters.instancingMorph )\n\t\t\t_programLayers.enable( 2 );\n\t\tif ( parameters.matcap )\n\t\t\t_programLayers.enable( 3 );\n\t\tif ( parameters.envMap )\n\t\t\t_programLayers.enable( 4 );\n\t\tif ( parameters.normalMapObjectSpace )\n\t\t\t_programLayers.enable( 5 );\n\t\tif ( parameters.normalMapTangentSpace )\n\t\t\t_programLayers.enable( 6 );\n\t\tif ( parameters.clearcoat )\n\t\t\t_programLayers.enable( 7 );\n\t\tif ( parameters.iridescence )\n\t\t\t_programLayers.enable( 8 );\n\t\tif ( parameters.alphaTest )\n\t\t\t_programLayers.enable( 9 );\n\t\tif ( parameters.vertexColors )\n\t\t\t_programLayers.enable( 10 );\n\t\tif ( parameters.vertexAlphas )\n\t\t\t_programLayers.enable( 11 );\n\t\tif ( parameters.vertexUv1s )\n\t\t\t_programLayers.enable( 12 );\n\t\tif ( parameters.vertexUv2s )\n\t\t\t_programLayers.enable( 13 );\n\t\tif ( parameters.vertexUv3s )\n\t\t\t_programLayers.enable( 14 );\n\t\tif ( parameters.vertexTangents )\n\t\t\t_programLayers.enable( 15 );\n\t\tif ( parameters.anisotropy )\n\t\t\t_programLayers.enable( 16 );\n\t\tif ( parameters.alphaHash )\n\t\t\t_programLayers.enable( 17 );\n\t\tif ( parameters.batching )\n\t\t\t_programLayers.enable( 18 );\n\t\tif ( parameters.dispersion )\n\t\t\t_programLayers.enable( 19 );\n\t\tif ( parameters.batchingColor )\n\t\t\t_programLayers.enable( 20 );\n\t\tif ( parameters.gradientMap )\n\t\t\t_programLayers.enable( 21 );\n\n\t\tarray.push( _programLayers.mask );\n\t\t_programLayers.disableAll();\n\n\t\tif ( parameters.fog )\n\t\t\t_programLayers.enable( 0 );\n\t\tif ( parameters.useFog )\n\t\t\t_programLayers.enable( 1 );\n\t\tif ( parameters.flatShading )\n\t\t\t_programLayers.enable( 2 );\n\t\tif ( parameters.logarithmicDepthBuffer )\n\t\t\t_programLayers.enable( 3 );\n\t\tif ( parameters.reversedDepthBuffer )\n\t\t\t_programLayers.enable( 4 );\n\t\tif ( parameters.skinning )\n\t\t\t_programLayers.enable( 5 );\n\t\tif ( parameters.morphTargets )\n\t\t\t_programLayers.enable( 6 );\n\t\tif ( parameters.morphNormals )\n\t\t\t_programLayers.enable( 7 );\n\t\tif ( parameters.morphColors )\n\t\t\t_programLayers.enable( 8 );\n\t\tif ( parameters.premultipliedAlpha )\n\t\t\t_programLayers.enable( 9 );\n\t\tif ( parameters.shadowMapEnabled )\n\t\t\t_programLayers.enable( 10 );\n\t\tif ( parameters.doubleSided )\n\t\t\t_programLayers.enable( 11 );\n\t\tif ( parameters.flipSided )\n\t\t\t_programLayers.enable( 12 );\n\t\tif ( parameters.useDepthPacking )\n\t\t\t_programLayers.enable( 13 );\n\t\tif ( parameters.dithering )\n\t\t\t_programLayers.enable( 14 );\n\t\tif ( parameters.transmission )\n\t\t\t_programLayers.enable( 15 );\n\t\tif ( parameters.sheen )\n\t\t\t_programLayers.enable( 16 );\n\t\tif ( parameters.opaque )\n\t\t\t_programLayers.enable( 17 );\n\t\tif ( parameters.pointsUvs )\n\t\t\t_programLayers.enable( 18 );\n\t\tif ( parameters.decodeVideoTexture )\n\t\t\t_programLayers.enable( 19 );\n\t\tif ( parameters.decodeVideoTextureEmissive )\n\t\t\t_programLayers.enable( 20 );\n\t\tif ( parameters.alphaToCoverage )\n\t\t\t_programLayers.enable( 21 );\n\n\t\tarray.push( _programLayers.mask );\n\n\t}\n\n\tfunction getUniforms( material ) {\n\n\t\tconst shaderID = shaderIDs[ material.type ];\n\t\tlet uniforms;\n\n\t\tif ( shaderID ) {\n\n\t\t\tconst shader = ShaderLib[ shaderID ];\n\t\t\tuniforms = UniformsUtils.clone( shader.uniforms );\n\n\t\t} else {\n\n\t\t\tuniforms = material.uniforms;\n\n\t\t}\n\n\t\treturn uniforms;\n\n\t}\n\n\tfunction acquireProgram( parameters, cacheKey ) {\n\n\t\tlet program = programsMap.get( cacheKey );\n\n\t\tif ( program !== undefined ) {\n\n\t\t\t++ program.usedTimes;\n\n\t\t} else {\n\n\t\t\tprogram = new WebGLProgram( renderer, cacheKey, parameters, bindingStates );\n\t\t\tprograms.push( program );\n\n\t\t\tprogramsMap.set( cacheKey, program );\n\n\t\t}\n\n\t\treturn program;\n\n\t}\n\n\tfunction releaseProgram( program ) {\n\n\t\tif ( -- program.usedTimes === 0 ) {\n\n\t\t\t// Remove from unordered set\n\t\t\tconst i = programs.indexOf( program );\n\t\t\tprograms[ i ] = programs[ programs.length - 1 ];\n\t\t\tprograms.pop();\n\n\t\t\t// Remove from map\n\t\t\tprogramsMap.delete( program.cacheKey );\n\n\t\t\t// Free WebGL resources\n\t\t\tprogram.destroy();\n\n\t\t}\n\n\t}\n\n\tfunction releaseShaderCache( material ) {\n\n\t\t_customShaders.remove( material );\n\n\t}\n\n\tfunction dispose() {\n\n\t\t_customShaders.dispose();\n\n\t}\n\n\treturn {\n\t\tgetParameters: getParameters,\n\t\tgetProgramCacheKey: getProgramCacheKey,\n\t\tgetUniforms: getUniforms,\n\t\tacquireProgram: acquireProgram,\n\t\treleaseProgram: releaseProgram,\n\t\treleaseShaderCache: releaseShaderCache,\n\t\t// Exposed for resource monitoring & error feedback via renderer.info:\n\t\tprograms: programs,\n\t\tdispose: dispose\n\t};\n\n}\n\nfunction WebGLProperties() {\n\n\tlet properties = new WeakMap();\n\n\tfunction has( object ) {\n\n\t\treturn properties.has( object );\n\n\t}\n\n\tfunction get( object ) {\n\n\t\tlet map = properties.get( object );\n\n\t\tif ( map === undefined ) {\n\n\t\t\tmap = {};\n\t\t\tproperties.set( object, map );\n\n\t\t}\n\n\t\treturn map;\n\n\t}\n\n\tfunction remove( object ) {\n\n\t\tproperties.delete( object );\n\n\t}\n\n\tfunction update( object, key, value ) {\n\n\t\tproperties.get( object )[ key ] = value;\n\n\t}\n\n\tfunction dispose() {\n\n\t\tproperties = new WeakMap();\n\n\t}\n\n\treturn {\n\t\thas: has,\n\t\tget: get,\n\t\tremove: remove,\n\t\tupdate: update,\n\t\tdispose: dispose\n\t};\n\n}\n\nfunction painterSortStable( a, b ) {\n\n\tif ( a.groupOrder !== b.groupOrder ) {\n\n\t\treturn a.groupOrder - b.groupOrder;\n\n\t} else if ( a.renderOrder !== b.renderOrder ) {\n\n\t\treturn a.renderOrder - b.renderOrder;\n\n\t} else if ( a.material.id !== b.material.id ) {\n\n\t\treturn a.material.id - b.material.id;\n\n\t} else if ( a.z !== b.z ) {\n\n\t\treturn a.z - b.z;\n\n\t} else {\n\n\t\treturn a.id - b.id;\n\n\t}\n\n}\n\nfunction reversePainterSortStable( a, b ) {\n\n\tif ( a.groupOrder !== b.groupOrder ) {\n\n\t\treturn a.groupOrder - b.groupOrder;\n\n\t} else if ( a.renderOrder !== b.renderOrder ) {\n\n\t\treturn a.renderOrder - b.renderOrder;\n\n\t} else if ( a.z !== b.z ) {\n\n\t\treturn b.z - a.z;\n\n\t} else {\n\n\t\treturn a.id - b.id;\n\n\t}\n\n}\n\n\nfunction WebGLRenderList() {\n\n\tconst renderItems = [];\n\tlet renderItemsIndex = 0;\n\n\tconst opaque = [];\n\tconst transmissive = [];\n\tconst transparent = [];\n\n\tfunction init() {\n\n\t\trenderItemsIndex = 0;\n\n\t\topaque.length = 0;\n\t\ttransmissive.length = 0;\n\t\ttransparent.length = 0;\n\n\t}\n\n\tfunction getNextRenderItem( object, geometry, material, groupOrder, z, group ) {\n\n\t\tlet renderItem = renderItems[ renderItemsIndex ];\n\n\t\tif ( renderItem === undefined ) {\n\n\t\t\trenderItem = {\n\t\t\t\tid: object.id,\n\t\t\t\tobject: object,\n\t\t\t\tgeometry: geometry,\n\t\t\t\tmaterial: material,\n\t\t\t\tgroupOrder: groupOrder,\n\t\t\t\trenderOrder: object.renderOrder,\n\t\t\t\tz: z,\n\t\t\t\tgroup: group\n\t\t\t};\n\n\t\t\trenderItems[ renderItemsIndex ] = renderItem;\n\n\t\t} else {\n\n\t\t\trenderItem.id = object.id;\n\t\t\trenderItem.object = object;\n\t\t\trenderItem.geometry = geometry;\n\t\t\trenderItem.material = material;\n\t\t\trenderItem.groupOrder = groupOrder;\n\t\t\trenderItem.renderOrder = object.renderOrder;\n\t\t\trenderItem.z = z;\n\t\t\trenderItem.group = group;\n\n\t\t}\n\n\t\trenderItemsIndex ++;\n\n\t\treturn renderItem;\n\n\t}\n\n\tfunction push( object, geometry, material, groupOrder, z, group ) {\n\n\t\tconst renderItem = getNextRenderItem( object, geometry, material, groupOrder, z, group );\n\n\t\tif ( material.transmission > 0.0 ) {\n\n\t\t\ttransmissive.push( renderItem );\n\n\t\t} else if ( material.transparent === true ) {\n\n\t\t\ttransparent.push( renderItem );\n\n\t\t} else {\n\n\t\t\topaque.push( renderItem );\n\n\t\t}\n\n\t}\n\n\tfunction unshift( object, geometry, material, groupOrder, z, group ) {\n\n\t\tconst renderItem = getNextRenderItem( object, geometry, material, groupOrder, z, group );\n\n\t\tif ( material.transmission > 0.0 ) {\n\n\t\t\ttransmissive.unshift( renderItem );\n\n\t\t} else if ( material.transparent === true ) {\n\n\t\t\ttransparent.unshift( renderItem );\n\n\t\t} else {\n\n\t\t\topaque.unshift( renderItem );\n\n\t\t}\n\n\t}\n\n\tfunction sort( customOpaqueSort, customTransparentSort ) {\n\n\t\tif ( opaque.length > 1 ) opaque.sort( customOpaqueSort || painterSortStable );\n\t\tif ( transmissive.length > 1 ) transmissive.sort( customTransparentSort || reversePainterSortStable );\n\t\tif ( transparent.length > 1 ) transparent.sort( customTransparentSort || reversePainterSortStable );\n\n\t}\n\n\tfunction finish() {\n\n\t\t// Clear references from inactive renderItems in the list\n\n\t\tfor ( let i = renderItemsIndex, il = renderItems.length; i < il; i ++ ) {\n\n\t\t\tconst renderItem = renderItems[ i ];\n\n\t\t\tif ( renderItem.id === null ) break;\n\n\t\t\trenderItem.id = null;\n\t\t\trenderItem.object = null;\n\t\t\trenderItem.geometry = null;\n\t\t\trenderItem.material = null;\n\t\t\trenderItem.group = null;\n\n\t\t}\n\n\t}\n\n\treturn {\n\n\t\topaque: opaque,\n\t\ttransmissive: transmissive,\n\t\ttransparent: transparent,\n\n\t\tinit: init,\n\t\tpush: push,\n\t\tunshift: unshift,\n\t\tfinish: finish,\n\n\t\tsort: sort\n\t};\n\n}\n\nfunction WebGLRenderLists() {\n\n\tlet lists = new WeakMap();\n\n\tfunction get( scene, renderCallDepth ) {\n\n\t\tconst listArray = lists.get( scene );\n\t\tlet list;\n\n\t\tif ( listArray === undefined ) {\n\n\t\t\tlist = new WebGLRenderList();\n\t\t\tlists.set( scene, [ list ] );\n\n\t\t} else {\n\n\t\t\tif ( renderCallDepth >= listArray.length ) {\n\n\t\t\t\tlist = new WebGLRenderList();\n\t\t\t\tlistArray.push( list );\n\n\t\t\t} else {\n\n\t\t\t\tlist = listArray[ renderCallDepth ];\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn list;\n\n\t}\n\n\tfunction dispose() {\n\n\t\tlists = new WeakMap();\n\n\t}\n\n\treturn {\n\t\tget: get,\n\t\tdispose: dispose\n\t};\n\n}\n\nfunction UniformsCache() {\n\n\tconst lights = {};\n\n\treturn {\n\n\t\tget: function ( light ) {\n\n\t\t\tif ( lights[ light.id ] !== undefined ) {\n\n\t\t\t\treturn lights[ light.id ];\n\n\t\t\t}\n\n\t\t\tlet uniforms;\n\n\t\t\tswitch ( light.type ) {\n\n\t\t\t\tcase 'DirectionalLight':\n\t\t\t\t\tuniforms = {\n\t\t\t\t\t\tdirection: new Vector3(),\n\t\t\t\t\t\tcolor: new Color()\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'SpotLight':\n\t\t\t\t\tuniforms = {\n\t\t\t\t\t\tposition: new Vector3(),\n\t\t\t\t\t\tdirection: new Vector3(),\n\t\t\t\t\t\tcolor: new Color(),\n\t\t\t\t\t\tdistance: 0,\n\t\t\t\t\t\tconeCos: 0,\n\t\t\t\t\t\tpenumbraCos: 0,\n\t\t\t\t\t\tdecay: 0\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'PointLight':\n\t\t\t\t\tuniforms = {\n\t\t\t\t\t\tposition: new Vector3(),\n\t\t\t\t\t\tcolor: new Color(),\n\t\t\t\t\t\tdistance: 0,\n\t\t\t\t\t\tdecay: 0\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'HemisphereLight':\n\t\t\t\t\tuniforms = {\n\t\t\t\t\t\tdirection: new Vector3(),\n\t\t\t\t\t\tskyColor: new Color(),\n\t\t\t\t\t\tgroundColor: new Color()\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'RectAreaLight':\n\t\t\t\t\tuniforms = {\n\t\t\t\t\t\tcolor: new Color(),\n\t\t\t\t\t\tposition: new Vector3(),\n\t\t\t\t\t\thalfWidth: new Vector3(),\n\t\t\t\t\t\thalfHeight: new Vector3()\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t\tlights[ light.id ] = uniforms;\n\n\t\t\treturn uniforms;\n\n\t\t}\n\n\t};\n\n}\n\nfunction ShadowUniformsCache() {\n\n\tconst lights = {};\n\n\treturn {\n\n\t\tget: function ( light ) {\n\n\t\t\tif ( lights[ light.id ] !== undefined ) {\n\n\t\t\t\treturn lights[ light.id ];\n\n\t\t\t}\n\n\t\t\tlet uniforms;\n\n\t\t\tswitch ( light.type ) {\n\n\t\t\t\tcase 'DirectionalLight':\n\t\t\t\t\tuniforms = {\n\t\t\t\t\t\tshadowIntensity: 1,\n\t\t\t\t\t\tshadowBias: 0,\n\t\t\t\t\t\tshadowNormalBias: 0,\n\t\t\t\t\t\tshadowRadius: 1,\n\t\t\t\t\t\tshadowMapSize: new Vector2()\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'SpotLight':\n\t\t\t\t\tuniforms = {\n\t\t\t\t\t\tshadowIntensity: 1,\n\t\t\t\t\t\tshadowBias: 0,\n\t\t\t\t\t\tshadowNormalBias: 0,\n\t\t\t\t\t\tshadowRadius: 1,\n\t\t\t\t\t\tshadowMapSize: new Vector2()\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'PointLight':\n\t\t\t\t\tuniforms = {\n\t\t\t\t\t\tshadowIntensity: 1,\n\t\t\t\t\t\tshadowBias: 0,\n\t\t\t\t\t\tshadowNormalBias: 0,\n\t\t\t\t\t\tshadowRadius: 1,\n\t\t\t\t\t\tshadowMapSize: new Vector2(),\n\t\t\t\t\t\tshadowCameraNear: 1,\n\t\t\t\t\t\tshadowCameraFar: 1000\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\n\t\t\t\t// TODO (abelnation): set RectAreaLight shadow uniforms\n\n\t\t\t}\n\n\t\t\tlights[ light.id ] = uniforms;\n\n\t\t\treturn uniforms;\n\n\t\t}\n\n\t};\n\n}\n\n\n\nlet nextVersion = 0;\n\nfunction shadowCastingAndTexturingLightsFirst( lightA, lightB ) {\n\n\treturn ( lightB.castShadow ? 2 : 0 ) - ( lightA.castShadow ? 2 : 0 ) + ( lightB.map ? 1 : 0 ) - ( lightA.map ? 1 : 0 );\n\n}\n\nfunction WebGLLights( extensions ) {\n\n\tconst cache = new UniformsCache();\n\n\tconst shadowCache = ShadowUniformsCache();\n\n\tconst state = {\n\n\t\tversion: 0,\n\n\t\thash: {\n\t\t\tdirectionalLength: -1,\n\t\t\tpointLength: -1,\n\t\t\tspotLength: -1,\n\t\t\trectAreaLength: -1,\n\t\t\themiLength: -1,\n\n\t\t\tnumDirectionalShadows: -1,\n\t\t\tnumPointShadows: -1,\n\t\t\tnumSpotShadows: -1,\n\t\t\tnumSpotMaps: -1,\n\n\t\t\tnumLightProbes: -1\n\t\t},\n\n\t\tambient: [ 0, 0, 0 ],\n\t\tprobe: [],\n\t\tdirectional: [],\n\t\tdirectionalShadow: [],\n\t\tdirectionalShadowMap: [],\n\t\tdirectionalShadowMatrix: [],\n\t\tspot: [],\n\t\tspotLightMap: [],\n\t\tspotShadow: [],\n\t\tspotShadowMap: [],\n\t\tspotLightMatrix: [],\n\t\trectArea: [],\n\t\trectAreaLTC1: null,\n\t\trectAreaLTC2: null,\n\t\tpoint: [],\n\t\tpointShadow: [],\n\t\tpointShadowMap: [],\n\t\tpointShadowMatrix: [],\n\t\themi: [],\n\t\tnumSpotLightShadowsWithMaps: 0,\n\t\tnumLightProbes: 0\n\n\t};\n\n\tfor ( let i = 0; i < 9; i ++ ) state.probe.push( new Vector3() );\n\n\tconst vector3 = new Vector3();\n\tconst matrix4 = new Matrix4();\n\tconst matrix42 = new Matrix4();\n\n\tfunction setup( lights ) {\n\n\t\tlet r = 0, g = 0, b = 0;\n\n\t\tfor ( let i = 0; i < 9; i ++ ) state.probe[ i ].set( 0, 0, 0 );\n\n\t\tlet directionalLength = 0;\n\t\tlet pointLength = 0;\n\t\tlet spotLength = 0;\n\t\tlet rectAreaLength = 0;\n\t\tlet hemiLength = 0;\n\n\t\tlet numDirectionalShadows = 0;\n\t\tlet numPointShadows = 0;\n\t\tlet numSpotShadows = 0;\n\t\tlet numSpotMaps = 0;\n\t\tlet numSpotShadowsWithMaps = 0;\n\n\t\tlet numLightProbes = 0;\n\n\t\t// ordering : [shadow casting + map texturing, map texturing, shadow casting, none ]\n\t\tlights.sort( shadowCastingAndTexturingLightsFirst );\n\n\t\tfor ( let i = 0, l = lights.length; i < l; i ++ ) {\n\n\t\t\tconst light = lights[ i ];\n\n\t\t\tconst color = light.color;\n\t\t\tconst intensity = light.intensity;\n\t\t\tconst distance = light.distance;\n\n\t\t\tlet shadowMap = null;\n\n\t\t\tif ( light.shadow && light.shadow.map ) {\n\n\t\t\t\tif ( light.shadow.map.texture.format === RGFormat ) {\n\n\t\t\t\t\t// VSM uses color texture with blurred mean/std_dev\n\t\t\t\t\tshadowMap = light.shadow.map.texture;\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// Other types use depth texture\n\t\t\t\t\tshadowMap = light.shadow.map.depthTexture || light.shadow.map.texture;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( light.isAmbientLight ) {\n\n\t\t\t\tr += color.r * intensity;\n\t\t\t\tg += color.g * intensity;\n\t\t\t\tb += color.b * intensity;\n\n\t\t\t} else if ( light.isLightProbe ) {\n\n\t\t\t\tfor ( let j = 0; j < 9; j ++ ) {\n\n\t\t\t\t\tstate.probe[ j ].addScaledVector( light.sh.coefficients[ j ], intensity );\n\n\t\t\t\t}\n\n\t\t\t\tnumLightProbes ++;\n\n\t\t\t} else if ( light.isDirectionalLight ) {\n\n\t\t\t\tconst uniforms = cache.get( light );\n\n\t\t\t\tuniforms.color.copy( light.color ).multiplyScalar( light.intensity );\n\n\t\t\t\tif ( light.castShadow ) {\n\n\t\t\t\t\tconst shadow = light.shadow;\n\n\t\t\t\t\tconst shadowUniforms = shadowCache.get( light );\n\n\t\t\t\t\tshadowUniforms.shadowIntensity = shadow.intensity;\n\t\t\t\t\tshadowUniforms.shadowBias = shadow.bias;\n\t\t\t\t\tshadowUniforms.shadowNormalBias = shadow.normalBias;\n\t\t\t\t\tshadowUniforms.shadowRadius = shadow.radius;\n\t\t\t\t\tshadowUniforms.shadowMapSize = shadow.mapSize;\n\n\t\t\t\t\tstate.directionalShadow[ directionalLength ] = shadowUniforms;\n\t\t\t\t\tstate.directionalShadowMap[ directionalLength ] = shadowMap;\n\t\t\t\t\tstate.directionalShadowMatrix[ directionalLength ] = light.shadow.matrix;\n\n\t\t\t\t\tnumDirectionalShadows ++;\n\n\t\t\t\t}\n\n\t\t\t\tstate.directional[ directionalLength ] = uniforms;\n\n\t\t\t\tdirectionalLength ++;\n\n\t\t\t} else if ( light.isSpotLight ) {\n\n\t\t\t\tconst uniforms = cache.get( light );\n\n\t\t\t\tuniforms.position.setFromMatrixPosition( light.matrixWorld );\n\n\t\t\t\tuniforms.color.copy( color ).multiplyScalar( intensity );\n\t\t\t\tuniforms.distance = distance;\n\n\t\t\t\tuniforms.coneCos = Math.cos( light.angle );\n\t\t\t\tuniforms.penumbraCos = Math.cos( light.angle * ( 1 - light.penumbra ) );\n\t\t\t\tuniforms.decay = light.decay;\n\n\t\t\t\tstate.spot[ spotLength ] = uniforms;\n\n\t\t\t\tconst shadow = light.shadow;\n\n\t\t\t\tif ( light.map ) {\n\n\t\t\t\t\tstate.spotLightMap[ numSpotMaps ] = light.map;\n\t\t\t\t\tnumSpotMaps ++;\n\n\t\t\t\t\t// make sure the lightMatrix is up to date\n\t\t\t\t\t// TODO : do it if required only\n\t\t\t\t\tshadow.updateMatrices( light );\n\n\t\t\t\t\tif ( light.castShadow ) numSpotShadowsWithMaps ++;\n\n\t\t\t\t}\n\n\t\t\t\tstate.spotLightMatrix[ spotLength ] = shadow.matrix;\n\n\t\t\t\tif ( light.castShadow ) {\n\n\t\t\t\t\tconst shadowUniforms = shadowCache.get( light );\n\n\t\t\t\t\tshadowUniforms.shadowIntensity = shadow.intensity;\n\t\t\t\t\tshadowUniforms.shadowBias = shadow.bias;\n\t\t\t\t\tshadowUniforms.shadowNormalBias = shadow.normalBias;\n\t\t\t\t\tshadowUniforms.shadowRadius = shadow.radius;\n\t\t\t\t\tshadowUniforms.shadowMapSize = shadow.mapSize;\n\n\t\t\t\t\tstate.spotShadow[ spotLength ] = shadowUniforms;\n\t\t\t\t\tstate.spotShadowMap[ spotLength ] = shadowMap;\n\n\t\t\t\t\tnumSpotShadows ++;\n\n\t\t\t\t}\n\n\t\t\t\tspotLength ++;\n\n\t\t\t} else if ( light.isRectAreaLight ) {\n\n\t\t\t\tconst uniforms = cache.get( light );\n\n\t\t\t\tuniforms.color.copy( color ).multiplyScalar( intensity );\n\n\t\t\t\tuniforms.halfWidth.set( light.width * 0.5, 0.0, 0.0 );\n\t\t\t\tuniforms.halfHeight.set( 0.0, light.height * 0.5, 0.0 );\n\n\t\t\t\tstate.rectArea[ rectAreaLength ] = uniforms;\n\n\t\t\t\trectAreaLength ++;\n\n\t\t\t} else if ( light.isPointLight ) {\n\n\t\t\t\tconst uniforms = cache.get( light );\n\n\t\t\t\tuniforms.color.copy( light.color ).multiplyScalar( light.intensity );\n\t\t\t\tuniforms.distance = light.distance;\n\t\t\t\tuniforms.decay = light.decay;\n\n\t\t\t\tif ( light.castShadow ) {\n\n\t\t\t\t\tconst shadow = light.shadow;\n\n\t\t\t\t\tconst shadowUniforms = shadowCache.get( light );\n\n\t\t\t\t\tshadowUniforms.shadowIntensity = shadow.intensity;\n\t\t\t\t\tshadowUniforms.shadowBias = shadow.bias;\n\t\t\t\t\tshadowUniforms.shadowNormalBias = shadow.normalBias;\n\t\t\t\t\tshadowUniforms.shadowRadius = shadow.radius;\n\t\t\t\t\tshadowUniforms.shadowMapSize = shadow.mapSize;\n\t\t\t\t\tshadowUniforms.shadowCameraNear = shadow.camera.near;\n\t\t\t\t\tshadowUniforms.shadowCameraFar = shadow.camera.far;\n\n\t\t\t\t\tstate.pointShadow[ pointLength ] = shadowUniforms;\n\t\t\t\t\tstate.pointShadowMap[ pointLength ] = shadowMap;\n\t\t\t\t\tstate.pointShadowMatrix[ pointLength ] = light.shadow.matrix;\n\n\t\t\t\t\tnumPointShadows ++;\n\n\t\t\t\t}\n\n\t\t\t\tstate.point[ pointLength ] = uniforms;\n\n\t\t\t\tpointLength ++;\n\n\t\t\t} else if ( light.isHemisphereLight ) {\n\n\t\t\t\tconst uniforms = cache.get( light );\n\n\t\t\t\tuniforms.skyColor.copy( light.color ).multiplyScalar( intensity );\n\t\t\t\tuniforms.groundColor.copy( light.groundColor ).multiplyScalar( intensity );\n\n\t\t\t\tstate.hemi[ hemiLength ] = uniforms;\n\n\t\t\t\themiLength ++;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( rectAreaLength > 0 ) {\n\n\t\t\tif ( extensions.has( 'OES_texture_float_linear' ) === true ) {\n\n\t\t\t\tstate.rectAreaLTC1 = UniformsLib.LTC_FLOAT_1;\n\t\t\t\tstate.rectAreaLTC2 = UniformsLib.LTC_FLOAT_2;\n\n\t\t\t} else {\n\n\t\t\t\tstate.rectAreaLTC1 = UniformsLib.LTC_HALF_1;\n\t\t\t\tstate.rectAreaLTC2 = UniformsLib.LTC_HALF_2;\n\n\t\t\t}\n\n\t\t}\n\n\t\tstate.ambient[ 0 ] = r;\n\t\tstate.ambient[ 1 ] = g;\n\t\tstate.ambient[ 2 ] = b;\n\n\t\tconst hash = state.hash;\n\n\t\tif ( hash.directionalLength !== directionalLength ||\n\t\t\thash.pointLength !== pointLength ||\n\t\t\thash.spotLength !== spotLength ||\n\t\t\thash.rectAreaLength !== rectAreaLength ||\n\t\t\thash.hemiLength !== hemiLength ||\n\t\t\thash.numDirectionalShadows !== numDirectionalShadows ||\n\t\t\thash.numPointShadows !== numPointShadows ||\n\t\t\thash.numSpotShadows !== numSpotShadows ||\n\t\t\thash.numSpotMaps !== numSpotMaps ||\n\t\t\thash.numLightProbes !== numLightProbes ) {\n\n\t\t\tstate.directional.length = directionalLength;\n\t\t\tstate.spot.length = spotLength;\n\t\t\tstate.rectArea.length = rectAreaLength;\n\t\t\tstate.point.length = pointLength;\n\t\t\tstate.hemi.length = hemiLength;\n\n\t\t\tstate.directionalShadow.length = numDirectionalShadows;\n\t\t\tstate.directionalShadowMap.length = numDirectionalShadows;\n\t\t\tstate.pointShadow.length = numPointShadows;\n\t\t\tstate.pointShadowMap.length = numPointShadows;\n\t\t\tstate.spotShadow.length = numSpotShadows;\n\t\t\tstate.spotShadowMap.length = numSpotShadows;\n\t\t\tstate.directionalShadowMatrix.length = numDirectionalShadows;\n\t\t\tstate.pointShadowMatrix.length = numPointShadows;\n\t\t\tstate.spotLightMatrix.length = numSpotShadows + numSpotMaps - numSpotShadowsWithMaps;\n\t\t\tstate.spotLightMap.length = numSpotMaps;\n\t\t\tstate.numSpotLightShadowsWithMaps = numSpotShadowsWithMaps;\n\t\t\tstate.numLightProbes = numLightProbes;\n\n\t\t\thash.directionalLength = directionalLength;\n\t\t\thash.pointLength = pointLength;\n\t\t\thash.spotLength = spotLength;\n\t\t\thash.rectAreaLength = rectAreaLength;\n\t\t\thash.hemiLength = hemiLength;\n\n\t\t\thash.numDirectionalShadows = numDirectionalShadows;\n\t\t\thash.numPointShadows = numPointShadows;\n\t\t\thash.numSpotShadows = numSpotShadows;\n\t\t\thash.numSpotMaps = numSpotMaps;\n\n\t\t\thash.numLightProbes = numLightProbes;\n\n\t\t\tstate.version = nextVersion ++;\n\n\t\t}\n\n\t}\n\n\tfunction setupView( lights, camera ) {\n\n\t\tlet directionalLength = 0;\n\t\tlet pointLength = 0;\n\t\tlet spotLength = 0;\n\t\tlet rectAreaLength = 0;\n\t\tlet hemiLength = 0;\n\n\t\tconst viewMatrix = camera.matrixWorldInverse;\n\n\t\tfor ( let i = 0, l = lights.length; i < l; i ++ ) {\n\n\t\t\tconst light = lights[ i ];\n\n\t\t\tif ( light.isDirectionalLight ) {\n\n\t\t\t\tconst uniforms = state.directional[ directionalLength ];\n\n\t\t\t\tuniforms.direction.setFromMatrixPosition( light.matrixWorld );\n\t\t\t\tvector3.setFromMatrixPosition( light.target.matrixWorld );\n\t\t\t\tuniforms.direction.sub( vector3 );\n\t\t\t\tuniforms.direction.transformDirection( viewMatrix );\n\n\t\t\t\tdirectionalLength ++;\n\n\t\t\t} else if ( light.isSpotLight ) {\n\n\t\t\t\tconst uniforms = state.spot[ spotLength ];\n\n\t\t\t\tuniforms.position.setFromMatrixPosition( light.matrixWorld );\n\t\t\t\tuniforms.position.applyMatrix4( viewMatrix );\n\n\t\t\t\tuniforms.direction.setFromMatrixPosition( light.matrixWorld );\n\t\t\t\tvector3.setFromMatrixPosition( light.target.matrixWorld );\n\t\t\t\tuniforms.direction.sub( vector3 );\n\t\t\t\tuniforms.direction.transformDirection( viewMatrix );\n\n\t\t\t\tspotLength ++;\n\n\t\t\t} else if ( light.isRectAreaLight ) {\n\n\t\t\t\tconst uniforms = state.rectArea[ rectAreaLength ];\n\n\t\t\t\tuniforms.position.setFromMatrixPosition( light.matrixWorld );\n\t\t\t\tuniforms.position.applyMatrix4( viewMatrix );\n\n\t\t\t\t// extract local rotation of light to derive width/height half vectors\n\t\t\t\tmatrix42.identity();\n\t\t\t\tmatrix4.copy( light.matrixWorld );\n\t\t\t\tmatrix4.premultiply( viewMatrix );\n\t\t\t\tmatrix42.extractRotation( matrix4 );\n\n\t\t\t\tuniforms.halfWidth.set( light.width * 0.5, 0.0, 0.0 );\n\t\t\t\tuniforms.halfHeight.set( 0.0, light.height * 0.5, 0.0 );\n\n\t\t\t\tuniforms.halfWidth.applyMatrix4( matrix42 );\n\t\t\t\tuniforms.halfHeight.applyMatrix4( matrix42 );\n\n\t\t\t\trectAreaLength ++;\n\n\t\t\t} else if ( light.isPointLight ) {\n\n\t\t\t\tconst uniforms = state.point[ pointLength ];\n\n\t\t\t\tuniforms.position.setFromMatrixPosition( light.matrixWorld );\n\t\t\t\tuniforms.position.applyMatrix4( viewMatrix );\n\n\t\t\t\tpointLength ++;\n\n\t\t\t} else if ( light.isHemisphereLight ) {\n\n\t\t\t\tconst uniforms = state.hemi[ hemiLength ];\n\n\t\t\t\tuniforms.direction.setFromMatrixPosition( light.matrixWorld );\n\t\t\t\tuniforms.direction.transformDirection( viewMatrix );\n\n\t\t\t\themiLength ++;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn {\n\t\tsetup: setup,\n\t\tsetupView: setupView,\n\t\tstate: state\n\t};\n\n}\n\nfunction WebGLRenderState( extensions ) {\n\n\tconst lights = new WebGLLights( extensions );\n\n\tconst lightsArray = [];\n\tconst shadowsArray = [];\n\n\tfunction init( camera ) {\n\n\t\tstate.camera = camera;\n\n\t\tlightsArray.length = 0;\n\t\tshadowsArray.length = 0;\n\n\t}\n\n\tfunction pushLight( light ) {\n\n\t\tlightsArray.push( light );\n\n\t}\n\n\tfunction pushShadow( shadowLight ) {\n\n\t\tshadowsArray.push( shadowLight );\n\n\t}\n\n\tfunction setupLights() {\n\n\t\tlights.setup( lightsArray );\n\n\t}\n\n\tfunction setupLightsView( camera ) {\n\n\t\tlights.setupView( lightsArray, camera );\n\n\t}\n\n\tconst state = {\n\t\tlightsArray: lightsArray,\n\t\tshadowsArray: shadowsArray,\n\n\t\tcamera: null,\n\n\t\tlights: lights,\n\n\t\ttransmissionRenderTarget: {}\n\t};\n\n\treturn {\n\t\tinit: init,\n\t\tstate: state,\n\t\tsetupLights: setupLights,\n\t\tsetupLightsView: setupLightsView,\n\n\t\tpushLight: pushLight,\n\t\tpushShadow: pushShadow\n\t};\n\n}\n\nfunction WebGLRenderStates( extensions ) {\n\n\tlet renderStates = new WeakMap();\n\n\tfunction get( scene, renderCallDepth = 0 ) {\n\n\t\tconst renderStateArray = renderStates.get( scene );\n\t\tlet renderState;\n\n\t\tif ( renderStateArray === undefined ) {\n\n\t\t\trenderState = new WebGLRenderState( extensions );\n\t\t\trenderStates.set( scene, [ renderState ] );\n\n\t\t} else {\n\n\t\t\tif ( renderCallDepth >= renderStateArray.length ) {\n\n\t\t\t\trenderState = new WebGLRenderState( extensions );\n\t\t\t\trenderStateArray.push( renderState );\n\n\t\t\t} else {\n\n\t\t\t\trenderState = renderStateArray[ renderCallDepth ];\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn renderState;\n\n\t}\n\n\tfunction dispose() {\n\n\t\trenderStates = new WeakMap();\n\n\t}\n\n\treturn {\n\t\tget: get,\n\t\tdispose: dispose\n\t};\n\n}\n\nconst vertex = \"void main() {\\n\\tgl_Position = vec4( position, 1.0 );\\n}\";\n\nconst fragment = \"uniform sampler2D shadow_pass;\\nuniform vec2 resolution;\\nuniform float radius;\\nvoid main() {\\n\\tconst float samples = float( VSM_SAMPLES );\\n\\tfloat mean = 0.0;\\n\\tfloat squared_mean = 0.0;\\n\\tfloat uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 );\\n\\tfloat uvStart = samples <= 1.0 ? 0.0 : - 1.0;\\n\\tfor ( float i = 0.0; i < samples; i ++ ) {\\n\\t\\tfloat uvOffset = uvStart + i * uvStride;\\n\\t\\t#ifdef HORIZONTAL_PASS\\n\\t\\t\\tvec2 distribution = texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ).rg;\\n\\t\\t\\tmean += distribution.x;\\n\\t\\t\\tsquared_mean += distribution.y * distribution.y + distribution.x * distribution.x;\\n\\t\\t#else\\n\\t\\t\\tfloat depth = texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ).r;\\n\\t\\t\\tmean += depth;\\n\\t\\t\\tsquared_mean += depth * depth;\\n\\t\\t#endif\\n\\t}\\n\\tmean = mean / samples;\\n\\tsquared_mean = squared_mean / samples;\\n\\tfloat std_dev = sqrt( max( 0.0, squared_mean - mean * mean ) );\\n\\tgl_FragColor = vec4( mean, std_dev, 0.0, 1.0 );\\n}\";\n\nconst _cubeDirections = [\n\t/*@__PURE__*/ new Vector3( 1, 0, 0 ), /*@__PURE__*/ new Vector3( -1, 0, 0 ), /*@__PURE__*/ new Vector3( 0, 1, 0 ),\n\t/*@__PURE__*/ new Vector3( 0, -1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ), /*@__PURE__*/ new Vector3( 0, 0, -1 )\n];\n\nconst _cubeUps = [\n\t/*@__PURE__*/ new Vector3( 0, -1, 0 ), /*@__PURE__*/ new Vector3( 0, -1, 0 ), /*@__PURE__*/ new Vector3( 0, 0, 1 ),\n\t/*@__PURE__*/ new Vector3( 0, 0, -1 ), /*@__PURE__*/ new Vector3( 0, -1, 0 ), /*@__PURE__*/ new Vector3( 0, -1, 0 )\n];\n\nconst _projScreenMatrix = /*@__PURE__*/ new Matrix4();\nconst _lightPositionWorld = /*@__PURE__*/ new Vector3();\nconst _lookTarget = /*@__PURE__*/ new Vector3();\n\nfunction WebGLShadowMap( renderer, objects, capabilities ) {\n\n\tlet _frustum = new Frustum();\n\n\tconst _shadowMapSize = new Vector2(),\n\t\t_viewportSize = new Vector2(),\n\n\t\t_viewport = new Vector4(),\n\n\t\t_depthMaterial = new MeshDepthMaterial(),\n\t\t_distanceMaterial = new MeshDistanceMaterial(),\n\n\t\t_materialCache = {},\n\n\t\t_maxTextureSize = capabilities.maxTextureSize;\n\n\tconst shadowSide = { [ FrontSide ]: BackSide, [ BackSide ]: FrontSide, [ DoubleSide ]: DoubleSide };\n\n\tconst shadowMaterialVertical = new ShaderMaterial( {\n\t\tdefines: {\n\t\t\tVSM_SAMPLES: 8\n\t\t},\n\t\tuniforms: {\n\t\t\tshadow_pass: { value: null },\n\t\t\tresolution: { value: new Vector2() },\n\t\t\tradius: { value: 4.0 }\n\t\t},\n\n\t\tvertexShader: vertex,\n\t\tfragmentShader: fragment\n\n\t} );\n\n\tconst shadowMaterialHorizontal = shadowMaterialVertical.clone();\n\tshadowMaterialHorizontal.defines.HORIZONTAL_PASS = 1;\n\n\tconst fullScreenTri = new BufferGeometry();\n\tfullScreenTri.setAttribute(\n\t\t'position',\n\t\tnew BufferAttribute(\n\t\t\tnew Float32Array( [ -1, -1, 0.5, 3, -1, 0.5, -1, 3, 0.5 ] ),\n\t\t\t3\n\t\t)\n\t);\n\n\tconst fullScreenMesh = new Mesh( fullScreenTri, shadowMaterialVertical );\n\n\tconst scope = this;\n\n\tthis.enabled = false;\n\n\tthis.autoUpdate = true;\n\tthis.needsUpdate = false;\n\n\tthis.type = PCFShadowMap;\n\tlet _previousType = this.type;\n\n\tthis.render = function ( lights, scene, camera ) {\n\n\t\tif ( scope.enabled === false ) return;\n\t\tif ( scope.autoUpdate === false && scope.needsUpdate === false ) return;\n\n\t\tif ( lights.length === 0 ) return;\n\n\t\tif ( lights.type === PCFSoftShadowMap ) {\n\n\t\t\twarn( 'WebGLShadowMap: PCFSoftShadowMap has been deprecated. Using PCFShadowMap instead.' );\n\t\t\tlights.type = PCFShadowMap;\n\n\t\t}\n\n\t\tconst currentRenderTarget = renderer.getRenderTarget();\n\t\tconst activeCubeFace = renderer.getActiveCubeFace();\n\t\tconst activeMipmapLevel = renderer.getActiveMipmapLevel();\n\n\t\tconst _state = renderer.state;\n\n\t\t// Set GL state for depth map.\n\t\t_state.setBlending( NoBlending );\n\n\t\tif ( _state.buffers.depth.getReversed() === true ) {\n\n\t\t\t_state.buffers.color.setClear( 0, 0, 0, 0 );\n\n\t\t} else {\n\n\t\t\t_state.buffers.color.setClear( 1, 1, 1, 1 );\n\n\t\t}\n\n\t\t_state.buffers.depth.setTest( true );\n\t\t_state.setScissorTest( false );\n\n\t\t// check for shadow map type changes\n\n\t\tconst typeChanged = _previousType !== this.type;\n\n\t\t// When shadow map type changes, materials need recompilation because sampler types change\n\t\t// (sampler2DShadow for PCF vs sampler2D for Basic)\n\t\tif ( typeChanged ) {\n\n\t\t\tscene.traverse( function ( object ) {\n\n\t\t\t\tif ( object.material ) {\n\n\t\t\t\t\tif ( Array.isArray( object.material ) ) {\n\n\t\t\t\t\t\tobject.material.forEach( mat => mat.needsUpdate = true );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tobject.material.needsUpdate = true;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t}\n\n\t\t// render depth map\n\n\t\tfor ( let i = 0, il = lights.length; i < il; i ++ ) {\n\n\t\t\tconst light = lights[ i ];\n\t\t\tconst shadow = light.shadow;\n\n\t\t\tif ( shadow === undefined ) {\n\n\t\t\t\twarn( 'WebGLShadowMap:', light, 'has no shadow.' );\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\tif ( shadow.autoUpdate === false && shadow.needsUpdate === false ) continue;\n\n\t\t\t_shadowMapSize.copy( shadow.mapSize );\n\n\t\t\tconst shadowFrameExtents = shadow.getFrameExtents();\n\n\t\t\t_shadowMapSize.multiply( shadowFrameExtents );\n\n\t\t\t_viewportSize.copy( shadow.mapSize );\n\n\t\t\tif ( _shadowMapSize.x > _maxTextureSize || _shadowMapSize.y > _maxTextureSize ) {\n\n\t\t\t\tif ( _shadowMapSize.x > _maxTextureSize ) {\n\n\t\t\t\t\t_viewportSize.x = Math.floor( _maxTextureSize / shadowFrameExtents.x );\n\t\t\t\t\t_shadowMapSize.x = _viewportSize.x * shadowFrameExtents.x;\n\t\t\t\t\tshadow.mapSize.x = _viewportSize.x;\n\n\t\t\t\t}\n\n\t\t\t\tif ( _shadowMapSize.y > _maxTextureSize ) {\n\n\t\t\t\t\t_viewportSize.y = Math.floor( _maxTextureSize / shadowFrameExtents.y );\n\t\t\t\t\t_shadowMapSize.y = _viewportSize.y * shadowFrameExtents.y;\n\t\t\t\t\tshadow.mapSize.y = _viewportSize.y;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( shadow.map === null || typeChanged === true ) {\n\n\t\t\t\tif ( shadow.map !== null ) {\n\n\t\t\t\t\tif ( shadow.map.depthTexture !== null ) {\n\n\t\t\t\t\t\tshadow.map.depthTexture.dispose();\n\t\t\t\t\t\tshadow.map.depthTexture = null;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tshadow.map.dispose();\n\n\t\t\t\t}\n\n\t\t\t\tif ( this.type === VSMShadowMap ) {\n\n\t\t\t\t\tif ( light.isPointLight ) {\n\n\t\t\t\t\t\twarn( 'WebGLShadowMap: VSM shadow maps are not supported for PointLights. Use PCF or BasicShadowMap instead.' );\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tshadow.map = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y, {\n\t\t\t\t\t\tformat: RGFormat,\n\t\t\t\t\t\ttype: HalfFloatType,\n\t\t\t\t\t\tminFilter: LinearFilter,\n\t\t\t\t\t\tmagFilter: LinearFilter,\n\t\t\t\t\t\tgenerateMipmaps: false\n\t\t\t\t\t} );\n\t\t\t\t\tshadow.map.texture.name = light.name + '.shadowMap';\n\n\t\t\t\t\t// Native depth texture for VSM - depth is captured here, then blurred into the color texture\n\t\t\t\t\tshadow.map.depthTexture = new DepthTexture( _shadowMapSize.x, _shadowMapSize.y, FloatType );\n\t\t\t\t\tshadow.map.depthTexture.name = light.name + '.shadowMapDepth';\n\t\t\t\t\tshadow.map.depthTexture.format = DepthFormat;\n\t\t\t\t\tshadow.map.depthTexture.compareFunction = null; // For regular sampling (not shadow comparison)\n\t\t\t\t\tshadow.map.depthTexture.minFilter = NearestFilter;\n\t\t\t\t\tshadow.map.depthTexture.magFilter = NearestFilter;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( light.isPointLight ) {\n\n\t\t\t\t\t\tshadow.map = new WebGLCubeRenderTarget( _shadowMapSize.x );\n\t\t\t\t\t\tshadow.map.depthTexture = new CubeDepthTexture( _shadowMapSize.x, UnsignedIntType );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tshadow.map = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y );\n\t\t\t\t\t\tshadow.map.depthTexture = new DepthTexture( _shadowMapSize.x, _shadowMapSize.y, UnsignedIntType );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tshadow.map.depthTexture.name = light.name + '.shadowMap';\n\t\t\t\t\tshadow.map.depthTexture.format = DepthFormat;\n\n\t\t\t\t\tconst reversedDepthBuffer = renderer.state.buffers.depth.getReversed();\n\n\t\t\t\t\tif ( this.type === PCFShadowMap ) {\n\n\t\t\t\t\t\tshadow.map.depthTexture.compareFunction = reversedDepthBuffer ? GreaterEqualCompare : LessEqualCompare;\n\t\t\t\t\t\tshadow.map.depthTexture.minFilter = LinearFilter;\n\t\t\t\t\t\tshadow.map.depthTexture.magFilter = LinearFilter;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tshadow.map.depthTexture.compareFunction = null;\n\t\t\t\t\t\tshadow.map.depthTexture.minFilter = NearestFilter;\n\t\t\t\t\t\tshadow.map.depthTexture.magFilter = NearestFilter;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tshadow.camera.updateProjectionMatrix();\n\n\t\t\t}\n\n\t\t\t// For cube render targets (PointLights), render all 6 faces. Otherwise, render once.\n\t\t\tconst faceCount = shadow.map.isWebGLCubeRenderTarget ? 6 : 1;\n\n\t\t\tfor ( let face = 0; face < faceCount; face ++ ) {\n\n\t\t\t\t// For cube render targets, render to each face separately\n\t\t\t\tif ( shadow.map.isWebGLCubeRenderTarget ) {\n\n\t\t\t\t\trenderer.setRenderTarget( shadow.map, face );\n\t\t\t\t\trenderer.clear();\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// For 2D render targets, use viewports\n\t\t\t\t\tif ( face === 0 ) {\n\n\t\t\t\t\t\trenderer.setRenderTarget( shadow.map );\n\t\t\t\t\t\trenderer.clear();\n\n\t\t\t\t\t}\n\n\t\t\t\t\tconst viewport = shadow.getViewport( face );\n\n\t\t\t\t\t_viewport.set(\n\t\t\t\t\t\t_viewportSize.x * viewport.x,\n\t\t\t\t\t\t_viewportSize.y * viewport.y,\n\t\t\t\t\t\t_viewportSize.x * viewport.z,\n\t\t\t\t\t\t_viewportSize.y * viewport.w\n\t\t\t\t\t);\n\n\t\t\t\t\t_state.viewport( _viewport );\n\n\t\t\t\t}\n\n\t\t\t\tif ( light.isPointLight ) {\n\n\t\t\t\t\tconst camera = shadow.camera;\n\t\t\t\t\tconst shadowMatrix = shadow.matrix;\n\n\t\t\t\t\tconst far = light.distance || camera.far;\n\n\t\t\t\t\tif ( far !== camera.far ) {\n\n\t\t\t\t\t\tcamera.far = far;\n\t\t\t\t\t\tcamera.updateProjectionMatrix();\n\n\t\t\t\t\t}\n\n\t\t\t\t\t_lightPositionWorld.setFromMatrixPosition( light.matrixWorld );\n\t\t\t\t\tcamera.position.copy( _lightPositionWorld );\n\n\t\t\t\t\t_lookTarget.copy( camera.position );\n\t\t\t\t\t_lookTarget.add( _cubeDirections[ face ] );\n\t\t\t\t\tcamera.up.copy( _cubeUps[ face ] );\n\t\t\t\t\tcamera.lookAt( _lookTarget );\n\t\t\t\t\tcamera.updateMatrixWorld();\n\n\t\t\t\t\tshadowMatrix.makeTranslation( - _lightPositionWorld.x, - _lightPositionWorld.y, - _lightPositionWorld.z );\n\n\t\t\t\t\t_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );\n\t\t\t\t\tshadow._frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tshadow.updateMatrices( light );\n\n\t\t\t\t}\n\n\t\t\t\t_frustum = shadow.getFrustum();\n\n\t\t\t\trenderObject( scene, camera, shadow.camera, light, this.type );\n\n\t\t\t}\n\n\t\t\t// do blur pass for VSM\n\n\t\t\tif ( shadow.isPointLightShadow !== true && this.type === VSMShadowMap ) {\n\n\t\t\t\tVSMPass( shadow, camera );\n\n\t\t\t}\n\n\t\t\tshadow.needsUpdate = false;\n\n\t\t}\n\n\t\t_previousType = this.type;\n\n\t\tscope.needsUpdate = false;\n\n\t\trenderer.setRenderTarget( currentRenderTarget, activeCubeFace, activeMipmapLevel );\n\n\t};\n\n\tfunction VSMPass( shadow, camera ) {\n\n\t\tconst geometry = objects.update( fullScreenMesh );\n\n\t\tif ( shadowMaterialVertical.defines.VSM_SAMPLES !== shadow.blurSamples ) {\n\n\t\t\tshadowMaterialVertical.defines.VSM_SAMPLES = shadow.blurSamples;\n\t\t\tshadowMaterialHorizontal.defines.VSM_SAMPLES = shadow.blurSamples;\n\n\t\t\tshadowMaterialVertical.needsUpdate = true;\n\t\t\tshadowMaterialHorizontal.needsUpdate = true;\n\n\t\t}\n\n\t\tif ( shadow.mapPass === null ) {\n\n\t\t\tshadow.mapPass = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y, {\n\t\t\t\tformat: RGFormat,\n\t\t\t\ttype: HalfFloatType\n\t\t\t} );\n\n\t\t}\n\n\t\t// vertical pass - read from native depth texture\n\n\t\tshadowMaterialVertical.uniforms.shadow_pass.value = shadow.map.depthTexture;\n\t\tshadowMaterialVertical.uniforms.resolution.value = shadow.mapSize;\n\t\tshadowMaterialVertical.uniforms.radius.value = shadow.radius;\n\t\trenderer.setRenderTarget( shadow.mapPass );\n\t\trenderer.clear();\n\t\trenderer.renderBufferDirect( camera, null, geometry, shadowMaterialVertical, fullScreenMesh, null );\n\n\t\t// horizontal pass\n\n\t\tshadowMaterialHorizontal.uniforms.shadow_pass.value = shadow.mapPass.texture;\n\t\tshadowMaterialHorizontal.uniforms.resolution.value = shadow.mapSize;\n\t\tshadowMaterialHorizontal.uniforms.radius.value = shadow.radius;\n\t\trenderer.setRenderTarget( shadow.map );\n\t\trenderer.clear();\n\t\trenderer.renderBufferDirect( camera, null, geometry, shadowMaterialHorizontal, fullScreenMesh, null );\n\n\t}\n\n\tfunction getDepthMaterial( object, material, light, type ) {\n\n\t\tlet result = null;\n\n\t\tconst customMaterial = ( light.isPointLight === true ) ? object.customDistanceMaterial : object.customDepthMaterial;\n\n\t\tif ( customMaterial !== undefined ) {\n\n\t\t\tresult = customMaterial;\n\n\t\t} else {\n\n\t\t\tresult = ( light.isPointLight === true ) ? _distanceMaterial : _depthMaterial;\n\n\t\t\tif ( ( renderer.localClippingEnabled && material.clipShadows === true && Array.isArray( material.clippingPlanes ) && material.clippingPlanes.length !== 0 ) ||\n\t\t\t\t( material.displacementMap && material.displacementScale !== 0 ) ||\n\t\t\t\t( material.alphaMap && material.alphaTest > 0 ) ||\n\t\t\t\t( material.map && material.alphaTest > 0 ) ||\n\t\t\t\t( material.alphaToCoverage === true ) ) {\n\n\t\t\t\t// in this case we need a unique material instance reflecting the\n\t\t\t\t// appropriate state\n\n\t\t\t\tconst keyA = result.uuid, keyB = material.uuid;\n\n\t\t\t\tlet materialsForVariant = _materialCache[ keyA ];\n\n\t\t\t\tif ( materialsForVariant === undefined ) {\n\n\t\t\t\t\tmaterialsForVariant = {};\n\t\t\t\t\t_materialCache[ keyA ] = materialsForVariant;\n\n\t\t\t\t}\n\n\t\t\t\tlet cachedMaterial = materialsForVariant[ keyB ];\n\n\t\t\t\tif ( cachedMaterial === undefined ) {\n\n\t\t\t\t\tcachedMaterial = result.clone();\n\t\t\t\t\tmaterialsForVariant[ keyB ] = cachedMaterial;\n\t\t\t\t\tmaterial.addEventListener( 'dispose', onMaterialDispose );\n\n\t\t\t\t}\n\n\t\t\t\tresult = cachedMaterial;\n\n\t\t\t}\n\n\t\t}\n\n\t\tresult.visible = material.visible;\n\t\tresult.wireframe = material.wireframe;\n\n\t\tif ( type === VSMShadowMap ) {\n\n\t\t\tresult.side = ( material.shadowSide !== null ) ? material.shadowSide : material.side;\n\n\t\t} else {\n\n\t\t\tresult.side = ( material.shadowSide !== null ) ? material.shadowSide : shadowSide[ material.side ];\n\n\t\t}\n\n\t\tresult.alphaMap = material.alphaMap;\n\t\tresult.alphaTest = ( material.alphaToCoverage === true ) ? 0.5 : material.alphaTest; // approximate alphaToCoverage by using a fixed alphaTest value\n\t\tresult.map = material.map;\n\n\t\tresult.clipShadows = material.clipShadows;\n\t\tresult.clippingPlanes = material.clippingPlanes;\n\t\tresult.clipIntersection = material.clipIntersection;\n\n\t\tresult.displacementMap = material.displacementMap;\n\t\tresult.displacementScale = material.displacementScale;\n\t\tresult.displacementBias = material.displacementBias;\n\n\t\tresult.wireframeLinewidth = material.wireframeLinewidth;\n\t\tresult.linewidth = material.linewidth;\n\n\t\tif ( light.isPointLight === true && result.isMeshDistanceMaterial === true ) {\n\n\t\t\tconst materialProperties = renderer.properties.get( result );\n\t\t\tmaterialProperties.light = light;\n\n\t\t}\n\n\t\treturn result;\n\n\t}\n\n\tfunction renderObject( object, camera, shadowCamera, light, type ) {\n\n\t\tif ( object.visible === false ) return;\n\n\t\tconst visible = object.layers.test( camera.layers );\n\n\t\tif ( visible && ( object.isMesh || object.isLine || object.isPoints ) ) {\n\n\t\t\tif ( ( object.castShadow || ( object.receiveShadow && type === VSMShadowMap ) ) && ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) ) {\n\n\t\t\t\tobject.modelViewMatrix.multiplyMatrices( shadowCamera.matrixWorldInverse, object.matrixWorld );\n\n\t\t\t\tconst geometry = objects.update( object );\n\t\t\t\tconst material = object.material;\n\n\t\t\t\tif ( Array.isArray( material ) ) {\n\n\t\t\t\t\tconst groups = geometry.groups;\n\n\t\t\t\t\tfor ( let k = 0, kl = groups.length; k < kl; k ++ ) {\n\n\t\t\t\t\t\tconst group = groups[ k ];\n\t\t\t\t\t\tconst groupMaterial = material[ group.materialIndex ];\n\n\t\t\t\t\t\tif ( groupMaterial && groupMaterial.visible ) {\n\n\t\t\t\t\t\t\tconst depthMaterial = getDepthMaterial( object, groupMaterial, light, type );\n\n\t\t\t\t\t\t\tobject.onBeforeShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial, group );\n\n\t\t\t\t\t\t\trenderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, group );\n\n\t\t\t\t\t\t\tobject.onAfterShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial, group );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t} else if ( material.visible ) {\n\n\t\t\t\t\tconst depthMaterial = getDepthMaterial( object, material, light, type );\n\n\t\t\t\t\tobject.onBeforeShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial, null );\n\n\t\t\t\t\trenderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, null );\n\n\t\t\t\t\tobject.onAfterShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial, null );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst children = object.children;\n\n\t\tfor ( let i = 0, l = children.length; i < l; i ++ ) {\n\n\t\t\trenderObject( children[ i ], camera, shadowCamera, light, type );\n\n\t\t}\n\n\t}\n\n\tfunction onMaterialDispose( event ) {\n\n\t\tconst material = event.target;\n\n\t\tmaterial.removeEventListener( 'dispose', onMaterialDispose );\n\n\t\t// make sure to remove the unique distance/depth materials used for shadow map rendering\n\n\t\tfor ( const id in _materialCache ) {\n\n\t\t\tconst cache = _materialCache[ id ];\n\n\t\t\tconst uuid = event.target.uuid;\n\n\t\t\tif ( uuid in cache ) {\n\n\t\t\t\tconst shadowMaterial = cache[ uuid ];\n\t\t\t\tshadowMaterial.dispose();\n\t\t\t\tdelete cache[ uuid ];\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}\n\nconst reversedFuncs = {\n\t[ NeverDepth ]: AlwaysDepth,\n\t[ LessDepth ]: GreaterDepth,\n\t[ EqualDepth ]: NotEqualDepth,\n\t[ LessEqualDepth ]: GreaterEqualDepth,\n\n\t[ AlwaysDepth ]: NeverDepth,\n\t[ GreaterDepth ]: LessDepth,\n\t[ NotEqualDepth ]: EqualDepth,\n\t[ GreaterEqualDepth ]: LessEqualDepth,\n};\n\nfunction WebGLState( gl, extensions ) {\n\n\tfunction ColorBuffer() {\n\n\t\tlet locked = false;\n\n\t\tconst color = new Vector4();\n\t\tlet currentColorMask = null;\n\t\tconst currentColorClear = new Vector4( 0, 0, 0, 0 );\n\n\t\treturn {\n\n\t\t\tsetMask: function ( colorMask ) {\n\n\t\t\t\tif ( currentColorMask !== colorMask && ! locked ) {\n\n\t\t\t\t\tgl.colorMask( colorMask, colorMask, colorMask, colorMask );\n\t\t\t\t\tcurrentColorMask = colorMask;\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tsetLocked: function ( lock ) {\n\n\t\t\t\tlocked = lock;\n\n\t\t\t},\n\n\t\t\tsetClear: function ( r, g, b, a, premultipliedAlpha ) {\n\n\t\t\t\tif ( premultipliedAlpha === true ) {\n\n\t\t\t\t\tr *= a; g *= a; b *= a;\n\n\t\t\t\t}\n\n\t\t\t\tcolor.set( r, g, b, a );\n\n\t\t\t\tif ( currentColorClear.equals( color ) === false ) {\n\n\t\t\t\t\tgl.clearColor( r, g, b, a );\n\t\t\t\t\tcurrentColorClear.copy( color );\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\treset: function () {\n\n\t\t\t\tlocked = false;\n\n\t\t\t\tcurrentColorMask = null;\n\t\t\t\tcurrentColorClear.set( -1, 0, 0, 0 ); // set to invalid state\n\n\t\t\t}\n\n\t\t};\n\n\t}\n\n\tfunction DepthBuffer() {\n\n\t\tlet locked = false;\n\n\t\tlet currentReversed = false;\n\t\tlet currentDepthMask = null;\n\t\tlet currentDepthFunc = null;\n\t\tlet currentDepthClear = null;\n\n\t\treturn {\n\n\t\t\tsetReversed: function ( reversed ) {\n\n\t\t\t\tif ( currentReversed !== reversed ) {\n\n\t\t\t\t\tconst ext = extensions.get( 'EXT_clip_control' );\n\n\t\t\t\t\tif ( reversed ) {\n\n\t\t\t\t\t\text.clipControlEXT( ext.LOWER_LEFT_EXT, ext.ZERO_TO_ONE_EXT );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\text.clipControlEXT( ext.LOWER_LEFT_EXT, ext.NEGATIVE_ONE_TO_ONE_EXT );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tcurrentReversed = reversed;\n\n\t\t\t\t\tconst oldDepth = currentDepthClear;\n\t\t\t\t\tcurrentDepthClear = null;\n\t\t\t\t\tthis.setClear( oldDepth );\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tgetReversed: function () {\n\n\t\t\t\treturn currentReversed;\n\n\t\t\t},\n\n\t\t\tsetTest: function ( depthTest ) {\n\n\t\t\t\tif ( depthTest ) {\n\n\t\t\t\t\tenable( gl.DEPTH_TEST );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tdisable( gl.DEPTH_TEST );\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tsetMask: function ( depthMask ) {\n\n\t\t\t\tif ( currentDepthMask !== depthMask && ! locked ) {\n\n\t\t\t\t\tgl.depthMask( depthMask );\n\t\t\t\t\tcurrentDepthMask = depthMask;\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tsetFunc: function ( depthFunc ) {\n\n\t\t\t\tif ( currentReversed ) depthFunc = reversedFuncs[ depthFunc ];\n\n\t\t\t\tif ( currentDepthFunc !== depthFunc ) {\n\n\t\t\t\t\tswitch ( depthFunc ) {\n\n\t\t\t\t\t\tcase NeverDepth:\n\n\t\t\t\t\t\t\tgl.depthFunc( gl.NEVER );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase AlwaysDepth:\n\n\t\t\t\t\t\t\tgl.depthFunc( gl.ALWAYS );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase LessDepth:\n\n\t\t\t\t\t\t\tgl.depthFunc( gl.LESS );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase LessEqualDepth:\n\n\t\t\t\t\t\t\tgl.depthFunc( gl.LEQUAL );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase EqualDepth:\n\n\t\t\t\t\t\t\tgl.depthFunc( gl.EQUAL );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase GreaterEqualDepth:\n\n\t\t\t\t\t\t\tgl.depthFunc( gl.GEQUAL );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase GreaterDepth:\n\n\t\t\t\t\t\t\tgl.depthFunc( gl.GREATER );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase NotEqualDepth:\n\n\t\t\t\t\t\t\tgl.depthFunc( gl.NOTEQUAL );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tdefault:\n\n\t\t\t\t\t\t\tgl.depthFunc( gl.LEQUAL );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tcurrentDepthFunc = depthFunc;\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tsetLocked: function ( lock ) {\n\n\t\t\t\tlocked = lock;\n\n\t\t\t},\n\n\t\t\tsetClear: function ( depth ) {\n\n\t\t\t\tif ( currentDepthClear !== depth ) {\n\n\t\t\t\t\tif ( currentReversed ) {\n\n\t\t\t\t\t\tdepth = 1 - depth;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tgl.clearDepth( depth );\n\t\t\t\t\tcurrentDepthClear = depth;\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\treset: function () {\n\n\t\t\t\tlocked = false;\n\n\t\t\t\tcurrentDepthMask = null;\n\t\t\t\tcurrentDepthFunc = null;\n\t\t\t\tcurrentDepthClear = null;\n\t\t\t\tcurrentReversed = false;\n\n\t\t\t}\n\n\t\t};\n\n\t}\n\n\tfunction StencilBuffer() {\n\n\t\tlet locked = false;\n\n\t\tlet currentStencilMask = null;\n\t\tlet currentStencilFunc = null;\n\t\tlet currentStencilRef = null;\n\t\tlet currentStencilFuncMask = null;\n\t\tlet currentStencilFail = null;\n\t\tlet currentStencilZFail = null;\n\t\tlet currentStencilZPass = null;\n\t\tlet currentStencilClear = null;\n\n\t\treturn {\n\n\t\t\tsetTest: function ( stencilTest ) {\n\n\t\t\t\tif ( ! locked ) {\n\n\t\t\t\t\tif ( stencilTest ) {\n\n\t\t\t\t\t\tenable( gl.STENCIL_TEST );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tdisable( gl.STENCIL_TEST );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tsetMask: function ( stencilMask ) {\n\n\t\t\t\tif ( currentStencilMask !== stencilMask && ! locked ) {\n\n\t\t\t\t\tgl.stencilMask( stencilMask );\n\t\t\t\t\tcurrentStencilMask = stencilMask;\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tsetFunc: function ( stencilFunc, stencilRef, stencilMask ) {\n\n\t\t\t\tif ( currentStencilFunc !== stencilFunc ||\n\t\t\t\t     currentStencilRef !== stencilRef ||\n\t\t\t\t     currentStencilFuncMask !== stencilMask ) {\n\n\t\t\t\t\tgl.stencilFunc( stencilFunc, stencilRef, stencilMask );\n\n\t\t\t\t\tcurrentStencilFunc = stencilFunc;\n\t\t\t\t\tcurrentStencilRef = stencilRef;\n\t\t\t\t\tcurrentStencilFuncMask = stencilMask;\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tsetOp: function ( stencilFail, stencilZFail, stencilZPass ) {\n\n\t\t\t\tif ( currentStencilFail !== stencilFail ||\n\t\t\t\t     currentStencilZFail !== stencilZFail ||\n\t\t\t\t     currentStencilZPass !== stencilZPass ) {\n\n\t\t\t\t\tgl.stencilOp( stencilFail, stencilZFail, stencilZPass );\n\n\t\t\t\t\tcurrentStencilFail = stencilFail;\n\t\t\t\t\tcurrentStencilZFail = stencilZFail;\n\t\t\t\t\tcurrentStencilZPass = stencilZPass;\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tsetLocked: function ( lock ) {\n\n\t\t\t\tlocked = lock;\n\n\t\t\t},\n\n\t\t\tsetClear: function ( stencil ) {\n\n\t\t\t\tif ( currentStencilClear !== stencil ) {\n\n\t\t\t\t\tgl.clearStencil( stencil );\n\t\t\t\t\tcurrentStencilClear = stencil;\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\treset: function () {\n\n\t\t\t\tlocked = false;\n\n\t\t\t\tcurrentStencilMask = null;\n\t\t\t\tcurrentStencilFunc = null;\n\t\t\t\tcurrentStencilRef = null;\n\t\t\t\tcurrentStencilFuncMask = null;\n\t\t\t\tcurrentStencilFail = null;\n\t\t\t\tcurrentStencilZFail = null;\n\t\t\t\tcurrentStencilZPass = null;\n\t\t\t\tcurrentStencilClear = null;\n\n\t\t\t}\n\n\t\t};\n\n\t}\n\n\t//\n\n\tconst colorBuffer = new ColorBuffer();\n\tconst depthBuffer = new DepthBuffer();\n\tconst stencilBuffer = new StencilBuffer();\n\n\tconst uboBindings = new WeakMap();\n\tconst uboProgramMap = new WeakMap();\n\n\tlet enabledCapabilities = {};\n\n\tlet currentBoundFramebuffers = {};\n\tlet currentDrawbuffers = new WeakMap();\n\tlet defaultDrawbuffers = [];\n\n\tlet currentProgram = null;\n\n\tlet currentBlendingEnabled = false;\n\tlet currentBlending = null;\n\tlet currentBlendEquation = null;\n\tlet currentBlendSrc = null;\n\tlet currentBlendDst = null;\n\tlet currentBlendEquationAlpha = null;\n\tlet currentBlendSrcAlpha = null;\n\tlet currentBlendDstAlpha = null;\n\tlet currentBlendColor = new Color( 0, 0, 0 );\n\tlet currentBlendAlpha = 0;\n\tlet currentPremultipledAlpha = false;\n\n\tlet currentFlipSided = null;\n\tlet currentCullFace = null;\n\n\tlet currentLineWidth = null;\n\n\tlet currentPolygonOffsetFactor = null;\n\tlet currentPolygonOffsetUnits = null;\n\n\tconst maxTextures = gl.getParameter( gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS );\n\n\tlet lineWidthAvailable = false;\n\tlet version = 0;\n\tconst glVersion = gl.getParameter( gl.VERSION );\n\n\tif ( glVersion.indexOf( 'WebGL' ) !== -1 ) {\n\n\t\tversion = parseFloat( /^WebGL (\\d)/.exec( glVersion )[ 1 ] );\n\t\tlineWidthAvailable = ( version >= 1.0 );\n\n\t} else if ( glVersion.indexOf( 'OpenGL ES' ) !== -1 ) {\n\n\t\tversion = parseFloat( /^OpenGL ES (\\d)/.exec( glVersion )[ 1 ] );\n\t\tlineWidthAvailable = ( version >= 2.0 );\n\n\t}\n\n\tlet currentTextureSlot = null;\n\tlet currentBoundTextures = {};\n\n\tconst scissorParam = gl.getParameter( gl.SCISSOR_BOX );\n\tconst viewportParam = gl.getParameter( gl.VIEWPORT );\n\n\tconst currentScissor = new Vector4().fromArray( scissorParam );\n\tconst currentViewport = new Vector4().fromArray( viewportParam );\n\n\tfunction createTexture( type, target, count, dimensions ) {\n\n\t\tconst data = new Uint8Array( 4 ); // 4 is required to match default unpack alignment of 4.\n\t\tconst texture = gl.createTexture();\n\n\t\tgl.bindTexture( type, texture );\n\t\tgl.texParameteri( type, gl.TEXTURE_MIN_FILTER, gl.NEAREST );\n\t\tgl.texParameteri( type, gl.TEXTURE_MAG_FILTER, gl.NEAREST );\n\n\t\tfor ( let i = 0; i < count; i ++ ) {\n\n\t\t\tif ( type === gl.TEXTURE_3D || type === gl.TEXTURE_2D_ARRAY ) {\n\n\t\t\t\tgl.texImage3D( target, 0, gl.RGBA, 1, 1, dimensions, 0, gl.RGBA, gl.UNSIGNED_BYTE, data );\n\n\t\t\t} else {\n\n\t\t\t\tgl.texImage2D( target + i, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, data );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn texture;\n\n\t}\n\n\tconst emptyTextures = {};\n\temptyTextures[ gl.TEXTURE_2D ] = createTexture( gl.TEXTURE_2D, gl.TEXTURE_2D, 1 );\n\temptyTextures[ gl.TEXTURE_CUBE_MAP ] = createTexture( gl.TEXTURE_CUBE_MAP, gl.TEXTURE_CUBE_MAP_POSITIVE_X, 6 );\n\temptyTextures[ gl.TEXTURE_2D_ARRAY ] = createTexture( gl.TEXTURE_2D_ARRAY, gl.TEXTURE_2D_ARRAY, 1, 1 );\n\temptyTextures[ gl.TEXTURE_3D ] = createTexture( gl.TEXTURE_3D, gl.TEXTURE_3D, 1, 1 );\n\n\t// init\n\n\tcolorBuffer.setClear( 0, 0, 0, 1 );\n\tdepthBuffer.setClear( 1 );\n\tstencilBuffer.setClear( 0 );\n\n\tenable( gl.DEPTH_TEST );\n\tdepthBuffer.setFunc( LessEqualDepth );\n\n\tsetFlipSided( false );\n\tsetCullFace( CullFaceBack );\n\tenable( gl.CULL_FACE );\n\n\tsetBlending( NoBlending );\n\n\t//\n\n\tfunction enable( id ) {\n\n\t\tif ( enabledCapabilities[ id ] !== true ) {\n\n\t\t\tgl.enable( id );\n\t\t\tenabledCapabilities[ id ] = true;\n\n\t\t}\n\n\t}\n\n\tfunction disable( id ) {\n\n\t\tif ( enabledCapabilities[ id ] !== false ) {\n\n\t\t\tgl.disable( id );\n\t\t\tenabledCapabilities[ id ] = false;\n\n\t\t}\n\n\t}\n\n\tfunction bindFramebuffer( target, framebuffer ) {\n\n\t\tif ( currentBoundFramebuffers[ target ] !== framebuffer ) {\n\n\t\t\tgl.bindFramebuffer( target, framebuffer );\n\n\t\t\tcurrentBoundFramebuffers[ target ] = framebuffer;\n\n\t\t\t// gl.DRAW_FRAMEBUFFER is equivalent to gl.FRAMEBUFFER\n\n\t\t\tif ( target === gl.DRAW_FRAMEBUFFER ) {\n\n\t\t\t\tcurrentBoundFramebuffers[ gl.FRAMEBUFFER ] = framebuffer;\n\n\t\t\t}\n\n\t\t\tif ( target === gl.FRAMEBUFFER ) {\n\n\t\t\t\tcurrentBoundFramebuffers[ gl.DRAW_FRAMEBUFFER ] = framebuffer;\n\n\t\t\t}\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\tfunction drawBuffers( renderTarget, framebuffer ) {\n\n\t\tlet drawBuffers = defaultDrawbuffers;\n\n\t\tlet needsUpdate = false;\n\n\t\tif ( renderTarget ) {\n\n\t\t\tdrawBuffers = currentDrawbuffers.get( framebuffer );\n\n\t\t\tif ( drawBuffers === undefined ) {\n\n\t\t\t\tdrawBuffers = [];\n\t\t\t\tcurrentDrawbuffers.set( framebuffer, drawBuffers );\n\n\t\t\t}\n\n\t\t\tconst textures = renderTarget.textures;\n\n\t\t\tif ( drawBuffers.length !== textures.length || drawBuffers[ 0 ] !== gl.COLOR_ATTACHMENT0 ) {\n\n\t\t\t\tfor ( let i = 0, il = textures.length; i < il; i ++ ) {\n\n\t\t\t\t\tdrawBuffers[ i ] = gl.COLOR_ATTACHMENT0 + i;\n\n\t\t\t\t}\n\n\t\t\t\tdrawBuffers.length = textures.length;\n\n\t\t\t\tneedsUpdate = true;\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tif ( drawBuffers[ 0 ] !== gl.BACK ) {\n\n\t\t\t\tdrawBuffers[ 0 ] = gl.BACK;\n\n\t\t\t\tneedsUpdate = true;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( needsUpdate ) {\n\n\t\t\tgl.drawBuffers( drawBuffers );\n\n\t\t}\n\n\t}\n\n\tfunction useProgram( program ) {\n\n\t\tif ( currentProgram !== program ) {\n\n\t\t\tgl.useProgram( program );\n\n\t\t\tcurrentProgram = program;\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\tconst equationToGL = {\n\t\t[ AddEquation ]: gl.FUNC_ADD,\n\t\t[ SubtractEquation ]: gl.FUNC_SUBTRACT,\n\t\t[ ReverseSubtractEquation ]: gl.FUNC_REVERSE_SUBTRACT\n\t};\n\n\tequationToGL[ MinEquation ] = gl.MIN;\n\tequationToGL[ MaxEquation ] = gl.MAX;\n\n\tconst factorToGL = {\n\t\t[ ZeroFactor ]: gl.ZERO,\n\t\t[ OneFactor ]: gl.ONE,\n\t\t[ SrcColorFactor ]: gl.SRC_COLOR,\n\t\t[ SrcAlphaFactor ]: gl.SRC_ALPHA,\n\t\t[ SrcAlphaSaturateFactor ]: gl.SRC_ALPHA_SATURATE,\n\t\t[ DstColorFactor ]: gl.DST_COLOR,\n\t\t[ DstAlphaFactor ]: gl.DST_ALPHA,\n\t\t[ OneMinusSrcColorFactor ]: gl.ONE_MINUS_SRC_COLOR,\n\t\t[ OneMinusSrcAlphaFactor ]: gl.ONE_MINUS_SRC_ALPHA,\n\t\t[ OneMinusDstColorFactor ]: gl.ONE_MINUS_DST_COLOR,\n\t\t[ OneMinusDstAlphaFactor ]: gl.ONE_MINUS_DST_ALPHA,\n\t\t[ ConstantColorFactor ]: gl.CONSTANT_COLOR,\n\t\t[ OneMinusConstantColorFactor ]: gl.ONE_MINUS_CONSTANT_COLOR,\n\t\t[ ConstantAlphaFactor ]: gl.CONSTANT_ALPHA,\n\t\t[ OneMinusConstantAlphaFactor ]: gl.ONE_MINUS_CONSTANT_ALPHA\n\t};\n\n\tfunction setBlending( blending, blendEquation, blendSrc, blendDst, blendEquationAlpha, blendSrcAlpha, blendDstAlpha, blendColor, blendAlpha, premultipliedAlpha ) {\n\n\t\tif ( blending === NoBlending ) {\n\n\t\t\tif ( currentBlendingEnabled === true ) {\n\n\t\t\t\tdisable( gl.BLEND );\n\t\t\t\tcurrentBlendingEnabled = false;\n\n\t\t\t}\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tif ( currentBlendingEnabled === false ) {\n\n\t\t\tenable( gl.BLEND );\n\t\t\tcurrentBlendingEnabled = true;\n\n\t\t}\n\n\t\tif ( blending !== CustomBlending ) {\n\n\t\t\tif ( blending !== currentBlending || premultipliedAlpha !== currentPremultipledAlpha ) {\n\n\t\t\t\tif ( currentBlendEquation !== AddEquation || currentBlendEquationAlpha !== AddEquation ) {\n\n\t\t\t\t\tgl.blendEquation( gl.FUNC_ADD );\n\n\t\t\t\t\tcurrentBlendEquation = AddEquation;\n\t\t\t\t\tcurrentBlendEquationAlpha = AddEquation;\n\n\t\t\t\t}\n\n\t\t\t\tif ( premultipliedAlpha ) {\n\n\t\t\t\t\tswitch ( blending ) {\n\n\t\t\t\t\t\tcase NormalBlending:\n\t\t\t\t\t\t\tgl.blendFuncSeparate( gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase AdditiveBlending:\n\t\t\t\t\t\t\tgl.blendFunc( gl.ONE, gl.ONE );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase SubtractiveBlending:\n\t\t\t\t\t\t\tgl.blendFuncSeparate( gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase MultiplyBlending:\n\t\t\t\t\t\t\tgl.blendFuncSeparate( gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\terror( 'WebGLState: Invalid blending: ', blending );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tswitch ( blending ) {\n\n\t\t\t\t\t\tcase NormalBlending:\n\t\t\t\t\t\t\tgl.blendFuncSeparate( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase AdditiveBlending:\n\t\t\t\t\t\t\tgl.blendFuncSeparate( gl.SRC_ALPHA, gl.ONE, gl.ONE, gl.ONE );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase SubtractiveBlending:\n\t\t\t\t\t\t\terror( 'WebGLState: SubtractiveBlending requires material.premultipliedAlpha = true' );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase MultiplyBlending:\n\t\t\t\t\t\t\terror( 'WebGLState: MultiplyBlending requires material.premultipliedAlpha = true' );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\terror( 'WebGLState: Invalid blending: ', blending );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tcurrentBlendSrc = null;\n\t\t\t\tcurrentBlendDst = null;\n\t\t\t\tcurrentBlendSrcAlpha = null;\n\t\t\t\tcurrentBlendDstAlpha = null;\n\t\t\t\tcurrentBlendColor.set( 0, 0, 0 );\n\t\t\t\tcurrentBlendAlpha = 0;\n\n\t\t\t\tcurrentBlending = blending;\n\t\t\t\tcurrentPremultipledAlpha = premultipliedAlpha;\n\n\t\t\t}\n\n\t\t\treturn;\n\n\t\t}\n\n\t\t// custom blending\n\n\t\tblendEquationAlpha = blendEquationAlpha || blendEquation;\n\t\tblendSrcAlpha = blendSrcAlpha || blendSrc;\n\t\tblendDstAlpha = blendDstAlpha || blendDst;\n\n\t\tif ( blendEquation !== currentBlendEquation || blendEquationAlpha !== currentBlendEquationAlpha ) {\n\n\t\t\tgl.blendEquationSeparate( equationToGL[ blendEquation ], equationToGL[ blendEquationAlpha ] );\n\n\t\t\tcurrentBlendEquation = blendEquation;\n\t\t\tcurrentBlendEquationAlpha = blendEquationAlpha;\n\n\t\t}\n\n\t\tif ( blendSrc !== currentBlendSrc || blendDst !== currentBlendDst || blendSrcAlpha !== currentBlendSrcAlpha || blendDstAlpha !== currentBlendDstAlpha ) {\n\n\t\t\tgl.blendFuncSeparate( factorToGL[ blendSrc ], factorToGL[ blendDst ], factorToGL[ blendSrcAlpha ], factorToGL[ blendDstAlpha ] );\n\n\t\t\tcurrentBlendSrc = blendSrc;\n\t\t\tcurrentBlendDst = blendDst;\n\t\t\tcurrentBlendSrcAlpha = blendSrcAlpha;\n\t\t\tcurrentBlendDstAlpha = blendDstAlpha;\n\n\t\t}\n\n\t\tif ( blendColor.equals( currentBlendColor ) === false || blendAlpha !== currentBlendAlpha ) {\n\n\t\t\tgl.blendColor( blendColor.r, blendColor.g, blendColor.b, blendAlpha );\n\n\t\t\tcurrentBlendColor.copy( blendColor );\n\t\t\tcurrentBlendAlpha = blendAlpha;\n\n\t\t}\n\n\t\tcurrentBlending = blending;\n\t\tcurrentPremultipledAlpha = false;\n\n\t}\n\n\tfunction setMaterial( material, frontFaceCW ) {\n\n\t\tmaterial.side === DoubleSide\n\t\t\t? disable( gl.CULL_FACE )\n\t\t\t: enable( gl.CULL_FACE );\n\n\t\tlet flipSided = ( material.side === BackSide );\n\t\tif ( frontFaceCW ) flipSided = ! flipSided;\n\n\t\tsetFlipSided( flipSided );\n\n\t\t( material.blending === NormalBlending && material.transparent === false )\n\t\t\t? setBlending( NoBlending )\n\t\t\t: setBlending( material.blending, material.blendEquation, material.blendSrc, material.blendDst, material.blendEquationAlpha, material.blendSrcAlpha, material.blendDstAlpha, material.blendColor, material.blendAlpha, material.premultipliedAlpha );\n\n\t\tdepthBuffer.setFunc( material.depthFunc );\n\t\tdepthBuffer.setTest( material.depthTest );\n\t\tdepthBuffer.setMask( material.depthWrite );\n\t\tcolorBuffer.setMask( material.colorWrite );\n\n\t\tconst stencilWrite = material.stencilWrite;\n\t\tstencilBuffer.setTest( stencilWrite );\n\t\tif ( stencilWrite ) {\n\n\t\t\tstencilBuffer.setMask( material.stencilWriteMask );\n\t\t\tstencilBuffer.setFunc( material.stencilFunc, material.stencilRef, material.stencilFuncMask );\n\t\t\tstencilBuffer.setOp( material.stencilFail, material.stencilZFail, material.stencilZPass );\n\n\t\t}\n\n\t\tsetPolygonOffset( material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits );\n\n\t\tmaterial.alphaToCoverage === true\n\t\t\t? enable( gl.SAMPLE_ALPHA_TO_COVERAGE )\n\t\t\t: disable( gl.SAMPLE_ALPHA_TO_COVERAGE );\n\n\t}\n\n\t//\n\n\tfunction setFlipSided( flipSided ) {\n\n\t\tif ( currentFlipSided !== flipSided ) {\n\n\t\t\tif ( flipSided ) {\n\n\t\t\t\tgl.frontFace( gl.CW );\n\n\t\t\t} else {\n\n\t\t\t\tgl.frontFace( gl.CCW );\n\n\t\t\t}\n\n\t\t\tcurrentFlipSided = flipSided;\n\n\t\t}\n\n\t}\n\n\tfunction setCullFace( cullFace ) {\n\n\t\tif ( cullFace !== CullFaceNone ) {\n\n\t\t\tenable( gl.CULL_FACE );\n\n\t\t\tif ( cullFace !== currentCullFace ) {\n\n\t\t\t\tif ( cullFace === CullFaceBack ) {\n\n\t\t\t\t\tgl.cullFace( gl.BACK );\n\n\t\t\t\t} else if ( cullFace === CullFaceFront ) {\n\n\t\t\t\t\tgl.cullFace( gl.FRONT );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tgl.cullFace( gl.FRONT_AND_BACK );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tdisable( gl.CULL_FACE );\n\n\t\t}\n\n\t\tcurrentCullFace = cullFace;\n\n\t}\n\n\tfunction setLineWidth( width ) {\n\n\t\tif ( width !== currentLineWidth ) {\n\n\t\t\tif ( lineWidthAvailable ) gl.lineWidth( width );\n\n\t\t\tcurrentLineWidth = width;\n\n\t\t}\n\n\t}\n\n\tfunction setPolygonOffset( polygonOffset, factor, units ) {\n\n\t\tif ( polygonOffset ) {\n\n\t\t\tenable( gl.POLYGON_OFFSET_FILL );\n\n\t\t\tif ( currentPolygonOffsetFactor !== factor || currentPolygonOffsetUnits !== units ) {\n\n\t\t\t\tgl.polygonOffset( factor, units );\n\n\t\t\t\tcurrentPolygonOffsetFactor = factor;\n\t\t\t\tcurrentPolygonOffsetUnits = units;\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tdisable( gl.POLYGON_OFFSET_FILL );\n\n\t\t}\n\n\t}\n\n\tfunction setScissorTest( scissorTest ) {\n\n\t\tif ( scissorTest ) {\n\n\t\t\tenable( gl.SCISSOR_TEST );\n\n\t\t} else {\n\n\t\t\tdisable( gl.SCISSOR_TEST );\n\n\t\t}\n\n\t}\n\n\t// texture\n\n\tfunction activeTexture( webglSlot ) {\n\n\t\tif ( webglSlot === undefined ) webglSlot = gl.TEXTURE0 + maxTextures - 1;\n\n\t\tif ( currentTextureSlot !== webglSlot ) {\n\n\t\t\tgl.activeTexture( webglSlot );\n\t\t\tcurrentTextureSlot = webglSlot;\n\n\t\t}\n\n\t}\n\n\tfunction bindTexture( webglType, webglTexture, webglSlot ) {\n\n\t\tif ( webglSlot === undefined ) {\n\n\t\t\tif ( currentTextureSlot === null ) {\n\n\t\t\t\twebglSlot = gl.TEXTURE0 + maxTextures - 1;\n\n\t\t\t} else {\n\n\t\t\t\twebglSlot = currentTextureSlot;\n\n\t\t\t}\n\n\t\t}\n\n\t\tlet boundTexture = currentBoundTextures[ webglSlot ];\n\n\t\tif ( boundTexture === undefined ) {\n\n\t\t\tboundTexture = { type: undefined, texture: undefined };\n\t\t\tcurrentBoundTextures[ webglSlot ] = boundTexture;\n\n\t\t}\n\n\t\tif ( boundTexture.type !== webglType || boundTexture.texture !== webglTexture ) {\n\n\t\t\tif ( currentTextureSlot !== webglSlot ) {\n\n\t\t\t\tgl.activeTexture( webglSlot );\n\t\t\t\tcurrentTextureSlot = webglSlot;\n\n\t\t\t}\n\n\t\t\tgl.bindTexture( webglType, webglTexture || emptyTextures[ webglType ] );\n\n\t\t\tboundTexture.type = webglType;\n\t\t\tboundTexture.texture = webglTexture;\n\n\t\t}\n\n\t}\n\n\tfunction unbindTexture() {\n\n\t\tconst boundTexture = currentBoundTextures[ currentTextureSlot ];\n\n\t\tif ( boundTexture !== undefined && boundTexture.type !== undefined ) {\n\n\t\t\tgl.bindTexture( boundTexture.type, null );\n\n\t\t\tboundTexture.type = undefined;\n\t\t\tboundTexture.texture = undefined;\n\n\t\t}\n\n\t}\n\n\tfunction compressedTexImage2D() {\n\n\t\ttry {\n\n\t\t\tgl.compressedTexImage2D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\tfunction compressedTexImage3D() {\n\n\t\ttry {\n\n\t\t\tgl.compressedTexImage3D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\tfunction texSubImage2D() {\n\n\t\ttry {\n\n\t\t\tgl.texSubImage2D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\tfunction texSubImage3D() {\n\n\t\ttry {\n\n\t\t\tgl.texSubImage3D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\tfunction compressedTexSubImage2D() {\n\n\t\ttry {\n\n\t\t\tgl.compressedTexSubImage2D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\tfunction compressedTexSubImage3D() {\n\n\t\ttry {\n\n\t\t\tgl.compressedTexSubImage3D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\tfunction texStorage2D() {\n\n\t\ttry {\n\n\t\t\tgl.texStorage2D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\tfunction texStorage3D() {\n\n\t\ttry {\n\n\t\t\tgl.texStorage3D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\tfunction texImage2D() {\n\n\t\ttry {\n\n\t\t\tgl.texImage2D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\tfunction texImage3D() {\n\n\t\ttry {\n\n\t\t\tgl.texImage3D( ...arguments );\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLState:', e );\n\n\t\t}\n\n\t}\n\n\t//\n\n\tfunction scissor( scissor ) {\n\n\t\tif ( currentScissor.equals( scissor ) === false ) {\n\n\t\t\tgl.scissor( scissor.x, scissor.y, scissor.z, scissor.w );\n\t\t\tcurrentScissor.copy( scissor );\n\n\t\t}\n\n\t}\n\n\tfunction viewport( viewport ) {\n\n\t\tif ( currentViewport.equals( viewport ) === false ) {\n\n\t\t\tgl.viewport( viewport.x, viewport.y, viewport.z, viewport.w );\n\t\t\tcurrentViewport.copy( viewport );\n\n\t\t}\n\n\t}\n\n\tfunction updateUBOMapping( uniformsGroup, program ) {\n\n\t\tlet mapping = uboProgramMap.get( program );\n\n\t\tif ( mapping === undefined ) {\n\n\t\t\tmapping = new WeakMap();\n\n\t\t\tuboProgramMap.set( program, mapping );\n\n\t\t}\n\n\t\tlet blockIndex = mapping.get( uniformsGroup );\n\n\t\tif ( blockIndex === undefined ) {\n\n\t\t\tblockIndex = gl.getUniformBlockIndex( program, uniformsGroup.name );\n\n\t\t\tmapping.set( uniformsGroup, blockIndex );\n\n\t\t}\n\n\t}\n\n\tfunction uniformBlockBinding( uniformsGroup, program ) {\n\n\t\tconst mapping = uboProgramMap.get( program );\n\t\tconst blockIndex = mapping.get( uniformsGroup );\n\n\t\tif ( uboBindings.get( program ) !== blockIndex ) {\n\n\t\t\t// bind shader specific block index to global block point\n\t\t\tgl.uniformBlockBinding( program, blockIndex, uniformsGroup.__bindingPointIndex );\n\n\t\t\tuboBindings.set( program, blockIndex );\n\n\t\t}\n\n\t}\n\n\t//\n\n\tfunction reset() {\n\n\t\t// reset state\n\n\t\tgl.disable( gl.BLEND );\n\t\tgl.disable( gl.CULL_FACE );\n\t\tgl.disable( gl.DEPTH_TEST );\n\t\tgl.disable( gl.POLYGON_OFFSET_FILL );\n\t\tgl.disable( gl.SCISSOR_TEST );\n\t\tgl.disable( gl.STENCIL_TEST );\n\t\tgl.disable( gl.SAMPLE_ALPHA_TO_COVERAGE );\n\n\t\tgl.blendEquation( gl.FUNC_ADD );\n\t\tgl.blendFunc( gl.ONE, gl.ZERO );\n\t\tgl.blendFuncSeparate( gl.ONE, gl.ZERO, gl.ONE, gl.ZERO );\n\t\tgl.blendColor( 0, 0, 0, 0 );\n\n\t\tgl.colorMask( true, true, true, true );\n\t\tgl.clearColor( 0, 0, 0, 0 );\n\n\t\tgl.depthMask( true );\n\t\tgl.depthFunc( gl.LESS );\n\n\t\tdepthBuffer.setReversed( false );\n\n\t\tgl.clearDepth( 1 );\n\n\t\tgl.stencilMask( 0xffffffff );\n\t\tgl.stencilFunc( gl.ALWAYS, 0, 0xffffffff );\n\t\tgl.stencilOp( gl.KEEP, gl.KEEP, gl.KEEP );\n\t\tgl.clearStencil( 0 );\n\n\t\tgl.cullFace( gl.BACK );\n\t\tgl.frontFace( gl.CCW );\n\n\t\tgl.polygonOffset( 0, 0 );\n\n\t\tgl.activeTexture( gl.TEXTURE0 );\n\n\t\tgl.bindFramebuffer( gl.FRAMEBUFFER, null );\n\t\tgl.bindFramebuffer( gl.DRAW_FRAMEBUFFER, null );\n\t\tgl.bindFramebuffer( gl.READ_FRAMEBUFFER, null );\n\n\t\tgl.useProgram( null );\n\n\t\tgl.lineWidth( 1 );\n\n\t\tgl.scissor( 0, 0, gl.canvas.width, gl.canvas.height );\n\t\tgl.viewport( 0, 0, gl.canvas.width, gl.canvas.height );\n\n\t\t// reset internals\n\n\t\tenabledCapabilities = {};\n\n\t\tcurrentTextureSlot = null;\n\t\tcurrentBoundTextures = {};\n\n\t\tcurrentBoundFramebuffers = {};\n\t\tcurrentDrawbuffers = new WeakMap();\n\t\tdefaultDrawbuffers = [];\n\n\t\tcurrentProgram = null;\n\n\t\tcurrentBlendingEnabled = false;\n\t\tcurrentBlending = null;\n\t\tcurrentBlendEquation = null;\n\t\tcurrentBlendSrc = null;\n\t\tcurrentBlendDst = null;\n\t\tcurrentBlendEquationAlpha = null;\n\t\tcurrentBlendSrcAlpha = null;\n\t\tcurrentBlendDstAlpha = null;\n\t\tcurrentBlendColor = new Color( 0, 0, 0 );\n\t\tcurrentBlendAlpha = 0;\n\t\tcurrentPremultipledAlpha = false;\n\n\t\tcurrentFlipSided = null;\n\t\tcurrentCullFace = null;\n\n\t\tcurrentLineWidth = null;\n\n\t\tcurrentPolygonOffsetFactor = null;\n\t\tcurrentPolygonOffsetUnits = null;\n\n\t\tcurrentScissor.set( 0, 0, gl.canvas.width, gl.canvas.height );\n\t\tcurrentViewport.set( 0, 0, gl.canvas.width, gl.canvas.height );\n\n\t\tcolorBuffer.reset();\n\t\tdepthBuffer.reset();\n\t\tstencilBuffer.reset();\n\n\t}\n\n\treturn {\n\n\t\tbuffers: {\n\t\t\tcolor: colorBuffer,\n\t\t\tdepth: depthBuffer,\n\t\t\tstencil: stencilBuffer\n\t\t},\n\n\t\tenable: enable,\n\t\tdisable: disable,\n\n\t\tbindFramebuffer: bindFramebuffer,\n\t\tdrawBuffers: drawBuffers,\n\n\t\tuseProgram: useProgram,\n\n\t\tsetBlending: setBlending,\n\t\tsetMaterial: setMaterial,\n\n\t\tsetFlipSided: setFlipSided,\n\t\tsetCullFace: setCullFace,\n\n\t\tsetLineWidth: setLineWidth,\n\t\tsetPolygonOffset: setPolygonOffset,\n\n\t\tsetScissorTest: setScissorTest,\n\n\t\tactiveTexture: activeTexture,\n\t\tbindTexture: bindTexture,\n\t\tunbindTexture: unbindTexture,\n\t\tcompressedTexImage2D: compressedTexImage2D,\n\t\tcompressedTexImage3D: compressedTexImage3D,\n\t\ttexImage2D: texImage2D,\n\t\ttexImage3D: texImage3D,\n\n\t\tupdateUBOMapping: updateUBOMapping,\n\t\tuniformBlockBinding: uniformBlockBinding,\n\n\t\ttexStorage2D: texStorage2D,\n\t\ttexStorage3D: texStorage3D,\n\t\ttexSubImage2D: texSubImage2D,\n\t\ttexSubImage3D: texSubImage3D,\n\t\tcompressedTexSubImage2D: compressedTexSubImage2D,\n\t\tcompressedTexSubImage3D: compressedTexSubImage3D,\n\n\t\tscissor: scissor,\n\t\tviewport: viewport,\n\n\t\treset: reset\n\n\t};\n\n}\n\nfunction WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info ) {\n\n\tconst multisampledRTTExt = extensions.has( 'WEBGL_multisampled_render_to_texture' ) ? extensions.get( 'WEBGL_multisampled_render_to_texture' ) : null;\n\tconst supportsInvalidateFramebuffer = typeof navigator === 'undefined' ? false : /OculusBrowser/g.test( navigator.userAgent );\n\n\tconst _imageDimensions = new Vector2();\n\tconst _videoTextures = new WeakMap();\n\tlet _canvas;\n\n\tconst _sources = new WeakMap(); // maps WebglTexture objects to instances of Source\n\n\t// cordova iOS (as of 5.0) still uses UIWebView, which provides OffscreenCanvas,\n\t// also OffscreenCanvas.getContext(\"webgl\"), but not OffscreenCanvas.getContext(\"2d\")!\n\t// Some implementations may only implement OffscreenCanvas partially (e.g. lacking 2d).\n\n\tlet useOffscreenCanvas = false;\n\n\ttry {\n\n\t\tuseOffscreenCanvas = typeof OffscreenCanvas !== 'undefined'\n\t\t\t&& ( new OffscreenCanvas( 1, 1 ).getContext( '2d' ) ) !== null;\n\n\n\t} catch ( err ) {\n\n\t\t// Ignore any errors\n\n\t}\n\n\tfunction createCanvas( width, height ) {\n\n\t\t// Use OffscreenCanvas when available. Specially needed in web workers\n\n\t\treturn useOffscreenCanvas ?\n\t\t\tnew OffscreenCanvas( width, height ) : createElementNS( 'canvas' );\n\n\t}\n\n\tfunction resizeImage( image, needsNewCanvas, maxSize ) {\n\n\t\tlet scale = 1;\n\n\t\tconst dimensions = getDimensions( image );\n\n\t\t// handle case if texture exceeds max size\n\n\t\tif ( dimensions.width > maxSize || dimensions.height > maxSize ) {\n\n\t\t\tscale = maxSize / Math.max( dimensions.width, dimensions.height );\n\n\t\t}\n\n\t\t// only perform resize if necessary\n\n\t\tif ( scale < 1 ) {\n\n\t\t\t// only perform resize for certain image types\n\n\t\t\tif ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||\n\t\t\t\t( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||\n\t\t\t\t( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ||\n\t\t\t\t( typeof VideoFrame !== 'undefined' && image instanceof VideoFrame ) ) {\n\n\t\t\t\tconst width = Math.floor( scale * dimensions.width );\n\t\t\t\tconst height = Math.floor( scale * dimensions.height );\n\n\t\t\t\tif ( _canvas === undefined ) _canvas = createCanvas( width, height );\n\n\t\t\t\t// cube textures can't reuse the same canvas\n\n\t\t\t\tconst canvas = needsNewCanvas ? createCanvas( width, height ) : _canvas;\n\n\t\t\t\tcanvas.width = width;\n\t\t\t\tcanvas.height = height;\n\n\t\t\t\tconst context = canvas.getContext( '2d' );\n\t\t\t\tcontext.drawImage( image, 0, 0, width, height );\n\n\t\t\t\twarn( 'WebGLRenderer: Texture has been resized from (' + dimensions.width + 'x' + dimensions.height + ') to (' + width + 'x' + height + ').' );\n\n\t\t\t\treturn canvas;\n\n\t\t\t} else {\n\n\t\t\t\tif ( 'data' in image ) {\n\n\t\t\t\t\twarn( 'WebGLRenderer: Image in DataTexture is too big (' + dimensions.width + 'x' + dimensions.height + ').' );\n\n\t\t\t\t}\n\n\t\t\t\treturn image;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn image;\n\n\t}\n\n\tfunction textureNeedsGenerateMipmaps( texture ) {\n\n\t\treturn texture.generateMipmaps;\n\n\t}\n\n\tfunction generateMipmap( target ) {\n\n\t\t_gl.generateMipmap( target );\n\n\t}\n\n\tfunction getTargetType( texture ) {\n\n\t\tif ( texture.isWebGLCubeRenderTarget ) return _gl.TEXTURE_CUBE_MAP;\n\t\tif ( texture.isWebGL3DRenderTarget ) return _gl.TEXTURE_3D;\n\t\tif ( texture.isWebGLArrayRenderTarget || texture.isCompressedArrayTexture ) return _gl.TEXTURE_2D_ARRAY;\n\t\treturn _gl.TEXTURE_2D;\n\n\t}\n\n\tfunction getInternalFormat( internalFormatName, glFormat, glType, colorSpace, forceLinearTransfer = false ) {\n\n\t\tif ( internalFormatName !== null ) {\n\n\t\t\tif ( _gl[ internalFormatName ] !== undefined ) return _gl[ internalFormatName ];\n\n\t\t\twarn( 'WebGLRenderer: Attempt to use non-existing WebGL internal format \\'' + internalFormatName + '\\'' );\n\n\t\t}\n\n\t\tlet internalFormat = glFormat;\n\n\t\tif ( glFormat === _gl.RED ) {\n\n\t\t\tif ( glType === _gl.FLOAT ) internalFormat = _gl.R32F;\n\t\t\tif ( glType === _gl.HALF_FLOAT ) internalFormat = _gl.R16F;\n\t\t\tif ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.R8;\n\n\t\t}\n\n\t\tif ( glFormat === _gl.RED_INTEGER ) {\n\n\t\t\tif ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.R8UI;\n\t\t\tif ( glType === _gl.UNSIGNED_SHORT ) internalFormat = _gl.R16UI;\n\t\t\tif ( glType === _gl.UNSIGNED_INT ) internalFormat = _gl.R32UI;\n\t\t\tif ( glType === _gl.BYTE ) internalFormat = _gl.R8I;\n\t\t\tif ( glType === _gl.SHORT ) internalFormat = _gl.R16I;\n\t\t\tif ( glType === _gl.INT ) internalFormat = _gl.R32I;\n\n\t\t}\n\n\t\tif ( glFormat === _gl.RG ) {\n\n\t\t\tif ( glType === _gl.FLOAT ) internalFormat = _gl.RG32F;\n\t\t\tif ( glType === _gl.HALF_FLOAT ) internalFormat = _gl.RG16F;\n\t\t\tif ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.RG8;\n\n\t\t}\n\n\t\tif ( glFormat === _gl.RG_INTEGER ) {\n\n\t\t\tif ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.RG8UI;\n\t\t\tif ( glType === _gl.UNSIGNED_SHORT ) internalFormat = _gl.RG16UI;\n\t\t\tif ( glType === _gl.UNSIGNED_INT ) internalFormat = _gl.RG32UI;\n\t\t\tif ( glType === _gl.BYTE ) internalFormat = _gl.RG8I;\n\t\t\tif ( glType === _gl.SHORT ) internalFormat = _gl.RG16I;\n\t\t\tif ( glType === _gl.INT ) internalFormat = _gl.RG32I;\n\n\t\t}\n\n\t\tif ( glFormat === _gl.RGB_INTEGER ) {\n\n\t\t\tif ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.RGB8UI;\n\t\t\tif ( glType === _gl.UNSIGNED_SHORT ) internalFormat = _gl.RGB16UI;\n\t\t\tif ( glType === _gl.UNSIGNED_INT ) internalFormat = _gl.RGB32UI;\n\t\t\tif ( glType === _gl.BYTE ) internalFormat = _gl.RGB8I;\n\t\t\tif ( glType === _gl.SHORT ) internalFormat = _gl.RGB16I;\n\t\t\tif ( glType === _gl.INT ) internalFormat = _gl.RGB32I;\n\n\t\t}\n\n\t\tif ( glFormat === _gl.RGBA_INTEGER ) {\n\n\t\t\tif ( glType === _gl.UNSIGNED_BYTE ) internalFormat = _gl.RGBA8UI;\n\t\t\tif ( glType === _gl.UNSIGNED_SHORT ) internalFormat = _gl.RGBA16UI;\n\t\t\tif ( glType === _gl.UNSIGNED_INT ) internalFormat = _gl.RGBA32UI;\n\t\t\tif ( glType === _gl.BYTE ) internalFormat = _gl.RGBA8I;\n\t\t\tif ( glType === _gl.SHORT ) internalFormat = _gl.RGBA16I;\n\t\t\tif ( glType === _gl.INT ) internalFormat = _gl.RGBA32I;\n\n\t\t}\n\n\t\tif ( glFormat === _gl.RGB ) {\n\n\t\t\tif ( glType === _gl.UNSIGNED_INT_5_9_9_9_REV ) internalFormat = _gl.RGB9_E5;\n\t\t\tif ( glType === _gl.UNSIGNED_INT_10F_11F_11F_REV ) internalFormat = _gl.R11F_G11F_B10F;\n\n\t\t}\n\n\t\tif ( glFormat === _gl.RGBA ) {\n\n\t\t\tconst transfer = forceLinearTransfer ? LinearTransfer : ColorManagement.getTransfer( colorSpace );\n\n\t\t\tif ( glType === _gl.FLOAT ) internalFormat = _gl.RGBA32F;\n\t\t\tif ( glType === _gl.HALF_FLOAT ) internalFormat = _gl.RGBA16F;\n\t\t\tif ( glType === _gl.UNSIGNED_BYTE ) internalFormat = ( transfer === SRGBTransfer ) ? _gl.SRGB8_ALPHA8 : _gl.RGBA8;\n\t\t\tif ( glType === _gl.UNSIGNED_SHORT_4_4_4_4 ) internalFormat = _gl.RGBA4;\n\t\t\tif ( glType === _gl.UNSIGNED_SHORT_5_5_5_1 ) internalFormat = _gl.RGB5_A1;\n\n\t\t}\n\n\t\tif ( internalFormat === _gl.R16F || internalFormat === _gl.R32F ||\n\t\t\tinternalFormat === _gl.RG16F || internalFormat === _gl.RG32F ||\n\t\t\tinternalFormat === _gl.RGBA16F || internalFormat === _gl.RGBA32F ) {\n\n\t\t\textensions.get( 'EXT_color_buffer_float' );\n\n\t\t}\n\n\t\treturn internalFormat;\n\n\t}\n\n\tfunction getInternalDepthFormat( useStencil, depthType ) {\n\n\t\tlet glInternalFormat;\n\t\tif ( useStencil ) {\n\n\t\t\tif ( depthType === null || depthType === UnsignedIntType || depthType === UnsignedInt248Type ) {\n\n\t\t\t\tglInternalFormat = _gl.DEPTH24_STENCIL8;\n\n\t\t\t} else if ( depthType === FloatType ) {\n\n\t\t\t\tglInternalFormat = _gl.DEPTH32F_STENCIL8;\n\n\t\t\t} else if ( depthType === UnsignedShortType ) {\n\n\t\t\t\tglInternalFormat = _gl.DEPTH24_STENCIL8;\n\t\t\t\twarn( 'DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.' );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tif ( depthType === null || depthType === UnsignedIntType || depthType === UnsignedInt248Type ) {\n\n\t\t\t\tglInternalFormat = _gl.DEPTH_COMPONENT24;\n\n\t\t\t} else if ( depthType === FloatType ) {\n\n\t\t\t\tglInternalFormat = _gl.DEPTH_COMPONENT32F;\n\n\t\t\t} else if ( depthType === UnsignedShortType ) {\n\n\t\t\t\tglInternalFormat = _gl.DEPTH_COMPONENT16;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn glInternalFormat;\n\n\t}\n\n\tfunction getMipLevels( texture, image ) {\n\n\t\tif ( textureNeedsGenerateMipmaps( texture ) === true || ( texture.isFramebufferTexture && texture.minFilter !== NearestFilter && texture.minFilter !== LinearFilter ) ) {\n\n\t\t\treturn Math.log2( Math.max( image.width, image.height ) ) + 1;\n\n\t\t} else if ( texture.mipmaps !== undefined && texture.mipmaps.length > 0 ) {\n\n\t\t\t// user-defined mipmaps\n\n\t\t\treturn texture.mipmaps.length;\n\n\t\t} else if ( texture.isCompressedTexture && Array.isArray( texture.image ) ) {\n\n\t\t\treturn image.mipmaps.length;\n\n\t\t} else {\n\n\t\t\t// texture without mipmaps (only base level)\n\n\t\t\treturn 1;\n\n\t\t}\n\n\t}\n\n\t//\n\n\tfunction onTextureDispose( event ) {\n\n\t\tconst texture = event.target;\n\n\t\ttexture.removeEventListener( 'dispose', onTextureDispose );\n\n\t\tdeallocateTexture( texture );\n\n\t\tif ( texture.isVideoTexture ) {\n\n\t\t\t_videoTextures.delete( texture );\n\n\t\t}\n\n\t}\n\n\tfunction onRenderTargetDispose( event ) {\n\n\t\tconst renderTarget = event.target;\n\n\t\trenderTarget.removeEventListener( 'dispose', onRenderTargetDispose );\n\n\t\tdeallocateRenderTarget( renderTarget );\n\n\t}\n\n\t//\n\n\tfunction deallocateTexture( texture ) {\n\n\t\tconst textureProperties = properties.get( texture );\n\n\t\tif ( textureProperties.__webglInit === undefined ) return;\n\n\t\t// check if it's necessary to remove the WebGLTexture object\n\n\t\tconst source = texture.source;\n\t\tconst webglTextures = _sources.get( source );\n\n\t\tif ( webglTextures ) {\n\n\t\t\tconst webglTexture = webglTextures[ textureProperties.__cacheKey ];\n\t\t\twebglTexture.usedTimes --;\n\n\t\t\t// the WebGLTexture object is not used anymore, remove it\n\n\t\t\tif ( webglTexture.usedTimes === 0 ) {\n\n\t\t\t\tdeleteTexture( texture );\n\n\t\t\t}\n\n\t\t\t// remove the weak map entry if no WebGLTexture uses the source anymore\n\n\t\t\tif ( Object.keys( webglTextures ).length === 0 ) {\n\n\t\t\t\t_sources.delete( source );\n\n\t\t\t}\n\n\t\t}\n\n\t\tproperties.remove( texture );\n\n\t}\n\n\tfunction deleteTexture( texture ) {\n\n\t\tconst textureProperties = properties.get( texture );\n\t\t_gl.deleteTexture( textureProperties.__webglTexture );\n\n\t\tconst source = texture.source;\n\t\tconst webglTextures = _sources.get( source );\n\t\tdelete webglTextures[ textureProperties.__cacheKey ];\n\n\t\tinfo.memory.textures --;\n\n\t}\n\n\tfunction deallocateRenderTarget( renderTarget ) {\n\n\t\tconst renderTargetProperties = properties.get( renderTarget );\n\n\t\tif ( renderTarget.depthTexture ) {\n\n\t\t\trenderTarget.depthTexture.dispose();\n\n\t\t\tproperties.remove( renderTarget.depthTexture );\n\n\t\t}\n\n\t\tif ( renderTarget.isWebGLCubeRenderTarget ) {\n\n\t\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\t\tif ( Array.isArray( renderTargetProperties.__webglFramebuffer[ i ] ) ) {\n\n\t\t\t\t\tfor ( let level = 0; level < renderTargetProperties.__webglFramebuffer[ i ].length; level ++ ) _gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer[ i ][ level ] );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t_gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer[ i ] );\n\n\t\t\t\t}\n\n\t\t\t\tif ( renderTargetProperties.__webglDepthbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglDepthbuffer[ i ] );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tif ( Array.isArray( renderTargetProperties.__webglFramebuffer ) ) {\n\n\t\t\t\tfor ( let level = 0; level < renderTargetProperties.__webglFramebuffer.length; level ++ ) _gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer[ level ] );\n\n\t\t\t} else {\n\n\t\t\t\t_gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer );\n\n\t\t\t}\n\n\t\t\tif ( renderTargetProperties.__webglDepthbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglDepthbuffer );\n\t\t\tif ( renderTargetProperties.__webglMultisampledFramebuffer ) _gl.deleteFramebuffer( renderTargetProperties.__webglMultisampledFramebuffer );\n\n\t\t\tif ( renderTargetProperties.__webglColorRenderbuffer ) {\n\n\t\t\t\tfor ( let i = 0; i < renderTargetProperties.__webglColorRenderbuffer.length; i ++ ) {\n\n\t\t\t\t\tif ( renderTargetProperties.__webglColorRenderbuffer[ i ] ) _gl.deleteRenderbuffer( renderTargetProperties.__webglColorRenderbuffer[ i ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( renderTargetProperties.__webglDepthRenderbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglDepthRenderbuffer );\n\n\t\t}\n\n\t\tconst textures = renderTarget.textures;\n\n\t\tfor ( let i = 0, il = textures.length; i < il; i ++ ) {\n\n\t\t\tconst attachmentProperties = properties.get( textures[ i ] );\n\n\t\t\tif ( attachmentProperties.__webglTexture ) {\n\n\t\t\t\t_gl.deleteTexture( attachmentProperties.__webglTexture );\n\n\t\t\t\tinfo.memory.textures --;\n\n\t\t\t}\n\n\t\t\tproperties.remove( textures[ i ] );\n\n\t\t}\n\n\t\tproperties.remove( renderTarget );\n\n\t}\n\n\t//\n\n\tlet textureUnits = 0;\n\n\tfunction resetTextureUnits() {\n\n\t\ttextureUnits = 0;\n\n\t}\n\n\tfunction allocateTextureUnit() {\n\n\t\tconst textureUnit = textureUnits;\n\n\t\tif ( textureUnit >= capabilities.maxTextures ) {\n\n\t\t\twarn( 'WebGLTextures: Trying to use ' + textureUnit + ' texture units while this GPU supports only ' + capabilities.maxTextures );\n\n\t\t}\n\n\t\ttextureUnits += 1;\n\n\t\treturn textureUnit;\n\n\t}\n\n\tfunction getTextureCacheKey( texture ) {\n\n\t\tconst array = [];\n\n\t\tarray.push( texture.wrapS );\n\t\tarray.push( texture.wrapT );\n\t\tarray.push( texture.wrapR || 0 );\n\t\tarray.push( texture.magFilter );\n\t\tarray.push( texture.minFilter );\n\t\tarray.push( texture.anisotropy );\n\t\tarray.push( texture.internalFormat );\n\t\tarray.push( texture.format );\n\t\tarray.push( texture.type );\n\t\tarray.push( texture.generateMipmaps );\n\t\tarray.push( texture.premultiplyAlpha );\n\t\tarray.push( texture.flipY );\n\t\tarray.push( texture.unpackAlignment );\n\t\tarray.push( texture.colorSpace );\n\n\t\treturn array.join();\n\n\t}\n\n\t//\n\n\tfunction setTexture2D( texture, slot ) {\n\n\t\tconst textureProperties = properties.get( texture );\n\n\t\tif ( texture.isVideoTexture ) updateVideoTexture( texture );\n\n\t\tif ( texture.isRenderTargetTexture === false && texture.isExternalTexture !== true && texture.version > 0 && textureProperties.__version !== texture.version ) {\n\n\t\t\tconst image = texture.image;\n\n\t\t\tif ( image === null ) {\n\n\t\t\t\twarn( 'WebGLRenderer: Texture marked for update but no image data found.' );\n\n\t\t\t} else if ( image.complete === false ) {\n\n\t\t\t\twarn( 'WebGLRenderer: Texture marked for update but image is incomplete' );\n\n\t\t\t} else {\n\n\t\t\t\tuploadTexture( textureProperties, texture, slot );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t} else if ( texture.isExternalTexture ) {\n\n\t\t\ttextureProperties.__webglTexture = texture.sourceTexture ? texture.sourceTexture : null;\n\n\t\t}\n\n\t\tstate.bindTexture( _gl.TEXTURE_2D, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );\n\n\t}\n\n\tfunction setTexture2DArray( texture, slot ) {\n\n\t\tconst textureProperties = properties.get( texture );\n\n\t\tif ( texture.isRenderTargetTexture === false && texture.version > 0 && textureProperties.__version !== texture.version ) {\n\n\t\t\tuploadTexture( textureProperties, texture, slot );\n\t\t\treturn;\n\n\t\t} else if ( texture.isExternalTexture ) {\n\n\t\t\ttextureProperties.__webglTexture = texture.sourceTexture ? texture.sourceTexture : null;\n\n\t\t}\n\n\t\tstate.bindTexture( _gl.TEXTURE_2D_ARRAY, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );\n\n\t}\n\n\tfunction setTexture3D( texture, slot ) {\n\n\t\tconst textureProperties = properties.get( texture );\n\n\t\tif ( texture.isRenderTargetTexture === false && texture.version > 0 && textureProperties.__version !== texture.version ) {\n\n\t\t\tuploadTexture( textureProperties, texture, slot );\n\t\t\treturn;\n\n\t\t}\n\n\t\tstate.bindTexture( _gl.TEXTURE_3D, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );\n\n\t}\n\n\tfunction setTextureCube( texture, slot ) {\n\n\t\tconst textureProperties = properties.get( texture );\n\n\t\tif ( texture.isCubeDepthTexture !== true && texture.version > 0 && textureProperties.__version !== texture.version ) {\n\n\t\t\tuploadCubeTexture( textureProperties, texture, slot );\n\t\t\treturn;\n\n\t\t}\n\n\t\tstate.bindTexture( _gl.TEXTURE_CUBE_MAP, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );\n\n\t}\n\n\tconst wrappingToGL = {\n\t\t[ RepeatWrapping ]: _gl.REPEAT,\n\t\t[ ClampToEdgeWrapping ]: _gl.CLAMP_TO_EDGE,\n\t\t[ MirroredRepeatWrapping ]: _gl.MIRRORED_REPEAT\n\t};\n\n\tconst filterToGL = {\n\t\t[ NearestFilter ]: _gl.NEAREST,\n\t\t[ NearestMipmapNearestFilter ]: _gl.NEAREST_MIPMAP_NEAREST,\n\t\t[ NearestMipmapLinearFilter ]: _gl.NEAREST_MIPMAP_LINEAR,\n\n\t\t[ LinearFilter ]: _gl.LINEAR,\n\t\t[ LinearMipmapNearestFilter ]: _gl.LINEAR_MIPMAP_NEAREST,\n\t\t[ LinearMipmapLinearFilter ]: _gl.LINEAR_MIPMAP_LINEAR\n\t};\n\n\tconst compareToGL = {\n\t\t[ NeverCompare ]: _gl.NEVER,\n\t\t[ AlwaysCompare ]: _gl.ALWAYS,\n\t\t[ LessCompare ]: _gl.LESS,\n\t\t[ LessEqualCompare ]: _gl.LEQUAL,\n\t\t[ EqualCompare ]: _gl.EQUAL,\n\t\t[ GreaterEqualCompare ]: _gl.GEQUAL,\n\t\t[ GreaterCompare ]: _gl.GREATER,\n\t\t[ NotEqualCompare ]: _gl.NOTEQUAL\n\t};\n\n\tfunction setTextureParameters( textureType, texture ) {\n\n\t\tif ( texture.type === FloatType && extensions.has( 'OES_texture_float_linear' ) === false &&\n\t\t\t( texture.magFilter === LinearFilter || texture.magFilter === LinearMipmapNearestFilter || texture.magFilter === NearestMipmapLinearFilter || texture.magFilter === LinearMipmapLinearFilter ||\n\t\t\ttexture.minFilter === LinearFilter || texture.minFilter === LinearMipmapNearestFilter || texture.minFilter === NearestMipmapLinearFilter || texture.minFilter === LinearMipmapLinearFilter ) ) {\n\n\t\t\twarn( 'WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device.' );\n\n\t\t}\n\n\t\t_gl.texParameteri( textureType, _gl.TEXTURE_WRAP_S, wrappingToGL[ texture.wrapS ] );\n\t\t_gl.texParameteri( textureType, _gl.TEXTURE_WRAP_T, wrappingToGL[ texture.wrapT ] );\n\n\t\tif ( textureType === _gl.TEXTURE_3D || textureType === _gl.TEXTURE_2D_ARRAY ) {\n\n\t\t\t_gl.texParameteri( textureType, _gl.TEXTURE_WRAP_R, wrappingToGL[ texture.wrapR ] );\n\n\t\t}\n\n\t\t_gl.texParameteri( textureType, _gl.TEXTURE_MAG_FILTER, filterToGL[ texture.magFilter ] );\n\t\t_gl.texParameteri( textureType, _gl.TEXTURE_MIN_FILTER, filterToGL[ texture.minFilter ] );\n\n\t\tif ( texture.compareFunction ) {\n\n\t\t\t_gl.texParameteri( textureType, _gl.TEXTURE_COMPARE_MODE, _gl.COMPARE_REF_TO_TEXTURE );\n\t\t\t_gl.texParameteri( textureType, _gl.TEXTURE_COMPARE_FUNC, compareToGL[ texture.compareFunction ] );\n\n\t\t}\n\n\t\tif ( extensions.has( 'EXT_texture_filter_anisotropic' ) === true ) {\n\n\t\t\tif ( texture.magFilter === NearestFilter ) return;\n\t\t\tif ( texture.minFilter !== NearestMipmapLinearFilter && texture.minFilter !== LinearMipmapLinearFilter ) return;\n\t\t\tif ( texture.type === FloatType && extensions.has( 'OES_texture_float_linear' ) === false ) return; // verify extension\n\n\t\t\tif ( texture.anisotropy > 1 || properties.get( texture ).__currentAnisotropy ) {\n\n\t\t\t\tconst extension = extensions.get( 'EXT_texture_filter_anisotropic' );\n\t\t\t\t_gl.texParameterf( textureType, extension.TEXTURE_MAX_ANISOTROPY_EXT, Math.min( texture.anisotropy, capabilities.getMaxAnisotropy() ) );\n\t\t\t\tproperties.get( texture ).__currentAnisotropy = texture.anisotropy;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tfunction initTexture( textureProperties, texture ) {\n\n\t\tlet forceUpload = false;\n\n\t\tif ( textureProperties.__webglInit === undefined ) {\n\n\t\t\ttextureProperties.__webglInit = true;\n\n\t\t\ttexture.addEventListener( 'dispose', onTextureDispose );\n\n\t\t}\n\n\t\t// create Source <-> WebGLTextures mapping if necessary\n\n\t\tconst source = texture.source;\n\t\tlet webglTextures = _sources.get( source );\n\n\t\tif ( webglTextures === undefined ) {\n\n\t\t\twebglTextures = {};\n\t\t\t_sources.set( source, webglTextures );\n\n\t\t}\n\n\t\t// check if there is already a WebGLTexture object for the given texture parameters\n\n\t\tconst textureCacheKey = getTextureCacheKey( texture );\n\n\t\tif ( textureCacheKey !== textureProperties.__cacheKey ) {\n\n\t\t\t// if not, create a new instance of WebGLTexture\n\n\t\t\tif ( webglTextures[ textureCacheKey ] === undefined ) {\n\n\t\t\t\t// create new entry\n\n\t\t\t\twebglTextures[ textureCacheKey ] = {\n\t\t\t\t\ttexture: _gl.createTexture(),\n\t\t\t\t\tusedTimes: 0\n\t\t\t\t};\n\n\t\t\t\tinfo.memory.textures ++;\n\n\t\t\t\t// when a new instance of WebGLTexture was created, a texture upload is required\n\t\t\t\t// even if the image contents are identical\n\n\t\t\t\tforceUpload = true;\n\n\t\t\t}\n\n\t\t\twebglTextures[ textureCacheKey ].usedTimes ++;\n\n\t\t\t// every time the texture cache key changes, it's necessary to check if an instance of\n\t\t\t// WebGLTexture can be deleted in order to avoid a memory leak.\n\n\t\t\tconst webglTexture = webglTextures[ textureProperties.__cacheKey ];\n\n\t\t\tif ( webglTexture !== undefined ) {\n\n\t\t\t\twebglTextures[ textureProperties.__cacheKey ].usedTimes --;\n\n\t\t\t\tif ( webglTexture.usedTimes === 0 ) {\n\n\t\t\t\t\tdeleteTexture( texture );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// store references to cache key and WebGLTexture object\n\n\t\t\ttextureProperties.__cacheKey = textureCacheKey;\n\t\t\ttextureProperties.__webglTexture = webglTextures[ textureCacheKey ].texture;\n\n\t\t}\n\n\t\treturn forceUpload;\n\n\t}\n\n\tfunction getRow( index, rowLength, componentStride ) {\n\n\t\treturn Math.floor( Math.floor( index / componentStride ) / rowLength );\n\n\t}\n\n\tfunction updateTexture( texture, image, glFormat, glType ) {\n\n\t\tconst componentStride = 4; // only RGBA supported\n\n\t\tconst updateRanges = texture.updateRanges;\n\n\t\tif ( updateRanges.length === 0 ) {\n\n\t\t\tstate.texSubImage2D( _gl.TEXTURE_2D, 0, 0, 0, image.width, image.height, glFormat, glType, image.data );\n\n\t\t} else {\n\n\t\t\t// Before applying update ranges, we merge any adjacent / overlapping\n\t\t\t// ranges to reduce load on `gl.texSubImage2D`. Empirically, this has led\n\t\t\t// to performance improvements for applications which make heavy use of\n\t\t\t// update ranges. Likely due to GPU command overhead.\n\t\t\t//\n\t\t\t// Note that to reduce garbage collection between frames, we merge the\n\t\t\t// update ranges in-place. This is safe because this method will clear the\n\t\t\t// update ranges once updated.\n\n\t\t\tupdateRanges.sort( ( a, b ) => a.start - b.start );\n\n\t\t\t// To merge the update ranges in-place, we work from left to right in the\n\t\t\t// existing updateRanges array, merging ranges. This may result in a final\n\t\t\t// array which is smaller than the original. This index tracks the last\n\t\t\t// index representing a merged range, any data after this index can be\n\t\t\t// trimmed once the merge algorithm is completed.\n\t\t\tlet mergeIndex = 0;\n\n\t\t\tfor ( let i = 1; i < updateRanges.length; i ++ ) {\n\n\t\t\t\tconst previousRange = updateRanges[ mergeIndex ];\n\t\t\t\tconst range = updateRanges[ i ];\n\n\t\t\t\t// Only merge if in the same row and overlapping/adjacent\n\t\t\t\tconst previousEnd = previousRange.start + previousRange.count;\n\t\t\t\tconst currentRow = getRow( range.start, image.width, componentStride );\n\t\t\t\tconst previousRow = getRow( previousRange.start, image.width, componentStride );\n\n\t\t\t\t// We add one here to merge adjacent ranges. This is safe because ranges\n\t\t\t\t// operate over positive integers.\n\t\t\t\tif (\n\t\t\t\t\trange.start <= previousEnd + 1 &&\n\t\t\t\t\tcurrentRow === previousRow &&\n\t\t\t\t\tgetRow( range.start + range.count - 1, image.width, componentStride ) === currentRow // ensure range doesn't spill\n\t\t\t\t) {\n\n\t\t\t\t\tpreviousRange.count = Math.max(\n\t\t\t\t\t\tpreviousRange.count,\n\t\t\t\t\t\trange.start + range.count - previousRange.start\n\t\t\t\t\t);\n\n\t\t\t\t} else {\n\n\t\t\t\t\t++ mergeIndex;\n\t\t\t\t\tupdateRanges[ mergeIndex ] = range;\n\n\t\t\t\t}\n\n\n\t\t\t}\n\n\t\t\t// Trim the array to only contain the merged ranges.\n\t\t\tupdateRanges.length = mergeIndex + 1;\n\n\t\t\tconst currentUnpackRowLen = _gl.getParameter( _gl.UNPACK_ROW_LENGTH );\n\t\t\tconst currentUnpackSkipPixels = _gl.getParameter( _gl.UNPACK_SKIP_PIXELS );\n\t\t\tconst currentUnpackSkipRows = _gl.getParameter( _gl.UNPACK_SKIP_ROWS );\n\n\t\t\t_gl.pixelStorei( _gl.UNPACK_ROW_LENGTH, image.width );\n\n\t\t\tfor ( let i = 0, l = updateRanges.length; i < l; i ++ ) {\n\n\t\t\t\tconst range = updateRanges[ i ];\n\n\t\t\t\tconst pixelStart = Math.floor( range.start / componentStride );\n\t\t\t\tconst pixelCount = Math.ceil( range.count / componentStride );\n\n\t\t\t\tconst x = pixelStart % image.width;\n\t\t\t\tconst y = Math.floor( pixelStart / image.width );\n\n\t\t\t\t// Assumes update ranges refer to contiguous memory\n\t\t\t\tconst width = pixelCount;\n\t\t\t\tconst height = 1;\n\n\t\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_PIXELS, x );\n\t\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_ROWS, y );\n\n\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_2D, 0, x, y, width, height, glFormat, glType, image.data );\n\n\t\t\t}\n\n\t\t\ttexture.clearUpdateRanges();\n\n\t\t\t_gl.pixelStorei( _gl.UNPACK_ROW_LENGTH, currentUnpackRowLen );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_PIXELS, currentUnpackSkipPixels );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_ROWS, currentUnpackSkipRows );\n\n\t\t}\n\n\t}\n\n\tfunction uploadTexture( textureProperties, texture, slot ) {\n\n\t\tlet textureType = _gl.TEXTURE_2D;\n\n\t\tif ( texture.isDataArrayTexture || texture.isCompressedArrayTexture ) textureType = _gl.TEXTURE_2D_ARRAY;\n\t\tif ( texture.isData3DTexture ) textureType = _gl.TEXTURE_3D;\n\n\t\tconst forceUpload = initTexture( textureProperties, texture );\n\t\tconst source = texture.source;\n\n\t\tstate.bindTexture( textureType, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );\n\n\t\tconst sourceProperties = properties.get( source );\n\n\t\tif ( source.version !== sourceProperties.__version || forceUpload === true ) {\n\n\t\t\tstate.activeTexture( _gl.TEXTURE0 + slot );\n\n\t\t\tconst workingPrimaries = ColorManagement.getPrimaries( ColorManagement.workingColorSpace );\n\t\t\tconst texturePrimaries = texture.colorSpace === NoColorSpace ? null : ColorManagement.getPrimaries( texture.colorSpace );\n\t\t\tconst unpackConversion = texture.colorSpace === NoColorSpace || workingPrimaries === texturePrimaries ? _gl.NONE : _gl.BROWSER_DEFAULT_WEBGL;\n\n\t\t\t_gl.pixelStorei( _gl.UNPACK_FLIP_Y_WEBGL, texture.flipY );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_ALIGNMENT, texture.unpackAlignment );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, unpackConversion );\n\n\t\t\tlet image = resizeImage( texture.image, false, capabilities.maxTextureSize );\n\t\t\timage = verifyColorSpace( texture, image );\n\n\t\t\tconst glFormat = utils.convert( texture.format, texture.colorSpace );\n\n\t\t\tconst glType = utils.convert( texture.type );\n\t\t\tlet glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace, texture.isVideoTexture );\n\n\t\t\tsetTextureParameters( textureType, texture );\n\n\t\t\tlet mipmap;\n\t\t\tconst mipmaps = texture.mipmaps;\n\n\t\t\tconst useTexStorage = ( texture.isVideoTexture !== true );\n\t\t\tconst allocateMemory = ( sourceProperties.__version === undefined ) || ( forceUpload === true );\n\t\t\tconst dataReady = source.dataReady;\n\t\t\tconst levels = getMipLevels( texture, image );\n\n\t\t\tif ( texture.isDepthTexture ) {\n\n\t\t\t\tglInternalFormat = getInternalDepthFormat( texture.format === DepthStencilFormat, texture.type );\n\n\t\t\t\t//\n\n\t\t\t\tif ( allocateMemory ) {\n\n\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\tstate.texStorage2D( _gl.TEXTURE_2D, 1, glInternalFormat, image.width, image.height );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_2D, 0, glInternalFormat, image.width, image.height, 0, glFormat, glType, null );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else if ( texture.isDataTexture ) {\n\n\t\t\t\t// use manually created mipmaps if available\n\t\t\t\t// if there are no manual mipmaps\n\t\t\t\t// set 0 level mipmap and then use GL to generate other mipmap levels\n\n\t\t\t\tif ( mipmaps.length > 0 ) {\n\n\t\t\t\t\tif ( useTexStorage && allocateMemory ) {\n\n\t\t\t\t\t\tstate.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, mipmaps[ 0 ].width, mipmaps[ 0 ].height );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfor ( let i = 0, il = mipmaps.length; i < il; i ++ ) {\n\n\t\t\t\t\t\tmipmap = mipmaps[ i ];\n\n\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_2D, i, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\ttexture.generateMipmaps = false;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\tif ( allocateMemory ) {\n\n\t\t\t\t\t\t\tstate.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, image.width, image.height );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\tupdateTexture( texture, image, glFormat, glType );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_2D, 0, glInternalFormat, image.width, image.height, 0, glFormat, glType, image.data );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else if ( texture.isCompressedTexture ) {\n\n\t\t\t\tif ( texture.isCompressedArrayTexture ) {\n\n\t\t\t\t\tif ( useTexStorage && allocateMemory ) {\n\n\t\t\t\t\t\tstate.texStorage3D( _gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, mipmaps[ 0 ].width, mipmaps[ 0 ].height, image.depth );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfor ( let i = 0, il = mipmaps.length; i < il; i ++ ) {\n\n\t\t\t\t\t\tmipmap = mipmaps[ i ];\n\n\t\t\t\t\t\tif ( texture.format !== RGBAFormat ) {\n\n\t\t\t\t\t\t\tif ( glFormat !== null ) {\n\n\t\t\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\t\t\tif ( texture.layerUpdates.size > 0 ) {\n\n\t\t\t\t\t\t\t\t\t\t\tconst layerByteLength = getByteLength( mipmap.width, mipmap.height, texture.format, texture.type );\n\n\t\t\t\t\t\t\t\t\t\t\tfor ( const layerIndex of texture.layerUpdates ) {\n\n\t\t\t\t\t\t\t\t\t\t\t\tconst layerData = mipmap.data.subarray(\n\t\t\t\t\t\t\t\t\t\t\t\t\tlayerIndex * layerByteLength / mipmap.data.BYTES_PER_ELEMENT,\n\t\t\t\t\t\t\t\t\t\t\t\t\t( layerIndex + 1 ) * layerByteLength / mipmap.data.BYTES_PER_ELEMENT\n\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\tstate.compressedTexSubImage3D( _gl.TEXTURE_2D_ARRAY, i, 0, 0, layerIndex, mipmap.width, mipmap.height, 1, glFormat, layerData );\n\n\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\ttexture.clearLayerUpdates();\n\n\t\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t\tstate.compressedTexSubImage3D( _gl.TEXTURE_2D_ARRAY, i, 0, 0, 0, mipmap.width, mipmap.height, image.depth, glFormat, mipmap.data );\n\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\tstate.compressedTexImage3D( _gl.TEXTURE_2D_ARRAY, i, glInternalFormat, mipmap.width, mipmap.height, image.depth, 0, mipmap.data, 0, 0 );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\twarn( 'WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()' );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\t\tstate.texSubImage3D( _gl.TEXTURE_2D_ARRAY, i, 0, 0, 0, mipmap.width, mipmap.height, image.depth, glFormat, glType, mipmap.data );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\tstate.texImage3D( _gl.TEXTURE_2D_ARRAY, i, glInternalFormat, mipmap.width, mipmap.height, image.depth, 0, glFormat, glType, mipmap.data );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( useTexStorage && allocateMemory ) {\n\n\t\t\t\t\t\tstate.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, mipmaps[ 0 ].width, mipmaps[ 0 ].height );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfor ( let i = 0, il = mipmaps.length; i < il; i ++ ) {\n\n\t\t\t\t\t\tmipmap = mipmaps[ i ];\n\n\t\t\t\t\t\tif ( texture.format !== RGBAFormat ) {\n\n\t\t\t\t\t\t\tif ( glFormat !== null ) {\n\n\t\t\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\t\t\tstate.compressedTexSubImage2D( _gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, mipmap.data );\n\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\tstate.compressedTexImage2D( _gl.TEXTURE_2D, i, glInternalFormat, mipmap.width, mipmap.height, 0, mipmap.data );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\twarn( 'WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()' );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_2D, i, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else if ( texture.isDataArrayTexture ) {\n\n\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\tif ( allocateMemory ) {\n\n\t\t\t\t\t\tstate.texStorage3D( _gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, image.width, image.height, image.depth );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\tif ( texture.layerUpdates.size > 0 ) {\n\n\t\t\t\t\t\t\tconst layerByteLength = getByteLength( image.width, image.height, texture.format, texture.type );\n\n\t\t\t\t\t\t\tfor ( const layerIndex of texture.layerUpdates ) {\n\n\t\t\t\t\t\t\t\tconst layerData = image.data.subarray(\n\t\t\t\t\t\t\t\t\tlayerIndex * layerByteLength / image.data.BYTES_PER_ELEMENT,\n\t\t\t\t\t\t\t\t\t( layerIndex + 1 ) * layerByteLength / image.data.BYTES_PER_ELEMENT\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tstate.texSubImage3D( _gl.TEXTURE_2D_ARRAY, 0, 0, 0, layerIndex, image.width, image.height, 1, glFormat, glType, layerData );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttexture.clearLayerUpdates();\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tstate.texSubImage3D( _gl.TEXTURE_2D_ARRAY, 0, 0, 0, 0, image.width, image.height, image.depth, glFormat, glType, image.data );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tstate.texImage3D( _gl.TEXTURE_2D_ARRAY, 0, glInternalFormat, image.width, image.height, image.depth, 0, glFormat, glType, image.data );\n\n\t\t\t\t}\n\n\t\t\t} else if ( texture.isData3DTexture ) {\n\n\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\tif ( allocateMemory ) {\n\n\t\t\t\t\t\tstate.texStorage3D( _gl.TEXTURE_3D, levels, glInternalFormat, image.width, image.height, image.depth );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\tstate.texSubImage3D( _gl.TEXTURE_3D, 0, 0, 0, 0, image.width, image.height, image.depth, glFormat, glType, image.data );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tstate.texImage3D( _gl.TEXTURE_3D, 0, glInternalFormat, image.width, image.height, image.depth, 0, glFormat, glType, image.data );\n\n\t\t\t\t}\n\n\t\t\t} else if ( texture.isFramebufferTexture ) {\n\n\t\t\t\tif ( allocateMemory ) {\n\n\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\tstate.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, image.width, image.height );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tlet width = image.width, height = image.height;\n\n\t\t\t\t\t\tfor ( let i = 0; i < levels; i ++ ) {\n\n\t\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_2D, i, glInternalFormat, width, height, 0, glFormat, glType, null );\n\n\t\t\t\t\t\t\twidth >>= 1;\n\t\t\t\t\t\t\theight >>= 1;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\t// regular Texture (image, video, canvas)\n\n\t\t\t\t// use manually created mipmaps if available\n\t\t\t\t// if there are no manual mipmaps\n\t\t\t\t// set 0 level mipmap and then use GL to generate other mipmap levels\n\n\t\t\t\tif ( mipmaps.length > 0 ) {\n\n\t\t\t\t\tif ( useTexStorage && allocateMemory ) {\n\n\t\t\t\t\t\tconst dimensions = getDimensions( mipmaps[ 0 ] );\n\n\t\t\t\t\t\tstate.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, dimensions.width, dimensions.height );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfor ( let i = 0, il = mipmaps.length; i < il; i ++ ) {\n\n\t\t\t\t\t\tmipmap = mipmaps[ i ];\n\n\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_2D, i, 0, 0, glFormat, glType, mipmap );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_2D, i, glInternalFormat, glFormat, glType, mipmap );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\ttexture.generateMipmaps = false;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\tif ( allocateMemory ) {\n\n\t\t\t\t\t\t\tconst dimensions = getDimensions( image );\n\n\t\t\t\t\t\t\tstate.texStorage2D( _gl.TEXTURE_2D, levels, glInternalFormat, dimensions.width, dimensions.height );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_2D, 0, 0, 0, glFormat, glType, image );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_2D, 0, glInternalFormat, glFormat, glType, image );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( textureNeedsGenerateMipmaps( texture ) ) {\n\n\t\t\t\tgenerateMipmap( textureType );\n\n\t\t\t}\n\n\t\t\tsourceProperties.__version = source.version;\n\n\t\t\tif ( texture.onUpdate ) texture.onUpdate( texture );\n\n\t\t}\n\n\t\ttextureProperties.__version = texture.version;\n\n\t}\n\n\tfunction uploadCubeTexture( textureProperties, texture, slot ) {\n\n\t\tif ( texture.image.length !== 6 ) return;\n\n\t\tconst forceUpload = initTexture( textureProperties, texture );\n\t\tconst source = texture.source;\n\n\t\tstate.bindTexture( _gl.TEXTURE_CUBE_MAP, textureProperties.__webglTexture, _gl.TEXTURE0 + slot );\n\n\t\tconst sourceProperties = properties.get( source );\n\n\t\tif ( source.version !== sourceProperties.__version || forceUpload === true ) {\n\n\t\t\tstate.activeTexture( _gl.TEXTURE0 + slot );\n\n\t\t\tconst workingPrimaries = ColorManagement.getPrimaries( ColorManagement.workingColorSpace );\n\t\t\tconst texturePrimaries = texture.colorSpace === NoColorSpace ? null : ColorManagement.getPrimaries( texture.colorSpace );\n\t\t\tconst unpackConversion = texture.colorSpace === NoColorSpace || workingPrimaries === texturePrimaries ? _gl.NONE : _gl.BROWSER_DEFAULT_WEBGL;\n\n\t\t\t_gl.pixelStorei( _gl.UNPACK_FLIP_Y_WEBGL, texture.flipY );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_ALIGNMENT, texture.unpackAlignment );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, unpackConversion );\n\n\t\t\tconst isCompressed = ( texture.isCompressedTexture || texture.image[ 0 ].isCompressedTexture );\n\t\t\tconst isDataTexture = ( texture.image[ 0 ] && texture.image[ 0 ].isDataTexture );\n\n\t\t\tconst cubeImage = [];\n\n\t\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\t\tif ( ! isCompressed && ! isDataTexture ) {\n\n\t\t\t\t\tcubeImage[ i ] = resizeImage( texture.image[ i ], true, capabilities.maxCubemapSize );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tcubeImage[ i ] = isDataTexture ? texture.image[ i ].image : texture.image[ i ];\n\n\t\t\t\t}\n\n\t\t\t\tcubeImage[ i ] = verifyColorSpace( texture, cubeImage[ i ] );\n\n\t\t\t}\n\n\t\t\tconst image = cubeImage[ 0 ],\n\t\t\t\tglFormat = utils.convert( texture.format, texture.colorSpace ),\n\t\t\t\tglType = utils.convert( texture.type ),\n\t\t\t\tglInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace );\n\n\t\t\tconst useTexStorage = ( texture.isVideoTexture !== true );\n\t\t\tconst allocateMemory = ( sourceProperties.__version === undefined ) || ( forceUpload === true );\n\t\t\tconst dataReady = source.dataReady;\n\t\t\tlet levels = getMipLevels( texture, image );\n\n\t\t\tsetTextureParameters( _gl.TEXTURE_CUBE_MAP, texture );\n\n\t\t\tlet mipmaps;\n\n\t\t\tif ( isCompressed ) {\n\n\t\t\t\tif ( useTexStorage && allocateMemory ) {\n\n\t\t\t\t\tstate.texStorage2D( _gl.TEXTURE_CUBE_MAP, levels, glInternalFormat, image.width, image.height );\n\n\t\t\t\t}\n\n\t\t\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\t\t\tmipmaps = cubeImage[ i ].mipmaps;\n\n\t\t\t\t\tfor ( let j = 0; j < mipmaps.length; j ++ ) {\n\n\t\t\t\t\t\tconst mipmap = mipmaps[ j ];\n\n\t\t\t\t\t\tif ( texture.format !== RGBAFormat ) {\n\n\t\t\t\t\t\t\tif ( glFormat !== null ) {\n\n\t\t\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\t\t\tstate.compressedTexSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, 0, 0, mipmap.width, mipmap.height, glFormat, mipmap.data );\n\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\tstate.compressedTexImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, glInternalFormat, mipmap.width, mipmap.height, 0, mipmap.data );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\twarn( 'WebGLRenderer: Attempt to load unsupported compressed texture format in .setTextureCube()' );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tmipmaps = texture.mipmaps;\n\n\t\t\t\tif ( useTexStorage && allocateMemory ) {\n\n\t\t\t\t\t// TODO: Uniformly handle mipmap definitions\n\t\t\t\t\t// Normal textures and compressed cube textures define base level + mips with their mipmap array\n\t\t\t\t\t// Uncompressed cube textures use their mipmap array only for mips (no base level)\n\n\t\t\t\t\tif ( mipmaps.length > 0 ) levels ++;\n\n\t\t\t\t\tconst dimensions = getDimensions( cubeImage[ 0 ] );\n\n\t\t\t\t\tstate.texStorage2D( _gl.TEXTURE_CUBE_MAP, levels, glInternalFormat, dimensions.width, dimensions.height );\n\n\t\t\t\t}\n\n\t\t\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\t\t\tif ( isDataTexture ) {\n\n\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, 0, 0, cubeImage[ i ].width, cubeImage[ i ].height, glFormat, glType, cubeImage[ i ].data );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, glInternalFormat, cubeImage[ i ].width, cubeImage[ i ].height, 0, glFormat, glType, cubeImage[ i ].data );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor ( let j = 0; j < mipmaps.length; j ++ ) {\n\n\t\t\t\t\t\t\tconst mipmap = mipmaps[ j ];\n\t\t\t\t\t\t\tconst mipmapImage = mipmap.image[ i ].image;\n\n\t\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j + 1, 0, 0, mipmapImage.width, mipmapImage.height, glFormat, glType, mipmapImage.data );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j + 1, glInternalFormat, mipmapImage.width, mipmapImage.height, 0, glFormat, glType, mipmapImage.data );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, 0, 0, glFormat, glType, cubeImage[ i ] );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, glInternalFormat, glFormat, glType, cubeImage[ i ] );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor ( let j = 0; j < mipmaps.length; j ++ ) {\n\n\t\t\t\t\t\t\tconst mipmap = mipmaps[ j ];\n\n\t\t\t\t\t\t\tif ( useTexStorage ) {\n\n\t\t\t\t\t\t\t\tif ( dataReady ) {\n\n\t\t\t\t\t\t\t\t\tstate.texSubImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j + 1, 0, 0, glFormat, glType, mipmap.image[ i ] );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\tstate.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j + 1, glInternalFormat, glFormat, glType, mipmap.image[ i ] );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( textureNeedsGenerateMipmaps( texture ) ) {\n\n\t\t\t\t// We assume images for cube map have the same size.\n\t\t\t\tgenerateMipmap( _gl.TEXTURE_CUBE_MAP );\n\n\t\t\t}\n\n\t\t\tsourceProperties.__version = source.version;\n\n\t\t\tif ( texture.onUpdate ) texture.onUpdate( texture );\n\n\t\t}\n\n\t\ttextureProperties.__version = texture.version;\n\n\t}\n\n\t// Render targets\n\n\t// Setup storage for target texture and bind it to correct framebuffer\n\tfunction setupFrameBufferTexture( framebuffer, renderTarget, texture, attachment, textureTarget, level ) {\n\n\t\tconst glFormat = utils.convert( texture.format, texture.colorSpace );\n\t\tconst glType = utils.convert( texture.type );\n\t\tconst glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace );\n\t\tconst renderTargetProperties = properties.get( renderTarget );\n\t\tconst textureProperties = properties.get( texture );\n\n\t\ttextureProperties.__renderTarget = renderTarget;\n\n\t\tif ( ! renderTargetProperties.__hasExternalTextures ) {\n\n\t\t\tconst width = Math.max( 1, renderTarget.width >> level );\n\t\t\tconst height = Math.max( 1, renderTarget.height >> level );\n\n\t\t\tif ( textureTarget === _gl.TEXTURE_3D || textureTarget === _gl.TEXTURE_2D_ARRAY ) {\n\n\t\t\t\tstate.texImage3D( textureTarget, level, glInternalFormat, width, height, renderTarget.depth, 0, glFormat, glType, null );\n\n\t\t\t} else {\n\n\t\t\t\tstate.texImage2D( textureTarget, level, glInternalFormat, width, height, 0, glFormat, glType, null );\n\n\t\t\t}\n\n\t\t}\n\n\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );\n\n\t\tif ( useMultisampledRTT( renderTarget ) ) {\n\n\t\t\tmultisampledRTTExt.framebufferTexture2DMultisampleEXT( _gl.FRAMEBUFFER, attachment, textureTarget, textureProperties.__webglTexture, 0, getRenderTargetSamples( renderTarget ) );\n\n\t\t} else if ( textureTarget === _gl.TEXTURE_2D || ( textureTarget >= _gl.TEXTURE_CUBE_MAP_POSITIVE_X && textureTarget <= _gl.TEXTURE_CUBE_MAP_NEGATIVE_Z ) ) { // see #24753\n\n\t\t\t_gl.framebufferTexture2D( _gl.FRAMEBUFFER, attachment, textureTarget, textureProperties.__webglTexture, level );\n\n\t\t}\n\n\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, null );\n\n\t}\n\n\t// Setup storage for internal depth/stencil buffers and bind to correct framebuffer\n\tfunction setupRenderBufferStorage( renderbuffer, renderTarget, useMultisample ) {\n\n\t\t_gl.bindRenderbuffer( _gl.RENDERBUFFER, renderbuffer );\n\n\t\tif ( renderTarget.depthBuffer ) {\n\n\t\t\t// retrieve the depth attachment types\n\t\t\tconst depthTexture = renderTarget.depthTexture;\n\t\t\tconst depthType = depthTexture && depthTexture.isDepthTexture ? depthTexture.type : null;\n\t\t\tconst glInternalFormat = getInternalDepthFormat( renderTarget.stencilBuffer, depthType );\n\t\t\tconst glAttachmentType = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;\n\n\t\t\t// set up the attachment\n\t\t\tif ( useMultisampledRTT( renderTarget ) ) {\n\n\t\t\t\tmultisampledRTTExt.renderbufferStorageMultisampleEXT( _gl.RENDERBUFFER, getRenderTargetSamples( renderTarget ), glInternalFormat, renderTarget.width, renderTarget.height );\n\n\t\t\t} else if ( useMultisample ) {\n\n\t\t\t\t_gl.renderbufferStorageMultisample( _gl.RENDERBUFFER, getRenderTargetSamples( renderTarget ), glInternalFormat, renderTarget.width, renderTarget.height );\n\n\t\t\t} else {\n\n\t\t\t\t_gl.renderbufferStorage( _gl.RENDERBUFFER, glInternalFormat, renderTarget.width, renderTarget.height );\n\n\t\t\t}\n\n\t\t\t_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, glAttachmentType, _gl.RENDERBUFFER, renderbuffer );\n\n\t\t} else {\n\n\t\t\tconst textures = renderTarget.textures;\n\n\t\t\tfor ( let i = 0; i < textures.length; i ++ ) {\n\n\t\t\t\tconst texture = textures[ i ];\n\n\t\t\t\tconst glFormat = utils.convert( texture.format, texture.colorSpace );\n\t\t\t\tconst glType = utils.convert( texture.type );\n\t\t\t\tconst glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace );\n\n\t\t\t\tif ( useMultisampledRTT( renderTarget ) ) {\n\n\t\t\t\t\tmultisampledRTTExt.renderbufferStorageMultisampleEXT( _gl.RENDERBUFFER, getRenderTargetSamples( renderTarget ), glInternalFormat, renderTarget.width, renderTarget.height );\n\n\t\t\t\t} else if ( useMultisample ) {\n\n\t\t\t\t\t_gl.renderbufferStorageMultisample( _gl.RENDERBUFFER, getRenderTargetSamples( renderTarget ), glInternalFormat, renderTarget.width, renderTarget.height );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t_gl.renderbufferStorage( _gl.RENDERBUFFER, glInternalFormat, renderTarget.width, renderTarget.height );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t_gl.bindRenderbuffer( _gl.RENDERBUFFER, null );\n\n\t}\n\n\t// Setup resources for a Depth Texture for a FBO (needs an extension)\n\tfunction setupDepthTexture( framebuffer, renderTarget, cubeFace ) {\n\n\t\tconst isCube = ( renderTarget.isWebGLCubeRenderTarget === true );\n\n\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );\n\n\t\tif ( ! ( renderTarget.depthTexture && renderTarget.depthTexture.isDepthTexture ) ) {\n\n\t\t\tthrow new Error( 'renderTarget.depthTexture must be an instance of THREE.DepthTexture' );\n\n\t\t}\n\n\t\tconst textureProperties = properties.get( renderTarget.depthTexture );\n\t\ttextureProperties.__renderTarget = renderTarget;\n\n\t\t// upload an empty depth texture with framebuffer size\n\t\tif ( ! textureProperties.__webglTexture ||\n\t\t\t\trenderTarget.depthTexture.image.width !== renderTarget.width ||\n\t\t\t\trenderTarget.depthTexture.image.height !== renderTarget.height ) {\n\n\t\t\trenderTarget.depthTexture.image.width = renderTarget.width;\n\t\t\trenderTarget.depthTexture.image.height = renderTarget.height;\n\t\t\trenderTarget.depthTexture.needsUpdate = true;\n\n\t\t}\n\n\t\tif ( isCube ) {\n\n\t\t\t// For cube depth textures, initialize and bind without uploading image data\n\t\t\tif ( textureProperties.__webglInit === undefined ) {\n\n\t\t\t\ttextureProperties.__webglInit = true;\n\t\t\t\trenderTarget.depthTexture.addEventListener( 'dispose', onTextureDispose );\n\n\t\t\t}\n\n\t\t\t// Only create and allocate storage once\n\t\t\tif ( textureProperties.__webglTexture === undefined ) {\n\n\t\t\t\ttextureProperties.__webglTexture = _gl.createTexture();\n\n\t\t\t\tstate.bindTexture( _gl.TEXTURE_CUBE_MAP, textureProperties.__webglTexture );\n\t\t\t\tsetTextureParameters( _gl.TEXTURE_CUBE_MAP, renderTarget.depthTexture );\n\n\t\t\t\t// Allocate storage for all 6 faces with correct depth texture format\n\t\t\t\tconst glFormat = utils.convert( renderTarget.depthTexture.format );\n\t\t\t\tconst glType = utils.convert( renderTarget.depthTexture.type );\n\n\t\t\t\t// Use proper internal format for depth textures\n\t\t\t\tlet glInternalFormat;\n\t\t\t\tif ( renderTarget.depthTexture.format === DepthFormat ) {\n\n\t\t\t\t\tglInternalFormat = _gl.DEPTH_COMPONENT24;\n\n\t\t\t\t} else if ( renderTarget.depthTexture.format === DepthStencilFormat ) {\n\n\t\t\t\t\tglInternalFormat = _gl.DEPTH24_STENCIL8;\n\n\t\t\t\t}\n\n\t\t\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\t\t\t_gl.texImage2D( _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, glInternalFormat, renderTarget.width, renderTarget.height, 0, glFormat, glType, null );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tsetTexture2D( renderTarget.depthTexture, 0 );\n\n\t\t}\n\n\t\tconst webglDepthTexture = textureProperties.__webglTexture;\n\t\tconst samples = getRenderTargetSamples( renderTarget );\n\n\t\tconst glTextureType = isCube ? _gl.TEXTURE_CUBE_MAP_POSITIVE_X + cubeFace : _gl.TEXTURE_2D;\n\t\tconst glAttachmentType = renderTarget.depthTexture.format === DepthStencilFormat ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;\n\n\t\tif ( renderTarget.depthTexture.format === DepthFormat ) {\n\n\t\t\tif ( useMultisampledRTT( renderTarget ) ) {\n\n\t\t\t\tmultisampledRTTExt.framebufferTexture2DMultisampleEXT( _gl.FRAMEBUFFER, glAttachmentType, glTextureType, webglDepthTexture, 0, samples );\n\n\t\t\t} else {\n\n\t\t\t\t_gl.framebufferTexture2D( _gl.FRAMEBUFFER, glAttachmentType, glTextureType, webglDepthTexture, 0 );\n\n\t\t\t}\n\n\t\t} else if ( renderTarget.depthTexture.format === DepthStencilFormat ) {\n\n\t\t\tif ( useMultisampledRTT( renderTarget ) ) {\n\n\t\t\t\tmultisampledRTTExt.framebufferTexture2DMultisampleEXT( _gl.FRAMEBUFFER, glAttachmentType, glTextureType, webglDepthTexture, 0, samples );\n\n\t\t\t} else {\n\n\t\t\t\t_gl.framebufferTexture2D( _gl.FRAMEBUFFER, glAttachmentType, glTextureType, webglDepthTexture, 0 );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tthrow new Error( 'Unknown depthTexture format' );\n\n\t\t}\n\n\t}\n\n\t// Setup GL resources for a non-texture depth buffer\n\tfunction setupDepthRenderbuffer( renderTarget ) {\n\n\t\tconst renderTargetProperties = properties.get( renderTarget );\n\t\tconst isCube = ( renderTarget.isWebGLCubeRenderTarget === true );\n\n\t\t// if the bound depth texture has changed\n\t\tif ( renderTargetProperties.__boundDepthTexture !== renderTarget.depthTexture ) {\n\n\t\t\t// fire the dispose event to get rid of stored state associated with the previously bound depth buffer\n\t\t\tconst depthTexture = renderTarget.depthTexture;\n\t\t\tif ( renderTargetProperties.__depthDisposeCallback ) {\n\n\t\t\t\trenderTargetProperties.__depthDisposeCallback();\n\n\t\t\t}\n\n\t\t\t// set up dispose listeners to track when the currently attached buffer is implicitly unbound\n\t\t\tif ( depthTexture ) {\n\n\t\t\t\tconst disposeEvent = () => {\n\n\t\t\t\t\tdelete renderTargetProperties.__boundDepthTexture;\n\t\t\t\t\tdelete renderTargetProperties.__depthDisposeCallback;\n\t\t\t\t\tdepthTexture.removeEventListener( 'dispose', disposeEvent );\n\n\t\t\t\t};\n\n\t\t\t\tdepthTexture.addEventListener( 'dispose', disposeEvent );\n\t\t\t\trenderTargetProperties.__depthDisposeCallback = disposeEvent;\n\n\t\t\t}\n\n\t\t\trenderTargetProperties.__boundDepthTexture = depthTexture;\n\n\t\t}\n\n\t\tif ( renderTarget.depthTexture && ! renderTargetProperties.__autoAllocateDepthBuffer ) {\n\n\t\t\tif ( isCube ) {\n\n\t\t\t\t// For cube render targets with depth texture, setup each face\n\t\t\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\t\t\tsetupDepthTexture( renderTargetProperties.__webglFramebuffer[ i ], renderTarget, i );\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tconst mipmaps = renderTarget.texture.mipmaps;\n\n\t\t\t\tif ( mipmaps && mipmaps.length > 0 ) {\n\n\t\t\t\t\tsetupDepthTexture( renderTargetProperties.__webglFramebuffer[ 0 ], renderTarget, 0 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tsetupDepthTexture( renderTargetProperties.__webglFramebuffer, renderTarget, 0 );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tif ( isCube ) {\n\n\t\t\t\trenderTargetProperties.__webglDepthbuffer = [];\n\n\t\t\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer[ i ] );\n\n\t\t\t\t\tif ( renderTargetProperties.__webglDepthbuffer[ i ] === undefined ) {\n\n\t\t\t\t\t\trenderTargetProperties.__webglDepthbuffer[ i ] = _gl.createRenderbuffer();\n\t\t\t\t\t\tsetupRenderBufferStorage( renderTargetProperties.__webglDepthbuffer[ i ], renderTarget, false );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// attach buffer if it's been created already\n\t\t\t\t\t\tconst glAttachmentType = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;\n\t\t\t\t\t\tconst renderbuffer = renderTargetProperties.__webglDepthbuffer[ i ];\n\t\t\t\t\t\t_gl.bindRenderbuffer( _gl.RENDERBUFFER, renderbuffer );\n\t\t\t\t\t\t_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, glAttachmentType, _gl.RENDERBUFFER, renderbuffer );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tconst mipmaps = renderTarget.texture.mipmaps;\n\n\t\t\t\tif ( mipmaps && mipmaps.length > 0 ) {\n\n\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer[ 0 ] );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer );\n\n\t\t\t\t}\n\n\t\t\t\tif ( renderTargetProperties.__webglDepthbuffer === undefined ) {\n\n\t\t\t\t\trenderTargetProperties.__webglDepthbuffer = _gl.createRenderbuffer();\n\t\t\t\t\tsetupRenderBufferStorage( renderTargetProperties.__webglDepthbuffer, renderTarget, false );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// attach buffer if it's been created already\n\t\t\t\t\tconst glAttachmentType = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;\n\t\t\t\t\tconst renderbuffer = renderTargetProperties.__webglDepthbuffer;\n\t\t\t\t\t_gl.bindRenderbuffer( _gl.RENDERBUFFER, renderbuffer );\n\t\t\t\t\t_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, glAttachmentType, _gl.RENDERBUFFER, renderbuffer );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, null );\n\n\t}\n\n\t// rebind framebuffer with external textures\n\tfunction rebindTextures( renderTarget, colorTexture, depthTexture ) {\n\n\t\tconst renderTargetProperties = properties.get( renderTarget );\n\n\t\tif ( colorTexture !== undefined ) {\n\n\t\t\tsetupFrameBufferTexture( renderTargetProperties.__webglFramebuffer, renderTarget, renderTarget.texture, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, 0 );\n\n\t\t}\n\n\t\tif ( depthTexture !== undefined ) {\n\n\t\t\tsetupDepthRenderbuffer( renderTarget );\n\n\t\t}\n\n\t}\n\n\t// Set up GL resources for the render target\n\tfunction setupRenderTarget( renderTarget ) {\n\n\t\tconst texture = renderTarget.texture;\n\n\t\tconst renderTargetProperties = properties.get( renderTarget );\n\t\tconst textureProperties = properties.get( texture );\n\n\t\trenderTarget.addEventListener( 'dispose', onRenderTargetDispose );\n\n\t\tconst textures = renderTarget.textures;\n\n\t\tconst isCube = ( renderTarget.isWebGLCubeRenderTarget === true );\n\t\tconst isMultipleRenderTargets = ( textures.length > 1 );\n\n\t\tif ( ! isMultipleRenderTargets ) {\n\n\t\t\tif ( textureProperties.__webglTexture === undefined ) {\n\n\t\t\t\ttextureProperties.__webglTexture = _gl.createTexture();\n\n\t\t\t}\n\n\t\t\ttextureProperties.__version = texture.version;\n\t\t\tinfo.memory.textures ++;\n\n\t\t}\n\n\t\t// Setup framebuffer\n\n\t\tif ( isCube ) {\n\n\t\t\trenderTargetProperties.__webglFramebuffer = [];\n\n\t\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\t\tif ( texture.mipmaps && texture.mipmaps.length > 0 ) {\n\n\t\t\t\t\trenderTargetProperties.__webglFramebuffer[ i ] = [];\n\n\t\t\t\t\tfor ( let level = 0; level < texture.mipmaps.length; level ++ ) {\n\n\t\t\t\t\t\trenderTargetProperties.__webglFramebuffer[ i ][ level ] = _gl.createFramebuffer();\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\trenderTargetProperties.__webglFramebuffer[ i ] = _gl.createFramebuffer();\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tif ( texture.mipmaps && texture.mipmaps.length > 0 ) {\n\n\t\t\t\trenderTargetProperties.__webglFramebuffer = [];\n\n\t\t\t\tfor ( let level = 0; level < texture.mipmaps.length; level ++ ) {\n\n\t\t\t\t\trenderTargetProperties.__webglFramebuffer[ level ] = _gl.createFramebuffer();\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\trenderTargetProperties.__webglFramebuffer = _gl.createFramebuffer();\n\n\t\t\t}\n\n\t\t\tif ( isMultipleRenderTargets ) {\n\n\t\t\t\tfor ( let i = 0, il = textures.length; i < il; i ++ ) {\n\n\t\t\t\t\tconst attachmentProperties = properties.get( textures[ i ] );\n\n\t\t\t\t\tif ( attachmentProperties.__webglTexture === undefined ) {\n\n\t\t\t\t\t\tattachmentProperties.__webglTexture = _gl.createTexture();\n\n\t\t\t\t\t\tinfo.memory.textures ++;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( ( renderTarget.samples > 0 ) && useMultisampledRTT( renderTarget ) === false ) {\n\n\t\t\t\trenderTargetProperties.__webglMultisampledFramebuffer = _gl.createFramebuffer();\n\t\t\t\trenderTargetProperties.__webglColorRenderbuffer = [];\n\n\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );\n\n\t\t\t\tfor ( let i = 0; i < textures.length; i ++ ) {\n\n\t\t\t\t\tconst texture = textures[ i ];\n\t\t\t\t\trenderTargetProperties.__webglColorRenderbuffer[ i ] = _gl.createRenderbuffer();\n\n\t\t\t\t\t_gl.bindRenderbuffer( _gl.RENDERBUFFER, renderTargetProperties.__webglColorRenderbuffer[ i ] );\n\n\t\t\t\t\tconst glFormat = utils.convert( texture.format, texture.colorSpace );\n\t\t\t\t\tconst glType = utils.convert( texture.type );\n\t\t\t\t\tconst glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType, texture.colorSpace, renderTarget.isXRRenderTarget === true );\n\t\t\t\t\tconst samples = getRenderTargetSamples( renderTarget );\n\t\t\t\t\t_gl.renderbufferStorageMultisample( _gl.RENDERBUFFER, samples, glInternalFormat, renderTarget.width, renderTarget.height );\n\n\t\t\t\t\t_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.RENDERBUFFER, renderTargetProperties.__webglColorRenderbuffer[ i ] );\n\n\t\t\t\t}\n\n\t\t\t\t_gl.bindRenderbuffer( _gl.RENDERBUFFER, null );\n\n\t\t\t\tif ( renderTarget.depthBuffer ) {\n\n\t\t\t\t\trenderTargetProperties.__webglDepthRenderbuffer = _gl.createRenderbuffer();\n\t\t\t\t\tsetupRenderBufferStorage( renderTargetProperties.__webglDepthRenderbuffer, renderTarget, true );\n\n\t\t\t\t}\n\n\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, null );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// Setup color buffer\n\n\t\tif ( isCube ) {\n\n\t\t\tstate.bindTexture( _gl.TEXTURE_CUBE_MAP, textureProperties.__webglTexture );\n\t\t\tsetTextureParameters( _gl.TEXTURE_CUBE_MAP, texture );\n\n\t\t\tfor ( let i = 0; i < 6; i ++ ) {\n\n\t\t\t\tif ( texture.mipmaps && texture.mipmaps.length > 0 ) {\n\n\t\t\t\t\tfor ( let level = 0; level < texture.mipmaps.length; level ++ ) {\n\n\t\t\t\t\t\tsetupFrameBufferTexture( renderTargetProperties.__webglFramebuffer[ i ][ level ], renderTarget, texture, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, level );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tsetupFrameBufferTexture( renderTargetProperties.__webglFramebuffer[ i ], renderTarget, texture, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0 );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( textureNeedsGenerateMipmaps( texture ) ) {\n\n\t\t\t\tgenerateMipmap( _gl.TEXTURE_CUBE_MAP );\n\n\t\t\t}\n\n\t\t\tstate.unbindTexture();\n\n\t\t} else if ( isMultipleRenderTargets ) {\n\n\t\t\tfor ( let i = 0, il = textures.length; i < il; i ++ ) {\n\n\t\t\t\tconst attachment = textures[ i ];\n\t\t\t\tconst attachmentProperties = properties.get( attachment );\n\n\t\t\t\tlet glTextureType = _gl.TEXTURE_2D;\n\n\t\t\t\tif ( renderTarget.isWebGL3DRenderTarget || renderTarget.isWebGLArrayRenderTarget ) {\n\n\t\t\t\t\tglTextureType = renderTarget.isWebGL3DRenderTarget ? _gl.TEXTURE_3D : _gl.TEXTURE_2D_ARRAY;\n\n\t\t\t\t}\n\n\t\t\t\tstate.bindTexture( glTextureType, attachmentProperties.__webglTexture );\n\t\t\t\tsetTextureParameters( glTextureType, attachment );\n\t\t\t\tsetupFrameBufferTexture( renderTargetProperties.__webglFramebuffer, renderTarget, attachment, _gl.COLOR_ATTACHMENT0 + i, glTextureType, 0 );\n\n\t\t\t\tif ( textureNeedsGenerateMipmaps( attachment ) ) {\n\n\t\t\t\t\tgenerateMipmap( glTextureType );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tstate.unbindTexture();\n\n\t\t} else {\n\n\t\t\tlet glTextureType = _gl.TEXTURE_2D;\n\n\t\t\tif ( renderTarget.isWebGL3DRenderTarget || renderTarget.isWebGLArrayRenderTarget ) {\n\n\t\t\t\tglTextureType = renderTarget.isWebGL3DRenderTarget ? _gl.TEXTURE_3D : _gl.TEXTURE_2D_ARRAY;\n\n\t\t\t}\n\n\t\t\tstate.bindTexture( glTextureType, textureProperties.__webglTexture );\n\t\t\tsetTextureParameters( glTextureType, texture );\n\n\t\t\tif ( texture.mipmaps && texture.mipmaps.length > 0 ) {\n\n\t\t\t\tfor ( let level = 0; level < texture.mipmaps.length; level ++ ) {\n\n\t\t\t\t\tsetupFrameBufferTexture( renderTargetProperties.__webglFramebuffer[ level ], renderTarget, texture, _gl.COLOR_ATTACHMENT0, glTextureType, level );\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tsetupFrameBufferTexture( renderTargetProperties.__webglFramebuffer, renderTarget, texture, _gl.COLOR_ATTACHMENT0, glTextureType, 0 );\n\n\t\t\t}\n\n\t\t\tif ( textureNeedsGenerateMipmaps( texture ) ) {\n\n\t\t\t\tgenerateMipmap( glTextureType );\n\n\t\t\t}\n\n\t\t\tstate.unbindTexture();\n\n\t\t}\n\n\t\t// Setup depth and stencil buffers\n\n\t\tif ( renderTarget.depthBuffer ) {\n\n\t\t\tsetupDepthRenderbuffer( renderTarget );\n\n\t\t}\n\n\t}\n\n\tfunction updateRenderTargetMipmap( renderTarget ) {\n\n\t\tconst textures = renderTarget.textures;\n\n\t\tfor ( let i = 0, il = textures.length; i < il; i ++ ) {\n\n\t\t\tconst texture = textures[ i ];\n\n\t\t\tif ( textureNeedsGenerateMipmaps( texture ) ) {\n\n\t\t\t\tconst targetType = getTargetType( renderTarget );\n\t\t\t\tconst webglTexture = properties.get( texture ).__webglTexture;\n\n\t\t\t\tstate.bindTexture( targetType, webglTexture );\n\t\t\t\tgenerateMipmap( targetType );\n\t\t\t\tstate.unbindTexture();\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tconst invalidationArrayRead = [];\n\tconst invalidationArrayDraw = [];\n\n\tfunction updateMultisampleRenderTarget( renderTarget ) {\n\n\t\tif ( renderTarget.samples > 0 ) {\n\n\t\t\tif ( useMultisampledRTT( renderTarget ) === false ) {\n\n\t\t\t\tconst textures = renderTarget.textures;\n\t\t\t\tconst width = renderTarget.width;\n\t\t\t\tconst height = renderTarget.height;\n\t\t\t\tlet mask = _gl.COLOR_BUFFER_BIT;\n\t\t\t\tconst depthStyle = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;\n\t\t\t\tconst renderTargetProperties = properties.get( renderTarget );\n\t\t\t\tconst isMultipleRenderTargets = ( textures.length > 1 );\n\n\t\t\t\t// If MRT we need to remove FBO attachments\n\t\t\t\tif ( isMultipleRenderTargets ) {\n\n\t\t\t\t\tfor ( let i = 0; i < textures.length; i ++ ) {\n\n\t\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );\n\t\t\t\t\t\t_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.RENDERBUFFER, null );\n\n\t\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer );\n\t\t\t\t\t\t_gl.framebufferTexture2D( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.TEXTURE_2D, null, 0 );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tstate.bindFramebuffer( _gl.READ_FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );\n\n\t\t\t\tconst mipmaps = renderTarget.texture.mipmaps;\n\n\t\t\t\tif ( mipmaps && mipmaps.length > 0 ) {\n\n\t\t\t\t\tstate.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, renderTargetProperties.__webglFramebuffer[ 0 ] );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tstate.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, renderTargetProperties.__webglFramebuffer );\n\n\t\t\t\t}\n\n\t\t\t\tfor ( let i = 0; i < textures.length; i ++ ) {\n\n\t\t\t\t\tif ( renderTarget.resolveDepthBuffer ) {\n\n\t\t\t\t\t\tif ( renderTarget.depthBuffer ) mask |= _gl.DEPTH_BUFFER_BIT;\n\n\t\t\t\t\t\t// resolving stencil is slow with a D3D backend. disable it for all transmission render targets (see #27799)\n\n\t\t\t\t\t\tif ( renderTarget.stencilBuffer && renderTarget.resolveStencilBuffer ) mask |= _gl.STENCIL_BUFFER_BIT;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( isMultipleRenderTargets ) {\n\n\t\t\t\t\t\t_gl.framebufferRenderbuffer( _gl.READ_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.RENDERBUFFER, renderTargetProperties.__webglColorRenderbuffer[ i ] );\n\n\t\t\t\t\t\tconst webglTexture = properties.get( textures[ i ] ).__webglTexture;\n\t\t\t\t\t\t_gl.framebufferTexture2D( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, webglTexture, 0 );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t_gl.blitFramebuffer( 0, 0, width, height, 0, 0, width, height, mask, _gl.NEAREST );\n\n\t\t\t\t\tif ( supportsInvalidateFramebuffer === true ) {\n\n\t\t\t\t\t\tinvalidationArrayRead.length = 0;\n\t\t\t\t\t\tinvalidationArrayDraw.length = 0;\n\n\t\t\t\t\t\tinvalidationArrayRead.push( _gl.COLOR_ATTACHMENT0 + i );\n\n\t\t\t\t\t\tif ( renderTarget.depthBuffer && renderTarget.resolveDepthBuffer === false ) {\n\n\t\t\t\t\t\t\tinvalidationArrayRead.push( depthStyle );\n\t\t\t\t\t\t\tinvalidationArrayDraw.push( depthStyle );\n\n\t\t\t\t\t\t\t_gl.invalidateFramebuffer( _gl.DRAW_FRAMEBUFFER, invalidationArrayDraw );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t_gl.invalidateFramebuffer( _gl.READ_FRAMEBUFFER, invalidationArrayRead );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tstate.bindFramebuffer( _gl.READ_FRAMEBUFFER, null );\n\t\t\t\tstate.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, null );\n\n\t\t\t\t// If MRT since pre-blit we removed the FBO we need to reconstruct the attachments\n\t\t\t\tif ( isMultipleRenderTargets ) {\n\n\t\t\t\t\tfor ( let i = 0; i < textures.length; i ++ ) {\n\n\t\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );\n\t\t\t\t\t\t_gl.framebufferRenderbuffer( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.RENDERBUFFER, renderTargetProperties.__webglColorRenderbuffer[ i ] );\n\n\t\t\t\t\t\tconst webglTexture = properties.get( textures[ i ] ).__webglTexture;\n\n\t\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer );\n\t\t\t\t\t\t_gl.framebufferTexture2D( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, _gl.TEXTURE_2D, webglTexture, 0 );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tstate.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, renderTargetProperties.__webglMultisampledFramebuffer );\n\n\t\t\t} else {\n\n\t\t\t\tif ( renderTarget.depthBuffer && renderTarget.resolveDepthBuffer === false && supportsInvalidateFramebuffer ) {\n\n\t\t\t\t\tconst depthStyle = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;\n\n\t\t\t\t\t_gl.invalidateFramebuffer( _gl.DRAW_FRAMEBUFFER, [ depthStyle ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tfunction getRenderTargetSamples( renderTarget ) {\n\n\t\treturn Math.min( capabilities.maxSamples, renderTarget.samples );\n\n\t}\n\n\tfunction useMultisampledRTT( renderTarget ) {\n\n\t\tconst renderTargetProperties = properties.get( renderTarget );\n\n\t\treturn renderTarget.samples > 0 && extensions.has( 'WEBGL_multisampled_render_to_texture' ) === true && renderTargetProperties.__useRenderToTexture !== false;\n\n\t}\n\n\tfunction updateVideoTexture( texture ) {\n\n\t\tconst frame = info.render.frame;\n\n\t\t// Check the last frame we updated the VideoTexture\n\n\t\tif ( _videoTextures.get( texture ) !== frame ) {\n\n\t\t\t_videoTextures.set( texture, frame );\n\t\t\ttexture.update();\n\n\t\t}\n\n\t}\n\n\tfunction verifyColorSpace( texture, image ) {\n\n\t\tconst colorSpace = texture.colorSpace;\n\t\tconst format = texture.format;\n\t\tconst type = texture.type;\n\n\t\tif ( texture.isCompressedTexture === true || texture.isVideoTexture === true ) return image;\n\n\t\tif ( colorSpace !== LinearSRGBColorSpace && colorSpace !== NoColorSpace ) {\n\n\t\t\t// sRGB\n\n\t\t\tif ( ColorManagement.getTransfer( colorSpace ) === SRGBTransfer ) {\n\n\t\t\t\t// in WebGL 2 uncompressed textures can only be sRGB encoded if they have the RGBA8 format\n\n\t\t\t\tif ( format !== RGBAFormat || type !== UnsignedByteType ) {\n\n\t\t\t\t\twarn( 'WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType.' );\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\terror( 'WebGLTextures: Unsupported texture color space:', colorSpace );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn image;\n\n\t}\n\n\tfunction getDimensions( image ) {\n\n\t\tif ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) {\n\n\t\t\t// if intrinsic data are not available, fallback to width/height\n\n\t\t\t_imageDimensions.width = image.naturalWidth || image.width;\n\t\t\t_imageDimensions.height = image.naturalHeight || image.height;\n\n\t\t} else if ( typeof VideoFrame !== 'undefined' && image instanceof VideoFrame ) {\n\n\t\t\t_imageDimensions.width = image.displayWidth;\n\t\t\t_imageDimensions.height = image.displayHeight;\n\n\t\t} else {\n\n\t\t\t_imageDimensions.width = image.width;\n\t\t\t_imageDimensions.height = image.height;\n\n\t\t}\n\n\t\treturn _imageDimensions;\n\n\t}\n\n\t//\n\n\tthis.allocateTextureUnit = allocateTextureUnit;\n\tthis.resetTextureUnits = resetTextureUnits;\n\n\tthis.setTexture2D = setTexture2D;\n\tthis.setTexture2DArray = setTexture2DArray;\n\tthis.setTexture3D = setTexture3D;\n\tthis.setTextureCube = setTextureCube;\n\tthis.rebindTextures = rebindTextures;\n\tthis.setupRenderTarget = setupRenderTarget;\n\tthis.updateRenderTargetMipmap = updateRenderTargetMipmap;\n\tthis.updateMultisampleRenderTarget = updateMultisampleRenderTarget;\n\tthis.setupDepthRenderbuffer = setupDepthRenderbuffer;\n\tthis.setupFrameBufferTexture = setupFrameBufferTexture;\n\tthis.useMultisampledRTT = useMultisampledRTT;\n\n\tthis.isReversedDepthBuffer = function () {\n\n\t\treturn state.buffers.depth.getReversed();\n\n\t};\n\n}\n\nfunction WebGLUtils( gl, extensions ) {\n\n\tfunction convert( p, colorSpace = NoColorSpace ) {\n\n\t\tlet extension;\n\n\t\tconst transfer = ColorManagement.getTransfer( colorSpace );\n\n\t\tif ( p === UnsignedByteType ) return gl.UNSIGNED_BYTE;\n\t\tif ( p === UnsignedShort4444Type ) return gl.UNSIGNED_SHORT_4_4_4_4;\n\t\tif ( p === UnsignedShort5551Type ) return gl.UNSIGNED_SHORT_5_5_5_1;\n\t\tif ( p === UnsignedInt5999Type ) return gl.UNSIGNED_INT_5_9_9_9_REV;\n\t\tif ( p === UnsignedInt101111Type ) return gl.UNSIGNED_INT_10F_11F_11F_REV;\n\n\t\tif ( p === ByteType ) return gl.BYTE;\n\t\tif ( p === ShortType ) return gl.SHORT;\n\t\tif ( p === UnsignedShortType ) return gl.UNSIGNED_SHORT;\n\t\tif ( p === IntType ) return gl.INT;\n\t\tif ( p === UnsignedIntType ) return gl.UNSIGNED_INT;\n\t\tif ( p === FloatType ) return gl.FLOAT;\n\t\tif ( p === HalfFloatType ) return gl.HALF_FLOAT;\n\n\t\tif ( p === AlphaFormat ) return gl.ALPHA;\n\t\tif ( p === RGBFormat ) return gl.RGB;\n\t\tif ( p === RGBAFormat ) return gl.RGBA;\n\t\tif ( p === DepthFormat ) return gl.DEPTH_COMPONENT;\n\t\tif ( p === DepthStencilFormat ) return gl.DEPTH_STENCIL;\n\n\t\t// WebGL2 formats.\n\n\t\tif ( p === RedFormat ) return gl.RED;\n\t\tif ( p === RedIntegerFormat ) return gl.RED_INTEGER;\n\t\tif ( p === RGFormat ) return gl.RG;\n\t\tif ( p === RGIntegerFormat ) return gl.RG_INTEGER;\n\t\tif ( p === RGBAIntegerFormat ) return gl.RGBA_INTEGER;\n\n\t\t// S3TC\n\n\t\tif ( p === RGB_S3TC_DXT1_Format || p === RGBA_S3TC_DXT1_Format || p === RGBA_S3TC_DXT3_Format || p === RGBA_S3TC_DXT5_Format ) {\n\n\t\t\tif ( transfer === SRGBTransfer ) {\n\n\t\t\t\textension = extensions.get( 'WEBGL_compressed_texture_s3tc_srgb' );\n\n\t\t\t\tif ( extension !== null ) {\n\n\t\t\t\t\tif ( p === RGB_S3TC_DXT1_Format ) return extension.COMPRESSED_SRGB_S3TC_DXT1_EXT;\n\t\t\t\t\tif ( p === RGBA_S3TC_DXT1_Format ) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;\n\t\t\t\t\tif ( p === RGBA_S3TC_DXT3_Format ) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;\n\t\t\t\t\tif ( p === RGBA_S3TC_DXT5_Format ) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;\n\n\t\t\t\t} else {\n\n\t\t\t\t\treturn null;\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\textension = extensions.get( 'WEBGL_compressed_texture_s3tc' );\n\n\t\t\t\tif ( extension !== null ) {\n\n\t\t\t\t\tif ( p === RGB_S3TC_DXT1_Format ) return extension.COMPRESSED_RGB_S3TC_DXT1_EXT;\n\t\t\t\t\tif ( p === RGBA_S3TC_DXT1_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT1_EXT;\n\t\t\t\t\tif ( p === RGBA_S3TC_DXT3_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT3_EXT;\n\t\t\t\t\tif ( p === RGBA_S3TC_DXT5_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT5_EXT;\n\n\t\t\t\t} else {\n\n\t\t\t\t\treturn null;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t// PVRTC\n\n\t\tif ( p === RGB_PVRTC_4BPPV1_Format || p === RGB_PVRTC_2BPPV1_Format || p === RGBA_PVRTC_4BPPV1_Format || p === RGBA_PVRTC_2BPPV1_Format ) {\n\n\t\t\textension = extensions.get( 'WEBGL_compressed_texture_pvrtc' );\n\n\t\t\tif ( extension !== null ) {\n\n\t\t\t\tif ( p === RGB_PVRTC_4BPPV1_Format ) return extension.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;\n\t\t\t\tif ( p === RGB_PVRTC_2BPPV1_Format ) return extension.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;\n\t\t\t\tif ( p === RGBA_PVRTC_4BPPV1_Format ) return extension.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;\n\t\t\t\tif ( p === RGBA_PVRTC_2BPPV1_Format ) return extension.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;\n\n\t\t\t} else {\n\n\t\t\t\treturn null;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// ETC\n\n\t\tif ( p === RGB_ETC1_Format || p === RGB_ETC2_Format || p === RGBA_ETC2_EAC_Format || p === R11_EAC_Format || p === SIGNED_R11_EAC_Format || p === RG11_EAC_Format || p === SIGNED_RG11_EAC_Format ) {\n\n\t\t\textension = extensions.get( 'WEBGL_compressed_texture_etc' );\n\n\t\t\tif ( extension !== null ) {\n\n\t\t\t\tif ( p === RGB_ETC1_Format || p === RGB_ETC2_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ETC2 : extension.COMPRESSED_RGB8_ETC2;\n\t\t\t\tif ( p === RGBA_ETC2_EAC_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC : extension.COMPRESSED_RGBA8_ETC2_EAC;\n\t\t\t\tif ( p === R11_EAC_Format ) return extension.COMPRESSED_R11_EAC;\n\t\t\t\tif ( p === SIGNED_R11_EAC_Format ) return extension.COMPRESSED_SIGNED_R11_EAC;\n\t\t\t\tif ( p === RG11_EAC_Format ) return extension.COMPRESSED_RG11_EAC;\n\t\t\t\tif ( p === SIGNED_RG11_EAC_Format ) return extension.COMPRESSED_SIGNED_RG11_EAC;\n\n\t\t\t} else {\n\n\t\t\t\treturn null;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// ASTC\n\n\t\tif ( p === RGBA_ASTC_4x4_Format || p === RGBA_ASTC_5x4_Format || p === RGBA_ASTC_5x5_Format ||\n\t\t\tp === RGBA_ASTC_6x5_Format || p === RGBA_ASTC_6x6_Format || p === RGBA_ASTC_8x5_Format ||\n\t\t\tp === RGBA_ASTC_8x6_Format || p === RGBA_ASTC_8x8_Format || p === RGBA_ASTC_10x5_Format ||\n\t\t\tp === RGBA_ASTC_10x6_Format || p === RGBA_ASTC_10x8_Format || p === RGBA_ASTC_10x10_Format ||\n\t\t\tp === RGBA_ASTC_12x10_Format || p === RGBA_ASTC_12x12_Format ) {\n\n\t\t\textension = extensions.get( 'WEBGL_compressed_texture_astc' );\n\n\t\t\tif ( extension !== null ) {\n\n\t\t\t\tif ( p === RGBA_ASTC_4x4_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR : extension.COMPRESSED_RGBA_ASTC_4x4_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_5x4_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR : extension.COMPRESSED_RGBA_ASTC_5x4_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_5x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR : extension.COMPRESSED_RGBA_ASTC_5x5_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_6x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR : extension.COMPRESSED_RGBA_ASTC_6x5_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_6x6_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR : extension.COMPRESSED_RGBA_ASTC_6x6_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_8x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR : extension.COMPRESSED_RGBA_ASTC_8x5_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_8x6_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR : extension.COMPRESSED_RGBA_ASTC_8x6_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_8x8_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR : extension.COMPRESSED_RGBA_ASTC_8x8_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_10x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR : extension.COMPRESSED_RGBA_ASTC_10x5_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_10x6_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR : extension.COMPRESSED_RGBA_ASTC_10x6_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_10x8_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR : extension.COMPRESSED_RGBA_ASTC_10x8_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_10x10_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR : extension.COMPRESSED_RGBA_ASTC_10x10_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_12x10_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR : extension.COMPRESSED_RGBA_ASTC_12x10_KHR;\n\t\t\t\tif ( p === RGBA_ASTC_12x12_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR : extension.COMPRESSED_RGBA_ASTC_12x12_KHR;\n\n\t\t\t} else {\n\n\t\t\t\treturn null;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// BPTC\n\n\t\tif ( p === RGBA_BPTC_Format || p === RGB_BPTC_SIGNED_Format || p === RGB_BPTC_UNSIGNED_Format ) {\n\n\t\t\textension = extensions.get( 'EXT_texture_compression_bptc' );\n\n\t\t\tif ( extension !== null ) {\n\n\t\t\t\tif ( p === RGBA_BPTC_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT : extension.COMPRESSED_RGBA_BPTC_UNORM_EXT;\n\t\t\t\tif ( p === RGB_BPTC_SIGNED_Format ) return extension.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;\n\t\t\t\tif ( p === RGB_BPTC_UNSIGNED_Format ) return extension.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT;\n\n\t\t\t} else {\n\n\t\t\t\treturn null;\n\n\t\t\t}\n\n\t\t}\n\n\t\t// RGTC\n\n\t\tif ( p === RED_RGTC1_Format || p === SIGNED_RED_RGTC1_Format || p === RED_GREEN_RGTC2_Format || p === SIGNED_RED_GREEN_RGTC2_Format ) {\n\n\t\t\textension = extensions.get( 'EXT_texture_compression_rgtc' );\n\n\t\t\tif ( extension !== null ) {\n\n\t\t\t\tif ( p === RED_RGTC1_Format ) return extension.COMPRESSED_RED_RGTC1_EXT;\n\t\t\t\tif ( p === SIGNED_RED_RGTC1_Format ) return extension.COMPRESSED_SIGNED_RED_RGTC1_EXT;\n\t\t\t\tif ( p === RED_GREEN_RGTC2_Format ) return extension.COMPRESSED_RED_GREEN_RGTC2_EXT;\n\t\t\t\tif ( p === SIGNED_RED_GREEN_RGTC2_Format ) return extension.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT;\n\n\t\t\t} else {\n\n\t\t\t\treturn null;\n\n\t\t\t}\n\n\t\t}\n\n\t\t//\n\n\t\tif ( p === UnsignedInt248Type ) return gl.UNSIGNED_INT_24_8;\n\n\t\t// if \"p\" can't be resolved, assume the user defines a WebGL constant as a string (fallback/workaround for packed RGB formats)\n\n\t\treturn ( gl[ p ] !== undefined ) ? gl[ p ] : null;\n\n\t}\n\n\treturn { convert: convert };\n\n}\n\nconst _occlusion_vertex = `\nvoid main() {\n\n\tgl_Position = vec4( position, 1.0 );\n\n}`;\n\nconst _occlusion_fragment = `\nuniform sampler2DArray depthColor;\nuniform float depthWidth;\nuniform float depthHeight;\n\nvoid main() {\n\n\tvec2 coord = vec2( gl_FragCoord.x / depthWidth, gl_FragCoord.y / depthHeight );\n\n\tif ( coord.x >= 1.0 ) {\n\n\t\tgl_FragDepth = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r;\n\n\t} else {\n\n\t\tgl_FragDepth = texture( depthColor, vec3( coord.x, coord.y, 0 ) ).r;\n\n\t}\n\n}`;\n\n/**\n * A XR module that manages the access to the Depth Sensing API.\n */\nclass WebXRDepthSensing {\n\n\t/**\n\t * Constructs a new depth sensing module.\n\t */\n\tconstructor() {\n\n\t\t/**\n\t\t * An opaque texture representing the depth of the user's environment.\n\t\t *\n\t\t * @type {?ExternalTexture}\n\t\t */\n\t\tthis.texture = null;\n\n\t\t/**\n\t\t * A plane mesh for visualizing the depth texture.\n\t\t *\n\t\t * @type {?Mesh}\n\t\t */\n\t\tthis.mesh = null;\n\n\t\t/**\n\t\t * The depth near value.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.depthNear = 0;\n\n\t\t/**\n\t\t * The depth near far.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tthis.depthFar = 0;\n\n\t}\n\n\t/**\n\t * Inits the depth sensing module\n\t *\n\t * @param {XRWebGLDepthInformation} depthData - The XR depth data.\n\t * @param {XRRenderState} renderState - The XR render state.\n\t */\n\tinit( depthData, renderState ) {\n\n\t\tif ( this.texture === null ) {\n\n\t\t\tconst texture = new ExternalTexture( depthData.texture );\n\n\t\t\tif ( ( depthData.depthNear !== renderState.depthNear ) || ( depthData.depthFar !== renderState.depthFar ) ) {\n\n\t\t\t\tthis.depthNear = depthData.depthNear;\n\t\t\t\tthis.depthFar = depthData.depthFar;\n\n\t\t\t}\n\n\t\t\tthis.texture = texture;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns a plane mesh that visualizes the depth texture.\n\t *\n\t * @param {ArrayCamera} cameraXR - The XR camera.\n\t * @return {?Mesh} The plane mesh.\n\t */\n\tgetMesh( cameraXR ) {\n\n\t\tif ( this.texture !== null ) {\n\n\t\t\tif ( this.mesh === null ) {\n\n\t\t\t\tconst viewport = cameraXR.cameras[ 0 ].viewport;\n\t\t\t\tconst material = new ShaderMaterial( {\n\t\t\t\t\tvertexShader: _occlusion_vertex,\n\t\t\t\t\tfragmentShader: _occlusion_fragment,\n\t\t\t\t\tuniforms: {\n\t\t\t\t\t\tdepthColor: { value: this.texture },\n\t\t\t\t\t\tdepthWidth: { value: viewport.z },\n\t\t\t\t\t\tdepthHeight: { value: viewport.w }\n\t\t\t\t\t}\n\t\t\t\t} );\n\n\t\t\t\tthis.mesh = new Mesh( new PlaneGeometry( 20, 20 ), material );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn this.mesh;\n\n\t}\n\n\t/**\n\t * Resets the module\n\t */\n\treset() {\n\n\t\tthis.texture = null;\n\t\tthis.mesh = null;\n\n\t}\n\n\t/**\n\t * Returns a texture representing the depth of the user's environment.\n\t *\n\t * @return {?ExternalTexture} The depth texture.\n\t */\n\tgetDepthTexture() {\n\n\t\treturn this.texture;\n\n\t}\n\n}\n\n/**\n * This class represents an abstraction of the WebXR Device API and is\n * internally used by {@link WebGLRenderer}. `WebXRManager` also provides a public\n * interface that allows users to enable/disable XR and perform XR related\n * tasks like for instance retrieving controllers.\n *\n * @augments EventDispatcher\n * @hideconstructor\n */\nclass WebXRManager extends EventDispatcher {\n\n\t/**\n\t * Constructs a new WebGL renderer.\n\t *\n\t * @param {WebGLRenderer} renderer - The renderer.\n\t * @param {WebGL2RenderingContext} gl - The rendering context.\n\t */\n\tconstructor( renderer, gl ) {\n\n\t\tsuper();\n\n\t\tconst scope = this;\n\n\t\tlet session = null;\n\n\t\tlet framebufferScaleFactor = 1.0;\n\n\t\tlet referenceSpace = null;\n\t\tlet referenceSpaceType = 'local-floor';\n\t\t// Set default foveation to maximum.\n\t\tlet foveation = 1.0;\n\t\tlet customReferenceSpace = null;\n\n\t\tlet pose = null;\n\t\tlet glBinding = null;\n\t\tlet glProjLayer = null;\n\t\tlet glBaseLayer = null;\n\t\tlet xrFrame = null;\n\n\t\tconst supportsGlBinding = typeof XRWebGLBinding !== 'undefined';\n\n\t\tconst depthSensing = new WebXRDepthSensing();\n\t\tconst cameraAccessTextures = {};\n\t\tconst attributes = gl.getContextAttributes();\n\n\t\tlet initialRenderTarget = null;\n\t\tlet newRenderTarget = null;\n\n\t\tconst controllers = [];\n\t\tconst controllerInputSources = [];\n\n\t\tconst currentSize = new Vector2();\n\t\tlet currentPixelRatio = null;\n\n\t\t//\n\n\t\tconst cameraL = new PerspectiveCamera();\n\t\tcameraL.viewport = new Vector4();\n\n\t\tconst cameraR = new PerspectiveCamera();\n\t\tcameraR.viewport = new Vector4();\n\n\t\tconst cameras = [ cameraL, cameraR ];\n\n\t\tconst cameraXR = new ArrayCamera();\n\n\t\tlet _currentDepthNear = null;\n\t\tlet _currentDepthFar = null;\n\n\t\t//\n\n\t\t/**\n\t\t * Whether the manager's XR camera should be automatically updated or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.cameraAutoUpdate = true;\n\n\t\t/**\n\t\t * This flag notifies the renderer to be ready for XR rendering. Set it to `true`\n\t\t * if you are going to use XR in your app.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.enabled = false;\n\n\t\t/**\n\t\t * Whether XR presentation is active or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default false\n\t\t */\n\t\tthis.isPresenting = false;\n\n\t\t/**\n\t\t * Returns a group representing the `target ray` space of the XR controller.\n\t\t * Use this space for visualizing 3D objects that support the user in pointing\n\t\t * tasks like UI interaction.\n\t\t *\n\t\t * @param {number} index - The index of the controller.\n\t\t * @return {Group} A group representing the `target ray` space.\n\t\t */\n\t\tthis.getController = function ( index ) {\n\n\t\t\tlet controller = controllers[ index ];\n\n\t\t\tif ( controller === undefined ) {\n\n\t\t\t\tcontroller = new WebXRController();\n\t\t\t\tcontrollers[ index ] = controller;\n\n\t\t\t}\n\n\t\t\treturn controller.getTargetRaySpace();\n\n\t\t};\n\n\t\t/**\n\t\t * Returns a group representing the `grip` space of the XR controller.\n\t\t * Use this space for visualizing 3D objects that support the user in pointing\n\t\t * tasks like UI interaction.\n\t\t *\n\t\t * Note: If you want to show something in the user's hand AND offer a\n\t\t * pointing ray at the same time, you'll want to attached the handheld object\n\t\t * to the group returned by `getControllerGrip()` and the ray to the\n\t\t * group returned by `getController()`. The idea is to have two\n\t\t * different groups in two different coordinate spaces for the same WebXR\n\t\t * controller.\n\t\t *\n\t\t * @param {number} index - The index of the controller.\n\t\t * @return {Group} A group representing the `grip` space.\n\t\t */\n\t\tthis.getControllerGrip = function ( index ) {\n\n\t\t\tlet controller = controllers[ index ];\n\n\t\t\tif ( controller === undefined ) {\n\n\t\t\t\tcontroller = new WebXRController();\n\t\t\t\tcontrollers[ index ] = controller;\n\n\t\t\t}\n\n\t\t\treturn controller.getGripSpace();\n\n\t\t};\n\n\t\t/**\n\t\t * Returns a group representing the `hand` space of the XR controller.\n\t\t * Use this space for visualizing 3D objects that support the user in pointing\n\t\t * tasks like UI interaction.\n\t\t *\n\t\t * @param {number} index - The index of the controller.\n\t\t * @return {Group} A group representing the `hand` space.\n\t\t */\n\t\tthis.getHand = function ( index ) {\n\n\t\t\tlet controller = controllers[ index ];\n\n\t\t\tif ( controller === undefined ) {\n\n\t\t\t\tcontroller = new WebXRController();\n\t\t\t\tcontrollers[ index ] = controller;\n\n\t\t\t}\n\n\t\t\treturn controller.getHandSpace();\n\n\t\t};\n\n\t\t//\n\n\t\tfunction onSessionEvent( event ) {\n\n\t\t\tconst controllerIndex = controllerInputSources.indexOf( event.inputSource );\n\n\t\t\tif ( controllerIndex === -1 ) {\n\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tconst controller = controllers[ controllerIndex ];\n\n\t\t\tif ( controller !== undefined ) {\n\n\t\t\t\tcontroller.update( event.inputSource, event.frame, customReferenceSpace || referenceSpace );\n\t\t\t\tcontroller.dispatchEvent( { type: event.type, data: event.inputSource } );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction onSessionEnd() {\n\n\t\t\tsession.removeEventListener( 'select', onSessionEvent );\n\t\t\tsession.removeEventListener( 'selectstart', onSessionEvent );\n\t\t\tsession.removeEventListener( 'selectend', onSessionEvent );\n\t\t\tsession.removeEventListener( 'squeeze', onSessionEvent );\n\t\t\tsession.removeEventListener( 'squeezestart', onSessionEvent );\n\t\t\tsession.removeEventListener( 'squeezeend', onSessionEvent );\n\t\t\tsession.removeEventListener( 'end', onSessionEnd );\n\t\t\tsession.removeEventListener( 'inputsourceschange', onInputSourcesChange );\n\n\t\t\tfor ( let i = 0; i < controllers.length; i ++ ) {\n\n\t\t\t\tconst inputSource = controllerInputSources[ i ];\n\n\t\t\t\tif ( inputSource === null ) continue;\n\n\t\t\t\tcontrollerInputSources[ i ] = null;\n\n\t\t\t\tcontrollers[ i ].disconnect( inputSource );\n\n\t\t\t}\n\n\t\t\t_currentDepthNear = null;\n\t\t\t_currentDepthFar = null;\n\n\t\t\tdepthSensing.reset();\n\t\t\tfor ( const key in cameraAccessTextures ) {\n\n\t\t\t\tdelete cameraAccessTextures[ key ];\n\n\t\t\t}\n\n\t\t\t// restore framebuffer/rendering state\n\n\t\t\trenderer.setRenderTarget( initialRenderTarget );\n\n\t\t\tglBaseLayer = null;\n\t\t\tglProjLayer = null;\n\t\t\tglBinding = null;\n\t\t\tsession = null;\n\t\t\tnewRenderTarget = null;\n\n\t\t\t//\n\n\t\t\tanimation.stop();\n\n\t\t\tscope.isPresenting = false;\n\n\t\t\trenderer.setPixelRatio( currentPixelRatio );\n\t\t\trenderer.setSize( currentSize.width, currentSize.height, false );\n\n\t\t\tscope.dispatchEvent( { type: 'sessionend' } );\n\n\t\t}\n\n\t\t/**\n\t\t * Sets the framebuffer scale factor.\n\t\t *\n\t\t * This method can not be used during a XR session.\n\t\t *\n\t\t * @param {number} value - The framebuffer scale factor.\n\t\t */\n\t\tthis.setFramebufferScaleFactor = function ( value ) {\n\n\t\t\tframebufferScaleFactor = value;\n\n\t\t\tif ( scope.isPresenting === true ) {\n\n\t\t\t\twarn( 'WebXRManager: Cannot change framebuffer scale while presenting.' );\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Sets the reference space type. Can be used to configure a spatial relationship with the user's physical\n\t\t * environment. Depending on how the user moves in 3D space, setting an appropriate reference space can\n\t\t * improve tracking. Default is `local-floor`. Valid values can be found here\n\t\t * https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpace#reference_space_types.\n\t\t *\n\t\t * This method can not be used during a XR session.\n\t\t *\n\t\t * @param {string} value - The reference space type.\n\t\t */\n\t\tthis.setReferenceSpaceType = function ( value ) {\n\n\t\t\treferenceSpaceType = value;\n\n\t\t\tif ( scope.isPresenting === true ) {\n\n\t\t\t\twarn( 'WebXRManager: Cannot change reference space type while presenting.' );\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the XR reference space.\n\t\t *\n\t\t * @return {XRReferenceSpace} The XR reference space.\n\t\t */\n\t\tthis.getReferenceSpace = function () {\n\n\t\t\treturn customReferenceSpace || referenceSpace;\n\n\t\t};\n\n\t\t/**\n\t\t * Sets a custom XR reference space.\n\t\t *\n\t\t * @param {XRReferenceSpace} space - The XR reference space.\n\t\t */\n\t\tthis.setReferenceSpace = function ( space ) {\n\n\t\t\tcustomReferenceSpace = space;\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the current base layer.\n\t\t *\n\t\t * This is an `XRProjectionLayer` when the targeted XR device supports the\n\t\t * WebXR Layers API, or an `XRWebGLLayer` otherwise.\n\t\t *\n\t\t * @return {?(XRWebGLLayer|XRProjectionLayer)} The XR base layer.\n\t\t */\n\t\tthis.getBaseLayer = function () {\n\n\t\t\treturn glProjLayer !== null ? glProjLayer : glBaseLayer;\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the current XR binding.\n\t\t *\n\t\t * Creates a new binding if needed and the browser is\n\t\t * capable of doing so.\n\t\t *\n\t\t * @return {?XRWebGLBinding} The XR binding. Returns `null` if one cannot be created.\n\t\t */\n\t\tthis.getBinding = function () {\n\n\t\t\tif ( glBinding === null && supportsGlBinding ) {\n\n\t\t\t\tglBinding = new XRWebGLBinding( session, gl );\n\n\t\t\t}\n\n\t\t\treturn glBinding;\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the current XR frame.\n\t\t *\n\t\t * @return {?XRFrame} The XR frame. Returns `null` when used outside a XR session.\n\t\t */\n\t\tthis.getFrame = function () {\n\n\t\t\treturn xrFrame;\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the current XR session.\n\t\t *\n\t\t * @return {?XRSession} The XR session. Returns `null` when used outside a XR session.\n\t\t */\n\t\tthis.getSession = function () {\n\n\t\t\treturn session;\n\n\t\t};\n\n\t\t/**\n\t\t * After a XR session has been requested usually with one of the `*Button` modules, it\n\t\t * is injected into the renderer with this method. This method triggers the start of\n\t\t * the actual XR rendering.\n\t\t *\n\t\t * @async\n\t\t * @param {XRSession} value - The XR session to set.\n\t\t * @return {Promise} A Promise that resolves when the session has been set.\n\t\t */\n\t\tthis.setSession = async function ( value ) {\n\n\t\t\tsession = value;\n\n\t\t\tif ( session !== null ) {\n\n\t\t\t\tinitialRenderTarget = renderer.getRenderTarget();\n\n\t\t\t\tsession.addEventListener( 'select', onSessionEvent );\n\t\t\t\tsession.addEventListener( 'selectstart', onSessionEvent );\n\t\t\t\tsession.addEventListener( 'selectend', onSessionEvent );\n\t\t\t\tsession.addEventListener( 'squeeze', onSessionEvent );\n\t\t\t\tsession.addEventListener( 'squeezestart', onSessionEvent );\n\t\t\t\tsession.addEventListener( 'squeezeend', onSessionEvent );\n\t\t\t\tsession.addEventListener( 'end', onSessionEnd );\n\t\t\t\tsession.addEventListener( 'inputsourceschange', onInputSourcesChange );\n\n\t\t\t\tif ( attributes.xrCompatible !== true ) {\n\n\t\t\t\t\tawait gl.makeXRCompatible();\n\n\t\t\t\t}\n\n\t\t\t\tcurrentPixelRatio = renderer.getPixelRatio();\n\t\t\t\trenderer.getSize( currentSize );\n\n\n\t\t\t\t// Check that the browser implements the necessary APIs to use an\n\t\t\t\t// XRProjectionLayer rather than an XRWebGLLayer\n\t\t\t\tconst supportsLayers = supportsGlBinding && 'createProjectionLayer' in XRWebGLBinding.prototype;\n\n\t\t\t\tif ( ! supportsLayers ) {\n\n\t\t\t\t\tconst layerInit = {\n\t\t\t\t\t\tantialias: attributes.antialias,\n\t\t\t\t\t\talpha: true,\n\t\t\t\t\t\tdepth: attributes.depth,\n\t\t\t\t\t\tstencil: attributes.stencil,\n\t\t\t\t\t\tframebufferScaleFactor: framebufferScaleFactor\n\t\t\t\t\t};\n\n\t\t\t\t\tglBaseLayer = new XRWebGLLayer( session, gl, layerInit );\n\n\t\t\t\t\tsession.updateRenderState( { baseLayer: glBaseLayer } );\n\n\t\t\t\t\trenderer.setPixelRatio( 1 );\n\t\t\t\t\trenderer.setSize( glBaseLayer.framebufferWidth, glBaseLayer.framebufferHeight, false );\n\n\t\t\t\t\tnewRenderTarget = new WebGLRenderTarget(\n\t\t\t\t\t\tglBaseLayer.framebufferWidth,\n\t\t\t\t\t\tglBaseLayer.framebufferHeight,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tformat: RGBAFormat,\n\t\t\t\t\t\t\ttype: UnsignedByteType,\n\t\t\t\t\t\t\tcolorSpace: renderer.outputColorSpace,\n\t\t\t\t\t\t\tstencilBuffer: attributes.stencil,\n\t\t\t\t\t\t\tresolveDepthBuffer: ( glBaseLayer.ignoreDepthValues === false ),\n\t\t\t\t\t\t\tresolveStencilBuffer: ( glBaseLayer.ignoreDepthValues === false )\n\n\t\t\t\t\t\t}\n\t\t\t\t\t);\n\n\t\t\t\t} else {\n\n\t\t\t\t\tlet depthFormat = null;\n\t\t\t\t\tlet depthType = null;\n\t\t\t\t\tlet glDepthFormat = null;\n\n\t\t\t\t\tif ( attributes.depth ) {\n\n\t\t\t\t\t\tglDepthFormat = attributes.stencil ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT24;\n\t\t\t\t\t\tdepthFormat = attributes.stencil ? DepthStencilFormat : DepthFormat;\n\t\t\t\t\t\tdepthType = attributes.stencil ? UnsignedInt248Type : UnsignedIntType;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tconst projectionlayerInit = {\n\t\t\t\t\t\tcolorFormat: gl.RGBA8,\n\t\t\t\t\t\tdepthFormat: glDepthFormat,\n\t\t\t\t\t\tscaleFactor: framebufferScaleFactor\n\t\t\t\t\t};\n\n\t\t\t\t\tglBinding = this.getBinding();\n\n\t\t\t\t\tglProjLayer = glBinding.createProjectionLayer( projectionlayerInit );\n\n\t\t\t\t\tsession.updateRenderState( { layers: [ glProjLayer ] } );\n\n\t\t\t\t\trenderer.setPixelRatio( 1 );\n\t\t\t\t\trenderer.setSize( glProjLayer.textureWidth, glProjLayer.textureHeight, false );\n\n\t\t\t\t\tnewRenderTarget = new WebGLRenderTarget(\n\t\t\t\t\t\tglProjLayer.textureWidth,\n\t\t\t\t\t\tglProjLayer.textureHeight,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tformat: RGBAFormat,\n\t\t\t\t\t\t\ttype: UnsignedByteType,\n\t\t\t\t\t\t\tdepthTexture: new DepthTexture( glProjLayer.textureWidth, glProjLayer.textureHeight, depthType, undefined, undefined, undefined, undefined, undefined, undefined, depthFormat ),\n\t\t\t\t\t\t\tstencilBuffer: attributes.stencil,\n\t\t\t\t\t\t\tcolorSpace: renderer.outputColorSpace,\n\t\t\t\t\t\t\tsamples: attributes.antialias ? 4 : 0,\n\t\t\t\t\t\t\tresolveDepthBuffer: ( glProjLayer.ignoreDepthValues === false ),\n\t\t\t\t\t\t\tresolveStencilBuffer: ( glProjLayer.ignoreDepthValues === false )\n\t\t\t\t\t\t} );\n\n\t\t\t\t}\n\n\t\t\t\tnewRenderTarget.isXRRenderTarget = true; // TODO Remove this when possible, see #23278\n\n\t\t\t\tthis.setFoveation( foveation );\n\n\t\t\t\tcustomReferenceSpace = null;\n\t\t\t\treferenceSpace = await session.requestReferenceSpace( referenceSpaceType );\n\n\t\t\t\tanimation.setContext( session );\n\t\t\t\tanimation.start();\n\n\t\t\t\tscope.isPresenting = true;\n\n\t\t\t\tscope.dispatchEvent( { type: 'sessionstart' } );\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the environment blend mode from the current XR session.\n\t\t *\n\t\t * @return {'opaque'|'additive'|'alpha-blend'|undefined} The environment blend mode. Returns `undefined` when used outside of a XR session.\n\t\t */\n\t\tthis.getEnvironmentBlendMode = function () {\n\n\t\t\tif ( session !== null ) {\n\n\t\t\t\treturn session.environmentBlendMode;\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the current depth texture computed via depth sensing.\n\t\t *\n\t\t * See {@link WebXRDepthSensing#getDepthTexture}.\n\t\t *\n\t\t * @return {?Texture} The depth texture.\n\t\t */\n\t\tthis.getDepthTexture = function () {\n\n\t\t\treturn depthSensing.getDepthTexture();\n\n\t\t};\n\n\t\tfunction onInputSourcesChange( event ) {\n\n\t\t\t// Notify disconnected\n\n\t\t\tfor ( let i = 0; i < event.removed.length; i ++ ) {\n\n\t\t\t\tconst inputSource = event.removed[ i ];\n\t\t\t\tconst index = controllerInputSources.indexOf( inputSource );\n\n\t\t\t\tif ( index >= 0 ) {\n\n\t\t\t\t\tcontrollerInputSources[ index ] = null;\n\t\t\t\t\tcontrollers[ index ].disconnect( inputSource );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// Notify connected\n\n\t\t\tfor ( let i = 0; i < event.added.length; i ++ ) {\n\n\t\t\t\tconst inputSource = event.added[ i ];\n\n\t\t\t\tlet controllerIndex = controllerInputSources.indexOf( inputSource );\n\n\t\t\t\tif ( controllerIndex === -1 ) {\n\n\t\t\t\t\t// Assign input source a controller that currently has no input source\n\n\t\t\t\t\tfor ( let i = 0; i < controllers.length; i ++ ) {\n\n\t\t\t\t\t\tif ( i >= controllerInputSources.length ) {\n\n\t\t\t\t\t\t\tcontrollerInputSources.push( inputSource );\n\t\t\t\t\t\t\tcontrollerIndex = i;\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t} else if ( controllerInputSources[ i ] === null ) {\n\n\t\t\t\t\t\t\tcontrollerInputSources[ i ] = inputSource;\n\t\t\t\t\t\t\tcontrollerIndex = i;\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// If all controllers do currently receive input we ignore new ones\n\n\t\t\t\t\tif ( controllerIndex === -1 ) break;\n\n\t\t\t\t}\n\n\t\t\t\tconst controller = controllers[ controllerIndex ];\n\n\t\t\t\tif ( controller ) {\n\n\t\t\t\t\tcontroller.connect( inputSource );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t//\n\n\t\tconst cameraLPos = new Vector3();\n\t\tconst cameraRPos = new Vector3();\n\n\t\t/**\n\t\t * Assumes 2 cameras that are parallel and share an X-axis, and that\n\t\t * the cameras' projection and world matrices have already been set.\n\t\t * And that near and far planes are identical for both cameras.\n\t\t * Visualization of this technique: https://computergraphics.stackexchange.com/a/4765\n\t\t *\n\t\t * @param {ArrayCamera} camera - The camera to update.\n\t\t * @param {PerspectiveCamera} cameraL - The left camera.\n\t\t * @param {PerspectiveCamera} cameraR - The right camera.\n\t\t */\n\t\tfunction setProjectionFromUnion( camera, cameraL, cameraR ) {\n\n\t\t\tcameraLPos.setFromMatrixPosition( cameraL.matrixWorld );\n\t\t\tcameraRPos.setFromMatrixPosition( cameraR.matrixWorld );\n\n\t\t\tconst ipd = cameraLPos.distanceTo( cameraRPos );\n\n\t\t\tconst projL = cameraL.projectionMatrix.elements;\n\t\t\tconst projR = cameraR.projectionMatrix.elements;\n\n\t\t\t// VR systems will have identical far and near planes, and\n\t\t\t// most likely identical top and bottom frustum extents.\n\t\t\t// Use the left camera for these values.\n\t\t\tconst near = projL[ 14 ] / ( projL[ 10 ] - 1 );\n\t\t\tconst far = projL[ 14 ] / ( projL[ 10 ] + 1 );\n\t\t\tconst topFov = ( projL[ 9 ] + 1 ) / projL[ 5 ];\n\t\t\tconst bottomFov = ( projL[ 9 ] - 1 ) / projL[ 5 ];\n\n\t\t\tconst leftFov = ( projL[ 8 ] - 1 ) / projL[ 0 ];\n\t\t\tconst rightFov = ( projR[ 8 ] + 1 ) / projR[ 0 ];\n\t\t\tconst left = near * leftFov;\n\t\t\tconst right = near * rightFov;\n\n\t\t\t// Calculate the new camera's position offset from the\n\t\t\t// left camera. xOffset should be roughly half `ipd`.\n\t\t\tconst zOffset = ipd / ( - leftFov + rightFov );\n\t\t\tconst xOffset = zOffset * - leftFov;\n\n\t\t\t// TODO: Better way to apply this offset?\n\t\t\tcameraL.matrixWorld.decompose( camera.position, camera.quaternion, camera.scale );\n\t\t\tcamera.translateX( xOffset );\n\t\t\tcamera.translateZ( zOffset );\n\t\t\tcamera.matrixWorld.compose( camera.position, camera.quaternion, camera.scale );\n\t\t\tcamera.matrixWorldInverse.copy( camera.matrixWorld ).invert();\n\n\t\t\t// Check if the projection uses an infinite far plane.\n\t\t\tif ( projL[ 10 ] === -1 ) {\n\n\t\t\t\t// Use the projection matrix from the left eye.\n\t\t\t\t// The camera offset is sufficient to include the view volumes\n\t\t\t\t// of both eyes (assuming symmetric projections).\n\t\t\t\tcamera.projectionMatrix.copy( cameraL.projectionMatrix );\n\t\t\t\tcamera.projectionMatrixInverse.copy( cameraL.projectionMatrixInverse );\n\n\t\t\t} else {\n\n\t\t\t\t// Find the union of the frustum values of the cameras and scale\n\t\t\t\t// the values so that the near plane's position does not change in world space,\n\t\t\t\t// although must now be relative to the new union camera.\n\t\t\t\tconst near2 = near + zOffset;\n\t\t\t\tconst far2 = far + zOffset;\n\t\t\t\tconst left2 = left - xOffset;\n\t\t\t\tconst right2 = right + ( ipd - xOffset );\n\t\t\t\tconst top2 = topFov * far / far2 * near2;\n\t\t\t\tconst bottom2 = bottomFov * far / far2 * near2;\n\n\t\t\t\tcamera.projectionMatrix.makePerspective( left2, right2, top2, bottom2, near2, far2 );\n\t\t\t\tcamera.projectionMatrixInverse.copy( camera.projectionMatrix ).invert();\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction updateCamera( camera, parent ) {\n\n\t\t\tif ( parent === null ) {\n\n\t\t\t\tcamera.matrixWorld.copy( camera.matrix );\n\n\t\t\t} else {\n\n\t\t\t\tcamera.matrixWorld.multiplyMatrices( parent.matrixWorld, camera.matrix );\n\n\t\t\t}\n\n\t\t\tcamera.matrixWorldInverse.copy( camera.matrixWorld ).invert();\n\n\t\t}\n\n\t\t/**\n\t\t * Updates the state of the XR camera. Use this method on app level if you\n\t\t * set `cameraAutoUpdate` to `false`. The method requires the non-XR\n\t\t * camera of the scene as a parameter. The passed in camera's transformation\n\t\t * is automatically adjusted to the position of the XR camera when calling\n\t\t * this method.\n\t\t *\n\t\t * @param {Camera} camera - The camera.\n\t\t */\n\t\tthis.updateCamera = function ( camera ) {\n\n\t\t\tif ( session === null ) return;\n\n\t\t\tlet depthNear = camera.near;\n\t\t\tlet depthFar = camera.far;\n\n\t\t\tif ( depthSensing.texture !== null ) {\n\n\t\t\t\tif ( depthSensing.depthNear > 0 ) depthNear = depthSensing.depthNear;\n\t\t\t\tif ( depthSensing.depthFar > 0 ) depthFar = depthSensing.depthFar;\n\n\t\t\t}\n\n\t\t\tcameraXR.near = cameraR.near = cameraL.near = depthNear;\n\t\t\tcameraXR.far = cameraR.far = cameraL.far = depthFar;\n\n\t\t\tif ( _currentDepthNear !== cameraXR.near || _currentDepthFar !== cameraXR.far ) {\n\n\t\t\t\t// Note that the new renderState won't apply until the next frame. See #18320\n\n\t\t\t\tsession.updateRenderState( {\n\t\t\t\t\tdepthNear: cameraXR.near,\n\t\t\t\t\tdepthFar: cameraXR.far\n\t\t\t\t} );\n\n\t\t\t\t_currentDepthNear = cameraXR.near;\n\t\t\t\t_currentDepthFar = cameraXR.far;\n\n\t\t\t}\n\n\t\t\t// inherit camera layers and enable eye layers (1 = left, 2 = right)\n\t\t\tcameraXR.layers.mask = camera.layers.mask | 0b110;\n\t\t\tcameraL.layers.mask = cameraXR.layers.mask & 0b011;\n\t\t\tcameraR.layers.mask = cameraXR.layers.mask & 0b101;\n\n\t\t\tconst parent = camera.parent;\n\t\t\tconst cameras = cameraXR.cameras;\n\n\t\t\tupdateCamera( cameraXR, parent );\n\n\t\t\tfor ( let i = 0; i < cameras.length; i ++ ) {\n\n\t\t\t\tupdateCamera( cameras[ i ], parent );\n\n\t\t\t}\n\n\t\t\t// update projection matrix for proper view frustum culling\n\n\t\t\tif ( cameras.length === 2 ) {\n\n\t\t\t\tsetProjectionFromUnion( cameraXR, cameraL, cameraR );\n\n\t\t\t} else {\n\n\t\t\t\t// assume single camera setup (AR)\n\n\t\t\t\tcameraXR.projectionMatrix.copy( cameraL.projectionMatrix );\n\n\t\t\t}\n\n\t\t\t// update user camera and its children\n\n\t\t\tupdateUserCamera( camera, cameraXR, parent );\n\n\t\t};\n\n\t\tfunction updateUserCamera( camera, cameraXR, parent ) {\n\n\t\t\tif ( parent === null ) {\n\n\t\t\t\tcamera.matrix.copy( cameraXR.matrixWorld );\n\n\t\t\t} else {\n\n\t\t\t\tcamera.matrix.copy( parent.matrixWorld );\n\t\t\t\tcamera.matrix.invert();\n\t\t\t\tcamera.matrix.multiply( cameraXR.matrixWorld );\n\n\t\t\t}\n\n\t\t\tcamera.matrix.decompose( camera.position, camera.quaternion, camera.scale );\n\t\t\tcamera.updateMatrixWorld( true );\n\n\t\t\tcamera.projectionMatrix.copy( cameraXR.projectionMatrix );\n\t\t\tcamera.projectionMatrixInverse.copy( cameraXR.projectionMatrixInverse );\n\n\t\t\tif ( camera.isPerspectiveCamera ) {\n\n\t\t\t\tcamera.fov = RAD2DEG * 2 * Math.atan( 1 / camera.projectionMatrix.elements[ 5 ] );\n\t\t\t\tcamera.zoom = 1;\n\n\t\t\t}\n\n\t\t}\n\n\t\t/**\n\t\t * Returns an instance of {@link ArrayCamera} which represents the XR camera\n\t\t * of the active XR session. For each view it holds a separate camera object.\n\t\t *\n\t\t * The camera's `fov` is currently not used and does not reflect the fov of\n\t\t * the XR camera. If you need the fov on app level, you have to compute in\n\t\t * manually from the XR camera's projection matrices.\n\t\t *\n\t\t * @return {ArrayCamera} The XR camera.\n\t\t */\n\t\tthis.getCamera = function () {\n\n\t\t\treturn cameraXR;\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the amount of foveation used by the XR compositor for the projection layer.\n\t\t *\n\t\t * @return {number|undefined} The amount of foveation.\n\t\t */\n\t\tthis.getFoveation = function () {\n\n\t\t\tif ( glProjLayer === null && glBaseLayer === null ) {\n\n\t\t\t\treturn undefined;\n\n\t\t\t}\n\n\t\t\treturn foveation;\n\n\t\t};\n\n\t\t/**\n\t\t * Sets the foveation value.\n\t\t *\n\t\t * @param {number} value - A number in the range `[0,1]` where `0` means no foveation (full resolution)\n\t\t * and `1` means maximum foveation (the edges render at lower resolution).\n\t\t */\n\t\tthis.setFoveation = function ( value ) {\n\n\t\t\t// 0 = no foveation = full resolution\n\t\t\t// 1 = maximum foveation = the edges render at lower resolution\n\n\t\t\tfoveation = value;\n\n\t\t\tif ( glProjLayer !== null ) {\n\n\t\t\t\tglProjLayer.fixedFoveation = value;\n\n\t\t\t}\n\n\t\t\tif ( glBaseLayer !== null && glBaseLayer.fixedFoveation !== undefined ) {\n\n\t\t\t\tglBaseLayer.fixedFoveation = value;\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Returns `true` if depth sensing is supported.\n\t\t *\n\t\t * @return {boolean} Whether depth sensing is supported or not.\n\t\t */\n\t\tthis.hasDepthSensing = function () {\n\n\t\t\treturn depthSensing.texture !== null;\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the depth sensing mesh.\n\t\t *\n\t\t * See {@link WebXRDepthSensing#getMesh}.\n\t\t *\n\t\t * @return {Mesh} The depth sensing mesh.\n\t\t */\n\t\tthis.getDepthSensingMesh = function () {\n\n\t\t\treturn depthSensing.getMesh( cameraXR );\n\n\t\t};\n\n\t\t/**\n\t\t * Retrieves an opaque texture from the view-aligned {@link XRCamera}.\n\t\t * Only available during the current animation loop.\n\t\t *\n\t\t * @param {XRCamera} xrCamera - The camera to query.\n\t\t * @return {?Texture} An opaque texture representing the current raw camera frame.\n\t\t */\n\t\tthis.getCameraTexture = function ( xrCamera ) {\n\n\t\t\treturn cameraAccessTextures[ xrCamera ];\n\n\t\t};\n\n\t\t// Animation Loop\n\n\t\tlet onAnimationFrameCallback = null;\n\n\t\tfunction onAnimationFrame( time, frame ) {\n\n\t\t\tpose = frame.getViewerPose( customReferenceSpace || referenceSpace );\n\t\t\txrFrame = frame;\n\n\t\t\tif ( pose !== null ) {\n\n\t\t\t\tconst views = pose.views;\n\n\t\t\t\tif ( glBaseLayer !== null ) {\n\n\t\t\t\t\trenderer.setRenderTargetFramebuffer( newRenderTarget, glBaseLayer.framebuffer );\n\t\t\t\t\trenderer.setRenderTarget( newRenderTarget );\n\n\t\t\t\t}\n\n\t\t\t\tlet cameraXRNeedsUpdate = false;\n\n\t\t\t\t// check if it's necessary to rebuild cameraXR's camera list\n\n\t\t\t\tif ( views.length !== cameraXR.cameras.length ) {\n\n\t\t\t\t\tcameraXR.cameras.length = 0;\n\t\t\t\t\tcameraXRNeedsUpdate = true;\n\n\t\t\t\t}\n\n\t\t\t\tfor ( let i = 0; i < views.length; i ++ ) {\n\n\t\t\t\t\tconst view = views[ i ];\n\n\t\t\t\t\tlet viewport = null;\n\n\t\t\t\t\tif ( glBaseLayer !== null ) {\n\n\t\t\t\t\t\tviewport = glBaseLayer.getViewport( view );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tconst glSubImage = glBinding.getViewSubImage( glProjLayer, view );\n\t\t\t\t\t\tviewport = glSubImage.viewport;\n\n\t\t\t\t\t\t// For side-by-side projection, we only produce a single texture for both eyes.\n\t\t\t\t\t\tif ( i === 0 ) {\n\n\t\t\t\t\t\t\trenderer.setRenderTargetTextures(\n\t\t\t\t\t\t\t\tnewRenderTarget,\n\t\t\t\t\t\t\t\tglSubImage.colorTexture,\n\t\t\t\t\t\t\t\tglSubImage.depthStencilTexture );\n\n\t\t\t\t\t\t\trenderer.setRenderTarget( newRenderTarget );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\tlet camera = cameras[ i ];\n\n\t\t\t\t\tif ( camera === undefined ) {\n\n\t\t\t\t\t\tcamera = new PerspectiveCamera();\n\t\t\t\t\t\tcamera.layers.enable( i );\n\t\t\t\t\t\tcamera.viewport = new Vector4();\n\t\t\t\t\t\tcameras[ i ] = camera;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tcamera.matrix.fromArray( view.transform.matrix );\n\t\t\t\t\tcamera.matrix.decompose( camera.position, camera.quaternion, camera.scale );\n\t\t\t\t\tcamera.projectionMatrix.fromArray( view.projectionMatrix );\n\t\t\t\t\tcamera.projectionMatrixInverse.copy( camera.projectionMatrix ).invert();\n\t\t\t\t\tcamera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height );\n\n\t\t\t\t\tif ( i === 0 ) {\n\n\t\t\t\t\t\tcameraXR.matrix.copy( camera.matrix );\n\t\t\t\t\t\tcameraXR.matrix.decompose( cameraXR.position, cameraXR.quaternion, cameraXR.scale );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( cameraXRNeedsUpdate === true ) {\n\n\t\t\t\t\t\tcameraXR.cameras.push( camera );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\t//\n\n\t\t\t\tconst enabledFeatures = session.enabledFeatures;\n\t\t\t\tconst gpuDepthSensingEnabled = enabledFeatures &&\n\t\t\t\t\tenabledFeatures.includes( 'depth-sensing' ) &&\n\t\t\t\t\tsession.depthUsage == 'gpu-optimized';\n\n\t\t\t\tif ( gpuDepthSensingEnabled && supportsGlBinding ) {\n\n\t\t\t\t\tglBinding = scope.getBinding();\n\n\t\t\t\t\tconst depthData = glBinding.getDepthInformation( views[ 0 ] );\n\n\t\t\t\t\tif ( depthData && depthData.isValid && depthData.texture ) {\n\n\t\t\t\t\t\tdepthSensing.init( depthData, session.renderState );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tconst cameraAccessEnabled = enabledFeatures &&\n\t\t\t\t    enabledFeatures.includes( 'camera-access' );\n\n\t\t\t\tif ( cameraAccessEnabled && supportsGlBinding ) {\n\n\t\t\t\t\trenderer.state.unbindTexture();\n\n\t\t\t\t\tglBinding = scope.getBinding();\n\n\t\t\t\t\tfor ( let i = 0; i < views.length; i ++ ) {\n\n\t\t\t\t\t\tconst camera = views[ i ].camera;\n\n\t\t\t\t\t\tif ( camera ) {\n\n\t\t\t\t\t\t\tlet cameraTex = cameraAccessTextures[ camera ];\n\n\t\t\t\t\t\t\tif ( ! cameraTex ) {\n\n\t\t\t\t\t\t\t\tcameraTex = new ExternalTexture();\n\t\t\t\t\t\t\t\tcameraAccessTextures[ camera ] = cameraTex;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst glTexture = glBinding.getCameraImage( camera );\n\t\t\t\t\t\t\tcameraTex.sourceTexture = glTexture;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t//\n\n\t\t\tfor ( let i = 0; i < controllers.length; i ++ ) {\n\n\t\t\t\tconst inputSource = controllerInputSources[ i ];\n\t\t\t\tconst controller = controllers[ i ];\n\n\t\t\t\tif ( inputSource !== null && controller !== undefined ) {\n\n\t\t\t\t\tcontroller.update( inputSource, frame, customReferenceSpace || referenceSpace );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( onAnimationFrameCallback ) onAnimationFrameCallback( time, frame );\n\n\t\t\tif ( frame.detectedPlanes ) {\n\n\t\t\t\tscope.dispatchEvent( { type: 'planesdetected', data: frame } );\n\n\t\t\t}\n\n\t\t\txrFrame = null;\n\n\t\t}\n\n\t\tconst animation = new WebGLAnimation();\n\n\t\tanimation.setAnimationLoop( onAnimationFrame );\n\n\t\tthis.setAnimationLoop = function ( callback ) {\n\n\t\t\tonAnimationFrameCallback = callback;\n\n\t\t};\n\n\t\tthis.dispose = function () {};\n\n\t}\n\n}\n\nconst _e1 = /*@__PURE__*/ new Euler();\nconst _m1 = /*@__PURE__*/ new Matrix4();\n\nfunction WebGLMaterials( renderer, properties ) {\n\n\tfunction refreshTransformUniform( map, uniform ) {\n\n\t\tif ( map.matrixAutoUpdate === true ) {\n\n\t\t\tmap.updateMatrix();\n\n\t\t}\n\n\t\tuniform.value.copy( map.matrix );\n\n\t}\n\n\tfunction refreshFogUniforms( uniforms, fog ) {\n\n\t\tfog.color.getRGB( uniforms.fogColor.value, getUnlitUniformColorSpace( renderer ) );\n\n\t\tif ( fog.isFog ) {\n\n\t\t\tuniforms.fogNear.value = fog.near;\n\t\t\tuniforms.fogFar.value = fog.far;\n\n\t\t} else if ( fog.isFogExp2 ) {\n\n\t\t\tuniforms.fogDensity.value = fog.density;\n\n\t\t}\n\n\t}\n\n\tfunction refreshMaterialUniforms( uniforms, material, pixelRatio, height, transmissionRenderTarget ) {\n\n\t\tif ( material.isMeshBasicMaterial ) {\n\n\t\t\trefreshUniformsCommon( uniforms, material );\n\n\t\t} else if ( material.isMeshLambertMaterial ) {\n\n\t\t\trefreshUniformsCommon( uniforms, material );\n\n\t\t} else if ( material.isMeshToonMaterial ) {\n\n\t\t\trefreshUniformsCommon( uniforms, material );\n\t\t\trefreshUniformsToon( uniforms, material );\n\n\t\t} else if ( material.isMeshPhongMaterial ) {\n\n\t\t\trefreshUniformsCommon( uniforms, material );\n\t\t\trefreshUniformsPhong( uniforms, material );\n\n\t\t} else if ( material.isMeshStandardMaterial ) {\n\n\t\t\trefreshUniformsCommon( uniforms, material );\n\t\t\trefreshUniformsStandard( uniforms, material );\n\n\t\t\tif ( material.isMeshPhysicalMaterial ) {\n\n\t\t\t\trefreshUniformsPhysical( uniforms, material, transmissionRenderTarget );\n\n\t\t\t}\n\n\t\t} else if ( material.isMeshMatcapMaterial ) {\n\n\t\t\trefreshUniformsCommon( uniforms, material );\n\t\t\trefreshUniformsMatcap( uniforms, material );\n\n\t\t} else if ( material.isMeshDepthMaterial ) {\n\n\t\t\trefreshUniformsCommon( uniforms, material );\n\n\t\t} else if ( material.isMeshDistanceMaterial ) {\n\n\t\t\trefreshUniformsCommon( uniforms, material );\n\t\t\trefreshUniformsDistance( uniforms, material );\n\n\t\t} else if ( material.isMeshNormalMaterial ) {\n\n\t\t\trefreshUniformsCommon( uniforms, material );\n\n\t\t} else if ( material.isLineBasicMaterial ) {\n\n\t\t\trefreshUniformsLine( uniforms, material );\n\n\t\t\tif ( material.isLineDashedMaterial ) {\n\n\t\t\t\trefreshUniformsDash( uniforms, material );\n\n\t\t\t}\n\n\t\t} else if ( material.isPointsMaterial ) {\n\n\t\t\trefreshUniformsPoints( uniforms, material, pixelRatio, height );\n\n\t\t} else if ( material.isSpriteMaterial ) {\n\n\t\t\trefreshUniformsSprites( uniforms, material );\n\n\t\t} else if ( material.isShadowMaterial ) {\n\n\t\t\tuniforms.color.value.copy( material.color );\n\t\t\tuniforms.opacity.value = material.opacity;\n\n\t\t} else if ( material.isShaderMaterial ) {\n\n\t\t\tmaterial.uniformsNeedUpdate = false; // #15581\n\n\t\t}\n\n\t}\n\n\tfunction refreshUniformsCommon( uniforms, material ) {\n\n\t\tuniforms.opacity.value = material.opacity;\n\n\t\tif ( material.color ) {\n\n\t\t\tuniforms.diffuse.value.copy( material.color );\n\n\t\t}\n\n\t\tif ( material.emissive ) {\n\n\t\t\tuniforms.emissive.value.copy( material.emissive ).multiplyScalar( material.emissiveIntensity );\n\n\t\t}\n\n\t\tif ( material.map ) {\n\n\t\t\tuniforms.map.value = material.map;\n\n\t\t\trefreshTransformUniform( material.map, uniforms.mapTransform );\n\n\t\t}\n\n\t\tif ( material.alphaMap ) {\n\n\t\t\tuniforms.alphaMap.value = material.alphaMap;\n\n\t\t\trefreshTransformUniform( material.alphaMap, uniforms.alphaMapTransform );\n\n\t\t}\n\n\t\tif ( material.bumpMap ) {\n\n\t\t\tuniforms.bumpMap.value = material.bumpMap;\n\n\t\t\trefreshTransformUniform( material.bumpMap, uniforms.bumpMapTransform );\n\n\t\t\tuniforms.bumpScale.value = material.bumpScale;\n\n\t\t\tif ( material.side === BackSide ) {\n\n\t\t\t\tuniforms.bumpScale.value *= -1;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( material.normalMap ) {\n\n\t\t\tuniforms.normalMap.value = material.normalMap;\n\n\t\t\trefreshTransformUniform( material.normalMap, uniforms.normalMapTransform );\n\n\t\t\tuniforms.normalScale.value.copy( material.normalScale );\n\n\t\t\tif ( material.side === BackSide ) {\n\n\t\t\t\tuniforms.normalScale.value.negate();\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( material.displacementMap ) {\n\n\t\t\tuniforms.displacementMap.value = material.displacementMap;\n\n\t\t\trefreshTransformUniform( material.displacementMap, uniforms.displacementMapTransform );\n\n\t\t\tuniforms.displacementScale.value = material.displacementScale;\n\t\t\tuniforms.displacementBias.value = material.displacementBias;\n\n\t\t}\n\n\t\tif ( material.emissiveMap ) {\n\n\t\t\tuniforms.emissiveMap.value = material.emissiveMap;\n\n\t\t\trefreshTransformUniform( material.emissiveMap, uniforms.emissiveMapTransform );\n\n\t\t}\n\n\t\tif ( material.specularMap ) {\n\n\t\t\tuniforms.specularMap.value = material.specularMap;\n\n\t\t\trefreshTransformUniform( material.specularMap, uniforms.specularMapTransform );\n\n\t\t}\n\n\t\tif ( material.alphaTest > 0 ) {\n\n\t\t\tuniforms.alphaTest.value = material.alphaTest;\n\n\t\t}\n\n\t\tconst materialProperties = properties.get( material );\n\n\t\tconst envMap = materialProperties.envMap;\n\t\tconst envMapRotation = materialProperties.envMapRotation;\n\n\t\tif ( envMap ) {\n\n\t\t\tuniforms.envMap.value = envMap;\n\n\t\t\t_e1.copy( envMapRotation );\n\n\t\t\t// accommodate left-handed frame\n\t\t\t_e1.x *= -1; _e1.y *= -1; _e1.z *= -1;\n\n\t\t\tif ( envMap.isCubeTexture && envMap.isRenderTargetTexture === false ) {\n\n\t\t\t\t// environment maps which are not cube render targets or PMREMs follow a different convention\n\t\t\t\t_e1.y *= -1;\n\t\t\t\t_e1.z *= -1;\n\n\t\t\t}\n\n\t\t\tuniforms.envMapRotation.value.setFromMatrix4( _m1.makeRotationFromEuler( _e1 ) );\n\n\t\t\tuniforms.flipEnvMap.value = ( envMap.isCubeTexture && envMap.isRenderTargetTexture === false ) ? -1 : 1;\n\n\t\t\tuniforms.reflectivity.value = material.reflectivity;\n\t\t\tuniforms.ior.value = material.ior;\n\t\t\tuniforms.refractionRatio.value = material.refractionRatio;\n\n\t\t}\n\n\t\tif ( material.lightMap ) {\n\n\t\t\tuniforms.lightMap.value = material.lightMap;\n\t\t\tuniforms.lightMapIntensity.value = material.lightMapIntensity;\n\n\t\t\trefreshTransformUniform( material.lightMap, uniforms.lightMapTransform );\n\n\t\t}\n\n\t\tif ( material.aoMap ) {\n\n\t\t\tuniforms.aoMap.value = material.aoMap;\n\t\t\tuniforms.aoMapIntensity.value = material.aoMapIntensity;\n\n\t\t\trefreshTransformUniform( material.aoMap, uniforms.aoMapTransform );\n\n\t\t}\n\n\t}\n\n\tfunction refreshUniformsLine( uniforms, material ) {\n\n\t\tuniforms.diffuse.value.copy( material.color );\n\t\tuniforms.opacity.value = material.opacity;\n\n\t\tif ( material.map ) {\n\n\t\t\tuniforms.map.value = material.map;\n\n\t\t\trefreshTransformUniform( material.map, uniforms.mapTransform );\n\n\t\t}\n\n\t}\n\n\tfunction refreshUniformsDash( uniforms, material ) {\n\n\t\tuniforms.dashSize.value = material.dashSize;\n\t\tuniforms.totalSize.value = material.dashSize + material.gapSize;\n\t\tuniforms.scale.value = material.scale;\n\n\t}\n\n\tfunction refreshUniformsPoints( uniforms, material, pixelRatio, height ) {\n\n\t\tuniforms.diffuse.value.copy( material.color );\n\t\tuniforms.opacity.value = material.opacity;\n\t\tuniforms.size.value = material.size * pixelRatio;\n\t\tuniforms.scale.value = height * 0.5;\n\n\t\tif ( material.map ) {\n\n\t\t\tuniforms.map.value = material.map;\n\n\t\t\trefreshTransformUniform( material.map, uniforms.uvTransform );\n\n\t\t}\n\n\t\tif ( material.alphaMap ) {\n\n\t\t\tuniforms.alphaMap.value = material.alphaMap;\n\n\t\t\trefreshTransformUniform( material.alphaMap, uniforms.alphaMapTransform );\n\n\t\t}\n\n\t\tif ( material.alphaTest > 0 ) {\n\n\t\t\tuniforms.alphaTest.value = material.alphaTest;\n\n\t\t}\n\n\t}\n\n\tfunction refreshUniformsSprites( uniforms, material ) {\n\n\t\tuniforms.diffuse.value.copy( material.color );\n\t\tuniforms.opacity.value = material.opacity;\n\t\tuniforms.rotation.value = material.rotation;\n\n\t\tif ( material.map ) {\n\n\t\t\tuniforms.map.value = material.map;\n\n\t\t\trefreshTransformUniform( material.map, uniforms.mapTransform );\n\n\t\t}\n\n\t\tif ( material.alphaMap ) {\n\n\t\t\tuniforms.alphaMap.value = material.alphaMap;\n\n\t\t\trefreshTransformUniform( material.alphaMap, uniforms.alphaMapTransform );\n\n\t\t}\n\n\t\tif ( material.alphaTest > 0 ) {\n\n\t\t\tuniforms.alphaTest.value = material.alphaTest;\n\n\t\t}\n\n\t}\n\n\tfunction refreshUniformsPhong( uniforms, material ) {\n\n\t\tuniforms.specular.value.copy( material.specular );\n\t\tuniforms.shininess.value = Math.max( material.shininess, 1e-4 ); // to prevent pow( 0.0, 0.0 )\n\n\t}\n\n\tfunction refreshUniformsToon( uniforms, material ) {\n\n\t\tif ( material.gradientMap ) {\n\n\t\t\tuniforms.gradientMap.value = material.gradientMap;\n\n\t\t}\n\n\t}\n\n\tfunction refreshUniformsStandard( uniforms, material ) {\n\n\t\tuniforms.metalness.value = material.metalness;\n\n\t\tif ( material.metalnessMap ) {\n\n\t\t\tuniforms.metalnessMap.value = material.metalnessMap;\n\n\t\t\trefreshTransformUniform( material.metalnessMap, uniforms.metalnessMapTransform );\n\n\t\t}\n\n\t\tuniforms.roughness.value = material.roughness;\n\n\t\tif ( material.roughnessMap ) {\n\n\t\t\tuniforms.roughnessMap.value = material.roughnessMap;\n\n\t\t\trefreshTransformUniform( material.roughnessMap, uniforms.roughnessMapTransform );\n\n\t\t}\n\n\t\tif ( material.envMap ) {\n\n\t\t\t//uniforms.envMap.value = material.envMap; // part of uniforms common\n\n\t\t\tuniforms.envMapIntensity.value = material.envMapIntensity;\n\n\t\t}\n\n\t}\n\n\tfunction refreshUniformsPhysical( uniforms, material, transmissionRenderTarget ) {\n\n\t\tuniforms.ior.value = material.ior; // also part of uniforms common\n\n\t\tif ( material.sheen > 0 ) {\n\n\t\t\tuniforms.sheenColor.value.copy( material.sheenColor ).multiplyScalar( material.sheen );\n\n\t\t\tuniforms.sheenRoughness.value = material.sheenRoughness;\n\n\t\t\tif ( material.sheenColorMap ) {\n\n\t\t\t\tuniforms.sheenColorMap.value = material.sheenColorMap;\n\n\t\t\t\trefreshTransformUniform( material.sheenColorMap, uniforms.sheenColorMapTransform );\n\n\t\t\t}\n\n\t\t\tif ( material.sheenRoughnessMap ) {\n\n\t\t\t\tuniforms.sheenRoughnessMap.value = material.sheenRoughnessMap;\n\n\t\t\t\trefreshTransformUniform( material.sheenRoughnessMap, uniforms.sheenRoughnessMapTransform );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( material.clearcoat > 0 ) {\n\n\t\t\tuniforms.clearcoat.value = material.clearcoat;\n\t\t\tuniforms.clearcoatRoughness.value = material.clearcoatRoughness;\n\n\t\t\tif ( material.clearcoatMap ) {\n\n\t\t\t\tuniforms.clearcoatMap.value = material.clearcoatMap;\n\n\t\t\t\trefreshTransformUniform( material.clearcoatMap, uniforms.clearcoatMapTransform );\n\n\t\t\t}\n\n\t\t\tif ( material.clearcoatRoughnessMap ) {\n\n\t\t\t\tuniforms.clearcoatRoughnessMap.value = material.clearcoatRoughnessMap;\n\n\t\t\t\trefreshTransformUniform( material.clearcoatRoughnessMap, uniforms.clearcoatRoughnessMapTransform );\n\n\t\t\t}\n\n\t\t\tif ( material.clearcoatNormalMap ) {\n\n\t\t\t\tuniforms.clearcoatNormalMap.value = material.clearcoatNormalMap;\n\n\t\t\t\trefreshTransformUniform( material.clearcoatNormalMap, uniforms.clearcoatNormalMapTransform );\n\n\t\t\t\tuniforms.clearcoatNormalScale.value.copy( material.clearcoatNormalScale );\n\n\t\t\t\tif ( material.side === BackSide ) {\n\n\t\t\t\t\tuniforms.clearcoatNormalScale.value.negate();\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( material.dispersion > 0 ) {\n\n\t\t\tuniforms.dispersion.value = material.dispersion;\n\n\t\t}\n\n\t\tif ( material.iridescence > 0 ) {\n\n\t\t\tuniforms.iridescence.value = material.iridescence;\n\t\t\tuniforms.iridescenceIOR.value = material.iridescenceIOR;\n\t\t\tuniforms.iridescenceThicknessMinimum.value = material.iridescenceThicknessRange[ 0 ];\n\t\t\tuniforms.iridescenceThicknessMaximum.value = material.iridescenceThicknessRange[ 1 ];\n\n\t\t\tif ( material.iridescenceMap ) {\n\n\t\t\t\tuniforms.iridescenceMap.value = material.iridescenceMap;\n\n\t\t\t\trefreshTransformUniform( material.iridescenceMap, uniforms.iridescenceMapTransform );\n\n\t\t\t}\n\n\t\t\tif ( material.iridescenceThicknessMap ) {\n\n\t\t\t\tuniforms.iridescenceThicknessMap.value = material.iridescenceThicknessMap;\n\n\t\t\t\trefreshTransformUniform( material.iridescenceThicknessMap, uniforms.iridescenceThicknessMapTransform );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( material.transmission > 0 ) {\n\n\t\t\tuniforms.transmission.value = material.transmission;\n\t\t\tuniforms.transmissionSamplerMap.value = transmissionRenderTarget.texture;\n\t\t\tuniforms.transmissionSamplerSize.value.set( transmissionRenderTarget.width, transmissionRenderTarget.height );\n\n\t\t\tif ( material.transmissionMap ) {\n\n\t\t\t\tuniforms.transmissionMap.value = material.transmissionMap;\n\n\t\t\t\trefreshTransformUniform( material.transmissionMap, uniforms.transmissionMapTransform );\n\n\t\t\t}\n\n\t\t\tuniforms.thickness.value = material.thickness;\n\n\t\t\tif ( material.thicknessMap ) {\n\n\t\t\t\tuniforms.thicknessMap.value = material.thicknessMap;\n\n\t\t\t\trefreshTransformUniform( material.thicknessMap, uniforms.thicknessMapTransform );\n\n\t\t\t}\n\n\t\t\tuniforms.attenuationDistance.value = material.attenuationDistance;\n\t\t\tuniforms.attenuationColor.value.copy( material.attenuationColor );\n\n\t\t}\n\n\t\tif ( material.anisotropy > 0 ) {\n\n\t\t\tuniforms.anisotropyVector.value.set( material.anisotropy * Math.cos( material.anisotropyRotation ), material.anisotropy * Math.sin( material.anisotropyRotation ) );\n\n\t\t\tif ( material.anisotropyMap ) {\n\n\t\t\t\tuniforms.anisotropyMap.value = material.anisotropyMap;\n\n\t\t\t\trefreshTransformUniform( material.anisotropyMap, uniforms.anisotropyMapTransform );\n\n\t\t\t}\n\n\t\t}\n\n\t\tuniforms.specularIntensity.value = material.specularIntensity;\n\t\tuniforms.specularColor.value.copy( material.specularColor );\n\n\t\tif ( material.specularColorMap ) {\n\n\t\t\tuniforms.specularColorMap.value = material.specularColorMap;\n\n\t\t\trefreshTransformUniform( material.specularColorMap, uniforms.specularColorMapTransform );\n\n\t\t}\n\n\t\tif ( material.specularIntensityMap ) {\n\n\t\t\tuniforms.specularIntensityMap.value = material.specularIntensityMap;\n\n\t\t\trefreshTransformUniform( material.specularIntensityMap, uniforms.specularIntensityMapTransform );\n\n\t\t}\n\n\t}\n\n\tfunction refreshUniformsMatcap( uniforms, material ) {\n\n\t\tif ( material.matcap ) {\n\n\t\t\tuniforms.matcap.value = material.matcap;\n\n\t\t}\n\n\t}\n\n\tfunction refreshUniformsDistance( uniforms, material ) {\n\n\t\tconst light = properties.get( material ).light;\n\n\t\tuniforms.referencePosition.value.setFromMatrixPosition( light.matrixWorld );\n\t\tuniforms.nearDistance.value = light.shadow.camera.near;\n\t\tuniforms.farDistance.value = light.shadow.camera.far;\n\n\t}\n\n\treturn {\n\t\trefreshFogUniforms: refreshFogUniforms,\n\t\trefreshMaterialUniforms: refreshMaterialUniforms\n\t};\n\n}\n\nfunction WebGLUniformsGroups( gl, info, capabilities, state ) {\n\n\tlet buffers = {};\n\tlet updateList = {};\n\tlet allocatedBindingPoints = [];\n\n\tconst maxBindingPoints = gl.getParameter( gl.MAX_UNIFORM_BUFFER_BINDINGS ); // binding points are global whereas block indices are per shader program\n\n\tfunction bind( uniformsGroup, program ) {\n\n\t\tconst webglProgram = program.program;\n\t\tstate.uniformBlockBinding( uniformsGroup, webglProgram );\n\n\t}\n\n\tfunction update( uniformsGroup, program ) {\n\n\t\tlet buffer = buffers[ uniformsGroup.id ];\n\n\t\tif ( buffer === undefined ) {\n\n\t\t\tprepareUniformsGroup( uniformsGroup );\n\n\t\t\tbuffer = createBuffer( uniformsGroup );\n\t\t\tbuffers[ uniformsGroup.id ] = buffer;\n\n\t\t\tuniformsGroup.addEventListener( 'dispose', onUniformsGroupsDispose );\n\n\t\t}\n\n\t\t// ensure to update the binding points/block indices mapping for this program\n\n\t\tconst webglProgram = program.program;\n\t\tstate.updateUBOMapping( uniformsGroup, webglProgram );\n\n\t\t// update UBO once per frame\n\n\t\tconst frame = info.render.frame;\n\n\t\tif ( updateList[ uniformsGroup.id ] !== frame ) {\n\n\t\t\tupdateBufferData( uniformsGroup );\n\n\t\t\tupdateList[ uniformsGroup.id ] = frame;\n\n\t\t}\n\n\t}\n\n\tfunction createBuffer( uniformsGroup ) {\n\n\t\t// the setup of an UBO is independent of a particular shader program but global\n\n\t\tconst bindingPointIndex = allocateBindingPointIndex();\n\t\tuniformsGroup.__bindingPointIndex = bindingPointIndex;\n\n\t\tconst buffer = gl.createBuffer();\n\t\tconst size = uniformsGroup.__size;\n\t\tconst usage = uniformsGroup.usage;\n\n\t\tgl.bindBuffer( gl.UNIFORM_BUFFER, buffer );\n\t\tgl.bufferData( gl.UNIFORM_BUFFER, size, usage );\n\t\tgl.bindBuffer( gl.UNIFORM_BUFFER, null );\n\t\tgl.bindBufferBase( gl.UNIFORM_BUFFER, bindingPointIndex, buffer );\n\n\t\treturn buffer;\n\n\t}\n\n\tfunction allocateBindingPointIndex() {\n\n\t\tfor ( let i = 0; i < maxBindingPoints; i ++ ) {\n\n\t\t\tif ( allocatedBindingPoints.indexOf( i ) === -1 ) {\n\n\t\t\t\tallocatedBindingPoints.push( i );\n\t\t\t\treturn i;\n\n\t\t\t}\n\n\t\t}\n\n\t\terror( 'WebGLRenderer: Maximum number of simultaneously usable uniforms groups reached.' );\n\n\t\treturn 0;\n\n\t}\n\n\tfunction updateBufferData( uniformsGroup ) {\n\n\t\tconst buffer = buffers[ uniformsGroup.id ];\n\t\tconst uniforms = uniformsGroup.uniforms;\n\t\tconst cache = uniformsGroup.__cache;\n\n\t\tgl.bindBuffer( gl.UNIFORM_BUFFER, buffer );\n\n\t\tfor ( let i = 0, il = uniforms.length; i < il; i ++ ) {\n\n\t\t\tconst uniformArray = Array.isArray( uniforms[ i ] ) ? uniforms[ i ] : [ uniforms[ i ] ];\n\n\t\t\tfor ( let j = 0, jl = uniformArray.length; j < jl; j ++ ) {\n\n\t\t\t\tconst uniform = uniformArray[ j ];\n\n\t\t\t\tif ( hasUniformChanged( uniform, i, j, cache ) === true ) {\n\n\t\t\t\t\tconst offset = uniform.__offset;\n\n\t\t\t\t\tconst values = Array.isArray( uniform.value ) ? uniform.value : [ uniform.value ];\n\n\t\t\t\t\tlet arrayOffset = 0;\n\n\t\t\t\t\tfor ( let k = 0; k < values.length; k ++ ) {\n\n\t\t\t\t\t\tconst value = values[ k ];\n\n\t\t\t\t\t\tconst info = getUniformSize( value );\n\n\t\t\t\t\t\t// TODO add integer and struct support\n\t\t\t\t\t\tif ( typeof value === 'number' || typeof value === 'boolean' ) {\n\n\t\t\t\t\t\t\tuniform.__data[ 0 ] = value;\n\t\t\t\t\t\t\tgl.bufferSubData( gl.UNIFORM_BUFFER, offset + arrayOffset, uniform.__data );\n\n\t\t\t\t\t\t} else if ( value.isMatrix3 ) {\n\n\t\t\t\t\t\t\t// manually converting 3x3 to 3x4\n\n\t\t\t\t\t\t\tuniform.__data[ 0 ] = value.elements[ 0 ];\n\t\t\t\t\t\t\tuniform.__data[ 1 ] = value.elements[ 1 ];\n\t\t\t\t\t\t\tuniform.__data[ 2 ] = value.elements[ 2 ];\n\t\t\t\t\t\t\tuniform.__data[ 3 ] = 0;\n\t\t\t\t\t\t\tuniform.__data[ 4 ] = value.elements[ 3 ];\n\t\t\t\t\t\t\tuniform.__data[ 5 ] = value.elements[ 4 ];\n\t\t\t\t\t\t\tuniform.__data[ 6 ] = value.elements[ 5 ];\n\t\t\t\t\t\t\tuniform.__data[ 7 ] = 0;\n\t\t\t\t\t\t\tuniform.__data[ 8 ] = value.elements[ 6 ];\n\t\t\t\t\t\t\tuniform.__data[ 9 ] = value.elements[ 7 ];\n\t\t\t\t\t\t\tuniform.__data[ 10 ] = value.elements[ 8 ];\n\t\t\t\t\t\t\tuniform.__data[ 11 ] = 0;\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tvalue.toArray( uniform.__data, arrayOffset );\n\n\t\t\t\t\t\t\tarrayOffset += info.storage / Float32Array.BYTES_PER_ELEMENT;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\tgl.bufferSubData( gl.UNIFORM_BUFFER, offset, uniform.__data );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tgl.bindBuffer( gl.UNIFORM_BUFFER, null );\n\n\t}\n\n\tfunction hasUniformChanged( uniform, index, indexArray, cache ) {\n\n\t\tconst value = uniform.value;\n\t\tconst indexString = index + '_' + indexArray;\n\n\t\tif ( cache[ indexString ] === undefined ) {\n\n\t\t\t// cache entry does not exist so far\n\n\t\t\tif ( typeof value === 'number' || typeof value === 'boolean' ) {\n\n\t\t\t\tcache[ indexString ] = value;\n\n\t\t\t} else {\n\n\t\t\t\tcache[ indexString ] = value.clone();\n\n\t\t\t}\n\n\t\t\treturn true;\n\n\t\t} else {\n\n\t\t\tconst cachedObject = cache[ indexString ];\n\n\t\t\t// compare current value with cached entry\n\n\t\t\tif ( typeof value === 'number' || typeof value === 'boolean' ) {\n\n\t\t\t\tif ( cachedObject !== value ) {\n\n\t\t\t\t\tcache[ indexString ] = value;\n\t\t\t\t\treturn true;\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tif ( cachedObject.equals( value ) === false ) {\n\n\t\t\t\t\tcachedObject.copy( value );\n\t\t\t\t\treturn true;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\tfunction prepareUniformsGroup( uniformsGroup ) {\n\n\t\t// determine total buffer size according to the STD140 layout\n\t\t// Hint: STD140 is the only supported layout in WebGL 2\n\n\t\tconst uniforms = uniformsGroup.uniforms;\n\n\t\tlet offset = 0; // global buffer offset in bytes\n\t\tconst chunkSize = 16; // size of a chunk in bytes\n\n\t\tfor ( let i = 0, l = uniforms.length; i < l; i ++ ) {\n\n\t\t\tconst uniformArray = Array.isArray( uniforms[ i ] ) ? uniforms[ i ] : [ uniforms[ i ] ];\n\n\t\t\tfor ( let j = 0, jl = uniformArray.length; j < jl; j ++ ) {\n\n\t\t\t\tconst uniform = uniformArray[ j ];\n\n\t\t\t\tconst values = Array.isArray( uniform.value ) ? uniform.value : [ uniform.value ];\n\n\t\t\t\tfor ( let k = 0, kl = values.length; k < kl; k ++ ) {\n\n\t\t\t\t\tconst value = values[ k ];\n\n\t\t\t\t\tconst info = getUniformSize( value );\n\n\t\t\t\t\tconst chunkOffset = offset % chunkSize; // offset in the current chunk\n\t\t\t\t\tconst chunkPadding = chunkOffset % info.boundary; // required padding to match boundary\n\t\t\t\t\tconst chunkStart = chunkOffset + chunkPadding; // the start position in the current chunk for the data\n\n\t\t\t\t\toffset += chunkPadding;\n\n\t\t\t\t\t// Check for chunk overflow\n\t\t\t\t\tif ( chunkStart !== 0 && ( chunkSize - chunkStart ) < info.storage ) {\n\n\t\t\t\t\t\t// Add padding and adjust offset\n\t\t\t\t\t\toffset += ( chunkSize - chunkStart );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// the following two properties will be used for partial buffer updates\n\t\t\t\t\tuniform.__data = new Float32Array( info.storage / Float32Array.BYTES_PER_ELEMENT );\n\t\t\t\t\tuniform.__offset = offset;\n\n\t\t\t\t\t// Update the global offset\n\t\t\t\t\toffset += info.storage;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t// ensure correct final padding\n\n\t\tconst chunkOffset = offset % chunkSize;\n\n\t\tif ( chunkOffset > 0 ) offset += ( chunkSize - chunkOffset );\n\n\t\t//\n\n\t\tuniformsGroup.__size = offset;\n\t\tuniformsGroup.__cache = {};\n\n\t\treturn this;\n\n\t}\n\n\tfunction getUniformSize( value ) {\n\n\t\tconst info = {\n\t\t\tboundary: 0, // bytes\n\t\t\tstorage: 0 // bytes\n\t\t};\n\n\t\t// determine sizes according to STD140\n\n\t\tif ( typeof value === 'number' || typeof value === 'boolean' ) {\n\n\t\t\t// float/int/bool\n\n\t\t\tinfo.boundary = 4;\n\t\t\tinfo.storage = 4;\n\n\t\t} else if ( value.isVector2 ) {\n\n\t\t\t// vec2\n\n\t\t\tinfo.boundary = 8;\n\t\t\tinfo.storage = 8;\n\n\t\t} else if ( value.isVector3 || value.isColor ) {\n\n\t\t\t// vec3\n\n\t\t\tinfo.boundary = 16;\n\t\t\tinfo.storage = 12; // evil: vec3 must start on a 16-byte boundary but it only consumes 12 bytes\n\n\t\t} else if ( value.isVector4 ) {\n\n\t\t\t// vec4\n\n\t\t\tinfo.boundary = 16;\n\t\t\tinfo.storage = 16;\n\n\t\t} else if ( value.isMatrix3 ) {\n\n\t\t\t// mat3 (in STD140 a 3x3 matrix is represented as 3x4)\n\n\t\t\tinfo.boundary = 48;\n\t\t\tinfo.storage = 48;\n\n\t\t} else if ( value.isMatrix4 ) {\n\n\t\t\t// mat4\n\n\t\t\tinfo.boundary = 64;\n\t\t\tinfo.storage = 64;\n\n\t\t} else if ( value.isTexture ) {\n\n\t\t\twarn( 'WebGLRenderer: Texture samplers can not be part of an uniforms group.' );\n\n\t\t} else {\n\n\t\t\twarn( 'WebGLRenderer: Unsupported uniform value type.', value );\n\n\t\t}\n\n\t\treturn info;\n\n\t}\n\n\tfunction onUniformsGroupsDispose( event ) {\n\n\t\tconst uniformsGroup = event.target;\n\n\t\tuniformsGroup.removeEventListener( 'dispose', onUniformsGroupsDispose );\n\n\t\tconst index = allocatedBindingPoints.indexOf( uniformsGroup.__bindingPointIndex );\n\t\tallocatedBindingPoints.splice( index, 1 );\n\n\t\tgl.deleteBuffer( buffers[ uniformsGroup.id ] );\n\n\t\tdelete buffers[ uniformsGroup.id ];\n\t\tdelete updateList[ uniformsGroup.id ];\n\n\t}\n\n\tfunction dispose() {\n\n\t\tfor ( const id in buffers ) {\n\n\t\t\tgl.deleteBuffer( buffers[ id ] );\n\n\t\t}\n\n\t\tallocatedBindingPoints = [];\n\t\tbuffers = {};\n\t\tupdateList = {};\n\n\t}\n\n\treturn {\n\n\t\tbind: bind,\n\t\tupdate: update,\n\n\t\tdispose: dispose\n\n\t};\n\n}\n\n/**\n * Precomputed DFG LUT for Image-Based Lighting\n * Resolution: 16x16\n * Samples: 4096 per texel\n * Format: RG16F (2 half floats per texel: scale, bias)\n */\n\n\nconst DATA = new Uint16Array( [\n\t0x30b5, 0x3ad1, 0x314c, 0x3a4d, 0x33d2, 0x391c, 0x35ef, 0x3828, 0x37f3, 0x36a6, 0x38d1, 0x3539, 0x3979, 0x3410, 0x39f8, 0x3252, 0x3a53, 0x30f0, 0x3a94, 0x2fc9, 0x3abf, 0x2e35, 0x3ada, 0x2d05, 0x3ae8, 0x2c1f, 0x3aed, 0x2ae0, 0x3aea, 0x29d1, 0x3ae1, 0x28ff,\n\t0x3638, 0x38e4, 0x364a, 0x38ce, 0x3699, 0x385e, 0x374e, 0x372c, 0x3839, 0x35a4, 0x38dc, 0x3462, 0x396e, 0x32c4, 0x39de, 0x3134, 0x3a2b, 0x3003, 0x3a59, 0x2e3a, 0x3a6d, 0x2ce1, 0x3a6e, 0x2bba, 0x3a5f, 0x2a33, 0x3a49, 0x290a, 0x3a2d, 0x2826, 0x3a0a, 0x26e8,\n\t0x3894, 0x36d7, 0x3897, 0x36c9, 0x38a3, 0x3675, 0x38bc, 0x35ac, 0x38ee, 0x349c, 0x393e, 0x3332, 0x3997, 0x3186, 0x39e2, 0x3038, 0x3a13, 0x2e75, 0x3a29, 0x2cf5, 0x3a2d, 0x2bac, 0x3a21, 0x29ff, 0x3a04, 0x28bc, 0x39dc, 0x2790, 0x39ad, 0x261a, 0x3978, 0x24fa,\n\t0x39ac, 0x34a8, 0x39ac, 0x34a3, 0x39ae, 0x3480, 0x39ae, 0x3423, 0x39b1, 0x330e, 0x39c2, 0x31a9, 0x39e0, 0x3063, 0x39fc, 0x2eb5, 0x3a0c, 0x2d1d, 0x3a14, 0x2bcf, 0x3a07, 0x29ff, 0x39e9, 0x28a3, 0x39be, 0x273c, 0x3989, 0x25b3, 0x394a, 0x2488, 0x3907, 0x2345,\n\t0x3a77, 0x3223, 0x3a76, 0x321f, 0x3a73, 0x3204, 0x3a6a, 0x31b3, 0x3a58, 0x3114, 0x3a45, 0x303b, 0x3a34, 0x2eb6, 0x3a26, 0x2d31, 0x3a1e, 0x2bef, 0x3a0b, 0x2a0d, 0x39ec, 0x28a1, 0x39c0, 0x271b, 0x3987, 0x2580, 0x3944, 0x2449, 0x38fa, 0x22bd, 0x38ac, 0x2155,\n\t0x3b07, 0x2fca, 0x3b06, 0x2fca, 0x3b00, 0x2fb8, 0x3af4, 0x2f7c, 0x3adb, 0x2eea, 0x3ab4, 0x2e00, 0x3a85, 0x2cec, 0x3a5e, 0x2bc5, 0x3a36, 0x2a00, 0x3a0d, 0x2899, 0x39dc, 0x2707, 0x39a0, 0x2562, 0x395a, 0x2424, 0x390b, 0x2268, 0x38b7, 0x20fd, 0x385f, 0x1fd1,\n\t0x3b69, 0x2cb9, 0x3b68, 0x2cbb, 0x3b62, 0x2cbb, 0x3b56, 0x2cae, 0x3b3b, 0x2c78, 0x3b0d, 0x2c0a, 0x3acf, 0x2ae3, 0x3a92, 0x2998, 0x3a54, 0x2867, 0x3a17, 0x26d0, 0x39d3, 0x253c, 0x3989, 0x2402, 0x3935, 0x2226, 0x38dc, 0x20bd, 0x387d, 0x1f54, 0x381d, 0x1db3,\n\t0x3ba9, 0x296b, 0x3ba8, 0x296f, 0x3ba3, 0x297b, 0x3b98, 0x2987, 0x3b7f, 0x2976, 0x3b4e, 0x2927, 0x3b0e, 0x2895, 0x3ac2, 0x27b7, 0x3a73, 0x263b, 0x3a23, 0x24e7, 0x39d0, 0x239b, 0x3976, 0x21d9, 0x3917, 0x207e, 0x38b2, 0x1ee7, 0x384b, 0x1d53, 0x37c7, 0x1c1e,\n\t0x3bd2, 0x25cb, 0x3bd1, 0x25d3, 0x3bcd, 0x25f0, 0x3bc2, 0x261f, 0x3bad, 0x2645, 0x3b7d, 0x262d, 0x3b3e, 0x25c4, 0x3aec, 0x250f, 0x3a93, 0x243a, 0x3a32, 0x22ce, 0x39d0, 0x215b, 0x3969, 0x202a, 0x38fe, 0x1e6e, 0x388f, 0x1cf1, 0x381f, 0x1b9b, 0x3762, 0x19dd,\n\t0x3be9, 0x21ab, 0x3be9, 0x21b7, 0x3be5, 0x21e5, 0x3bdd, 0x2241, 0x3bc9, 0x22a7, 0x3ba0, 0x22ec, 0x3b62, 0x22cd, 0x3b0f, 0x2247, 0x3aae, 0x2175, 0x3a44, 0x2088, 0x39d4, 0x1f49, 0x3960, 0x1dbe, 0x38e9, 0x1c77, 0x3870, 0x1ae8, 0x37f1, 0x1953, 0x3708, 0x181b,\n\t0x3bf6, 0x1cea, 0x3bf6, 0x1cfb, 0x3bf3, 0x1d38, 0x3bec, 0x1dbd, 0x3bda, 0x1e7c, 0x3bb7, 0x1f25, 0x3b7d, 0x1f79, 0x3b2c, 0x1f4c, 0x3ac6, 0x1ea6, 0x3a55, 0x1dbb, 0x39da, 0x1cbd, 0x395a, 0x1b9d, 0x38d8, 0x1a00, 0x3855, 0x18ac, 0x37ab, 0x173c, 0x36b7, 0x1598,\n\t0x3bfc, 0x1736, 0x3bfc, 0x1759, 0x3bf9, 0x17e7, 0x3bf4, 0x1896, 0x3be4, 0x1997, 0x3bc6, 0x1aa8, 0x3b91, 0x1b84, 0x3b43, 0x1bd2, 0x3ade, 0x1b8a, 0x3a65, 0x1acd, 0x39e2, 0x19d3, 0x3957, 0x18cd, 0x38ca, 0x17b3, 0x383e, 0x1613, 0x376d, 0x14bf, 0x366f, 0x135e,\n\t0x3bff, 0x101b, 0x3bff, 0x1039, 0x3bfc, 0x10c8, 0x3bf9, 0x1226, 0x3bea, 0x1428, 0x3bcf, 0x1584, 0x3b9f, 0x16c5, 0x3b54, 0x179a, 0x3af0, 0x17ce, 0x3a76, 0x1771, 0x39ea, 0x16a4, 0x3956, 0x15a7, 0x38bf, 0x14a7, 0x3829, 0x1379, 0x3735, 0x11ea, 0x362d, 0x10a1,\n\t0x3c00, 0x061b, 0x3c00, 0x066a, 0x3bfe, 0x081c, 0x3bfa, 0x0a4c, 0x3bed, 0x0d16, 0x3bd5, 0x0fb3, 0x3ba9, 0x114d, 0x3b63, 0x127c, 0x3b01, 0x132f, 0x3a85, 0x1344, 0x39f4, 0x12d2, 0x3957, 0x120d, 0x38b5, 0x1122, 0x3817, 0x103c, 0x3703, 0x0ed3, 0x35f0, 0x0d6d,\n\t0x3c00, 0x007a, 0x3c00, 0x0089, 0x3bfe, 0x011d, 0x3bfb, 0x027c, 0x3bf0, 0x04fa, 0x3bda, 0x0881, 0x3bb1, 0x0acd, 0x3b6f, 0x0c97, 0x3b10, 0x0d7b, 0x3a93, 0x0df1, 0x39fe, 0x0def, 0x3959, 0x0d8a, 0x38af, 0x0ce9, 0x3808, 0x0c31, 0x36d5, 0x0af0, 0x35b9, 0x09a3,\n\t0x3c00, 0x0000, 0x3c00, 0x0001, 0x3bff, 0x0015, 0x3bfb, 0x0059, 0x3bf2, 0x00fd, 0x3bdd, 0x01df, 0x3bb7, 0x031c, 0x3b79, 0x047c, 0x3b1d, 0x05d4, 0x3aa0, 0x06d5, 0x3a08, 0x075a, 0x395d, 0x075e, 0x38aa, 0x06f7, 0x37f4, 0x0648, 0x36ac, 0x0576, 0x3586, 0x049f\n] );\n\nlet lut = null;\n\nfunction getDFGLUT() {\n\n\tif ( lut === null ) {\n\n\t\tlut = new DataTexture( DATA, 16, 16, RGFormat, HalfFloatType );\n\t\tlut.name = 'DFG_LUT';\n\t\tlut.minFilter = LinearFilter;\n\t\tlut.magFilter = LinearFilter;\n\t\tlut.wrapS = ClampToEdgeWrapping;\n\t\tlut.wrapT = ClampToEdgeWrapping;\n\t\tlut.generateMipmaps = false;\n\t\tlut.needsUpdate = true;\n\n\t}\n\n\treturn lut;\n\n}\n\n/**\n * This renderer uses WebGL 2 to display scenes.\n *\n * WebGL 1 is not supported since `r163`.\n */\nclass WebGLRenderer {\n\n\t/**\n\t * Constructs a new WebGL renderer.\n\t *\n\t * @param {WebGLRenderer~Options} [parameters] - The configuration parameter.\n\t */\n\tconstructor( parameters = {} ) {\n\n\t\tconst {\n\t\t\tcanvas = createCanvasElement(),\n\t\t\tcontext = null,\n\t\t\tdepth = true,\n\t\t\tstencil = false,\n\t\t\talpha = false,\n\t\t\tantialias = false,\n\t\t\tpremultipliedAlpha = true,\n\t\t\tpreserveDrawingBuffer = false,\n\t\t\tpowerPreference = 'default',\n\t\t\tfailIfMajorPerformanceCaveat = false,\n\t\t\treversedDepthBuffer = false,\n\t\t\toutputBufferType = UnsignedByteType,\n\t\t} = parameters;\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isWebGLRenderer = true;\n\n\t\tlet _alpha;\n\n\t\tif ( context !== null ) {\n\n\t\t\tif ( typeof WebGLRenderingContext !== 'undefined' && context instanceof WebGLRenderingContext ) {\n\n\t\t\t\tthrow new Error( 'THREE.WebGLRenderer: WebGL 1 is not supported since r163.' );\n\n\t\t\t}\n\n\t\t\t_alpha = context.getContextAttributes().alpha;\n\n\t\t} else {\n\n\t\t\t_alpha = alpha;\n\n\t\t}\n\n\t\tconst _outputBufferType = outputBufferType;\n\n\t\tconst INTEGER_FORMATS = new Set( [\n\t\t\tRGBAIntegerFormat,\n\t\t\tRGIntegerFormat,\n\t\t\tRedIntegerFormat\n\t\t] );\n\n\t\tconst UNSIGNED_TYPES = new Set( [\n\t\t\tUnsignedByteType,\n\t\t\tUnsignedIntType,\n\t\t\tUnsignedShortType,\n\t\t\tUnsignedInt248Type,\n\t\t\tUnsignedShort4444Type,\n\t\t\tUnsignedShort5551Type\n\t\t] );\n\n\t\tconst uintClearColor = new Uint32Array( 4 );\n\t\tconst intClearColor = new Int32Array( 4 );\n\n\t\tlet currentRenderList = null;\n\t\tlet currentRenderState = null;\n\n\t\t// render() can be called from within a callback triggered by another render.\n\t\t// We track this so that the nested render call gets its list and state isolated from the parent render call.\n\n\t\tconst renderListStack = [];\n\t\tconst renderStateStack = [];\n\n\t\t// internal render target for non-UnsignedByteType color buffer\n\n\t\tlet output = null;\n\n\t\t// public properties\n\n\t\t/**\n\t\t * A canvas where the renderer draws its output.This is automatically created by the renderer\n\t\t * in the constructor (if not provided already); you just need to add it to your page like so:\n\t\t * ```js\n\t\t * document.body.appendChild( renderer.domElement );\n\t\t * ```\n\t\t *\n\t\t * @type {HTMLCanvasElement|OffscreenCanvas}\n\t\t */\n\t\tthis.domElement = canvas;\n\n\t\t/**\n\t\t * A object with debug configuration settings.\n\t\t *\n\t\t * - `checkShaderErrors`: If it is `true`, defines whether material shader programs are\n\t\t * checked for errors during compilation and linkage process. It may be useful to disable\n\t\t * this check in production for performance gain. It is strongly recommended to keep these\n\t\t * checks enabled during development. If the shader does not compile and link - it will not\n\t\t * work and associated material will not render.\n\t\t * - `onShaderError(gl, program, glVertexShader,glFragmentShader)`: A callback function that\n\t\t * can be used for custom error reporting. The callback receives the WebGL context, an instance\n\t\t * of WebGLProgram as well two instances of WebGLShader representing the vertex and fragment shader.\n\t\t * Assigning a custom function disables the default error reporting.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tthis.debug = {\n\n\t\t\t/**\n\t\t\t * Enables error checking and reporting when shader programs are being compiled.\n\t\t\t * @type {boolean}\n\t\t\t */\n\t\t\tcheckShaderErrors: true,\n\t\t\t/**\n\t\t\t * Callback for custom error reporting.\n\t\t\t * @type {?Function}\n\t\t\t */\n\t\t\tonShaderError: null\n\t\t};\n\n\t\t// clearing\n\n\t\t/**\n\t\t * Whether the renderer should automatically clear its output before rendering a frame or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.autoClear = true;\n\n\t\t/**\n\t\t * If {@link WebGLRenderer#autoClear} set to `true`, whether the renderer should clear\n\t\t * the color buffer or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.autoClearColor = true;\n\n\t\t/**\n\t\t * If {@link WebGLRenderer#autoClear} set to `true`, whether the renderer should clear\n\t\t * the depth buffer or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.autoClearDepth = true;\n\n\t\t/**\n\t\t * If {@link WebGLRenderer#autoClear} set to `true`, whether the renderer should clear\n\t\t * the stencil buffer or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.autoClearStencil = true;\n\n\t\t// scene graph\n\n\t\t/**\n\t\t * Whether the renderer should sort objects or not.\n\t\t *\n\t\t * Note: Sorting is used to attempt to properly render objects that have some\n\t\t * degree of transparency. By definition, sorting objects may not work in all\n\t\t * cases. Depending on the needs of application, it may be necessary to turn\n\t\t * off sorting and use other methods to deal with transparency rendering e.g.\n\t\t * manually determining each object's rendering order.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.sortObjects = true;\n\n\t\t// user-defined clipping\n\n\t\t/**\n\t\t * User-defined clipping planes specified in world space. These planes apply globally.\n\t\t * Points in space whose dot product with the plane is negative are cut away.\n\t\t *\n\t\t * @type {Array<Plane>}\n\t\t */\n\t\tthis.clippingPlanes = [];\n\n\t\t/**\n\t\t * Whether the renderer respects object-level clipping planes or not.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @default false\n\t\t */\n\t\tthis.localClippingEnabled = false;\n\n\t\t// tone mapping\n\n\t\t/**\n\t\t * The tone mapping technique of the renderer.\n\t\t *\n\t\t * @type {(NoToneMapping|LinearToneMapping|ReinhardToneMapping|CineonToneMapping|ACESFilmicToneMapping|CustomToneMapping|AgXToneMapping|NeutralToneMapping)}\n\t\t * @default NoToneMapping\n\t\t */\n\t\tthis.toneMapping = NoToneMapping;\n\n\t\t/**\n\t\t * Exposure level of tone mapping.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.toneMappingExposure = 1.0;\n\n\t\t// transmission\n\n\t\t/**\n\t\t * The normalized resolution scale for the transmission render target, measured in percentage\n\t\t * of viewport dimensions. Lowering this value can result in significant performance improvements\n\t\t * when using {@link MeshPhysicalMaterial#transmission}.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 1\n\t\t */\n\t\tthis.transmissionResolutionScale = 1.0;\n\n\t\t// internal properties\n\n\t\tconst _this = this;\n\n\t\tlet _isContextLost = false;\n\n\t\t// internal state cache\n\n\t\tthis._outputColorSpace = SRGBColorSpace;\n\n\t\tlet _currentActiveCubeFace = 0;\n\t\tlet _currentActiveMipmapLevel = 0;\n\t\tlet _currentRenderTarget = null;\n\t\tlet _currentMaterialId = -1;\n\n\t\tlet _currentCamera = null;\n\n\t\tconst _currentViewport = new Vector4();\n\t\tconst _currentScissor = new Vector4();\n\t\tlet _currentScissorTest = null;\n\n\t\tconst _currentClearColor = new Color( 0x000000 );\n\t\tlet _currentClearAlpha = 0;\n\n\t\t//\n\n\t\tlet _width = canvas.width;\n\t\tlet _height = canvas.height;\n\n\t\tlet _pixelRatio = 1;\n\t\tlet _opaqueSort = null;\n\t\tlet _transparentSort = null;\n\n\t\tconst _viewport = new Vector4( 0, 0, _width, _height );\n\t\tconst _scissor = new Vector4( 0, 0, _width, _height );\n\t\tlet _scissorTest = false;\n\n\t\t// frustum\n\n\t\tconst _frustum = new Frustum();\n\n\t\t// clipping\n\n\t\tlet _clippingEnabled = false;\n\t\tlet _localClippingEnabled = false;\n\n\t\t// camera matrices cache\n\n\t\tconst _projScreenMatrix = new Matrix4();\n\n\t\tconst _vector3 = new Vector3();\n\n\t\tconst _vector4 = new Vector4();\n\n\t\tconst _emptyScene = { background: null, fog: null, environment: null, overrideMaterial: null, isScene: true };\n\n\t\tlet _renderBackground = false;\n\n\t\tfunction getTargetPixelRatio() {\n\n\t\t\treturn _currentRenderTarget === null ? _pixelRatio : 1;\n\n\t\t}\n\n\t\t// initialize\n\n\t\tlet _gl = context;\n\n\t\tfunction getContext( contextName, contextAttributes ) {\n\n\t\t\treturn canvas.getContext( contextName, contextAttributes );\n\n\t\t}\n\n\t\ttry {\n\n\t\t\tconst contextAttributes = {\n\t\t\t\talpha: true,\n\t\t\t\tdepth,\n\t\t\t\tstencil,\n\t\t\t\tantialias,\n\t\t\t\tpremultipliedAlpha,\n\t\t\t\tpreserveDrawingBuffer,\n\t\t\t\tpowerPreference,\n\t\t\t\tfailIfMajorPerformanceCaveat,\n\t\t\t};\n\n\t\t\t// OffscreenCanvas does not have setAttribute, see #22811\n\t\t\tif ( 'setAttribute' in canvas ) canvas.setAttribute( 'data-engine', `three.js r${REVISION}` );\n\n\t\t\t// event listeners must be registered before WebGL context is created, see #12753\n\t\t\tcanvas.addEventListener( 'webglcontextlost', onContextLost, false );\n\t\t\tcanvas.addEventListener( 'webglcontextrestored', onContextRestore, false );\n\t\t\tcanvas.addEventListener( 'webglcontextcreationerror', onContextCreationError, false );\n\n\t\t\tif ( _gl === null ) {\n\n\t\t\t\tconst contextName = 'webgl2';\n\n\t\t\t\t_gl = getContext( contextName, contextAttributes );\n\n\t\t\t\tif ( _gl === null ) {\n\n\t\t\t\t\tif ( getContext( contextName ) ) {\n\n\t\t\t\t\t\tthrow new Error( 'Error creating WebGL context with your selected attributes.' );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tthrow new Error( 'Error creating WebGL context.' );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} catch ( e ) {\n\n\t\t\terror( 'WebGLRenderer: ' + e.message );\n\t\t\tthrow e;\n\n\t\t}\n\n\t\tlet extensions, capabilities, state, info;\n\t\tlet properties, textures, cubemaps, cubeuvmaps, attributes, geometries, objects;\n\t\tlet programCache, materials, renderLists, renderStates, clipping, shadowMap;\n\n\t\tlet background, morphtargets, bufferRenderer, indexedBufferRenderer;\n\n\t\tlet utils, bindingStates, uniformsGroups;\n\n\t\tfunction initGLContext() {\n\n\t\t\textensions = new WebGLExtensions( _gl );\n\t\t\textensions.init();\n\n\t\t\tutils = new WebGLUtils( _gl, extensions );\n\n\t\t\tcapabilities = new WebGLCapabilities( _gl, extensions, parameters, utils );\n\n\t\t\tstate = new WebGLState( _gl, extensions );\n\n\t\t\tif ( capabilities.reversedDepthBuffer && reversedDepthBuffer ) {\n\n\t\t\t\tstate.buffers.depth.setReversed( true );\n\n\t\t\t}\n\n\t\t\tinfo = new WebGLInfo( _gl );\n\t\t\tproperties = new WebGLProperties();\n\t\t\ttextures = new WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info );\n\t\t\tcubemaps = new WebGLCubeMaps( _this );\n\t\t\tcubeuvmaps = new WebGLCubeUVMaps( _this );\n\t\t\tattributes = new WebGLAttributes( _gl );\n\t\t\tbindingStates = new WebGLBindingStates( _gl, attributes );\n\t\t\tgeometries = new WebGLGeometries( _gl, attributes, info, bindingStates );\n\t\t\tobjects = new WebGLObjects( _gl, geometries, attributes, info );\n\t\t\tmorphtargets = new WebGLMorphtargets( _gl, capabilities, textures );\n\t\t\tclipping = new WebGLClipping( properties );\n\t\t\tprogramCache = new WebGLPrograms( _this, cubemaps, cubeuvmaps, extensions, capabilities, bindingStates, clipping );\n\t\t\tmaterials = new WebGLMaterials( _this, properties );\n\t\t\trenderLists = new WebGLRenderLists();\n\t\t\trenderStates = new WebGLRenderStates( extensions );\n\t\t\tbackground = new WebGLBackground( _this, cubemaps, cubeuvmaps, state, objects, _alpha, premultipliedAlpha );\n\t\t\tshadowMap = new WebGLShadowMap( _this, objects, capabilities );\n\t\t\tuniformsGroups = new WebGLUniformsGroups( _gl, info, capabilities, state );\n\n\t\t\tbufferRenderer = new WebGLBufferRenderer( _gl, extensions, info );\n\t\t\tindexedBufferRenderer = new WebGLIndexedBufferRenderer( _gl, extensions, info );\n\n\t\t\tinfo.programs = programCache.programs;\n\n\t\t\t/**\n\t\t\t * Holds details about the capabilities of the current rendering context.\n\t\t\t *\n\t\t\t * @name WebGLRenderer#capabilities\n\t\t\t * @type {WebGLRenderer~Capabilities}\n\t\t\t */\n\t\t\t_this.capabilities = capabilities;\n\n\t\t\t/**\n\t\t\t * Provides methods for retrieving and testing WebGL extensions.\n\t\t\t *\n\t\t\t * - `get(extensionName:string)`: Used to check whether a WebGL extension is supported\n\t\t\t * and return the extension object if available.\n\t\t\t * - `has(extensionName:string)`: returns `true` if the extension is supported.\n\t\t\t *\n\t\t\t * @name WebGLRenderer#extensions\n\t\t\t * @type {Object}\n\t\t\t */\n\t\t\t_this.extensions = extensions;\n\n\t\t\t/**\n\t\t\t * Used to track properties of other objects like native WebGL objects.\n\t\t\t *\n\t\t\t * @name WebGLRenderer#properties\n\t\t\t * @type {Object}\n\t\t\t */\n\t\t\t_this.properties = properties;\n\n\t\t\t/**\n\t\t\t * Manages the render lists of the renderer.\n\t\t\t *\n\t\t\t * @name WebGLRenderer#renderLists\n\t\t\t * @type {Object}\n\t\t\t */\n\t\t\t_this.renderLists = renderLists;\n\n\n\n\t\t\t/**\n\t\t\t * Interface for managing shadows.\n\t\t\t *\n\t\t\t * @name WebGLRenderer#shadowMap\n\t\t\t * @type {WebGLRenderer~ShadowMap}\n\t\t\t */\n\t\t\t_this.shadowMap = shadowMap;\n\n\t\t\t/**\n\t\t\t * Interface for managing the WebGL state.\n\t\t\t *\n\t\t\t * @name WebGLRenderer#state\n\t\t\t * @type {Object}\n\t\t\t */\n\t\t\t_this.state = state;\n\n\t\t\t/**\n\t\t\t * Holds a series of statistical information about the GPU memory\n\t\t\t * and the rendering process. Useful for debugging and monitoring.\n\t\t\t *\n\t\t\t * By default these data are reset at each render call but when having\n\t\t\t * multiple render passes per frame (e.g. when using post processing) it can\n\t\t\t * be preferred to reset with a custom pattern. First, set `autoReset` to\n\t\t\t * `false`.\n\t\t\t * ```js\n\t\t\t * renderer.info.autoReset = false;\n\t\t\t * ```\n\t\t\t * Call `reset()` whenever you have finished to render a single frame.\n\t\t\t * ```js\n\t\t\t * renderer.info.reset();\n\t\t\t * ```\n\t\t\t *\n\t\t\t * @name WebGLRenderer#info\n\t\t\t * @type {WebGLRenderer~Info}\n\t\t\t */\n\t\t\t_this.info = info;\n\n\t\t}\n\n\t\tinitGLContext();\n\n\t\t// initialize internal render target for non-UnsignedByteType color buffer\n\n\t\tif ( _outputBufferType !== UnsignedByteType ) {\n\n\t\t\toutput = new WebGLOutput( _outputBufferType, canvas.width, canvas.height, depth, stencil );\n\n\t\t}\n\n\t\t// xr\n\n\t\tconst xr = new WebXRManager( _this, _gl );\n\n\t\t/**\n\t\t * A reference to the XR manager.\n\t\t *\n\t\t * @type {WebXRManager}\n\t\t */\n\t\tthis.xr = xr;\n\n\t\t/**\n\t\t * Returns the rendering context.\n\t\t *\n\t\t * @return {WebGL2RenderingContext} The rendering context.\n\t\t */\n\t\tthis.getContext = function () {\n\n\t\t\treturn _gl;\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the rendering context attributes.\n\t\t *\n\t\t * @return {WebGLContextAttributes} The rendering context attributes.\n\t\t */\n\t\tthis.getContextAttributes = function () {\n\n\t\t\treturn _gl.getContextAttributes();\n\n\t\t};\n\n\t\t/**\n\t\t * Simulates a loss of the WebGL context. This requires support for the `WEBGL_lose_context` extension.\n\t\t */\n\t\tthis.forceContextLoss = function () {\n\n\t\t\tconst extension = extensions.get( 'WEBGL_lose_context' );\n\t\t\tif ( extension ) extension.loseContext();\n\n\t\t};\n\n\t\t/**\n\t\t * Simulates a restore of the WebGL context. This requires support for the `WEBGL_lose_context` extension.\n\t\t */\n\t\tthis.forceContextRestore = function () {\n\n\t\t\tconst extension = extensions.get( 'WEBGL_lose_context' );\n\t\t\tif ( extension ) extension.restoreContext();\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the pixel ratio.\n\t\t *\n\t\t * @return {number} The pixel ratio.\n\t\t */\n\t\tthis.getPixelRatio = function () {\n\n\t\t\treturn _pixelRatio;\n\n\t\t};\n\n\t\t/**\n\t\t * Sets the given pixel ratio and resizes the canvas if necessary.\n\t\t *\n\t\t * @param {number} value - The pixel ratio.\n\t\t */\n\t\tthis.setPixelRatio = function ( value ) {\n\n\t\t\tif ( value === undefined ) return;\n\n\t\t\t_pixelRatio = value;\n\n\t\t\tthis.setSize( _width, _height, false );\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the renderer's size in logical pixels. This method does not honor the pixel ratio.\n\t\t *\n\t\t * @param {Vector2} target - The method writes the result in this target object.\n\t\t * @return {Vector2} The renderer's size in logical pixels.\n\t\t */\n\t\tthis.getSize = function ( target ) {\n\n\t\t\treturn target.set( _width, _height );\n\n\t\t};\n\n\t\t/**\n\t\t * Resizes the output canvas to (width, height) with device pixel ratio taken\n\t\t * into account, and also sets the viewport to fit that size, starting in (0,\n\t\t * 0). Setting `updateStyle` to false prevents any style changes to the output canvas.\n\t\t *\n\t\t * @param {number} width - The width in logical pixels.\n\t\t * @param {number} height - The height in logical pixels.\n\t\t * @param {boolean} [updateStyle=true] - Whether to update the `style` attribute of the canvas or not.\n\t\t */\n\t\tthis.setSize = function ( width, height, updateStyle = true ) {\n\n\t\t\tif ( xr.isPresenting ) {\n\n\t\t\t\twarn( 'WebGLRenderer: Can\\'t change size while VR device is presenting.' );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\t_width = width;\n\t\t\t_height = height;\n\n\t\t\tcanvas.width = Math.floor( width * _pixelRatio );\n\t\t\tcanvas.height = Math.floor( height * _pixelRatio );\n\n\t\t\tif ( updateStyle === true ) {\n\n\t\t\t\tcanvas.style.width = width + 'px';\n\t\t\t\tcanvas.style.height = height + 'px';\n\n\t\t\t}\n\n\t\t\tif ( output !== null ) {\n\n\t\t\t\toutput.setSize( canvas.width, canvas.height );\n\n\t\t\t}\n\n\t\t\tthis.setViewport( 0, 0, width, height );\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the drawing buffer size in physical pixels. This method honors the pixel ratio.\n\t\t *\n\t\t * @param {Vector2} target - The method writes the result in this target object.\n\t\t * @return {Vector2} The drawing buffer size.\n\t\t */\n\t\tthis.getDrawingBufferSize = function ( target ) {\n\n\t\t\treturn target.set( _width * _pixelRatio, _height * _pixelRatio ).floor();\n\n\t\t};\n\n\t\t/**\n\t\t * This method allows to define the drawing buffer size by specifying\n\t\t * width, height and pixel ratio all at once. The size of the drawing\n\t\t * buffer is computed with this formula:\n\t\t * ```js\n\t\t * size.x = width * pixelRatio;\n\t\t * size.y = height * pixelRatio;\n\t\t * ```\n\t\t *\n\t\t * @param {number} width - The width in logical pixels.\n\t\t * @param {number} height - The height in logical pixels.\n\t\t * @param {number} pixelRatio - The pixel ratio.\n\t\t */\n\t\tthis.setDrawingBufferSize = function ( width, height, pixelRatio ) {\n\n\t\t\t_width = width;\n\t\t\t_height = height;\n\n\t\t\t_pixelRatio = pixelRatio;\n\n\t\t\tcanvas.width = Math.floor( width * pixelRatio );\n\t\t\tcanvas.height = Math.floor( height * pixelRatio );\n\n\t\t\tthis.setViewport( 0, 0, width, height );\n\n\t\t};\n\n\t\t/**\n\t\t * Sets the post-processing effects to be applied after rendering.\n\t\t *\n\t\t * @param {Array} effects - An array of post-processing effects.\n\t\t */\n\t\tthis.setEffects = function ( effects ) {\n\n\t\t\tif ( _outputBufferType === UnsignedByteType ) {\n\n\t\t\t\tconsole.error( 'THREE.WebGLRenderer: setEffects() requires outputBufferType set to HalfFloatType or FloatType.' );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tif ( effects ) {\n\n\t\t\t\tfor ( let i = 0; i < effects.length; i ++ ) {\n\n\t\t\t\t\tif ( effects[ i ].isOutputPass === true ) {\n\n\t\t\t\t\t\tconsole.warn( 'THREE.WebGLRenderer: OutputPass is not needed in setEffects(). Tone mapping and color space conversion are applied automatically.' );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\toutput.setEffects( effects || [] );\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the current viewport definition.\n\t\t *\n\t\t * @param {Vector2} target - The method writes the result in this target object.\n\t\t * @return {Vector2} The current viewport definition.\n\t\t */\n\t\tthis.getCurrentViewport = function ( target ) {\n\n\t\t\treturn target.copy( _currentViewport );\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the viewport definition.\n\t\t *\n\t\t * @param {Vector4} target - The method writes the result in this target object.\n\t\t * @return {Vector4} The viewport definition.\n\t\t */\n\t\tthis.getViewport = function ( target ) {\n\n\t\t\treturn target.copy( _viewport );\n\n\t\t};\n\n\t\t/**\n\t\t * Sets the viewport to render from `(x, y)` to `(x + width, y + height)`.\n\t\t *\n\t\t * @param {number | Vector4} x - The horizontal coordinate for the lower left corner of the viewport origin in logical pixel unit.\n\t\t * Or alternatively a four-component vector specifying all the parameters of the viewport.\n\t\t * @param {number} y - The vertical coordinate for the lower left corner of the viewport origin  in logical pixel unit.\n\t\t * @param {number} width - The width of the viewport in logical pixel unit.\n\t\t * @param {number} height - The height of the viewport in logical pixel unit.\n\t\t */\n\t\tthis.setViewport = function ( x, y, width, height ) {\n\n\t\t\tif ( x.isVector4 ) {\n\n\t\t\t\t_viewport.set( x.x, x.y, x.z, x.w );\n\n\t\t\t} else {\n\n\t\t\t\t_viewport.set( x, y, width, height );\n\n\t\t\t}\n\n\t\t\tstate.viewport( _currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ).round() );\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the scissor region.\n\t\t *\n\t\t * @param {Vector4} target - The method writes the result in this target object.\n\t\t * @return {Vector4} The scissor region.\n\t\t */\n\t\tthis.getScissor = function ( target ) {\n\n\t\t\treturn target.copy( _scissor );\n\n\t\t};\n\n\t\t/**\n\t\t * Sets the scissor region to render from `(x, y)` to `(x + width, y + height)`.\n\t\t *\n\t\t * @param {number | Vector4} x - The horizontal coordinate for the lower left corner of the scissor region origin in logical pixel unit.\n\t\t * Or alternatively a four-component vector specifying all the parameters of the scissor region.\n\t\t * @param {number} y - The vertical coordinate for the lower left corner of the scissor region origin  in logical pixel unit.\n\t\t * @param {number} width - The width of the scissor region in logical pixel unit.\n\t\t * @param {number} height - The height of the scissor region in logical pixel unit.\n\t\t */\n\t\tthis.setScissor = function ( x, y, width, height ) {\n\n\t\t\tif ( x.isVector4 ) {\n\n\t\t\t\t_scissor.set( x.x, x.y, x.z, x.w );\n\n\t\t\t} else {\n\n\t\t\t\t_scissor.set( x, y, width, height );\n\n\t\t\t}\n\n\t\t\tstate.scissor( _currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ).round() );\n\n\t\t};\n\n\t\t/**\n\t\t * Returns `true` if the scissor test is enabled.\n\t\t *\n\t\t * @return {boolean} Whether the scissor test is enabled or not.\n\t\t */\n\t\tthis.getScissorTest = function () {\n\n\t\t\treturn _scissorTest;\n\n\t\t};\n\n\t\t/**\n\t\t * Enable or disable the scissor test. When this is enabled, only the pixels\n\t\t * within the defined scissor area will be affected by further renderer\n\t\t * actions.\n\t\t *\n\t\t * @param {boolean} boolean - Whether the scissor test is enabled or not.\n\t\t */\n\t\tthis.setScissorTest = function ( boolean ) {\n\n\t\t\tstate.setScissorTest( _scissorTest = boolean );\n\n\t\t};\n\n\t\t/**\n\t\t * Sets a custom opaque sort function for the render lists. Pass `null`\n\t\t * to use the default `painterSortStable` function.\n\t\t *\n\t\t * @param {?Function} method - The opaque sort function.\n\t\t */\n\t\tthis.setOpaqueSort = function ( method ) {\n\n\t\t\t_opaqueSort = method;\n\n\t\t};\n\n\t\t/**\n\t\t * Sets a custom transparent sort function for the render lists. Pass `null`\n\t\t * to use the default `reversePainterSortStable` function.\n\t\t *\n\t\t * @param {?Function} method - The opaque sort function.\n\t\t */\n\t\tthis.setTransparentSort = function ( method ) {\n\n\t\t\t_transparentSort = method;\n\n\t\t};\n\n\t\t// Clearing\n\n\t\t/**\n\t\t * Returns the clear color.\n\t\t *\n\t\t * @param {Color} target - The method writes the result in this target object.\n\t\t * @return {Color} The clear color.\n\t\t */\n\t\tthis.getClearColor = function ( target ) {\n\n\t\t\treturn target.copy( background.getClearColor() );\n\n\t\t};\n\n\t\t/**\n\t\t * Sets the clear color and alpha.\n\t\t *\n\t\t * @param {Color} color - The clear color.\n\t\t * @param {number} [alpha=1] - The clear alpha.\n\t\t */\n\t\tthis.setClearColor = function () {\n\n\t\t\tbackground.setClearColor( ...arguments );\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the clear alpha. Ranges within `[0,1]`.\n\t\t *\n\t\t * @return {number} The clear alpha.\n\t\t */\n\t\tthis.getClearAlpha = function () {\n\n\t\t\treturn background.getClearAlpha();\n\n\t\t};\n\n\t\t/**\n\t\t * Sets the clear alpha.\n\t\t *\n\t\t * @param {number} alpha - The clear alpha.\n\t\t */\n\t\tthis.setClearAlpha = function () {\n\n\t\t\tbackground.setClearAlpha( ...arguments );\n\n\t\t};\n\n\t\t/**\n\t\t * Tells the renderer to clear its color, depth or stencil drawing buffer(s).\n\t\t * This method initializes the buffers to the current clear color values.\n\t\t *\n\t\t * @param {boolean} [color=true] - Whether the color buffer should be cleared or not.\n\t\t * @param {boolean} [depth=true] - Whether the depth buffer should be cleared or not.\n\t\t * @param {boolean} [stencil=true] - Whether the stencil buffer should be cleared or not.\n\t\t */\n\t\tthis.clear = function ( color = true, depth = true, stencil = true ) {\n\n\t\t\tlet bits = 0;\n\n\t\t\tif ( color ) {\n\n\t\t\t\t// check if we're trying to clear an integer target\n\t\t\t\tlet isIntegerFormat = false;\n\t\t\t\tif ( _currentRenderTarget !== null ) {\n\n\t\t\t\t\tconst targetFormat = _currentRenderTarget.texture.format;\n\t\t\t\t\tisIntegerFormat = INTEGER_FORMATS.has( targetFormat );\n\n\t\t\t\t}\n\n\t\t\t\t// use the appropriate clear functions to clear the target if it's a signed\n\t\t\t\t// or unsigned integer target\n\t\t\t\tif ( isIntegerFormat ) {\n\n\t\t\t\t\tconst targetType = _currentRenderTarget.texture.type;\n\t\t\t\t\tconst isUnsignedType = UNSIGNED_TYPES.has( targetType );\n\n\t\t\t\t\tconst clearColor = background.getClearColor();\n\t\t\t\t\tconst a = background.getClearAlpha();\n\t\t\t\t\tconst r = clearColor.r;\n\t\t\t\t\tconst g = clearColor.g;\n\t\t\t\t\tconst b = clearColor.b;\n\n\t\t\t\t\tif ( isUnsignedType ) {\n\n\t\t\t\t\t\tuintClearColor[ 0 ] = r;\n\t\t\t\t\t\tuintClearColor[ 1 ] = g;\n\t\t\t\t\t\tuintClearColor[ 2 ] = b;\n\t\t\t\t\t\tuintClearColor[ 3 ] = a;\n\t\t\t\t\t\t_gl.clearBufferuiv( _gl.COLOR, 0, uintClearColor );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tintClearColor[ 0 ] = r;\n\t\t\t\t\t\tintClearColor[ 1 ] = g;\n\t\t\t\t\t\tintClearColor[ 2 ] = b;\n\t\t\t\t\t\tintClearColor[ 3 ] = a;\n\t\t\t\t\t\t_gl.clearBufferiv( _gl.COLOR, 0, intClearColor );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tbits |= _gl.COLOR_BUFFER_BIT;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( depth ) {\n\n\t\t\t\tbits |= _gl.DEPTH_BUFFER_BIT;\n\n\t\t\t}\n\n\t\t\tif ( stencil ) {\n\n\t\t\t\tbits |= _gl.STENCIL_BUFFER_BIT;\n\t\t\t\tthis.state.buffers.stencil.setMask( 0xffffffff );\n\n\t\t\t}\n\n\t\t\t_gl.clear( bits );\n\n\t\t};\n\n\t\t/**\n\t\t * Clears the color buffer. Equivalent to calling `renderer.clear( true, false, false )`.\n\t\t */\n\t\tthis.clearColor = function () {\n\n\t\t\tthis.clear( true, false, false );\n\n\t\t};\n\n\t\t/**\n\t\t * Clears the depth buffer. Equivalent to calling `renderer.clear( false, true, false )`.\n\t\t */\n\t\tthis.clearDepth = function () {\n\n\t\t\tthis.clear( false, true, false );\n\n\t\t};\n\n\t\t/**\n\t\t * Clears the stencil buffer. Equivalent to calling `renderer.clear( false, false, true )`.\n\t\t */\n\t\tthis.clearStencil = function () {\n\n\t\t\tthis.clear( false, false, true );\n\n\t\t};\n\n\t\t/**\n\t\t * Frees the GPU-related resources allocated by this instance. Call this\n\t\t * method whenever this instance is no longer used in your app.\n\t\t */\n\t\tthis.dispose = function () {\n\n\t\t\tcanvas.removeEventListener( 'webglcontextlost', onContextLost, false );\n\t\t\tcanvas.removeEventListener( 'webglcontextrestored', onContextRestore, false );\n\t\t\tcanvas.removeEventListener( 'webglcontextcreationerror', onContextCreationError, false );\n\n\t\t\tbackground.dispose();\n\t\t\trenderLists.dispose();\n\t\t\trenderStates.dispose();\n\t\t\tproperties.dispose();\n\t\t\tcubemaps.dispose();\n\t\t\tcubeuvmaps.dispose();\n\t\t\tobjects.dispose();\n\t\t\tbindingStates.dispose();\n\t\t\tuniformsGroups.dispose();\n\t\t\tprogramCache.dispose();\n\n\t\t\txr.dispose();\n\n\t\t\txr.removeEventListener( 'sessionstart', onXRSessionStart );\n\t\t\txr.removeEventListener( 'sessionend', onXRSessionEnd );\n\n\t\t\tanimation.stop();\n\n\t\t};\n\n\t\t// Events\n\n\t\tfunction onContextLost( event ) {\n\n\t\t\tevent.preventDefault();\n\n\t\t\tlog( 'WebGLRenderer: Context Lost.' );\n\n\t\t\t_isContextLost = true;\n\n\t\t}\n\n\t\tfunction onContextRestore( /* event */ ) {\n\n\t\t\tlog( 'WebGLRenderer: Context Restored.' );\n\n\t\t\t_isContextLost = false;\n\n\t\t\tconst infoAutoReset = info.autoReset;\n\t\t\tconst shadowMapEnabled = shadowMap.enabled;\n\t\t\tconst shadowMapAutoUpdate = shadowMap.autoUpdate;\n\t\t\tconst shadowMapNeedsUpdate = shadowMap.needsUpdate;\n\t\t\tconst shadowMapType = shadowMap.type;\n\n\t\t\tinitGLContext();\n\n\t\t\tinfo.autoReset = infoAutoReset;\n\t\t\tshadowMap.enabled = shadowMapEnabled;\n\t\t\tshadowMap.autoUpdate = shadowMapAutoUpdate;\n\t\t\tshadowMap.needsUpdate = shadowMapNeedsUpdate;\n\t\t\tshadowMap.type = shadowMapType;\n\n\t\t}\n\n\t\tfunction onContextCreationError( event ) {\n\n\t\t\terror( 'WebGLRenderer: A WebGL context could not be created. Reason: ', event.statusMessage );\n\n\t\t}\n\n\t\tfunction onMaterialDispose( event ) {\n\n\t\t\tconst material = event.target;\n\n\t\t\tmaterial.removeEventListener( 'dispose', onMaterialDispose );\n\n\t\t\tdeallocateMaterial( material );\n\n\t\t}\n\n\t\t// Buffer deallocation\n\n\t\tfunction deallocateMaterial( material ) {\n\n\t\t\treleaseMaterialProgramReferences( material );\n\n\t\t\tproperties.remove( material );\n\n\t\t}\n\n\n\t\tfunction releaseMaterialProgramReferences( material ) {\n\n\t\t\tconst programs = properties.get( material ).programs;\n\n\t\t\tif ( programs !== undefined ) {\n\n\t\t\t\tprograms.forEach( function ( program ) {\n\n\t\t\t\t\tprogramCache.releaseProgram( program );\n\n\t\t\t\t} );\n\n\t\t\t\tif ( material.isShaderMaterial ) {\n\n\t\t\t\t\tprogramCache.releaseShaderCache( material );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t// Buffer rendering\n\n\t\tthis.renderBufferDirect = function ( camera, scene, geometry, material, object, group ) {\n\n\t\t\tif ( scene === null ) scene = _emptyScene; // renderBufferDirect second parameter used to be fog (could be null)\n\n\t\t\tconst frontFaceCW = ( object.isMesh && object.matrixWorld.determinant() < 0 );\n\n\t\t\tconst program = setProgram( camera, scene, geometry, material, object );\n\n\t\t\tstate.setMaterial( material, frontFaceCW );\n\n\t\t\t//\n\n\t\t\tlet index = geometry.index;\n\t\t\tlet rangeFactor = 1;\n\n\t\t\tif ( material.wireframe === true ) {\n\n\t\t\t\tindex = geometries.getWireframeAttribute( geometry );\n\n\t\t\t\tif ( index === undefined ) return;\n\n\t\t\t\trangeFactor = 2;\n\n\t\t\t}\n\n\t\t\t//\n\n\t\t\tconst drawRange = geometry.drawRange;\n\t\t\tconst position = geometry.attributes.position;\n\n\t\t\tlet drawStart = drawRange.start * rangeFactor;\n\t\t\tlet drawEnd = ( drawRange.start + drawRange.count ) * rangeFactor;\n\n\t\t\tif ( group !== null ) {\n\n\t\t\t\tdrawStart = Math.max( drawStart, group.start * rangeFactor );\n\t\t\t\tdrawEnd = Math.min( drawEnd, ( group.start + group.count ) * rangeFactor );\n\n\t\t\t}\n\n\t\t\tif ( index !== null ) {\n\n\t\t\t\tdrawStart = Math.max( drawStart, 0 );\n\t\t\t\tdrawEnd = Math.min( drawEnd, index.count );\n\n\t\t\t} else if ( position !== undefined && position !== null ) {\n\n\t\t\t\tdrawStart = Math.max( drawStart, 0 );\n\t\t\t\tdrawEnd = Math.min( drawEnd, position.count );\n\n\t\t\t}\n\n\t\t\tconst drawCount = drawEnd - drawStart;\n\n\t\t\tif ( drawCount < 0 || drawCount === Infinity ) return;\n\n\t\t\t//\n\n\t\t\tbindingStates.setup( object, material, program, geometry, index );\n\n\t\t\tlet attribute;\n\t\t\tlet renderer = bufferRenderer;\n\n\t\t\tif ( index !== null ) {\n\n\t\t\t\tattribute = attributes.get( index );\n\n\t\t\t\trenderer = indexedBufferRenderer;\n\t\t\t\trenderer.setIndex( attribute );\n\n\t\t\t}\n\n\t\t\t//\n\n\t\t\tif ( object.isMesh ) {\n\n\t\t\t\tif ( material.wireframe === true ) {\n\n\t\t\t\t\tstate.setLineWidth( material.wireframeLinewidth * getTargetPixelRatio() );\n\t\t\t\t\trenderer.setMode( _gl.LINES );\n\n\t\t\t\t} else {\n\n\t\t\t\t\trenderer.setMode( _gl.TRIANGLES );\n\n\t\t\t\t}\n\n\t\t\t} else if ( object.isLine ) {\n\n\t\t\t\tlet lineWidth = material.linewidth;\n\n\t\t\t\tif ( lineWidth === undefined ) lineWidth = 1; // Not using Line*Material\n\n\t\t\t\tstate.setLineWidth( lineWidth * getTargetPixelRatio() );\n\n\t\t\t\tif ( object.isLineSegments ) {\n\n\t\t\t\t\trenderer.setMode( _gl.LINES );\n\n\t\t\t\t} else if ( object.isLineLoop ) {\n\n\t\t\t\t\trenderer.setMode( _gl.LINE_LOOP );\n\n\t\t\t\t} else {\n\n\t\t\t\t\trenderer.setMode( _gl.LINE_STRIP );\n\n\t\t\t\t}\n\n\t\t\t} else if ( object.isPoints ) {\n\n\t\t\t\trenderer.setMode( _gl.POINTS );\n\n\t\t\t} else if ( object.isSprite ) {\n\n\t\t\t\trenderer.setMode( _gl.TRIANGLES );\n\n\t\t\t}\n\n\t\t\tif ( object.isBatchedMesh ) {\n\n\t\t\t\tif ( object._multiDrawInstances !== null ) {\n\n\t\t\t\t\t// @deprecated, r174\n\t\t\t\t\twarnOnce( 'WebGLRenderer: renderMultiDrawInstances has been deprecated and will be removed in r184. Append to renderMultiDraw arguments and use indirection.' );\n\t\t\t\t\trenderer.renderMultiDrawInstances( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount, object._multiDrawInstances );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( ! extensions.get( 'WEBGL_multi_draw' ) ) {\n\n\t\t\t\t\t\tconst starts = object._multiDrawStarts;\n\t\t\t\t\t\tconst counts = object._multiDrawCounts;\n\t\t\t\t\t\tconst drawCount = object._multiDrawCount;\n\t\t\t\t\t\tconst bytesPerElement = index ? attributes.get( index ).bytesPerElement : 1;\n\t\t\t\t\t\tconst uniforms = properties.get( material ).currentProgram.getUniforms();\n\t\t\t\t\t\tfor ( let i = 0; i < drawCount; i ++ ) {\n\n\t\t\t\t\t\t\tuniforms.setValue( _gl, '_gl_DrawID', i );\n\t\t\t\t\t\t\trenderer.render( starts[ i ] / bytesPerElement, counts[ i ] );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\trenderer.renderMultiDraw( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else if ( object.isInstancedMesh ) {\n\n\t\t\t\trenderer.renderInstances( drawStart, drawCount, object.count );\n\n\t\t\t} else if ( geometry.isInstancedBufferGeometry ) {\n\n\t\t\t\tconst maxInstanceCount = geometry._maxInstanceCount !== undefined ? geometry._maxInstanceCount : Infinity;\n\t\t\t\tconst instanceCount = Math.min( geometry.instanceCount, maxInstanceCount );\n\n\t\t\t\trenderer.renderInstances( drawStart, drawCount, instanceCount );\n\n\t\t\t} else {\n\n\t\t\t\trenderer.render( drawStart, drawCount );\n\n\t\t\t}\n\n\t\t};\n\n\t\t// Compile\n\n\t\tfunction prepareMaterial( material, scene, object ) {\n\n\t\t\tif ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) {\n\n\t\t\t\tmaterial.side = BackSide;\n\t\t\t\tmaterial.needsUpdate = true;\n\t\t\t\tgetProgram( material, scene, object );\n\n\t\t\t\tmaterial.side = FrontSide;\n\t\t\t\tmaterial.needsUpdate = true;\n\t\t\t\tgetProgram( material, scene, object );\n\n\t\t\t\tmaterial.side = DoubleSide;\n\n\t\t\t} else {\n\n\t\t\t\tgetProgram( material, scene, object );\n\n\t\t\t}\n\n\t\t}\n\n\t\t/**\n\t\t * Compiles all materials in the scene with the camera. This is useful to precompile shaders\n\t\t * before the first rendering. If you want to add a 3D object to an existing scene, use the third\n\t\t * optional parameter for applying the target scene.\n\t\t *\n\t\t * Note that the (target) scene's lighting and environment must be configured before calling this method.\n\t\t *\n\t\t * @param {Object3D} scene - The scene or another type of 3D object to precompile.\n\t\t * @param {Camera} camera - The camera.\n\t\t * @param {?Scene} [targetScene=null] - The target scene.\n\t\t * @return {Set<Material>} The precompiled materials.\n\t\t */\n\t\tthis.compile = function ( scene, camera, targetScene = null ) {\n\n\t\t\tif ( targetScene === null ) targetScene = scene;\n\n\t\t\tcurrentRenderState = renderStates.get( targetScene );\n\t\t\tcurrentRenderState.init( camera );\n\n\t\t\trenderStateStack.push( currentRenderState );\n\n\t\t\t// gather lights from both the target scene and the new object that will be added to the scene.\n\n\t\t\ttargetScene.traverseVisible( function ( object ) {\n\n\t\t\t\tif ( object.isLight && object.layers.test( camera.layers ) ) {\n\n\t\t\t\t\tcurrentRenderState.pushLight( object );\n\n\t\t\t\t\tif ( object.castShadow ) {\n\n\t\t\t\t\t\tcurrentRenderState.pushShadow( object );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\tif ( scene !== targetScene ) {\n\n\t\t\t\tscene.traverseVisible( function ( object ) {\n\n\t\t\t\t\tif ( object.isLight && object.layers.test( camera.layers ) ) {\n\n\t\t\t\t\t\tcurrentRenderState.pushLight( object );\n\n\t\t\t\t\t\tif ( object.castShadow ) {\n\n\t\t\t\t\t\t\tcurrentRenderState.pushShadow( object );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t\tcurrentRenderState.setupLights();\n\n\t\t\t// Only initialize materials in the new scene, not the targetScene.\n\n\t\t\tconst materials = new Set();\n\n\t\t\tscene.traverse( function ( object ) {\n\n\t\t\t\tif ( ! ( object.isMesh || object.isPoints || object.isLine || object.isSprite ) ) {\n\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t\tconst material = object.material;\n\n\t\t\t\tif ( material ) {\n\n\t\t\t\t\tif ( Array.isArray( material ) ) {\n\n\t\t\t\t\t\tfor ( let i = 0; i < material.length; i ++ ) {\n\n\t\t\t\t\t\t\tconst material2 = material[ i ];\n\n\t\t\t\t\t\t\tprepareMaterial( material2, targetScene, object );\n\t\t\t\t\t\t\tmaterials.add( material2 );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tprepareMaterial( material, targetScene, object );\n\t\t\t\t\t\tmaterials.add( material );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\tcurrentRenderState = renderStateStack.pop();\n\n\t\t\treturn materials;\n\n\t\t};\n\n\t\t// compileAsync\n\n\t\t/**\n\t\t * Asynchronous version of {@link WebGLRenderer#compile}.\n\t\t *\n\t\t * This method makes use of the `KHR_parallel_shader_compile` WebGL extension. Hence,\n\t\t * it is recommended to use this version of `compile()` whenever possible.\n\t\t *\n\t\t * @async\n\t\t * @param {Object3D} scene - The scene or another type of 3D object to precompile.\n\t\t * @param {Camera} camera - The camera.\n\t\t * @param {?Scene} [targetScene=null] - The target scene.\n\t\t * @return {Promise} A Promise that resolves when the given scene can be rendered without unnecessary stalling due to shader compilation.\n\t\t */\n\t\tthis.compileAsync = function ( scene, camera, targetScene = null ) {\n\n\t\t\tconst materials = this.compile( scene, camera, targetScene );\n\n\t\t\t// Wait for all the materials in the new object to indicate that they're\n\t\t\t// ready to be used before resolving the promise.\n\n\t\t\treturn new Promise( ( resolve ) => {\n\n\t\t\t\tfunction checkMaterialsReady() {\n\n\t\t\t\t\tmaterials.forEach( function ( material ) {\n\n\t\t\t\t\t\tconst materialProperties = properties.get( material );\n\t\t\t\t\t\tconst program = materialProperties.currentProgram;\n\n\t\t\t\t\t\tif ( program.isReady() ) {\n\n\t\t\t\t\t\t\t// remove any programs that report they're ready to use from the list\n\t\t\t\t\t\t\tmaterials.delete( material );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} );\n\n\t\t\t\t\t// once the list of compiling materials is empty, call the callback\n\n\t\t\t\t\tif ( materials.size === 0 ) {\n\n\t\t\t\t\t\tresolve( scene );\n\t\t\t\t\t\treturn;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// if some materials are still not ready, wait a bit and check again\n\n\t\t\t\t\tsetTimeout( checkMaterialsReady, 10 );\n\n\t\t\t\t}\n\n\t\t\t\tif ( extensions.get( 'KHR_parallel_shader_compile' ) !== null ) {\n\n\t\t\t\t\t// If we can check the compilation status of the materials without\n\t\t\t\t\t// blocking then do so right away.\n\n\t\t\t\t\tcheckMaterialsReady();\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// Otherwise start by waiting a bit to give the materials we just\n\t\t\t\t\t// initialized a chance to finish.\n\n\t\t\t\t\tsetTimeout( checkMaterialsReady, 10 );\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t};\n\n\t\t// Animation Loop\n\n\t\tlet onAnimationFrameCallback = null;\n\n\t\tfunction onAnimationFrame( time ) {\n\n\t\t\tif ( onAnimationFrameCallback ) onAnimationFrameCallback( time );\n\n\t\t}\n\n\t\tfunction onXRSessionStart() {\n\n\t\t\tanimation.stop();\n\n\t\t}\n\n\t\tfunction onXRSessionEnd() {\n\n\t\t\tanimation.start();\n\n\t\t}\n\n\t\tconst animation = new WebGLAnimation();\n\t\tanimation.setAnimationLoop( onAnimationFrame );\n\n\t\tif ( typeof self !== 'undefined' ) animation.setContext( self );\n\n\t\t/**\n\t\t * Applications are advised to always define the animation loop\n\t\t * with this method and not manually with `requestAnimationFrame()`\n\t\t * for best compatibility.\n\t\t *\n\t\t * @param {?onAnimationCallback} callback - The application's animation loop.\n\t\t */\n\t\tthis.setAnimationLoop = function ( callback ) {\n\n\t\t\tonAnimationFrameCallback = callback;\n\t\t\txr.setAnimationLoop( callback );\n\n\t\t\t( callback === null ) ? animation.stop() : animation.start();\n\n\t\t};\n\n\t\txr.addEventListener( 'sessionstart', onXRSessionStart );\n\t\txr.addEventListener( 'sessionend', onXRSessionEnd );\n\n\t\t// Rendering\n\n\t\t/**\n\t\t * Renders the given scene (or other type of 3D object) using the given camera.\n\t\t *\n\t\t * The render is done to a previously specified render target set by calling {@link WebGLRenderer#setRenderTarget}\n\t\t * or to the canvas as usual.\n\t\t *\n\t\t * By default render buffers are cleared before rendering but you can prevent\n\t\t * this by setting the property `autoClear` to `false`. If you want to prevent\n\t\t * only certain buffers being cleared you can `autoClearColor`, `autoClearDepth`\n\t\t * or `autoClearStencil` to `false`. To force a clear, use {@link WebGLRenderer#clear}.\n\t\t *\n\t\t * @param {Object3D} scene - The scene to render.\n\t\t * @param {Camera} camera - The camera.\n\t\t */\n\t\tthis.render = function ( scene, camera ) {\n\n\t\t\tif ( camera !== undefined && camera.isCamera !== true ) {\n\n\t\t\t\terror( 'WebGLRenderer.render: camera is not an instance of THREE.Camera.' );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tif ( _isContextLost === true ) return;\n\n\t\t\t// use internal render target for HalfFloatType color buffer (only when tone mapping is enabled)\n\n\t\t\tconst isXRPresenting = xr.enabled === true && xr.isPresenting === true;\n\n\t\t\tconst useOutput = output !== null && ( _currentRenderTarget === null || isXRPresenting ) && output.begin( _this, _currentRenderTarget );\n\n\t\t\t// update scene graph\n\n\t\t\tif ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();\n\n\t\t\t// update camera matrices and frustum\n\n\t\t\tif ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();\n\n\t\t\tif ( xr.enabled === true && xr.isPresenting === true && ( output === null || output.isCompositing() === false ) ) {\n\n\t\t\t\tif ( xr.cameraAutoUpdate === true ) xr.updateCamera( camera );\n\n\t\t\t\tcamera = xr.getCamera(); // use XR camera for rendering\n\n\t\t\t}\n\n\t\t\t//\n\t\t\tif ( scene.isScene === true ) scene.onBeforeRender( _this, scene, camera, _currentRenderTarget );\n\n\t\t\tcurrentRenderState = renderStates.get( scene, renderStateStack.length );\n\t\t\tcurrentRenderState.init( camera );\n\n\t\t\trenderStateStack.push( currentRenderState );\n\n\t\t\t_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );\n\t\t\t_frustum.setFromProjectionMatrix( _projScreenMatrix, WebGLCoordinateSystem, camera.reversedDepth );\n\n\t\t\t_localClippingEnabled = this.localClippingEnabled;\n\t\t\t_clippingEnabled = clipping.init( this.clippingPlanes, _localClippingEnabled );\n\n\t\t\tcurrentRenderList = renderLists.get( scene, renderListStack.length );\n\t\t\tcurrentRenderList.init();\n\n\t\t\trenderListStack.push( currentRenderList );\n\n\t\t\tif ( xr.enabled === true && xr.isPresenting === true ) {\n\n\t\t\t\tconst depthSensingMesh = _this.xr.getDepthSensingMesh();\n\n\t\t\t\tif ( depthSensingMesh !== null ) {\n\n\t\t\t\t\tprojectObject( depthSensingMesh, camera, - Infinity, _this.sortObjects );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tprojectObject( scene, camera, 0, _this.sortObjects );\n\n\t\t\tcurrentRenderList.finish();\n\n\t\t\tif ( _this.sortObjects === true ) {\n\n\t\t\t\tcurrentRenderList.sort( _opaqueSort, _transparentSort );\n\n\t\t\t}\n\n\t\t\t_renderBackground = xr.enabled === false || xr.isPresenting === false || xr.hasDepthSensing() === false;\n\t\t\tif ( _renderBackground ) {\n\n\t\t\t\tbackground.addToRenderList( currentRenderList, scene );\n\n\t\t\t}\n\n\t\t\t//\n\n\t\t\tthis.info.render.frame ++;\n\n\t\t\tif ( _clippingEnabled === true ) clipping.beginShadows();\n\n\t\t\tconst shadowsArray = currentRenderState.state.shadowsArray;\n\n\t\t\tshadowMap.render( shadowsArray, scene, camera );\n\n\t\t\tif ( _clippingEnabled === true ) clipping.endShadows();\n\n\t\t\t//\n\n\t\t\tif ( this.info.autoReset === true ) this.info.reset();\n\n\t\t\t// render scene (skip if first effect is a render pass - it will render the scene itself)\n\n\t\t\tconst skipSceneRender = useOutput && output.hasRenderPass();\n\n\t\t\tif ( skipSceneRender === false ) {\n\n\t\t\t\tconst opaqueObjects = currentRenderList.opaque;\n\t\t\t\tconst transmissiveObjects = currentRenderList.transmissive;\n\n\t\t\t\tcurrentRenderState.setupLights();\n\n\t\t\t\tif ( camera.isArrayCamera ) {\n\n\t\t\t\t\tconst cameras = camera.cameras;\n\n\t\t\t\t\tif ( transmissiveObjects.length > 0 ) {\n\n\t\t\t\t\t\tfor ( let i = 0, l = cameras.length; i < l; i ++ ) {\n\n\t\t\t\t\t\t\tconst camera2 = cameras[ i ];\n\n\t\t\t\t\t\t\trenderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera2 );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( _renderBackground ) background.render( scene );\n\n\t\t\t\t\tfor ( let i = 0, l = cameras.length; i < l; i ++ ) {\n\n\t\t\t\t\t\tconst camera2 = cameras[ i ];\n\n\t\t\t\t\t\trenderScene( currentRenderList, scene, camera2, camera2.viewport );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( transmissiveObjects.length > 0 ) renderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera );\n\n\t\t\t\t\tif ( _renderBackground ) background.render( scene );\n\n\t\t\t\t\trenderScene( currentRenderList, scene, camera );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t//\n\n\t\t\tif ( _currentRenderTarget !== null && _currentActiveMipmapLevel === 0 ) {\n\n\t\t\t\t// resolve multisample renderbuffers to a single-sample texture if necessary\n\n\t\t\t\ttextures.updateMultisampleRenderTarget( _currentRenderTarget );\n\n\t\t\t\t// Generate mipmap if we're using any kind of mipmap filtering\n\n\t\t\t\ttextures.updateRenderTargetMipmap( _currentRenderTarget );\n\n\t\t\t}\n\n\t\t\t// copy from internal render target to canvas using fullscreen quad\n\n\t\t\tif ( useOutput ) {\n\n\t\t\t\toutput.end( _this );\n\n\t\t\t}\n\n\t\t\t//\n\n\t\t\tif ( scene.isScene === true ) scene.onAfterRender( _this, scene, camera );\n\n\t\t\t// _gl.finish();\n\n\t\t\tbindingStates.resetDefaultState();\n\t\t\t_currentMaterialId = -1;\n\t\t\t_currentCamera = null;\n\n\t\t\trenderStateStack.pop();\n\n\t\t\tif ( renderStateStack.length > 0 ) {\n\n\t\t\t\tcurrentRenderState = renderStateStack[ renderStateStack.length - 1 ];\n\n\t\t\t\tif ( _clippingEnabled === true ) clipping.setGlobalState( _this.clippingPlanes, currentRenderState.state.camera );\n\n\t\t\t} else {\n\n\t\t\t\tcurrentRenderState = null;\n\n\t\t\t}\n\n\t\t\trenderListStack.pop();\n\n\t\t\tif ( renderListStack.length > 0 ) {\n\n\t\t\t\tcurrentRenderList = renderListStack[ renderListStack.length - 1 ];\n\n\t\t\t} else {\n\n\t\t\t\tcurrentRenderList = null;\n\n\t\t\t}\n\n\t\t};\n\n\t\tfunction projectObject( object, camera, groupOrder, sortObjects ) {\n\n\t\t\tif ( object.visible === false ) return;\n\n\t\t\tconst visible = object.layers.test( camera.layers );\n\n\t\t\tif ( visible ) {\n\n\t\t\t\tif ( object.isGroup ) {\n\n\t\t\t\t\tgroupOrder = object.renderOrder;\n\n\t\t\t\t} else if ( object.isLOD ) {\n\n\t\t\t\t\tif ( object.autoUpdate === true ) object.update( camera );\n\n\t\t\t\t} else if ( object.isLight ) {\n\n\t\t\t\t\tcurrentRenderState.pushLight( object );\n\n\t\t\t\t\tif ( object.castShadow ) {\n\n\t\t\t\t\t\tcurrentRenderState.pushShadow( object );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else if ( object.isSprite ) {\n\n\t\t\t\t\tif ( ! object.frustumCulled || _frustum.intersectsSprite( object ) ) {\n\n\t\t\t\t\t\tif ( sortObjects ) {\n\n\t\t\t\t\t\t\t_vector4.setFromMatrixPosition( object.matrixWorld )\n\t\t\t\t\t\t\t\t.applyMatrix4( _projScreenMatrix );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst geometry = objects.update( object );\n\t\t\t\t\t\tconst material = object.material;\n\n\t\t\t\t\t\tif ( material.visible ) {\n\n\t\t\t\t\t\t\tcurrentRenderList.push( object, geometry, material, groupOrder, _vector4.z, null );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t} else if ( object.isMesh || object.isLine || object.isPoints ) {\n\n\t\t\t\t\tif ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) {\n\n\t\t\t\t\t\tconst geometry = objects.update( object );\n\t\t\t\t\t\tconst material = object.material;\n\n\t\t\t\t\t\tif ( sortObjects ) {\n\n\t\t\t\t\t\t\tif ( object.boundingSphere !== undefined ) {\n\n\t\t\t\t\t\t\t\tif ( object.boundingSphere === null ) object.computeBoundingSphere();\n\t\t\t\t\t\t\t\t_vector4.copy( object.boundingSphere.center );\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\tif ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();\n\t\t\t\t\t\t\t\t_vector4.copy( geometry.boundingSphere.center );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t_vector4\n\t\t\t\t\t\t\t\t.applyMatrix4( object.matrixWorld )\n\t\t\t\t\t\t\t\t.applyMatrix4( _projScreenMatrix );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( Array.isArray( material ) ) {\n\n\t\t\t\t\t\t\tconst groups = geometry.groups;\n\n\t\t\t\t\t\t\tfor ( let i = 0, l = groups.length; i < l; i ++ ) {\n\n\t\t\t\t\t\t\t\tconst group = groups[ i ];\n\t\t\t\t\t\t\t\tconst groupMaterial = material[ group.materialIndex ];\n\n\t\t\t\t\t\t\t\tif ( groupMaterial && groupMaterial.visible ) {\n\n\t\t\t\t\t\t\t\t\tcurrentRenderList.push( object, geometry, groupMaterial, groupOrder, _vector4.z, group );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else if ( material.visible ) {\n\n\t\t\t\t\t\t\tcurrentRenderList.push( object, geometry, material, groupOrder, _vector4.z, null );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tconst children = object.children;\n\n\t\t\tfor ( let i = 0, l = children.length; i < l; i ++ ) {\n\n\t\t\t\tprojectObject( children[ i ], camera, groupOrder, sortObjects );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction renderScene( currentRenderList, scene, camera, viewport ) {\n\n\t\t\tconst { opaque: opaqueObjects, transmissive: transmissiveObjects, transparent: transparentObjects } = currentRenderList;\n\n\t\t\tcurrentRenderState.setupLightsView( camera );\n\n\t\t\tif ( _clippingEnabled === true ) clipping.setGlobalState( _this.clippingPlanes, camera );\n\n\t\t\tif ( viewport ) state.viewport( _currentViewport.copy( viewport ) );\n\n\t\t\tif ( opaqueObjects.length > 0 ) renderObjects( opaqueObjects, scene, camera );\n\t\t\tif ( transmissiveObjects.length > 0 ) renderObjects( transmissiveObjects, scene, camera );\n\t\t\tif ( transparentObjects.length > 0 ) renderObjects( transparentObjects, scene, camera );\n\n\t\t\t// Ensure depth buffer writing is enabled so it can be cleared on next render\n\n\t\t\tstate.buffers.depth.setTest( true );\n\t\t\tstate.buffers.depth.setMask( true );\n\t\t\tstate.buffers.color.setMask( true );\n\n\t\t\tstate.setPolygonOffset( false );\n\n\t\t}\n\n\t\tfunction renderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera ) {\n\n\t\t\tconst overrideMaterial = scene.isScene === true ? scene.overrideMaterial : null;\n\n\t\t\tif ( overrideMaterial !== null ) {\n\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tif ( currentRenderState.state.transmissionRenderTarget[ camera.id ] === undefined ) {\n\n\t\t\t\tconst hasHalfFloatSupport = extensions.has( 'EXT_color_buffer_half_float' ) || extensions.has( 'EXT_color_buffer_float' );\n\n\t\t\t\tcurrentRenderState.state.transmissionRenderTarget[ camera.id ] = new WebGLRenderTarget( 1, 1, {\n\t\t\t\t\tgenerateMipmaps: true,\n\t\t\t\t\ttype: hasHalfFloatSupport ? HalfFloatType : UnsignedByteType,\n\t\t\t\t\tminFilter: LinearMipmapLinearFilter,\n\t\t\t\t\tsamples: capabilities.samples,\n\t\t\t\t\tstencilBuffer: stencil,\n\t\t\t\t\tresolveDepthBuffer: false,\n\t\t\t\t\tresolveStencilBuffer: false,\n\t\t\t\t\tcolorSpace: ColorManagement.workingColorSpace,\n\t\t\t\t} );\n\n\t\t\t\t// debug\n\n\t\t\t\t/*\n\t\t\t\tconst geometry = new PlaneGeometry();\n\t\t\t\tconst material = new MeshBasicMaterial( { map: _transmissionRenderTarget.texture } );\n\n\t\t\t\tconst mesh = new Mesh( geometry, material );\n\t\t\t\tscene.add( mesh );\n\t\t\t\t*/\n\n\t\t\t}\n\n\t\t\tconst transmissionRenderTarget = currentRenderState.state.transmissionRenderTarget[ camera.id ];\n\n\t\t\tconst activeViewport = camera.viewport || _currentViewport;\n\t\t\ttransmissionRenderTarget.setSize( activeViewport.z * _this.transmissionResolutionScale, activeViewport.w * _this.transmissionResolutionScale );\n\n\t\t\t//\n\n\t\t\tconst currentRenderTarget = _this.getRenderTarget();\n\t\t\tconst currentActiveCubeFace = _this.getActiveCubeFace();\n\t\t\tconst currentActiveMipmapLevel = _this.getActiveMipmapLevel();\n\n\t\t\t_this.setRenderTarget( transmissionRenderTarget );\n\n\t\t\t_this.getClearColor( _currentClearColor );\n\t\t\t_currentClearAlpha = _this.getClearAlpha();\n\t\t\tif ( _currentClearAlpha < 1 ) _this.setClearColor( 0xffffff, 0.5 );\n\n\t\t\t_this.clear();\n\n\t\t\tif ( _renderBackground ) background.render( scene );\n\n\t\t\t// Turn off the features which can affect the frag color for opaque objects pass.\n\t\t\t// Otherwise they are applied twice in opaque objects pass and transmission objects pass.\n\t\t\tconst currentToneMapping = _this.toneMapping;\n\t\t\t_this.toneMapping = NoToneMapping;\n\n\t\t\t// Remove viewport from camera to avoid nested render calls resetting viewport to it (e.g Reflector).\n\t\t\t// Transmission render pass requires viewport to match the transmissionRenderTarget.\n\t\t\tconst currentCameraViewport = camera.viewport;\n\t\t\tif ( camera.viewport !== undefined ) camera.viewport = undefined;\n\n\t\t\tcurrentRenderState.setupLightsView( camera );\n\n\t\t\tif ( _clippingEnabled === true ) clipping.setGlobalState( _this.clippingPlanes, camera );\n\n\t\t\trenderObjects( opaqueObjects, scene, camera );\n\n\t\t\ttextures.updateMultisampleRenderTarget( transmissionRenderTarget );\n\t\t\ttextures.updateRenderTargetMipmap( transmissionRenderTarget );\n\n\t\t\tif ( extensions.has( 'WEBGL_multisampled_render_to_texture' ) === false ) { // see #28131\n\n\t\t\t\tlet renderTargetNeedsUpdate = false;\n\n\t\t\t\tfor ( let i = 0, l = transmissiveObjects.length; i < l; i ++ ) {\n\n\t\t\t\t\tconst renderItem = transmissiveObjects[ i ];\n\n\t\t\t\t\tconst { object, geometry, material, group } = renderItem;\n\n\t\t\t\t\tif ( material.side === DoubleSide && object.layers.test( camera.layers ) ) {\n\n\t\t\t\t\t\tconst currentSide = material.side;\n\n\t\t\t\t\t\tmaterial.side = BackSide;\n\t\t\t\t\t\tmaterial.needsUpdate = true;\n\n\t\t\t\t\t\trenderObject( object, scene, camera, geometry, material, group );\n\n\t\t\t\t\t\tmaterial.side = currentSide;\n\t\t\t\t\t\tmaterial.needsUpdate = true;\n\n\t\t\t\t\t\trenderTargetNeedsUpdate = true;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tif ( renderTargetNeedsUpdate === true ) {\n\n\t\t\t\t\ttextures.updateMultisampleRenderTarget( transmissionRenderTarget );\n\t\t\t\t\ttextures.updateRenderTargetMipmap( transmissionRenderTarget );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t_this.setRenderTarget( currentRenderTarget, currentActiveCubeFace, currentActiveMipmapLevel );\n\n\t\t\t_this.setClearColor( _currentClearColor, _currentClearAlpha );\n\n\t\t\tif ( currentCameraViewport !== undefined ) camera.viewport = currentCameraViewport;\n\n\t\t\t_this.toneMapping = currentToneMapping;\n\n\t\t}\n\n\t\tfunction renderObjects( renderList, scene, camera ) {\n\n\t\t\tconst overrideMaterial = scene.isScene === true ? scene.overrideMaterial : null;\n\n\t\t\tfor ( let i = 0, l = renderList.length; i < l; i ++ ) {\n\n\t\t\t\tconst renderItem = renderList[ i ];\n\n\t\t\t\tconst { object, geometry, group } = renderItem;\n\t\t\t\tlet material = renderItem.material;\n\n\t\t\t\tif ( material.allowOverride === true && overrideMaterial !== null ) {\n\n\t\t\t\t\tmaterial = overrideMaterial;\n\n\t\t\t\t}\n\n\t\t\t\tif ( object.layers.test( camera.layers ) ) {\n\n\t\t\t\t\trenderObject( object, scene, camera, geometry, material, group );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction renderObject( object, scene, camera, geometry, material, group ) {\n\n\t\t\tobject.onBeforeRender( _this, scene, camera, geometry, material, group );\n\n\t\t\tobject.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );\n\t\t\tobject.normalMatrix.getNormalMatrix( object.modelViewMatrix );\n\n\t\t\tmaterial.onBeforeRender( _this, scene, camera, geometry, object, group );\n\n\t\t\tif ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) {\n\n\t\t\t\tmaterial.side = BackSide;\n\t\t\t\tmaterial.needsUpdate = true;\n\t\t\t\t_this.renderBufferDirect( camera, scene, geometry, material, object, group );\n\n\t\t\t\tmaterial.side = FrontSide;\n\t\t\t\tmaterial.needsUpdate = true;\n\t\t\t\t_this.renderBufferDirect( camera, scene, geometry, material, object, group );\n\n\t\t\t\tmaterial.side = DoubleSide;\n\n\t\t\t} else {\n\n\t\t\t\t_this.renderBufferDirect( camera, scene, geometry, material, object, group );\n\n\t\t\t}\n\n\t\t\tobject.onAfterRender( _this, scene, camera, geometry, material, group );\n\n\t\t}\n\n\t\tfunction getProgram( material, scene, object ) {\n\n\t\t\tif ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ...\n\n\t\t\tconst materialProperties = properties.get( material );\n\n\t\t\tconst lights = currentRenderState.state.lights;\n\t\t\tconst shadowsArray = currentRenderState.state.shadowsArray;\n\n\t\t\tconst lightsStateVersion = lights.state.version;\n\n\t\t\tconst parameters = programCache.getParameters( material, lights.state, shadowsArray, scene, object );\n\t\t\tconst programCacheKey = programCache.getProgramCacheKey( parameters );\n\n\t\t\tlet programs = materialProperties.programs;\n\n\t\t\t// always update environment and fog - changing these trigger an getProgram call, but it's possible that the program doesn't change\n\n\t\t\tmaterialProperties.environment = material.isMeshStandardMaterial ? scene.environment : null;\n\t\t\tmaterialProperties.fog = scene.fog;\n\t\t\tmaterialProperties.envMap = ( material.isMeshStandardMaterial ? cubeuvmaps : cubemaps ).get( material.envMap || materialProperties.environment );\n\t\t\tmaterialProperties.envMapRotation = ( materialProperties.environment !== null && material.envMap === null ) ? scene.environmentRotation : material.envMapRotation;\n\n\t\t\tif ( programs === undefined ) {\n\n\t\t\t\t// new material\n\n\t\t\t\tmaterial.addEventListener( 'dispose', onMaterialDispose );\n\n\t\t\t\tprograms = new Map();\n\t\t\t\tmaterialProperties.programs = programs;\n\n\t\t\t}\n\n\t\t\tlet program = programs.get( programCacheKey );\n\n\t\t\tif ( program !== undefined ) {\n\n\t\t\t\t// early out if program and light state is identical\n\n\t\t\t\tif ( materialProperties.currentProgram === program && materialProperties.lightsStateVersion === lightsStateVersion ) {\n\n\t\t\t\t\tupdateCommonMaterialProperties( material, parameters );\n\n\t\t\t\t\treturn program;\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tparameters.uniforms = programCache.getUniforms( material );\n\n\t\t\t\tmaterial.onBeforeCompile( parameters, _this );\n\n\t\t\t\tprogram = programCache.acquireProgram( parameters, programCacheKey );\n\t\t\t\tprograms.set( programCacheKey, program );\n\n\t\t\t\tmaterialProperties.uniforms = parameters.uniforms;\n\n\t\t\t}\n\n\t\t\tconst uniforms = materialProperties.uniforms;\n\n\t\t\tif ( ( ! material.isShaderMaterial && ! material.isRawShaderMaterial ) || material.clipping === true ) {\n\n\t\t\t\tuniforms.clippingPlanes = clipping.uniform;\n\n\t\t\t}\n\n\t\t\tupdateCommonMaterialProperties( material, parameters );\n\n\t\t\t// store the light setup it was created for\n\n\t\t\tmaterialProperties.needsLights = materialNeedsLights( material );\n\t\t\tmaterialProperties.lightsStateVersion = lightsStateVersion;\n\n\t\t\tif ( materialProperties.needsLights ) {\n\n\t\t\t\t// wire up the material to this renderer's lighting state\n\n\t\t\t\tuniforms.ambientLightColor.value = lights.state.ambient;\n\t\t\t\tuniforms.lightProbe.value = lights.state.probe;\n\t\t\t\tuniforms.directionalLights.value = lights.state.directional;\n\t\t\t\tuniforms.directionalLightShadows.value = lights.state.directionalShadow;\n\t\t\t\tuniforms.spotLights.value = lights.state.spot;\n\t\t\t\tuniforms.spotLightShadows.value = lights.state.spotShadow;\n\t\t\t\tuniforms.rectAreaLights.value = lights.state.rectArea;\n\t\t\t\tuniforms.ltc_1.value = lights.state.rectAreaLTC1;\n\t\t\t\tuniforms.ltc_2.value = lights.state.rectAreaLTC2;\n\t\t\t\tuniforms.pointLights.value = lights.state.point;\n\t\t\t\tuniforms.pointLightShadows.value = lights.state.pointShadow;\n\t\t\t\tuniforms.hemisphereLights.value = lights.state.hemi;\n\n\t\t\t\tuniforms.directionalShadowMap.value = lights.state.directionalShadowMap;\n\t\t\t\tuniforms.directionalShadowMatrix.value = lights.state.directionalShadowMatrix;\n\t\t\t\tuniforms.spotShadowMap.value = lights.state.spotShadowMap;\n\t\t\t\tuniforms.spotLightMatrix.value = lights.state.spotLightMatrix;\n\t\t\t\tuniforms.spotLightMap.value = lights.state.spotLightMap;\n\t\t\t\tuniforms.pointShadowMap.value = lights.state.pointShadowMap;\n\t\t\t\tuniforms.pointShadowMatrix.value = lights.state.pointShadowMatrix;\n\t\t\t\t// TODO (abelnation): add area lights shadow info to uniforms\n\n\t\t\t}\n\n\t\t\tmaterialProperties.currentProgram = program;\n\t\t\tmaterialProperties.uniformsList = null;\n\n\t\t\treturn program;\n\n\t\t}\n\n\t\tfunction getUniformList( materialProperties ) {\n\n\t\t\tif ( materialProperties.uniformsList === null ) {\n\n\t\t\t\tconst progUniforms = materialProperties.currentProgram.getUniforms();\n\t\t\t\tmaterialProperties.uniformsList = WebGLUniforms.seqWithValue( progUniforms.seq, materialProperties.uniforms );\n\n\t\t\t}\n\n\t\t\treturn materialProperties.uniformsList;\n\n\t\t}\n\n\t\tfunction updateCommonMaterialProperties( material, parameters ) {\n\n\t\t\tconst materialProperties = properties.get( material );\n\n\t\t\tmaterialProperties.outputColorSpace = parameters.outputColorSpace;\n\t\t\tmaterialProperties.batching = parameters.batching;\n\t\t\tmaterialProperties.batchingColor = parameters.batchingColor;\n\t\t\tmaterialProperties.instancing = parameters.instancing;\n\t\t\tmaterialProperties.instancingColor = parameters.instancingColor;\n\t\t\tmaterialProperties.instancingMorph = parameters.instancingMorph;\n\t\t\tmaterialProperties.skinning = parameters.skinning;\n\t\t\tmaterialProperties.morphTargets = parameters.morphTargets;\n\t\t\tmaterialProperties.morphNormals = parameters.morphNormals;\n\t\t\tmaterialProperties.morphColors = parameters.morphColors;\n\t\t\tmaterialProperties.morphTargetsCount = parameters.morphTargetsCount;\n\t\t\tmaterialProperties.numClippingPlanes = parameters.numClippingPlanes;\n\t\t\tmaterialProperties.numIntersection = parameters.numClipIntersection;\n\t\t\tmaterialProperties.vertexAlphas = parameters.vertexAlphas;\n\t\t\tmaterialProperties.vertexTangents = parameters.vertexTangents;\n\t\t\tmaterialProperties.toneMapping = parameters.toneMapping;\n\n\t\t}\n\n\t\tfunction setProgram( camera, scene, geometry, material, object ) {\n\n\t\t\tif ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ...\n\n\t\t\ttextures.resetTextureUnits();\n\n\t\t\tconst fog = scene.fog;\n\t\t\tconst environment = material.isMeshStandardMaterial ? scene.environment : null;\n\t\t\tconst colorSpace = ( _currentRenderTarget === null ) ? _this.outputColorSpace : ( _currentRenderTarget.isXRRenderTarget === true ? _currentRenderTarget.texture.colorSpace : LinearSRGBColorSpace );\n\t\t\tconst envMap = ( material.isMeshStandardMaterial ? cubeuvmaps : cubemaps ).get( material.envMap || environment );\n\t\t\tconst vertexAlphas = material.vertexColors === true && !! geometry.attributes.color && geometry.attributes.color.itemSize === 4;\n\t\t\tconst vertexTangents = !! geometry.attributes.tangent && ( !! material.normalMap || material.anisotropy > 0 );\n\t\t\tconst morphTargets = !! geometry.morphAttributes.position;\n\t\t\tconst morphNormals = !! geometry.morphAttributes.normal;\n\t\t\tconst morphColors = !! geometry.morphAttributes.color;\n\n\t\t\tlet toneMapping = NoToneMapping;\n\n\t\t\tif ( material.toneMapped ) {\n\n\t\t\t\tif ( _currentRenderTarget === null || _currentRenderTarget.isXRRenderTarget === true ) {\n\n\t\t\t\t\ttoneMapping = _this.toneMapping;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tconst morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;\n\t\t\tconst morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;\n\n\t\t\tconst materialProperties = properties.get( material );\n\t\t\tconst lights = currentRenderState.state.lights;\n\n\t\t\tif ( _clippingEnabled === true ) {\n\n\t\t\t\tif ( _localClippingEnabled === true || camera !== _currentCamera ) {\n\n\t\t\t\t\tconst useCache =\n\t\t\t\t\t\tcamera === _currentCamera &&\n\t\t\t\t\t\tmaterial.id === _currentMaterialId;\n\n\t\t\t\t\t// we might want to call this function with some ClippingGroup\n\t\t\t\t\t// object instead of the material, once it becomes feasible\n\t\t\t\t\t// (#8465, #8379)\n\t\t\t\t\tclipping.setState( material, camera, useCache );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t//\n\n\t\t\tlet needsProgramChange = false;\n\n\t\t\tif ( material.version === materialProperties.__version ) {\n\n\t\t\t\tif ( materialProperties.needsLights && ( materialProperties.lightsStateVersion !== lights.state.version ) ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.outputColorSpace !== colorSpace ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( object.isBatchedMesh && materialProperties.batching === false ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( ! object.isBatchedMesh && materialProperties.batching === true ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( object.isBatchedMesh && materialProperties.batchingColor === true && object.colorTexture === null ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( object.isBatchedMesh && materialProperties.batchingColor === false && object.colorTexture !== null ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( object.isInstancedMesh && materialProperties.instancing === false ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( ! object.isInstancedMesh && materialProperties.instancing === true ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( object.isSkinnedMesh && materialProperties.skinning === false ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( ! object.isSkinnedMesh && materialProperties.skinning === true ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( object.isInstancedMesh && materialProperties.instancingColor === true && object.instanceColor === null ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( object.isInstancedMesh && materialProperties.instancingColor === false && object.instanceColor !== null ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( object.isInstancedMesh && materialProperties.instancingMorph === true && object.morphTexture === null ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( object.isInstancedMesh && materialProperties.instancingMorph === false && object.morphTexture !== null ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.envMap !== envMap ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( material.fog === true && materialProperties.fog !== fog ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.numClippingPlanes !== undefined &&\n\t\t\t\t\t( materialProperties.numClippingPlanes !== clipping.numPlanes ||\n\t\t\t\t\tmaterialProperties.numIntersection !== clipping.numIntersection ) ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.vertexAlphas !== vertexAlphas ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.vertexTangents !== vertexTangents ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.morphTargets !== morphTargets ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.morphNormals !== morphNormals ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.morphColors !== morphColors ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.toneMapping !== toneMapping ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t} else if ( materialProperties.morphTargetsCount !== morphTargetsCount ) {\n\n\t\t\t\t\tneedsProgramChange = true;\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tneedsProgramChange = true;\n\t\t\t\tmaterialProperties.__version = material.version;\n\n\t\t\t}\n\n\t\t\t//\n\n\t\t\tlet program = materialProperties.currentProgram;\n\n\t\t\tif ( needsProgramChange === true ) {\n\n\t\t\t\tprogram = getProgram( material, scene, object );\n\n\t\t\t}\n\n\t\t\tlet refreshProgram = false;\n\t\t\tlet refreshMaterial = false;\n\t\t\tlet refreshLights = false;\n\n\t\t\tconst p_uniforms = program.getUniforms(),\n\t\t\t\tm_uniforms = materialProperties.uniforms;\n\n\t\t\tif ( state.useProgram( program.program ) ) {\n\n\t\t\t\trefreshProgram = true;\n\t\t\t\trefreshMaterial = true;\n\t\t\t\trefreshLights = true;\n\n\t\t\t}\n\n\t\t\tif ( material.id !== _currentMaterialId ) {\n\n\t\t\t\t_currentMaterialId = material.id;\n\n\t\t\t\trefreshMaterial = true;\n\n\t\t\t}\n\n\t\t\tif ( refreshProgram || _currentCamera !== camera ) {\n\n\t\t\t\t// common camera uniforms\n\n\t\t\t\tconst reversedDepthBuffer = state.buffers.depth.getReversed();\n\n\t\t\t\tif ( reversedDepthBuffer && camera.reversedDepth !== true ) {\n\n\t\t\t\t\tcamera._reversedDepth = true;\n\t\t\t\t\tcamera.updateProjectionMatrix();\n\n\t\t\t\t}\n\n\t\t\t\tp_uniforms.setValue( _gl, 'projectionMatrix', camera.projectionMatrix );\n\n\t\t\t\tp_uniforms.setValue( _gl, 'viewMatrix', camera.matrixWorldInverse );\n\n\t\t\t\tconst uCamPos = p_uniforms.map.cameraPosition;\n\n\t\t\t\tif ( uCamPos !== undefined ) {\n\n\t\t\t\t\tuCamPos.setValue( _gl, _vector3.setFromMatrixPosition( camera.matrixWorld ) );\n\n\t\t\t\t}\n\n\t\t\t\tif ( capabilities.logarithmicDepthBuffer ) {\n\n\t\t\t\t\tp_uniforms.setValue( _gl, 'logDepthBufFC',\n\t\t\t\t\t\t2.0 / ( Math.log( camera.far + 1.0 ) / Math.LN2 ) );\n\n\t\t\t\t}\n\n\t\t\t\t// consider moving isOrthographic to UniformLib and WebGLMaterials, see https://github.com/mrdoob/three.js/pull/26467#issuecomment-1645185067\n\n\t\t\t\tif ( material.isMeshPhongMaterial ||\n\t\t\t\t\tmaterial.isMeshToonMaterial ||\n\t\t\t\t\tmaterial.isMeshLambertMaterial ||\n\t\t\t\t\tmaterial.isMeshBasicMaterial ||\n\t\t\t\t\tmaterial.isMeshStandardMaterial ||\n\t\t\t\t\tmaterial.isShaderMaterial ) {\n\n\t\t\t\t\tp_uniforms.setValue( _gl, 'isOrthographic', camera.isOrthographicCamera === true );\n\n\t\t\t\t}\n\n\t\t\t\tif ( _currentCamera !== camera ) {\n\n\t\t\t\t\t_currentCamera = camera;\n\n\t\t\t\t\t// lighting uniforms depend on the camera so enforce an update\n\t\t\t\t\t// now, in case this material supports lights - or later, when\n\t\t\t\t\t// the next material that does gets activated:\n\n\t\t\t\t\trefreshMaterial = true;\t\t// set to true on material change\n\t\t\t\t\trefreshLights = true;\t\t// remains set until update done\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// Pre-allocate texture units for shadow samplers before setting data textures\n\t\t\tif ( materialProperties.needsLights ) {\n\n\t\t\t\t// Set shadow map uniforms first to ensure they get the first texture units\n\t\t\t\tif ( lights.state.directionalShadowMap.length > 0 ) {\n\n\t\t\t\t\tp_uniforms.setValue( _gl, 'directionalShadowMap', lights.state.directionalShadowMap, textures );\n\n\t\t\t\t}\n\n\t\t\t\tif ( lights.state.spotShadowMap.length > 0 ) {\n\n\t\t\t\t\tp_uniforms.setValue( _gl, 'spotShadowMap', lights.state.spotShadowMap, textures );\n\n\t\t\t\t}\n\n\t\t\t\tif ( lights.state.pointShadowMap.length > 0 ) {\n\n\t\t\t\t\tp_uniforms.setValue( _gl, 'pointShadowMap', lights.state.pointShadowMap, textures );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// skinning and morph target uniforms must be set even if material didn't change\n\t\t\t// auto-setting of texture unit for bone and morph texture must go before other textures\n\t\t\t// otherwise textures used for skinning and morphing can take over texture units reserved for other material textures\n\n\t\t\tif ( object.isSkinnedMesh ) {\n\n\t\t\t\tp_uniforms.setOptional( _gl, object, 'bindMatrix' );\n\t\t\t\tp_uniforms.setOptional( _gl, object, 'bindMatrixInverse' );\n\n\t\t\t\tconst skeleton = object.skeleton;\n\n\t\t\t\tif ( skeleton ) {\n\n\t\t\t\t\tif ( skeleton.boneTexture === null ) skeleton.computeBoneTexture();\n\n\t\t\t\t\tp_uniforms.setValue( _gl, 'boneTexture', skeleton.boneTexture, textures );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( object.isBatchedMesh ) {\n\n\t\t\t\tp_uniforms.setOptional( _gl, object, 'batchingTexture' );\n\t\t\t\tp_uniforms.setValue( _gl, 'batchingTexture', object._matricesTexture, textures );\n\n\t\t\t\tp_uniforms.setOptional( _gl, object, 'batchingIdTexture' );\n\t\t\t\tp_uniforms.setValue( _gl, 'batchingIdTexture', object._indirectTexture, textures );\n\n\t\t\t\tp_uniforms.setOptional( _gl, object, 'batchingColorTexture' );\n\t\t\t\tif ( object._colorsTexture !== null ) {\n\n\t\t\t\t\tp_uniforms.setValue( _gl, 'batchingColorTexture', object._colorsTexture, textures );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tconst morphAttributes = geometry.morphAttributes;\n\n\t\t\tif ( morphAttributes.position !== undefined || morphAttributes.normal !== undefined || ( morphAttributes.color !== undefined ) ) {\n\n\t\t\t\tmorphtargets.update( object, geometry, program );\n\n\t\t\t}\n\n\t\t\tif ( refreshMaterial || materialProperties.receiveShadow !== object.receiveShadow ) {\n\n\t\t\t\tmaterialProperties.receiveShadow = object.receiveShadow;\n\t\t\t\tp_uniforms.setValue( _gl, 'receiveShadow', object.receiveShadow );\n\n\t\t\t}\n\n\t\t\t// https://github.com/mrdoob/three.js/pull/24467#issuecomment-1209031512\n\n\t\t\tif ( material.isMeshGouraudMaterial && material.envMap !== null ) {\n\n\t\t\t\tm_uniforms.envMap.value = envMap;\n\n\t\t\t\tm_uniforms.flipEnvMap.value = ( envMap.isCubeTexture && envMap.isRenderTargetTexture === false ) ? -1 : 1;\n\n\t\t\t}\n\n\t\t\tif ( material.isMeshStandardMaterial && material.envMap === null && scene.environment !== null ) {\n\n\t\t\t\tm_uniforms.envMapIntensity.value = scene.environmentIntensity;\n\n\t\t\t}\n\n\t\t\t// Set DFG LUT for physically-based materials\n\t\t\tif ( m_uniforms.dfgLUT !== undefined ) {\n\n\t\t\t\tm_uniforms.dfgLUT.value = getDFGLUT();\n\n\t\t\t}\n\n\t\t\tif ( refreshMaterial ) {\n\n\t\t\t\tp_uniforms.setValue( _gl, 'toneMappingExposure', _this.toneMappingExposure );\n\n\t\t\t\tif ( materialProperties.needsLights ) {\n\n\t\t\t\t\t// the current material requires lighting info\n\n\t\t\t\t\t// note: all lighting uniforms are always set correctly\n\t\t\t\t\t// they simply reference the renderer's state for their\n\t\t\t\t\t// values\n\t\t\t\t\t//\n\t\t\t\t\t// use the current material's .needsUpdate flags to set\n\t\t\t\t\t// the GL state when required\n\n\t\t\t\t\tmarkUniformsLightsNeedsUpdate( m_uniforms, refreshLights );\n\n\t\t\t\t}\n\n\t\t\t\t// refresh uniforms common to several materials\n\n\t\t\t\tif ( fog && material.fog === true ) {\n\n\t\t\t\t\tmaterials.refreshFogUniforms( m_uniforms, fog );\n\n\t\t\t\t}\n\n\t\t\t\tmaterials.refreshMaterialUniforms( m_uniforms, material, _pixelRatio, _height, currentRenderState.state.transmissionRenderTarget[ camera.id ] );\n\n\t\t\t\tWebGLUniforms.upload( _gl, getUniformList( materialProperties ), m_uniforms, textures );\n\n\t\t\t}\n\n\t\t\tif ( material.isShaderMaterial && material.uniformsNeedUpdate === true ) {\n\n\t\t\t\tWebGLUniforms.upload( _gl, getUniformList( materialProperties ), m_uniforms, textures );\n\t\t\t\tmaterial.uniformsNeedUpdate = false;\n\n\t\t\t}\n\n\t\t\tif ( material.isSpriteMaterial ) {\n\n\t\t\t\tp_uniforms.setValue( _gl, 'center', object.center );\n\n\t\t\t}\n\n\t\t\t// common matrices\n\n\t\t\tp_uniforms.setValue( _gl, 'modelViewMatrix', object.modelViewMatrix );\n\t\t\tp_uniforms.setValue( _gl, 'normalMatrix', object.normalMatrix );\n\t\t\tp_uniforms.setValue( _gl, 'modelMatrix', object.matrixWorld );\n\n\t\t\t// UBOs\n\n\t\t\tif ( material.isShaderMaterial || material.isRawShaderMaterial ) {\n\n\t\t\t\tconst groups = material.uniformsGroups;\n\n\t\t\t\tfor ( let i = 0, l = groups.length; i < l; i ++ ) {\n\n\t\t\t\t\tconst group = groups[ i ];\n\n\t\t\t\t\tuniformsGroups.update( group, program );\n\t\t\t\t\tuniformsGroups.bind( group, program );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn program;\n\n\t\t}\n\n\t\t// If uniforms are marked as clean, they don't need to be loaded to the GPU.\n\n\t\tfunction markUniformsLightsNeedsUpdate( uniforms, value ) {\n\n\t\t\tuniforms.ambientLightColor.needsUpdate = value;\n\t\t\tuniforms.lightProbe.needsUpdate = value;\n\n\t\t\tuniforms.directionalLights.needsUpdate = value;\n\t\t\tuniforms.directionalLightShadows.needsUpdate = value;\n\t\t\tuniforms.pointLights.needsUpdate = value;\n\t\t\tuniforms.pointLightShadows.needsUpdate = value;\n\t\t\tuniforms.spotLights.needsUpdate = value;\n\t\t\tuniforms.spotLightShadows.needsUpdate = value;\n\t\t\tuniforms.rectAreaLights.needsUpdate = value;\n\t\t\tuniforms.hemisphereLights.needsUpdate = value;\n\n\t\t}\n\n\t\tfunction materialNeedsLights( material ) {\n\n\t\t\treturn material.isMeshLambertMaterial || material.isMeshToonMaterial || material.isMeshPhongMaterial ||\n\t\t\t\tmaterial.isMeshStandardMaterial || material.isShadowMaterial ||\n\t\t\t\t( material.isShaderMaterial && material.lights === true );\n\n\t\t}\n\n\t\t/**\n\t\t * Returns the active cube face.\n\t\t *\n\t\t * @return {number} The active cube face.\n\t\t */\n\t\tthis.getActiveCubeFace = function () {\n\n\t\t\treturn _currentActiveCubeFace;\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the active mipmap level.\n\t\t *\n\t\t * @return {number} The active mipmap level.\n\t\t */\n\t\tthis.getActiveMipmapLevel = function () {\n\n\t\t\treturn _currentActiveMipmapLevel;\n\n\t\t};\n\n\t\t/**\n\t\t * Returns the active render target.\n\t\t *\n\t\t * @return {?WebGLRenderTarget} The active render target. Returns `null` if no render target\n\t\t * is currently set.\n\t\t */\n\t\tthis.getRenderTarget = function () {\n\n\t\t\treturn _currentRenderTarget;\n\n\t\t};\n\n\t\tthis.setRenderTargetTextures = function ( renderTarget, colorTexture, depthTexture ) {\n\n\t\t\tconst renderTargetProperties = properties.get( renderTarget );\n\n\t\t\trenderTargetProperties.__autoAllocateDepthBuffer = renderTarget.resolveDepthBuffer === false;\n\t\t\tif ( renderTargetProperties.__autoAllocateDepthBuffer === false ) {\n\n\t\t\t\t// The multisample_render_to_texture extension doesn't work properly if there\n\t\t\t\t// are midframe flushes and an external depth buffer. Disable use of the extension.\n\t\t\t\trenderTargetProperties.__useRenderToTexture = false;\n\n\t\t\t}\n\n\t\t\tproperties.get( renderTarget.texture ).__webglTexture = colorTexture;\n\t\t\tproperties.get( renderTarget.depthTexture ).__webglTexture = renderTargetProperties.__autoAllocateDepthBuffer ? undefined : depthTexture;\n\n\t\t\trenderTargetProperties.__hasExternalTextures = true;\n\n\t\t};\n\n\t\tthis.setRenderTargetFramebuffer = function ( renderTarget, defaultFramebuffer ) {\n\n\t\t\tconst renderTargetProperties = properties.get( renderTarget );\n\t\t\trenderTargetProperties.__webglFramebuffer = defaultFramebuffer;\n\t\t\trenderTargetProperties.__useDefaultFramebuffer = defaultFramebuffer === undefined;\n\n\t\t};\n\n\t\tconst _scratchFrameBuffer = _gl.createFramebuffer();\n\n\t\t/**\n\t\t * Sets the active rendertarget.\n\t\t *\n\t\t * @param {?WebGLRenderTarget} renderTarget - The render target to set. When `null` is given,\n\t\t * the canvas is set as the active render target instead.\n\t\t * @param {number} [activeCubeFace=0] - The active cube face when using a cube render target.\n\t\t * Indicates the z layer to render in to when using 3D or array render targets.\n\t\t * @param {number} [activeMipmapLevel=0] - The active mipmap level.\n\t\t */\n\t\tthis.setRenderTarget = function ( renderTarget, activeCubeFace = 0, activeMipmapLevel = 0 ) {\n\n\t\t\t_currentRenderTarget = renderTarget;\n\t\t\t_currentActiveCubeFace = activeCubeFace;\n\t\t\t_currentActiveMipmapLevel = activeMipmapLevel;\n\n\t\t\tlet framebuffer = null;\n\t\t\tlet isCube = false;\n\t\t\tlet isRenderTarget3D = false;\n\n\t\t\tif ( renderTarget ) {\n\n\t\t\t\tconst renderTargetProperties = properties.get( renderTarget );\n\n\t\t\t\tif ( renderTargetProperties.__useDefaultFramebuffer !== undefined ) {\n\n\t\t\t\t\t// Externally-managed framebuffer (e.g. XR)\n\t\t\t\t\t// Bind to the stored framebuffer (may be null for default, or a WebGLFramebuffer)\n\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer );\n\n\t\t\t\t\t_currentViewport.copy( renderTarget.viewport );\n\t\t\t\t\t_currentScissor.copy( renderTarget.scissor );\n\t\t\t\t\t_currentScissorTest = renderTarget.scissorTest;\n\n\t\t\t\t\tstate.viewport( _currentViewport );\n\t\t\t\t\tstate.scissor( _currentScissor );\n\t\t\t\t\tstate.setScissorTest( _currentScissorTest );\n\n\t\t\t\t\t_currentMaterialId = -1;\n\n\t\t\t\t\treturn;\n\n\t\t\t\t} else if ( renderTargetProperties.__webglFramebuffer === undefined ) {\n\n\t\t\t\t\ttextures.setupRenderTarget( renderTarget );\n\n\t\t\t\t} else if ( renderTargetProperties.__hasExternalTextures ) {\n\n\t\t\t\t\t// Color and depth texture must be rebound in order for the swapchain to update.\n\t\t\t\t\ttextures.rebindTextures( renderTarget, properties.get( renderTarget.texture ).__webglTexture, properties.get( renderTarget.depthTexture ).__webglTexture );\n\n\t\t\t\t} else if ( renderTarget.depthBuffer ) {\n\n\t\t\t\t\t// check if the depth texture is already bound to the frame buffer and that it's been initialized\n\t\t\t\t\tconst depthTexture = renderTarget.depthTexture;\n\t\t\t\t\tif ( renderTargetProperties.__boundDepthTexture !== depthTexture ) {\n\n\t\t\t\t\t\t// check if the depth texture is compatible\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tdepthTexture !== null &&\n\t\t\t\t\t\t\tproperties.has( depthTexture ) &&\n\t\t\t\t\t\t\t( renderTarget.width !== depthTexture.image.width || renderTarget.height !== depthTexture.image.height )\n\t\t\t\t\t\t) {\n\n\t\t\t\t\t\t\tthrow new Error( 'WebGLRenderTarget: Attached DepthTexture is initialized to the incorrect size.' );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Swap the depth buffer to the currently attached one\n\t\t\t\t\t\ttextures.setupDepthRenderbuffer( renderTarget );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tconst texture = renderTarget.texture;\n\n\t\t\t\tif ( texture.isData3DTexture || texture.isDataArrayTexture || texture.isCompressedArrayTexture ) {\n\n\t\t\t\t\tisRenderTarget3D = true;\n\n\t\t\t\t}\n\n\t\t\t\tconst __webglFramebuffer = properties.get( renderTarget ).__webglFramebuffer;\n\n\t\t\t\tif ( renderTarget.isWebGLCubeRenderTarget ) {\n\n\t\t\t\t\tif ( Array.isArray( __webglFramebuffer[ activeCubeFace ] ) ) {\n\n\t\t\t\t\t\tframebuffer = __webglFramebuffer[ activeCubeFace ][ activeMipmapLevel ];\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tframebuffer = __webglFramebuffer[ activeCubeFace ];\n\n\t\t\t\t\t}\n\n\t\t\t\t\tisCube = true;\n\n\t\t\t\t} else if ( ( renderTarget.samples > 0 ) && textures.useMultisampledRTT( renderTarget ) === false ) {\n\n\t\t\t\t\tframebuffer = properties.get( renderTarget ).__webglMultisampledFramebuffer;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tif ( Array.isArray( __webglFramebuffer ) ) {\n\n\t\t\t\t\t\tframebuffer = __webglFramebuffer[ activeMipmapLevel ];\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tframebuffer = __webglFramebuffer;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\t_currentViewport.copy( renderTarget.viewport );\n\t\t\t\t_currentScissor.copy( renderTarget.scissor );\n\t\t\t\t_currentScissorTest = renderTarget.scissorTest;\n\n\t\t\t} else {\n\n\t\t\t\t_currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ).floor();\n\t\t\t\t_currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ).floor();\n\t\t\t\t_currentScissorTest = _scissorTest;\n\n\t\t\t}\n\n\t\t\t// Use a scratch frame buffer if rendering to a mip level to avoid depth buffers\n\t\t\t// being bound that are different sizes.\n\t\t\tif ( activeMipmapLevel !== 0 ) {\n\n\t\t\t\tframebuffer = _scratchFrameBuffer;\n\n\t\t\t}\n\n\t\t\tconst framebufferBound = state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );\n\n\t\t\tif ( framebufferBound ) {\n\n\t\t\t\tstate.drawBuffers( renderTarget, framebuffer );\n\n\t\t\t}\n\n\t\t\tstate.viewport( _currentViewport );\n\t\t\tstate.scissor( _currentScissor );\n\t\t\tstate.setScissorTest( _currentScissorTest );\n\n\t\t\tif ( isCube ) {\n\n\t\t\t\tconst textureProperties = properties.get( renderTarget.texture );\n\t\t\t\t_gl.framebufferTexture2D( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_CUBE_MAP_POSITIVE_X + activeCubeFace, textureProperties.__webglTexture, activeMipmapLevel );\n\n\t\t\t} else if ( isRenderTarget3D ) {\n\n\t\t\t\tconst layer = activeCubeFace;\n\n\t\t\t\tfor ( let i = 0; i < renderTarget.textures.length; i ++ ) {\n\n\t\t\t\t\tconst textureProperties = properties.get( renderTarget.textures[ i ] );\n\n\t\t\t\t\t_gl.framebufferTextureLayer( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0 + i, textureProperties.__webglTexture, activeMipmapLevel, layer );\n\n\t\t\t\t}\n\n\t\t\t} else if ( renderTarget !== null && activeMipmapLevel !== 0 ) {\n\n\t\t\t\t// Only bind the frame buffer if we are using a scratch frame buffer to render to a mipmap.\n\t\t\t\t// If we rebind the texture when using a multi sample buffer then an error about inconsistent samples will be thrown.\n\t\t\t\tconst textureProperties = properties.get( renderTarget.texture );\n\t\t\t\t_gl.framebufferTexture2D( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, textureProperties.__webglTexture, activeMipmapLevel );\n\n\t\t\t}\n\n\t\t\t_currentMaterialId = -1; // reset current material to ensure correct uniform bindings\n\n\t\t};\n\n\t\t/**\n\t\t * Reads the pixel data from the given render target into the given buffer.\n\t\t *\n\t\t * @param {WebGLRenderTarget} renderTarget - The render target to read from.\n\t\t * @param {number} x - The `x` coordinate of the copy region's origin.\n\t\t * @param {number} y - The `y` coordinate of the copy region's origin.\n\t\t * @param {number} width - The width of the copy region.\n\t\t * @param {number} height - The height of the copy region.\n\t\t * @param {TypedArray} buffer - The result buffer.\n\t\t * @param {number} [activeCubeFaceIndex] - The active cube face index.\n\t\t * @param {number} [textureIndex=0] - The texture index of an MRT render target.\n\t\t */\n\t\tthis.readRenderTargetPixels = function ( renderTarget, x, y, width, height, buffer, activeCubeFaceIndex, textureIndex = 0 ) {\n\n\t\t\tif ( ! ( renderTarget && renderTarget.isWebGLRenderTarget ) ) {\n\n\t\t\t\terror( 'WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.' );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tlet framebuffer = properties.get( renderTarget ).__webglFramebuffer;\n\n\t\t\tif ( renderTarget.isWebGLCubeRenderTarget && activeCubeFaceIndex !== undefined ) {\n\n\t\t\t\tframebuffer = framebuffer[ activeCubeFaceIndex ];\n\n\t\t\t}\n\n\t\t\tif ( framebuffer ) {\n\n\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );\n\n\t\t\t\ttry {\n\n\t\t\t\t\tconst texture = renderTarget.textures[ textureIndex ];\n\t\t\t\t\tconst textureFormat = texture.format;\n\t\t\t\t\tconst textureType = texture.type;\n\n\t\t\t\t\tif ( ! capabilities.textureFormatReadable( textureFormat ) ) {\n\n\t\t\t\t\t\terror( 'WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.' );\n\t\t\t\t\t\treturn;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! capabilities.textureTypeReadable( textureType ) ) {\n\n\t\t\t\t\t\terror( 'WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.' );\n\t\t\t\t\t\treturn;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// the following if statement ensures valid read requests (no out-of-bounds pixels, see #8604)\n\n\t\t\t\t\tif ( ( x >= 0 && x <= ( renderTarget.width - width ) ) && ( y >= 0 && y <= ( renderTarget.height - height ) ) ) {\n\n\t\t\t\t\t\t// when using MRT, select the correct color buffer for the subsequent read command\n\n\t\t\t\t\t\tif ( renderTarget.textures.length > 1 ) _gl.readBuffer( _gl.COLOR_ATTACHMENT0 + textureIndex );\n\n\t\t\t\t\t\t_gl.readPixels( x, y, width, height, utils.convert( textureFormat ), utils.convert( textureType ), buffer );\n\n\t\t\t\t\t}\n\n\t\t\t\t} finally {\n\n\t\t\t\t\t// restore framebuffer of current render target if necessary\n\n\t\t\t\t\tconst framebuffer = ( _currentRenderTarget !== null ) ? properties.get( _currentRenderTarget ).__webglFramebuffer : null;\n\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Asynchronous, non-blocking version of {@link WebGLRenderer#readRenderTargetPixels}.\n\t\t *\n\t\t * It is recommended to use this version of `readRenderTargetPixels()` whenever possible.\n\t\t *\n\t\t * @async\n\t\t * @param {WebGLRenderTarget} renderTarget - The render target to read from.\n\t\t * @param {number} x - The `x` coordinate of the copy region's origin.\n\t\t * @param {number} y - The `y` coordinate of the copy region's origin.\n\t\t * @param {number} width - The width of the copy region.\n\t\t * @param {number} height - The height of the copy region.\n\t\t * @param {TypedArray} buffer - The result buffer.\n\t\t * @param {number} [activeCubeFaceIndex] - The active cube face index.\n\t\t * @param {number} [textureIndex=0] - The texture index of an MRT render target.\n\t\t * @return {Promise<TypedArray>} A Promise that resolves when the read has been finished. The resolve provides the read data as a typed array.\n\t\t */\n\t\tthis.readRenderTargetPixelsAsync = async function ( renderTarget, x, y, width, height, buffer, activeCubeFaceIndex, textureIndex = 0 ) {\n\n\t\t\tif ( ! ( renderTarget && renderTarget.isWebGLRenderTarget ) ) {\n\n\t\t\t\tthrow new Error( 'THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.' );\n\n\t\t\t}\n\n\t\t\tlet framebuffer = properties.get( renderTarget ).__webglFramebuffer;\n\t\t\tif ( renderTarget.isWebGLCubeRenderTarget && activeCubeFaceIndex !== undefined ) {\n\n\t\t\t\tframebuffer = framebuffer[ activeCubeFaceIndex ];\n\n\t\t\t}\n\n\t\t\tif ( framebuffer ) {\n\n\t\t\t\t// the following if statement ensures valid read requests (no out-of-bounds pixels, see #8604)\n\t\t\t\tif ( ( x >= 0 && x <= ( renderTarget.width - width ) ) && ( y >= 0 && y <= ( renderTarget.height - height ) ) ) {\n\n\t\t\t\t\t// set the active frame buffer to the one we want to read\n\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer );\n\n\t\t\t\t\tconst texture = renderTarget.textures[ textureIndex ];\n\t\t\t\t\tconst textureFormat = texture.format;\n\t\t\t\t\tconst textureType = texture.type;\n\n\t\t\t\t\tif ( ! capabilities.textureFormatReadable( textureFormat ) ) {\n\n\t\t\t\t\t\tthrow new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.' );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! capabilities.textureTypeReadable( textureType ) ) {\n\n\t\t\t\t\t\tthrow new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.' );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tconst glBuffer = _gl.createBuffer();\n\t\t\t\t\t_gl.bindBuffer( _gl.PIXEL_PACK_BUFFER, glBuffer );\n\t\t\t\t\t_gl.bufferData( _gl.PIXEL_PACK_BUFFER, buffer.byteLength, _gl.STREAM_READ );\n\n\t\t\t\t\t// when using MRT, select the correct color buffer for the subsequent read command\n\n\t\t\t\t\tif ( renderTarget.textures.length > 1 ) _gl.readBuffer( _gl.COLOR_ATTACHMENT0 + textureIndex );\n\n\t\t\t\t\t_gl.readPixels( x, y, width, height, utils.convert( textureFormat ), utils.convert( textureType ), 0 );\n\n\t\t\t\t\t// reset the frame buffer to the currently set buffer before waiting\n\t\t\t\t\tconst currFramebuffer = _currentRenderTarget !== null ? properties.get( _currentRenderTarget ).__webglFramebuffer : null;\n\t\t\t\t\tstate.bindFramebuffer( _gl.FRAMEBUFFER, currFramebuffer );\n\n\t\t\t\t\t// check if the commands have finished every 8 ms\n\t\t\t\t\tconst sync = _gl.fenceSync( _gl.SYNC_GPU_COMMANDS_COMPLETE, 0 );\n\n\t\t\t\t\t_gl.flush();\n\n\t\t\t\t\tawait probeAsync( _gl, sync, 4 );\n\n\t\t\t\t\t// read the data and delete the buffer\n\t\t\t\t\t_gl.bindBuffer( _gl.PIXEL_PACK_BUFFER, glBuffer );\n\t\t\t\t\t_gl.getBufferSubData( _gl.PIXEL_PACK_BUFFER, 0, buffer );\n\t\t\t\t\t_gl.deleteBuffer( glBuffer );\n\t\t\t\t\t_gl.deleteSync( sync );\n\n\t\t\t\t\treturn buffer;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthrow new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.' );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Copies pixels from the current bound framebuffer into the given texture.\n\t\t *\n\t\t * @param {FramebufferTexture} texture - The texture.\n\t\t * @param {?Vector2} [position=null] - The start position of the copy operation.\n\t\t * @param {number} [level=0] - The mip level. The default represents the base mip.\n\t\t */\n\t\tthis.copyFramebufferToTexture = function ( texture, position = null, level = 0 ) {\n\n\t\t\tconst levelScale = Math.pow( 2, - level );\n\t\t\tconst width = Math.floor( texture.image.width * levelScale );\n\t\t\tconst height = Math.floor( texture.image.height * levelScale );\n\n\t\t\tconst x = position !== null ? position.x : 0;\n\t\t\tconst y = position !== null ? position.y : 0;\n\n\t\t\ttextures.setTexture2D( texture, 0 );\n\n\t\t\t_gl.copyTexSubImage2D( _gl.TEXTURE_2D, level, 0, 0, x, y, width, height );\n\n\t\t\tstate.unbindTexture();\n\n\t\t};\n\n\t\tconst _srcFramebuffer = _gl.createFramebuffer();\n\t\tconst _dstFramebuffer = _gl.createFramebuffer();\n\n\t\t/**\n\t\t * Copies data of the given source texture into a destination texture.\n\t\t *\n\t\t * When using render target textures as `srcTexture` and `dstTexture`, you must make sure both render targets are initialized\n\t\t * {@link WebGLRenderer#initRenderTarget}.\n\t\t *\n\t\t * @param {Texture} srcTexture - The source texture.\n\t\t * @param {Texture} dstTexture - The destination texture.\n\t\t * @param {?(Box2|Box3)} [srcRegion=null] - A bounding box which describes the source region. Can be two or three-dimensional.\n\t\t * @param {?(Vector2|Vector3)} [dstPosition=null] - A vector that represents the origin of the destination region. Can be two or three-dimensional.\n\t\t * @param {number} [srcLevel=0] - The source mipmap level to copy.\n\t\t * @param {?number} [dstLevel=null] - The destination mipmap level.\n\t\t */\n\t\tthis.copyTextureToTexture = function ( srcTexture, dstTexture, srcRegion = null, dstPosition = null, srcLevel = 0, dstLevel = null ) {\n\n\t\t\t// support the previous signature with just a single dst mipmap level\n\t\t\tif ( dstLevel === null ) {\n\n\t\t\t\tif ( srcLevel !== 0 ) {\n\n\t\t\t\t\t// @deprecated, r171\n\t\t\t\t\twarnOnce( 'WebGLRenderer: copyTextureToTexture function signature has changed to support src and dst mipmap levels.' );\n\t\t\t\t\tdstLevel = srcLevel;\n\t\t\t\t\tsrcLevel = 0;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tdstLevel = 0;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// gather the necessary dimensions to copy\n\t\t\tlet width, height, depth, minX, minY, minZ;\n\t\t\tlet dstX, dstY, dstZ;\n\t\t\tconst image = srcTexture.isCompressedTexture ? srcTexture.mipmaps[ dstLevel ] : srcTexture.image;\n\t\t\tif ( srcRegion !== null ) {\n\n\t\t\t\twidth = srcRegion.max.x - srcRegion.min.x;\n\t\t\t\theight = srcRegion.max.y - srcRegion.min.y;\n\t\t\t\tdepth = srcRegion.isBox3 ? srcRegion.max.z - srcRegion.min.z : 1;\n\t\t\t\tminX = srcRegion.min.x;\n\t\t\t\tminY = srcRegion.min.y;\n\t\t\t\tminZ = srcRegion.isBox3 ? srcRegion.min.z : 0;\n\n\t\t\t} else {\n\n\t\t\t\tconst levelScale = Math.pow( 2, - srcLevel );\n\t\t\t\twidth = Math.floor( image.width * levelScale );\n\t\t\t\theight = Math.floor( image.height * levelScale );\n\t\t\t\tif ( srcTexture.isDataArrayTexture ) {\n\n\t\t\t\t\tdepth = image.depth;\n\n\t\t\t\t} else if ( srcTexture.isData3DTexture ) {\n\n\t\t\t\t\tdepth = Math.floor( image.depth * levelScale );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tdepth = 1;\n\n\t\t\t\t}\n\n\t\t\t\tminX = 0;\n\t\t\t\tminY = 0;\n\t\t\t\tminZ = 0;\n\n\t\t\t}\n\n\t\t\tif ( dstPosition !== null ) {\n\n\t\t\t\tdstX = dstPosition.x;\n\t\t\t\tdstY = dstPosition.y;\n\t\t\t\tdstZ = dstPosition.z;\n\n\t\t\t} else {\n\n\t\t\t\tdstX = 0;\n\t\t\t\tdstY = 0;\n\t\t\t\tdstZ = 0;\n\n\t\t\t}\n\n\t\t\t// Set up the destination target\n\t\t\tconst glFormat = utils.convert( dstTexture.format );\n\t\t\tconst glType = utils.convert( dstTexture.type );\n\t\t\tlet glTarget;\n\n\t\t\tif ( dstTexture.isData3DTexture ) {\n\n\t\t\t\ttextures.setTexture3D( dstTexture, 0 );\n\t\t\t\tglTarget = _gl.TEXTURE_3D;\n\n\t\t\t} else if ( dstTexture.isDataArrayTexture || dstTexture.isCompressedArrayTexture ) {\n\n\t\t\t\ttextures.setTexture2DArray( dstTexture, 0 );\n\t\t\t\tglTarget = _gl.TEXTURE_2D_ARRAY;\n\n\t\t\t} else {\n\n\t\t\t\ttextures.setTexture2D( dstTexture, 0 );\n\t\t\t\tglTarget = _gl.TEXTURE_2D;\n\n\t\t\t}\n\n\t\t\t_gl.pixelStorei( _gl.UNPACK_FLIP_Y_WEBGL, dstTexture.flipY );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, dstTexture.premultiplyAlpha );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_ALIGNMENT, dstTexture.unpackAlignment );\n\n\t\t\t// used for copying data from cpu\n\t\t\tconst currentUnpackRowLen = _gl.getParameter( _gl.UNPACK_ROW_LENGTH );\n\t\t\tconst currentUnpackImageHeight = _gl.getParameter( _gl.UNPACK_IMAGE_HEIGHT );\n\t\t\tconst currentUnpackSkipPixels = _gl.getParameter( _gl.UNPACK_SKIP_PIXELS );\n\t\t\tconst currentUnpackSkipRows = _gl.getParameter( _gl.UNPACK_SKIP_ROWS );\n\t\t\tconst currentUnpackSkipImages = _gl.getParameter( _gl.UNPACK_SKIP_IMAGES );\n\n\t\t\t_gl.pixelStorei( _gl.UNPACK_ROW_LENGTH, image.width );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_IMAGE_HEIGHT, image.height );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_PIXELS, minX );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_ROWS, minY );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_IMAGES, minZ );\n\n\t\t\t// set up the src texture\n\t\t\tconst isSrc3D = srcTexture.isDataArrayTexture || srcTexture.isData3DTexture;\n\t\t\tconst isDst3D = dstTexture.isDataArrayTexture || dstTexture.isData3DTexture;\n\t\t\tif ( srcTexture.isDepthTexture ) {\n\n\t\t\t\tconst srcTextureProperties = properties.get( srcTexture );\n\t\t\t\tconst dstTextureProperties = properties.get( dstTexture );\n\t\t\t\tconst srcRenderTargetProperties = properties.get( srcTextureProperties.__renderTarget );\n\t\t\t\tconst dstRenderTargetProperties = properties.get( dstTextureProperties.__renderTarget );\n\t\t\t\tstate.bindFramebuffer( _gl.READ_FRAMEBUFFER, srcRenderTargetProperties.__webglFramebuffer );\n\t\t\t\tstate.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, dstRenderTargetProperties.__webglFramebuffer );\n\n\t\t\t\tfor ( let i = 0; i < depth; i ++ ) {\n\n\t\t\t\t\t// if the source or destination are a 3d target then a layer needs to be bound\n\t\t\t\t\tif ( isSrc3D ) {\n\n\t\t\t\t\t\t_gl.framebufferTextureLayer( _gl.READ_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, properties.get( srcTexture ).__webglTexture, srcLevel, minZ + i );\n\t\t\t\t\t\t_gl.framebufferTextureLayer( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, properties.get( dstTexture ).__webglTexture, dstLevel, dstZ + i );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t_gl.blitFramebuffer( minX, minY, width, height, dstX, dstY, width, height, _gl.DEPTH_BUFFER_BIT, _gl.NEAREST );\n\n\t\t\t\t}\n\n\t\t\t\tstate.bindFramebuffer( _gl.READ_FRAMEBUFFER, null );\n\t\t\t\tstate.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, null );\n\n\t\t\t} else if ( srcLevel !== 0 || srcTexture.isRenderTargetTexture || properties.has( srcTexture ) ) {\n\n\t\t\t\t// get the appropriate frame buffers\n\t\t\t\tconst srcTextureProperties = properties.get( srcTexture );\n\t\t\t\tconst dstTextureProperties = properties.get( dstTexture );\n\n\t\t\t\t// bind the frame buffer targets\n\t\t\t\tstate.bindFramebuffer( _gl.READ_FRAMEBUFFER, _srcFramebuffer );\n\t\t\t\tstate.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, _dstFramebuffer );\n\n\t\t\t\tfor ( let i = 0; i < depth; i ++ ) {\n\n\t\t\t\t\t// assign the correct layers and mip maps to the frame buffers\n\t\t\t\t\tif ( isSrc3D ) {\n\n\t\t\t\t\t\t_gl.framebufferTextureLayer( _gl.READ_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, srcTextureProperties.__webglTexture, srcLevel, minZ + i );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_gl.framebufferTexture2D( _gl.READ_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, srcTextureProperties.__webglTexture, srcLevel );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( isDst3D ) {\n\n\t\t\t\t\t\t_gl.framebufferTextureLayer( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, dstTextureProperties.__webglTexture, dstLevel, dstZ + i );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_gl.framebufferTexture2D( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, dstTextureProperties.__webglTexture, dstLevel );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// copy the data using the fastest function that can achieve the copy\n\t\t\t\t\tif ( srcLevel !== 0 ) {\n\n\t\t\t\t\t\t_gl.blitFramebuffer( minX, minY, width, height, dstX, dstY, width, height, _gl.COLOR_BUFFER_BIT, _gl.NEAREST );\n\n\t\t\t\t\t} else if ( isDst3D ) {\n\n\t\t\t\t\t\t_gl.copyTexSubImage3D( glTarget, dstLevel, dstX, dstY, dstZ + i, minX, minY, width, height );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_gl.copyTexSubImage2D( glTarget, dstLevel, dstX, dstY, minX, minY, width, height );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\t// unbind read, draw buffers\n\t\t\t\tstate.bindFramebuffer( _gl.READ_FRAMEBUFFER, null );\n\t\t\t\tstate.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, null );\n\n\t\t\t} else {\n\n\t\t\t\tif ( isDst3D ) {\n\n\t\t\t\t\t// copy data into the 3d texture\n\t\t\t\t\tif ( srcTexture.isDataTexture || srcTexture.isData3DTexture ) {\n\n\t\t\t\t\t\t_gl.texSubImage3D( glTarget, dstLevel, dstX, dstY, dstZ, width, height, depth, glFormat, glType, image.data );\n\n\t\t\t\t\t} else if ( dstTexture.isCompressedArrayTexture ) {\n\n\t\t\t\t\t\t_gl.compressedTexSubImage3D( glTarget, dstLevel, dstX, dstY, dstZ, width, height, depth, glFormat, image.data );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_gl.texSubImage3D( glTarget, dstLevel, dstX, dstY, dstZ, width, height, depth, glFormat, glType, image );\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// copy data into the 2d texture\n\t\t\t\t\tif ( srcTexture.isDataTexture ) {\n\n\t\t\t\t\t\t_gl.texSubImage2D( _gl.TEXTURE_2D, dstLevel, dstX, dstY, width, height, glFormat, glType, image.data );\n\n\t\t\t\t\t} else if ( srcTexture.isCompressedTexture ) {\n\n\t\t\t\t\t\t_gl.compressedTexSubImage2D( _gl.TEXTURE_2D, dstLevel, dstX, dstY, image.width, image.height, glFormat, image.data );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_gl.texSubImage2D( _gl.TEXTURE_2D, dstLevel, dstX, dstY, width, height, glFormat, glType, image );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// reset values\n\t\t\t_gl.pixelStorei( _gl.UNPACK_ROW_LENGTH, currentUnpackRowLen );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_IMAGE_HEIGHT, currentUnpackImageHeight );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_PIXELS, currentUnpackSkipPixels );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_ROWS, currentUnpackSkipRows );\n\t\t\t_gl.pixelStorei( _gl.UNPACK_SKIP_IMAGES, currentUnpackSkipImages );\n\n\t\t\t// Generate mipmaps only when copying level 0\n\t\t\tif ( dstLevel === 0 && dstTexture.generateMipmaps ) {\n\n\t\t\t\t_gl.generateMipmap( glTarget );\n\n\t\t\t}\n\n\t\t\tstate.unbindTexture();\n\n\t\t};\n\n\t\t/**\n\t\t * Initializes the given WebGLRenderTarget memory. Useful for initializing a render target so data\n\t\t * can be copied into it using {@link WebGLRenderer#copyTextureToTexture} before it has been\n\t\t * rendered to.\n\t\t *\n\t\t * @param {WebGLRenderTarget} target - The render target.\n\t\t */\n\t\tthis.initRenderTarget = function ( target ) {\n\n\t\t\tif ( properties.get( target ).__webglFramebuffer === undefined ) {\n\n\t\t\t\ttextures.setupRenderTarget( target );\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Initializes the given texture. Useful for preloading a texture rather than waiting until first\n\t\t * render (which can cause noticeable lags due to decode and GPU upload overhead).\n\t\t *\n\t\t * @param {Texture} texture - The texture.\n\t\t */\n\t\tthis.initTexture = function ( texture ) {\n\n\t\t\tif ( texture.isCubeTexture ) {\n\n\t\t\t\ttextures.setTextureCube( texture, 0 );\n\n\t\t\t} else if ( texture.isData3DTexture ) {\n\n\t\t\t\ttextures.setTexture3D( texture, 0 );\n\n\t\t\t} else if ( texture.isDataArrayTexture || texture.isCompressedArrayTexture ) {\n\n\t\t\t\ttextures.setTexture2DArray( texture, 0 );\n\n\t\t\t} else {\n\n\t\t\t\ttextures.setTexture2D( texture, 0 );\n\n\t\t\t}\n\n\t\t\tstate.unbindTexture();\n\n\t\t};\n\n\t\t/**\n\t\t * Can be used to reset the internal WebGL state. This method is mostly\n\t\t * relevant for applications which share a single WebGL context across\n\t\t * multiple WebGL libraries.\n\t\t */\n\t\tthis.resetState = function () {\n\n\t\t\t_currentActiveCubeFace = 0;\n\t\t\t_currentActiveMipmapLevel = 0;\n\t\t\t_currentRenderTarget = null;\n\n\t\t\tstate.reset();\n\t\t\tbindingStates.reset();\n\n\t\t};\n\n\t\tif ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {\n\n\t\t\t__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Defines the coordinate system of the renderer.\n\t *\n\t * In `WebGLRenderer`, the value is always `WebGLCoordinateSystem`.\n\t *\n\t * @type {WebGLCoordinateSystem|WebGPUCoordinateSystem}\n\t * @default WebGLCoordinateSystem\n\t * @readonly\n\t */\n\tget coordinateSystem() {\n\n\t\treturn WebGLCoordinateSystem;\n\n\t}\n\n\t/**\n\t * Defines the output color space of the renderer.\n\t *\n\t * @type {SRGBColorSpace|LinearSRGBColorSpace}\n\t * @default SRGBColorSpace\n\t */\n\tget outputColorSpace() {\n\n\t\treturn this._outputColorSpace;\n\n\t}\n\n\tset outputColorSpace( colorSpace ) {\n\n\t\tthis._outputColorSpace = colorSpace;\n\n\t\tconst gl = this.getContext();\n\t\tgl.drawingBufferColorSpace = ColorManagement._getDrawingBufferColorSpace( colorSpace );\n\t\tgl.unpackColorSpace = ColorManagement._getUnpackColorSpace();\n\n\t}\n\n}\n\nexport { ACESFilmicToneMapping, AddEquation, AddOperation, AdditiveBlending, AgXToneMapping, AlphaFormat, AlwaysCompare, AlwaysDepth, ArrayCamera, BackSide, BoxGeometry, BufferAttribute, BufferGeometry, ByteType, CineonToneMapping, ClampToEdgeWrapping, Color, ColorManagement, ConstantAlphaFactor, ConstantColorFactor, CubeDepthTexture, CubeReflectionMapping, CubeRefractionMapping, CubeTexture, CubeUVReflectionMapping, CullFaceBack, CullFaceFront, CullFaceNone, CustomBlending, CustomToneMapping, Data3DTexture, DataArrayTexture, DataTexture, DepthFormat, DepthStencilFormat, DepthTexture, DoubleSide, DstAlphaFactor, DstColorFactor, EqualCompare, EqualDepth, EquirectangularReflectionMapping, EquirectangularRefractionMapping, Euler, EventDispatcher, ExternalTexture, Float32BufferAttribute, FloatType, FrontSide, Frustum, GLSL3, GreaterCompare, GreaterDepth, GreaterEqualCompare, GreaterEqualDepth, HalfFloatType, IntType, Layers, LessCompare, LessDepth, LessEqualCompare, LessEqualDepth, LinearFilter, LinearMipmapLinearFilter, LinearMipmapNearestFilter, LinearSRGBColorSpace, LinearToneMapping, LinearTransfer, Matrix3, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshDistanceMaterial, MinEquation, MirroredRepeatWrapping, MixOperation, MultiplyBlending, MultiplyOperation, NearestFilter, NearestMipmapLinearFilter, NearestMipmapNearestFilter, NeutralToneMapping, NeverCompare, NeverDepth, NoBlending, NoColorSpace, NoToneMapping, NormalBlending, NotEqualCompare, NotEqualDepth, ObjectSpaceNormalMap, OneFactor, OneMinusConstantAlphaFactor, OneMinusConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcAlphaFactor, OneMinusSrcColorFactor, OrthographicCamera, PCFShadowMap, PCFSoftShadowMap, PMREMGenerator, PerspectiveCamera, Plane, PlaneGeometry, R11_EAC_Format, RED_GREEN_RGTC2_Format, RED_RGTC1_Format, REVISION, RG11_EAC_Format, RGBAFormat, RGBAIntegerFormat, RGBA_ASTC_10x10_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_BPTC_Format, RGBA_ETC2_EAC_Format, RGBA_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGBFormat, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGB_PVRTC_2BPPV1_Format, RGB_PVRTC_4BPPV1_Format, RGB_S3TC_DXT1_Format, RGFormat, RGIntegerFormat, RawShaderMaterial, RedFormat, RedIntegerFormat, ReinhardToneMapping, RepeatWrapping, ReverseSubtractEquation, SIGNED_R11_EAC_Format, SIGNED_RED_GREEN_RGTC2_Format, SIGNED_RED_RGTC1_Format, SIGNED_RG11_EAC_Format, SRGBColorSpace, SRGBTransfer, ShaderChunk, ShaderLib, ShaderMaterial, ShortType, SrcAlphaFactor, SrcAlphaSaturateFactor, SrcColorFactor, SubtractEquation, SubtractiveBlending, TangentSpaceNormalMap, Texture, Uint16BufferAttribute, Uint32BufferAttribute, UniformsLib, UniformsUtils, UnsignedByteType, UnsignedInt101111Type, UnsignedInt248Type, UnsignedInt5999Type, UnsignedIntType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedShortType, VSMShadowMap, Vector2, Vector3, Vector4, WebGLCoordinateSystem, WebGLCubeRenderTarget, WebGLRenderTarget, WebGLRenderer, WebGLUtils, WebXRController, ZeroFactor, createCanvasElement, error, log, warn, warnOnce };\n"
  },
  {
    "path": "web/src/styles/area.less",
    "content": "@marker-size: 14px;\n@marker-color: rgb(255, 0, 255);\n\n@import \"./src/styles/variables.less\";\n\n#area-selection-method {\n  max-width: @col2-width - 130px;\n}\n\n.printing-tech-container {\n    white-space: nowrap;\n}\n\n.all-addresses li {\n    list-style: none;\n}\n.all-addresses li input {\n  margin-right: 6px;\n}\n#advanced-input {\n  position: relative;\n  top: 1px;\n  margin-right: 2px;\n}\nhtml.advanced-mode .grey-in-advanced {\n  color: #aaa;\n}\nhtml.advanced-mode  input.grey-in-advanced:hover,\nhtml.advanced-mode button.grey-in-advanced:hover,\nhtml.advanced-mode select.grey-in-advanced:hover {\n  background-color: @input-color;\n}\n#printing-method,\n#map-size-preset,\n#map-scale-preset,\n#content-mode {\n  width: 100%;\n}\n#target-road-density-ui {\n  width: 90px;\n}\n\n.map-scale-coverage {\n  margin-top: 4px;\n  font-size: 13px;\n  color: #666;\n}\n\n.coords-input-row .col2 .single-input:first-child {\n  margin-bottom: 4px;\n}\n.coords-input-row .col2 .single-input label {\n  min-width: 128px;\n  display: inline-block;\n}\n.coords-input-row .col2 .single-input input {\n  width: 150px;\n}\n\n.map-offset-row .col2 .single-input:first-child {\n  margin-bottom: 4px;\n}\n.map-offset-row .col2 .single-input label {\n  display: inline-block;\n}\n.map-offset-row .col2 .single-input:last-child label {\n  padding-right: 2px;\n}\n.map-offset-row .col2 .single-input input {\n  width: 75px;\n}\n.map-offset-row .col2 .more {\n  white-space: nowrap;\n  display: inline-block;\n  min-width: 115px;\n}\n\n#scale-input,\n#radius-input {\n  width: 80px;\n}\n#map-size-input {\n  width: 60px;\n}\n#search-results li {\n  list-style: none;\n  margin-bottom: 4px;\n  //box-shadow: 0 0 10px #a8a8a8;\n  border-radius: 4px;\n}\n#search-results li:hover,\n.printing-method-container:hover,\n.advanced-input-container:hover,\n.multipart-map .col2:hover,\nselect:hover {\n  background-color: #eaeaea;\n}\n.printing-method-container {\n  white-space: nowrap;\n}\n#search-results li:last-child {\n  margin-bottom: 0;\n}\n#search-results input {\n  margin-right: 4px;\n  position: relative;\n  top: 1px;\n}\n.search-result-info {\n    margin: 12px 0 2px 0;\n    font-weight: 600;\n}\n.print-diameter,\n.print-road-width {\n  font-weight: 700;\n}\n.print-scale-row .one-to {\n  letter-spacing: 1px;\n}\n.drag-map-with-mouse {\n  text-align: center;\n}\nhtml.multipart-mode .drag-map-with-mouse {\n  display: none;\n}\n.map-info {\n  text-align: center;\n  position: relative;\n  top: -5px;\n}\n.map-info .texts {\n  background: white;\n  color: #444;\n  padding: 0 5px;\n  z-index: 1;\n  position: relative;\n}\n.map-info-arrow {\n  height: 15px;\n  border-top: 1px solid #999;\n  position: absolute;\n  top: 11px;\n  width: 100%;\n}\n.map-info-arrow .arrow {\n  position: absolute;\n  height: 100%;\n  top: -50%;\n  left: 0;\n}\n.map-info-arrow .arrow.left {\n  border-left: 1px solid #999;\n}\n\n.map-info-arrow .arrow.right {\n  border-right: 1px solid #999;\n  right: 0;\n}\n/* Show square aspect ratio map */\n#map-area-preview-container {\n  border: 1px solid #888;\n}\n\n/* Area movement buttons */\n\nhtml.multipart-mode .area-movement-buttons {\n  display: table;\n}\n.area-movement-buttons {\n  display: none;\n  text-align: center;\n  margin: 10px auto;\n  background-color: #f0f0f0;\n  border-radius: 4px;\n  border: 1px solid #ccc;\n}\n.area-movement-buttons h3 {\n  text-align: center;\n  margin: 5px 10px 5px 10px;\n}\n.area-movement-buttons .column {\n  display: inline-block;\n  vertical-align: middle;\n  margin: 5px 5px 10px 5px;\n  padding: 0 6px;\n}\n.area-movement-buttons .column .btn {\n  display: inline-block;\n  text-align: left;\n  padding: 0px 8px;\n  background-color: #ffffff;\n  border: 1px solid #888;\n  border-radius: 2px;\n  cursor: pointer;\n  transition: 0.2s;\n}\n.area-movement-buttons .column .btn:hover {\n  background-color: #e0e0e0;\n}\n.area-movement-buttons .column.middle .btn {\n  width: 100%;\n  margin-top: 3px;\n  display: block;\n}\n.area-movement-buttons .column.left,\n.area-movement-buttons .column.right {\n  display: inline-block;\n}\n.area-movement-buttons .column .spacer {\n  height: 13px;\n}\n@media only screen and (max-width: 504px) {\n  .area-movement-buttons .column.left .btn,\n  .area-movement-buttons .column.right .btn {\n    width: 100%;\n    display: block;\n    margin-top: 3px;\n  }\n  .area-movement-buttons .column {\n    margin-left: 0;\n    margin-right: 0;\n  }\n}\n.area-movement-buttons .current-values {\n  margin-bottom: 5px;\n}\n\n.marker-container {\n  position: absolute;\n  transform-origin: center;\n  transform: rotate(45deg);\n}\n.marker1-star {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  border: @marker-size/4 solid transparent;\n}\n.marker1-star.part1 {\n  top: -(@marker-size * 5 / 4);\n  left: -(@marker-size / 4);\n  border-bottom: @marker-size solid @marker-color;\n}\n.marker1-star.part2 {\n  left: -(@marker-size / 4);\n  border-top: @marker-size solid @marker-color;\n}\n.marker1-star.part3 {\n  top: -(@marker-size / 4);\n  border-left: @marker-size solid @marker-color;\n}\n.marker1-star.part4 {\n  left: -(@marker-size * 5 / 4);\n  top: -(@marker-size / 4);\n  border-right: @marker-size solid @marker-color;\n}\n.marker-tag {\n  display: none;\n  position: absolute;\n  width: @marker-size / (8 / 3);\n  height: @marker-size / (8 / 3);\n  background-color: white;\n  z-index: 10000;\n  top: -@marker-size / (16 / 3);\n  left: -@marker-size / (16 / 3);\n}\n"
  },
  {
    "path": "web/src/styles/bottom.less",
    "content": ".bottom {\n  padding-left: @main-wrapper-padding;\n  padding-right: @main-wrapper-padding;\n  padding-top: 30px;\n  padding-bottom: 30px;\n  box-shadow: 0 0 45px rgba(0, 0, 0, 0.5);\n  z-index: 1;\n  position: relative;\n}\n.bottom h2 {\n  margin: 20px 0;\n  font-size: 24px;\n}\n.fb-like-container {\n  text-align: center;\n  height: 15px;\n}\n.credits {\n  padding-top: 12px;\n  color: #8b8b8b;\n  font-weight: 600;\n  text-align: center;\n}\n.credits .sentence {\n  font-size: 14px;\n  white-space: nowrap;\n}\n.credits .powered-by-google {\n  height: 17px;\n  position: relative;\n  top: 4px;\n}\n.credits .playful-pixels-logo img {\n  height: 19px;\n  position: relative;\n  top: 5px;\n}\n.credits a {\n  color: #8b8b8b;\n}\n.open-source {\n  text-align: center;\n  font-size: 14px;\n}\n"
  },
  {
    "path": "web/src/styles/common.less",
    "content": "@import \"./src/styles/variables.less\";\n@import \"./src/styles/reset.less\";\n\n/*\n * General\n */\nhtml,\nbody {\n  background-color: #f0f0f0;\n\tmin-width: 320px;\n}\nhtml,\nbody,\n* {\n  font-family: 'Quattrocento Sans';\n  font-size: 17px;\n}\n.clearfix:after {\n  content: \".\";\n  visibility: hidden;\n  display: block;\n  height: 0;\n  clear: both;\n}\na {\n  text-decoration: none;\n}\ninput:invalid {\n  border-color: red !important;\n  box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);\n}\ninput {\n  background-color: #ffffff;\n  transition-duration: 0.2s;\n}\n\ninput:hover,\ninput:focus {\n  background-color: #ffffcc;\n  transition-duration: 0.2s;\n  border-color: #888;\n}\ninput[type=submit] {\n  appearance: none;\n}\n.visuallyhidden {\n  border: 0;\n  clip: rect(0 0 0 0);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  width: 1px;\n}\nh2 {\n  font-size: 150%;\n  margin-bottom: 20px;\n}\n/*\n * Rows, columns\n */\n.main-wrapper {\n  max-width: @total-width + 2 * @main-wrapper-padding;\n  min-width: 260px - 2 * @outside-margin-narrow;\n  margin: auto;\n  min-height: 100vh; // extend white BG to at least the bottom\n  border-bottom: 1px solid transparent; // keep contained bottom margins collapsing with body's, which would leave a bar of grey to the top\n  background-color: white;\n  box-shadow: 0 0 30px #ddd;\n  overflow: hidden;\n}\n.content {\n  padding: 10px @outside-margin 15px @outside-margin;\n  @media only screen and (max-width: 600px) {\n    padding-left: @outside-margin-semi;\n    padding-right: @outside-margin-semi;\n  }\n}\n.main-row {\n  margin: 17px 0 0;\n  display: flex;\n}\n.col1 {\n  width: @col1-width;\n  flex-shrink: 0;\n  font-weight: 700;\n}\n.col2 {\n  flex-basis: @col2-width;\n}\n\n/*\n * Square block container\n */\n.square-block-container {\n  display: inline-block;\n  position: relative;\n  width: 100%;\n}\n.square-block-container:after {\n  content: '';\n  display: block;\n  margin-top: 100%;\n}\n.square-block-container .dummy {\n  position: absolute;\n  overflow: hidden;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n}\n.square-block {\n  transform-origin: center center;\n  height: 100%;\n}\n\n/*\n * Breadcrumb\n */\n\n.breadcrumb {\n  margin-bottom: 15px;\n}\n.breadcrumb ul {\n  list-style: none;\n}\n.breadcrumb li {\n  display: inline;\n  color: #444 !important;\n}\n.breadcrumb li a {\n  color: #444 !important;\n}\n.breadcrumb li:before {\n  content: \"> \";\n}\n.breadcrumb li:first-child:before {\n  content: \"\";\n}\n.breadcrumb li[aria-current=\"location\"] {\n  font-weight: bold;\n}\n.breadcrumb li {\n  display: inline;\n  color: #888;\n}\n\n/*\n * Other\n */\ninput,\nbutton,\nselect {\n  padding: 2px 4px;\n  height: @form-field-height;\n  background-color: @input-color;\n  border: 1px solid #bbb;\n}\nselect {\n  padding: 1px 4px;\n}\nselect::-ms-expand {\n  display: none; /* hide arrow in IE */\n}\ninput[type=checkbox],\ninput[type=radio] {\n  padding: 0;\n  margin: 0;\n  height: auto;\n}\n\n.controls {\n\tz-index: 1;\n\tposition: relative;\n\tmargin: 0 !important;\n}\n\n.large-row {\n  width: 100%;\n  text-align: center;\n  border: 0;\n  border-radius: 4px;\n  line-height: normal;\n  color: black;\n  display: block;\n  cursor: pointer;\n  height: auto;\n  padding: 7px 10px;\n  box-shadow: 0px 3px 9px 0px rgba(0, 0, 0, 0.3);\n}\n.submit-row {\n  margin-bottom: 30px;\n}\n\n.green-button {\n  background-color: @green;\n  transition-duration: 0.2s;\n}\n.green-button:hover {\n  background-color: darken(@green, 5%);\n}\n\n.back-to-previous-page-container {\n  margin: 15px 5px 40px 5px;\n  text-align: center;\n}\n.back-to-previous-page {\n  background-color: #84ff00;\n  border-radius: 4px;\n  padding: 4px 10px;\n  color: black;\n}\n.back-to-previous-page::before {\n  content: \"← \";\n}\n\n@media only screen and (max-width: 450px) {\n  .main-row {\n    flex-wrap: wrap;\n  }\n  .col1 {\n    width: auto;\n  }\n}\n@media only screen and (max-width: 400px) {\n  .content {\n    padding-left: @outside-margin-narrow;\n    padding-right: @outside-margin-narrow;\n  }\n}\n\n@import \"./src/styles/top.less\";\n@import \"./src/styles/bottom.less\";\n"
  },
  {
    "path": "web/src/styles/help.less",
    "content": "@import \"./src/styles/variables.less\";\n\n.help-link {\n  display: none;\n}\n.close-help {\n  text-align: center;\n  margin-top: -15px;\n  margin-bottom: 10px;\n}\n\n.help-content {\n  h3 {\n    font-size: 110%;\n    margin: 35px 0 10px;\n  }\n  p {\n    margin-top: 10px;\n  }\n  .info-list {\n    margin-top: 10px;\n    margin-left: 20px;\n    margin-bottom: 10px;\n  }\n  .email-me {\n  \tmargin-top: 24px;\n    margin-bottom: 40px;\n  }\n  .email-me a {\n    color: black;\n  }\n}\n"
  },
  {
    "path": "web/src/styles/index.less",
    "content": "@import \"./src/styles/variables.less\";\n\n.intro {\n  padding-top: 25px;\n  margin-bottom: 25px;\n  font-size: 115%;\n}\n.steps {\n  margin-bottom: 28px;\n\n  h2 {\n    font-size: 25px;\n    margin-bottom: 10px;\n  }\n  ul {\n    list-style-type: none;\n  }\n  li {\n    margin: 13px;\n    position: relative;\n    font-size: 115%;\n  }\n  .bullet {\n    margin-right: 15px;\n    color: white;\n    font-weight: bold;\n    width: 12px;\n    text-align: center;\n    display: inline-block;\n  }\n  .bullet:before {\n    content: \"\";\n    z-index: -1;\n    background-color: #84ff00;\n    width: 30px;\n    height: 30px;\n    border-radius: 15px;\n    display: inline-block;\n    position: absolute;\n    top: 0px;\n    left: -9px;\n  }\n}\n\n.address-input-row {\n  background-color: #f0f0f0;\n  padding: 10px;\n  border-radius: 4px;\n  box-shadow: 0 0 30px rgba(0, 0, 0, .3);\n  margin-bottom: 21px;\n\n  .col1 {\n    width: 140px;\n  }\n  label {\n    font-size: 105%;\n  }\n  form {\n    display: flex;\n  }\n  form .address-input-container {\n    flex-grow: 18;\n    flex-basis: 0;\n  }\n  form #address-input {\n    width: 100%;\n    background-color: #ffffff;\n    border: 1px solid #ccc;\n  }\n}\n.address-search-submit-container {\n  flex-grow: 10;\n  flex-basis: 0;\n}\n#address-search-submit {\n  width: 100%;\n  border: 0;\n  padding: 0 10px;\n  height: @form-field-height;\n  font-size: 100%;\n  font-family: 'Quattrocento Sans';\n  background-color: @orange;\n  height: 26px;\n  cursor: pointer;\n}\n#address-search-submit:hover {\n  transition-duration: 0.2s;\n  background-color: lighten(saturate(@orange, 100%), 5%);\n}\n\n#no-search-results {\n  margin-top: 5px;\n}\n\n.help-has-more-info {\n  margin: 27px 0 40px 0;\n  text-align: center;\n}\n\n@media only screen and (max-width: 450px) {\n  .intro {\n    font-size: 108%;\n  }\n  .steps li {\n    margin: 5px;\n    position: relative;\n    font-size: 108%;\n  }\n  .steps .bullet:before {\n    width: 26px;\n    height: 26px;\n    border-radius: 13px;\n    top: 1px;\n    left: -8px;\n  }\n}\n@media only screen and (max-width: 400px) {\n  .banner {\n    padding-top: 85%;\n  }\n  .intro {\n    font-size: 108%;\n  }\n}\n"
  },
  {
    "path": "web/src/styles/map.less",
    "content": "@import \"./src/styles/variables.less\";\n\n.printing-method-container:first-child {\n  margin-right: 17px;\n}\n.map-address {\n  font-size: inherit;\n}\n.map-content-row {\n  padding: 0;\n  border-radius: 4px;\n  position: relative;\n}\n.map-content-row h3 {\n  font-size: 23px;\n  margin-top: 1em;\n  padding: 0;\n  font-weight: normal;\n}\n.map-description {\n  padding-left: 1em;\n}\n\n.map-content-buildings {\n  margin-left: 1em;\n}\n.map-content-roads,\n.map-content-paths,\n.map-content-railways,\n.map-content-waterways,\n.map-content-water-areas,\n.map-content-other-linear,\n.map-content-poi-familiar,\n.map-content-poi-daily,\n.map-content-poi-transport,\n.map-content-poi-accessibility {\n  margin-left: 1em;\n}\n.map-content h4 {\n  font-size: 1.12em;\n  font-weight: bold;\n}\n.map-content .map-content-section-height {\n  margin-bottom: .5em;\n}\n.map-content .row {\n  margin-top: 7px;\n}\n.map-content ul {\n  display: inline-block;\n}\n.map-content .map-content-summary {\n  display: block;\n  margin: 0 0 0.5em 1em;\n  padding: 0;\n}\n.map-content .map-content-summary-item {\n  list-style: none;\n  margin-bottom: 8px;\n}\n.map-content .map-content-summary-item:last-child {\n  margin-bottom: 0;\n}\n.map-content .map-content-summary-toggle {\n  margin: 0 0 1em 1em;\n  border: 1px solid #406271;\n  border-radius: 6px;\n  color: #1f2f36;\n  background-color: #fff;\n  padding: 4px 12px;\n  cursor: pointer;\n}\n.map-content .map-content-summary-toggle:hover {\n  background-color: #f4e9b2;\n}\n.map-content .map-content-summary-toggle:focus-visible {\n  outline: 3px solid #005fcc;\n  outline-offset: 2px;\n}\n.map-content .map-content-full[hidden] {\n  display: none;\n}\n.map-content li {\n  list-style: none;\n  margin-bottom: 10px;\n}\n.map-content li:last-child {\n  margin-bottom: 0;\n}\n.map-content .map-content-title-line {\n  font-weight: bold;\n}\n.map-content h5.map-content-title-line {\n  margin: 0;\n  font-size: inherit;\n}\n.map-content .map-content-title {\n  font-weight: inherit;\n}\n.map-content .map-content-inline-external-link {\n  color: #1f4e79;\n  text-decoration: none;\n  font-weight: normal;\n}\n.map-content .map-content-external-indicator {\n  margin-left: 0.25em;\n  font-weight: normal;\n}\n.map-content .map-content-inline-external-link:hover,\n.map-content .map-content-inline-external-link:focus {\n  color: #12314f;\n  text-decoration: none;\n}\n.map-content .map-content-has-importance-popup {\n  position: relative;\n}\n.map-content .map-content-importance-popup {\n  display: none;\n  position: absolute;\n  left: 0;\n  top: calc(100% + 4px);\n  z-index: 20;\n  width: 640px;\n  max-width: 92vw;\n  background: #ffffff;\n  border: 1px solid #9aa4af;\n  border-radius: 4px;\n  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);\n  padding: 6px 8px;\n}\n.map-content .map-content-importance-popup pre {\n  margin: 0;\n  font-family: Consolas, \"Courier New\", monospace;\n  font-size: 12px;\n  line-height: 1.3;\n  white-space: pre-wrap;\n}\n.map-content .map-content-has-importance-popup:hover .map-content-importance-popup {\n  display: block;\n}\n.map-content-section-toggle {\n  margin: 0 0 1.5em 1em;\n  border: 1px solid #406271;\n  border-radius: 6px;\n  color: #1f2f36;\n  cursor: pointer;\n  display: block;\n}\n.map-content-section-toggle:hover {\n  background-color: #f4e9b2;\n}\n.map-content-section-toggle:focus-visible {\n  outline: 3px solid #005fcc;\n  outline-offset: 2px;\n}\n.map-content.initial-state .initially-hidden {\n  display: none;\n}\n.map-content-row button.show-more {\n  margin: 3px 0 3px 0;\n  padding: 2px 60px;\n}\n.map-content-row button.show-more:hover {\n  background-color: #ffffcc;\n  transition-duration: 0.2s;\n  cursor: pointer;\n}\n\n.ready-map {\n  margin-top: 20px;\n  border-radius: 10px;\n  padding: 10px 20px !important;\n  background-color: #e7edf3;\n  border: 1px solid #c7d1dc;\n  box-shadow: inset 0 0 11px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.12);\n}\n.warning-no-buildings {\n  display: block;\n  margin: 10px 0;\n}\n.xdownload-link#order-map {\n  padding: 4px 10px;\n}\n.xdownload-link .order-download-link {\n  position: relative;\n  top: -4px;\n}\n.download-link img.order-pp-logo {\n  height: 22px;\n  position: relative;\n  top: 6px;\n  left: 1px;\n}\n.email-sending-row {\n  cursor: initial;\n  padding-top: 5px;\n}\n.email-sending {\n  line-height: 1.5em;\n  display: block;\n}\n.email-sending .inputs {\n  display: block;\n}\n.email-sending #email-addr {\n  border: 0;\n  width: 260px;\n  padding-left: 6px;\n}\n.email-sending #email-addr.invalid {\n  background-color: #ff6677;\n}\n.email-sending-result {\n  text-align: center;\n  margin: 14px 0;\n}\n.email-sending-success .success-msg-text {\n  background-color: #f0f0f0;\n  margin-top: 0 !important;\n  padding: 6px 50px;\n  border-radius: 4px;\n  box-shadow: 0 0 12px rgba(0, 0, 0, 0.3);\n}\n.email-sending .submit-email {\n  padding-left: 8px;\n  padding-right: 8px;\n  background-color: white;\n  border: 0;\n  box-shadow: 0 0 15px rgba(0, 0, 0, .25);\n  cursor: pointer;\n}\n.osm-attribution {\n  margin: 16px 15px 0;\n  text-align: center;\n  line-height: 1.4;\n}\n.osm-attribution a {\n  color: inherit;\n}\n.other-downloads {\n  margin-top: 17px;\n}\n.other-downloads-details {\n  width: 100%;\n}\n.other-downloads-summary {\n  text-align: center;\n  border: 0;\n  border-radius: 4px;\n  line-height: normal;\n  color: black;\n  display: block;\n  cursor: pointer;\n  height: auto;\n  padding: 7px 10px;\n  box-shadow: 0px 3px 9px 0px rgba(0, 0, 0, 0.3);\n  background-color: @green;\n  transition-duration: 0.2s;\n  user-select: none;\n}\n.other-downloads-summary:hover {\n  background-color: darken(@green, 5%);\n}\n.other-downloads-summary:focus-visible {\n  outline: 3px solid #005fcc;\n  outline-offset: 2px;\n}\n.other-downloads-summary::marker {\n  content: \"\";\n}\n.other-downloads-summary::-webkit-details-marker {\n  display: none;\n}\n.other-downloads-summary::after {\n  content: \"\\25BE\";\n  margin-left: 0.45em;\n  display: inline-block;\n  transition: transform 0.2s ease;\n  position: absolute;\n}\n.other-downloads-details[open] .other-downloads-summary::after {\n  transform: rotate(180deg);\n}\n.other-downloads-details[open] .other-downloads-summary {\n  border-bottom-left-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.other-downloads-list {\n  list-style: none;\n  margin: 0;\n  padding: 0.75em 0.9em 0.8em;\n  background-color: #f6f9fc;\n  border: 1px solid #c7d1dc;\n  border-top: 0;\n  border-bottom-left-radius: 4px;\n  border-bottom-right-radius: 4px;\n}\n.other-downloads-list li {\n  margin: 4px 0;\n}\n.other-downloads-link {\n  color: #0a3f67;\n  text-decoration: none;\n}\n.other-downloads-link:hover {\n  text-decoration: underline;\n}\n.other-downloads-link:focus-visible {\n  outline: 2px solid #005fcc;\n  outline-offset: 2px;\n  text-decoration: underline;\n}\n.downloads-2d {\n  cursor: default;\n}\n.ordering-2d-later {\n  margin: 20px 0;\n  text-align: center;\n}\n.no-data-available-msg {\n  margin: 20px;\n}\n.no-data-available-msg span {\n  font-size: 115%;\n}\n.ready-map canvas {\n  border-radius: 4px;\n}\n.ready-map p {\n  text-align: center;\n}\n.loading-3d-preview {\n  text-align: center;\n  margin-top: 50px;\n  font-size: 200%;\n  font-weight: bold;\n  color: #e8e8e8;\n  text-shadow: #000 0px 0px 1px;\n  font-smoothing: antialiased;\n  -webkit-font-smoothing: antialiased;\n}\n.preview-3d-container {\n  opacity: 0;\n  transition: 1s;\n}\n.preview-3d-container .dummy,\n.preview-3d {\n  background: transparent;\n}\n.preview-3d-container.square-block-container:after {\n  margin-top: 69%;\n}\n.preview-3d-hint {\n  margin: 12px 0 2px;\n  text-align: center;\n  color: #000000;\n}\n\n.preview-2d-container {\n  margin-top: 28px;\n}\n\n#svg-preview {\n  width: 100%;\n  border: 1px solid #888;\n  box-shadow: 0px 3px 9px 0px rgba(0, 0, 0, 0.3);\n}\n\n.back-to-previous-page-container {\n  margin-top: 27px;\n}\n\n@media only screen and (max-width: 450px) {\n  .email-sending #email-addr {\n    width: 100%;\n  }\n  .email-sending .submit-email {\n    margin-top: 6px;\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "web/src/styles/reset.less",
    "content": "/* Global Reset of all HTML Elements */\nhtml,\nbody {\n  border: 0;\n  line-height: 1.5;\n  margin: 0;\n  padding: 0;\n}\ndiv,\nspan,\nobject,\niframe,\nimg,\ntable,\ncaption,\nthead,\ntbody,\ntfoot,\ntr,\ntr,\ntd,\narticle,\naside,\ncanvas,\ndetails,\nfigure,\nhgroup,\nmenu,\nnav,\nfooter,\nheader,\nsection,\nsummary,\nmark,\naudio,\nvideo {\n  border: 0;\n  margin: 0;\n  padding: 0;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\np,\nblockquote,\npre,\na,\nabbr,\naddress,\ncit,\ncode,\ndel,\ndfn,\nem,\nins,\nq,\nsamp,\nsmall,\nstrong,\nsub,\nsup,\nb,\ni,\nhr,\ndl,\ndt,\ndd,\nol,\nul,\nli,\nfieldset,\nlegend,\nlabel {\n  border: 0;\n  font-size: 100%;\n  vertical-align: baseline;\n  margin: 0;\n  padding: 0;\n}\narticle,\naside,\ncanvas,\nfigure,\nfigure img,\nfigcaption,\nhgroup,\nfooter,\nheader,\nnav,\nsection,\naudio,\nvideo {\n  display: block;\n}\ntable {\n  border-collapse: separate;\n  border-spacing: 0;\n  caption,\n  th,\n  td {\n    text-align: left;\n    vertical-align: middle;\n  }\n}\na img {\n  border: 0;\n}\n :focus {\n  outline: 0;\n}\nhtml {\n  box-sizing: border-box;\n}\n*,\n*:before,\n*:after {\n  box-sizing: inherit;\n}\n"
  },
  {
    "path": "web/src/styles/top.less",
    "content": ".banner {\n  background-color: #098400;\n  position: relative;\n  z-index: 2;\n}\n.page-index .banner {\n  background-image: url(/images/kartta-kasissa.jpg);\n  background-size: 100%;\n  background-position: 50% 100%;\n  height: 100vh;\n\n  @media only screen and (max-aspect-ratio: 110 / 100) {\n    height: 85vw;\n  }\n  @media only screen and (min-width: (@total-width + 2 * @main-wrapper-padding)) {\n    height: 517px;\n  }\n}\n\n.page-index .banner-texts {\n  position: absolute;\n  top: 0px;\n  width: 100%;\n  background-color: rgba(0, 0, 0, 0.3);\n}\n.banner-texts h1 {\n  font-size: 42px;\n  text-align: center;\n  padding: 7px 0 0 0;\n  text-align: center;\n  letter-spacing: 2px;\n  line-height: 1.0em;\n  color: @green;\n}\n.page-index .banner-texts h1 {\n  text-shadow: 0 0 25px rgba(0, 0, 0, 1);\n}\n.banner-texts p {\n  text-align: center;\n  font-size: 17px;\n  font-weight: bold;\n  color: white;\n  padding: 16px 10px 15px 10px;\n  line-height: 110%;\n  @media only screen and (max-width: 370px) {\n    font-size: 14px;\n  }\n}\n.banner .logo {\n  height: 65px;\n  position: relative;\n  display: block;\n  margin: 0 auto;\n  padding-right: 8px;\n  max-width: 100%;\n}\n.page-index .banner .logo-bg {\n  position: absolute;\n  left: 50%;\n  width: 100px;\n  margin-left: -38px;\n  top: 51px;\n  height: 16px;\n  background-color: rgba(0, 0, 0, 0.8);\n  box-shadow: 0 0 60px rgba(0, 0, 0, 1);\n}\n.page-index .banner-texts p {\n  text-shadow: 0 0 10px rgba(0, 0, 0, 1);\n}\n\n/*\n * Top right corner\n */\n.top-corner {\n  position: absolute;\n  right: 10px;\n  top: 5px;\n  color: white;\n}\n.language-selector {\n  color: white;\n  background-color: rgba(0, 0, 0, 0.25);\n  border: 1px solid rgba(255, 255, 255, .3);\n  height: auto;\n  padding: 3px 6px 1px 8px;\n  border-radius: 2px;\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  cursor: pointer;\n}\n.language-selector:hover,\n.language-selector:active {\n  background-color: white;\n  color: black;\n  -webkit-appearance: initial;\n  -moz-appearance: initial;\n  appearance: initial;\n}\n.language-selector option {\n  background-color: white;\n  color: black;\n}\n.help-link {\n  margin-left: 10px;\n  color: white;\n}\n@media only screen and (max-width: 450px) {\n  .top-corner {\n    right: 0px;\n  }\n  .language-selector {\n    margin-right: 5px;\n  }\n  .help-link {\n    display: block;\n    text-align: right;\n    padding-right: 10px;\n  }\n  .top-corner * {\n    font-size: 14px;\n  }\n}\n@media only screen and (max-width: 400px),\n       only screen and (max-height: 400px) {\n  .banner-texts h1 {\n    font-size: 32px;\n  }\n  .banner .logo {\n    height: 52px;\n    padding-right: 6px;\n  }\n  .page-index .banner .logo-bg {\n    width: 75px;\n    margin-left: -24px;\n    top: 41px;\n    height: 15px;\n  }\n  .banner-texts p {\n    xmargin-top: 11px;\n    xmargin-bottom: 12px;\n  }\n}\n"
  },
  {
    "path": "web/src/styles/variables.less",
    "content": "@total-width: 700px;\n@main-wrapper-padding: 30px;\n@col1-width: 170px;\n@col2-width: @total-width - @col1-width;\n@coord-input-width: 75px;\n@outside-margin: 30px;\n@outside-margin-semi: 20px;\n@outside-margin-narrow: 15px;\n@form-field-height: 26px;\n@input-color: #f8f8f8;\n@green: hsl(89, 100%, 50%);\n@orange: hsl(34, 100%, 60%);\n"
  },
  {
    "path": "web/web.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"WEB_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/dist\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/node_modules\" />\n    </content>\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n    <orderEntry type=\"library\" name=\"jquery-2.1.4.min\" level=\"project\" />\n  </component>\n</module>"
  }
]