import { FilterParams } from "pip-services3-commons-nodex"; import { FireMapTileBlockV1 } from "./FireMapTileBlockV1"; import { FireMapTileStatusV1 } from "./FireMapTileStatusV1"; import { FireMapTileV1 } from "./FireMapTileV1"; import { FireMapUpdateTypeV1 } from "./FireMapUpdateTypeV1"; import { FireMapUpdateV1 } from "./FireMapUpdateV1"; import { FireMapZoomV1 } from "./FireMapZoomV1"; import { IFireMapClientV1 } from "./IFireMapClientV1"; import { IFireMapListenerV1 } from "./IFireMapListenerV1"; import { LocationV1 } from "./LocationV1"; export class FireMapMockClientV1 implements IFireMapClientV1 { private _listeners: IFireMapListenerV1[] = []; private _fireMapTilesBlocks: FireMapTileBlockV1[] = []; private _appendLoc = '00AA00AA'; private composeFilter(filter: FilterParams): any { filter = filter || new FilterParams(); let flng = filter.getAsNullableFloat('flng'); let flat = filter.getAsNullableFloat('flat'); let tlng = filter.getAsNullableFloat('tlng'); let tlat = filter.getAsNullableFloat('tlat'); let flng2: number = null; let tlng2: number = null; // round to the next tile for current zoom if ([flng, flat, tlng, tlat].every((el) => el != null)) { // if coordinates is left top and right bottom corners if (flat > tlat) { [flat, tlat] = [tlat, flat]; } // if coordinates pass through the last meridian if (flng > tlng) { flng2 = -180; tlng2 = tlng; tlng = 180; } } return (item: FireMapTileV1) => { // if item inside flng, flat, tlng, tlat coordinates let notInRange = (x1, x2, from, to) => (from != null && to != null && !(x1 <= to && x1 >= from || x2 <= to && x2 >= from)); if (notInRange(item.flng, item.tlng, flng, tlng) && ((flng2 == null && tlng2 == null) || notInRange(item.flng, item.tlng, flng2, tlng2))) return false; if (notInRange(item.flat, item.tlat, flat, tlat)) return false; return true; }; } public async addListener(listener: IFireMapListenerV1): Promise { this._listeners.push(listener); } public async removeListener(listener: IFireMapListenerV1): Promise { this._listeners = this._listeners.filter((l) => l != listener); } public async getTiles(correlationId: string, from: LocationV1, to: LocationV1, zoom: number): Promise { if (zoom > FireMapZoomV1.Zoom100km) { return null; } 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, ); let tilesBlocks = zoom != null ? this._fireMapTilesBlocks.filter((s) => s.zoom == zoom) : this._fireMapTilesBlocks; let tiles = this.tilesBlocksToTiles(tilesBlocks); let filterFunc = this.composeFilter(filter); tiles = tiles.filter((s) => filterFunc(s)); return tiles; } public async updateTiles(correlationId: string, updates: FireMapUpdateV1[]): Promise { for (let update of updates) { update = Object.assign({}, update); // compose locator from coordinates let locator = this.getLocator(update.lat, update.long).toUpperCase(); let updateBlocks: FireMapTileBlockV1[] = []; // find blocks for update let absentZooms = Object.values(FireMapZoomV1); for (let block of this._fireMapTilesBlocks) { // check if update coordinates in block coordinates if (update.long >= block.flng && update.long <= block.tlng && update.lat <= block.flat && update.lat >= block.tlat) { updateBlocks.push(block); // find absent zooms absentZooms = absentZooms.filter((z) => z != block.zoom); } } // if block is not created, or some absent if (updateBlocks.length < Object.keys(FireMapZoomV1).length) { updateBlocks = this.createBlocks(update.lat, update.long, absentZooms); this._fireMapTilesBlocks = this._fireMapTilesBlocks.concat(updateBlocks); } // update blocks for all zooms for (let block of updateBlocks) { // update current based on states of the children if (update.type != null && !this.canUpdateBlock(block, updateBlocks, update.type)) { continue; } // compose id by zoom let indexOfappend = 10 - (block.zoom * 2); let id = locator.slice(0, indexOfappend) + this._appendLoc.slice(indexOfappend - 2); switch (update.type) { case FireMapUpdateTypeV1.Clear: block.clear[id].updated = update.time; block.clear[id].drone_id = update.drone_id; block.clear[id].people = update.people; break; case FireMapUpdateTypeV1.Smoke: block.smoke[id].updated = update.time; block.smoke[id].drone_id = update.drone_id; block.smoke[id].people = update.people; break; case FireMapUpdateTypeV1.Fire: block.fire[id].updated = update.time; block.fire[id].drone_id = update.drone_id; block.fire[id].people = update.people; break; } // update peoples count for all statuses block.clear[id].peopleUpdated = update.time; block.clear[id].people = update.people; block.smoke[id].peopleUpdated = update.time; block.smoke[id].people = update.people; block.fire[id].peopleUpdated = update.time; block.fire[id].people = update.people; // block to tiles let tiles: FireMapTileV1[] = this.tilesBlocksToTiles([block]); // Send update notifications for (let listener of this._listeners) { listener.mapUpdated(correlationId, block.zoom, tiles); } } } } /** * Check if we can set state for current block * Update strategy: * 1) set clear if all child clear too * 2) set smoke if not child with fire state * 3) set fire always * * @param childBlocks child fire 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.clear).length; for (let id of Object.keys(childBlock.clear)) { switch (type) { case FireMapUpdateTypeV1.Clear: if (childBlock.clear[id].updated >= childBlock.smoke[id].updated && childBlock.clear[id].updated >= childBlock.fire[id].updated) checked++; break; case FireMapUpdateTypeV1.Smoke: if (childBlock.smoke[id].updated >= childBlock.fire[id].updated) checked++; break; case FireMapUpdateTypeV1.Fire: return true; } } } // if checked all blocks return countOfTiles == checked; } private getLocator(latPlace: number, longPlace: number): string { const alphabet: string[] = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); let word = ''; // first lvl AA-RR let lon = longPlace + 180; let lat = latPlace + 90; let lon1 = lon / 20; let lat1 = lat / 10; word += alphabet[Math.trunc(lon1)] + alphabet[Math.trunc(lat1)]; // second lvl 00-99 let lonR = lon - (Math.trunc(lon1) * 20); let latR = lat - (Math.trunc(lat1) * 10); let lon2 = lonR / 2; let lat2 = latR / 1; word += Math.trunc(lon2).toString() + Math.trunc(lat2).toString(); // third lvl aa-xx lonR = lonR - (Math.trunc(lon2) * 2); latR = latR - (Math.trunc(lat2) * 1); let lon3 = lonR * 12; let lat3 = latR * 24; word += (alphabet[Math.trunc(lon3)] + alphabet[Math.trunc(lat3)]); // fourth lvl 00-99 lonR = lonR - (Math.trunc(lon3) / 12); latR = latR - (Math.trunc(lat3) / 24); let lon4 = lonR * 120 let lat4 = latR * 240 word += Math.trunc(lon4).toString() + Math.trunc(lat4).toString(); // fifth lvl AA-XX lonR = lonR - (Math.trunc(lon4) / 120); latR = latR - (Math.trunc(lat4) / 240); let lon5 = lonR * 2880; let lat5 = latR * 5760; word += alphabet[Math.trunc(lon5)] + alphabet[Math.trunc(lat5)]; // correct special chars word = word.toUpperCase(); let resultLoc: string = ''; let index = 0; for (let char of word) { // if is digit if (char.charCodeAt(0) >= '0'.charCodeAt(0) - 1 && char.charCodeAt(0) <= '9'.charCodeAt(0) + 1) { // check number chars of loc if (char.charCodeAt(0) - '0'.charCodeAt(0) >= 10) { char = '9'; } else if ('9'.charCodeAt(0) - char.charCodeAt(0) >= 10) { char = '0'; } } else { // if is char if (char.charCodeAt(0) >= 'A'.charCodeAt(0) - 1 && char.charCodeAt(0) <= 'X'.charCodeAt(0) + 1) { if (index == 0 || index == 1) { // check for first chars pair in loc if (char.charCodeAt(0) - 'A'.charCodeAt(0) >= 18) { char = 'R'; } else if ('R'.charCodeAt(0) - char.charCodeAt(0) >= 18) { char = 'A'; } } else { if (char.charCodeAt(0) - 'A'.charCodeAt(0) >= 24) { char = 'X'; } else if ('X'.charCodeAt(0) - char.charCodeAt(0) >= 24) { char = 'A'; } } } } resultLoc += char; index++; } return resultLoc; } private locatorToPolar(loc: string): LocationV1 { let o = 0; let e = new Array(); for (loc = loc.toUpperCase(); o < 10;) e[o] = loc.charCodeAt(o++) - 65; e[2] += 17; e[3] += 17; e[6] += 17; e[7] += 17; let long = 20 * e[0] + 2 * e[2] + e[4] / 12 + e[6] / 120 + e[8] / 2880 - 180 let lat = 10 * e[1] + e[3] + e[5] / 24 + e[7] / 240 + e[9] / 5760 - 90; return { lat: lat, long: long }; } /** * Calculates the positions of the extreme corners of the locator * * @param loc QTH locator * @param locatorSym symbol position inside locator (locator zoom) * @param invertAngles flag for returns left bottom and right top angles * @returns location array [from, to] (left top and right bottom) */ private getLocatorCoordinates(loc: string, locatorSym: number, invertAngles: boolean = false): LocationV1[] { // if it smallest zoom if (locatorSym == 0) { return [{ lat: 90, long: -180 }, { lat: -90, long: 180 }]; } let appendLoc = this._appendLoc.substring(locatorSym - 2); loc = loc.toUpperCase(); // calculate coordinates of corners let leftBottom = this.locatorToPolar( loc.slice(0, locatorSym - 2) + loc.slice(locatorSym - 2, locatorSym - 1) + loc.slice(locatorSym - 1, locatorSym) + appendLoc ); let rightBottom = this.locatorToPolar( loc.slice(0, locatorSym - 2) + String.fromCharCode(1 + loc[locatorSym - 2].charCodeAt(0)) + loc.slice(locatorSym - 1, locatorSym) + appendLoc ); let rightTop = this.locatorToPolar( loc.slice(0, locatorSym - 2) + String.fromCharCode(1 + loc[locatorSym - 2].charCodeAt(0)) + String.fromCharCode(1 + loc[locatorSym - 1].charCodeAt(0)) + appendLoc ) let leftTop = this.locatorToPolar( loc.slice(0, locatorSym - 2) + loc.slice(locatorSym - 2, locatorSym - 1) + String.fromCharCode(1 + loc[locatorSym - 1].charCodeAt(0)) + appendLoc ); if (invertAngles) { return [leftBottom, rightTop]; } return [leftTop, rightBottom]; } private createBlocks(lat: number, lng: number, zooms: number[] = null): FireMapTileBlockV1[] { let blocks: FireMapTileBlockV1[] = []; const locatorSym = [8, 6, 4, 2, 0]; // mapping zoom to locator symbol zooms = zooms || Object.values(FireMapZoomV1); let locator = this.getLocator(lat, lng); for (let zoom of zooms) { let blockCoord = this.getLocatorCoordinates(locator, locatorSym[zoom]); let block = new FireMapTileBlockV1(); block.zoom = zoom; block.flat = blockCoord[0].lat; block.flng = blockCoord[0].long; block.tlat = blockCoord[1].lat; block.tlng = blockCoord[1].long; block.clear = {}; block.smoke = {}; block.fire = {}; // calc id as block locator let center = this.calcCenterCoordinates(block.flat, block.flng, block.tlat, block.tlng); let locOfBlock = this.getLocator(center.lat, center.long).slice(0, locatorSym[zoom]); let id = (locOfBlock + this._appendLoc.slice(locatorSym[zoom] - 2) + '_' + zoom).toUpperCase(); // if it biggest block if (locOfBlock == '') { id = 'AA' + this._appendLoc + '_' + zoom; } block.id = id let iterations: number; // detect count of tiles for current zoom switch (zoom) { case FireMapZoomV1.Zoom50m: iterations = 24; // 576 tiles for 'AA-XX' break; case FireMapZoomV1.Zoom1km: iterations = 10; // 100 tiles for '00-99' break; case FireMapZoomV1.Zoom20km: iterations = 24; // 576 tiles for 'AA-XX' break; 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 = locator.slice(0, locatorSym[zoom]); switch (locatorSym[zoom]) { 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 = this.getLocatorCoordinates(tileLocator, locatorSym[zoom] + 2); let tileStatus: FireMapTileStatusV1 = { id: null, flat: tileCoord[0].lat, flng: tileCoord[0].long, tlat: tileCoord[1].lat, tlng: tileCoord[1].long, updated: null, drone_id: null, people: null, peopleUpdated: null } // calc tile id as locator id = (tileLocator + this._appendLoc.slice(locatorSym[zoom])).toUpperCase(); tileStatus.id = id; block.clear[id] = Object.assign({}, tileStatus); block.smoke[id] = Object.assign({}, tileStatus); block.fire[id] = Object.assign({}, tileStatus); } } blocks.push(block); } return blocks; } private calcCenterCoordinates(flat: number, flng: number, tlat: number, tlng: number): LocationV1 { let toRad = (c) => c * Math.PI / 180; let toDeegre = (c) => c * 180 / Math.PI; flat = toRad(flat); flng = toRad(flng); tlat = toRad(tlat); tlng = toRad(tlng); let deltaLong = tlng - flng; let xModified = Math.cos(tlat) * Math.cos(deltaLong); let yModified = Math.cos(tlat) * Math.sin(deltaLong); let centerLat = Math.atan2(Math.sin(flat) + Math.sin(tlat), Math.sqrt((Math.cos(flat) + xModified) * (Math.cos(flat) + xModified) + yModified * yModified)); let centerLong = flng + Math.atan2(yModified, Math.cos(flat) + xModified); centerLat = toDeegre(centerLat); centerLong = toDeegre(centerLong); return { lat: centerLat, long: centerLong }; } private tilesBlocksToTiles(tilesBlocks: FireMapTileBlockV1[]): FireMapTileV1[] { let tiles: { [id: string]: FireMapTileV1 } = {}; for (let block of tilesBlocks) { for (let id in block.clear) { if (tiles[id] == undefined) { tiles[id] = { id: id, zoom: block.zoom, flng: block.clear[id].flng, flat: block.clear[id].flat, tlng: block.clear[id].tlng, tlat: block.clear[id].tlat, clear: block.clear[id].updated, cdrone: block.clear[id].drone_id, smoke: block.smoke[id].updated, sdrone: block.smoke[id].drone_id, fire: block.fire[id].updated, fdrone: block.fire[id].drone_id, people: block.fire[id].peopleUpdated, pdrone: block.fire[id].drone_id, people_count: block.fire[id].people } } else { tiles[id].clear = block.clear[id].updated; tiles[id].cdrone = block.clear[id].drone_id; tiles[id].smoke = block.smoke[id].updated; tiles[id].sdrone = block.smoke[id].drone_id; tiles[id].fire = block.fire[id].updated; tiles[id].fdrone = block.fire[id].drone_id; tiles[id].people = block.fire[id].peopleUpdated; tiles[id].pdrone = block.fire[id].drone_id; tiles[id].people_count = block.fire[id].people; } } } // obj to list let listTiles = Object.keys(tiles).map((key) => tiles[key]); return listTiles; } }