1766 lines
100 KiB
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}}
|