Newer
Older

Timothee P
committed
<template>
<div v-frag>
<script
type="application/javascript"
:src="
baseUrl +
'/resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js'
"
></script>

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

Timothee P
committed
@blur="updateStore"

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

Timothee P
committed
<!-- Import GeoImage -->
<div v-frag v-if="feature_type && feature_type.geom_type === 'point'">
<button

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

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"

Timothee P
committed
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<i class="close icon" @click="toggleGeoRefModal"></i>
<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"></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" style="color: red">
{{ erreurUploadMessage }}
</p>
</div>
<button
@click="georeferencement"
id="get-geom-from-image-file"
type="button"
:class="[
'ui compact button',
file && !erreurUploadMessage ? 'green' : 'disabled',
{ red: erreurUploadMessage },
]"
>
<i class="plus icon"></i>
Importer
</button>
</form>
</div>
</div>

Timothee P
committed
<button

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

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
type="hidden"
:name="form.geom.html_name"
:id="form.geom.id_for_label"
v-model="form.geom.value"

Timothee P
committed
@blur="updateStore"

Timothee P
committed
/>
<div class="ui tab active map-container" data-tab="map">
<div id="map"></div>
<!-- {% if serialized_base_maps|length > 0 %} {% include

Timothee P
committed
"geocontrib/map-layers/sidebar-layers.html" with
basemaps=serialized_base_maps layers=serialized_layers
project=project.slug%} {% endif %} -->
<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 class="ui horizontal divider">PIÈCES JOINTES</div>
<div id="formsets-attachment">

Timothee P
committed
v-for="form in attachmentFormset"
:key="form.dataKey"
:attachmentForm="form"
ref="attachementForm"

Timothee P
committed
/>

Timothee P
committed

Timothee P
committed
<button

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

Timothee P
committed
<div id="formsets-link">
<FeatureLinkedForm
v-for="form in linkedFormset"
:key="form.dataKey"
:linkedForm="form"
:features="features"
ref="linkedForm"

Timothee P
committed
<button

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

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

Timothee P
committed
axios.defaults.headers.common["X-CSRFToken"] = ((name) => {
var re = new RegExp(name + "=([^;]+)");
var value = re.exec(document.cookie);

Timothee P
committed
export default {
name: "Feature_edit",

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,
statusChoices: [
{
name: "Brouillon",
value: "draft",
},

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",
value: {
value: "draft",
name: "Brouillon",
},

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

Timothee P
committed
},
geom: {

Timothee P
committed
},
},
};
},

Timothee P
committed
...mapGetters("feature_type", ["feature_type"]),
...mapState(["user", "USER_LEVEL_PROJECTS"]),
...mapState("map", ["basemaps"]),
...mapState("feature", [
"attachmentFormset",
"attachmentsToDelete",
"attachmentsToPut",
"linkedFormset",
"features",
"extra_form",

Timothee P
committed
"linked_features",
field_title() {
if (this.feature_type) {
if (this.feature_type.title_optional) {
return "field";
currentRouteName() {
return this.$route.name;
},
Sébastien DA ROCHA
committed
feature: function () {
return this.$store.state.feature.features.find(
(el) => el.feature_id === this.$route.params.slug_signal
);
},
orderedCustomFields() {
return [...this.extra_form].sort(
(a, b) => a.position - b.position
);
},
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 //* prevent undefined feature
? this.feature.creator === this.user.id
: false; //* si le contributeur est l'auteur du signalement
//* 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
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"
);

Timothee P
committed
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
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 =
"La géolocalisation a été désactivé par l'utilisateur";
}
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
this.erreurUploadMessage = "";
this.file = null;
}
this.showGeoRef = !this.showGeoRef;
},

Timothee P
committed
this.erreurUploadMessage = "";
georeferencement() {
console.log("georeferencement");
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}exif-geom-reader/`;
let formData = new FormData();
console.log(">> formData >> ", formData);
axios
.post(url, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})

Timothee P
committed
.then((response) => {
console.log("SUCCESS!!", response.data);
if (response.data.geom.indexOf("POINT") >= 0) {
let regexp = /POINT\s\((.*)\s(.*)\)/;

Timothee P
committed
let arr = regexp.exec(response.data.geom);
let json = {
type: "Feature",
geometry: {
type: "Point",
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,

Timothee P
committed
.catch((response) => {
console.log("FAILURE!!");

Timothee P
committed
this.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);
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
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);
}
},

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

Timothee P
committed
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;
}
},

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.");
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" });
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();
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",
title:
"Annuler la modification, annule toutes les modifications",
text: "Annuler",
title: "Effacer l'objet",
text: "Effacer",
},
edit: "Modifier l'objet",
editDisabled: "Aucun objet à modifier",
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({
position: "topright",
featureGroup: this.drawnItems,
this.drawControlEditOnly = new L.Control.Draw({
position: "topright",
featureGroup: this.drawnItems,
});
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] });
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();
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;

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)
const features = response.data.features;
if (features) {
const allFeaturesExceptCurrent = features.filter(
(feat) => feat.id !== currentFeatureId
);
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));
},

Timothee P
committed
getLinkedFeatures() {
featureAPI
.getFeatureLinks(this.$route.params.slug_signal)
.then((data) => this.addExistingLinkedFormset(data));
},

Timothee P
committed
},
this.$store.commit(
"feature_type/SET_CURRENT_FEATURE_TYPE_SLUG",
this.$route.params.slug_type_signal
);
if (this.$route.params.slug_signal) {

Timothee P
committed
this.getLinkedFeatures();
this.$store
.dispatch("GET_PROJECT_INFO", this.$route.params.slug)
.then((data) => {
console.log(data);