refactor: streamline SQL migration scripts and enhance schema management for units and incidents

This commit is contained in:
vergiLgood1 2025-05-06 15:37:07 +07:00
parent e891df87d0
commit 969d10958c
12 changed files with 564 additions and 91 deletions

View File

@ -68,60 +68,51 @@ CREATE TRIGGER on_auth_user_deleted BEFORE DELETE ON auth.users FOR EACH ROW EXE
CREATE TRIGGER on_auth_user_updated AFTER UPDATE ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_user_update(); CREATE TRIGGER on_auth_user_updated AFTER UPDATE ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_user_update();
drop trigger if exists "objects_delete_delete_prefix" on "storage"."objects"; drop trigger if exists "objects_delete_delete_prefix" on "storage"."objects";
drop trigger if exists "objects_insert_create_prefix" on "storage"."objects"; drop trigger if exists "objects_insert_create_prefix" on "storage"."objects";
drop trigger if exists "objects_update_create_prefix" on "storage"."objects"; drop trigger if exists "objects_update_create_prefix" on "storage"."objects";
drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes"; DO $$
BEGIN
IF EXISTS (SELECT FROM pg_catalog.pg_tables
WHERE schemaname = 'storage'
AND tablename = 'prefixes') THEN
drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes"; EXECUTE 'drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes"';
EXECUTE 'drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes"';
revoke delete on table "storage"."prefixes" from "anon"; EXECUTE 'revoke delete on table "storage"."prefixes" from "anon"';
EXECUTE 'revoke insert on table "storage"."prefixes" from "anon"';
EXECUTE 'revoke references on table "storage"."prefixes" from "anon"';
EXECUTE 'revoke select on table "storage"."prefixes" from "anon"';
EXECUTE 'revoke trigger on table "storage"."prefixes" from "anon"';
EXECUTE 'revoke truncate on table "storage"."prefixes" from "anon"';
EXECUTE 'revoke update on table "storage"."prefixes" from "anon"';
revoke insert on table "storage"."prefixes" from "anon"; EXECUTE 'revoke delete on table "storage"."prefixes" from "authenticated"';
EXECUTE 'revoke insert on table "storage"."prefixes" from "authenticated"';
EXECUTE 'revoke references on table "storage"."prefixes" from "authenticated"';
EXECUTE 'revoke select on table "storage"."prefixes" from "authenticated"';
EXECUTE 'revoke trigger on table "storage"."prefixes" from "authenticated"';
EXECUTE 'revoke truncate on table "storage"."prefixes" from "authenticated"';
EXECUTE 'revoke update on table "storage"."prefixes" from "authenticated"';
revoke references on table "storage"."prefixes" from "anon"; EXECUTE 'revoke delete on table "storage"."prefixes" from "service_role"';
EXECUTE 'revoke insert on table "storage"."prefixes" from "service_role"';
EXECUTE 'revoke references on table "storage"."prefixes" from "service_role"';
EXECUTE 'revoke select on table "storage"."prefixes" from "service_role"';
EXECUTE 'revoke trigger on table "storage"."prefixes" from "service_role"';
EXECUTE 'revoke truncate on table "storage"."prefixes" from "service_role"';
EXECUTE 'revoke update on table "storage"."prefixes" from "service_role"';
revoke select on table "storage"."prefixes" from "anon"; EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_bucketId_fkey"';
EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_pkey"';
revoke trigger on table "storage"."prefixes" from "anon"; EXECUTE 'drop table "storage"."prefixes"';
END IF;
revoke truncate on table "storage"."prefixes" from "anon"; END $$;
revoke update on table "storage"."prefixes" from "anon";
revoke delete on table "storage"."prefixes" from "authenticated";
revoke insert on table "storage"."prefixes" from "authenticated";
revoke references on table "storage"."prefixes" from "authenticated";
revoke select on table "storage"."prefixes" from "authenticated";
revoke trigger on table "storage"."prefixes" from "authenticated";
revoke truncate on table "storage"."prefixes" from "authenticated";
revoke update on table "storage"."prefixes" from "authenticated";
revoke delete on table "storage"."prefixes" from "service_role";
revoke insert on table "storage"."prefixes" from "service_role";
revoke references on table "storage"."prefixes" from "service_role";
revoke select on table "storage"."prefixes" from "service_role";
revoke trigger on table "storage"."prefixes" from "service_role";
revoke truncate on table "storage"."prefixes" from "service_role";
revoke update on table "storage"."prefixes" from "service_role";
alter table "storage"."prefixes" drop constraint "prefixes_bucketId_fkey";
drop function if exists "storage"."add_prefixes"(_bucket_id text, _name text); drop function if exists "storage"."add_prefixes"(_bucket_id text, _name text);
@ -147,21 +138,41 @@ drop function if exists "storage"."search_v1_optimised"(prefix text, bucketname
drop function if exists "storage"."search_v2"(prefix text, bucket_name text, limits integer, levels integer, start_after text); drop function if exists "storage"."search_v2"(prefix text, bucket_name text, limits integer, levels integer, start_after text);
alter table "storage"."prefixes" drop constraint "prefixes_pkey"; DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_name_bucket_level_unique') THEN
EXECUTE 'drop index "storage"."idx_name_bucket_level_unique"';
END IF;
drop index if exists "storage"."idx_name_bucket_level_unique"; IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_objects_lower_name') THEN
EXECUTE 'drop index "storage"."idx_objects_lower_name"';
END IF;
drop index if exists "storage"."idx_objects_lower_name"; IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_prefixes_lower_name') THEN
EXECUTE 'drop index "storage"."idx_prefixes_lower_name"';
END IF;
drop index if exists "storage"."idx_prefixes_lower_name"; IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'objects_bucket_id_level_idx') THEN
EXECUTE 'drop index "storage"."objects_bucket_id_level_idx"';
END IF;
drop index if exists "storage"."objects_bucket_id_level_idx"; IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'prefixes_pkey') THEN
EXECUTE 'drop index "storage"."prefixes_pkey"';
END IF;
END $$;
drop index if exists "storage"."prefixes_pkey"; DO $$
BEGIN
drop table "storage"."prefixes"; IF EXISTS (
SELECT 1
alter table "storage"."objects" drop column "level"; FROM information_schema.columns
WHERE table_schema = 'storage'
AND table_name = 'objects'
AND column_name = 'level'
) THEN
EXECUTE 'alter table "storage"."objects" drop column "level"';
END IF;
END $$;
set check_function_bodies = off; set check_function_bodies = off;

