leaflet-city-mapper 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +78 -2
- package/cucumber.mjs +9 -0
- package/dist/leaflet-city-mapper.js +2 -0
- package/dist/leaflet-city-mapper.js.map +7 -0
- package/dist/utils/CELL.js +2 -0
- package/dist/utils/CELL.js.map +7 -0
- package/dist/utils/addBridges.js +2 -0
- package/dist/utils/addBridges.js.map +7 -0
- package/dist/utils/addRasterTile.js +2 -0
- package/dist/utils/addRasterTile.js.map +7 -0
- package/dist/utils/along.js +2 -0
- package/dist/utils/along.js.map +7 -0
- package/dist/utils/atmosphere.js +2 -0
- package/dist/utils/atmosphere.js.map +7 -0
- package/dist/utils/attach.js +2 -0
- package/dist/utils/attach.js.map +7 -0
- package/dist/utils/bindSunlight.js +2 -0
- package/dist/utils/bindSunlight.js.map +7 -0
- package/dist/utils/box.js +2 -0
- package/dist/utils/box.js.map +7 -0
- package/dist/utils/bridgeElevation.js +2 -0
- package/dist/utils/bridgeElevation.js.map +7 -0
- package/dist/utils/build.js +2 -0
- package/dist/utils/build.js.map +7 -0
- package/dist/utils/cameraControls.js +2 -0
- package/dist/utils/cameraControls.js.map +7 -0
- package/dist/utils/channel.js +2 -0
- package/dist/utils/channel.js.map +7 -0
- package/dist/utils/collider.js +2 -0
- package/dist/utils/collider.js.map +7 -0
- package/dist/utils/colliders.js +2 -0
- package/dist/utils/colliders.js.map +7 -0
- package/dist/utils/computeBounds.js +2 -0
- package/dist/utils/computeBounds.js.map +7 -0
- package/dist/utils/conformModelToTerrain.js +2 -0
- package/dist/utils/conformModelToTerrain.js.map +7 -0
- package/dist/utils/createMesh.js +2 -0
- package/dist/utils/createMesh.js.map +7 -0
- package/dist/utils/createProgram.js +3 -0
- package/dist/utils/createProgram.js.map +7 -0
- package/dist/utils/decodeDirectory.js +2 -0
- package/dist/utils/decodeDirectory.js.map +7 -0
- package/dist/utils/decodeHeader.js +2 -0
- package/dist/utils/decodeHeader.js.map +7 -0
- package/dist/utils/decodeModel.js +2 -0
- package/dist/utils/decodeModel.js.map +7 -0
- package/dist/utils/decompress.js +2 -0
- package/dist/utils/decompress.js.map +7 -0
- package/dist/utils/destroyTexture.js +2 -0
- package/dist/utils/destroyTexture.js.map +7 -0
- package/dist/utils/disposeMesh.js +2 -0
- package/dist/utils/disposeMesh.js.map +7 -0
- package/dist/utils/drawGeometry.js +2 -0
- package/dist/utils/drawGeometry.js.map +7 -0
- package/dist/utils/elevationWGSL.js +24 -0
- package/dist/utils/elevationWGSL.js.map +7 -0
- package/dist/utils/emptyNode.js +2 -0
- package/dist/utils/emptyNode.js.map +7 -0
- package/dist/utils/endZoom.js +2 -0
- package/dist/utils/endZoom.js.map +7 -0
- package/dist/utils/findFeature.js +2 -0
- package/dist/utils/findFeature.js.map +7 -0
- package/dist/utils/geometry.js +1 -0
- package/dist/utils/geometry.js.map +7 -0
- package/dist/utils/getElevation.js +2 -0
- package/dist/utils/getElevation.js.map +7 -0
- package/dist/utils/getHeader.js +2 -0
- package/dist/utils/getHeader.js.map +7 -0
- package/dist/utils/getZxy.js +2 -0
- package/dist/utils/getZxy.js.map +7 -0
- package/dist/utils/glass.js +2 -0
- package/dist/utils/glass.js.map +7 -0
- package/dist/utils/headlight.js +2 -0
- package/dist/utils/headlight.js.map +7 -0
- package/dist/utils/hit.js +2 -0
- package/dist/utils/hit.js.map +7 -0
- package/dist/utils/horizontalPosition.js +2 -0
- package/dist/utils/horizontalPosition.js.map +7 -0
- package/dist/utils/horizontalRay.js +2 -0
- package/dist/utils/horizontalRay.js.map +7 -0
- package/dist/utils/hull.js +2 -0
- package/dist/utils/hull.js.map +7 -0
- package/dist/utils/initSunlight.js +2 -0
- package/dist/utils/initSunlight.js.map +7 -0
- package/dist/utils/initTerrain.js +2 -0
- package/dist/utils/initTerrain.js.map +7 -0
- package/dist/utils/initTraffic.js +40 -0
- package/dist/utils/initTraffic.js.map +7 -0
- package/dist/utils/initWalk.js +2 -0
- package/dist/utils/initWalk.js.map +7 -0
- package/dist/utils/intersectMeshes.js +2 -0
- package/dist/utils/intersectMeshes.js.map +7 -0
- package/dist/utils/isClockWise.js +2 -0
- package/dist/utils/isClockWise.js.map +7 -0
- package/dist/utils/lampPostGeometry.js +2 -0
- package/dist/utils/lampPostGeometry.js.map +7 -0
- package/dist/utils/lightGeometry.js +2 -0
- package/dist/utils/lightGeometry.js.map +7 -0
- package/dist/utils/lightGrid.js +2 -0
- package/dist/utils/lightGrid.js.map +7 -0
- package/dist/utils/limbs.js +2 -0
- package/dist/utils/limbs.js.map +7 -0
- package/dist/utils/loadModel.js +2 -0
- package/dist/utils/loadModel.js.map +7 -0
- package/dist/utils/loadSource.js +2 -0
- package/dist/utils/loadSource.js.map +7 -0
- package/dist/utils/loadTerrain.js +2 -0
- package/dist/utils/loadTerrain.js.map +7 -0
- package/dist/utils/mapCamera.js +2 -0
- package/dist/utils/mapCamera.js.map +7 -0
- package/dist/utils/materialShaders.js +211 -0
- package/dist/utils/materialShaders.js.map +7 -0
- package/dist/utils/mirror.js +2 -0
- package/dist/utils/mirror.js.map +7 -0
- package/dist/utils/moonPosition.js +2 -0
- package/dist/utils/moonPosition.js.map +7 -0
- package/dist/utils/moves.js +2 -0
- package/dist/utils/moves.js.map +7 -0
- package/dist/utils/nativeEvents.js +2 -0
- package/dist/utils/nativeEvents.js.map +7 -0
- package/dist/utils/nativeGeometry.js +2 -0
- package/dist/utils/nativeGeometry.js.map +7 -0
- package/dist/utils/nativeLayer.js +2 -0
- package/dist/utils/nativeLayer.js.map +7 -0
- package/dist/utils/nodeMatrix.js +2 -0
- package/dist/utils/nodeMatrix.js.map +7 -0
- package/dist/utils/onAdd3DLayer.js +2 -0
- package/dist/utils/onAdd3DLayer.js.map +7 -0
- package/dist/utils/onAddSun.js +2 -0
- package/dist/utils/onAddSun.js.map +7 -0
- package/dist/utils/onClick.js +2 -0
- package/dist/utils/onClick.js.map +7 -0
- package/dist/utils/onMapChange.js +2 -0
- package/dist/utils/onMapChange.js.map +7 -0
- package/dist/utils/onPMTileAdd.js +2 -0
- package/dist/utils/onPMTileAdd.js.map +7 -0
- package/dist/utils/onPMTileRemove.js +2 -0
- package/dist/utils/onPMTileRemove.js.map +7 -0
- package/dist/utils/onPointerLeave.js +2 -0
- package/dist/utils/onPointerLeave.js.map +7 -0
- package/dist/utils/onPointerMove.js +2 -0
- package/dist/utils/onPointerMove.js.map +7 -0
- package/dist/utils/onRemove3DLayer.js +2 -0
- package/dist/utils/onRemove3DLayer.js.map +7 -0
- package/dist/utils/onRemoveSun.js +2 -0
- package/dist/utils/onRemoveSun.js.map +7 -0
- package/dist/utils/onTerrainAdd.js +2 -0
- package/dist/utils/onTerrainAdd.js.map +7 -0
- package/dist/utils/onTerrainChange.js +2 -0
- package/dist/utils/onTerrainChange.js.map +7 -0
- package/dist/utils/onTerrainRemove.js +2 -0
- package/dist/utils/onTerrainRemove.js.map +7 -0
- package/dist/utils/onZoomAnim.js +2 -0
- package/dist/utils/onZoomAnim.js.map +7 -0
- package/dist/utils/paints.js +2 -0
- package/dist/utils/paints.js.map +7 -0
- package/dist/utils/parseColor.js +2 -0
- package/dist/utils/parseColor.js.map +7 -0
- package/dist/utils/parts.js +2 -0
- package/dist/utils/parts.js.map +7 -0
- package/dist/utils/projectCelestialBody.js +2 -0
- package/dist/utils/projectCelestialBody.js.map +7 -0
- package/dist/utils/pump.js +2 -0
- package/dist/utils/pump.js.map +7 -0
- package/dist/utils/rasterTiles.js +2 -0
- package/dist/utils/rasterTiles.js.map +7 -0
- package/dist/utils/raycast.js +2 -0
- package/dist/utils/raycast.js.map +7 -0
- package/dist/utils/read.js +2 -0
- package/dist/utils/read.js.map +7 -0
- package/dist/utils/readAccessor.js +2 -0
- package/dist/utils/readAccessor.js.map +7 -0
- package/dist/utils/releaseArea.js +2 -0
- package/dist/utils/releaseArea.js.map +7 -0
- package/dist/utils/removeRasterTile.js +2 -0
- package/dist/utils/removeRasterTile.js.map +7 -0
- package/dist/utils/render.js +15 -0
- package/dist/utils/render.js.map +7 -0
- package/dist/utils/renderSunlight.js +13 -0
- package/dist/utils/renderSunlight.js.map +7 -0
- package/dist/utils/renderers.js +2 -0
- package/dist/utils/renderers.js.map +7 -0
- package/dist/utils/resizeSunlight.js +2 -0
- package/dist/utils/resizeSunlight.js.map +7 -0
- package/dist/utils/roofCoverage.js +2 -0
- package/dist/utils/roofCoverage.js.map +7 -0
- package/dist/utils/routeGeometry.js +2 -0
- package/dist/utils/routeGeometry.js.map +7 -0
- package/dist/utils/scheduledTrips.js +2 -0
- package/dist/utils/scheduledTrips.js.map +7 -0
- package/dist/utils/setGeoJSONData.js +2 -0
- package/dist/utils/setGeoJSONData.js.map +7 -0
- package/dist/utils/setHighlight.js +2 -0
- package/dist/utils/setHighlight.js.map +7 -0
- package/dist/utils/setPMTileOpacity.js +2 -0
- package/dist/utils/setPMTileOpacity.js.map +7 -0
- package/dist/utils/setPMTileZIndex.js +2 -0
- package/dist/utils/setPMTileZIndex.js.map +7 -0
- package/dist/utils/setParent.js +2 -0
- package/dist/utils/setParent.js.map +7 -0
- package/dist/utils/setTerrainHeight.js +2 -0
- package/dist/utils/setTerrainHeight.js.map +7 -0
- package/dist/utils/setVertexColor.js +2 -0
- package/dist/utils/setVertexColor.js.map +7 -0
- package/dist/utils/showFeature.js +2 -0
- package/dist/utils/showFeature.js.map +7 -0
- package/dist/utils/sizes.js +2 -0
- package/dist/utils/sizes.js.map +7 -0
- package/dist/utils/skin.js +2 -0
- package/dist/utils/skin.js.map +7 -0
- package/dist/utils/solarPosition.js +2 -0
- package/dist/utils/solarPosition.js.map +7 -0
- package/dist/utils/sortMeshes.js +2 -0
- package/dist/utils/sortMeshes.js.map +7 -0
- package/dist/utils/speed.js +2 -0
- package/dist/utils/speed.js.map +7 -0
- package/dist/utils/startZoom.js +2 -0
- package/dist/utils/startZoom.js.map +7 -0
- package/dist/utils/taillight.js +2 -0
- package/dist/utils/taillight.js.map +7 -0
- package/dist/utils/terrainAtlas.js +2 -0
- package/dist/utils/terrainAtlas.js.map +7 -0
- package/dist/utils/terrainElevation.js +2 -0
- package/dist/utils/terrainElevation.js.map +7 -0
- package/dist/utils/terrainGrid.js +2 -0
- package/dist/utils/terrainGrid.js.map +7 -0
- package/dist/utils/textureSampler.js +2 -0
- package/dist/utils/textureSampler.js.map +7 -0
- package/dist/utils/tileId.js +2 -0
- package/dist/utils/tileId.js.map +7 -0
- package/dist/utils/toLanes.js +2 -0
- package/dist/utils/toLanes.js.map +7 -0
- package/dist/utils/toSRGB.js +2 -0
- package/dist/utils/toSRGB.js.map +7 -0
- package/dist/utils/tooltip.js +3 -0
- package/dist/utils/tooltip.js.map +7 -0
- package/dist/utils/traffic.js +2 -0
- package/dist/utils/traffic.js.map +7 -0
- package/dist/utils/traverse.js +2 -0
- package/dist/utils/traverse.js.map +7 -0
- package/dist/utils/treeGeometry.js +2 -0
- package/dist/utils/treeGeometry.js.map +7 -0
- package/dist/utils/triangulate.js +2 -0
- package/dist/utils/triangulate.js.map +7 -0
- package/dist/utils/tripsAt.js +2 -0
- package/dist/utils/tripsAt.js.map +7 -0
- package/dist/utils/types.js +2 -0
- package/dist/utils/types.js.map +7 -0
- package/dist/utils/underEntry.js +2 -0
- package/dist/utils/underEntry.js.map +7 -0
- package/dist/utils/update.js +2 -0
- package/dist/utils/update.js.map +7 -0
- package/dist/utils/updateArea.js +2 -0
- package/dist/utils/updateArea.js.map +7 -0
- package/dist/utils/updateCamera.js +2 -0
- package/dist/utils/updateCamera.js.map +7 -0
- package/dist/utils/updatePointLights.js +2 -0
- package/dist/utils/updatePointLights.js.map +7 -0
- package/dist/utils/updateWorldMatrix.js +2 -0
- package/dist/utils/updateWorldMatrix.js.map +7 -0
- package/dist/utils/vehicleOffsets.js +2 -0
- package/dist/utils/vehicleOffsets.js.map +7 -0
- package/dist/utils/vehicles.js +2 -0
- package/dist/utils/vehicles.js.map +7 -0
- package/dist/utils/vertexNormals.js +2 -0
- package/dist/utils/vertexNormals.js.map +7 -0
- package/dist/utils/walk.js +2 -0
- package/dist/utils/walk.js.map +7 -0
- package/dist/utils/white.js +2 -0
- package/dist/utils/white.js.map +7 -0
- package/dist/utils/worldLights.js +2 -0
- package/dist/utils/worldLights.js.map +7 -0
- package/dist/utils/wrap.js +2 -0
- package/dist/utils/wrap.js.map +7 -0
- package/package.json +43 -4
- package/src/index.js +397 -0
- package/src/utils/CELL.js +4 -0
- package/src/utils/addBridges.js +66 -0
- package/src/utils/addRasterTile.js +112 -0
- package/src/utils/along.js +9 -0
- package/src/utils/atmosphere.js +20 -0
- package/src/utils/attach.js +255 -0
- package/src/utils/bindSunlight.js +12 -0
- package/src/utils/box.js +1 -0
- package/src/utils/bridgeElevation.js +18 -0
- package/src/utils/build.js +20 -0
- package/src/utils/cameraControls.js +287 -0
- package/src/utils/channel.js +4 -0
- package/src/utils/collider.js +62 -0
- package/src/utils/colliders.js +2 -0
- package/src/utils/computeBounds.js +17 -0
- package/src/utils/conformModelToTerrain.js +69 -0
- package/src/utils/createMesh.js +31 -0
- package/src/utils/createProgram.js +92 -0
- package/src/utils/decodeDirectory.js +40 -0
- package/src/utils/decodeHeader.js +15 -0
- package/src/utils/decodeModel.js +43 -0
- package/src/utils/decompress.js +5 -0
- package/src/utils/destroyTexture.js +7 -0
- package/src/utils/disposeMesh.js +13 -0
- package/src/utils/drawGeometry.js +122 -0
- package/src/utils/elevationWGSL.js +27 -0
- package/src/utils/emptyNode.js +9 -0
- package/src/utils/endZoom.js +15 -0
- package/src/utils/findFeature.js +49 -0
- package/src/utils/geometry.js +2 -0
- package/src/utils/getElevation.js +10 -0
- package/src/utils/getHeader.js +9 -0
- package/src/utils/getZxy.js +76 -0
- package/src/utils/glass.js +1 -0
- package/src/utils/headlight.js +1 -0
- package/src/utils/hit.js +41 -0
- package/src/utils/horizontalPosition.js +16 -0
- package/src/utils/horizontalRay.js +12 -0
- package/src/utils/hull.js +1 -0
- package/src/utils/initSunlight.js +9 -0
- package/src/utils/initTerrain.js +19 -0
- package/src/utils/initTraffic.js +93 -0
- package/src/utils/initWalk.js +72 -0
- package/src/utils/intersectMeshes.js +121 -0
- package/src/utils/isClockWise.js +12 -0
- package/src/utils/lampPostGeometry.js +26 -0
- package/src/utils/lightGeometry.js +22 -0
- package/src/utils/lightGrid.js +50 -0
- package/src/utils/limbs.js +9 -0
- package/src/utils/loadModel.js +217 -0
- package/src/utils/loadSource.js +17 -0
- package/src/utils/loadTerrain.js +65 -0
- package/src/utils/mapCamera.js +432 -0
- package/src/utils/materialShaders.js +243 -0
- package/src/utils/mirror.js +6 -0
- package/src/utils/moonPosition.js +17 -0
- package/src/utils/moves.js +11 -0
- package/src/utils/nativeEvents.js +77 -0
- package/src/utils/nativeGeometry.js +131 -0
- package/src/utils/nativeLayer.js +317 -0
- package/src/utils/nodeMatrix.js +22 -0
- package/src/utils/onAdd3DLayer.js +31 -0
- package/src/utils/onAddSun.js +147 -0
- package/src/utils/onClick.js +13 -0
- package/src/utils/onMapChange.js +16 -0
- package/src/utils/onPMTileAdd.js +15 -0
- package/src/utils/onPMTileRemove.js +8 -0
- package/src/utils/onPointerLeave.js +11 -0
- package/src/utils/onPointerMove.js +15 -0
- package/src/utils/onRemove3DLayer.js +78 -0
- package/src/utils/onRemoveSun.js +3 -0
- package/src/utils/onTerrainAdd.js +18 -0
- package/src/utils/onTerrainChange.js +13 -0
- package/src/utils/onTerrainRemove.js +40 -0
- package/src/utils/onZoomAnim.js +14 -0
- package/src/utils/paints.js +10 -0
- package/src/utils/parseColor.js +42 -0
- package/src/utils/parts.js +16 -0
- package/src/utils/projectCelestialBody.js +13 -0
- package/src/utils/pump.js +226 -0
- package/src/utils/rasterTiles.js +285 -0
- package/src/utils/raycast.js +60 -0
- package/src/utils/read.js +29 -0
- package/src/utils/readAccessor.js +68 -0
- package/src/utils/releaseArea.js +34 -0
- package/src/utils/removeRasterTile.js +13 -0
- package/src/utils/render.js +406 -0
- package/src/utils/renderSunlight.js +170 -0
- package/src/utils/renderers.js +2 -0
- package/src/utils/resizeSunlight.js +15 -0
- package/src/utils/roofCoverage.js +47 -0
- package/src/utils/routeGeometry.js +22 -0
- package/src/utils/scheduledTrips.js +29 -0
- package/src/utils/setGeoJSONData.js +11 -0
- package/src/utils/setHighlight.js +13 -0
- package/src/utils/setPMTileOpacity.js +7 -0
- package/src/utils/setPMTileZIndex.js +7 -0
- package/src/utils/setParent.js +7 -0
- package/src/utils/setTerrainHeight.js +10 -0
- package/src/utils/setVertexColor.js +14 -0
- package/src/utils/showFeature.js +27 -0
- package/src/utils/sizes.js +1 -0
- package/src/utils/skin.js +2 -0
- package/src/utils/solarPosition.js +17 -0
- package/src/utils/sortMeshes.js +12 -0
- package/src/utils/speed.js +3 -0
- package/src/utils/startZoom.js +5 -0
- package/src/utils/taillight.js +1 -0
- package/src/utils/terrainAtlas.js +21 -0
- package/src/utils/terrainElevation.js +20 -0
- package/src/utils/terrainGrid.js +23 -0
- package/src/utils/textureSampler.js +10 -0
- package/src/utils/tileId.js +18 -0
- package/src/utils/toLanes.js +7 -0
- package/src/utils/toSRGB.js +1 -0
- package/src/utils/tooltip.js +6 -0
- package/src/utils/traffic.js +315 -0
- package/src/utils/traverse.js +6 -0
- package/src/utils/treeGeometry.js +45 -0
- package/src/utils/triangulate.js +100 -0
- package/src/utils/tripsAt.js +16 -0
- package/src/utils/types.js +8 -0
- package/src/utils/underEntry.js +17 -0
- package/src/utils/update.js +18 -0
- package/src/utils/updateArea.js +40 -0
- package/src/utils/updateCamera.js +32 -0
- package/src/utils/updatePointLights.js +38 -0
- package/src/utils/updateWorldMatrix.js +47 -0
- package/src/utils/vehicleOffsets.js +8 -0
- package/src/utils/vehicles.js +65 -0
- package/src/utils/vertexNormals.js +30 -0
- package/src/utils/walk.js +187 -0
- package/src/utils/white.js +1 -0
- package/src/utils/worldLights.js +25 -0
- package/src/utils/wrap.js +1 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import * as createMeshApi from './createMesh.js';
|
|
2
|
+
import * as disposeMesh from './disposeMesh.js';
|
|
3
|
+
import * as setHighlight from './setHighlight.js';
|
|
4
|
+
import * as showFeature from './showFeature.js';
|
|
5
|
+
import { update as updateScene } from './update.js';
|
|
6
|
+
import { along } from './along.js';
|
|
7
|
+
import { build } from './build.js';
|
|
8
|
+
import { computeBounds } from './computeBounds.js';
|
|
9
|
+
import { routeGeometry } from './routeGeometry.js';
|
|
10
|
+
import { toLanes } from './toLanes.js';
|
|
11
|
+
import { tripsAt } from './tripsAt.js';
|
|
12
|
+
import { underEntry } from './underEntry.js';
|
|
13
|
+
import { getElevation } from './getElevation.js';
|
|
14
|
+
|
|
15
|
+
export function trafficNow(state) {
|
|
16
|
+
let control = { live: true };
|
|
17
|
+
state.layer._map?.eachLayer(other => { if (other.control?.time) control = other.control; });
|
|
18
|
+
return { live: control.live, time: control.live ? new Date() : control.time };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createTrafficMesh(state, data, featureFaces, material, usage = 0) {
|
|
22
|
+
const result = createMeshApi.createMesh(state.layer, data, material, usage);
|
|
23
|
+
result.extras.featureFaces = featureFaces;
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function disposeTrafficObject(state, object) {
|
|
28
|
+
if (!object) return;
|
|
29
|
+
if (state.layer.highlightedFeature?.mesh === object) {
|
|
30
|
+
setHighlight.setHighlight(state.layer, null);
|
|
31
|
+
state.layer.hoveredFeature = null;
|
|
32
|
+
showFeature.showFeature(state.layer, null);
|
|
33
|
+
};
|
|
34
|
+
disposeMesh.disposeMesh(state.layer, object);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function failTraffic(state, error) {
|
|
38
|
+
state.status.textContent = `Error: ${error.message}`;
|
|
39
|
+
state.status.setAttribute('role', 'alert');
|
|
40
|
+
console.error('Traffic layer', error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function groundTraffic(state) {
|
|
44
|
+
const { layer, lanes } = state;
|
|
45
|
+
const terrain = layer._map._terrain3D?._terrainReady && layer._map._terrain3D, base = layer.root.position[1];
|
|
46
|
+
for (const lane of lanes) lane.points.forEach((point, i) => {
|
|
47
|
+
const elevation = terrain && (lane.elevation ?? getElevation(terrain, lane.latlngs[i]));
|
|
48
|
+
point[2] = Number.isFinite(elevation) && Number.isFinite(base) ? elevation - base : 0;
|
|
49
|
+
});
|
|
50
|
+
state.lanesDirty = true;
|
|
51
|
+
state.nextCheck = -Infinity;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function prepareTraffic(state) {
|
|
55
|
+
const { layer, lanes } = state;
|
|
56
|
+
const crs = layer._map.options.crs, origin = crs.project(layer.location);
|
|
57
|
+
const metres = Math.cos(layer.location.lat * Math.PI / 180);
|
|
58
|
+
for (const lane of lanes) {
|
|
59
|
+
lane.points = lane.latlngs.map(([lat, lng]) => {
|
|
60
|
+
const point = crs.project(new L.LatLng(lat, lng));
|
|
61
|
+
return [(point.x - origin.x) * metres, -(point.y - origin.y) * metres, 0];
|
|
62
|
+
});
|
|
63
|
+
lane.lengths = [0];
|
|
64
|
+
for (let i = 1; i < lane.points.length; i++) {
|
|
65
|
+
lane.lengths.push(lane.lengths[i - 1] + Math.hypot(lane.points[i][0] - lane.points[i - 1][0], lane.points[i][1] - lane.points[i - 1][1]));
|
|
66
|
+
}
|
|
67
|
+
lane.length = lane.lengths.at(-1);
|
|
68
|
+
}
|
|
69
|
+
groundTraffic(state);
|
|
70
|
+
disposeTrafficObject(state, state.route);
|
|
71
|
+
state.route = null;
|
|
72
|
+
state.tripsKey = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function drawTrafficRoute(state) {
|
|
76
|
+
state.route = createTrafficMesh(state, routeGeometry(state.lanes), [], { emissive: [0.03, 0.25, 0.3] });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function rebuildTrafficMesh(state) {
|
|
80
|
+
const { trips, model, vertices } = state;
|
|
81
|
+
disposeTrafficObject(state, state.mesh);
|
|
82
|
+
state.mesh = null;
|
|
83
|
+
if (!trips.length) return;
|
|
84
|
+
const color = trips.flatMap((trip, i) => build(model(i)).color), count = color.length / 3;
|
|
85
|
+
state.mesh = createTrafficMesh(state, { position: color.map(() => 0), normal: color.map(() => 0), color },
|
|
86
|
+
trips.map((trip, i) => ({
|
|
87
|
+
id: trip.id, properties: trip.properties, faceStart: i * vertices / 3, faceEnd: (i + 1) * vertices / 3,
|
|
88
|
+
vertexStart: i * vertices, vertexEnd: (i + 1) * vertices
|
|
89
|
+
})), {}, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
|
|
90
|
+
state.mesh.geometry.drawRange.count = count;
|
|
91
|
+
trafficBounds(state);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function trafficBounds(state) {
|
|
95
|
+
const extent = { attributes: { position: { data: state.lanes.flatMap(lane => lane.points.flatMap(([x, z, y]) => [x - 20, y, z - 20, x + 20, y + 5, z + 20])) } } };
|
|
96
|
+
computeBounds(extent);
|
|
97
|
+
state.mesh.geometry.bounds = extent.bounds;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function destroyTrafficGpu(state) {
|
|
101
|
+
for (const buffer of [state.gpu?.params, state.gpu?.template, state.gpu?.points, state.gpu?.vehicles]) buffer?.destroy();
|
|
102
|
+
state.gpu = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function trafficStorage(device, data) {
|
|
106
|
+
const buffer = device.createBuffer({ size: Math.max(16, data.byteLength), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
107
|
+
device.queue.writeBuffer(buffer, 0, data);
|
|
108
|
+
return buffer;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function rebaseTraffic(state) {
|
|
112
|
+
const { device } = state.layer.renderer;
|
|
113
|
+
if (state.gpu?.device !== device) {
|
|
114
|
+
destroyTrafficGpu(state);
|
|
115
|
+
state.gpu = {
|
|
116
|
+
device,
|
|
117
|
+
pipeline: device.createComputePipeline({ layout: 'auto', compute: { module: device.createShaderModule({ code: state.shader }), entryPoint: 'main' } }),
|
|
118
|
+
params: device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }),
|
|
119
|
+
template: trafficStorage(device, new Float32Array([...state.template.position, ...state.template.normal])),
|
|
120
|
+
};
|
|
121
|
+
state.lanesDirty = true;
|
|
122
|
+
}
|
|
123
|
+
if (state.lanesDirty) {
|
|
124
|
+
const data = new Float32Array(state.lanes.reduce((count, lane) => count + lane.points.length, 0) * 4);
|
|
125
|
+
let offset = 0;
|
|
126
|
+
for (const lane of state.lanes) {
|
|
127
|
+
lane.start = offset / 4;
|
|
128
|
+
lane.points.forEach((point, i) => { data.set([point[0], point[1], point[2], lane.lengths[i]], offset); offset += 4; });
|
|
129
|
+
}
|
|
130
|
+
state.gpu.points?.destroy();
|
|
131
|
+
state.gpu.points = trafficStorage(device, data);
|
|
132
|
+
state.lanesDirty = false;
|
|
133
|
+
}
|
|
134
|
+
const data = new Float32Array(state.trips.length * 4), index = new Uint32Array(data.buffer);
|
|
135
|
+
state.trips.forEach((trip, i) => {
|
|
136
|
+
index[i * 4] = trip.lane.start;
|
|
137
|
+
index[i * 4 + 1] = trip.lane.points.length;
|
|
138
|
+
data[i * 4 + 2] = trip.distance;
|
|
139
|
+
data[i * 4 + 3] = trip.speed;
|
|
140
|
+
state.mesh.extras.featureFaces[i].properties = trip.properties;
|
|
141
|
+
});
|
|
142
|
+
if (state.gpu.vehicles?.size >= data.byteLength) device.queue.writeBuffer(state.gpu.vehicles, 0, data);
|
|
143
|
+
else {
|
|
144
|
+
state.gpu.vehicles?.destroy();
|
|
145
|
+
state.gpu.vehicles = trafficStorage(device, data);
|
|
146
|
+
}
|
|
147
|
+
const { position, normal } = state.mesh.geometry.attributes;
|
|
148
|
+
state.gpu.bindGroup = device.createBindGroup({
|
|
149
|
+
layout: state.gpu.pipeline.getBindGroupLayout(0),
|
|
150
|
+
entries: [state.gpu.params, state.gpu.template, state.gpu.points, state.gpu.vehicles, position.buffer, normal.buffer]
|
|
151
|
+
.map((buffer, binding) => ({ binding, resource: { buffer } })),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function moveTraffic(state, seconds) {
|
|
156
|
+
const { device } = state.gpu;
|
|
157
|
+
state.paramFloats[0] = state.elapsed = seconds;
|
|
158
|
+
state.paramFloats[1] = state.side;
|
|
159
|
+
state.paramInts[2] = state.vertices;
|
|
160
|
+
state.paramInts[3] = state.trips.length * state.vertices;
|
|
161
|
+
device.queue.writeBuffer(state.gpu.params, 0, state.params);
|
|
162
|
+
const encoder = device.createCommandEncoder(), pass = encoder.beginComputePass();
|
|
163
|
+
pass.setPipeline(state.gpu.pipeline);
|
|
164
|
+
pass.setBindGroup(0, state.gpu.bindGroup);
|
|
165
|
+
pass.dispatchWorkgroups(Math.ceil(state.paramInts[3] / 64));
|
|
166
|
+
pass.end();
|
|
167
|
+
device.queue.submit([encoder.finish()]);
|
|
168
|
+
state.stale = true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function locateTraffic(state, trip) {
|
|
172
|
+
const { points, lengths, length } = trip.lane;
|
|
173
|
+
let distance = trip.distance + state.elapsed * trip.speed;
|
|
174
|
+
distance -= length * Math.floor(distance / length);
|
|
175
|
+
const { x, y, z, dx, dz } = along(points, lengths, distance);
|
|
176
|
+
return { x: x - dz * state.side, y, z: z + dx * state.side, dx, dz };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function placeTraffic(state) {
|
|
180
|
+
if (!state.mesh || !state.stale) return;
|
|
181
|
+
state.stale = false;
|
|
182
|
+
const { position, normal } = state.mesh.geometry.attributes;
|
|
183
|
+
state.trips.forEach((trip, i) => {
|
|
184
|
+
const { x: cx, y, z: cz, dx, dz } = locateTraffic(state, trip);
|
|
185
|
+
for (let k = 0, o = i * state.stride; k < state.stride; k += 3, o += 3) {
|
|
186
|
+
const lx = state.template.position[k], lz = state.template.position[k + 2], nx = state.template.normal[k], nz = state.template.normal[k + 2];
|
|
187
|
+
position.data[o] = cx - lx * dz - lz * dx;
|
|
188
|
+
position.data[o + 1] = y + state.template.position[k + 1];
|
|
189
|
+
position.data[o + 2] = cz + lx * dx - lz * dz;
|
|
190
|
+
normal.data[o] = -nx * dz - nz * dx;
|
|
191
|
+
normal.data[o + 1] = state.template.normal[k + 1];
|
|
192
|
+
normal.data[o + 2] = nx * dx - nz * dz;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function tickTraffic(state) {
|
|
198
|
+
state.frame = undefined;
|
|
199
|
+
if (!state.ready || !state.layer._map) return;
|
|
200
|
+
if (!state.layer.renderer) { state.frame = requestAnimationFrame(state.tick); return; }
|
|
201
|
+
if (state.mesh && !(state.mesh.geometry.attributes.position.buffer.usage & GPUBufferUsage.STORAGE)) {
|
|
202
|
+
state.tripsKey = null;
|
|
203
|
+
state.nextCheck = -Infinity;
|
|
204
|
+
}
|
|
205
|
+
const { live, time } = trafficNow(state), ms = time?.getTime(), valid = Number.isFinite(ms);
|
|
206
|
+
let changed = false;
|
|
207
|
+
if (!live || !valid || !(ms >= state.rebasedAt && ms < state.nextCheck)) {
|
|
208
|
+
let text;
|
|
209
|
+
try {
|
|
210
|
+
state.trips = valid ? tripsAt(time, state.lanes, state.velocity) : [];
|
|
211
|
+
text = !valid ? 'Select a valid simulation date' : !state.loaded ? 'Loading…' :
|
|
212
|
+
`${state.trips.length} vehicle${state.trips.length === 1 ? '' : 's'}${state.loaded.note ? ` · ${state.loaded.note}` : ''}`;
|
|
213
|
+
} catch (error) { return failTraffic(state, error); }
|
|
214
|
+
state.rebasedAt = ms;
|
|
215
|
+
state.nextCheck = ms + 1000;
|
|
216
|
+
const key = state.trips.map(trip => trip.id).join();
|
|
217
|
+
changed = key !== state.tripsKey || (state.outline && !state.route && state.lanes.length);
|
|
218
|
+
if (state.outline && !state.route && state.lanes.length) drawTrafficRoute(state);
|
|
219
|
+
if (key !== state.tripsKey) { rebuildTrafficMesh(state); state.tripsKey = key; }
|
|
220
|
+
if (state.mesh) rebaseTraffic(state);
|
|
221
|
+
if (state.status.textContent !== text) state.status.textContent = text;
|
|
222
|
+
state.status.removeAttribute('role');
|
|
223
|
+
}
|
|
224
|
+
if (state.mesh) moveTraffic(state, (ms - state.rebasedAt) / 1000);
|
|
225
|
+
if (state.mesh && state.lights && (!live || !(performance.now() - state.litAt < 100))) {
|
|
226
|
+
state.litAt = performance.now();
|
|
227
|
+
state.mesh.extras.lights = state.trips.flatMap(trip => {
|
|
228
|
+
const { x, y, z, dx, dz } = locateTraffic(state, trip);
|
|
229
|
+
return state.lights.map(({ position: [lx, ly, lz], ...light }) => ({ ...light, position: [x - lx * dz - lz * dx, y + ly, z + lx * dx - lz * dz] }));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (state.mesh || changed) updateScene(state.layer);
|
|
233
|
+
if (live && valid) state.frame = requestAnimationFrame(state.tick);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function fetchTrafficLanes(state, time, signal) {
|
|
237
|
+
const href = new URL(state.url, document.baseURI).href;
|
|
238
|
+
if (/\.m?js(?:[?#]|$)/i.test(state.url)) return (await import(href)).default(time, state.layer, signal);
|
|
239
|
+
if (!state.file) {
|
|
240
|
+
const response = await fetch(href, { signal });
|
|
241
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
242
|
+
state.file = await response.json();
|
|
243
|
+
}
|
|
244
|
+
return state.file;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function loadedTrafficLanes(state, signal, data) {
|
|
248
|
+
if (signal.aborted || !state.layer._map || data === state.loaded) return;
|
|
249
|
+
state.loaded = data;
|
|
250
|
+
state.lanes = toLanes(data);
|
|
251
|
+
prepareTraffic(state);
|
|
252
|
+
if (!state.frame) state.tick();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function failedTrafficLoad(state, error) {
|
|
256
|
+
if (error.name !== 'AbortError') failTraffic(state, error);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function loadTraffic(state) {
|
|
260
|
+
const { time } = trafficNow(state);
|
|
261
|
+
if (!Number.isFinite(time?.getTime())) return;
|
|
262
|
+
state.request?.abort();
|
|
263
|
+
const { signal } = state.request = new AbortController();
|
|
264
|
+
fetchTrafficLanes(state, time, signal).then(
|
|
265
|
+
loadedTrafficLanes.bind(null, state, signal),
|
|
266
|
+
failedTrafficLoad.bind(null, state),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function updateTraffic(state, event) {
|
|
271
|
+
if (event?.layer === state.layer) return;
|
|
272
|
+
cancelAnimationFrame(state.frame);
|
|
273
|
+
loadTraffic(state);
|
|
274
|
+
state.tick();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function updateTrafficTerrain(state) {
|
|
278
|
+
if (!state.lanes.length) return;
|
|
279
|
+
groundTraffic(state);
|
|
280
|
+
disposeTrafficObject(state, state.route);
|
|
281
|
+
state.route = null;
|
|
282
|
+
if (state.mesh) trafficBounds(state);
|
|
283
|
+
if (!state.frame) state.tick();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function startTraffic(state) {
|
|
287
|
+
state.layer._map.off('simulationtimechange layerremove', state.update).on('simulationtimechange layerremove', state.update);
|
|
288
|
+
state.layer._map.off('terrainchange', state.terrain).on('terrainchange', state.terrain);
|
|
289
|
+
underEntry(state.layer, state.status);
|
|
290
|
+
state.update();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function readyTraffic(state) {
|
|
294
|
+
state.ready = true;
|
|
295
|
+
if (state.layer._map) startTraffic(state);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function addTraffic(state) {
|
|
299
|
+
if (state.ready) startTraffic(state);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function removeTraffic(state) {
|
|
303
|
+
state.request?.abort();
|
|
304
|
+
cancelAnimationFrame(state.frame);
|
|
305
|
+
state.frame = undefined;
|
|
306
|
+
state.layer._map.off('simulationtimechange layerremove', state.update).off('terrainchange', state.terrain);
|
|
307
|
+
disposeTrafficObject(state, state.mesh);
|
|
308
|
+
disposeTrafficObject(state, state.route);
|
|
309
|
+
state.mesh = state.route = null;
|
|
310
|
+
delete state.layer._beforeRaycast;
|
|
311
|
+
destroyTrafficGpu(state);
|
|
312
|
+
state.lanes = [];
|
|
313
|
+
state.trips = [];
|
|
314
|
+
state.tripsKey = state.loaded = null;
|
|
315
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { parseColor } from "./parseColor.js";
|
|
2
|
+
|
|
3
|
+
/** Low-poly tree, sharing the source block's buffers, picking and terrain offset. */
|
|
4
|
+
|
|
5
|
+
export function treeGeometry(point, style, addTriangle) {
|
|
6
|
+
const { top, bottom = 0, trunkRadius, crownRadius, crownBottom, crownShape = 'round' } = style;
|
|
7
|
+
if (![trunkRadius, crownRadius, crownBottom].every(Number.isFinite) ||
|
|
8
|
+
trunkRadius <= 0 || crownRadius < trunkRadius || crownBottom <= bottom || crownBottom >= top ||
|
|
9
|
+
!['round', 'cone'].includes(crownShape)) {
|
|
10
|
+
throw new Error('Invalid tree dimensions or crown shape');
|
|
11
|
+
}
|
|
12
|
+
const trunk = parseColor(style.trunkColor ?? 0x72513a);
|
|
13
|
+
const crown = parseColor(style.color ?? 0x4f823b);
|
|
14
|
+
const ring = radius => Array.from({ length: 8 }, (_, i) => ({
|
|
15
|
+
x: point.x + Math.cos(i * Math.PI / 4) * radius,
|
|
16
|
+
y: point.y + Math.sin(i * Math.PI / 4) * radius,
|
|
17
|
+
ground: point.ground,
|
|
18
|
+
}));
|
|
19
|
+
const surface = (lowRadius, low, highRadius, high, color) => {
|
|
20
|
+
const lower = ring(lowRadius), upper = ring(highRadius);
|
|
21
|
+
for (let i = 0; i < 8; i++) {
|
|
22
|
+
const next = (i + 1) % 8;
|
|
23
|
+
const angle = (i + 0.5) * Math.PI / 4;
|
|
24
|
+
const slope = (lowRadius - highRadius) / (high - low);
|
|
25
|
+
const length = Math.hypot(1, slope);
|
|
26
|
+
const nx = Math.cos(angle) / length, ny = slope / length, nz = -Math.sin(angle) / length;
|
|
27
|
+
if (lowRadius) addTriangle(lower[i], low, lower[next], low, upper[next], high, nx, ny, nz, color);
|
|
28
|
+
if (highRadius) addTriangle(lower[i], low, upper[next], high, upper[i], high, nx, ny, nz, color);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
surface(trunkRadius, bottom, trunkRadius, crownBottom + (top - crownBottom) * 0.25, trunk);
|
|
32
|
+
if (crownShape === 'cone') {
|
|
33
|
+
const base = ring(crownRadius);
|
|
34
|
+
for (let i = 0; i < 8; i++) addTriangle(point, crownBottom, base[i], crownBottom,
|
|
35
|
+
base[(i + 1) % 8], crownBottom, 0, -1, 0, crown);
|
|
36
|
+
surface(crownRadius, crownBottom, 0, top, crown);
|
|
37
|
+
} else {
|
|
38
|
+
const profile = [[0, 0], [0.35, 0.8], [0.6, 1], [0.85, 0.7], [1, 0]];
|
|
39
|
+
for (let i = 1; i < profile.length; i++) {
|
|
40
|
+
const [low, lr] = profile[i - 1], [high, hr] = profile[i];
|
|
41
|
+
surface(lr * crownRadius, crownBottom + low * (top - crownBottom),
|
|
42
|
+
hr * crownRadius, crownBottom + high * (top - crownBottom), crown);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { isClockWise } from "./isClockWise.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Triangulates a polygon given as `{x, y}` points.
|
|
5
|
+
* Rings omit the repeated closing point; winding is normalized internally.
|
|
6
|
+
* Vertex indices remain relative to the original inputs after duplicate/collinear
|
|
7
|
+
* cleanup. Hole bridges duplicate endpoints, not source vertices.
|
|
8
|
+
* @param {Object[]} contour Outer boundary.
|
|
9
|
+
* @param {Object[][]} [holes] Interior boundaries.
|
|
10
|
+
*
|
|
11
|
+
* @returns {number[][]} Triangles as indices in `[...contour, ...holes.flat()]`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function triangulate(contour, holes = []) {
|
|
15
|
+
const points = [contour, ...holes].flat();
|
|
16
|
+
if (points.some(p => !Number.isFinite(p.x) || !Number.isFinite(p.y))) throw new Error('Invalid polygon coordinates');
|
|
17
|
+
// Translate near the origin to avoid cancellation with geographic coordinates.
|
|
18
|
+
const vertices = points.map((p, index) => ({ x: p.x - contour[0].x, y: p.y - contour[0].y, index }));
|
|
19
|
+
const cross = (a, b, c) => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
20
|
+
const equal = (a, b) => a.x === b.x && a.y === b.y;
|
|
21
|
+
// Remove zero-area corners before connecting holes; keep original vertex indices.
|
|
22
|
+
const clean = ring => {
|
|
23
|
+
let changed = true;
|
|
24
|
+
while (changed && ring.length > 3) {
|
|
25
|
+
changed = false;
|
|
26
|
+
for (let i = 0; i < ring.length; i++) {
|
|
27
|
+
const a = ring[(i + ring.length - 1) % ring.length], b = ring[i], c = ring[(i + 1) % ring.length];
|
|
28
|
+
if (equal(a, b) || Math.abs(cross(a, b, c)) < 1e-10) {
|
|
29
|
+
ring.splice(i, 1); changed = true; break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (ring.length < 3) throw new Error('Degenerate polygon ring');
|
|
34
|
+
return ring;
|
|
35
|
+
};
|
|
36
|
+
let offset = 0;
|
|
37
|
+
const rings = [contour, ...holes].map((path, index) => {
|
|
38
|
+
const ring = clean(vertices.slice(offset, offset += path.length));
|
|
39
|
+
if (isClockWise(ring) !== Boolean(index)) ring.reverse();
|
|
40
|
+
return ring;
|
|
41
|
+
});
|
|
42
|
+
const inside = (point, ring) => {
|
|
43
|
+
let result = false;
|
|
44
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
45
|
+
const a = ring[i], b = ring[j];
|
|
46
|
+
if ((a.y > point.y) !== (b.y > point.y) &&
|
|
47
|
+
point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x) result = !result;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
const intersects = (a, b, c, d) => {
|
|
52
|
+
if (equal(a, c) || equal(a, d) || equal(b, c) || equal(b, d)) return false;
|
|
53
|
+
const abC = cross(a, b, c), abD = cross(a, b, d), cdA = cross(c, d, a), cdB = cross(c, d, b);
|
|
54
|
+
const on = (p, q, r) => Math.min(p.x, q.x) <= r.x && r.x <= Math.max(p.x, q.x) &&
|
|
55
|
+
Math.min(p.y, q.y) <= r.y && r.y <= Math.max(p.y, q.y);
|
|
56
|
+
return abC * abD < 0 && cdA * cdB < 0 ||
|
|
57
|
+
abC === 0 && on(a, b, c) || abD === 0 && on(a, b, d) ||
|
|
58
|
+
cdA === 0 && on(c, d, a) || cdB === 0 && on(c, d, b);
|
|
59
|
+
};
|
|
60
|
+
let polygon = rings[0].slice();
|
|
61
|
+
// A bridge must stay inside the outer boundary, outside every hole, and cross no edge.
|
|
62
|
+
// ponytail: quadratic ear clipping for building footprints; use spatial indexing for large rings.
|
|
63
|
+
for (const hole of rings.slice(1).sort((a, b) => Math.min(...a.map(p => p.x)) - Math.min(...b.map(p => p.x)))) {
|
|
64
|
+
let bridge, distance = Infinity;
|
|
65
|
+
for (let h = 0; h < hole.length; h++) for (let p = 0; p < polygon.length; p++) {
|
|
66
|
+
const a = hole[h], b = polygon[p], d = (a.x - b.x) ** 2 + (a.y - b.y) ** 2;
|
|
67
|
+
if (d >= distance) continue;
|
|
68
|
+
const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
69
|
+
if (!inside(mid, rings[0]) || rings.slice(1).some(r => inside(mid, r))) continue;
|
|
70
|
+
if ([polygon, ...rings.slice(1)].some(r => r.some((c, i) => intersects(a, b, c, r[(i + 1) % r.length])))) continue;
|
|
71
|
+
bridge = [h, p]; distance = d;
|
|
72
|
+
}
|
|
73
|
+
if (!bridge) throw new Error('Unable to connect polygon hole');
|
|
74
|
+
const [h, p] = bridge;
|
|
75
|
+
polygon = [...polygon.slice(0, p + 1), ...hole.slice(h), ...hole.slice(0, h + 1),
|
|
76
|
+
polygon[p], ...polygon.slice(p + 1)];
|
|
77
|
+
}
|
|
78
|
+
// Clip a convex corner only when no other vertex is inside/on its triangle.
|
|
79
|
+
const result = [];
|
|
80
|
+
while (polygon.length > 3) {
|
|
81
|
+
let clipped = false;
|
|
82
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
83
|
+
const a = polygon[(i + polygon.length - 1) % polygon.length], b = polygon[i], c = polygon[(i + 1) % polygon.length];
|
|
84
|
+
if (cross(a, b, c) <= 1e-10) continue;
|
|
85
|
+
if (polygon.some(p => !equal(p, a) && !equal(p, b) && !equal(p, c) &&
|
|
86
|
+
cross(a, b, p) >= 0 && cross(b, c, p) >= 0 && cross(c, a, p) >= 0)) continue;
|
|
87
|
+
result.push([a.index, b.index, c.index]); polygon.splice(i, 1); clipped = true; break;
|
|
88
|
+
}
|
|
89
|
+
if (!clipped) throw new Error('Unable to triangulate polygon: invalid or self-intersecting rings');
|
|
90
|
+
}
|
|
91
|
+
if (cross(...polygon) > 1e-10) result.push(polygon.map(p => p.index));
|
|
92
|
+
const area = ring => Math.abs(ring.reduce((sum, p, i) => {
|
|
93
|
+
const q = ring[(i + 1) % ring.length]; return sum + p.x * q.y - q.x * p.y;
|
|
94
|
+
}, 0)) / 2;
|
|
95
|
+
// Reject incomplete coverage instead of silently emitting a partial roof.
|
|
96
|
+
const expected = area(rings[0]) - rings.slice(1).reduce((sum, ring) => sum + area(ring), 0);
|
|
97
|
+
const actual = result.reduce((sum, triangle) => sum + cross(...triangle.map(i => vertices[i])) / 2, 0);
|
|
98
|
+
if (expected <= 0 || Math.abs(actual - expected) > Math.max(1e-7, expected * 1e-8)) throw new Error('Polygon triangulation area mismatch');
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { vehicleOffsets } from "./vehicleOffsets.js";
|
|
2
|
+
import { scheduledTrips } from "./scheduledTrips.js";
|
|
3
|
+
|
|
4
|
+
// A lane with a `timetable` runs scheduled trips end to end; one with a `flow` (vehicles/h) loops evenly spaced vehicles.
|
|
5
|
+
|
|
6
|
+
export const tripsAt = (time, lanes, velocity) => lanes.flatMap((lane, l) => {
|
|
7
|
+
const { timetable, flow = 0 } = lane.properties;
|
|
8
|
+
if (!lane.length) return [];
|
|
9
|
+
if (timetable) return scheduledTrips(time, timetable).map(trip => ({
|
|
10
|
+
id: `${l}-${trip.id}`, lane, distance: trip.progress * lane.length, speed: lane.length / (timetable.duration * 60),
|
|
11
|
+
properties: { ...lane.properties, departure: trip.departure },
|
|
12
|
+
}));
|
|
13
|
+
const travelled = time / 1000 * velocity;
|
|
14
|
+
return (lane.offsets ||= vehicleOffsets(lane.length, flow, velocity))
|
|
15
|
+
.map((offset, k) => ({ id: `${l}-${k}`, lane, distance: (offset + travelled) % lane.length, speed: velocity, properties: lane.properties }));
|
|
16
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps `element` right under `layer`'s entry in the map's layers control while the layer is on the map.
|
|
3
|
+
* Returns false when no layers control lists the layer.
|
|
4
|
+
*/
|
|
5
|
+
export function underEntry(layer, element) {
|
|
6
|
+
const map = layer._map;
|
|
7
|
+
const place = () => {
|
|
8
|
+
const input = [...map.getContainer().querySelectorAll('.leaflet-control-layers-selector')].find(input => input.layerId === L.Util.stamp(layer));
|
|
9
|
+
input?.closest('label').after(element);
|
|
10
|
+
return Boolean(input);
|
|
11
|
+
};
|
|
12
|
+
// Leaflet rebuilds the list after 'layeradd'/'layerremove' (on the layer's own 'add'/'remove'): re-place afterwards.
|
|
13
|
+
const replace = () => queueMicrotask(() => map.hasLayer(layer) && place());
|
|
14
|
+
map.on('layeradd layerremove', replace);
|
|
15
|
+
layer.once('remove', () => { map.off('layeradd layerremove', replace); element.remove(); });
|
|
16
|
+
return place();
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { render } from "./render.js";
|
|
2
|
+
|
|
3
|
+
/** Scene graph, shared camera and frame rendering. */
|
|
4
|
+
/** Content changes: render once on the next animation frame. */
|
|
5
|
+
|
|
6
|
+
export function update(backend) {
|
|
7
|
+
const shared = backend._shared;
|
|
8
|
+
if (!shared) return backend;
|
|
9
|
+
if (shared !== backend) {
|
|
10
|
+
update(shared);
|
|
11
|
+
return backend;
|
|
12
|
+
}
|
|
13
|
+
backend._frame ||= requestAnimationFrame(() => {
|
|
14
|
+
backend._frame = null;
|
|
15
|
+
if (backend._shared === backend && backend.renderer) render(backend);
|
|
16
|
+
});
|
|
17
|
+
return backend;
|
|
18
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { pump } from "./pump.js";
|
|
2
|
+
import { releaseArea } from "./releaseArea.js";
|
|
3
|
+
|
|
4
|
+
/** Cover the visible ground bounds and one bordering block, clipped to the archive. */
|
|
5
|
+
|
|
6
|
+
export function updateArea(backend) {
|
|
7
|
+
if (!backend._map || !backend._shared) return;
|
|
8
|
+
if (backend._geojson) {
|
|
9
|
+
if (!backend._areas.has('geojson')) {
|
|
10
|
+
backend._areas.set('geojson', { request: new AbortController() });
|
|
11
|
+
}
|
|
12
|
+
pump(backend);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!backend.header) return;
|
|
16
|
+
const z = backend.header.minZoom;
|
|
17
|
+
const center = backend._map.project(backend._map.getCenter(), z).divideBy(256).floor();
|
|
18
|
+
const bounds = backend._map.getBounds();
|
|
19
|
+
const nw = backend._map.project(bounds.getNorthWest(), z).divideBy(256).floor();
|
|
20
|
+
const se = backend._map.project(bounds.getSouthEast(), z).divideBy(256).floor();
|
|
21
|
+
const archiveNW = backend._map.project([backend.header.maxLat, backend.header.minLon], z).divideBy(256).floor();
|
|
22
|
+
const archiveSE = backend._map.project([backend.header.minLat, backend.header.maxLon], z).divideBy(256).floor();
|
|
23
|
+
const wanted = new Map();
|
|
24
|
+
for (let x = Math.max(0, nw.x - 1, archiveNW.x); x <= Math.min(2 ** z - 1, se.x + 1, archiveSE.x); x++) {
|
|
25
|
+
for (let y = Math.max(0, nw.y - 1, archiveNW.y); y <= Math.min(2 ** z - 1, se.y + 1, archiveSE.y); y++) {
|
|
26
|
+
wanted.set(`${x}/${y}`, { z, x, y });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const [key, area] of backend._areas) {
|
|
30
|
+
if (!wanted.has(key)) {
|
|
31
|
+
releaseArea(backend, area);
|
|
32
|
+
backend._areas.delete(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Load the nearest new blocks first without rebuilding retained geometry.
|
|
36
|
+
for (const [key, coords] of [...wanted].sort(([, a], [, b]) => Math.hypot(a.x - center.x, a.y - center.y) - Math.hypot(b.x - center.x, b.y - center.y))) {
|
|
37
|
+
if (!backend._areas.has(key)) backend._areas.set(key, { coords, request: new AbortController() });
|
|
38
|
+
}
|
|
39
|
+
pump(backend);
|
|
40
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Orthonormal view basis plus perspective matrices, refreshed before projecting/drawing. */
|
|
2
|
+
|
|
3
|
+
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
4
|
+
|
|
5
|
+
export function updateCamera(camera) {
|
|
6
|
+
const z = camera.position.map((v, i) => v - camera.target[i]);
|
|
7
|
+
const zLength = Math.hypot(...z) || 1;
|
|
8
|
+
for (let i = 0; i < 3; i++) {
|
|
9
|
+
z[i] /= zLength;
|
|
10
|
+
}
|
|
11
|
+
const x = cross(camera.up, z), xLength = Math.hypot(...x) || 1;
|
|
12
|
+
for (let i = 0; i < 3; i++) {
|
|
13
|
+
x[i] /= xLength;
|
|
14
|
+
}
|
|
15
|
+
const y = cross(z, x);
|
|
16
|
+
const view = new DOMMatrix([x[0], x[1], x[2], 0, y[0], y[1], y[2], 0, z[0], z[1], z[2], 0, ...camera.position, 1]).inverse().toFloat64Array();
|
|
17
|
+
if (![...view].every(Number.isFinite)) {
|
|
18
|
+
throw new Error('Singular 3D transform');
|
|
19
|
+
}
|
|
20
|
+
for (let i = 0; i < 16; i++) {
|
|
21
|
+
camera.viewMatrix[i] = view[i];
|
|
22
|
+
}
|
|
23
|
+
const f = 1 / Math.tan(camera.fov * Math.PI / 360), n = camera.near, r = camera.far;
|
|
24
|
+
const projection = [f / camera.aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, r / (n - r), -1, 0, 0, r * n / (n - r), 0];
|
|
25
|
+
for (let i = 0; i < 16; i++) {
|
|
26
|
+
camera.projectionMatrix[i] = projection[i];
|
|
27
|
+
}
|
|
28
|
+
const projectionView = new DOMMatrix(camera.projectionMatrix).multiply(new DOMMatrix(camera.viewMatrix)).toFloat64Array();
|
|
29
|
+
for (let i = 0; i < 16; i++) {
|
|
30
|
+
camera.projectionViewMatrix[i] = projectionView[i];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { lightGrid } from './lightGrid.js';
|
|
2
|
+
import { traverse } from './traverse.js';
|
|
3
|
+
import { worldLights } from './worldLights.js';
|
|
4
|
+
import { destroyTexture } from './destroyTexture.js';
|
|
5
|
+
import { bindSunlight } from './bindSunlight.js';
|
|
6
|
+
|
|
7
|
+
/** A cached spatial grid limits fragment lighting to nearby lamps, without dropping dense cells. */
|
|
8
|
+
|
|
9
|
+
export function updatePointLights(backend) {
|
|
10
|
+
const { device, sunlight: sun } = backend.renderer;
|
|
11
|
+
const meshes = [];
|
|
12
|
+
for (const layer of backend._layers) {
|
|
13
|
+
if (!layer.options.nightLight || !layer.root.visible) continue;
|
|
14
|
+
traverse(layer.root, mesh => {
|
|
15
|
+
if (!mesh.visible) return true;
|
|
16
|
+
if (mesh.extras?.lights) meshes.push(mesh);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (sun.lightMeshes?.length === meshes.length && meshes.every((mesh, i) => sun.lightMeshes[i].mesh === mesh &&
|
|
20
|
+
sun.lightMeshes[i].lights === mesh.extras.lights && mesh.worldMatrix.every((v, j) => v === sun.lightMeshes[i].matrix[j]))) return;
|
|
21
|
+
// Per mesh, world-space [x, y, z, radius, r, g, b, intensity] per light: re-packed only when its lights or matrix change.
|
|
22
|
+
const packed = meshes.map(mesh => {
|
|
23
|
+
const cache = mesh._worldLights, m = mesh.worldMatrix, lights = mesh.extras.lights;
|
|
24
|
+
if (cache?.lights === lights && m.every((v, j) => v === cache.matrix[j])) return cache.data;
|
|
25
|
+
const values = worldLights(lights, m);
|
|
26
|
+
mesh._worldLights = { lights, matrix: [...m], data: values };
|
|
27
|
+
return values;
|
|
28
|
+
});
|
|
29
|
+
const { data, count } = lightGrid(packed, device.limits.maxStorageBufferBindingSize);
|
|
30
|
+
if (sun.lights.size < data.byteLength) {
|
|
31
|
+
destroyTexture(backend, sun.lights);
|
|
32
|
+
sun.lights = device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
33
|
+
bindSunlight(backend);
|
|
34
|
+
}
|
|
35
|
+
device.queue.writeBuffer(sun.lights, 0, data);
|
|
36
|
+
sun.lightCount = count;
|
|
37
|
+
sun.lightMeshes = meshes.map(mesh => ({ mesh, lights: mesh.extras.lights, matrix: [...mesh.worldMatrix] }));
|
|
38
|
+
}
|