OpenStreetMap-Service/server.js

309 lines
9.4 KiB
JavaScript

const express = require('express');
const path = require('path');
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const app = express();
const PORT = process.env.PORT || 3000;
const markers = [];
const circles = [];
const alerts = [];
app.use(express.static(path.join(__dirname, 'public')));
app.get('/openapi.json', (req, res) => {
res.sendFile(path.join(__dirname, 'openapi.json'));
});
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'OpenStreetMap Service API',
version: '1.0.0',
description: 'API for displaying markers and circles on OpenStreetMap',
contact: {
name: 'API Support'
}
},
servers: [
{
url: `http://localhost:${PORT}`,
description: 'Development server'
}
],
tags: [
{
name: 'Map',
description: 'Map visualization endpoints'
}
]
},
apis: ['./server.js'],
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
app.get('/api-docs.json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(swaggerSpec);
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/api/marker', (req, res) => {
const { lat, lng, icon, title } = req.query;
if (!lat || !lng) {
return res.status(400).json({
error: 'Missing required parameters: lat and lng'
});
}
const latNum = parseFloat(lat);
const lngNum = parseFloat(lng);
if (isNaN(latNum) || isNaN(lngNum)) {
return res.status(400).json({
error: 'Invalid coordinates: lat and lng must be numbers'
});
}
if (latNum < -90 || latNum > 90 || lngNum < -180 || lngNum > 180) {
return res.status(400).json({
error: 'Invalid coordinates: lat must be between -90 and 90, lng must be between -180 and 180'
});
}
const marker = {
id: Date.now().toString(),
lat: latNum,
lng: lngNum,
icon: icon || 'default',
title: title || `Marker at ${latNum}, ${lngNum}`
};
markers.push(marker);
res.json({
success: true,
type: 'marker',
data: marker
});
});
app.get('/api/markers', (req, res) => {
res.json({ markers });
});
app.post('/api/markers/clear', (req, res) => {
markers.length = 0;
res.json({ success: true });
});
app.get('/api/circle', (req, res) => {
const { lat, lng, radius, icon, title } = req.query;
if (!lat || !lng || !radius) {
return res.status(400).json({
error: 'Missing required parameters: lat, lng, and radius'
});
}
const latNum = parseFloat(lat);
const lngNum = parseFloat(lng);
const radiusNum = parseFloat(radius);
if (isNaN(latNum) || isNaN(lngNum) || isNaN(radiusNum)) {
return res.status(400).json({
error: 'Invalid parameters: lat, lng, and radius must be numbers'
});
}
if (latNum < -90 || latNum > 90 || lngNum < -180 || lngNum > 180) {
return res.status(400).json({
error: 'Invalid coordinates: lat must be between -90 and 90, lng must be between -180 and 180'
});
}
if (radiusNum <= 0) {
return res.status(400).json({
error: 'Invalid radius: must be a positive number'
});
}
const circle = {
id: Date.now().toString(),
lat: latNum,
lng: lngNum,
radius: radiusNum,
icon: icon || 'default',
title: title || `Circle at ${latNum}, ${lngNum} with radius ${radiusNum}`
};
circles.push(circle);
res.json({
success: true,
type: 'circle',
data: circle
});
});
app.get('/api/circles', (req, res) => {
res.json({ circles });
});
app.post('/api/circles/clear', (req, res) => {
circles.length = 0;
res.json({ success: true });
});
app.post('/api/alert', (req, res) => {
const { message, type } = req.query || req.body;
if (!message) {
return res.status(400).json({ error: 'Missing required parameter: message' });
}
const alertType = ['danger', 'warning', 'info', 'success'].includes(type) ? type : 'danger';
const alert = {
id: Date.now().toString(),
message: message,
type: alertType,
timestamp: new Date().toISOString()
};
alerts.push(alert);
res.json({ success: true, alert });
});
app.get('/api/alerts', (req, res) => {
res.json({ alerts });
});
app.post('/api/alerts/clear', (req, res) => {
alerts.length = 0;
res.json({ success: true });
});
app.get('/api/embed', (req, res) => {
res.send(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenStreetMap Embed</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
body { margin: 0; padding: 0; }
#map { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([20, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
const defaultIcon = L.Icon.Default;
const customIcons = {};
function createCustomIcon(iconUrl) {
if (!iconUrl || iconUrl === 'default') return defaultIcon;
if (customIcons[iconUrl]) return customIcons[iconUrl];
const icon = L.icon({
iconUrl: iconUrl,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32]
});
customIcons[iconUrl] = icon;
return icon;
}
async function loadMarkers() {
try {
const res = await fetch('/api/markers');
const data = await res.json();
if (data.markers) {
data.markers.forEach(marker => {
const icon = createCustomIcon(marker.icon);
L.marker([marker.lat, marker.lng], { icon: icon })
.addTo(map)
.bindPopup(marker.title);
});
if (data.markers.length > 0) {
const last = data.markers[data.markers.length - 1];
map.setView([last.lat, last.lng], 13);
}
}
} catch (e) { console.error('Error loading markers:', e); }
}
async function loadCircles() {
try {
const res = await fetch('/api/circles');
const data = await res.json();
if (data.circles) {
data.circles.forEach(circle => {
const circleLayer = L.circle([circle.lat, circle.lng], {
radius: circle.radius,
color: '#3388ff',
fillColor: '#3388ff',
fillOpacity: 0.3,
weight: 2
}).addTo(map);
if (circle.icon && circle.icon !== 'default') {
const icon = createCustomIcon(circle.icon);
L.marker([circle.lat, circle.lng], { icon: icon })
.addTo(map)
.bindPopup(circle.title);
} else {
circleLayer.bindPopup(circle.title);
}
});
}
} catch (e) { console.error('Error loading circles:', e); }
}
async function pollAlerts() {
try {
const res = await fetch('/api/alerts');
const data = await res.json();
if (data.alerts) {
const colors = { danger: '#dc2626', warning: '#f59e0b', info: '#2563eb', success: '#16a34a' };
data.alerts.forEach(alert => {
const color = colors[alert.type] || colors.danger;
L.popup()
.setLatLng(map.getCenter())
.setContent(\`<div style="background:\${color};color:white;padding:10px;border-radius:5px;">\${alert.message}</div>\`)
.openOn(map);
});
}
} catch (e) { console.error('Error polling alerts:', e); }
}
loadMarkers();
loadCircles();
setInterval(pollAlerts, 5000);
</script>
</body>
</html>`);
});
app.listen(PORT, () => {
console.log(`OpenStreetMap Service running at http://localhost:${PORT}`);
console.log(`Swagger docs available at http://localhost:${PORT}/api-docs`);
console.log(`Embed API available at http://localhost:${PORT}/api/embed`);
});