View File

@ -0,0 +1,204 @@
grant delete on table "storage"."s3_multipart_uploads" to "postgres";
grant insert on table "storage"."s3_multipart_uploads" to "postgres";
grant references on table "storage"."s3_multipart_uploads" to "postgres";
grant select on table "storage"."s3_multipart_uploads" to "postgres";
grant trigger on table "storage"."s3_multipart_uploads" to "postgres";
grant truncate on table "storage"."s3_multipart_uploads" to "postgres";
grant update on table "storage"."s3_multipart_uploads" to "postgres";
grant delete on table "storage"."s3_multipart_uploads_parts" to "postgres";
grant insert on table "storage"."s3_multipart_uploads_parts" to "postgres";
grant references on table "storage"."s3_multipart_uploads_parts" to "postgres";
grant select on table "storage"."s3_multipart_uploads_parts" to "postgres";
grant trigger on table "storage"."s3_multipart_uploads_parts" to "postgres";
grant truncate on table "storage"."s3_multipart_uploads_parts" to "postgres";
grant update on table "storage"."s3_multipart_uploads_parts" to "postgres";
-- drop type "gis"."geometry_dump";
-- drop type "gis"."valid_detail";
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION gis.calculate_unit_incident_distances(p_unit_id character varying, p_district_id character varying DEFAULT NULL::character varying)
RETURNS TABLE(unit_code character varying, unit_name character varying, unit_lat double precision, unit_lng double precision, incident_id character varying, incident_description text, incident_lat double precision, incident_lng double precision, category_name character varying, district_name character varying, distance_meters double precision)
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
BEGIN
RETURN QUERY
WITH unit_locations AS (
SELECT
u.code_unit,
u.name,
u.latitude,
u.longitude,
u.district_id,
ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::geography AS location
FROM
units u
WHERE
(p_unit_id IS NULL OR u.code_unit = p_unit_id)
AND (p_district_id IS NULL OR u.district_id = p_district_id)
AND u.latitude IS NOT NULL
AND u.longitude IS NOT NULL
),
incident_locations AS (
SELECT
ci.id,
ci.description,
ci.crime_id,
ci.crime_category_id,
l.latitude,
l.longitude,
ST_SetSRID(ST_MakePoint(l.longitude, l.latitude), 4326)::geography AS location
FROM
crime_incidents ci
JOIN
locations l ON ci.location_id = l.id
WHERE
l.latitude IS NOT NULL
AND l.longitude IS NOT NULL
)
SELECT
ul.code_unit as unit_code,
ul.name as unit_name,
ul.latitude as unit_lat,
ul.longitude as unit_lng,
il.id as incident_id,
il.description as incident_description,
il.latitude as incident_lat,
il.longitude as incident_lng,
cc.name as category_name,
d.name as district_name,
ST_Distance(ul.location, il.location) as distance_meters
FROM
unit_locations ul
JOIN
districts d ON ul.district_id = d.id
JOIN
crimes c ON c.district_id = d.id
JOIN
incident_locations il ON il.crime_id = c.id
JOIN
crime_categories cc ON il.crime_category_id = cc.id
ORDER BY
ul.code_unit,
ul.location <-> il.location; -- Use KNN operator for efficient ordering
END;
$function$
;
CREATE OR REPLACE FUNCTION gis.find_nearest_unit_to_incident(p_incident_id integer)
RETURNS TABLE(unit_code text, unit_name text, distance_meters double precision)
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
BEGIN
RETURN QUERY
WITH incident_location AS (
SELECT
ci.id,
ST_SetSRID(ST_MakePoint(
(ci.locations->>'longitude')::float,
(ci.locations->>'latitude')::float
), 4326)::geography AS location
FROM
crime_incidents ci
WHERE
ci.id = p_incident_id
AND (ci.locations->>'latitude') IS NOT NULL
AND (ci.locations->>'longitude') IS NOT NULL
),
unit_locations AS (
SELECT
u.code_unit,
u.name,
ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::geography AS location
FROM
units u
WHERE
u.latitude IS NOT NULL
AND u.longitude IS NOT NULL
)
SELECT
ul.code_unit as unit_code,
ul.name as unit_name,
ST_Distance(ul.location, il.location) as distance_meters
FROM
unit_locations ul
CROSS JOIN
incident_location il
ORDER BY
ul.location <-> il.location
LIMIT 1;
END;
$function$
;
CREATE OR REPLACE FUNCTION gis.find_units_within_distance(p_incident_id integer, p_max_distance_meters double precision DEFAULT 5000)
RETURNS TABLE(unit_code text, unit_name text, distance_meters double precision)
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
BEGIN
RETURN QUERY
WITH incident_location AS (
SELECT
ci.id,
ST_SetSRID(ST_MakePoint(
(ci.locations->>'longitude')::float,
(ci.locations->>'latitude')::float
), 4326)::geography AS location
FROM
crime_incidents ci
WHERE
ci.id = p_incident_id
AND (ci.locations->>'latitude') IS NOT NULL
AND (ci.locations->>'longitude') IS NOT NULL
),
unit_locations AS (
SELECT
u.code_unit,
u.name,
ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::geography AS location
FROM
units u
WHERE
u.latitude IS NOT NULL
AND u.longitude IS NOT NULL
)
SELECT
ul.code_unit as unit_code,
ul.name as unit_name,
ST_Distance(ul.location, il.location) as distance_meters
FROM
unit_locations ul
CROSS JOIN
incident_location il
WHERE
ST_DWithin(ul.location, il.location, p_max_distance_meters)
ORDER BY
ST_Distance(ul.location, il.location);
END;
$function$
;
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);

