2018-09-30 14:50:09 -04:00
|
|
|
/**
|
2019-04-27 08:36:17 -04:00
|
|
|
* Tileserver
|
|
|
|
* - routes for Express app
|
|
|
|
* - stitch tiles above a certain zoom level (compositing from sharply-rendered lower zooms)
|
|
|
|
* - render empty tile outside extent of geographical area of interest
|
2018-09-30 14:50:09 -04:00
|
|
|
*
|
|
|
|
*/
|
2018-09-10 05:44:32 -04:00
|
|
|
import express from 'express';
|
2019-02-24 14:23:59 -05:00
|
|
|
import sharp from 'sharp';
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2019-02-24 10:15:52 -05:00
|
|
|
import { get, put } from './cache';
|
2019-05-27 13:26:29 -04:00
|
|
|
import { renderTile, getBbox, getXYZ, TILE_SIZE } from './tile';
|
2019-02-24 07:17:59 -05:00
|
|
|
import { strictParseInt } from '../parse';
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2019-02-24 15:07:22 -05:00
|
|
|
// zoom level when we switch from rendering direct from database to instead composing tiles
|
|
|
|
// from the zoom level below - gets similar effect, with much lower load on Postgres
|
|
|
|
const STITCH_THRESHOLD = 12
|
|
|
|
|
|
|
|
// Hard-code extent so we can short-circuit rendering and return empty/transparent tiles outside the area of interest
|
|
|
|
// bbox in CRS espg:3957 in form: [w, s, e, n]
|
|
|
|
const EXTENT_BBOX = [-61149.622628, 6667754.851372, 28128.826409, 6744803.375884]
|
|
|
|
|
2018-09-10 05:44:32 -04:00
|
|
|
// tiles router
|
|
|
|
const router = express.Router()
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
router.get('/highlight/:z/:x/:y.png', handleHighlightTileRequest);
|
2018-10-25 05:16:18 -04:00
|
|
|
|
2019-02-24 08:34:40 -05:00
|
|
|
router.get('/base_light/:z/:x/:y.png', (req, res) => {
|
2019-05-27 13:26:29 -04:00
|
|
|
handleTileRequest('base_light', req, res)
|
2018-10-25 05:16:18 -04:00
|
|
|
});
|
|
|
|
|
2019-02-24 08:34:40 -05:00
|
|
|
router.get('/base_night/:z/:x/:y.png', (req, res) => {
|
2019-05-27 13:26:29 -04:00
|
|
|
handleTileRequest('base_night', req, res)
|
2019-02-24 08:34:40 -05:00
|
|
|
});
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2019-02-24 08:34:40 -05:00
|
|
|
router.get('/date_year/:z/:x/:y.png', (req, res) => {
|
2019-05-27 13:26:29 -04:00
|
|
|
handleTileRequest('date_year', req, res)
|
2018-09-10 05:44:32 -04:00
|
|
|
});
|
|
|
|
|
2019-02-24 08:34:40 -05:00
|
|
|
router.get('/size_storeys/:z/:x/:y.png', (req, res) => {
|
2019-05-27 13:26:29 -04:00
|
|
|
handleTileRequest('size_storeys', req, res)
|
2019-02-24 08:34:40 -05:00
|
|
|
});
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2019-02-24 08:34:40 -05:00
|
|
|
router.get('/location/:z/:x/:y.png', (req, res) => {
|
2019-05-27 13:26:29 -04:00
|
|
|
handleTileRequest('location', req, res)
|
2018-09-10 05:44:32 -04:00
|
|
|
});
|
|
|
|
|
2019-02-24 08:34:40 -05:00
|
|
|
router.get('/likes/:z/:x/:y.png', (req, res) => {
|
2019-05-27 13:26:29 -04:00
|
|
|
handleTileRequest('likes', req, res)
|
2019-02-24 08:34:40 -05:00
|
|
|
});
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2019-02-24 08:34:40 -05:00
|
|
|
router.get('/conservation_area/:z/:x/:y.png', (req, res) => {
|
2019-05-27 13:26:29 -04:00
|
|
|
handleTileRequest('conservation_area', req, res)
|
2018-09-10 05:44:32 -04:00
|
|
|
});
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function handleTileRequest(tileset, req, res) {
|
2019-02-24 08:34:40 -05:00
|
|
|
const { z, x, y } = req.params
|
2019-05-27 13:26:29 -04:00
|
|
|
const intZ = strictParseInt(z);
|
|
|
|
const intX = strictParseInt(x);
|
|
|
|
const intY = strictParseInt(y);
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
if (isNaN(intX) || isNaN(intY) || isNaN(intZ)) {
|
2019-05-27 11:31:48 -04:00
|
|
|
console.error('Missing x or y or z')
|
2019-02-24 14:28:11 -05:00
|
|
|
return { error: 'Bad parameter' }
|
2019-02-24 08:34:40 -05:00
|
|
|
}
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
loadTile(tileset, intZ, intX, intY).then((im) => {
|
2019-02-24 14:28:11 -05:00
|
|
|
res.writeHead(200, { 'Content-Type': 'image/png' })
|
2019-02-24 14:23:59 -05:00
|
|
|
res.end(im)
|
|
|
|
}).catch((err) => {
|
|
|
|
console.error(err)
|
2019-02-24 14:28:11 -05:00
|
|
|
res.status(500).send({ error: err })
|
2019-02-24 14:23:59 -05:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function loadTile(tileset, z, x, y) {
|
|
|
|
if (outsideExtent(z, x, y)) {
|
|
|
|
return emptyTile()
|
2019-02-24 15:07:22 -05:00
|
|
|
}
|
2019-04-27 10:52:12 -04:00
|
|
|
return get(tileset, z, x, y).then((im) => {
|
|
|
|
console.log(`From cache ${tileset}/${z}/${x}/${y}`)
|
|
|
|
return im
|
2019-05-27 11:25:31 -04:00
|
|
|
}).catch(() => {
|
2019-05-27 13:26:29 -04:00
|
|
|
return renderOrStitchTile(tileset, z, x, y)
|
2019-02-24 14:23:59 -05:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function renderOrStitchTile(tileset, z, x, y) {
|
2019-02-24 14:23:59 -05:00
|
|
|
if (z <= STITCH_THRESHOLD) {
|
2019-05-27 13:26:29 -04:00
|
|
|
return StitchTile(tileset, z, x, y).then(im => {
|
2019-04-27 10:52:12 -04:00
|
|
|
return put(im, tileset, z, x, y).then(() => {
|
|
|
|
console.log(`Stitch ${tileset}/${z}/${x}/${y}`)
|
|
|
|
return im
|
|
|
|
}).catch((err) => {
|
|
|
|
console.error(err)
|
|
|
|
return im
|
2019-02-24 10:15:52 -05:00
|
|
|
})
|
2019-02-24 14:23:59 -05:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2019-05-27 13:26:29 -04:00
|
|
|
renderTile(tileset, z, x, y, undefined, (err, im) => {
|
2019-02-24 14:23:59 -05:00
|
|
|
if (err) {
|
|
|
|
reject(err)
|
|
|
|
return
|
|
|
|
}
|
2019-04-27 10:52:12 -04:00
|
|
|
put(im, tileset, z, x, y).then(() => {
|
|
|
|
console.log(`Render ${tileset}/${z}/${x}/${y}`)
|
|
|
|
resolve(im)
|
|
|
|
}).catch((err) => {
|
|
|
|
console.error(err)
|
2019-02-24 14:23:59 -05:00
|
|
|
resolve(im)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2019-02-24 10:15:52 -05:00
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function outsideExtent(z, x, y) {
|
|
|
|
const xy = getXYZ(EXTENT_BBOX, z);
|
2019-02-24 15:07:22 -05:00
|
|
|
return xy.minY > y || xy.maxY < y || xy.minX > x || xy.maxX < x;
|
|
|
|
}
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function emptyTile() {
|
2019-02-24 15:07:22 -05:00
|
|
|
return sharp({
|
|
|
|
create: {
|
2019-04-27 08:35:11 -04:00
|
|
|
width: 1,
|
|
|
|
height: 1,
|
2019-02-24 15:07:22 -05:00
|
|
|
channels: 4,
|
|
|
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
|
|
}
|
|
|
|
}).png().toBuffer()
|
|
|
|
}
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function StitchTile(tileset, z, x, y) {
|
|
|
|
const bbox = getBbox(z, x, y)
|
|
|
|
const nextZ = z + 1
|
|
|
|
const nextXY = getXYZ(bbox, nextZ)
|
2019-02-24 14:23:59 -05:00
|
|
|
|
|
|
|
return Promise.all([
|
2019-05-27 11:39:16 -04:00
|
|
|
// recurse down through zoom levels, using cache if available...
|
2019-05-27 13:26:29 -04:00
|
|
|
loadTile(tileset, nextZ, nextXY.minX, nextXY.minY),
|
|
|
|
loadTile(tileset, nextZ, nextXY.maxX, nextXY.minY),
|
|
|
|
loadTile(tileset, nextZ, nextXY.minX, nextXY.maxY),
|
|
|
|
loadTile(tileset, nextZ, nextXY.maxX, nextXY.maxY)
|
2019-02-24 14:23:59 -05:00
|
|
|
]).then(([
|
2019-05-27 13:26:29 -04:00
|
|
|
topLeft,
|
|
|
|
topRight,
|
|
|
|
bottomLeft,
|
|
|
|
bottomRight
|
2019-02-24 14:23:59 -05:00
|
|
|
]) => {
|
2019-05-27 11:39:16 -04:00
|
|
|
// not possible to chain overlays in a single pipeline, but there may still be a better
|
|
|
|
// way to create image buffer here (four tiles resize to one at the next zoom level)
|
|
|
|
// instead of repeatedly creating `sharp` objects, to png, to buffer...
|
2019-02-24 14:23:59 -05:00
|
|
|
return sharp({
|
|
|
|
create: {
|
2019-02-24 15:07:22 -05:00
|
|
|
width: TILE_SIZE * 2,
|
|
|
|
height: TILE_SIZE * 2,
|
2019-02-24 14:23:59 -05:00
|
|
|
channels: 4,
|
|
|
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
|
|
}
|
|
|
|
}).overlayWith(
|
2019-05-27 13:26:29 -04:00
|
|
|
topLeft, { gravity: sharp.gravity.northwest }
|
2019-02-24 14:23:59 -05:00
|
|
|
).png().toBuffer().then((buf) => {
|
|
|
|
return sharp(buf).overlayWith(
|
2019-05-27 13:26:29 -04:00
|
|
|
topRight, { gravity: sharp.gravity.northeast }
|
2019-02-24 14:23:59 -05:00
|
|
|
).png().toBuffer()
|
|
|
|
}).then((buf) => {
|
|
|
|
return sharp(buf).overlayWith(
|
2019-05-27 13:26:29 -04:00
|
|
|
bottomLeft, { gravity: sharp.gravity.southwest }
|
2019-02-24 14:23:59 -05:00
|
|
|
).png().toBuffer()
|
|
|
|
}).then((buf) => {
|
|
|
|
return sharp(buf).overlayWith(
|
2019-05-27 13:26:29 -04:00
|
|
|
bottomRight, { gravity: sharp.gravity.southeast }
|
2019-02-24 14:23:59 -05:00
|
|
|
).png().toBuffer()
|
|
|
|
}).then((buf) => {
|
|
|
|
return sharp(buf
|
2019-02-24 15:07:22 -05:00
|
|
|
).resize(TILE_SIZE, TILE_SIZE, { fit: 'inside' }
|
2019-02-24 14:28:11 -05:00
|
|
|
).png().toBuffer()
|
2019-02-24 14:23:59 -05:00
|
|
|
})
|
|
|
|
});
|
2019-02-24 08:34:40 -05:00
|
|
|
}
|
2018-10-25 07:39:41 -04:00
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function handleHighlightTileRequest(req, res) {
|
2019-02-24 08:34:40 -05:00
|
|
|
const { z, x, y } = req.params
|
2019-05-27 13:26:29 -04:00
|
|
|
const intZ = strictParseInt(z);
|
|
|
|
const intX = strictParseInt(x);
|
|
|
|
const intY = strictParseInt(y);
|
2018-10-25 08:48:48 -04:00
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
if (isNaN(intX) || isNaN(intY) || isNaN(intZ)) {
|
2019-05-27 11:31:48 -04:00
|
|
|
console.error('Missing x or y or z')
|
2019-02-24 14:28:11 -05:00
|
|
|
return { error: 'Bad parameter' }
|
2019-02-24 08:34:40 -05:00
|
|
|
}
|
2018-10-25 08:48:48 -04:00
|
|
|
|
2019-02-24 08:34:40 -05:00
|
|
|
// highlight layer uses geometry_id to outline a single building
|
|
|
|
const { highlight } = req.query
|
2019-05-27 13:26:29 -04:00
|
|
|
const geometryId = strictParseInt(highlight);
|
|
|
|
if (isNaN(geometryId)) {
|
2019-02-24 14:28:11 -05:00
|
|
|
res.status(400).send({ error: 'Bad parameter' })
|
2019-02-24 08:34:40 -05:00
|
|
|
return
|
|
|
|
}
|
2019-01-19 11:55:30 -05:00
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
if (outsideExtent(z, x, y)) {
|
|
|
|
return emptyTile()
|
2019-04-27 08:35:51 -04:00
|
|
|
}
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
renderTile('highlight', intZ, intX, intY, geometryId, function (err, im) {
|
2019-05-27 11:20:00 -04:00
|
|
|
if (err) {throw err}
|
2019-01-19 11:55:30 -05:00
|
|
|
|
2019-02-24 14:28:11 -05:00
|
|
|
res.writeHead(200, { 'Content-Type': 'image/png' })
|
2019-02-24 08:49:16 -05:00
|
|
|
res.end(im)
|
2019-01-19 11:55:30 -05:00
|
|
|
})
|
2019-02-24 08:34:40 -05:00
|
|
|
}
|
2019-01-19 11:55:30 -05:00
|
|
|
|
2018-09-10 05:44:32 -04:00
|
|
|
export default router;
|