Map caching in Redis

This commit is contained in:
cracklesparkle
2024-08-23 17:50:53 +09:00
parent 97b44a4db7
commit 579bbf7764
23 changed files with 688 additions and 143 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@
.vscode
__pycache__
.env
redis_data
redis_data
psql_data

View File

@ -28,6 +28,7 @@
"file-type": "^19.0.0",
"ol": "^10.0.0",
"postcss": "^8.4.38",
"proj4": "^2.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.0",
@ -36,6 +37,7 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/proj4": "^2.5.5",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
@ -3743,6 +3745,12 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
"node_modules/@types/proj4": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/@types/proj4/-/proj4-2.5.5.tgz",
"integrity": "sha512-y4tHUVVoMEOm2nxRLQ2/ET8upj/pBmoutGxFw2LZJTQWPgWXI+cbxVEUFFmIzr/bpFR83hGDOTSXX6HBeObvZA==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
@ -7661,6 +7669,11 @@
"node": ">= 8"
}
},
"node_modules/mgrs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz",
"integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA=="
},
"node_modules/micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
@ -8501,6 +8514,15 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"node_modules/proj4": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/proj4/-/proj4-2.12.0.tgz",
"integrity": "sha512-cQJxcVX7+fmAhOxoazKgk76GkGYQ5HcLod4rdy2MizhPvLdrZQJThxsHoz/TjjdxUvTm/rbozMgE0q9mdXKWIw==",
"dependencies": {
"mgrs": "1.0.0",
"wkt-parser": "^1.3.3"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -10485,6 +10507,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wkt-parser": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz",
"integrity": "sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw=="
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",

View File

@ -31,6 +31,7 @@
"file-type": "^19.0.0",
"ol": "^10.0.0",
"postcss": "^8.4.38",
"proj4": "^2.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.0",
@ -39,6 +40,7 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/proj4": "^2.5.5",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
@ -54,4 +56,4 @@
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.0"
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -73,7 +73,7 @@ export default function ServersView() {
)}
/>
),
width: 200
flex: 1
}
]

View File

