Something went wrong on our end
-
Timothee P authoredTimothee P authored
FeatureEdit.vue 27.20 KiB
<template>
<div id="feature-edit">
<h1 v-if="feature && currentRouteName === 'editer-signalement'">
Mise à jour du signalement "{{ feature.title || feature.feature_id }}"
</h1>
<h1
v-else-if="feature_type && currentRouteName === 'ajouter-signalement'"
>
Création d'un signalement <small>[{{ feature_type.title }}]</small>
</h1>
<form
id="form-feature-edit"
action=""
method="post"
enctype="multipart/form-data"
class="ui form"
>
<!-- Feature Fields -->
<div 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"
>
{{ error }}
</li>
</ul>
</div>
<div class="required field">
<label :for="form.status.id_for_label">{{
form.status.label
}}</label>
<Dropdown
:options="allowedStatusChoices"
:selected="selected_status.name"
:selection.sync="selected_status"
/>
</div>
</div>
<div 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 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>
<div
v-if="showGeoRef"
class="ui dimmer modals page transition visible active"
style="display: flex !important"
>
<div
class="ui mini modal transition visible active"
style="display: block !important"
>
<i
class="close icon"
aria-hidden="true"
@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"
>
<i
class="file icon"
aria-hidden="true"
/>
<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"
>
<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"
>
<i
class="plus icon"
aria-hidden="true"
/>
Importer
</button>
</form>
</div>
</div>
</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>
<div
id="map"
ref="map"
/>
<div
id="popup"
class="ol-popup"
>
<a
id="popup-closer"
href="#"
class="ol-popup-closer"
/>
<div
id="popup-content"
/>
</div>
<SidebarLayers v-if="basemaps && map" />
<EditingToolbar v-if="basemaps && map" />
</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"
>
<FeatureExtraForm
:id="field.label"
:field="field"
class="field"
/>
{{ field.errors }}
</div>
<!-- Pièces jointes -->
<div v-if="isOnline">
<div class="ui horizontal divider">
PIÈCES JOINTES
</div>
<div
v-if="isOnline"
id="formsets-attachment"
>
<FeatureAttachmentForm
v-for="attachForm in attachmentFormset"
:key="attachForm.dataKey"
ref="attachementForm"
:attachment-form="attachForm"
/>
</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
</button>
</div>
<!-- Signalements liés -->
<div v-if="isOnline">
<div class="ui horizontal divider">
SIGNALEMENTS LIÉS
</div>
<div id="formsets-link">
<FeatureLinkedForm
v-for="linkForm in linkedFormset"
:key="linkForm.dataKey"
ref="linkedForm"
:linked-form="linkForm"
/>
</div>
<button
id="add-link"
type="button"
class="ui compact basic button"
@click="add_linked_formset"
>
<i
class="ui plus icon"
aria-hidden="true"
/>
Ajouter une liaison
</button>
</div>
<div class="ui divider" />
<button
type="button"
:class="['ui teal icon button', { loading: sendingFeature }]"
@click="postForm"
>
<i
class="white save icon"
aria-hidden="true"
/>
Enregistrer les changements
</button>
</form>
</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';
export default {
name: 'FeatureEdit',
components: {
FeatureAttachmentForm,
FeatureLinkedForm,
Dropdown,
SidebarLayers,
EditingToolbar,
FeatureExtraForm,
},
data() {
return {
map: null,
mapLoading: false,
sendingFeature: false,
baseUrl: this.$store.state.configuration.BASE_URL,
file: null,
showGeoRef: false,
showGeoPositionBtn: true,
erreurGeolocalisationMessage: null,
erreurUploadMessage: null,
attachmentDataKey: 0,
linkedDataKey: 0,
form: {
title: {
errors: [],
infos: [],
id_for_label: 'name',
field: {
max_length: 128,
},
html_name: 'name',
label: 'Nom',
value: '',
},
status: {
id_for_label: 'status',
html_name: 'status',
label: 'Statut',
value: {
value: 'draft',
name: 'Brouillon',
},
},
description: {
errors: [],
id_for_label: 'description',
html_name: 'description',
label: 'Description',
value: '',
},
geom: {
errors: [],
label: 'Localisation',
value: null,
},
},
};
},
computed: {
...mapState([
'user',
'USER_LEVEL_PROJECTS',
'isOnline'
]),
...mapState('projects', [
'project'
]),
...mapState('map', [
'basemaps'
]),
...mapState('feature', [
'attachmentFormset',
'linkedFormset',
'features',
'extra_forms',
]),
...mapState('feature-type', [
'feature_types'
]),
...mapGetters([
'permissions'
]),
...mapGetters('feature-type', [
'feature_type'
]),
field_title() {
if (this.feature_type) {
if (this.feature_type.title_optional) {
return 'field';
}
}
return 'required field';
},
currentRouteName() {
return this.$route.name;
},
feature() {
return this.$store.state.feature.currentFeature;
},
orderedCustomFields() {
return [...this.extra_forms].sort((a, b) => a.position - b.position);
},
geoRefFileLabel() {
if (this.file) {
return this.file.name;
}
return 'Sélectionner une image ...';
},
selected_status: {
get() {
return this.form.status.value;
},
set(newValue) {
this.form.status.value = newValue;
this.updateStore();
},
},
allowedStatusChoices() {
if (this.project && this.feature && this.user) {
const isModerate = this.project.moderation;
const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug];
const isOwnFeature = this.feature.creator === this.user.id; //* si le contributeur est l'auteur du signalement
return allowedStatus2change(this.user, isModerate, userStatus, isOwnFeature, this.currentRouteName);
}
return [];
},
},
watch: {
'form.title.value': function(newValue) {
if (newValue.length === 128) {
this.form.title.infos.push('Le nombre de caractères et limité à 128.');
} else {
this.form.title.infos = [];
}
}
},
created() {
this.$store.commit(
'feature-type/SET_CURRENT_FEATURE_TYPE_SLUG',
this.$route.params.slug_type_signal
);
//* empty previous feature data, not emptying by itself since it doesn't update by itself anymore
if (this.currentRouteName === 'ajouter-signalement') {
this.$store.commit('feature/SET_CURRENT_FEATURE', []);
}
if (this.$route.params.slug_signal) {
this.getFeatureAttachments();
this.getLinkedFeatures();
}
},
mounted() {
const promises = [
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(() => {
this.initForm();
this.initMap();
this.onFeatureTypeLoaded();
this.$store.dispatch('feature/INIT_EXTRA_FORMS');
});
},
destroyed() {
editionService.removeActiveFeatures();
//* 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');
},
methods: {
initForm() {
if (this.currentRouteName === 'editer-signalement') {
for (const key in this.feature) {
if (key && this.form[key]) {
if (key === 'status') {
const value = this.feature[key];
this.form[key].value = statusChoices.find(
(key) => key.value === value
);
} else {
this.form[key].value = this.feature[key];
}
}
}
this.updateStore();
}
},
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]);
}
function error(err) {
this.erreurGeolocalisationMessage = err.message;
if (err.message === 'User denied geolocation prompt') {
this.erreurGeolocalisationMessage =
"La géolocalisation a été désactivée par l'utilisateur";
}
}
this.erreurGeolocalisationMessage = null;
if (!navigator.geolocation) {
this.erreurGeolocalisationMessage =
"La géolocalisation n'est pas supportée par votre navigateur.";
} else {
navigator.geolocation.getCurrentPosition(
success.bind(this),
error.bind(this)
);
}
},
toggleGeoRefModal() {
if (this.showGeoRef) {
//* when popup closes, empty form
this.erreurUploadMessage = '';
this.file = null;
}
this.showGeoRef = !this.showGeoRef;
},
handleFileUpload() {
this.erreurUploadMessage = '';
this.file = this.$refs.file.files[0];
},
georeferencement() {
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}exif-geom-reader/`;
const formData = new FormData();
formData.append('image_file', this.file);
axios
.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.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])]);
// Set Attachment
this.addAttachment({
title: 'Localisation',
info: '',
attachment_file: this.file.name,
fileToImport: this.file,
});
this.toggleGeoRefModal();
}
})
.catch((error) => {
console.error({ error });
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";
}
});
},
add_attachement_formset() {
this.$store.commit('feature/ADD_ATTACHMENT_FORM', {
dataKey: this.attachmentDataKey,
}); // * create an object with the counter in store
this.attachmentDataKey += 1; // * increment counter for key in v-for
},
addAttachment(attachment) {
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);
}
},
add_linked_formset() {
this.$store.commit('feature/ADD_LINKED_FORM', {
dataKey: this.linkedDataKey,
}); // * create an object with the counter in store
this.linkedDataKey += 1; // * increment counter for key in v-for
},
addExistingLinkedFormset(linkedFormset) {
for (const linked of linkedFormset) {
this.$store.commit('feature/ADD_LINKED_FORM', {
dataKey: this.linkedDataKey,
...linked
});
this.linkedDataKey += 1;
}
},
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.feature ? this.feature.feature_id : '',
});
},
checkFormTitle() {
if (this.form.title.value) {
this.form.title.errors = [];
return true;
} else if (
!this.form.title.errors.includes('Veuillez compléter ce champ.')
) {
this.form.title.errors.push('Veuillez compléter ce champ.');
document
.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.');
document
.getElementById('errorlist-geom')
.scrollIntoView({ block: 'end', inline: 'nearest' });
}
return false;
},
checkAddedForm() {
let isValid = true; //* fallback if all customForms returned true
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;
},
postForm() {
let is_valid = true;
if (!this.feature_type.title_optional) {
is_valid =
this.checkFormTitle() &&
this.checkFormGeom() &&
this.checkAddedForm();
} else {
is_valid = this.checkFormGeom() && this.checkAddedForm();
}
if (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 === 'editer-signalement' &&
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.updateStore();
}
this.sendingFeature = true;
this.$store.dispatch('feature/SEND_FEATURE', this.currentRouteName)
.then(() => this.sendingFeature = false);
}
},
//* ************* MAP *************** *//
onFeatureTypeLoaded() {
const geomType = this.feature_type.geom_type;
editionService.addEditionControls(geomType);
editionService.draw.on('drawend', (evt) => {
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(
'draw:deleted',
function () {
this.drawControlEditOnly.remove(this.map);
this.drawControlFull.addTo(this.map);
this.updateGeomField('');
if (geomType === 'point') {
this.showGeoPositionBtn = true;
this.erreurGeolocalisationMessage = '';
}
}.bind(this)
);
},
updateMap(geomFeatureJSON) {
if (editionService.drawSource) {
editionService.drawSource.clear();
}
if (geomFeatureJSON) {
let retour = new GeoJSON().readFeature(geomFeatureJSON,{ dataProjection:'EPSG:4326',featureProjection:'EPSG:3857' });
editionService.startEditFeature(retour);
} else {
this.map.setView(
this.$store.state.configuration.DEFAULT_MAP_VIEW.center,
this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom
);
}
},
updateGeomField(newGeom) {
this.form.geom.value = newGeom;
this.updateStore();
},
initMap() {
this.mapLoading = true;
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
this.map = mapService.createMap(this.$refs.map, {
mapDefaultViewCenter,
mapDefaultViewZoom,
maxZoom: this.project.map_max_zoom_level,
interactions : { doubleClickZoom :false, mouseWheelZoom:true, dragPan:true }
});
const currentFeatureId = this.$route.params.slug_signal;
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?feature_type__slug=${this.$route.params.slug_type_signal}&output=geojson`;
axios
.get(url)
.then((response) => {
const features = response.data.features;
if (features) {
const allFeaturesExceptCurrent = features.filter(
(feat) => feat.id !== currentFeatureId
);
mapService.addFeatures(
allFeaturesExceptCurrent,
{},
this.feature_types,
true
);
if (this.currentRouteName === 'editer-signalement') {
const currentFeature = features.filter(
(feat) => feat.id === currentFeatureId
)[0];
editionService.setFeatureToEdit(currentFeature);
this.updateMap(currentFeature);
}
}
this.mapLoading = false;
})
.catch((error) => {
this.mapLoading = false;
throw error;
});
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());
});
},
getFeatureAttachments() {
featureAPI
.getFeatureAttachments(this.$route.params.slug_signal)
.then((data) => this.addExistingAttachementFormset(data));
},
getLinkedFeatures() {
featureAPI
.getFeatureLinks(this.$route.params.slug_signal)
.then((data) => this.addExistingLinkedFormset(data));
},
},
};
</script>
<style scoped>
#map {
height: 70vh;
width: 100%;
border: 1px solid grey;
}
#get-geom-from-image-file {
margin-bottom: 5px;
}
.georef-btn {
max-width: 400px;
}
@media only screen and (max-width: 767px) {
#map {
height: 80vh;
}
}
/* // ! missing style in semantic.min.css */
.ui.right.floated.button {
float: right;
margin-right: 0;
margin-left: 0.25em;
}
/* // ! margin écrasé par class last-child first-child */
.ui.segment {
margin: 1rem 0 !important;
}
/* override to display buttons under the dimmer of modal */
.leaflet-top,
.leaflet-bottom {
z-index: 800;
}
</style>