Map caching, clickhouse test service
This commit is contained in:
14
.env.example
Normal file
14
.env.example
Normal 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=
|
@ -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: <MonitorHeart />,
|
||||
component: <MonitorPage />,
|
||||
drawer: true,
|
||||
dashboard: true
|
||||
},
|
||||
]
|
||||
|
||||
function App() {
|
||||
|
@ -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<HTMLDivElement | null>(null)
|
||||
const [currentTool, setCurrentTool] = useState<Type | null>(null)
|
||||
|
||||
@ -37,18 +25,9 @@ const MapComponent = () => {
|
||||
|
||||
const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('yandex')
|
||||
|
||||
const gMapsSatSource = useRef<XYZ>(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<XYZ>(googleMapsSatelliteSource)
|
||||
|
||||
const yMapsSatSource = useRef<XYZ>(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<XYZ>(yandexMapsSatelliteSource)
|
||||
|
||||
const satLayer = useRef<TileLayer>(new TileLayer({
|
||||
source: gMapsSatSource.current,
|
||||
@ -56,60 +35,27 @@ const MapComponent = () => {
|
||||
|
||||
const draw = useRef<Draw | 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 drawingLayerSource = useRef<VectorSource>(new VectorSource())
|
||||
|
||||
const regionsLayer = useRef<VectorLayer>(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<Feature | null>(null)
|
||||
|
||||
const geoLayer = useRef<VectorLayer | null>(null)
|
||||
|
||||
const geoLayerSource = useRef<VectorSource>(new VectorSource({
|
||||
url: 'https://openlayers.org/data/vector/ecoregions.json',
|
||||
format: new GeoJSON(),
|
||||
const baseLayer = useRef<TileLayer>(new TileLayer({
|
||||
source: new OSM(),
|
||||
}))
|
||||
|
||||
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 = () => {
|
||||
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<FeatureLike | null>(null)
|
||||
|
||||
// Visibility setting
|
||||
useEffect(() => {
|
||||
satLayer.current?.setOpacity(satelliteOpacity)
|
||||
@ -352,10 +316,14 @@ const MapComponent = () => {
|
||||
>
|
||||
<OpenWith />
|
||||
</IconButton>
|
||||
|
||||
<IconButton>
|
||||
<Straighten />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
<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>
|
||||
|
||||
<Stack>
|
||||
|
9
client/src/components/map/MapConstants.ts
Normal file
9
client/src/components/map/MapConstants.ts
Normal 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
|
||||
}
|
32
client/src/components/map/MapSources.ts
Normal file
32
client/src/components/map/MapSources.ts
Normal 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
|
||||
}
|
38
client/src/components/map/MapStyles.ts
Normal file
38
client/src/components/map/MapStyles.ts
Normal 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
|
||||
}
|
9
client/src/pages/MonitorPage.tsx
Normal file
9
client/src/pages/MonitorPage.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
function MonitorPage() {
|
||||
return (
|
||||
<div>Monitor</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MonitorPage
|
@ -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=
|
@ -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
|
||||
@ -65,6 +80,17 @@ services:
|
||||
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
|
||||
|
3
ems/.gitignore
vendored
3
ems/.gitignore
vendored
@ -22,3 +22,6 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Docker volumes
|
||||
tile_data
|
||||
|
1
ems/README.md
Normal file
1
ems/README.md
Normal file
@ -0,0 +1 @@
|
||||
# EMS (ИКС)
|
119
ems/src/index-redis.ts
Normal file
119
ems/src/index-redis.ts
Normal 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}`);
|
||||
})
|
135
ems/src/index.ts
135
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<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 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}`));
|
||||
|
177
monitor/.gitignore
vendored
Normal file
177
monitor/.gitignore
vendored
Normal 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
19
monitor/Dockerfile
Normal 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
15
monitor/README.md
Normal 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
BIN
monitor/bun.lockb
Normal file
Binary file not shown.
97
monitor/index.ts
Normal file
97
monitor/index.ts
Normal 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
11
monitor/package.json
Normal 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
27
monitor/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user