<template> <div> <div v-if="permissions && permissions.can_view_project && project" id="project-detail" > <ProjectHeader :arrays-offline="arraysOffline" @retrieveInfo="retrieveProjectInfo" @updateLocalStorage="updateLocalStorage" /> <div class="ui grid stackable"> <div class="row"> <div class="eight wide column"> <ProjectFeatureTypes :loading="featureTypesLoading" :project="project" @delete="toggleDeleteFeatureTypeModal" @update="updateAfterImport" /> </div> <div class="eight wide column block-map"> <div class="map-container"> <div id="map" ref="map" /> <div :class="{ active: mapLoading }" class="ui inverted dimmer" > <div class="ui text loader"> Chargement de la carte... </div> </div> <SidebarLayers v-if="basemaps && map && !projectInfoLoading" ref="sidebar" /> <Geolocation /> <div id="popup" class="ol-popup" > <a id="popup-closer" href="#" class="ol-popup-closer" /> <div id="popup-content" /> </div> </div> <router-link id="features-list" :to="{ name: 'liste-signalements', params: { slug: slug }, }" custom > <div class="ui button fluid teal" > <i class="ui icon arrow right" /> Voir tous les signalements </div> </router-link> </div> </div> <div class="row"> <div class="sixteen wide column"> <div class="ui two stackable cards"> <ProjectLastFeatures ref="lastFeatures" /> <ProjectLastComments :loading="projectInfoLoading" /> </div> </div> </div> <div class="row"> <div class="sixteen wide column"> <ProjectParameters :project="project" /> </div> </div> </div> </div> <span v-else-if="!projectInfoLoading"> <i class="icon exclamation triangle" aria-hidden="true" /> <span>Vous ne disposez pas des droits nécessaires pour consulter ce projet.</span> </span> <ProjectModal :is-subscriber="is_suscriber" :feature-type-to-delete="featureTypeToDelete" @action="handleModalAction" /> </div> </template> <script> import mapService from '@/services/map-service'; import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'; import projectAPI from '@/services/project-api'; import featureTypeAPI from '@/services/featureType-api'; import featureAPI from '@/services/feature-api'; import ProjectHeader from '@/components/Project/Detail/ProjectHeader'; import ProjectFeatureTypes from '@/components/Project/Detail/ProjectFeatureTypes'; import ProjectLastFeatures from '@/components/Project/Detail/ProjectLastFeatures'; import ProjectLastComments from '@/components/Project/Detail/ProjectLastComments'; import ProjectParameters from '@/components/Project/Detail/ProjectParameters'; import ProjectModal from '@/components/Project/Detail/ProjectModal'; import SidebarLayers from '@/components/Map/SidebarLayers'; import Geolocation from '@/components/Map/Geolocation'; export default { name: 'ProjectDetail', components: { ProjectHeader, ProjectFeatureTypes, ProjectLastFeatures, ProjectLastComments, ProjectParameters, ProjectModal, SidebarLayers, Geolocation, }, filters: { setDate(value) { const date = new Date(value); const d = date.toLocaleDateString('fr', { year: '2-digit', month: 'numeric', day: 'numeric', }); return d; }, }, props: { message: { type: Object, default: () => {} } }, data() { return { infoMessage: '', importMessage: null, arraysOffline: [], arraysOfflineErrors: [], slug: this.$route.params.slug, is_suscriber: false, tempMessage: null, projectInfoLoading: true, featureTypesLoading: false, featureTypeToDelete: null, mapLoading: true, }; }, computed: { ...mapGetters([ 'permissions' ]), ...mapState('projects', [ 'project' ]), ...mapState([ 'configuration', ]), ...mapState('feature', [ 'features' ]), ...mapState('feature-type', [ 'feature_types' ]), ...mapState([ 'user', 'user_permissions', 'reloadIntervalId', ]), ...mapState('map', [ 'map', 'basemaps', 'availableLayers', ]), API_BASE_URL() { return this.configuration.VUE_APP_DJANGO_API_BASE; }, }, created() { if (this.user) { projectAPI .getProjectSubscription({ baseUrl: this.$store.state.configuration.VUE_APP_DJANGO_API_BASE, projectSlug: this.$route.params.slug }) .then((data) => (this.is_suscriber = data.is_suscriber)); } }, mounted() { this.retrieveProjectInfo(); if (this.message) { this.DISPLAY_MESSAGE(this.message); } }, beforeDestroy() { this.$store.dispatch('CANCEL_CURRENT_SEARCH_REQUEST'); this.CLEAR_RELOAD_INTERVAL_ID(); this.CLOSE_PROJECT_MODAL(); }, methods: { ...mapMutations([ 'CLEAR_RELOAD_INTERVAL_ID', 'DISPLAY_MESSAGE', ]), ...mapMutations('modals', [ 'OPEN_PROJECT_MODAL', 'CLOSE_PROJECT_MODAL' ]), ...mapActions('projects', [ 'GET_PROJECT_INFO', 'GET_PROJECT', ]), ...mapActions('map', [ 'INITIATE_MAP' ]), ...mapActions('feature', [ 'GET_PROJECT_FEATURES' ]), ...mapActions('feature-type', [ 'GET_PROJECT_FEATURE_TYPES', ]), retrieveProjectInfo() { Promise.all([ this.GET_PROJECT(this.slug), this.GET_PROJECT_INFO(this.slug) ]) .then(() => { this.$nextTick(() => { const map = mapService.getMap(); if (map) mapService.destroyMap(); this.initMap(); }); }) .catch((err) => { console.error(err); }) .finally(() => { this.projectInfoLoading = false; }); }, checkForOfflineFeature() { let arraysOffline = []; const localStorageArray = localStorage.getItem('geocontrib_offline'); if (localStorageArray) { arraysOffline = JSON.parse(localStorageArray); this.arraysOffline = arraysOffline.filter( (x) => x.project === this.slug ); } }, updateLocalStorage() { let arraysOffline = []; const localStorageArray = localStorage.getItem('geocontrib_offline'); if (localStorageArray) { arraysOffline = JSON.parse(localStorageArray); } const arraysOfflineOtherProject = arraysOffline.filter( (x) => x.project !== this.slug ); this.arraysOffline = []; arraysOffline = arraysOfflineOtherProject.concat( this.arraysOfflineErrors ); localStorage.setItem('geocontrib_offline', JSON.stringify(arraysOffline)); }, subscribeProject() { projectAPI .subscribeProject({ baseUrl: this.$store.state.configuration.VUE_APP_DJANGO_API_BASE, suscribe: !this.is_suscriber, projectSlug: this.$route.params.slug, }) .then((data) => { this.is_suscriber = data.is_suscriber; this.CLOSE_PROJECT_MODAL(); if (this.is_suscriber) { this.DISPLAY_MESSAGE({ comment: 'Vous êtes maintenant abonné aux notifications de ce projet.', level: 'positive' }); } else { this.DISPLAY_MESSAGE({ comment: 'Vous ne recevrez plus les notifications de ce projet.', level: 'negative' }); } }); }, deleteProject() { projectAPI.deleteProject(this.API_BASE_URL, this.slug) .then((response) => { if (response === 'success') { this.$router.push('/'); this.DISPLAY_MESSAGE({ comment: `Le projet ${this.project.title} a bien été supprimé.`, level: 'positive' }); } else { this.DISPLAY_MESSAGE({ comment: `Une erreur est survenu lors de la suppression du projet ${this.project.title}.`, level: 'negative' }); } }); }, deleteFeatureType() { featureTypeAPI.deleteFeatureType(this.featureTypeToDelete.slug) .then((response) => { this.CLOSE_PROJECT_MODAL(); if (response === 'success') { this.GET_PROJECT(this.slug); this.retrieveProjectInfo(); this.DISPLAY_MESSAGE({ comment: `Le type de signalement ${this.featureTypeToDelete.title} a bien été supprimé.`, level: 'positive', }); } else { this.DISPLAY_MESSAGE({ comment: `Une erreur est survenu lors de la suppression du type de signalement ${this.featureTypeToDelete.title}.`, level: 'negative', }); } this.featureTypeToDelete = null; }) .catch(() => { this.DISPLAY_MESSAGE({ comment: `Une erreur est survenu lors de la suppression du type de signalement ${this.featureTypeToDelete.title}.`, level: 'negative', }); this.CLOSE_PROJECT_MODAL(); }); }, handleModalAction(e) { switch (e) { case 'subscribe': this.subscribeProject(); break; case 'deleteProject': this.deleteProject(); break; case 'deleteFeatureType': this.deleteFeatureType(); break; } }, toggleDeleteFeatureTypeModal(featureType) { this.featureTypeToDelete = featureType; this.OPEN_PROJECT_MODAL('deleteFeatureType'); }, /** * Initializes the map if the project is accessible and the user has view permissions. * This method sets up the map, loads vector tile layers, and handles offline features. */ async initMap() { // Check if the project is accessible and the user has view permissions if (this.project && this.permissions.can_view_project) { // Initialize the map using the provided element reference await this.INITIATE_MAP({ el: this.$refs.map }); // Check for offline features this.checkForOfflineFeature(); // Define the URL for vector tile layers const mvtUrl = `${this.API_BASE_URL}features.mvt`; // Define parameters for loading layers const params = { project_slug: this.slug, featureTypes: this.feature_types, queryParams: { ordering: this.project.feature_browsing_default_sort, filter: this.project.feature_browsing_default_filter, }, }; // Add vector tile layers to the map mapService.addVectorTileLayer({ url: mvtUrl, ...params }); // Modify offline feature properties (setting color to 'red') this.arraysOffline.forEach((x) => (x.geojson.properties.color = 'red')); // Extract offline features from arraysOffline const featuresOffline = this.arraysOffline.map((x) => x.geojson); // Add offline features to the map if available if (featuresOffline && featuresOffline.length > 0) { mapService.addFeatures({ addToMap: true, features: featuresOffline, ...params }); } // Get the bounding box of features and fit the map to it featureAPI.getFeaturesBbox(this.slug).then((bbox) => { if (bbox) { mapService.fitBounds(bbox); } this.mapLoading = false; // Mark map loading as complete }); } }, updateAfterImport() { // reload feature types this.featureTypesLoading = true; this.GET_PROJECT_FEATURE_TYPES(this.slug) .then(() => { this.featureTypesLoading = false; }); // reload last features this.$refs.lastFeatures.fetchLastFeatures(); // reload map const map = mapService.getMap(); if (map) mapService.destroyMap(); this.mapLoading = true; this.initMap(); }, }, }; </script> <style lang="less" scoped> .fullwidth { width: 100%; } .block-map { display: flex !important; flex-direction: column; .map-container { position: relative; height: 100%; #map { border: 1px solid grey; } } .button { margin-top: 0.5em; } } div.geolocation-container { /* each button have .5em space between, zoom buttons are 60px high and full screen button is 34px high */ top: calc(1.1em + 60px); } </style>