Skip to content
Snippets Groups Projects
map-service.js 17 KiB
Newer Older
DESPRES Damien's avatar
DESPRES Damien committed
import TileWMS from 'ol/source/TileWMS';
import axios from '@/axios-client.js';
import { View, Map } from 'ol';
import { ScaleLine, Zoom, Attribution } from 'ol/control';
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';
DESPRES Damien's avatar
DESPRES Damien committed

let dictLayersToLeaflet = {};
let statusList = [
  {
    name: 'Brouillon',
    value: 'draft',
  },
  {
    name: 'Publié',
    value: 'published',
  },
  {
    name: 'Archivé',
    value: 'archived',
  },
  {
    name: 'En attente de publication',
    value: 'pending',
  },
];

const mapService = {
  layers: [],

  mvtLayer: undefined,

  content: {},

  overlay: {},

  map: undefined,


  getMap() {
    return this.map;
  },
  destroyMap() {
    this.map=undefined;
  },
  createMap(el, options) {
    const {
      lat,
      lng,
      mapDefaultViewCenter,
      mapDefaultViewZoom,
      zoom,
      zoomControl = true,
      interactions = { doubleClickZoom: false, mouseWheelZoom: false, dragPan: true },
    } = options;
DESPRES Damien's avatar
DESPRES Damien committed
    this.map = new Map({
DESPRES Damien's avatar
DESPRES Damien committed
      layers: [],
      target: el,
      controls: [
        new Attribution(),
        new ScaleLine({
          units: 'metric',
        })],
      interactions: defaults(interactions),
      view: new View({
        center: transform([
          !lng ? mapDefaultViewCenter[1] : lng,
          !lat ? mapDefaultViewCenter[0] : lat,

        ], 'EPSG:4326', 'EPSG:3857'),
        zoom: !zoom ? mapDefaultViewZoom : zoom
      }),
    });
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;
    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
  },
 
  onMapClick(event) {
    let self = this;
    const features = this.map.getFeaturesAtPixel(event.pixel, {
      layerFilter(l) {
        return l === self.mvtLayer;
      },
    });
    if (features && features.length > 0) {
      const popupContent = this._createContentPopup(features[0],
DESPRES Damien's avatar
DESPRES Damien committed
        this.mvtLayer.featureTypes, this.mvtLayer.project_slug);
DESPRES Damien's avatar
DESPRES Damien committed
      this.content.innerHTML = popupContent;
      this.overlay.setPosition(event.coordinate);
    }
    //const queryableLayerSelected = document.getElementById(`queryable-layers-selector-${this.wmsParams.basemapId}`).getElementsByClassName('selected')[0].textContent;
    if (this.layers) {
      let queryLayer = this.layers.find(x => x.query);
      // pour compatibilité avec le proxy django
      let proxyparams = [
        'request',
        'service',
        'srs',
        'version',
        'bbox',
        'height',
        'width',
        'layers',
        'query_layers',
        'info_format', 'x', 'y', 'i', 'j',
      ];
      if (queryLayer) {
        let url = this.getFeatureInfoUrl(event, queryLayer);
        let urlInfos = url.split('?');
        const urlParams = new URLSearchParams(urlInfos[1]);
        let params = {};
        Array.from(urlParams.keys()).forEach(param => {
          if (proxyparams.indexOf(param.toLowerCase()) >= 0)
            params[param.toLowerCase()] = urlParams.get(param);
        });
        params.url = urlInfos[0];
        let self = this;
        axios.get(
          window.proxy_url,
          {
            params: params,
          }
        ).then(response => {
          let data = response.data;
          var err = typeof data === 'object' ? null : data;
          if (data.features || err) {
            self.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
      let 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) {
    let olLayer = dictLayersToLeaflet[layer.id];
    const source = olLayer.getSource();
    const viewResolution = this.map.getView().getResolution();
    let url;
    let 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] });

  },
  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
      layers.forEach((layer) => {
        if (layer) {
          let options = layer.options;
          if (options) {
            options.noWrap = true;
            options.opacity = layer.opacity;
            if (layer.schema_type === 'wms') {
              if (layer.queryable) {
                options.title = layer.title;
                dictLayersToLeaflet[layer.id] = this.addWMSLayer(layer.service, options);
              } else {
                dictLayersToLeaflet[layer.id] = this.addWMSLayer(layer.service, options);
              }
            } 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}')
                })
              });
DESPRES Damien's avatar
DESPRES Damien committed
              this.map.addLayer(layerTms);
              dictLayersToLeaflet[layer.id] = layerTms;
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
    });
    this.map.addLayer(layer);
    return layer;
  },

  // Remove the base layers (not the features)
  removeLayers: function () {
    Object.values(dictLayersToLeaflet).forEach(element => {
      this.map.removeLayer(element);
    });

    dictLayersToLeaflet = {};
  },

  updateOpacity(layerId, opacity) {
    const layer = dictLayersToLeaflet[layerId];
    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);
  },

  retrieveFeatureColor: function (featureType, properties) {
    const colorsStyle = featureType.colors_style;
    if (featureType && colorsStyle && colorsStyle.custom_field_name) {
      const currentValue = properties[colorsStyle.custom_field_name];
      const colorStyle = colorsStyle.colors[currentValue];
      return colorStyle ? colorStyle : featureType.color;
    } else {
      return featureType.color;
    }
  },

  addVectorTileLayer: function (url, project_id, project_slug, featureTypes, form_filters) {
    let format_cfg = {/*featureClass: Feature*/ };

    let mvt = new MVT(format_cfg);
    let options = {
      urls: [],
      matrixSet: 'EPSG:3857'
    };
    options.format = mvt;
    let layerSource = new VectorTileSource(options);
    layerSource.setTileUrlFunction((p0) => {
      return url+'/?tile=' + p0[0] + '/' + p0[1] + '/' + p0[2] + '&project_id=' + project_id;
    });
    const styleFunction = (feature) => this.getStyle(feature, featureTypes, form_filters);
    this.mvtLayer = new VectorTileLayer({
      style: styleFunction,
      source: layerSource
    });
    this.mvtLayer.featureTypes = featureTypes;
    this.mvtLayer.project_slug = project_slug;
    this.map.addLayer(this.mvtLayer);
    window.layerMVT = this.mvtLayer;
  },

  getStyle: function (feature, featureTypes, form_filters) {
    let properties = feature.getProperties();
    let featureType;
    // GeoJSON
    if(properties.feature_type){
      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);
    }
    const color = this.retrieveFeatureColor(featureType, properties);
    const colorValue =
      color.value && color.value.length ?
        color.value : typeof color === 'string' && color.length ?
          color : '#000000';

    let rgbaColor = asArray(colorValue);
    rgbaColor[3] = 0.5;//opacity
    const hiddenStyle = new Style();

    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,
          },
        ),
      },
    );

    // Filtre sur le feature type
    if(form_filters){
      if (form_filters.type && form_filters.type.selected) {
        if (featureType.title !== form_filters.type.selected) {
          return hiddenStyle;
        }
      }
      // Filtre sur le statut
      if (form_filters.status && form_filters.status.selected.value) {
        if (properties.status !== form_filters.status.selected.value) {
          return hiddenStyle;
        }
      }
      // Filtre sur le titre
      if (form_filters.title) {
        if (!properties.title.toLowerCase().includes(form_filters.title.toLowerCase())) {
          return hiddenStyle;
        }
      }
    }
   
    return defaultStyle;
  },

  addFeatures: function (features, filter, addToMap = true, featureTypes) {
    console.log(addToMap);
DESPRES Damien's avatar
DESPRES Damien committed
    let drawSource = new VectorSource();
    let retour;
    // TODO verifier utilité de cette boucle et remplacer par readFeatures plutot
    features.forEach((feature) => {
      retour = new GeoJSON().readFeature(feature, { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' });
      drawSource.addFeature(retour);
      const featureProperties = feature.properties ? feature.properties : feature;
      const featureType = featureTypes
        .find((ft) => ft.slug === (featureProperties.feature_type.slug || featureProperties.feature_type));
      let filters = [];
      if (filter) {
        const typeCheck = filter.featureType && featureProperties.feature_type.slug === filter.featureType;
        const statusCheck = filter.featureStatus && featureProperties.status.value === filter.featureStatus;
        const titleCheck = filter.featureTitle && featureProperties.title.includes(filter.featureTitle);
        filters = [typeCheck, statusCheck, titleCheck];
      }
      console.log(featureType, filters);

    });
    const styleFunction = (feature) => this.getStyle(feature, featureTypes, filter);
    const olLayer = new VectorLayer({
      source: drawSource,
      style: styleFunction,
    });

    this.map.addLayer(olLayer);
    return drawSource;
  },

  addMapEventListener: function (eventName, callback) {
    this.map.on(eventName, callback);
  },

  _createContentPopup: function (feature, featureTypes, project_slug) {
    const formatDate = (current_datetime) => {
      let formatted_date = current_datetime.getFullYear() + '-' + ('0' + (current_datetime.getMonth() + 1)).slice(-2) + '-' + ('0' + current_datetime.getDate()).slice(-2) + ' ' +
        ('0' + current_datetime.getHours()).slice(-2) + ':' + ('0' + current_datetime.getMinutes()).slice(-2);
      return formatted_date;
    };
    let feature_type;
    let status;
    let date_maj;
    let feature_type_url;
    let feature_url;

    if (feature.getProperties) {
      status = feature.getProperties().status;
      date_maj = feature.getProperties().updated_on;
      feature_type_url = feature.getProperties().feature_type_url;
      feature_url = feature.getProperties().feature_url;
    } else {
      status = feature.status;
      date_maj = feature.updated_on;
      feature_type_url = feature.feature_type_url;
      feature_url = feature.feature_url;
    }

    if (featureTypes) { // => VectorTile
      feature_type = featureTypes.find((x) => x.slug.split('-')[0] === '' + feature.getProperties().feature_type_id);
      status = statusList.find((x) => x.value === feature.getProperties().status).name;
      date_maj = formatDate(new Date(feature.getProperties().updated_on));
      feature_type_url = '/geocontrib/projet/' + project_slug + '/type-signalement/' + feature_type.slug + '/';
      feature_url = feature_type_url + 'signalement/' + feature.getProperties().feature_id + '/';
    } else {
      feature_type = feature.getProperties ? feature.getProperties().feature_type : feature.feature_type;
      status = feature.getProperties ? feature.getProperties().status.label : feature.status.label;
    }

    //* adapt link url for shared-project restricted navigation
    if (window.location.pathname.includes('projet-partage')) {
      feature_url = feature_url.replace('projet', 'projet-partage');
      feature_type_url = feature_type_url.replace('projet', 'projet-partage');
    }
    let author = '';
    const creator = feature.getProperties ? feature.getProperties().creator : feature.creator;
    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;
    return `
              <h4>
                  <a href="${feature_url}">${title}</a>
              </h4>
              <div>
                  Statut : ${status}
              </div>
              <div>
                  Type : <a href="${feature_type_url}"> ${feature_type.title} </a>
              </div>
              <div>
                  Dernière mise à jour : ${date_maj}
              </div>
              ${author}
          `;
  },
  zoomTo(location, zoomlevel, lon, lat) {
    if (lon && lat) {
      location = [+lon, +lat];
    }
    this.map.getView().setCenter(transform(location, 'EPSG:4326', 'EPSG:3857'));
    this.map.getView().setZoom(zoomlevel);
  },

  addOverlay(loc) {
    var pos = fromLonLat(loc);
    var marker = new Overlay({
      position: pos, 
      positioning: OverlayPositioning.CENTER_CENTER,
      element: document.getElementById('marker'),
      stopEvent: false
    });
    this.map.addOverlay(marker);
  }
};

export default mapService;