<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="statusChoicesFilter" :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"> <label>Image (png ou jpeg)</label> <label class="ui icon button" for="image_file"> <i class="file icon"></i> <span class="label">Sélectionner une image ...</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" style="color: red"> {{ 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 } 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, feature_type:null, baseUrl:this.$store.state.configuration.BASE_URL, file: null, showGeoRef: false, showGeoPositionBtn: true, erreurGeolocalisationMessage: null, erreurUploadMessage: null, attachmentDataKey: 0, linkedDataKey: 0, statusChoicesFilter:[], 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: { // ...mapState(["project"]), ...mapState("map", ["basemaps"]), ...mapState("feature", [ "attachmentFormset", "linkedFormset", "features", "extra_form", ]), 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 ); }, selected_status: { get() { return this.form.status.value; }, set(newValue) { this.form.status.value = newValue; this.updateStore(); }, }, }, watch: { feature(newValue) { if (this.$route.name === "editer-signalement") { this.initForm(); this.initExtraForms(newValue); } }, }, methods: { makeStatusChoicesFilter(){ let newStatusChoices = this.statusChoices if (this.project){ if (!this.project.moderation){ newStatusChoices = [] this.statusChoices.forEach(function(status) { if (status.value !== 'pending') { newStatusChoices.push(status) } }); } } this.statusChoicesFilter = newStatusChoices }, 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]; console.log(">>>> 1st element in files array >>>> ", this.file); }, 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": [arr[1], arr[2]] }, "properties": { } }; self.updateMap(self.map, json) self.updateGeomField(json) // Set Attachment //self.addAttachment(self.file) } }) .catch(function (response) { console.log("FAILURE!!"); self.erreurUploadMessage = response.data.message; }); }, 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 }, addExistingAttachementFormset(attachementFormset) { for (const attachment of attachementFormset) { console.log("attachment", attachment); this.$store.commit("feature/ADD_ATTACHMENT_FORM", { dataKey: this.attachmentDataKey, title: attachment.title, attachment_file: attachment.attachment_file, info: attachment.info, id: attachment.id, }); this.attachmentDataKey += 1; } }, add_linked_formset() { this.$store.commit("feature/ADD_LINKED_FORM", this.linkedDataKey); // * create an object with the counter in store this.linkedDataKey += 1; // * increment counter for key in v-for }, 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(map, geomFeatureJSON) { 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()); } 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/?output=geojson`; axios .get(url) .then((response) => { //console.log(response); 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(this.map, 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 = ""; } }, }, created() { if (!this.project) { this.project = this.$store.state.projects.find((project) => project.slug === this.$store.state.project_slug); this.makeStatusChoicesFilter(); } this.$store.commit( "feature_type/SET_CURRENT_FEATURE_TYPE_SLUG", this.$route.params.slug_type_signal ); // todo : mutualize in store with feature_detail.vue if (this.$route.params.slug_signal) { featureAPI .getFeatureAttachments(this.$route.params.slug_signal) .then((data) => this.addExistingAttachementFormset(data)); } else { //* be sure that previous attachemntFormset has been cleared for creation this.$store.commit("feature/CLEAR_ATTACHMENT_FORM"); } }, mounted() { let ftSlug=this.$route.params.slug_type_signal; this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug).then(data=>{ console.log(data) this.initForm(); this.initMap(); this.feature_type=this.$store.state.feature_type.feature_types.find( (el) => el.slug === ftSlug ); this.onFeatureTypeLoaded(); this.initExtraForms(); setTimeout( function () { mapUtil.addGeocoders(this.$store.state.configuration); }.bind(this), 1000); }) }, }; </script> <style> #map { height: 70vh; width: 100%; border: 1px solid grey; } @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; } </style>