Add search and PARCOURSMOB integration
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 40s

This commit is contained in:
Arnaud Delcasse
2026-01-07 11:46:19 +01:00
parent ed5dade5c3
commit 36dd81e661
10 changed files with 14048 additions and 110 deletions

View File

@@ -0,0 +1,765 @@
{{/*
Partial: search-results
Affiche les résultats de recherche en 2 colonnes :
- Gauche : accordéons par catégorie
- Droite : carte MapLibre
*/}}
{{ $iconArrow := resources.Get "images/picto/keyboard_arrow_up_24dp_1F1F1F_FILL1_wght400_GRAD0_opsz24.svg" }}
{{ $iconArrowRight := resources.Get "images/picto/arrow_right_alt_24dp_1F1F1F_FILL1_wght400_GRAD0_opsz24.svg" }}
{{ $iconWalk := resources.Get "images/picto/directions_walk_24dp_1F1F1F_FILL1_wght400_GRAD0_opsz24.svg" }}
{{ $iconBus := resources.Get "images/picto/directions_bus_24dp_1F1F1F_FILL1_wght400_GRAD0_opsz24.svg" }}
<div class="search-results-container" x-data="selectionManager()">
<!-- Colonne gauche : Accordéons -->
<div class="search-results-accordions">
<!-- Transports en commun -->
<div class="result-accordion" x-data="{ open: false }">
<button type="button" class="accordion-header" @click="open = !open" :class="{ 'active': open }">
<span class="accordion-title">Transports en commun</span>
<span class="accordion-badge" x-text="results.public_transit?.number || 0"></span>
{{ if $iconArrow }}
<img src="{{ $iconArrow.RelPermalink }}" alt="" class="accordion-arrow" :class="{ 'open': open }" />
{{ end }}
</button>
<div class="accordion-content" x-show="open" x-collapse x-cloak>
<div class="accordion-inner">
<template x-if="results.public_transit?.number > 0">
<div class="transit-journeys">
<template x-for="(journey, index) in results.public_transit.results" :key="index">
<div class="transit-journey" @click="selectItem('transit', index, journey)" :class="{ 'active': isSelected('transit', index) }">
<!-- En-tête du trajet -->
<div class="journey-header">
<div class="journey-date" x-text="formatJourneyDate(journey.startTime)"></div>
<div class="journey-times">
<span class="journey-time" x-text="formatTime(journey.startTime)"></span>
<img src="{{ $iconArrowRight.RelPermalink }}" alt="→" class="journey-arrow" />
<span class="journey-time" x-text="formatTime(journey.endTime)"></span>
<span class="journey-duration" x-text="formatDuration(journey.duration)"></span>
</div>
</div>
<!-- Séparateur -->
<div class="journey-separator"></div>
<!-- Détail des legs -->
<div class="journey-legs">
<template x-for="(leg, legIndex) in journey.legs" :key="legIndex">
<div class="journey-leg">
<div class="leg-icon">
<template x-if="leg.mode === 'WALK'">
<img src="{{ $iconWalk.RelPermalink }}" alt="Marche" />
</template>
<template x-if="leg.mode === 'BUS' || leg.mode === 'COACH'">
<img src="{{ $iconBus.RelPermalink }}" alt="Bus" />
</template>
<template x-if="leg.mode !== 'WALK' && leg.mode !== 'BUS' && leg.mode !== 'COACH'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
<path d="M4 15.5C4 17.43 5.57 19 7.5 19L6 20.5v.5h12v-.5L16.5 19c1.93 0 3.5-1.57 3.5-3.5V5c0-3.5-3.58-4-8-4s-8 .5-8 4v10.5zm8-12.5c3.52 0 5.78.28 6 1h-12c.22-.72 2.48-1 6-1zM6 7h12v4H6V7zm6 10.5a1.5 1.5 0 110-3 1.5 1.5 0 010 3z"/>
</svg>
</template>
</div>
<div class="leg-details">
<template x-if="leg.mode === 'WALK'">
<div>
<p class="leg-line">Marche à pieds</p>
<p class="leg-text">
<span x-text="formatDistance(leg.distance)"></span>
<span class="leg-duration" x-text="'(' + formatDuration(leg.duration) + ')'"></span>
</p>
</div>
</template>
<template x-if="leg.mode !== 'WALK'">
<div>
<p class="leg-line" x-text="'Ligne ' + (leg.routeShortName || leg.route?.shortName || '')"></p>
<p class="leg-operator" x-text="leg.agencyName"></p>
<p class="leg-direction" x-show="leg.headsign">
Direction <span x-text="leg.headsign"></span>
</p>
<div class="leg-timeline">
<div class="leg-stop leg-stop-departure">
<span class="leg-stop-dot"></span>
<span class="leg-stop-time" x-text="formatTime(leg.startTime)"></span>
<span class="leg-stop-name" x-text="leg.from.name"></span>
</div>
<img src="{{ $iconArrowRight.RelPermalink }}" alt="↓" class="leg-timeline-arrow" />
<div class="leg-stop leg-stop-arrival">
<span class="leg-stop-dot"></span>
<span class="leg-stop-time" x-text="formatTime(leg.endTime)"></span>
<span class="leg-stop-name" x-text="leg.to.name"></span>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
<template x-if="!results.public_transit?.number">
<p class="no-result-text">Aucun transport en commun disponible pour ce trajet.</p>
</template>
{{ with .Params.contactCTA.transit }}
<div class="accordion-cta">
<p>{{ . }}</p>
<a href="tel:{{ site.Params.phone | replaceRE " " "" }}" class="accordion-cta-phone">{{ site.Params.phone }}</a>
</div>
{{ end }}
</div>
</div>
</div>
<!-- Covoiturage -->
<div class="result-accordion" x-data="{ open: false }">
<button type="button" class="accordion-header" @click="open = !open" :class="{ 'active': open }">
<span class="accordion-title">Covoiturage</span>
<span class="accordion-badge" x-text="results.carpools?.number || 0"></span>
{{ if $iconArrow }}
<img src="{{ $iconArrow.RelPermalink }}" alt="" class="accordion-arrow" :class="{ 'open': open }" />
{{ end }}
</button>
<div class="accordion-content" x-show="open" x-collapse x-cloak>
<div class="accordion-inner">
<template x-if="results.carpools?.number > 0">
<div class="carpool-list">
<template x-for="(carpool, index) in results.carpools.results" :key="index">
<div class="carpool-item" @click="selectItem('carpool', index, carpool)" :class="{ 'active': isSelected('carpool', index) }">
<div class="carpool-preview">
<div class="carpool-header">
<span class="carpool-driver">Trajet avec <span x-text="carpool.ocss?.driver?.alias || 'un conducteur'"></span></span>
<span class="carpool-price" x-show="carpool.ocss?.price?.amount" x-text="formatCarpoolPrice(carpool.ocss?.price?.amount)"></span>
</div>
<div class="carpool-date" x-show="carpool.ocss?.passengerPickupDate" x-text="'Départ ' + formatCarpoolDate(carpool.ocss?.passengerPickupDate)"></div>
</div>
<template x-if="carpool.ocss?.webUrl">
<div class="carpool-link-container">
<a :href="carpool.ocss.webUrl" target="_blank" rel="noopener" class="carpool-operator-link">
Voir les détails sur <span x-text="carpool.ocss?.operator || 'le site'"></span>
</a>
</div>
</template>
</div>
</template>
</div>
</template>
<template x-if="!results.carpools?.number">
<p class="no-result-text">Aucun covoiturage disponible pour ce trajet.</p>
</template>
{{ with .Params.contactCTA.carpool }}
<div class="accordion-cta">
<p>{{ . }}</p>
<a href="tel:{{ site.Params.phone | replaceRE " " "" }}" class="accordion-cta-phone">{{ site.Params.phone }}</a>
</div>
{{ end }}
</div>
</div>
</div>
<!-- Transport solidaire -->
<div class="result-accordion" x-data="{ open: false, get totalDrivers() { return (results.solidarity_drivers?.number || 0) + (results.organized_carpools?.number || 0); } }">
<button type="button" class="accordion-header" @click="open = !open" :class="{ 'active': open }">
<span class="accordion-title">Transport solidaire</span>
<span class="accordion-badge" x-text="totalDrivers"></span>
{{ if $iconArrow }}
<img src="{{ $iconArrow.RelPermalink }}" alt="" class="accordion-arrow" :class="{ 'open': open }" />
{{ end }}
</button>
<div class="accordion-content" x-show="open" x-collapse x-cloak>
<div class="accordion-inner">
<template x-if="totalDrivers > 0">
<div class="solidarity-result">
<p>Nous avons trouvé</p>
<p class="solidarity-count" x-text="totalDrivers + ' conducteur' + (totalDrivers > 1 ? 's' : '')"></p>
<p>disponible<span x-text="totalDrivers > 1 ? 's' : ''"></span> pour faire le trajet que vous recherchez</p>
</div>
</template>
<template x-if="totalDrivers === 0">
<p class="no-result-text">Aucun transport solidaire disponible pour ce trajet.</p>
</template>
{{ with .Params.contactCTA.solidarity }}
<div class="accordion-cta">
<p>{{ . }}</p>
<a href="tel:{{ site.Params.phone | replaceRE " " "" }}" class="accordion-cta-phone">{{ site.Params.phone }}</a>
</div>
{{ end }}
</div>
</div>
</div>
<!-- Location de véhicules -->
<div class="result-accordion" x-data="{ open: false }">
<button type="button" class="accordion-header" @click="open = !open" :class="{ 'active': open }">
<span class="accordion-title">Location de véhicules</span>
<span class="accordion-badge" x-text="results.vehicles?.number || 0"></span>
{{ if $iconArrow }}
<img src="{{ $iconArrow.RelPermalink }}" alt="" class="accordion-arrow" :class="{ 'open': open }" />
{{ end }}
</button>
<div class="accordion-content" x-show="open" x-collapse x-cloak>
<div class="accordion-inner">
<template x-if="results.vehicles?.number > 0">
<div class="solidarity-result">
<p>Nous avons trouvé</p>
<p class="solidarity-count" x-text="results.vehicles.number + ' véhicule' + (results.vehicles.number > 1 ? 's' : '')"></p>
<p>disponible<span x-text="results.vehicles.number > 1 ? 's' : ''"></span></p>
</div>
</template>
<template x-if="!results.vehicles?.number">
<p class="no-result-text">Aucun véhicule disponible pour ce trajet.</p>
</template>
{{ with .Params.contactCTA.vehicles }}
<div class="accordion-cta">
<p>{{ . }}</p>
<a href="tel:{{ site.Params.phone | replaceRE " " "" }}" class="accordion-cta-phone">{{ site.Params.phone }}</a>
</div>
{{ end }}
</div>
</div>
</div>
<!-- Solutions complémentaires -->
<div class="result-accordion" x-data="{ open: false }">
<button type="button" class="accordion-header" @click="open = !open" :class="{ 'active': open }">
<span class="accordion-title">Solutions complémentaires</span>
<span class="accordion-badge" x-text="results.local_solutions?.number || 0"></span>
{{ if $iconArrow }}
<img src="{{ $iconArrow.RelPermalink }}" alt="" class="accordion-arrow" :class="{ 'open': open }" />
{{ end }}
</button>
<div class="accordion-content" x-show="open" x-collapse x-cloak>
<div class="accordion-inner">
<template x-if="results.local_solutions?.number > 0">
<div class="local-solutions-list">
<template x-for="(solution, index) in results.local_solutions.results" :key="index">
<div class="local-solution-item" @click="selectItem('solution', index, solution)" :class="{ 'active': isSelected('solution', index) }">
<h4 class="local-solution-title" x-text="solution.title || solution.name"></h4>
<p class="local-solution-description" x-text="solution.description" x-show="solution.description"></p>
<template x-if="solution.url">
<a :href="solution.url" target="_blank" rel="noopener" class="local-solution-link" @click.stop>En savoir plus</a>
</template>
</div>
</template>
</div>
</template>
<template x-if="!results.local_solutions?.number">
<p class="no-result-text">Aucune solution complémentaire disponible pour ce trajet.</p>
</template>
{{ with .Params.localSolutionsText }}
<p class="local-solutions-text">{{ . }}</p>
{{ end }}
{{ with .Params.contactCTA.localSolutions }}
<div class="accordion-cta">
<p>{{ . }}</p>
<a href="tel:{{ site.Params.phone | replaceRE " " "" }}" class="accordion-cta-phone">{{ site.Params.phone }}</a>
</div>
{{ end }}
</div>
</div>
</div>
</div>
<!-- Colonne droite : Carte MapLibre -->
<div class="search-results-map">
<div id="map" x-ref="map" x-init="initMap()"></div>
</div>
</div>
<script>
// Gestionnaire de sélection Alpine.js
function selectionManager() {
return {
selectedMode: null,
selectedIndex: null,
selectItem(mode, index, item) {
this.selectedMode = mode;
this.selectedIndex = index;
// Appeler la fonction d'affichage appropriée
if (mode === 'transit') {
window.showJourneyRoute(item, index);
} else if (mode === 'carpool') {
window.showCarpoolRoute(item, index);
} else if (mode === 'solution') {
window.showSolutionZone(item, index);
}
},
isSelected(mode, index) {
return this.selectedMode === mode && this.selectedIndex === index;
}
};
}
// Décodeur polyline Google (implémentation native)
function decodePolyline(encoded, precision) {
precision = precision || 5;
const factor = Math.pow(10, precision);
const len = encoded.length;
let index = 0;
let lat = 0;
let lng = 0;
const coordinates = [];
while (index < len) {
let b;
let shift = 0;
let result = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
const dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
const dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
lng += dlng;
coordinates.push([lat / factor, lng / factor]);
}
return coordinates;
}
// Compteur pour les layers de legs
window.journeyLayerIds = [];
// Fonction globale pour afficher le tracé sur la carte
window.showJourneyRoute = function(journey, index) {
console.log('showJourneyRoute called', index, journey);
const map = window.parcoursmobMap;
if (!map) {
console.log('Map not ready');
return;
}
// Supprimer le message d'info covoiturage s'il existe
const existingInfo = document.querySelector('.carpool-route-info');
if (existingInfo) existingInfo.remove();
// Supprimer les anciens tracés et zones
window.journeyLayerIds.forEach(id => {
if (map.getSource(id)) {
if (map.getLayer(id + '-line')) map.removeLayer(id + '-line');
if (map.getLayer(id + '-fill')) map.removeLayer(id + '-fill');
map.removeSource(id);
}
});
window.journeyLayerIds = [];
// Convertir le Proxy Alpine en objet simple
const legs = JSON.parse(JSON.stringify(journey.legs));
// Bounds pour ajuster la vue
const bounds = new maplibregl.LngLatBounds();
legs.forEach((leg, legIndex) => {
console.log('Leg:', leg);
const coordinates = [];
if (leg.legGeometry && leg.legGeometry.points) {
console.log('Decoding polyline:', leg.legGeometry.points, 'precision:', leg.legGeometry.precision);
try {
const decoded = decodePolyline(leg.legGeometry.points, leg.legGeometry.precision || 5);
console.log('Decoded points:', decoded.length);
decoded.forEach(point => {
const lat = point[0];
const lng = point[1];
if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
coordinates.push([lng, lat]);
}
});
} catch (e) {
console.error('Erreur décodage polyline:', e);
}
} else if (leg.from && leg.to) {
console.log('Using from/to fallback');
const fromLng = leg.from.lon || leg.from.lng;
const fromLat = leg.from.lat;
const toLng = leg.to.lon || leg.to.lng;
const toLat = leg.to.lat;
if (fromLat && fromLng) coordinates.push([fromLng, fromLat]);
if (toLat && toLng) coordinates.push([toLng, toLat]);
}
if (coordinates.length < 2) {
console.log('Not enough coordinates for leg', legIndex);
return;
}
// Ajouter les coordonnées aux bounds
coordinates.forEach(coord => bounds.extend(coord));
const sourceId = 'journey-leg-' + legIndex;
window.journeyLayerIds.push(sourceId);
// Ajouter la source
map.addSource(sourceId, {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: coordinates
}
}
});
// Déterminer la couleur et le style selon le type de leg
const isWalk = leg.mode === 'WALK';
let lineColor = '#888888'; // Gris par défaut pour la marche
if (!isWalk) {
// Utiliser la couleur de la route si disponible
lineColor = leg.routeColor ? '#' + leg.routeColor : '#283959';
}
// Ajouter le layer
map.addLayer({
id: sourceId + '-line',
type: 'line',
source: sourceId,
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': lineColor,
'line-width': isWalk ? 3 : 4,
'line-dasharray': isWalk ? [2, 2] : [1, 0]
}
});
});
// Ajuster la vue si on a des coordonnées
if (!bounds.isEmpty()) {
map.fitBounds(bounds, { padding: 50 });
}
};
// Fonction pour afficher le trajet covoiturage sur la carte
window.showCarpoolRoute = function(carpool, index) {
console.log('showCarpoolRoute called', index, carpool);
const map = window.parcoursmobMap;
if (!map) {
console.log('Map not ready');
return;
}
// Supprimer les anciens tracés et zones
window.journeyLayerIds.forEach(id => {
if (map.getSource(id)) {
if (map.getLayer(id + '-line')) map.removeLayer(id + '-line');
if (map.getLayer(id + '-fill')) map.removeLayer(id + '-fill');
map.removeSource(id);
}
});
window.journeyLayerIds = [];
// Supprimer l'ancien message d'info s'il existe
const existingInfo = document.querySelector('.carpool-route-info');
if (existingInfo) existingInfo.remove();
// Vérifier si une polyline est disponible
const polylineData = carpool.ocss?.journeyPolyline;
if (!polylineData) {
// Afficher un message d'info au lieu du tracé
const isMobile = window.matchMedia('(max-width: 1024px)').matches;
const mapContainer = document.getElementById(isMobile ? 'mobile-map' : 'map');
if (mapContainer) {
const infoDiv = document.createElement('div');
infoDiv.className = 'carpool-route-info';
infoDiv.textContent = "L'itinéraire exact n'est pas disponible";
mapContainer.parentElement.appendChild(infoDiv);
}
return;
}
// Si polyline disponible, décoder et afficher
const decoded = decodePolyline(polylineData, 5);
const coordinates = decoded.map(point => [point[1], point[0]]);
const sourceId = 'carpool-route';
window.journeyLayerIds.push(sourceId);
map.addSource(sourceId, {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: coordinates
}
}
});
map.addLayer({
id: sourceId + '-line',
type: 'line',
source: sourceId,
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': '#00A396',
'line-width': 4
}
});
// Ajuster la vue pour montrer le trajet
const bounds = new maplibregl.LngLatBounds();
coordinates.forEach(coord => bounds.extend(coord));
map.fitBounds(bounds, { padding: 50 });
};
// Fonction pour afficher la zone géographique d'une solution locale
window.showSolutionZone = function(solution, index) {
console.log('showSolutionZone called', index, solution);
const map = window.parcoursmobMap;
if (!map) {
console.log('Map not ready');
return;
}
// Supprimer le message d'info covoiturage s'il existe
const existingInfo = document.querySelector('.carpool-route-info');
if (existingInfo) existingInfo.remove();
// Supprimer les anciens tracés
window.journeyLayerIds.forEach(id => {
if (map.getSource(id)) {
if (map.getLayer(id + '-line')) map.removeLayer(id + '-line');
if (map.getLayer(id + '-fill')) map.removeLayer(id + '-fill');
map.removeSource(id);
}
});
window.journeyLayerIds = [];
// Chercher les données géographiques dans la solution
const geographies = solution.geography || [];
const bounds = new maplibregl.LngLatBounds();
let hasGeometry = false;
geographies.forEach((geo, geoIndex) => {
if (geo.geography && geo.geography.geometry) {
hasGeometry = true;
const sourceId = 'solution-zone-' + index + '-' + geoIndex;
window.journeyLayerIds.push(sourceId);
map.addSource(sourceId, {
type: 'geojson',
data: geo.geography
});
// Ajouter le remplissage
map.addLayer({
id: sourceId + '-fill',
type: 'fill',
source: sourceId,
paint: {
'fill-color': '#F39200',
'fill-opacity': 0.2
}
});
// Ajouter le contour
map.addLayer({
id: sourceId + '-line',
type: 'line',
source: sourceId,
paint: {
'line-color': '#F39200',
'line-width': 2
}
});
// Étendre les bounds avec les coordonnées
const coords = geo.geography.geometry.coordinates;
if (geo.geography.geometry.type === 'Polygon') {
coords[0].forEach(coord => bounds.extend(coord));
} else if (geo.geography.geometry.type === 'MultiPolygon') {
coords.forEach(polygon => {
polygon[0].forEach(coord => bounds.extend(coord));
});
}
}
});
if (hasGeometry && !bounds.isEmpty()) {
map.fitBounds(bounds, { padding: 50 });
}
};
// Fonctions de formatage pour les trajets
function formatTime(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
}
function formatJourneyDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' });
}
function formatDuration(seconds) {
if (!seconds) return '';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h${minutes.toString().padStart(2, '0')}m`;
}
return `${minutes}m`;
}
function formatDistance(meters) {
if (!meters) return '';
if (meters >= 1000) {
const km = (meters / 1000).toFixed(1).replace('.', ',');
return `${km} kilomètres`;
}
return `${Math.round(meters)} mètres`;
}
function formatCarpoolPrice(amount) {
if (!amount && amount !== 0) return '';
return amount.toFixed(2).replace('.', ',') + ' €';
}
function formatCarpoolDate(timestamp) {
if (!timestamp) return '';
// Le timestamp OCSS est en secondes, pas en millisecondes
const date = new Date(timestamp * 1000);
return date.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' }) +
' à ' + date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
}
function initMap() {
const data = window.__PARCOURSMOB_DATA__ || {};
const departure = data.departure;
const destination = data.destination;
if (!departure || !destination) return;
const depCoords = departure.geometry.coordinates;
const destCoords = destination.geometry.coordinates;
// Calculer les bounds
const bounds = [
[Math.min(depCoords[0], destCoords[0]), Math.min(depCoords[1], destCoords[1])],
[Math.max(depCoords[0], destCoords[0]), Math.max(depCoords[1], destCoords[1])]
];
// Initialiser le protocole PMTiles
const protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
// Fonction pour créer une carte dans un conteneur donné
function createMapInContainer(containerId) {
const container = document.getElementById(containerId);
if (!container) return null;
const map = new maplibregl.Map({
container: containerId,
style: '/maps/protomaps-light.json',
bounds: bounds,
fitBoundsOptions: { padding: 50 }
});
map.addControl(new maplibregl.NavigationControl(), 'top-right');
// Marqueur départ (secondary/vert)
const departureEl = document.createElement('div');
departureEl.className = 'map-marker map-marker-departure';
departureEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18" height="18">
<path fill-rule="evenodd" d="M11.54 22.351l.07.04.028.016a.76.76 0 00.723 0l.028-.015.071-.041a16.975 16.975 0 001.144-.742 19.58 19.58 0 002.683-2.282c1.944-1.99 3.963-4.98 3.963-8.827a8.25 8.25 0 00-16.5 0c0 3.846 2.02 6.837 3.963 8.827a19.58 19.58 0 002.682 2.282 16.975 16.975 0 001.145.742zM12 13.5a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" />
</svg>
`;
new maplibregl.Marker({ element: departureEl })
.setLngLat(depCoords)
.setPopup(new maplibregl.Popup().setHTML(`<strong>Départ</strong><br>${departure.properties.label}`))
.addTo(map);
// Marqueur arrivée (highlight/orange)
const arrivalEl = document.createElement('div');
arrivalEl.className = 'map-marker map-marker-arrival';
arrivalEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18" height="18">
<path fill-rule="evenodd" d="M3 2.25a.75.75 0 01.75.75v.54l1.838-.46a9.75 9.75 0 016.725.738l.108.054a8.25 8.25 0 005.58.652l3.109-.732a.75.75 0 01.917.81 47.784 47.784 0 00.005 10.337.75.75 0 01-.574.812l-3.114.733a9.75 9.75 0 01-6.594-.77l-.108-.054a8.25 8.25 0 00-5.69-.625l-2.202.55V21a.75.75 0 01-1.5 0V3A.75.75 0 013 2.25z" clip-rule="evenodd" />
</svg>
`;
new maplibregl.Marker({ element: arrivalEl })
.setLngLat(destCoords)
.setPopup(new maplibregl.Popup().setHTML(`<strong>Arrivée</strong><br>${destination.properties.label}`))
.addTo(map);
return map;
}
// Déterminer si on est en mobile
const isMobile = window.matchMedia('(max-width: 1024px)').matches;
// Créer la carte dans le bon conteneur
if (isMobile) {
// En mobile, créer uniquement la carte mobile
const mobileMap = createMapInContainer('mobile-map');
if (mobileMap) {
window.parcoursmobMap = mobileMap;
}
} else {
// En desktop, créer uniquement la carte desktop
const desktopMap = createMapInContainer('map');
if (desktopMap) {
window.parcoursmobMap = desktopMap;
}
}
// Écouter les changements de taille d'écran pour recréer la carte si nécessaire
const mediaQuery = window.matchMedia('(max-width: 1024px)');
mediaQuery.addEventListener('change', (e) => {
// Supprimer l'ancienne carte
if (window.parcoursmobMap) {
window.parcoursmobMap.remove();
window.parcoursmobMap = null;
}
// Créer la nouvelle carte dans le bon conteneur
if (e.matches) {
const mobileMap = createMapInContainer('mobile-map');
if (mobileMap) {
window.parcoursmobMap = mobileMap;
}
} else {
const desktopMap = createMapInContainer('map');
if (desktopMap) {
window.parcoursmobMap = desktopMap;
}
}
});
}
</script>