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
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'">
<p v-if="isOffline() !== true">
<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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<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"
/>
<ul v-if="erreurUploadMessage" class="errorlist">
<li>
{{ erreurUploadMessage }}
</li>
</ul>

Timothee P
committed
</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
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">
<!-- {% 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 v-if="isOffline() !== true">
<div class="ui horizontal divider">PIÈCES JOINTES</div>
<div v-if="isOffline() !== true" id="formsets-attachment">
<FeatureAttachmentForm
v-for="form in attachmentFormset"
:key="form.dataKey"
:attachmentForm="form"
ref="attachementForm"
/>
</div>

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

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
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>

Timothee P
committed
<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";
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);
// return value !== null ? unescape(value[1]) : null;
// })("csrftoken");

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,

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
},
},
};
},
...mapGetters(["project", "permissions"]),

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 () {
const result = this.$store.state.feature.current_feature;
return result;
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
? this.feature.creator === this.user.id //* prevent undefined feature
: false; //* si le contributeur est l'auteur du signalement
//* si admin, modérateur ou super contributeur, statuts toujours disponible: Brouillon, Publié, Archivé

Timothee P
committed
(userStatus === "Super Contributeur" && !isModerate)
) {
return this.statusChoices.filter((el) => el.value !== "pending");

Timothee P
committed
} else if (userStatus === "Super Contributeur" && isModerate) {
return this.statusChoices.filter(
(el) => el.value === "draft" || 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: {
isOffline() {
return navigator.onLine == false;
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 =
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,
.catch((error) => {
console.log({ 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";
}
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();
//* 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.$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.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;
setTimeout(() => {
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)
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);
}
})
.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",