geolocation-8.x-3.x-dev/js/MapProvider/GeolocationMapBase.js
js/MapProvider/GeolocationMapBase.js
/**
* @typedef {Object} GeolocationTileLayerSettings
*
* @prop {String} label
* @prop {String} url
* @prop {String} attribution
* @prop {Object} settings
* @prop {GeolocationBoundaries} [bounds]
* @prop {Integer} [minZoom]
* @prop {Integer} [maxZoom]
*/
/**
* @typedef {Object} GeolocationMapSettings
*
* @prop {String} [type] Map type
* @prop {String} id
* @prop {Object} settings
* @prop {Number} lat
* @prop {Number} lng
* @prop {Object.<string, object>} mapCenter
* @prop {Object} wrapper
* @prop {String} import_path
* @prop {String[]} scripts
* @prop {String[]} async_scripts
* @prop {String[]} stylesheets
* @prop {String} conditional_initialization
* @prop {String} conditional_description
* @prop {String} conditional_label
* @prop {Number} conditional_viewport_threshold
* @prop {Object.<string, GeolocationDataLayerSettings>} data_layers
* @prop {Object.<string, GeolocationTileLayerSettings>} tile_layers
* @prop {Object.<string, GeolocationMapFeatureSettings>} features
*/
import { GeolocationCoordinates } from "../Base/GeolocationCoordinates.js";
import { GeolocationBoundaries } from "../Base/GeolocationBoundaries.js";
import { GeolocationMapMarker } from "../Base/GeolocationMapMarker.js";
import { GeolocationShapePolygon } from "../Base/GeolocationShapePolygon.js";
import { GeolocationShapeLine } from "../Base/GeolocationShapeLine.js";
import { GeolocationShapeMultiLine } from "../Base/GeolocationShapeMultiLine.js";
import { GeolocationShapeMultiPolygon } from "../Base/GeolocationShapeMultiPolygon.js";
import { GeolocationCircle } from "../Base/GeolocationCircle.js";
import { GeolocationShape } from "../Base/GeolocationShape.js";
/**
* @prop {String} id
* @prop {GeolocationMapSettings} settings
* @prop {HTMLElement} wrapper
* @prop {HTMLElement} container
* @prop {Map<String, GeolocationDataLayer>} dataLayers
* @prop {Map<String, Object>} tileLayers
* @prop {Map<String, GeolocationMapFeature>} features
* @prop {GeolocationMapCenterBase[]} mapCenter
*/
export class GeolocationMapBase {
constructor(mapSettings) {
this.updatingBounds = false;
this.settings = mapSettings || {};
this.wrapper = mapSettings.wrapper;
this.container = mapSettings.wrapper.querySelector(".geolocation-map-container");
if (!this.container) {
throw new Error("Geolocation - Map container not found");
}
this.features = new Map();
this.mapCenter = [];
this.dataLayers = new Map();
this.tileLayers = new Map();
this.id = mapSettings.id ?? `map${Math.floor(Math.random() * 10000)}`;
}
readyFeatures() {
this.features.forEach((feature) => {
feature.onMapReady();
});
}
/**
* @return {Promise<GeolocationMapBase>}
* Initialized map.
*/
initialize() {
const scripts = this.settings.scripts || [];
const scriptLoads = [];
scripts.forEach((script) => {
scriptLoads.push(Drupal.geolocation.addScript(script));
});
const asyncScripts = this.settings.async_scripts || [];
const asyncScriptLoads = [];
asyncScripts.forEach((script) => {
asyncScriptLoads.push(Drupal.geolocation.addScript(script, true));
});
const stylesheets = this.settings.stylesheets || [];
const stylesheetLoads = [];
stylesheets.forEach((stylesheet) => {
stylesheetLoads.push(Drupal.geolocation.addStylesheet(stylesheet));
});
return Promise.all(scriptLoads)
.then(() => {
return Promise.all(asyncScriptLoads);
})
.then(() => {
return Promise.all(stylesheetLoads);
})
.then(() => {
// Some features depend on libraries loaded AFTER main script but BEFORE instantiating.
const featureScriptLoads = [];
Object.keys(this.settings.features ?? {}).forEach((featureName) => {
const featureScripts = this.settings.features[featureName]?.scripts || [];
featureScripts.forEach((featureScript) => {
featureScriptLoads.push(Drupal.geolocation.addScript(featureScript));
});
});
return Promise.all(featureScriptLoads);
})
.then(() => {
return this;
});
}
/**
* @param {GeolocationMapFeatureSettings} featureSettings
* Feature settings.
* @param {?string} id
* Feature ID.
* @return {Promise<GeolocationMapFeature>|null}
* Loaded feature.
*/
loadFeature(featureSettings, id = null) {
if (!featureSettings.import_path) {
return null;
}
const asyncScripts = featureSettings.async_scripts || [];
const asyncScriptLoads = [];
asyncScripts.forEach((script) => {
asyncScriptLoads.push(Drupal.geolocation.addScript(script, true));
});
const stylesheets = featureSettings.stylesheets || [];
const stylesheetLoads = [];
stylesheets.forEach((stylesheet) => {
stylesheetLoads.push(Drupal.geolocation.addStylesheet(stylesheet));
});
return Promise.all(asyncScriptLoads)
.then(() => {
return Promise.all(stylesheetLoads);
})
.then(() => {
return import(featureSettings.import_path);
})
.then((featureImport) => {
try {
const feature = new featureImport.default(featureSettings.settings, this);
this.features.set(id, feature);
return feature;
} catch (e) {
console.error(e, `Loading ${featureSettings.import_path} failed: ${e.toString()}`);
}
})
.catch((error) => {
console.error(error, `Loading ' ${featureSettings.import_path}' failed`);
});
}
async loadFeatures() {
const featureImports = [];
Object.keys(this.settings.features ?? {}).forEach((featureName) => {
const featurePromise = this.loadFeature(this.settings.features[featureName], featureName);
if (featurePromise) {
featureImports.push(featurePromise);
}
});
return Promise.all(featureImports).then(() => {
return this;
});
}
async loadCenterOptions() {
const mapCenterImports = [];
Object.keys(this.settings.mapCenter ?? {}).forEach((mapCenterName) => {
const mapCenterSettings = this.settings.mapCenter[mapCenterName];
if (mapCenterSettings.import_path) {
const promise = import(mapCenterSettings.import_path);
promise
.then((mapCenter) => {
const plugin = new mapCenter.default(this, mapCenterSettings.settings);
plugin.weight = this.settings.mapCenter[mapCenterName].weight;
this.mapCenter.push(plugin);
})
.catch((error) => {
console.error(error, `Loading '${mapCenterSettings.import_path}' failed`);
});
mapCenterImports.push(promise);
}
});
return Promise.all(mapCenterImports).then(() => {
this.mapCenter.sort(
/**
* @param {GeolocationMapCenterBase} a
* Center one.
* @param {Number} a.weight
* Weight one.
* @param {GeolocationMapCenterBase} b
* Center two.
* @param {Number} b.weight
* Weight two.
*
* @return {int}
* Compare value
*/
(a, b) => {
if (a.weight > b.weight) {
return 1;
}
if (a.weight < b.weight) {
return -1;
}
return 0;
}
);
return this;
});
}
addControl(element) {
// Stub.
}
removeControls() {
// Stub.
}
async getZoom() {
// Stub.
}
setZoom(zoom, defer) {
// Stub.
}
/**
* @return {GeolocationBoundaries}
* Boundaries.
*/
getBoundaries() {
return null;
}
/**
* @param {GeolocationBoundaries} boundaries
* Boundaries.
*
* @return {boolean}
* Change.
*/
setBoundaries(boundaries) {
if (!boundaries) {
return false;
}
if (this.getBoundaries()?.equals(boundaries)) {
return false;
}
this.updatingBounds = true;
}
/**
*
* @param {Array.<GeolocationMapMarker|GeolocationShape>} elements
*
* @return {GeolocationBoundaries}
* Boundaries.
*/
getElementBoundaries(elements) {
elements = elements || (this.dataLayers.get("default").markers || []).concat(this.dataLayers.get("default").shapes || []);
if (!elements.length) {
return null;
}
const markers = [];
const shapes = [];
elements.forEach((element) => {
if (element instanceof GeolocationMapMarker) {
markers.push(element);
} else if (element instanceof GeolocationShape) {
shapes.push(element);
}
});
let bounds = this.getMarkerBoundaries(markers);
if (bounds === null) {
bounds = this.getShapeBoundaries(shapes);
} else {
bounds.extend(this.getShapeBoundaries(shapes));
}
return bounds;
}
/**
* @param {GeolocationMapMarker[]} markers
* Markers.
*
* @return {GeolocationBoundaries}
* Boundaries.
*/
getMarkerBoundaries(markers) {
return null;
}
/**
* @param {GeolocationShape[]} shapes
* Shapes.
*
* @return {GeolocationBoundaries}
* Boundaries.
*/
getShapeBoundaries(shapes) {
shapes = shapes || this.dataLayers.get("default").shapes;
if (!shapes.length) {
return null;
}
let bounds;
shapes.forEach((shape) => {
const currentBounds = shape.getBounds();
if (currentBounds === null) {
return;
}
if (bounds) {
bounds.extend(currentBounds);
} else {
bounds = currentBounds;
}
});
if (bounds) {
return bounds;
}
return null;
}
/**
* @return {GeolocationCoordinates}
* Coordinates.
*/
getCenter() {
return null;
}
setCenterByOptions() {
this.setZoom();
Object.values(this.mapCenter).every((center) => {
return center.setCenter() !== true;
});
return this;
}
/**
* @param {GeolocationCoordinates} coordinates
* Coordinates.
* @param {Number} accuracy
* Accuracy.
*/
setCenterByCoordinates(coordinates, accuracy = undefined) {
this.updatingBounds = true;
if (typeof accuracy === "undefined") {
return;
}
const earth = 6378.137;
const m = 1 / (((2 * Math.PI) / 360) * earth) / 1000;
this.setBoundaries(
new GeolocationBoundaries(
coordinates.lat + accuracy * m,
coordinates.lng + (accuracy * m) / Math.cos(coordinates.lat * (Math.PI / 180)),
coordinates.lat + -1 * accuracy * m,
coordinates.lng + (-1 * accuracy * m) / Math.cos(coordinates.lat * (Math.PI / 180))
)
);
const circle = this.createCircle(coordinates, accuracy, {
fillColor: "#4285F4",
fillOpacity: 0.15,
strokeColor: "#4285F4",
strokeOpacity: 0.3,
strokeWidth: 1,
});
// Fade circle away.
const intervalId = setInterval(() => {
let fillOpacity = circle.fillOpacity;
fillOpacity -= 0.03;
let strokeOpacity = circle.strokeOpacity;
strokeOpacity -= 0.06;
if (strokeOpacity > 0 && fillOpacity > 0) {
circle.update(null, 0, {
fillOpacity,
strokeOpacity,
});
} else {
circle.remove();
clearInterval(intervalId);
}
}, 500);
return false;
}
/**
* @param {GeolocationCoordinates} coordinates
* Coordinates.
* @param {GeolocationMarkerSettings} settings
* Settings.
*
* @return {GeolocationMapMarker}
* Marker.
*/
createMarker(coordinates, settings) {
return new GeolocationMapMarker(coordinates, settings, this);
}
getMarkerById(id, layerId = null) {
if (layerId) {
this.dataLayers.get(layerId).markers.forEach((marker) => {
if (id === marker.id ?? null) {
return marker;
}
});
return null;
}
// Check default first, then the rest.
this.dataLayers.get("default").markers.forEach((marker) => {
if (id === marker.id ?? null) {
return marker;
}
});
this.dataLayers.forEach((layer) => {
if (id === "default") {
return;
}
layer.markers.forEach((marker) => {
if (id === marker.id ?? null) {
return marker;
}
});
});
return null;
}
removeMapMarkers() {
this.dataLayers.forEach((layer) => {
layer.removeMarkers();
});
}
/**
* @param {GeolocationCoordinates} center
* Center.
* @param {int} radius
* Radius.
* @param {GeolocationCircleSettings} [settings]
* Settings.
*
* @return {GeolocationCircle}
* Shape.
*/
createCircle(center, radius, settings = {}) {
return new GeolocationCircle(center, radius, this, settings);
}
/**
* @param {GeolocationGeometry} geometry
* Geometry.
* @param {GeolocationShapeSettings} settings
* Settings.
*
* @return {GeolocationShapeLine}
* Shape.
*/
createShapeLine(geometry, settings) {
return new GeolocationShapeLine(geometry, settings, this);
}
/**
* @param {GeolocationGeometry} geometry
* Geometry.
* @param {GeolocationShapeSettings} settings
* Settings.
*
* @return {GeolocationShapePolygon}
* Shape.
*/
createShapePolygon(geometry, settings) {
return new GeolocationShapePolygon(geometry, settings, this);
}
/**
* @param {GeolocationGeometry} geometry
* Geometry.
* @param {GeolocationShapeSettings} settings
* Settings.
*
* @return {GeolocationShapeMultiLine}
* Shape.
*/
createShapeMultiLine(geometry, settings) {
return new GeolocationShapeMultiLine(geometry, settings, this);
}
/**
* @param {GeolocationGeometry} geometry
* Geometry.
* @param {GeolocationShapeSettings} settings
* Settings.
*
* @return {GeolocationShapeMultiPolygon}
* Shape.
*/
createShapeMultiPolygon(geometry, settings) {
return new GeolocationShapeMultiPolygon(geometry, settings, this);
}
removeMapShapes() {
this.dataLayers.forEach((layer) => {
layer.removeShapes();
});
}
/**
* @param {String} layerId
* Layer ID.
* @param {GeolocationDataLayerSettings} layerSettings
* Layer settings.
*
* @return {Promise<GeolocationDataLayer>|null}
* Layer.
*/
loadDataLayer(layerId, layerSettings) {
if (!layerSettings.import_path) {
return null;
}
const scripts = layerSettings.scripts || [];
const scriptLoads = [];
scripts.forEach((script) => {
scriptLoads.push(Drupal.geolocation.addScript(script));
});
const asyncScripts = layerSettings.async_scripts || [];
const asyncScriptLoads = [];
asyncScripts.forEach((script) => {
asyncScriptLoads.push(Drupal.geolocation.addScript(script, true));
});
const stylesheets = layerSettings.stylesheets || [];
const stylesheetLoads = [];
stylesheets.forEach((stylesheet) => {
stylesheetLoads.push(Drupal.geolocation.addStylesheet(stylesheet));
});
return Promise.all(scriptLoads)
.then(() => {
return Promise.all(asyncScriptLoads);
})
.then(() => {
return Promise.all(stylesheetLoads);
})
.then(() => {
return import(layerSettings.import_path);
})
.then((layerImport) => {
try {
/** @type {GeolocationDataLayer} */
const layer = new layerImport.default(this, layerId, layerSettings);
// Default data layer.
if (layerId === "geolocation_default_layer_default") {
this.dataLayers.set("default", layer);
} else {
this.dataLayers.set(layerId, layer);
}
return layer.loadFeatures();
} catch (e) {
console.error(`Layer ${layerId} failed: ${e.toString()}`);
}
})
.then((layer) => {
return layer.loadMarkers();
})
.then((layer) => {
return layer.loadShapes();
})
.catch((error) => {
console.error(error.toString(), error, `Loading '${layerSettings.import_path}' failed`);
});
}
/**
* Load data layers.
*
* @return {Promise<GeolocationMapBase>}
*/
async loadDataLayers() {
const dataLayerImports = [];
Object.keys(this.settings.data_layers ?? {}).forEach((dataLayerName) => {
const dataLayerPromise = this.loadDataLayer(dataLayerName, this.settings.data_layers[dataLayerName] ?? {});
if (dataLayerPromise) {
dataLayerImports.push(dataLayerPromise);
}
});
return Promise.all(dataLayerImports).then(() => {
return this;
});
}
/**
* @param {string} layerId
* @param {GeolocationTileLayerSettings} layerSettings
*/
loadTileLayer(layerId, layerSettings) {
// Example: this.tileLayers.set(layerId, layer);
}
async loadTileLayers() {
Object.keys(this.settings.tile_layers ?? {}).forEach((layerId) => {
this.loadTileLayer(layerId, this.settings.tile_layers[layerId] ?? {});
});
return Promise.resolve(this);
}
/**
* @param {string} layerId
*/
unloadTileLayer(layerId) {
// this.tileLayers.delete(layerId);
}
fitMapToElements(elements) {
const boundaries = this.getElementBoundaries(elements);
if (!boundaries) {
return false;
}
this.setBoundaries(boundaries);
}
}
