UNPKG

imtiler

Version:
304 lines (265 loc) 9.77 kB
const { writeFileSync } = require("fs"); const path = require("path"); const geotiff_from = require("geotiff-from"); const geowarp = require("geowarp"); const tilebelt = require("@mapbox/tilebelt"); const { GeoExtent } = require("geo-extent"); const reprojectBoundingBox = require("reproject-bbox"); const LRU = require("lru-cache"); const writeImage = require("write-image"); const readBoundingBox = require("geotiff-read-bbox"); const parseAnyInt = require("parse-any-int"); const proj4 = require("proj4-fully-loaded"); const lds = require("lambda-dev-server"); const caches = { geotiff: new LRU(100), tile: new LRU(100) }; async function handler(event, context) { try { const request_id = (1e5 + Math.random() * 1e10).toString().split(".")[0].slice(0, 3); const debugLevel = Number(process.env.IM_TILER_DEBUG_LEVEL || 0); if (debugLevel) console.log("debugLevel:", debugLevel); let body; let isBase64Encoded = false; let headers = { "Access-Control-Allow-Origin": "*", "Content-Type": "application/json" }; const { queryStringParameters } = event; if (debugLevel >= 1) console.log("queryStringParameters:", queryStringParameters); const params = {}; Object.entries(queryStringParameters).forEach(([k, v]) => { if (k.trim() !== "" && typeof v === "string" && v.trim() !== "") { params[k.trim()] = v.trim(); } }); if (debugLevel) console.log("params:", params); const queryKeys = Object.keys(params).map(k => k.trim()); if (debugLevel) console.log("queryKeys:", queryKeys); if (queryKeys.length === 0) { throw new Error("no url parameters were found"); } ["url", "x", "y", "z"].forEach(k => { if (!(k in params)) { throw new Error(`missing "${k}"`); } }); if (debugLevel >= 1) console.log("[imtiler] params before running parse-any-int:", params); ["x", "y", "z"].forEach(k => { params[k] = parseAnyInt(params[k], { debug: true }); }); params.r = Number(params.r || 1); if (debugLevel >= 1) console.log("[imtiler] cleaned params:", params); let { url, method, f, r, x, y, z } = params; if (["", undefined, null].includes(f)) f = "png"; const size = Math.round(r * 256); if (debugLevel >= 1) console.log(`[imtiler] size: ${size}`); const keyToTileCache = JSON.stringify([url, method, f, r, x, y, z]); if (debugLevel >= 1) console.log(`[imtiler] caches.tile.keys():`, caches.tile.keys()); if (caches.tile.has(keyToTileCache)) { if (debugLevel >= 1) console.log(`[imtiler] using tile cache`); if ([undefined, "image/png", "png"].includes(f)) { headers["Content-Type"] = "image/png"; } else { if (["image/jpeg", "image/jpg", "jpeg", "jpg"].includes(f)) { headers["Content-Type"] = "image/jpeg"; } } return { statusCode: 200, isBase64Encoded: true, headers, body: caches.tile.get(keyToTileCache) }; } if (caches.geotiff.has(url)) { if (debugLevel >= 1) console.log("[imtiler] using cache"); geotiff = await caches.geotiff.get(url)(); } else { const promise = geotiff_from({ data: url, ovr: true }); caches.geotiff.set(url, () => promise); if (debugLevel >= 1) console.log("[imtiler] added to cache"); geotiff = await promise; } const bbox4326 = tilebelt.tileToBBOX([x, y, z]); if (debugLevel >= 1) console.log("[imtiler] bbox in 4326: " + JSON.stringify(bbox4326)); const bbox3857 = reprojectBoundingBox({ bbox: bbox4326, from: 4326, to: 3857 }); if (debugLevel >= 1) console.log("[imtiler] bbox in 3857: " + JSON.stringify(bbox3857)); const image = await geotiff.getImage(); if (debugLevel >= 1) console.log("[imtiler] got image"); const { geoKeys } = image; if (debugLevel >= 2) console.log("[imtiler] geoKeys:", JSON.stringify(geoKeys)); if (!geoKeys) { throw new Error("[imtiler] can't create tile because geotiff doesn't appear to have geoKeys"); } const geotiff_srs = geoKeys.GeographicTypeGeoKey || geoKeys.ProjectedCSTypeGeoKey; if (debugLevel >= 2) console.log("[imtiler] geotiff_srs:", geotiff_srs); if (!geotiff_srs) { throw new Error("[imtiler] geotiff has geokeys, but is missing both GeographicTypeGeoKey and ProjectedCSTypeGeoKey"); } const extentOfGeoTIFF = new GeoExtent(image.getBoundingBox(), { srs: geotiff_srs }); const extentOfTile = new GeoExtent(bbox4326, { srs: 4326 }); if (!extentOfGeoTIFF.overlaps(extentOfTile)) { if (debugLevel >= 1) console.log("[imtiler] tile and geotiff do not overlap, so return a blank image"); const out_data = new Uint8ClampedArray(4 * size * size); if (f === "png") { const { data: buffer } = writeImage({ data: out_data, debug: false, format: "PNG", height: size, width: size }); body = buffer.toString("base64"); isBase64Encoded = true; headers["Content-Type"] = "image/png"; } else if (f === "jpg") { const { data: buffer } = writeImage({ data: out_data, debug: false, format: "JPG", height: size, width: size, quality: 85 }); body = buffer.toString("base64"); isBase64Encoded = true; headers["Content-Type"] = "image/jpeg"; } else { throw Error(`format "${f}" not supported`); } } else { console.time(`${request_id}: reading bounding box`); const info = await readBoundingBox({ bbox: bbox3857, debugLevel: 2, srs: 3857, geotiff, use_overview: true, target_height: size, target_width: size }); console.timeEnd(`${request_id}: reading bounding box`); console.time(`${request_id}: warping`); const warped = geowarp({ debug_level: 0, reproject: proj4("EPSG:3857", "EPSG:" + info.srs_of_geotiff).forward, in_data: info.data, in_bbox: info.read_bbox, in_srs: info.srs_of_geotiff, in_width: info.width, in_height: info.height, out_bbox: bbox3857, out_srs: 3857, out_height: size, out_width: size, method, round: true }); console.timeEnd(`${request_id}: warping`); if (f === undefined || f === "png") { console.time(`${request_id}: writing image`); const { data: buffer } = writeImage({ data: warped.data, height: size, width: size, format: "PNG" }); body = buffer.toString("base64"); isBase64Encoded = true; headers["Content-Type"] = "image/png"; } else if (f === "jpg") { const { data: buffer } = writeImage({ data: warped.data, height: size, width: size, format: "JPG" }); body = buffer.toString("base64"); isBase64Encoded = true; headers["Content-Type"] = "image/jpeg"; } console.timeEnd(`${request_id}: writing image`); } console.log("setting ", { keyToTileCache }); caches.tile.set(keyToTileCache, body); if (debugLevel >= 1) console.log(`[imtiler] after setting caches.tile.keys():`, caches.tile.keys()); const result = { statusCode: 200, isBase64Encoded, headers, body }; return result; } catch (error) { console.log(error); return { statusCode: 500, body: process.env.IM_TILER_DEV_MODE === "true" ? JSON.stringify({ msg: error.message }) : "internal error" }; } } exports.handler = handler; if (require.main === module) { const args = Array.from(process.argv); const str = args.join(" "); const debug = !!str.match(/-?-debug((=|== )(true|True|TRUE))?/); const serve = !!str.match(/-?-serve((=|== )(true|True|TRUE))?/); if (serve) { const max = Array.prototype.slice.call(str.match(/-?-max(?:=|== )(\d+)/) || [], 1)[0]; const port = Array.prototype.slice.call(str.match(/-?-port(?:=|== )(\d+)/) || [], 1)[0]; lds.serve({ debug, handler, max, port }); } else { const url = Array.prototype.slice.call(str.match(/-?-url(?:=|== )([^ ]+)/) || [], 1)[0]; const method = Array.prototype.slice.call(str.match(/-?-method(?:=|== )([^ ]+)/) || [], 1)[0]; let f = Array.prototype.slice.call(str.match(/-?-f(?:=|== )([^ ]+)/) || [], 1)[0]; const r = Array.prototype.slice.call(str.match(/-?-r(?:=|== )([^ ]+)/) || [], 1)[0]; const x = Array.prototype.slice.call(str.match(/-?-x(?:=|== )(\d+)/) || [], 1)[0]; const y = Array.prototype.slice.call(str.match(/-?-y(?:=|== )(\d+)/) || [], 1)[0]; const z = Array.prototype.slice.call(str.match(/-?-z(?:=|== )(\d+)/) || [], 1)[0]; let output = Array.prototype.slice.call(str.match(/-?-output(?:=|== )([^ ]+)/) || [], 1)[0]; if (!f && output) { if (output.endsWith("png")) f = "png"; else if (output.endsWith("jpg") || output.endsWith("jpeg")) f = "jpg"; } if (output) { const cwd = process.cwd(); if (!path.isAbsolute(output)) { output = path.resolve(cwd, output); } console.log(`[imtiler] saving to "${output}"`); } const event = { queryStringParameters: { url, method, f, x, r, y, z } }; (async () => { const result = await handler(event); // console.log("result:", result); if (output) { writeFileSync(output, Buffer.from(result.body, "base64")); } else { console.log(result.body); } })(); } }