Skip to content
Snippets Groups Projects
map-service.js 21 KiB
Newer Older
DESPRES Damien's avatar
DESPRES Damien committed
import TileWMS from 'ol/source/TileWMS';
import { View, Map } from 'ol';
import { ScaleLine, Zoom, Attribution, FullScreen } from 'ol/control';
DESPRES Damien's avatar
DESPRES Damien committed
import TileLayer from 'ol/layer/Tile';
import { transform, transformExtent } from 'ol/proj.js';
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 } 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 { fromLonLat } from 'ol/proj.js';
import OverlayPositioning from 'ol/OverlayPositioning';
import axios from '@/axios-client.js';
import router from '@/router';
import { statusChoices } from '@/utils';
let dictLayersToMap = {};
DESPRES Damien's avatar
DESPRES Damien committed

const mapService = {
  layers: [],

  mvtLayer: undefined,

  content: {},

  overlay: {},

  map: undefined,

DESPRES Damien's avatar
DESPRES Damien committed

  getMap() {
    return this.map;
  },
DESPRES Damien's avatar
DESPRES Damien committed
  destroyMap() {
    this.map = undefined;
DESPRES Damien's avatar
DESPRES Damien committed
  },
DESPRES Damien's avatar
DESPRES Damien committed
  createMap(el, options) {
    const {
      lat,
      lng,
      mapDefaultViewCenter,
      mapDefaultViewZoom,
DESPRES Damien's avatar
DESPRES Damien committed
      zoom,
      zoomControl = true,
      fullScreenControl = false,
DESPRES Damien's avatar
DESPRES Damien committed
      interactions = { doubleClickZoom: false, mouseWheelZoom: false, dragPan: true },
      controls = [
        new Attribution({ collapsible: false }),
        new ScaleLine({
          units: 'metric',
        }),
      ],
DESPRES Damien's avatar
DESPRES Damien committed
    } = options;
    if (fullScreenControl) {
      controls.push(new FullScreen());
    }
    const mapOptions = {
DESPRES Damien's avatar
DESPRES Damien committed
      layers: [],
      target: el,
      controls,
DESPRES Damien's avatar
DESPRES Damien committed
      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],
DESPRES Damien's avatar
DESPRES Damien committed

        ], 'EPSG:4326', 'EPSG:3857'),
        zoom: Number(mapDefaultViewZoom) ? mapDefaultViewZoom : zoom,
        maxZoom
DESPRES Damien's avatar
DESPRES Damien committed
      }),
    };

    this.map = new Map(mapOptions);
Florent Lavelle's avatar
Florent Lavelle committed

DESPRES Damien's avatar
DESPRES Damien committed
    if (zoomControl) {
DESPRES Damien's avatar
DESPRES Damien committed
      this.map.addControl(new Zoom({ zoomInTipLabel: 'Zoomer', zoomOutTipLabel: 'Dézoomer' }));
DESPRES Damien's avatar
DESPRES Damien committed
    }
DESPRES Damien's avatar
DESPRES Damien committed
    this.map.once('rendercomplete', () => {
      this.map.updateSize();
DESPRES Damien's avatar
DESPRES Damien committed
    });
    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;
DESPRES Damien's avatar
DESPRES Damien committed
    this.map.addOverlay(this.overlay);
DESPRES Damien's avatar
DESPRES Damien committed
    this.map.on('click', this.onMapClick.bind(this));
    return this.map;
DESPRES Damien's avatar
DESPRES Damien committed
  },
