feat: server-side checking
All checks were successful
deploy to cloudflare pages / deploy (push) Successful in 33s

This commit is contained in:
uku 2024-10-24 14:22:18 +02:00
parent 4b953ba898
commit b6ecbe45d4
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
4 changed files with 104 additions and 40 deletions

View file

@ -2,5 +2,34 @@ export interface GameData {
center: [number, number]; center: [number, number];
lines: [GeoJSON.Feature, string][]; lines: [GeoJSON.Feature, string][];
stopName: string; stopName: string;
stop: GeoJSON.Position; stopId: string;
}
export interface CheckData {
stopId: string;
latlng: [number, number];
}
export interface CheckResponse {
solution: [number, number];
distance: number;
score: number;
}
type FetchType = typeof fetch;
const linesUrl =
"https://data.grandlyon.com/geoserver/sytral/ows?SERVICE=WFS&VERSION=2.0.0&request=GetFeature&typename=sytral:tcl_sytral.tcllignemf_2_0_0&outputFormat=application/json&SRSNAME=EPSG:4171&sortBy=gid";
const stopsUrl =
"https://data.grandlyon.com/geoserver/sytral/ows?SERVICE=WFS&VERSION=2.0.0&request=GetFeature&typename=sytral:tcl_sytral.tclarret&outputFormat=application/json&SRSNAME=EPSG:4171&sortBy=gid";
let lazyLines: GeoJSON.FeatureCollection | null = null;
let lazyStops: GeoJSON.FeatureCollection | null = null;
export async function getLines(fetch: FetchType): Promise<GeoJSON.FeatureCollection> {
return lazyLines ?? (lazyLines = await fetch(linesUrl).then((r) => r.json()));
}
export async function getStops(fetch: FetchType): Promise<GeoJSON.FeatureCollection> {
return lazyStops ?? (lazyStops = await fetch(stopsUrl).then((r) => r.json()));
} }

View file

@ -6,6 +6,7 @@
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility"; import "leaflet-defaulticon-compatibility";
import type { CheckData, CheckResponse } from "$lib";
interface Props { interface Props {
data: PageData; data: PageData;
@ -16,7 +17,6 @@
const hidden = !$page.url.searchParams.has("debug"); const hidden = !$page.url.searchParams.has("debug");
const lignes = data.gameData.lines; const lignes = data.gameData.lines;
const point = L.latLng(data.gameData.stop[1], data.gameData.stop[0]);
const center = L.latLng(data.gameData.center); const center = L.latLng(data.gameData.center);
const linesJson = lignes.map(([feature, color]) => { const linesJson = lignes.map(([feature, color]) => {
@ -24,12 +24,10 @@
}); });
let latlng = $state(L.latLng(0, 0)); let latlng = $state(L.latLng(0, 0));
let distance = $derived(latlng.distanceTo(point)); let results: CheckResponse | null = $state(null);
let points = $derived(calculatePoints());
let lines = $state(true); let lines = $state(true);
let labels = $state(false); let labels = $state(false);
let submitted = $state(false);
let map: L.Map | null = $state(null); let map: L.Map | null = $state(null);
let playerMarker: L.Marker | null = $state(null); let playerMarker: L.Marker | null = $state(null);
@ -61,28 +59,29 @@
playerMarker = L.marker([0, 0]).addTo(map); playerMarker = L.marker([0, 0]).addTo(map);
map.on("click", (e) => { map.on("click", (e) => {
if (map && playerMarker && !submitted) { if (map && playerMarker && !results) {
playerMarker.setLatLng(e.latlng); playerMarker.setLatLng(e.latlng);
latlng = e.latlng; latlng = e.latlng;
} }
}); });
} }
function calculatePoints(): number { async function checkLocation() {
const lenientDistance = Math.max(0, distance - 20); const checkData: CheckData = {
const score = 5000 * Math.exp(-lenientDistance / 750); stopId: data.gameData.stopId,
latlng: [latlng.lat, latlng.lng],
};
let multiplier = 1; /* const response = await fetch("/api/check", {
if (lines) multiplier *= 0.5; method: "POST",
if (labels) multiplier *= 0.5; */ body: JSON.stringify(checkData),
});
return score * multiplier; if (response.ok && map) {
} const res: CheckResponse = await response.json();
function checkLocation() { L.marker(res.solution).bindPopup(data.gameData.stopName).addTo(map).openPopup();
submitted = true; results = res;
if (map) {
L.marker(point).bindPopup(data.gameData.stopName).addTo(map).openPopup();
} }
} }
</script> </script>
@ -90,16 +89,12 @@
<div class="container"> <div class="container">
<h1>{data.gameData.stopName}</h1> <h1>{data.gameData.stopName}</h1>
<div hidden={submitted}> <div hidden={results === null}>
<button onclick={checkLocation} disabled={latlng.lat === 0 || latlng.lng === 0}>SUBMIT</button> <button onclick={checkLocation} disabled={latlng.lat === 0 || latlng.lng === 0}>SUBMIT</button>
</div> </div>
<div class="results" hidden={!submitted}> <div class="results" hidden={!results}>
{Math.floor(points)} points! Vous etiez à {Math.floor(distance)}m. {Math.floor(results?.score ?? 0)} points! Vous etiez à {Math.floor(results?.distance ?? 0)}m.
</div>
<div {hidden}>
<span>distance: {distance}, points: {points}</span>
</div> </div>
<div {hidden}> <div {hidden}>

View file

@ -0,0 +1,51 @@
import { getStops, type CheckData, type CheckResponse } from "$lib";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ fetch, request }) => {
const stops = await getStops(fetch);
const data: CheckData = await request.json();
const stop = stops.features.find((f) => f.id === data.stopId);
if (stop) {
// GeoJSON data is LonLat, not LatLon
const coords = stop.geometry.type === "Point" ? stop.geometry.coordinates : [0, 0];
const latlng: [number, number] = [coords[1], coords[0]];
const distance = calcDistance(data.latlng, latlng);
const score = calculatePoints(distance);
const res: CheckResponse = { solution: latlng, distance, score };
return Response.json(res);
} else {
return new Response(null, { status: 404 });
}
};
function calculatePoints(distance: number): number {
const lenientDistance = Math.max(0, distance - 20);
const score = 5000 * Math.exp(-lenientDistance / 750);
const multiplier = 1; /*
if (lines) multiplier *= 0.5;
if (labels) multiplier *= 0.5; */
return score * multiplier;
}
// Mean Earth Radius, as recommended for use by
// the International Union of Geodesy and Geophysics,
// see https://rosettacode.org/wiki/Haversine_formula
const R = 6371000;
// https://github.com/Leaflet/Leaflet/blob/142f94a9ba5757f7e7180ffa6cbed2b3a9bc73c9/src/geo/crs/CRS.Earth.js#L23
function calcDistance(latlng1: [number, number], latlng2: [number, number]): number {
const rad = Math.PI / 180,
lat1 = latlng1[0] * rad,
lat2 = latlng2[0] * rad,
sinDLat = Math.sin(((latlng2[0] - latlng1[0]) * rad) / 2),
sinDLon = Math.sin(((latlng2[1] - latlng1[1]) * rad) / 2),
a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon,
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}

View file

@ -1,19 +1,9 @@
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
import type { GameData } from "$lib"; import { getLines, getStops, type GameData } from "$lib";
const linesUrl =
"https://data.grandlyon.com/geoserver/sytral/ows?SERVICE=WFS&VERSION=2.0.0&request=GetFeature&typename=sytral:tcl_sytral.tcllignemf_2_0_0&outputFormat=application/json&SRSNAME=EPSG:4171&sortBy=gid";
const stopsUrl =
"https://data.grandlyon.com/geoserver/sytral/ows?SERVICE=WFS&VERSION=2.0.0&request=GetFeature&typename=sytral:tcl_sytral.tclarret&outputFormat=application/json&SRSNAME=EPSG:4171&sortBy=gid";
let lazyLines: GeoJSON.FeatureCollection | null = null;
let lazyStops: GeoJSON.FeatureCollection | null = null;
export const GET: RequestHandler = async ({ fetch }) => { export const GET: RequestHandler = async ({ fetch }) => {
const lines: GeoJSON.FeatureCollection = const lines = await getLines(fetch);
lazyLines || (lazyLines = await fetch(linesUrl).then((r) => r.json())); const stops = await getStops(fetch);
const stops: GeoJSON.FeatureCollection =
lazyStops || (lazyStops = await fetch(stopsUrl).then((r) => r.json()));
const bbox = lines.bbox ?? [0, 0, 0, 0]; const bbox = lines.bbox ?? [0, 0, 0, 0];
const centerLat = (bbox[1] + bbox[3]) / 2; const centerLat = (bbox[1] + bbox[3]) / 2;
@ -42,13 +32,12 @@ export const GET: RequestHandler = async ({ fetch }) => {
); );
const randomStop = uniqueStops[Math.floor(Math.random() * uniqueStops.length)]; const randomStop = uniqueStops[Math.floor(Math.random() * uniqueStops.length)];
const coords = randomStop.geometry.type === "Point" ? randomStop.geometry.coordinates : [0, 0];
const data: GameData = { const data: GameData = {
center: [centerLat, centerLon], center: [centerLat, centerLon],
lines: lineColors, lines: lineColors,
stopName: randomStop.properties!.nom, stopName: randomStop.properties!.nom,
stop: coords, stopId: randomStop.id!.toString(),
}; };
return Response.json(data); return Response.json(data);