diff --git a/service.py b/service.py index 84543ef..760242c 100644 --- a/service.py +++ b/service.py @@ -126,6 +126,9 @@ class MongoDriver(MeshArgsParse): sys.exit(1) self.dbStore = self.dbClient[self.args.mongo_db] self.dbService = DbService(self.dbStore) + + from tileManager import TileManager + self.tileManager = TileManager(self) async def dbSaveRadio(self, new_from_radio): '''try: diff --git a/tileManager.py b/tileManager.py new file mode 100644 index 0000000..78f65f6 --- /dev/null +++ b/tileManager.py @@ -0,0 +1,44 @@ +import aiohttp +from pymongo.asynchronous.database import AsyncDatabase + +from logger import logger +from time import time + +class TileManager: + domain = 'a.tile.openstreetmap.org' + format = "png" + + def __init__(self, core): + self.core = core + self.dbStore:AsyncDatabase = self.core.dbStore + + def generateHeaders(self): + return { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36", + "Referer": "http://localhost:4200/", + "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" + } + + async def grabTile(self, z:int, x:int, y:int): + #grab from db + collection = self.dbStore['openstreetmap'] + query = {"x":x, "y":y, "z": z} + t = await collection.find_one(query) + if t: + return t["img"] + else: + #ищем картинку чтож поделать + url = f"https://{self.domain}/{z}/{x}/{y}.{self.format}" + async with aiohttp.ClientSession() as session: + async with session.get(url, ssl=False, headers=self.generateHeaders()) as resp: + # Read the entire response body as bytes + img = await resp.read() + logger.info(url, resp.status) + if resp.status == 200: + query['ts'] = time() + query['img'] = img + query['format'] = self.format + await collection.insert_one(query) + return img + else: + raise Exception("cannot get img") \ No newline at end of file diff --git a/ui/angular.json b/ui/angular.json index 7774b5c..9381d73 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -29,7 +29,8 @@ ], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", - "src/styles.scss" + "src/styles.scss", + "./node_modules/leaflet/dist/leaflet.css" ], "scripts": [] }, @@ -101,6 +102,7 @@ ], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", + "./node_modules/leaflet/dist/leaflet.css", "src/styles.scss" ], "scripts": [] diff --git a/ui/package-lock.json b/ui/package-lock.json index b58db08..8e80359 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -19,6 +19,7 @@ "@angular/platform-browser-dynamic": "^14.2.0", "@angular/router": "^14.2.0", "chart.js": "^4.5.1", + "leaflet": "^1.9.4", "rxjs": "~7.5.0", "tslib": "^2.3.0", "zone.js": "~0.11.4" @@ -28,6 +29,7 @@ "@angular/cli": "~14.2.13", "@angular/compiler-cli": "^14.2.0", "@types/jasmine": "~4.0.0", + "@types/leaflet": "^1.9.21", "jasmine-core": "~4.3.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.1.0", @@ -3343,6 +3345,13 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -3374,6 +3383,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -8099,6 +8118,12 @@ "node": ">= 8" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/less": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", diff --git a/ui/package.json b/ui/package.json index d26780d..296f1f9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -21,6 +21,7 @@ "@angular/platform-browser-dynamic": "^14.2.0", "@angular/router": "^14.2.0", "chart.js": "^4.5.1", + "leaflet": "^1.9.4", "rxjs": "~7.5.0", "tslib": "^2.3.0", "zone.js": "~0.11.4" @@ -30,6 +31,7 @@ "@angular/cli": "~14.2.13", "@angular/compiler-cli": "^14.2.0", "@types/jasmine": "~4.0.0", + "@types/leaflet": "^1.9.21", "jasmine-core": "~4.3.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.1.0", diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index 20dc854..84ecb41 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -4,8 +4,10 @@ import {NodesListComponent} from "./components/nodes/nodes-list.component"; import {BotCommandsComponent} from "./components/botCommands/BotCommands.component"; import {MessageHistoryComponent} from "./components/messages/MessageHistory.component"; import {NetworkStatusComponent} from "./components/packet/NetworkStatus.component"; +import {NodesMapComponent} from "./components/nodes/nodes-map.component"; const routes: Routes = [ + {path: "nodes/map", component: NodesMapComponent}, {path: "nodes/:type", component: NodesListComponent}, {path: "messages", component: MessageHistoryComponent}, {path: "network/status/:num", component: NetworkStatusComponent}, diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index a259c8b..37f630d 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -26,6 +26,7 @@ export class AppComponent implements OnInit { {name: "История сообщений", url:"messages"}, {name: "Прямые ноды", url:"nodes/direct"}, {name: "Все ноды", url:"nodes/list"}, + {name: "Карта сети", url: "nodes/map"} //{name: "Все ноды", url:""}, ] diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 04e02a0..4a38dd5 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -25,6 +25,7 @@ import {MatSelectModule} from "@angular/material/select"; import {NetworkStatusComponent} from "./components/packet/NetworkStatus.component"; import {MatRadioModule} from "@angular/material/radio"; import {DatePipe} from "@angular/common"; +import {NodesMapComponent} from "./components/nodes/nodes-map.component"; @NgModule({ declarations: [ @@ -33,7 +34,8 @@ import {DatePipe} from "@angular/common"; MessageHistoryComponent, NodesListComponent, AuthDialog, - NetworkStatusComponent + NetworkStatusComponent, + NodesMapComponent ], imports: [ BrowserModule, diff --git a/ui/src/app/components/nodes/nodes-map.component.ts b/ui/src/app/components/nodes/nodes-map.component.ts new file mode 100644 index 0000000..c4dffeb --- /dev/null +++ b/ui/src/app/components/nodes/nodes-map.component.ts @@ -0,0 +1,69 @@ +import {Component, OnInit} from "@angular/core"; +import * as L from 'leaflet'; +import {HttpClient} from "@angular/common/http"; +import {NodeDTO} from "../../entities/NodeDTO"; +import {Subscription} from "rxjs"; + +@Component({ + selector: "app-nodes-map", + styleUrls: ['nodes.styles.scss'], + template: ` +
+
+
+
+
+
+
+ ` +}) +export class NodesMapComponent implements OnInit { + map!: L.Map; + nodes: NodeDTO[] = [] + + constructor(private http: HttpClient) { + } + + ngOnInit(): void { + this.http.get(`api/nodes/list?p=true`).subscribe( + (obj) => { + this.nodes = (obj as NodeDTO[]) + } + ).add( + () => { + + this.createMap(this.convertPosition(this.nodes.filter((node) => node.havePosition).pop())).add( + () => { + this.nodes.filter((node) => node.havePosition).forEach( + (node) => { + L.marker(this.convertPosition(node)).addTo(this.map) + } + ) + } + ) + } + ) + } + + convertPosition(node:NodeDTO|undefined):[number, number] { + if (node) { + let lat = node.position.latitude_i / 10000000; + let lng = node.position.longitude_i / 10000000; + return [lat, lng]; + } else return [0,0]; + } + + createMap(center:[number, number]) { + this.map = L.map("map",{ + center: center, + zoom: 11 + }); + const tiles = L.tileLayer('api/tile/{z}/{x}/{y}.png', { + maxZoom: 18, + minZoom: 3, + attribution: '© OpenStreetMap' + }); + tiles.addTo(this.map) + return Subscription.EMPTY; + } +} diff --git a/ui/src/app/components/nodes/nodes.styles.scss b/ui/src/app/components/nodes/nodes.styles.scss index e69de29..d2db7ba 100644 --- a/ui/src/app/components/nodes/nodes.styles.scss +++ b/ui/src/app/components/nodes/nodes.styles.scss @@ -0,0 +1,21 @@ +.map-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 30px; +} + +.map-frame { + border: 2px solid black; + height: 100%; +} + +#map { + height: 100%; +} + +:host ::ng-deep .leaflet-control-attribution { + display: none; +} diff --git a/ui/src/app/entities/NodeDTO.ts b/ui/src/app/entities/NodeDTO.ts index 406a78e..5de8f2a 100644 --- a/ui/src/app/entities/NodeDTO.ts +++ b/ui/src/app/entities/NodeDTO.ts @@ -1,7 +1,13 @@ import {NodeMiniDTO} from "./NodeMiniDTO"; +import {PositionDTO} from "./node/PositionDTO"; +import {DeviceMetricsDTO} from "./node/DeviceMetricsDTO"; export interface NodeDTO extends NodeMiniDTO { snr: number, hops_away: number, ts: number + havePosition:boolean, + position: PositionDTO, + haveMetrics:boolean, + device_metrics: DeviceMetricsDTO } diff --git a/ui/src/app/entities/node/DeviceMetricsDTO.ts b/ui/src/app/entities/node/DeviceMetricsDTO.ts new file mode 100644 index 0000000..faf18d5 --- /dev/null +++ b/ui/src/app/entities/node/DeviceMetricsDTO.ts @@ -0,0 +1,7 @@ +export interface DeviceMetricsDTO { + battery_level: number, + voltage: number, + channel_utilization: number, + air_util_tx: number, + uptime_seconds: number +} diff --git a/ui/src/app/entities/node/PositionDTO.ts b/ui/src/app/entities/node/PositionDTO.ts new file mode 100644 index 0000000..6901a47 --- /dev/null +++ b/ui/src/app/entities/node/PositionDTO.ts @@ -0,0 +1,6 @@ +export interface PositionDTO { + latitude_i: number, + longitude_i: number, + time: number, + location_source: number +} diff --git a/webExtensions/publicEndpoints.py b/webExtensions/publicEndpoints.py index 2b69b0c..1dd8a8b 100644 --- a/webExtensions/publicEndpoints.py +++ b/webExtensions/publicEndpoints.py @@ -1,8 +1,9 @@ from fastapi import FastAPI from fastapi.requests import Request -from fastapi.responses import Response, JSONResponse +from fastapi.responses import Response, JSONResponse, StreamingResponse from fastapi.exceptions import HTTPException from extra.NodeDTO import NodeDTO +import traceback class WebExtension: app: FastAPI @@ -59,4 +60,16 @@ class WebExtension: if userNum is not None: return NodeDTO(userNode) else: - return {} \ No newline at end of file + return {} + + @self.app.get(self.core.context+"/tile/{z}/{x}/{y}.png") + @self.core.authManager.authRequest() + async def grabTile(z:int, x:int, y:int): + try: + img = await self.core.tileManager.grabTile(z, x, y) + print(type(img)) + #return StreamingResponse(img, media_type="image/png") + return Response(content=img, media_type="image/png") + except: + traceback.print_exc() + raise HTTPException(status_code=404, detail="not found tile") \ No newline at end of file