@ -22,20 +22,20 @@ import {
} from '@mui/x-data-grid';
interface EditToolbarProps {
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void
setRowModesModel: (
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void;
columns: GridColDef[];
autoComplete?: React.ReactElement | null;
) => void
columns: GridColDef[]
autoComplete?: React.ReactElement | null
}
function EditToolbar(props: EditToolbarProps) {
const { setRows, setRowModesModel, columns, autoComplete } = props;
const { setRows, setRowModesModel, columns, autoComplete } = props
const handleClick = () => {
const id = Date.now().toString(36)
const newValues: any = {};
const newValues: any = {}
columns.forEach(column => {
if (column.type === 'number') {
@ -59,7 +59,7 @@ function EditToolbar(props: EditToolbarProps) {
setRowModesModel((oldModel) => ({
...oldModel,
[id]: { mode: GridRowModes.Edit, fieldToFocus: columns[0].field },
}));
}))
};
return (

View File

@ -3,34 +3,124 @@ import GeoJSON from 'ol/format/GeoJSON'
import 'ol/ol.css'
import Map from 'ol/Map'
import View from 'ol/View'
import { Draw, Modify, Snap } from 'ol/interaction'
import { OSM, Vector as VectorSource } from 'ol/source'
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { OSM, Source, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { transform, transformExtent } from 'ol/proj'
import { Divider, IconButton, Stack } from '@mui/material'
import { Adjust, Api, CircleOutlined, RectangleOutlined, Timeline, Undo, Warning } from '@mui/icons-material'
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 { 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 { 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'
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>('Point')
const [currentTool, setCurrentTool] = useState<Type | null>(null)
const map = useRef<Map | null>(null)
const source = useRef<VectorSource>(new VectorSource())
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 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 satLayer = useRef<TileLayer>(new TileLayer({
source: gMapsSatSource.current,
}))
const draw = useRef<Draw | null>(null)
const snap = useRef<Snap | null>(null)
const selectFeature = useRef<Select | null>(null)
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)',
}),
}),
}))
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 | 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 = () => {
draw.current = new Draw({
source: source.current,
type: currentTool,
})
map?.current?.addInteraction(draw.current)
snap.current = new Snap({ source: source.current })
map?.current?.addInteraction(snap.current)
if (currentTool) {
draw.current = new Draw({
source: drawingLayerSource.current,
type: currentTool,
condition: noModifierKeys
})
map?.current?.addInteraction(draw.current)
snap.current = new Snap({ source: drawingLayerSource.current })
map?.current?.addInteraction(snap.current)
}
}
// Function to save features to localStorage
@ -41,6 +131,8 @@ const MapComponent = () => {
const featuresJSON = geoJSON.writeFeatures(features)
localStorage.setItem('savedFeatures', featuresJSON)
}
console.log(drawingLayer.current?.getSource()?.getFeatures())
}
// Function to load features from localStorage
@ -51,40 +143,37 @@ const MapComponent = () => {
const features = geoJSON.readFeatures(savedFeatures, {
featureProjection: 'EPSG:4326', // Ensure the projection is correct
})
source.current?.addFeatures(features) // Add features to the vector source
drawingLayerSource.current?.addFeatures(features) // Add features to the vector source
//drawingLayer.current?.getSource()?.changed()
}
}
const handleToolSelect = (tool: Type) => {
if (currentTool == tool) {
setCurrentTool(null)
} else {
setCurrentTool(tool)
}
}
useEffect(() => {
const geoLayer = new VectorLayer({
geoLayer.current = new VectorLayer({
background: '#1a2b39',
source: new VectorSource({
url: 'https://openlayers.org/data/vector/ecoregions.json',
format: new GeoJSON(),
}),
style: {
'fill-color': ['string', ['get', 'COLOR'], '#eee'],
},
source: geoLayerSource.current,
style: geoLayerStyle,
})
const raster = new TileLayer({
baseLayer.current = new TileLayer({
source: new OSM(),
})
drawingLayer.current = new VectorLayer({
source: source.current,
style: {
'fill-color': 'rgba(255, 255, 255, 0.2)',
'stroke-color': '#ffcc33',
'stroke-width': 2,
'circle-radius': 7,
'circle-fill-color': '#ffcc33',
},
source: drawingLayerSource.current,
style: drawingLayerStyle,
})
// Center coordinates of Yakutia in EPSG:3857
const center = transform([129.7694, 66.9419], 'EPSG:4326', '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
@ -92,22 +181,64 @@ const MapComponent = () => {
const extent = transformExtent(extent4326, 'EPSG:4326', 'EPSG:3857')
map.current = new Map({
layers: [geoLayer, raster, drawingLayer.current],
layers: [baseLayer.current, satLayer.current, regionsLayer.current, drawingLayer.current],
target: mapElement.current as HTMLDivElement,
view: new View({
center,
zoom: 4,
extent,
zoom: 2,
extent: [
11388546.533293726,
7061866.113051185,
18924313.434856508,
13932243.11199202
],
}),
})
const modify = new Modify({ source: source.current })
const modify = new Modify({ source: drawingLayerSource.current })
map.current.addInteraction(modify)
addInteractions()
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)
})
}
})
loadFeatures()
map.current.on('pointermove', function (e) {
if (selectedRegion.current !== null) {
selectedRegion.current.setStyle(undefined)
selectedRegion.current = null
}
if (map.current && selectStyle !== null) {
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
})
}
})
return () => {
map?.current?.setTarget(undefined)
}
@ -118,12 +249,61 @@ const MapComponent = () => {
if (draw.current) map?.current?.removeInteraction(draw.current)
if (snap.current) map?.current?.removeInteraction(snap.current)
addInteractions()
} else {
if (draw.current) map?.current?.removeInteraction(draw.current)
if (snap.current) map?.current?.removeInteraction(snap.current)
}
}, [currentTool])
const [satelliteOpacity, setSatelliteOpacity] = useState<number>(0)
const [statusText, setStatusText] = useState('')
const selected = useRef<FeatureLike | null>(null)
// Visibility setting
useEffect(() => {
satLayer.current?.setOpacity(satelliteOpacity)
if (satelliteOpacity == 0) {
baseLayer.current?.setVisible(true)
satLayer.current?.setVisible(false)
} if (satelliteOpacity == 1) {
baseLayer.current?.setVisible(false)
satLayer.current?.setVisible(true)
} else if (satelliteOpacity > 0 && satelliteOpacity < 1) {
baseLayer.current?.setVisible(true)
satLayer.current?.setVisible(true)
}
}, [satelliteOpacity])
// Satellite tiles setting
useEffect(() => {
satLayer.current?.setSource(satMapsProvider == 'google' ? gMapsSatSource.current : satMapsProvider == 'yandex' ? yMapsSatSource.current : gMapsSatSource.current)
satLayer.current?.getSource()?.refresh()
}, [satMapsProvider])
return (
<div>
<Stack flex={1} flexDirection='column'>
<Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>
<Stack flex={1} alignItems='center' justifyContent='center'>
<Slider aria-label="Opacity" min={0} max={1} step={0.001} defaultValue={satelliteOpacity} value={satelliteOpacity} onChange={(_, value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
</Stack>
<MUISelect
variant='standard'
labelId="demo-simple-select-label"
id="demo-simple-select"
value={satMapsProvider}
label="Satellite Provider"
onChange={(e) => setSatMapsProvider(e.target.value as SatelliteMapsProvider)}
>
<MenuItem value={'google'}>Google</MenuItem>
<MenuItem value={'yandex'}>Яндекс</MenuItem>
</MUISelect>
<IconButton onClick={() => {
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
}}>
@ -145,30 +325,43 @@ const MapComponent = () => {
<IconButton
sx={{ backgroundColor: currentTool === 'Point' ? 'Highlight' : 'transparent' }}
onClick={() => setCurrentTool('Point')}>
onClick={() => handleToolSelect('Point')}>
<Adjust />
</IconButton>
<IconButton
sx={{ backgroundColor: currentTool === 'LineString' ? 'Highlight' : 'transparent' }}
onClick={() => setCurrentTool('LineString')}>
onClick={() => handleToolSelect('LineString')}>
<Timeline />
</IconButton>
<IconButton
sx={{ backgroundColor: currentTool === 'Polygon' ? 'Highlight' : 'transparent' }}
onClick={() => setCurrentTool('Polygon')}>
onClick={() => handleToolSelect('Polygon')}>
<RectangleOutlined />
</IconButton>
<IconButton
sx={{ backgroundColor: currentTool === 'Circle' ? 'Highlight' : 'transparent' }}
onClick={() => setCurrentTool('Circle')}>
onClick={() => handleToolSelect('Circle')}>
<CircleOutlined />
</IconButton>
<IconButton
onClick={() => map?.current?.addInteraction(new Translate())}
>
<OpenWith />
</IconButton>
</Stack>
<div ref={mapElement} style={{ width: '100%', height: '400px' }}></div>
</div>
<Box>
<div id="map-container" ref={mapElement} style={{ width: '100%', height: '500px', maxHeight: '100%', position: 'relative', flexGrow: 1 }}></div>
</Box>
<Stack>
{statusText}
</Stack>
</Stack>
);
};

View File

@ -1,12 +1,16 @@
import useSWR from "swr";
import useSWR, { SWRConfiguration } from "swr";
import RoleService from "../services/RoleService";
import UserService from "../services/UserService";
import { fetcher } from "../http/axiosInstance";
import { fileTypeFromBlob } from "file-type/core";
import { BASE_URL } from "../constants";
const swrOptions: SWRConfiguration = {
revalidateOnFocus: false,
}
export function useRoles() {
const { data, error, isLoading } = useSWR(`/auth/roles`, RoleService.getRoles)
const { data, error, isLoading } = useSWR(`/auth/roles`, RoleService.getRoles, swrOptions)
return {
roles: data?.data,
@ -16,7 +20,7 @@ export function useRoles() {
}
export function useUsers() {
const { data, error, isLoading } = useSWR(`/auth/user`, UserService.getUsers)
const { data, error, isLoading } = useSWR(`/auth/user`, UserService.getUsers, swrOptions)
return {
users: data?.data,
@ -26,7 +30,7 @@ export function useUsers() {
}
export function useCompanies(limit?: number, offset?: number) {
const { data, error, isLoading } = useSWR(`/info/companies?limit=${limit || 10}&offset=${offset || 0}`, fetcher)
const { data, error, isLoading } = useSWR(`/info/companies?limit=${limit || 10}&offset=${offset || 0}`, fetcher, swrOptions)
return {
companies: data,
@ -39,9 +43,7 @@ export function useFolders(limit?: number, offset?: number) {
const { data, error, isLoading } = useSWR(
`/info/document_folder?limit=${limit || 10}&offset=${offset || 0}`,
fetcher,
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -55,9 +57,7 @@ export function useDocuments(folder_id?: number) {
const { data, error, isLoading } = useSWR(
folder_id ? `/info/documents/${folder_id}` : null,
fetcher,
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -71,9 +71,7 @@ export function useDownload(folder_id?: number | null, id?: number | null) {
const { data, error, isLoading } = useSWR(
folder_id && id ? `/info/document/${folder_id}&${id}` : null,
folder_id && id ? (url) => fetcher(url, BASE_URL.info, "blob") : null,
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -87,9 +85,7 @@ export function useFileType(fileName?: string | null, file?: Blob | null) {
const { data, error, isLoading } = useSWR(
fileName && file ? `/filetype/${fileName}` : null,
file ? () => fileTypeFromBlob(file) : null,
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -103,9 +99,7 @@ export function useReport(city_id?: number | null) {
const { data, error, isLoading } = useSWR(
city_id ? `/info/reports/${city_id}?to_export=false` : null,
(url) => fetcher(url, BASE_URL.info),
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -119,9 +113,7 @@ export function useReportExport(city_id?: number | null, to_export?: boolean) {
const { data, error, isLoading } = useSWR(
city_id && to_export ? `/info/reports/${city_id}?to_export=${to_export}` : null,
(url) => fetcher(url, BASE_URL.info, 'blob'),
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -138,9 +130,7 @@ export function useAddress(limit?: number, page?: number) {
const { data, error, isLoading } = useSWR(
`/general/address?limit=${limit || 10}&page=${page || 1}`,
(url) => fetcher(url, BASE_URL.fuel),
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -154,9 +144,7 @@ export function useRegions(limit?: number, page?: number, search?: string | null
const { data, error, isLoading } = useSWR(
`/general/regions?limit=${limit || 10}&page=${page || 1}${search ? `&search=${search}` : ''}`,
(url) => fetcher(url, BASE_URL.fuel),
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -170,9 +158,7 @@ export function useCities(limit?: number, page?: number, search?: string | null)
const { data, error, isLoading } = useSWR(
`/general/cities?limit=${limit || 10}&page=${page || 1}${search ? `&search=${search}` : ''}`,
(url) => fetcher(url, BASE_URL.fuel),
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -186,9 +172,7 @@ export function useBoilers(limit?: number, page?: number, search?: string) {
const { data, error, isLoading } = useSWR(
`/general/boilers?limit=${limit || 10}&page=${page || 1}${search ? `&search=${search}` : ''}`,
(url) => fetcher(url, BASE_URL.fuel),
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -204,9 +188,7 @@ export function useServers(region_id?: number | null, offset?: number, limit?: n
const { data, error, isLoading } = useSWR(
region_id ? `/api/servers?region_id=${region_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/servers?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
}
swrOptions
)
return {
@ -220,9 +202,7 @@ export function useServersInfo(region_id?: number, offset?: number, limit?: numb
const { data, error, isLoading } = useSWR(
region_id ? `/api/servers_info?region_id=${region_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/servers_info?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
}
swrOptions
)
return {
@ -236,9 +216,7 @@ export function useServer(server_id?: number) {
const { data, error, isLoading } = useSWR(
server_id ? `/api/server/${server_id}` : null,
(url) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
}
swrOptions
)
return {
@ -252,9 +230,7 @@ export function useServerIps(server_id?: number | null, offset?: number, limit?:
const { data, error, isLoading } = useSWR(
server_id ? `/api/server_ips?server_id=${server_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/server_ips?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
}
swrOptions
)
return {
@ -270,9 +246,7 @@ export function useHardwares(server_id?: number, offset?: number, limit?: number
const { data, error, isLoading } = useSWR(
server_id ? `/api/hardwares?server_id=${server_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/hardwares?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
}
swrOptions
)
return {
@ -287,9 +261,7 @@ export function useHardware(hardware_id?: number) {
const { data, error, isLoading } = useSWR(
hardware_id ? `/api/hardware/${hardware_id}` : null,
(url) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false
}
swrOptions
)
return {
@ -305,9 +277,7 @@ export function useStorages(hardware_id?: number, offset?: number, limit?: numbe
const { data, error, isLoading } = useSWR(
hardware_id ? `/api/storages?hardware_id=${hardware_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/storages?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
}
swrOptions
)
return {
@ -321,9 +291,7 @@ export function useStorage(storage_id?: number) {
const { data, error, isLoading } = useSWR(
storage_id ? `/api/storage/${storage_id}` : null,
(url) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false
}
swrOptions
)
return {

View File

@ -0,0 +1,5 @@
export interface SatelliteMapsProviders {
google: 'google';
yandex: 'yandex';
}
export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders]

View File

@ -205,7 +205,7 @@ export default function DashboardLayout() {
>
<Toolbar />
<Container
maxWidth="lg"
maxWidth={false}
sx={{ mt: 4, mb: 4 }}
>
<Outlet />

View File

@ -18,26 +18,35 @@ const mainTheme = createTheme(
components: {
MuiListItemButton: {
defaultProps: {
disableRipple: true
//disableRipple: true
}
},
MuiButton: {
defaultProps: {
disableRipple: true
//disableRipple: true
}
},
MuiButtonBase: {
defaultProps: {
disableRipple: true,
//disableRipple: true,
}
},
MuiButtonGroup: {
defaultProps: {
disableRipple: true,
//disableRipple: true,
}
},
MuiIconButton: {
defaultProps: {
}
},
MuiIcon: {
defaultProps: {
}
}
},
}
},
ruRU
)
const darkTheme = createTheme(
@ -48,7 +57,6 @@ const darkTheme = createTheme(
primary: { main: '#1976d2' },
},
},
ruRU,
);
const lightTheme = createTheme(
@ -59,7 +67,6 @@ const lightTheme = createTheme(
primary: { main: '#1976d2' },
},
},
ruRU,
);
function ThemedApp() {

View File

@ -27,10 +27,10 @@ function Boilers() {
const boilersColumns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number" },
{ field: 'boiler_name', headerName: 'Название', type: "string" },
{ field: 'boiler_code', headerName: 'Код', type: "string" },
{ field: 'id_city', headerName: 'Город', type: "string" },
{ field: 'activity', headerName: 'Активен', type: "boolean" },
{ field: 'boiler_name', headerName: 'Название', type: "string", flex: 1 },
{ field: 'boiler_code', headerName: 'Код', type: "string", flex: 1 },
{ field: 'id_city', headerName: 'Город', type: "string", flex: 1 },
{ field: 'activity', headerName: 'Активен', type: "boolean", flex: 1 },
]
return (

View File

@ -17,9 +17,9 @@ export default function Roles() {
]
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number", width: 70 },
{ field: 'name', headerName: 'Название', width: 90, editable: true },
{ field: 'description', headerName: 'Описание', width: 90, editable: true },
{ field: 'id', headerName: 'ID', type: "number" },
{ field: 'name', headerName: 'Название', flex: 1, editable: true },
{ field: 'description', headerName: 'Описание', flex: 1, editable: true },
];
if (isError) return <div>Произошла ошибка при получении данных.</div>

View File

@ -4,6 +4,7 @@ import ServersView from "../components/ServersView"
import ServerIpsView from "../components/ServerIpsView"
import ServerHardware from "../components/ServerHardware"
import ServerStorage from "../components/ServerStorages"
import { BarChart } from '@mui/x-charts/BarChart';
export default function Servers() {
const [currentTab, setCurrentTab] = useState(0)
@ -64,12 +65,12 @@ export default function Servers() {
<ServerStorage />
</CustomTabPanel>
{/* <BarChart
<BarChart
xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]}
series={[{ data: [4, 3, 5] }, { data: [1, 6, 3] }, { data: [2, 5, 6] }]}
width={500}
height={300}
/> */}
/>
</Box>
</Box>
)

View File

@ -24,19 +24,19 @@ export default function Users() {
]
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number", width: 70 },
{ field: 'email', headerName: 'Email', width: 130, editable: true },
{ field: 'login', headerName: 'Логин', width: 130, editable: true },
{ field: 'phone', headerName: 'Телефон', width: 90, editable: true },
{ field: 'name', headerName: 'Имя', width: 90, editable: true },
{ field: 'surname', headerName: 'Фамилия', width: 90, editable: true },
{ field: 'is_active', headerName: 'Активен', type: "boolean", width: 90, editable: true },
{ field: 'id', headerName: 'ID', type: "number", flex: 1 },
{ field: 'email', headerName: 'Email', flex: 1, editable: true },
{ field: 'login', headerName: 'Логин', flex: 1, editable: true },
{ field: 'phone', headerName: 'Телефон', flex: 1, editable: true },
{ field: 'name', headerName: 'Имя', flex: 1, editable: true },
{ field: 'surname', headerName: 'Фамилия', flex: 1, editable: true },
{ field: 'is_active', headerName: 'Активен', type: "boolean", flex: 1, editable: true },
{
field: 'role_id',
headerName: 'Роль',
valueOptions: roles ? roles.map((role: IRole) => ({ label: role.name, value: role.id })) : [],
type: 'singleSelect',
width: 90,
flex: 1,
editable: true
},
];
@ -78,6 +78,7 @@ export default function Users() {
</Modal>
<DataGrid
density="compact"
autoHeight
style={{ width: "100%" }}
rows={users}

View File

@ -1622,6 +1622,11 @@
resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz"
integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==
"@types/proj4@^2.5.5":
version "2.5.5"
resolved "https://registry.npmjs.org/@types/proj4/-/proj4-2.5.5.tgz"
integrity sha512-y4tHUVVoMEOm2nxRLQ2/ET8upj/pBmoutGxFw2LZJTQWPgWXI+cbxVEUFFmIzr/bpFR83hGDOTSXX6HBeObvZA==
"@types/prop-types@*", "@types/prop-types@^15.7.11":
version "15.7.12"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz"
@ -4018,6 +4023,11 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
mgrs@1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz"
integrity sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==
micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.7"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz"
@ -4544,6 +4554,14 @@ process@^0.11.10:
resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
proj4@^2.12.0:
version "2.12.0"
resolved "https://registry.npmjs.org/proj4/-/proj4-2.12.0.tgz"
integrity sha512-cQJxcVX7+fmAhOxoazKgk76GkGYQ5HcLod4rdy2MizhPvLdrZQJThxsHoz/TjjdxUvTm/rbozMgE0q9mdXKWIw==
dependencies:
mgrs "1.0.0"
wkt-parser "^1.3.3"
prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
@ -5747,6 +5765,11 @@ widest-line@^4.0.1:
dependencies:
string-width "^5.0.1"
wkt-parser@^1.3.3:
version "1.3.3"
resolved "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz"
integrity sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw==
word-wrap@^1.2.5:
version "1.2.5"
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"

View File

@ -4,9 +4,6 @@ services:
build:
context: ./client
dockerfile: Dockerfile
volumes:
- ./client:/app
- /app/node_modules
ports:
- 5173:5173
restart: always
@ -32,16 +29,42 @@ services:
dockerfile: Dockerfile
links:
- redis_db:redis_db
- psql_db:psql_db
depends_on:
- redis_db
- psql_db
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- EMS_PORT=${EMS_PORT}
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@psql_db:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public
ports:
- ${EMS_PORT}:${EMS_PORT}
restart: always
psql_db:
container_name: psql_db
image: postgres:16.4-alpine
volumes:
- ./psql_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
ports:
- ${POSTGRES_PORT}:${POSTGRES_PORT}
expose:
- ${POSTGRES_PORT}
healthcheck:
test:
['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always
volumes:
redis_data:
psql_data:

View File

@ -17,6 +17,8 @@ ENV REDIS_PORT=$REDIS_PORT
ENV REDIS_PASSWORD=$REDIS_PASSWORD
ENV EMS_PORT=$EMS_PORT
ENV DATABASE_URL=$DATABASE_URL
EXPOSE $EMS_PORT
CMD ["npm", "run", "start"]

148
ems/package-lock.json generated
View File

@ -9,11 +9,14 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@prisma/client": "^5.18.0",
"axios": "^1.7.4",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"ioredis": "^5.4.1"
"ioredis": "^5.4.1",
"prisma": "^5.18.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
@ -66,6 +69,63 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@prisma/client": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.18.0.tgz",
"integrity": "sha512-BWivkLh+af1kqC89zCJYkHsRcyWsM8/JHpsDMM76DjP3ZdEquJhXa4IeX+HkWPnwJ5FanxEJFZZDTWiDs/Kvyw==",
"hasInstallScript": true,
"engines": {
"node": ">=16.13"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
}
},
"node_modules/@prisma/debug": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.18.0.tgz",
"integrity": "sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw=="
},
"node_modules/@prisma/engines": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.18.0.tgz",
"integrity": "sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==",
"hasInstallScript": true,
"dependencies": {
"@prisma/debug": "5.18.0",
"@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169",
"@prisma/fetch-engine": "5.18.0",
"@prisma/get-platform": "5.18.0"
}
},
"node_modules/@prisma/engines-version": {
"version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169.tgz",
"integrity": "sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg=="
},
"node_modules/@prisma/fetch-engine": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.18.0.tgz",
"integrity": "sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==",
"dependencies": {
"@prisma/debug": "5.18.0",
"@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169",
"@prisma/get-platform": "5.18.0"
}
},
"node_modules/@prisma/get-platform": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.18.0.tgz",
"integrity": "sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==",
"dependencies": {
"@prisma/debug": "5.18.0"
}
},
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
@ -316,6 +376,21 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -437,6 +512,17 @@
"node": ">=0.10.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -517,6 +603,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -677,6 +771,38 @@
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -1156,6 +1282,21 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/prisma": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.18.0.tgz",
"integrity": "sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==",
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "5.18.0"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=16.13"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -1168,6 +1309,11 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@ -13,11 +13,14 @@
"license": "ISC",
"description": "",
"dependencies": {
"@prisma/client": "^5.18.0",
"axios": "^1.7.4",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"ioredis": "^5.4.1"
"ioredis": "^5.4.1",
"prisma": "^5.18.0"
},
"devDependencies": {
"@types/express": "^4.17.21",

40
ems/prisma/schema.prisma Normal file
View File

@ -0,0 +1,40 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String @db.VarChar(255)
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
model Profile {
id Int @id @default(autoincrement())
bio String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
profile Profile?
}

View File

@ -2,6 +2,8 @@ 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')
@ -19,6 +21,61 @@ 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!')
})

View File

@ -0,0 +1,5 @@
export interface SatelliteMapsProviders {
google: 'google';
yandex: 'yandex';
}
export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders]