import { ConfigParams, CommandSet, ICommandable, IConfigurable, IReferenceable, IReferences, Descriptor, FilterParams, PagingParams } from "pip-services3-commons-nodex"; import { IMessageQueue, MessageEnvelope } from "pip-services3-messaging-nodex"; import { LocationV1, FireMapTileV1, FireMapUpdateV1, FireMapTileBlockV1, FireMapZoomV1, FireMapUpdateTypeV1 } from "../data/version1"; import { FireMapUtil } from "../map_utils/FireMapUtil"; import { IFireMapPersistence } from "../persistence/IFireMapPersistence"; import { FireMapCommandSet } from "./FireMapCommandSet"; import { IFireMapController } from "./IFireMapController"; export class FireMapController implements IFireMapController, IConfigurable, IReferenceable, ICommandable { private _persistence: IFireMapPersistence; private _commandSet: FireMapCommandSet; private _messageQueue: IMessageQueue; public configure(config: ConfigParams): void { } public setReferences(references: IReferences): void { this._persistence = references.getOneRequired( new Descriptor('eic-stopfires-services-firemap', 'persistence', '*', '*', '1.0') ); this._messageQueue = references.getOneOptional( new Descriptor('pip-services', 'message-queue', 'kafka', '*', '1.0') ) } public getCommandSet(): CommandSet { this._commandSet ??= new FireMapCommandSet(this); return this._commandSet; } public async getTiles(correlationId: string, from: LocationV1, to: LocationV1, zoom: number): Promise { let filter = FilterParams.fromTuples( 'flng', from != null ? from.long: null , 'flat', from != null ? from.lat : null , 'tlng', to != null ? to.long: null, 'tlat', to != null ? to.lat: null, 'zoom', zoom ); let page = await this._persistence.getTileBlocksByFilter(correlationId, filter, new PagingParams()); let tiles: FireMapTileV1[] = []; for (let tileBlock of page.data) { tiles.push(...Object.values(tileBlock.tiles)); } return tiles; } public async updateTiles(correlationId: string, updates: FireMapUpdateV1[]): Promise { let coordinates: string[] = []; // let resultBlocks: { [id: string]: FireMapTileBlockV1 } = {} for (let update of updates) coordinates.push(`${update.lat};${update.long}`); // get tile blocks for update let updateBlocks = (await this._persistence.getTileBlocksByFilter(null, FilterParams.fromTuples('coordinates', coordinates), null )).data; let absentZooms: number[] = Object.values(FireMapZoomV1); for (let update of updates) { let currentUpdateBlocks: FireMapTileBlockV1[] = []; for (let block of updateBlocks) { // check if update coordinates in block coordinates if (block.flng <= update.long && update.long <= block.tlng && block.tlat <= update.lat && update.lat <= block.flat) { // find absent zooms currentUpdateBlocks.push(block); absentZooms = absentZooms.filter(z => z != block.zoom); } } // if block is not created, or some absent if (absentZooms.length > 0) { let newBlocks = this.createBlocks(update.lat, update.long, absentZooms); currentUpdateBlocks = currentUpdateBlocks.concat(newBlocks); } // sort by zoom for updating from small to big currentUpdateBlocks = currentUpdateBlocks.sort((a, b) => a.zoom - b.zoom); // update blocks for all zooms for (let block of currentUpdateBlocks) { // compose id by zoom let loc: string = FireMapUtil.getLocator({ lat: update.lat, long: update.long }); let id: string = FireMapUtil.composeTileLocByZoom(loc, block.zoom); // update current based on states of the children if (update.type == null || this.canUpdateBlock(block, currentUpdateBlocks, update.type)) { switch (update.type) { case FireMapUpdateTypeV1.Clear: block.tiles[id].clear = update.time; block.tiles[id].cdrone = update.drone_id; break; case FireMapUpdateTypeV1.Smoke: block.tiles[id].smoke = update.time; block.tiles[id].sdrone = update.drone_id; break; case FireMapUpdateTypeV1.Fire: block.tiles[id].fire = update.time; block.tiles[id].fdrone = update.drone_id; break; } block.tiles[id].pdrone = update.drone_id; } // calc peoples on child blocks let peopleCount: number = update.people; if (block.zoom != FireMapZoomV1.Zoom50m) { peopleCount = 0; let childBlock: FireMapTileBlockV1 = currentUpdateBlocks.find(b => b.zoom + 1 == block.zoom); if (childBlock != null) { for (let tile of Object.values(childBlock.tiles)) peopleCount += tile.people_count != null ? tile.people_count : 0; } } // update peoples count for all statuses block.tiles[id].people = update.time; block.tiles[id].people_count = peopleCount; if (this._messageQueue != null) { let message = new MessageEnvelope(correlationId, 'FireMapTileV1', JSON.stringify(block.tiles[id])); await this._messageQueue.send(correlationId, message); } } updateBlocks = Object.values(currentUpdateBlocks); } await this._persistence.updateTileBlocks(correlationId, updateBlocks); } private createBlocks(lat: number, long: number, zooms: number[]): FireMapTileBlockV1[] { let blocks: FireMapTileBlockV1[] = []; zooms = zooms ?? Object.values(FireMapZoomV1); let locator: string = FireMapUtil.getLocator({ lat: lat, long: long }); for (let tileZoom of zooms) { let blockZoom = tileZoom + 1; let blockCoord = FireMapUtil.getLocatorCoordinates(locator, blockZoom); // calc id as block locator let center: LocationV1 = FireMapUtil.calcCenterCoordinates( { lat: blockCoord[0].lat, long: blockCoord[0].long }, { lat: blockCoord[1].lat, long: blockCoord[1].long } ); let locOfCenter: string = FireMapUtil.getLocator({ lat: center.lat, long: center.long }); let locOfBlock: string = FireMapUtil.composeTileLocByZoom(locOfCenter, blockZoom) + '_' + (blockZoom - 1).toString(); locOfBlock = locOfBlock.toUpperCase(); let block: FireMapTileBlockV1 = { id: locOfBlock, zoom: tileZoom, flat: blockCoord[0].lat, flng: blockCoord[0].long, tlat: blockCoord[1].lat, tlng: blockCoord[1].long, tiles: {} }; // if it biggest block let iterations = 0; switch (tileZoom) { case FireMapZoomV1.Zoom50m: case FireMapZoomV1.Zoom20km: iterations = 24; // 576 tiles for 'AA-XX' break; case FireMapZoomV1.Zoom1km: case FireMapZoomV1.Zoom50km: iterations = 10; // 100 tiles for '00-99' break; case FireMapZoomV1.Zoom100km: iterations = 18; // 324 tiles for 'AA-XX' break; } // fill all current block tiles for (let i = 0; i < iterations; i++) { for (let j = 0; j < iterations; j++) { let tileLocator = FireMapUtil.getLocByZoom(locator, blockZoom); switch (tileLocator.length) { case 2: case 6: tileLocator += i.toString() + j.toString(); break; case 0: case 4: case 8: tileLocator += String.fromCharCode(i + 65) + String.fromCharCode(j + 65); break; } let tileCoord: LocationV1[] = FireMapUtil.getLocatorCoordinates(tileLocator, tileZoom); // calc tile id as locator let id: string = FireMapUtil.composeTileLocByZoom(tileLocator, tileZoom); let tile: FireMapTileV1 = { id: id, flat: tileCoord[0].lat, flng: tileCoord[0].long, tlat: tileCoord[1].lat, tlng: tileCoord[1].long, zoom: block.zoom, }; block.tiles[id] = tile; } } blocks.push(block); } return blocks; } /** * Check if we can set state for current block * Update strategy: * 1) set clear if all child clear too * 2) set cond one if not child with cond two state * 3) set cond two always * * @param childBlocks child area map blocks * @param type type of update * @returns can update */ private canUpdateBlock(currBlock: FireMapTileBlockV1, blocks: FireMapTileBlockV1[], type: FireMapUpdateTypeV1): boolean { let childBlocks = blocks.filter(b => b.zoom < currBlock.zoom); let checked: number = 0; let countOfTiles: number = 0; for (let childBlock of childBlocks) { countOfTiles += Object.keys(childBlock.tiles).length; for (let id of Object.keys(childBlock.tiles)) { switch (type) { case FireMapUpdateTypeV1.Clear: if (childBlock.tiles[id].clear >= childBlock.tiles[id].smoke && childBlock.tiles[id].clear >= childBlock.tiles[id].fire) checked++; break; case FireMapUpdateTypeV1.Smoke: if (childBlock.tiles[id].smoke >= childBlock.tiles[id].fire) checked++; break; case FireMapUpdateTypeV1.Fire: return true; } } } // if checked all blocks return countOfTiles == checked; } }