View File

@ -0,0 +1,94 @@
-- drop type "gis"."geometry_dump";
-- drop type "gis"."valid_detail";
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION gis.find_nearest_unit(p_incident_id character varying)
RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision)
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
BEGIN
RETURN QUERY
WITH incident_location AS (
SELECT
ci.id,
l.location AS location
FROM
crime_incidents ci
JOIN
locations l ON ci.location_id = l.id
WHERE
ci.id = p_incident_id
),
unit_locations AS (
SELECT
u.code_unit,
u.name,
u.location
FROM
units u
)
SELECT
ul.code_unit as unit_code,
ul.name as unit_name,
ST_Distance(ul.location, il.location) as distance_meters
FROM
unit_locations ul
CROSS JOIN
incident_location il
ORDER BY
ul.location <-> il.location
LIMIT 1;
END;
$function$
;
CREATE OR REPLACE FUNCTION gis.find_units_within_distance(p_incident_id character varying, p_max_distance_meters double precision DEFAULT 5000)
RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision)
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
BEGIN
RETURN QUERY
WITH incident_location AS (
SELECT
ci.id,
l.location AS location
FROM
crime_incidents ci
JOIN
locations l ON ci.location_id = l.id
WHERE
ci.id = p_incident_id
),
unit_locations AS (
SELECT
u.code_unit,
u.name,
u.location
FROM
units u
)
SELECT
ul.code_unit as unit_code,
ul.name as unit_name,
ST_Distance(ul.location, il.location) as distance_meters
FROM
unit_locations ul
CROSS JOIN
incident_location il
WHERE
ST_DWithin(ul.location, il.location, p_max_distance_meters)
ORDER BY
ST_Distance(ul.location, il.location);
END;
$function$
;
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);

