Newer
Older

Timothee P
committed
<template>
<div v-frag>
<div class="fourteen wide column">
<h1 v-if="feature && currentRouteName === 'editer-signalement'">
Mise à jour du signalement "{{ feature.title || feature.feature_id }}"
<h1
v-else-if="feature_type && currentRouteName === 'ajouter-signalement'"
Création d'un signalement <small>[{{ feature_type.title }}]</small>

Timothee P
committed
</h1>
<form
id="form-feature-edit"
action=""
method="post"
enctype="multipart/form-data"
class="ui form"
>
<!-- Feature Fields -->
<div class="two fields">
<label :for="form.title.id_for_label">{{ form.title.label }}</label>

Timothee P
committed
<input
:id="form.title.id_for_label"
v-model="form.title.value"

Timothee P
committed
type="text"
required
:maxlength="form.title.field.max_length"
:name="form.title.html_name"

Timothee P
committed
@blur="updateStore"
>
<ul
id="errorlist-title"
class="errorlist"
>
<li
v-for="error in form.title.errors"
:key="error"
>
{{ error }}
</li>
</ul>

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

Timothee P
committed
}}</label>

Timothee P
committed
:selection.sync="selected_status"

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

Timothee P
committed
}}</label>
<textarea

Timothee P
committed
rows="5"

Timothee P
committed
@blur="updateStore"

Timothee P
committed
</div>
<!-- Geom Field -->
<div class="field">
<label :for="form.geom.id_for_label">{{ form.geom.label }}</label>

Timothee P
committed
<!-- Import GeoImage -->
<div
v-if="feature_type && feature_type.geom_type === 'point'"
v-frag
>
<p v-if="isOffline() !== true">
<button
id="add-geo-image"
type="button"
class="ui compact button"
<i class="file image icon" />Importer une image géoréférencée
</button>
Vous pouvez utiliser une image géoréférencée pour localiser le
signalement.
</p>

Timothee P
committed
<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"
@click="toggleGeoRefModal"
/>

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

Timothee P
committed
<span class="label">{{ geoRefFileLabel }}</span>
</label>
<input

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

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

Timothee P
committed
>

Timothee P
committed
Importer
</button>
</form>
</div>
</div>

Timothee P
committed
<button
id="create-point-geoposition"
type="button"
class="ui compact button"
<i class="ui map marker alternate icon" />Positionner le
signalement à partir de votre géolocalisation
</button>
</p>
<span
v-if="erreurGeolocalisationMessage"
<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>

Timothee P
committed
</div>
<ul
id="errorlist-geom"
class="errorlist"
>
<li
v-for="error in form.geom.errors"
:key="error"
>
{{ error }}
</li>
</ul>

Timothee P
committed
<!-- Map -->
<input
:id="form.geom.id_for_label"
v-model="form.geom.value"
type="hidden"
:name="form.geom.html_name"

Timothee P
committed
@blur="updateStore"
>
<div
class="ui tab active map-container"
data-tab="map"
>
<div
id="map"
ref="map"
/>
<SidebarLayers v-if="basemaps && map" />

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

Timothee P
committed
{{ field.errors }}
</div>
<!-- Pièces jointes -->
<div v-if="isOffline() !== true">
<div class="ui horizontal divider">
PIÈCES JOINTES
</div>
<div
v-if="isOffline() !== true"
id="formsets-attachment"
>
v-for="attachForm in attachmentFormset"
:key="attachForm.dataKey"

Timothee P
committed
<button
id="add-attachment"
type="button"

Timothee P
committed
class="ui compact basic button"
<i class="ui plus icon" />Ajouter une pièce jointe

Timothee P
committed
<!-- Signalements liés -->
<div v-if="isOffline() !== true">
<div class="ui horizontal divider">
SIGNALEMENTS LIÉS
</div>
<div id="formsets-link">
<FeatureLinkedForm
/>
</div>
<button
id="add-link"
type="button"

Timothee P
committed
class="ui compact basic button"
<i class="ui plus icon" />Ajouter une liaison

Timothee P
committed
<button
type="button"
class="ui teal icon button"
@click="postForm"
>
<i class="white save icon" /> Enregistrer les changements

