Initial commit
This commit is contained in:
44
test-backend/README.md
Normal file
44
test-backend/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Test Backend - Saudi Meters API
|
||||
|
||||
A simple Express.js server that serves the Saudi meters GeoJSON data.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Development Mode
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Production Mode
|
||||
```bash
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /` - API information and available endpoints
|
||||
- `GET /api/meters` - Get all meters data (GeoJSON FeatureCollection)
|
||||
- `GET /api/meters/:id` - Get specific meter by ID
|
||||
- `GET /health` - Health check endpoint
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# Get all meters
|
||||
curl http://localhost:3001/api/meters
|
||||
|
||||
# Get specific meter
|
||||
curl http://localhost:3001/api/meters/meter-757
|
||||
|
||||
# Health check
|
||||
curl http://localhost:3001/health
|
||||
```
|
||||
|
||||
The server runs on port 3001 by default, but you can override it with the `PORT` environment variable.
|
||||
80
test-backend/app.ts
Normal file
80
test-backend/app.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import express, { Express } from "express"
|
||||
import cors from "cors"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import { fileURLToPath } from "url"
|
||||
import { dirname } from "path"
|
||||
import { MetersCollection, MeterFeature } from "shared-types/meters"
|
||||
|
||||
// ES module equivalent of __dirname
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const app: Express = express()
|
||||
const PORT = process.env.PORT || 3001
|
||||
|
||||
// Middleware
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
|
||||
// Route to serve the saudi_meters.json file
|
||||
app.get("/api/meters", (req, res) => {
|
||||
try {
|
||||
const filePath = path.join(__dirname, "saudi_meters.json")
|
||||
const data = fs.readFileSync(filePath, "utf8")
|
||||
const metersData: MetersCollection = JSON.parse(data)
|
||||
|
||||
res.json(metersData)
|
||||
} catch (error) {
|
||||
console.error("Error reading meters data:", error)
|
||||
res.status(500).json({ error: "Failed to load meters data" })
|
||||
}
|
||||
})
|
||||
|
||||
// Route to get individual meter by ID
|
||||
app.get("/api/meters/:id", (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const filePath = path.join(__dirname, "saudi_meters.json")
|
||||
const data = fs.readFileSync(filePath, "utf8")
|
||||
const metersData: MetersCollection = JSON.parse(data)
|
||||
|
||||
const meter = metersData.features.find(
|
||||
(feature: MeterFeature) => feature.properties.id === id
|
||||
)
|
||||
|
||||
if (meter) {
|
||||
res.json(meter)
|
||||
} else {
|
||||
res.status(404).json({ error: "Meter not found" })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error reading meters data:", error)
|
||||
res.status(500).json({ error: "Failed to load meters data" })
|
||||
}
|
||||
})
|
||||
|
||||
// Health check endpoint
|
||||
app.get("/health", (req, res) => {
|
||||
res.json({ status: "OK", timestamp: new Date().toISOString() })
|
||||
})
|
||||
|
||||
// Basic info endpoint
|
||||
app.get("/", (req, res) => {
|
||||
res.json({
|
||||
message: "Saudi Meters API Server",
|
||||
endpoints: {
|
||||
meters: "/api/meters",
|
||||
meterById: "/api/meters/:id",
|
||||
health: "/health",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Start the server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Server running on http://localhost:${PORT}`)
|
||||
console.log(`📊 Meters data available at http://localhost:${PORT}/api/meters`)
|
||||
})
|
||||
|
||||
export default app
|
||||
266
test-backend/generate_meters.js
Normal file
266
test-backend/generate_meters.js
Normal file
@@ -0,0 +1,266 @@
|
||||
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!")
|
||||
}
|
||||
1534
test-backend/package-lock.json
generated
Normal file
1534
test-backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
test-backend/package.json
Normal file
25
test-backend/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "test-backend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Express server for serving saudi_meters.json",
|
||||
"main": "app.ts",
|
||||
"scripts": {
|
||||
"start": "node dist/app.js",
|
||||
"dev": "tsx app.ts",
|
||||
"build": "tsc",
|
||||
"generate": "node generate_meters.js",
|
||||
"validate": "node validate_borders.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "2.8.5",
|
||||
"express": "5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/node": "24.3.0",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "5.9.2"
|
||||
}
|
||||
}
|
||||
1001
test-backend/pnpm-lock.yaml
generated
Normal file
1001
test-backend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
9535
test-backend/sa.json
Normal file
9535
test-backend/sa.json
Normal file
File diff suppressed because it is too large
Load Diff
320005
test-backend/saudi_meters.json
Normal file
320005
test-backend/saudi_meters.json
Normal file
File diff suppressed because it is too large
Load Diff
34
test-backend/tsconfig.json
Normal file
34
test-backend/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"lib": [
|
||||
"ES2020"
|
||||
],
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"shared-types/*": [
|
||||
"../types/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"*.ts",
|
||||
"../types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user