Merge pull request 'dont use postgis for animaldex' (#30) from dev into staging
Reviewed-on: #30
This commit is contained in:
@@ -178,10 +178,6 @@ export const animals = pgTable('animals', {
|
|||||||
deniedAt: timestamp('denied_at', { withTimezone: true })
|
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', {
|
export const sightings = pgTable('sightings', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
animalId: uuid('animal_id')
|
animalId: uuid('animal_id')
|
||||||
@@ -193,7 +189,6 @@ export const sightings = pgTable('sightings', {
|
|||||||
seenAt: timestamp('seen_at', { withTimezone: true }).notNull(),
|
seenAt: timestamp('seen_at', { withTimezone: true }).notNull(),
|
||||||
lat: doublePrecision('lat').notNull(),
|
lat: doublePrecision('lat').notNull(),
|
||||||
lng: doublePrecision('lng').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(),
|
submittedAt: timestamp('submitted_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
acceptedAt: timestamp('accepted_at', { withTimezone: true }),
|
acceptedAt: timestamp('accepted_at', { withTimezone: true }),
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
if (filterFrom) params.set('fromDate', new Date(filterFrom).toISOString());
|
if (filterFrom) params.set('fromDate', new Date(filterFrom).toISOString());
|
||||||
if (filterTo) params.set('toDate', new Date(filterTo).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;
|
if (!res.ok) return;
|
||||||
const geojson: { features: GeoJSONFeature[] } = await res.json();
|
const geojson: { features: GeoJSONFeature[] } = await res.json();
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,22 @@ import type { RequestHandler } from './$types';
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { animals, sightings, photos } from '$lib/server/db/schema';
|
import { animals, sightings, photos } from '$lib/server/db/schema';
|
||||||
import { addSightingSchema } from '$lib/validation/sighting';
|
import { addSightingSchema } from '$lib/validation/sighting';
|
||||||
import { eq, sql } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
const MAX_DISTANCE_METERS = 30_000;
|
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 }) => {
|
export const POST: RequestHandler = async ({ request, params }) => {
|
||||||
const body = await request.json().catch(() => null);
|
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' });
|
throw error(404, { message: 'Animal not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [distRow] = await db.execute(sql`
|
const dist = distanceMeters(data.deviceLat, data.deviceLng, data.lat, data.lng);
|
||||||
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);
|
|
||||||
if (dist > MAX_DISTANCE_METERS) {
|
if (dist > MAX_DISTANCE_METERS) {
|
||||||
throw error(422, {
|
throw error(422, {
|
||||||
message: `Location is ${Math.round(dist / 1000)}km from your device. Maximum is 30km.`
|
message: `Location is ${Math.round(dist / 1000)}km from your device. Maximum is 30km.`
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ export const GET: RequestHandler = async ({ url }) => {
|
|||||||
isNull(animals.deniedAt),
|
isNull(animals.deniedAt),
|
||||||
isNotNull(sightings.acceptedAt),
|
isNotNull(sightings.acceptedAt),
|
||||||
isNull(sightings.deniedAt),
|
isNull(sightings.deniedAt),
|
||||||
sql`ST_Within(
|
gte(sightings.lat, q.minLat),
|
||||||
${sightings}.location::geometry,
|
lte(sightings.lat, q.maxLat),
|
||||||
ST_MakeEnvelope(${q.minLng}, ${q.minLat}, ${q.maxLng}, ${q.maxLat}, 4326)
|
gte(sightings.lng, q.minLng),
|
||||||
)`
|
lte(sightings.lng, q.maxLng)
|
||||||
];
|
];
|
||||||
|
|
||||||
if (q.species) conditions.push(ilike(animals.species, `%${q.species}%`));
|
if (q.species) conditions.push(ilike(animals.species, `%${q.species}%`));
|
||||||
|
|||||||
Reference in New Issue
Block a user