Skip to content

Commit a3b788f

Browse files
committed
Feat/immersive position (#139)
1 parent 55a5d11 commit a3b788f

File tree

4 files changed

+234
-18
lines changed

4 files changed

+234
-18
lines changed

src/js/immersive-position.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* Copyright (c) Institut national de l'information géographique et forestière
3+
*
4+
* This program and the accompanying materials are made available under the terms of the GPL License, Version 3.0.
5+
*/
6+
7+
import proj4 from "proj4";
8+
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");
9+
10+
const queryConfig = [
11+
{
12+
layer: "LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:commune",
13+
attributes: ["nom", "population"],
14+
},
15+
{
16+
layer: "LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:departement",
17+
attributes: ["nom"],
18+
},
19+
{
20+
layer: "BDTOPO_V3:parc_ou_reserve",
21+
attributes: ["nature", "toponyme"],
22+
geom_name: "geometrie",
23+
},
24+
{
25+
layer: "BDTOPO_V3:foret_publique",
26+
attributes: ["toponyme"],
27+
geom_name: "geometrie",
28+
around: 5,
29+
},
30+
{
31+
layer: "BDTOPO_V3:toponymie_lieux_nommes",
32+
attributes: ["graphie_du_toponyme"],
33+
geom_name: "geometrie",
34+
around: 5,
35+
additional_cql: "AND nature_de_l_objet='Bois'",
36+
},
37+
{
38+
layer: "LANDCOVER.FORESTINVENTORY.V2:formation_vegetale",
39+
attributes: ["essence"],
40+
around: 5,
41+
epsg: 2154,
42+
},
43+
{
44+
layer: "RPG.LATEST:parcelles_graphiques",
45+
attributes: ["code_cultu"],
46+
around: 5,
47+
},
48+
{
49+
layer: "BDTOPO_V3:zone_d_activite_ou_d_interet",
50+
attributes: ["nature", "toponyme"],
51+
geom_name: "geometrie",
52+
around: 5,
53+
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')",
54+
},
55+
{
56+
layer: "BDTOPO_V3:cours_d_eau",
57+
attributes: ["toponyme"],
58+
geom_name: "geometrie",
59+
around: 5,
60+
},
61+
{
62+
layer: "BDTOPO_V3:plan_d_eau",
63+
attributes: ["nature", "toponyme"],
64+
geom_name: "geometrie",
65+
around: 5,
66+
additional_cql: "AND nature IN ('Canal', 'Estuaire', 'Camping', 'Glacier, névé', 'Lac', 'Lagune', 'Mangrove', 'Marais', 'Mare')",
67+
},
68+
];
69+
70+
/**
71+
* Gestion de la "position immersive" avec des requêtes faites aux données autour d'une position
72+
* @fires dataLoaded
73+
*/
74+
class ImmersivePosion extends EventTarget {
75+
/**
76+
* constructeur
77+
* @param {*} options -
78+
* @param {*} options.lat - latitude
79+
* @param {*} options.lng - longitude
80+
*/
81+
constructor(options) {
82+
super();
83+
this.options = options || {
84+
lat : 0,
85+
lng : 0,
86+
};
87+
this.lat = this.options.lat;
88+
this.lng = this.options.lng;
89+
90+
this.data = {};
91+
92+
// récupération des codes culture pour RPG
93+
this.codes_culture = {};
94+
fetch(
95+
"https://data.geopf.fr/wfs/ows?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&typename=RPG.LATEST:codes_cultures&outputFormat=json&count=1000"
96+
).then((resp) => resp.json()).then( (resp) => {
97+
resp.features.forEach((feature) => {
98+
this.codes_culture[feature.properties.code] = feature.properties.libelle;
99+
});
100+
});
101+
}
102+
103+
/**
104+
* Computes html string from availmable data
105+
*/
106+
computeHtml() {
107+
const htmlTemplate = `
108+
<p>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</p>
109+
<p>Département : ${this.data["LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:departement"] ? this.data["LIMITES_ADMINISTRATIVES_EXPRESS.LATEST:departement"][0] : "chargement..."}</p>
110+
<p>Parcs naturels : ${this.data["BDTOPO_V3:parc_ou_reserve"] ? JSON.stringify(this.data["BDTOPO_V3:parc_ou_reserve"]) : "aucun"}</p>
111+
<p>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"]) : "..."}</p>
112+
<p>Essence principale : ${this.data["LANDCOVER.FORESTINVENTORY.V2:formation_vegetale"] ? JSON.stringify(this.data["LANDCOVER.FORESTINVENTORY.V2:formation_vegetale"]) : "..."}</p>
113+
<p>Cultures : ${this.data["RPG.LATEST:parcelles_graphiques"] ? JSON.stringify(this.data["RPG.LATEST:parcelles_graphiques"]) : "..."}</p>
114+
<p>ZAI loisirs : ${this.data["BDTOPO_V3:zone_d_activite_ou_d_interet"] ? JSON.stringify(this.data["BDTOPO_V3:zone_d_activite_ou_d_interet"]) : "..."}</p>
115+
<p>Cours d'eau : ${this.data["BDTOPO_V3:cours_d_eau"] ? JSON.stringify(this.data["BDTOPO_V3:cours_d_eau"]) : "..."}</p>
116+
<p>Plans d'eau : ${this.data["BDTOPO_V3:plan_d_eau"] ? JSON.stringify(this.data["BDTOPO_V3:plan_d_eau"]) : "..."}</p>
117+
`;
118+
return htmlTemplate;
119+
}
120+
121+
/**
122+
* Computes all data queries
123+
*/
124+
computeAll() {
125+
queryConfig.forEach( (config) => {
126+
this.#computeFromConfig(config);
127+
});
128+
}
129+
130+
/**
131+
* Queries GPF's WFS for info defined in the config
132+
*/
133+
async #computeFromConfig(config) {
134+
const result = await this.#computeGenericGPFWFS(
135+
config.layer,
136+
config.attributes,
137+
config.around || 0,
138+
config.geom_name || "geom",
139+
config.additional_cql || "",
140+
config.epsg || 4326,
141+
);
142+
143+
this.data[config.layer] = result;
144+
145+
this.dispatchEvent(
146+
new CustomEvent("dataLoaded", {
147+
bubbles: true,
148+
})
149+
);
150+
}
151+
152+
/**
153+
* Computes data for a given layer of Geoplateforme's WFS
154+
* @param {string} layer name of the WFS layer
155+
* @param {Array} attributes list of strings of the relevant attributes to return
156+
* @param {number} around distance around the point in km for the query, default 0
157+
* @param {string} geom_name name of the geometry column, default "geom"
158+
* @param {string} additional_cql cql filter needed other than geometry, e.g. "AND nature_de_l_objet='Bois'", default ""
159+
* @param {number} epsg epsg number of the layer's CRS, default 4326
160+
*/
161+
async #computeGenericGPFWFS(layer, attributes, around=0, geom_name="geom", additional_cql="", epsg=4326) {
162+
let coord1 = this.lat;
163+
let coord2 = this.lng;
164+
if (epsg !== 4326) {
165+
[coord1, coord2] = proj4(proj4.defs("EPSG:4326"), proj4.defs(`EPSG:${epsg}`), [this.lng, this.lat]);
166+
}
167+
let cql_filter = `INTERSECTS(${geom_name},Point(${coord1}%20${coord2}))`;
168+
if (around > 0) {
169+
cql_filter = `DWITHIN(${geom_name},Point(${coord1}%20${coord2}),${around},kilometers)`;
170+
}
171+
if (additional_cql) {
172+
cql_filter += ` ${additional_cql}`;
173+
}
174+
175+
const results = await fetch(
176+
`https://data.geopf.fr/wfs/ows?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&typename=${layer}&outputFormat=json&count=10&CQL_FILTER=${cql_filter}`
177+
);
178+
const json = await results.json();
179+
180+
const results_attributes = [];
181+
json.features.forEach((feature) => {
182+
const feature_attributes = [];
183+
attributes.forEach((attribute) => {
184+
// Cas particulier du RPG : décodage de la culture en libellé
185+
if (layer === "RPG.LATEST:parcelles_graphiques" && attribute === "code_cultu" && Object.keys(this.codes_culture).length) {
186+
feature.properties[attribute] = this.codes_culture[feature.properties[attribute]];
187+
}
188+
feature_attributes.push(feature.properties[attribute]);
189+
});
190+
if (attributes.length === 1) {
191+
results_attributes.push(feature_attributes[0]);
192+
} else {
193+
results_attributes.push(feature_attributes);
194+
}
195+
});
196+
return Array.from( new Set(results_attributes) );
197+
}
198+
}
199+
200+
export default ImmersivePosion;

