Skip to content
Snippets Groups Projects
Feature_edit.vue 25.1 KiB
Newer Older
<template>
  <div v-frag>
    <div class="fourteen wide column">
      <h1 v-if="feature && currentRouteName === 'editer-signalement'">
Timothee P's avatar
Timothee P committed
        Mise à jour du signalement "{{ feature.title || feature.feature_id }}"
        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="required field">
            <label :for="form.title.id_for_label">{{ form.title.label }}</label>
              :maxlength="form.title.field.max_length"
              :name="form.title.html_name"
              :id="form.title.id_for_label"
              v-model="form.title.value"
            <ul id="errorlist" class="errorlist">
              <li v-for="error in form.title.errors" :key="error">
                {{ error }}
              </li>
            </ul>
            <label :for="form.status.id_for_label">{{
              form.status.label
              :options="statusChoices"
              :selected="selected_status.name"
            <ul id="errorlist" class="errorlist">
              <li v-for="error in form.status.errors" :key="error">
                {{ error }}
              </li>
            </ul>
          <label :for="form.description.id_for_label">{{
            form.description.label
            :name="form.description.html_name"
            v-model="form.description.value"
          {{ form.description.errors }}
          <label :for="form.geom.id_for_label">{{ form.geom.label }}</label>
          <div v-frag v-if="feature_type && feature_type.geom_type === 'point'">
                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>
DESPRES Damien's avatar
DESPRES Damien committed
            </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>
          {{ form.geom.errors }}
            :name="form.geom.html_name"
            :id="form.geom.id_for_label"
            v-model="form.geom.value"
          />
          <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>
          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>
        <!-- {{ attachment_formset.non_form_errors }} -->
          <!-- {{ attachment_formset.management_form }} -->
          <FeatureAttachmentForm
            v-for="form in attachmentFormset"
            :key="form.dataKey"
            :attachmentForm="form"
          />
          @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>
        <!-- {{ linked_formset.non_form_errors }} -->
          <!-- {{ linked_formset.management_form }} -->

          <FeatureLinkedForm
            v-for="form in linkedFormset"
            :key="form.dataKey"
            :linkedForm="form"
            :features="features"
          />
          @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 { mapGetters, 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";
DESPRES Damien's avatar
DESPRES Damien committed
import L from "leaflet";
import "leaflet-draw";
import { mapUtil } from "@/assets/js/map-util.js";
const axios = require("axios");
import flip from "@turf/flip";
    FeatureAttachmentForm,
    FeatureLinkedForm,
      map: null,
      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" },
      ],
      form: {
          errors: null,
          id_for_label: "name",
          html_name: "name",
          label: "Nom",
          value: "",
          errors: null,
          id_for_label: "status",
          html_name: "status",
          label: "Statut",
          value: {
            value: "draft",
            name: "Brouillon",
          },
          errors: null,
          id_for_label: "description",
          html_name: "description",
          label: "Description",
          value: "",
          label: "Localisation",
  computed: {
    ...mapState(["project"]),
    ...mapState("map", ["basemaps"]),
    ...mapState("feature", [
      "attachmentFormset",
      "linkedFormset",
      "features",
      "extra_form",
    ]),
    ...mapGetters("feature_type", ["feature_type"]),
    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() {
        console.log(this.form.status.value);
        return this.form.status.value;
      },
      set(newValue) {
        this.form.status.value = newValue;
        console.log(this.form.status.value);
    feature_type() {
      this.onFeatureTypeLoaded();
      this.initExtraForms();
      if (this.$route.name === "editer-signalement") {
        this.initForm();
        this.initExtraForms(newValue);
      }
    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();
DESPRES Damien's avatar
DESPRES Damien committed
    create_point_geoposition() {
      function success(position) {
        const latitude = position.coords.latitude;
        const longitude = position.coords.longitude;
DESPRES Damien's avatar
DESPRES Damien committed

        var layer = L.circleMarker([latitude, longitude]);
        this.add_layer_call_back(layer);
DESPRES Damien's avatar
DESPRES Damien committed
        this.map.setView([latitude, longitude]);
      }

      function error(err) {
        this.erreurGeolocalisationMessage = err.message;
DESPRES Damien's avatar
DESPRES Damien committed
      }
      this.erreurGeolocalisationMessage = null;
DESPRES Damien's avatar
DESPRES Damien committed
      if (!navigator.geolocation) {
        this.erreurGeolocalisationMessage =
          "La géolocalisation n'est pas supportée par votre navigateur.";
DESPRES Damien's avatar
DESPRES Damien committed
      } else {
        navigator.geolocation.getCurrentPosition(
          success.bind(this),
          error.bind(this)
        );
DESPRES Damien's avatar
DESPRES Damien committed
      }
    },
    handleFileUpload() {
      this.file = this.$refs.file.files[0];
      console.log(">>>> 1st element in files array >>>> ", this.file);
DESPRES Damien's avatar
DESPRES Damien committed
    },
DESPRES Damien's avatar
DESPRES Damien committed
      console.log("georeferencement");
      const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}exif-geom-reader/`;
      let formData = new FormData();
      formData.append("file", this.file);
      console.log(">> formData >> ", formData);
      let self = this;
      axios
        .post(url, formData, {
          headers: {
            "Content-Type": "multipart/form-data",
          },
        })
        .then(function () {
          console.log("SUCCESS!!");
DESPRES Damien's avatar
DESPRES Damien committed
        })
        .catch(function () {
          console.log("FAILURE!!");
          self.erreurUploadMessage = "FAILURE!!";
DESPRES Damien's avatar
DESPRES Damien committed
        });
    },
    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 : "",
      if (this.form.title.value) {
        this.form.title.errors = [];
        return this.checkCustomForms(); //* if customForms are ok, validate, if get out function
      } else if (
        !this.form.title.errors.includes("Veuillez compléter ce champ.") // TODO : Gérer les autres champs
      ) {
        this.form.title.errors.push("Veuillez compléter ce champ.");
        document
          .getElementById("errorlist")
          .scrollIntoView({ block: "end", inline: "nearest" });
      return false;
    },

    postForm() {
      if (!this.checkForms()) return;
      this.$store.dispatch("feature/SEND_FEATURE", this.currentRouteName);
      // if (this.form.title.value) {
      //   this.form.title.errors = null;
      // } else {
      //   this.form.title.errors = "Veuillez compléter ce champ.";
      // }
DESPRES Damien's avatar
DESPRES Damien committed
      var geomLeaflet = {
        point: "circlemarker",
        linestring: "polyline",
        polygon: "polygon",
      };
      // console.log(this.feature_type)
DESPRES Damien's avatar
DESPRES Damien committed
      var geomType = this.feature_type.geom_type;
DESPRES Damien's avatar
DESPRES Damien committed
      var drawConfig = {
        polygon: false,
        marker: false,
        polyline: false,
        rectangle: false,
        circle: false,
        circlemarker: false,
      };
      drawConfig[geomLeaflet[geomType]] = true;
DESPRES Damien's avatar
DESPRES Damien committed

      L.drawLocal = {
        draw: {
          toolbar: {
            actions: {
              title: "Annuler le dessin",
              text: "Annuler",
DESPRES Damien's avatar
DESPRES Damien committed
            },
            finish: {
              title: "Terminer le dessin",
              text: "Terminer",
DESPRES Damien's avatar
DESPRES Damien committed
            },
            undo: {
              title: "Supprimer le dernier point dessiné",
              text: "Supprimer le dernier point",
DESPRES Damien's avatar
DESPRES Damien committed
            },
            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",
            },
DESPRES Damien's avatar
DESPRES Damien committed
          },
          handlers: {
            circle: {
              tooltip: {
                start: "Cliquer et glisser pour dessiner le cercle.",
DESPRES Damien's avatar
DESPRES Damien committed
            },
            circlemarker: {
              tooltip: {
                start: "Cliquer sur la carte pour placer le point.",
              },
DESPRES Damien's avatar
DESPRES Damien committed
            },
            marker: {
              tooltip: {
                start: "Cliquer sur la carte pour placer la balise.",
              },
DESPRES Damien's avatar
DESPRES Damien committed
            },
            polygon: {
              tooltip: {
                start: "Cliquer pour commencer à dessiner.",
                cont: "Cliquer pour continuer à dessiner.",
                end: "Cliquer sur le premier point pour terminer le dessin.",
              },
DESPRES Damien's avatar
DESPRES Damien committed
            },
            polyline: {
              error: "<strong>Error:</strong> shape edges cannot cross!",
DESPRES Damien's avatar
DESPRES Damien committed
              tooltip: {
                start: "Cliquer pour commencer à dessiner.",
                cont: "Cliquer pour continuer à dessiner.",
                end: "Cliquer sur le dernier point pour terminer le dessin.",
              },
DESPRES Damien's avatar
DESPRES Damien committed
            },
            rectangle: {
              tooltip: {
                start: "Cliquer et glisser pour dessiner le rectangle.",
              },
DESPRES Damien's avatar
DESPRES Damien committed
            },
            simpleshape: {
              tooltip: {
                end: "Relâcher la souris pour terminer de dessiner.",
              },
            },
          },
DESPRES Damien's avatar
DESPRES Damien committed
        },
        edit: {
          toolbar: {
            actions: {
              save: {
                title: "Sauver les modifications",
                text: "Sauver",
DESPRES Damien's avatar
DESPRES Damien committed
              },
              cancel: {
                title:
                  "Annuler la modification, annule toutes les modifications",
                text: "Annuler",
DESPRES Damien's avatar
DESPRES Damien committed
              },
              clearAll: {
                title: "Effacer l'objet",
                text: "Effacer",
              },
DESPRES Damien's avatar
DESPRES Damien committed
            },
            buttons: {
              edit: "Modifier l'objet",
              editDisabled: "Aucun objet à modifier",
              remove: "Supprimer l'objet",
              removeDisabled: "Aucun objet à supprimer",
            },
DESPRES Damien's avatar
DESPRES Damien committed
          },
          handlers: {
            edit: {
              tooltip: {
                text: "Faites glisser les marqueurs ou les balises pour modifier l'élément.",
                subtext: "Cliquez sur Annuler pour annuler les modifications..",
              },
DESPRES Damien's avatar
DESPRES Damien committed
            },
            remove: {
              tooltip: {
                text: "Cliquez sur un élément pour le supprimer.",
              },
            },
          },
        },
DESPRES Damien's avatar
DESPRES Damien committed
      this.drawnItems = new L.FeatureGroup();
      this.map.addLayer(this.drawnItems);

      this.drawControlFull = new L.Control.Draw({
DESPRES Damien's avatar
DESPRES Damien committed
        edit: {
          featureGroup: this.drawnItems,
DESPRES Damien's avatar
DESPRES Damien committed
        },
        draw: drawConfig,
DESPRES Damien's avatar
DESPRES Damien committed

      this.drawControlEditOnly = new L.Control.Draw({
DESPRES Damien's avatar
DESPRES Damien committed
        edit: {
          featureGroup: this.drawnItems,
DESPRES Damien's avatar
DESPRES Damien committed
        },
DESPRES Damien's avatar
DESPRES Damien committed
      });

      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)
      );
DESPRES Damien's avatar
DESPRES Damien committed
      //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 = "";
DESPRES Damien's avatar
DESPRES Damien committed
    },
DESPRES Damien's avatar
DESPRES Damien committed
    updateMap(map, geomFeatureJSON) {
      this.drawnItems.clearLayers();
      console.log("update map");
DESPRES Damien's avatar
DESPRES Damien committed
      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);
DESPRES Damien's avatar
DESPRES Damien committed
        }
        this.map.fitBounds(this.drawnItems.getBounds());
DESPRES Damien's avatar
DESPRES Damien committed
      } 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();
DESPRES Damien's avatar
DESPRES Damien committed
    },
DESPRES Damien's avatar
DESPRES Damien committed
      //console.log(drawnItems);
      //console.log(configuration);
      var mapDefaultViewCenter =
        this.$store.state.configuration.DEFAULT_MAP_VIEW.center;
      var mapDefaultViewZoom =
        this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom;
DESPRES Damien's avatar
DESPRES Damien committed

      // 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)
DESPRES Damien's avatar
DESPRES Damien committed
        .then((response) => {
          //console.log(response);
DESPRES Damien's avatar
DESPRES Damien committed
          const features = response.data.features;
          if (features) {
            const allFeaturesExceptCurrent = features.filter(
              (feat) => feat.id !== currentFeatureId
            );
DESPRES Damien's avatar
DESPRES Damien committed
            mapUtil.addFeatures(allFeaturesExceptCurrent);
DESPRES Damien's avatar
DESPRES Damien committed
            if (this.currentRouteName === "editer-signalement") {
              const currentFeature = features.filter(
                (feat) => feat.id === currentFeatureId
              )[0];
              this.updateMap(this.map, currentFeature);
DESPRES Damien's avatar
DESPRES Damien committed
            }
DESPRES Damien's avatar
DESPRES Damien committed
          }
        })
        .catch((error) => {
          throw error;
        });

      document.addEventListener("change-layers-order", (event) => {
DESPRES Damien's avatar
DESPRES Damien committed
        // 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);
DESPRES Damien's avatar
DESPRES Damien committed
      //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 = "";
DESPRES Damien's avatar
DESPRES Damien committed
      }
  created() {
    if (!this.project) {
      this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug);
Timothee P's avatar
Timothee P committed
    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");
    }
    this.initForm();
DESPRES Damien's avatar
DESPRES Damien committed
    this.initMap();
};

// TODO : add script from django and convert:
</script>

<style>
#map {
  height: 70vh;
  width: 100%;

@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;
}