parcoursmob-default-theme/web/layouts/journeys/search-compact.html

1766 lines
100 KiB
HTML

{{define "content"}}
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 py-8">
<div class="bg-white shadow rounded-lg overflow-hidden" style="height: calc(100vh - 12rem);" x-data="compactJourneySearch()">
<!-- Top Bar with Search Form -->
<div class="border-b border-gray-200 p-4">
<form method="GET" class="flex items-end">
<div class="flex-1" x-data='{
input: {{if .ViewState.departure}}"{{.ViewState.departure.Properties.label}}"{{else}}null{{end}},
address: {{if .ViewState.departure}}JSON.stringify({{template "geojson" .ViewState.departure}}){{else}}null{{end}},
addressObject: {{if .ViewState.departure}}{{template "geojson" .ViewState.departure }}{{else}}null{{end}},
responselength: 0,
async autocomplete() {
if(this.input == null || this.input == "") {
this.responselength = 0
return []
}
if(this.addressObject != null && this.input == this.addressObject.properties.label) {
this.responselength = 0
return []
}
if(this.input.length < 3) {
this.responselength = 0
return []
}
result = await fetch("https://api-adresse.data.gouv.fr/search/?q=" + this.input)
json = await result.json()
this.responselength = json["features"].length
return json["features"]
},
select(a) {
this.address = JSON.stringify(a)
this.addressObject = a
this.input = a.properties.label
}
}' class="relative">
<input type="hidden" name="departure" x-model="address">
<label for="departure-compact" class="block text-sm font-medium text-gray-700 mb-1">Départ</label>
<input type="text"
id="departure-compact"
class="p-2 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm border-gray-300 rounded-l-lg"
x-model="input"
placeholder="Adresse de départ">
<ul x-show="responselength > 0"
class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-xl py-1 text-base overflow-auto focus:outline-none sm:text-sm" tabindex="-1">
<template x-for="item in autocomplete">
<a href="#">
<li class="text-gray-900 hover:bg-gray-200 cursor-default select-none relative py-2 pl-3 pr-9"
@click="select(item)">
<span class="font-normal block truncate" x-text="item.properties.label"></span>
</li>
</a>
</template>
</ul>
</div>
<div class="flex-1" x-data='{
input: {{if .ViewState.destination}}"{{.ViewState.destination.Properties.label}}"{{else}}null{{end}},
address: {{if .ViewState.destination}}JSON.stringify({{template "geojson" .ViewState.destination}}){{else}}null{{end}},
addressObject: {{if .ViewState.destination}}{{template "geojson" .ViewState.destination }}{{else}}null{{end}},
responselength: 0,
async autocomplete() {
if(this.input == null || this.input == "") {
this.responselength = 0
return []
}
if(this.addressObject != null && this.input == this.addressObject.properties.label) {
this.responselength = 0
return []
}
if(this.input.length < 3) {
this.responselength = 0
return []
}
result = await fetch("https://api-adresse.data.gouv.fr/search/?q=" + this.input)
json = await result.json()
this.responselength = json["features"].length
return json["features"]
},
select(a) {
this.address = JSON.stringify(a)
this.addressObject = a
this.input = a.properties.label
}
}' class="relative">
<input type="hidden" name="destination" x-model="address">
<label for="destination-compact" class="block text-sm font-medium text-gray-700 mb-1">Destination</label>
<input type="text"
id="destination-compact"
class="p-2 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm border-gray-300 rounded-none"
x-model="input"
placeholder="Adresse de destination">
<ul x-show="responselength > 0"
class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-xl py-1 text-base overflow-auto focus:outline-none sm:text-sm" tabindex="-1">
<template x-for="item in autocomplete">
<a href="#">
<li class="text-gray-900 hover:bg-gray-200 cursor-default select-none relative py-2 pl-3 pr-9"
@click="select(item)">
<span class="font-normal block truncate" x-text="item.properties.label"></span>
</li>
</a>
</template>
</ul>
</div>
<div class="w-40">
<label for="departuredate-compact" class="block text-sm font-medium text-gray-700 mb-1">Date</label>
<input type="date"
id="departuredate-compact"
name="departuredate"
value="{{.ViewState.departuredate}}"
class="p-2 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm border-gray-300 rounded-none">
</div>
<div class="w-32">
<label for="departuretime-compact" class="block text-sm font-medium text-gray-700 mb-1">Heure</label>
<input type="time"
id="departuretime-compact"
name="departuretime"
value="{{.ViewState.departuretime}}"
class="p-2 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm border-gray-300 rounded-none">
</div>
<button type="submit"
class="px-4 py-2 border border-transparent text-sm font-medium rounded-r-lg text-white bg-co-blue hover:bg-co-blue-dark focus:outline-none">
Rechercher
</button>
</form>
{{if .ViewState.searched}}
<div class="mt-2 flex items-center justify-between gap-4">
<!-- Filtres -->
<div class="flex-1 flex items-center gap-4 text-sm">
<span class="text-gray-700 font-medium">Filtres:</span>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="filters.transit" class="rounded border-gray-300 text-co-blue focus:ring-co-blue">
<span class="ml-2 text-gray-600">Transports en commun</span>
</label>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="filters.solidarity" class="rounded border-gray-300 text-co-blue focus:ring-co-blue">
<span class="ml-2 text-gray-600">Transport solidaire</span>
</label>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="filters.organizedCarpool" class="rounded border-gray-300 text-co-blue focus:ring-co-blue">
<span class="ml-2 text-gray-600">Covoiturage solidaire</span>
</label>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="filters.carpool" class="rounded border-gray-300 text-co-blue focus:ring-co-blue">
<span class="ml-2 text-gray-600">Covoiturage</span>
</label>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="filters.kb" class="rounded border-gray-300 text-co-blue focus:ring-co-blue">
<span class="ml-2 text-gray-600">Solutions locales</span>
</label>
</div>
<!-- Bouton Enregistrer -->
<a href="/app/journeys/save?departure={{json .ViewState.departure}}&destination={{json .ViewState.destination}}&departuredate={{.ViewState.departuredate}}&departuretime={{.ViewState.departuretime}}{{if ne .ViewState.passengerid ""}}&passengerid={{.ViewState.passengerid}}{{end}}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-2xl text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue flex-shrink-0">
{{$.IconSet.Icon "hero:outline/bookmark" "h-4 w-4 mr-2"}}
Enregistrer pour plus tard
</a>
</div>
{{end}}
</div>
<!-- Main Content: Results List (Left) + Map (Right) -->
<div class="flex overflow-hidden" style="height: calc(100% - 11rem);">
<!-- Results List (Left Side) -->
<div class="w-96 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
{{if .ViewState.searched}}
<div class="divide-y divide-gray-200">
<!-- Transit Results -->
{{range $index, $journey := .ViewState.journeys}}
<div x-show="filters.transit" @click="selectSolution('transit', {{$index}})"
:class="selectedType === 'transit' && selectedIndex === {{$index}} ? 'bg-blue-50 border-l-4 border-co-blue' : 'hover:bg-gray-50'"
class="p-4 cursor-pointer transition-colors">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="text-sm font-semibold text-gray-900">{{ timeFormat $journey.StartTime "15:04" }}</span>
<span class="text-gray-400"></span>
<span class="text-sm font-semibold text-gray-900">{{ timeFormat $journey.EndTime "15:04" }}</span>
</div>
<span class="text-xs font-medium text-gray-600">{{ shortDuration $journey.Duration }}</span>
</div>
<div class="flex items-center gap-1 flex-wrap">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
{{$.IconSet.Icon "tabler-icons:bus" "h-3 w-3"}}
<span class="ml-1">Transport</span>
</span>
{{range $leg := $journey.Legs}}
{{if or (eq $leg.Mode "BUS") (eq $leg.Mode "COACH")}}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold"
style="background-color: #{{$leg.RouteColor}}; color: #{{$leg.RouteTextColor}}">
{{$leg.RouteShortName}}
</span>
{{else if or (eq $leg.Mode "REGIONAL_FAST_RAIL") (eq $leg.Mode "REGIONAL_RAIL")}}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold"
style="background-color: #{{$leg.RouteColor}}; color: #{{$leg.RouteTextColor}}">
{{$leg.RouteShortName}}
</span>
{{end}}
{{end}}
</div>
</div>
{{end}}
<!-- Solidarity Transport Results -->
{{if .ViewState.driver_journeys}}
<div x-show="filters.solidarity" @click="selectSolution('solidarity', 0)"
:class="selectedType === 'solidarity' ? 'bg-blue-50 border-l-4 border-co-lightblue' : 'hover:bg-gray-50'"
class="p-4 cursor-pointer transition-colors">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-semibold text-gray-900">Transport solidaire</span>
<span class="text-xs font-medium bg-blue-50 text-co-lightblue px-2 py-1 rounded-full">
{{len .ViewState.driver_journeys}} conducteur{{if gt (len .ViewState.driver_journeys) 1}}s{{end}}
</span>
</div>
<div class="flex items-center gap-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-co-lightblue">
{{$.IconSet.Icon "tabler-icons:car" "h-3 w-3"}}
<span class="ml-1">Conducteurs disponibles</span>
</span>
</div>
</div>
{{end}}
<!-- Organized Carpool Results -->
{{if .ViewState.organized_carpools}}
<div x-show="filters.organizedCarpool" @click="selectSolution('organized_carpool', 0)"
:class="selectedType === 'organized_carpool' ? 'bg-blue-50 border-l-4 border-co-blue' : 'hover:bg-gray-50'"
class="p-4 cursor-pointer transition-colors">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-semibold text-gray-900">Covoiturage solidaire</span>
<span class="text-xs font-medium bg-blue-50 text-co-blue px-2 py-1 rounded-full">
{{len .ViewState.organized_carpools}} trajet{{if gt (len .ViewState.organized_carpools) 1}}s{{end}}
</span>
</div>
<div class="flex items-center gap-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
{{$.IconSet.Icon "tabler-icons:users" "h-3 w-3"}}
<span class="ml-1">Trajets disponibles</span>
</span>
</div>
</div>
{{end}}
<!-- Carpool Operator Results -->
{{range $carpoolIndex, $carpoolFC := .ViewState.carpools}}
{{$carpoolData := index $carpoolFC.ExtraMembers "ocss"}}
<div x-show="filters.carpool" @click="selectSolution('carpool', {{$carpoolIndex}})"
:class="selectedType === 'carpool' && selectedIndex === {{$carpoolIndex}} ? 'bg-gray-50 border-l-4 border-co-blue' : 'hover:bg-gray-50'"
class="p-4 cursor-pointer transition-colors">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="text-sm font-semibold text-gray-900">{{$carpoolData.Driver.Alias}}</span>
</div>
<template x-if="carpools[{{$carpoolIndex}}] && carpools[{{$carpoolIndex}}].price">
<span class="text-xs font-medium text-gray-600" x-text="carpools[{{$carpoolIndex}}].price.toFixed(2) + '€'"></span>
</template>
</div>
{{if or $carpoolData.PassengerPickupAddress $carpoolData.PassengerDropAddress}}
<div class="text-xs text-gray-600 mb-2">
{{if $carpoolData.PassengerPickupAddress}}
<div class="flex items-start gap-1">
{{$.IconSet.Icon "hero:outline/map-pin" "h-3 w-3 mt-0.5 flex-shrink-0"}}
<span>{{$carpoolData.PassengerPickupAddress}}</span>
</div>
{{end}}
{{if $carpoolData.PassengerDropAddress}}
<div class="flex items-start gap-1 mt-1">
{{$.IconSet.Icon "hero:outline/flag" "h-3 w-3 mt-0.5 flex-shrink-0"}}
<span>{{$carpoolData.PassengerDropAddress}}</span>
</div>
{{end}}
</div>
{{end}}
<div class="flex items-center gap-1 flex-wrap">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-co-blue">
{{$.IconSet.Icon "tabler-icons:car" "h-3 w-3"}}
<span class="ml-1">Covoiturage</span>
</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
{{$carpoolData.Operator}}
</span>
{{if $carpoolData.AvailableSteats}}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
{{$carpoolData.AvailableSteats}} place(s)
</span>
{{end}}
</div>
</div>
{{end}}
<!-- Knowledge Base Results -->
{{range $index, $solution := .ViewState.kb_data}}
<div x-show="filters.kb" @click="selectSolution('kb', {{$index}})"
:class="selectedType === 'kb' && selectedIndex === {{$index}} ? 'bg-yellow-50 border-l-4 border-co-orange' : 'hover:bg-gray-50'"
class="p-4 cursor-pointer transition-colors">
<div class="mb-2">
<span class="text-sm font-semibold text-gray-900">{{if $solution.title}}{{$solution.title}}{{else if $solution.name}}{{$solution.name}}{{else}}Solution locale{{end}}</span>
</div>
<div class="flex items-center gap-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-50 text-co-orange">
{{$.IconSet.Icon "hero:outline/map" "h-3 w-3"}}
<span class="ml-1">Solution locale</span>
</span>
</div>
</div>
{{end}}
</div>
{{else}}
<div class="p-8 text-center text-gray-500">
<p>Effectuez une recherche pour voir les résultats</p>
</div>
{{end}}
</div>
<!-- Solidarity Transport Driver List (Middle Column) -->
<template x-if="selectedType === 'solidarity'">
<div x-transition
class="w-80 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
<div class="p-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-sm font-semibold text-gray-900">Conducteurs disponibles</h3>
</div>
<div class="divide-y divide-gray-200">
<template x-for="(journey, idx) in solidarityJourneys" :key="idx">
<div @click="selectDriver(idx)"
:class="selectedDriverIndex === idx ? 'bg-blue-50 border-l-4 border-co-lightblue' : 'hover:bg-gray-50'"
class="p-4 cursor-pointer transition-colors">
<div class="mb-2">
<span class="text-sm font-semibold text-gray-900" x-text="journey.driverName"></span>
</div>
<div class="space-y-1 text-xs text-gray-600">
<div class="flex items-center gap-2">
<span>Distance conducteur:</span>
<span class="font-medium" x-text="journey.driverDistance + ' km'"></span>
</div>
<div class="flex items-center gap-2">
<span>Distance passager:</span>
<span class="font-medium" x-text="journey.passengerDistance + ' km'"></span>
</div>
<div x-show="journey.duration > 0" class="flex items-center gap-2">
<span>Durée:</span>
<span class="font-medium" x-text="Math.round(journey.duration / 60) + ' min'"></span>
</div>
<div class="flex items-center gap-2">
<span>Profil validé:</span>
<template x-if="journey.profileValidated">
<span class="p-1 px-2 text-xs bg-co-green text-white rounded-2xl">Oui</span>
</template>
<template x-if="!journey.profileValidated">
<span class="p-1 px-2 text-xs bg-co-red text-white rounded-2xl">Non</span>
</template>
</div>
<div x-show="journey.comment" class="pt-1">
<span class="italic text-gray-500" x-text="journey.comment"></span>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- Transit Journey Details (Middle Column) -->
<template x-if="selectedType === 'transit' && selectedIndex !== null">
<div x-transition
class="w-96 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
<template x-if="selectedIndex !== null && transitJourneys[selectedIndex]">
<div>
<!-- Header -->
<div class="p-4 border-b border-gray-200 bg-co-blue">
<div class="flex items-center justify-between text-white">
<div class="flex items-center gap-3">
<div class="text-2xl font-bold" x-text="transitJourneys[selectedIndex].startTime"></div>
<div class="text-lg"></div>
<div class="text-2xl font-bold" x-text="transitJourneys[selectedIndex].endTime"></div>
</div>
<div class="text-sm font-medium text-white bg-co-lightblue px-3 py-1 rounded-full" x-text="transitJourneys[selectedIndex].duration"></div>
</div>
</div>
<!-- Journey Steps -->
<div class="p-4 space-y-3">
<template x-for="(leg, idx) in transitJourneys[selectedIndex].detailedLegs" :key="idx">
<div>
<!-- Walk Leg -->
<template x-if="leg.mode === 'WALK' && leg.distance">
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
<div class="flex-shrink-0 w-10 h-10 rounded-co bg-gray-300 flex items-center justify-center">
{{$.IconSet.Icon "tabler-icons:walk" "h-5 w-5 stroke-gray-700"}}
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900">Marche à pied</div>
<div class="text-xs text-gray-600" x-text="leg.distance + ' mètres'"></div>
</div>
</div>
</template>
<!-- Bus Leg -->
<template x-if="leg.mode === 'BUS' || leg.mode === 'COACH'">
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow">
<!-- Route Header -->
<div class="p-3 flex items-center gap-3 bg-gray-50">
<div class="flex-shrink-0 w-10 h-10 rounded-co flex items-center justify-center"
:style="'background-color: #' + leg.color + '; color: #' + leg.textColor">
{{$.IconSet.Icon "tabler-icons:bus" "h-6 w-6"}}
</div>
<div class="flex-1 min-w-0">
<div class="text-lg font-bold text-gray-900" x-text="'Ligne ' + leg.routeShortName"></div>
<div class="text-xs text-gray-600" x-text="leg.agencyName"></div>
</div>
</div>
<!-- Route Details -->
<div class="p-3 space-y-2 bg-gray-50">
<!-- Times -->
<div class="flex items-center justify-between text-sm">
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-900" x-text="leg.startTime"></span>
<span class="text-gray-500">départ</span>
</div>
<div class="flex items-center gap-2">
<span class="text-gray-500">arrivée</span>
<span class="font-semibold text-gray-900" x-text="leg.endTime"></span>
</div>
</div>
<!-- Stops -->
<div class="text-xs text-gray-600">
<div class="flex items-start gap-2 mb-1">
<div class="w-2 h-2 rounded-full bg-co-green mt-1 flex-shrink-0"></div>
<span class="font-medium" x-text="leg.fromName"></span>
</div>
<div class="flex items-start gap-2">
<div class="w-2 h-2 rounded-full bg-co-red mt-1 flex-shrink-0"></div>
<span class="font-medium" x-text="leg.toName"></span>
</div>
</div>
<!-- Direction -->
<div x-show="leg.headsign" class="text-xs text-gray-500 flex items-center gap-1 pt-1 border-t border-gray-200">
<span>Direction</span>
<span class="font-medium text-gray-700" x-text="leg.headsign"></span>
</div>
</div>
</div>
</template>
<!-- Rail Leg -->
<template x-if="leg.mode === 'REGIONAL_FAST_RAIL' || leg.mode === 'REGIONAL_RAIL'">
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow">
<!-- Route Header -->
<div class="p-3 flex items-center gap-3 bg-gray-50">
<div class="flex-shrink-0 w-10 h-10 rounded-co flex items-center justify-center"
:style="'background-color: #' + leg.color + '; color: #' + leg.textColor">
{{$.IconSet.Icon "tabler-icons:train" "h-6 w-6"}}
</div>
<div class="flex-1 min-w-0">
<div class="text-lg font-bold text-gray-900" x-text="'TER ' + leg.routeShortName"></div>
<div class="text-xs text-gray-600" x-text="leg.agencyName"></div>
</div>
</div>
<!-- Route Details -->
<div class="p-3 space-y-2 bg-gray-50">
<!-- Times -->
<div class="flex items-center justify-between text-sm">
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-900" x-text="leg.startTime"></span>
<span class="text-gray-500">départ</span>
</div>
<div class="flex items-center gap-2">
<span class="text-gray-500">arrivée</span>
<span class="font-semibold text-gray-900" x-text="leg.endTime"></span>
</div>
</div>
<!-- Stops -->
<div class="text-xs text-gray-600">
<div class="flex items-start gap-2 mb-1">
<div class="w-2 h-2 rounded-full bg-co-green mt-1 flex-shrink-0"></div>
<span class="font-medium" x-text="leg.fromName"></span>
</div>
<div class="flex items-start gap-2">
<div class="w-2 h-2 rounded-full bg-co-red mt-1 flex-shrink-0"></div>
<span class="font-medium" x-text="leg.toName"></span>
</div>
</div>
<!-- Direction -->
<div x-show="leg.headsign" class="text-xs text-gray-500 flex items-center gap-1 pt-1 border-t border-gray-200">
<span>Direction</span>
<span class="font-medium text-gray-700" x-text="leg.headsign"></span>
</div>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
<!-- Organized Carpool List (Middle Column) -->
<template x-if="selectedType === 'organized_carpool'">
<div x-transition
class="w-80 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
<div class="p-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-sm font-semibold text-gray-900">Trajets disponibles</h3>
</div>
<div class="divide-y divide-gray-200">
<template x-for="(carpool, idx) in organizedCarpools" :key="idx">
<div @click="selectOrganizedCarpool(idx)"
:class="selectedOrganizedCarpoolIndex === idx ? 'bg-blue-50 border-l-4 border-co-blue' : 'hover:bg-gray-50'"
class="p-4 cursor-pointer transition-colors">
<div class="mb-2">
<span class="text-sm font-semibold text-gray-900" x-text="carpool.driverName"></span>
</div>
<div class="space-y-1 text-xs text-gray-600">
<div x-show="carpool.driverDepartureAddress" class="flex items-start gap-2">
<span class="flex-shrink-0">Départ conducteur:</span>
<span class="font-medium" x-text="carpool.driverDepartureAddress"></span>
</div>
<div x-show="carpool.driverArrivalAddress" class="flex items-start gap-2">
<span class="flex-shrink-0">Arrivée conducteur:</span>
<span class="font-medium" x-text="carpool.driverArrivalAddress"></span>
</div>
<div x-show="carpool.pickupDate" class="flex items-center gap-2">
<span>Date et heure:</span>
<span class="font-medium" x-text="carpool.pickupDate"></span>
</div>
<div class="flex items-center gap-2">
<span>Profil validé:</span>
<template x-if="carpool.profileValidated">
<span class="p-1 px-2 text-xs bg-co-green text-white rounded-2xl">Oui</span>
</template>
<template x-if="!carpool.profileValidated">
<span class="p-1 px-2 text-xs bg-co-red text-white rounded-2xl">Non</span>
</template>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- Organized Carpool Details (Third Column) -->
<template x-if="selectedType === 'organized_carpool' && selectedOrganizedCarpoolIndex !== null">
<div x-transition
class="w-96 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
<template x-if="selectedOrganizedCarpoolIndex !== null && organizedCarpools[selectedOrganizedCarpoolIndex]">
<div>
<!-- Header -->
<div class="p-4 border-b border-gray-200 bg-co-blue">
<div class="text-white">
<div class="text-xl font-bold mb-2" x-text="organizedCarpools[selectedOrganizedCarpoolIndex].driverName"></div>
<div class="text-sm" x-text="'Trajet du ' + organizedCarpools[selectedOrganizedCarpoolIndex].pickupDate"></div>
</div>
</div>
<!-- Journey Details -->
<div class="p-4 space-y-4">
<!-- Driver Info -->
<div class="bg-blue-50 rounded-lg p-3 border border-blue-200">
<div class="text-xs font-semibold text-gray-700 mb-2">Conducteur</div>
<div class="space-y-2 text-sm">
<div>
<span class="text-gray-600">Nom:</span>
<span class="font-medium ml-2" x-text="organizedCarpools[selectedOrganizedCarpoolIndex].driverName"></span>
</div>
<div class="flex items-center gap-2">
<span class="text-gray-600">Profil validé:</span>
<template x-if="organizedCarpools[selectedOrganizedCarpoolIndex].profileValidated">
<span class="p-1 px-2 text-xs bg-co-green text-white rounded-2xl">Oui</span>
</template>
<template x-if="!organizedCarpools[selectedOrganizedCarpoolIndex].profileValidated">
<span class="p-1 px-2 text-xs bg-co-red text-white rounded-2xl">Non</span>
</template>
</div>
</div>
</div>
<!-- Locations -->
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 w-8 h-8 rounded-co bg-co-blue flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="text-xs text-gray-500">Départ conducteur</div>
<div class="text-sm font-medium text-gray-900" x-text="organizedCarpools[selectedOrganizedCarpoolIndex].driverDepartureAddress"></div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 w-8 h-8 rounded-co bg-co-green flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="14" height="14">
<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>
</div>
<div class="flex-1 min-w-0">
<div class="text-xs text-gray-500">Prise en charge passager</div>
<div class="text-sm font-medium text-gray-900" x-text="'Coordonnées: ' + organizedCarpools[selectedOrganizedCarpoolIndex].departureLocation[1].toFixed(5) + ', ' + organizedCarpools[selectedOrganizedCarpoolIndex].departureLocation[0].toFixed(5)"></div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 w-8 h-8 rounded-co bg-co-red flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="14" height="14">
<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>
</div>
<div class="flex-1 min-w-0">
<div class="text-xs text-gray-500">Dépose passager</div>
<div class="text-sm font-medium text-gray-900" x-text="'Coordonnées: ' + organizedCarpools[selectedOrganizedCarpoolIndex].arrivalLocation[1].toFixed(5) + ', ' + organizedCarpools[selectedOrganizedCarpoolIndex].arrivalLocation[0].toFixed(5)"></div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 w-8 h-8 rounded-co bg-co-blue flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="text-xs text-gray-500">Arrivée conducteur</div>
<div class="text-sm font-medium text-gray-900" x-text="organizedCarpools[selectedOrganizedCarpoolIndex].driverArrivalAddress"></div>
</div>
</div>
</div>
<!-- Action Button -->
<div class="pt-4 border-t border-gray-200">
<a :href="'/app/solidarity-transport/drivers/' + organizedCarpools[selectedOrganizedCarpoolIndex].driverId + '/journeys/' + organizedCarpools[selectedOrganizedCarpoolIndex].id + '{{if ne .ViewState.passengerid ""}}?passengerid={{.ViewState.passengerid}}{{end}}'"
class="block w-full text-center bg-co-blue text-white px-4 py-2 rounded-2xl hover:bg-blue-700 transition-colors">
Organiser le covoiturage solidaire
</a>
</div>
</div>
</div>
</template>
</div>
</template>
<!-- Knowledge Base Solution Details (Middle Column) -->
<template x-if="selectedType === 'kb' && selectedIndex !== null">
<div x-transition
class="w-80 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
<template x-if="selectedIndex !== null && kbSolutions[selectedIndex]">
<div>
<div class="p-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-sm font-semibold text-gray-900" x-text="kbSolutions[selectedIndex].title"></h3>
</div>
<div class="p-4">
<div x-show="kbSolutions[selectedIndex].description" class="mb-4">
<p class="text-sm text-gray-700" x-text="kbSolutions[selectedIndex].description"></p>
</div>
<div x-show="kbSolutions[selectedIndex].url" class="mb-4">
<a :href="kbSolutions[selectedIndex].url"
target="_blank"
class="text-sm text-co-blue hover:underline">
Voir plus →
</a>
</div>
<div x-show="kbSolutions[selectedIndex].geography && kbSolutions[selectedIndex].geography.length > 0">
<h4 class="text-xs font-semibold text-gray-700 mb-2">Zone couverte</h4>
<div class="space-y-1">
<template x-for="geo in kbSolutions[selectedIndex].geography">
<div class="text-xs text-gray-600">
<span x-text="geo.layer"></span>: <span class="font-medium" x-text="geo.name ? geo.name + ' (' + geo.code + ')' : geo.code"></span>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
<!-- Carpool Details (Third Column) -->
<template x-if="selectedType === 'carpool' && selectedIndex !== null">
<div x-transition
class="w-96 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
<template x-if="selectedIndex !== null && carpools[selectedIndex]">
<div>
<!-- Header -->
<div class="p-4 border-b border-gray-200 bg-co-blue">
<div class="text-white">
<div class="text-xl font-bold mb-2" x-text="carpools[selectedIndex].driverAlias"></div>
<div class="text-sm" x-text="carpools[selectedIndex].operator"></div>
</div>
</div>
<!-- Journey Details -->
<div class="p-4 space-y-4">
<!-- Price -->
<template x-if="carpools[selectedIndex].price">
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
<div class="text-xs font-semibold text-gray-700 mb-1">Prix</div>
<div class="text-2xl font-bold text-co-blue" x-text="carpools[selectedIndex].price.toFixed(2) + ' €'"></div>
<template x-if="carpools[selectedIndex].seats">
<div class="text-xs text-gray-600 mt-1" x-text="carpools[selectedIndex].seats + ' place' + (carpools[selectedIndex].seats > 1 ? 's' : '') + ' disponible' + (carpools[selectedIndex].seats > 1 ? 's' : '')"></div>
</template>
</div>
</template>
<!-- Locations -->
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 w-8 h-8 rounded-co bg-co-green flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="14" height="14">
<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>
</div>
<div class="flex-1 min-w-0">
<div class="text-xs text-gray-500">Départ</div>
<div class="text-sm font-medium text-gray-900" x-text="carpools[selectedIndex].pickupAddress || 'Adresse non disponible'"></div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 w-8 h-8 rounded-co bg-co-red flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="14" height="14">
<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>
</div>
<div class="flex-1 min-w-0">
<div class="text-xs text-gray-500">Arrivée</div>
<div class="text-sm font-medium text-gray-900" x-text="carpools[selectedIndex].dropAddress || 'Adresse non disponible'"></div>
</div>
</div>
</div>
<!-- Distance & Duration -->
<div class="grid grid-cols-2 gap-3">
<template x-if="carpools[selectedIndex].distance">
<div class="bg-gray-50 rounded-lg p-3">
<div class="text-xs text-gray-500 mb-1">Distance</div>
<div class="text-sm font-semibold text-gray-900" x-text="(carpools[selectedIndex].distance / 1000).toFixed(1) + ' km'"></div>
</div>
</template>
<template x-if="carpools[selectedIndex].duration">
<div class="bg-gray-50 rounded-lg p-3">
<div class="text-xs text-gray-500 mb-1">Durée</div>
<div class="text-sm font-semibold text-gray-900" x-text="Math.floor(carpools[selectedIndex].duration / 60) + ' min'"></div>
</div>
</template>
</div>
<!-- Action Button -->
<template x-if="carpools[selectedIndex].webUrl">
<div class="pt-4 border-t border-gray-200">
<a :href="carpools[selectedIndex].webUrl"
target="_blank"
class="block w-full text-center bg-co-blue text-white px-4 py-2 rounded-2xl hover:bg-co-darkblue transition-colors">
<span>Voir l'offre sur </span><span x-text="carpools[selectedIndex].operator"></span>
</a>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
<!-- Map (Right Side) -->
<div class="flex-1 bg-gray-100 relative">
<!-- Driver Detail Panel -->
<div x-show="selectedType === 'solidarity' && selectedDriverIndex !== null"
x-transition
class="absolute top-4 left-4 right-4 bg-white rounded-lg shadow-lg p-4 z-10 max-w-md">
<template x-if="selectedDriverIndex !== null && solidarityJourneys[selectedDriverIndex]">
<div>
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-gray-900" x-text="solidarityJourneys[selectedDriverIndex].driverName"></h3>
<button @click="selectedDriverIndex = null" class="text-gray-400 hover:text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="space-y-2 text-sm">
<div class="flex items-center gap-2">
<span class="text-gray-500">Distance conducteur:</span>
<span class="font-medium text-gray-900" x-text="solidarityJourneys[selectedDriverIndex].driverDistance + ' km'"></span>
</div>
<div class="flex items-center gap-2">
<span class="text-gray-500">Distance passager:</span>
<span class="font-medium text-gray-900" x-text="solidarityJourneys[selectedDriverIndex].passengerDistance + ' km'"></span>
</div>
<div class="flex items-center gap-2" x-show="solidarityJourneys[selectedDriverIndex].duration > 0">
<span class="text-gray-500">Durée:</span>
<span class="font-medium text-gray-900" x-text="Math.round(solidarityJourneys[selectedDriverIndex].duration / 60) + ' min'"></span>
</div>
<div class="flex items-center gap-2">
<span class="text-gray-500">Profil validé:</span>
<template x-if="solidarityJourneys[selectedDriverIndex].profileValidated">
<span class="p-1 px-2 text-xs bg-co-green text-white rounded-2xl">Oui</span>
</template>
<template x-if="!solidarityJourneys[selectedDriverIndex].profileValidated">
<span class="p-1 px-2 text-xs bg-co-red text-white rounded-2xl">Non</span>
</template>
</div>
<div x-show="solidarityJourneys[selectedDriverIndex].comment" class="pt-2 border-t border-gray-200">
<span class="text-gray-700 italic" x-text="solidarityJourneys[selectedDriverIndex].comment"></span>
</div>
</div>
<div class="mt-4">
<a :href="'/app/solidarity-transport/drivers/' + solidarityJourneys[selectedDriverIndex].driverId + '/journeys/' + solidarityJourneys[selectedDriverIndex].id"
class="block w-full text-center px-4 py-2 bg-co-blue text-white rounded-lg hover:bg-co-blue-dark transition-colors">
Organiser le transport solidaire
</a>
</div>
</div>
</template>
</div>
<div id="compact-journey-map" class="w-full h-full"></div>
</div>
</div>
</div>
</div>
<script>
function compactJourneySearch() {
const transitJourneys = [
{{range $journeyIndex, $journey := .ViewState.journeys}}
{
startTime: '{{ timeFormat $journey.StartTime "15:04" }}',
endTime: '{{ timeFormat $journey.EndTime "15:04" }}',
duration: '{{ shortDuration $journey.Duration }}',
legs: [
{{range $legIndex, $leg := $journey.Legs}}
{
mode: '{{$leg.Mode}}',
color: '{{$leg.RouteColor}}',
from: {{if and $leg.From $leg.From.Lon $leg.From.Lat}}[{{$leg.From.Lon}}, {{$leg.From.Lat}}]{{else}}null{{end}},
to: {{if and $leg.To $leg.To.Lon $leg.To.Lat}}[{{$leg.To.Lon}}, {{$leg.To.Lat}}]{{else}}null{{end}},
polyline: {{if $leg.LegGeometry}}'{{$leg.LegGeometry.Points}}'{{else}}null{{end}},
precision: {{if $leg.LegGeometry}}{{$leg.LegGeometry.Precision}}{{else}}6{{end}}
},
{{end}}
].filter(function(leg) { return leg.from && leg.to; }),
detailedLegs: [
{{range $legIndex, $leg := $journey.Legs}}
{
mode: '{{$leg.Mode}}',
color: '{{$leg.RouteColor}}',
textColor: '{{$leg.RouteTextColor}}',
distance: {{if $leg.Distance}}{{$leg.Distance}}{{else}}0{{end}},
duration: {{if $leg.Duration}}{{$leg.Duration}}{{else}}0{{end}},
agencyName: {{if $leg.AgencyName}}'{{$leg.AgencyName}}'{{else}}null{{end}},
routeShortName: {{if $leg.RouteShortName}}'{{$leg.RouteShortName}}'{{else}}null{{end}},
headsign: {{if $leg.Headsign}}'{{$leg.Headsign}}'{{else}}null{{end}},
startTime: '{{ timeFormat $leg.StartTime "15:04" }}',
endTime: '{{ timeFormat $leg.EndTime "15:04" }}',
fromName: {{if and $leg.From $leg.From.Name}}'{{$leg.From.Name}}'{{else}}null{{end}},
toName: {{if and $leg.To $leg.To.Name}}'{{$leg.To.Name}}'{{else}}null{{end}}
},
{{end}}
]
},
{{end}}
];
const solidarityJourneys = [
{{range $index, $driverJourney := .ViewState.driver_journeys}}
{{$driver := index $.ViewState.solidarity_drivers $driverJourney.DriverId}}
{
id: '{{$driverJourney.Id}}',
driverId: '{{$driverJourney.DriverId}}',
driverName: '{{$driver.Data.first_name}} {{$driver.Data.last_name}}',
driverDistance: {{$driverJourney.DriverDistance}},
passengerDistance: {{$driverJourney.PassengerDistance}},
duration: {{if $driverJourney.Duration}}{{$driverJourney.Duration}}{{else}}0{{end}},
polyline: {{if $driverJourney.JourneyPolyline}}'{{$driverJourney.JourneyPolyline}}'{{else}}null{{end}},
comment: {{if $driver.Data.other_properties}}{{if $driver.Data.other_properties.comment}}"{{$driver.Data.other_properties.comment}}"{{else}}null{{end}}{{else}}null{{end}},
profileValidated: {{solidarityDriverValidatedProfile $driver (solidarityDocuments $driver.ID)}},
driverLocation: {{if $driverJourney.DriverDeparture}}(function() {
try {
const f = JSON.parse('{{$driverJourney.DriverDeparture.Serialized}}');
return f.geometry && f.geometry.coordinates ? f.geometry.coordinates : null;
} catch(e) { return null; }
})(){{else}}null{{end}},
passengerPickup: {{if $driverJourney.PassengerPickup}}(function() {
try {
const f = JSON.parse('{{$driverJourney.PassengerPickup.Serialized}}');
return f.geometry && f.geometry.coordinates ? f.geometry.coordinates : null;
} catch(e) { return null; }
})(){{else}}null{{end}},
passengerDropoff: {{if $driverJourney.PassengerDrop}}(function() {
try {
const f = JSON.parse('{{$driverJourney.PassengerDrop.Serialized}}');
return f.geometry && f.geometry.coordinates ? f.geometry.coordinates : null;
} catch(e) { return null; }
})(){{else}}null{{end}},
driverDestination: {{if $driverJourney.DriverArrival}}(function() {
try {
const f = JSON.parse('{{$driverJourney.DriverArrival.Serialized}}');
return f.geometry && f.geometry.coordinates ? f.geometry.coordinates : null;
} catch(e) { return null; }
})(){{else}}null{{end}}
},
{{end}}
];
const organizedCarpools = [
{{range $index, $carpool := .ViewState.organized_carpools}}
{{$driver := index $.ViewState.solidarity_drivers $carpool.Driver.Id}}
{
id: '{{$carpool.Id}}',
driverId: '{{$carpool.Driver.Id}}',
driverName: '{{$driver.Data.first_name}} {{$driver.Data.last_name}}',
departureLocation: {{if and $carpool.PassengerPickupLng $carpool.PassengerPickupLat}}[{{$carpool.PassengerPickupLng}}, {{$carpool.PassengerPickupLat}}]{{else}}null{{end}},
arrivalLocation: {{if and $carpool.PassengerDropLng $carpool.PassengerDropLat}}[{{$carpool.PassengerDropLng}}, {{$carpool.PassengerDropLat}}]{{else}}null{{end}},
distance: {{if $carpool.Distance}}{{$carpool.Distance}}{{else}}0{{end}},
passengerPickupAddress: {{if $carpool.PassengerPickupAddress}}"{{$carpool.PassengerPickupAddress}}"{{else}}null{{end}},
passengerDropAddress: {{if $carpool.PassengerDropAddress}}"{{$carpool.PassengerDropAddress}}"{{else}}null{{end}},
driverDepartureAddress: {{if $carpool.DriverDepartureAddress}}"{{$carpool.DriverDepartureAddress}}"{{else}}null{{end}},
driverArrivalAddress: {{if $carpool.DriverArrivalAddress}}"{{$carpool.DriverArrivalAddress}}"{{else}}null{{end}},
pickupDate: {{if $carpool.PassengerPickupDate}}"{{$carpool.PassengerPickupDate.AsTime.Format "02/01/2006 15:04"}}"{{else}}null{{end}},
profileValidated: {{carpoolDriverValidatedProfile $driver (carpoolDocuments $driver.ID)}},
polyline: {{if $carpool.JourneyPolyline}}'{{$carpool.JourneyPolyline}}'{{else}}null{{end}},
driverDepartureLocation: {{if and $carpool.DriverDepartureLng $carpool.DriverDepartureLat}}[{{$carpool.DriverDepartureLng}}, {{$carpool.DriverDepartureLat}}]{{else}}null{{end}},
driverArrivalLocation: {{if and $carpool.DriverArrivalLng $carpool.DriverArrivalLat}}[{{$carpool.DriverArrivalLng}}, {{$carpool.DriverArrivalLat}}]{{else}}null{{end}}
},
{{end}}
];
const carpools = [
{{range $carpoolIndex, $carpoolFC := .ViewState.carpools}}
{{$carpoolData := index $carpoolFC.ExtraMembers "ocss"}}
{{$departure := index $carpoolFC.Features 0}}
{{$arrival := index $carpoolFC.Features 1}}
{
operator: '{{$carpoolData.Operator}}',
driverAlias: {{if $carpoolData.Driver}}'{{$carpoolData.Driver.Alias}}'{{else}}'Conducteur'{{end}},
pickupLocation: [{{$carpoolData.PassengerPickupLng}}, {{$carpoolData.PassengerPickupLat}}],
dropLocation: [{{$carpoolData.PassengerDropLng}}, {{$carpoolData.PassengerDropLat}}],
driverDepartureLocation: {{if and $carpoolData.DriverDepartureLng $carpoolData.DriverDepartureLat}}[{{$carpoolData.DriverDepartureLng}}, {{$carpoolData.DriverDepartureLat}}]{{else}}null{{end}},
driverArrivalLocation: {{if and $carpoolData.DriverArrivalLng $carpoolData.DriverArrivalLat}}[{{$carpoolData.DriverArrivalLng}}, {{$carpoolData.DriverArrivalLat}}]{{else}}null{{end}},
pickupAddress: {{if $carpoolData.PassengerPickupAddress}}'{{$carpoolData.PassengerPickupAddress}}'{{else}}null{{end}},
dropAddress: {{if $carpoolData.PassengerDropAddress}}'{{$carpoolData.PassengerDropAddress}}'{{else}}null{{end}},
distance: {{if $carpoolData.Distance}}{{$carpoolData.Distance}}{{else}}null{{end}},
duration: {{if $carpoolData.Duration}}{{$carpoolData.Duration.Seconds}}{{else}}null{{end}},
price: {{if and $carpoolData.Price $carpoolData.Price.Amount}}{{$carpoolData.Price.Amount}}{{else}}null{{end}},
seats: {{if $carpoolData.AvailableSteats}}{{$carpoolData.AvailableSteats}}{{else}}null{{end}},
webUrl: {{if $carpoolData.JourneySchedule}}{{if $carpoolData.JourneySchedule.WebUrl}}'{{$carpoolData.JourneySchedule.WebUrl}}'{{else}}null{{end}}{{else}}null{{end}},
polyline: {{if $carpoolData.JourneyPolyline}}'{{$carpoolData.JourneyPolyline}}'{{else}}null{{end}}
},
{{end}}
];
const kbSolutions = [
{{range $index, $solution := .ViewState.kb_data}}
{
title: {{if $solution.title}}"{{$solution.title}}"{{else if $solution.name}}"{{$solution.name}}"{{else}}"Solution locale"{{end}},
description: {{if $solution.description}}"{{$solution.description}}"{{else}}null{{end}},
url: {{if $solution.url}}"{{$solution.url}}"{{else}}null{{end}},
geography: {{if $solution.geography}}{{json $solution.geography}}{{else}}[]{{end}}
},
{{end}}
];
return {
selectedType: null,
selectedIndex: null,
selectedDriverIndex: null,
selectedOrganizedCarpoolIndex: null,
map: null,
startMarker: null,
endMarker: null,
routeMarkers: [],
transitJourneys: transitJourneys,
solidarityJourneys: solidarityJourneys,
organizedCarpools: organizedCarpools,
carpools: carpools,
kbSolutions: kbSolutions,
filters: {
transit: true,
solidarity: true,
organizedCarpool: true,
carpool: true,
kb: true
},
init() {
// Initialize map
if (typeof maplibregl !== 'undefined' && typeof pmtiles !== 'undefined') {
let protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
{{if and .ViewState.departure .ViewState.departure.Geometry}}
const departureFeature = {{json .ViewState.departure}};
const destinationFeature = {{if .ViewState.destination}}{{json .ViewState.destination}}{{else}}null{{end}};
this.map = new maplibregl.Map({
container: 'compact-journey-map',
style: '/public/maps/protomaps-light/style.json',
center: departureFeature.geometry.coordinates,
zoom: 12
});
this.map.on('load', () => {
// Create custom departure marker with map-pin from heroicons
const departureEl = document.createElement('div');
departureEl.className = 'w-8 h-8 rounded-co bg-co-green border border-white shadow-md flex items-center justify-center';
departureEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="16" height="16">
<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>
`;
this.startMarker = new maplibregl.Marker({element: departureEl})
.setLngLat(departureFeature.geometry.coordinates)
.addTo(this.map);
if (destinationFeature) {
// Create custom destination marker with flag from heroicons
const destinationEl = document.createElement('div');
destinationEl.className = 'w-8 h-8 rounded-co bg-co-red border border-white shadow-md flex items-center justify-center';
destinationEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="16" height="16">
<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>
`;
this.endMarker = new maplibregl.Marker({element: destinationEl})
.setLngLat(destinationFeature.geometry.coordinates)
.addTo(this.map);
// Fit bounds to show both markers
const bounds = new maplibregl.LngLatBounds()
.extend(departureFeature.geometry.coordinates)
.extend(destinationFeature.geometry.coordinates);
this.map.fitBounds(bounds, { padding: 50 });
}
});
{{else}}
this.map = new maplibregl.Map({
container: 'compact-journey-map',
style: '/public/maps/protomaps-light/style.json',
center: [2.3522, 48.8566],
zoom: 12
});
{{end}}
}
},
selectSolution(type, index) {
// If clicking on the already selected solution, close it
if (this.selectedType === type && this.selectedIndex === index) {
this.clearRoutes();
this.selectedType = null;
this.selectedIndex = null;
this.selectedDriverIndex = null;
this.selectedOrganizedCarpoolIndex = null;
return;
}
// Clear existing route layers first
this.clearRoutes();
// Reset selections when changing solution type
if (type !== 'solidarity') {
this.selectedDriverIndex = null;
}
if (type !== 'organized_carpool') {
this.selectedOrganizedCarpoolIndex = null;
}
// Update selection
this.selectedType = type;
this.selectedIndex = index;
// Use setTimeout to ensure clearing completes before displaying new route
setTimeout(() => {
// Display the selected solution on the map
if (type === 'transit') {
this.displayTransitRoute(index);
} else if (type === 'solidarity') {
this.selectedDriverIndex = null;
this.displaySolidarityRoute(index);
} else if (type === 'organized_carpool') {
this.selectedOrganizedCarpoolIndex = null;
this.displayOrganizedCarpoolRoute(index);
} else if (type === 'carpool') {
this.displayCarpoolRoute(index);
} else if (type === 'kb') {
this.displayKBSolution(index);
}
}, 500);
},
selectDriver(driverIndex) {
// Clear routes before selecting new driver
this.clearRoutes();
// Update driver selection
this.selectedDriverIndex = driverIndex;
// Use setTimeout to ensure clearing completes before displaying new route
setTimeout(() => {
this.displaySolidarityRoute(0);
}, 500);
},
selectOrganizedCarpool(carpoolIndex) {
// Clear routes before selecting new carpool
this.clearRoutes();
// Update carpool selection
this.selectedOrganizedCarpoolIndex = carpoolIndex;
// Use setTimeout to ensure clearing completes before displaying new route
setTimeout(() => {
this.displayOrganizedCarpoolRoute(carpoolIndex);
}, 500);
},
clearRoutes() {
// Remove route markers
this.routeMarkers.forEach(marker => marker.remove());
this.routeMarkers = [];
// Remove existing layers and sources
if (this.map && this.map.loaded()) {
// Create a copy of the layers array to avoid modification during iteration
const layers = [...this.map.getStyle().layers];
const layersToRemove = layers.filter(layer => layer.id.startsWith('route-'));
layersToRemove.forEach((layer) => {
if (this.map.getLayer(layer.id)) {
this.map.removeLayer(layer.id);
}
});
// Create a copy of the sources object keys
const sources = Object.keys(this.map.getStyle().sources);
const sourcesToRemove = sources.filter(sourceId => sourceId.startsWith('route-'));
sourcesToRemove.forEach((sourceId) => {
if (this.map.getSource(sourceId)) {
this.map.removeSource(sourceId);
}
});
}
},
displayTransitRoute(index) {
if (!this.map || !this.map.loaded() || !this.transitJourneys[index]) return;
const journey = this.transitJourneys[index];
const allCoords = [];
journey.legs.forEach((leg, legIndex) => {
const lineColor = leg.mode === 'WALK' ? '#9ca3af' : '#' + leg.color;
const lineWidth = leg.mode === 'WALK' ? 2 : 4;
const lineDasharray = leg.mode === 'WALK' ? [2, 2] : undefined;
let coordinates;
if (leg.polyline && typeof polyline !== 'undefined') {
// Decode polyline with the precision from the API response
const decoded = polyline.decode(leg.polyline, leg.precision);
// Convert from [lat, lon] to [lon, lat] for MapLibre
coordinates = decoded.map((coord) => [coord[1], coord[0]]);
coordinates.forEach((coord) => allCoords.push(coord));
} else {
// Fallback to straight line
coordinates = [leg.from, leg.to];
allCoords.push(leg.from);
allCoords.push(leg.to);
}
this.map.addSource('route-transit-' + index + '-' + legIndex, {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': coordinates
}
}
});
const layerConfig = {
'id': 'route-transit-' + index + '-' + legIndex,
'type': 'line',
'source': 'route-transit-' + index + '-' + legIndex,
'layout': {
'line-join': 'round',
'line-cap': 'round'
},
'paint': {
'line-color': lineColor,
'line-width': lineWidth
}
};
if (lineDasharray) {
layerConfig.paint['line-dasharray'] = lineDasharray;
}
this.map.addLayer(layerConfig);
});
// Fit map to show all coordinates
if (allCoords.length > 0) {
const bounds = allCoords.reduce((bounds, coord) => {
return bounds.extend(coord);
}, new maplibregl.LngLatBounds(allCoords[0], allCoords[0]));
this.map.fitBounds(bounds, {
padding: 50
});
}
},
displaySolidarityRoute(index) {
if (!this.map || !this.map.loaded()) return;
// If a specific driver is selected, show their route
if (this.selectedDriverIndex !== null && this.solidarityJourneys[this.selectedDriverIndex]) {
const journey = this.solidarityJourneys[this.selectedDriverIndex];
const allCoords = [];
// If we have a detailed polyline, use it
if (journey.polyline && typeof polyline !== 'undefined') {
const decoded = polyline.decode(journey.polyline, 5); // Assume precision 5 for Google polyline
// Convert from [lat, lon] to [lon, lat] for MapLibre
const coordinates = decoded.map((coord) => [coord[1], coord[0]]);
coordinates.forEach((coord) => allCoords.push(coord));
this.map.addSource('route-solidarity-polyline', {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': coordinates
}
}
});
this.map.addLayer({
'id': 'route-solidarity-polyline',
'type': 'line',
'source': 'route-solidarity-polyline',
'paint': {
'line-color': getComputedStyle(document.documentElement).getPropertyValue('--color-co-lightblue').trim(),
'line-width': 4
}
});
} else {
// Fallback to simple line segments if no polyline
if (journey.driverLocation && journey.passengerPickup) {
allCoords.push(journey.driverLocation, journey.passengerPickup);
this.map.addSource('route-solidarity-driver-pickup', {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': [journey.driverLocation, journey.passengerPickup]
}
}
});
this.map.addLayer({
'id': 'route-solidarity-driver-pickup',
'type': 'line',
'source': 'route-solidarity-driver-pickup',
'paint': {
'line-color': getComputedStyle(document.documentElement).getPropertyValue('--color-co-lightblue').trim(),
'line-width': 4
}
});
}
if (journey.passengerPickup && journey.passengerDropoff) {
allCoords.push(journey.passengerDropoff);
this.map.addSource('route-solidarity-passenger', {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': [journey.passengerPickup, journey.passengerDropoff]
}
}
});
this.map.addLayer({
'id': 'route-solidarity-passenger',
'type': 'line',
'source': 'route-solidarity-passenger',
'paint': {
'line-color': getComputedStyle(document.documentElement).getPropertyValue('--color-co-blue').trim(),
'line-width': 4
}
});
}
if (journey.passengerDropoff && journey.driverDestination) {
allCoords.push(journey.driverDestination);
this.map.addSource('route-solidarity-driver-dest', {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': [journey.passengerDropoff, journey.driverDestination]
}
}
});
this.map.addLayer({
'id': 'route-solidarity-driver-dest',
'type': 'line',
'source': 'route-solidarity-driver-dest',
'paint': {
'line-color': getComputedStyle(document.documentElement).getPropertyValue('--color-co-blue').trim(),
'line-width': 4,
'line-dasharray': [2, 2]
}
});
}
}
// Add markers for driver departure and destination
if (journey.driverLocation) {
const driverStartEl = document.createElement('div');
driverStartEl.className = 'w-8 h-8 rounded-co bg-co-blue border border-white shadow-md flex items-center justify-center';
driverStartEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
const driverStartMarker = new maplibregl.Marker({element: driverStartEl})
.setLngLat(journey.driverLocation)
.addTo(this.map);
this.routeMarkers.push(driverStartMarker);
}
if (journey.driverDestination) {
const driverEndEl = document.createElement('div');
driverEndEl.className = 'w-8 h-8 rounded-co bg-co-blue border border-white shadow-md flex items-center justify-center';
driverEndEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
const driverEndMarker = new maplibregl.Marker({element: driverEndEl})
.setLngLat(journey.driverDestination)
.addTo(this.map);
this.routeMarkers.push(driverEndMarker);
}
// Fit bounds to show the journey
if (allCoords.length > 0) {
const bounds = allCoords.reduce((bounds, coord) => {
return bounds.extend(coord);
}, new maplibregl.LngLatBounds(allCoords[0], allCoords[0]));
this.map.fitBounds(bounds, {
padding: 80
});
}
} else {
// Show all driver locations when no specific driver is selected
const driverLocations = [];
this.solidarityJourneys.forEach((journey) => {
if (journey.driverLocation) {
driverLocations.push(journey.driverLocation);
}
});
if (driverLocations.length === 0) return;
// Create custom marker element for each driver
this.solidarityJourneys.forEach((journey, idx) => {
if (!journey.driverLocation) return;
const el = document.createElement('div');
el.className = 'w-8 h-8 rounded-co bg-co-blue border border-white shadow-md flex items-center justify-center cursor-pointer transition-all';
el.setAttribute('data-driver-index', idx);
// Add person icon (using SVG)
el.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
// Add click handler
el.addEventListener('click', (e) => {
e.stopPropagation();
this.selectedDriverIndex = idx;
this.displaySolidarityRoute(0);
});
const marker = new maplibregl.Marker({element: el})
.setLngLat(journey.driverLocation)
.addTo(this.map);
this.routeMarkers.push(marker);
});
// Fit bounds to show all driver locations
if (driverLocations.length > 0) {
const bounds = driverLocations.reduce((bounds, coord) => {
return bounds.extend(coord);
}, new maplibregl.LngLatBounds(driverLocations[0], driverLocations[0]));
this.map.fitBounds(bounds, {
padding: 80
});
}
}
},
displayOrganizedCarpoolRoute(index) {
if (!this.map || !this.map.loaded() || !this.organizedCarpools[index]) return;
const carpool = this.organizedCarpools[index];
const allCoords = [];
// If we have a detailed polyline, use it
if (carpool.polyline && typeof polyline !== 'undefined') {
const decoded = polyline.decode(carpool.polyline, 5);
const coordinates = decoded.map((coord) => [coord[1], coord[0]]);
coordinates.forEach((coord) => allCoords.push(coord));
this.map.addSource('route-carpool-polyline', {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': coordinates
}
}
});
this.map.addLayer({
'id': 'route-carpool-polyline',
'type': 'line',
'source': 'route-carpool-polyline',
'paint': {
'line-color': getComputedStyle(document.documentElement).getPropertyValue('--color-co-blue').trim(),
'line-width': 4
}
});
}
// Add markers for driver departure and arrival
if (carpool.driverDepartureLocation) {
const driverStartEl = document.createElement('div');
driverStartEl.className = 'w-8 h-8 rounded-co bg-co-blue border border-white shadow-md flex items-center justify-center';
driverStartEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
const driverStartMarker = new maplibregl.Marker({element: driverStartEl})
.setLngLat(carpool.driverDepartureLocation)
.addTo(this.map);
this.routeMarkers.push(driverStartMarker);
allCoords.push(carpool.driverDepartureLocation);
}
if (carpool.driverArrivalLocation) {
const driverEndEl = document.createElement('div');
driverEndEl.className = 'w-8 h-8 rounded-co bg-co-blue border border-white shadow-md flex items-center justify-center';
driverEndEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
const driverEndMarker = new maplibregl.Marker({element: driverEndEl})
.setLngLat(carpool.driverArrivalLocation)
.addTo(this.map);
this.routeMarkers.push(driverEndMarker);
allCoords.push(carpool.driverArrivalLocation);
}
// Add passenger pickup and drop locations to bounds (markers already exist)
if (carpool.departureLocation) {
allCoords.push(carpool.departureLocation);
}
if (carpool.arrivalLocation) {
allCoords.push(carpool.arrivalLocation);
}
// Fit bounds to show all points
if (allCoords.length > 0) {
const bounds = allCoords.reduce((bounds, coord) => {
return bounds.extend(coord);
}, new maplibregl.LngLatBounds(allCoords[0], allCoords[0]));
this.map.fitBounds(bounds, {
padding: 80
});
}
},
displayCarpoolRoute(index) {
if (!this.map || !this.map.loaded() || !this.carpools[index]) return;
const carpool = this.carpools[index];
const allCoords = [];
// If we have a detailed polyline, use it
if (carpool.polyline && typeof polyline !== 'undefined') {
const decoded = polyline.decode(carpool.polyline, 5);
const coordinates = decoded.map((coord) => [coord[1], coord[0]]);
coordinates.forEach((coord) => allCoords.push(coord));
this.map.addSource('route-carpool-rdex-polyline', {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': coordinates
}
}
});
this.map.addLayer({
'id': 'route-carpool-rdex-polyline',
'type': 'line',
'source': 'route-carpool-rdex-polyline',
'paint': {
'line-color': getComputedStyle(document.documentElement).getPropertyValue('--color-co-blue').trim(),
'line-width': 4
}
});
} else {
// If no polyline, draw lines between driver departure -> pickup -> drop -> driver arrival
const routeCoords = [];
// Always use at least pickup and drop (they should always exist)
if (carpool.driverDepartureLocation) {
routeCoords.push(carpool.driverDepartureLocation);
allCoords.push(carpool.driverDepartureLocation);
}
if (carpool.pickupLocation) {
routeCoords.push(carpool.pickupLocation);
allCoords.push(carpool.pickupLocation);
}
if (carpool.dropLocation) {
routeCoords.push(carpool.dropLocation);
allCoords.push(carpool.dropLocation);
}
if (carpool.driverArrivalLocation) {
routeCoords.push(carpool.driverArrivalLocation);
allCoords.push(carpool.driverArrivalLocation);
}
console.log('Carpool route coords:', routeCoords);
if (routeCoords.length >= 2) {
this.map.addSource('route-carpool-rdex-simple', {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': routeCoords
}
}
});
this.map.addLayer({
'id': 'route-carpool-rdex-simple',
'type': 'line',
'source': 'route-carpool-rdex-simple',
'paint': {
'line-color': getComputedStyle(document.documentElement).getPropertyValue('--color-co-blue').trim(),
'line-width': 4,
'line-dasharray': [2, 2] // dashed line to indicate it's not the actual route
}
});
}
}
// Add markers for driver departure and arrival (if they exist)
if (carpool.driverDepartureLocation) {
const driverStartEl = document.createElement('div');
driverStartEl.className = 'w-8 h-8 rounded-co bg-blue-600 border border-white shadow-md flex items-center justify-center';
driverStartEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
const driverStartMarker = new maplibregl.Marker({element: driverStartEl})
.setLngLat(carpool.driverDepartureLocation)
.addTo(this.map);
this.routeMarkers.push(driverStartMarker);
}
if (carpool.driverArrivalLocation) {
const driverEndEl = document.createElement('div');
driverEndEl.className = 'w-8 h-8 rounded-co bg-blue-600 border border-white shadow-md flex items-center justify-center';
driverEndEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
const driverEndMarker = new maplibregl.Marker({element: driverEndEl})
.setLngLat(carpool.driverArrivalLocation)
.addTo(this.map);
this.routeMarkers.push(driverEndMarker);
}
// Add marker for pickup location (green)
if (carpool.pickupLocation) {
const pickupEl = document.createElement('div');
pickupEl.className = 'w-8 h-8 rounded-co bg-green-600 border border-white shadow-md flex items-center justify-center';
pickupEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
`;
const pickupMarker = new maplibregl.Marker({element: pickupEl})
.setLngLat(carpool.pickupLocation)
.addTo(this.map);
this.routeMarkers.push(pickupMarker);
}
// Add marker for drop location (red)
if (carpool.dropLocation) {
const dropEl = document.createElement('div');
dropEl.className = 'w-8 h-8 rounded-co bg-red-600 border border-white shadow-md flex items-center justify-center';
dropEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path>
<line x1="4" y1="22" x2="4" y2="15"></line>
</svg>
`;
const dropMarker = new maplibregl.Marker({element: dropEl})
.setLngLat(carpool.dropLocation)
.addTo(this.map);
this.routeMarkers.push(dropMarker);
}
// Fit bounds to show all points
if (allCoords.length > 0) {
const bounds = allCoords.reduce((bounds, coord) => {
return bounds.extend(coord);
}, new maplibregl.LngLatBounds(allCoords[0], allCoords[0]));
this.map.fitBounds(bounds, {
padding: 80
});
}
},
async displayKBSolution(index) {
if (!this.map || !this.map.loaded() || !this.kbSolutions[index]) return;
const solution = this.kbSolutions[index];
if (!solution.geography || solution.geography.length === 0) return;
// Collect geography features from the solution
const allFeatures = [];
for (const geo of solution.geography) {
// Use the geography feature that was fetched by the backend
if (geo.geography && geo.geography.geometry) {
allFeatures.push(geo.geography);
}
}
if (allFeatures.length === 0) return;
// Add all polygons to the map
allFeatures.forEach((feature, idx) => {
const sourceId = 'route-kb-' + index + '-' + idx;
this.map.addSource(sourceId, {
'type': 'geojson',
'data': feature
});
// Add fill layer
this.map.addLayer({
'id': sourceId + '-fill',
'type': 'fill',
'source': sourceId,
'paint': {
'fill-color': getComputedStyle(document.documentElement).getPropertyValue('--color-co-orange').trim(),
'fill-opacity': 0.2
}
});
// Add outline layer
this.map.addLayer({
'id': sourceId + '-outline',
'type': 'line',
'source': sourceId,
'paint': {
'line-color': getComputedStyle(document.documentElement).getPropertyValue('--color-co-orange').trim(),
'line-width': 2
}
});
});
// Fit bounds to show all polygons
const bounds = new maplibregl.LngLatBounds();
allFeatures.forEach(feature => {
if (feature.geometry.type === 'Polygon') {
feature.geometry.coordinates[0].forEach(coord => {
bounds.extend(coord);
});
} else if (feature.geometry.type === 'MultiPolygon') {
feature.geometry.coordinates.forEach(polygon => {
polygon[0].forEach(coord => {
bounds.extend(coord);
});
});
}
});
if (!bounds.isEmpty()) {
this.map.fitBounds(bounds, {
padding: 50
});
}
}
};
}
</script>
{{end}}