Florent Lavelle's avatar
Florent Lavelle committed

  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()}`);
    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',
          ...Object.fromEntries(searchParams.entries()), // transform search params into object and spread it into query
    };

    function goToFeatureDetail() {
      router.push({
        name: 'details-signalement',
        params: {
          slug_type_signal: featureTypeSlug,
          slug_signal: featureId,
        },
      });
      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');
    document.getElementById('goToFeatureDetail').onclick = isFeatureBrowsing ? goToBrowseFeatureDetail : goToFeatureDetail;
    document.getElementById('goToFeatureTypeDetail').onclick = goToFeatureTypeDetail;
  },

DESPRES Damien's avatar
DESPRES Damien committed
  onMapClick(event) {
    //* retrieve features under pointer
DESPRES Damien's avatar
DESPRES Damien committed
    const features = this.map.getFeaturesAtPixel(event.pixel, {
      layerFilter: (l) => l === this.mvtLayer || this.olLayer
DESPRES Damien's avatar
DESPRES Damien committed
    });
    //* 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 = this._createContentPopup(features[0]);
        this.content.innerHTML = popupContent.html;
        this.overlay.setPosition(event.coordinate);
        this.addRouterToPopup({
          featureTypeSlug: popupContent.featureType ? popupContent.featureType.slug : '',
          index: popupContent.index,
        });
DESPRES Damien's avatar
DESPRES Damien committed
    }
    //const queryableLayerSelected = document.getElementById(`queryable-layers-selector-${this.wmsParams.basemapId}`).getElementsByClassName('selected')[0].textContent;
    if (this.layers) {
Florent Lavelle's avatar
Florent Lavelle committed
      const queryLayer = this.layers.find(x => x.query);
DESPRES Damien's avatar
DESPRES Damien committed
      // pour compatibilité avec le proxy django
Florent Lavelle's avatar
Florent Lavelle committed
      const proxyparams = [
DESPRES Damien's avatar
DESPRES Damien committed
        'request',
        'service',
        'srs',
        'version',
        'bbox',
        'height',
        'width',
        'layers',
        'query_layers',
        'info_format', 'x', 'y', 'i', 'j',
      ];
      if (queryLayer) {
Florent Lavelle's avatar
Florent Lavelle committed
        const url = this.getFeatureInfoUrl(event, queryLayer);
        const urlInfos = url.split('?');
DESPRES Damien's avatar
DESPRES Damien committed
        const urlParams = new URLSearchParams(urlInfos[1]);
Florent Lavelle's avatar
Florent Lavelle committed
        const params = {};
DESPRES Damien's avatar
DESPRES Damien committed
        Array.from(urlParams.keys()).forEach(param => {
Florent Lavelle's avatar
Florent Lavelle committed
          if (proxyparams.indexOf(param.toLowerCase()) >= 0) {
DESPRES Damien's avatar
DESPRES Damien committed
            params[param.toLowerCase()] = urlParams.get(param);
Florent Lavelle's avatar
Florent Lavelle committed
          }
DESPRES Damien's avatar
DESPRES Damien committed
        });
        params.url = urlInfos[0];
        axios.get(
          window.proxy_url,
DESPRES Damien's avatar
DESPRES Damien committed
        ).then(response => {
Florent Lavelle's avatar
Florent Lavelle committed
          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) {
DESPRES Damien's avatar
DESPRES Damien committed
    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
Florent Lavelle's avatar
Florent Lavelle committed
      const contentLines = [];
DESPRES Damien's avatar
DESPRES Damien committed
      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);
      }
    }
  },
DESPRES Damien's avatar
DESPRES Damien committed
  getFeatureInfoUrl(event, layer) {
    const olLayer = dictLayersToMap[layer.id];
DESPRES Damien's avatar
DESPRES Damien committed
    const source = olLayer.getSource();
    const viewResolution = this.map.getView().getResolution();
    let url;
Florent Lavelle's avatar
Florent Lavelle committed
    const wmsOptions = { info_format: 'application/json', query_layers: layer.options.layers };
DESPRES Damien's avatar
DESPRES Damien committed
    if (source && source.getFeatureInfoUrl) {
      url = source.getFeatureInfoUrl(event.coordinate, viewResolution, 'EPSG:3857', wmsOptions);
    }
    return url;
  },
DESPRES Damien's avatar
DESPRES Damien committed
  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] });

  },
DESPRES Damien's avatar
DESPRES Damien committed
  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
DESPRES Damien's avatar
DESPRES Damien committed
      let count = 0;
DESPRES Damien's avatar
DESPRES Damien committed
      layers.forEach((layer) => {
        if (layer) {
Florent Lavelle's avatar
Florent Lavelle committed
          const options = layer.options;
DESPRES Damien's avatar
DESPRES Damien committed
          if (options) {
            options.noWrap = true;
            options['opacity'] = layer.opacity;
DESPRES Damien's avatar
DESPRES Damien committed
            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);
DESPRES Damien's avatar
DESPRES Damien committed
            } else if (layer.schema_type === 'tms') {
DESPRES Damien's avatar
DESPRES Damien committed
              const layerTms = new TileLayer({
DESPRES Damien's avatar
DESPRES Damien committed
                source: new XYZ({
                  attributions: options.attribution,
                  url: layer.service.replace('{s}', '{a-c}')
                })
              });
              layerTms.setOpacity(parseFloat(options.opacity));
DESPRES Damien's avatar
DESPRES Damien committed
              this.map.addLayer(layerTms);
              dictLayersToMap[layer.id] = layerTms;
          dictLayersToMap[layer.id].setZIndex(count);
DESPRES Damien's avatar
DESPRES Damien committed
        }
      });
    } else { //* else when no basemaps defined
      optionsMap.noWrap = true;
      if (schemaType === 'wms') {
        this.addWMSLayer(serviceMap, optionsMap);
      } else {
        const layer = new TileLayer({
          source: new XYZ({
            attributions: optionsMap.attribution,
            url: serviceMap.replace('{s}', '{a-c}')
          })
        });
        this.map.addLayer(layer);
      }
    }
  },

  addWMSLayer: function (url, options) {
    options.VERSION = '1.1.1'; // 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),
DESPRES Damien's avatar
DESPRES Damien committed
    });
    this.map.addLayer(layer);
    return layer;
  },

  // Remove the base layers (not the features)
  removeLayers: function () {
    Object.values(dictLayersToMap).forEach(element => {
DESPRES Damien's avatar
DESPRES Damien committed
      this.map.removeLayer(element);
    });
    dictLayersToMap = {};
DESPRES Damien's avatar
DESPRES Damien committed
  },

  updateOpacity(layerId, opacity) {
    const layer = dictLayersToMap[layerId];
DESPRES Damien's avatar
DESPRES Damien committed
    layer.setOpacity(parseFloat(opacity));
  },

  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 fieldType = customfield_set.find((el) => el.name === colors_style.custom_field_name).field_type;
      let currentValue = properties[colors_style.custom_field_name];
      if(currentValue & typeof currentValue === 'string') currentValue = currentValue.trim(); // remove leading and trailing whitespaces
          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'];
        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é'];
    return { color, opacity };
  addVectorTileLayer: function ({ url, projectId, featureTypes, formFilters = {}, queryParams = {} }) {
    const format_cfg = {/*featureClass: Feature*/ };
Florent Lavelle's avatar
Florent Lavelle committed
    const mvt = new MVT(format_cfg);
    const options = {
DESPRES Damien's avatar
DESPRES Damien committed
      urls: [],
      matrixSet: 'EPSG:3857'
    };
    options.format = mvt;
Florent Lavelle's avatar
Florent Lavelle committed
    const layerSource = new VectorTileSource(options);
DESPRES Damien's avatar
DESPRES Damien committed
    layerSource.setTileUrlFunction((p0) => {
Florent Lavelle's avatar
Florent Lavelle committed
      return `${url}/?tile=${p0[0]}/${p0[1]}/${p0[2]}&project_id=${projectId}`;
DESPRES Damien's avatar
DESPRES Damien committed
    });
Florent Lavelle's avatar
Florent Lavelle committed
    const styleFunction = (feature) => this.getStyle(feature, featureTypes, formFilters);
DESPRES Damien's avatar
DESPRES Damien committed
    this.mvtLayer = new VectorTileLayer({
      style: styleFunction,
      source: layerSource
    });
    this.featureTypes = featureTypes; // store featureTypes for popups
    this.queryParams = queryParams; // store queryParams for popups

DESPRES Damien's avatar
DESPRES Damien committed
    this.mvtLayer.setZIndex(30);
DESPRES Damien's avatar
DESPRES Damien committed
    this.map.addLayer(this.mvtLayer);
    window.layerMVT = this.mvtLayer;
  },

Florent Lavelle's avatar
Florent Lavelle committed
  getStyle: function (feature, featureTypes, formFilters) {
    const properties = feature.getProperties();
DESPRES Damien's avatar
DESPRES Damien committed
    let featureType;
    // GeoJSON
    if (properties && properties.feature_type) {
DESPRES Damien's avatar
DESPRES Damien committed
      featureType = featureTypes
        .find((ft) => ft.slug === (properties.feature_type.slug || properties.feature_type));
    } else { //MVT
      featureType = featureTypes.find((x) => x.slug.split('-')[0] === '' + properties.feature_type_id);
    }

    if (featureType) {
      const { color, opacity } = this.retrieveFeatureStyle(featureType, properties);
      //console.log(color, opacity, featureType, properties);
      const colorValue =
        color && color.value && color.value.length ?
          color.value : typeof color === 'string' && color.length ?
            color : '#000000';
      const rgbaColor = asArray(colorValue);
      rgbaColor[3] = opacity || 0.5;//opacity

      const defaultStyle = new Style(
        {
          image: new Circle({
            fill: new Fill(
              {
                color: rgbaColor,
              },
            ),
            stroke: new Stroke(
              {
                color: colorValue,
                width: 2,
              },
            ),
            radius: 5,
          }),
DESPRES Damien's avatar
DESPRES Damien committed
          stroke: new Stroke(
            {
              color: colorValue,
              width: 2,
            },
          ),
          fill: new Fill(
            {
              color: rgbaColor,
            },
          ),
        },
      );
      const hiddenStyle = new Style(); // hide the feature to apply filters
      // Filtre sur le feature type
        if (formFilters.type && formFilters.type.selected) {
          if (featureType.title !== formFilters.type.selected) {
            return hiddenStyle;
          }
DESPRES Damien's avatar
DESPRES Damien committed
        }
        // Filtre sur le statut
        if (formFilters.status && formFilters.status.selected.value) {
          if (properties.status !== formFilters.status.selected.value) {
            return hiddenStyle;
          }
DESPRES Damien's avatar
DESPRES Damien committed
        }
        // Filtre sur le titre
        if (formFilters.title) {
          if (!properties.title.toLowerCase().includes(formFilters.title.toLowerCase())) {
            return hiddenStyle;
          }
      return defaultStyle;
    } else {
      console.error('No corresponding featureType found.');
      return;
  addFeatures: function ({ features, filter = {}, featureTypes, addToMap = true, queryParams = {} }) {
    console.log('addToMap', addToMap);
Florent Lavelle's avatar
Florent Lavelle committed
    const drawSource = new VectorSource();
DESPRES Damien's avatar
DESPRES Damien committed
    let retour;
DESPRES Damien's avatar
DESPRES Damien committed
    features.forEach((feature) => {
        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);
      }
DESPRES Damien's avatar
DESPRES Damien committed
    });
    const styleFunction = (feature) => this.getStyle(feature, featureTypes, filter);
    const olLayer = new VectorLayer({
      source: drawSource,
      style: styleFunction,
    });
DESPRES Damien's avatar
DESPRES Damien committed
    olLayer.setZIndex(29);
DESPRES Damien's avatar
DESPRES Damien committed
    this.map.addLayer(olLayer);
    this.olLayer = olLayer;
    this.featureTypes = featureTypes; // store featureTypes for popups
    this.queryParams = queryParams; // store queryParams for popup routes
DESPRES Damien's avatar
DESPRES Damien committed
    return drawSource;
  },

  removeFeatures: function () {
    this.drawSource.clear();
  },

DESPRES Damien's avatar
DESPRES Damien committed
  addMapEventListener: function (eventName, callback) {
    this.map.on(eventName, callback);
  },

  _createContentPopup: function (feature) {
DESPRES Damien's avatar
DESPRES Damien committed
    const formatDate = (current_datetime) => {
      let formatted_date = current_datetime.getFullYear() + '-' + ('0' + (current_datetime.getMonth() + 1)).slice(-2) + '-' + ('0' + current_datetime.getDate()).slice(-2) + '&nbsp;' +
DESPRES Damien's avatar
DESPRES Damien committed
        ('0' + current_datetime.getHours()).slice(-2) + ':' + ('0' + current_datetime.getMinutes()).slice(-2);
      return formatted_date;
    };
    let featureType, status, updated_on, creator, index; // index is used to retrieve feature by query when browsing features
DESPRES Damien's avatar
DESPRES Damien committed
    if (feature.getProperties) {
      const properties = feature.getProperties();
      ({ status, updated_on, creator, index } = properties); // using parenthesis to allow destructuring object without declaration
      if (this.featureTypes) {
        featureType = feature.getProperties().feature_type ||
          this.featureTypes.find((x) => x.slug.split('-')[0] === '' + feature.getProperties().feature_type_id);
    } else { //? TPD: I couldn't find when this code is used, is this still in use ?
DESPRES Damien's avatar
DESPRES Damien committed
      status = feature.status;
      if (status) status = status.name;
      updated_on = feature.updated_on;
      if (this.featureTypes) {
        featureType = this.featureTypes.find((x) => x.slug === feature.feature_type.slug);
DESPRES Damien's avatar
DESPRES Damien committed
    }
    if (updated_on && !isNaN(new Date(updated_on))) { //* check if it is already formatted
      updated_on = formatDate(new Date(updated_on));
    }
    if (status) {
      if (status.label) { //* when the label is already in the feature
        status = status.label;
      } else if (this.featureTypes) { //* if not, retrieve the name/label from the list
        status = statusChoices.find((x) => x.value === status).name;
DESPRES Damien's avatar
DESPRES Damien committed

    let author = '';
    if (creator) {
      author = creator.full_name
        ? `<div>
              Auteur : ${creator.first_name} ${creator.last_name}
          </div>`
        : creator.username ? `<div>Auteur: ${creator.username}</div>` : '';
    }

    const title = feature.getProperties ? feature.getProperties().title : feature.title;
    const html = `
                  <h4>
                    <a id="goToFeatureDetail" class="pointer">${title}</a>
                  </h4>
                  <div>
                    Statut : ${status}
                  </div>
                  <div>
                    Type : ${featureType ? '<a id="goToFeatureTypeDetail" class="pointer">' + featureType.title + '</a>' : 'Type de signalement inconnu'}
                    Dernière&nbsp;mise&nbsp;à&nbsp;jour&nbsp;:&nbsp;${updated_on}
                  ${author}`;
    return { html, featureType, index };
DESPRES Damien's avatar
DESPRES Damien committed
  },
  zoom(zoomlevel) {
    this.map.getView().setZoom(zoomlevel);
  },

DESPRES Damien's avatar
DESPRES Damien committed
  zoomTo(location, zoomlevel, lon, lat) {
    if (lon && lat) {
      location = [+lon, +lat];
    }
    this.map.getView().setCenter(transform(location, 'EPSG:4326', 'EPSG:3857'));
    this.zoom(zoomlevel);
DESPRES Damien's avatar
DESPRES Damien committed
  },

  addOverlay(loc) {
    const pos = fromLonLat(loc);
    const marker = new Overlay({
DESPRES Damien's avatar
DESPRES Damien committed
      positioning: OverlayPositioning.CENTER_CENTER,
      element: document.getElementById('marker'),
      stopEvent: false
    });
    this.map.addOverlay(marker);
  }
};

export default mapService;