Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • geocontrib/geocontrib-frontend
  • ext_matthieu/geocontrib-frontend
  • fnecas/geocontrib-frontend
  • MatthieuE/geocontrib-frontend
4 results
Show changes
Showing
with 3551 additions and 453 deletions
import axios from '@/axios-client.js';
import store from '../store';
const featureAPI = {
async getFeaturesBbox(project_slug, queryString) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const response = await axios.get(
`${baseUrl}projects/${project_slug}/feature-bbox/${queryString ? '?' + queryString : ''}`
);
if (
response.status === 200 &&
response.data
) {
const bbox = response.data;
return [
[bbox.minLat, bbox.minLon],
[bbox.maxLat, bbox.maxLon],
];
} else {
return null;
}
},
async getProjectFeature(project_slug, feature_id) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const response = await axios.get(
`${baseUrl}v2/features/${feature_id}/?project__slug=${project_slug}`
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async getPaginatedFeatures(url) {
// Cancel any ongoing search request.
store.dispatch('CANCEL_CURRENT_SEARCH_REQUEST');
// Prepare the cancel token for the new request and store it.
const cancelToken = axios.CancelToken.source();
store.commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken);
const response = await axios.get(url, { cancelToken: cancelToken.token });
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async getFeatureEvents(featureId) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const response = await axios.get(
`${baseUrl}features/${featureId}/events/`
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async getFeatureAttachments(featureId) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const response = await axios.get(
`${baseUrl}features/${featureId}/attachments/`
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async getFeatureLinks(featureId) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const response = await axios.get(
`${baseUrl}features/${featureId}/feature-links/`
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async getFeaturesBlob(url) {
const response = await axios
.get(url, { responseType: 'blob' });
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async postOrPutFeature({ method, feature_id, feature_type__slug, project__slug, data }) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
let url = `${baseUrl}v2/features/`;
if (method === 'PUT') {
url += `${feature_id}/?
feature_type__slug=${feature_type__slug}
&project__slug=${project__slug}`;
}
const response = await axios({
url,
method,
data,
});
if ((response.status === 200 || response.status === 201) && response.data) {
return response;
} else {
return null;
}
},
async updateFeature({ feature_id, feature_type__slug, project__slug, newStatus }) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const url = `${baseUrl}v2/features/${feature_id}/?feature_type__slug=${feature_type__slug}&project__slug=${project__slug}`;
const response = await axios({
url,
method: 'PATCH',
data: { id: feature_id, status: newStatus, feature_type: feature_type__slug }
});
if (response.status === 200 && response.data) {
return response;
} else {
return null;
}
},
async projectFeatureBulkUpdateStatus(projecSlug, queryString, newStatus) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const url = `${baseUrl}projects/${projecSlug}/feature-bulk-modify/?${queryString}`;
const response = await axios({
url,
method: 'PUT',
data: { status: newStatus }
});
if (response.status === 200 && response.data) {
return response;
} else {
return null;
}
},
async projectFeatureBulkDelete(projecSlug, queryString) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const url = `${baseUrl}projects/${projecSlug}/feature-bulk-modify/?${queryString}`;
const response = await axios({
url,
method: 'DELETE'
});
if (response.status === 200 && response.data) {
return response;
} else {
return null;
}
},
async postComment({ featureId, comment }) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const response = await axios.post(
`${baseUrl}features/${featureId}/comments/`, { comment }
);
if (
response.status === 201 &&
response.data
) {
return response;
} else {
return null;
}
},
async postCommentAttachment({ featureId, file, fileName, title, isKeyDocument, commentId }) {
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const formdata = new FormData();
formdata.append('file', file, fileName);
formdata.append('title', title);
formdata.append('is_key_document', isKeyDocument);
const response = await axios.put(
`${baseUrl}features/${featureId}/comments/${commentId}/upload-file/`, formdata
);
if (
response.status === 200 &&
response.data
) {
return response;
} else {
return null;
}
},
async getFeaturePosition(projectSlug, featureId, query) {
const searchParams = new URLSearchParams(query);
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const response = await axios.get(`${baseUrl}projects/${projectSlug}/feature/${featureId}/position-in-list/?${searchParams.toString()}`);
if (response && response.status === 200) {
return response.data;
} else if (response.status === 204) {
return response.statusText;
}
return null;
},
};
export default featureAPI;
import axios from '@/axios-client.js';
import store from '@/store';
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const featureTypeAPI = {
async deleteFeatureType(featureType_slug) {
const response = await axios.delete(
`${baseUrl}v2/feature-types/${featureType_slug}/`
);
if (
response.status === 204
) {
return 'success';
} else {
return null;
}
},
};
export default featureTypeAPI;
import axios from '@/axios-client.js';
import store from '../store';
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const mapAPI = {
async postOrPut({ basemap, projectSlug, newBasemapIds }) {
basemap['project'] = projectSlug;
if (newBasemapIds.includes(basemap.id)) {
return axios
.post(`${baseUrl}v2/base-maps/`, basemap)
.then((response) => response)
.catch((error) => {
throw error;
});
} else {
return axios
.put(`${baseUrl}v2/base-maps/${basemap.id}/`, basemap)
.then((response) => response)
.catch((error) => {
throw error;
});
}
}
};
export default mapAPI;
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 } 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 } from 'ol/extent';
import Overlay from 'ol/Overlay';
import { Fill, Stroke, Style, Circle } from 'ol/style';
import { asArray } from 'ol/color';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
import WMTSTileGrid from 'ol/tilegrid/WMTS';
import WMTS, { optionsFromCapabilities } from 'ol/source/WMTS';
import WMTSCapabilities from 'ol/format/WMTSCapabilities';
import Geolocation from 'ol/Geolocation.js';
import Feature from 'ol/Feature.js';
import Point from 'ol/geom/Point.js';
import { applyStyle } from 'ol-mapbox-style';
import { isEqual } from 'lodash';
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] });
},
/**
* Add multiple layers to the map. If custom layers are defined, they will be added using `addConfigLayer`.
* If no custom layers are defined, a default basemap will be added based on the schema type (WMS, WMTS, or XYZ).
*
* @param {Array} layers - Array of layer configurations to be added.
* @param {string} serviceMap - URL or service for the map base layer.
* @param {Object} optionsMap - Options for the base layer (e.g., attribution, noWrap).
* @param {string} schemaType - Type of the base layer (either 'wms', 'wmts', or fallback to XYZ).
*
* @returns {void}
*/
addLayers: function (layers, serviceMap, optionsMap, schemaType) {
// Set the current layers to the provided layers array
this.layers = layers;
// Check if custom layers are defined (admin-defined basemaps)
if (layers) {
// Reset the layer count for managing Z-index
layersCount = 0;
// Loop through each layer and add it using the addConfigLayer method
layers.forEach((layer) => {
if (!layer) {
console.error('Layer is missing in the provided layers array.');
} else {
this.addConfigLayer(layer);
}
});
}
// If no custom layers are defined, fall back to the base map
else {
// Ensure that options for the base map are provided
if (!optionsMap) {
console.error('Options for the base map are missing.');
return;
}
// Set noWrap to true to prevent map wrapping around the globe
optionsMap.noWrap = true;
// Handle the base map based on the schema type (WMS, WMTS, or fallback)
if (schemaType === 'wms') {
// Add WMS layer if the schema type is 'wms'
if (!serviceMap) {
console.error('Service URL is missing for WMS base layer.');
} else {
this.addWMSLayer(serviceMap, optionsMap);
}
} else if (schemaType === 'wmts') {
// Add WMTS layer if the schema type is 'wmts'
if (!serviceMap) {
console.error('Service URL is missing for WMTS base layer.');
} else {
this.addWMTSLayerFromCapabilities(serviceMap, optionsMap);
}
} else {
// Default to XYZ tile layer if the schema type is not WMS or WMTS
if (!serviceMap) {
console.error('Service URL is missing for XYZ base layer.');
} else {
const layer = new TileLayer({
source: new XYZ({
attributions: optionsMap.attribution, // Attribution for the layer
url: serviceMap.replace('{s}', '{a-c}') // Handle subdomains in the URL
})
});
this.map.addLayer(layer); // Add the layer to the map
}
}
}
},
/**
* Add a configuration layer (WMS, WMTS, or TMS) to the map based on the layer's schema type.
* The function handles multiple types of map layers and applies the necessary configurations.
*
* @param {Object} layer - The layer configuration object.
* @param {Object} layer.options - Options for the layer (e.g., opacity, noWrap).
* @param {string} layer.schema_type - Type of the layer ('wms', 'wmts', or 'tms').
* @param {string} layer.service - URL or service for the layer.
* @param {number} layer.opacity - Opacity of the layer.
*
* @returns {void}
*/
addConfigLayer: async function (layer) {
// Check if the layer object is provided
if (!layer) {
console.error('Layer object is missing');
return;
}
// Increment the layers count (to manage Z-index)
layersCount += 1;
// Extract options from the layer
const options = layer.options;
// Check if options are provided for the layer
if (!options) {
console.error(`Options are missing for layer: ${layer.id}`);
return;
}
// Set default layer options (noWrap and opacity)
options.noWrap = true; // Prevent wrapping of the layer around the globe
options['opacity'] = layer.opacity; // Set opacity based on the layer's configuration
// Handle WMS layers
if (layer.schema_type === 'wms') {
// Add title for queryable WMS layers
if (layer.queryable) options['title'] = layer.title;
dictLayersToMap[layer.id] = this.addWMSLayer(layer.service, options); // Add WMS layer
}
// Handle WMTS layers
else if (layer.schema_type === 'wmts') {
try {
const newLayer = await this.addWMTSLayerFromCapabilities(layer.service, options); // Add WMTS layer asynchronously
dictLayersToMap[layer.id] = newLayer;
} catch (error) {
console.error(`Error adding WMTS layer: ${layer.id}`, error);
}
}
// Handle TMS layers
else if (layer.schema_type === 'tms') {
try {
const newLayer = await this.addTMSLayer(layer.service, options); // Add TMS layer asynchronously
dictLayersToMap[layer.id] = newLayer;
} catch (error) {
console.error(`Error adding TMS layer: ${layer.id}`, error);
}
} else {
console.error(`Unsupported schema type: ${layer.schema_type}`);
}
// Set Z-index for the layer if it was successfully added to the map
if (dictLayersToMap[layer.id]) {
dictLayersToMap[layer.id].setZIndex(layersCount);
} else {
console.error(`Failed to add layer to map: ${layer.id}`);
}
},
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;
},
/**
* Add a TMS (Tile Map Service) layer to the map.
* If the URL includes `.pbf` (vector tiles), it will apply a style from the provided options.
* The layer will be added to the map, and the style will be applied asynchronously if needed.
*
* @param {string} url - The URL of the TMS layer service.
* @param {Object} options - Configuration options for the TMS layer, including opacity and style.
* @param {string} options.style - URL to the style JSON (required for vector tiles).
* @param {number} options.opacity - Opacity of the layer (optional).
*
* @returns {VectorTileLayer} - The TMS layer added to the map.
*/
async addTMSLayer(url, options) {
// Check if the URL is missing
if (!url) {
console.error('TMS layer service URL is missing');
// Check if the options object is missing
} else if (!options) {
console.error('TMS layer options object is missing');
} else {
let layerTms;
// Check if the URL refers to PBF (vector tiles)
if (url.includes('pbf')) {
// Ensure that a style is provided for vector tiles
if (!options.style) {
console.error('TMS layer from PBF requires a style in the options');
} else {
// Handle PBF vector tiles
layerTms = new VectorTileLayer({
source: new VectorTileSource({
format: new MVT(), // Format for vector tiles (Mapbox Vector Tiles)
url: url.replace('{s}', '{a-c}'), // Handle subdomain pattern in the URL if present
attributions: options.attribution,
})
});
try {
// Fetch the style JSON from the provided URL
const response = await fetch(options.style);
const json = await response.json();
// Apply the fetched style to the layer (asynchronous)
await applyStyle(layerTms, json);
} catch (error) {
// Handle any errors during the fetch process
console.error('Error loading the style JSON:', error);
}
}
} else {
// Handle PNG raster tiles
layerTms = new TileLayer({
source: new XYZ({
url: url.replace('{s}', '{a-c}'), // Use the PNG TMS URL pattern
attributions: options.attribution,
})
});
}
// Set the opacity for the layer (default to 1.0 if not specified)
layerTms.setOpacity(parseFloat(options.opacity || 1.0));
// Add the TMS layer to the map
this.map.addLayer(layerTms);
// Return the TMS layer for further manipulation if needed
return layerTms;
}
},
// 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: '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', () => {
const currentPosition = this.geolocation.getPosition();
if (!currentPosition || !isEqual(this.lastPosition, currentPosition)) {
console.log('current position: ', currentPosition); // keeping this console.log for debug purpose in case needed
}
this.lastPosition = currentPosition;
this.changeTrackerPosition();
});
this.geolocationSource = new VectorSource({
features: [this.positionFeature],
});
new VectorLayer({
map: this.map,
source: this.geolocationSource,
});
},
changeTrackerPosition() {
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;
\ No newline at end of file
import axios from '@/axios-client.js';
import store from '../store';
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const miscAPI = {
async getIdgoCatalog(username) {
try {
const response = await axios.get(
`${baseUrl}idgo-catalog/?user=${username}`
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
} catch (err) {
return err;
}
},
async getExternalGeojson(queryParams) {
const response = await axios.get(
`${baseUrl}external-geojson/${queryParams ? '?' + queryParams : ''}`
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async getUserEvents(project_slug) {
const response = await axios.get(`${baseUrl}events/${project_slug ? '?project_slug=' + project_slug : ''}`);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
};
export default miscAPI;
import axios from '@/axios-client.js';
const projectAPI = {
async getProject( baseUrl, projectSlug ) {
const response = await axios.get(
`${baseUrl}v2/projects/${projectSlug}/`
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async getProjectSubscription({ baseUrl, projectSlug }) {
const response = await axios.get(
`${baseUrl}projects/${projectSlug}/subscription/`
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async subscribeProject({ baseUrl, projectSlug, suscribe }) {
const response = await axios.put(
`${baseUrl}projects/${projectSlug}/subscription/`,
{ is_suscriber: suscribe }
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async getProjects({ baseUrl, filters, page, projectSlug, myaccount, text }) {
let url = `${baseUrl}v2/projects/`;
if (projectSlug) {
url += `${projectSlug}/`;
}
url += `?page=${page}`;
if (myaccount) {
url += '&myaccount=true';
}
// Append search text if provided.
if (text) {
url += `&search=${encodeURIComponent(text)}`;
}
try {
if (Object.values(filters).some(el => el && el.length > 0)) {
for (const filter in filters) {
if (filters[filter]) {
url = url.concat('', `&${filter}=${filters[filter]}`);
}
}
}
const response = await axios.get(url);
if (response.status === 200 && response.data) {
return response.data;
}
} catch (error) {
console.error(error);
throw error;
}
},
async getProjectUsers( baseUrl, projectSlug) {
const response = await axios.get(
`${baseUrl}projects/${projectSlug}/utilisateurs/`
);
if (
response.status === 200 &&
response.data
) {
return response.data;
} else {
return null;
}
},
async getProjectTypes( baseUrl ) {
const response = await axios.get(
`${baseUrl}v2/projects/?is_project_type=true`
);
if (
response.status === 200 &&
response.data
) {
return response.data.results;
} else {
return null;
}
},
async deleteProject(baseUrl, projectSlug) {
const response = await axios.delete(
`${baseUrl}v2/projects/${projectSlug}/`
);
if ( response.status === 204 ) {
return 'success';
} else {
return null;
}
},
};
export default projectAPI;
import axios from '@/axios-client.js';
import store from '../store';
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const userAPI = {
async signup(data, url) {
try {
const response = await axios.post(url || `${baseUrl}v2/users/`, data);
return response; // Retourne directement la réponse si succès
} catch (err) {
console.error('Erreur lors de l\'inscription :', err.response || err);
return err.response || { status: 500, data: { detail: 'Erreur inconnue' } }; // 👈 Retourne la réponse d'erreur si disponible
}
},
};
export default userAPI;
const axios = require("axios");
import axios from '@/axios-client.js';
import Vue from 'vue';
import Vuex from 'vuex';
import router from '../router'
//import modules from './modules';
import feature_type from "./modules/feature_type"
import feature from "./modules/feature"
import map from "./modules/map"
import router from '../router';
import modules from './modules';
Vue.use(Vuex);
axios.defaults.withCredentials = true; // * add cookies to axios
function updateAxiosHeader() {
axios.defaults.headers.common['X-CSRFToken'] = (name => {
var re = new RegExp(name + "=([^;]+)");
var value = re.exec(document.cookie);
return (value != null) ? unescape(value[1]) : null;
})('csrftoken');
}
// ! À vérifier s'il y a un changement de token pendant l'éxécution de l'appli
updateAxiosHeader();
const DJANGO_API_BASE = process.env.VUE_APP_DJANGO_API_BASE;
const noPermissions = {
can_view_project: true,
can_create_project: false,
can_update_project: false,
can_view_feature: true,
can_view_archived_feature: true,
can_create_feature: false,
can_update_feature: false,
can_delete_feature: false,
can_publish_feature: false,
can_create_feature_type: false,
can_view_feature_type: true,
is_project_administrator: false
};
export default new Vuex.Store({
modules: {
feature_type,
feature,
map
},
modules,
state: {
status_choices: [],
cancellableSearchRequest: [],
configuration: null,
isOnline: true,
levelsPermissions: [],
loader: {
isLoading: false,
message: 'En cours de chargement'
},
logged: false,
user: false,
project_slug: null,
projectMembers: null,
projects: [],
messageCount: 0,
messages: [],
projectAttributes: [],
reloadIntervalId: null,
staticPages: null,
SSO_SETTED: false,
USER_LEVEL_PROJECTS: null
user: false,
usersGroups: [],
USER_LEVEL_PROJECTS: null,
user_permissions: null,
userToken: null
},
mutations: {
SET_PROJECTS(state, projects) {
state.projects = projects;
},
SET_PROJECT_SLUG(state, slug) {
state.project_slug = slug;
},
SET_PROJECT_MEMBERS(state, projectMembers) {
state.projectMembers = projectMembers;
SET_IS_ONLINE(state, payload) {
state.isOnline = payload;
},
SET_USER(state, payload) {
state.user = payload;
},
SET_COOKIE(state, cookie) {
state.cookie = cookie
SET_CONFIG(state, payload) {
state.configuration = payload;
},
SET_STATIC_PAGES(state, staticPages) {
state.staticPages = staticPages
state.staticPages = staticPages;
},
SET_SSO(state, SSO_SETTED) {
state.SSO_SETTED = SSO_SETTED
SET_USERS_GROUPS(state, usersGroups) {
state.usersGroups = usersGroups;
},
SET_USER_LEVEL_PROJECTS(state, USER_LEVEL_PROJECTS) {
state.USER_LEVEL_PROJECTS = USER_LEVEL_PROJECTS
state.USER_LEVEL_PROJECTS = USER_LEVEL_PROJECTS;
},
SET_LOGGED(state, value) {
state.logged = value
state.logged = value;
},
SET_USER_TOKEN(state, payload) {
state.userToken = payload;
},
SET_USER_PERMISSIONS(state, userPermissions) {
state.user_permissions = userPermissions;
},
SET_LEVELS_PERMISSIONS(state, levelsPermissions) {
state.levelsPermissions = levelsPermissions;
},
SET_PROJECT_ATTRIBUTES(state, userPermissions) {
state.projectAttributes = userPermissions;
},
DISPLAY_MESSAGE(state, message) {
message['counter'] = state.messageCount;
state.messageCount += 1;
state.messages = [message, ...state.messages]; // add new message at the beginning of the list
if (document.getElementById('scroll-top-anchor')) {
document.getElementById('scroll-top-anchor').scrollIntoView({ block: 'start', inline: 'nearest' });
}
setTimeout(() => {
state.messages = state.messages.slice(0, -1); // remove one message from the end of the list
}, 3000);
},
DISCARD_MESSAGE(state, messageCount) {
state.messages = state.messages.filter((mess) => mess.counter !== messageCount);
},
CLEAR_MESSAGES(state) {
state.messages = [];
},
DISPLAY_LOADER(state, message) {
state.loader = { isLoading: true, message };
},
DISCARD_LOADER(state) {
state.loader = {
isLoading: false,
message: 'En cours de chargement'
};
},
SET_CANCELLABLE_SEARCH_REQUEST(state, payload) {
state.cancellableSearchRequest.push(payload);
},
RESET_CANCELLABLE_SEARCH_REQUEST(state) {
state.cancellableSearchRequest = [];
},
REMOVE_LAST_CANCELLABLE_SEARCH_REQUEST(state) {
const updatedCancellableSearchRequest = state.cancellableSearchRequest.slice(0, -1);
state.cancellableSearchRequest = updatedCancellableSearchRequest;
},
SET_RELOAD_INTERVAL_ID(state, payload) {
state.reloadIntervalId = payload;
},
CLEAR_RELOAD_INTERVAL_ID(state) {
clearInterval(state.reloadIntervalId);
state.reloadIntervalId = null;
},
},
getters: {
project: state => state.projects.find((project) => project.slug === state.project_slug),
project_types: state => state.projects.filter(projet => projet.is_project_type),
project_user: state => state.projects.filter(projet => projet.creator === state.user.id), // todo: add id to user in api
permissions: state => state.user_permissions && state.projects.project ?
state.user_permissions[state.projects.project.slug] : noPermissions,
usersGroupsOptions: state => state.usersGroups.map((group) => ({
name: group.display_name,
value: group.codename,
isGlobal: group.is_global,
})),
usersGroupsFeatureOptions: (state, getters) => getters.usersGroupsOptions.filter((group) => !group.isGlobal)
},
actions: {
async GET_ALL_PROJECTS({ commit }) {
function parseDate(date) {
let dateArr = date.split("/").reverse()
return new Date(dateArr[0], dateArr[1] - 1, dateArr[2])
}
await axios
.get(`${DJANGO_API_BASE}projects/`)
.then((response) => {
const orderedProjects = response.data.sort((a, b) => parseDate(b.created_on) - parseDate(a.created_on));
commit("SET_PROJECTS", orderedProjects)
})
.catch((error) => {
throw error;
});
},
async GET_STATIC_PAGES({ commit }) {
await axios
.get(`${DJANGO_API_BASE}flat-pages/`)
.then((response) => (commit("SET_STATIC_PAGES", response.data)))
GET_STATIC_PAGES({ commit }) {
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}flat-pages/`)
.then((response) => (commit('SET_STATIC_PAGES', response.data)))
.catch((error) => {
throw error;
});
},
LOGIN({ commit, dispatch }, payload) {
if (payload.username && payload.password) {
axios
.post(`${DJANGO_API_BASE}login/`, {
return axios
.post(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}login/`, {
username: payload.username,
password: payload.password,
})
.then((response) => {
console.log(router.options.routerHistory)
// * use stored previous route to go back after login if page not open on login at first
const routerHistory = router.options.routerHistory[0].name !== "login" ? router.options.routerHistory : "/"
commit("SET_USER", response.data.user);
router.push(routerHistory[routerHistory.length - 1] || "/")
dispatch("GET_USER_LEVEL_PROJECTS");
if (response.status === 200 && response.data) {
commit('SET_USER', response.data.user);
dispatch('REDIRECT_AFTER_LOGIN');
dispatch('GET_USER_LEVEL_PROJECTS');
dispatch('GET_USER_LEVEL_PERMISSIONS');
return response.status;
}
})
.catch(() => {
commit("SET_USER", false)
commit('SET_USER', false);
return 'error';
});
}
},
USER_INFO({ commit }) {
REDIRECT_AFTER_LOGIN() {
// * use stored previous route to go back after login if page not open on login at first
let routerHistory = '';
if (router.options.routerHistory[0] !== undefined) {
routerHistory = router.options.routerHistory[0].name !== 'login' ? router.options.routerHistory : '/';
} else {
routerHistory = '/';
}
const slug = router.history.current.params.slug;
if (slug) {
router.push({ name: 'project_detail', params: { slug } }).catch(() => { // prevent redundant navigation error
console.error('Not critic: caught error from vue-router -> redundant navigation to same url.');
});
} else { //* not working at page load, routerHistory filled afterwards, could try history.back()
router.push(routerHistory[routerHistory.length - 1] || '/').catch(() => { // prevent redundant navigation error
console.error('Not critic: caught error from vue-router -> redundant navigation to same url.');
});
}
},
/**
* Action to retrieve user information.
* - If a token is present in the URL, it indicates a Single Sign-On (SSO) attempt,
* in which case it logs out the user (if logged in) and connects via SSO with the token.
* - Otherwise, it fetches user information from the Django API endpoint:
* - If no user is logged in AND if the login should be done through SSO with a redirect,
* it navigates to the login platform. Afterward, the user will be redirected with the token and the original URL to open in Geocontrib.
* - Else if the response contains a login_url (to connect with OGS), the user is redirected to this url which will redirect the user
* to geocontrib after login, which will check again if the OGS session is activated
* - Otherwise, it displays a message that the user is not logged in but can still access the app as an anonymous user.
* A 'next' parameter is added to transfer to the OGS login page for redirection.
*/
async GET_USER_INFO({ state, commit, dispatch }) {
// Extract token from URL query parameters
const searchParams = new URLSearchParams(window.location.search);
const token = searchParams.get('token');
const url_redirect = searchParams.get('url_redirect');
const currentUrl = window.location.href;
// Check if token exists and SSO login URL is configured
if (token && state.configuration.VUE_APP_LOGIN_URL) {
// If user was previously connected through SSO, ensure they are logged out before reconnecting through SSO, in case user changed
await dispatch('LOGOUT');
dispatch('CONNECT_SSO_WITH_TOKEN', { token, url_redirect });
} else if (!state.user) {
// If user infos are not set, try to fetch them
return axios
.get(`${state.configuration.VUE_APP_DJANGO_API_BASE}user_info/`)
.then((response) => {
// Update the user state with received user data
if (response && response.status === 200) {
const user = response.data.user;
commit('SET_USER', user);
// Fetch user related data
dispatch('GET_USER_LEVEL_PERMISSIONS');
dispatch('GET_USER_LEVEL_PROJECTS');
dispatch('projects/GET_PROJECTS');
return;
}
})
.catch(() => {
// If the instance is set to accept login with redirection
if (state.configuration.VUE_APP_SSO_LOGIN_URL_WITH_REDIRECT) {
commit('DISPLAY_MESSAGE', {
comment: 'Vous allez être redirigé vers la plateforme de connexion.'
});
// Call the SSO login plateform with url to redirect after login
window.open(`${state.configuration.VUE_APP_SSO_LOGIN_URL_WITH_REDIRECT}/?url_redirect=${encodeURIComponent(currentUrl)}`, '_self');
}
// If the user is not logged in, display an info message
commit('DISPLAY_MESSAGE', {
comment: `Vous n'êtes pas connecté actuellement.
Vous pouvez accéder à l'application en tant qu'utilisateur anonyme`
});
return;
});
}
},
/**
* Action to connect user through SSO with a token.
* If the app was opened with a token in the url, it attempts a login,
* if the login is succesfull, it set the user in the state
* and retrieve information that would have been retrieved in GET_USER_INFO when logged.
* If the url contained a url to redirect, it calls the router to open this page.
*/
async CONNECT_SSO_WITH_TOKEN({ state, commit, dispatch }, { token, url_redirect }) {
axios
.get(`${DJANGO_API_BASE}user_info/`)
.get(`${state.configuration.VUE_APP_DJANGO_API_BASE}login-token/?token=${token}`)
.then((response) => {
const user = response.data.user;
commit("SET_USER", user)
window.localStorage.setItem("user", JSON.stringify(user))
}) // todo: ajouter au localestorage
.catch(() => {
router.push({ name: "login" });
if (response && (response.status === 200 || response.status === 201)) {
const user = response.data;
commit('SET_USER', user);
dispatch('GET_USER_LEVEL_PROJECTS');
dispatch('GET_USER_LEVEL_PERMISSIONS');
commit('DISPLAY_MESSAGE', {
comment: `Vous êtes maintenant connecté ${user.first_name} ${user.last_name}`,
level: 'positive'
});
dispatch('projects/GET_PROJECTS');
if (url_redirect) {
// Prepare the url to redirect with vue-router that prefix the url with DOMAIN+BASE_URL
const substringToRemove = state.configuration.BASE_URL;
// Find the index of the string to remove
const index = url_redirect.indexOf(substringToRemove);
// If found, keep only the remaining part after the substring to remove
if (index !== -1) {
url_redirect = url_redirect.substring(index + substringToRemove.length);
}
// catch error from the router, because of second redirection to feature when call with a feature's id
router.push(url_redirect).catch((e) => e);
}
}
})
.catch((err) => {
console.error(err);
commit('DISPLAY_MESSAGE', {
comment: 'La connexion a échoué.',
level: 'negative'
});
});
},
LOGOUT({ commit }) { // ? logout dans django ?
axios
.get(`${DJANGO_API_BASE}logout/`)
async GET_USER_TOKEN({ state, commit }) {
const response = await axios.get(`${state.configuration.VUE_APP_DJANGO_API_BASE}get-token`);
if (
response.status === 200 &&
response.data
) {
commit('SET_USER_TOKEN', response.data);
}
},
LOGOUT({ state, commit, dispatch }) {
return axios
.get(`${state.configuration.VUE_APP_DJANGO_API_BASE}logout/`)
.then((response) => {
console.log(response)
commit("SET_USER", false) // ? better false or null
commit("SET_USER_LEVEL_PROJECTS", null)
if (response && response.status === 200) {
commit('SET_USER', false);
commit('SET_USER_LEVEL_PROJECTS', null);
dispatch('GET_USER_LEVEL_PERMISSIONS');
if (router.history.current.name !== 'index' &&
!window.location.pathname.includes('projet-partage') &&
!state.configuration.VUE_APP_LOGIN_URL
) {
router.push('/');
}
}
})
.catch((error) => {
throw error
console.error(error);
});
},
GET_CONFIG({ commit }) {
axios
.get('./config/config.json')
.then((response) => {
if (response && response.status === 200) {
commit('SET_CONFIG', response.data);
}
})
.catch((error) => {
throw error;
});
},
GET_USERS_GROUPS({ commit }) {
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}users-groups/`)
.then((response) => {
if (response && response.status === 200) {
commit('SET_USERS_GROUPS', response.data);
}
})
.catch((error) => {
throw error;
});
},
GET_USER_LEVEL_PROJECTS({ commit }) {
commit("SET_USER_LEVEL_PROJECTS", "Administrateur projet") // todo : use authentification)
/* axios
.get(`${DJANGO_API_BASE}user_level_project/`)
.then((response) => (commit("SET_USER_LEVEL_PROJECTS", response.data)))
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}user-level-projects/`)
.then((response) => {
if (response && response.status === 200) {
commit('SET_USER_LEVEL_PROJECTS', response.data);
}
})
.catch((error) => {
throw error;
}); */
});
},
/* GET_PROJECT({ commit }, project_slug) {
axios
.get(`${DJANGO_API_BASE}projet/${project_slug}/project`)
.then((response) => commit("SET_PROJECT", response.data))
GET_USER_LEVEL_PERMISSIONS({ commit }) {
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}user-permissions/`)
.then((response) => {
if (response && response.status === 200) {
commit('SET_USER_PERMISSIONS', response.data);
}
})
.catch((error) => {
throw error;
});
},
GET_PROJECT_USER({ commit }, project_slug) {
axios
.get(`${DJANGO_API_BASE}projet/${project_slug}/utilisateurs`)
.then((response) => (commit("SET_PROJECT_MEMBERS", response.data.members)))
GET_LEVELS_PERMISSIONS({ commit }) {
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}v2/levels-permissions/`)
.then((response) => {
if (response && response.status === 200) {
commit('SET_LEVELS_PERMISSIONS', response.data);
}
})
.catch((error) => {
throw error;
});
}, */
/* GET_COOKIE({ commit }, name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
},
GET_PROJECT_ATTRIBUTES({ commit }) {
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}project-attributes/`)
.then((response) => {
if (response && response.status === 200) {
commit('SET_PROJECT_ATTRIBUTES', response.data);
}
}
})
.catch((error) => {
throw error;
});
},
CANCEL_CURRENT_SEARCH_REQUEST({ state, commit }) {
if (state.cancellableSearchRequest.length > 0) {
const currentRequestCancelToken =
state.cancellableSearchRequest[state.cancellableSearchRequest.length - 1];
currentRequestCancelToken.cancel('Current search request was canceled');
commit('REMOVE_LAST_CANCELLABLE_SEARCH_REQUEST');
}
commit("SET_COOKIE", cookieValue);
}, */
},
}
});
import axios from '@/axios-client.js';
import Vue from 'vue';
const getColorsStyles = (customForms) => customForms
.filter(customForm => customForm.options && customForm.options.length)
.map(el => {
//* in dropdown, value is the name and name is the label to be displayed, could be changed...
return { value: el.name, name: el.label, options: el.options };
});
const pending2draftFeatures = (features) => {
for (const el of features) {
if (el.properties && el.properties.status === 'pending') {
el.properties.status = 'draft';
} else if (el.status === 'pending') {
el.status = 'draft';
}
}
return features;
};
const feature_type = {
namespaced: true,
state: {
form: null,
colorsStyleList: [],
customForms: [],
current_feature_type_slug: null,
feature_types: [],
fileToImport: null,
importFeatureTypeData: [],
preRecordedLists: [],
selectedPrerecordedListValues: {}
},
getters: {
feature_type: state => state.feature_types.find(
(el) => el.slug === state.current_feature_type_slug
),
},
mutations: {
SET_FEATURE_TYPES(state, feature_types) {
state.feature_types = feature_types;
},
SET_CURRENT_FEATURE_TYPE_SLUG(state, payload) {
//* reset import status of features for a specific feature_type when changing
state.importFeatureTypeData = null;
state.current_feature_type_slug = payload;
},
UPDATE_FORM(state, payload) {
state.form = payload;
},
EMPTY_FORM(state) {
state.form = null;
},
SET_CUSTOM_FORMS(state, customForms) {
state.customForms = customForms;
state.colorsStyleList = getColorsStyles(state.customForms);
},
ADD_CUSTOM_FORM(state, customForm) {
state.customForms = [...state.customForms, customForm];
state.colorsStyleList = getColorsStyles(state.customForms);
},
UPDATE_CUSTOM_FORM(state, payload) {
const index = state.customForms.findIndex((el) => el.dataKey === payload.dataKey);
if (index !== -1) {
state.customForms[index] = payload;
//* trigger update of available options for colors style in feature_type_edit (getters not working)
state.colorsStyleList = getColorsStyles(state.customForms);
}
},
REMOVE_CUSTOM_FORM(state, payload) {
state.customForms = state.customForms.filter(form => form.dataKey !== payload);
state.colorsStyleList = getColorsStyles(state.customForms);
},
EMPTY_CUSTOM_FORMS(state) {
state.customForms = [];
state.colorsStyleList = [];
},
SET_IMPORT_FEATURE_TYPES_DATA(state, payload) {
state.importFeatureTypeData = payload;
},
SET_FILE_TO_IMPORT(state, payload) {
state.fileToImport = payload;
},
SET_PRERECORDED_LISTS(state, payload) {
state.preRecordedLists = payload;
},
SET_SELECTED_PRERECORDED_LIST_VALUES(state, { name, values }) {
Vue.set(state.selectedPrerecordedListValues, name, values.slice(0, 10).map(el => { return { label: el };}));
}
},
actions: {
GET_PROJECT_FEATURE_TYPES({ commit }, project_slug) {
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}v2/feature-types/?project__slug=${project_slug}`)
.then((response) => {
if (response.status === 200 && response.data) {
commit('SET_FEATURE_TYPES', response.data);
return response;
}
})
.catch((error) => {
throw error;
});
},
async GET_PRERECORDED_LISTS({ commit }) {
try {
const response = await axios.get(
`${this.state.configuration.VUE_APP_DJANGO_API_BASE}prerecorded-list-values/`
);
if (response.status === 200) {
commit('SET_PRERECORDED_LISTS', response.data.map(el => el.name));
}
} catch (err) {
console.error(err);
}
},
async GET_SELECTED_PRERECORDED_LIST_VALUES({ commit }, { name, pattern }) {
try {
let url = `${this.state.configuration.VUE_APP_DJANGO_API_BASE}prerecorded-list-values/${name}/`;
if (pattern) {
url += `?pattern=${pattern}`;
}
const response = await axios.get(url);
if (response.status === 200) {
commit('SET_SELECTED_PRERECORDED_LIST_VALUES', {
name,
values: response.data
});
}
} catch (err) {
console.error(err);
}
return;
},
async SEND_FEATURE_TYPE({ state, getters, rootState }, requestType) {
const data = {
title: state.form.title.value,
title_optional: state.form.title_optional.value,
enable_key_doc_notif: state.form.enable_key_doc_notif.value,
disable_notification: state.form.disable_notification.value,
geom_type: state.form.geom_type.value,
color: state.form.color.value,
colors_style: state.form.colors_style.value,
project: rootState.projects.project.slug,
customfield_set: state.customForms
};
let url = `${this.state.configuration.VUE_APP_DJANGO_API_BASE}v2/feature-types/`;
if (requestType === 'put') url += `${getters.feature_type.slug}/`;
return axios({
url,
method: requestType,
data,
}).then((response) => {
if (response) {
const feature_type_slug = response.data.slug;
const status = response.status;
return { feature_type_slug, status };
}
})
.catch((error) => error.response);
},
async SEND_FEATURE_DISPLAY_CONFIG({ getters, rootState }, displayConfig) {
const data = {
title: getters.feature_type.title,
project: rootState.projects.project.slug,
...displayConfig
};
return axios
.put(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}v2/feature-types/${getters.feature_type.slug}/`, data)
.then((response) => {
if (response) {
const feature_type_slug = response.data.slug;
const status = response.status;
return { feature_type_slug, status };
}
})
.catch((error) => {
throw (error);
});
},
async SEND_FEATURES_FROM_GEOJSON({ state, dispatch, rootState }, payload) {
let { feature_type_slug, geojson } = payload;
//* check if geojson then build a file
if(!geojson && !state.fileToImport && state.fileToImport.size === 0 ) {
return;
}
const formData = new FormData();
let { name, type } = geojson || state.fileToImport;
if (!name && state.fileToImport) {
name = state.fileToImport.name;
}
if (rootState.projects.project.moderation) {
if (state.fileToImport && state.fileToImport.size > 0) { //* if data in a binary file, read it as text
const textFile = await state.fileToImport.text();
geojson = JSON.parse(textFile);
}
const unmoderatedFeatures = pending2draftFeatures(geojson.features || geojson);
geojson = geojson.features ? {
type: 'FeatureCollection', features: unmoderatedFeatures
} : unmoderatedFeatures;
}
const fileToImport = new File([JSON.stringify(geojson)], name, { type });
formData.append('json_file', geojson ? fileToImport : state.fileToImport);
formData.append('feature_type_slug', feature_type_slug);
const url =
this.state.configuration.VUE_APP_DJANGO_API_BASE +
'v2/import-tasks/';
return axios
.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then((response) => {
if (response && response.status === 200) {
return dispatch('GET_IMPORTS', {
feature_type: feature_type_slug
});
}
return response;
})
.catch((error) => {
throw (error);
});
},
async SEND_FEATURES_FROM_CSV({ state, dispatch }, payload) {
const { feature_type_slug, csv } = payload;
if (!csv && !state.fileToImport && state.fileToImport.size === 0 ) {
return;
}
const formData = new FormData();
formData.append('csv_file', state.fileToImport);
formData.append('feature_type_slug', feature_type_slug);
const url = `${this.state.configuration.VUE_APP_DJANGO_API_BASE}v2/import-tasks/`;
return axios
.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then((response) => {
if (response && response.status === 200) {
return dispatch('GET_IMPORTS', {
feature_type: feature_type_slug
});
}
return response;
})
.catch((error) => {
throw (error);
});
},
GET_IMPORTS({ state, commit, dispatch }, { project_slug, feature_type }) {
let url = `${this.state.configuration.VUE_APP_DJANGO_API_BASE}v2/import-tasks/`;
if (project_slug) {
url = url.concat('', `${url.includes('?') ? '&' : '?'}project_slug=${project_slug}`);
}
if (feature_type) {
url = url.concat('', `${url.includes('?') ? '&' : '?'}feature_type_slug=${feature_type}`);
}
return axios
.get(url)
.then((response) => {
if (response) {
const diffStatus = [];
if (state.importFeatureTypeData) {
for (const data of response.data) {
const index =
state.importFeatureTypeData
.findIndex(el => el.geojson_file_name === data.geojson_file_name);
if (index !== -1 && state.importFeatureTypeData[index].status !== data.status && data.status === 'finished') {
diffStatus.push(data);
}
}
}
if (diffStatus.length > 0 && project_slug && feature_type) {
try {
dispatch(
'feature/GET_PROJECT_FEATURES',
{
project_slug: project_slug,
feature_type__slug: feature_type,
},
{ root: true }
);
} catch (err) {
console.error(err);
}
}
commit('SET_IMPORT_FEATURE_TYPES_DATA', response.data);
}
return response;
})
.catch((error) => {
throw (error);
});
}
}
};
export default feature_type;
const feature = {
namespaced: true,
state: {
attachmentFormset: [],
linkedFormset: [],
features: [ // TODO: récupérer dans api
{
status: "archived",
title: "What the title !",
description:
"Very very very very very loooooooooooooong tiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiittttttttttttttlllllllllllllllllllllllllllllllleeeeeeeeeeeeeeeeeeeeee, isn't it ? 987653210",
created_on: "16/08/2021",
display_creator: "Babar",
feature_type: {
geom_type: "point",
title: "Éolienne",
is_editable: true,
}
},
{
status: "pending",
title: "Éolienne offshore",
description: "Feature pending",
created_on: "16/08/2021",
display_creator: "Babar",
feature_type: {
geom_type: "point",
title: "Éolienne",
is_editable: true,
}
},
{
status: "published",
title: "Éolienne privé",
description: "Feature published",
created_on: "16/08/2021",
display_creator: "Babar",
feature_type: {
geom_type: "point",
title: "Éolienne",
is_editable: true,
}
},
{
status: "draft",
title: "Éolienne terrestre",
description: "Fedraft description",
created_on: "16/08/2021",
display_creator: "Babar",
feature_type: {
geom_type: "point",
title: "Éolienne",
is_editable: true,
}
},
],
form: null,
extra_form: [
{
field_type: "char",
name: "deplacement_frontiere",
label: "Déplacement frontière"
},
{
field_type: "list",
name: "deplacement_status",
label: "Statut du déplacement",
choices: [
"à confirmer",
"confirmé"
]
},
{
field_type: "integer",
name: "deplacement_value",
label: "Mesure du déplacement"
},
{
field_type: "boolean",
name: "is_deplacement",
label: "Déplacement confirmé"
},
{
field_type: "date",
name: "deplacement_date",
label: "Date de déplacement"
},
{
field_type: "decimal",
name: "deplacement_decimal",
label: "Importance de déplacement"
},
{
field_type: "text",
name: "deplacement_description",
label: "Déscription du déplacement"
},
]
},
mutations: {
UPDATE_FORM(state, payload) {
state.form = payload;
},
UPDATE_EXTRA_FORM(state, payload) {
state.extra_form = payload;
},
ADD_ATTACHMENT_FORM(state, dataKey) {
state.attachmentFormset = [...state.attachmentFormset, { dataKey }];
},
UPDATE_ATTACHMENT_FORM(state, payload) {
const index = state.attachmentFormset.findIndex((el) => el.dataKey === payload.dataKey);
if (index !== -1) state.attachmentFormset[index] = payload
},
REMOVE_ATTACHMENT_FORM(state, payload) {
state.attachmentFormset = state.attachmentFormset.filter(form => form.dataKey !== payload);
},
ADD_LINKED_FORM(state, dataKey) {
state.linkedFormset = [...state.linkedFormset, { dataKey }];
},
UPDATE_LINKED_FORM(state, payload) {
const index = state.linkedFormset.findIndex((el) => el.dataKey === payload.dataKey);
if (index !== -1) state.linkedFormset[index] = payload
},
REMOVE_LINKED_FORM(state, payload) {
state.linkedFormset = state.linkedFormset.filter(form => form.dataKey !== payload);
},
},
getters: {
},
actions: {
POST_FEATURE({ state }) {
const data = {
form: state.form,
attachmentFormset: state.attachmentFormset,
linkedFormset: state.linkedFormset,
extra_form: state.extra_form,
}
console.log("data", data)
/* axios
.post(`${DJANGO_API_BASE}feature_type/`, data)
.then((response) => {
const routerHistory = router.options.routerHistory
commit("SET_USER", response.data.user);
router.push(routerHistory[routerHistory.length - 1] || "/")
dispatch("GET_USER_LEVEL_PROJECTS");
})
.catch(() => {
commit("SET_USER", false)
}); */
},
},
}
export default feature
\ No newline at end of file
import axios from '@/axios-client.js';
import router from '../../router';
import { objIsEmpty, findXformValue, activateFieldsNforceValues } from'@/utils';
const feature = {
namespaced: true,
state: {
attachmentFormset: [],
attachmentsToDelete: [],
checkedFeatures: [],
clickedFeatures: [],
extra_forms: [],
features: [],
features_count: 0,
currentFeature: null,
form: null,
linkedFormset: [], //* used to edit in feature_edit
linked_features: [], //* used to display in feature_detail
massMode: 'edit-status',
},
mutations: {
SET_FEATURES(state, features) {
state.features = features.sort((a, b) => {
return new Date(b.created_on) - new Date(a.created_on); // sort features chronologically
});
},
SET_FEATURES_COUNT(state, features_count) {
state.features_count = features_count;
},
SET_CURRENT_FEATURE(state, feature) {
state.currentFeature = feature;
},
UPDATE_FORM(state, payload) {
state.form = payload;
},
INIT_FORM(state) {
state.form = {
title: state.currentFeature.properties.title,
description: { value: state.currentFeature.properties.description },
status: { value: state.currentFeature.properties.status },
assigned_member: { value: state.currentFeature.properties.assigned_member },
};
},
UPDATE_FORM_FIELD(state, field) {
if (state.form[field.name].value !== undefined) {
state.form[field.name].value = field.value;
} else {
state.form[field.name] = field.value;
}
},
UPDATE_EXTRA_FORM(state, extra_form) {
const updatedExtraForms = state.extra_forms.map((field) => field.name === extra_form.name ? extra_form : field);
state.extra_forms = activateFieldsNforceValues(updatedExtraForms);
},
SET_EXTRA_FORMS(state, extra_forms) {
state.extra_forms = extra_forms;
},
CLEAR_EXTRA_FORM(state) {
state.extra_forms = [];
},
ADD_ATTACHMENT_FORM(state, attachmentFormset) {
state.attachmentFormset = [...state.attachmentFormset, attachmentFormset];
},
UPDATE_ATTACHMENT_FORM(state, payload) {
const index = state.attachmentFormset.findIndex((el) => el.dataKey === payload.dataKey);
if (index !== -1) {
state.attachmentFormset[index] = payload;
}
},
REMOVE_ATTACHMENT_FORM(state, payload) {
state.attachmentFormset = state.attachmentFormset.filter(form => form.dataKey !== payload);
},
CLEAR_ATTACHMENT_FORM(state) {
state.attachmentFormset = [];
},
ADD_LINKED_FORM(state, linkedFormset) {
state.linkedFormset = [...state.linkedFormset, linkedFormset];
},
UPDATE_LINKED_FORM(state, payload) {
const index = state.linkedFormset.findIndex((el) => el.dataKey === payload.dataKey);
if (index !== -1) {
state.linkedFormset[index] = payload;
}
},
REMOVE_LINKED_FORM(state, payload) {
state.linkedFormset = state.linkedFormset.filter(form => form.dataKey !== payload);
},
SET_LINKED_FEATURES(state, payload) {
state.linked_features = payload;
},
CLEAR_LINKED_FORM(state) {
state.linkedFormset = [];
},
ADD_ATTACHMENT_TO_DELETE(state, attachementId) {
state.attachmentsToDelete.push(attachementId);
},
REMOVE_ATTACHMENTS_ID_TO_DELETE(state, attachementId) {
state.attachmentsToDelete = state.attachmentsToDelete.filter(el => el !== attachementId);
},
UPDATE_CHECKED_FEATURES(state, checkedFeatures) {
state.checkedFeatures = checkedFeatures;
},
UPDATE_CLICKED_FEATURES(state, clickedFeatures) {
state.clickedFeatures = clickedFeatures;
},
TOGGLE_MASS_MODE(state, payload) {
state.massMode = payload;
},
},
actions: {
async GET_PROJECT_FEATURES({ commit, dispatch, rootState }, {
project_slug,
feature_type__slug,
ordering,
search,
limit,
geojson = false
}) {
dispatch('CANCEL_CURRENT_SEARCH_REQUEST', null, { root: true });
const cancelToken = axios.CancelToken.source();
commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true });
commit('SET_FEATURES', []);
commit('SET_FEATURES_COUNT', 0);
let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}v2/features/?project__slug=${project_slug}`;
if (feature_type__slug) {
url = url.concat('', `${url.includes('?') ? '&' : '?'}feature_type__slug=${feature_type__slug}`);
}
if (ordering) {
url = url.concat('', `${url.includes('?') ? '&' : '?'}ordering=${ordering}`);
}
if (search) {
url = url.concat('', `${url.includes('?') ? '&' : '?'}title__icontains=${search}`);
}
if (limit) {
url = url.concat('', `${url.includes('?') ? '&' : '?'}limit=${limit}`);
}
if (geojson) {
url = url.concat('', '&output=geojson');
}
try {
const response = await axios.get(url, { cancelToken: cancelToken.token });
if (response.status === 200 && response.data) {
const features = response.data.features;
commit('SET_FEATURES', features);
const features_count = response.data.count;
commit('SET_FEATURES_COUNT', features_count);
}
return response;
} catch (error) {
if (error.message) {
console.error(error);
}
throw error; // 'throw' instead of 'return', in order to pass inside the 'catch' error instead of 'then', to avoid initiating map in another component after navigation
}
},
GET_PROJECT_FEATURE({ commit, dispatch, rootState }, { project_slug, feature_id }) {
dispatch('CANCEL_CURRENT_SEARCH_REQUEST', null, { root: true });
const cancelToken = axios.CancelToken.source();
commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true });
const url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}v2/features/${feature_id}/?project__slug=${project_slug}`;
return axios
.get(url, { cancelToken: cancelToken.token })
.then((response) => {
if (response.status === 200 && response.data) {
commit('SET_CURRENT_FEATURE', response.data);
}
return response;
})
.catch((error) => {
console.error('Error while getting feature for id = ', feature_id, error);
throw error;
});
},
/**
* Handles the entire lifecycle of a feature submission, from sending data to handling additional forms
* and managing redirections based on the operation performed (create or update).
* @param {Object} context - Vuex action context, including state and dispatch functions.
* @param {Object} payload - Contains parameters like routeName, query, and extraForms for form handling.
*/
SEND_FEATURE({ state, rootState, rootGetters, commit, dispatch }, { routeName, query, extraForms }) {
/**
* Handles redirection after a feature operation, updating URL queries or navigating to new routes.
* @param {string} featureName - The name of the feature being handled.
* @param {Object} response - The server response object.
* @return {Object} - Either the server response or a string to trigger page reload.
*/
function redirect(featureName, response) {
// when modifying more than 2 features, exit this function (to avoid conflict with next feature call to GET_PROJECT_FEATURE)
if (routeName === 'editer-attribut-signalement') return response;
let newQuery = { ...query }; // create a copy of query from the route to avoid redundant navigation error since the router object would be modified
// Display a success message in the UI.
commit(
'DISPLAY_MESSAGE',
{
comment: routeName === 'ajouter-signalement' ?
'Le signalement a été crée' :
`Le signalement ${featureName} a été mis à jour`,
level: 'positive'
},
{ root: true },
);
// Construct the query for navigation based on the current state and feature details.
const slug_type_signal = rootState['feature-type'].current_feature_type_slug;
const project = rootState.projects.project;
if (routeName === 'ajouter-signalement' && !query.ordering) {
newQuery = {
ordering: project.feature_browsing_default_sort,
offset: 0,// if feature was just created, in both ordering it would be the first in project features list
};
if (project.feature_browsing_default_filter === 'feature_type_slug') {
newQuery['feature_type_slug'] = slug_type_signal;
}
}
if (query && query.ordering === '-updated_on') { // if the list is ordered by update time
newQuery.offset = 0;// it would be in first position (else, if ordered by creation, the position won't change anyway)
}
// in fast edition avoid redundant navigation if query didn't change
if (routeName === 'details-signalement-filtre' && parseInt(query.offset) === parseInt(newQuery.offset)) {
return 'reloadPage';
}
// Perform the actual route navigation if needed.
if (!objIsEmpty(newQuery)) {
router.push({
name: 'details-signalement-filtre',
params: { slug_type_signal },
query: newQuery,
});
} else {
router.push({
name: 'details-signalement',
params: { slug_type_signal },
});
}
return response;
}
/**
* Manages the uploading of attachments and linked features after the main feature submission.
* @param {number} featureId - The ID of the feature to which attachments and linked features relate.
* @param {string} featureName - The name of the feature for messaging purposes.
* @param {Object} response - The server response from the main feature submission.
* @return {Object} - Redirect response or a string to trigger page reload.
*/
async function handleOtherForms(featureId, featureName, response) {
await dispatch('SEND_ATTACHMENTS', featureId);
await dispatch('PUT_LINKED_FEATURES', featureId);
return redirect(featureName, response);
}
/**
* Prepares a GeoJSON object from the current state and extra forms provided in the payload.
* @return {Object} - A GeoJSON object representing the feature with additional properties.
*/
function createGeojson() { //* prepare feature data to send
const extraFormObject = {}; //* prepare an object to be flatten in properties of geojson
// use extraForms from argument if defined, overiding data from the store, in order to not use mutation (in case of multiple features)
for (const field of extraForms || state.extra_forms) {
// send extra form only if there is a value defined or if no value, if there was a value before, in order to avoid sending empty value when user didn't touch the extraform
if (field.value !== null ||
(state.currentFeature.properties && state.currentFeature.properties[field.name])) {
extraFormObject[field.name] = field.value;
}
}
let geojson = {
id: state.form.feature_id || state.currentFeature.id,
type: 'Feature',
properties: {
title: state.form.title,
description: state.form.description.value,
status: state.form.status.value.value || state.form.status.value,
project: rootState.projects.project.slug,
feature_type: rootState['feature-type'].current_feature_type_slug,
assigned_member: state.form.assigned_member.value,
...extraFormObject
}
};
// if not in the case of a non geographical feature type, add geometry to geojson, else send without geometry
if (rootGetters['feature-type/feature_type'].geom_type !== 'none') {
geojson['geometry'] = state.form.geometry || state.form.geom ||
state.currentFeature.geometry || state.currentFeature.properties.geom;
}
return geojson;
}
const geojson = createGeojson(); // Construct the GeoJSON from current state.
let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}v2/features/`;
if (routeName !== 'ajouter-signalement') {
url += `${geojson.id}/?
feature_type__slug=${rootState['feature-type'].current_feature_type_slug}
&project__slug=${rootState.projects.project.slug}`;
}
//* postOrPutFeature function from service featureAPI could be used here, but because configuration is in store,
//* projectBase would need to be sent with each function which imply to modify all function from this service,
//* which could create regression
return axios({
url,
method: routeName === 'ajouter-signalement' ? 'POST' : 'PUT',
data: geojson
}).then((response) => {
if ((response.status === 200 || response.status === 201) && response.data) {
const featureId = response.data.id;
const featureName = response.data.properties.title;
// Handle additional forms if needed.
if (state.attachmentFormset.length > 0 ||
state.linkedFormset.length > 0 ||
state.attachmentsToDelete.length > 0) {
return handleOtherForms(featureId, featureName, response);
} else {
return redirect(featureName, response);
}
}
}).catch((error) => {
// If offline, store the edited feature in localeStorage to send them when back online
if (error.message === 'Network Error' || !rootState.isOnline) {
let arraysOffline = [];
const localStorageArray = localStorage.getItem('geocontrib_offline');
if (localStorageArray) {
arraysOffline = JSON.parse(localStorageArray);
}
const updateMsg = {
project: rootState.projects.project.slug,
type: routeName === 'ajouter-signalement' ? 'post' : 'put',
featureId: geojson.id,
geojson: geojson
};
arraysOffline.push(updateMsg);
localStorage.setItem('geocontrib_offline', JSON.stringify(arraysOffline));
router.push({
name: 'offline-signalement',
params: {
slug_type_signal: rootState['feature-type'].current_feature_type_slug
},
});
} else {
console.error('Error while sending feature', error);
throw error; // Re-throw the error for further handling.
}
throw error; // Ensure any error is thrown to be handled by calling code.
});
},
async SEND_ATTACHMENTS({ state, rootState, dispatch }, featureId) {
const DJANGO_API_BASE = rootState.configuration.VUE_APP_DJANGO_API_BASE;
/**
* Adds a file to an existing attachment by uploading it to the server.
* @param {Object} attachment - The attachment object containing the file and other details.
* @param {number} attchmtId - The ID of the attachment to which the file is being added.
* @return {Promise<Object>} - The server's response to the file upload.
*/
function addFileToRequest(attachment, attchmtId) {
const formdata = new FormData();
formdata.append('file', attachment.fileToImport, attachment.fileToImport.name);
return axios
.put(`${DJANGO_API_BASE}features/${featureId}/attachments/${attchmtId}/upload-file/`, formdata)
.then((response) => {
return response;
})
.catch((error) => {
console.error(error);
return error;
});
}
/**
* Handles creating or updating an attachment, optionally uploading a file if included.
* @param {Object} attachment - The attachment data, including title, info, and optional file.
* @return {Promise<Object>} - The server response, either from creating/updating the attachment or from file upload.
*/
function putOrPostAttachement(attachment) {
const formdata = new FormData();
formdata.append('title', attachment.title);
formdata.append('info', attachment.info);
formdata.append('is_key_document', attachment.is_key_document);
let url = `${DJANGO_API_BASE}features/${featureId}/attachments/`;
if (attachment.id) {
url += `${attachment.id}/`;
}
return axios({
url,
method: attachment.id ? 'PUT' : 'POST',
data: formdata
}).then((response) => {
if (response && (response.status === 200 || response.status === 201) && attachment.fileToImport) {
return addFileToRequest(attachment, response.data.id);
}
return response;
}).catch((error) => {
console.error(error);
return error;
});
}
/**
* Deletes attachments by dispatching a Vuex action.
* @param {number[]} attachmentsId - The IDs of the attachments to be deleted.
* @param {number} featureId - The ID of the feature related to the attachments.
* @return {Promise<Object>} - The server response to the deletion request.
*/
function deleteAttachement(attachmentsId, featureId) {
const payload = {
attachmentsId: attachmentsId,
featureId: featureId
};
return dispatch('DELETE_ATTACHMENTS', payload)
.then((response) => response);
}
const promisesResult = await Promise.all([
...state.attachmentFormset.map((attachment) => putOrPostAttachement(attachment)),
...state.attachmentsToDelete.map((attachmentsId) => deleteAttachement(attachmentsId, featureId))
]);
state.attachmentsToDelete = [];
return promisesResult;
},
DELETE_ATTACHMENTS({ commit }, payload) {
const url = `${this.state.configuration.VUE_APP_DJANGO_API_BASE}features/${payload.featureId}/attachments/${payload.attachmentsId}/`;
return axios
.delete(url)
.then((response) => {
if (response && response.status === 204) {
commit('REMOVE_ATTACHMENTS_ID_TO_DELETE', payload.attachmentsId);
return response;
}
})
.catch((error) => {
console.error(error);
return error;
});
},
PUT_LINKED_FEATURES({ state, rootState }, featureId) {
return axios
.put(`${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/${featureId}/feature-links/`, state.linkedFormset)
.then((response) => {
if (response.status === 200 && response.data) {
return 'La relation a bien été ajouté';
}
})
.catch((error) => {
throw error;
});
},
DELETE_FEATURE({ rootState }, payload) {
const { feature_id, noFeatureType } = payload;
let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}v2/features/${feature_id}/?` +
`project__slug=${rootState.projects.project.slug}`;
if (!noFeatureType) {
url +=`&feature_type__slug=${rootState['feature-type'].current_feature_type_slug}`;
}
return axios
.delete(url)
.then((response) => response)
.catch(() => {
return false;
});
},
/**
* Initializes extra forms based on the current feature type and its custom fields.
* This function retrieves custom fields for the current feature type, assigns values to them based on the current feature's properties,
* and commits them to the store to be displayed in the form.
*
* @param {Object} context - The Vuex action context, including state, rootGetters, and commit function.
*/
INIT_EXTRA_FORMS({ state, rootGetters, commit }) {
const feature = state.currentFeature; // Current feature being edited or viewed.
const featureType = rootGetters['feature-type/feature_type']; // Retrieves the feature type from root getters.
const customFields = featureType.customfield_set; // Custom fields defined for the feature type.
if (customFields) {
commit('SET_EXTRA_FORMS',
activateFieldsNforceValues( // A hypothetical function to activate fields and enforce their values.
customFields.map((field) => {
// Determines the initial value for the field
let value = feature.properties ? feature.properties[field.name] : findXformValue(feature, field);
// If the field is a boolean and the value is null, sets it to false
if (field.field_type === 'boolean' && value === null) {
value = false;
}
// Returns a new object with the updated value and the rest of the field's properties
return { ...field, value };
})
).sort((a, b) => a.position - b.position) // Sorts fields by their user-defined position.
);
}
},
},
};
export default feature;
const feature_type = {
namespaced: true,
state: {
form: null,
colorsStyleList: [],
customForms: [],
current_feature_type_slug: null,
feature_types: [ // TODO: récupérer a partir du projet depuis api
{
geom_type: "point",
title: "Éolienne",
is_editable: true,
},
{
geom_type: "linestring",
title: "Limite illimitée",
is_editable: true,
},
{
geom_type: "polygon",
title: "Zone de zonage",
is_editable: true,
}
],
},
mutations: {
SET_CURRENT_FEATURE_TYPE_SLUG(state, payload) {
state.current_feature_type_slug = payload;
},
UPDATE_FORM(state, payload) {
state.form = payload;
},
ADD_CUSTOM_FORM(state, dataKey) {
state.customForms = [...state.customForms, { dataKey }];
},
UPDATE_CUSTOM_FORM(state, payload) {
const index = state.customForms.findIndex((el) => el.dataKey === payload.dataKey);
if (index !== -1) state.customForms[index] = payload
},
REMOVE_CUSTOM_FORM(state, payload) {
state.customForms = state.customForms.filter(form => form.dataKey !== payload);
},
UPDATE_COLOR_STYLE(state) { // * un peu dirty (mais pas quick) car getter pas réactif avec nested et compliqué de faire dans composants enfants multiples
let res = [];
for (let form of state.customForms) {
if (form.label) res.push(form);
}
state.colorsStyleList = res;
}
},
getters: {
feature_type: state => state.feature_types.find(
(el) => el.title === state.current_feature_type_slug
)
},
actions: {
POST_FEATURE_TYPE({ state }) {
const data = { form: state.form, formset: state.customForms }
console.log("data", data)
/* axios
.post(`${DJANGO_API_BASE}feature_type/`, data)
.then((response) => {
const routerHistory = router.options.routerHistory
commit("SET_USER", response.data.user);
router.push(routerHistory[routerHistory.length - 1] || "/")
dispatch("GET_USER_LEVEL_PROJECTS");
})
.catch(() => {
commit("SET_USER", false)
}); */
},
},
}
export default feature_type
\ No newline at end of file
/**
* Automatically imports all the modules and exports as a single module object
**/
const requireModule = require.context('.', false, /\.store\.js$/);
const modules = {};
requireModule.keys().forEach(filename => {
// create the module name from fileName
// remove the store.js extension
const moduleName = filename.replace(/(\.\/|\.store\.js)/g, '');
modules[moduleName] = requireModule(filename).default || requireModule(filename);
});
export default modules;
\ No newline at end of file
import { mapUtil } from "@/assets/js/map-util.js";
const map = {
namespaced: true,
state: {
basemaps: [
{
"id": 1,
"title": "Fond par défaut",
"layers": [
{
"id": 1,
"title": "Open street map",
"opacity": "1.00",
"order": 0,
"queryable": false
}
]
}
],
features: [],
layers: [
{
"id": 1,
"options": {
"maxZoom": 20,
"attribution": "© les contributeurs d’OpenStreetMap"
},
"title": "Open street map",
"service": "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
"schema_type": "tms"
}
],
serviceMap: "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
optionsMap: {
"attribution": "&copy; contributeurs d'<a href=\"https://osm.org/copyright\">OpenStreetMap</a>",
"maxZoom": 20
}
},
actions: {
INITIATE_MAP({ state, rootGetters }) {
const project = rootGetters.project
var mapDefaultViewCenter = [37.7749, -122.4194]; // defaultMapView.center;
var mapDefaultViewZoom = 13; // defaultMapView.zoom;
mapUtil.createMap({
mapDefaultViewCenter,
mapDefaultViewZoom,
});
// Load the layers.
// - if one basemap exists, check in the localstorage if one active basemap is set
// - if no current active basemap, get the first index
// - if not, load the default map and service options
// todo : create endpoints to get : 'baseMaps' ,'project' ,'layers' ,'serviceMap' ,'optionsMap' ,'features'
let layersToLoad = null;
if (state.baseMaps && state.baseMaps.length > 0) {
// Use active one if exists, otherwise index 0 (first basemap in the list)
const mapOptions =
JSON.parse(localStorage.getItem("geocontrib-map-options")) || {};
const basemapIndex =
mapOptions &&
mapOptions[project] &&
mapOptions[project]["current-basemap-index"]
? mapOptions[project]["current-basemap-index"]
: 0;
layersToLoad = state.baseMaps[basemapIndex].layers;
layersToLoad.forEach((layerToLoad) => {
state.layers.forEach((layer) => {
if (layer.id === layerToLoad.id) {
layerToLoad = Object.assign(layerToLoad, layer);
}
});
});
layersToLoad.reverse();
}
mapUtil.addLayers(layersToLoad, state.serviceMap, state.optionsMap);
// Remove multiple interactions with the map
mapUtil.getMap().dragging.disable();
mapUtil.getMap().doubleClickZoom.disable();
mapUtil.getMap().scrollWheelZoom.disable();
// Add the features
const featureGroup = mapUtil.addFeatures(state.features);
if (featureGroup && featureGroup.getLayers().length > 0) {
mapUtil.getMap().fitBounds(featureGroup.getBounds());
}
}
}
}
export default map
\ No newline at end of file
import axios from '@/axios-client.js';
import mapService from '@/services/map-service';
const map = {
namespaced: true,
state: {
map: null,
basemaps: null,
basemapsToDelete: [],
features: [],
geojsonFeatures: null,
availableLayers: null,
},
mutations: {
SET_MAP(state, payload) {
state.map = payload;
},
SET_LAYERS(state, availableLayers) {
state.availableLayers = availableLayers;
},
SET_GEOJSON_FEATURES(state, geojsonFeatures) {
state.geojsonFeatures = geojsonFeatures;
},
SET_BASEMAPS(state, basemaps) {
state.basemaps = basemaps;
},
CREATE_BASEMAP(state, id) {
state.basemaps = [...state.basemaps, { id, title: '', layers: [], errors: [] }];
},
UPDATE_BASEMAPS(state, basemaps) {
state.basemaps = basemaps;
},
UPDATE_BASEMAP(state, { title, id, layers, errors }) {
const index = state.basemaps.findIndex((el) => el.id === id);
if (index !== -1) {
state.basemaps[index].title = title;
state.basemaps[index].errors = errors;
if (layers) {
state.basemaps[index].layers = layers;
}
}
},
DELETE_BASEMAP(state, basemapId) {
state.basemaps = state.basemaps.filter(el => el.id !== basemapId);
state.basemapsToDelete.push(basemapId);
},
REMOVE_BASEMAP_ID_TO_DELETE(state, basemapId) {
state.basemapsToDelete = state.basemapsToDelete.filter(el => el !== basemapId);
},
REPLACE_BASEMAP_LAYERS(state, { basemapId, layers }) {
const index = state.basemaps.findIndex((el) => el.id === basemapId);
if (index !== -1) {
state.basemaps[index].layers = layers;
}
},
DELETE_BASEMAP_LAYER(state, { basemapId, layerId }) {
const index = state.basemaps.findIndex((el) => el.id === basemapId);
if (index !== -1) {
state.basemaps[index].layers = state.basemaps[index].layers.filter((el) => el.dataKey !== layerId);
}
},
UPDATE_BASEMAP_LAYER(state, { basemapId, layerId, layer }) {
const index = state.basemaps.findIndex((el) => el.id === basemapId);
if (index !== -1) {
state.basemaps[index].layers = state.basemaps[index].layers.map(
(el) => el.dataKey === layerId ? layer : el
);
}
},
},
getters: {
basemapMaxId: (state) => state.basemaps.reduce((acc, curr) => {
if (curr.id > acc) {
return curr.id;
} else {
return acc;
}
}, 0)
},
actions: {
GET_AVAILABLE_LAYERS({ commit }) {
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}v2/layers/`)
.then((response) => (commit('SET_LAYERS', response.data)))
.catch((error) => {
throw error;
});
},
GET_BASEMAPS({ commit }, project_slug) {
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}v2/base-maps/?project__slug=${project_slug}`)
.then((response) => {
if (response.status === 200 && response.data) {
commit('SET_BASEMAPS', response.data);
}
return response;
})
.catch((error) => {
throw error;
});
},
INITIATE_MAP({ commit, rootState }, options) {
var mapDefaultViewCenter =
this.state.configuration.DEFAULT_MAP_VIEW.center;
var mapDefaultViewZoom =
this.state.configuration.DEFAULT_MAP_VIEW.zoom;
mapService.createMap(options.el, {
mapDefaultViewZoom: options.zoom || mapDefaultViewZoom || 5,
mapDefaultViewCenter: options.center || mapDefaultViewCenter || [46.0, 2.0],
maxZoom: options.maxZoom || rootState.projects.project.map_max_zoom_level,
controls: options.controls,
zoomControl: options.zoomControl,
interactions : { doubleClickZoom :false, mouseWheelZoom:true, dragPan:true , ...options.interactions },
geolocationControl: true,
});
const map = { ...mapService.getMap() };
commit('SET_MAP', map);
},
async SAVE_BASEMAPS({ state, rootState, dispatch }, newBasemapIds) {
//* send new basemaps synchronously to create their ids in the order they were created in the form
let promisesResult = [];
function postOrPut(basemapsToSend) {
if (basemapsToSend.length > 0) { //* execute the function recursively as long as there is still a basemap to send
let basemap = basemapsToSend.shift(); //* remove and return first item in array
basemap['project'] = rootState.projects.project.slug;
let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}v2/base-maps/`;
if (!newBasemapIds.includes(basemap.id)) url += `${basemap.id}/`;
axios({
url,
method: newBasemapIds.includes(basemap.id) ? 'POST' : 'PUT',
data: basemap,
})
.then((response) => {
postOrPut(basemapsToSend);
promisesResult.push(response);
})
.catch((error) => {
postOrPut(basemapsToSend);
promisesResult.push(error);
});
}
}
function deleteBMap(basemapId) {
//* delete in the backend the basemaps that was rewoved from the front
return dispatch('DELETE_BASEMAP', basemapId)
.then((response) => response);
}
//* save new or modifed basemap
postOrPut([...state.basemaps]);
//* delete basemaps
const deletedResult = await Promise.all(state.basemapsToDelete.map((basemapId) => deleteBMap(basemapId)));
state.basemapsToDelete = [];
//* return promises results
return [...promisesResult, ...deletedResult];
},
DELETE_BASEMAP({ commit, rootState }, basemapId) {
const url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}v2/base-maps/${basemapId}/`;
return axios
.delete(url)
.then((response) => {
if (response && response.status === 204) {
commit('REMOVE_BASEMAP_ID_TO_DELETE', basemapId);
return response;
}
})
.catch((error) => {
console.error(error);
return error;
});
}
},
};
export default map;
const modals = {
namespaced: true,
state: {
isProjectModalOpen: false,
projectModalType: null
},
mutations: {
OPEN_PROJECT_MODAL(state, payload) {
state.isProjectModalOpen = true;
state.projectModalType = payload;
},
CLOSE_PROJECT_MODAL(state) {
state.isProjectModalOpen = false;
state.projectModalType = null;
}
},
actions: {
}
};
export default modals;
import axios from '@/axios-client.js';
import projectAPI from '@/services/project-api';
const initialFilters = {
moderation: null,
access_level: null,
user_access_level: null,
accessible: null
};
/**
* Constructs the URL for the search request, appending search text and any active filters.
*
* @param {Object} rootState - The root state to access global configuration settings.
* @param {Object} filters - The current state of filters applied to the search.
* @param {String} text - The current search text.
* @returns {String} The fully constructed URL for the search request.
*/
function constructSearchUrl({ baseUrl, filters, text, page }) {
let url = `${baseUrl}v2/projects/?`;
// Append page number if provided.
if (page) {
url += `page=${page}`;
}
// Append search text if provided.
if (text) {
url += `&search=${encodeURIComponent(text)}`;
}
// Append each active filter to the URL.
Object.entries(filters).forEach(([key, value]) => {
if (value) {
url += `&${key}=${encodeURIComponent(value)}`;
}
});
return url;
}
const projectsStore = {
namespaced: true,
state: {
count: 0,
currentPage: 1,
filters: { ...initialFilters },
isProjectsListSearched: null,
last_comments: [],
projects: [],
project: null,
projectUsers: [],
searchProjectsFilter: null,
},
mutations: {
SET_CURRENT_PAGE (state, payload) {
state.currentPage = payload;
},
SET_PROJECTS(state, projects) {
if (projects.results) {
state.projects = projects.results;
state.count = projects.count;
} else {
state.projects = projects;
state.count = projects.length;
}
},
ADD_PROJECT(state, project) {
state.projects = [project, ...state.projects];
},
SET_PROJECT(state, project) {
state.project = project;
},
SET_PROJECT_USERS(state, users) {
state.projectUsers = users;
},
SET_PROJECTS_FILTER(state, payload) {
state.filters[payload.filter] = payload.value;
},
SET_PROJECTS_SEARCH_STATE(state, payload) {
state.isProjectsListSearched = payload.isSearched;
state.searchProjectsFilter = payload.text;
},
SET_PROJECT_COMMENTS(state, last_comments) {
state.last_comments = last_comments;
},
},
actions: {
async GET_PROJECTS({ state, rootState, commit }, payload) {
let { page, myaccount, projectSlug } = payload || {};
if (!page) {
page = state.currentPage;
}
const baseUrl = rootState.configuration.VUE_APP_DJANGO_API_BASE;
const projects = await projectAPI.getProjects({
baseUrl,
filters : state.filters,
page,
projectSlug,
myaccount,
text: state.searchProjectsFilter
});
commit('SET_PROJECTS', projects);
return;
},
async SEARCH_PROJECTS({ commit, dispatch }, text) {
if (text) {
await dispatch('HANDLE_PROJECTS_SEARCH_REQUEST', text);
} else {
commit('SET_PROJECTS_SEARCH_STATE', {
isSearched: false,
text: null
});
await dispatch('GET_PROJECTS');
}
},
async GET_PROJECT({ rootState, commit }, slug) { // todo : use GET_PROJECTS instead, with slug
const baseUrl = rootState.configuration.VUE_APP_DJANGO_API_BASE;
const project = await projectAPI.getProject(baseUrl, slug);
commit('SET_PROJECT', project);
return project;
},
async GET_PROJECT_USERS({ rootState, commit }, projectSlug) {
const baseUrl = rootState.configuration.VUE_APP_DJANGO_API_BASE;
const users = await projectAPI.getProjectUsers(baseUrl, projectSlug);
commit('SET_PROJECT_USERS', users);
return users;
},
async GET_PROJECT_INFO({ dispatch }, slug) {
const promises = [
dispatch('GET_PROJECT_LAST_MESSAGES', slug).then(response => response),
dispatch('feature-type/GET_PROJECT_FEATURE_TYPES', slug, { root: true }).then(response => response),
dispatch('map/GET_BASEMAPS', slug, { root: true }).then(response => response)
];
const promiseResult = await Promise.all(promises);
return promiseResult;
},
GET_PROJECT_LAST_MESSAGES({ commit }, project_slug) {
return axios
.get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${project_slug}/comments/`)
.then((response) => {
if (response && response.status === 200) {
commit('SET_PROJECT_COMMENTS', response.data.last_comments);
}
return response;
})
.catch((error) => {
throw error;
});
},
/**
* Asynchronously handles the search request for projects, incorporating search text and applied filters.
* Cancels any ongoing search request to ensure that only the latest request is processed,
* which enhances the responsiveness of search functionality.
*
* @param {Object} context - Destructured to gain access to Vuex state, rootState, and commit function.
* @param {String} text - The search text used for filtering projects.
*/
async HANDLE_PROJECTS_SEARCH_REQUEST({ state, rootState, commit }, { page, text }) {
// Cancel any ongoing search request.
if (rootState.cancellableSearchRequest.length > 0) {
const currentRequestCancelToken =
rootState.cancellableSearchRequest[rootState.cancellableSearchRequest.length - 1];
currentRequestCancelToken.cancel();
}
// Prepare the cancel token for the new request and store it.
const cancelToken = axios.CancelToken.source();
commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true });
// Construct the search URL with any applied filters.
const searchUrl = constructSearchUrl({
baseUrl: rootState.configuration.VUE_APP_DJANGO_API_BASE,
filters: state.filters,
text,
page
});
try {
// Perform the search request.
const response = await axios.get(searchUrl, { cancelToken: cancelToken.token });
// Process successful response.
if (response.status === 200 && response.data) {
commit('SET_PROJECTS', response.data);
commit('SET_PROJECTS_SEARCH_STATE', {
isSearched: true,
text: text
});
}
} catch (error) {
// Handle potential errors, such as request cancellation.
console.error('Search request canceled or failed', error);
}
}
}
};
export default projectsStore;
import featureAPI from '@/services/feature-api';
import { isEqual, isNil } from 'lodash';
export function formatStringDate(stringDate) {
const date = new Date(stringDate);
if (date instanceof Date && !isNaN(date.valueOf())) {
const formatted_date = date.getFullYear() + '/' + ('0' + (date.getMonth() + 1)).slice(-2) + '/' + ('0' + date.getDate()).slice(-2) + ' ' +
('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
return formatted_date;
}
return stringDate;
}
export const statusChoices = [
{
name: 'Brouillon',
value: 'draft',
},
{
name: 'En attente de publication',
value: 'pending',
},
{
name: 'Publié',
value: 'published',
},
{
name: 'Archivé',
value: 'archived',
},
];
export function allowedStatus2change(user, isModerate, userStatus, isOwnFeature, currentRouteName) {
if ( //* si 'super-admin'(superuser) admin, modérateur ou super contributeur, statuts toujours disponibles: Brouillon, Publié, Archivé
user.is_superuser ||
userStatus === 'Modérateur' ||
userStatus === 'Administrateur projet' ||
(userStatus === 'Super Contributeur' && !isModerate)
) {
return statusChoices.filter((el) => el.value !== 'pending');
} else if (userStatus === 'Super Contributeur' && isModerate) {
return statusChoices.filter(
(el) => el.value === 'draft' || el.value === 'pending'
);
} else if (userStatus === 'Contributeur') { //* cas particuliers du contributeur
if (currentRouteName === 'ajouter-signalement' || !isOwnFeature) {
//* même cas à l'ajout d'une feature ou si feature n'a pas été créé par le contributeur
//? Incohérence / Inutile ? Un contributeur ne peut pas modifier un signalement d'un autre utilisateur
return isModerate
? statusChoices.filter(
(el) => el.value === 'draft' || el.value === 'pending'
)
: statusChoices.filter(
(el) => el.value === 'draft' || el.value === 'published'
);
} else {
//* à l'édition d'une feature et si le contributeur est l'auteur de la feature
return isModerate
? statusChoices.filter(
(el) => el.value !== 'published' //* toutes sauf "Publié"
)
: statusChoices.filter(
(el) => el.value !== 'pending' //* toutes sauf "En cours de publication"
);
}
}
return [];
}
/**
* Determines the type of a property based on its value.
*
* This function inspects a given property and returns a string indicating its type,
* such as 'boolean', 'integer', 'decimal', 'date', 'text', or 'char'.
* It uses various checks to determine the appropriate type for different value formats.
*
* @param {any} prop - The property value to be evaluated.
* @returns {string} The determined type of the property ('boolean', 'integer', 'decimal', 'date', 'text', or 'char').
*/
export function transformProperties(prop) {
const type = typeof prop;
const regInteger = /^-?\d+$/; // Regular expression to match integer numbers
const regFloat = /^-?\d*\.\d+$/; // Regular expression to match decimal numbers
const regText = /[\r\n]/; // Regular expression to detect multiline text (newlines)
const regDate = /^\d{4}-\d{2}-\d{2}$|^\d{2}\/\d{2}\/\d{4}$/; // Regular expression to match common date formats
// Check if the property is a boolean or a string that represents a boolean
if (type === 'boolean' || (type === 'string' && (prop.toLowerCase() === 'true' || prop.toLowerCase() === 'false'))) {
return 'boolean';
} else if (regInteger.test(prop) || (type === 'number' && Number.isSafeInteger(prop))) {
// Check if the property is an integer or a string that represents an integer
return 'integer';
} else if (type === 'string' && regDate.test(prop.trim())) {
// More specific check for date strings using regular expressions
return 'date';
} else if (regFloat.test(prop) || (type === 'number' && !Number.isSafeInteger(prop))) {
// Check if the property is a decimal number or a string that represents a decimal
return 'decimal';
} else if (regText.test(prop) || (type === 'string' && prop.length > 255)) {
// Check if the property contains newline characters or is a long text
return 'text';
}
// Default case for all other types: assume it is a short text or character field
return 'char';
}
export function objIsEmpty(obj) {
for(const prop in obj) {
if(Object.hasOwn(obj, prop)) {
return false;
}
}
return true;
}
export const reservedKeywords = [
// todo : add keywords for mapstyle (strokewidth...)
'id',
'title',
'description',
'status',
'created_on',
'updated_on',
'archived_on',
'deletion_on',
'feature_type',
'feature_id',
'display_creator',
'display_last_editor',
'project',
'creator',
'lat',
'lon'
];
export const customFieldTypeChoices = [
{ name: 'Booléen', value: 'boolean' },
{ name: 'Chaîne de caractères', value: 'char' },
{ name: 'Date', value: 'date' },
{ name: 'Liste de valeurs', value: 'list' },
{ name: 'Liste de valeurs pré-enregistrées', value: 'pre_recorded_list' },
{ name: 'Liste à choix multiples', value: 'multi_choices_list' },
{ name: 'Nombre entier', value: 'integer' },
{ name: 'Nombre décimal', value: 'decimal' },
{ name: 'Texte multiligne', value: 'text' },
{ name: 'Notification à un groupe', value: 'notif_group' },
];
export const featureNativeFields = [
{ name: 'status', label: 'Statut', field_type: 'Champ GéoContrib' },
{ name: 'feature_type', label: 'Type', field_type: 'Champ GéoContrib' },
{ name: 'updated_on', label: 'Dernière mise à jour', field_type: 'Champ GéoContrib' },
{ name: 'created_on', label: 'Date de création', field_type: 'Champ GéoContrib' },
{ name: 'display_creator', label: 'Auteur', field_type: 'Champ GéoContrib' },
{ name: 'display_last_editor', label: 'Dernier éditeur', field_type: 'Champ GéoContrib' },
];
export const formatDate = (current_datetime) => {
let formatted_date = current_datetime.getFullYear() + '-' + ('0' + (current_datetime.getMonth() + 1)).slice(-2) + '-' + ('0' + current_datetime.getDate()).slice(-2) + '&nbsp;' +
('0' + current_datetime.getHours()).slice(-2) + ':' + ('0' + current_datetime.getMinutes()).slice(-2);
return formatted_date;
};
export const retrieveFeatureProperties = async (feature, featureTypes, projectSlug) => {
const properties = feature.getProperties();
let { feature_type, status, updated_on, created_on, creator, display_last_editor, index } = properties;
if (creator) {
creator = creator.full_name ? `${creator.first_name} ${creator.last_name}` : creator.username;
} else if (properties.feature_id) {
//* if *** MVT *** feature, retrieve display_creator and display_last_editor by fetching the feature details from API
const fetchedFeature = await featureAPI.getProjectFeature(projectSlug, properties.feature_id);
if (fetchedFeature) {
creator = fetchedFeature.properties.display_creator;
display_last_editor = fetchedFeature.properties.display_last_editor;
feature_type = fetchedFeature.properties.feature_type;
}
}
if (featureTypes && feature_type) {
feature_type = featureTypes.find((el) => el.slug === (feature_type.slug || feature_type));
}
if (updated_on && !isNaN(new Date(updated_on))) { //* check if date is already formatted
updated_on = formatDate(new Date(updated_on));
}
if (created_on && !isNaN(new Date(created_on))) { //* check if date is already formatted
created_on = formatDate(new Date(created_on));
}
if (status) {
if (status.label) { //* when the label is already in the feature
status = status.label;
} else if (featureTypes) { //* if not, retrieve the name/label from the list
status = statusChoices.find((el) => el.value === status).name;
}
}
return { feature_type, status, updated_on, created_on, creator, display_last_editor, index };
};
export function findXformValue(feature, customField) {
if (!feature) return null;
if (feature.properties) {
return feature.properties[customField.name] || null;
} else if (feature.feature_data) {
const field = feature.feature_data.find((el) => el.label === customField.label);
return field ? field.value : null;
}
return null;
}
export function isXtraFormActive(extraForms, config) { // return true if no config or if the condition is fullfilled
if (config) { // if conditional field configuration is not null
// get name and value in condition
const { conditionField, conditionValue } = config;
// get the customForm which activates conditional field
const conditioningXForm = extraForms.find((xForm) => xForm.name === conditionField);
// check if the conditioning extraform value match the condition value
if (conditioningXForm) {
// if the values to compare are null or undefined the field can't be activated
if (isNil(conditioningXForm.value) || isNil(conditionValue)) {
return false;
} else if (Array.isArray(conditionValue) && Array.isArray(conditioningXForm.value)) { // case of multiple list or prerecorded values list
return conditioningXForm.value.some((value) => conditionValue.includes(value));
} else if (typeof conditioningXForm.value === 'object' && conditioningXForm.value.label) { // case of simple list
return conditioningXForm.value.label === conditionValue.label;
} else {
return conditioningXForm.value === conditionValue; // more simple case of other fields
}
}
}
return true;
}
export function checkDeactivatedValues(extraForms) {
// if changes occured, update extraForms array with freshly checked active customForms
let newExtraForms = extraForms.map((xForm) => { // we use 'deactivate' instead of 'activate' because at initialization this property cannot be evaluated ...
const isDeactivated = !isXtraFormActive(extraForms, xForm.conditional_field_config); // ... if the component is not created to set this property, thus no extra form would appear at all
// toggle value to null to deactivate other fields conditioned by it
if (isDeactivated) {
xForm['value'] = null;
}
return { ...xForm, ['isDeactivated']: isDeactivated };
});
return newExtraForms;
}
export function checkFieldForcedValue(field, extraForms) {
field['disabled'] = false; //* create a property disabled and (re)set to false by default
if (field.forced_value_config) {
//* loop over each forced value config for this extraForm
for (const config of field.forced_value_config) {
//* find the extraForm field conditioning the forced value
const conditioningField = extraForms.find((xtraForm) => xtraForm.name === config.conditionField);
//* if found check that its value match the condtionValue
if (conditioningField && isEqual(conditioningField.value, config.conditionValue)) {
//* set this value with the forced value and disable the form field
field.value = config.forcedValue;
field.disabled = true;
}
}
}
return field;
}
export function activateFieldsNforceValues(extraForms) {
for (const [index, field] of extraForms.entries()) {
const checkedField = checkFieldForcedValue(field, extraForms);
//* each time a value changes, call this function recursively, until there is no more change
if (checkedField.value !== field.value) {
extraForms[index] = checkedField; //* update the value in extraForms
activateFieldsNforceValues(extraForms); //* call the function with new extraForms
}
}
//* when no more changes detected in the loop, check for deactivated extraForms
extraForms = checkDeactivatedValues(extraForms);
//* return extraForms from the lastly called function
return extraForms;
}
export function formatUserOption(user) {
let name = user.first_name || '';
if (user.last_name) {
name = name + ' ' + user.last_name;
}
return {
name: [name, user.username],
value: user.id,
};
}
\ No newline at end of file
<template>
<div id="account">
<h1>Mon compte</h1>
<div class="ui stackable grid">
<div class="five wide column">
<UserProfile />
<UserActivity />
</div>
<div class="eleven wide column">
<UserProjectsList />
</div>
</div>
</div>
</template>
<script>
import UserProfile from '@/components/Account/UserProfile.vue';
import UserProjectsList from '@/components/Account/UserProjectsList.vue';
import UserActivity from '@/components/Account/UserActivity.vue';
export default {
name: 'Account',
components: {
UserProfile,
UserProjectsList,
UserActivity
},
};
</script>
<style lang="less" scoped>
#account {
max-width: 1000px;
}
</style>
\ No newline at end of file
<template>
<div
v-if="(configuration.VUE_APP_SSO_LOGIN_URL_WITH_REDIRECT && !user) || !currentFeature"
class="no-access"
>
<h3>
🔒&nbsp;Vous n'avez pas accès à ce signalement
<span v-if="!user"> en tant qu'utilisateur anonyme&nbsp;🥸</span>
</h3>
<p v-if="!user">
Veuillez vous connectez afin de pouvoir visualiser le document
</p>
</div>
<div
v-else
:class="['preview', { is_pdf }]"
>
<embed
v-if="is_pdf"
:src="src"
type="application/pdf"
>
<div v-else>
<img
:src="src"
alt="Aperçu de l'image"
>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'AttachmentPreview',
computed: {
...mapState([
'configuration',
'user'
]),
...mapState('feature', [
'currentFeature'
]),
src() {
return this.$route.query.file;
},
is_pdf() {
return this.src && this.src.includes('pdf');
},
},
watch: {
user() {
/**
* Specific for platform with login by token
* When the user is setted, fetching again the feature with the cookies setted
* since automatic authentification can take time to return the response
* setting the cookies, while the app is loading already
*/
this.getFeature();
}
},
mounted() {
this.getFeature();
},
methods: {
getFeature() {
console.log('getFeature'); // Keeping for debugging after deployment
this.$store.dispatch('feature/GET_PROJECT_FEATURE', {
project_slug: this.$route.params.slug,
feature_id: this.$route.params.slug_signal,
});
}
}
};
</script>
<style scoped lang="less">
.no-access {
> h1 {
margin: 2em 0;
}
height: 60vh;
display: flex;
justify-content: center;
flex-direction: column;
> * {
text-align: center;
}
}
.preview {
width: 100vw;
&.is_pdf {
padding: 0;
@media screen and (min-width: 726px) {
height: calc(100vh - 70px - 1em);
margin: .5em auto;
box-shadow: 1px 2px 10px grey;
}
@media screen and (max-width: 725px) {
height: calc(100vh - 110px);
margin: 0 auto;
}
}
> * {
height: 100%;
width: 100%;
}
> div {
display: flex;
justify-content: center;
img {
max-width: 100%;
}
}
}
</style>
\ No newline at end of file