Compare commits
3 Commits
395c837dd1
...
8b9ba7f81b
Author | SHA1 | Date |
---|---|---|
Vegard Berg | 8b9ba7f81b | |
Vegard Berg | 52c38966c4 | |
Vegard Berg | bd2be948eb |
|
@ -11,6 +11,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.3",
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^1.0.4",
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
||||
"svelte": "^4.1.2",
|
||||
|
@ -21,6 +22,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@urql/svelte": "^4.0.4",
|
||||
"geojson": "^0.5.0",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-ws": "^5.14.1",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
import { Client, setContextClient, cacheExchange, fetchExchange, subscriptionExchange } from '@urql/svelte';
|
||||
import { createClient as createWsClient } from 'graphql-ws';
|
||||
import { SubscriptionClient } from 'subscriptions-transport-ws';
|
||||
import { createJourneyPlannerClientContext, createVehiclesClientContext } from './lib/entur';
|
||||
|
||||
let subscriptionClient = new SubscriptionClient('wss://api.entur.io/realtime/v1/vehicles/subscriptions', { reconnect: true})
|
||||
createVehiclesClientContext();
|
||||
createJourneyPlannerClientContext();
|
||||
|
||||
/*
|
||||
let wsClient = createWsClient({
|
||||
url: 'wss://api.entur.io/realtime/v1/vehicles/subscriptions'
|
||||
})
|
||||
*/
|
||||
let subscriptionClient = new SubscriptionClient('wss://api.entur.io/realtime/v1/vehicles/subscriptions', { reconnect: true})
|
||||
|
||||
let client = new Client({
|
||||
url: 'https://api.entur.io/realtime/v1/vehicles/graphql',
|
||||
|
@ -20,21 +19,13 @@
|
|||
subscriptionExchange({
|
||||
forwardSubscription(request) {
|
||||
return subscriptionClient.request(request);
|
||||
/*
|
||||
const input = { ...request, query: request.query || "" };
|
||||
return {
|
||||
subscribe(sink) {
|
||||
const unsubscribe = wsClient.subscribe(input, sink);
|
||||
return { unsubscribe };
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
})
|
||||
],
|
||||
})
|
||||
|
||||
setContextClient(client)
|
||||
*/
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
|
|
@ -3,10 +3,6 @@
|
|||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
<script lang="ts">
|
||||
import { GeoJSON, LineLayer, MarkerLayer } from "svelte-maplibre";
|
||||
import { getJourneyPlannerClientContext, quaysToGeoJSON } from "./entur";
|
||||
import { gql, queryStore } from "@urql/svelte";
|
||||
import type { GeoJSON as IGeoJSON } from "geojson";
|
||||
|
||||
export let lineRef: string | null = null;
|
||||
console.log(lineRef + "hi")
|
||||
|
||||
$: queryResult = queryStore({
|
||||
client: getJourneyPlannerClientContext(),
|
||||
query: gql`
|
||||
query ($line: ID!) {
|
||||
line(id: $line) {
|
||||
publicCode
|
||||
name
|
||||
journeyPatterns {
|
||||
directionType
|
||||
quays {
|
||||
longitude
|
||||
latitude
|
||||
name
|
||||
publicCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { line: lineRef }
|
||||
})
|
||||
|
||||
let data: IGeoJSON;
|
||||
|
||||
$: if ($queryResult.data) {
|
||||
data = quaysToGeoJSON(...$queryResult.data.line.journeyPatterns.map((x: any) => x.quays)) as IGeoJSON
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
z-index: 1000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
code {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
left: 5em;
|
||||
right: 5em;
|
||||
max-height: 10em;
|
||||
overflow-y: scroll;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if lineRef != null}
|
||||
<GeoJSON data={data}>
|
||||
<LineLayer
|
||||
|
||||
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
||||
paint={{
|
||||
'line-width': 5,
|
||||
'line-dasharray': [5, 2],
|
||||
'line-color': '#008800',
|
||||
'line-opacity': 0.8,
|
||||
}}
|
||||
/>
|
||||
<MarkerLayer applyToClusters={false} let:feature>
|
||||
<span>{feature.properties?.name ?? "N/A"}</span>
|
||||
</MarkerLayer>
|
||||
</GeoJSON>
|
||||
{/if}
|
|
@ -1,18 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { Control, ControlButton, ControlGroup, MapLibre, Marker, NavigationControl, Popup, mapContext } from 'svelte-maplibre';
|
||||
import { Control, ControlButton, ControlGroup, LineLayer, MapLibre, Marker, NavigationControl, Popup, mapContext, GeoJSON, MarkerLayer } from 'svelte-maplibre';
|
||||
import VehicleMarker from "./VehicleMarker.svelte"
|
||||
import {writable} from 'svelte/store';
|
||||
import {writable, type Writable} from 'svelte/store';
|
||||
import { queryStore, gql, getContextClient, subscriptionStore } from '@urql/svelte';
|
||||
import { getJourneyPlannerClientContext, getVehiclesClientContext, quaysToGeoJSON } from './entur';
|
||||
import { buses, selectedLine } from './store';
|
||||
import LineMarker from './LineMarker.svelte';
|
||||
import type { GeoJSON as IGeoJSON } from 'geojson';
|
||||
|
||||
interface Vehicle {
|
||||
vehicleId: string;
|
||||
location: {longitude: number, latitude: number};
|
||||
bearing: number;
|
||||
line: {lineRef: string, lineName: string, publicCode: string};
|
||||
originName: string;
|
||||
destinationName: string;
|
||||
occupancy: string;
|
||||
delay: number;
|
||||
vehicleStatus: string;
|
||||
}
|
||||
|
||||
$: bearing = 0.0;
|
||||
setInterval(() => bearing = 0, 100)
|
||||
|
||||
$: iconSize = 30;
|
||||
$: iconSize = 35;
|
||||
|
||||
let dataStore = writable({});
|
||||
let dataStore: Writable<Record<string, Vehicle>> = writable({});
|
||||
|
||||
let vehiclesInit = queryStore({
|
||||
client: getContextClient(),
|
||||
client: getVehiclesClientContext(),
|
||||
query: gql`
|
||||
query {
|
||||
vehicles(codespaceId:"SKY") {
|
||||
|
@ -41,7 +57,7 @@ import { queryStore, gql, getContextClient, subscriptionStore } from '@urql/svel
|
|||
|
||||
vehiclesInit.subscribe((v) => {
|
||||
if (v.data) {
|
||||
for (const veh of v.data.vehicles.filter(x => ["AT_ORIGIN", "IN_PROGRESS", "OFF_ROUTE"].includes(x.vehicleStatus))) {
|
||||
for (const veh of v.data.vehicles.filter((x: Vehicle) => ["AT_ORIGIN", "IN_PROGRESS", "OFF_ROUTE"].includes(x.vehicleStatus))) {
|
||||
dataStore.update((d: any) => {
|
||||
d[veh.vehicleId] = veh;
|
||||
return d;
|
||||
|
@ -51,10 +67,10 @@ vehiclesInit.subscribe((v) => {
|
|||
})
|
||||
|
||||
const vehicles = subscriptionStore({
|
||||
client: getContextClient(),
|
||||
client: getVehiclesClientContext(),
|
||||
query: gql`
|
||||
subscription {
|
||||
vehicles(codespaceId:"SKY") {
|
||||
vehicles(codespaceId: "SKY") {
|
||||
vehicleId
|
||||
line {
|
||||
lineRef
|
||||
|
@ -85,10 +101,44 @@ vehiclesInit.subscribe((v) => {
|
|||
}
|
||||
});
|
||||
|
||||
$: queryResult = queryStore({
|
||||
client: getJourneyPlannerClientContext(),
|
||||
query: gql`
|
||||
query ($line: ID!) {
|
||||
line(id: $line) {
|
||||
publicCode
|
||||
name
|
||||
journeyPatterns {
|
||||
directionType
|
||||
quays {
|
||||
longitude
|
||||
latitude
|
||||
name
|
||||
publicCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { line: $selectedLine }
|
||||
})
|
||||
|
||||
let data: IGeoJSON;
|
||||
|
||||
$: if ($queryResult.data) {
|
||||
data = quaysToGeoJSON(...$queryResult.data.line.journeyPatterns.map((x: any) => x.quays)) as IGeoJSON
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div.quay {
|
||||
z-index: -1;
|
||||
padding: 0.1em;
|
||||
background-color: #baddad;
|
||||
border: 1px dotted #333;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
|
@ -112,14 +162,34 @@ vehiclesInit.subscribe((v) => {
|
|||
<ControlButton on:click={() => iconSize = 20}>S</ControlButton>
|
||||
</ControlGroup>
|
||||
</Control>
|
||||
<Control position="bottom-right">
|
||||
<ControlGroup>
|
||||
<ControlButton>{$buses}</ControlButton>
|
||||
</ControlGroup>
|
||||
</Control>
|
||||
|
||||
{#if $selectedLine != null}
|
||||
<p id="foo">
|
||||
<GeoJSON {data}>
|
||||
<LineLayer
|
||||
paint={{'line-width': 5, 'line-color': 'orange'}}
|
||||
beforeLayerType={(l) => {console.log(l.type); return false}}
|
||||
/>
|
||||
<MarkerLayer let:feature>
|
||||
<div class="quay">{feature.properties?.name ?? ""}</div>
|
||||
</MarkerLayer>
|
||||
</GeoJSON>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if $vehicles}
|
||||
{#each Object.entries($dataStore) as [id, vehicle]}
|
||||
<VehicleMarker
|
||||
lngLat={[vehicle.location.longitude, vehicle.location.latitude]}
|
||||
lineRef={vehicle.line.lineRef}
|
||||
lineNumber={vehicle.line.publicCode}
|
||||
lineName={vehicle.line.lineName}
|
||||
bearing={vehicle.bearing + map?.getBearing() % 360}
|
||||
bearing={vehicle.bearing}
|
||||
origin={vehicle.originName}
|
||||
destination={vehicle.destinationName}
|
||||
iconSize={iconSize}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { Marker, Popup } from 'svelte-maplibre';
|
||||
import Delay from './micro/Delay.svelte';
|
||||
import Bus from './micro/Bus.svelte';
|
||||
import { buses, selectedLine } from './store';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let lngLat: [number, number];
|
||||
export let lineRef: string | null = null;
|
||||
export let lineNumber: string;
|
||||
export let lineName: string = "N/A";
|
||||
export let bearing: number = 0.0;
|
||||
|
@ -11,34 +16,43 @@ export let iconSize: number = 30;
|
|||
export let delay: number = 0;
|
||||
export let occupancy: string = "Unknown";
|
||||
|
||||
console.log("lineNumber: %s", lineNumber)
|
||||
$buses += 1
|
||||
onDestroy(() => {
|
||||
$buses -= 1
|
||||
})
|
||||
|
||||
|
||||
const clickHandler = () => {
|
||||
if ($selectedLine == lineRef)
|
||||
selectedLine.set(null)
|
||||
else
|
||||
selectedLine.set(lineRef)
|
||||
console.log("click\t%s", $selectedLine)
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
svg text {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
color: black;
|
||||
background-color: white;;
|
||||
}
|
||||
|
||||
div.popover-content {
|
||||
color: black;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<Marker {lngLat}>
|
||||
<svg width={iconSize} height={iconSize} viewBox="0 0 50 50">
|
||||
<g transform="rotate({bearing}, 25, 25)">
|
||||
<circle cx="25" cy="25" r="24" stroke="black" stroke-width="3" fill="red" />
|
||||
<circle cx="25" cy="5" r="5" stroke="black" stroke-width="1.5" fill="white" />
|
||||
</g>
|
||||
<text x="25" y="32.5" text-anchor="middle">{lineNumber ?? "🚍"}</text>
|
||||
</svg>
|
||||
|
||||
<Marker {lngLat} on:click={clickHandler}>
|
||||
<Bus {iconSize} {bearing} face={lineNumber} />
|
||||
|
||||
<Popup openOn="hover" offset={[0, -10]}>
|
||||
<h4>{lineNumber}: {lineName}</h4>
|
||||
<p>Going from <i>{origin}</i> to <i>{destination}</i></p>
|
||||
<ul>
|
||||
<li>Occupancy: {occupancy}</li>
|
||||
<li>Delay: {delay}</li>
|
||||
</ul>
|
||||
<div class="popover-content">
|
||||
<h4>{lineNumber}: {lineName}</h4>
|
||||
<p>
|
||||
Going from <i>{origin}</i> to <i>{destination}</i>
|
||||
<br />
|
||||
<Delay {delay} />
|
||||
<br />
|
||||
{occupancy}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</Popup>
|
||||
</Marker>
|
|
@ -0,0 +1,83 @@
|
|||
import { Client, cacheExchange, fetchExchange, subscriptionExchange } from "@urql/svelte";
|
||||
import { SubscriptionClient } from "subscriptions-transport-ws";
|
||||
import { setContext, getContext } from "svelte";
|
||||
|
||||
|
||||
export function createVehiclesClientContext() {
|
||||
let subscriptionClient = new SubscriptionClient('wss://api.entur.io/realtime/v1/vehicles/subscriptions', { reconnect: true})
|
||||
|
||||
let client = new Client({
|
||||
url: 'https://api.entur.io/realtime/v1/vehicles/graphql',
|
||||
exchanges: [
|
||||
fetchExchange,
|
||||
cacheExchange,
|
||||
subscriptionExchange({
|
||||
forwardSubscription(request) {
|
||||
return subscriptionClient.request(request);
|
||||
}
|
||||
})
|
||||
],
|
||||
})
|
||||
|
||||
setContext("entur-graphql-vehiclepositions-client", client)
|
||||
}
|
||||
|
||||
export function getVehiclesClientContext(): Client {
|
||||
return getContext("entur-graphql-vehiclepositions-client")
|
||||
}
|
||||
|
||||
export function createJourneyPlannerClientContext() {
|
||||
let client = new Client({
|
||||
url: 'https://api.entur.io/journey-planner/v3/graphql',
|
||||
exchanges: [
|
||||
fetchExchange,
|
||||
cacheExchange,
|
||||
]
|
||||
})
|
||||
|
||||
setContext("entur-graphql-journeyplannet-client", client)
|
||||
}
|
||||
|
||||
export function getJourneyPlannerClientContext(): Client {
|
||||
return getContext("entur-graphql-journeyplannet-client")
|
||||
}
|
||||
|
||||
|
||||
export interface Quay {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
name: string;
|
||||
publicCode: string;
|
||||
}
|
||||
|
||||
export function quaysToGeoJSON(...quayCollections: Array<Array<Quay>>) {
|
||||
const multilineString = {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: "MultiLineString",
|
||||
coordinates: quayCollections.map((quays => quays.map(quay => [quay.longitude, quay.latitude]))),
|
||||
},
|
||||
}
|
||||
const features = quayCollections.map(quays =>
|
||||
quays.map(quay => {
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
name: quay.name + (quay.publicCode ? (" " + quay.publicCode) : "")
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [quay.longitude, quay.latitude]
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
multilineString,
|
||||
...features.flat(1)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
export let iconSize = 35;
|
||||
export let bearing = 0;
|
||||
export let face: string = "";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
z-index: -1000;
|
||||
}
|
||||
text {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
color: darkgray;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svg width={iconSize * 1.2} height={iconSize * 1.2} viewBox="-10 -10 70 70">
|
||||
<g transform="rotate({bearing}, 25, 25)">
|
||||
<!--
|
||||
<circle cx="25" cy="25" r="24" stroke="black" stroke-width="3" fill="red" />
|
||||
<circle cx="25" cy="5" r="5" stroke="black" stroke-width="1.5" fill="white" />
|
||||
-->
|
||||
<path d="M 25 0 l -25 10 v 40 l 25 -10 l 25 10 v -40 l -25 -10" fill="green" stroke="black" stroke-width="3" />
|
||||
</g>
|
||||
<text x="25" y="32.5" text-anchor="middle" fill="white" stroke="black" stroke-width="0.5">{face ?? "🚍"}</text>
|
||||
</svg>
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
/** The delay in seconds.*/
|
||||
export let delay: number;
|
||||
|
||||
let delay_min = Math.round(delay / 60);
|
||||
let delay_abs = Math.abs(delay_min);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
The bus is
|
||||
{#if delay_min == 1}
|
||||
<span>one minute delayed.</span>
|
||||
{:else if delay_min > 1}
|
||||
<span>{delay_abs} minutes delayed.</span>
|
||||
{:else if delay_min == -1}
|
||||
<span>one minute ahead of schedule.</span>
|
||||
{:else if delay_min < -1}
|
||||
<span>{delay_abs} minutes ahead of schedule.</span>
|
||||
{:else}
|
||||
<span>on time.</span>
|
||||
{/if}
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
export const buses = writable(0);
|
||||
|
||||
export const selectedLine: Writable<string|null> = writable(null);
|
|
@ -4,4 +4,11 @@ export default {
|
|||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
vitePlugin: {
|
||||
inspector: {
|
||||
toggleKeyCombo: 'meta-shift',
|
||||
showToggleButton: 'always',
|
||||
toggleButtonPos: 'bottom-right'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import basicSsl from '@vitejs/plugin-basic-ssl'
|
||||
import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
svelte(),
|
||||
//basicSsl(),
|
||||
/*svelteInspector({
|
||||
showToggleButton: 'always',
|
||||
toggleButtonPos: 'top-right',
|
||||
openKey: 'meta-shift'
|
||||
})*/
|
||||
],
|
||||
base: "",
|
||||
})
|
||||
|
|
|
@ -232,7 +232,7 @@
|
|||
|
||||
"@sveltejs/vite-plugin-svelte-inspector@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz#c99fcb73aaa845a3e2c0563409aeb3ee0b863add"
|
||||
integrity sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
|
@ -594,6 +594,11 @@ geojson-vt@^3.2.1:
|
|||
resolved "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz"
|
||||
integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==
|
||||
|
||||
geojson@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0"
|
||||
integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==
|
||||
|
||||
get-stream@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz"
|
||||
|
|
Loading…
Reference in New Issue