View File

@ -0,0 +1,9 @@
-- drop type "gis"."geometry_dump";
-- drop type "gis"."valid_detail";
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);

View File

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "units" ADD COLUMN "phone" TEXT; ALTER TABLE "units" ADD COLUMN "phone" TEXT;

View File

@ -0,0 +1,41 @@
/*
Warnings:
- You are about to drop the column `code_unit` on the `unit_statistics` table. All the data in the column will be lost.
- A unique constraint covering the columns `[unit_id,month,year]` on the table `unit_statistics` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[district_id]` on the table `units` will be added. If there are existing duplicate values, this will fail.
- Made the column `year` on table `crimes` required. This step will fail if there are existing NULL values in that column.
- Added the required column `unit_id` to the `unit_statistics` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "unit_statistics" DROP CONSTRAINT "unit_statistics_code_unit_fkey";
-- DropForeignKey
ALTER TABLE "units" DROP CONSTRAINT "units_district_id_fkey";
-- DropIndex
DROP INDEX "unit_statistics_code_unit_month_year_key";
-- AlterTable
ALTER TABLE "crimes" ALTER COLUMN "year" SET NOT NULL;
-- AlterTable
ALTER TABLE "unit_statistics" DROP COLUMN "code_unit",
ADD COLUMN "unit_id" UUID NOT NULL;
-- AlterTable
ALTER TABLE "units" ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(),
ADD CONSTRAINT "units_pkey" PRIMARY KEY ("id");
-- CreateIndex
CREATE UNIQUE INDEX "unit_statistics_unit_id_month_year_key" ON "unit_statistics"("unit_id", "month", "year");
-- CreateIndex
CREATE UNIQUE INDEX "units_district_id_key" ON "units"("district_id");
-- AddForeignKey
ALTER TABLE "units" ADD CONSTRAINT "units_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "districts"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "unit_statistics" ADD CONSTRAINT "unit_statistics_unit_id_fkey" FOREIGN KEY ("unit_id") REFERENCES "units"("id") ON DELETE CASCADE ON UPDATE NO ACTION;

View File

@ -0,0 +1,29 @@
/*
Warnings:
- You are about to drop the column `unit_id` on the `unit_statistics` table. All the data in the column will be lost.
- The primary key for the `units` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `units` table. All the data in the column will be lost.
- A unique constraint covering the columns `[code_unit,month,year]` on the table `unit_statistics` will be added. If there are existing duplicate values, this will fail.
- Added the required column `code_unit` to the `unit_statistics` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "unit_statistics" DROP CONSTRAINT "unit_statistics_unit_id_fkey";
-- DropIndex
DROP INDEX "unit_statistics_unit_id_month_year_key";
-- AlterTable
ALTER TABLE "unit_statistics" DROP COLUMN "unit_id",
ADD COLUMN "code_unit" VARCHAR(20) NOT NULL;
-- AlterTable
ALTER TABLE "units" DROP CONSTRAINT "units_pkey",
DROP COLUMN "id";
-- CreateIndex
CREATE UNIQUE INDEX "unit_statistics_code_unit_month_year_key" ON "unit_statistics"("code_unit", "month", "year");
-- AddForeignKey
ALTER TABLE "unit_statistics" ADD CONSTRAINT "unit_statistics_code_unit_fkey" FOREIGN KEY ("code_unit") REFERENCES "units"("code_unit") ON DELETE CASCADE ON UPDATE NO ACTION;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "crimes" ALTER COLUMN "year" DROP NOT NULL;

View File

@ -0,0 +1,15 @@
/*
Warnings:
- Added the required column `city_id` to the `units` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "units_district_id_key";
-- AlterTable
ALTER TABLE "units" ADD COLUMN "city_id" VARCHAR(20) NOT NULL,
ADD CONSTRAINT "units_pkey" PRIMARY KEY ("code_unit");
-- AddForeignKey
ALTER TABLE "units" ADD CONSTRAINT "units_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "cities"("id") ON DELETE CASCADE ON UPDATE NO ACTION;

View File

@ -124,6 +124,7 @@ model cities {
created_at DateTime? @default(now()) @db.Timestamptz(6) created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6) updated_at DateTime? @default(now()) @db.Timestamptz(6)
districts districts[] districts districts[]
units units[]
@@index([name], map: "idx_cities_name") @@index([name], map: "idx_cities_name")
} }
@ -260,8 +261,9 @@ model incident_logs {
} }
model units { model units {
code_unit String @unique @db.VarChar(20) code_unit String @id @unique @db.VarChar(20)
district_id String @db.VarChar(20) district_id String @db.VarChar(20)
city_id String @db.VarChar(20)
name String @db.VarChar(100) name String @db.VarChar(100)
description String? description String?
type unit_type type unit_type
@ -274,7 +276,8 @@ model units {
location Unsupported("geography") location Unsupported("geography")
phone String? phone String?
unit_statistics unit_statistics[] unit_statistics unit_statistics[]
districts districts @relation(fields: [district_id], references: [id]) districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@index([name], map: "idx_units_name") @@index([name], map: "idx_units_name")
@@index([type], map: "idx_units_type") @@index([type], map: "idx_units_type")
@ -285,7 +288,6 @@ model units {
model unit_statistics { model unit_statistics {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
code_unit String @db.VarChar(20)
crime_total Int crime_total Int
crime_cleared Int crime_cleared Int
percentage Float? percentage Float?
@ -294,7 +296,8 @@ model unit_statistics {
year Int year Int
created_at DateTime? @default(now()) @db.Timestamptz(6) created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6) updated_at DateTime? @default(now()) @db.Timestamptz(6)
unit units @relation(fields: [code_unit], references: [code_unit], onDelete: Cascade, onUpdate: NoAction) code_unit String @db.VarChar(20)
units units @relation(fields: [code_unit], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
@@unique([code_unit, month, year]) @@unique([code_unit, month, year])
@@index([year, month], map: "idx_unit_statistics_year_month") @@index([year, month], map: "idx_unit_statistics_year_month")

View File

@ -65,39 +65,83 @@ export class CrimeIncidentsSeeder {
} }
/** /**
* Generates well-distributed points within a district's area * Generates well-distributed points within a district's area with geographical constraints
* @param centerLat - The center latitude of the district * @param centerLat - The center latitude of the district
* @param centerLng - The center longitude of the district * @param centerLng - The center longitude of the district
* @param landArea - Land area in square km * @param landArea - Land area in square km
* @param numPoints - Number of points to generate * @param numPoints - Number of points to generate
* @param districtId - ID of the district for special handling
* @param districtName - Name of the district for constraints
* @returns Array of {latitude, longitude, radius} points * @returns Array of {latitude, longitude, radius} points
*/ */
private generateDistributedPoints( private generateDistributedPoints(
centerLat: number, centerLat: number,
centerLng: number, centerLng: number,
landArea: number, landArea: number,
numPoints: number numPoints: number,
districtId: string,
districtName: string
): Array<{ latitude: number; longitude: number; radius: number }> { ): Array<{ latitude: number; longitude: number; radius: number }> {
const points = []; const points = [];
// Calculate a reasonable radius based on land area // Calculate a reasonable radius based on land area
// Using square root of area as an approximation of district "radius"
const areaFactor = Math.sqrt(landArea) / 100; const areaFactor = Math.sqrt(landArea) / 100;
const baseRadius = Math.max(0.01, Math.min(0.05, areaFactor * 0.03)); const baseRadius = Math.max(0.01, Math.min(0.05, areaFactor * 0.03));
// Create a grid-based distribution for better coverage // Create a grid-based distribution for better coverage
const gridSize = Math.ceil(Math.sqrt(numPoints * 1.5)); // Slightly larger grid for variety const gridSize = Math.ceil(Math.sqrt(numPoints * 1.5));
// Define district bounds approximately // Define district bounds with geographical constraints
// 0.1 degrees is roughly 11km at equator // Standard calculation for general districts
const estimatedDistrictRadius = Math.sqrt(landArea / Math.PI) / 111; let estimatedDistrictRadius = Math.sqrt(landArea / Math.PI) / 111;
const bounds = {
// District-specific adjustments to avoid generating points in the ocean
const southernCoastalDistricts = [
'puger',
'tempurejo',
'ambulu',
'gumukmas',
'kencong',
'wuluhan',
'kencong',
];
const isCoastalDistrict = southernCoastalDistricts.some((district) =>
districtName.toLowerCase().includes(district)
);
// Default bounds
let bounds = {
minLat: centerLat - estimatedDistrictRadius, minLat: centerLat - estimatedDistrictRadius,
maxLat: centerLat + estimatedDistrictRadius, maxLat: centerLat + estimatedDistrictRadius,
minLng: centerLng - estimatedDistrictRadius, minLng: centerLng - estimatedDistrictRadius,
maxLng: centerLng + estimatedDistrictRadius, maxLng: centerLng + estimatedDistrictRadius,
}; };
// Apply special constraints for coastal districts
if (isCoastalDistrict) {
// Shift points northward for southern coastal districts to avoid ocean
if (
districtName.toLowerCase().includes('puger') ||
districtName.toLowerCase().includes('tempurejo')
) {
// For Puger and Tempurejo, shift more aggressively northward
bounds = {
minLat: centerLat, // Don't go south of the center
maxLat: centerLat + estimatedDistrictRadius * 1.5, // Extend more to the north
minLng: centerLng - estimatedDistrictRadius * 0.8,
maxLng: centerLng + estimatedDistrictRadius * 0.8,
};
} else {
// For other coastal districts, shift moderately northward
bounds = {
minLat: centerLat - estimatedDistrictRadius * 0.5, // Less southward
maxLat: centerLat + estimatedDistrictRadius * 1.2, // More northward
minLng: centerLng - estimatedDistrictRadius,
maxLng: centerLng + estimatedDistrictRadius,
};
}
}
const latStep = (bounds.maxLat - bounds.minLat) / gridSize; const latStep = (bounds.maxLat - bounds.minLat) / gridSize;
const lngStep = (bounds.maxLng - bounds.minLng) / gridSize; const lngStep = (bounds.maxLng - bounds.minLng) / gridSize;
@ -105,7 +149,7 @@ export class CrimeIncidentsSeeder {
let totalPoints = 0; let totalPoints = 0;
for (let i = 0; i < gridSize && totalPoints < numPoints; i++) { for (let i = 0; i < gridSize && totalPoints < numPoints; i++) {
for (let j = 0; j < gridSize && totalPoints < numPoints; j++) { for (let j = 0; j < gridSize && totalPoints < numPoints; j++) {
// Base position within the grid cell // Base position within the grid cell with randomness
const cellLat = const cellLat =
bounds.minLat + (i + 0.2 + Math.random() * 0.6) * latStep; bounds.minLat + (i + 0.2 + Math.random() * 0.6) * latStep;
const cellLng = const cellLng =
@ -121,22 +165,38 @@ export class CrimeIncidentsSeeder {
const latitude = cellLat + (Math.random() * 2 - 1) * jitter; const latitude = cellLat + (Math.random() * 2 - 1) * jitter;
const longitude = cellLng + (Math.random() * 2 - 1) * jitter; const longitude = cellLng + (Math.random() * 2 - 1) * jitter;
points.push({ // Ensure the point is within district boundaries
latitude, // Simple check to ensure points don't stray too far from center
longitude, if (distance <= estimatedDistrictRadius * 1.2) {
radius: distance * 111000, // Convert to meters (approx) points.push({
}); latitude,
longitude,
radius: distance * 111000, // Convert to meters (approx)
});
totalPoints++; totalPoints++;
}
} }
} }
// Add some completely random points for diversity // If we still need more points, add some with tighter constraints
while (points.length < numPoints) { while (points.length < numPoints) {
const latitude = // For coastal districts, use more controlled distribution
centerLat + (Math.random() * 2 - 1) * estimatedDistrictRadius; let latitude, longitude;
const longitude =
centerLng + (Math.random() * 2 - 1) * estimatedDistrictRadius; if (isCoastalDistrict) {
// Generate points with northward bias for coastal districts
const northBias = Math.random() * 0.7 + 0.3; // 0.3 to 1.0, favoring north
latitude = centerLat + northBias * estimatedDistrictRadius * 0.8;
longitude =
centerLng + (Math.random() * 2 - 1) * estimatedDistrictRadius * 0.8;
} else {
// Standard distribution for non-coastal districts
latitude =
centerLat + (Math.random() * 2 - 1) * estimatedDistrictRadius * 0.8;
longitude =
centerLng + (Math.random() * 2 - 1) * estimatedDistrictRadius * 0.8;
}
const latDiff = latitude - centerLat; const latDiff = latitude - centerLat;
const lngDiff = longitude - centerLng; const lngDiff = longitude - centerLng;
@ -249,15 +309,17 @@ export class CrimeIncidentsSeeder {
return []; return [];
} }
// Generate a variable number of incidents between 10 and 25 for more variability // Use the actual number of crimes instead of a random count
const numLocations = Math.floor(Math.random() * 16) + 10; // 10-25 locations const numLocations = crime.number_of_crime;
// Generate distributed locations using our new utility function // Update the call to the function in createIncidentsForCrime method:
const locationPool = this.generateDistributedPoints( const locationPool = this.generateDistributedPoints(
geo.latitude, geo.latitude,
geo.longitude, geo.longitude,
geo.land_area || 100, // Default to 100 km² if not available geo.land_area || 100, // Default to 100 km² if not available
numLocations numLocations,
district.id,
district.name
); );
// List of common street names in Jember with more variety // List of common street names in Jember with more variety

View File

@ -32,6 +32,7 @@ interface UnitStatistics {
interface CreateLocationDto { interface CreateLocationDto {
district_id: string; district_id: string;
code_unit: string; code_unit: string;
city_id: string;
name?: string; name?: string;
description?: string; description?: string;
address?: string; address?: string;
@ -101,6 +102,7 @@ export class UnitSeeder {
let locationData: CreateLocationDto = { let locationData: CreateLocationDto = {
district_id: city.districts[0].id, // This will be replaced with Patrang's ID district_id: city.districts[0].id, // This will be replaced with Patrang's ID
city_id: city.id,
code_unit: newId, code_unit: newId,
name: `Polres ${city.name}`, name: `Polres ${city.name}`,
description: `Unit ${city.name} is categorized as POLRES and operates in the ${city.name} area.`, description: `Unit ${city.name} is categorized as POLRES and operates in the ${city.name} area.`,
@ -163,6 +165,7 @@ export class UnitSeeder {
const locationData: CreateLocationDto = { const locationData: CreateLocationDto = {
district_id: district.id, district_id: district.id,
city_id: district.city_id,
code_unit: newId, code_unit: newId,
name: `Polsek ${district.name}`, name: `Polsek ${district.name}`,
description: `Unit ${district.name} is categorized as POLSEK and operates in the ${district.name} area.`, description: `Unit ${district.name} is categorized as POLSEK and operates in the ${district.name} area.`,