Map caching, clickhouse test service

This commit is contained in:
cracklesparkle
2024-08-26 16:11:37 +09:00
parent 579bbf7764
commit ab88fd5ea5
20 changed files with 737 additions and 220 deletions

14
.env.example Normal file
View File

@ -0,0 +1,14 @@
REDIS_HOST=
REDIS_PORT=
REDIS_PASSWORD=
POSTGRES_HOST=
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_PORT=
EMS_PORT=
MONITOR_PORT=
CLICKHOUSE_DB=
CLICKHOUSE_USER=
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=
CLICKHOUSE_PASSWORD=

View File

@ -15,10 +15,11 @@ import Documents from "./pages/Documents"
import Reports from "./pages/Reports" import Reports from "./pages/Reports"
import Boilers from "./pages/Boilers" import Boilers from "./pages/Boilers"
import Servers from "./pages/Servers" import Servers from "./pages/Servers"
import { Api, Assignment, Cloud, Factory, Home, Login, Map, Password, People, Settings as SettingsIcon, Shield, Storage } from "@mui/icons-material" import { Api, Assignment, Cloud, Factory, Home, Login, Map, MonitorHeart, Password, People, Settings as SettingsIcon, Shield, Storage } from "@mui/icons-material"
import Settings from "./pages/Settings" import Settings from "./pages/Settings"
import PasswordReset from "./pages/auth/PasswordReset" import PasswordReset from "./pages/auth/PasswordReset"
import MapTest from "./pages/MapTest" import MapTest from "./pages/MapTest"
import MonitorPage from "./pages/MonitorPage"
// Определение страниц с путями и компонентом для рендера // Определение страниц с путями и компонентом для рендера
export const pages = [ export const pages = [
@ -126,6 +127,14 @@ export const pages = [
drawer: true, drawer: true,
dashboard: true dashboard: true
}, },
{
label: "Монитор",
path: "/monitor",
icon: <MonitorHeart />,
component: <MonitorPage />,
drawer: true,
dashboard: true
},
] ]
function App() { function App() {

View File

@ -4,32 +4,20 @@ import 'ol/ol.css'
import Map from 'ol/Map' import Map from 'ol/Map'
import View from 'ol/View' import View from 'ol/View'
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction' import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { OSM, Source, Vector as VectorSource, XYZ } from 'ol/source' import { OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer' import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { get, Projection, transform, transformExtent } from 'ol/proj' import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box } from '@mui/material'
import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Container, Box } from '@mui/material' import { Adjust, Api, CircleOutlined, OpenWith, RectangleOutlined, Rule, Straighten, Timeline, Undo, Warning } from '@mui/icons-material'
import { Adjust, Api, CircleOutlined, DoubleArrow, FeaturedVideoSharp, Handyman, OpenWith, RectangleOutlined, Timeline, Undo, Warning } from '@mui/icons-material'
import { Type } from 'ol/geom/Geometry' import { Type } from 'ol/geom/Geometry'
import { altKeyOnly, click, doubleClick, noModifierKeys, platformModifierKey, pointerMove, shiftKeyOnly, singleClick } from 'ol/events/condition' import { click, noModifierKeys, shiftKeyOnly } from 'ol/events/condition'
import Feature, { FeatureLike } from 'ol/Feature' import Feature from 'ol/Feature'
import Style from 'ol/style/Style'
import Fill from 'ol/style/Fill'
import Stroke from 'ol/style/Stroke'
import { FlatStyleLike } from 'ol/style/flat'
import { SatelliteMapsProvider } from '../../interfaces/map' import { SatelliteMapsProvider } from '../../interfaces/map'
import Tile from 'ol/Tile' import { containsExtent } from 'ol/extent'
import ImageTile from 'ol/ImageTile' import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles'
import { createXYZ, TileGrid } from 'ol/tilegrid' import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
import { TileCoord } from 'ol/tilecoord' import { mapCenter, mapExtent } from './MapConstants'
import { register } from 'ol/proj/proj4'
import proj4 from 'proj4'
const MapComponent = () => { const MapComponent = () => {
proj4.defs('EPSG:3395', '+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs')
register(proj4);
const yandexProjection = get('EPSG:3395')?.setExtent([-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]) || 'EPSG:3395'
const mapElement = useRef<HTMLDivElement | null>(null) const mapElement = useRef<HTMLDivElement | null>(null)
const [currentTool, setCurrentTool] = useState<Type | null>(null) const [currentTool, setCurrentTool] = useState<Type | null>(null)
@ -37,18 +25,9 @@ const MapComponent = () => {
const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('yandex') const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('yandex')
const gMapsSatSource = useRef<XYZ>(new XYZ({ const gMapsSatSource = useRef<XYZ>(googleMapsSatelliteSource)
//url: `https://khms2.google.com/kh/v=984?x={x}&y={y}&z={z}`,
url: `http://localhost:5000/tile/google/{z}/{x}/{y}`,
attributions: 'Map data © Google'
}))
const yMapsSatSource = useRef<XYZ>(new XYZ({ const yMapsSatSource = useRef<XYZ>(yandexMapsSatelliteSource)
//url: `https://sat0{1-4}.maps.yandex.net/tiles?l=sat&x={x}&y={y}&z={z}&scale=1&lang=ru_RU&client_id=yandex-web-maps`,
url: `https://core-sat.maps.yandex.net/tiles?l=sat&x={x}&y={y}&z={z}&scale=1&lang=ru_RU`,
attributions: 'Map data © Yandex',
projection: yandexProjection,
}))
const satLayer = useRef<TileLayer>(new TileLayer({ const satLayer = useRef<TileLayer>(new TileLayer({
source: gMapsSatSource.current, source: gMapsSatSource.current,
@ -56,60 +35,27 @@ const MapComponent = () => {
const draw = useRef<Draw | null>(null) const draw = useRef<Draw | null>(null)
const snap = useRef<Snap | null>(null) const snap = useRef<Snap | null>(null)
const selectFeature = useRef<Select | null>(null)
const selectFeature = useRef<Select>(new Select({
condition: function (mapBrowserEvent) {
return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent);
},
}))
const drawingLayer = useRef<VectorLayer | null>(null) const drawingLayer = useRef<VectorLayer | null>(null)
const drawingLayerSource = useRef<VectorSource>(new VectorSource()) const drawingLayerSource = useRef<VectorSource>(new VectorSource())
const regionsLayer = useRef<VectorLayer>(new VectorLayer({ const regionsLayer = useRef<VectorLayer>(new VectorLayer({
source: new VectorSource({ source: regionsLayerSource,
url: 'sakha_republic.geojson', style: regionsLayerStyle
format: new GeoJSON(),
}),
style: new Style({
stroke: new Stroke({
color: 'blue',
width: 1,
}),
fill: new Fill({
color: 'rgba(0, 0, 255, 0.1)',
}),
}),
})) }))
const selectStyle = new Style({
fill: new Fill({
color: 'rgba(0, 0, 255, 0.3)',
}),
stroke: new Stroke({
color: 'rgba(255, 255, 255, 0.7)',
width: 2,
}),
})
const selectedRegion = useRef<Feature | null>(null) const selectedRegion = useRef<Feature | null>(null)
const geoLayer = useRef<VectorLayer | null>(null) const baseLayer = useRef<TileLayer>(new TileLayer({
source: new OSM(),
const geoLayerSource = useRef<VectorSource>(new VectorSource({
url: 'https://openlayers.org/data/vector/ecoregions.json',
format: new GeoJSON(),
})) }))
const baseLayer = useRef<TileLayer | null>(null)
const geoLayerStyle: FlatStyleLike = {
'fill-color': ['string', ['get', 'COLOR'], '#eee'],
}
const drawingLayerStyle: FlatStyleLike = {
'fill-color': 'rgba(255, 255, 255, 0.2)',
'stroke-color': '#ffcc33',
'stroke-width': 2,
'circle-radius': 7,
'circle-fill-color': '#ffcc33',
}
const addInteractions = () => { const addInteractions = () => {
if (currentTool) { if (currentTool) {
draw.current = new Draw({ draw.current = new Draw({
@ -156,60 +102,43 @@ const MapComponent = () => {
} }
} }
const zoomToFeature = (feature: Feature) => {
const geometry = feature.getGeometry()
const extent = geometry?.getExtent()
if (map.current && extent) {
map.current.getView().fit(extent, {
duration: 300,
maxZoom: 19,
})
}
}
useEffect(() => { useEffect(() => {
geoLayer.current = new VectorLayer({
background: '#1a2b39',
source: geoLayerSource.current,
style: geoLayerStyle,
})
baseLayer.current = new TileLayer({
source: new OSM(),
})
drawingLayer.current = new VectorLayer({ drawingLayer.current = new VectorLayer({
source: drawingLayerSource.current, source: drawingLayerSource.current,
style: drawingLayerStyle, style: drawingLayerStyle,
}) })
// Center coordinates of Yakutia in EPSG:3857
const center = transform([129.7578941, 62.030804], 'EPSG:4326', 'EPSG:3857')
// Extent for Yakutia in EPSG:4326
const extent4326 = [105.0, 55.0, 170.0, 75.0] // Approximate bounding box
// Transform extent to EPSG:3857
const extent = transformExtent(extent4326, 'EPSG:4326', 'EPSG:3857')
map.current = new Map({ map.current = new Map({
layers: [baseLayer.current, satLayer.current, regionsLayer.current, drawingLayer.current], layers: [baseLayer.current, satLayer.current, regionsLayer.current, drawingLayer.current],
target: mapElement.current as HTMLDivElement, target: mapElement.current as HTMLDivElement,
view: new View({ view: new View({
center, center: mapCenter,
zoom: 2, zoom: 2,
extent: [ maxZoom: 21,
11388546.533293726, extent: mapExtent,
7061866.113051185,
18924313.434856508,
13932243.11199202
],
}), }),
}) })
const modify = new Modify({ source: drawingLayerSource.current }) const modify = new Modify({ source: drawingLayerSource.current })
map.current.addInteraction(modify) map.current.addInteraction(modify)
selectFeature.current = new Select({
condition: function (mapBrowserEvent) {
return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent);
},
})
map.current.addInteraction(selectFeature.current) map.current.addInteraction(selectFeature.current)
selectFeature.current.on('select', (e) => { selectFeature.current.on('select', (e) => {
const selectedFeatures = e.selected const selectedFeatures = e.selected
console.log(selectedFeatures)
if (selectedFeatures.length > 0) { if (selectedFeatures.length > 0) {
selectedFeatures.forEach((feature) => { selectedFeatures.forEach((feature) => {
drawingLayerSource.current?.removeFeature(feature) drawingLayerSource.current?.removeFeature(feature)
@ -219,26 +148,63 @@ const MapComponent = () => {
loadFeatures() loadFeatures()
// Show current selected region
map.current.on('pointermove', function (e) { map.current.on('pointermove', function (e) {
if (selectedRegion.current !== null) { if (selectedRegion.current !== null) {
selectedRegion.current.setStyle(undefined) selectedRegion.current.setStyle(undefined)
selectedRegion.current = null selectedRegion.current = null
} }
if (map.current && selectStyle !== null) { if (map.current) {
map.current.forEachFeatureAtPixel(e.pixel, function (f) { map.current.forEachFeatureAtPixel(e.pixel, function (f) {
selectedRegion.current = f as Feature selectedRegion.current = f as Feature
selectedRegion.current.setStyle(selectStyle) selectedRegion.current.setStyle(selectStyle)
if (f.get('district')) { if (f.get('district')) {
setStatusText(f.get('district')) setStatusText(f.get('district'))
} }
return true return true
}) })
} }
}) })
map.current.on('click', function (e) {
if (selectedRegion.current !== null) {
selectedRegion.current = null
}
if (map.current) {
map.current.forEachFeatureAtPixel(e.pixel, function (f) {
selectedRegion.current = f as Feature
// Zoom to the selected feature
zoomToFeature(selectedRegion.current)
return true
});
}
})
// Hide regions layer when fully visible
map.current.on('moveend', function () {
const viewExtent = map.current?.getView().calculateExtent(map.current.getSize())
const features = regionsLayer.current.getSource()?.getFeatures()
let isViewCovered = false
features?.forEach((feature: Feature) => {
const featureExtent = feature?.getGeometry()?.getExtent()
if (viewExtent && featureExtent) {
if (containsExtent(featureExtent, viewExtent)) {
isViewCovered = true
}
}
})
regionsLayer.current.setVisible(!isViewCovered)
})
return () => { return () => {
map?.current?.setTarget(undefined) map?.current?.setTarget(undefined)
} }
@ -259,8 +225,6 @@ const MapComponent = () => {
const [statusText, setStatusText] = useState('') const [statusText, setStatusText] = useState('')
const selected = useRef<FeatureLike | null>(null)
// Visibility setting // Visibility setting
useEffect(() => { useEffect(() => {
satLayer.current?.setOpacity(satelliteOpacity) satLayer.current?.setOpacity(satelliteOpacity)
@ -352,10 +316,14 @@ const MapComponent = () => {
> >
<OpenWith /> <OpenWith />
</IconButton> </IconButton>
<IconButton>
<Straighten />
</IconButton>
</Stack> </Stack>
<Box> <Box>
<div id="map-container" ref={mapElement} style={{ width: '100%', height: '500px', maxHeight: '100%', position: 'relative', flexGrow: 1 }}></div> <div id="map-container" ref={mapElement} style={{ width: '100%', height: '600px', maxHeight: '100%', position: 'relative', flexGrow: 1 }}></div>
</Box> </Box>
<Stack> <Stack>

View File

@ -0,0 +1,9 @@
import { transform } from "ol/proj"
const mapExtent = [11388546.533293726, 7061866.113051185, 18924313.434856508, 13932243.11199202]
const mapCenter = transform([129.7578941, 62.030804], 'EPSG:4326', 'EPSG:3857')
export {
mapExtent,
mapCenter
}

View File

@ -0,0 +1,32 @@
import GeoJSON from "ol/format/GeoJSON";
import { get } from "ol/proj";
import { register } from "ol/proj/proj4";
import { XYZ } from "ol/source";
import VectorSource from "ol/source/Vector";
import proj4 from "proj4";
proj4.defs('EPSG:3395', '+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs')
register(proj4);
const yandexProjection = get('EPSG:3395')?.setExtent([-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]) || 'EPSG:3395'
const googleMapsSatelliteSource = new XYZ({
url: `${import.meta.env.VITE_API_EMS_URL}/tile/google/{z}/{x}/{y}`,
attributions: 'Map data © Google'
})
const yandexMapsSatelliteSource = new XYZ({
url: `${import.meta.env.VITE_API_EMS_URL}/tile/yandex/{z}/{x}/{y}`,
attributions: 'Map data © Yandex',
projection: yandexProjection,
})
const regionsLayerSource = new VectorSource({
url: 'sakha_republic.geojson',
format: new GeoJSON(),
})
export {
googleMapsSatelliteSource,
yandexMapsSatelliteSource,
regionsLayerSource
}

View File

@ -0,0 +1,38 @@
import Fill from "ol/style/Fill";
import { FlatStyleLike } from "ol/style/flat";
import Stroke from "ol/style/Stroke";
import Style from "ol/style/Style";
const drawingLayerStyle: FlatStyleLike = {
'fill-color': 'rgba(255, 255, 255, 0.2)',
'stroke-color': '#ffcc33',
'stroke-width': 2,
'circle-radius': 7,
'circle-fill-color': '#ffcc33',
}
const selectStyle = new Style({
fill: new Fill({
color: 'rgba(0, 0, 255, 0.3)',
}),
stroke: new Stroke({
color: 'rgba(255, 255, 255, 0.7)',
width: 2,
}),
})
const regionsLayerStyle = new Style({
stroke: new Stroke({
color: 'blue',
width: 1,
}),
fill: new Fill({
color: 'rgba(0, 0, 255, 0.1)',
}),
})
export {
drawingLayerStyle,
selectStyle,
regionsLayerStyle
}

View File

@ -0,0 +1,9 @@
import React from 'react'
function MonitorPage() {
return (
<div>Monitor</div>
)
}
export default MonitorPage

View File

@ -0,0 +1,14 @@
// vite.config.ts
import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js";
import react from "file:///app/node_modules/@vitejs/plugin-react-swc/index.mjs";
import { nodePolyfills } from "file:///app/node_modules/vite-plugin-node-polyfills/dist/index.js";
var vite_config_default = defineConfig({
plugins: [
nodePolyfills(),
react()
]
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvYXBwXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvYXBwL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9hcHAvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xyXG5pbXBvcnQgcmVhY3QgZnJvbSAnQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djJ1xyXG5pbXBvcnQgeyBub2RlUG9seWZpbGxzIH0gZnJvbSAndml0ZS1wbHVnaW4tbm9kZS1wb2x5ZmlsbHMnXHJcblxyXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xyXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xyXG4gIHBsdWdpbnM6IFtcclxuICAgIG5vZGVQb2x5ZmlsbHMoKSxcclxuICAgIHJlYWN0KCksXHJcbiAgXSxcclxufSlcclxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUE4TCxTQUFTLG9CQUFvQjtBQUMzTixPQUFPLFdBQVc7QUFDbEIsU0FBUyxxQkFBcUI7QUFHOUIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUztBQUFBLElBQ1AsY0FBYztBQUFBLElBQ2QsTUFBTTtBQUFBLEVBQ1I7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=

View File

@ -27,6 +27,8 @@ services:
build: build:
context: ./ems context: ./ems
dockerfile: Dockerfile dockerfile: Dockerfile
volumes:
- ./ems/tile_data:/app/tiles
links: links:
- redis_db:redis_db - redis_db:redis_db
- psql_db:psql_db - psql_db:psql_db
@ -43,6 +45,19 @@ services:
- ${EMS_PORT}:${EMS_PORT} - ${EMS_PORT}:${EMS_PORT}
restart: always restart: always
monitor:
container_name: monitor
build:
context: ./monitor
dockerfile: Dockerfile
environment:
- MONITOR_PORT=${MONITOR_PORT}
ports:
- ${MONITOR_PORT}:${MONITOR_PORT}
volumes:
- ./monitor/data:/app/data
restart: always
psql_db: psql_db:
container_name: psql_db container_name: psql_db
image: postgres:16.4-alpine image: postgres:16.4-alpine
@ -64,7 +79,18 @@ services:
retries: 5 retries: 5
start_period: 10s start_period: 10s
restart: always restart: always
volumes: clickhouse_test:
redis_data: container_name: clickhouse_test
psql_data: image: clickhouse/clickhouse-server
environment:
- CLICKHOUSE_DB=${CLICKHOUSE_DB}
- CLICKHOUSE_USER=${CLICKHOUSE_USER}
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=${CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT}
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
ports:
- 8123:8123
- 9000:9000
expose:
- 8123
- 9000

3
ems/.gitignore vendored
View File

@ -22,3 +22,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Docker volumes
tile_data

1
ems/README.md Normal file
View File

@ -0,0 +1 @@
# EMS (ИКС)

119
ems/src/index-redis.ts Normal file
View File

@ -0,0 +1,119 @@
import express, { Request, Response } from 'express'
import { Redis } from 'ioredis'
import dotenv from 'dotenv'
import bodyParser from 'body-parser'
import { SatelliteMapsProvider } from './interfaces/map'
const axios = require('axios');
const cors = require('cors')
const redis = new Redis({
port: Number(process.env.REDIS_PORT) || 6379,
host: process.env.REDIS_HOST,
password: process.env.REDIS_PASSWORD,
})
dotenv.config()
const app = express()
const port = process.env.EMS_PORT
// Middleware to parse JSON requests
app.use(bodyParser.json())
const getTileUrl = (provider: string, x: string, y: string, z: string) => {
if (provider === 'google') {
return `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`;
} else if (provider === 'yandex') {
return `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`;
}
throw new Error('Invalid provider');
}
app.get('/tile/:provider/:z/:x/:y', async (req, res) => {
const { provider, x, y, z } = req.params;
const cacheKey = `${provider}:${z}:${x}:${y}`;
try {
// Check if tile is in cache
redis.get(cacheKey, async (err, cachedTile) => {
if (err) {
console.error('Redis GET error:', err);
return res.status(500).send('Server error');
}
if (cachedTile) {
// If cached, return tile
console.log('Tile served from cache');
const imgBuffer = Buffer.from(cachedTile, 'base64');
res.writeHead(200, {
'Content-Type': 'image/png',
'Content-Length': imgBuffer.length,
});
return res.end(imgBuffer);
} else {
// Fetch tile from provider
const tileUrl = getTileUrl(provider, x, y, z);
const response = await axios.get(tileUrl, {
responseType: 'arraybuffer',
});
// Cache the tile in Redis
const base64Tile = Buffer.from(response.data).toString('base64');
redis.setex(cacheKey, 3600 * 24 * 30, base64Tile); // Cache for 1 hour
// Return the tile to the client
res.writeHead(200, {
'Content-Type': 'image/png',
'Content-Length': response.data.length,
});
return res.end(response.data);
}
});
} catch (error) {
console.error('Error fetching tile:', error);
res.status(500).send('Error fetching tile');
}
})
app.get('/hello', cors(), (req: Request, res: Response) => {
res.send('Hello, World!')
})
// Route to store GeoJSON data
app.post('/geojson', cors(), async (req: Request, res: Response) => {
const geoJSON = req.body
if (!geoJSON || !geoJSON.features) {
return res.status(400).send('Invalid GeoJSON')
}
const id = `geojson:${Date.now()}`;
redis.set(id, JSON.stringify(geoJSON), (err, reply) => {
if (err) {
return res.status(500).send('Error saving GeoJSON to Redis');
}
res.send({ status: 'success', id });
})
})
// Route to fetch GeoJSON data
app.get('/geojson/:id', cors(), async (req: Request, res: Response) => {
const id = req.params.id;
redis.get(id, (err, data) => {
if (err) {
return res.status(500).send('Error fetching GeoJSON from Redis');
}
if (data) {
res.send(JSON.parse(data));
} else {
res.status(404).send('GeoJSON not found');
}
})
})
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
})

View File

@ -1,119 +1,48 @@
import express, { Request, Response } from 'express' import express, { Request, Response } from 'express';
import { Redis } from 'ioredis' import fs from 'fs';
import dotenv from 'dotenv' import path from 'path';
import bodyParser from 'body-parser' import axios from 'axios';
import { SatelliteMapsProvider } from './interfaces/map'
const axios = require('axios');
const cors = require('cors') const app = express();
const PORT = process.env.EMS_PORT;
const redis = new Redis({ const tileFolder = path.join(__dirname, '..', 'tiles');
port: Number(process.env.REDIS_PORT) || 6379,
host: process.env.REDIS_HOST,
password: process.env.REDIS_PASSWORD,
})
dotenv.config() const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise<Buffer> => {
const url = provider === 'google'
? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`
: `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`;
const app = express() const response = await axios.get(url, { responseType: 'arraybuffer' });
const port = process.env.EMS_PORT return response.data;
};
// Middleware to parse JSON requests app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => {
app.use(bodyParser.json()) const { provider, z, x, y } = req.params;
const getTileUrl = (provider: string, x: string, y: string, z: string) => { if (!['google', 'yandex'].includes(provider)) {
if (provider === 'google') { return res.status(400).send('Invalid provider');
return `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`;
} else if (provider === 'yandex') {
return `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`;
} }
throw new Error('Invalid provider');
}
app.get('/tile/:provider/:z/:x/:y', async (req, res) => { const tilePath = path.join(tileFolder, provider, z, x, `${y}.jpg`);
const { provider, x, y, z } = req.params;
const cacheKey = `${provider}:${z}:${x}:${y}`; if (fs.existsSync(tilePath)) {
return res.sendFile(tilePath);
}
try { try {
// Check if tile is in cache const tileData = await fetchTileFromAPI(provider, z, x, y);
redis.get(cacheKey, async (err, cachedTile) => {
if (err) {
console.error('Redis GET error:', err);
return res.status(500).send('Server error');
}
if (cachedTile) { fs.mkdirSync(path.dirname(tilePath), { recursive: true });
// If cached, return tile
console.log('Tile served from cache');
const imgBuffer = Buffer.from(cachedTile, 'base64');
res.writeHead(200, {
'Content-Type': 'image/png',
'Content-Length': imgBuffer.length,
});
return res.end(imgBuffer);
} else {
// Fetch tile from provider
const tileUrl = getTileUrl(provider, x, y, z);
const response = await axios.get(tileUrl, {
responseType: 'arraybuffer',
});
// Cache the tile in Redis fs.writeFileSync(tilePath, tileData);
const base64Tile = Buffer.from(response.data).toString('base64');
redis.setex(cacheKey, 3600 * 24 * 30, base64Tile); // Cache for 1 hour
// Return the tile to the client res.contentType('image/jpeg');
res.writeHead(200, { res.send(tileData);
'Content-Type': 'image/png',
'Content-Length': response.data.length,
});
return res.end(response.data);
}
});
} catch (error) { } catch (error) {
console.error('Error fetching tile:', error); console.error('Error fetching tile from API:', error);
res.status(500).send('Error fetching tile'); res.status(500).send('Error fetching tile from API');
} }
}) });
app.get('/hello', cors(), (req: Request, res: Response) => { app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
res.send('Hello, World!')
})
// Route to store GeoJSON data
app.post('/geojson', cors(), async (req: Request, res: Response) => {
const geoJSON = req.body
if (!geoJSON || !geoJSON.features) {
return res.status(400).send('Invalid GeoJSON')
}
const id = `geojson:${Date.now()}`;
redis.set(id, JSON.stringify(geoJSON), (err, reply) => {
if (err) {
return res.status(500).send('Error saving GeoJSON to Redis');
}
res.send({ status: 'success', id });
})
})
// Route to fetch GeoJSON data
app.get('/geojson/:id', cors(), async (req: Request, res: Response) => {
const id = req.params.id;
redis.get(id, (err, data) => {
if (err) {
return res.status(500).send('Error fetching GeoJSON from Redis');
}
if (data) {
res.send(JSON.parse(data));
} else {
res.status(404).send('GeoJSON not found');
}
})
})
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
})

177
monitor/.gitignore vendored Normal file
View File

@ -0,0 +1,177 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
data

19
monitor/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
# Start with an official lightweight image
FROM oven/bun:slim
# Set the working directory in the container
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . .
# Install project dependencies
RUN bun install
ENV MONITOR_PORT=$MONITOR_PORT
# Expose the port the app runs on
EXPOSE $MONITOR_PORT
# Command to run the application
CMD ["bun", "index.ts"]

15
monitor/README.md Normal file
View File

@ -0,0 +1,15 @@
# monitor
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.1.21. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

BIN
monitor/bun.lockb Normal file

Binary file not shown.

97
monitor/index.ts Normal file
View File

@ -0,0 +1,97 @@
import { serve } from 'bun';
import { Database } from 'bun:sqlite';
const db = new Database('./data/servers.db');
// Initialize the database table if it doesn't exist
db.run(`
CREATE TABLE IF NOT EXISTS servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
ip TEXT NOT NULL,
ping_rate INTEGER NOT NULL
)
`);
// Function to check server availability
async function checkServer(server: { name: string; ip: string; ping_rate: number; }) {
try {
const response = await Bun.spawn(['ping', '-c', '1', server.ip]);
if (response.exitCode === 0) {
console.log(`[${new Date().toISOString()}] ${server.name} (${server.ip}) is up.`);
} else {
console.error(`[${new Date().toISOString()}] ${server.name} (${server.ip}) is down!`);
}
} catch (error) {
console.error(`[${new Date().toISOString()}] Error pinging ${server.name} (${server.ip}):`, error);
}
}
// Function to monitor servers based on their individual ping rates
async function monitorServer(server: any) {
while (true) {
await checkServer(server);
await new Promise(resolve => setTimeout(resolve, server.ping_rate));
}
}
// Start monitoring all servers
async function startMonitoring() {
const servers = db.query(`SELECT * FROM servers`).all();
servers.forEach(server => monitorServer(server));
}
// Start monitoring in the background
startMonitoring();
// API Server to manage servers
const server = serve({
port: process.env.MONITOR_PORT || 1234,
fetch: async (req) => {
const url = new URL(req.url);
const pathname = url.pathname;
const method = req.method;
if (pathname === '/servers' && method === 'GET') {
const servers = db.query(`SELECT * FROM servers`).all();
return new Response(JSON.stringify(servers), {
headers: { 'Content-Type': 'application/json' },
});
}
if (pathname === '/server' && method === 'POST') {
const data = await req.json();
const { name, ip, ping_rate } = data;
if (!name || !ip || !ping_rate) {
return new Response('Missing fields', { status: 400 });
}
db.run(
`INSERT INTO servers (name, ip, ping_rate) VALUES (?, ?, ?)`,
name, ip, ping_rate
);
return new Response('Server added', { status: 201 });
}
if (pathname.startsWith('/server/') && method === 'PUT') {
const id = pathname.split('/').pop();
const data = await req.json();
const { name, ip, ping_rate } = data;
if (!id || !name || !ip || !ping_rate) {
return new Response('Missing fields', { status: 400 });
}
db.run(
`UPDATE servers SET name = ?, ip = ?, ping_rate = ? WHERE id = ?`,
name, ip, ping_rate, id
);
return new Response('Server updated', { status: 200 });
}
return new Response('Not Found', { status: 404 });
},
});
console.log(`API server and monitoring running on http://localhost:${server.port}`);

11
monitor/package.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "monitor",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}

27
monitor/tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}