src/js/layer-manager/layer-manager.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,24 @@ import LayersConfig from "./layer-config";
4444
*/
4545
class LayerManager extends EventTarget {
4646
/**
47-
* constructeur
48-
* @param {*} options -
49-
* @param {*} options.target - ...
50-
* @param {*} options.layers - ...
51-
* @example
52-
* new LayerManger({
53-
* layers : [
54-
* layers : "couche1, couche2, ...",
55-
* type : "base" // data ou thematic
56-
* ]
57-
* });
58-
*/
47+
* constructeur
48+
* @param {*} options -
49+
* @param {*} options.target - ...
50+
* @param {*} options.layers - ...
51+
* @example
52+
* new LayerManger({
53+
* layers : [
54+
* layers : "couche1, couche2, ...",
55+
* type : "base" // data ou thematic
56+
* ]
57+
* });
58+
*/
5959
constructor(options) {
6060
super();
6161
this.options = options || {
6262
/**
63-
* ["layerid", "layer2id"]
64-
*/
63+
* ["layerid", "layer2id"]
64+
*/
6565
layers : [],
6666
target : null
6767
};

src/js/map-listeners.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const addListeners = () => {
6161
if (Globals.backButtonState.split("-")[0] === "position") {
6262
Globals.menu.close("position");
6363
}
64-
Globals.position.compute({ lngLat: evt.lngLat }).then(() => {
64+
Globals.position.compute({ lngLat: evt.lngLat, type: "context" }).then(() => {
6565
Globals.menu.open("position");
6666
});
6767
Globals.searchResultMarker = new maplibregl.Marker({element: Globals.searchResultIcon, anchor: "bottom"})

src/js/position.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import ActionSheet from "./action-sheet";
1818
import PopupUtils from "./utils/popup-utils";
1919

2020
import LoadingDark from "../css/assets/loading-darkgrey.svg";
21+
import ImmersivePosion from "./immersive-position";
2122

2223
/**
2324
* Permet d'afficher ma position sur la carte
@@ -83,6 +84,8 @@ class Position {
8384
popup: null
8485
};
8586

87+
this.immersivePosition = null;
88+
8689
return this;
8790
}
8891

@@ -414,13 +417,13 @@ class Position {
414417
* @param {string} options.html html situé avant les boutons d'action
415418
* @param {string} options.html2 html situé après les boutons d'action
416419
* @param {Function} options.hideCallback fonction de callback pour la fermeture de la position (pour les animations)
417-
* @param {string} options.type type de position : default, myposition ou landmark
420+
* @param {string} options.type type de position : default, context, myposition ou landmark
418421
* @public
419422
*/
420423
async compute(options = {}) {
421424
const lngLat = options.lngLat || false;
422425
const text = options.text || "Repère placé";
423-
const html = options.html || "";
426+
let html = options.html || "";
424427
const html2 = options.html2 || "";
425428
const hideCallback = options.hideCallback || null;
426429
const type = options.type || "default";
@@ -441,6 +444,11 @@ class Position {
441444
text: text
442445
};
443446
}
447+
this.coordinates = position.coordinates;
448+
if (type === "myposition" || type === "context") {
449+
this.immersivePosition = new ImmersivePosion({lat: this.coordinates.lat, lng: this.coordinates.lon});
450+
html = `<div id="immersivePostionHtmlBefore">${this.immersivePosition.computeHtml()}</div>`;
451+
}
444452

445453
this.header = position.text;
446454
try {
@@ -457,7 +465,6 @@ class Position {
457465
this.additionalHtml.beforeButtons = html;
458466
this.additionalHtml.afterButtons = html2;
459467

460-
this.coordinates = position.coordinates;
461468
this.address = Reverse.getAddress() || {
462469
number: "",
463470
street: "",
@@ -488,6 +495,13 @@ class Position {
488495
this.#setShareContent(this.coordinates.lat, this.coordinates.lon, this.elevation);
489496
document.getElementById("positionAltitudeSpan").innerText = this.elevation;
490497
});
498+
499+
if (type === "myposition" || type === "context") {
500+
this.immersivePosition.addEventListener("dataLoaded", () => {
501+
document.getElementById("immersivePostionHtmlBefore").innerHTML = this.immersivePosition.computeHtml();
502+
});
503+
this.immersivePosition.computeAll();
504+
}
491505
}
492506

493507
#setShareContent(latitude, longitude, altitude = "") {
@@ -578,6 +592,8 @@ https://cartes-ign.ign.fr?lng=${longitude}&lat=${latitude}&z=${zoom}`;
578592
this.elevation = null;
579593
this.opened = false;
580594
this.shareContent = null;
595+
this.immersivePosition = null;
596+
581597
// nettoyage du DOM
582598
if (this.container) {
583599
this.container.remove();

0 commit comments

Comments
 (0)