colouring-montreal/app/src/tiles/tileserver.js

196 lines
5.8 KiB
JavaScript
Raw Normal View History

2018-09-30 14:50:09 -04:00
/**
* Tileserver routes for Express app
*
*/
2018-09-10 05:44:32 -04:00
import express from 'express';
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';
import { render_tile, get_bbox, get_xyz } from './tile';
2019-02-24 07:17:59 -05:00
import { strictParseInt } from '../parse';
2018-09-10 05:44:32 -04:00
// tiles router
const router = express.Router()
router.get('/highlight/:z/:x/:y.png', handle_highlight_tile_request);
router.get('/base_light/:z/:x/:y.png', (req, res) => {
handle_tile_request('base_light', req, res)
});
router.get('/base_night/:z/:x/:y.png', (req, res) => {
handle_tile_request('base_night', req, res)
});
2018-09-10 05:44:32 -04:00
router.get('/date_year/:z/:x/:y.png', (req, res) => {
handle_tile_request('date_year', req, res)
2018-09-10 05:44:32 -04:00
});
router.get('/size_storeys/:z/:x/:y.png', (req, res) => {
handle_tile_request('size_storeys', req, res)
});
2018-09-10 05:44:32 -04:00
router.get('/location/:z/:x/:y.png', (req, res) => {
handle_tile_request('location', req, res)
2018-09-10 05:44:32 -04:00
});
router.get('/likes/:z/:x/:y.png', (req, res) => {
handle_tile_request('likes', req, res)
});
2018-09-10 05:44:32 -04:00
router.get('/conservation_area/:z/:x/:y.png', (req, res) => {
handle_tile_request('conservation_area', req, res)
2018-09-10 05:44:32 -04:00
});
function handle_tile_request(tileset, req, res) {
const { z, x, y } = req.params
const int_z = strictParseInt(z);
const int_x = strictParseInt(x);
const int_y = strictParseInt(y);
2018-09-10 05:44:32 -04:00
2019-02-24 14:28:11 -05:00
if (isNaN(int_x) || isNaN(int_y) || isNaN(int_z)) {
console.error("Missing x or y or z")
2019-02-24 14:28:11 -05:00
return { error: 'Bad parameter' }
}
2018-09-10 05:44:32 -04:00
load_tile(tileset, int_z, int_x, int_y).then((im) => {
2019-02-24 14:28:11 -05:00
res.writeHead(200, { 'Content-Type': 'image/png' })
res.end(im)
}).catch((err) => {
console.error(err)
2019-02-24 14:28:11 -05:00
res.status(500).send({ error: err })
})
}
function load_tile(tileset, z, x, y) {
return new Promise((resolve) => {
get(tileset, z, x, y, (err, im) => {
if (err) {
render_or_stitch_tile(tileset, z, x, y)
.then((im) => {
resolve(im)
})
} else {
console.log(`From cache ${tileset}/${z}/${x}/${y}`)
resolve(im)
}
})
})
}
function render_or_stitch_tile(tileset, z, x, y) {
const STITCH_THRESHOLD = 12
if (z <= STITCH_THRESHOLD) {
2019-02-24 10:15:52 -05:00
return stitch_tile(tileset, z, x, y).then(im => {
return new Promise((resolve, reject) => {
2019-02-24 10:15:52 -05:00
put(im, tileset, z, x, y, (err) => {
if (err) {
console.error(err)
} else {
console.log(`Stitch ${tileset}/${z}/${x}/${y}`)
2019-02-24 10:15:52 -05:00
}
resolve(im)
2019-02-24 10:15:52 -05:00
})
})
})
} else {
return new Promise((resolve, reject) => {
render_tile(tileset, z, x, y, undefined, (err, im) => {
if (err) {
reject(err)
return
}
put(im, tileset, z, x, y, (err) => {
if (err) {
console.error(err)
} else {
console.log(`Render ${tileset}/${z}/${x}/${y}`)
}
resolve(im)
})
})
})
}
}
2019-02-24 10:15:52 -05:00
function stitch_tile(tileset, z, x, y) {
const bbox = get_bbox(z, x, y)
const next_z = z + 1
const next_xy = get_xyz(bbox, next_z)
return Promise.all([
// recurse down through zoom levels, using cache if available...
load_tile(tileset, next_z, next_xy.minX, next_xy.minY),
load_tile(tileset, next_z, next_xy.maxX, next_xy.minY),
load_tile(tileset, next_z, next_xy.minX, next_xy.maxY),
load_tile(tileset, next_z, next_xy.maxX, next_xy.maxY)
]).then(([
top_left,
top_right,
bottom_left,
bottom_right
]) => {
// 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...
return sharp({
create: {
width: 512,
height: 512,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
}).overlayWith(
top_left, { gravity: sharp.gravity.northwest }
).png().toBuffer().then((buf) => {
return sharp(buf).overlayWith(
top_right, { gravity: sharp.gravity.northeast }
).png().toBuffer()
}).then((buf) => {
return sharp(buf).overlayWith(
bottom_left, { gravity: sharp.gravity.southwest }
).png().toBuffer()
}).then((buf) => {
return sharp(buf).overlayWith(
bottom_right, { gravity: sharp.gravity.southeast }
).png().toBuffer()
}).then((buf) => {
return sharp(buf
2019-02-24 14:28:11 -05:00
).resize(256, 256, { fit: 'inside' }
).png().toBuffer()
})
});
}
function handle_highlight_tile_request(req, res) {
const { z, x, y } = req.params
const int_z = strictParseInt(z);
const int_x = strictParseInt(x);
const int_y = strictParseInt(y);
2019-02-24 14:28:11 -05:00
if (isNaN(int_x) || isNaN(int_y) || isNaN(int_z)) {
console.error("Missing x or y or z")
2019-02-24 14:28:11 -05:00
return { error: 'Bad parameter' }
}
// highlight layer uses geometry_id to outline a single building
const { highlight } = req.query
const geometry_id = strictParseInt(highlight);
2019-02-24 14:28:11 -05:00
if (isNaN(geometry_id)) {
res.status(400).send({ error: 'Bad parameter' })
return
}
2019-02-24 14:28:11 -05:00
render_tile('highlight', int_z, int_x, int_y, geometry_id, function (err, im) {
if (err) throw err
2019-02-24 14:28:11 -05:00
res.writeHead(200, { 'Content-Type': 'image/png' })
res.end(im)
})
}
2018-09-10 05:44:32 -04:00
export default router;