Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235cdacf9e | ||
|
|
9f1104b34f | ||
|
|
0ec0f681b2 | ||
|
|
d8e9db3c59 | ||
|
|
efbac6cef7 | ||
|
|
34c12135ca | ||
|
|
23c1186689 | ||
|
|
9324a08cf9 | ||
|
|
d345ea9ec3 |
@@ -0,0 +1 @@
|
|||||||
|
GTFS : le C6 c'est pas présent
|
||||||
+23
-3
@@ -1,13 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="main">
|
<NuxtLayout>
|
||||||
<NuxtRouteAnnouncer />
|
<NuxtRouteAnnouncer />
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const router = useRoute();
|
||||||
|
const store = useMresoStore();
|
||||||
|
watch(
|
||||||
|
() => router.params.line_id,
|
||||||
|
() => {
|
||||||
|
if (router.params.line_id === undefined) {
|
||||||
|
store.selectedRouteIds = [];
|
||||||
|
}
|
||||||
|
if (typeof router.params.line_id !== "string") {
|
||||||
|
store.selectedRouteIds = [];
|
||||||
|
}
|
||||||
|
store.selectedRouteIds = [router.params.line_id];
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
body,
|
body,
|
||||||
#main {
|
#main,
|
||||||
|
#__nuxt,
|
||||||
|
main {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<mgl-geo-json-source
|
<mgl-geo-json-source source-id="clusters" :data="clusters">
|
||||||
v-if="clusters"
|
|
||||||
source-id="clusters"
|
|
||||||
:data="clusters"
|
|
||||||
>
|
|
||||||
<mgl-circle-layer
|
<mgl-circle-layer
|
||||||
layer-id="clusters_dots"
|
layer-id="clusters_dots"
|
||||||
:paint="cluster_paint"
|
:paint="cluster_paint"
|
||||||
@@ -12,11 +8,37 @@
|
|||||||
@mouseenter="clusterEnter"
|
@mouseenter="clusterEnter"
|
||||||
@mouseleave="clusterLeave"
|
@mouseleave="clusterLeave"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<MglSymbolLayer
|
||||||
|
layer-id="cluster_icon"
|
||||||
|
:layout="{
|
||||||
|
'icon-image': 'cluster',
|
||||||
|
'icon-size': 0.1,
|
||||||
|
'icon-overlap': 'always',
|
||||||
|
'text-overlap': 'always',
|
||||||
|
}"
|
||||||
|
@mouseenter="clusterEnter"
|
||||||
|
@mouseleave="clusterLeave"
|
||||||
|
/>
|
||||||
<MglSymbolLayer
|
<MglSymbolLayer
|
||||||
layer-id="clusters_labels"
|
layer-id="clusters_labels"
|
||||||
:layout="cluster_label"
|
:layout="cluster_label"
|
||||||
/>
|
/>
|
||||||
</mgl-geo-json-source>
|
</mgl-geo-json-source>
|
||||||
|
|
||||||
|
<div ref="popup-div">
|
||||||
|
<div v-for="data in popupData" :key="data.pattern.id">
|
||||||
|
Direction {{ data.pattern.lastStopName }}
|
||||||
|
<div v-for="time in data.times" :key="time.tripId">
|
||||||
|
{{
|
||||||
|
stopTimeToRealTime(
|
||||||
|
time.realtimeArrival,
|
||||||
|
time.serviceDay,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -24,20 +46,45 @@
|
|||||||
import type { FeatureCollection, Point } from "geojson";
|
import type { FeatureCollection, Point } from "geojson";
|
||||||
import { useMap } from "@indoorequal/vue-maplibre-gl";
|
import { useMap } from "@indoorequal/vue-maplibre-gl";
|
||||||
import { Popup } from "maplibre-gl";
|
import { Popup } from "maplibre-gl";
|
||||||
const cluster_paint = {
|
|
||||||
"circle-radius": 5,
|
|
||||||
"circle-color": "#FF0000",
|
|
||||||
};
|
|
||||||
const cluster_label = {
|
|
||||||
"text-field": ["get", "id"],
|
|
||||||
};
|
|
||||||
const map = useMap();
|
|
||||||
|
|
||||||
const { clusters } = defineProps<{
|
const cluster_label = {
|
||||||
|
"text-field": ["get", "name"],
|
||||||
|
};
|
||||||
|
const map = useMap("main");
|
||||||
|
const popupDiv = useTemplateRef("popup-div");
|
||||||
|
|
||||||
|
const { clusters, route } = defineProps<{
|
||||||
clusters: FeatureCollection<Point>;
|
clusters: FeatureCollection<Point>;
|
||||||
|
route: Route;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const cluster_paint = {
|
||||||
|
"circle-radius": 18,
|
||||||
|
"circle-color": "#" + route.color,
|
||||||
|
};
|
||||||
|
|
||||||
|
const popupData = ref<StopTimesPattern[]>([]);
|
||||||
|
const popup = new Popup().setMaxWidth("500px").addTo(map.map!);
|
||||||
|
const router = useRoute();
|
||||||
|
const store = useMresoStore();
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
popup.off("close", closePopupEventHandler);
|
||||||
|
popup.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => router.params.cluster_id,
|
||||||
|
async () => {
|
||||||
|
popup.off("close", closePopupEventHandler);
|
||||||
|
await routerParamChange();
|
||||||
|
popup.on("close", closePopupEventHandler);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
async function clusterClick(e: FeatureCollection) {
|
async function clusterClick(e: FeatureCollection) {
|
||||||
|
popup.off("close", closePopupEventHandler);
|
||||||
const feature = e.features[0];
|
const feature = e.features[0];
|
||||||
if (!feature || !feature.properties) {
|
if (!feature || !feature.properties) {
|
||||||
return;
|
return;
|
||||||
@@ -46,17 +93,61 @@ async function clusterClick(e: FeatureCollection) {
|
|||||||
if (!code) {
|
if (!code) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const coordinates = feature.geometry.coordinates.slice();
|
console.log(code);
|
||||||
const data = await $fetch<FeatureCollection<Point>>(
|
navigateTo(`/lines/${router.params.line_id}/cluster/${code}`);
|
||||||
`https://data.mobilites-m.fr/api/clusters/${code}/stops`,
|
popup.remove();
|
||||||
);
|
popup.on("close", closePopupEventHandler);
|
||||||
if (!map.map) {
|
}
|
||||||
|
|
||||||
|
async function routerParamChange() {
|
||||||
|
if (router.params.cluster_id === undefined) {
|
||||||
|
popup.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
new Popup()
|
if (typeof router.params.cluster_id !== "string") {
|
||||||
.setLngLat(coordinates)
|
return;
|
||||||
.setHTML(`This cluster contains ${data.features.length} stops`)
|
}
|
||||||
.addTo(map.map);
|
const cluster = store.clustersByCode[router.params.cluster_id];
|
||||||
|
if (!cluster) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const code = cluster.properties?.code;
|
||||||
|
if (!code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
popupData.value = [];
|
||||||
|
|
||||||
|
const coordinates = cluster.geometry.coordinates.slice();
|
||||||
|
|
||||||
|
popupData.value = await $fetch<StopTimesPattern[]>(
|
||||||
|
`https://data.mobilites-m.fr/api/routers/default/index/clusters/${code}/stoptimes?showCancelledTrips=false&route=${route.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
origin: "PersonalProjectMreso",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!popupDiv.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
popup.setDOMContent(popupDiv.value).setLngLat(coordinates).addTo(map.map!);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePopupEventHandler() {
|
||||||
|
navigateTo(`/lines/${router.params.line_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimeToRealTime(realtimeArrival: number, serviceDay: number) {
|
||||||
|
const seconds =
|
||||||
|
realtimeArrival -
|
||||||
|
(Math.floor(new Date().getTime() / 1000) - serviceDay);
|
||||||
|
if (seconds < 0) {
|
||||||
|
return "00:00:00";
|
||||||
|
}
|
||||||
|
const date = new Date(0);
|
||||||
|
date.setSeconds(seconds); // specify value for SECONDS here
|
||||||
|
const timeString = date.toISOString().substring(11, 19);
|
||||||
|
return timeString;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clusterEnter() {
|
function clusterEnter() {
|
||||||
|
|||||||
@@ -1,36 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<MglMap :map-style="style" :center="center" :zoom="zoom" height="100%">
|
<MglMap :map-style="style" :center="center" :zoom="zoom" map-key="main">
|
||||||
<MglNavigationControl />
|
<MglNavigationControl />
|
||||||
<MapLines
|
<MglImage id="cluster" :image="image" />
|
||||||
v-for="r of routeWithLines"
|
<template v-for="r of shownLines" :key="r.route.id">
|
||||||
:key="r.route.id"
|
<MapLines :route-with-line="r" />
|
||||||
:route-with-line="r"
|
|
||||||
/>
|
|
||||||
<MapClusters
|
<MapClusters
|
||||||
v-if="clustersData.features"
|
v-if="shownClusters"
|
||||||
:clusters="clustersData"
|
:clusters="shownClusters"
|
||||||
/>
|
:route="r.route"
|
||||||
|
/></template>
|
||||||
</MglMap>
|
</MglMap>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
|
||||||
<div v-if="routesRequest.data.value" id="lineSelector">
|
|
||||||
<div
|
|
||||||
v-for="route of routesRequest.data.value"
|
|
||||||
:key="route.id"
|
|
||||||
:class="{
|
|
||||||
selected: selectedRoutes.includes(route.id),
|
|
||||||
lineLabel: true,
|
|
||||||
}"
|
|
||||||
:style="{
|
|
||||||
background: '#' + route.color,
|
|
||||||
color: '#' + route.textColor,
|
|
||||||
}"
|
|
||||||
@click="selectRoute(route.id)"
|
|
||||||
>
|
|
||||||
{{ route.type }} : {{ route.shortName }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -40,94 +21,55 @@ import type {
|
|||||||
MultiLineString,
|
MultiLineString,
|
||||||
Point,
|
Point,
|
||||||
} from "geojson";
|
} from "geojson";
|
||||||
|
|
||||||
import { useMap } from "@indoorequal/vue-maplibre-gl";
|
import { useMap } from "@indoorequal/vue-maplibre-gl";
|
||||||
import { LngLat, LngLatBounds } from "maplibre-gl";
|
import { LngLat, LngLatBounds } from "maplibre-gl";
|
||||||
const style = "https://tiles.versatiles.org/assets/styles/colorful/style.json";
|
const style = "https://tiles.versatiles.org/assets/styles/colorful/style.json";
|
||||||
const center = new LngLat(5.735, 45.185);
|
const center = new LngLat(5.735, 45.185);
|
||||||
const zoom = 12;
|
const zoom = 12;
|
||||||
|
|
||||||
const map = useMap();
|
const store = useMresoStore();
|
||||||
|
await store.fetchData();
|
||||||
|
const shownClusters = ref<FeatureCollection<Point> | null>(null);
|
||||||
|
const image = useSvgImage();
|
||||||
|
const map = useMap("main");
|
||||||
|
|
||||||
const selectedRoutes = ref<string[]>([]);
|
watchEffect(async () => {
|
||||||
const selectedStops = ref<string[]>([]);
|
const allClusters = [];
|
||||||
|
for (const routeId of store.selectedRouteIds) {
|
||||||
const clustersRequest =
|
if (routeId === undefined) {
|
||||||
await useFetch<FeatureCollection<Point>>("/clusters.json");
|
continue;
|
||||||
const clusters = computed(() => clustersRequest.data.value?.features ?? []);
|
}
|
||||||
const clustersData = computed<FeatureCollection<Point>>(() => {
|
const apiClusters = await $fetch<Cluster[]>(
|
||||||
return {
|
`https://data.mobilites-m.fr/api/routers/default/index/routes/${routeId}/clusters`,
|
||||||
|
);
|
||||||
|
allClusters.push(...apiClusters);
|
||||||
|
}
|
||||||
|
shownClusters.value = {
|
||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
features: clusters.value.filter((f) =>
|
features: allClusters
|
||||||
selectedStops.value.includes(f.properties?.id),
|
.map((c) => store.clustersByCode[c.code])
|
||||||
),
|
.filter((c) => c !== undefined),
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const linesRequest =
|
const firstPoint = shownClusters.value.features[0];
|
||||||
await useFetch<FeatureCollection<MultiLineString>>("/lignes.json");
|
|
||||||
const transport_lines = computed(() => linesRequest.data.value?.features ?? []);
|
|
||||||
|
|
||||||
const routesRequest = await useFetch<Route[]>("/routes.json");
|
|
||||||
|
|
||||||
// Selected routes with lines
|
|
||||||
const routeWithLines = computed(() => {
|
|
||||||
const shownLines: { route: Route; line: Feature<MultiLineString> }[] = [];
|
|
||||||
for (const routeId of selectedRoutes.value) {
|
|
||||||
const route = routesRequest.data.value?.find((r) => r.id == routeId);
|
|
||||||
if (!route) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const line = transport_lines.value.find(
|
|
||||||
(f) => routeId == f.properties?.id.replace("_", ":"),
|
|
||||||
);
|
|
||||||
if (!line) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
shownLines.push({
|
|
||||||
route,
|
|
||||||
line,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return shownLines;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function selectRoute(routeId: string) {
|
|
||||||
if (!map.map) {
|
|
||||||
console.error("Map is not yet initialized ! aborting");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedRoutes.value = [routeId];
|
|
||||||
const stops: string[] = [];
|
|
||||||
for (const route of selectedRoutes.value) {
|
|
||||||
const data = await $fetch<Cluster[]>(
|
|
||||||
`https://data.mobilites-m.fr/api/routers/default/index/routes/${route.replace("_", ":")}/clusters`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const stop of data) {
|
|
||||||
stops.push(stop.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedStops.value = stops;
|
|
||||||
|
|
||||||
const points = clusters.value.filter((c) =>
|
|
||||||
stops.includes(c.properties?.id),
|
|
||||||
);
|
|
||||||
const firstPoint = points[0];
|
|
||||||
if (!firstPoint) {
|
if (!firstPoint) {
|
||||||
console.error("Data does not contains any points. Aborting");
|
map.map?.flyTo({ center, zoom });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bounds = clusters.value
|
const coordinates = [
|
||||||
.filter((c) => stops.includes(c.properties?.id))
|
...shownClusters.value.features.map(
|
||||||
.reduce(
|
(point) => point.geometry.coordinates,
|
||||||
(bounds, point) => {
|
|
||||||
return bounds.extend(
|
|
||||||
new LngLat(
|
|
||||||
point.geometry.coordinates[0]!,
|
|
||||||
point.geometry.coordinates[1]!,
|
|
||||||
),
|
),
|
||||||
);
|
...shownLines.value
|
||||||
|
.map((line) => line.line.geometry.coordinates)
|
||||||
|
.flat(2),
|
||||||
|
];
|
||||||
|
const bounds = coordinates
|
||||||
|
// .filter((c) => stops.includes(c.properties?.id))
|
||||||
|
.reduce(
|
||||||
|
(bounds, coords) => {
|
||||||
|
return bounds.extend(new LngLat(coords[0]!, coords[1]!));
|
||||||
},
|
},
|
||||||
new LngLatBounds([
|
new LngLatBounds([
|
||||||
firstPoint.geometry.coordinates[0]!,
|
firstPoint.geometry.coordinates[0]!,
|
||||||
@@ -136,27 +78,27 @@ async function selectRoute(routeId: string) {
|
|||||||
firstPoint.geometry.coordinates[1]!,
|
firstPoint.geometry.coordinates[1]!,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
map.map!.fitBounds(bounds, {
|
||||||
map.map.fitBounds(bounds, {
|
|
||||||
padding: 20,
|
padding: 20,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const shownLines = computed(() => {
|
||||||
|
const l: { route: Route; line: Feature<MultiLineString> }[] = [];
|
||||||
|
for (const routeId of store.selectedRouteIds) {
|
||||||
|
const route = store.routesById[routeId];
|
||||||
|
if (!route) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
const line = store.linesById[routeId.replace(":", "_")];
|
||||||
|
if (!line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
l.push({
|
||||||
|
route,
|
||||||
|
line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="css">
|
|
||||||
#lineSelector {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: auto;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lineLabel {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.lineLabel.selected {
|
|
||||||
color: red !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div id="lineSelector">
|
||||||
|
<div
|
||||||
|
v-for="route of store.routes"
|
||||||
|
:key="route.id"
|
||||||
|
:class="{
|
||||||
|
selected: store.selectedRouteIds.includes(route.id),
|
||||||
|
lineLabel: true,
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
background: '#' + route.color,
|
||||||
|
color: '#' + route.textColor,
|
||||||
|
}"
|
||||||
|
@click="selectRoute(route.id)"
|
||||||
|
>
|
||||||
|
{{ route.type }} : {{ route.shortName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const store = useMresoStore();
|
||||||
|
|
||||||
|
function selectRoute(routeId: string) {
|
||||||
|
navigateTo(`/lines/${routeId}`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="css">
|
||||||
|
#lineSelector {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lineLabel {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.lineLabel.selected {
|
||||||
|
color: red !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const clustersRequest = $fetch<Blob>(
|
||||||
|
"https://pass.mobilites-m.fr/assets/icons/clusters.svg",
|
||||||
|
);
|
||||||
|
|
||||||
|
const image = new Image();
|
||||||
|
const imageLoadPromise = new Promise((resolve) => {
|
||||||
|
image.onload = resolve;
|
||||||
|
});
|
||||||
|
clustersRequest.then((blob) => {
|
||||||
|
image.src = URL.createObjectURL(blob);
|
||||||
|
});
|
||||||
|
await Promise.all([imageLoadPromise, clustersRequest]); // Wait for the image to load
|
||||||
|
|
||||||
|
export const useSvgImage = () => {
|
||||||
|
return useState("cluster.svg", () => image);
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<Map map-key="main" />
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<SidebarContainer />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
definePageMeta({ key: "lines" });
|
||||||
|
</script>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Map />
|
|
||||||
</template>
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type {
|
||||||
|
Feature,
|
||||||
|
FeatureCollection,
|
||||||
|
MultiLineString,
|
||||||
|
Point,
|
||||||
|
} from "geojson";
|
||||||
|
|
||||||
|
export const useMresoStore = defineStore("mresoStore", {
|
||||||
|
state: () => ({
|
||||||
|
routes: [] as Route[],
|
||||||
|
lines: {} as FeatureCollection<MultiLineString>,
|
||||||
|
clusters: {} as FeatureCollection<Point>,
|
||||||
|
stops: {} as FeatureCollection<Point>,
|
||||||
|
selectedRouteIds: [] as string[],
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async fetchData(options: { signal?: AbortSignal } = {}) {
|
||||||
|
[this.routes, this.lines, this.clusters, this.stops] = await Promise.all([
|
||||||
|
$fetch<Route[]>("/routes.json", options),
|
||||||
|
$fetch<FeatureCollection<MultiLineString>>("/lines.json", options),
|
||||||
|
$fetch<FeatureCollection<Point>>("/clusters.json", options),
|
||||||
|
$fetch<FeatureCollection<Point>>("/stops.json", options),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
routesById(state): { [id: string]: Route } {
|
||||||
|
return Object.fromEntries(state.routes.map((r) => [r.id, r]));
|
||||||
|
},
|
||||||
|
linesById(state): { [id: string]: Feature<MultiLineString> } {
|
||||||
|
if (!("features" in state.lines)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return Object.fromEntries(
|
||||||
|
state.lines.features.map((l) => [l.properties!.id, l]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
clustersByCode(state): { [id: string]: Feature<Point> } {
|
||||||
|
if (!("features" in state.clusters)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return Object.fromEntries(
|
||||||
|
state.clusters.features.map((l) => [l.properties!.code, l]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Vendored
+32
-1
@@ -7,7 +7,7 @@ interface Route {
|
|||||||
textColor: string;
|
textColor: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
type: string;
|
type: string;
|
||||||
timeSheet: string;
|
timeSheet: boolean;
|
||||||
pdfTimeSheet: boolean;
|
pdfTimeSheet: boolean;
|
||||||
pdfMap: boolean;
|
pdfMap: boolean;
|
||||||
}
|
}
|
||||||
@@ -21,3 +21,34 @@ interface Cluster {
|
|||||||
name: string;
|
name: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface StopTimesPattern {
|
||||||
|
pattern: {
|
||||||
|
id: string;
|
||||||
|
desc: string;
|
||||||
|
dir: number;
|
||||||
|
shortDesc: string;
|
||||||
|
lastStop: string;
|
||||||
|
lastStopName: string;
|
||||||
|
};
|
||||||
|
times: StopTimes[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StopTimes {
|
||||||
|
stopId: string;
|
||||||
|
stopName: string;
|
||||||
|
scheduledArrival: number;
|
||||||
|
scheduledDeparture: number;
|
||||||
|
realtimeArrival: number;
|
||||||
|
realtimeDeparture: number;
|
||||||
|
arrivalDelay: number;
|
||||||
|
departureDelay: number;
|
||||||
|
timepoint: boolean;
|
||||||
|
realtime: boolean;
|
||||||
|
realtimeState: string;
|
||||||
|
serviceDay: number;
|
||||||
|
tripId: string;
|
||||||
|
pickupType: string;
|
||||||
|
occupancy: string;
|
||||||
|
occupancyId: number;
|
||||||
|
}
|
||||||
|
|||||||
+18
-6
@@ -1,12 +1,24 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2025-07-15",
|
compatibilityDate: "2025-07-15",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: false },
|
||||||
modules: ["@nuxt/eslint", "nuxt-maplibre"],
|
modules: ["@nuxt/eslint", "nuxt-maplibre", "@pinia/nuxt"],
|
||||||
vite: {
|
ssr: true,
|
||||||
optimizeDeps: {
|
hooks: {
|
||||||
// noDiscovery: true,
|
"pages:extend"(pages) {
|
||||||
// include: ["maplibre-gl"],
|
// add a route
|
||||||
|
pages.push({
|
||||||
|
name: "lines",
|
||||||
|
path: "/lines/:line_id()",
|
||||||
|
file: "~/pages/index.vue",
|
||||||
|
meta: { key: "lines" },
|
||||||
|
});
|
||||||
|
pages.push({
|
||||||
|
name: "lines/cluster",
|
||||||
|
path: "/lines/:line_id()/cluster/:cluster_id()",
|
||||||
|
file: "~/pages/index.vue",
|
||||||
|
meta: { key: "lines" },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+152
@@ -8,10 +8,12 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/eslint": "^1.16.0",
|
"@nuxt/eslint": "^1.16.0",
|
||||||
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
"eslint": "^10.6.0",
|
"eslint": "^10.6.0",
|
||||||
"nuxt": "^4.4.8",
|
"nuxt": "^4.4.8",
|
||||||
"nuxt-maplibre": "^1.2.2",
|
"nuxt-maplibre": "^1.2.2",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.38",
|
"vue": "^3.5.38",
|
||||||
"vue-router": "^5.1.0"
|
"vue-router": "^5.1.0"
|
||||||
}
|
}
|
||||||
@@ -3471,6 +3473,21 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pinia/nuxt": {
|
||||||
|
"version": "0.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.11.3.tgz",
|
||||||
|
"integrity": "sha512-7WVNHpWx4qAEzOlnyrRC88kYrwnlR/PrThWT0XI1dSNyUAXu/KBv9oR37uCgYkZroqP5jn8DfzbkNF3BtKvE9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/kit": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"pinia": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -6231,6 +6248,21 @@
|
|||||||
"integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
|
"integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/copy-anything": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-what": "^5.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.49.0",
|
"version": "3.49.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
|
||||||
@@ -8485,6 +8517,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-what": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-wsl": {
|
"node_modules/is-wsl": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
|
||||||
@@ -9324,6 +9368,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mitt": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mlly": {
|
"node_modules/mlly": {
|
||||||
"version": "1.8.2",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
|
||||||
@@ -10574,6 +10624,81 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^7.7.7"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.5.0",
|
||||||
|
"vue": "^3.5.11"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/@vue/devtools-api": {
|
||||||
|
"version": "7.7.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz",
|
||||||
|
"integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-kit": "^7.7.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/@vue/devtools-kit": {
|
||||||
|
"version": "7.7.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz",
|
||||||
|
"integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-shared": "^7.7.10",
|
||||||
|
"birpc": "^2.3.0",
|
||||||
|
"hookable": "^5.5.3",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"speakingurl": "^14.0.1",
|
||||||
|
"superjson": "^2.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/@vue/devtools-shared": {
|
||||||
|
"version": "7.7.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz",
|
||||||
|
"integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"rfdc": "^1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/birpc": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/hookable": {
|
||||||
|
"version": "5.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/perfect-debounce": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pkg-types": {
|
"node_modules/pkg-types": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
|
||||||
@@ -11422,6 +11547,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rfdc": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
||||||
@@ -11882,6 +12013,15 @@
|
|||||||
"integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==",
|
"integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/speakingurl": {
|
||||||
|
"version": "14.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||||
|
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/srvx": {
|
"node_modules/srvx": {
|
||||||
"version": "0.11.21",
|
"version": "0.11.21",
|
||||||
"resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.21.tgz",
|
"resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.21.tgz",
|
||||||
@@ -12104,6 +12244,18 @@
|
|||||||
"postcss": "^8.5.15"
|
"postcss": "^8.5.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/superjson": {
|
||||||
|
"version": "2.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
||||||
|
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-anything": "^4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "10.2.2",
|
"version": "10.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
|
||||||
|
|||||||
@@ -11,10 +11,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/eslint": "^1.16.0",
|
"@nuxt/eslint": "^1.16.0",
|
||||||
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
"eslint": "^10.6.0",
|
"eslint": "^10.6.0",
|
||||||
"nuxt": "^4.4.8",
|
"nuxt": "^4.4.8",
|
||||||
"nuxt-maplibre": "^1.2.2",
|
"nuxt-maplibre": "^1.2.2",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.38",
|
"vue": "^3.5.38",
|
||||||
"vue-router": "^5.1.0"
|
"vue-router": "^5.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user