Newer
Older
import TileWMS from 'ol/source/TileWMS';
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';
import router from '@/router';

Timothee P
committed
import store from '@/store';
let dictLayersToLeaflet = {};
const mapService = {
layers: [],
mvtLayer: undefined,
content: {},
overlay: {},
map: undefined,

Timothee P
committed
queryParams: {},
createMap(el, options) {
const {
lat,
lng,
mapDefaultViewCenter,
mapDefaultViewZoom,
zoom,
zoomControl = true,
interactions = { doubleClickZoom: false, mouseWheelZoom: false, dragPan: true },
if (el.innerHTML) {
el.innerHTML = '';
}
controls: controls ? controls : [
new Attribution(),
new ScaleLine({
units: 'metric',
})],
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],
zoom: Number(mapDefaultViewZoom) ? mapDefaultViewZoom : zoom,
maxZoom
};
this.map = new Map(mapOptions);
this.map.addControl(new Zoom({ zoomInTipLabel: 'Zoomer', zoomOutTipLabel: 'Dézoomer' }));
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.on('click', this.onMapClick.bind(this));
return this.map;

Timothee P
committed
async addRouterToPopup({ featureTypeSlug, featureId, index }) {
const currentRoute = router.history.current;
const isFeatureBrowsingPage = ( currentRoute.name === 'project_detail' || currentRoute.name === 'liste-signalements');
const searchParams = new URLSearchParams(this.queryParams); // urlSearchParams allow to get rid of undefined values

Timothee P
committed
if (isFeatureBrowsingPage && !index >= 0) {
const url = new URL(`${store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${currentRoute.params.slug}/feature/${featureId}/position-in-list/`);
url.search = searchParams;

Timothee P
committed
const response = await axios.get(url);
index = response.data;
}
function goToBrowseFeatureDetail() {
router.push({
name: 'details-signalement-filtre',

Timothee P
committed
query: {
... Object.fromEntries(searchParams.entries()), // transform search params into object and spread it
offset: index
}
});
}
function goToFeatureTypeDetail() {
router.push({
name: 'details-type-signalement',
params: {

Timothee P
committed
document.getElementById('goToFeatureDetail').onclick = goToBrowseFeatureDetail;
document.getElementById('goToFeatureTypeDetail').onclick = goToFeatureTypeDetail;
},
const features = this.map.getFeaturesAtPixel(event.pixel, {
layerFilter: (l) => l === this.mvtLayer || this.olLayer

Timothee P
committed
if (features && features.length > 0 && this.content) {

Timothee P
committed
console.log(features, router);
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) {

Timothee P
committed
const popupContent = this._createContentPopup(features[0]);
this.content.innerHTML = popupContent.html;
this.overlay.setPosition(event.coordinate);

Timothee P
committed
featureId,
featureTypeSlug: popupContent.feature_type ? popupContent.feature_type.slug : '',
index: popupContent.index,
});
}
//const queryableLayerSelected = document.getElementById(`queryable-layers-selector-${this.wmsParams.basemapId}`).getElementsByClassName('selected')[0].textContent;
if (this.layers) {
'request',
'service',
'srs',
'version',
'bbox',
'height',
'width',
'layers',
'query_layers',
'info_format', 'x', 'y', 'i', 'j',
];
if (queryLayer) {
const url = this.getFeatureInfoUrl(event, queryLayer);
const urlInfos = url.split('?');
const urlParams = new URLSearchParams(urlInfos[1]);
params[param.toLowerCase()] = urlParams.get(param);
});
params.url = urlInfos[0];
axios.get(
window.proxy_url,
{ params }
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
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);
}
}
},
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] });
},
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
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') {
source: new XYZ({
attributions: options.attribution,
url: layer.service.replace('{s}', '{a-c}')
})
});
this.map.addLayer(layerTms);
dictLayersToLeaflet[layer.id] = layerTms;
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
}
});
} 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);
},
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;
const currentValue = properties[colors_style.custom_field_name];
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é'];
return { color, opacity };

Timothee P
committed
addVectorTileLayer: function (url, projectId, featureTypes, formFilters, queryParams) {
const format_cfg = {/*featureClass: Feature*/ };
const mvt = new MVT(format_cfg);
const options = {
urls: [],
matrixSet: 'EPSG:3857'
};
options.format = mvt;
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

Timothee P
committed
this.queryParams = queryParams; // store queryParams for popups
this.map.addLayer(this.mvtLayer);
window.layerMVT = this.mvtLayer;
},
getStyle: function (feature, featureTypes, formFilters) {
const properties = feature.getProperties();
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, opacity } = this.retrieveFeatureStyle(featureType, properties);
color.value && color.value.length ?
color.value : typeof color === 'string' && color.length ?
color : '#000000';
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,
}),
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){
if (formFilters.type && formFilters.type.selected) {
if (featureType.title !== formFilters.type.selected) {
return hiddenStyle;
}
// Filtre sur le statut
if (formFilters.status && formFilters.status.selected.value) {
if (properties.status !== formFilters.status.selected.value) {
return hiddenStyle;
}
// 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;

Timothee P
committed
addFeatures: function ({ features, filter = {}, featureTypes, addToMap = true, queryParams = {} }) {
console.log('addToMap', addToMap);
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,
});

Timothee P
committed
this.drawSource = drawSource;
this.featureTypes = featureTypes; // store featureTypes for popups

Timothee P
committed
this.queryParams = queryParams; // store queryParams for popup routes

Timothee P
committed
removeFeatures: function () {
this.drawSource.clear();
},
addMapEventListener: function (eventName, callback) {
this.map.on(eventName, callback);
},

Timothee P
committed
_createContentPopup: function (feature) {
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;
};

Timothee P
committed
let featureType, status, updated_on, creator, index; // index is used to retrieve feature by query when browsing features
const properties = feature.getProperties();

Timothee P
committed
({ 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 ?
if (status) status = status.name;
updated_on = feature.updated_on;
creator = feature.creator;

Timothee P
committed
if (this.featureTypes) {
featureType = this.featureTypes.find((x) => x.slug === feature.feature_type.slug);
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;

Timothee P
committed
} else if (this.featureTypes) { //* if not, retrieve the name/label from the list
status = statusChoices.find((x) => x.value === status).name;
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>

Timothee P
committed
Type : ${ featureType ? '<a id="goToFeatureTypeDetail" class="pointer">' + featureType.title + '</a>' : 'Type de signalement inconnu' }
Dernière mise à jour : ${updated_on}

Timothee P
committed
${author}`;
return { html, featureType, 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'));
const pos = fromLonLat(loc);
const marker = new Overlay({