Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/app/api/schools/[name]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { schools, projects, yearlyTeacherParticipation } from "@/lib/schema";
import { eq, sql, and, sum } from "drizzle-orm";
import { findRegionOf } from "@/lib/region-finder";

export async function PATCH(
req: NextRequest,
Expand Down Expand Up @@ -50,9 +51,15 @@ export async function PATCH(
);
}

const region: string = findRegionOf(latitude, longitude);

await db
.update(schools)
.set({ latitude, longitude })
.set({
latitude: latitude,
longitude: longitude,
region: region as string,
})
.where(eq(schools.id, schoolResult[0].id));

return NextResponse.json({
Expand Down Expand Up @@ -139,6 +146,7 @@ export async function GET(
return NextResponse.json({
name: school.name,
town: school.town,
region: school.region,
latitude: school.latitude,
longitude: school.longitude,
studentCount: studentCount[0]?.total
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/schools/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export async function GET(req: NextRequest) {
return {
name: school.name,
city: school.city,
region: school.city, // Using city as region since schema only has 'town'
region: school.city,
instructionModel: "N/A", // Not in schema
implementationModel: "N/A", // Not in schema
numStudents: currStudents,
Expand Down
3 changes: 3 additions & 0 deletions src/app/api/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from "@/lib/schema";
import { requiredColumns } from "@/lib/required-spreadsheet-columns";
import { standardize } from "@/lib/school-name-standardize";
import { findRegionOf } from "@/lib/region-finder";

type RowData = Array<string | number | boolean | null>;

Expand Down Expand Up @@ -124,6 +125,7 @@ export async function POST(req: NextRequest) {
if (!school) {
// Get coordinates from frontend matching
const coords = coordsMap.get(schoolIdValue);
const region = findRegionOf(coords?.lat, coords?.long);

const [inserted] = await db
.insert(schools)
Expand All @@ -134,6 +136,7 @@ export async function POST(req: NextRequest) {
town: schoolTown,
latitude: coords?.lat ?? null,
longitude: coords?.long ?? null,
region: region,
})
.returning();
school = inserted;
Expand Down
1 change: 1 addition & 0 deletions src/app/schools/[name]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type SchoolData = {
firstYear: string;
projects: ProjectRow[];
instructionalModel: string;
region: string;
};

type MapCoordinates = {
Expand Down
143 changes: 143 additions & 0 deletions src/lib/region-finder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/***************************************************************
*
* region-finder.ts
*
* Author: Zander & Chiara
* Date: 03/25/2026
*
* Summary: Main raycasting method for determining
* the region of a long/lat coordinate.
*
**************************************************************/

/**
* Flow:
* 1. Validate that latitude and longitude are provided
* 2. Load all regions from the JSON data file
* 3. Run point-in-polygon raycasting for each region
* 4. Return the name of the matching region, or "" if none
*/

import rawData from "@/data/regions.json";

/**
* Represents a geographic point.
*/
export type Coordinate = {
lat: number;
long: number;
};

/**
* Represents a named geographic region defined by a polygon boundary.
*/
export type Region = {
name: string;
fips: string;
polygon: Array<Coordinate>;
};

/**
* Raw shape of a region entry as stored in regions.json.
*/
type RawRegion = {
name: string;
fips: string;
coordinates: number[][];
};

/**
* Determines which region a given coordinate falls within.
*
* Iterates over all known regions and uses point-in-polygon
* raycasting to find the first match.
*
* @param latitude Latitude of the point to locate
* @param longitude Longitude of the point to locate
* @returns The name of the containing region, or "" if unresolved
*/
export function findRegionOf(
latitude: number | null | undefined,
longitude: number | null | undefined,
): string {
if (!latitude || !longitude) {
return "";
}

const point: Coordinate = { lat: latitude, long: longitude };
const regions: Array<Region> = loadRegions();

let region_in: string = "";

for (const region of regions) {
if (checkForCollision(point, region)) {
region_in = region.name;
break;
}
}

return region_in;
}

/**
* Parses the raw regions JSON into typed Region objects.
*
* Remaps GeoJSON-style [long, lat] coordinate pairs into
* the Coordinate format used throughout this module.
*
* @returns Array of Region objects with normalized polygon coordinates
*/
function loadRegions(): Array<Region> {
const regions: Region[] = Object.values(
rawData as Record<string, RawRegion>,
).map((r) => ({
name: r.name,
fips: r.fips,
polygon: r.coordinates.map(([long, lat]) => ({
lat,
long,
})),
}));

return regions;
}

/**
* Determines whether a coordinate falls inside a region's polygon
* using the ray casting algorithm.
*
* Casts a ray from the point and counts edge crossings. An odd
* number of crossings indicates the point is inside the polygon.
*
* @param coord The coordinate to test
* @param region The region whose polygon boundary is checked
* @returns True if the coordinate is inside the region, false otherwise
*/
function checkForCollision(coord: Coordinate, region: Region): boolean {
let inside: boolean = false;
const lat: number = coord.lat;
const long: number = coord.long;

for (
let i = 0, j = region.polygon.length - 1;
i < region.polygon.length;
j = i++
) {
// Check one edge at a time
const lat_i: number = region.polygon[i].lat;
const long_i: number = region.polygon[i].long;

const lat_j: number = region.polygon[j].lat;
const long_j: number = region.polygon[j].long;

const intersects =
long_i > long !== long_j > long &&
lat <
((lat_j - lat_i) * (long - long_i)) / (long_j - long_i) + lat_i;

if (intersects) {
inside = !inside;
}
}
return inside;
}
1 change: 1 addition & 0 deletions src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const schools = pgTable("schools", {
longitude: doublePrecision("longitude"),
zipcode: text("zipcode"),
gateway: boolean("gateway").default(false).notNull(),
region: text("region").default("").notNull(),
});

// Ties a school to the years it has participated
Expand Down