feat: server-side checking
All checks were successful
deploy to cloudflare pages / deploy (push) Successful in 33s
All checks were successful
deploy to cloudflare pages / deploy (push) Successful in 33s
This commit is contained in:
parent
4b953ba898
commit
b6ecbe45d4
4 changed files with 104 additions and 40 deletions
|
@ -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()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
51
src/routes/api/check/+server.ts
Normal file
51
src/routes/api/check/+server.ts
Normal 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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue