diff options
Diffstat (limited to 'src/Api/api.ts')
-rw-r--r-- | src/Api/api.ts | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/src/Api/api.ts b/src/Api/api.ts new file mode 100644 index 0000000..2da4594 --- /dev/null +++ b/src/Api/api.ts @@ -0,0 +1,251 @@ +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<LoadedResult> => { + const chunk = tilesToLoad.splice(0, 25) + const loadedData: LoadedResult = { + fields: {}, + links: {}, + portals: {}, + portalsByTile: {}, + fieldsByTile: {}, + linksByTile: {}, + failedTiles: tilesToLoad, + loaded: false, + } + await getEntities(chunk, 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']) { + loadedData.failedTiles.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 => { + loadedData.failedTiles = [...loadedData.failedTiles, ...chunk] + }) + return loadedData +} + +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<GetEntitiesResponse> => { + 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<GetPortalResponse> => { + 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() + })) +} |