Newer
Older
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 } 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';
const mapService = {
layers: [],
mvtLayer: undefined,
content: {},
overlay: {},
map: undefined,

Timothee P
committed
queryParams: {},
createMap(el, options) {
const {
lat,
lng,
mapDefaultViewCenter,
mapDefaultViewZoom,
interactions = { doubleClickZoom: false, mouseWheelZoom: false, dragPan: true },
controls = [
new Attribution({ collapsible: false }),
new ScaleLine({
units: 'metric',
}),
],
if (fullScreenControl) {
controls.push(new FullScreen());
}
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
addRouterToPopup({ featureId, featureTypeSlug, index }) {

Timothee P
committed
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()}`);

Timothee P
committed
return response.data;
};

Timothee P
committed
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,

Timothee P
committed
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

Timothee P
committed
if (!index >= 0) { // with mvt feature, there can't be an index
index = await getFeaturePosition(searchParams);
}

Timothee P
committed
router.push({
name: 'details-signalement-filtre',

Timothee P
committed
query: {
...Object.fromEntries(searchParams.entries()), // transform search params into object and spread it into query

Timothee P
committed
};
function goToFeatureDetail() {
router.push({
name: 'details-signalement',
params: {
slug_type_signal: featureTypeSlug,
slug_signal: featureId,
},
});

Timothee P
committed
const goToFeatureTypeDetail = () => {
router.push({
name: 'details-type-signalement',
params: {

Timothee P
committed
};

Timothee P
committed
const isFeatureBrowsing = (router.history.current.name === 'project_detail' || router.history.current.name === 'liste-signalements');

Timothee P
committed
document.getElementById('goToFeatureDetail').onclick = isFeatureBrowsing ? goToBrowseFeatureDetail : goToFeatureDetail;
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) {
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.featureType ? popupContent.featureType.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 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] });
},
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 (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);
source: new XYZ({
attributions: options.attribution,
url: layer.service.replace('{s}', '{a-c}')
})
});
layerTms.setOpacity(parseFloat(options.opacity));
dictLayersToMap[layer.id] = layerTms;
dictLayersToMap[layer.id].setZIndex(count);
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
}
});
} 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),
});
this.map.addLayer(layer);
return layer;
},
// Remove the base layers (not the features)
removeLayers: function () {
Object.values(dictLayersToMap).forEach(element => {
const layer = dictLayersToMap[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;
let currentValue = properties[colors_style.custom_field_name];
if(currentValue & typeof currentValue === 'string') currentValue = currentValue.trim(); // remove leading and trailing whitespaces
switch (fieldType) {
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 };
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();
if (properties && 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, opacity } = this.retrieveFeatureStyle(featureType, properties);
//console.log(color, opacity, featureType, properties);
color && 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.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>
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({