import TileWMS from 'ol/source/TileWMS'; import { View, Map } from 'ol'; import { ScaleLine, Zoom, Attribution, FullScreen } from 'ol/control'; import TileLayer from 'ol/layer/Tile'; import { transform, transformExtent, fromLonLat/* , get as getProjection */ } from 'ol/proj'; import { defaults } from 'ol/interaction'; import XYZ from 'ol/source/XYZ'; import VectorTileLayer from 'ol/layer/VectorTile'; import VectorTileSource from 'ol/source/VectorTile'; import { MVT, GeoJSON } from 'ol/format'; import { boundingExtent/* , getTopLeft, getWidth */ } from 'ol/extent'; import Overlay from 'ol/Overlay'; import { Fill, Stroke, Style, Circle //RegularShape, Circle as CircleStyle, Text,Icon } from 'ol/style'; import { asArray } from 'ol/color'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; //import WMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; import WMTS, { optionsFromCapabilities } from 'ol/source/WMTS'; import WMTSCapabilities from 'ol/format/WMTSCapabilities'; import OverlayPositioning from 'ol/OverlayPositioning'; import Geolocation from 'ol/Geolocation.js'; import Feature from 'ol/Feature.js'; import Point from 'ol/geom/Point.js'; import axios from '@/axios-client.js'; import router from '@/router'; import store from '@/store'; import { retrieveFeatureProperties } from '@/utils'; const parser = new WMTSCapabilities(); let dictLayersToMap = {}; let layersCount = 0; const geolocationStyle = new Style({ image: new Circle({ radius: 6, fill: new Fill({ color: '#3399CC', }), stroke: new Stroke({ color: '#fff', width: 2, }), }), }); const mapService = { layers: [], mvtLayer: undefined, content: {}, overlay: {}, map: undefined, queryParams: {}, geolocation: undefined, // for geolocation geolocationSource: null, // for geolocation positionFeature: null, // for geolocation lastPosition: null, // for geolocation getMap() { return this.map; }, destroyMap() { this.map = undefined; }, createMap(el, options) { const { lat, lng, mapDefaultViewCenter, mapDefaultViewZoom, maxZoom, zoom, zoomControl = true, fullScreenControl = false, geolocationControl = false, interactions = { doubleClickZoom: false, mouseWheelZoom: false, dragPan: true }, controls = [ new Attribution({ collapsible: false }), new ScaleLine({ units: 'metric', }), ], } = options; if (fullScreenControl) { controls.push(new FullScreen({ tipLabel: 'Mode plein écran' })); } const mapOptions = { layers: [], target: el, controls, interactions: defaults(interactions), view: new View({ center: transform([ //* since 0 is considered false, check for number instead of just defined (though boolean will pass through) Number(lng) ? lng : mapDefaultViewCenter[1], Number(lat) ? lat : mapDefaultViewCenter[0], ], 'EPSG:4326', 'EPSG:3857'), zoom: Number(mapDefaultViewZoom) ? mapDefaultViewZoom : zoom, maxZoom }), }; this.map = new Map(mapOptions); if (zoomControl) { this.map.addControl(new Zoom({ zoomInTipLabel: 'Zoomer', zoomOutTipLabel: 'Dézoomer' })); } if (geolocationControl) { this.initGeolocation(); } this.map.once('rendercomplete', () => { this.map.updateSize(); }); const container = document.getElementById('popup'); this.content = document.getElementById('popup-content'); const closer = document.getElementById('popup-closer'); this.overlay = new Overlay({ element: container, autoPan: true, autoPanAnimation: { duration: 500, }, }); let overlay = this.overlay; if (closer) { closer.onclick = function () { overlay.setPosition(undefined); closer.blur(); return false; }; } this.map.addOverlay(this.overlay); this.map.on('click', this.onMapClick.bind(this)); // catch event from sidebarLayer to update layers order (since all maps use it now) document.addEventListener('change-layers-order', (event) => { // Reverse is done because the first layer in order has to be added in the map in last. // Slice is done because reverse() changes the original array, so we make a copy first this.updateOrder(event.detail.layers.slice().reverse()); }); return this.map; }, addRouterToPopup({ featureId, featureTypeSlug, index }) { const getFeaturePosition = async (searchParams) => { const response = await axios.get(`${store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${router.history.current.params.slug}/feature/${featureId}/position-in-list/?${searchParams.toString()}`); return response.data; }; const goToBrowseFeatureDetail = async () => { const currentQuery = { ...this.queryParams }; if (this.queryParams && this.queryParams.filter === 'feature_type_slug') { // when feature_type is the default filter of the project, currentQuery['feature_type_slug'] = featureTypeSlug; // get its slug for the current feature } const searchParams = new URLSearchParams(currentQuery); // urlSearchParams allow to get rid of undefined values if (!index >= 0) { // with mvt feature, there can't be an index index = await getFeaturePosition(searchParams); } router.push({ name: 'details-signalement-filtre', query: { ...Object.fromEntries(searchParams.entries()), // transform search params into object and spread it into query offset: index } }); }; function goToFeatureDetail() { router.push({ name: 'details-signalement', params: { slug_type_signal: featureTypeSlug, slug_signal: featureId, }, }); } const goToFeatureTypeDetail = () => { router.push({ name: 'details-type-signalement', params: { feature_type_slug: featureTypeSlug, }, }); }; const isFeatureBrowsing = (router.history.current.name === 'project_detail' || router.history.current.name === 'liste-signalements'); const featureEl = document.getElementById('goToFeatureDetail'); if (featureEl) featureEl.onclick = isFeatureBrowsing ? goToBrowseFeatureDetail : goToFeatureDetail; const featureTypeEl = document.getElementById('goToFeatureTypeDetail'); if (featureTypeEl) featureTypeEl.onclick = goToFeatureTypeDetail; }, async onMapClick (event) { //* retrieve features under pointer const features = this.map.getFeaturesAtPixel(event.pixel, { layerFilter: (l) => l === this.mvtLayer || this.olLayer }); //* prepare popup content if (features && features.length > 0 && this.content) { const featureId = features[0].properties_ ? features[0].properties_.feature_id : features[0].id_; const isEdited = router.history.current.name === 'editer-signalement' && router.history.current.params.slug_signal === featureId; //* avoid opening popup on feature currently edited if (featureId && !isEdited) { const popupContent = await this._createContentPopup(features[0]); this.content.innerHTML = popupContent.html; this.overlay.setPosition(event.coordinate); this.addRouterToPopup({ featureId, featureTypeSlug: popupContent.feature_type ? popupContent.feature_type.slug : '', index: popupContent.index, }); } } else if (this.layers) { // If no feature under the mouse pointer, attempt to find a query layer const queryLayer = this.layers.find(x => x.query); if (queryLayer) { // pour compatibilité avec le proxy django const proxyparams = [ 'request', 'service', 'srs', 'version', 'bbox', 'height', 'width', 'layers', 'query_layers', 'info_format', 'x', 'y', 'i', 'j', ]; const url = this.getFeatureInfoUrl(event, queryLayer); const urlInfos = url ? url.split('?') : []; const urlParams = new URLSearchParams(urlInfos[1]); const params = {}; Array.from(urlParams.keys()).forEach(param => { if (proxyparams.indexOf(param.toLowerCase()) >= 0) { params[param.toLowerCase()] = urlParams.get(param); } }); params.url = urlInfos[0]; axios.get( window.proxy_url, { params } ).then(response => { const data = response.data; const err = typeof data === 'object' ? null : data; if (data.features || err) this.showGetFeatureInfo(err, event, data, queryLayer); }).catch(error => { throw error; }); } } }, showGetFeatureInfo: function (err, event, data, layer) { let content; if (err) { content = ` <h4>${layer.options.title}</h4> <p>Données de la couche inaccessibles</p> `; this.content.innerHTML = content; this.overlay.setPosition(event.coordinate); } else { // Otherwise show the content in a popup const contentLines = []; let contentTitle; if (data.features.length > 0) { Object.entries(data.features[0].properties).forEach(entry => { const [key, value] = entry; if (key !== 'bbox') { contentLines.push(`<div>${key}: ${value}</div>`); } }); contentTitle = `<h4>${layer.options.title}</h4>`; content = contentTitle.concat(contentLines.join('')); this.content.innerHTML = content; this.overlay.setPosition(event.coordinate); } } }, getFeatureInfoUrl(event, layer) { const olLayer = dictLayersToMap[layer.id]; const source = olLayer.getSource(); const viewResolution = this.map.getView().getResolution(); let url; const wmsOptions = { info_format: 'application/json', query_layers: layer.options.layers }; if (source && source.getFeatureInfoUrl) { url = source.getFeatureInfoUrl(event.coordinate, viewResolution, 'EPSG:3857', wmsOptions); } return url; }, fitBounds(bounds) { let ext = boundingExtent([[bounds[0][1], bounds[0][0]], [bounds[1][1], bounds[1][0]]]); ext = transformExtent(ext, 'EPSG:4326', 'EPSG:3857'); this.map.getView().fit(ext, { padding: [25, 25, 25, 25], maxZoom: 16 }); }, fitExtent(ext) { //ext = transformExtent(ext, 'EPSG:4326', 'EPSG:3857'); this.map.getView().fit(ext, { padding: [25, 25, 25, 25] }); }, addLayers: function (layers, serviceMap, optionsMap, schemaType) { this.layers = layers; if (layers) { //* if admin has defined basemaps for this project layersCount = 0; layers.forEach((layer) => this.addConfigLayer(layer)); } else { //* else when no basemaps defined optionsMap.noWrap = true; if (schemaType === 'wms') { this.addWMSLayer(serviceMap, optionsMap); } else if (schemaType === 'wmts') { this.addWMTSLayerFromCapabilities(serviceMap, optionsMap); } else { const layer = new TileLayer({ source: new XYZ({ attributions: optionsMap.attribution, url: serviceMap.replace('{s}', '{a-c}') }) }); this.map.addLayer(layer); } } }, addConfigLayer: async function (layer) { if (layer) { layersCount += 1; const options = layer.options; if (options) { options.noWrap = true; options['opacity'] = layer.opacity; if (layer.schema_type === 'wms') { if (layer.queryable) options['title'] = layer.title; // wasn't effective before, is it still necessary now that title will be added ? dictLayersToMap[layer.id] = this.addWMSLayer(layer.service, options); } else if (layer.schema_type === 'wmts') { const newLayer = await this.addWMTSLayerFromCapabilities(layer.service, options); dictLayersToMap[layer.id] = newLayer; } else if (layer.schema_type === 'tms') { const layerTms = new TileLayer({ source: new XYZ({ attributions: options.attribution, url: layer.service.replace('{s}', '{a-c}') }) }); layerTms.setOpacity(parseFloat(options.opacity)); this.map.addLayer(layerTms); dictLayersToMap[layer.id] = layerTms; } } dictLayersToMap[layer.id].setZIndex(layersCount); } }, addWMSLayer: function (url, options) { options.VERSION = options.version || '1.3.0'; // pour compatibilité avec le proxy django const source = new TileWMS({ attributions: options.attribution, url: url, crossOrigin: 'anonymous', params: options }); const layer = new TileLayer({ source: source, opacity: parseFloat(options.opacity), }); this.map.addLayer(layer); return layer; }, getWMTSLayerCapabilities: async function (url) { // adapted from : https://openlayers.org/en/latest/examples/wmts-layer-from-capabilities.html // get capabilities with request to the service try { const response = await fetch(url); const text = await response.text(); const capabilities = parser.read(text); return capabilities; } catch (error) { console.error(error); } }, addWMTSLayerFromCapabilities: async function (url, options) { // adapted from : https://git.neogeo.fr/onegeo-suite/sites/onegeo-suite-site-maps-vuejs/-/blob/draft/src/services/MapService.ts const wmtsCapabilities = await this.getWMTSLayerCapabilities(url); const { layer, opacity, attributions, format, ignoreUrlInCapabiltiesResponse } = options; let sourceOptions; try { if (format) { sourceOptions = optionsFromCapabilities(wmtsCapabilities, { layer, format }); } else { sourceOptions = optionsFromCapabilities(wmtsCapabilities, { layer }); } } catch (e) { console.error(e); if (e.message == 'projection is null') { return 'Projection non reconnue'; } else { return 'Problème d\'analyse du getCapabilities'; } } if (ignoreUrlInCapabiltiesResponse) { var searchMask = 'request(=|%3D)getCapabilities'; var regEx = new RegExp(searchMask, 'ig'); var replaceMask = ''; sourceOptions.urls[0] = url.replace(regEx, replaceMask); } sourceOptions.attributions = attributions; sourceOptions.crossOrigin= 'anonymous'; if (layer === 'ORTHOIMAGERY.ORTHOPHOTOS') { // un peu bourrin mais il semble y avoir qq chose de spécifique avec cette couche ORTHO // https://geoservices.ign.fr/documentation/services/utilisation-web/affichage-wmts/openlayers-et-wmts sourceOptions.tileGrid = new WMTSTileGrid({ origin: [-20037508,20037508], resolutions: [ 156543.03392804103, 78271.5169640205, 39135.75848201024, 19567.879241005125, 9783.939620502562, 4891.969810251281, 2445.9849051256406, 1222.9924525628203, 611.4962262814101, 305.74811314070485, 152.87405657035254, 76.43702828517625, 38.218514142588134, 19.109257071294063, 9.554628535647034, 4.777314267823517, 2.3886571339117584, 1.1943285669558792, 0.5971642834779396, 0.29858214173896974, 0.14929107086948493, 0.07464553543474241 ], matrixIds: ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19'], }); } const newLayer = new TileLayer({ opacity: parseFloat(opacity) || 1, source: new WMTS(sourceOptions), }); this.map.addLayer(newLayer); return newLayer; }, // Remove the base layers (not the features) removeLayers: function () { Object.values(dictLayersToMap).forEach(element => { this.map.removeLayer(element); }); dictLayersToMap = {}; }, updateOpacity(layerId, opacity) { const layer = dictLayersToMap[layerId]; if (layer) { layer.setOpacity(parseFloat(opacity)); } else { console.error(`Layer with id: ${layerId} couldn't be found for opacity update`); } }, updateOrder(layers) { // First remove existing layers undefined layers = layers.filter(function (x) { return x !== undefined; }); this.removeLayers(); // Redraw the layers this.addLayers(layers); }, retrieveFeatureStyle: function (featureType, properties) { const { colors_style, customfield_set } = featureType; let { color, opacity } = featureType; if (colors_style && colors_style.custom_field_name && customfield_set) { const customField = customfield_set.find((el) => el.name === colors_style.custom_field_name); if (customField) { const fieldType = customField.field_type; let currentValue = properties[colors_style.custom_field_name]; if (currentValue && typeof currentValue === 'string') currentValue = currentValue.trim(); // remove leading and trailing whitespaces switch (fieldType) { case 'list': if (currentValue) { color = colors_style.colors && colors_style.colors[currentValue]; opacity = colors_style.opacities && colors_style.opacities[currentValue]; } break; case 'char': //* if the custom field is supposed to be a string //* check if its current value is empty or not, to select a color | https://redmine.neogeo.fr/issues/14048 color = colors_style.value.colors && colors_style.value.colors[currentValue ? 'Non vide' : 'Vide']; opacity = colors_style.value.opacities && colors_style.value.opacities[currentValue ? 'Non vide' : 'Vide']; break; case 'boolean': color = colors_style.value.colors && colors_style.value.colors[currentValue ? 'Coché' : 'Décoché']; opacity = colors_style.value.opacities && colors_style.value.opacities[currentValue ? 'Coché' : 'Décoché']; break; } } } return { color, opacity }; }, addVectorTileLayer: function ({ url, project_slug, featureTypes, formFilters = {}, queryParams = {} }) { const projectId = project_slug.split('-')[0]; const format_cfg = {/*featureClass: Feature*/ }; const mvt = new MVT(format_cfg); function customLoader(tile, src) { tile.setLoader(function(extent, resolution, projection) { const token = () => { const re = new RegExp('csrftoken=([^;]+)'); const value = re.exec(document.cookie); return (value != null) ? unescape(value[1]) : null; }; fetch(src, { credentials: 'include', headers: { 'X-CSRFToken': token() }, }).then(function(response) { response.arrayBuffer().then(function(data) { const format = tile.getFormat(); // ol/format/MVT configured as source format const features = format.readFeatures(data, { extent: extent, featureProjection: projection }); tile.setFeatures(features); }); }); }); } const options = { urls: [], matrixSet: 'EPSG:3857', tileLoadFunction: customLoader, }; options.format = mvt; const layerSource = new VectorTileSource(options); layerSource.setTileUrlFunction((p0) => { return `${url}/?tile=${p0[0]}/${p0[1]}/${p0[2]}&project_id=${projectId}`; }); const styleFunction = (feature) => this.getStyle(feature, featureTypes, formFilters); this.mvtLayer = new VectorTileLayer({ style: styleFunction, source: layerSource }); this.featureTypes = featureTypes; // store featureTypes for popups this.projectSlug = project_slug; // store projectSlug for popups this.queryParams = queryParams; // store queryParams for popups this.mvtLayer.setZIndex(30); this.map.addLayer(this.mvtLayer); window.layerMVT = this.mvtLayer; }, /** * Determines the style for a given feature based on its type and applicable filters. * * @param {Object} feature - The feature to style. * @param {Array} featureTypes - An array of available feature types. * @param {Object} formFilters - Filters applied through the form. * @returns {ol.style.Style} - The OpenLayers style for the feature. */ getStyle: function (feature, featureTypes, formFilters) { const properties = feature.getProperties(); let featureType; // Determine the feature type. Differentiate between GeoJSON and MVT sources. if (properties && properties.feature_type) { // Handle GeoJSON feature type featureType = featureTypes .find((ft) => ft.slug === (properties.feature_type.slug || properties.feature_type)); } else { // Handle MVT feature type featureType = featureTypes.find((x) => x.slug.split('-')[0] === '' + properties.feature_type_id); } if (featureType) { // Retrieve the style (color, opacity) for the feature. const { color, opacity } = this.retrieveFeatureStyle(featureType, properties); let colorValue = '#000000'; // Default color // Determine the color value based on the feature type. if (color && color.value && color.value.length) { colorValue = color.value; } else if (typeof color === 'string' && color.length) { colorValue = color; } // Convert the color value to RGBA and apply the opacity. const rgbaColor = asArray(colorValue); rgbaColor[3] = opacity || 0.5; // Default opacity // Define the default style for the feature. const defaultStyle = new Style({ image: new Circle({ fill: new Fill({ color: rgbaColor }), stroke: new Stroke({ color: colorValue, width: 2 }), radius: 5, }), stroke: new Stroke({ color: colorValue, width: 2 }), fill: new Fill({ color: rgbaColor }), }); // Define a hidden style to apply when filters are active. const hiddenStyle = new Style(); // Apply filters based on feature type, status, and title. if (formFilters) { if (formFilters.type && formFilters.type.length > 0 && !formFilters.type.includes(featureType.slug)) { return hiddenStyle; } if (formFilters.status && formFilters.status.length > 0 && !formFilters.status.includes(properties.status)) { return hiddenStyle; } if (formFilters.title && !properties.title.toLowerCase().includes(formFilters.title.toLowerCase())) { return hiddenStyle; } } // Return the default style if no filters are applied or if the feature passes the filters. return defaultStyle; } else { console.error('No corresponding featureType found.'); return new Style(); } }, addFeatures: function ({ features, filter = {}, featureTypes, addToMap = true, project_slug, queryParams = {} }) { console.log('addToMap', addToMap); const drawSource = new VectorSource(); let retour; let index = 0; features.forEach((feature) => { try { if (feature.properties) { feature.properties['index'] = index; index += 1; } retour = new GeoJSON().readFeature(feature, { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }, featureTypes); drawSource.addFeature(retour); } catch (err) { console.error(err); } }); const styleFunction = (feature) => this.getStyle(feature, featureTypes, filter); const olLayer = new VectorLayer({ source: drawSource, style: styleFunction, }); olLayer.setZIndex(29); this.map.addLayer(olLayer); this.olLayer = olLayer; this.drawSource = drawSource; this.featureTypes = featureTypes; // store featureTypes for popups this.projectSlug = project_slug; // store projectSlug for popups this.queryParams = queryParams; // store queryParams for popup routes return drawSource; }, removeFeatures: function () { this.drawSource.clear(); }, addMapEventListener: function (eventName, callback) { this.map.on(eventName, callback); }, createCustomFiedsContent(featureType, feature) { const { customfield_set } = featureType; // generate html for each customField configured to be displayed let rows = ''; for (const { label, name } of customfield_set) { const value = feature.getProperties()[name]; // check if the value is not null nor undefined (to allow false value if boolean) if (featureType.displayed_fields.includes(name) && value !== null && value !== undefined) { rows += `<div class="customField-row">${label} : ${value}</div>`; } } // wrap all rows into customFields container return rows.length > 0 ? `<div id="customFields"> <div class="ui divider"></div> <h5>Champs personnalisés</h5> ${rows} </div>` : ''; }, _createContentPopup: async function (feature) { const properties = await retrieveFeatureProperties(feature, this.featureTypes, this.projectSlug); const { feature_type, index, status, updated_on, created_on, creator, display_last_editor } = properties; // index is used to retrieve feature by query when browsing features const { displayed_fields } = feature_type; // generate html for each native fields const statusHtml = `<div>Statut : ${status}</div>`; const featureTypeHtml = `<div>Type de signalement : ${feature_type ? '<a id="goToFeatureTypeDetail" class="pointer">' + feature_type.title + '</a>' : 'Type de signalement inconnu'}</div>`; const updatedOnHtml = `<div>Dernière mise à jour : ${updated_on}</div>`; const createdOnHtml = `<div>Date de création : ${created_on}</div>`; const creatorHtml = creator ? `<div>Auteur : ${creator}</div>` : ''; const lastEditorHtml = display_last_editor ? `<div>Dernier éditeur : ${display_last_editor}</div>` : ''; // wrapping up finale html to fill popup, filtering native fields to display and adding filtered customFields const html = `<h4> <a id="goToFeatureDetail" class="pointer">${feature.getProperties ? feature.getProperties().title : feature.title}</a> </h4> <div class="fields"> ${displayed_fields.includes('status') ? statusHtml : ''} ${displayed_fields.includes('feature_type') ? featureTypeHtml : ''} ${displayed_fields.includes('updated_on') ? updatedOnHtml : ''} ${displayed_fields.includes('created_on') ? createdOnHtml : ''} ${displayed_fields.includes('display_creator') ? creatorHtml : ''} ${displayed_fields.includes('display_last_editor') ? lastEditorHtml : ''} ${this.createCustomFiedsContent(feature_type, feature)} </div>`; return { html, feature_type, index }; }, zoom(zoomlevel) { this.map.getView().setZoom(zoomlevel); }, zoomTo(location, zoomlevel, lon, lat) { if (lon && lat) { location = [+lon, +lat]; } this.map.getView().setCenter(transform(location, 'EPSG:4326', 'EPSG:3857')); this.zoom(zoomlevel); }, animateTo(center, zoom) { this.map.getView().animate({ center, zoom }); }, addOverlay(loc, zoom) { const pos = fromLonLat(loc); const marker = new Overlay({ position: pos, positioning: OverlayPositioning.CENTER_CENTER, element: document.getElementById('marker'), stopEvent: false, }); this.map.addOverlay(marker); this.animateTo(pos, zoom); }, initGeolocation() { this.geolocation = new Geolocation({ // enableHighAccuracy must be set to true to have the heading value. trackingOptions: { enableHighAccuracy: true, }, projection: this.map.getView().getProjection(), }); // handle this.geolocation error. this.geolocation.on('error', (error) => { console.error(error.message); }); this.positionFeature = new Feature(); this.positionFeature.setStyle( geolocationStyle ); this.geolocation.on('change:position', () => { this.lastPosition = this.geolocation.getPosition(); console.log('change:position', this.lastPosition); // keeping this console.log for debug purpose in case needed this.changeTrackerPosition(); }); this.geolocationSource = new VectorSource({ features: [this.positionFeature], }); new VectorLayer({ map: this.map, source: this.geolocationSource, }); }, changeTrackerPosition() { console.log('changeTrackerPosition', this.lastPosition); // keeping this console.log for debug purpose in case needed if (this.lastPosition) { this.positionFeature.setGeometry(new Point(this.lastPosition)); this.animateTo(this.lastPosition, 16); } }, displayGeolocationPoint(isVisible) { let features = this.geolocationSource.getFeatures(); if (!features) return; const hiddenStyle = new Style(); // hide the feature for (let i = 0; i < features.length; i++) { features[i].setStyle(isVisible ? geolocationStyle : hiddenStyle); } }, toggleGeolocation(isTracking) { if (this.geolocation) { this.geolocation.setTracking(isTracking); if (this.geolocationSource) { this.displayGeolocationPoint(isTracking); if (isTracking) { this.changeTrackerPosition(); } } } }, getMapCenter() { const location = this.map.getView().getCenter(); if (location) { return transform(location, 'EPSG:3857', 'EPSG:4326'); } return null; } }; export default mapService;