dont use postgis for animaldex

This commit is contained in:
2026-09-17 05:00:59 +00:00
parent dff5c98e37
commit 247e65324e
4 changed files with 19 additions and 19 deletions

View File

@@ -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 }),

View File

@@ -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();

View File

@@ -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<string, unknown>).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.`

View File

@@ -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}%`));