Newer
Older

Timothee P
committed
<template>

Timothee P
committed
<div id="feature-edit">
<h1>
<span v-if="feature_type && currentRouteName === 'ajouter-signalement'">
Création d'un signalement <small>[{{ feature_type.title }}]</small>
</span>
<span v-else-if="currentFeature && currentRouteName === 'editer-signalement'">
Mise à jour du signalement "{{ currentFeature.properties ?
currentFeature.properties.title : currentFeature.id }}"
</span>
<span v-else-if="feature_type && currentRouteName === 'editer-attribut-signalement'">
Mise à jour des attributs de {{ checkedFeatures.length }} signalements
</span>
</h1>
<form
id="form-feature-edit"
action=""
method="post"
enctype="multipart/form-data"
class="ui form"
>
<!-- Feature Fields -->
<div
v-if="currentRouteName !== 'editer-attribut-signalement'"
class="two fields"
>
<div :class="field_title">
<label :for="form.title.id_for_label">{{ form.title.label }}</label>
<input
:id="form.title.id_for_label"
v-model="form.title.value"
type="text"
required
:maxlength="form.title.field.max_length"
:name="form.title.html_name"
@blur="updateStore"
>
<ul
id="infoslist-title"
class="infoslist"
>
<li
v-for="info in form.title.infos"
:key="info"
>
{{ info }}
</li>
</ul>
<ul
id="errorlist-title"
class="errorlist"
>
<li
v-for="error in form.title.errors"
:key="error"

Timothee P
committed
</div>
<div class="required field">
<label :for="form.status.id_for_label">{{
form.status.label

Timothee P
committed
}}</label>
<Dropdown
:options="allowedStatusChoices"
:selected="selected_status.name"
:selection.sync="selected_status"

Timothee P
committed
</div>
<div
v-if="currentRouteName !== 'editer-attribut-signalement'"
class="field"
>
<label :for="form.description.id_for_label">{{
form.description.label
}}</label>
<textarea
v-model="form.description.value"
:name="form.description.html_name"
rows="5"
@blur="updateStore"
/>
</div>
<!-- Geom Field -->
<div
v-if="currentRouteName !== 'editer-attribut-signalement'"
class="field"
>
<label :for="form.geom.id_for_label">{{ form.geom.label }}</label>
<!-- Import GeoImage -->
<div
v-if="feature_type && feature_type.geom_type === 'point'"
>
<p v-if="isOnline">
<button
id="add-geo-image"
type="button"
class="ui compact button"
@click="toggleGeoRefModal"
>
<i
class="file image icon"
aria-hidden="true"
/>Importer une image géoréférencée
</button>
Vous pouvez utiliser une image géoréférencée pour localiser le
signalement.
</p>
v-if="showGeoRef"
class="ui dimmer modals page transition visible active"
style="display: flex !important"

Timothee P
committed
<div
class="ui mini modal transition visible active"
style="display: block !important"

Timothee P
committed
>
@click="toggleGeoRefModal"
/>
<div class="content">
<h3>Importer une image géoréférencée</h3>
<form
id="form-geo-image"
class="ui form"
enctype="multipart/form-data"
>
<p>
Attention, si vous avez déjà saisi une géométrie, celle
issue de l'image importée l'écrasera.
</p>
<div class="field georef-btn">
<label>Image (png ou jpeg)</label>
<label
class="ui icon button"
for="image_file"
>
<span class="label">{{ geoRefFileLabel }}</span>
</label>
<input
id="image_file"
ref="file"
type="file"
accept="image/jpeg, image/png"
style="display: none"
name="image_file"
class="image_file"
@change="handleFileUpload"

Timothee P
committed
>
<ul
v-if="erreurUploadMessage"
class="errorlist"
>
<li>
{{ erreurUploadMessage }}
</li>
</ul>
</div>
<button
id="get-geom-from-image-file"
type="button"
:class="[
'ui compact button',
file && !erreurUploadMessage ? 'green' : 'disabled',
{ red: erreurUploadMessage },
]"
@click="georeferencement"
>

