<template> <div v-frag> <script type="application/javascript" :src=" baseUrl + '/resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js' " ></script> <div class="fourteen wide column"> <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 type="text" required :maxlength="form.title.field.max_length" :name="form.title.html_name" :id="form.title.id_for_label" v-model="form.title.value" @blur="updateStore" /> <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 :name="form.description.html_name" rows="5" v-model="form.description.value" @blur="updateStore" ></textarea> </div> <!-- Geom Field --> <div class="field"> <label :for="form.geom.id_for_label">{{ form.geom.label }}</label> <!-- Import GeoImage --> <div v-frag v-if="feature_type && feature_type.geom_type === 'point'"> <p> <button @click="showGeoRef = true" id="add-geo-image" type="button" class="ui compact button" > <i class="file image icon"></i>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"> <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"></i> <span class="label">{{ geoRefFileLabel }}</span> </label> <input type="file" accept="image/jpeg, image/png" style="display: none" ref="file" v-on:change="handleFileUpload" name="image_file" class="image_file" id="image_file" /> <p class="error-message"> {{ erreurUploadMessage }} </p> </div> <button @click="georeferencement" id="get-geom-from-image-file" type="button" class="ui positive right labeled icon button" > Importer <i class="checkmark icon"></i> </button> </div> <p v-if="showGeoPositionBtn"> <button @click="create_point_geoposition" id="create-point-geoposition" type="button" class="ui compact button" > <i class="ui map marker alternate icon"></i>Positionner le signalement à partir de votre géolocalisation </button> </p> <span id="erreur-geolocalisation" 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> </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 type="hidden" :name="form.geom.html_name" :id="form.geom.id_for_label" v-model="form.geom.value" @blur="updateStore" /> <div class="ui tab active map-container" data-tab="map"> <div id="map"></div> <!-- // todo: ajouter v-if --> <!-- {% if serialized_base_maps|length > 0 %} {% include "geocontrib/map-layers/sidebar-layers.html" with basemaps=serialized_base_maps layers=serialized_layers project=project.slug%} {% endif %} --> <SidebarLayers v-if="basemaps && map" /> </div> </div> <!-- Extra Fields --> <div class="ui horizontal divider">DONNÉES MÉTIER</div> <div v-for="(field, index) in extra_form" :key="field.field_type + index" class="field" > <FeatureExtraForm :field="field" /> {{ field.errors }} </div> <!-- Pièces jointes --> <div class="ui horizontal divider">PIÈCES JOINTES</div> <div id="formsets-attachment"> <FeatureAttachmentForm v-for="form in attachmentFormset" :key="form.dataKey" :attachmentForm="form" ref="attachementForm" /> </div> <button @click="add_attachement_formset" id="add-attachment" type="button" class="ui compact basic button button-hover-green" > <i class="ui plus icon"></i>Ajouter une pièce jointe </button> <!-- Signalements liés --> <div class="ui horizontal divider">SIGNALEMENTS LIÉS</div> <div id="formsets-link"> <FeatureLinkedForm v-for="form in linkedFormset" :key="form.dataKey" :linkedForm="form" :features="features" ref="linkedForm" /> </div> <button @click="add_linked_formset" id="add-link" type="button" class="ui compact basic button button-hover-green" > <i class="ui plus icon"></i>Ajouter une liaison </button> <div class="ui divider"></div> <button @click="postForm" type="button" class="ui teal icon button"> <i class="white save icon"></i> Enregistrer les changements </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"; const axios = require("axios"); import flip from "@turf/flip"; 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"); export default { name: "Feature_edit", directives: { frag, }, components: { FeatureAttachmentForm, FeatureLinkedForm, Dropdown, SidebarLayers, FeatureExtraForm, }, 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, statusChoices: [ { name: "Brouillon", value: "draft", }, { name: "Publié", value: "published", }, { name: "Archivé", value: "archived", }, { name: "En attente de publication", value: "pending", }, ], form: { title: { errors: [], id_for_label: "name", field: { max_length: 30, }, 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: { ...mapGetters(["project"]), ...mapGetters("feature_type", ["feature_type"]), ...mapState(["user", "USER_LEVEL_PROJECTS"]), ...mapState("map", ["basemaps"]), ...mapState("feature", [ "attachmentFormset", "attachmentsToDelete", "attachmentsToPut", "linkedFormset", "features", "extra_form", "linked_features", ]), field_title() { if (this.feature_type) { if (this.feature_type.title_optional) { return "field"; } } return "required field"; }, currentRouteName() { return this.$route.name; }, feature: function () { return this.$store.state.feature.features.find( (el) => el.feature_id === this.$route.params.slug_signal ); }, 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) { const isModerate = this.project.moderation; const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug]; const isOwnFeature = this.feature ? this.feature.creator === this.user.id //* prevent undefined feature : false; //* si le contributeur est l'auteur du signalement if ( //* si admin ou modérateur, statuts toujours disponible : Brouillon, Publié, Archivé userStatus === "Modérateur" || userStatus === "Administrateur projet" ) { return this.statusChoices.filter((el) => el.value !== "pending"); } else if (userStatus === "Contributeur") { //* cas particuliers du contributeur if ( this.currentRouteName === "ajouter-signalement" || !isOwnFeature ) { //* même cas à l'ajout d'une feature ou si feature n'a pas été créé par le contributeur return isModerate ? this.statusChoices.filter( (el) => el.value === "draft" || el.value === "pending" ) : this.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 ? this.statusChoices.filter( (el) => el.value !== "published" //* toutes sauf "Publié" ) : this.statusChoices.filter( (el) => el.value !== "pending" //* toutes sauf "En cours de publication" ); } } } return []; }, }, methods: { initForm() { if (this.currentRouteName === "editer-signalement") { for (let key in this.feature) { if (key && this.form[key]) { if (key === "status") { const value = this.feature[key]; this.form[key].value = this.statusChoices.find( (key) => key.value === value ); } else { this.form[key].value = this.feature[key]; } } } this.updateStore(); } }, 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; } 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) ); } }, handleFileUpload() { this.file = this.$refs.file.files[0]; }, georeferencement() { console.log("georeferencement"); const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}exif-geom-reader/`; let formData = new FormData(); formData.append("image_file", this.file); console.log(">> formData >> ", formData); let self = this; axios .post(url, formData, { headers: { "Content-Type": "multipart/form-data", }, }) .then(function (response) { console.log("SUCCESS!!", response.data); if (response.data.geom.indexOf("POINT") >= 0) { let regexp = /POINT\s\((.*)\s(.*)\)/; var arr = regexp.exec(response.data.geom); let json = { type: "Feature", geometry: { type: "Point", coordinates: [parseFloat(arr[1]), parseFloat(arr[2])], }, properties: {}, }; self.updateMap(json); self.updateGeomField(json); // Set Attachment self.addAttachment({ title: "Localisation", info: "", id: "loc", attachment_file: self.file.name, fileToImport: self.file, }); } }) .catch(function (error) { if (error && error.response && error.response) { self.erreurUploadMessage = error.response.data.error; } else { self.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 ? findCurrentValue(field.label) : null, }; }); this.$store.commit("feature/SET_EXTRA_FORM", extraForm); }, 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) { console.log(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, relation_type: linked.relation_type, feature_to: linked.feature_to, }); 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) { this.$store.dispatch("feature/SEND_FEATURE", this.currentRouteName); } }, //* ************* MAP *************** *// onFeatureTypeLoaded() { var geomLeaflet = { point: "circlemarker", linestring: "polyline", polygon: "polygon", }; var geomType = this.feature_type.geom_type; 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", }, finish: { title: "Terminer le dessin", text: "Terminer", }, undo: { title: "Supprimer le dernier point dessiné", text: "Supprimer le dernier point", }, buttons: { polyline: "Dessiner une polyligne", polygon: "Dessiner un polygone", rectangle: "Dessiner un rectangle", circle: "Dessiner un cercle", marker: "Dessiner une balise", circlemarker: "Dessiner un point", }, }, handlers: { circle: { tooltip: { start: "Cliquer et glisser pour dessiner le cercle.", }, radius: "Rayon", }, circlemarker: { tooltip: { start: "Cliquer sur la carte pour placer le point.", }, }, marker: { tooltip: { start: "Cliquer sur la carte pour placer la balise.", }, }, polygon: { tooltip: { start: "Cliquer pour commencer à dessiner.", cont: "Cliquer pour continuer à dessiner.", end: "Cliquer sur le premier point pour terminer le dessin.", }, }, polyline: { error: "<strong>Error:</strong> shape edges cannot cross!", tooltip: { start: "Cliquer pour commencer à dessiner.", cont: "Cliquer pour continuer à dessiner.", end: "Cliquer sur le dernier point pour terminer le dessin.", }, }, rectangle: { tooltip: { start: "Cliquer et glisser pour dessiner le rectangle.", }, }, simpleshape: { tooltip: { end: "Relâcher la souris pour terminer de dessiner.", }, }, }, }, edit: { toolbar: { actions: { save: { title: "Sauver les modifications", text: "Sauver", }, cancel: { title: "Annuler la modification, annule toutes les modifications", text: "Annuler", }, clearAll: { title: "Effacer l'objet", text: "Effacer", }, }, buttons: { edit: "Modifier l'objet", editDisabled: "Aucun objet à modifier", remove: "Supprimer l'objet", removeDisabled: "Aucun objet à supprimer", }, }, handlers: { edit: { tooltip: { text: "Faites glisser les marqueurs ou les balises pour modifier l'élément.", subtext: "Cliquez sur Annuler pour annuler les modifications..", }, }, remove: { tooltip: { 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({ position: "topright", edit: { featureGroup: this.drawnItems, }, draw: drawConfig, }); this.drawControlEditOnly = new L.Control.Draw({ position: "topright", edit: { featureGroup: this.drawnItems, }, draw: false, }); if (this.currentRouteName === "editer-signalement") { this.map.addControl(this.drawControlEditOnly); } else this.map.addControl(this.drawControlFull); this.map.on( "draw:created", function (e) { var layer = e.layer; this.add_layer_call_back(layer); }.bind(this) ); //var wellknown;// TODO Remplacer par autre chose this.map.on( "draw:edited", function (e) { var layers = e.layers; let self = this; layers.eachLayer(function (layer) { //this.updateGeomField(wellknown.stringify(layer.toGeoJSON())) self.updateGeomField(layer.toGeoJSON()); }); }.bind(this) ); 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 (this.drawnItems) this.drawnItems.clearLayers(); console.log("update map"); var geomType = this.feature_type.geom_type; if (geomFeatureJSON) { var geomJSON = flip(geomFeatureJSON.geometry); //turf.flip(geomFeatureJSON) if (geomType === "point") { L.circleMarker(geomJSON.coordinates).addTo(this.drawnItems); } else if (geomType === "linestring") { L.polyline(geomJSON.coordinates).addTo(this.drawnItems); } else if (geomType === "polygon") { L.polygon(geomJSON.coordinates).addTo(this.drawnItems); } this.map.fitBounds(this.drawnItems.getBounds(), { padding: [25, 25] }); } else { this.map.setView( this.$store.state.configuration.DEFAULT_MAP_VIEW.center, this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom ); } }, updateGeomField(newGeom) { //this.geometry = newGeom; this.form.geom.value = newGeom.geometry; this.updateStore(); }, initMap() { 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({ mapDefaultViewCenter, mapDefaultViewZoom, }); 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) => { console.log(response.data.features); const features = response.data.features; if (features) { const allFeaturesExceptCurrent = features.filter( (feat) => feat.id !== currentFeatureId ); mapUtil.addFeatures(allFeaturesExceptCurrent); if (this.currentRouteName === "editer-signalement") { const currentFeature = features.filter( (feat) => feat.id === currentFeatureId )[0]; this.updateMap(currentFeature); } } }) .catch((error) => { 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 mapUtil.updateOrder(event.detail.layers.slice().reverse()); }); }, add_layer_call_back(layer) { layer.addTo(this.drawnItems); this.drawControlFull.remove(this.map); this.drawControlEditOnly.addTo(this.map); //var wellknown;// TODO Remplacer par autre chose //this.updateGeomField(wellknown.stringify(layer.toGeoJSON())) this.updateGeomField(layer.toGeoJSON()); if (this.feature_type.geomType === "point") { this.showGeoPositionBtn = false; this.erreurGeolocalisationMessage = ""; } }, 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)); }, }, created() { this.$store.commit( "feature_type/SET_CURRENT_FEATURE_TYPE_SLUG", this.$route.params.slug_type_signal ); if (this.$route.params.slug_signal) { this.getFeatureAttachments(); this.getLinkedFeatures(); } }, mounted() { this.$store .dispatch("GET_PROJECT_INFO", this.$route.params.slug) .then((data) => { console.log(data); this.initForm(); this.initMap(); this.onFeatureTypeLoaded(); this.initExtraForms(); 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"); }, }; </script> <style> #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, je ne comprends pas comment... */ .ui.right.floated.button { float: right; margin-right: 0; margin-left: 0.25em; } /* // ! margin écrasé par class last-child first-child, pas normal ... */ .ui.segment { margin: 1rem 0 !important; } .error-message { color: red; } </style>