Better split of responsibilities for tile routing/rendering

This commit is contained in:
Tom Russell 2019-02-24 13:34:40 +00:00
parent 961441c5c0
commit de5ba78d3f
3 changed files with 179 additions and 201 deletions

View File

@ -40,7 +40,7 @@
<LineSymbolizer stroke="#00ffffff" stroke-width="2.5" /> <LineSymbolizer stroke="#00ffffff" stroke-width="2.5" />
</Rule> </Rule>
</Style> </Style>
<Style name="location_info_count"> <Style name="location">
<Rule> <Rule>
<Filter>[location_info_count] &gt;= 8</Filter> <Filter>[location_info_count] &gt;= 8</Filter>
<PolygonSymbolizer fill="#084081" /> <PolygonSymbolizer fill="#084081" />
@ -178,7 +178,7 @@
<PolygonSymbolizer fill="#7a5732" /> <PolygonSymbolizer fill="#7a5732" />
</Rule> </Rule>
</Style> </Style>
<Style name="planning_in_conservation_area"> <Style name="conservation_area">
<Rule> <Rule>
<PolygonSymbolizer fill="#73ebaf" /> <PolygonSymbolizer fill="#73ebaf" />
</Rule> </Rule>

View File

@ -1,13 +1,11 @@
/** /**
* Tile-rendering helpers * Render tiles
* *
*/ */
import path from 'path'; import path from 'path';
import mapnik from 'mapnik'; import mapnik from 'mapnik';
import SphericalMercator from '@mapbox/sphericalmercator'; import SphericalMercator from '@mapbox/sphericalmercator';
import { strictParseInt } from '../parse';
// connection details from environment variables // connection details from environment variables
const DATASOURCE_CONFIG = { const DATASOURCE_CONFIG = {
'host': process.env.PGHOST, 'host': process.env.PGHOST,
@ -25,6 +23,103 @@ const TILE_SIZE = 256
const TILE_BUFFER_SIZE = 64 const TILE_BUFFER_SIZE = 64
const PROJ4_STRING = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over'; const PROJ4_STRING = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over';
// Mapnik uses table definitions to query geometries and attributes from PostGIS.
// The queries here are eventually used as subqueries when Mapnik fetches data to render a
// tile - so given a table_definition like:
// (SELECT geometry_geom FROM geometries) as my_table_definition
// Mapnik will wrap it in a bbox query and PostGIS will eventually see something like:
// SELECT AsBinary("geometry") AS geom from
// (SELECT geometry_geom FROM geometries) as my_table_definition
// WHERE "geometry" && SetSRID('BOX3D(0,1,2,3)'::box3d, 3857)
// see docs: https://github.com/mapnik/mapnik/wiki/OptimizeRenderingWithPostGIS
const MAP_STYLE_TABLE_DEFINITIONS = {
base_light: `(
SELECT
b.location_number as location_number,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as outline`,
base_night: `(
SELECT
b.location_number as location_number,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as outline`,
date_year: `(
SELECT
b.date_year as date_year,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as outline`,
size_storeys: `(
SELECT
(
coalesce(b.size_storeys_attic, 0) +
coalesce(b.size_storeys_core, 0)
) as size_storeys,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as outline`,
location: `(
SELECT
(
case when b.location_name is null then 0 else 1 end +
case when b.location_number is null then 0 else 1 end +
case when b.location_street is null then 0 else 1 end +
case when b.location_line_two is null then 0 else 1 end +
case when b.location_town is null then 0 else 1 end +
case when b.location_postcode is null then 0 else 1 end +
case when b.location_latitude is null then 0 else 1 end +
case when b.location_longitude is null then 0 else 1 end +
case when b.ref_toid is null then 0 else 1 end +
case when b.ref_osm_id is null then 0 else 1 end
) as location_info_count,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as location`,
likes: `(
SELECT
g.geometry_geom,
b.likes_total as likes
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
AND b.likes_total > 0
) as location`,
conservation_area: `(
SELECT
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
AND b.planning_in_conservation_area = true
) as conservation_area`
}
// register datasource adapters for mapnik database connection // register datasource adapters for mapnik database connection
if (mapnik.register_default_input_plugins) mapnik.register_default_input_plugins(); if (mapnik.register_default_input_plugins) mapnik.register_default_input_plugins();
// register fonts for text rendering // register fonts for text rendering
@ -44,39 +139,24 @@ function get_bbox(int_z, int_x, int_y){
); );
} }
function should_try_cache(style, int_z) { function render_tile(tileset, z, x, y, geometry_id, cb){
if (style === 'base_light' || style === 'base_light') { const bbox = get_bbox(z, x, y)
// cache for higher zoom levels (unlikely to change)
return int_z <= 15
}
// else cache for lower zoom levels (change slowly)
return int_z <= 12
}
function render_tile(params, table_def, style_def, cb){
const { z, x, y } = params
const int_z = strictParseInt(z);
const int_x = strictParseInt(x);
const int_y = strictParseInt(y);
if (isNaN(int_x) || isNaN(int_y) || isNaN(int_z)){
console.error("Missing x or y or z")
return {error:'Bad parameter'}
}
const bbox = get_bbox(int_z, int_x, int_y)
// const should_cache = should_try_cache(style_def[0], int_z)
const map = new mapnik.Map(TILE_SIZE, TILE_SIZE, PROJ4_STRING); const map = new mapnik.Map(TILE_SIZE, TILE_SIZE, PROJ4_STRING);
map.bufferSize = TILE_BUFFER_SIZE; map.bufferSize = TILE_BUFFER_SIZE;
const layer = new mapnik.Layer('tile', PROJ4_STRING); const layer = new mapnik.Layer('tile', PROJ4_STRING);
const table_def = (tileset === 'highlight')?
get_highlight_table_def(geometry_id)
: MAP_STYLE_TABLE_DEFINITIONS[tileset];
const conf = Object.assign({table: table_def}, DATASOURCE_CONFIG) const conf = Object.assign({table: table_def}, DATASOURCE_CONFIG)
var postgis; var postgis;
try { try {
postgis = new mapnik.Datasource(conf); postgis = new mapnik.Datasource(conf);
layer.datasource = postgis; layer.datasource = postgis;
layer.styles = style_def layer.styles = [tileset]
map.load( map.load(
path.join(__dirname, '..', 'map_styles', 'polygon.xml'), path.join(__dirname, '..', 'map_styles', 'polygon.xml'),
@ -98,4 +178,19 @@ function render_tile(params, table_def, style_def, cb){
} }
} }
// highlight single geometry, requires geometry_id in the table query
function get_highlight_table_def(geometry_id) {
return `(
SELECT
g.geometry_id = ${geometry_id} as focus,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as highlight`
}
export { get_bbox, render_tile }; export { get_bbox, render_tile };

View File

@ -10,197 +10,80 @@ import { strictParseInt } from '../parse';
// tiles router // tiles router
const router = express.Router() const router = express.Router()
// basic geometry tiles router.get('/highlight/:z/:x/:y.png', handle_highlight_tile_request);
router.get('/base_light/:z/:x/:y.png', function(req, res) {
const table_def = `( router.get('/base_light/:z/:x/:y.png', (req, res) => {
SELECT handle_tile_request('base_light', req, res)
b.location_number as location_number, });
g.geometry_geom
FROM router.get('/base_night/:z/:x/:y.png', (req, res) => {
geometries as g, handle_tile_request('base_night', req, res)
buildings as b });
WHERE
g.geometry_id = b.geometry_id router.get('/date_year/:z/:x/:y.png', (req, res) => {
) as outline` handle_tile_request('date_year', req, res)
const style_def = ['base_light'] });
render_tile(req.params, table_def, style_def, function(err, im) {
router.get('/size_storeys/:z/:x/:y.png', (req, res) => {
handle_tile_request('size_storeys', req, res)
});
router.get('/location/:z/:x/:y.png', (req, res) => {
handle_tile_request('location', req, res)
});
router.get('/likes/:z/:x/:y.png', (req, res) => {
handle_tile_request('likes', req, res)
});
router.get('/conservation_area/:z/:x/:y.png', (req, res) => {
handle_tile_request('conservation_area', req, res)
});
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);
if (isNaN(int_x) || isNaN(int_y) || isNaN(int_z)){
console.error("Missing x or y or z")
return {error:'Bad parameter'}
}
render_tile(tileset, int_z, int_x, int_y, undefined, function(err, im) {
if (err) throw err if (err) throw err
res.writeHead(200, {'Content-Type': 'image/png'}) res.writeHead(200, {'Content-Type': 'image/png'})
res.end(im.encodeSync('png')) res.end(im.encodeSync('png'))
}) })
}); }
// dark theme function handle_highlight_tile_request(req, res) {
router.get('/base_night/:z/:x/:y.png', function(req, res) { const { z, x, y } = req.params
const table_def = `( const int_z = strictParseInt(z);
SELECT const int_x = strictParseInt(x);
b.location_number as location_number, const int_y = strictParseInt(y);
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as outline`
const style_def = ['base_night']
render_tile(req.params, table_def, style_def, function(err, im) {
if (err) throw err
res.writeHead(200, {'Content-Type': 'image/png'}) if (isNaN(int_x) || isNaN(int_y) || isNaN(int_z)){
res.end(im.encodeSync('png')) console.error("Missing x or y or z")
}) return {error:'Bad parameter'}
}); }
// highlight single geometry // highlight layer uses geometry_id to outline a single building
router.get('/highlight/:z/:x/:y.png', function(req, res) {
const { highlight } = req.query const { highlight } = req.query
const geometry_id = strictParseInt(highlight); const geometry_id = strictParseInt(highlight);
if(isNaN(geometry_id)){ if(isNaN(geometry_id)){
res.status(400).send({error:'Bad parameter'}) res.status(400).send({error:'Bad parameter'})
return return
} }
const table_def = `(
SELECT render_tile('highlight', int_z, int_x, int_y, geometry_id, function(err, im) {
g.geometry_id = ${geometry_id} as focus,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as highlight`
const style_def = ['highlight']
render_tile(req.params, table_def, style_def, function(err, im) {
if (err) throw err if (err) throw err
res.writeHead(200, {'Content-Type': 'image/png'}) res.writeHead(200, {'Content-Type': 'image/png'})
res.end(im.encodeSync('png')) res.end(im.encodeSync('png'))
}) })
}); }
// date_year choropleth
router.get('/date_year/:z/:x/:y.png', function(req, res) {
// const table_def = 'geometries'
const table_def = `(
SELECT
b.date_year as date_year,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as outline`
const style_def = ['date_year']
render_tile(req.params, table_def, style_def, function(err, im) {
if (err) throw err
res.writeHead(200, {'Content-Type': 'image/png'})
res.end(im.encodeSync('png'))
})
});
// date_year choropleth
router.get('/size_storeys/:z/:x/:y.png', function(req, res) {
// const table_def = 'geometries'
const table_def = `(
SELECT
(
coalesce(b.size_storeys_attic, 0) +
coalesce(b.size_storeys_core, 0)
) as size_storeys,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as outline`
const style_def = ['size_storeys']
render_tile(req.params, table_def, style_def, function(err, im) {
if (err) throw err
res.writeHead(200, {'Content-Type': 'image/png'})
res.end(im.encodeSync('png'))
})
});
// location information depth
router.get('/location/:z/:x/:y.png', function(req, res) {
const table_def = `(
SELECT
(
case when b.location_name is null then 0 else 1 end +
case when b.location_number is null then 0 else 1 end +
case when b.location_street is null then 0 else 1 end +
case when b.location_line_two is null then 0 else 1 end +
case when b.location_town is null then 0 else 1 end +
case when b.location_postcode is null then 0 else 1 end +
case when b.location_latitude is null then 0 else 1 end +
case when b.location_longitude is null then 0 else 1 end +
case when b.ref_toid is null then 0 else 1 end +
case when b.ref_osm_id is null then 0 else 1 end
) as location_info_count,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
) as location`
const style_def = ['location_info_count']
render_tile(req.params, table_def, style_def, function(err, im) {
if (err) throw err
res.writeHead(200, {'Content-Type': 'image/png'})
res.end(im.encodeSync('png'))
})
});
// likes
router.get('/likes/:z/:x/:y.png', function(req, res) {
const table_def = `(
SELECT
g.geometry_geom,
b.likes_total as likes
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
AND b.likes_total > 0
) as location`
const style_def = ['likes']
render_tile(req.params, table_def, style_def, function(err, im) {
if (err) throw err
res.writeHead(200, {'Content-Type': 'image/png'})
res.end(im.encodeSync('png'))
})
});
// conservation status
router.get('/conservation_area/:z/:x/:y.png', function(req, res) {
const table_def = `(
SELECT
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE
g.geometry_id = b.geometry_id
AND b.planning_in_conservation_area = true
) as conservation_area`
const style_def = ['planning_in_conservation_area']
render_tile(req.params, table_def, style_def, function(err, im) {
if (err) throw err
res.writeHead(200, {'Content-Type': 'image/png'})
res.end(im.encodeSync('png'))
})
});
export default router; export default router;