Temporary implement pixel to region feature

This commit is contained in:
2025-10-03 16:02:55 -07:00
parent 30f6a76891
commit 995b9127c1
5 changed files with 247 additions and 29 deletions
+20 -3
View File
@@ -94,6 +94,10 @@ model Tile {
y Int
imageData Bytes?
pixels Pixel[]
regionId Int?
region Region? @relation(fields: [regionId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
@@unique([season, x, y])
}
@@ -116,10 +120,14 @@ model Pixel {
model Region {
id Int @id @default(autoincrement())
cityId Int @unique
name String
cityId Int
city City @relation(fields: [cityId], references: [id])
number Int
countryId Int
tiles Tile[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
@@unique([cityId, number])
}
model ProfilePicture {
@@ -174,3 +182,12 @@ model SiteContent {
@@index([key, locale])
}
model City {
id Int @id @default(autoincrement())
name String
countryId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
regions Region[]
}
-21
View File
@@ -1,21 +0,0 @@
export interface Region {
id: number;
cityId: number;
name: string;
number: number;
countryId: number;
flagId: number;
}
export function getRegionForCoordinates(_tileX: number, _tileY: number, _x: number, _y: number): Region | null {
// TODO: implement region lookup using coordinate data
// After running the scraper (scripts/scrape_regions.py) and importing the data,
// you can implement a proper lookup using the Region table or a spatial index.
//
// const globalX = tileX * 1000 + x;
// const globalY = tileY * 1000 + y;
//
// For now, return null to avoid showing incorrect region data.
// See scripts/README.md for instructions on scraping and importing region data.
return null;
}
+15 -2
View File
@@ -4,6 +4,8 @@ import { checkColorUnlocked, COLOR_PALETTE } from "../utils/colors.js";
import { calculateChargeRecharge } from "../utils/charges.js";
import { getRegionForCoordinates } from "../config/regions.js";
import { calculateLevel, calculateMaxChargesForLevel } from "../utils/levels.js";
import { LEVEL_BASE_PIXEL, LEVEL_EXPONENT, LEVEL_UP_DROPLETS_REWARD, LEVEL_UP_MAX_CHARGES_REWARD, PAINTED_DROPLETS_REWARD } from "../config/pixel.js";
import { RegionService } from "./region.js";
export interface PaintPixelsInput {
tileX: number;
@@ -36,6 +38,17 @@ export interface PixelInfoResult {
export class PixelService {
constructor(private prisma: PrismaClient) {}
private regionService: RegionService;
constructor(private prisma: PrismaClient) {
const canvas = createCanvas(1, 1);
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, 1, 1);
this.emptyTile = canvas.toBuffer("image/png");
this.regionService = new RegionService(prisma);
}
async getRandomTile(): Promise<RandomTileResult> {
const recentThreshold = new Date(Date.now() - 24 * 60 * 60 * 1000);
@@ -128,7 +141,7 @@ export class PixelService {
};
}
const region = getRegionForCoordinates(tileX, tileY, x, y);
const region = await this.regionService.getRegionForCoordinates(tileX, tileY, season);
return { paintedBy, region };
}
@@ -267,7 +280,7 @@ export class PixelService {
if (regionCache.has(coordKey)) {
region = regionCache.get(coordKey);
} else {
region = getRegionForCoordinates(tileX, tileY, x, y);
region = await this.regionService.getRegionForCoordinates(tileX, tileY, season);
regionCache.set(coordKey, region);
}
+142
View File
@@ -0,0 +1,142 @@
import { Prisma, PrismaClient } from "@prisma/client";
import {
getCountryByCode,
getCountryById,
getUnknownRegion,
pixelsToLatLon,
} from "../utils/region.js";
export interface Region {
id: number;
cityId: number;
name: string;
number: number;
countryId: number;
flagId: number;
}
export class RegionService {
constructor(private prisma: PrismaClient) {}
// Ref: https://nominatim.org/release-docs/develop/api/Reverse/
async nominatim(lat: number, lon: number) {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}&zoom=5&accept-language=en`
);
const data: any = await response.json();
let city =
data.address.city || data.address.city_district || data.address.state;
if (city) {
city = city.replace(/Province/i, "").trim();
}
return {
city,
country: data.address.country,
country_code: data.address.country_code,
};
}
async resetAllRegionsData() {
await this.prisma.tile.updateMany({
data: { regionId: null },
});
await this.prisma.region.deleteMany();
await this.prisma.city.deleteMany();
}
async getRegionForCoordinates(
tileX: number,
tileY: number,
season: number = 0
): Promise<Region> {
// For debugging purposes only
// await this.resetAllRegionsData();
let tile = await this.prisma.tile.findFirst({
where: { season, x: tileX, y: tileY },
include: { region: { include: { city: true } } },
});
if (!tile) {
tile = await this.prisma.tile.create({
data: { season, x: tileX, y: tileY },
include: { region: { include: { city: true } } },
});
}
try {
if (!tile.regionId) {
const centerX = tile.x * 1000 + 500;
const centerY = tile.y * 1000 + 500;
const [lat, lon] = pixelsToLatLon(centerX, centerY);
console.log(`Getting region for coordinates ${lat}, ${lon}.`);
const nominatimData = await this.nominatim(lat, lon);
if (!nominatimData.city)
throw new Error(`City not found for coordinates ${lat}, ${lon}.`);
const country = getCountryByCode(nominatimData.country_code);
if (!country)
throw new Error(
`Country code ${nominatimData.country_code} not found.`
);
let city = await this.prisma.city.findFirst({
where: { name: nominatimData.city },
});
if (!city) {
city = await this.prisma.city.create({
data: {
name: nominatimData.city,
countryId: country.id,
},
});
}
let region = await this.prisma.region.findFirst({
where: { cityId: city.id, number: 1 },
});
if (!region) {
region = await this.prisma.region.create({
data: { cityId: city.id, number: 1 },
});
}
tile = await this.prisma.tile.update({
where: { id: tile.id },
data: { regionId: region.id },
include: { region: { include: { city: true } } },
});
}
const country = getCountryById(tile.region.city.countryId);
if (!country)
throw new Error(`Country ID ${tile.region.city.countryId} not found.`);
return {
id: tile.region.id,
cityId: tile.region.cityId,
number: tile.region.number,
name: tile.region.city.name,
countryId: country.id,
flagId: country.flag,
};
} catch (error) {
console.error(error);
const country = getCountryById(242);
return {
id: country!.id,
cityId: 0,
number: 0,
name: country!.name,
countryId: country!.id,
flagId: country!.flag,
};
}
}
}
File diff suppressed because one or more lines are too long