/**
* @mixin
* @memberof module:geoflo
* @name Layers
* @description This module provides the layer functionality for the Geoflo application.
* Refactored to use ES6 Maps for caching and optimized for performance.
* @param {Object} options - The options object to configure the object.
* @returns {Object} Returns the Layers object.
*/
const Layers = function () {
const geoflo = this.geoflo;
if (!geoflo.map) throw new Error('No map object provided!');
const map = geoflo.map;
const id = geoflo.id;
const layerTypes = {
Polygon: ['-fill', '-border'],
Polyline: ['-line', '-dash', '-buffer'],
Point: ['-circle', '-icon', '-cluster-circle', '-cluster-icon', '-count-icon', '-count-text'],
Image: ['-image'],
All: ['-fill', '-border', '-line', '-dash', '-buffer', '-circle', '-icon', '-cluster-circle', '-cluster-icon', '-count-icon', '-count-text', '-image']
};
this.options = {};
this.layersMap = new Map();
this.sourcesMap = new Map();
/**
* Initializes the object with provided options and refreshes it.
*/
this.init = function (options = {}) {
this.options = { ...this.options, ...options };
this.defaultLayers = this.defaultLayers = [
{
source: geoflo.statics.constants.sources.COLD,
id: id + '-fill-cold',
type: 'fill',
layout: {},
filter: ["==", "$type", "Polygon"],
paint: {
'fill-color': geoflo.options.colors.secondaryCold,
'fill-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 0.3]
}
},
{
source: geoflo.statics.constants.sources.COLD,
id: id + '-line-cold',
type: 'line',
layout: {
'line-cap': 'round',
'line-join': 'miter'
},
paint: {
'line-color': geoflo.options.colors.primaryCold,
'line-width': 4,
'line-gap-width': ["match", ["get", "type"], "Polygon", 0, 0],
'line-offset': ['case', ["boolean", ["has", "offset"], true], ["get", "offset"], 0],
'line-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 1]
}
},
{
source: geoflo.statics.constants.sources.COLD,
id: id + '-circle-cold',
filter: ['all', ['==', ['get', 'type'], 'Circle'], ["!=", ["geometry-type"], "Polygon"]],
type: 'circle',
paint: {
'circle-radius': { 'base': 6, 'stops': [[10, 8], [14, 10]] },
'circle-stroke-width': 2,
'circle-color': geoflo.options.colors.primaryCold,
'circle-stroke-color': geoflo.options.colors.secondaryCold,
'circle-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 1],
'circle-stroke-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 1]
}
},
{
source: geoflo.statics.constants.sources.COLD,
id: id + '-icon-cold',
type: 'symbol',
filter: ['==', ['get', 'type'], 'Icon'],
layout: {
'visibility': 'visible',
'icon-optional': true,
'text-field': ['get', 'primaryIcon', ['get', 'style', ['properties']]],
'text-size': {
'base': 16,
'stops': [[10, 16], [14, 12]]
},
'text-line-height': 1,
'text-padding': 0,
'text-offset': [0, 0.2],
'text-justify': 'auto',
'text-anchor': 'center',
'text-allow-overlap': true,
'text-font': ['Font Awesome 6 Pro Solid'],
'text-ignore-placement': true
},
paint: {
'text-translate-anchor': 'viewport',
'text-halo-width': 0,
'text-halo-color': geoflo.options.colors.primaryCold,
'text-color': geoflo.options.colors.secondaryBackground,
'text-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 1]
}
},
{
source: geoflo.statics.constants.sources.COLD,
id: id + '-text-cold',
type: 'symbol',
filter: ["==", "$type", "Point"],
layout: {
"symbol-placement": "point",
'text-field': ['get', 'text'],
'text-font': ['DIN Pro Regular', 'DIN Pro Italic', 'Arial Unicode MS Regular', 'DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-keep-upright': true,
'text-size': 18,
'text-justify': ['get', 'justify'],
'text-letter-spacing': 0.05,
'text-line-height': 1.2,
'text-max-angle': 10,
'text-offset': [0, 0],
'text-padding': 2,
'text-rotate': 0,
'text-transform': ['get', 'transform']
},
paint: {
'text-color': geoflo.options.colors.primaryCold,
'text-halo-color': geoflo.options.colors.primaryBackground,
'text-halo-width': 0.5,
'text-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 1]
}
},
{
source: geoflo.statics.constants.sources.COLD,
id: id + '-text-icon-cold',
type: 'symbol',
filter: ['==', ['get', 'type'], 'Text'],
layout: {
'visibility': 'visible',
'icon-optional': true,
'text-field': ['get', 'primaryIcon', ['get', 'style', ['properties']]],
'text-size': {
'base': 16,
'stops': [[10, 16], [14, 12]]
},
'text-line-height': 1,
'text-padding': 0,
'text-offset': [0, 0.2],
'text-justify': 'auto',
'text-anchor': 'center',
'text-allow-overlap': true,
'text-font': ['Font Awesome 6 Pro Solid'],
'text-ignore-placement': true
},
paint: {
'text-translate-anchor': 'viewport',
'text-halo-width': 0,
'text-halo-color': geoflo.options.colors.primaryCold,
'text-color': geoflo.options.colors.secondaryBackground,
'text-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 1]
}
},
{
id: geoflo.statics.constants.layers.MESH + '-line',
source: geoflo.statics.constants.sources.MESH,
type: "line",
paint: {
"line-color": geoflo.options.colors.primaryBase,
"line-width": 2,
"line-opacity": 0.3
}
},
{
id: geoflo.statics.constants.layers.MESH + '-circle',
source: geoflo.statics.constants.sources.MESH,
type: 'circle',
paint: {
'circle-radius': 2,
'circle-color': geoflo.options.colors.primaryBase,
'circle-opacity': 0.3
}
},
{
source: geoflo.statics.constants.sources.HOT,
id: id + '-fill-hot',
type: 'fill',
layout: {},
filter: ["==", "$type", "Polygon"],
paint: {
'fill-color': geoflo.options.colors.secondaryHot,
'fill-opacity': ['case', ["boolean", ["has", "new"], true], 0.5, 0.1],
}
},
{
'source': geoflo.statics.constants.sources.HOT,
'id': id + '-line-hot',
'type': 'line',
'layout': {
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'line-color': geoflo.options.colors.primaryHot,
'line-width': 4,
'line-dasharray': [1, 2],
}
},
{
'source': geoflo.statics.constants.sources.HOT,
'id': id + '-point-hot',
'filter': ['==', '$type', 'Point'],
'type': 'circle',
//'filter': ["==", 0, ['number', ['get', 'painting']]],
'paint': {
'circle-radius': ["match", ["get", "type"], "Circle", 10, 4],
'circle-stroke-width': 2,
'circle-color': geoflo.options.colors.primaryHot,
'circle-stroke-color': geoflo.options.colors.secondaryHot
}
},
{
source: geoflo.statics.constants.sources.HOT,
id: id + '-icon-hot',
filter: ['==', ['get', 'type'], 'Icon'],
type: 'symbol',
layout: {
'visibility': 'visible',
'icon-optional': true,
'text-field': ['get', 'primaryIcon', ['get', 'style', ['properties']]],
'text-size': {
'base': 16,
'stops': [[10, 16], [14, 12]]
},
'text-line-height': 1,
'text-padding': 0,
'text-offset': [0, 0.2],
'text-justify': 'auto',
'text-anchor': 'center',
'text-allow-overlap': true,
'text-font': ['Font Awesome 6 Pro Solid'],
'text-ignore-placement': true
},
paint: {
'text-translate-anchor': 'viewport',
'text-halo-color': geoflo.options.colors.primaryHot,
'text-halo-width': 0, //[ 'case', ['boolean', ['feature-state', 'hover'], false], 0.5, 0 ],
'text-color': geoflo.options.colors.secondaryHot
}
},
{
source: geoflo.statics.constants.sources.HOT,
id: id + '-image-hot',
filter: ['==', ['get', 'type'], 'Image'],
type: 'symbol',
layout: {
'visibility': 'visible',
'icon-image': ['get', 'primaryImage', ['get', 'style', ['properties']]],
'icon-size': ['interpolate', ['linear'], ['zoom'], 1, 0.4, 15, 1],
'icon-allow-overlap': true,
'icon-anchor': 'bottom'
}
},
{
source: geoflo.statics.constants.sources.PIN,
id: id + '-fill-pin',
type: 'fill',
layout: {},
filter: ["==", "$type", "Polygon"],
paint: {
'fill-color': geoflo.options.colors.primaryHot,
'fill-opacity': ['case', ["boolean", ["has", "new"], true], 0.5, 0.1],
}
},
{
'source': geoflo.statics.constants.sources.PIN,
'id': id + '-line-pin',
'type': 'line',
'layout': {
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'line-color': geoflo.options.colors.primaryDebug,
'line-width': 4,
'line-dasharray': [1, 2],
}
},
{
'source': geoflo.statics.constants.sources.PIN,
'id': id + '-point-pin',
'filter': ['==', '$type', 'Point'],
'type': 'circle',
//'filter': ["==", 0, ['number', ['get', 'painting']]],
'paint': {
'circle-radius': ["match", ["get", "type"], "Circle", 10, 4],
'circle-stroke-width': 2,
'circle-color': geoflo.options.colors.primaryDebug,
'circle-stroke-color': geoflo.options.colors.primaryHot
}
},
{
source: geoflo.statics.constants.sources.PIN,
id: id + '-icon-pin',
filter: ['==', ['get', 'type'], 'Icon'],
type: 'symbol',
layout: {
'visibility': 'visible',
'icon-optional': true,
'text-field': ['get', 'primaryIcon', ['get', 'style', ['properties']]],
'text-size': {
'base': 16,
'stops': [[10, 16], [14, 12]]
},
'text-line-height': 1,
'text-padding': 0,
'text-offset': [0, 0.2],
'text-justify': 'auto',
'text-anchor': 'center',
'text-allow-overlap': true,
'text-font': ['Font Awesome 6 Pro Solid'],
'text-ignore-placement': true
},
paint: {
'text-translate-anchor': 'viewport',
'text-halo-color': geoflo.options.colors.primaryDebug,
'text-halo-width': 0, //[ 'case', ['boolean', ['feature-state', 'hover'], false], 0.5, 0 ],
'text-color': geoflo.options.colors.primaryHot
}
},
{
source: geoflo.statics.constants.sources.PIN,
id: id + '-image-pin',
filter: ['==', ['get', 'type'], 'Image'],
type: 'symbol',
layout: {
'visibility': 'visible',
'icon-image': ['get', 'primaryImage', ['get', 'style', ['properties']]],
'icon-size': ['interpolate', ['linear'], ['zoom'], 1, 0.4, 15, 1],
'icon-allow-overlap': true,
'icon-anchor': 'bottom'
}
},
{
'source': geoflo.statics.constants.sources.HOTTEXT,
'id': id + '-text-hot',
'type': 'symbol',
'layout': {
'symbol-placement': 'point',
'text-field': ['get', 'text'],
'text-font': ['Arial Unicode MS Regular', 'DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-keep-upright': true,
'text-anchor': ['get', 'anchor'],
'text-size': 18,
'text-justify': ['get', 'justify'],
'text-letter-spacing': 0.1,
'text-line-height': 1.2,
'text-max-angle': 10,
'text-offset': [0, -1.5],
'text-padding': 2,
'text-rotate': 0,
'text-transform': ['get', 'transform']
},
'paint': {
'text-color': geoflo.options.colors.primaryText,
'text-halo-color': geoflo.options.colors.primaryBackground,
'text-halo-width': 1,
'text-opacity': 1,
}
},
{
'source': geoflo.statics.constants.sources.SNAP,
'id': id + '-point-snap',
'type': 'circle',
'filter': ['==', '$type', 'Point'],
'paint': {
'circle-radius': ["match", ["get", "type"], "Circle", 6, "Icon", 0, 6],
'circle-stroke-width': 2,
'circle-color': geoflo.options.colors.primarySnap,
'circle-stroke-color': geoflo.options.colors.secondarySnap
}
},
{
source: geoflo.statics.constants.sources.SNAP,
id: id + '-icon-snap',
type: 'symbol',
filter: ['==', ['get', 'type'], 'Icon'],
layout: {
'visibility': 'visible',
'icon-optional': true,
'text-field': ['get', 'primaryIcon', ['get', 'style', ['properties']]],
'text-size': {
'base': 18,
'stops': [[10, 18], [14, 16]]
},
'text-line-height': 1,
'text-padding': 0,
'text-offset': [0, 0.2],
'text-justify': 'auto',
'text-anchor': 'center',
'text-allow-overlap': true,
'text-font': ['Font Awesome 6 Pro Solid'],
'text-ignore-placement': true
},
paint: {
'text-translate-anchor': 'viewport',
'text-halo-color': geoflo.options.colors.primarySnap,
'text-halo-width': 0, //[ 'case', ['boolean', ['feature-state', 'hover'], false], 0.5, 0 ],
'text-color': geoflo.options.colors.secondarySnap
}
},
{
source: geoflo.statics.constants.sources.SNAP,
id: id + '-image-snap',
filter: ['==', ['get', 'type'], 'Image'],
type: 'symbol',
layout: {
'visibility': 'visible',
'icon-image': ['get', 'primaryImage', ['get', 'style', ['properties']]],
'icon-size': ['interpolate', ['linear'], ['zoom'], 1, 0.4, 15, 1],
'icon-allow-overlap': true,
'icon-anchor': 'bottom'
}
},
{
'source': geoflo.statics.constants.sources.SNAP,
'id': id + '-line-snap',
'type': 'line',
//'filter': ["==", "$type", "LineString"],
'layout': {
'visibility': 'visible',
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'line-color': geoflo.options.colors.secondarySnap,
'line-width': 4,
'line-dasharray': [1, 2]
}
},
{
'source': geoflo.statics.constants.sources.ROUTE,
'id': id + '-line-route',
'type': 'line',
'filter': ["==", "$type", "LineString"],
'layout': {
'visibility': 'visible',
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'line-color': geoflo.options.colors.error,
'line-width': 4,
'line-dasharray': []
}
},
{
'source': geoflo.statics.constants.sources.VERTEX,
'id': id + '-point-vertex',
'type': 'circle',
'filter': ['==', "$type", 'LineString'],
'paint': {
'circle-radius': 4,
'circle-stroke-width': 3,
'circle-color': geoflo.options.colors.primaryVertex,
'circle-stroke-color': geoflo.options.colors.secondaryVertex
}
},
{
'source': geoflo.statics.constants.sources.GAMEPAD,
'id': id + '-gamepad',
'type': 'symbol',
'layout': {
'visibility': 'visible',
'icon-image': 'gamepad',
'icon-size': 0.25
}
}
]
this.selectLayers = [{
'source': geoflo.statics.constants.sources.SELECT,
'id': id + '-line-select-background',
'type': 'line',
'layout': {
'visibility': 'visible',
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'line-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'line-width': 6,
'line-opacity': 0.4,
'line-dasharray': [4, 3]
},
'metadata': { types: ['Polyline', 'Polygon', 'Rectangle'] }
}, {
'source': geoflo.statics.constants.sources.SELECT,
'id': id + '-line-select',
'type': 'line',
'layout': {
'visibility': 'visible',
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'line-color': [
'case',
['has', 'secondarySelect', ['get', 'style', ['properties']]],
['get', 'secondarySelect', ['get', 'style', ['properties']]],
['get', 'secondaryColor', ['get', 'style', ['properties']]]
],
'line-width': 6,
'line-dasharray': [0, 4, 3]
},
'metadata': { types: ['Polyline', 'Polygon', 'Rectangle'] }
},
{
'source': geoflo.statics.constants.sources.SELECT,
'id': id + '-fill-select',
'type': 'fill',
'layout': {},
'filter': ["==", "$type", "Polygon"],
'paint': {
'fill-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'fill-opacity': 0.4
},
'metadata': { types: ['Polygon', 'Rectangle'] }
},
{
'source': geoflo.statics.constants.sources.SELECT,
'id': id + '-point-select',
'filter': ['all', ['!=', ['get', 'type'], 'Text'], ["==", ["geometry-type"], "Point"]],
'type': 'circle',
'layout': {
'visibility': 'visible',
},
'paint': {
'circle-radius': 10,
'circle-stroke-width': 3,
'circle-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'circle-stroke-color': [
'case',
['has', 'secondarySelect', ['get', 'style', ['properties']]],
['get', 'secondarySelect', ['get', 'style', ['properties']]],
['get', 'secondaryColor', ['get', 'style', ['properties']]]
],
'circle-stroke-opacity': 1,
'circle-opacity': 0.8
},
'metadata': { types: ['Point', 'Circle', 'Marker', 'Icon'] }
},
{
'source': geoflo.statics.constants.sources.SELECT,
'id': id + '-symbol-select',
'filter': ['==', ['get', 'type'], 'Icon'],
'type': 'symbol',
'layout': {
'visibility': 'visible',
'icon-optional': true,
'symbol-placement': 'point',
'text-rotation-alignment': 'viewport',
'text-field': ['get', 'primaryIcon', ['get', 'style', ['properties']]],
'text-rotate': ['get', 'rotate', ['get', 'style', ['properties']]],
'text-size': 14,
'text-line-height': 1,
'text-padding': 0,
'text-offset': [0, 0.2],
'text-justify': 'auto',
'text-anchor': 'center',
'text-allow-overlap': true,
'text-font': ['Font Awesome 6 Pro Solid'],
'text-ignore-placement': true
},
'paint': {
'text-translate-anchor': 'viewport',
'text-halo-color': [
'case',
['has', 'secondarySelect', ['get', 'style', ['properties']]],
['get', 'secondarySelect', ['get', 'style', ['properties']]],
['get', 'secondaryColor', ['get', 'style', ['properties']]]
],
'text-halo-width': 2,
'text-color': geoflo.options.colors.primaryBackground,
},
'metadata': { types: ['Icon'] }
},
{
'source': geoflo.statics.constants.sources.SELECT,
'id': id + '-text-point-select',
'filter': ['==', ['get', 'type'], 'Text'],
'type': 'circle',
'layout': {
'visibility': 'visible',
},
'paint': {
'circle-radius': 4,
'circle-stroke-width': 1,
'circle-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'circle-stroke-color': geoflo.options.colors.primaryBackground,
'circle-stroke-opacity': 1,
'circle-opacity': 1
},
'metadata': { types: ['Text'] }
},
{
'source': geoflo.statics.constants.sources.SELECT,
'id': id + '-text-select',
'filter': ['==', ['get', 'type'], 'Text'],
'type': 'symbol',
'layout': {
'visibility': 'visible',
"symbol-placement": "point",
'text-rotation-alignment': 'viewport',
'text-field': ['get', 'text'],
'text-font': ['DIN Pro Regular', 'DIN Pro Italic', 'Arial Unicode MS Regular', 'DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-keep-upright': true,
'text-allow-overlap': true,
'text-size': 18,
'text-justify': ['get', 'justify'],
'text-letter-spacing': 0.13,
'text-line-height': 1.2,
'text-max-angle': 10,
'text-offset': [0, -1],
'text-padding': 2,
'text-rotate': 0,
'text-transform': ['get', 'transform']
},
'paint': {
'text-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'text-halo-color': geoflo.options.colors.primaryBackground,
'text-halo-width': 2,
'text-opacity': 1,
},
'metadata': { types: ['Text'] }
},
{
'source': geoflo.statics.constants.sources.SELECT,
'id': id + '-image-select',
'filter': ['==', ['get', 'type'], 'Image'],
'type': 'symbol',
'layout': {
'visibility': 'visible',
'icon-image': ['get', 'primaryImage', ['get', 'style', ['properties']]],
'icon-size': ['interpolate', ['linear'], ['zoom'], 1, 0.4, 15, 1],
'icon-allow-overlap': true,
'icon-anchor': 'bottom'
},
'metadata': { types: ['Image'] }
}]
return this.refresh({ init: true });
};
/**
* Refreshes the map by removing and re-adding layers and sources.
*/
this.refresh = async (options = {}) => {
if (options.select) {
const selectIds = this.selectLayers.map(layer => layer.id);
const selectLayers = selectIds.map(id => this.layersMap.get(id)).filter(Boolean);
this.moveLayers(selectLayers);
return selectLayers;
}
if (!options.init) return this.init(options);
const defaultSources = Object.values(geoflo.statics.constants.sources);
const layersBackup = Array.from(this.layersMap.values());
this.removeEventListeners();
this.removeLayers(this.getLayers().filter(l => !l.metadata?.custom).map(l => l.id));
this.removeSources(defaultSources);
this.addSources(defaultSources);
this.addLayers(this.defaultLayers, this.options);
if (layersBackup.length) await this.setLayers(layersBackup.filter(l => l.custom || l.metadata?.custom || l.layer?.metadata?.custom), this.options);
this.addLayers(this.selectLayers, this.options);
await this.refresh({ select: true });
this.addEventListeners();
return this.getLayers();
};
this.addEventListeners = (options = {}) => buildEvents.call(this, { on: true, ...options });
this.removeEventListeners = (options = {}) => buildEvents.call(this, { off: true, ...options });
this.getSources = () => Array.from(this.sourcesMap.values());
this.getSource = (id) => (id ? this.sourcesMap.get(id) : false);
this.getSourceIds = () => Array.from(this.sourcesMap.keys());
this.getLayers = () => Array.from(this.layersMap.values());
this.getCustomLayers = () => Array.from(this.layersMap.values()).filter(l => l.custom || l.metadata?.custom || l.layer?.metadata?.custom);
this.getLayer = (id) => this.layersMap.get(id);
this.getLayerIds = (layers) => Array.isArray(layers) ? layers.map(l => (typeof l === 'object' ? l.id : l)) : Array.from(this.layersMap.keys());
this.getLayerType = (id) => this.getLayer(id, true)?.details?.type || '';
this.getLayerTypes = () => Array.from(this.layersMap.values()).map(l => l.details?.type || '');
this.getLayerTypesBySource = (source) => Array.from(this.layersMap.values()).filter(l => l.source === source).map(l => l.details?.type || '');
this.getLayerByType = (type) => Array.from(this.layersMap.values()).filter(l => l.details?.type === type);
this.getLayerBySource = (source) => Array.from(this.layersMap.values()).filter(l => l.source === source);
this.getLayerById = (id) => this.layersMap.get(id);
this.getFeatures = (id) => this.getSource(id)?._data?.features || [];
this.getFeature = (id) => this.getSource(id)?._data?.features?.find(f => f.id === id) || false;
this.getSelection = (features = [], coords) => {
if (features[0]?.properties?.cluster) {
this.onClusterClick(features[0], coords);
return false;
}
return true;
};
this.getType = (type) => {
if (['Polygon', 'Rectangle'].includes(type)) return 'Polygon';
if (['Polyline', 'LineString', 'Line'].includes(type)) return 'Polyline';
if (['Point', 'Circle', 'Marker', 'Icon', 'Text'].includes(type)) return 'Point';
if (type === 'Image') return 'Image';
return false;
};
this.setLayers = async (layers, options = {}) => {
if (!layers || layers.length === 0) return [];
this.removeLayers();
this.removeSources(layers.map(layer => layer.source).filter((v, i, a) => a.indexOf(v) === i));
return await buildLayers.call(this, layers, options);
};
this.setLayerFilter = (source, filter, reset = false) => {
if (!source) return false;
const layers = this.getLayerBySource(source);
if (!layers) return false;
if (!filter) reset = true;
for (const layer of layers) {
const id = layer.id;
if (!id) continue; // Skip if no ID is provided
if (!layer.layer) layer.layer = map.getLayer(id); // Get the layer from the map if not already in layersMap
if (!layer.layer) continue; // Skip if layer not found in map
if (reset) {
if (layer.originalFilter) {
map.setFilter(id, layer.originalFilter); // Apply original filter
layer.filter = layer.originalFilter; // Restore in layersMap
layer.hasCustomFilter = false; // Mark as reverted
return true;
}
return false; // No original filter stored
}
// Ensure the original filter is stored only once
if (!layer.originalFilter) {
layer.originalFilter = layer.filter || ["all"];
}
// Merge new filter with the existing one
const mergedFilter = ["all", layer.originalFilter, filter];
// Apply the merged filter to the Mapbox layer
map.setFilter(id, mergedFilter);
// Update the layer in layersMap
this.layersMap.set(id, {
...layer,
filter: mergedFilter,
hasCustomFilter: true, // Mark it as having a custom filter
});
}
return true;
};
/**
* Adds multiple sources to the map.
*/
this.addSources = (sources = []) => {
sources.forEach(sourceId => { if (!this.sourcesMap.has(sourceId)) this.addSource(sourceId); });
geoflo.fire('sources.add', { sources: this.getSources() });
};
/**
* Adds a single source to the map and caches it.
*/
this.addSource = (id, type, options = {}) => {
if (!id) throw new Error('No source was provided!');
const existingSource = map.getSource(id);
if (existingSource) {
this.sourcesMap.set(id, this.getSource(id) || existingSource);
return this.getSource(id);
}
let opts = {
type: options.type || 'geojson',
data: turf.featureCollection(options.features || []),
promoteId: options.promoteId || 'id',
cluster: false
};
if ((type === 'Point' || type === 'Image') && !options.noCluster) {
opts = { ...opts, cluster: true, clusterMaxZoom: options.clusterMaxZoom || 14, clusterRadius: options.clusterRadius || 50 };
}
map.addSource(id, opts);
const newSource = map.getSource(id);
this.sourcesMap.set(id, newSource);
geoflo.fire('source.add', { id, source: newSource });
return newSource;
};
/**
* Adds multiple layers to the map.
*/
this.addLayers = (layers = [], options = {}, settings = {}) => {
requestAnimationFrame(() => {
layers.forEach(layer => this.addLayer(layer, options, settings));
geoflo.fire('layers.add', { layers: this.getLayers() });
buildEvents.call(this);
});
return this.getLayers();
};
/**
* Adds a single layer to the map.
*/
this.addLayer = (layer, options = {}, settings = {}) => {
if (!layer || !layer.id) return false;
layer.metadata = layer.metadata || options;
if (map.getLayer(layer.id)) {
layer.layer = map.getLayer(layer.id);
layer.settings = settings;
if (!this.layersMap.has(layer.id)) this.layersMap.set(layer.id, layer);
return this.getLayer(layer.id);
}
map.addLayer(layer);
layer.layer = map.getLayer(layer.id);
layer.settings = settings;
layer.currentFilter = layer.filter || layer.layer.filter || layer.settings?.filter || null;
this.layersMap.set(layer.id, layer);
geoflo.fire('layer.add', { id: layer.id, layer: layer });
return layer;
};
/**
* Adds text layers for labeling.
* (Currently disabled – remove the "return;" to enable.)
*/
this.addTextLayer = (options = {}) => {
return; // Disabled for now.
const layers = options.select
? this.getLayer(geoflo.statics.constants.sources.SELECT)
: this.getCustomLayers();
const field = options.field || 'text';
this.removeTextLayer(options);
layers.forEach(layer => {
const id = `${layer.id}-Text`;
let filter = ['all', ['==', ["geometry-type"], 'Point'], ["has", field]];
if (options.filter) filter = options.filter;
if (!options.select && options.ids) filter = ['in', 'id', ...options.ids];
const layout = {
visibility: options.visibility || 'visible',
'symbol-placement': 'point',
'text-rotation-alignment': 'viewport',
'text-field': ['get', field],
'text-keep-upright': true,
'text-allow-overlap': true,
'text-anchor': 'top',
'text-size': 12,
'text-justify': 'center',
'text-letter-spacing': 0.25,
'text-line-height': 1.2,
'text-max-angle': 10,
'text-offset': [0, 0.5],
'text-padding': 2,
'text-rotate': 0,
'text-transform': 'none',
'text-font': ['Arial Unicode MS Regular', 'DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-ignore-placement': false,
'text-max-width': 15,
...(layer.text?.layout || {}),
...(options.layout || {})
};
const paint = {
'text-translate-anchor': 'viewport',
'text-halo-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'text-halo-width': 1.2,
'text-color': ['get', 'secondaryColor', ['get', 'style', ['properties']]],
'text-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, ['get', 'opacity', ['get', 'style', ['properties']]]],
...(layer.text?.paint || {}),
...(options.paint || {})
};
const textLayer = {
id,
type: 'symbol',
source: layer.source || layer.details?.source || id,
slot: 'top',
filter,
layout,
paint,
metadata: { text: true, name: id }
};
if (!map.getLayer(id)) map.addLayer(textLayer);
this.layersMap.set(id, textLayer);
});
this.showTextLayers = true;
};
/**
* Removes sources from the map.
*/
this.removeSources = (ids = []) => {
ids.forEach(id => { if (this.sourcesMap.has(id)) this.removeSource(id); });
geoflo.fire('sources.remove', { removed: true });
};
/**
* Removes a single source from the map.
*/
this.removeSource = (id) => {
if (!id) return false;
if (map.getSource(id)) map.removeSource(id);
this.sourcesMap.delete(id);
geoflo.fire('source.remove', { removed: id });
return id;
};
/**
* Removes multiple layers. Accepts an array of layer objects or IDs.
*/
this.removeLayers = (layers) => {
const ids = Array.isArray(layers) ? layers.map(l => (typeof l === 'object' ? l.id : l)) : this.getLayers().filter(l => l.metadata?.custom).map(l => l.id);
if (!ids.length) return false;
this.removeTextLayer();
ids.forEach(id => this.removeLayer(id));
geoflo.fire('layers.remove', { removed: ids });
return ids;
};
/**
* Removes a single layer from the map.
*/
this.removeLayer = (id) => {
if (!id) return false;
if (map.getLayer(id)) map.removeLayer(id);
const layer = this.layersMap.get(id);
if (layer.settings?.images) layer.settings.images.forEach(image => { if (map.hasImage(image.id)) map.removeImage(image.id) });
this.layersMap.delete(id);
geoflo.fire('layer.remove', { removed: id });
return id;
};
/**
* Removes text layers (layers with metadata.text).
*/
this.removeTextLayer = (options = {}) => {
for (const [id, layer] of this.layersMap.entries()) {
if (layer.metadata && layer.metadata.text && map.getLayer(id)) {
map.removeLayer(id);
this.layersMap.delete(id);
}
}
this.showTextLayers = false;
};
/**
* Moves the specified layers on the map.
*/
this.moveLayers = (layers) => {
const targetLayers = layers || this.getLayers();
targetLayers.forEach(layer => {
if (map.getLayer(layer.id)) {
map.moveLayer(layer.id);
}
});
};
/**
* Handles cluster clicks by expanding clusters.
*/
this.onClusterClick = (feature) => {
if (!feature.source) return false;
const source = map.getSource(feature.source);
if (!source) return false;
source.getClusterExpansionZoom(feature.properties.cluster_id, (err, zoom) => { if (!err) map.easeTo({ center: feature.geometry.coordinates, zoom: zoom + 2 }); });
return false;
};
/**
* Handles mouseover events for layers.
* (Currently commented out – remove comments to enable.)
*/
this.onLayerMouseover = function (event) {
// Uncomment below to enable highlighting on mouseover:
// const MapObj = app.Map;
// if (MapObj.getActions().editing || MapObj.getActions().drawing || MapObj.getActions().viewing) return false;
// if (app[app.ns('layer')]._importing) return false;
// if (!event.features.length) return false;
// MapObj.setHighlight({ clear: true, features: event.features });
};
/**
* Handles mouseout events for layers.
* (Currently commented out – remove comments to enable.)
*/
this.onLayerMouseout = function (event) {
// Uncomment below to enable removing highlights on mouseout:
// const MapObj = app.Map;
// if (MapObj.getActions().editing || MapObj.getActions().drawing || MapObj.getActions().viewing) return false;
// if (app[app.ns('layer')]._importing) return false;
// MapObj._removeHighlight();
};
// --- Helper functions for building layers ---
async function buildLayers(layers = [], options = {}) {
await buildText.call(this);
if (!layers.length) return false;
await Promise.all(layers.map(layer => buildLayer.call(this, layer, options)));
setTimeout(() => { this.moveLayers(); }, 250);
return this.getLayers();
}
async function buildLayer(layer, opts) {
let error;
const details = !layer.details && layer.id ? layer : layer.details || {};
const layerOptions = layer.options || {};
let layersArr = layer.layers || [];
const features = layer.features || [];
const hasFeatures = features && features.length;
let style = layer.style || false;
if (!details.id || !details.type) error = true;
const type = details.type === 'ALL' ? 'ALL' : this.getType(details.type);
if (!type) error = true;
const metadata = { type: details.type };
details.default ? (metadata.default = true) : (metadata.custom = true);
if (details.name) metadata.name = details.name;
const source = details.source || details.id;
metadata.source = source;
if (details.style) delete details.style;
const settings = {
type,
source,
id: details.id,
types: layerTypes[type],
style: style || {},
filter: layer.filter,
images: layer.images,
details,
options: layerOptions,
layers: layersArr
};
if (settings.images) await addImages(settings.images);
if (type === 'ALL') {
const promises = Object.keys(layerTypes)
.filter(key => key !== 'All')
.map(async key => {
const layerConfig = { ...settings, type: key, types: layerTypes[key] };
if (key === 'Image') return buildImage.call(this, layerConfig, layerOptions);
else if (key === 'Polygon') return buildPolygon.call(this, layerConfig, layerOptions);
else if (key === 'Polyline') return buildPolyline.call(this, layerConfig, layerOptions);
else if (key === 'Point') return buildPoint.call(this, layerConfig, layerOptions);
else return [];
});
const results = await Promise.all(promises);
layersArr = results.flat();
} else {
layersArr =
type === 'Image'
? await buildImage.call(this, settings, layerOptions)
: type === 'Polygon'
? await buildPolygon.call(this, settings, layerOptions)
: type === 'Polyline'
? await buildPolyline.call(this, settings, layerOptions)
: type === 'Point'
? await buildPoint.call(this, settings, layerOptions)
: [];
}
settings.metadata = metadata;
this.addSource(source, type, layerOptions);
this.addLayers(layersArr, metadata, settings);
if (hasFeatures) {
geoflo.Features.addFeatures(features.map(feature => ({ ...feature, source })));
}
return new Promise((resolve, reject) => {
if (error) return resolve(error);
const ready = setInterval(() => {
const feats = geoflo.Layers.getFeatures(source);
if (hasFeatures && !feats.length) return;
if (!map.getSource(source)) return;
clearInterval(ready);
resolve({ layer: settings, features: feats });
}, 1);
});
}
async function buildText() {
return new Promise((resolve, reject) => {
const url = 'https://docs.mapbox.com/mapbox-gl-js/assets/popup.png';
if (map.hasImage('text-marker')) return resolve(true);
map.loadImage(url, (error, image) => {
if (error) return reject(error);
if (map.hasImage('text-marker')) return resolve(image);
map.addImage('text-marker', image, {
content: [25, 25, 115, 100],
stretchX: [[25, 115]],
stretchY: [[25, 100]],
pixelRatio: 2,
sdf: false
});
resolve(image);
});
});
}
async function buildImage(settings = {}, options = {}) {
if (!settings.source) return [];
const layersArr = [];
const source = settings.source;
for (const type of settings.types) {
let style = settings.style;
const id = settings.id + type;
let layout = {
visibility: options.visibility || 'visible',
'icon-image': ['get', 'primaryImage', ['get', 'style', ['properties']]],
'icon-size': ['interpolate', ['linear'], ['zoom'], 1, 0.6, 10, 0.8, 15, 1],
'icon-allow-overlap': true,
'icon-anchor': 'bottom'
};
layout = { ...layout, ...(style.image?.layout || {}) };
let paint = {
'icon-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0,
['case', ["boolean", ["feature-state", "hidden"], true], 0,
['get', 'opacity', ['get', 'style', ['properties']]]]]
};
paint = { ...paint, ...(style.image?.paint || {}) };
style = {
id,
type: 'symbol',
source,
slot: style.slot || 'top',
filter: settings.filter || ['==', "$type", "Point"],
layout,
paint
};
if (style.minzoom) style.minzoom = style.minzoom;
if (style.maxzoom) style.maxzoom = style.maxzoom;
layersArr.push(style);
}
return layersArr;
}
async function buildPolygon(settings = {}, options = {}) {
if (!settings.source) return [];
const layersArr = [];
const source = settings.source;
for (const type of settings.types) {
let style = settings.style;
const id = settings.id + type;
let layout, paint;
if (type.includes('border')) {
layout = { visibility: options.visibility || 'visible', ...((style.border && style.border.layout) || {}) };
paint = {
'line-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'line-width': ['case', ["boolean", ['has', 'width', ['get', 'style', ['properties']]], true],
['get', 'width', ['get', 'style', ['properties']]], 2],
'line-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 0.8],
...((style.border && style.border.paint) || {})
};
style = {
id,
type: 'line',
source,
slot: style.slot || 'bottom',
filter: (style.border && style.border.filter) || ['==', "$type", "Polygon"],
layout,
paint
};
} else if (type.includes('fill')) {
layout = { visibility: options.visibility || 'visible', ...((style.fill && style.fill.layout) || {}) };
paint = {
'fill-color': ['get', 'secondaryColor', ['get', 'style', ['properties']]],
'fill-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 0.5],
...((style.fill && style.fill.paint) || {})
};
style = {
id,
type: 'fill',
source,
slot: style.slot || 'bottom',
filter: (style.fill && style.fill.filter) || ['==', "$type", "Polygon"],
layout,
paint
};
}
if (settings.style.minzoom) style.minzoom = settings.style.minzoom;
if (settings.style.maxzoom) style.maxzoom = settings.style.maxzoom;
layersArr.push(style);
}
return layersArr;
}
async function buildPolyline(settings = {}, options = {}) {
if (!settings.source) return [];
const layersArr = [];
const source = settings.source;
for (const type of settings.types) {
let style = settings.style;
const id = settings.id + type;
let layout, paint;
if (type.includes('line')) {
layout = {
visibility: options.visibility || 'visible',
'line-miter-limit': 2,
'line-join': 'round',
'line-cap': 'round',
...((style.line && style.line.layout) || {})
};
paint = {
'line-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'line-width': ['case', ["boolean", ['has', 'width', ['get', 'style', ['properties']]], true],
['get', 'width', ['get', 'style', ['properties']]], 4],
'line-offset': ['case', ["boolean", ["has", "offset"], true], ["get", "offset"], 0],
'line-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0, 1],
...((style.line && style.line.paint) || {})
};
style = {
id,
type: 'line',
source,
slot: style.slot || 'middle',
filter: (style.line && style.line.filter) || ['==', "$type", "LineString"],
layout,
paint
};
}
if (settings.style.minzoom) style.minzoom = settings.style.minzoom;
if (settings.style.maxzoom) style.maxzoom = settings.style.maxzoom;
layersArr.push(style);
}
return layersArr;
}
async function buildPoint(settings = {}, options = {}) {
if (!settings.source) return [];
const layersArr = [];
const source = settings.source;
const dontRender = false;
for (const type of settings.types) {
let style = settings.style;
const id = settings.id + type;
let layout, paint;
if (type.includes('circle')) {
if (options.noCircle) continue;
layout = { visibility: options.visibility || 'visible', ...((style.circle && style.circle.layout) || {}) };
paint = {
'circle-radius': 10,
'circle-stroke-width': 2,
'circle-color': ['get', 'secondaryColor', ['get', 'style', ['properties']]],
'circle-stroke-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'circle-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0,
['case', ["boolean", ["feature-state", "hidden"], true], 0,
['get', 'opacity', ['get', 'style', ['properties']]]]],
'circle-stroke-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0,
['case', ["boolean", ["feature-state", "hidden"], true], 0,
['get', 'opacity', ['get', 'style', ['properties']]]]],
...((style.circle && style.circle.paint) || {})
};
style = {
id,
type: 'circle',
source,
slot: style.slot || 'top',
filter: (style.circle && style.circle.filter) || ['==', "$type", "Point"],
layout,
paint
};
if (type.includes('cluster')) {
if (options.noCluster) continue;
style.filter = ['has', 'point_count'];
style.paint['circle-color'] = options.secondaryColor || geoflo.options.colors.secondaryColor;
style.paint['circle-stroke-color'] = options.primaryColor || geoflo.options.colors.primaryColor;
}
} else if (type.includes('icon')) {
if (dontRender) continue;
layout = {
visibility: options.visibility || 'visible',
'icon-optional': true,
'text-field': ['get', 'primaryIcon', ['get', 'style', ['properties']]],
'text-rotate': ['get', 'rotate', ['get', 'style', ['properties']]],
'text-rotation-alignment': 'viewport',
'text-size': 14,
'text-line-height': 1,
'text-padding': 0,
'text-offset': [0, 0.2],
'text-justify': 'auto',
'text-anchor': 'center',
'text-allow-overlap': true,
'text-font': ['Font Awesome 6 Pro Solid'],
'text-ignore-placement': true,
...((style.icon && style.icon.layout) || {})
};
paint = {
'text-translate-anchor': 'viewport',
'text-halo-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'text-halo-width': 0,
'text-color': [
'case',
['has', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primarySelect', ['get', 'style', ['properties']]],
['get', 'primaryColor', ['get', 'style', ['properties']]]
],
'text-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0,
['case', ["boolean", ["feature-state", "hidden"], true], 0,
['get', 'opacity', ['get', 'style', ['properties']]]]],
...((style.icon && style.icon.paint) || {})
};
style = {
id,
type: 'symbol',
source,
slot: style.slot || 'top',
filter: (style.icon && style.icon.filter) || ['==', "$type", "Point"],
layout,
paint
};
if (type.includes('cluster')) {
if (options.noCluster) continue;
style.filter = ['has', 'point_count'];
style.layout['text-field'] = options.primaryIcon || '';
style.paint['text-halo-color'] = options.secondaryColor || geoflo.options.colors.secondaryCold;
style.paint['text-color'] = options.primaryColor || geoflo.options.colors.secondaryText;
} else if (type.includes('count')) {
if (options.noCluster) continue;
style.filter = ['has', 'point_count'];
style.layout = {
visibility: options.visibility || 'visible',
'icon-optional': true,
'text-field': options.countIcon || '',
'text-size': { base: 14, stops: [[10, 16], [14, 14]] },
'text-line-height': 1,
'text-padding': 0,
'text-offset': [0.5, -0.6],
'text-justify': 'auto',
'text-anchor': 'center',
'text-allow-overlap': true,
'text-font': ['Font Awesome 6 Pro Solid'],
'text-ignore-placement': true
};
style.paint = {
'text-translate-anchor': 'viewport',
'text-color': options.countIconColor || geoflo.options.colors.primaryText,
'text-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0,
['get', 'opacity', ['get', 'style', ['properties']]]]
};
}
} else if (type.includes('text')) {
if (dontRender) continue;
if (type.includes('count')) {
if (options.noCluster) continue;
layout = {
'text-field': ['get', 'point_count_abbreviated'],
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': { base: 14, stops: [[10, 14], [14, 12]] },
'text-offset': [0.55, -0.9],
...((style.text && style.text.layout) || {})
};
paint = {
'text-color': options.countTextColor || geoflo.options.colors.secondaryText,
'text-opacity': ['case', ["boolean", ["feature-state", "hidden"], true], 0,
['get', 'opacity', ['get', 'style', ['properties']]]],
...((style.text && style.text.paint) || {})
};
style = {
id,
type: 'symbol',
source,
slot: style.slot || 'top',
filter: ['has', 'point_count'],
layout,
paint
};
}
}
if (!style) continue;
if (settings.style.minzoom) style.minzoom = settings.style.minzoom;
if (settings.style.maxzoom) style.maxzoom = settings.style.maxzoom;
layersArr.push(style);
}
return layersArr;
}
async function addImages(images = []) {
for (const image of images) {
if (!image) continue;
if (!map.hasImage(image.id)) {
const img = await loadImage(image);
if (img) map.addImage(image.id, img, { pixelRatio: 2 });
}
}
}
async function loadImage(options = {}) {
if (!options.url || !options.id) return false;
return new Promise((resolve, reject) => {
const url = options.url + '?' + new Date().getTime();
map.loadImage(url, (error, image) => error ? reject(error) : resolve(image));
});
}
function loadImageAsDataURL(imageUrl, callback) {
const img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
const dataURL = canvas.toDataURL("image/png");
callback(dataURL);
};
img.src = imageUrl + '?' + new Date().getTime();
}
function createSVGMarker(options = {}) {
const svgNS = "http://www.w3.org/2000/svg";
const data = options.data;
const width = options.width;
const height = options.height;
const borderWidth = options["stroke-width"] || 5;
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("width", width);
svg.setAttribute("height", height);
svg.setAttribute("viewBox", `0 0 ${width + 2 * borderWidth} ${height + 2 * borderWidth}`);
const marker = document.createElementNS(svgNS, "ellipse");
marker.setAttribute("cx", (width + 2 * borderWidth) / 2);
marker.setAttribute("cy", (height + 2 * borderWidth) / 2);
marker.setAttribute("rx", width / 2);
marker.setAttribute("ry", height / 2);
marker.setAttribute("fill", 'transparent');
marker.setAttribute("stroke", options.stroke || geoflo.getColors().secondaryBackground);
marker.setAttribute("stroke-width", borderWidth);
const image = document.createElementNS(svgNS, "image");
image.setAttributeNS("http://www.w3.org/1999/xlink", "href", data);
image.setAttribute("x", borderWidth);
image.setAttribute("y", borderWidth);
image.setAttribute("width", width);
image.setAttribute("height", height);
image.setAttribute("preserveAspectRatio", "xMidYMid slice");
image.setAttribute("clip-path", "ellipse()");
svg.appendChild(marker);
svg.appendChild(image);
return svg;
}
function svgToImage(svgElement, callback) {
const svgData = new XMLSerializer().serializeToString(svgElement);
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(svgBlob);
const img = new Image();
img.onload = () => { URL.revokeObjectURL(url); callback(img); };
img.src = url;
}
function buildEvents(options = {}) {
const ids = this.getLayerIds();
if (!ids.length) return;
ids.forEach(id => {
if (options.off) {
map.off('mousemove', id, this.onLayerMouseover);
map.off('mouseleave', id, this.onLayerMouseout);
} else if (options.on) {
map.on('mousemove', id, this.onLayerMouseover);
map.on('mouseleave', id, this.onLayerMouseout);
}
});
}
this.init();
};
export default Layers;