Better split of responsibilities for tile routing/rendering
This commit is contained in:
parent
961441c5c0
commit
de5ba78d3f
@ -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] >= 8</Filter>
|
<Filter>[location_info_count] >= 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>
|
||||||
|
@ -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 };
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user