feat: option to translate labels for static maps
Signed-off-by: Martin d'Allens <martin.dallens@liberty-rider.com>
This commit is contained in:
parent
1b97491739
commit
57931c889d
8 changed files with 294 additions and 24 deletions
|
|
@ -28,7 +28,7 @@ docker build -t tileserver-gl-light .
|
||||||
[Download from OpenMapTiles.com](https://openmaptiles.com/downloads/planet/) or [create](https://github.com/openmaptiles/openmaptiles) your vector tile, and run following in directory contains your *.mbtiles.
|
[Download from OpenMapTiles.com](https://openmaptiles.com/downloads/planet/) or [create](https://github.com/openmaptiles/openmaptiles) your vector tile, and run following in directory contains your *.mbtiles.
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --rm -it -v $(pwd):/data -p 8000:80 tileserver-gl-light
|
docker run --rm -it -v $(pwd):/data -p 8080:8080 tileserver-gl-light
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ Example:
|
||||||
"serveAllStyles": false,
|
"serveAllStyles": false,
|
||||||
"serveStaticMaps": true,
|
"serveStaticMaps": true,
|
||||||
"allowRemoteMarkerIcons": true,
|
"allowRemoteMarkerIcons": true,
|
||||||
|
"languages": ["en", "fr", "it"],
|
||||||
"tileMargin": 0
|
"tileMargin": 0
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
|
|
@ -150,6 +151,13 @@ Allows the rendering of marker icons fetched via http(s) hyperlinks.
|
||||||
For security reasons only allow this if you can control the origins from where the markers are fetched!
|
For security reasons only allow this if you can control the origins from where the markers are fetched!
|
||||||
Default is to disallow fetching of icons from remote sources.
|
Default is to disallow fetching of icons from remote sources.
|
||||||
|
|
||||||
|
``languages``
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Allows translating labels when rendering static maps. This is a list of allowed languages.
|
||||||
|
Note that your vector tile source needs to contain the translated labels (e.g. ``name:en``, ``name:fr``...).
|
||||||
|
Not used by default.
|
||||||
|
|
||||||
``styles``
|
``styles``
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,13 @@ WMTS Capabilities
|
||||||
|
|
||||||
Static images
|
Static images
|
||||||
=============
|
=============
|
||||||
* Several endpoints:
|
* There are three endpoints depending on how you want to define the map location:
|
||||||
|
|
||||||
* ``/styles/{id}/static/{lon},{lat},{zoom}[@{bearing}[,{pitch}]]/{width}x{height}[@2x].{format}`` (center-based)
|
* Center and zoom level: ``/styles/{id}/static/{lon},{lat},{zoom}[@{bearing}[,{pitch}]]/{width}x{height}[@2x].{format}``
|
||||||
* ``/styles/{id}/static/{minx},{miny},{maxx},{maxy}/{width}x{height}[@2x].{format}`` (area-based)
|
* Bounds, e.g. latitude/longitude of corners: ``/styles/{id}/static/{minx},{miny},{maxx},{maxy}/{width}x{height}[@2x].{format}``
|
||||||
* ``/styles/{id}/static/auto/{width}x{height}[@2x].{format}`` (autofit path -- see below)
|
* Autofit to a path: ``/styles/{id}/static/auto/{width}x{height}[@2x].{format}``
|
||||||
|
|
||||||
* All the static image endpoints additionally support following query parameters:
|
* All these endpoints accept these query parameters:
|
||||||
|
|
||||||
* ``path`` - ``((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))``
|
* ``path`` - ``((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))``
|
||||||
|
|
||||||
|
|
@ -84,6 +84,7 @@ Static images
|
||||||
* value of ``0.1`` means "add 10% size to each side to make sure the area of interest is nicely visible"
|
* value of ``0.1`` means "add 10% size to each side to make sure the area of interest is nicely visible"
|
||||||
|
|
||||||
* ``maxzoom`` - Maximum zoom level (only for auto endpoint where zoom level is calculated and not provided)
|
* ``maxzoom`` - Maximum zoom level (only for auto endpoint where zoom level is calculated and not provided)
|
||||||
|
* ``language`` - Language code to translate all labels from the selected style. If no language is set or if the language is not found in ``options.languages``, no translation happens.
|
||||||
|
|
||||||
* You can also use (experimental) ``/styles/{id}/static/raw/...`` endpoints with raw spherical mercator coordinates (EPSG:3857) instead of WGS84.
|
* You can also use (experimental) ``/styles/{id}/static/raw/...`` endpoints with raw spherical mercator coordinates (EPSG:3857) instead of WGS84.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@
|
||||||
{{#if serving_rendered}}
|
{{#if serving_rendered}}
|
||||||
| <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a>
|
| <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if serving_rendered}}
|
||||||
|
| <a href="{{public_url}}styles/{{@key}}/static/{{static_center}}/500x400@2x.png{{&../key_query}}">Static</a>
|
||||||
|
{{/if}}
|
||||||
{{#if xyz_link}}
|
{{#if xyz_link}}
|
||||||
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>
|
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>
|
||||||
<input id="xyz_style_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
|
<input id="xyz_style_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import polyline from '@mapbox/polyline';
|
||||||
import proj4 from 'proj4';
|
import proj4 from 'proj4';
|
||||||
import request from 'request';
|
import request from 'request';
|
||||||
import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
|
import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
|
||||||
|
import translateLayers from './translate_layers.js';
|
||||||
|
|
||||||
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
|
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
|
||||||
const PATH_PATTERN =
|
const PATH_PATTERN =
|
||||||
|
|
@ -202,6 +203,19 @@ const extractPathsFromQuery = (query, transformer) => {
|
||||||
return paths;
|
return paths;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses language provided via query.
|
||||||
|
* @param {object} query Request query parameters.
|
||||||
|
* @param {object} options Configuration options.
|
||||||
|
*/
|
||||||
|
const extractLanguageFromQuery = (query, options) => {
|
||||||
|
const languages = options.languages || [];
|
||||||
|
if ('language' in query && languages.includes(query.language)) {
|
||||||
|
return query.language;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses marker options provided via query and sets corresponding attributes
|
* Parses marker options provided via query and sets corresponding attributes
|
||||||
* on marker object.
|
* on marker object.
|
||||||
|
|
@ -330,7 +344,7 @@ const precisePx = (ll, zoom) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws a marker in cavans context.
|
* Draws a marker in canvas context.
|
||||||
* @param {object} ctx Canvas context object.
|
* @param {object} ctx Canvas context object.
|
||||||
* @param {object} marker Marker object parsed by extractMarkersFromQuery.
|
* @param {object} marker Marker object parsed by extractMarkersFromQuery.
|
||||||
* @param {number} z Map zoom level.
|
* @param {number} z Map zoom level.
|
||||||
|
|
@ -646,6 +660,7 @@ export const serve_rendered = {
|
||||||
next,
|
next,
|
||||||
opt_overlay,
|
opt_overlay,
|
||||||
opt_mode = 'tile',
|
opt_mode = 'tile',
|
||||||
|
language = null,
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
Math.abs(lon) > 180 ||
|
Math.abs(lon) > 180 ||
|
||||||
|
|
@ -677,7 +692,7 @@ export const serve_rendered = {
|
||||||
if (opt_mode === 'tile' && tileMargin === 0) {
|
if (opt_mode === 'tile' && tileMargin === 0) {
|
||||||
pool = item.map.renderers[scale];
|
pool = item.map.renderers[scale];
|
||||||
} else {
|
} else {
|
||||||
pool = item.map.renderers_static[scale];
|
pool = item.map.renderers_static[`${scale}${language}`];
|
||||||
}
|
}
|
||||||
pool.acquire((err, renderer) => {
|
pool.acquire((err, renderer) => {
|
||||||
const mlglZ = Math.max(0, z - 1);
|
const mlglZ = Math.max(0, z - 1);
|
||||||
|
|
@ -712,6 +727,7 @@ export const serve_rendered = {
|
||||||
|
|
||||||
// Fix semi-transparent outlines on raw, premultiplied input
|
// Fix semi-transparent outlines on raw, premultiplied input
|
||||||
// https://github.com/maptiler/tileserver-gl/issues/350#issuecomment-477857040
|
// https://github.com/maptiler/tileserver-gl/issues/350#issuecomment-477857040
|
||||||
|
// FIXME: unnecessay now? https://github.com/lovell/sharp/issues/1599#issuecomment-837004081
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
const alpha = data[i + 3];
|
const alpha = data[i + 3];
|
||||||
const norm = alpha / 255;
|
const norm = alpha / 255;
|
||||||
|
|
@ -901,6 +917,7 @@ export const serve_rendered = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const paths = extractPathsFromQuery(req.query, transformer);
|
const paths = extractPathsFromQuery(req.query, transformer);
|
||||||
|
const language = extractLanguageFromQuery(req.query, options);
|
||||||
const markers = extractMarkersFromQuery(
|
const markers = extractMarkersFromQuery(
|
||||||
req.query,
|
req.query,
|
||||||
options,
|
options,
|
||||||
|
|
@ -935,6 +952,7 @@ export const serve_rendered = {
|
||||||
next,
|
next,
|
||||||
overlay,
|
overlay,
|
||||||
'static',
|
'static',
|
||||||
|
language,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
|
|
@ -983,6 +1001,7 @@ export const serve_rendered = {
|
||||||
const pitch = 0;
|
const pitch = 0;
|
||||||
|
|
||||||
const paths = extractPathsFromQuery(req.query, transformer);
|
const paths = extractPathsFromQuery(req.query, transformer);
|
||||||
|
const language = extractLanguageFromQuery(req.query, options);
|
||||||
const markers = extractMarkersFromQuery(
|
const markers = extractMarkersFromQuery(
|
||||||
req.query,
|
req.query,
|
||||||
options,
|
options,
|
||||||
|
|
@ -1016,6 +1035,7 @@ export const serve_rendered = {
|
||||||
next,
|
next,
|
||||||
overlay,
|
overlay,
|
||||||
'static',
|
'static',
|
||||||
|
language,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
|
|
@ -1077,6 +1097,7 @@ export const serve_rendered = {
|
||||||
: item.dataProjWGStoInternalWGS;
|
: item.dataProjWGStoInternalWGS;
|
||||||
|
|
||||||
const paths = extractPathsFromQuery(req.query, transformer);
|
const paths = extractPathsFromQuery(req.query, transformer);
|
||||||
|
const language = extractLanguageFromQuery(req.query, options);
|
||||||
const markers = extractMarkersFromQuery(
|
const markers = extractMarkersFromQuery(
|
||||||
req.query,
|
req.query,
|
||||||
options,
|
options,
|
||||||
|
|
@ -1150,6 +1171,7 @@ export const serve_rendered = {
|
||||||
next,
|
next,
|
||||||
overlay,
|
overlay,
|
||||||
'static',
|
'static',
|
||||||
|
language,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
|
|
@ -1184,14 +1206,16 @@ export const serve_rendered = {
|
||||||
};
|
};
|
||||||
|
|
||||||
let styleJSON;
|
let styleJSON;
|
||||||
const createPool = (ratio, mode, min, max) => {
|
const createPool = (scale, mode, min, max, language) => {
|
||||||
const createRenderer = (ratio, createCallback) => {
|
const createRenderer = (createCallback) => {
|
||||||
const renderer = new mlgl.Map({
|
const renderer = new mlgl.Map({
|
||||||
mode: mode,
|
mode: mode,
|
||||||
ratio: ratio,
|
ratio: scale,
|
||||||
request: (req, callback) => {
|
request: (req, callback) => {
|
||||||
const protocol = req.url.split(':')[0];
|
const protocol = req.url.split(':')[0];
|
||||||
// console.log('Handling request:', req);
|
if (options.verbose) {
|
||||||
|
console.log('[VERBOSE] Handling request:', req);
|
||||||
|
}
|
||||||
if (protocol === 'sprites') {
|
if (protocol === 'sprites') {
|
||||||
const dir = options.paths[protocol];
|
const dir = options.paths[protocol];
|
||||||
const file = unescape(req.url).substring(protocol.length + 3);
|
const file = unescape(req.url).substring(protocol.length + 3);
|
||||||
|
|
@ -1282,7 +1306,9 @@ export const serve_rendered = {
|
||||||
const extension = path.extname(parts.pathname).toLowerCase();
|
const extension = path.extname(parts.pathname).toLowerCase();
|
||||||
const format = extensionToFormat[extension] || '';
|
const format = extensionToFormat[extension] || '';
|
||||||
if (err || res.statusCode < 200 || res.statusCode >= 300) {
|
if (err || res.statusCode < 200 || res.statusCode >= 300) {
|
||||||
// console.log('HTTP error', err || res.statusCode);
|
if (options.verbose) {
|
||||||
|
console.log('HTTP error', err || res.statusCode);
|
||||||
|
}
|
||||||
createEmptyResponse(format, '', callback);
|
createEmptyResponse(format, '', callback);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1305,13 +1331,37 @@ export const serve_rendered = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
renderer.load(styleJSON);
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log('[VERBOSE] createRenderer', scale, mode, language);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rendererStyle = styleJSON;
|
||||||
|
if (language) {
|
||||||
|
const layers = [...styleJSON.layers];
|
||||||
|
rendererStyle = { ...rendererStyle, layers };
|
||||||
|
|
||||||
|
const translator = {
|
||||||
|
getLayoutProperty: (id, key) =>
|
||||||
|
layers.find((layer) => layer.id === id).layout[key],
|
||||||
|
setLayoutProperty: (id, key, value) => {
|
||||||
|
const i = layers.findIndex((layer) => layer.id === id);
|
||||||
|
const layer = { ...layers[i] };
|
||||||
|
layers[i] = layer;
|
||||||
|
layer.layout = { ...layer.layout, [key]: value };
|
||||||
|
},
|
||||||
|
translateLayers,
|
||||||
|
};
|
||||||
|
translator.translateLayers(layers, language);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.load(rendererStyle);
|
||||||
createCallback(null, renderer);
|
createCallback(null, renderer);
|
||||||
};
|
};
|
||||||
return new advancedPool.Pool({
|
return new advancedPool.Pool({
|
||||||
min: min,
|
min: min,
|
||||||
max: max,
|
max: max,
|
||||||
create: createRenderer.bind(null, ratio),
|
create: createRenderer.bind(null),
|
||||||
destroy: (renderer) => {
|
destroy: (renderer) => {
|
||||||
renderer.release();
|
renderer.release();
|
||||||
},
|
},
|
||||||
|
|
@ -1471,14 +1521,19 @@ export const serve_rendered = {
|
||||||
const j = Math.min(maxPoolSizes.length - 1, s - 1);
|
const j = Math.min(maxPoolSizes.length - 1, s - 1);
|
||||||
const minPoolSize = minPoolSizes[i];
|
const minPoolSize = minPoolSizes[i];
|
||||||
const maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]);
|
const maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]);
|
||||||
map.renderers[s] = createPool(s, 'tile', minPoolSize, maxPoolSize);
|
const languages = new Set([null, ...(options.languages || [])]);
|
||||||
map.renderers_static[s] = createPool(
|
|
||||||
|
map.renderers[s] = createPool(s, 'tile', minPoolSize, maxPoolSize, null);
|
||||||
|
for (const language of languages) {
|
||||||
|
map.renderers_static[`${s}${language}`] = createPool(
|
||||||
s,
|
s,
|
||||||
'static',
|
'static',
|
||||||
minPoolSize,
|
language ? 1 : minPoolSize,
|
||||||
maxPoolSize,
|
language ? 1 : maxPoolSize,
|
||||||
|
language,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([renderersReadyPromise]);
|
return Promise.all([renderersReadyPromise]);
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ function start(opts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = config.options || {};
|
const options = config.options || {};
|
||||||
|
options.verbose = opts.verbose;
|
||||||
const paths = options.paths || {};
|
const paths = options.paths || {};
|
||||||
options.paths = paths;
|
options.paths = paths;
|
||||||
paths.root = path.resolve(
|
paths.root = path.resolve(
|
||||||
|
|
@ -441,6 +442,10 @@ function start(opts) {
|
||||||
style.thumbnail = `${center[2]}/${Math.floor(
|
style.thumbnail = `${center[2]}/${Math.floor(
|
||||||
centerPx[0] / 256,
|
centerPx[0] / 256,
|
||||||
)}/${Math.floor(centerPx[1] / 256)}.png`;
|
)}/${Math.floor(centerPx[1] / 256)}.png`;
|
||||||
|
|
||||||
|
style.static_center = `${center[0].toFixed(5)},${center[1].toFixed(
|
||||||
|
5,
|
||||||
|
)},${center[2]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
style.xyz_link = getTileUrls(
|
style.xyz_link = getTileUrls(
|
||||||
|
|
|
||||||
186
src/translate_layers.js
Normal file
186
src/translate_layers.js
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
|
||||||
|
File copied from https://github.com/maptiler/maptiler-sdk-js/blob/c5c7a343dcf4083a5eca75ede319991b80fcb652/src/Map.ts#L579
|
||||||
|
|
||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2022, MapTiler
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
export default function translateLayers(layers, language) {
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// detects pattern like "{name:somelanguage}" with loose spacing
|
||||||
|
const strLanguageRegex = /^\s*{\s*name\s*(:\s*(\S*))?\s*}$/;
|
||||||
|
|
||||||
|
// detects pattern like "name:somelanguage" with loose spacing
|
||||||
|
const strLanguageInArrayRegex = /^\s*name\s*(:\s*(\S*))?\s*$/;
|
||||||
|
|
||||||
|
// for string based bilingual lang such as "{name:latin} {name:nonlatin}" or "{name:latin} {name}"
|
||||||
|
const strBilingualRegex =
|
||||||
|
/^\s*{\s*name\s*(:\s*(\S*))?\s*}(\s*){\s*name\s*(:\s*(\S*))?\s*}$/;
|
||||||
|
|
||||||
|
// Regex to capture when there are more info, such as mountains elevation with unit m/ft
|
||||||
|
const strMoreInfoRegex = /^(.*)({\s*name\s*(:\s*(\S*))?\s*})(.*)$/;
|
||||||
|
|
||||||
|
const langStr = language ? `name:${language}` : "name"; // to handle local lang
|
||||||
|
const replacer = [
|
||||||
|
"case",
|
||||||
|
["has", langStr],
|
||||||
|
["get", langStr],
|
||||||
|
["get", "name"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < layers.length; i += 1) {
|
||||||
|
const layer = layers[i];
|
||||||
|
const layout = layer.layout;
|
||||||
|
|
||||||
|
if (!layout) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!layout["text-field"]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textFieldLayoutProp = this.getLayoutProperty(
|
||||||
|
layer.id,
|
||||||
|
"text-field"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note:
|
||||||
|
// The value of the 'text-field' property can take multiple shape;
|
||||||
|
// 1. can be an array with 'concat' on its first element (most likely means bilingual)
|
||||||
|
// 2. can be an array with 'get' on its first element (monolingual)
|
||||||
|
// 3. can be a string of shape '{name:latin}'
|
||||||
|
// 4. can be a string referencing another prop such as '{housenumber}' or '{ref}'
|
||||||
|
//
|
||||||
|
// The case 1, 2 and 3 will be updated while maintaining their original type and shape.
|
||||||
|
// The case 3 will not be updated
|
||||||
|
|
||||||
|
let regexMatch;
|
||||||
|
|
||||||
|
// This is case 1
|
||||||
|
if (
|
||||||
|
Array.isArray(textFieldLayoutProp) &&
|
||||||
|
textFieldLayoutProp.length >= 2 &&
|
||||||
|
textFieldLayoutProp[0].trim().toLowerCase() === "concat"
|
||||||
|
) {
|
||||||
|
const newProp = textFieldLayoutProp.slice(); // newProp is Array
|
||||||
|
// The style could possibly have defined more than 2 concatenated language strings but we only want to edit the first
|
||||||
|
// The style could also define that there are more things being concatenated and not only languages
|
||||||
|
|
||||||
|
for (let j = 0; j < textFieldLayoutProp.length; j += 1) {
|
||||||
|
const elem = textFieldLayoutProp[j];
|
||||||
|
|
||||||
|
// we are looking for an elem of shape '{name:somelangage}' (string) of `["get", "name:somelanguage"]` (array)
|
||||||
|
|
||||||
|
// the entry of of shape '{name:somelangage}', possibly with loose spacing
|
||||||
|
if (
|
||||||
|
(typeof elem === "string" || elem instanceof String) &&
|
||||||
|
strLanguageRegex.exec(elem.toString())
|
||||||
|
) {
|
||||||
|
newProp[j] = replacer;
|
||||||
|
break; // we just want to update the primary language
|
||||||
|
}
|
||||||
|
// the entry is of an array of shape `["get", "name:somelanguage"]`
|
||||||
|
else if (
|
||||||
|
Array.isArray(elem) &&
|
||||||
|
elem.length >= 2 &&
|
||||||
|
elem[0].trim().toLowerCase() === "get" &&
|
||||||
|
strLanguageInArrayRegex.exec(elem[1].toString())
|
||||||
|
) {
|
||||||
|
newProp[j] = replacer;
|
||||||
|
break; // we just want to update the primary language
|
||||||
|
} else if (
|
||||||
|
Array.isArray(elem) &&
|
||||||
|
elem.length === 4 &&
|
||||||
|
elem[0].trim().toLowerCase() === "case"
|
||||||
|
) {
|
||||||
|
newProp[j] = replacer;
|
||||||
|
break; // we just want to update the primary language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setLayoutProperty(layer.id, "text-field", newProp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is case 2
|
||||||
|
else if (
|
||||||
|
Array.isArray(textFieldLayoutProp) &&
|
||||||
|
textFieldLayoutProp.length >= 2 &&
|
||||||
|
textFieldLayoutProp[0].trim().toLowerCase() === "get" &&
|
||||||
|
strLanguageInArrayRegex.exec(textFieldLayoutProp[1].toString())
|
||||||
|
) {
|
||||||
|
const newProp = replacer;
|
||||||
|
this.setLayoutProperty(layer.id, "text-field", newProp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is case 3
|
||||||
|
else if (
|
||||||
|
(typeof textFieldLayoutProp === "string" ||
|
||||||
|
textFieldLayoutProp instanceof String) &&
|
||||||
|
strLanguageRegex.exec(textFieldLayoutProp.toString())
|
||||||
|
) {
|
||||||
|
const newProp = replacer;
|
||||||
|
this.setLayoutProperty(layer.id, "text-field", newProp);
|
||||||
|
} else if (
|
||||||
|
Array.isArray(textFieldLayoutProp) &&
|
||||||
|
textFieldLayoutProp.length === 4 &&
|
||||||
|
textFieldLayoutProp[0].trim().toLowerCase() === "case"
|
||||||
|
) {
|
||||||
|
const newProp = replacer;
|
||||||
|
this.setLayoutProperty(layer.id, "text-field", newProp);
|
||||||
|
} else if (
|
||||||
|
(typeof textFieldLayoutProp === "string" ||
|
||||||
|
textFieldLayoutProp instanceof String) &&
|
||||||
|
(regexMatch = strBilingualRegex.exec(
|
||||||
|
textFieldLayoutProp.toString()
|
||||||
|
)) !== null
|
||||||
|
) {
|
||||||
|
const newProp = `{${langStr}}${regexMatch[3]}{name${
|
||||||
|
regexMatch[4] || ""
|
||||||
|
}}`;
|
||||||
|
this.setLayoutProperty(layer.id, "text-field", newProp);
|
||||||
|
} else if (
|
||||||
|
(typeof textFieldLayoutProp === "string" ||
|
||||||
|
textFieldLayoutProp instanceof String) &&
|
||||||
|
(regexMatch = strMoreInfoRegex.exec(
|
||||||
|
textFieldLayoutProp.toString()
|
||||||
|
)) !== null
|
||||||
|
) {
|
||||||
|
const newProp = `${regexMatch[1]}{${langStr}}${regexMatch[5]}`;
|
||||||
|
this.setLayoutProperty(layer.id, "text-field", newProp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-enable */
|
||||||
|
}
|
||||||
|
|
@ -128,6 +128,18 @@ describe('Static endpoints', function () {
|
||||||
|
|
||||||
testStatic(prefix, '-280,-80,0,80/280x160', 'png', 200);
|
testStatic(prefix, '-280,-80,0,80/280x160', 'png', 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ignores an unknown language gracefully', function () {
|
||||||
|
testStatic(
|
||||||
|
prefix,
|
||||||
|
'-180,-90,180,90/20x20',
|
||||||
|
'png',
|
||||||
|
200,
|
||||||
|
2,
|
||||||
|
undefined,
|
||||||
|
'?language=kr',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalid requests return 4xx', function () {
|
describe('invalid requests return 4xx', function () {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue