Initial commit
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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!")
|
||||
}
|
||||
Generated
+1534
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+1001
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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