Files
maplibre-poc/test-backend/generate_meters.js
2025-08-20 10:33:17 +02:00

267 lines
7.7 KiB
JavaScript

import fs from "fs"
// Load the actual Saudi Arabia borders from sa.json
const saudiPolygon = JSON.parse(fs.readFileSync("sa.json", "utf8"))
// Saudi Arabia accurate geographical bounds (for quick bounding box checks)
// More precise boundaries to avoid points in neighboring countries
const SAUDI_BOUNDS = {
minLng: 36.0, // More conservative western boundary (Red Sea coast)
maxLng: 55.0, // More conservative eastern boundary (Persian Gulf coast)
minLat: 17.0, // More conservative southern boundary (Yemen border)
maxLat: 32.0, // Northern boundary (Jordan/Iraq border)
}
// Point-in-polygon algorithm (ray casting)
function pointInPolygon(point, polygon) {
const [x, y] = point
let inside = false
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const [xi, yi] = polygon[i]
const [xj, yj] = polygon[j]
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) {
inside = !inside
}
}
return inside
}
// Check if point is inside any of the Saudi polygons
function isPointInSaudiArabia(point) {
const multiPolygon = saudiPolygon.features[0].geometry.coordinates
for (const polygon of multiPolygon) {
// Each polygon might have holes, first ring is exterior, others are holes
const exteriorRing = polygon[0]
if (pointInPolygon(point, exteriorRing)) {
// Check if point is in any holes
let inHole = false
for (let i = 1; i < polygon.length; i++) {
if (pointInPolygon(point, polygon[i])) {
inHole = true
break
}
}
if (!inHole) {
return true
}
}
}
return false
}
// Major Saudi cities for reference and better distribution (verified coordinates)
const MAJOR_CITIES = [
{ name: "Riyadh", lng: 46.6753, lat: 24.7136 },
{ name: "Jeddah", lng: 39.2376, lat: 21.4858 },
{ name: "Mecca", lng: 39.8579, lat: 21.3891 },
{ name: "Medina", lng: 39.6118, lat: 24.5247 },
{ name: "Dammam", lng: 50.088, lat: 26.4207 },
{ name: "Khobar", lng: 50.208, lat: 26.2791 },
{ name: "Tabuk", lng: 36.5951, lat: 28.3998 },
{ name: "Buraidah", lng: 43.975, lat: 26.326 },
{ name: "Khamis Mushait", lng: 42.7284, lat: 18.3057 },
{ name: "Hail", lng: 41.69, lat: 27.5114 },
{ name: "Hofuf", lng: 49.586, lat: 25.3491 },
{ name: "Jubail", lng: 49.6603, lat: 27.0174 },
{ name: "Abha", lng: 42.5058, lat: 18.2164 },
{ name: "Yanbu", lng: 38.0618, lat: 24.0895 },
{ name: "Najran", lng: 44.13, lat: 17.4924 },
{ name: "Arar", lng: 41.0377, lat: 30.9753 },
{ name: "Al Qatif", lng: 50.0089, lat: 26.5056 },
{ name: "Sakaka", lng: 40.2062, lat: 29.9697 },
{ name: "Al Bahah", lng: 41.4687, lat: 20.0129 },
].filter((city) => isPointInSaudiArabia([city.lng, city.lat]))
const MAKERS = [
"Maker A",
"Maker B",
"Maker C",
"Maker D",
"Maker E",
"Maker F",
"Maker G",
"Maker H",
"Maker I",
"Maker J",
]
function randomInRange(min, max) {
return Math.random() * (max - min) + min
}
function isWithinSaudiBounds(lng, lat) {
// First do a quick bounding box check for performance
if (
lng < SAUDI_BOUNDS.minLng ||
lng > SAUDI_BOUNDS.maxLng ||
lat < SAUDI_BOUNDS.minLat ||
lat > SAUDI_BOUNDS.maxLat
) {
return false
}
// Then do the precise polygon check
return isPointInSaudiArabia([lng, lat])
}
function generateCoordinatesAroundCity(city, radius = 0.3) {
// Reduced radius for better accuracy
let attempts = 0
let coordinates
do {
const angle = Math.random() * 2 * Math.PI
const distance = Math.random() * radius
const lng = city.lng + distance * Math.cos(angle)
const lat = city.lat + distance * Math.sin(angle)
coordinates = [lng, lat]
attempts++
} while (
!isWithinSaudiBounds(coordinates[0], coordinates[1]) &&
attempts < 20
)
// If we can't find a valid coordinate around the city, use the city coordinates
if (!isWithinSaudiBounds(coordinates[0], coordinates[1])) {
coordinates = [city.lng, city.lat]
}
return coordinates
}
function generateRandomCoordinates() {
let attempts = 0
let coordinates
do {
coordinates = [
randomInRange(SAUDI_BOUNDS.minLng, SAUDI_BOUNDS.maxLng),
randomInRange(SAUDI_BOUNDS.minLat, SAUDI_BOUNDS.maxLat),
]
attempts++
} while (
!isWithinSaudiBounds(coordinates[0], coordinates[1]) &&
attempts < 20
)
return coordinates
}
function generateMeterFeature(id, coordinates) {
const maker = MAKERS[Math.floor(Math.random() * MAKERS.length)]
const model = `Model ${id}`
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: coordinates,
},
properties: {
id: `meter-${id}`,
maker: maker,
model: model,
timestamp: "2025-08-13T11:47:00Z",
},
}
}
function generateMetersData(totalPoints = 1000) {
const features = []
console.log(`Valid major cities within Saudi borders: ${MAJOR_CITIES.length}`)
// Generate 60% of points around major cities for realistic distribution
const cityPoints = Math.floor(totalPoints * 0.6)
const randomPoints = totalPoints - cityPoints
console.log(`Generating ${cityPoints} points around major cities...`)
for (let i = 1; i <= cityPoints; i++) {
const city = MAJOR_CITIES[Math.floor(Math.random() * MAJOR_CITIES.length)]
const coordinates = generateCoordinatesAroundCity(city, 0.3) // Smaller radius for accuracy
features.push(generateMeterFeature(i, coordinates))
}
console.log(`Generating ${randomPoints} random points across Saudi Arabia...`)
for (let i = cityPoints + 1; i <= totalPoints; i++) {
const coordinates = generateRandomCoordinates()
features.push(generateMeterFeature(i, coordinates))
}
// Validate all coordinates are within actual Saudi borders
const validFeatures = features.filter((feature) => {
const [lng, lat] = feature.geometry.coordinates
return isWithinSaudiBounds(lng, lat)
})
console.log(
`Generated ${features.length} features, ${validFeatures.length} are within actual Saudi borders`
)
// If we have fewer than desired points, generate more around cities
if (validFeatures.length < totalPoints) {
const needed = totalPoints - validFeatures.length
console.log(`Generating ${needed} additional points around major cities...`)
for (let i = 0; i < needed; i++) {
const city = MAJOR_CITIES[Math.floor(Math.random() * MAJOR_CITIES.length)]
const coordinates = generateCoordinatesAroundCity(city, 0.2) // Even smaller radius
const newFeature = generateMeterFeature(
validFeatures.length + i + 1,
coordinates
)
validFeatures.push(newFeature)
}
}
return {
type: "FeatureCollection",
features: validFeatures.slice(0, totalPoints), // Ensure exactly the requested number
}
}
// Generate the data
const points =
parseInt(process.env.POINTS, 10) ||
(process.argv[2] ? parseInt(process.argv[2], 10) : 100000)
console.log(
`Generating ${points} meter points across Saudi Arabia using actual borders...`
)
const metersData = generateMetersData(points)
// Write to file
fs.writeFileSync("saudi_meters.json", JSON.stringify(metersData, null, 2))
console.log(
`✅ Successfully generated ${metersData.features.length} meter points!`
)
console.log("📍 Distribution:")
console.log(" - 60% around major cities")
console.log(" - 40% randomly distributed")
console.log(
"🗺️ Coverage: Entire Saudi Arabia territory (using actual polygon borders)"
)
// Final validation
const invalidPoints = metersData.features.filter((feature) => {
const [lng, lat] = feature.geometry.coordinates
return !isPointInSaudiArabia([lng, lat])
})
console.log(
`\n🔍 Final validation: ${invalidPoints.length} points outside Saudi borders`
)
if (invalidPoints.length === 0) {
console.log("✅ All points are within Saudi Arabia's actual borders!")
}