From 96161081cc05e4c2209dfe7044469463104735cd Mon Sep 17 00:00:00 2001 From: azarz Date: Mon, 24 Jun 2024 18:46:43 +0200 Subject: [PATCH 01/16] feat(navigation): add 3d pitch and buildings --- src/js/services/location.js | 4 +- src/js/three-d.js | 36 ++++++++ www/data/bati-3d.json | 169 ++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/js/three-d.js create mode 100644 www/data/bati-3d.json diff --git a/src/js/services/location.js b/src/js/services/location.js index e6c2402a..ecd028f2 100644 --- a/src/js/services/location.js +++ b/src/js/services/location.js @@ -9,6 +9,7 @@ import maplibregl from "maplibre-gl"; import DOM from "../dom"; import Globals from "../globals"; import GisUtils from "../utils/gis-utils"; +import threeD from "../three-d"; import { Toast } from "@capacitor/toast"; import { ScreenOrientation } from "@capacitor/screen-orientation"; @@ -497,7 +498,7 @@ const getOrientation = async (event) => { mapBearing = tempMapBearing; if (navigation_active) { if (!isMapPanning) { - Globals.map.easeTo({bearing: -mapBearing, duration: 100}); + Globals.map.easeTo({bearing: -mapBearing, pitch: 45, duration: 100}); } DOM.$compassBtn.classList.remove("d-none"); DOM.$compassBtn.style.transform = "rotate(" + mapBearing + "deg)"; @@ -557,6 +558,7 @@ const disableTracking = () => { tracking_active = false; if (navigation_active) { navigation_active = false; + threeD.remove3dBuildings(); } Globals.map.touchZoomRotate.enable(); Globals.map.getCanvasContainer().removeEventListener("touchstart", locationOnTouchStartHandler); diff --git a/src/js/three-d.js b/src/js/three-d.js new file mode 100644 index 00000000..4f1cb647 --- /dev/null +++ b/src/js/three-d.js @@ -0,0 +1,36 @@ +import Globals from "./globals"; + +let buildingsLayers = []; + +async function _fetch3dBuildingsLayers() { + const response = await fetch("data/bati-3d.json"); + const data = await response.json(); + buildingsLayers = data.layers; +} + +async function add3dBuildings() { + if (buildingsLayers.length === 0) { + await _fetch3dBuildingsLayers(); + } + // HACK + // on positionne toujours le style avant ceux du calcul d'itineraires (directions) + // afin que le calcul soit toujours la couche visible du dessus ! + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; + buildingsLayers.forEach((layer) => { + Globals.map.addLayer(layer, layerIdBefore); + }) + Globals.interactivityIndicator.hardDisable(); +} + +function remove3dBuildings() { + buildingsLayers.forEach((layer) => { + Globals.map.removeLayer(layer.id); + }) + Globals.interactivityIndicator.enable(); +} + +export default { + add3dBuildings, + remove3dBuildings, +} diff --git a/www/data/bati-3d.json b/www/data/bati-3d.json new file mode 100644 index 00000000..7ac8d696 --- /dev/null +++ b/www/data/bati-3d.json @@ -0,0 +1,169 @@ +{ + "version": 8, + "name": "PLAN IGN bâti 3d", + "glyphs": "https://data.geopf.fr/annexes/ressources/vectorTiles/fonts/{fontstack}/{range}.pbf", + "sprite": "data/poi-osm-sprite", + "metadata": { + "geoportail:tooltip": "BDTOPO/multicouche_bdtopo" + }, + "sources": { + "bdtopo": { + "type": "vector", + "maxzoom": 19, + "minzoom": 15, + "tiles": [ + "https://data.geopf.fr/tms/1.0.0/BDTOPO/{z}/{x}/{y}.pbf" + ] + } + }, + "transition": { + "duration": 300, + "delay": 0 + }, + "layers": [ + { + "id": "batiment_residentiel_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Résidentiel" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#F1EBD9" + } + }, + { + "id": "batiment_annexe_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Annexe" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#F1EBD9" + } + }, + { + "id": "batiment_agricole_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Agricole" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#E6E6E6" + } + }, + { + "id": "batiment_commercial_et_services_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Commercial et services" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#E3BFE2" + } + }, + { + "id": "batiment_industriel_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Industriel" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#E6E6E6" + } + }, + { + "id": "batiment_sportif_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Sportif" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#DCE6E4" + } + }, + { + "id": "batiment_religieux_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Religieux" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#F7E1E1" + } + }, + { + "id": "batiment_indifferencie_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Indifférencié" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#F1EBD9" + } + } + ] +} \ No newline at end of file From 537be3a385178d41e8aacafa9356a74fb15aeca3 Mon Sep 17 00:00:00 2001 From: azarz Date: Thu, 27 Jun 2024 15:11:32 +0200 Subject: [PATCH 02/16] feat(3d): add terrain --- src/js/three-d.js | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/js/three-d.js b/src/js/three-d.js index 4f1cb647..4328acba 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -1,4 +1,11 @@ +/** + * Copyright (c) Institut national de l'information géographique et forestière + * + * This program and the accompanying materials are made available under the terms of the GPL License, Version 3.0. + */ + import Globals from "./globals"; +import maplibregl from "maplibre-gl"; let buildingsLayers = []; @@ -8,6 +15,63 @@ async function _fetch3dBuildingsLayers() { buildingsLayers = data.layers; } +// Function to fetch and parse x-bil tile data +async function _fetchAndParseXBil(url) { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + const dataView = new DataView(arrayBuffer); + const width = Math.sqrt(dataView.byteLength / 4); // Assuming square tiles + const height = width; + const elevations = new Float32Array(width * height); + for (let i = 0; i < width * height; i++) { + elevations[i] = dataView.getFloat32(i * 4, true); + } + return { elevations, width, height }; +} + +function add3dTerrain() { + if (!Globals.map.getSource("bil-terrain")) { + Globals.map.addSource("bil-terrain", { + type: "raster-dem", + tiles: [ + "dem://data.geopf.fr/wms-r/wms?bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES" + ], + minzoom: 6, + maxzoom: 14, + tileSize: 256 + }); + + maplibregl.addProtocol("dem", async (params, abortController) => { + try { + const { elevations, width, height } = await _fetchAndParseXBil(`https://${params.url.split("://")[1]}`); + const data = new Uint8ClampedArray(width * height * 4); + for (let i = 0; i < elevations.length; i++) { + const elevation = Math.round(elevations[i] * 10) / 10; + // reverse https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data + const baseElevationValue = 10 * (elevation + 10000); + const red = Math.floor(baseElevationValue / (256 * 256)) % 256; + const green = Math.floor((baseElevationValue - red * 256 * 256) / 256) % 256; + const blue = baseElevationValue - red * 256 * 256 - green * 256; + data[4 * i] = red; + data[4 * i + 1] = green; + data[4 * i + 2] = blue; + data[4 * i + 3] = 255; + } + const imageData = new ImageData(data, width, height); + const imageBitmap = await createImageBitmap(imageData); + return { + data: imageBitmap + }; + } catch (error) { + throw error; + } + }); + } + + // Set terrain using the custom source + Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); +} + async function add3dBuildings() { if (buildingsLayers.length === 0) { await _fetch3dBuildingsLayers(); @@ -33,4 +97,5 @@ function remove3dBuildings() { export default { add3dBuildings, remove3dBuildings, + add3dTerrain, } From 7dc3da96e9e45e756dfdae9f31a89c3a76c2dd2b Mon Sep 17 00:00:00 2001 From: azarz Date: Thu, 4 Jul 2024 12:26:56 +0200 Subject: [PATCH 03/16] feat(3d): 3d now a global control --- src/js/controls.js | 4 + src/js/globals.js | 4 + src/js/services/location.js | 9 +- src/js/three-d.js | 192 +++++++++++++++++++++--------------- 4 files changed, 129 insertions(+), 80 deletions(-) diff --git a/src/js/controls.js b/src/js/controls.js index 01e24ff6..d4c6a9c4 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -22,6 +22,7 @@ import SignalementOSM from "./signalement-osm"; import Landmark from "./landmark"; import MapboxAccessibility from "./poi-accessibility"; import DOM from "./dom"; +import ThreeD from "./three-d"; import LocationLayers from "./services/location-styles"; import compareLandmark from "./compare-landmark"; @@ -133,6 +134,9 @@ const addControls = () => { }); Globals.compareLandmark = new compareLandmark(Globals.mapRLT1, Globals.mapRLT2, {}); + // 3d + Globals.threeD = new ThreeD(map, {}); + // contrôle filtres POI Globals.poi = new POI(map, {}); Globals.poi.load() // promise ! diff --git a/src/js/globals.js b/src/js/globals.js index f0161b84..aef779a3 100644 --- a/src/js/globals.js +++ b/src/js/globals.js @@ -98,6 +98,9 @@ let signalementOSM = null; let landmark = null; let compareLandmark = null; +// Global control 3d +let threeD = null; + // Global flag: is the device connected to the internet? let online = true; @@ -155,4 +158,5 @@ export default { osmPoiAccessibility, landmark, compareLandmark, + threeD, }; diff --git a/src/js/services/location.js b/src/js/services/location.js index ecd028f2..71bfbc5a 100644 --- a/src/js/services/location.js +++ b/src/js/services/location.js @@ -9,7 +9,6 @@ import maplibregl from "maplibre-gl"; import DOM from "../dom"; import Globals from "../globals"; import GisUtils from "../utils/gis-utils"; -import threeD from "../three-d"; import { Toast } from "@capacitor/toast"; import { ScreenOrientation } from "@capacitor/screen-orientation"; @@ -556,6 +555,7 @@ const disableTracking = () => { DOM.$geolocateBtn.classList.remove("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); tracking_active = false; +<<<<<<< HEAD if (navigation_active) { navigation_active = false; threeD.remove3dBuildings(); @@ -569,7 +569,14 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { DOM.$geolocateBtn.classList.add("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); navigation_active = false; +<<<<<<< HEAD Globals.map.setPadding({top: 0, right: 0, bottom: 0, left: 0}); +======= +======= + Globals.threeD.remove3dBuildings(); + Globals.threeD.remove3dTerrain(); +>>>>>>> 4617b4d (feat(3d): 3d now a global control) +>>>>>>> d8b8e79 (feat(3d): 3d now a global control) Globals.map.flyTo({ bearing: bearing, pitch: 0, diff --git a/src/js/three-d.js b/src/js/three-d.js index 4328acba..151416d4 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -7,95 +7,129 @@ import Globals from "./globals"; import maplibregl from "maplibre-gl"; -let buildingsLayers = []; +/** + * Interface sur le contrôle 3d + * @module ThreeD + */ +class ThreeD { + /** + * constructeur + * @constructs + * @param {*} map + * @param {*} options + */ + constructor(map, options) { + this.options = options || { + target: null, + // callback + openSearchControlCbk: null, + closeSearchControlCbk: null + }; -async function _fetch3dBuildingsLayers() { - const response = await fetch("data/bati-3d.json"); - const data = await response.json(); - buildingsLayers = data.layers; -} + this.map = map; -// Function to fetch and parse x-bil tile data -async function _fetchAndParseXBil(url) { - const response = await fetch(url); - const arrayBuffer = await response.arrayBuffer(); - const dataView = new DataView(arrayBuffer); - const width = Math.sqrt(dataView.byteLength / 4); // Assuming square tiles - const height = width; - const elevations = new Float32Array(width * height); - for (let i = 0; i < width * height; i++) { - elevations[i] = dataView.getFloat32(i * 4, true); + this.buildingsLayers = []; + + return this; } - return { elevations, width, height }; -} -function add3dTerrain() { - if (!Globals.map.getSource("bil-terrain")) { - Globals.map.addSource("bil-terrain", { - type: "raster-dem", - tiles: [ - "dem://data.geopf.fr/wms-r/wms?bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES" - ], - minzoom: 6, - maxzoom: 14, - tileSize: 256 - }); + async #fetch3dBuildingsLayers() { + if (!Globals.map.getSource("bdtopo")) { + Globals.map.addSource("bdtopo", { + "type": "vector", + "maxzoom": 19, + "minzoom": 15, + "tiles": [ + "https://data.geopf.fr/tms/1.0.0/BDTOPO/{z}/{x}/{y}.pbf" + ] + }); + } + const response = await fetch("data/bati-3d.json"); + const data = await response.json(); + this.buildingsLayers = data.layers; + } - maplibregl.addProtocol("dem", async (params, abortController) => { - try { - const { elevations, width, height } = await _fetchAndParseXBil(`https://${params.url.split("://")[1]}`); - const data = new Uint8ClampedArray(width * height * 4); - for (let i = 0; i < elevations.length; i++) { - const elevation = Math.round(elevations[i] * 10) / 10; - // reverse https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data - const baseElevationValue = 10 * (elevation + 10000); - const red = Math.floor(baseElevationValue / (256 * 256)) % 256; - const green = Math.floor((baseElevationValue - red * 256 * 256) / 256) % 256; - const blue = baseElevationValue - red * 256 * 256 - green * 256; - data[4 * i] = red; - data[4 * i + 1] = green; - data[4 * i + 2] = blue; - data[4 * i + 3] = 255; - } - const imageData = new ImageData(data, width, height); - const imageBitmap = await createImageBitmap(imageData); - return { - data: imageBitmap - }; - } catch (error) { - throw error; + // Function to fetch and parse x-bil tile data + async #fetchAndParseXBil(url) { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + const dataView = new DataView(arrayBuffer); + const width = Math.sqrt(dataView.byteLength / 4); // Assuming square tiles + const height = width; + const elevations = new Float32Array(width * height); + for (let i = 0; i < width * height; i++) { + elevations[i] = dataView.getFloat32(i * 4, true); } - }); + return { elevations, width, height }; } - // Set terrain using the custom source - Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); -} + add3dTerrain() { + if (!Globals.map.getSource("bil-terrain")) { + Globals.map.addSource("bil-terrain", { + type: "raster-dem", + tiles: [ + "dem://data.geopf.fr/wms-r/wms?bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES" + ], + minzoom: 6, + maxzoom: 14, + tileSize: 256 + }); -async function add3dBuildings() { - if (buildingsLayers.length === 0) { - await _fetch3dBuildingsLayers(); + maplibregl.addProtocol("dem", async (params, abortController) => { + try { + const { elevations, width, height } = await this.#fetchAndParseXBil(`https://${params.url.split("://")[1]}`); + const data = new Uint8ClampedArray(width * height * 4); + for (let i = 0; i < elevations.length; i++) { + const elevation = Math.round(elevations[i] * 10) / 10; + // reverse https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data + const baseElevationValue = 10 * (elevation + 10000); + const red = Math.floor(baseElevationValue / (256 * 256)) % 256; + const green = Math.floor((baseElevationValue - red * 256 * 256) / 256) % 256; + const blue = baseElevationValue - red * 256 * 256 - green * 256; + data[4 * i] = red; + data[4 * i + 1] = green; + data[4 * i + 2] = blue; + data[4 * i + 3] = 255; + } + const imageData = new ImageData(data, width, height); + const imageBitmap = await createImageBitmap(imageData); + return { + data: imageBitmap + }; + } catch (error) { + throw error; + } + }); + } + + // Set terrain using the custom source + Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); } - // HACK - // on positionne toujours le style avant ceux du calcul d'itineraires (directions) - // afin que le calcul soit toujours la couche visible du dessus ! - var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); - var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; - buildingsLayers.forEach((layer) => { - Globals.map.addLayer(layer, layerIdBefore); - }) - Globals.interactivityIndicator.hardDisable(); -} -function remove3dBuildings() { - buildingsLayers.forEach((layer) => { - Globals.map.removeLayer(layer.id); - }) - Globals.interactivityIndicator.enable(); -} + async add3dBuildings() { + if (this.buildingsLayers.length === 0) { + await this.#fetch3dBuildingsLayers(); + } + // HACK + // on positionne toujours le style avant ceux du calcul d'itineraires (directions) + // afin que le calcul soit toujours la couche visible du dessus ! + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; + this.buildingsLayers.forEach((layer) => { + Globals.map.addLayer(layer, layerIdBefore); + }); + } + + remove3dBuildings() { + this.buildingsLayers.forEach((layer) => { + Globals.map.removeLayer(layer.id); + }) + Globals.interactivityIndicator.enable(); + } -export default { - add3dBuildings, - remove3dBuildings, - add3dTerrain, + remove3dTerrain() { + Globals.map.setTerrain(); + } } + +export default ThreeD; From f13dca79458db9617fb543cdc07b116ce855f5c7 Mon Sep 17 00:00:00 2001 From: azarz Date: Thu, 4 Jul 2024 14:36:29 +0200 Subject: [PATCH 04/16] feat(3d): add hillshade, sky. Now through map button and not navigation --- package-lock.json | 19 +++++++++---------- package.json | 2 +- src/css/map-buttons.css | 9 +++++++++ src/html/mapButtons.html | 2 ++ src/js/dom.js | 5 +++++ src/js/map-buttons-listeners.js | 23 +++++++++++++++++++++++ src/js/services/location.js | 16 +++++++++++++++- src/js/three-d.js | 21 ++++++++++++++++++++- 8 files changed, 84 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index e3b0876c..261f8892 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "chart.js": "^4.4.1", "install": "^0.13.0", "lodash": "^4.17.21", - "maplibre-gl": "^4.3.2", + "maplibre-gl": "^4.5.0", "npm": "^10.5.0", "proj4": "^2.10.0" }, @@ -8440,10 +8440,9 @@ } }, "node_modules/geojson-vt": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", - "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", - "license": "ISC" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==" }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -10796,9 +10795,9 @@ } }, "node_modules/maplibre-gl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.3.2.tgz", - "integrity": "sha512-/oXDsb9I+LkjweL/28aFMLDZoIcXKNEhYNAZDLA4xgTNkfvKQmV/r0KZdxEMcVthincJzdyc6Y4N8YwZtHKNnQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.5.0.tgz", + "integrity": "sha512-qOS1hn4d/pn2i0uva4S5Oz+fACzTkgBKq+NpwT/Tqzi4MSyzcWNtDELzLUSgWqHfNIkGCl5CZ/w7dtis+t4RCw==", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -10807,7 +10806,7 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.2.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.0", "@types/geojson": "^7946.0.14", "@types/geojson-vt": "3.2.5", "@types/junit-report-builder": "^3.0.2", @@ -10816,7 +10815,7 @@ "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", "earcut": "^2.2.4", - "geojson-vt": "^3.2.1", + "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.3", "global-prefix": "^3.0.0", "kdbush": "^4.0.2", diff --git a/package.json b/package.json index efdce944..0a0e2ba0 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "chart.js": "^4.4.1", "install": "^0.13.0", "lodash": "^4.17.21", - "maplibre-gl": "^4.3.2", + "maplibre-gl": "^4.5.0", "npm": "^10.5.0", "proj4": "^2.10.0" }, diff --git a/src/css/map-buttons.css b/src/css/map-buttons.css index cb83cd07..29f3179b 100644 --- a/src/css/map-buttons.css +++ b/src/css/map-buttons.css @@ -194,6 +194,15 @@ bottom: 132px; } +#threeDBtn { + position: absolute; + top: calc(130px + var(--safe-area-inset-top)); + font-family: "Open Sans Semibold"; + display: flex; + align-items: center; + justify-content: center; +} + #filterPoiBtn { background-image: url("assets/map-buttons/filtrer.svg"); position: fixed; diff --git a/src/html/mapButtons.html b/src/html/mapButtons.html index 94901e12..623378b8 100644 --- a/src/html/mapButtons.html +++ b/src/html/mapButtons.html @@ -2,6 +2,8 @@
Filtres
+ +
3D
diff --git a/src/js/dom.js b/src/js/dom.js index b1c88824..3baa448c 100644 --- a/src/js/dom.js +++ b/src/js/dom.js @@ -21,6 +21,7 @@ const $selectOnMap = document.getElementById("selectOnMap"); const $geolocateBtn = document.getElementById("geolocateBtn"); const $backTopLeftBtn = document.getElementById("backTopLeftBtn"); const $compassBtn = document.getElementById("compassBtn"); +const $threeDBtn = document.getElementById("threeDBtn"); const $layerManagerBtn = document.getElementById("layerManagerBtn"); const $sideBySideBtn = document.getElementById("sideBySideBtn"); const $compareMode = document.getElementById("compareMode"); @@ -134,6 +135,10 @@ export default { $fullScreenBtn, $mapScale, $map, +<<<<<<< HEAD $createCompareLandmarkBtn, $compareLandmarkWindow, +======= + $threeDBtn, +>>>>>>> d1a4abc (feat(3d): add hillshade, sky. Now through map button and not navigation) }; diff --git a/src/js/map-buttons-listeners.js b/src/js/map-buttons-listeners.js index 04e92206..b3c36321 100644 --- a/src/js/map-buttons-listeners.js +++ b/src/js/map-buttons-listeners.js @@ -88,6 +88,29 @@ const addListeners = () => { Globals.compareLandmark.location = [Globals.mapRLT1.getCenter().lng, Globals.mapRLT1.getCenter().lat]; } }); + + // Toggle 3D + DOM.$threeDBtn.addEventListener("click", () => { + if (Globals.threeD.on) { + Globals.threeD.remove3dBuildings(); + Globals.threeD.remove3dTerrain(); + if (!Location.isTrackingActive()) { + Globals.map.flyTo({ + pitch: 0, + duration: 500, + }); + setTimeout( () => {Globals.map.setMaxPitch(0)}, 500); + } + } else { + Globals.map.setMaxPitch(80); + Globals.threeD.add3dBuildings(); + Globals.threeD.add3dTerrain(); + Globals.map.flyTo({ + pitch: 45, + duration: 500, + }); + } + }); }; export default { diff --git a/src/js/services/location.js b/src/js/services/location.js index 71bfbc5a..b28137dc 100644 --- a/src/js/services/location.js +++ b/src/js/services/location.js @@ -555,7 +555,6 @@ const disableTracking = () => { DOM.$geolocateBtn.classList.remove("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); tracking_active = false; -<<<<<<< HEAD if (navigation_active) { navigation_active = false; threeD.remove3dBuildings(); @@ -569,6 +568,7 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { DOM.$geolocateBtn.classList.add("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); navigation_active = false; +<<<<<<< HEAD <<<<<<< HEAD Globals.map.setPadding({top: 0, right: 0, bottom: 0, left: 0}); ======= @@ -584,6 +584,20 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { }); if (bearing === 0) { DOM.$compassBtn.classList.add("d-none"); +======= + Globals.threeD.remove3dBuildings(); + Globals.threeD.remove3dTerrain(); + if (!Globals.threeD.on) { + Globals.map.flyTo({ + pitch: 0, + bearing: bearing, + duration: 500, + }) + if (bearing === 0) { + DOM.$compassBtn.classList.add("d-none"); + } + setTimeout( () => {Globals.map.setMaxPitch(0)}, 500); +>>>>>>> bb1e8d8 (feat(3d): add hillshade, sky. Now through map button and not navigation) } }; diff --git a/src/js/three-d.js b/src/js/three-d.js index 151416d4..ddc0e3df 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -7,6 +7,14 @@ import Globals from "./globals"; import maplibregl from "maplibre-gl"; +const hillsLayer = { + id: "hills", + type: "hillshade", + source: "bil-terrain", + layout: {visibility: "visible"}, + paint: {"hillshade-shadow-color": "#473B24"} +} + /** * Interface sur le contrôle 3d * @module ThreeD @@ -30,6 +38,8 @@ class ThreeD { this.buildingsLayers = []; + this.on = false; + return this; } @@ -104,6 +114,12 @@ class ThreeD { // Set terrain using the custom source Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); + Globals.map.setSky({ + "sky-color": "#199EF3", + "fog-ground-blend": 0.8, + }); + Globals.map.addLayer(hillsLayer); + this.on = true; } async add3dBuildings() { @@ -118,17 +134,20 @@ class ThreeD { this.buildingsLayers.forEach((layer) => { Globals.map.addLayer(layer, layerIdBefore); }); + this.on = true; } remove3dBuildings() { this.buildingsLayers.forEach((layer) => { Globals.map.removeLayer(layer.id); }) - Globals.interactivityIndicator.enable(); + this.on = false; } remove3dTerrain() { Globals.map.setTerrain(); + Globals.map.removeLayer(hillsLayer.id); + this.on = false; } } From 9488bb162b4163eb2dfcbe749d51b2be72388ba8 Mon Sep 17 00:00:00 2001 From: azarz Date: Thu, 4 Jul 2024 15:54:57 +0200 Subject: [PATCH 05/16] feat(elevation): ignore nodata --- src/js/three-d.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/js/three-d.js b/src/js/three-d.js index ddc0e3df..03ef7fd7 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -69,6 +69,9 @@ class ThreeD { const elevations = new Float32Array(width * height); for (let i = 0; i < width * height; i++) { elevations[i] = dataView.getFloat32(i * 4, true); + if (elevations[i] < 100) { + elevations[i] = 0; + } } return { elevations, width, height }; } From 8153f4f1c0fb1507071934097a81b9fa0cc15e90 Mon Sep 17 00:00:00 2001 From: "amaury.zarzelli" Date: Wed, 10 Jul 2024 11:04:05 +0200 Subject: [PATCH 06/16] new terrain layer, better resampling --- src/js/dom.js | 3 --- src/js/services/location.js | 25 +------------------------ src/js/three-d.js | 13 +++++++++---- 3 files changed, 10 insertions(+), 31 deletions(-) diff --git a/src/js/dom.js b/src/js/dom.js index 3baa448c..e590bdc8 100644 --- a/src/js/dom.js +++ b/src/js/dom.js @@ -135,10 +135,7 @@ export default { $fullScreenBtn, $mapScale, $map, -<<<<<<< HEAD $createCompareLandmarkBtn, $compareLandmarkWindow, -======= $threeDBtn, ->>>>>>> d1a4abc (feat(3d): add hillshade, sky. Now through map button and not navigation) }; diff --git a/src/js/services/location.js b/src/js/services/location.js index b28137dc..e6c2402a 100644 --- a/src/js/services/location.js +++ b/src/js/services/location.js @@ -497,7 +497,7 @@ const getOrientation = async (event) => { mapBearing = tempMapBearing; if (navigation_active) { if (!isMapPanning) { - Globals.map.easeTo({bearing: -mapBearing, pitch: 45, duration: 100}); + Globals.map.easeTo({bearing: -mapBearing, duration: 100}); } DOM.$compassBtn.classList.remove("d-none"); DOM.$compassBtn.style.transform = "rotate(" + mapBearing + "deg)"; @@ -557,7 +557,6 @@ const disableTracking = () => { tracking_active = false; if (navigation_active) { navigation_active = false; - threeD.remove3dBuildings(); } Globals.map.touchZoomRotate.enable(); Globals.map.getCanvasContainer().removeEventListener("touchstart", locationOnTouchStartHandler); @@ -568,15 +567,7 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { DOM.$geolocateBtn.classList.add("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); navigation_active = false; -<<<<<<< HEAD -<<<<<<< HEAD Globals.map.setPadding({top: 0, right: 0, bottom: 0, left: 0}); -======= -======= - Globals.threeD.remove3dBuildings(); - Globals.threeD.remove3dTerrain(); ->>>>>>> 4617b4d (feat(3d): 3d now a global control) ->>>>>>> d8b8e79 (feat(3d): 3d now a global control) Globals.map.flyTo({ bearing: bearing, pitch: 0, @@ -584,20 +575,6 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { }); if (bearing === 0) { DOM.$compassBtn.classList.add("d-none"); -======= - Globals.threeD.remove3dBuildings(); - Globals.threeD.remove3dTerrain(); - if (!Globals.threeD.on) { - Globals.map.flyTo({ - pitch: 0, - bearing: bearing, - duration: 500, - }) - if (bearing === 0) { - DOM.$compassBtn.classList.add("d-none"); - } - setTimeout( () => {Globals.map.setMaxPitch(0)}, 500); ->>>>>>> bb1e8d8 (feat(3d): add hillshade, sky. Now through map button and not navigation) } }; diff --git a/src/js/three-d.js b/src/js/three-d.js index 03ef7fd7..51d6e0a5 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -69,7 +69,7 @@ class ThreeD { const elevations = new Float32Array(width * height); for (let i = 0; i < width * height; i++) { elevations[i] = dataView.getFloat32(i * 4, true); - if (elevations[i] < 100) { + if (elevations[i] < -10 || elevations[i] > 4900) { elevations[i] = 0; } } @@ -81,7 +81,7 @@ class ThreeD { Globals.map.addSource("bil-terrain", { type: "raster-dem", tiles: [ - "dem://data.geopf.fr/wms-r/wms?bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES" + `dem://data.geopf.fr/private/wms-r/wms?apikey=${process.env.GPF_key}&bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES.LINEAR` ], minzoom: 6, maxzoom: 14, @@ -93,7 +93,7 @@ class ThreeD { const { elevations, width, height } = await this.#fetchAndParseXBil(`https://${params.url.split("://")[1]}`); const data = new Uint8ClampedArray(width * height * 4); for (let i = 0; i < elevations.length; i++) { - const elevation = Math.round(elevations[i] * 10) / 10; + let elevation = Math.round(elevations[i] * 10) / 10; // reverse https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data const baseElevationValue = 10 * (elevation + 10000); const red = Math.floor(baseElevationValue / (256 * 256)) % 256; @@ -121,7 +121,12 @@ class ThreeD { "sky-color": "#199EF3", "fog-ground-blend": 0.8, }); - Globals.map.addLayer(hillsLayer); + // HACK + // on positionne toujours le style avant ceux du calcul d'itineraires (directions) + // afin que le calcul soit toujours la couche visible du dessus ! + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; + Globals.map.addLayer(hillsLayer, layerIdBefore); this.on = true; } From aba2ac4934970b4730a8d86a3130526f185b8791 Mon Sep 17 00:00:00 2001 From: "amaury.zarzelli" Date: Tue, 14 Jan 2025 17:30:51 +0100 Subject: [PATCH 07/16] feat: move 3d to layer catalogue instead of button --- src/css/map-buttons.css | 9 ---- src/html/mapButtons.html | 2 - src/js/controls.js | 1 + src/js/index.js | 2 +- src/js/layer-manager/layer-catalogue.js | 59 +++++++++++++++++++++++++ src/js/layer-manager/layer-switcher.js | 6 ++- src/js/map-buttons-listeners.js | 22 --------- src/js/three-d.js | 42 +++++++++++------- 8 files changed, 91 insertions(+), 52 deletions(-) diff --git a/src/css/map-buttons.css b/src/css/map-buttons.css index 29f3179b..cb83cd07 100644 --- a/src/css/map-buttons.css +++ b/src/css/map-buttons.css @@ -194,15 +194,6 @@ bottom: 132px; } -#threeDBtn { - position: absolute; - top: calc(130px + var(--safe-area-inset-top)); - font-family: "Open Sans Semibold"; - display: flex; - align-items: center; - justify-content: center; -} - #filterPoiBtn { background-image: url("assets/map-buttons/filtrer.svg"); position: fixed; diff --git a/src/html/mapButtons.html b/src/html/mapButtons.html index 623378b8..94901e12 100644 --- a/src/html/mapButtons.html +++ b/src/html/mapButtons.html @@ -2,8 +2,6 @@
Filtres
- -
3D
diff --git a/src/js/controls.js b/src/js/controls.js index d4c6a9c4..b793d91e 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -136,6 +136,7 @@ const addControls = () => { // 3d Globals.threeD = new ThreeD(map, {}); + Globals.manager.layerCatalogue.add3DThematicLayers(); // contrôle filtres POI Globals.poi = new POI(map, {}); diff --git a/src/js/index.js b/src/js/index.js index d40b95c0..e1a64b19 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -163,7 +163,7 @@ function app() { attributionControl: false, maxZoom: 21, locale: "fr", - maxPitch: 45, + maxPitch: 60, crossSourceCollisions: false, }); diff --git a/src/js/layer-manager/layer-catalogue.js b/src/js/layer-manager/layer-catalogue.js index 8314b300..d47c636d 100644 --- a/src/js/layer-manager/layer-catalogue.js +++ b/src/js/layer-manager/layer-catalogue.js @@ -153,6 +153,65 @@ class LayerCatalogue extends EventTarget { target.appendChild(container); } + add3DThematicLayers() { + var target = this.options.target || document.getElementById("layer-thematics"); + if (!target) { + console.warn(); + return; + } + var container = target.querySelector("#thematicLayers"); + + var buildings3DLayerHtml = ` +
+
+ Bâtiments 3D +
+
+
+
3D
+
Bâtiments 3D
+
+ `; + + var buildings3DLayerElement = DomUtils.stringToHTML(buildings3DLayerHtml.trim()); + buildings3DLayerElement.addEventListener("click", () => { + if (buildings3DLayerElement.classList.contains("selectedLayer")) { + Globals.threeD.remove3dBuildings(); + buildings3DLayerElement.classList.remove("selectedLayer"); + } else { + Globals.threeD.add3dBuildings(); + buildings3DLayerElement.classList.add("selectedLayer"); + } + }); + + container.appendChild(buildings3DLayerElement); + + var terrainLayerHtml = ` +
+
+ Relief 3D +
+
+
+
3D
+
Relief 3D
+
+ `; + + var terrainLayerElement = DomUtils.stringToHTML(terrainLayerHtml.trim()); + terrainLayerElement.addEventListener("click", () => { + if (terrainLayerElement.classList.contains("selectedLayer")) { + Globals.threeD.remove3dTerrain(); + terrainLayerElement.classList.remove("selectedLayer"); + } else { + Globals.threeD.add3dTerrain(); + terrainLayerElement.classList.add("selectedLayer"); + } + }); + + container.appendChild(terrainLayerElement); + } + /** * Ecouteurs */ diff --git a/src/js/layer-manager/layer-switcher.js b/src/js/layer-manager/layer-switcher.js index b5e577c3..af1ce16b 100644 --- a/src/js/layer-manager/layer-switcher.js +++ b/src/js/layer-manager/layer-switcher.js @@ -622,7 +622,7 @@ class LayerSwitcher extends EventTarget { this.layers[id].style = data_2.layers; // sauvegarde ! } catch (e) { if (fallback) { - fetchStyle(fallback, null); + await fetchStyle(fallback, null); } else { this.layers[id].error = true; throw new Error(e); @@ -680,6 +680,10 @@ class LayerSwitcher extends EventTarget { this.#setColor(id, !layerOptions.gray); } this.#setVisibility(id, layerOptions.visible); + // Cas particulier : ajout de l'ombrage à plan IGN si la 3D est activée + if (id === "PLAN.IGN.INTERACTIF$GEOPORTAIL:GPP:TMS" && Globals.threeD && Globals.threeD.terrainOn) { + Globals.threeD.addHillShadeToPlanIgn(); + } /** * Evenement "addlayer" * @event addlayer diff --git a/src/js/map-buttons-listeners.js b/src/js/map-buttons-listeners.js index b3c36321..9f34c929 100644 --- a/src/js/map-buttons-listeners.js +++ b/src/js/map-buttons-listeners.js @@ -89,28 +89,6 @@ const addListeners = () => { } }); - // Toggle 3D - DOM.$threeDBtn.addEventListener("click", () => { - if (Globals.threeD.on) { - Globals.threeD.remove3dBuildings(); - Globals.threeD.remove3dTerrain(); - if (!Location.isTrackingActive()) { - Globals.map.flyTo({ - pitch: 0, - duration: 500, - }); - setTimeout( () => {Globals.map.setMaxPitch(0)}, 500); - } - } else { - Globals.map.setMaxPitch(80); - Globals.threeD.add3dBuildings(); - Globals.threeD.add3dTerrain(); - Globals.map.flyTo({ - pitch: 45, - duration: 500, - }); - } - }); }; export default { diff --git a/src/js/three-d.js b/src/js/three-d.js index 51d6e0a5..3b0f71ec 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -12,7 +12,8 @@ const hillsLayer = { type: "hillshade", source: "bil-terrain", layout: {visibility: "visible"}, - paint: {"hillshade-shadow-color": "#473B24"} + paint: {"hillshade-shadow-color": "#473B24"}, + metadata: {group: "PLAN.IGN.INTERACTIF$GEOPORTAIL:GPP:TMS"}, } /** @@ -37,8 +38,7 @@ class ThreeD { this.map = map; this.buildingsLayers = []; - - this.on = false; + this.terrainOn = false; return this; } @@ -117,17 +117,25 @@ class ThreeD { // Set terrain using the custom source Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); - Globals.map.setSky({ - "sky-color": "#199EF3", - "fog-ground-blend": 0.8, - }); + this.addHillShadeToPlanIgn(); + this.terrainOn = true; + } + + addHillShadeToPlanIgn() { + if (Globals.map.getLayer(hillsLayer.id)) { + return; + } // HACK - // on positionne toujours le style avant ceux du calcul d'itineraires (directions) - // afin que le calcul soit toujours la couche visible du dessus ! - var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + // on positionne toujours le layer après la dernière couche de PLAN IGN + var beforeId = "detail_hydrographique$$$PLAN.IGN.INTERACTIF$GEOPORTAIL:GPP:TMS"; + if (!Globals.map.getLayer(beforeId)) { + return; + } + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.id === beforeId) + 1; var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; - Globals.map.addLayer(hillsLayer, layerIdBefore); - this.on = true; + if (layerIdBefore) { + Globals.map.addLayer(hillsLayer, layerIdBefore); + } } async add3dBuildings() { @@ -137,25 +145,25 @@ class ThreeD { // HACK // on positionne toujours le style avant ceux du calcul d'itineraires (directions) // afin que le calcul soit toujours la couche visible du dessus ! - var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions") + 1; var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; this.buildingsLayers.forEach((layer) => { Globals.map.addLayer(layer, layerIdBefore); }); - this.on = true; } remove3dBuildings() { this.buildingsLayers.forEach((layer) => { Globals.map.removeLayer(layer.id); }) - this.on = false; } remove3dTerrain() { Globals.map.setTerrain(); - Globals.map.removeLayer(hillsLayer.id); - this.on = false; + if (Globals.map.getLayer(hillsLayer.id)) { + Globals.map.removeLayer(hillsLayer.id); + } + this.terrainOn = false; } } From 557059598866b0bb0a50b55648366445924a2901 Mon Sep 17 00:00:00 2001 From: Amaury Zarzelli Date: Thu, 16 Jan 2025 14:41:23 +0100 Subject: [PATCH 08/16] Feat/immersive position (#139) --- src/js/immersive-position.js | 199 ++++++++++++++++++++++++++ src/js/layer-manager/layer-manager.js | 28 ++-- src/js/map-listeners.js | 2 +- src/js/position.js | 22 ++- 4 files changed, 233 insertions(+), 18 deletions(-) create mode 100644 src/js/immersive-position.js diff --git a/src/js/immersive-position.js b/src/js/immersive-position.js new file mode 100644 index 00000000..8aa93e4b --- /dev/null +++ b/src/js/immersive-position.js @@ -0,0 +1,199 @@ +/** + * Copyright (c) Institut national de l'information géographique et forestière + * + * This program and the accompanying materials are made available under the terms of the GPL License, Version 3.0. + */ + +import proj4 from "proj4"; +proj4.defs("EPSG:2154","+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs"); + +const queryConfig = [ + { + layer: "LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:commune", + attributes: ["nom", "population"], + }, + { + layer: "LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:departement", + attributes: ["nom"], + }, + { + layer: "BDTOPO_V3:parc_ou_reserve", + attributes: ["nature", "toponyme"], + geom_name: "geometrie", + }, + { + layer: "BDTOPO_V3:foret_publique", + attributes: ["toponyme"], + geom_name: "geometrie", + around: 5, + }, + { + layer: "BDTOPO_V3:toponymie_lieux_nommes", + attributes: ["graphie_du_toponyme"], + geom_name: "geometrie", + around: 5, + additional_cql: "AND nature_de_l_objet='Bois'", + }, + { + layer: "LANDCOVER.FORESTINVENTORY.V2:formation_vegetale", + attributes: ["essence"], + around: 5, + epsg: 2154, + }, + { + layer: "RPG.LATEST:parcelles_graphiques", + attributes: ["code_cultu"], + around: 5, + }, + { + layer: "BDTOPO_V3:zone_d_activite_ou_d_interet", + attributes: ["nature", "toponyme"], + geom_name: "geometrie", + around: 5, + additional_cql: "AND categorie='Culture et loisirs' AND nature IN ('Abri de montagne', 'Aire de détente', 'Camping', 'Construction', 'Ecomusée', 'Hébergement de loisirs', 'Monument', 'Musée', 'Office de tourisme', 'Parc de loisirs', 'Parc zoologique', 'Point de vue', 'Refuge', 'Vestige archéologique')", + }, + { + layer: "BDTOPO_V3:cours_d_eau", + attributes: ["toponyme"], + geom_name: "geometrie", + around: 5, + }, + { + layer: "BDTOPO_V3:plan_d_eau", + attributes: ["nature", "toponyme"], + geom_name: "geometrie", + around: 5, + }, +]; + +/** + * Gestion de la "position immersive" avec des requêtes faites aux données autour d'une position + * @fires dataLoaded + */ +class ImmersivePosion extends EventTarget { + /** + * constructeur + * @param {*} options - + * @param {*} options.lat - latitude + * @param {*} options.lng - longitude + */ + constructor(options) { + super(); + this.options = options || { + lat : 0, + lng : 0, + }; + this.lat = this.options.lat; + this.lng = this.options.lng; + + this.data = {}; + + // récupération des codes culture pour RPG + this.codes_culture = {}; + fetch( + "https://data.geopf.fr/wfs/ows?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&typename=RPG.LATEST:codes_cultures&outputFormat=json&count=1000" + ).then((resp) => resp.json()).then( (resp) => { + resp.features.forEach((feature) => { + this.codes_culture[feature.properties.code] = feature.properties.libelle; + }); + }); + } + + /** + * Computes html string from availmable data + */ + computeHtml() { + const htmlTemplate = ` +

Ville : ${this.data["LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:commune"] ? this.data["LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:commune"][0][0] : "chargement..."}, ${this.data["LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:commune"] ? this.data["LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:commune"][0][1] : "chargement..."} habitants

+

Département : ${this.data["LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:departement"] ? this.data["LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:departement"][0] : "chargement..."}

+

Parcs naturels : ${this.data["BDTOPO_V3:parc_ou_reserve"] ? JSON.stringify(this.data["BDTOPO_V3:parc_ou_reserve"]) : "aucun"}

+

Foret : ${this.data["BDTOPO_V3:foret_publique"] ? JSON.stringify(this.data["BDTOPO_V3:foret_publique"]) : "..."} ${this.data["BDTOPO_V3:toponymie_lieux_nommes"] ? JSON.stringify(this.data["BDTOPO_V3:toponymie_lieux_nommes"]) : "..."}

+

Essence principale : ${this.data["LANDCOVER.FORESTINVENTORY.V2:formation_vegetale"] ? JSON.stringify(this.data["LANDCOVER.FORESTINVENTORY.V2:formation_vegetale"]) : "..."}

+

Cultures : ${this.data["RPG.LATEST:parcelles_graphiques"] ? JSON.stringify(this.data["RPG.LATEST:parcelles_graphiques"]) : "..."}

+

ZAI loisirs : ${this.data["BDTOPO_V3:zone_d_activite_ou_d_interet"] ? JSON.stringify(this.data["BDTOPO_V3:zone_d_activite_ou_d_interet"]) : "..."}

+

Cours d'eau : ${this.data["BDTOPO_V3:cours_d_eau"] ? JSON.stringify(this.data["BDTOPO_V3:cours_d_eau"]) : "..."}

+

Plans d'eau : ${this.data["BDTOPO_V3:plan_d_eau"] ? JSON.stringify(this.data["BDTOPO_V3:plan_d_eau"]) : "..."}

+ `; + return htmlTemplate; + } + + /** + * Computes all data queries + */ + computeAll() { + queryConfig.forEach( (config) => { + this.#computeFromConfig(config); + }); + } + + /** + * Queries GPF's WFS for info defined in the config + */ + async #computeFromConfig(config) { + const result = await this.#computeGenericGPFWFS( + config.layer, + config.attributes, + config.around || 0, + config.geom_name || "geom", + config.additional_cql || "", + config.epsg || 4326, + ); + + this.data[config.layer] = result; + + this.dispatchEvent( + new CustomEvent("dataLoaded", { + bubbles: true, + }) + ); + } + + /** + * Computes data for a given layer of Geoplateforme's WFS + * @param {string} layer name of the WFS layer + * @param {Array} attributes list of strings of the relevant attributes to return + * @param {number} around distance around the point in km for the query, default 0 + * @param {string} geom_name name of the geometry column, default "geom" + * @param {string} additional_cql cql filter needed other than geometry, e.g. "AND nature_de_l_objet='Bois'", default "" + * @param {number} epsg epsg number of the layer's CRS, default 4326 + */ + async #computeGenericGPFWFS(layer, attributes, around=0, geom_name="geom", additional_cql="", epsg=4326) { + let coord1 = this.lat; + let coord2 = this.lng; + if (epsg !== 4326) { + [coord1, coord2] = proj4(proj4.defs("EPSG:4326"), proj4.defs(`EPSG:${epsg}`), [this.lng, this.lat]); + } + let cql_filter = `INTERSECTS(${geom_name},Point(${coord1}%20${coord2}))`; + if (around > 0) { + cql_filter = `DWITHIN(${geom_name},Point(${coord1}%20${coord2}),${around},kilometers)`; + } + if (additional_cql) { + cql_filter += ` ${additional_cql}`; + } + + const results = await fetch( + `https://data.geopf.fr/wfs/ows?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&typename=${layer}&outputFormat=json&count=10&CQL_FILTER=${cql_filter}` + ); + const json = await results.json(); + + const results_attributes = []; + json.features.forEach((feature) => { + const feature_attributes = []; + attributes.forEach((attribute) => { + // Cas particulier du RPG : décodage de la culture en libellé + if (layer === "RPG.LATEST:parcelles_graphiques" && attribute === "code_cultu" && Object.keys(this.codes_culture).length) { + feature.properties[attribute] = this.codes_culture[feature.properties[attribute]]; + } + feature_attributes.push(feature.properties[attribute]); + }); + if (attributes.length === 1) { + results_attributes.push(feature_attributes[0]); + } else { + results_attributes.push(feature_attributes); + } + }); + return Array.from( new Set(results_attributes) ); + } +} + +export default ImmersivePosion; diff --git a/src/js/layer-manager/layer-manager.js b/src/js/layer-manager/layer-manager.js index 9ba2ce47..a21fe98a 100644 --- a/src/js/layer-manager/layer-manager.js +++ b/src/js/layer-manager/layer-manager.js @@ -44,24 +44,24 @@ import LayersConfig from "./layer-config"; */ class LayerManager extends EventTarget { /** - * constructeur - * @param {*} options - - * @param {*} options.target - ... - * @param {*} options.layers - ... - * @example - * new LayerManger({ - * layers : [ - * layers : "couche1, couche2, ...", - * type : "base" // data ou thematic - * ] - * }); - */ + * constructeur + * @param {*} options - + * @param {*} options.target - ... + * @param {*} options.layers - ... + * @example + * new LayerManger({ + * layers : [ + * layers : "couche1, couche2, ...", + * type : "base" // data ou thematic + * ] + * }); + */ constructor(options) { super(); this.options = options || { /** - * ["layerid", "layer2id"] - */ + * ["layerid", "layer2id"] + */ layers : [], target : null }; diff --git a/src/js/map-listeners.js b/src/js/map-listeners.js index d8e335fd..643f1fa9 100644 --- a/src/js/map-listeners.js +++ b/src/js/map-listeners.js @@ -61,7 +61,7 @@ const addListeners = () => { if (Globals.backButtonState.split("-")[0] === "position") { Globals.menu.close("position"); } - Globals.position.compute({ lngLat: evt.lngLat }).then(() => { + Globals.position.compute({ lngLat: evt.lngLat, type: "context" }).then(() => { Globals.menu.open("position"); }); Globals.searchResultMarker = new maplibregl.Marker({element: Globals.searchResultIcon, anchor: "bottom"}) diff --git a/src/js/position.js b/src/js/position.js index be583cf0..9813fa32 100644 --- a/src/js/position.js +++ b/src/js/position.js @@ -18,6 +18,7 @@ import ActionSheet from "./action-sheet"; import PopupUtils from "./utils/popup-utils"; import LoadingDark from "../css/assets/loading-darkgrey.svg"; +import ImmersivePosion from "./immersive-position"; /** * Permet d'afficher ma position sur la carte @@ -83,6 +84,8 @@ class Position { popup: null }; + this.immersivePosition = null; + return this; } @@ -414,13 +417,13 @@ class Position { * @param {string} options.html html situé avant les boutons d'action * @param {string} options.html2 html situé après les boutons d'action * @param {Function} options.hideCallback fonction de callback pour la fermeture de la position (pour les animations) - * @param {string} options.type type de position : default, myposition ou landmark + * @param {string} options.type type de position : default, context, myposition ou landmark * @public */ async compute(options = {}) { const lngLat = options.lngLat || false; const text = options.text || "Repère placé"; - const html = options.html || ""; + let html = options.html || ""; const html2 = options.html2 || ""; const hideCallback = options.hideCallback || null; const type = options.type || "default"; @@ -441,6 +444,11 @@ class Position { text: text }; } + this.coordinates = position.coordinates; + if (type === "myposition" || type === "context") { + this.immersivePosition = new ImmersivePosion({lat: this.coordinates.lat, lng: this.coordinates.lon}); + html = `
${this.immersivePosition.computeHtml()}
`; + } this.header = position.text; try { @@ -457,7 +465,6 @@ class Position { this.additionalHtml.beforeButtons = html; this.additionalHtml.afterButtons = html2; - this.coordinates = position.coordinates; this.address = Reverse.getAddress() || { number: "", street: "", @@ -488,6 +495,13 @@ class Position { this.#setShareContent(this.coordinates.lat, this.coordinates.lon, this.elevation); document.getElementById("positionAltitudeSpan").innerText = this.elevation; }); + + if (type === "myposition" || type === "context") { + this.immersivePosition.addEventListener("dataLoaded", () => { + document.getElementById("immersivePostionHtmlBefore").innerHTML = this.immersivePosition.computeHtml(); + }); + this.immersivePosition.computeAll(); + } } #setShareContent(latitude, longitude, altitude = "") { @@ -578,6 +592,8 @@ https://cartes-ign.ign.fr?lng=${longitude}&lat=${latitude}&z=${zoom}`; this.elevation = null; this.opened = false; this.shareContent = null; + this.immersivePosition = null; + // nettoyage du DOM if (this.container) { this.container.remove(); From 8459ffff68686f64f2998a8c4354d5eaab402d3d Mon Sep 17 00:00:00 2001 From: azarz Date: Mon, 24 Jun 2024 18:46:43 +0200 Subject: [PATCH 09/16] feat(navigation): add 3d pitch and buildings --- src/js/services/location.js | 4 +- src/js/three-d.js | 36 ++++++++ www/data/bati-3d.json | 169 ++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/js/three-d.js create mode 100644 www/data/bati-3d.json diff --git a/src/js/services/location.js b/src/js/services/location.js index e6c2402a..ecd028f2 100644 --- a/src/js/services/location.js +++ b/src/js/services/location.js @@ -9,6 +9,7 @@ import maplibregl from "maplibre-gl"; import DOM from "../dom"; import Globals from "../globals"; import GisUtils from "../utils/gis-utils"; +import threeD from "../three-d"; import { Toast } from "@capacitor/toast"; import { ScreenOrientation } from "@capacitor/screen-orientation"; @@ -497,7 +498,7 @@ const getOrientation = async (event) => { mapBearing = tempMapBearing; if (navigation_active) { if (!isMapPanning) { - Globals.map.easeTo({bearing: -mapBearing, duration: 100}); + Globals.map.easeTo({bearing: -mapBearing, pitch: 45, duration: 100}); } DOM.$compassBtn.classList.remove("d-none"); DOM.$compassBtn.style.transform = "rotate(" + mapBearing + "deg)"; @@ -557,6 +558,7 @@ const disableTracking = () => { tracking_active = false; if (navigation_active) { navigation_active = false; + threeD.remove3dBuildings(); } Globals.map.touchZoomRotate.enable(); Globals.map.getCanvasContainer().removeEventListener("touchstart", locationOnTouchStartHandler); diff --git a/src/js/three-d.js b/src/js/three-d.js new file mode 100644 index 00000000..4f1cb647 --- /dev/null +++ b/src/js/three-d.js @@ -0,0 +1,36 @@ +import Globals from "./globals"; + +let buildingsLayers = []; + +async function _fetch3dBuildingsLayers() { + const response = await fetch("data/bati-3d.json"); + const data = await response.json(); + buildingsLayers = data.layers; +} + +async function add3dBuildings() { + if (buildingsLayers.length === 0) { + await _fetch3dBuildingsLayers(); + } + // HACK + // on positionne toujours le style avant ceux du calcul d'itineraires (directions) + // afin que le calcul soit toujours la couche visible du dessus ! + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; + buildingsLayers.forEach((layer) => { + Globals.map.addLayer(layer, layerIdBefore); + }) + Globals.interactivityIndicator.hardDisable(); +} + +function remove3dBuildings() { + buildingsLayers.forEach((layer) => { + Globals.map.removeLayer(layer.id); + }) + Globals.interactivityIndicator.enable(); +} + +export default { + add3dBuildings, + remove3dBuildings, +} diff --git a/www/data/bati-3d.json b/www/data/bati-3d.json new file mode 100644 index 00000000..7ac8d696 --- /dev/null +++ b/www/data/bati-3d.json @@ -0,0 +1,169 @@ +{ + "version": 8, + "name": "PLAN IGN bâti 3d", + "glyphs": "https://data.geopf.fr/annexes/ressources/vectorTiles/fonts/{fontstack}/{range}.pbf", + "sprite": "data/poi-osm-sprite", + "metadata": { + "geoportail:tooltip": "BDTOPO/multicouche_bdtopo" + }, + "sources": { + "bdtopo": { + "type": "vector", + "maxzoom": 19, + "minzoom": 15, + "tiles": [ + "https://data.geopf.fr/tms/1.0.0/BDTOPO/{z}/{x}/{y}.pbf" + ] + } + }, + "transition": { + "duration": 300, + "delay": 0 + }, + "layers": [ + { + "id": "batiment_residentiel_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Résidentiel" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#F1EBD9" + } + }, + { + "id": "batiment_annexe_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Annexe" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#F1EBD9" + } + }, + { + "id": "batiment_agricole_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Agricole" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#E6E6E6" + } + }, + { + "id": "batiment_commercial_et_services_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Commercial et services" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#E3BFE2" + } + }, + { + "id": "batiment_industriel_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Industriel" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#E6E6E6" + } + }, + { + "id": "batiment_sportif_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Sportif" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#DCE6E4" + } + }, + { + "id": "batiment_religieux_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Religieux" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#F7E1E1" + } + }, + { + "id": "batiment_indifferencie_vol", + "type": "fill-extrusion", + "source": "bdtopo", + "source-layer": "batiment", + "minzoom": 16, + "filter": [ + "==", + "usage_1", + "Indifférencié" + ], + "paint": { + "fill-extrusion-height": [ + "get", "hauteur" + ], + "fill-extrusion-color": "#F1EBD9" + } + } + ] +} \ No newline at end of file From a2bf2405fdf66a581604d36c98331efd1bd3d1ce Mon Sep 17 00:00:00 2001 From: azarz Date: Thu, 27 Jun 2024 15:11:32 +0200 Subject: [PATCH 10/16] feat(3d): add terrain --- src/js/three-d.js | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/js/three-d.js b/src/js/three-d.js index 4f1cb647..4328acba 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -1,4 +1,11 @@ +/** + * Copyright (c) Institut national de l'information géographique et forestière + * + * This program and the accompanying materials are made available under the terms of the GPL License, Version 3.0. + */ + import Globals from "./globals"; +import maplibregl from "maplibre-gl"; let buildingsLayers = []; @@ -8,6 +15,63 @@ async function _fetch3dBuildingsLayers() { buildingsLayers = data.layers; } +// Function to fetch and parse x-bil tile data +async function _fetchAndParseXBil(url) { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + const dataView = new DataView(arrayBuffer); + const width = Math.sqrt(dataView.byteLength / 4); // Assuming square tiles + const height = width; + const elevations = new Float32Array(width * height); + for (let i = 0; i < width * height; i++) { + elevations[i] = dataView.getFloat32(i * 4, true); + } + return { elevations, width, height }; +} + +function add3dTerrain() { + if (!Globals.map.getSource("bil-terrain")) { + Globals.map.addSource("bil-terrain", { + type: "raster-dem", + tiles: [ + "dem://data.geopf.fr/wms-r/wms?bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES" + ], + minzoom: 6, + maxzoom: 14, + tileSize: 256 + }); + + maplibregl.addProtocol("dem", async (params, abortController) => { + try { + const { elevations, width, height } = await _fetchAndParseXBil(`https://${params.url.split("://")[1]}`); + const data = new Uint8ClampedArray(width * height * 4); + for (let i = 0; i < elevations.length; i++) { + const elevation = Math.round(elevations[i] * 10) / 10; + // reverse https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data + const baseElevationValue = 10 * (elevation + 10000); + const red = Math.floor(baseElevationValue / (256 * 256)) % 256; + const green = Math.floor((baseElevationValue - red * 256 * 256) / 256) % 256; + const blue = baseElevationValue - red * 256 * 256 - green * 256; + data[4 * i] = red; + data[4 * i + 1] = green; + data[4 * i + 2] = blue; + data[4 * i + 3] = 255; + } + const imageData = new ImageData(data, width, height); + const imageBitmap = await createImageBitmap(imageData); + return { + data: imageBitmap + }; + } catch (error) { + throw error; + } + }); + } + + // Set terrain using the custom source + Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); +} + async function add3dBuildings() { if (buildingsLayers.length === 0) { await _fetch3dBuildingsLayers(); @@ -33,4 +97,5 @@ function remove3dBuildings() { export default { add3dBuildings, remove3dBuildings, + add3dTerrain, } From dc6ef7a12f06c57ddb8bb059f0b421e668635be8 Mon Sep 17 00:00:00 2001 From: azarz Date: Thu, 4 Jul 2024 12:26:56 +0200 Subject: [PATCH 11/16] feat(3d): 3d now a global control --- src/js/controls.js | 4 + src/js/globals.js | 4 + src/js/services/location.js | 9 +- src/js/three-d.js | 192 +++++++++++++++++++++--------------- 4 files changed, 129 insertions(+), 80 deletions(-) diff --git a/src/js/controls.js b/src/js/controls.js index 01e24ff6..d4c6a9c4 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -22,6 +22,7 @@ import SignalementOSM from "./signalement-osm"; import Landmark from "./landmark"; import MapboxAccessibility from "./poi-accessibility"; import DOM from "./dom"; +import ThreeD from "./three-d"; import LocationLayers from "./services/location-styles"; import compareLandmark from "./compare-landmark"; @@ -133,6 +134,9 @@ const addControls = () => { }); Globals.compareLandmark = new compareLandmark(Globals.mapRLT1, Globals.mapRLT2, {}); + // 3d + Globals.threeD = new ThreeD(map, {}); + // contrôle filtres POI Globals.poi = new POI(map, {}); Globals.poi.load() // promise ! diff --git a/src/js/globals.js b/src/js/globals.js index f0161b84..aef779a3 100644 --- a/src/js/globals.js +++ b/src/js/globals.js @@ -98,6 +98,9 @@ let signalementOSM = null; let landmark = null; let compareLandmark = null; +// Global control 3d +let threeD = null; + // Global flag: is the device connected to the internet? let online = true; @@ -155,4 +158,5 @@ export default { osmPoiAccessibility, landmark, compareLandmark, + threeD, }; diff --git a/src/js/services/location.js b/src/js/services/location.js index ecd028f2..71bfbc5a 100644 --- a/src/js/services/location.js +++ b/src/js/services/location.js @@ -9,7 +9,6 @@ import maplibregl from "maplibre-gl"; import DOM from "../dom"; import Globals from "../globals"; import GisUtils from "../utils/gis-utils"; -import threeD from "../three-d"; import { Toast } from "@capacitor/toast"; import { ScreenOrientation } from "@capacitor/screen-orientation"; @@ -556,6 +555,7 @@ const disableTracking = () => { DOM.$geolocateBtn.classList.remove("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); tracking_active = false; +<<<<<<< HEAD if (navigation_active) { navigation_active = false; threeD.remove3dBuildings(); @@ -569,7 +569,14 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { DOM.$geolocateBtn.classList.add("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); navigation_active = false; +<<<<<<< HEAD Globals.map.setPadding({top: 0, right: 0, bottom: 0, left: 0}); +======= +======= + Globals.threeD.remove3dBuildings(); + Globals.threeD.remove3dTerrain(); +>>>>>>> 4617b4d (feat(3d): 3d now a global control) +>>>>>>> d8b8e79 (feat(3d): 3d now a global control) Globals.map.flyTo({ bearing: bearing, pitch: 0, diff --git a/src/js/three-d.js b/src/js/three-d.js index 4328acba..151416d4 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -7,95 +7,129 @@ import Globals from "./globals"; import maplibregl from "maplibre-gl"; -let buildingsLayers = []; +/** + * Interface sur le contrôle 3d + * @module ThreeD + */ +class ThreeD { + /** + * constructeur + * @constructs + * @param {*} map + * @param {*} options + */ + constructor(map, options) { + this.options = options || { + target: null, + // callback + openSearchControlCbk: null, + closeSearchControlCbk: null + }; -async function _fetch3dBuildingsLayers() { - const response = await fetch("data/bati-3d.json"); - const data = await response.json(); - buildingsLayers = data.layers; -} + this.map = map; -// Function to fetch and parse x-bil tile data -async function _fetchAndParseXBil(url) { - const response = await fetch(url); - const arrayBuffer = await response.arrayBuffer(); - const dataView = new DataView(arrayBuffer); - const width = Math.sqrt(dataView.byteLength / 4); // Assuming square tiles - const height = width; - const elevations = new Float32Array(width * height); - for (let i = 0; i < width * height; i++) { - elevations[i] = dataView.getFloat32(i * 4, true); + this.buildingsLayers = []; + + return this; } - return { elevations, width, height }; -} -function add3dTerrain() { - if (!Globals.map.getSource("bil-terrain")) { - Globals.map.addSource("bil-terrain", { - type: "raster-dem", - tiles: [ - "dem://data.geopf.fr/wms-r/wms?bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES" - ], - minzoom: 6, - maxzoom: 14, - tileSize: 256 - }); + async #fetch3dBuildingsLayers() { + if (!Globals.map.getSource("bdtopo")) { + Globals.map.addSource("bdtopo", { + "type": "vector", + "maxzoom": 19, + "minzoom": 15, + "tiles": [ + "https://data.geopf.fr/tms/1.0.0/BDTOPO/{z}/{x}/{y}.pbf" + ] + }); + } + const response = await fetch("data/bati-3d.json"); + const data = await response.json(); + this.buildingsLayers = data.layers; + } - maplibregl.addProtocol("dem", async (params, abortController) => { - try { - const { elevations, width, height } = await _fetchAndParseXBil(`https://${params.url.split("://")[1]}`); - const data = new Uint8ClampedArray(width * height * 4); - for (let i = 0; i < elevations.length; i++) { - const elevation = Math.round(elevations[i] * 10) / 10; - // reverse https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data - const baseElevationValue = 10 * (elevation + 10000); - const red = Math.floor(baseElevationValue / (256 * 256)) % 256; - const green = Math.floor((baseElevationValue - red * 256 * 256) / 256) % 256; - const blue = baseElevationValue - red * 256 * 256 - green * 256; - data[4 * i] = red; - data[4 * i + 1] = green; - data[4 * i + 2] = blue; - data[4 * i + 3] = 255; - } - const imageData = new ImageData(data, width, height); - const imageBitmap = await createImageBitmap(imageData); - return { - data: imageBitmap - }; - } catch (error) { - throw error; + // Function to fetch and parse x-bil tile data + async #fetchAndParseXBil(url) { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + const dataView = new DataView(arrayBuffer); + const width = Math.sqrt(dataView.byteLength / 4); // Assuming square tiles + const height = width; + const elevations = new Float32Array(width * height); + for (let i = 0; i < width * height; i++) { + elevations[i] = dataView.getFloat32(i * 4, true); } - }); + return { elevations, width, height }; } - // Set terrain using the custom source - Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); -} + add3dTerrain() { + if (!Globals.map.getSource("bil-terrain")) { + Globals.map.addSource("bil-terrain", { + type: "raster-dem", + tiles: [ + "dem://data.geopf.fr/wms-r/wms?bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES" + ], + minzoom: 6, + maxzoom: 14, + tileSize: 256 + }); -async function add3dBuildings() { - if (buildingsLayers.length === 0) { - await _fetch3dBuildingsLayers(); + maplibregl.addProtocol("dem", async (params, abortController) => { + try { + const { elevations, width, height } = await this.#fetchAndParseXBil(`https://${params.url.split("://")[1]}`); + const data = new Uint8ClampedArray(width * height * 4); + for (let i = 0; i < elevations.length; i++) { + const elevation = Math.round(elevations[i] * 10) / 10; + // reverse https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data + const baseElevationValue = 10 * (elevation + 10000); + const red = Math.floor(baseElevationValue / (256 * 256)) % 256; + const green = Math.floor((baseElevationValue - red * 256 * 256) / 256) % 256; + const blue = baseElevationValue - red * 256 * 256 - green * 256; + data[4 * i] = red; + data[4 * i + 1] = green; + data[4 * i + 2] = blue; + data[4 * i + 3] = 255; + } + const imageData = new ImageData(data, width, height); + const imageBitmap = await createImageBitmap(imageData); + return { + data: imageBitmap + }; + } catch (error) { + throw error; + } + }); + } + + // Set terrain using the custom source + Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); } - // HACK - // on positionne toujours le style avant ceux du calcul d'itineraires (directions) - // afin que le calcul soit toujours la couche visible du dessus ! - var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); - var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; - buildingsLayers.forEach((layer) => { - Globals.map.addLayer(layer, layerIdBefore); - }) - Globals.interactivityIndicator.hardDisable(); -} -function remove3dBuildings() { - buildingsLayers.forEach((layer) => { - Globals.map.removeLayer(layer.id); - }) - Globals.interactivityIndicator.enable(); -} + async add3dBuildings() { + if (this.buildingsLayers.length === 0) { + await this.#fetch3dBuildingsLayers(); + } + // HACK + // on positionne toujours le style avant ceux du calcul d'itineraires (directions) + // afin que le calcul soit toujours la couche visible du dessus ! + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; + this.buildingsLayers.forEach((layer) => { + Globals.map.addLayer(layer, layerIdBefore); + }); + } + + remove3dBuildings() { + this.buildingsLayers.forEach((layer) => { + Globals.map.removeLayer(layer.id); + }) + Globals.interactivityIndicator.enable(); + } -export default { - add3dBuildings, - remove3dBuildings, - add3dTerrain, + remove3dTerrain() { + Globals.map.setTerrain(); + } } + +export default ThreeD; From 7f1062cd88769619a05097deda71ccfbddc7bad3 Mon Sep 17 00:00:00 2001 From: azarz Date: Thu, 4 Jul 2024 14:36:29 +0200 Subject: [PATCH 12/16] feat(3d): add hillshade, sky. Now through map button and not navigation --- package-lock.json | 19 +++++++++---------- package.json | 2 +- src/css/map-buttons.css | 9 +++++++++ src/html/mapButtons.html | 2 ++ src/js/dom.js | 5 +++++ src/js/map-buttons-listeners.js | 23 +++++++++++++++++++++++ src/js/services/location.js | 16 +++++++++++++++- src/js/three-d.js | 21 ++++++++++++++++++++- 8 files changed, 84 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53bf9fa3..e2781183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "chart.js": "^4.4.1", "install": "^0.13.0", "lodash": "^4.17.21", - "maplibre-gl": "^4.3.2", + "maplibre-gl": "^4.5.0", "npm": "^10.5.0", "proj4": "^2.10.0" }, @@ -8440,10 +8440,9 @@ } }, "node_modules/geojson-vt": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", - "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", - "license": "ISC" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==" }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -10796,9 +10795,9 @@ } }, "node_modules/maplibre-gl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.3.2.tgz", - "integrity": "sha512-/oXDsb9I+LkjweL/28aFMLDZoIcXKNEhYNAZDLA4xgTNkfvKQmV/r0KZdxEMcVthincJzdyc6Y4N8YwZtHKNnQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.5.0.tgz", + "integrity": "sha512-qOS1hn4d/pn2i0uva4S5Oz+fACzTkgBKq+NpwT/Tqzi4MSyzcWNtDELzLUSgWqHfNIkGCl5CZ/w7dtis+t4RCw==", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -10807,7 +10806,7 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.2.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.0", "@types/geojson": "^7946.0.14", "@types/geojson-vt": "3.2.5", "@types/junit-report-builder": "^3.0.2", @@ -10816,7 +10815,7 @@ "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", "earcut": "^2.2.4", - "geojson-vt": "^3.2.1", + "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.3", "global-prefix": "^3.0.0", "kdbush": "^4.0.2", diff --git a/package.json b/package.json index 83470966..d20bcadc 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "chart.js": "^4.4.1", "install": "^0.13.0", "lodash": "^4.17.21", - "maplibre-gl": "^4.3.2", + "maplibre-gl": "^4.5.0", "npm": "^10.5.0", "proj4": "^2.10.0" }, diff --git a/src/css/map-buttons.css b/src/css/map-buttons.css index cb83cd07..29f3179b 100644 --- a/src/css/map-buttons.css +++ b/src/css/map-buttons.css @@ -194,6 +194,15 @@ bottom: 132px; } +#threeDBtn { + position: absolute; + top: calc(130px + var(--safe-area-inset-top)); + font-family: "Open Sans Semibold"; + display: flex; + align-items: center; + justify-content: center; +} + #filterPoiBtn { background-image: url("assets/map-buttons/filtrer.svg"); position: fixed; diff --git a/src/html/mapButtons.html b/src/html/mapButtons.html index 94901e12..623378b8 100644 --- a/src/html/mapButtons.html +++ b/src/html/mapButtons.html @@ -2,6 +2,8 @@
Filtres
+ +
3D
diff --git a/src/js/dom.js b/src/js/dom.js index b1c88824..3baa448c 100644 --- a/src/js/dom.js +++ b/src/js/dom.js @@ -21,6 +21,7 @@ const $selectOnMap = document.getElementById("selectOnMap"); const $geolocateBtn = document.getElementById("geolocateBtn"); const $backTopLeftBtn = document.getElementById("backTopLeftBtn"); const $compassBtn = document.getElementById("compassBtn"); +const $threeDBtn = document.getElementById("threeDBtn"); const $layerManagerBtn = document.getElementById("layerManagerBtn"); const $sideBySideBtn = document.getElementById("sideBySideBtn"); const $compareMode = document.getElementById("compareMode"); @@ -134,6 +135,10 @@ export default { $fullScreenBtn, $mapScale, $map, +<<<<<<< HEAD $createCompareLandmarkBtn, $compareLandmarkWindow, +======= + $threeDBtn, +>>>>>>> d1a4abc (feat(3d): add hillshade, sky. Now through map button and not navigation) }; diff --git a/src/js/map-buttons-listeners.js b/src/js/map-buttons-listeners.js index 04e92206..b3c36321 100644 --- a/src/js/map-buttons-listeners.js +++ b/src/js/map-buttons-listeners.js @@ -88,6 +88,29 @@ const addListeners = () => { Globals.compareLandmark.location = [Globals.mapRLT1.getCenter().lng, Globals.mapRLT1.getCenter().lat]; } }); + + // Toggle 3D + DOM.$threeDBtn.addEventListener("click", () => { + if (Globals.threeD.on) { + Globals.threeD.remove3dBuildings(); + Globals.threeD.remove3dTerrain(); + if (!Location.isTrackingActive()) { + Globals.map.flyTo({ + pitch: 0, + duration: 500, + }); + setTimeout( () => {Globals.map.setMaxPitch(0)}, 500); + } + } else { + Globals.map.setMaxPitch(80); + Globals.threeD.add3dBuildings(); + Globals.threeD.add3dTerrain(); + Globals.map.flyTo({ + pitch: 45, + duration: 500, + }); + } + }); }; export default { diff --git a/src/js/services/location.js b/src/js/services/location.js index 71bfbc5a..b28137dc 100644 --- a/src/js/services/location.js +++ b/src/js/services/location.js @@ -555,7 +555,6 @@ const disableTracking = () => { DOM.$geolocateBtn.classList.remove("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); tracking_active = false; -<<<<<<< HEAD if (navigation_active) { navigation_active = false; threeD.remove3dBuildings(); @@ -569,6 +568,7 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { DOM.$geolocateBtn.classList.add("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); navigation_active = false; +<<<<<<< HEAD <<<<<<< HEAD Globals.map.setPadding({top: 0, right: 0, bottom: 0, left: 0}); ======= @@ -584,6 +584,20 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { }); if (bearing === 0) { DOM.$compassBtn.classList.add("d-none"); +======= + Globals.threeD.remove3dBuildings(); + Globals.threeD.remove3dTerrain(); + if (!Globals.threeD.on) { + Globals.map.flyTo({ + pitch: 0, + bearing: bearing, + duration: 500, + }) + if (bearing === 0) { + DOM.$compassBtn.classList.add("d-none"); + } + setTimeout( () => {Globals.map.setMaxPitch(0)}, 500); +>>>>>>> bb1e8d8 (feat(3d): add hillshade, sky. Now through map button and not navigation) } }; diff --git a/src/js/three-d.js b/src/js/three-d.js index 151416d4..ddc0e3df 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -7,6 +7,14 @@ import Globals from "./globals"; import maplibregl from "maplibre-gl"; +const hillsLayer = { + id: "hills", + type: "hillshade", + source: "bil-terrain", + layout: {visibility: "visible"}, + paint: {"hillshade-shadow-color": "#473B24"} +} + /** * Interface sur le contrôle 3d * @module ThreeD @@ -30,6 +38,8 @@ class ThreeD { this.buildingsLayers = []; + this.on = false; + return this; } @@ -104,6 +114,12 @@ class ThreeD { // Set terrain using the custom source Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); + Globals.map.setSky({ + "sky-color": "#199EF3", + "fog-ground-blend": 0.8, + }); + Globals.map.addLayer(hillsLayer); + this.on = true; } async add3dBuildings() { @@ -118,17 +134,20 @@ class ThreeD { this.buildingsLayers.forEach((layer) => { Globals.map.addLayer(layer, layerIdBefore); }); + this.on = true; } remove3dBuildings() { this.buildingsLayers.forEach((layer) => { Globals.map.removeLayer(layer.id); }) - Globals.interactivityIndicator.enable(); + this.on = false; } remove3dTerrain() { Globals.map.setTerrain(); + Globals.map.removeLayer(hillsLayer.id); + this.on = false; } } From ecc0420abca03d926573e968d50de4665398db06 Mon Sep 17 00:00:00 2001 From: azarz Date: Thu, 4 Jul 2024 15:54:57 +0200 Subject: [PATCH 13/16] feat(elevation): ignore nodata --- src/js/three-d.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/js/three-d.js b/src/js/three-d.js index ddc0e3df..03ef7fd7 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -69,6 +69,9 @@ class ThreeD { const elevations = new Float32Array(width * height); for (let i = 0; i < width * height; i++) { elevations[i] = dataView.getFloat32(i * 4, true); + if (elevations[i] < 100) { + elevations[i] = 0; + } } return { elevations, width, height }; } From 896cb9fee0c7964e4e2494c037be02dca01ae697 Mon Sep 17 00:00:00 2001 From: "amaury.zarzelli" Date: Wed, 10 Jul 2024 11:04:05 +0200 Subject: [PATCH 14/16] new terrain layer, better resampling --- src/js/dom.js | 3 --- src/js/services/location.js | 25 +------------------------ src/js/three-d.js | 13 +++++++++---- 3 files changed, 10 insertions(+), 31 deletions(-) diff --git a/src/js/dom.js b/src/js/dom.js index 3baa448c..e590bdc8 100644 --- a/src/js/dom.js +++ b/src/js/dom.js @@ -135,10 +135,7 @@ export default { $fullScreenBtn, $mapScale, $map, -<<<<<<< HEAD $createCompareLandmarkBtn, $compareLandmarkWindow, -======= $threeDBtn, ->>>>>>> d1a4abc (feat(3d): add hillshade, sky. Now through map button and not navigation) }; diff --git a/src/js/services/location.js b/src/js/services/location.js index b28137dc..e6c2402a 100644 --- a/src/js/services/location.js +++ b/src/js/services/location.js @@ -497,7 +497,7 @@ const getOrientation = async (event) => { mapBearing = tempMapBearing; if (navigation_active) { if (!isMapPanning) { - Globals.map.easeTo({bearing: -mapBearing, pitch: 45, duration: 100}); + Globals.map.easeTo({bearing: -mapBearing, duration: 100}); } DOM.$compassBtn.classList.remove("d-none"); DOM.$compassBtn.style.transform = "rotate(" + mapBearing + "deg)"; @@ -557,7 +557,6 @@ const disableTracking = () => { tracking_active = false; if (navigation_active) { navigation_active = false; - threeD.remove3dBuildings(); } Globals.map.touchZoomRotate.enable(); Globals.map.getCanvasContainer().removeEventListener("touchstart", locationOnTouchStartHandler); @@ -568,15 +567,7 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { DOM.$geolocateBtn.classList.add("locationFixe"); DOM.$geolocateBtn.classList.remove("locationFollow"); navigation_active = false; -<<<<<<< HEAD -<<<<<<< HEAD Globals.map.setPadding({top: 0, right: 0, bottom: 0, left: 0}); -======= -======= - Globals.threeD.remove3dBuildings(); - Globals.threeD.remove3dTerrain(); ->>>>>>> 4617b4d (feat(3d): 3d now a global control) ->>>>>>> d8b8e79 (feat(3d): 3d now a global control) Globals.map.flyTo({ bearing: bearing, pitch: 0, @@ -584,20 +575,6 @@ const disableNavigation = (bearing = Globals.map.getBearing()) => { }); if (bearing === 0) { DOM.$compassBtn.classList.add("d-none"); -======= - Globals.threeD.remove3dBuildings(); - Globals.threeD.remove3dTerrain(); - if (!Globals.threeD.on) { - Globals.map.flyTo({ - pitch: 0, - bearing: bearing, - duration: 500, - }) - if (bearing === 0) { - DOM.$compassBtn.classList.add("d-none"); - } - setTimeout( () => {Globals.map.setMaxPitch(0)}, 500); ->>>>>>> bb1e8d8 (feat(3d): add hillshade, sky. Now through map button and not navigation) } }; diff --git a/src/js/three-d.js b/src/js/three-d.js index 03ef7fd7..51d6e0a5 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -69,7 +69,7 @@ class ThreeD { const elevations = new Float32Array(width * height); for (let i = 0; i < width * height; i++) { elevations[i] = dataView.getFloat32(i * 4, true); - if (elevations[i] < 100) { + if (elevations[i] < -10 || elevations[i] > 4900) { elevations[i] = 0; } } @@ -81,7 +81,7 @@ class ThreeD { Globals.map.addSource("bil-terrain", { type: "raster-dem", tiles: [ - "dem://data.geopf.fr/wms-r/wms?bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES" + `dem://data.geopf.fr/private/wms-r/wms?apikey=${process.env.GPF_key}&bbox={bbox-epsg-3857}&format=image/x-bil;bits=32&service=WMS&version=1.3.0&request=GetMap&crs=EPSG:3857&width=256&height=256&styles=normal&layers=ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES.LINEAR` ], minzoom: 6, maxzoom: 14, @@ -93,7 +93,7 @@ class ThreeD { const { elevations, width, height } = await this.#fetchAndParseXBil(`https://${params.url.split("://")[1]}`); const data = new Uint8ClampedArray(width * height * 4); for (let i = 0; i < elevations.length; i++) { - const elevation = Math.round(elevations[i] * 10) / 10; + let elevation = Math.round(elevations[i] * 10) / 10; // reverse https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data const baseElevationValue = 10 * (elevation + 10000); const red = Math.floor(baseElevationValue / (256 * 256)) % 256; @@ -121,7 +121,12 @@ class ThreeD { "sky-color": "#199EF3", "fog-ground-blend": 0.8, }); - Globals.map.addLayer(hillsLayer); + // HACK + // on positionne toujours le style avant ceux du calcul d'itineraires (directions) + // afin que le calcul soit toujours la couche visible du dessus ! + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; + Globals.map.addLayer(hillsLayer, layerIdBefore); this.on = true; } From 8defc2757a8181f9faefb631f10bda390e6fdb48 Mon Sep 17 00:00:00 2001 From: "amaury.zarzelli" Date: Tue, 14 Jan 2025 17:30:51 +0100 Subject: [PATCH 15/16] feat: move 3d to layer catalogue instead of button --- src/css/map-buttons.css | 9 --- src/html/img/layers/3D.BUILDINGS.jpg | Bin 0 -> 25663 bytes src/html/img/layers/3D.TERRAIN.jpg | Bin 0 -> 27965 bytes src/html/mapButtons.html | 2 - src/js/controls.js | 1 + src/js/index.js | 2 +- src/js/layer-manager/layer-catalogue.js | 101 ++++++++++++++++++++++++ src/js/layer-manager/layer-switcher.js | 6 +- src/js/map-buttons-listeners.js | 22 ------ src/js/three-d.js | 42 ++++++---- 10 files changed, 133 insertions(+), 52 deletions(-) create mode 100644 src/html/img/layers/3D.BUILDINGS.jpg create mode 100644 src/html/img/layers/3D.TERRAIN.jpg diff --git a/src/css/map-buttons.css b/src/css/map-buttons.css index 29f3179b..cb83cd07 100644 --- a/src/css/map-buttons.css +++ b/src/css/map-buttons.css @@ -194,15 +194,6 @@ bottom: 132px; } -#threeDBtn { - position: absolute; - top: calc(130px + var(--safe-area-inset-top)); - font-family: "Open Sans Semibold"; - display: flex; - align-items: center; - justify-content: center; -} - #filterPoiBtn { background-image: url("assets/map-buttons/filtrer.svg"); position: fixed; diff --git a/src/html/img/layers/3D.BUILDINGS.jpg b/src/html/img/layers/3D.BUILDINGS.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5cf8e775afd5183b8c3cbe44abbfd3adcabd979b GIT binary patch literal 25663 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 literal 0 HcmV?d00001 diff --git a/src/html/img/layers/3D.TERRAIN.jpg b/src/html/img/layers/3D.TERRAIN.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b14641b8f155a604e46d7efde3ea3f82d39b8ea3 GIT binary patch literal 27965 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 qY#Q#O{(K?d05Oqh$ge_#KcO3lt@P7aYQAKtD literal 0 HcmV?d00001 diff --git a/src/html/mapButtons.html b/src/html/mapButtons.html index 623378b8..94901e12 100644 --- a/src/html/mapButtons.html +++ b/src/html/mapButtons.html @@ -2,8 +2,6 @@

Filtres
- -
3D
diff --git a/src/js/controls.js b/src/js/controls.js index d4c6a9c4..b793d91e 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -136,6 +136,7 @@ const addControls = () => { // 3d Globals.threeD = new ThreeD(map, {}); + Globals.manager.layerCatalogue.add3DThematicLayers(); // contrôle filtres POI Globals.poi = new POI(map, {}); diff --git a/src/js/index.js b/src/js/index.js index c37d03cb..4d6869cc 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -194,7 +194,7 @@ function app() { attributionControl: false, maxZoom: 21, locale: "fr", - maxPitch: 45, + maxPitch: 60, crossSourceCollisions: false, }); diff --git a/src/js/layer-manager/layer-catalogue.js b/src/js/layer-manager/layer-catalogue.js index 8314b300..4c42ce02 100644 --- a/src/js/layer-manager/layer-catalogue.js +++ b/src/js/layer-manager/layer-catalogue.js @@ -9,6 +9,8 @@ import LayersConfig from "./layer-config"; import LayersAdditional from "./layer-additional"; import ImageNotFound from "../../html/img/image-not-found.png"; +import ReliefBuildingsImage from "../../html/img/layers/3D.BUILDINGS.jpg"; +import ReliefTerrainImage from "../../html/img/layers/3D.TERRAIN.jpg"; import DomUtils from "../utils/dom-utils"; import { Toast } from "@capacitor/toast"; @@ -153,6 +155,105 @@ class LayerCatalogue extends EventTarget { target.appendChild(container); } + /** + * Ajout de 2 faux "Layers" 3D qui n'apparaissent pas dans le Layer Switcher et du bouton 3D pour les filtrer + * - Bâtiments 3D + * - Relief + */ + add3DThematicLayers() { + var target = this.options.target || document.getElementById("layer-thematics"); + if (!target) { + console.warn(); + return; + } + var container = target.querySelector("#thematicLayers"); + + var buildings3DLayerHtml = ` +
+
+ Bâtiments 3D +
+
+
+
3D
+
Bâtiments 3D
+
+ `; + var buildings3DLayerElement = DomUtils.stringToHTML(buildings3DLayerHtml.trim()); + buildings3DLayerElement.addEventListener("click", () => { + if (buildings3DLayerElement.classList.contains("selectedLayer")) { + Globals.threeD.remove3dBuildings(); + buildings3DLayerElement.classList.remove("selectedLayer"); + } else { + Globals.threeD.add3dBuildings(); + buildings3DLayerElement.classList.add("selectedLayer"); + } + }); + container.appendChild(buildings3DLayerElement); + + var terrainLayerHtml = ` +
+
+ Relief 3D +
+
+
+
3D
+
Relief 3D
+
+ `; + var terrainLayerElement = DomUtils.stringToHTML(terrainLayerHtml.trim()); + terrainLayerElement.addEventListener("click", () => { + if (terrainLayerElement.classList.contains("selectedLayer")) { + Globals.threeD.remove3dTerrain(); + terrainLayerElement.classList.remove("selectedLayer"); + } else { + Globals.threeD.add3dTerrain(); + terrainLayerElement.classList.add("selectedLayer"); + } + }); + container.appendChild(terrainLayerElement); + + // Ajout de la pastille de filtre "3D" + var buttonsContainer = target.querySelector("#thematicButtons"); + var buttonElement = DomUtils.stringToHTML(` + + `.trim()); + + buttonElement.addEventListener("click", (e) => { + var buttons = document.querySelectorAll(".thematicButton"); + for (let h = 0; h < buttons.length; h++) { + const element = buttons[h]; + element.classList.remove("thematic-button-active"); + } + var layers = document.querySelectorAll(".thematicLayer"); + for (let i = 0; i < layers.length; i++) { + const element = layers[i]; + element.classList.add("layer-hidden"); + } + var layersId = ["3D.BUILDINGS", "3D.TERRAIN"]; + for (let j = 0; j < layersId.length; j++) { + const id = layersId[j]; + var element = document.getElementById(id); + element.classList.remove("layer-hidden"); + } + e.target.classList.add("thematic-button-active"); + }); + buttonsContainer.firstElementChild.after(buttonElement); + + // Ajoute le réaffichage des boutons 3D au click sur la pastille "Tous" + buttonsContainer.querySelector("[data-name=Tous]").addEventListener("click", () => { + var layersId = ["3D.BUILDINGS", "3D.TERRAIN"]; + for (let j = 0; j < layersId.length; j++) { + const id = layersId[j]; + var element = document.getElementById(id); + element.classList.remove("layer-hidden"); + } + }); + } + /** * Ecouteurs */ diff --git a/src/js/layer-manager/layer-switcher.js b/src/js/layer-manager/layer-switcher.js index b5e577c3..af1ce16b 100644 --- a/src/js/layer-manager/layer-switcher.js +++ b/src/js/layer-manager/layer-switcher.js @@ -622,7 +622,7 @@ class LayerSwitcher extends EventTarget { this.layers[id].style = data_2.layers; // sauvegarde ! } catch (e) { if (fallback) { - fetchStyle(fallback, null); + await fetchStyle(fallback, null); } else { this.layers[id].error = true; throw new Error(e); @@ -680,6 +680,10 @@ class LayerSwitcher extends EventTarget { this.#setColor(id, !layerOptions.gray); } this.#setVisibility(id, layerOptions.visible); + // Cas particulier : ajout de l'ombrage à plan IGN si la 3D est activée + if (id === "PLAN.IGN.INTERACTIF$GEOPORTAIL:GPP:TMS" && Globals.threeD && Globals.threeD.terrainOn) { + Globals.threeD.addHillShadeToPlanIgn(); + } /** * Evenement "addlayer" * @event addlayer diff --git a/src/js/map-buttons-listeners.js b/src/js/map-buttons-listeners.js index b3c36321..9f34c929 100644 --- a/src/js/map-buttons-listeners.js +++ b/src/js/map-buttons-listeners.js @@ -89,28 +89,6 @@ const addListeners = () => { } }); - // Toggle 3D - DOM.$threeDBtn.addEventListener("click", () => { - if (Globals.threeD.on) { - Globals.threeD.remove3dBuildings(); - Globals.threeD.remove3dTerrain(); - if (!Location.isTrackingActive()) { - Globals.map.flyTo({ - pitch: 0, - duration: 500, - }); - setTimeout( () => {Globals.map.setMaxPitch(0)}, 500); - } - } else { - Globals.map.setMaxPitch(80); - Globals.threeD.add3dBuildings(); - Globals.threeD.add3dTerrain(); - Globals.map.flyTo({ - pitch: 45, - duration: 500, - }); - } - }); }; export default { diff --git a/src/js/three-d.js b/src/js/three-d.js index 51d6e0a5..3b0f71ec 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -12,7 +12,8 @@ const hillsLayer = { type: "hillshade", source: "bil-terrain", layout: {visibility: "visible"}, - paint: {"hillshade-shadow-color": "#473B24"} + paint: {"hillshade-shadow-color": "#473B24"}, + metadata: {group: "PLAN.IGN.INTERACTIF$GEOPORTAIL:GPP:TMS"}, } /** @@ -37,8 +38,7 @@ class ThreeD { this.map = map; this.buildingsLayers = []; - - this.on = false; + this.terrainOn = false; return this; } @@ -117,17 +117,25 @@ class ThreeD { // Set terrain using the custom source Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); - Globals.map.setSky({ - "sky-color": "#199EF3", - "fog-ground-blend": 0.8, - }); + this.addHillShadeToPlanIgn(); + this.terrainOn = true; + } + + addHillShadeToPlanIgn() { + if (Globals.map.getLayer(hillsLayer.id)) { + return; + } // HACK - // on positionne toujours le style avant ceux du calcul d'itineraires (directions) - // afin que le calcul soit toujours la couche visible du dessus ! - var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + // on positionne toujours le layer après la dernière couche de PLAN IGN + var beforeId = "detail_hydrographique$$$PLAN.IGN.INTERACTIF$GEOPORTAIL:GPP:TMS"; + if (!Globals.map.getLayer(beforeId)) { + return; + } + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.id === beforeId) + 1; var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; - Globals.map.addLayer(hillsLayer, layerIdBefore); - this.on = true; + if (layerIdBefore) { + Globals.map.addLayer(hillsLayer, layerIdBefore); + } } async add3dBuildings() { @@ -137,25 +145,25 @@ class ThreeD { // HACK // on positionne toujours le style avant ceux du calcul d'itineraires (directions) // afin que le calcul soit toujours la couche visible du dessus ! - var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions"); + var layerIndexBefore = Globals.map.getStyle().layers.findIndex((l) => l.source === "maplibre-gl-directions") + 1; var layerIdBefore = (layerIndexBefore !== -1) ? Globals.map.getStyle().layers[layerIndexBefore].id : null; this.buildingsLayers.forEach((layer) => { Globals.map.addLayer(layer, layerIdBefore); }); - this.on = true; } remove3dBuildings() { this.buildingsLayers.forEach((layer) => { Globals.map.removeLayer(layer.id); }) - this.on = false; } remove3dTerrain() { Globals.map.setTerrain(); - Globals.map.removeLayer(hillsLayer.id); - this.on = false; + if (Globals.map.getLayer(hillsLayer.id)) { + Globals.map.removeLayer(hillsLayer.id); + } + this.terrainOn = false; } } From 5fb48bf803ab92ddaa420b117fe845f70a30e9bd Mon Sep 17 00:00:00 2001 From: azarz Date: Fri, 17 Jan 2025 11:51:40 +0100 Subject: [PATCH 16/16] lint --- src/js/layer-manager/layer-catalogue.js | 59 ------------------------- src/js/three-d.js | 15 ++++--- 2 files changed, 8 insertions(+), 66 deletions(-) diff --git a/src/js/layer-manager/layer-catalogue.js b/src/js/layer-manager/layer-catalogue.js index 80c849ab..4c42ce02 100644 --- a/src/js/layer-manager/layer-catalogue.js +++ b/src/js/layer-manager/layer-catalogue.js @@ -155,65 +155,6 @@ class LayerCatalogue extends EventTarget { target.appendChild(container); } - add3DThematicLayers() { - var target = this.options.target || document.getElementById("layer-thematics"); - if (!target) { - console.warn(); - return; - } - var container = target.querySelector("#thematicLayers"); - - var buildings3DLayerHtml = ` -
-
- Bâtiments 3D -
-
-
-
3D
-
Bâtiments 3D
-
- `; - - var buildings3DLayerElement = DomUtils.stringToHTML(buildings3DLayerHtml.trim()); - buildings3DLayerElement.addEventListener("click", () => { - if (buildings3DLayerElement.classList.contains("selectedLayer")) { - Globals.threeD.remove3dBuildings(); - buildings3DLayerElement.classList.remove("selectedLayer"); - } else { - Globals.threeD.add3dBuildings(); - buildings3DLayerElement.classList.add("selectedLayer"); - } - }); - - container.appendChild(buildings3DLayerElement); - - var terrainLayerHtml = ` -
-
- Relief 3D -
-
-
-
3D
-
Relief 3D
-
- `; - - var terrainLayerElement = DomUtils.stringToHTML(terrainLayerHtml.trim()); - terrainLayerElement.addEventListener("click", () => { - if (terrainLayerElement.classList.contains("selectedLayer")) { - Globals.threeD.remove3dTerrain(); - terrainLayerElement.classList.remove("selectedLayer"); - } else { - Globals.threeD.add3dTerrain(); - terrainLayerElement.classList.add("selectedLayer"); - } - }); - - container.appendChild(terrainLayerElement); - } - /** * Ajout de 2 faux "Layers" 3D qui n'apparaissent pas dans le Layer Switcher et du bouton 3D pour les filtrer * - Bâtiments 3D diff --git a/src/js/three-d.js b/src/js/three-d.js index 3b0f71ec..565ecd37 100644 --- a/src/js/three-d.js +++ b/src/js/three-d.js @@ -14,7 +14,7 @@ const hillsLayer = { layout: {visibility: "visible"}, paint: {"hillshade-shadow-color": "#473B24"}, metadata: {group: "PLAN.IGN.INTERACTIF$GEOPORTAIL:GPP:TMS"}, -} +}; /** * Interface sur le contrôle 3d @@ -88,7 +88,7 @@ class ThreeD { tileSize: 256 }); - maplibregl.addProtocol("dem", async (params, abortController) => { + maplibregl.addProtocol("dem", async (params) => { try { const { elevations, width, height } = await this.#fetchAndParseXBil(`https://${params.url.split("://")[1]}`); const data = new Uint8ClampedArray(width * height * 4); @@ -109,14 +109,15 @@ class ThreeD { return { data: imageBitmap }; - } catch (error) { - throw error; - } + } catch (error) { + console.error(error); + throw error; + } }); } // Set terrain using the custom source - Globals.map.setTerrain({ source: 'bil-terrain', exaggeration: 1.5 }); + Globals.map.setTerrain({ source: "bil-terrain", exaggeration: 1.5 }); this.addHillShadeToPlanIgn(); this.terrainOn = true; } @@ -155,7 +156,7 @@ class ThreeD { remove3dBuildings() { this.buildingsLayers.forEach((layer) => { Globals.map.removeLayer(layer.id); - }) + }); } remove3dTerrain() {