Use functions instead of classes in tile rendering
This commit is contained in:
parent
dd7d66e5fc
commit
5417b5c8b6
@ -1,5 +1,5 @@
|
|||||||
import { strictParseInt } from "../parse";
|
import { strictParseInt } from "../parse";
|
||||||
import { DataConfig } from "./renderers/datasourceRenderer";
|
import { DataConfig } from "./types";
|
||||||
|
|
||||||
const BUILDING_LAYER_DEFINITIONS = {
|
const BUILDING_LAYER_DEFINITIONS = {
|
||||||
base_light: `(
|
base_light: `(
|
||||||
|
@ -1,33 +1,28 @@
|
|||||||
import { TileCache } from "./tileCache";
|
import { TileCache } from "./tileCache";
|
||||||
import { BoundingBox, TileParams } from "./types";
|
import { BoundingBox, TileParams, Tile } from "./types";
|
||||||
import { StitchRenderer } from "./renderers/stitchRenderer";
|
|
||||||
import { CachedRenderer } from "./renderers/cachedRenderer";
|
|
||||||
import { BranchingRenderer } from "./renderers/branchingRenderer";
|
|
||||||
import { WindowedRenderer } from "./renderers/windowedRenderer";
|
|
||||||
import { BlankRenderer } from "./renderers/blankRenderer";
|
|
||||||
import { DatasourceRenderer } from "./renderers/datasourceRenderer";
|
|
||||||
import { getBuildingsDataConfig, getHighlightDataConfig, BUILDING_LAYER_DEFINITIONS } from "./dataDefinition";
|
import { getBuildingsDataConfig, getHighlightDataConfig, BUILDING_LAYER_DEFINITIONS } from "./dataDefinition";
|
||||||
|
import { isOutsideExtent } from "./util";
|
||||||
|
import { renderDataSourceTile } from "./renderers/renderDataSourceTile";
|
||||||
|
import { getTileWithCaching } from "./renderers/getTileWithCaching";
|
||||||
|
import { stitchTile } from "./renderers/stitchTile";
|
||||||
|
import { createBlankTile } from "./renderers/createBlankTile";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all tilesets handled by the tile server
|
* A list of all tilesets handled by the tile server
|
||||||
*/
|
*/
|
||||||
const allTilesets = ['highlight', ...Object.keys(BUILDING_LAYER_DEFINITIONS)];
|
const allTilesets = ['highlight', ...Object.keys(BUILDING_LAYER_DEFINITIONS)];
|
||||||
|
|
||||||
const buildingDataRenderer = new DatasourceRenderer(getBuildingsDataConfig);
|
|
||||||
|
|
||||||
const stitchRenderer = new StitchRenderer(undefined); // depends recurrently on cache, so parameter will be set later
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zoom level when we switch from rendering direct from database to instead composing tiles
|
* 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
|
* from the zoom level below - gets similar effect, with much lower load on Postgres
|
||||||
*/
|
*/
|
||||||
const STITCH_THRESHOLD = 12;
|
const STITCH_THRESHOLD = 12;
|
||||||
|
|
||||||
const renderOrStitchRenderer = new BranchingRenderer(
|
/**
|
||||||
({ z }) => z <= STITCH_THRESHOLD,
|
* Hard-code extent so we can short-circuit rendering and return empty/transparent tiles outside the area of interest
|
||||||
stitchRenderer, // refer to the prepared stitch renderer
|
* bbox in CRS epsg:3857 in form: [w, s, e, n]
|
||||||
buildingDataRenderer
|
*/
|
||||||
);
|
const EXTENT_BBOX: BoundingBox = [-61149.622628, 6667754.851372, 28128.826409, 6744803.375884];
|
||||||
|
|
||||||
const tileCache = new TileCache(
|
const tileCache = new TileCache(
|
||||||
process.env.TILECACHE_PATH,
|
process.env.TILECACHE_PATH,
|
||||||
@ -42,37 +37,36 @@ const tileCache = new TileCache(
|
|||||||
z <= 13
|
z <= 13
|
||||||
);
|
);
|
||||||
|
|
||||||
const cachedRenderer = new CachedRenderer(
|
const renderBuildingTile = (t: TileParams, d: any) => renderDataSourceTile(t, d, getBuildingsDataConfig);
|
||||||
tileCache,
|
const renderHighlightTile = (t: TileParams, d: any) => renderDataSourceTile(t, d, getHighlightDataConfig);
|
||||||
renderOrStitchRenderer
|
|
||||||
);
|
|
||||||
|
|
||||||
// set up stitch renderer to use the data renderer with caching
|
function cacheOrCreateBuildingTile(tileParams: TileParams, dataParams: any): Promise<Tile> {
|
||||||
stitchRenderer.tileRenderer = cachedRenderer;
|
return getTileWithCaching(tileParams, dataParams, tileCache, stitchOrRenderBuildingTile);
|
||||||
|
}
|
||||||
|
|
||||||
const highlightRenderer = new DatasourceRenderer(getHighlightDataConfig);
|
function stitchOrRenderBuildingTile(tileParams: TileParams, dataParams: any): Promise<Tile> {
|
||||||
|
if (tileParams.z <= STITCH_THRESHOLD) {
|
||||||
|
// stitch tile, using cache recursively
|
||||||
|
return stitchTile(tileParams, dataParams, cacheOrCreateBuildingTile);
|
||||||
|
} else {
|
||||||
|
return renderBuildingTile(tileParams, dataParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const highlightOrBuildingRenderer = new BranchingRenderer(
|
function renderTile(tileParams: TileParams, dataParams: any): Promise<Tile> {
|
||||||
({ tileset }) => tileset === 'highlight',
|
if (isOutsideExtent(tileParams, EXTENT_BBOX)) {
|
||||||
highlightRenderer,
|
return createBlankTile();
|
||||||
cachedRenderer
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const blankRenderer = new BlankRenderer();
|
if (tileParams.tileset === 'highlight') {
|
||||||
|
return renderHighlightTile(tileParams, dataParams);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
return cacheOrCreateBuildingTile(tileParams, dataParams);
|
||||||
* Hard-code extent so we can short-circuit rendering and return empty/transparent tiles outside the area of interest
|
}
|
||||||
* bbox in CRS epsg:3857 in form: [w, s, e, n]
|
|
||||||
*/
|
|
||||||
const EXTENT_BBOX: BoundingBox = [-61149.622628, 6667754.851372, 28128.826409, 6744803.375884];
|
|
||||||
const mainRenderer = new WindowedRenderer(
|
|
||||||
EXTENT_BBOX,
|
|
||||||
highlightOrBuildingRenderer,
|
|
||||||
blankRenderer
|
|
||||||
);
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
allTilesets,
|
allTilesets,
|
||||||
mainRenderer,
|
renderTile,
|
||||||
tileCache
|
tileCache
|
||||||
};
|
};
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { Image } from "mapnik";
|
|
||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import { TileParams, TileRenderer } from "../types";
|
|
||||||
|
|
||||||
class BlankRenderer implements TileRenderer {
|
|
||||||
getTile(tileParams: TileParams): Promise<Image> {
|
|
||||||
return sharp({
|
|
||||||
create: {
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
channels: 4,
|
|
||||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
||||||
}
|
|
||||||
}).png().toBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
BlankRenderer
|
|
||||||
};
|
|
@ -1,23 +0,0 @@
|
|||||||
import { Image } from "mapnik";
|
|
||||||
|
|
||||||
import { TileParams, TileRenderer } from "../types";
|
|
||||||
|
|
||||||
class BranchingRenderer {
|
|
||||||
constructor(
|
|
||||||
public branchTestFn: (tileParams: TileParams) => boolean,
|
|
||||||
public trueResultTileRenderer: TileRenderer,
|
|
||||||
public falseResultTileRenderer: TileRenderer
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getTile(tileParams: TileParams, dataParams: any): Promise<Image> {
|
|
||||||
if(this.branchTestFn(tileParams)) {
|
|
||||||
return this.trueResultTileRenderer.getTile(tileParams, dataParams);
|
|
||||||
} else {
|
|
||||||
return this.falseResultTileRenderer.getTile(tileParams, dataParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
BranchingRenderer
|
|
||||||
};
|
|
@ -1,32 +0,0 @@
|
|||||||
import { Image } from "mapnik";
|
|
||||||
|
|
||||||
import { TileParams, TileRenderer } from "../types";
|
|
||||||
import { TileCache } from "../tileCache";
|
|
||||||
import { formatParams } from "../util";
|
|
||||||
|
|
||||||
class CachedRenderer implements TileRenderer {
|
|
||||||
constructor(
|
|
||||||
/** Cache to use for tiles */
|
|
||||||
public tileCache: TileCache,
|
|
||||||
|
|
||||||
/** Renderer to use when tile hasn't been cached yet */
|
|
||||||
public tileRenderer: TileRenderer
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getTile(tileParams: TileParams, dataParams: any): Promise<Image> {
|
|
||||||
try {
|
|
||||||
const tile = await this.tileCache.get(tileParams);
|
|
||||||
return tile;
|
|
||||||
} catch(err) {
|
|
||||||
const im = await this.tileRenderer.getTile(tileParams, dataParams);
|
|
||||||
try {
|
|
||||||
await this.tileCache.put(im, tileParams);
|
|
||||||
} catch (err) {}
|
|
||||||
return im;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
CachedRenderer
|
|
||||||
};
|
|
18
app/src/tiles/renderers/createBlankTile.ts
Normal file
18
app/src/tiles/renderers/createBlankTile.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
import { Tile } from "../types";
|
||||||
|
|
||||||
|
function createBlankTile(): Promise<Tile> {
|
||||||
|
return sharp({
|
||||||
|
create: {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
}
|
||||||
|
}).png().toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createBlankTile
|
||||||
|
};
|
@ -1,105 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
|
|
||||||
import mapnik from "mapnik";
|
|
||||||
|
|
||||||
import { TileParams, TileRenderer } from "../types";
|
|
||||||
import { getBbox, TILE_SIZE } from "../util";
|
|
||||||
import { promisify } from "util";
|
|
||||||
|
|
||||||
interface DataConfig {
|
|
||||||
table: string;
|
|
||||||
geometry_field: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
// connection details from environment variables
|
|
||||||
const DATASOURCE_CONFIG = {
|
|
||||||
'host': process.env.PGHOST,
|
|
||||||
'dbname': process.env.PGDATABASE,
|
|
||||||
'user': process.env.PGUSER,
|
|
||||||
'password': process.env.PGPASSWORD,
|
|
||||||
'port': process.env.PGPORT,
|
|
||||||
'extent': '-20005048.4188,-9039211.13765,19907487.2779,17096598.5401',
|
|
||||||
'srid': 3857,
|
|
||||||
'type': 'postgis'
|
|
||||||
};
|
|
||||||
|
|
||||||
// register datasource adapters for mapnik database connection
|
|
||||||
if (mapnik.register_default_input_plugins) {
|
|
||||||
mapnik.register_default_input_plugins();
|
|
||||||
}
|
|
||||||
// register fonts for text rendering
|
|
||||||
mapnik.register_default_fonts();
|
|
||||||
|
|
||||||
|
|
||||||
class DatasourceRenderer implements TileRenderer {
|
|
||||||
constructor(private getTableDefinitionFn: (tileset: string, dataParams: any) => DataConfig) {}
|
|
||||||
|
|
||||||
async getTile({tileset, z, x, y, scale}: TileParams, dataParams: any): Promise<mapnik.Image> {
|
|
||||||
const bbox = getBbox(z, x, y);
|
|
||||||
|
|
||||||
const tileSize = TILE_SIZE * scale;
|
|
||||||
let map = new mapnik.Map(tileSize, tileSize, PROJ4_STRING);
|
|
||||||
map.bufferSize = TILE_BUFFER_SIZE;
|
|
||||||
const layer = new mapnik.Layer('tile', PROJ4_STRING);
|
|
||||||
|
|
||||||
const dataSourceConfig = this.getTableDefinitionFn(tileset, dataParams);
|
|
||||||
|
|
||||||
const conf = Object.assign(dataSourceConfig, DATASOURCE_CONFIG);
|
|
||||||
|
|
||||||
const postgis = new mapnik.Datasource(conf);
|
|
||||||
layer.datasource = postgis;
|
|
||||||
layer.styles = [tileset];
|
|
||||||
|
|
||||||
const stylePath = path.join(__dirname, '..', 'map_styles', 'polygon.xml');
|
|
||||||
|
|
||||||
map = await promisify(map.load.bind(map))(stylePath, {strict: true});
|
|
||||||
|
|
||||||
map.add_layer(layer);
|
|
||||||
const im = new mapnik.Image(map.width, map.height);
|
|
||||||
map.extent = bbox;
|
|
||||||
const rendered = await promisify(map.render.bind(map))(im, {});
|
|
||||||
|
|
||||||
return await promisify(rendered.encode.bind(rendered))('png');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function promiseHandler(resolve, reject) {
|
|
||||||
return function(err, result) {
|
|
||||||
if(err) reject(err);
|
|
||||||
else resolve(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function which promisifies a method of an object and binds it to the object
|
|
||||||
* This makes it easier to use callback-based object methods in a promise-based way
|
|
||||||
* @param obj Object containing the target method
|
|
||||||
* @param methodName Method name to promisify and return
|
|
||||||
*/
|
|
||||||
function promisifyMethod<T, F>(obj: T, methodName: keyof T);
|
|
||||||
/**
|
|
||||||
* @param methodGetter accessor function to get the method from the object
|
|
||||||
*/
|
|
||||||
function promisifyMethod<T, S>(obj: T, methodGetter: (o: T) => S);
|
|
||||||
function promisifyMethod<T, S>(obj: T, paramTwo: keyof T | ((o: T) => S)) {
|
|
||||||
let method;
|
|
||||||
if (typeof paramTwo === 'string') {
|
|
||||||
method = obj[paramTwo];
|
|
||||||
} else if (typeof paramTwo === 'function') {
|
|
||||||
method = paramTwo(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof method === 'function') {
|
|
||||||
return promisify(method.bind(obj));
|
|
||||||
} else {
|
|
||||||
throw new Error(`Cannot promisify non-function property '${paramTwo}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
DatasourceRenderer,
|
|
||||||
DataConfig
|
|
||||||
};
|
|
21
app/src/tiles/renderers/getTileWithCaching.ts
Normal file
21
app/src/tiles/renderers/getTileWithCaching.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { TileParams, RendererFunction, Tile } from "../types";
|
||||||
|
import { TileCache } from "../tileCache";
|
||||||
|
|
||||||
|
|
||||||
|
async function getTileWithCaching(tileParams: TileParams, dataParams: any, tileCache: TileCache, renderTile: RendererFunction): Promise<Tile> {
|
||||||
|
try {
|
||||||
|
const tile = await tileCache.get(tileParams);
|
||||||
|
return tile;
|
||||||
|
} catch (err) {
|
||||||
|
const im = await renderTile(tileParams, dataParams);
|
||||||
|
try {
|
||||||
|
await tileCache.put(im, tileParams);
|
||||||
|
} catch (err) {}
|
||||||
|
return im;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
getTileWithCaching
|
||||||
|
};
|
64
app/src/tiles/renderers/renderDataSourceTile.ts
Normal file
64
app/src/tiles/renderers/renderDataSourceTile.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import mapnik from "mapnik";
|
||||||
|
|
||||||
|
import { TileParams, Tile, TableDefinitionFunction } from "../types";
|
||||||
|
import { getBbox, TILE_SIZE } from "../util";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
// connection details from environment variables
|
||||||
|
const DATASOURCE_CONFIG = {
|
||||||
|
'host': process.env.PGHOST,
|
||||||
|
'dbname': process.env.PGDATABASE,
|
||||||
|
'user': process.env.PGUSER,
|
||||||
|
'password': process.env.PGPASSWORD,
|
||||||
|
'port': process.env.PGPORT,
|
||||||
|
'extent': '-20005048.4188,-9039211.13765,19907487.2779,17096598.5401',
|
||||||
|
'srid': 3857,
|
||||||
|
'type': 'postgis'
|
||||||
|
};
|
||||||
|
|
||||||
|
// register datasource adapters for mapnik database connection
|
||||||
|
if (mapnik.register_default_input_plugins) {
|
||||||
|
mapnik.register_default_input_plugins();
|
||||||
|
}
|
||||||
|
// register fonts for text rendering
|
||||||
|
mapnik.register_default_fonts();
|
||||||
|
|
||||||
|
|
||||||
|
async function renderDataSourceTile({ tileset, z, x, y, scale }: TileParams, dataParams: any, getTableDefinitionFn: TableDefinitionFunction): Promise<Tile> {
|
||||||
|
const bbox = getBbox(z, x, y);
|
||||||
|
|
||||||
|
const tileSize = TILE_SIZE * scale;
|
||||||
|
let map = new mapnik.Map(tileSize, tileSize, PROJ4_STRING);
|
||||||
|
map.bufferSize = TILE_BUFFER_SIZE;
|
||||||
|
const layer = new mapnik.Layer('tile', PROJ4_STRING);
|
||||||
|
|
||||||
|
const dataSourceConfig = getTableDefinitionFn(tileset, dataParams);
|
||||||
|
|
||||||
|
const conf = Object.assign(dataSourceConfig, DATASOURCE_CONFIG);
|
||||||
|
|
||||||
|
const postgis = new mapnik.Datasource(conf);
|
||||||
|
layer.datasource = postgis;
|
||||||
|
layer.styles = [tileset];
|
||||||
|
|
||||||
|
const stylePath = path.join(__dirname, '..', 'map_styles', 'polygon.xml');
|
||||||
|
|
||||||
|
map = await promisify(map.load.bind(map))(stylePath, { strict: true });
|
||||||
|
|
||||||
|
map.add_layer(layer);
|
||||||
|
const im = new mapnik.Image(map.width, map.height);
|
||||||
|
map.extent = bbox;
|
||||||
|
const rendered = await promisify(map.render.bind(map))(im, {});
|
||||||
|
|
||||||
|
return await promisify(rendered.encode.bind(rendered))('png');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
renderDataSourceTile
|
||||||
|
};
|
@ -1,67 +0,0 @@
|
|||||||
import sharp from 'sharp';
|
|
||||||
import { Image } from 'mapnik';
|
|
||||||
|
|
||||||
import { TileParams, TileRenderer } from "../types";
|
|
||||||
import { getBbox, getXYZ, TILE_SIZE, formatParams } from "../util";
|
|
||||||
|
|
||||||
class StitchRenderer implements TileRenderer {
|
|
||||||
constructor(
|
|
||||||
/** Renderer to use when retrieving tiles to be stitched together */
|
|
||||||
public tileRenderer: TileRenderer
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getTile(tileParams: TileParams, dataParams: any): Promise<Image> {
|
|
||||||
console.log(`Stitching tile ${formatParams(tileParams)}`);
|
|
||||||
return this.stitchTile(tileParams, dataParams, this.tileRenderer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async stitchTile({ tileset, z, x, y, scale }: TileParams, dataParams: any, tileRenderer: TileRenderer) {
|
|
||||||
const bbox = getBbox(z, x, y);
|
|
||||||
const nextZ = z + 1;
|
|
||||||
const nextXY = getXYZ(bbox, nextZ);
|
|
||||||
const tileSize = TILE_SIZE * scale;
|
|
||||||
|
|
||||||
|
|
||||||
const [topLeft, topRight, bottomLeft, bottomRight] = await Promise.all([
|
|
||||||
[nextXY.minX, nextXY.minY],
|
|
||||||
[nextXY.maxX, nextXY.minY],
|
|
||||||
[nextXY.minX, nextXY.maxY],
|
|
||||||
[nextXY.maxX, nextXY.maxY]
|
|
||||||
].map(([x, y]) => tileRenderer.getTile({ tileset, z: nextZ, x, y, scale }, dataParams)));
|
|
||||||
|
|
||||||
// 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: tileSize * 2,
|
|
||||||
height: tileSize * 2,
|
|
||||||
channels: 4,
|
|
||||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
||||||
}
|
|
||||||
}).overlayWith(
|
|
||||||
topLeft, { gravity: sharp.gravity.northwest }
|
|
||||||
).png().toBuffer().then((buf) => {
|
|
||||||
return sharp(buf).overlayWith(
|
|
||||||
topRight, { gravity: sharp.gravity.northeast }
|
|
||||||
).png().toBuffer()
|
|
||||||
}).then((buf) => {
|
|
||||||
return sharp(buf).overlayWith(
|
|
||||||
bottomLeft, { gravity: sharp.gravity.southwest }
|
|
||||||
).png().toBuffer()
|
|
||||||
}).then((buf) => {
|
|
||||||
return sharp(buf).overlayWith(
|
|
||||||
bottomRight, { gravity: sharp.gravity.southeast }
|
|
||||||
).png().toBuffer()
|
|
||||||
}).then((buf) => {
|
|
||||||
return sharp(buf
|
|
||||||
).resize(tileSize, tileSize, { fit: 'inside' }
|
|
||||||
).png().toBuffer()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
StitchRenderer
|
|
||||||
};
|
|
54
app/src/tiles/renderers/stitchTile.ts
Normal file
54
app/src/tiles/renderers/stitchTile.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
import { TileParams, RendererFunction, Tile } from "../types";
|
||||||
|
import { getBbox, getXYZ, TILE_SIZE } from "../util";
|
||||||
|
|
||||||
|
|
||||||
|
async function stitchTile({ tileset, z, x, y, scale }: TileParams, dataParams: any, renderTile: RendererFunction): Promise<Tile> {
|
||||||
|
const bbox = getBbox(z, x, y);
|
||||||
|
const nextZ = z + 1;
|
||||||
|
const nextXY = getXYZ(bbox, nextZ);
|
||||||
|
const tileSize = TILE_SIZE * scale;
|
||||||
|
|
||||||
|
|
||||||
|
const [topLeft, topRight, bottomLeft, bottomRight] = await Promise.all([
|
||||||
|
[nextXY.minX, nextXY.minY],
|
||||||
|
[nextXY.maxX, nextXY.minY],
|
||||||
|
[nextXY.minX, nextXY.maxY],
|
||||||
|
[nextXY.maxX, nextXY.maxY]
|
||||||
|
].map(([x, y]) => renderTile({ tileset, z: nextZ, x, y, scale }, dataParams)));
|
||||||
|
|
||||||
|
// 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: tileSize * 2,
|
||||||
|
height: tileSize * 2,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||||
|
}
|
||||||
|
}).overlayWith(
|
||||||
|
topLeft, { gravity: sharp.gravity.northwest }
|
||||||
|
).png().toBuffer().then((buf) => {
|
||||||
|
return sharp(buf).overlayWith(
|
||||||
|
topRight, { gravity: sharp.gravity.northeast }
|
||||||
|
).png().toBuffer()
|
||||||
|
}).then((buf) => {
|
||||||
|
return sharp(buf).overlayWith(
|
||||||
|
bottomLeft, { gravity: sharp.gravity.southwest }
|
||||||
|
).png().toBuffer()
|
||||||
|
}).then((buf) => {
|
||||||
|
return sharp(buf).overlayWith(
|
||||||
|
bottomRight, { gravity: sharp.gravity.southeast }
|
||||||
|
).png().toBuffer()
|
||||||
|
}).then((buf) => {
|
||||||
|
return sharp(buf
|
||||||
|
).resize(tileSize, tileSize, { fit: 'inside' }
|
||||||
|
).png().toBuffer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
stitchTile
|
||||||
|
};
|
@ -1,34 +0,0 @@
|
|||||||
import { Image } from "mapnik";
|
|
||||||
|
|
||||||
import { BoundingBox, TileParams, TileRenderer } from "../types";
|
|
||||||
import { getXYZ } from "../util";
|
|
||||||
|
|
||||||
class WindowedRenderer implements TileRenderer {
|
|
||||||
constructor(
|
|
||||||
/** Bounding box defining the renderer window */
|
|
||||||
public bbox: BoundingBox,
|
|
||||||
|
|
||||||
/** Renderer to use for tile requests inside window */
|
|
||||||
public insideWindowRenderer: TileRenderer,
|
|
||||||
|
|
||||||
/** Renderer to use for tile requests outside window */
|
|
||||||
public outsideWindowRenderer: TileRenderer
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getTile(tileParams: TileParams, dataParams: any): Promise<Image> {
|
|
||||||
if(this.isOutsideExtent(tileParams)) {
|
|
||||||
return this.outsideWindowRenderer.getTile(tileParams, dataParams);
|
|
||||||
} else {
|
|
||||||
return this.insideWindowRenderer.getTile(tileParams, dataParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isOutsideExtent({x, y, z}: TileParams) {
|
|
||||||
const xy = getXYZ(this.bbox, z);
|
|
||||||
return xy.minY > y || xy.maxY < y || xy.minX > x || xy.maxX < x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
WindowedRenderer
|
|
||||||
};
|
|
@ -7,7 +7,7 @@ import express from 'express';
|
|||||||
|
|
||||||
import { strictParseInt } from '../parse';
|
import { strictParseInt } from '../parse';
|
||||||
import { TileParams } from './types';
|
import { TileParams } from './types';
|
||||||
import { mainRenderer, allTilesets } from './rendererDefinition';
|
import { renderTile, allTilesets } from './rendererDefinition';
|
||||||
import asyncController from '../api/routes/asyncController';
|
import asyncController from '../api/routes/asyncController';
|
||||||
|
|
||||||
const handleTileRequest = asyncController(async function (req: express.Request, res: express.Response) {
|
const handleTileRequest = asyncController(async function (req: express.Request, res: express.Response) {
|
||||||
@ -20,7 +20,7 @@ const handleTileRequest = asyncController(async function (req: express.Request,
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const im = await mainRenderer.getTile(tileParams, dataParams);
|
const im = await renderTile(tileParams, dataParams);
|
||||||
res.writeHead(200, { 'Content-Type': 'image/png' });
|
res.writeHead(200, { 'Content-Type': 'image/png' });
|
||||||
res.end(im);
|
res.end(im);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Image } from 'mapnik';
|
import { Image } from 'mapnik';
|
||||||
|
import { Sharp } from 'sharp';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bounding box in the format [w, s, e, n]
|
* Bounding box in the format [w, s, e, n]
|
||||||
@ -32,12 +33,26 @@ interface TileParams {
|
|||||||
scale: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DataConfig {
|
||||||
|
table: string;
|
||||||
|
geometry_field: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableDefinitionFunction = (tileset: string, dataParams: any) => DataConfig;
|
||||||
|
|
||||||
|
type Tile = Image | Sharp;
|
||||||
|
type RendererFunction = (tileParams: TileParams, dataParams: any) => Promise<Tile>;
|
||||||
|
|
||||||
interface TileRenderer {
|
interface TileRenderer {
|
||||||
getTile(tileParams: TileParams, dataParams: any): Promise<Image>
|
getTile: RendererFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
BoundingBox,
|
BoundingBox,
|
||||||
TileParams,
|
TileParams,
|
||||||
TileRenderer
|
TileRenderer,
|
||||||
|
Tile,
|
||||||
|
RendererFunction,
|
||||||
|
DataConfig,
|
||||||
|
TableDefinitionFunction
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import SphericalMercator from '@mapbox/sphericalmercator';
|
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||||
|
|
||||||
import { TileParams } from './types';
|
import { TileParams, BoundingBox } from './types';
|
||||||
|
|
||||||
const TILE_SIZE = 256;
|
const TILE_SIZE = 256;
|
||||||
|
|
||||||
@ -20,9 +20,15 @@ function formatParams({ tileset, z, x, y, scale }: TileParams): string {
|
|||||||
return `${tileset}/${z}/${x}/${y}@${scale}x`;
|
return `${tileset}/${z}/${x}/${y}@${scale}x`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOutsideExtent({ x, y, z }: TileParams, bbox: BoundingBox) {
|
||||||
|
const xy = getXYZ(bbox, z);
|
||||||
|
return xy.minY > y || xy.maxY < y || xy.minX > x || xy.maxX < x;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
TILE_SIZE,
|
TILE_SIZE,
|
||||||
getBbox,
|
getBbox,
|
||||||
getXYZ,
|
getXYZ,
|
||||||
formatParams
|
formatParams,
|
||||||
|
isOutsideExtent
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user