Timothee P
committed
</div>
<p v-if="showGeoPositionBtn">
<button
id="create-point-geoposition"
type="button"
class="ui compact button"
@click="create_point_geoposition"
<i
class="ui map marker alternate icon"
aria-hidden="true"
/>
Positionner le
signalement à partir de votre géolocalisation
</button>
</p>
<span
v-if="erreurGeolocalisationMessage"
id="erreur-geolocalisation"
<div class="ui negative message">
<div class="header">
Une erreur est survenue avec la fonctionnalité de
géolocalisation
</div>
<p id="erreur-geolocalisation-message">
{{ erreurGeolocalisationMessage }}
</p>
</div>
<br>
</span>
</div>
<ul
id="errorlist-geom"
class="errorlist"
>
<li
v-for="error in form.geom.errors"
:key="error"
{{ error }}
</li>
</ul>
<!-- Map -->
<input
:id="form.geom.id_for_label"
v-model="form.geom.value"
type="hidden"
:name="form.geom.html_name"
@blur="updateStore"
>
<div
class="ui tab active map-container"
data-tab="map"
>
<div
:class="{ active: mapLoading }"
class="ui inverted dimmer"
>
<div class="ui loader" />
</div>
>
<SidebarLayers v-if="basemaps && map" />
<EditingToolbar
v-if="isEditable"
:map="map"
/>
</div>
<div
id="popup"
class="ol-popup"
>
<a
id="popup-closer"
href="#"
class="ol-popup-closer"
/>
<div
id="popup-content"
/>
</div>

Timothee P
committed
</div>
</div>
<!-- Extra Fields -->
<div class="ui horizontal divider">
DONNÉES MÉTIER
</div>
<div
v-for="(field, index) in orderedCustomFields"
:key="field.field_type + index"

Timothee P
committed
<div v-if="isOnline && currentRouteName !== 'editer-attribut-signalement'">
<FeatureAttachmentForm
v-for="attachForm in attachmentFormset"
:key="attachForm.dataKey"
ref="attachementForm"
:attachment-form="attachForm"
/>

Timothee P
committed
</div>
<button
id="add-attachment"
type="button"
class="ui compact basic button"
@click="add_attachement_formset"
>
<i
class="ui plus icon"
aria-hidden="true"
/>
Ajouter une pièce jointe

Timothee P
committed
<div v-if="isOnline && currentRouteName !== 'editer-attribut-signalement'">
<div class="ui horizontal divider">
SIGNALEMENTS LIÉS
<div id="formsets-link">
<FeatureLinkedForm
v-for="linkForm in linkedFormset"
:key="linkForm.dataKey"
ref="linkedForm"
:linked-form="linkForm"
/>
class="ui compact basic button"
@click="add_linked_formset"
<i
class="ui plus icon"
aria-hidden="true"
/>
Ajouter une liaison

Timothee P
committed
</button>
:class="['ui teal icon button', { loading: sendingFeature }]"
<i
class="white save icon"
aria-hidden="true"
/>
Enregistrer les changements

Timothee P
committed
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import FeatureAttachmentForm from '@/components/Feature/FeatureAttachmentForm';
import FeatureLinkedForm from '@/components/Feature/FeatureLinkedForm';
import FeatureExtraForm from '@/components/Feature/Edit/FeatureExtraForm';
import Dropdown from '@/components/Dropdown.vue';
import SidebarLayers from '@/components/Map/SidebarLayers';
import EditingToolbar from '@/components/Map/EditingToolbar';
import featureAPI from '@/services/feature-api';
import mapService from '@/services/map-service';
import editionService from '@/services/edition-service';
import { statusChoices, allowedStatus2change } from '@/utils';
import axios from '@/axios-client.js';
import { GeoJSON } from 'ol/format';

Timothee P
committed
export default {

Timothee P
committed
components: {
FeatureAttachmentForm,
FeatureLinkedForm,

Timothee P
committed
SidebarLayers,

Timothee P
committed
data() {
return {
map: null,
sendingFeature: false,
baseUrl: this.$store.state.configuration.BASE_URL,
file: null,
showGeoRef: false,
showGeoPositionBtn: true,
erreurGeolocalisationMessage: null,
erreurUploadMessage: null,
attachmentDataKey: 0,
linkedDataKey: 0,

Timothee P
committed
title: {

Timothee P
committed
field: {

Timothee P
committed
},
html_name: 'name',
label: 'Nom',
value: '',

Timothee P
committed
},
status: {
id_for_label: 'status',
html_name: 'status',
label: 'Statut',

Timothee P
committed
},
description: {
id_for_label: 'description',
html_name: 'description',
label: 'Description',
value: '',

Timothee P
committed
},
geom: {

Timothee P
committed
},
},
};
},
...mapState([
'user',
'USER_LEVEL_PROJECTS',
'isOnline'
]),
...mapState('projects', [
'project'
]),
...mapState('map', [
'basemaps'
...mapState('feature', [
'attachmentFormset',
'checkedFeatures',
'currentFeature',
'features',
'linkedFormset',
...mapState('feature-type', [
'feature_types'
]),
field_title() {
if (this.feature_type) {
if (this.feature_type.title_optional) {
currentRouteName() {
return this.$route.name;
},
Sébastien DA ROCHA
committed
return [...this.extra_forms].sort((a, b) => a.position - b.position);
selected_status: {
get() {
return this.form.status.value;
},
set(newValue) {
this.form.status.value = newValue;
this.updateStore();
},
},
allowedStatusChoices() {
if (this.project && this.user) {
const isModerate = this.project.moderation;
const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug];
const isOwnFeature = this.currentFeature && this.currentFeature.properties && //* check if feature exist already
this.currentFeature.properties.creator === this.user.id; //* si le contributeur est l'auteur du signalement
return allowedStatus2change(this.user, isModerate, userStatus, isOwnFeature, this.currentRouteName);
isEditable() {
return this.basemaps && this.map && (this.feature_type && !this.feature_type.geom_type.includes('multi'));
}
watch: {
'form.title.value': function(newValue) {
if (newValue && newValue.length === 128) {
this.form.title.infos.push('Le nombre de caractères et limité à 128.');
} else {
this.form.title.infos = [];
}
}
},
this.$route.params.slug_type_signal
);
//* empty previous feature data, not emptying by itself since it doesn't update by itself anymore

Timothee P
committed
if (this.currentRouteName === 'ajouter-signalement' || this.currentRouteName === 'editer-attribut-signalement') {
this.$store.commit('feature/SET_CURRENT_FEATURE', []);
}
if (this.$route.params.slug_signal) {
this.getFeatureAttachments();
this.getLinkedFeatures();
}
},
mounted() {
const promises = [];
if (!this.project) {
promises.push(
this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug),
this.$store.dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug),
);
}
if (this.$route.params.slug_signal) {
promises.push(
this.$store.dispatch('feature/GET_PROJECT_FEATURE', {
project_slug: this.$route.params.slug,
feature_id: this.$route.params.slug_signal,
})
);
}
Promise.all(promises).then(() => {
if (this.currentRouteName !== 'editer-attribut-signalement') {
this.initForm();
this.initMap();
this.onFeatureTypeLoaded(); // init map tools
}
this.$store.dispatch('feature/INIT_EXTRA_FORMS');
beforeDestroy() {
this.$store.dispatch('CANCEL_CURRENT_SEARCH_REQUEST');
},
editionService.removeActiveFeatures();

Timothee P
committed
// emptying to enable adding event listener at feature edition straight after creation
editionService.selectForDeletion = null;
//* be sure that previous Formset have been cleared for creation
this.$store.commit('feature/CLEAR_ATTACHMENT_FORM');
this.$store.commit('feature/CLEAR_LINKED_FORM');
this.$store.commit('feature/CLEAR_EXTRA_FORM');
},

Timothee P
committed
methods: {
if (this.currentRouteName.includes('editer')) {
for (const key in this.currentFeature.properties) {
if (key && this.form[key]) {
const value = this.currentFeature.properties[key];
(key) => key.value === value
this.form[key].value = this.currentFeature.properties[key];
this.form.geom.value = this.currentFeature.geometry;
addPointToCoordinates(coordinates){
let json = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: coordinates,
},
properties: {},
};
this.updateMap(json);
this.updateGeomField(json.geometry);
this.showGeoPositionBtn = false;
this.erreurGeolocalisationMessage = '';
},
create_point_geoposition() {
function success(position) {
this.addPointToCoordinates([position.coords.longitude, position.coords.latitude]);
this.erreurGeolocalisationMessage = err.message;
if (err.message === 'User denied geolocation prompt') {
this.erreurGeolocalisationMessage = null;
this.erreurGeolocalisationMessage =
"La géolocalisation n'est pas supportée par votre navigateur.";
navigator.geolocation.getCurrentPosition(
success.bind(this),
error.bind(this)
);

Timothee P
committed
toggleGeoRefModal() {
if (this.showGeoRef) {
//* when popup closes, empty form

Timothee P
committed
this.file = null;
}
this.showGeoRef = !this.showGeoRef;
},
georeferencement() {
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}exif-geom-reader/`;
axios
.post(url, formData, {
headers: {
},
})

Timothee P
committed
.then((response) => {
if (response.data.geom.indexOf('POINT') >= 0) {
const regexp = /POINT\s\((.*)\s(.*)\)/;
const arr = regexp.exec(response.data.geom);
this.addPointToCoordinates([parseFloat(arr[1]), parseFloat(arr[2])]);

Timothee P
committed
this.addAttachment({

Timothee P
committed
attachment_file: this.file.name,
fileToImport: this.file,
this.toggleGeoRefModal();
if (error && error.response && error.response) {
this.erreurUploadMessage = error.response.data.error;
} else {
this.erreurUploadMessage =
"Une erreur est survenue pendant l'import de l'image géoréférencée";
}
this.$store.commit('feature/ADD_ATTACHMENT_FORM', {
dataKey: this.attachmentDataKey,
}); // * create an object with the counter in store

Timothee P
committed
this.attachmentDataKey += 1; // * increment counter for key in v-for
this.$store.commit('feature/ADD_ATTACHMENT_FORM', {
dataKey: this.attachmentDataKey,
title: attachment.title,
attachment_file: attachment.attachment_file,
info: attachment.info,
fileToImport: attachment.fileToImport,
id: attachment.id,
});
this.attachmentDataKey += 1;
addExistingAttachementFormset(attachementFormset) {
for (const attachment of attachementFormset) {
this.addAttachment(attachment);
}
},
this.$store.commit('feature/ADD_LINKED_FORM', {

Timothee P
committed
dataKey: this.linkedDataKey,
}); // * create an object with the counter in store
this.linkedDataKey += 1; // * increment counter for key in v-for

Timothee P
committed
addExistingLinkedFormset(linkedFormset) {
for (const linked of linkedFormset) {
this.$store.commit('feature/ADD_LINKED_FORM', {

Timothee P
committed
dataKey: this.linkedDataKey,

Timothee P
committed
...linked

Timothee P
committed
});
this.linkedDataKey += 1;
}
},

Timothee P
committed
updateStore() {
this.$store.commit('feature/UPDATE_FORM', {
title: this.form.title.value,
status: this.form.status.value,
description: this.form.description,
geometry: this.form.geom.value,
feature_id: this.currentFeature ? this.currentFeature.id : '',

Timothee P
committed
});
checkFormTitle() {
this.form.title.errors = [];
!this.form.title.errors.includes('Veuillez compléter ce champ.')
this.form.title.errors.push('Veuillez compléter ce champ.');
.getElementById('errorlist-title')
.scrollIntoView({ block: 'end', inline: 'nearest' });
}
return false;
},
checkFormGeom() {
if (this.form.geom.value) {
this.form.geom.errors = [];
return true;
} else if (
!this.form.geom.errors.includes('Valeur géométrique non valide.')
this.form.geom.errors.push('Valeur géométrique non valide.');
.getElementById('errorlist-geom')
.scrollIntoView({ block: 'end', inline: 'nearest' });
checkAddedForm() {
let isValid = true; //* fallback if all customForms returned true
if (this.$refs.extraForm) {
for (const extraForm of this.$refs.extraForm) {
if (extraForm.checkForm() === false) {
isValid = false;
}
}
}
if (this.$refs.attachementForm) {
for (const attachementForm of this.$refs.attachementForm) {
if (attachementForm.checkForm() === false) {
isValid = false;
}
}
}
if (this.$refs.linkedForm) {
for (const linkedForm of this.$refs.linkedForm) {
if (linkedForm.checkForm() === false) {
isValid = false;
}
}
}
return isValid;
},
onSave() {
if (this.currentRouteName === 'editer-attribut-signalement') {
this.postMultipleFeatures();
async postForm(extraForms) {
let response;
let is_valid = this.checkFormGeom() && this.checkAddedForm();
if (!this.feature_type.title_optional) {
is_valid = this.checkFormTitle() && is_valid;
}
//* in a moderate project, at edition of a published feature by someone else than admin or moderator, switch published status to draft.
if (
this.project.moderation &&
this.currentRouteName.includes('editer') &&
this.form.status.value.value === 'published' &&
!this.permissions.is_project_administrator &&
!this.permissions.is_project_moderator
this.form.status.value = { name: 'Brouillon', value: 'draft' };
this.sendingFeature = true;
response = await this.$store.dispatch(
'feature/SEND_FEATURE',
{
routeName: this.currentRouteName,
query: this.$route.query,
extraForms// if multiple features, pass directly extraForms object to avoid mutating the store
}
this.sendingFeature = false;
return response;
async postMultipleFeatures() {
this.$store.commit('DISPLAY_LOADER', 'Envoi des signalements en cours...');
const responses = [];
// loop over each selected feature id
for (const featureId of this.checkedFeatures) {
// get other infos from this feature to feel the form
const response = await this.$store.dispatch('feature/GET_PROJECT_FEATURE', {
project_slug: this.$route.params.slug,
});
if (response.status === 200) {
// fill title, status & description in store, required to send feature update request
this.initForm();
// create a new object of custom form to send directly with the request, to avoid multiple asynchronous mutation in store
const newXtraForms = [];
// parse each current custom form values to update the new custom form for this feature
for (const extraForm of this.extra_forms) {
// copy current custom form to prevent modifying the original one
let newXtForm = { ...extraForm };
// if value wasn't changed in this page, get back previous value of the feature (rather than using feature orginal form, which is missing information to send in request)
if (newXtForm.value === null) {
newXtForm.value = this.currentFeature.properties[newXtForm.name];
newXtraForms.push(newXtForm);
const response = await this.postForm(newXtraForms);
responses.push(response);
this.$store.commit('DISCARD_LOADER');
const errors = responses.filter((res) => res === undefined || res.status !== 200).length > 0;
const message = {
comment: errors ? 'Des signalements n\'ont pas pu être mis à jour' : 'Les signalements ont été mis à jour',
level: errors ? 'negative' : 'positive'
};
this.$store.commit('DISPLAY_MESSAGE', message);

Timothee P
committed
this.$router.push({
name: 'liste-signalements',
params: {
slug: this.$route.params.slug,
},
});
//* ************* MAP *************** *//
onFeatureTypeLoaded() {
const geomType = this.feature_type.geom_type;
editionService.addEditionControls(geomType, this.isEditable);
const feature = evt.feature;
this.updateGeomField(new GeoJSON().writeGeometry(feature.getGeometry(),{ dataProjection:'EPSG:4326',featureProjection:'EPSG:3857' }));
if (this.feature_type.geomType === 'point') {
this.showGeoPositionBtn = false;
this.erreurGeolocalisationMessage = '';
}
editionService.modify.on('modifyend', (evt) => {
let feature = evt.features.getArray()[0];
this.updateGeomField(new GeoJSON().writeGeometry(feature.getGeometry(),{ dataProjection:'EPSG:4326',featureProjection:'EPSG:3857' }));
this.map.on(
function () {
this.drawControlEditOnly.remove(this.map);
this.drawControlFull.addTo(this.map);
this.updateGeomField('');
if (geomType === 'point') {
this.showGeoPositionBtn = true;
}
}.bind(this)
);
if (editionService.drawSource) {
editionService.drawSource.clear();
let retour = new GeoJSON().readFeature(geomFeatureJSON,{ dataProjection:'EPSG:4326',featureProjection:'EPSG:3857' });
editionService.startEditFeature(retour);
this.map.setView(
this.$store.state.configuration.DEFAULT_MAP_VIEW.center,
this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom
);
async updateGeomField(newGeom) {
await this.updateStore();
var mapDefaultViewCenter =
this.$store.state.configuration.DEFAULT_MAP_VIEW.center;
var mapDefaultViewZoom =
this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom;
// Create the map, then init the layers and features
mapDefaultViewCenter,
mapDefaultViewZoom,
maxZoom: this.project.map_max_zoom_level,
interactions : { doubleClickZoom :false, mouseWheelZoom:true, dragPan:true },
fullScreenControl: true
const currentFeatureId = this.$route.params.slug_signal;

Timothee P
committed
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}v2/features/?feature_type__slug=${this.$route.params.slug_type_signal}&project__slug=${this.$route.params.slug}&output=geojson`;
axios
.get(url)
.then((response) => {
const features = response.data.features;

Timothee P
committed
if (features.length > 0) {
const allFeaturesExceptCurrent = features.filter(
(feat) => feat.id !== currentFeatureId
);

Timothee P
committed
mapService.addFeatures({
features: allFeaturesExceptCurrent,
featureTypes: this.feature_types,
addToMap: true
});
if (this.currentRouteName === 'editer-signalement') {
editionService.setFeatureToEdit(this.currentFeature);
this.updateMap(this.currentFeature);
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
mapService.updateOrder(event.detail.layers.slice().reverse());