From ab88fd5ea5f6dee7ae8b36b2d001e883344420e4 Mon Sep 17 00:00:00 2001 From: cracklesparkle Date: Mon, 26 Aug 2024 16:11:37 +0900 Subject: [PATCH] Map caching, clickhouse test service --- .env.example | 14 ++ client/src/App.tsx | 11 +- client/src/components/map/MapComponent.tsx | 192 ++++++++---------- client/src/components/map/MapConstants.ts | 9 + client/src/components/map/MapSources.ts | 32 +++ client/src/components/map/MapStyles.ts | 38 ++++ client/src/pages/MonitorPage.tsx | 9 + ....timestamp-1724631266472-b5c6d89e0dd7b.mjs | 14 ++ docker-compose.yml | 34 +++- ems/.gitignore | 3 + ems/README.md | 1 + ems/src/index-redis.ts | 119 +++++++++++ ems/src/index.ts | 135 +++--------- monitor/.gitignore | 177 ++++++++++++++++ monitor/Dockerfile | 19 ++ monitor/README.md | 15 ++ monitor/bun.lockb | Bin 0 -> 3124 bytes monitor/index.ts | 97 +++++++++ monitor/package.json | 11 + monitor/tsconfig.json | 27 +++ 20 files changed, 737 insertions(+), 220 deletions(-) create mode 100644 .env.example create mode 100644 client/src/components/map/MapConstants.ts create mode 100644 client/src/components/map/MapSources.ts create mode 100644 client/src/components/map/MapStyles.ts create mode 100644 client/src/pages/MonitorPage.tsx create mode 100644 client/vite.config.ts.timestamp-1724631266472-b5c6d89e0dd7b.mjs create mode 100644 ems/README.md create mode 100644 ems/src/index-redis.ts create mode 100644 monitor/.gitignore create mode 100644 monitor/Dockerfile create mode 100644 monitor/README.md create mode 100644 monitor/bun.lockb create mode 100644 monitor/index.ts create mode 100644 monitor/package.json create mode 100644 monitor/tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..befe7f5 --- /dev/null +++ b/.env.example @@ -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= \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index d8566c6..a6f6381 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -15,10 +15,11 @@ import Documents from "./pages/Documents" import Reports from "./pages/Reports" import Boilers from "./pages/Boilers" 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 PasswordReset from "./pages/auth/PasswordReset" import MapTest from "./pages/MapTest" +import MonitorPage from "./pages/MonitorPage" // Определение страниц с путями и компонентом для рендера export const pages = [ @@ -126,6 +127,14 @@ export const pages = [ drawer: true, dashboard: true }, + { + label: "Монитор", + path: "/monitor", + icon: , + component: , + drawer: true, + dashboard: true + }, ] function App() { diff --git a/client/src/components/map/MapComponent.tsx b/client/src/components/map/MapComponent.tsx index bf81c4a..7b62d69 100644 --- a/client/src/components/map/MapComponent.tsx +++ b/client/src/components/map/MapComponent.tsx @@ -4,32 +4,20 @@ import 'ol/ol.css' import Map from 'ol/Map' import View from 'ol/View' 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 { get, Projection, transform, transformExtent } from 'ol/proj' -import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Container, Box } from '@mui/material' -import { Adjust, Api, CircleOutlined, DoubleArrow, FeaturedVideoSharp, Handyman, OpenWith, RectangleOutlined, Timeline, Undo, Warning } from '@mui/icons-material' +import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box } from '@mui/material' +import { Adjust, Api, CircleOutlined, OpenWith, RectangleOutlined, Rule, Straighten, Timeline, Undo, Warning } from '@mui/icons-material' import { Type } from 'ol/geom/Geometry' -import { altKeyOnly, click, doubleClick, noModifierKeys, platformModifierKey, pointerMove, shiftKeyOnly, singleClick } from 'ol/events/condition' -import Feature, { FeatureLike } 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 { click, noModifierKeys, shiftKeyOnly } from 'ol/events/condition' +import Feature from 'ol/Feature' import { SatelliteMapsProvider } from '../../interfaces/map' -import Tile from 'ol/Tile' -import ImageTile from 'ol/ImageTile' -import { createXYZ, TileGrid } from 'ol/tilegrid' -import { TileCoord } from 'ol/tilecoord' -import { register } from 'ol/proj/proj4' -import proj4 from 'proj4' +import { containsExtent } from 'ol/extent' +import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles' +import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources' +import { mapCenter, mapExtent } from './MapConstants' 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(null) const [currentTool, setCurrentTool] = useState(null) @@ -37,18 +25,9 @@ const MapComponent = () => { const [satMapsProvider, setSatMapsProvider] = useState('yandex') - const gMapsSatSource = useRef(new XYZ({ - //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 gMapsSatSource = useRef(googleMapsSatelliteSource) - const yMapsSatSource = useRef(new XYZ({ - //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 yMapsSatSource = useRef(yandexMapsSatelliteSource) const satLayer = useRef(new TileLayer({ source: gMapsSatSource.current, @@ -56,60 +35,27 @@ const MapComponent = () => { const draw = useRef(null) const snap = useRef(null) - const selectFeature = useRef(new Select({ + condition: function (mapBrowserEvent) { + return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent); + }, + })) const drawingLayer = useRef(null) const drawingLayerSource = useRef(new VectorSource()) const regionsLayer = useRef(new VectorLayer({ - source: new VectorSource({ - url: 'sakha_republic.geojson', - format: new GeoJSON(), - }), - style: new Style({ - stroke: new Stroke({ - color: 'blue', - width: 1, - }), - fill: new Fill({ - color: 'rgba(0, 0, 255, 0.1)', - }), - }), + source: regionsLayerSource, + style: regionsLayerStyle })) - 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(null) - const geoLayer = useRef(null) - - const geoLayerSource = useRef(new VectorSource({ - url: 'https://openlayers.org/data/vector/ecoregions.json', - format: new GeoJSON(), + const baseLayer = useRef(new TileLayer({ + source: new OSM(), })) - const baseLayer = useRef(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 = () => { if (currentTool) { 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(() => { - geoLayer.current = new VectorLayer({ - background: '#1a2b39', - source: geoLayerSource.current, - style: geoLayerStyle, - }) - - baseLayer.current = new TileLayer({ - source: new OSM(), - }) - drawingLayer.current = new VectorLayer({ source: drawingLayerSource.current, 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({ layers: [baseLayer.current, satLayer.current, regionsLayer.current, drawingLayer.current], target: mapElement.current as HTMLDivElement, view: new View({ - center, + center: mapCenter, zoom: 2, - extent: [ - 11388546.533293726, - 7061866.113051185, - 18924313.434856508, - 13932243.11199202 - ], + maxZoom: 21, + extent: mapExtent, }), }) const modify = new Modify({ source: drawingLayerSource.current }) map.current.addInteraction(modify) - selectFeature.current = new Select({ - condition: function (mapBrowserEvent) { - return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent); - }, - }) map.current.addInteraction(selectFeature.current) selectFeature.current.on('select', (e) => { const selectedFeatures = e.selected - console.log(selectedFeatures) - if (selectedFeatures.length > 0) { selectedFeatures.forEach((feature) => { drawingLayerSource.current?.removeFeature(feature) @@ -219,26 +148,63 @@ const MapComponent = () => { loadFeatures() + // Show current selected region map.current.on('pointermove', function (e) { if (selectedRegion.current !== null) { selectedRegion.current.setStyle(undefined) selectedRegion.current = null } - if (map.current && selectStyle !== null) { + if (map.current) { map.current.forEachFeatureAtPixel(e.pixel, function (f) { selectedRegion.current = f as Feature - selectedRegion.current.setStyle(selectStyle) if (f.get('district')) { setStatusText(f.get('district')) } + 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 () => { map?.current?.setTarget(undefined) } @@ -259,8 +225,6 @@ const MapComponent = () => { const [statusText, setStatusText] = useState('') - const selected = useRef(null) - // Visibility setting useEffect(() => { satLayer.current?.setOpacity(satelliteOpacity) @@ -352,10 +316,14 @@ const MapComponent = () => { > + + + + -
+
diff --git a/client/src/components/map/MapConstants.ts b/client/src/components/map/MapConstants.ts new file mode 100644 index 0000000..4180a86 --- /dev/null +++ b/client/src/components/map/MapConstants.ts @@ -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 +} \ No newline at end of file diff --git a/client/src/components/map/MapSources.ts b/client/src/components/map/MapSources.ts new file mode 100644 index 0000000..7403867 --- /dev/null +++ b/client/src/components/map/MapSources.ts @@ -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 +} \ No newline at end of file diff --git a/client/src/components/map/MapStyles.ts b/client/src/components/map/MapStyles.ts new file mode 100644 index 0000000..cecaf7a --- /dev/null +++ b/client/src/components/map/MapStyles.ts @@ -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 +} \ No newline at end of file diff --git a/client/src/pages/MonitorPage.tsx b/client/src/pages/MonitorPage.tsx new file mode 100644 index 0000000..02bbb62 --- /dev/null +++ b/client/src/pages/MonitorPage.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +function MonitorPage() { + return ( +
Monitor
+ ) +} + +export default MonitorPage \ No newline at end of file diff --git a/client/vite.config.ts.timestamp-1724631266472-b5c6d89e0dd7b.mjs b/client/vite.config.ts.timestamp-1724631266472-b5c6d89e0dd7b.mjs new file mode 100644 index 0000000..09a6df0 --- /dev/null +++ b/client/vite.config.ts.timestamp-1724631266472-b5c6d89e0dd7b.mjs @@ -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= diff --git a/docker-compose.yml b/docker-compose.yml index 11e5810..9ecfa8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,8 @@ services: build: context: ./ems dockerfile: Dockerfile + volumes: + - ./ems/tile_data:/app/tiles links: - redis_db:redis_db - psql_db:psql_db @@ -43,6 +45,19 @@ services: - ${EMS_PORT}:${EMS_PORT} 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: container_name: psql_db image: postgres:16.4-alpine @@ -64,7 +79,18 @@ services: retries: 5 start_period: 10s restart: always - -volumes: - redis_data: - psql_data: + + clickhouse_test: + container_name: clickhouse_test + 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 diff --git a/ems/.gitignore b/ems/.gitignore index a547bf3..b80f573 100644 --- a/ems/.gitignore +++ b/ems/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Docker volumes +tile_data diff --git a/ems/README.md b/ems/README.md new file mode 100644 index 0000000..ea0ed7f --- /dev/null +++ b/ems/README.md @@ -0,0 +1 @@ +# EMS (ИКС) \ No newline at end of file diff --git a/ems/src/index-redis.ts b/ems/src/index-redis.ts new file mode 100644 index 0000000..6bb10bf --- /dev/null +++ b/ems/src/index-redis.ts @@ -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}`); +}) diff --git a/ems/src/index.ts b/ems/src/index.ts index 6bb10bf..1f49817 100644 --- a/ems/src/index.ts +++ b/ems/src/index.ts @@ -1,119 +1,48 @@ -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'); +import express, { Request, Response } from 'express'; +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; -const cors = require('cors') +const app = express(); +const PORT = process.env.EMS_PORT; -const redis = new Redis({ - port: Number(process.env.REDIS_PORT) || 6379, - host: process.env.REDIS_HOST, - password: process.env.REDIS_PASSWORD, -}) +const tileFolder = path.join(__dirname, '..', 'tiles'); -dotenv.config() +const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise => { + 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 port = process.env.EMS_PORT + const response = await axios.get(url, { responseType: 'arraybuffer' }); + return response.data; +}; -// Middleware to parse JSON requests -app.use(bodyParser.json()) +app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => { + const { provider, z, x, y } = req.params; -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`; + if (!['google', 'yandex'].includes(provider)) { + return res.status(400).send('Invalid provider'); } - 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}`; + const tilePath = path.join(tileFolder, provider, z, x, `${y}.jpg`); + + if (fs.existsSync(tilePath)) { + return res.sendFile(tilePath); + } 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'); - } + const tileData = await fetchTileFromAPI(provider, z, x, y); - 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', - }); + fs.mkdirSync(path.dirname(tilePath), { recursive: true }); - // Cache the tile in Redis - const base64Tile = Buffer.from(response.data).toString('base64'); - redis.setex(cacheKey, 3600 * 24 * 30, base64Tile); // Cache for 1 hour + fs.writeFileSync(tilePath, tileData); - // Return the tile to the client - res.writeHead(200, { - 'Content-Type': 'image/png', - 'Content-Length': response.data.length, - }); - return res.end(response.data); - } - }); + res.contentType('image/jpeg'); + res.send(tileData); } catch (error) { - console.error('Error fetching tile:', error); - res.status(500).send('Error fetching tile'); + console.error('Error fetching tile from API:', error); + res.status(500).send('Error fetching tile from API'); } -}) +}); -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}`); -}) +app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/monitor/.gitignore b/monitor/.gitignore new file mode 100644 index 0000000..b8c6b16 --- /dev/null +++ b/monitor/.gitignore @@ -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 \ No newline at end of file diff --git a/monitor/Dockerfile b/monitor/Dockerfile new file mode 100644 index 0000000..87179e4 --- /dev/null +++ b/monitor/Dockerfile @@ -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"] diff --git a/monitor/README.md b/monitor/README.md new file mode 100644 index 0000000..e62b40b --- /dev/null +++ b/monitor/README.md @@ -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. diff --git a/monitor/bun.lockb b/monitor/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..b28274c4e23148e27f1caf7cbbce1a00d436970e GIT binary patch literal 3124 zcmY#Z)GsYA(of3F(@)JSQ%EY!;{sycoc!eMw9K4T-L(9o+{6;yG6OCq1_p-YkjVMM zOSD}?rB3L4{Fa->``FWWo z`9%y|Kp7xz@C4GFKzbUG1{&6I8%Xm2>0YQfkOHgwRj|TSB#6`M6r1K=Z-d4!)o;?z zb_l&zny4SutG3nFk~81t*sH)BQs#Ws1|99z)Feib0TcjGD1d+n5Ss$s z1u_qWVLk;i@dB9sAfPI1B>hr&bs;2S`WFI)tbqDK?f`}l10O;OUKULMbD%0mpng^$ z4YC_V69ZuSHGrNr0qO_&8>AQH24VnAe+E#$4b*;+eimXhL)C!H0AZjJ49rkA$P5sl z4TwPiM1%P7ga%}jr|%#CKafGx1VHl`86f5&8_iuUSVm!S!!RVxxOqftiIq)$?D%TC!1}4#UGPdVsu1k&rMr@g%t&PuaL}zg)_+6 zEc169u3p={L3Uq+zrg38lkcv1V!LduR>c1sXZl}BY@f0E{P|n_OXNDEKRtah@vX`3 zxmgEYcYS{_DNIQC)$5AvgZ)V6g2EdHST>$K{QOF0f_>?+3T;QGV-x&$1#P@yZ{C%f zk=P?L>rTm%LuMC>>+M#TWGDt29A%t%xY(|LMy3g~-89zn!YJ)oNaiBPKZ{yGkM;9~ zB~Dzc0t+WEk`HST3Cl2kd(`@j{ll+iC9BO>Y*c&}FxBs@ivHe~8IhH&G5Q@fn2Sz%NHm8b=l_d@?3pb~~KwpK#*#q^R#< zcNyDb6Uq+oiixhBESURs-?g}igQd<|9nLCO3@0L)3rlC9uwXH|we8!XW(GI@w=0v~ zxc#lBytn-J!8J4J<>ia#IqnKS*y7&yI{xF7Q-YT&j~zD%I?J-1Q{i{*y!ayNDL1dP zxBmp1J5T@??l77hcMPEO$#L#L>Dd4+i;h5PE}P<_WWCJ1;*!Li96eZ#s~1v~nrf$D zWS~%-S(TcfrlVk@keHL1o|m5nsv|+*-+u@Iu|etZ1JpncXeGpDQ)+Bv2h@kd1WEl*vtdJMV1y#nIHmckb%Vb?=7}@ zCtJ9yq_{Y>2xwaDWrfC|H18iVz(@qOi&`v9&oEZ-{rdrR!5nA{444Lh;cbXBxJxPv zQj3#|G7Cx^z)XE$t1qLZq@dVJU%x0dJ+ru^s8TPlAUCU6FTW^VAE8KJ7o=1d6w(%Y zCFxZl4%h@dy5VY|n!)P9MgYSXXa>~u@?t0>FFz#}k8wzq6qt&;(>!XFv89#F9zx^FV+PoFe7k~fa4AlUwHH*q5x 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}`); diff --git a/monitor/package.json b/monitor/package.json new file mode 100644 index 0000000..af3d9d8 --- /dev/null +++ b/monitor/package.json @@ -0,0 +1,11 @@ +{ + "name": "monitor", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/monitor/tsconfig.json b/monitor/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/monitor/tsconfig.json @@ -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 + } +}