import { Region, LoadedResult, BoundingBox, TileParameters, GetEntitiesResponse, GetPortalResponse } from "./interfaces"; import { Portal, Field, Link } from "./types"; import { decodeEntity } from "./entityDecoder"; const baseHeaders = { 'Content-Type': 'application/json; charset=UTF-8', 'Pragma': 'no-cache', 'Accept': '*/*', 'Host': 'intel.ingress.com', 'Accept-Language': 'ru', 'Cache-Control': 'no-cache', 'Accept-Encoding': 'br, gzip, deflate', 'Origin': 'https://intel.ingress.com', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Safari/605.1.15', 'Referer': 'https://intel.ingress.com/', } //getPlexts : {"minLatE6":55740916,"minLngE6":49151362,"maxLatE6":55752993,"maxLngE6":49221056,"minTimestampMs":-1,"maxTimestampMs":-1,"tab":"all","v":"93c872fd0763076709bed329e421c37f5fa45fa4"} //getPortalDetails : {"guid":"e9ac08ad60db42abb948970defb28548.16","v":"93c872fd0763076709bed329e421c37f5fa45fa4"} type Cache = { portals: { [tile: string]: string[] }, links: { [tile: string]: string[] }, fields: { [tile: string]: string[] } } const cache: Cache = { fields: {}, links: {}, portals: {}, } const ZOOM_TO_LEVEL = [8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 4, 3, 2, 2, 1, 1] const TILES_PER_EDGE = [1, 1, 1, 40, 40, 80, 80, 320, 1000, 2000, 2000, 4000, 8000, 16000, 16000, 32000] const ZOOM_TO_LINK_LENGTH = [200000, 200000, 200000, 200000, 200000, 60000, 60000, 10000, 5000, 2500, 2500, 800, 300, 0, 0] export const getTilesToLoad = (region: Region, width: number): string[] => { const zoom = getZoomByRegion(width, region) const dataZoom = getDataZoomForMapZoom(zoom); const tileParams = getMapZoomTileParameters(dataZoom) const bbox = getBoundingBox(region) const x1 = lngToTile(bbox.west, tileParams); const x2 = lngToTile(bbox.east, tileParams); const y1 = latToTile(bbox.north, tileParams); const y2 = latToTile(bbox.south, tileParams); let tilesToLoad: string[] = [] for (var y = y1; y <= y2; y++) { for (var x = x1; x <= x2; x++) { const tile_id = pointToTileId(x, y, tileParams); tilesToLoad.push(tile_id) } } return tilesToLoad } export const loadTiles = async (tilesToLoad: string[], params: RequestParams): Promise<{ result: LoadedResult, failed: string[] }> => { const loadedData: LoadedResult = { fields: {}, links: {}, portals: {}, portalsByTile: {}, fieldsByTile: {}, linksByTile: {}, } const failed = [] await getEntities(tilesToLoad, params) .then(loaded => { const result = loaded.result if (!result || !result['map']) { return false } return Object.keys(result['map']).map(tile => { if (result['map'][tile]['error'] || !result['map'][tile]['gameEntities']) { failed.push(tile) return true } if (!loadedData.portalsByTile[tile]) { loadedData.portalsByTile[tile] = [] } if (!loadedData.fieldsByTile[tile]) { loadedData.fieldsByTile[tile] = [] } if (!loadedData.linksByTile[tile]) { loadedData.linksByTile[tile] = [] } return result['map'][tile]['gameEntities'].map((e: any[]) => { const guid = e[0] const de = decodeEntity(e) if (de instanceof Portal) { if (loadedData.portalsByTile[tile].indexOf(guid) == -1) { loadedData.portalsByTile[tile].push(guid) } loadedData.portals[guid] = de } else if (de instanceof Field) { if (loadedData.fieldsByTile[tile].indexOf(guid) == -1) { loadedData.fieldsByTile[tile].push(guid) } loadedData.fields[guid] = de } else if (de instanceof Link) { if (loadedData.linksByTile[tile].indexOf(guid) == -1) { loadedData.linksByTile[tile].push(guid) } loadedData.links[guid] = de } return true }) }) }).catch(e => { tilesToLoad.forEach(element => { failed.push(element) }); }) return { result: loadedData, failed } } const getBoundingBox = (region: Region): BoundingBox => { return { east: region.longitude + (region.longitudeDelta / 2), north: region.latitude + (region.latitudeDelta / 2), south: region.latitude - (region.latitudeDelta / 2), west: region.longitude - (region.longitudeDelta / 2), } } const lngToTile = (lng: any, tileParams: TileParameters) => { return Math.floor((lng + 180) / 360 * tileParams.tilesPerEdge); } const latToTile = (lat: any, tileParams: TileParameters) => { return Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * tileParams.tilesPerEdge); } const tileToLng = (x: number, params: TileParameters): number => { return x / params.tilesPerEdge * 360 - 180; } const tileToLat = (y: number, params: TileParameters): number => { var n = Math.PI - 2 * Math.PI * y / params.tilesPerEdge; return 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); } const pointToTileId = (x: number, y: number, params: TileParameters): string => { //change to quadkey construction //as of 2014-05-06: zoom_x_y_minlvl_maxlvl_maxhealth return params.zoom + "_" + x + "_" + y + "_" + params.level + "_8_100"; } const getZoomByRegion = (width: number, region: Region): number => { return Math.ceil(Math.log2(360 * ((width / 256) / region.longitudeDelta))) + 1 } const getDataZoomForMapZoom = (zoom: number): number => { if (zoom > 21) { zoom = 21; } var origTileParams = getMapZoomTileParameters(zoom); while (zoom > 3) { var newTileParams = getMapZoomTileParameters(zoom - 1); if (newTileParams.tilesPerEdge != origTileParams.tilesPerEdge || newTileParams.hasPortals != origTileParams.hasPortals || newTileParams.level * (newTileParams.hasPortals ? 1 : 0) != origTileParams.level * (origTileParams.hasPortals ? 1 : 0) // multiply by 'hasPortals' bool - so comparison does not matter when no portals available ) { // switching to zoom-1 would result in a different detail level - so we abort changing things break; } else { // changing to zoom = zoom-1 results in identical tile parameters - so we can safely step back // with no increase in either server load or number of requests zoom = zoom - 1; } } return zoom; } const getMapZoomTileParameters = (zoom: number): TileParameters => { var level = ZOOM_TO_LEVEL.slice(0, 15)[zoom] || 0; if (level <= 7 && level >= 4) { level = level + 1; } var maxTilesPerEdge = TILES_PER_EDGE[TILES_PER_EDGE.length - 1]; return { level: level, maxLevel: ZOOM_TO_LEVEL.slice(0, 15)[zoom] || 0, // for reference, for log purposes, etc tilesPerEdge: TILES_PER_EDGE[zoom] || maxTilesPerEdge, minLinkLength: ZOOM_TO_LINK_LENGTH[zoom] || 0, hasPortals: zoom >= ZOOM_TO_LINK_LENGTH.length, // no portals returned at all when link length limits things zoom: zoom // include the zoom level, for reference }; } type RequestParams = { csrf: string, v: string } function timeout(ms, promise) { return new Promise(function (resolve, reject) { setTimeout(function () { reject(new Error("timeout")) }, ms) return promise.then(resolve, reject) }) } const getEntities = (tileKeys: string[], params: RequestParams): Promise => { const promise = fetch("https://intel.ingress.com/r/getEntities", { "credentials": 'include', "headers": { ...baseHeaders, 'X-CSRFToken': params.csrf, }, "referrer": "https://intel.ingress.com/", "body": JSON.stringify({ tileKeys, v: params.v }), "method": "POST", }) return timeout(10000, promise.then(r => { if (r.status != 200) { return { result: { map: {} } } } return r.json() })) } export const getPortalDetails = (guid: string, params: RequestParams): Promise => { const promise = fetch("https://intel.ingress.com/r/getPortalDetails", { "credentials": 'include', "headers": { ...baseHeaders, 'X-CSRFToken': params.csrf, }, "referrer": "https://intel.ingress.com/", "body": JSON.stringify({ guid, v: params.v }), "method": "POST", }) return timeout(10000, promise.then(r => { if (r.status != 200) { return { result: [] } } return r.json() })) }