diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index cfb548f..5dcc73a 100755 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -178,10 +178,6 @@ export const animals = pgTable('animals', { deniedAt: timestamp('denied_at', { withTimezone: true }) }); -// The PostGIS `location geography(Point, 4326)` column is managed outside Drizzle -// via a trigger in the migration — it auto-syncs from lat/lng on insert/update. -// Never write to `location` directly from application code. - export const sightings = pgTable('sightings', { id: uuid('id').primaryKey().defaultRandom(), animalId: uuid('animal_id') @@ -193,7 +189,6 @@ export const sightings = pgTable('sightings', { seenAt: timestamp('seen_at', { withTimezone: true }).notNull(), lat: doublePrecision('lat').notNull(), lng: doublePrecision('lng').notNull(), - // `location` geography column exists in DB but not mapped in Drizzle — use sql`` for spatial queries submittedAt: timestamp('submitted_at', { withTimezone: true }).notNull().defaultNow(), acceptedAt: timestamp('accepted_at', { withTimezone: true }), diff --git a/src/routes/projects/animaldex/+page.svelte b/src/routes/projects/animaldex/+page.svelte index 46151c5..d8f9677 100644 --- a/src/routes/projects/animaldex/+page.svelte +++ b/src/routes/projects/animaldex/+page.svelte @@ -122,7 +122,7 @@ if (filterFrom) params.set('fromDate', new Date(filterFrom).toISOString()); if (filterTo) params.set('toDate', new Date(filterTo).toISOString()); - const res = await fetch(`/api/map?${params}`); + const res = await fetch(`/projects/animaldex/api/map?${params}`); if (!res.ok) return; const geojson: { features: GeoJSONFeature[] } = await res.json(); diff --git a/src/routes/projects/animaldex/api/animals/[id]/sightings/+server.ts b/src/routes/projects/animaldex/api/animals/[id]/sightings/+server.ts index 9a0c875..eb9ff6e 100644 --- a/src/routes/projects/animaldex/api/animals/[id]/sightings/+server.ts +++ b/src/routes/projects/animaldex/api/animals/[id]/sightings/+server.ts @@ -3,10 +3,22 @@ import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { animals, sightings, photos } from '$lib/server/db/schema'; import { addSightingSchema } from '$lib/validation/sighting'; -import { eq, sql } from 'drizzle-orm'; +import { eq } from 'drizzle-orm'; import { env } from '$env/dynamic/private'; const MAX_DISTANCE_METERS = 30_000; +const EARTH_RADIUS_METERS = 6_371_000; + +// Haversine distance between two lat/lng points, in meters. +function distanceMeters(lat1: number, lng1: number, lat2: number, lng2: number): number { + const toRad = (deg: number) => (deg * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return 2 * EARTH_RADIUS_METERS * Math.asin(Math.sqrt(a)); +} export const POST: RequestHandler = async ({ request, params }) => { const body = await request.json().catch(() => null); @@ -28,14 +40,7 @@ export const POST: RequestHandler = async ({ request, params }) => { throw error(404, { message: 'Animal not found' }); } - const [distRow] = await db.execute(sql` - SELECT ST_Distance( - ST_MakePoint(${data.deviceLng}, ${data.deviceLat})::geography, - ST_MakePoint(${data.lng}, ${data.lat})::geography - ) AS dist - `); - - const dist = Number((distRow as Record).dist); + const dist = distanceMeters(data.deviceLat, data.deviceLng, data.lat, data.lng); if (dist > MAX_DISTANCE_METERS) { throw error(422, { message: `Location is ${Math.round(dist / 1000)}km from your device. Maximum is 30km.` diff --git a/src/routes/projects/animaldex/api/map/+server.ts b/src/routes/projects/animaldex/api/map/+server.ts index d3d1914..7bee728 100644 --- a/src/routes/projects/animaldex/api/map/+server.ts +++ b/src/routes/projects/animaldex/api/map/+server.ts @@ -20,10 +20,10 @@ export const GET: RequestHandler = async ({ url }) => { isNull(animals.deniedAt), isNotNull(sightings.acceptedAt), isNull(sightings.deniedAt), - sql`ST_Within( - ${sightings}.location::geometry, - ST_MakeEnvelope(${q.minLng}, ${q.minLat}, ${q.maxLng}, ${q.maxLat}, 4326) - )` + gte(sightings.lat, q.minLat), + lte(sightings.lat, q.maxLat), + gte(sightings.lng, q.minLng), + lte(sightings.lng, q.maxLng) ]; if (q.species) conditions.push(ilike(animals.species, `%${q.species}%`));