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