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];
|
||||
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()));
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
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 { 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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue