332 lines
No EOL
11 KiB
JavaScript
332 lines
No EOL
11 KiB
JavaScript
|
||
var map;
|
||
|
||
const popup = new maplibregl.Popup({
|
||
closeButton: false,
|
||
closeOnClick: false,
|
||
anchor: 'bottom-left'
|
||
});
|
||
|
||
|
||
/**
|
||
* Creates a handler to load the map with custom events.
|
||
*/
|
||
class MapLoader extends EventTarget {
|
||
constructor() {
|
||
super();
|
||
}
|
||
|
||
/**
|
||
* Loads the map wihtin the given container
|
||
*
|
||
* @param {string} url - name of the container
|
||
*/
|
||
async loadMap(containerId) {
|
||
try {
|
||
map = new maplibregl.Map({
|
||
container: containerId,
|
||
//style: 'https://demotiles.maplibre.org/style.json',
|
||
style: window.MAP_STYLE_LIGHT,
|
||
//style: 'https://tiles.openfreemap.org/styles/positron',
|
||
center: [4.3572, 50.8477],
|
||
zoom: 11,
|
||
minzoom: 0,
|
||
maxzoom: 24
|
||
});
|
||
map.on("load", () => {
|
||
this.dispatchEvent(new CustomEvent("mapLoaded", { detail: map }));
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error("Erreur when loading the map :", error.message);
|
||
|
||
// Déclenche un événement pour signaler une erreur
|
||
this.dispatchEvent(new CustomEvent("mapError", { detail: error }));
|
||
}
|
||
}
|
||
}
|
||
const maploader = new MapLoader();
|
||
|
||
|
||
/**
|
||
* Creates a handler to load a GeoJSON with custom events.
|
||
*/
|
||
class GeojsonLoader extends EventTarget {
|
||
constructor() {
|
||
super();
|
||
}
|
||
|
||
/**
|
||
* Loads a GeoJSON from a URL and triggers an event once loaded.
|
||
*
|
||
* @param {string} url - The URL of the GeoJSON file.
|
||
*/
|
||
async loadGeojson(url) {
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
throw new Error(`Erreur when loading GeoJSON : ${response.statusText}`);
|
||
}
|
||
|
||
var geojson = await response.json();
|
||
|
||
if (!geojson || geojson.type !== "FeatureCollection") {
|
||
throw new Error("The GeoJSON must be an object of type FeatureCollection.");
|
||
}
|
||
|
||
// Triggers a custom event with the loaded GeoJSON
|
||
this.dispatchEvent(new CustomEvent("geojsonLoaded", { detail: geojson }));
|
||
|
||
} catch (error) {
|
||
console.error("Error loading GeoJSON: ", error.message);
|
||
|
||
// Triggers an event to report an error
|
||
this.dispatchEvent(new CustomEvent("geojsonError", { detail: error }));
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
/**
|
||
* updates the map when geojson is loaded and map is loaded.
|
||
*/
|
||
async function handleGeojsonMapLoading(init=true, geojsonloader, geojsonsource, geojsonlayers=[], cluster=false) {
|
||
try {
|
||
var geojson = {};
|
||
|
||
if (init) {
|
||
// Wait for both events simultaneously
|
||
[geojson, map] = await Promise.all([
|
||
waitForEvent(geojsonloader, "geojsonLoaded"),
|
||
waitForEvent(maploader, "mapLoaded")
|
||
]);
|
||
}
|
||
else {
|
||
// Wait for new geojson loading
|
||
[geojson] = await Promise.all([
|
||
waitForEvent(geojsonloader, "geojsonLoaded"),
|
||
]);
|
||
}
|
||
|
||
// Update Map
|
||
if (map.getSource(geojsonsource)) {
|
||
map.getSource(geojsonsource).setData(reverseGeoJson(geojson));
|
||
}
|
||
else {
|
||
if (cluster) {
|
||
map.addSource(geojsonsource, {
|
||
'type': 'geojson',
|
||
'data': geojson,
|
||
cluster: true,
|
||
clusterMaxZoom: 24,
|
||
clusterRadius: 25,
|
||
clusterMinPoints: 1
|
||
});
|
||
}
|
||
else {
|
||
map.addSource(geojsonsource, {
|
||
'type': 'geojson',
|
||
'data': reverseGeoJson(geojson)
|
||
});
|
||
}
|
||
|
||
geojsonlayers.forEach(layer => {
|
||
if (!map.getLayer(layer.id)) {
|
||
map.addLayer(layer);
|
||
}
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error("Error while updating map :", error.message);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
// Function to update a table with the elements visible on the map
|
||
function filterTableByMapBounds(map, geojson_data, callback) {
|
||
// Récupérer les limites actuelles de la carte
|
||
const bounds = map.getBounds();
|
||
|
||
// Filtrer les points visibles dans les limites de la carte
|
||
const visibleFeatures = geojson_data.features.filter((feature) => {
|
||
if (feature.geometry && feature.geometry.type === 'Point') {
|
||
const [lon, lat] = feature.geometry.coordinates;
|
||
return bounds.contains([lon, lat]); // Vérifie si le point est dans les limites
|
||
}
|
||
return false; // Ignorer les autres types de géométries
|
||
});
|
||
|
||
callback(visibleFeatures);
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Registers a mouseenter event on a specific MapLibre layer.
|
||
* Displays a popup with feature properties if the zoom level is above the threshold.
|
||
*
|
||
* @param {object} map - The MapLibre GL map instance.
|
||
* @param {string} layerId - The ID of the layer to listen for mouseenter events.
|
||
* @param {object} popup - The MapLibre popup instance to display information.
|
||
* @param {string} property - The feature property to display in the popup.
|
||
* @param {number} zoomThreshold - The minimum zoom level required to show the popup.
|
||
*/
|
||
function registerLayerMouseEnter(map, layerId, popup, property, zoomThreshold = 13) {
|
||
map.on('mouseenter', layerId, (e) => {
|
||
const currentZoom = map.getZoom();
|
||
|
||
if (currentZoom > zoomThreshold) {
|
||
// Change the cursor style to indicate interactivity
|
||
map.getCanvas().style.cursor = 'pointer';
|
||
|
||
const description = e.features[0].properties[property] || 'No data';
|
||
|
||
// const coordinates = e.features[0].geometry.coordinates.slice();
|
||
// // Handle longitude
|
||
// while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
||
// coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
|
||
// }
|
||
const coordinates = e.lngLat;
|
||
|
||
// Set and display the popup
|
||
popup.setLngLat(coordinates).setHTML(description).addTo(map);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Registers a mouseleave event on a specific MapLibre layer.
|
||
* Resets the cursor and removes the popup when the mouse leaves the layer.
|
||
*
|
||
* @param {object} map - The MapLibre GL map instance.
|
||
* @param {string} layerId - The ID of the layer to listen for mouseleave events.
|
||
* @param {object} popup - The MapLibre popup instance to be removed.
|
||
*/
|
||
function registerLayerMouseLeave(map, layerId, popup) {
|
||
map.on('mouseleave', layerId, () => {
|
||
// Reset cursor style
|
||
map.getCanvas().style.cursor = '';
|
||
|
||
// Remove popup from the map
|
||
popup.remove();
|
||
});
|
||
}
|
||
|
||
|
||
/**
|
||
* Registers a mousemove event on a specific MapLibre layer.
|
||
* Displays a popup with feature properties if the zoom level is above the threshold.
|
||
*
|
||
* @param {object} map - The MapLibre GL map instance.
|
||
* @param {string} layerId - The ID of the layer to listen for mouseenter events.
|
||
* @param {object} popup - The MapLibre popup instance to display information.
|
||
* @param {string} property - The feature property to display in the popup.
|
||
* @param {number} zoomThreshold - The minimum zoom level required to show the popup.
|
||
*/
|
||
function registerLayerMouseMove(map, layerId, popup, property, zoomThreshold = 13) {
|
||
map.on('mousemove', layerId, (e) => {
|
||
const currentZoom = map.getZoom();
|
||
|
||
if (currentZoom > zoomThreshold) {
|
||
map.getCanvas().style.cursor = 'pointer';
|
||
const coordinates = e.lngLat;
|
||
const description = e.features[0].properties[property] || 'No data';
|
||
popup.setLngLat(coordinates).setHTML(description);
|
||
if (!popup.isOpen()) {
|
||
popup.addTo(map);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
/**
|
||
* Registers a click event on a specific table for zooming to a location on the map.
|
||
*
|
||
* @param {string} tableSelector - The selector for the table(s) containing zoom buttons (e.g., '#intersections-table').
|
||
* @param {string} buttonClass - The class of the zoom button (e.g., '.zoom-btn').
|
||
* @param {object} map - The MapLibre GL map instance.
|
||
* @param {number} zoomLevel - The zoom level to apply when flying to the location (default: 16).
|
||
*/
|
||
function registerZoomAction(tableSelector, buttonClass, map, zoomLevel = 16) {
|
||
$(tableSelector).on('click', buttonClass, function () {
|
||
const lon = parseFloat($(this).data('lon'));
|
||
const lat = parseFloat($(this).data('lat'));
|
||
|
||
if (!isNaN(lon) && !isNaN(lat)) {
|
||
map.flyTo({
|
||
center: [lon, lat],
|
||
zoom: zoomLevel,
|
||
essential: true // Ensures smooth animation
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
/**
|
||
* Highlights a specific feature on a MapLibre layer while removing the highlight from the previous feature.
|
||
*
|
||
* @param {object} map - The MapLibre GL map instance.
|
||
* @param {string} sourceId - The source ID containing the features.
|
||
* @param {string} featureId - The ID of the feature to highlight.
|
||
* @param {object} highlightState - Object to track the currently highlighted feature.
|
||
*/
|
||
function highlightMapFeature(map, sourceId, featureId, highlightState) {
|
||
if (!highlightState || !map) return;
|
||
// Remove highlight from the previous feature
|
||
if (highlightState.previousId !== null && highlightState.previousId !== undefined) {
|
||
try {
|
||
map.setFeatureState(
|
||
{ source: sourceId, id: highlightState.previousId },
|
||
{ highlighted: false }
|
||
);
|
||
} catch (e) {
|
||
// Ignore feature state errors if layer/source is not present
|
||
}
|
||
}
|
||
|
||
// Set highlight on the newly selected feature
|
||
if (featureId !== null && featureId !== undefined) {
|
||
highlightState.previousId = featureId;
|
||
try {
|
||
map.setFeatureState(
|
||
{ source: sourceId, id: featureId },
|
||
{ highlighted: true }
|
||
);
|
||
} catch (e) {
|
||
// Ignore feature state errors if layer/source is not present
|
||
}
|
||
} else {
|
||
highlightState.previousId = null;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
function reverseGeoJson(data) {
|
||
if (data.type === 'FeatureCollection' && Array.isArray(data.features)) {
|
||
return {
|
||
...data,
|
||
features: [...data.features].reverse()
|
||
};
|
||
}
|
||
return data;
|
||
}
|
||
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Companion controls (loaded separately, depend on maplibre-gl.js)
|
||
// ---------------------------------------------------------------------------
|
||
// address_search_control.js – AddressSearchControl
|
||
// A reusable MapLibre IControl that queries the internal autocomplete_street
|
||
// endpoint and places a draggable marker on the selected address.
|
||
// Usage: map.addControl(new AddressSearchControl({ autocompleteUrl, ... }), 'top-left');
|
||
// Events: 'address-selected' | 'marker-placed' | 'marker-moved' | 'marker-cleared'
|
||
// ---------------------------------------------------------------------------
|