diff --git a/.eslintrc.json b/.eslintrc.json index 8d6c83c5a..bc0bf1308 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,6 +11,7 @@ ], "reportUnusedDisableDirectives": true, "rules": { + "prettier/prettier": ["warn", { "endOfLine": "auto" }], "no-unused-vars": "warn", "no-console": "warn", "vue/no-v-html": "off", diff --git a/docker/nginx.conf b/docker/nginx.conf index 9cf69d1f0..f29a8293f 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,5 +1,6 @@ server { - listen ${PORT} proxy_protocol; + listen ${PORT}; + #listen ${PORT} proxy_protocol; server_name ${SERVER_NAME}; root /usr/share/nginx/html; index index.html; diff --git a/src/assets/img/boxes/public_transport.svg b/src/assets/img/boxes/public_transport.svg new file mode 100644 index 000000000..fc5b32ff4 --- /dev/null +++ b/src/assets/img/boxes/public_transport.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/boxes/toggle_minus.svg b/src/assets/img/boxes/toggle_minus.svg new file mode 100644 index 000000000..d619a5253 --- /dev/null +++ b/src/assets/img/boxes/toggle_minus.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/boxes/toggle_plus.svg b/src/assets/img/boxes/toggle_plus.svg new file mode 100644 index 000000000..1790edf46 --- /dev/null +++ b/src/assets/img/boxes/toggle_plus.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/img/boxes/transport_not_found.svg b/src/assets/img/boxes/transport_not_found.svg new file mode 100644 index 000000000..08187c036 --- /dev/null +++ b/src/assets/img/boxes/transport_not_found.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/map/OlMap.vue b/src/components/map/OlMap.vue index a9f9924a7..c5d703810 100644 --- a/src/components/map/OlMap.vue +++ b/src/components/map/OlMap.vue @@ -1064,8 +1064,7 @@ export default { resultFeature = feature; return true; }); - - this.setHighlightedFeature(resultFeature); + this.setHighlightedFeature(resultFeature); /////////// this.$emit('highlight-document', resultFeature ? resultFeature.get('document') : null); }, @@ -1094,6 +1093,13 @@ export default { if (feature) { const document = feature.get('document'); + if (document && document.type === 'z') { + return; + } + if (document && document.type === 's') { + this.$emit('stop-clicked', document.document_id); + return; + } if (document) { if (this.documents.length === 1 && document.document_id === this.documents[0].document_id) { return; @@ -1256,6 +1262,75 @@ export default { ); this.editMapLink = `https://www.openstreetmap.org/#map=13/${coords[1]}/${coords[0]}`; }, + + highlightStop(stopId) { + this.resetStopStyles(); + const stopFeature = this.findStopPoint(stopId); + + if (!stopFeature) { + console.warn(`Stop with ID ${stopId} not found.`); + return; + } + + const title = stopFeature.get('document').stoparea_name; + stopFeature.setStyle(getDocumentPointStyle(stopFeature.get('document'), title, true)); + }, + + goAndZoomOnStop(stopId) { + const stopFeature = this.findStopPoint(stopId); + + if (!stopFeature) { + console.warn(`Stop with ID ${stopId} not found.`); + return; + } + const geometry = stopFeature.getGeometry(); + if (geometry) { + const coordinate = geometry.getCoordinates(); + + const view = this.map.getView(); + view.cancelAnimations(); + + view.animate({ + center: coordinate, + zoom: Math.max(view.getZoom(), 15), + duration: 300, + }); + } + }, + + resetStopStyles() { + const documentsSource = this.documentsLayer.getSource(); + const waypointsSource = this.waypointsLayer.getSource(); + + documentsSource.forEachFeature((feature) => { + if (feature.values_.document.type !== 'w' && feature.values_.document.type !== 'z') { + feature.setStyle(feature.get('normalStyle')); + } + }); + + waypointsSource.forEachFeature((feature) => { + feature.setStyle(feature.get('normalStyle')); + }); + }, + + findStopPoint(stopId) { + const documentsSource = this.documentsLayer.getSource(); + const documentFeature = documentsSource.getFeatureById(stopId); + + if (documentFeature) { + return documentFeature; + } + + const waypointsSource = this.waypointsLayer.getSource(); + const waypointFeature = waypointsSource.getFeatureById(stopId); + + if (waypointFeature) { + return waypointFeature; + } + + console.warn(`Stop with ID ${stopId} not found.`); + return null; + }, }, }; diff --git a/src/components/map/map-utils.js b/src/components/map/map-utils.js index baa3bb282..16089f96c 100644 --- a/src/components/map/map-utils.js +++ b/src/components/map/map-utils.js @@ -57,23 +57,38 @@ export const buildDiffStyle = function (isOld) { const buildPointStyle = function (title, svgSrc, color, highlight) { const imgSize = highlight ? 30 : 20; const circleRadius = highlight ? 20 : 15; + const zIndexValue = highlight ? 101 : 1; + + if (svgSrc.includes('bus')) { + svgSrc = svgSrc.replace('fill="currentColor"', 'fill="white"'); + } else { + svgSrc = svgSrc.replace('fill="currentColor"', `fill="${color}"`); + } svgSrc = svgSrc.replace(' { + const image = style.getImage(); + + if (image instanceof ol.style.Circle) { + image.getFill().setColor('rgba(255, 255, 255, 1)'); + image.getStroke().setColor('#4baf50'); + + style.setZIndex(highlight ? 101 : 50); + } + + if (image instanceof ol.style.Icon) { + style.setZIndex(highlight ? 102 : 51); + } + }); + + return styles; } else if (type === 'a') { return new ol.style.Style(); } else { diff --git a/src/js/apis/transport-service.js b/src/js/apis/transport-service.js new file mode 100644 index 000000000..290f01ff8 --- /dev/null +++ b/src/js/apis/transport-service.js @@ -0,0 +1,85 @@ +import axios from 'axios'; + +import config from '@/js/config'; + +function TransportService() { + this.axios = axios.create({ + baseURL: config.urls.api, + }); +} + +/** + * Retrieves public transport stops for a given waypoint + * + * @param {string | number} waypointId + * @returns {Promise} + */ +TransportService.prototype.getStopareas = function (waypointId) { + return this.axios.get(`/waypoints/${waypointId}/stopareas`); +}; + +/** + * Check if a waypoint is accessible by public transport + * + * @param {string | number} waypointId + * @returns {Promise} + */ +TransportService.prototype.isReachable = function (waypointId) { + return this.axios.get(`/waypoints/${waypointId}/isReachable`).then((response) => response.data); +}; + +/** + * Retrieves stops for multiple waypoints and determines if all are reachable + * + * @param {Array} documents + * @returns {Promise} + */ +TransportService.prototype.getStopareasForDocuments = function (documents) { + if (!documents || documents.length === 0) { + return Promise.resolve({ stopareas: [], missingTransportForWaypoint: false, documentResults: {} }); + } + + const documentResults = {}; + const allStopareas = []; + + const fetchPromises = documents.map((doc) => { + if (!doc || !doc.document_id) { + console.warn('Invalid document or missing ID :', doc); + return Promise.resolve(); + } + + return this.getStopareas(doc.document_id) + .then((response) => { + const data = response.data; + const hasStopareas = data.stopareas && data.stopareas.length > 0; + + // Store whether this document has stops + documentResults[doc.document_id] = hasStopareas; + + if (hasStopareas) { + // Add document_id to each stoparea for reference + const stopareasForDocument = data.stopareas.map((stoparea) => ({ + ...stoparea, + document_id: doc.document_id, // Associate stoparea with document + distance: stoparea.distance ?? 0, + })); + allStopareas.push(...stopareasForDocument); + } + }) + .catch((error) => { + console.error(`Error retrieving stops for document ${doc.document_id}:`, error); + documentResults[doc.document_id] = false; + }); + }); + + return Promise.all(fetchPromises).then(() => { + const missingTransportForWaypoint = Object.values(documentResults).some((hasTransport) => !hasTransport); + return { + stopareas: allStopareas, + missingTransportForWaypoint, + documentResults, // Indicates which documents have stops + }; + }); +}; + +export default new TransportService(); diff --git a/src/translations/fr.json b/src/translations/fr.json index ee76aee3a..9d9634034 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -12,6 +12,7 @@ "4": { "paragliding_ratings": "P4" }, + "(less than 5km from one of the topo access points)" : "(à moins de 5km depuis l'un des points d'accès du topo)", "(MRD), you do not have to enter any parameters other than the danger level(s) given by the avalanche bulletin.": "(MRD), vous n’avez pas d’autres paramètres à entrer que le (ou les) niveau(x) de danger donné(s) par le bulletin d'avalanche.", "(MRE), you can enter the sectors of the compass rose that are reported as critical in the avalanche bulletin.": "(MRE), vous pouvez saisir les secteurs de la rose des vents signalés comme critique dans le bulletin d'avalanche.", "(MRP), you can refine the hazard potential, set group size and any precautionary measures being considered.": "(MRP), vous pouvez affiner le potentiel de danger, tenir compte de la taille du groupe et des mesures de précaution envisagées.", @@ -33,6 +34,8 @@ "50 meters while descending": "50 mètres à la descente", "8 days": "8 jours", "A message as been sent to the new address provided. You have to follow the link within to confirm the email address change.": "Un message a été envoyé à la nouvelle adresse renseignée. Vous devez cliquer sur le lien qu'il contient pour confirmer le changement d'adresse.", + "Access by public transport" : "Accès en transport en commun", + "Accessible by public transport" : "Accessible en transport en commun", "Accident database": "Incidents et accidents", "Accidents/Incidents database:": "Une base de données incidents/accidents :", "Account": "Compte", @@ -72,6 +75,7 @@ "Associated routes cotations": "Cotations des itinéraires associés au point de passage", "Association": "Camptocamp Association", "Associations history": "Historique des associations", + "At least one access point in the route don't have a public transport stop within 5km.": "Au moins un point d'accès du topo ne dispose pas d'arrêt de transport en commun à moins de 5km.", "Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.": "Attribution — Vous devez créditer l'Œuvre, intégrer un lien vers la licence et indiquer si des modifications ont été effectuées à l'Oeuvre. ", "Avalanche bulletins": "Bulletins d'avalanche", "Avalanche risk": "Risque d'avalanche", @@ -214,6 +218,7 @@ "Directions (Google Maps)": "Se rendre à ce lieu (Google Maps)", "Disabled at this zoom level": "Désactivé à ce niveau de zoom", "Disclaimer": "Avertissement", + "Distance from the route access point" : "Distance avec le point d'arrêt du topo", "Do you really want to leave? you have unsaved changes!": "Voulez-vous vraiment quitter cette page ? Des changements n'ont pas été sauvegardés !", "Document quality": "Qualité du document", "Donate": "Faire un don à l'association Camptocamp", @@ -302,6 +307,7 @@ "Help research activities to define new prevention means": "Aider la recherche scientifique à dégager des pistes de prévention", "Here is the list of users you are following and whose activity you will see in your personal feed.": "Voici la liste des contributeurs que vous suivez et dont vous verrez les activités dans votre fil personnel.", "Here you may set activity and region filters that will apply to the homepage feed.": "Définissez ici les filtres d'activités et de régions qui seront appliqués dans le fil de la page d'accueil.", + "Hide lines details" : "Masquer le détail des lignes", "High danger level from the avalanche bulletin": "Niveau de danger haut du bulletin d'avalanche", "Hiking gear": "Randonnée pédestre estivale", "History": "Versions", @@ -366,6 +372,7 @@ "Issue report": "Signalement d'un problème", "Join us": "Adhérer", "Know more about SERAC": "En savoir plus sur SERAC", + "km from the access point": "km du point d'accès", "Large group = 5 people and more": "Grand groupe = 5 personnes et plus", "Large group with load shedding distance": "Grand groupe avec distance de délestage", "Last activities": "Dernières activités", @@ -384,6 +391,7 @@ "Let's preserve our playground! I create an account on Outdoorvision to share my GPS tracks for the preservation and design of my practice sites in France.": "Préservons notre terrain de jeu ! Je crée un compte sur Outdoorvision pour y partager mes traces GPS pour la préservation et l’aménagement de mes sites de pratique en France.", "Library:": "Une bibliothèque comportant ", "Licenses": "Licences", + "Line" : "Ligne", "Lines chunks": "Portions de lignes", "Links": "Liens", "List mode": "Vue en liste", @@ -490,6 +498,7 @@ "Open fields to deeply explain the event circumstances.": "Champs libres pour raconter au mieux les circonstances de l'évènement.", "Open fields to detail the picture's context": "Champs libre pour décrire le contexte de l'image", "Open in a new tab": "Ouvrir dans un nouvel onglet", + "Operator" : "Opérateur", "Ordered list": "Liste triée", "Ordnance Survey (uk)": { "Map layer": "Ordn. Survey" @@ -518,6 +527,7 @@ "Personal informations": "Informations personnelles", "Pin the map to a side of the screen": "Épingler la carte à un bord de l'écran", "Pitch description tag": "Balise de description des longueurs", + "Plan my trip →" : "Planifier mon déplacement →", "Please describe your technical and experience level related to the chosen goal, your level of fitness, prior tiredness accumulated, acclimatization if in altitude, etc.": "Décrivez votre niveau technique et expérience par rapport à l’objectif choisi, votre condition physique, la fatigue accumulée avant la sortie, l’acclimatation pour une sortie en altitude, etc.", "Please move the map, or change the route's name.": "Déplacez la carte, ou modifier le nom de l'itinéraire.", "Please select a method before computing.": "Veuillez sélectionner une méthode pour le calcul.", @@ -541,6 +551,7 @@ "Protection areas": "Zones de protection", "Protection status": "Status de la protection", "Provide information and improve its quality, following the contribution rules and respecting copyright.": "Vous êtes invité à compléter les informations et améliorer la qualité du document en suivant les règles de contribution et dans le respect du droit d'auteur.", + "Public transport stops nearby (less than 5km)" : "Arrêts de transports en commun à proximité (à moins de 5km)", "Put it back": "Remettre", "Quote": "Citation", "ROMMA station": "Station ROMMA", @@ -580,6 +591,7 @@ "Save": "Enregistrer", "Search ...": "Rechercher…", "Search a document to associate...": "Rechercher un document à associer", + "See lines details" : "Voir le détail des lignes", "See more": "Voir plus", "See more results": "Plus de résultats", "See other documents nearby": "À proximité de...", @@ -607,6 +619,7 @@ "API message": "Champ trop court (minimum 3 caractères)" }, "Show document history": "Voir l'historique du document", + "Show nearby stops" : "Afficher les arrêts à proximité", "Show only updates from followed users in the homepage feed": "N'afficher dans le fil de la page d'accueil que les documents qui concernent les utilisateurs suivis.", "Sign in to camptocamp today, it's free": "Créez votre compte sur Camptocamp, c'est gratuit !", "Simplify": "Simplifier", @@ -627,6 +640,7 @@ "Statistical cookies": "Cookies statistiques", "Statistics": "Statistiques", "Stay careful while skiing": "Skier avec prudence", + "Stop" : "Arrêt", "Stop following this contributor": "Ne plus suivre ce contributeur", "Subcription revokal failed": "La souscription au service n'a pu être révoquée.", "Subcription revoked": "Souscription supprimée", @@ -669,6 +683,7 @@ "This is where you may describe the route extensively, maybe starting with a brief summary and then developing the description (including approach, descent, etc.). Don't forget to mention the route history if you know it.": "Description complète de l'itinéraire (résumé, approche, descente, etc.) N'oubliez pas l'historique si vous le connaissez.", "This picture depicts a book cover. It is the property of its editor and/or author. It is presented here only for illustration purposes.": "Cette image montre une couverture de livre. Elle est la propriété de l'éditeur et/ou de l'auteur et n'est présentée ici qu'à des fins d'illustration.", "This profile is only available to authenticated users.": "Ce profil est accessible aux seuls utilisateurs authentifiés.", + "This route is partially accessible by public transport.": "Ce topo est partiellement accessible en transport en commun.", "This tool estimates the technical difficulty of a ski route (ski rating).": "Cet outil permet d'estimer la difficulté technique d'un itinéraire à ski (cotation ski)", "This version has been masked.": "Cette version a été masquée.", "Time": "Heure", @@ -692,6 +707,7 @@ "Unblock account": "Débloquer cet utilisateur", "Unfold section": "Déplier la section", "Unformatted text": "Texte brut", + "Unfortunately, this route may not be deserved by public transport": "Malheureusement, cet itinéraire ne semble pas être desservi", "Unmark this route as to do": "Enlever cet itinéraire de ses favoris", "Unordered list": "Liste non triée", "Unprotect document": "Déprotéger ce document", @@ -722,6 +738,7 @@ "Warning: This action cannot be undone!": "Attention : cette action est irréversible !", "Waypoint's main properties": "Principales données du point de passage", "Waypoint's textual description": "Descriptions textuelles du point de passage", + "We didn't find any public transport stop point in a 5 km foot range from any route access point.": "Nous n'avons pas trouvé d'arrêt de transport en commun à moins de 5km à pied de l'un des points d'accès à l'itinéraire.", "We sent you an email, please click on the link to reset password.": "Vous allez bientôt recevoir un email. Veuillez cliquer sur le lien qu'il contient pour réinitialiser votre mot de passe.", "Weather & conditions": "Météo & conditions", "Weather forecast (meteoblue)": "Prévisions météo (meteoblue)", diff --git a/src/views/document/RouteView.vue b/src/views/document/RouteView.vue index 692c88538..c53284bee 100644 --- a/src/views/document/RouteView.vue +++ b/src/views/document/RouteView.vue @@ -11,6 +11,8 @@ + + @@ -123,6 +125,8 @@ + + diff --git a/src/views/document/WaypointView.vue b/src/views/document/WaypointView.vue index a87c4f0e8..96c900f3e 100644 --- a/src/views/document/WaypointView.vue +++ b/src/views/document/WaypointView.vue @@ -129,6 +129,7 @@ + diff --git a/src/views/document/utils/boxes/IsReachableByPublicTransportsBox.vue b/src/views/document/utils/boxes/IsReachableByPublicTransportsBox.vue new file mode 100644 index 000000000..0459eea02 --- /dev/null +++ b/src/views/document/utils/boxes/IsReachableByPublicTransportsBox.vue @@ -0,0 +1,149 @@ + + + + + + + {{ $gettext('Accessible by public transport') }} + {{ $gettext('Plan my trip →') }} + + + + + + + + + diff --git a/src/views/document/utils/boxes/TransportsBox.vue b/src/views/document/utils/boxes/TransportsBox.vue new file mode 100644 index 000000000..621dcc4d7 --- /dev/null +++ b/src/views/document/utils/boxes/TransportsBox.vue @@ -0,0 +1,549 @@ + + + + {{ $gettext('Access by public transport') }} + + + + {{ $gettext('Show nearby stops') }} + + + + + + + + {{ $gettext('Stop') }} : {{ stopName }} + + + {{ $gettext('Distance from the route access point') }} : {{ stopGroup[0].distance }} km + + + {{ $gettext('See lines details') }} + + + {{ $gettext('Hide lines details') }} + + + + + + + {{ $gettext('Line') }} : {{ stop.line }} + + + {{ $gettext('Operator') }} : {{ stop.operator }} + + + + + + + + ! + + {{ $gettext('This route is partially accessible by public transport.') }} + + {{ $gettext("At least one access point in the route don't have a public transport stop within 5km.") }} + + + + + + + + + + + + + + {{ $gettext('Unfortunately, this route may not be deserved by public transport') }} + + {{ + $gettext("We didn't find any public transport stop point in a 5 km foot range from any route access point.") + }} + + + + + + + + + diff --git a/src/views/document/utils/document-view-mixin.js b/src/views/document/utils/document-view-mixin.js index 9a4e9f092..dc82709a5 100644 --- a/src/views/document/utils/document-view-mixin.js +++ b/src/views/document/utils/document-view-mixin.js @@ -2,10 +2,12 @@ import DocumentPrintLicense from './DocumentPrintLicense'; import DocumentViewHeader from './DocumentViewHeader'; import CommentsBox from './boxes/CommentsBox'; import ImagesBox from './boxes/ImagesBox'; +import IsReachableByPublicTransportsBox from './boxes/IsReachableByPublicTransportsBox'; import MapBox from './boxes/MapBox'; import RecentOutingsBox from './boxes/RecentOutingsBox'; import RoutesBox from './boxes/RoutesBox'; import ToolBox from './boxes/ToolBox'; +import TransportsBox from './boxes/TransportsBox'; import ActivitiesField from './field-viewers/ActivitiesField'; import DoubleNumericField from './field-viewers/DoubleNumericField'; import EventActivityField from './field-viewers/EventActivityField'; @@ -33,6 +35,7 @@ export default { LabelValue, ActivitiesField, EventActivityField, + IsReachableByPublicTransportsBox, MapBox, MarkdownSection, ProfilesLinks, @@ -40,6 +43,7 @@ export default { ToolBox, RoutesBox, ImagesBox, + TransportsBox, }, mixins: [viewModeMixin, isEditableMixin], diff --git a/vue.config.js b/vue.config.js index 32d616025..342ca8c1e 100644 --- a/vue.config.js +++ b/vue.config.js @@ -92,7 +92,7 @@ const config = { urlsConfigurations: { demo: { name: 'demo', - api: 'https://api.demov6.camptocamp.org', + api: 'https://preprod.c2c.preprod0.smart-origin.com/api', media: 'https://sos-ch-dk-2.exo.io/c2corg-demov6-active', imageBackend: 'https://images.demov6.camptocamp.org', tracking: 'https://tracking.demov6.camptocamp.org', @@ -114,7 +114,7 @@ const config = { }, prod: { name: 'prod', - api: 'https://api.camptocamp.org', + api: 'https://preprod.c2c.preprod0.smart-origin.com/api', media: 'https://media.camptocamp.org/c2corg-active', imageBackend: 'https://images.camptocamp.org', tracking: 'https://tracking.camptocamp.org', @@ -134,10 +134,19 @@ const config = { corosConnectAuthUrl: 'https://open.coros.com/oauth2/authorize', corosClientId: 'f263ed9257c74e808befaf548a27852c', }, + localhost: { + name: 'localhost', + api: 'http://localhost:6543', + media: 'https://sos-ch-dk-2.exo.io/c2corg-demov6-active', + imageBackend: 'https://images.demov6.camptocamp.org', + forum: 'https://forum.demov6.camptocamp.org', + modernThumbnailsTimestamp: 0, + modernThumbnailsId: 0, + }, }, }; -config.urls = config.urlsConfigurations.prod; // default: prod +config.urls = config.urlsConfigurations.localhost; // default: prod const bundleAnalyzerConfig = { analyzerMode: 'disabled',
{{ $gettext('Plan my trip →') }}
+ {{ $gettext('Stop') }} : {{ stopName }} +
+ {{ $gettext('Distance from the route access point') }} : {{ stopGroup[0].distance }} km +
{{ $gettext('See lines details') }}
{{ $gettext('Hide lines details') }}
!
+ {{ $gettext("At least one access point in the route don't have a public transport stop within 5km.") }} +
+ {{ + $gettext("We didn't find any public transport stop point in a 5 km foot range from any route access point.") + }} +