From c414b9d2d611b46ee69b0b0ca534329b01529022 Mon Sep 17 00:00:00 2001 From: Thomas Fuhrmann Date: Tue, 11 Mar 2025 11:56:33 +0100 Subject: [PATCH] Add OSM topography --- app/components/Form.tsx | 14 ++++++-- app/components/Map.tsx | 7 +++- app/three/SceneView.ts | 36 +++++++++++++++++---- app/three/utils/build-coordinate-grid.ts | 11 +++++-- app/three/utils/build-meshes.ts | 11 +++++-- app/three/utils/build-scene.ts | 2 +- app/three/utils/{parsers.ts => decoders.ts} | 0 app/three/utils/utils.ts | 13 +++++++- package-lock.json | 16 +++++++++ package.json | 4 ++- 10 files changed, 96 insertions(+), 18 deletions(-) rename app/three/utils/{parsers.ts => decoders.ts} (100%) diff --git a/app/components/Form.tsx b/app/components/Form.tsx index becb99d..e7a72c5 100644 --- a/app/components/Form.tsx +++ b/app/components/Form.tsx @@ -7,6 +7,7 @@ import { SceneViewContextType, } from "../providers/scene-view-provider"; import { Mesh, MeshStandardMaterial } from "three"; +import { scheduler } from "timers/promises"; function Toggle({ title, @@ -123,16 +124,23 @@ export function Form() { } } + function handleChangeTopography() { + if (!sceneView) return; + + sceneView.toggleTopography(); + } + return (
+ + - {
diff --git a/app/components/Map.tsx b/app/components/Map.tsx index bc6a69e..06bb451 100644 --- a/app/components/Map.tsx +++ b/app/components/Map.tsx @@ -1,12 +1,16 @@ "use client"; import { useContext, useEffect, useRef } from "react"; -import { SceneView } from "../three/SceneView"; import { SceneViewContext, SceneViewContextType, } from "../providers/scene-view-provider"; +async function lazyLoad() { + const { SceneView } = await import("../three/SceneView"); + return SceneView; +} + export function Map() { const divRef = useRef(null); const { setSceneView } = useContext(SceneViewContext) as SceneViewContextType; @@ -17,6 +21,7 @@ export function Map() { async function loadScene() { if (divRef.current) { + const SceneView = await lazyLoad(); const _sceneView = await SceneView.create(divRef.current, "20"); if (_sceneView) { setSceneView(_sceneView); diff --git a/app/three/SceneView.ts b/app/three/SceneView.ts index de33477..10b9e92 100644 --- a/app/three/SceneView.ts +++ b/app/three/SceneView.ts @@ -1,7 +1,7 @@ import { Group, Mesh, MeshStandardMaterial, Scene } from "three"; import { buildMeshes } from "./utils/build-meshes"; import { Extent, buildScene } from "./utils/build-scene"; -import { getMetadata } from "./utils/utils"; +import { getCenter3D, getMetadata, transform } from "./utils/utils"; import { MODEL_ID, SERVICE_URL } from "./config"; import { Orientation, @@ -9,6 +9,7 @@ import { } from "./utils/build-clipping-planes"; import { buildCoordinateGrid } from "./utils/build-coordinate-grid"; import { DragControls } from "three/examples/jsm/Addons.js"; +import { MapView, OpenStreetMapsProvider } from "geo-three"; export class SceneView { private _scene: Scene; @@ -88,6 +89,13 @@ export class SceneView { } } } + + toggleTopography() { + const topo = this._scene.getObjectByName("topography"); + if (topo) { + topo.visible = !topo.visible; + } + } } async function init(container: HTMLElement, modelId = MODEL_ID) { @@ -95,13 +103,16 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { const mappedFeatures = modelData.mappedfeatures; const modelarea = modelData.modelarea; + // Transfrom extent to EPSG 3857 + const pmin = transform([modelarea.x.min, modelarea.y.min, modelarea.z.min]); + const pmax = transform([modelarea.x.max, modelarea.y.max, modelarea.z.max]); const extent: Extent = { - xmin: modelarea.x.min, - xmax: modelarea.x.max, - ymin: modelarea.y.min, - ymax: modelarea.y.max, - zmin: modelarea.z.min, - zmax: modelarea.z.max, + xmin: pmin[0], + xmax: pmax[0], + ymin: pmin[1], + ymax: pmax[1], + zmin: pmin[2], + zmax: pmax[2], }; const { renderer, scene, camera, controls } = buildScene(container, extent); @@ -133,10 +144,21 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { const annotationsGroup = new Group(); annotationsGroup.name = "coordinate-grid"; annotationsGroup.add(...annotations, gridHelper); + annotationsGroup.visible = false; scene.add(annotationsGroup); //const axesHelper = new AxesHelper(5); //scene.add(axesHelper); + // Create a map tiles provider object + const provider = new OpenStreetMapsProvider(); + + // Create the map view of OSM topography + const map = new MapView(MapView.PLANAR, provider); + map.rotateX(Math.PI / 2); + // map.position.setComponent(2, -100); + map.name = "topography"; + scene.add(map); + return { scene, model, dragControls }; } diff --git a/app/three/utils/build-coordinate-grid.ts b/app/three/utils/build-coordinate-grid.ts index 5ceffae..f697cfe 100644 --- a/app/three/utils/build-coordinate-grid.ts +++ b/app/three/utils/build-coordinate-grid.ts @@ -82,7 +82,13 @@ function createLabel( transparent: true, }); const sprite = new Sprite(spriteMaterial); - sprite.position.set(position.x, position.y, position.z); + + // Set position according to axis orientation + if (orientation === Orientation.Horizontal) { + sprite.position.set(position.x + 1000, position.y - 1500, position.z + 500); + } else { + sprite.position.set(position.x, position.y - 500, position.z + 500); + } sprite.scale.set(5000, 2500, 1); // Scale the sprite to make the text readable return sprite; } @@ -101,7 +107,8 @@ function generateTextCanvas(text: string, orientation: Orientation) { context.fillStyle = "black"; if (orientation === Orientation.Horizontal) { - context.fillText(text, 300, 160); + //context.fillText(text, 300, 160); + context.fillText(text, 100, 90); } else { context.fillText(text, 100, 90); } diff --git a/app/three/utils/build-meshes.ts b/app/three/utils/build-meshes.ts index 7ba3e02..56f4031 100644 --- a/app/three/utils/build-meshes.ts +++ b/app/three/utils/build-meshes.ts @@ -6,7 +6,7 @@ import { MeshStandardMaterial, } from "three"; -import { fetchVertices, fetchTriangleIndices } from "./utils"; +import { fetchVertices, fetchTriangleIndices, transform } from "./utils"; import { TRIANGLE_INDICES_URL, VERTICES_URL } from "../config"; interface MappedFeature { @@ -38,7 +38,14 @@ async function buildMesh(layerData: MappedFeature) { const geometry = new BufferGeometry(); const vertices = await fetchVertices(VERTICES_URL, geomId); - const positions = new BufferAttribute(vertices, 3); + // Transform coordinates to EPSG 3857 + const vertices3857 = new Float32Array(vertices.length); + for (let i = 0; i < vertices.length; i += 3) { + const vertex = Array.from(vertices.slice(i, i + 3)); + vertices3857.set(transform(vertex), i); + } + + const positions = new BufferAttribute(vertices3857, 3); geometry.setAttribute("position", positions); const indexArray = await fetchTriangleIndices(TRIANGLE_INDICES_URL, geomId); diff --git a/app/three/utils/build-scene.ts b/app/three/utils/build-scene.ts index 2c825eb..f04a7b9 100644 --- a/app/three/utils/build-scene.ts +++ b/app/three/utils/build-scene.ts @@ -64,7 +64,7 @@ export function buildScene(container: HTMLElement, extent: Extent) { maxSize * 25 ); - camera.position.set(center.x, center.y - 125000, extent.zmax + 100000); + camera.position.set(center.x, center.y - 200000, extent.zmax + 100000); camera.up.set(0, 0, 1); camera.lookAt(center); diff --git a/app/three/utils/parsers.ts b/app/three/utils/decoders.ts similarity index 100% rename from app/three/utils/parsers.ts rename to app/three/utils/decoders.ts diff --git a/app/three/utils/utils.ts b/app/three/utils/utils.ts index 74e7e63..8569625 100644 --- a/app/three/utils/utils.ts +++ b/app/three/utils/utils.ts @@ -1,6 +1,7 @@ import { Vector3 } from "three"; import { Extent } from "./build-scene"; -import { unpackEdges, unpackVertices } from "./parsers"; +import { unpackEdges, unpackVertices } from "./decoders"; +import proj4 from "proj4"; export function getMaxSize(extent: Extent) { return Math.max( @@ -54,3 +55,13 @@ export async function fetchVertices(pointUrl: string, geomId: string) { const buffer = await request(url); return unpackVertices(buffer); } + +// Transformation from EPSG 3034 to EPSG 3857 +const SOURCE = "EPSG:3034"; +const PROJ_STRING = + "+proj=lcc +lat_0=52 +lon_0=10 +lat_1=35 +lat_2=65 +x_0=4000000 +y_0=2800000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs"; +const DEST = "EPSG:3857"; +proj4.defs(SOURCE, PROJ_STRING); +export function transform(p: number[]) { + return proj4(SOURCE, DEST, p); +} diff --git a/package-lock.json b/package-lock.json index 3b860fe..4554206 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "earcut": "^3.0.1", + "geo-three": "^0.1.15", "next": "15.2.1", "proj4": "^2.15.0", "react": "^19.0.0", @@ -20,6 +21,7 @@ "@tailwindcss/postcss": "^4.0.11", "@types/earcut": "^3.0.0", "@types/node": "^22", + "@types/proj4": "^2.5.6", "@types/react": "^19", "@types/react-dom": "^19", "@types/three": "^0.174.0", @@ -1047,6 +1049,12 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/proj4": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@types/proj4/-/proj4-2.5.6.tgz", + "integrity": "sha512-zfMrPy9fx+8DchqM0kIUGeu2tTVB5ApO1KGAYcSGFS8GoqRIkyL41xq2yCx/iV3sOLzo7v4hEgViSLTiPI1L0w==", + "dev": true + }, "node_modules/@types/react": { "version": "19.0.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", @@ -2714,6 +2722,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/geo-three": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/geo-three/-/geo-three-0.1.15.tgz", + "integrity": "sha512-mU5K5QfnRwOawTjXg1zmAMTu+c/uC6cZFNGYqf41eCRXfX4bkDHFaPhXa08z1WjYzg5kq1BSjpFR9weW2vNJZg==", + "peerDependencies": { + "three": ">0.120.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", diff --git a/package.json b/package.json index 4d79fdb..9df59c5 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "earcut": "^3.0.1", + "geo-three": "^0.1.15", "next": "15.2.1", "proj4": "^2.15.0", "react": "^19.0.0", @@ -21,6 +22,7 @@ "@tailwindcss/postcss": "^4.0.11", "@types/earcut": "^3.0.0", "@types/node": "^22", + "@types/proj4": "^2.5.6", "@types/react": "^19", "@types/react-dom": "^19", "@types/three": "^0.174.0", @@ -30,4 +32,4 @@ "tailwindcss": "^4.0.11", "typescript": "^5" } -} \ No newline at end of file +}