Timothee P
committed
</button>
</form>
</div>
</div>
</template>
<script>
import frag from 'vue-frag';
import { mapState, mapGetters } from 'vuex';
import FeatureAttachmentForm from '@/components/feature/FeatureAttachmentForm';
import FeatureLinkedForm from '@/components/feature/FeatureLinkedForm';
import FeatureExtraForm from '@/components/feature/FeatureExtraForm';
import Dropdown from '@/components/Dropdown.vue';
import SidebarLayers from '@/components/map-layers/SidebarLayers';
import featureAPI from '@/services/feature-api';
import L from 'leaflet';
import 'leaflet-draw';
import { mapUtil } from '@/assets/js/map-util.js';
import { allowedStatus2change } from '@/utils';
import axios from '@/axios-client.js';
import flip from '@turf/flip';

Timothee P
committed
export default {

Timothee P
committed
directives: {
frag,
},
components: {
FeatureAttachmentForm,
FeatureLinkedForm,

Timothee P
committed
SidebarLayers,

Timothee P
committed
data() {
return {
map: null,
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: {
max_length: 30,
},
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
},
},
};
},
...mapGetters(['permissions']),
...mapGetters('feature_type', ['feature_type']),
...mapState(['user', 'USER_LEVEL_PROJECTS']),
...mapState('projects', ['project']),
...mapState('map', ['basemaps']),
...mapState('feature', [
'attachmentFormset',
'linkedFormset',
'features',
'extra_form',
'statusChoices',
field_title() {
if (this.feature_type) {
if (this.feature_type.title_optional) {
currentRouteName() {
return this.$route.name;
},
Sébastien DA ROCHA
committed
return this.$store.state.feature.current_feature;
return [...this.extra_form].sort((a, b) => a.position - b.position);
selected_status: {
get() {
return this.form.status.value;
},
set(newValue) {
this.form.status.value = newValue;
this.updateStore();
},
},
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.statusChoices, isModerate, userStatus, isOwnFeature, this.currentRouteName);
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() {
let promises = [
this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug),
this.$store.dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug),
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
];
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.initExtraForms(this.feature);
setTimeout(
function () {
mapUtil.addGeocoders(this.$store.state.configuration);
}.bind(this),
1000
);
});
},
destroyed() {
//* 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: {
isOffline() {
return navigator.onLine == false;
if (this.currentRouteName === 'editer-signalement') {
for (let key in this.feature) {
if (key && this.form[key]) {
const value = this.feature[key];
this.form[key].value = this.statusChoices.find(
(key) => key.value === value
this.form[key].value = this.feature[key];
create_point_geoposition() {
function success(position) {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
var layer = L.circleMarker([latitude, longitude]);
this.add_layer_call_back(layer);
this.map.setView([latitude, longitude]);
}
function error(err) {
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/`;
let formData = new FormData();
axios
.post(url, formData, {
headers: {
},
})

Timothee P
committed
.then((response) => {
if (response.data.geom.indexOf('POINT') >= 0) {
let regexp = /POINT\s\((.*)\s(.*)\)/;

Timothee P
committed
let arr = regexp.exec(response.data.geom);
coordinates: [parseFloat(arr[1]), parseFloat(arr[2])],

Timothee P
committed
this.updateMap(json);
this.updateGeomField(json);

Timothee P
committed
this.addAttachment({
title: 'Localisation',
info: '',
id: 'loc',

Timothee P
committed
attachment_file: this.file.name,
fileToImport: this.file,
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";
}
initExtraForms(feature) {
function findCurrentValue(label) {
const field = feature.feature_data.find((el) => el.label === label);
return field ? field.value : null;
let extraForm = this.feature_type.customfield_set.map((field) => {
return {
...field,
//* add value field to extra forms from feature_type and existing values if feature is defined
value:
feature && feature.feature_data
? findCurrentValue(field.label)
: null,
this.$store.commit('feature/SET_EXTRA_FORM', extraForm);
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,
relation_type: linked.relation_type,
feature_to: linked.feature_to,
});
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.feature ? this.feature.feature_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.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;
},
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();
//* 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.$store.dispatch('feature/SEND_FEATURE', this.currentRouteName);
//* ************* MAP *************** *//
onFeatureTypeLoaded() {
point: 'circlemarker',
linestring: 'polyline',
polygon: 'polygon',
var drawConfig = {
polygon: false,
marker: false,
polyline: false,
rectangle: false,
circle: false,
circlemarker: false,
};
drawConfig[geomLeaflet[geomType]] = true;
L.drawLocal = {
draw: {
toolbar: {
actions: {
title: 'Annuler le dessin',
text: 'Annuler',
title: 'Terminer le dessin',
text: 'Terminer',
title: 'Supprimer le dernier point dessiné',
text: 'Supprimer le dernier point',
polyline: 'Dessiner une polyligne',
polygon: 'Dessiner un polygone',
rectangle: 'Dessiner un rectangle',
circle: 'Dessiner un cercle',
marker: 'Dessiner une balise',
circlemarker: 'Dessiner un point',
start: 'Cliquer et glisser pour dessiner le cercle.',
start: 'Cliquer sur la carte pour placer le point.',
start: 'Cliquer sur la carte pour placer la balise.',
start: 'Cliquer pour commencer à dessiner.',
cont: 'Cliquer pour continuer à dessiner.',
end: 'Cliquer sur le premier point pour terminer le dessin.',
error: '<strong>Error:</strong> shape edges cannot cross!',
start: 'Cliquer pour commencer à dessiner.',
cont: 'Cliquer pour continuer à dessiner.',
end: 'Cliquer sur le dernier point pour terminer le dessin.',
start: 'Cliquer et glisser pour dessiner le rectangle.',
end: 'Relâcher la souris pour terminer de dessiner.',
},
edit: {
toolbar: {
actions: {
save: {
title: 'Sauver les modifications',
text: 'Sauver',
'Annuler la modification, annule toutes les modifications',
text: 'Annuler',
title: "Effacer l'objet",
edit: "Modifier l'objet",
remove: "Supprimer l'objet",
removeDisabled: 'Aucun objet à supprimer',
text: "Faites glisser les marqueurs ou les balises pour modifier l'élément.",
subtext: 'Cliquez sur Annuler pour annuler les modifications..',
text: 'Cliquez sur un élément pour le supprimer.',
},
},
},
},
this.drawnItems = new L.FeatureGroup();
this.map.addLayer(this.drawnItems);
this.drawControlFull = new L.Control.Draw({
featureGroup: this.drawnItems,
this.drawControlEditOnly = new L.Control.Draw({
featureGroup: this.drawnItems,
if (this.currentRouteName === 'editer-signalement') {
} else {
this.map.addControl(this.drawControlFull);
}
this.changeMobileBtnOrder();
this.map.on(
function (e) {
var layer = e.layer;
this.add_layer_call_back(layer);
}.bind(this)
);
//var wellknown;// TODO Remplacer par autre chose
function (e) {
var layers = e.layers;
let self = this;
layers.eachLayer(function (layer) {
self.updateGeomField(layer.toGeoJSON());
});
}.bind(this)
);
this.map.on(
function () {
this.drawControlEditOnly.remove(this.map);
this.drawControlFull.addTo(this.map);
this.updateGeomField('');
if (geomType === 'point') {
this.showGeoPositionBtn = true;
}
}.bind(this)
);
updateMap(geomFeatureJSON) {
if (this.drawnItems) this.drawnItems.clearLayers();
var geomType = this.feature_type.geom_type;
if (geomFeatureJSON) {
L.circleMarker(geomJSON.coordinates).addTo(this.drawnItems);
L.polyline(geomJSON.coordinates).addTo(this.drawnItems);
L.polygon(geomJSON.coordinates).addTo(this.drawnItems);
this.map.fitBounds(this.drawnItems.getBounds(), { padding: [25, 25] });
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.geometry;
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
this.map = mapUtil.createMap(this.$refs.map, {
mapDefaultViewCenter,
mapDefaultViewZoom,
});
const currentFeatureId = this.$route.params.slug_signal;
let project_id = this.$route.params.slug.split('-')[0];
const mvtUrl = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features.mvt/?tile={z}/{x}/{y}&project_id=${project_id}`;
mapUtil.addVectorTileLayer(
mvtUrl,
this.$route.params.slug,
this.$store.state.feature_type.feature_types
);
}, 1000);

Timothee P
committed
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
);
mapUtil.addFeatures(
allFeaturesExceptCurrent,
{},
this.$store.state.feature_type.feature_types
);
if (this.currentRouteName === 'editer-signalement') {
const currentFeature = features.filter(
(feat) => feat.id === currentFeatureId
)[0];
this.updateMap(currentFeature);