imtiler
Version:
Modern Image Tiler
304 lines (265 loc) • 9.77 kB
JavaScript
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);
}
})();
}
}