Merge master

This commit is contained in:
pietervdvn 2021-10-11 22:31:05 +02:00
commit 5e5528eb7d
13 changed files with 160 additions and 126 deletions

View file

@ -489,7 +489,7 @@ export class InitUiElements {
} }
return true return true
}, [State.state.currentBounds] }, [State.state.currentBounds, source.layer.isDisplayed]
) )
new ShowDataLayer( new ShowDataLayer(

View file

@ -38,8 +38,7 @@ export default class OverpassFeatureSource implements FeatureSource {
readonly currentBounds: UIEventSource<BBox> readonly currentBounds: UIEventSource<BBox>
} }
private readonly _isActive: UIEventSource<boolean>; private readonly _isActive: UIEventSource<boolean>;
private readonly onBboxLoaded: (bbox: BBox, date: Date, layers: LayerConfig[]) => void; private readonly onBboxLoaded: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void;
constructor( constructor(
state: { state: {
readonly locationControl: UIEventSource<Loc>, readonly locationControl: UIEventSource<Loc>,
@ -49,10 +48,11 @@ export default class OverpassFeatureSource implements FeatureSource {
readonly overpassMaxZoom: UIEventSource<number>, readonly overpassMaxZoom: UIEventSource<number>,
readonly currentBounds: UIEventSource<BBox> readonly currentBounds: UIEventSource<BBox>
}, },
options?: { options: {
padToTiles: UIEventSource<number>,
isActive?: UIEventSource<boolean>, isActive?: UIEventSource<boolean>,
relationTracker: RelationsTracker, relationTracker: RelationsTracker,
onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[]) => void onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void
}) { }) {
this.state = state this.state = state
@ -61,7 +61,7 @@ export default class OverpassFeatureSource implements FeatureSource {
this.relationsTracker = options.relationTracker this.relationsTracker = options.relationTracker
const self = this; const self = this;
state.currentBounds.addCallback(_ => { state.currentBounds.addCallback(_ => {
self.update() self.update(options.padToTiles.data)
}) })
} }
@ -84,21 +84,21 @@ export default class OverpassFeatureSource implements FeatureSource {
return new Overpass(new Or(filters), extraScripts, interpreterUrl, this.state.overpassTimeout, this.relationsTracker); return new Overpass(new Or(filters), extraScripts, interpreterUrl, this.state.overpassTimeout, this.relationsTracker);
} }
private update() { private update(paddedZoomLevel: number) {
if (!this._isActive.data) { if (!this._isActive.data) {
return; return;
} }
const self = this; const self = this;
this.updateAsync().then(bboxDate => { this.updateAsync(paddedZoomLevel).then(bboxDate => {
if(bboxDate === undefined || self.onBboxLoaded === undefined){ if(bboxDate === undefined || self.onBboxLoaded === undefined){
return; return;
} }
const [bbox, date, layers] = bboxDate const [bbox, date, layers] = bboxDate
self.onBboxLoaded(bbox, date, layers) self.onBboxLoaded(bbox, date, layers, paddedZoomLevel)
}) })
} }
private async updateAsync(): Promise<[BBox, Date, LayerConfig[]]> { private async updateAsync(padToZoomLevel: number): Promise<[BBox, Date, LayerConfig[]]> {
if (this.runningQuery.data) { if (this.runningQuery.data) {
console.log("Still running a query, not updating"); console.log("Still running a query, not updating");
return undefined; return undefined;
@ -109,7 +109,7 @@ export default class OverpassFeatureSource implements FeatureSource {
return undefined; return undefined;
} }
const bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(14); const bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(padToZoomLevel);
if (bounds === undefined) { if (bounds === undefined) {
return undefined; return undefined;

View file

@ -116,16 +116,15 @@ export class BBox {
return this.minLat return this.minLat
} }
pad(factor: number): BBox { pad(factor: number, maxIncrease = 2): BBox {
const latDiff = this.maxLat - this.minLat
const lat = (this.maxLat + this.minLat) / 2 const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor)
const lonDiff = this.maxLon - this.minLon const lonDiff =Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor)
const lon = (this.maxLon + this.minLon) / 2
return new BBox([[ return new BBox([[
lon - lonDiff * factor, this.minLon - lonDiff,
lat - latDiff * factor this.minLat - latDiff
], [lon + lonDiff * factor, ], [this.maxLon + lonDiff,
lat + latDiff * factor]]) this.maxLat + latDiff]])
} }
toLeaflet() { toLeaflet() {

View file

@ -8,7 +8,7 @@ import SimpleMetaTagger from "../../SimpleMetaTagger";
export default class SaveTileToLocalStorageActor { export default class SaveTileToLocalStorageActor {
public static readonly storageKey: string = "cached-features"; public static readonly storageKey: string = "cached-features";
public static readonly formatVersion: string = "1" public static readonly formatVersion: string = "2"
constructor(source: FeatureSourceForLayer, tileIndex: number) { constructor(source: FeatureSourceForLayer, tileIndex: number) {
@ -37,6 +37,5 @@ export default class SaveTileToLocalStorageActor {
}catch(e){ }catch(e){
console.error("Could not mark tile ", key, "as visited") console.error("Could not mark tile ", key, "as visited")
} }
} }
} }

View file

@ -58,7 +58,7 @@ export default class FeaturePipeline {
private readonly freshnesses = new Map<string, TileFreshnessCalculator>(); private readonly freshnesses = new Map<string, TileFreshnessCalculator>();
private readonly oldestAllowedDate: Date = new Date(new Date().getTime() - 60 * 60 * 24 * 30 * 1000); private readonly oldestAllowedDate: Date = new Date(new Date().getTime() - 60 * 60 * 24 * 30 * 1000);
private readonly osmSourceZoomLevel = 14 private readonly osmSourceZoomLevel = 15
constructor( constructor(
handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
@ -147,7 +147,7 @@ export default class FeaturePipeline {
// We split them up into tiles anyway as it is an OSM source // We split them up into tiles anyway as it is an OSM source
TiledFeatureSource.createHierarchy(src, { TiledFeatureSource.createHierarchy(src, {
layer: src.layer, layer: src.layer,
minZoomLevel: 14, minZoomLevel: this.osmSourceZoomLevel,
dontEnforceMinZoom: true, dontEnforceMinZoom: true,
registerTile: (tile) => { registerTile: (tile) => {
new RegisteringAllFromFeatureSourceActor(tile) new RegisteringAllFromFeatureSourceActor(tile)
@ -155,7 +155,7 @@ export default class FeaturePipeline {
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
} }
}) })
}else{ } else {
new RegisteringAllFromFeatureSourceActor(src) new RegisteringAllFromFeatureSourceActor(src)
perLayerHierarchy.get(id).registerTile(src) perLayerHierarchy.get(id).registerTile(src)
src.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(src)) src.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(src))
@ -200,7 +200,7 @@ export default class FeaturePipeline {
new PerLayerFeatureSourceSplitter(state.filteredLayers, new PerLayerFeatureSourceSplitter(state.filteredLayers,
(source) => TiledFeatureSource.createHierarchy(source, { (source) => TiledFeatureSource.createHierarchy(source, {
layer: source.layer, layer: source.layer,
minZoomLevel: 14, minZoomLevel: source.layer.layerDef.minzoom,
dontEnforceMinZoom: true, dontEnforceMinZoom: true,
maxFeatureCount: state.layoutToUse.clustering.minNeededElements, maxFeatureCount: state.layoutToUse.clustering.minNeededElements,
maxZoomLevel: state.layoutToUse.clustering.maxZoom, maxZoomLevel: state.layoutToUse.clustering.maxZoom,
@ -235,7 +235,7 @@ export default class FeaturePipeline {
// Whenever fresh data comes in, we need to update the metatagging // Whenever fresh data comes in, we need to update the metatagging
self.newDataLoadedSignal.stabilized(1000).addCallback(src => { self.newDataLoadedSignal.stabilized(1000).addCallback(_ => {
self.updateAllMetaTagging() self.updateAllMetaTagging()
}) })
@ -276,15 +276,15 @@ export default class FeaturePipeline {
const self = this const self = this
return this.state.currentBounds.map(bbox => { return this.state.currentBounds.map(bbox => {
if (bbox === undefined) { if (bbox === undefined) {
return return undefined
} }
if (!isSufficientlyZoomed.data) { if (!isSufficientlyZoomed.data) {
return; return undefined;
} }
const osmSourceZoomLevel = self.osmSourceZoomLevel const osmSourceZoomLevel = self.osmSourceZoomLevel
const range = bbox.containingTileRange(osmSourceZoomLevel) const range = bbox.containingTileRange(osmSourceZoomLevel)
const tileIndexes = [] const tileIndexes = []
if (range.total > 100) { if (range.total >= 100) {
// Too much tiles! // Too much tiles!
return [] return []
} }
@ -294,7 +294,7 @@ export default class FeaturePipeline {
if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) { if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
console.debug("Skipping tile", osmSourceZoomLevel, x, y, "as a decently fresh one is available") console.debug("Skipping tile", osmSourceZoomLevel, x, y, "as a decently fresh one is available")
// The cached tiles contain decently fresh data // The cached tiles contain decently fresh data
return; return undefined;
} }
tileIndexes.push(i) tileIndexes.push(i)
}) })
@ -327,28 +327,30 @@ export default class FeaturePipeline {
} }
const range = bbox.containingTileRange(zoom) const range = bbox.containingTileRange(zoom)
if (range.total > 100) { if (range.total >= 5000) {
return false return false
} }
const self = this; const self = this;
const allFreshnesses = Tiles.MapRange(range, (x, y) => self.freshnessForVisibleLayers(zoom, x, y)) const allFreshnesses = Tiles.MapRange(range, (x, y) => self.freshnessForVisibleLayers(zoom, x, y))
return allFreshnesses.some(freshness => freshness === undefined || freshness < this.oldestAllowedDate) return allFreshnesses.some(freshness => freshness === undefined || freshness < this.oldestAllowedDate)
}, [state.locationControl]) }, [state.locationControl])
const self = this; const self = this;
const updater = new OverpassFeatureSource(state, const updater = new OverpassFeatureSource(state,
{ {
padToTiles: state.locationControl.map(l => Math.min(15, l.zoom + 1)),
relationTracker: this.relationTracker, relationTracker: this.relationTracker,
isActive: useOsmApi.map(b => !b && overpassIsActive.data, [overpassIsActive]), isActive: useOsmApi.map(b => !b && overpassIsActive.data, [overpassIsActive]),
onBboxLoaded: ((bbox, date, downloadedLayers) => { onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => {
Tiles.MapRange(bbox.containingTileRange(self.osmSourceZoomLevel), (x, y) => { Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => {
const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y)
downloadedLayers.forEach(layer => { downloadedLayers.forEach(layer => {
SaveTileToLocalStorageActor.MarkVisited(layer.id, Tiles.tile_index(this.osmSourceZoomLevel, x, y), date) self.freshnesses.get(layer.id).addTileLoad(tileIndex, date)
SaveTileToLocalStorageActor.MarkVisited(layer.id, tileIndex, date)
}) })
}) })
}) }
}); });

View file

@ -1,14 +1,16 @@
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc";
import TileHierarchy from "./TileHierarchy"; import TileHierarchy from "./TileHierarchy";
import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor"; import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor";
import {Tiles} from "../../../Models/TileRange"; import {Tiles} from "../../../Models/TileRange";
import {BBox} from "../../BBox"; import {BBox} from "../../BBox";
export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> { export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
public loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>(); public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
private readonly layer: FilteredLayer;
private readonly handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void;
private readonly undefinedTiles: Set<number>;
public static GetFreshnesses(layerId: string): Map<number, Date> { public static GetFreshnesses(layerId: string): Map<number, Date> {
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layerId + "-" const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layerId + "-"
@ -29,14 +31,15 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
constructor(layer: FilteredLayer, constructor(layer: FilteredLayer,
handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void, handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void,
state: { state: {
locationControl: UIEventSource<Loc> currentBounds: UIEventSource<BBox>
leafletMap: any
}) { }) {
this.layer = layer;
this.handleFeatureSource = handleFeatureSource;
const undefinedTiles = new Set<number>()
this.undefinedTiles = new Set<number>()
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-"
// @ts-ignore const knownTiles: number[] = Object.keys(localStorage)
const indexes: number[] = Object.keys(localStorage)
.filter(key => { .filter(key => {
return key.startsWith(prefix) && !key.endsWith("-time") && !key.endsWith("-format"); return key.startsWith(prefix) && !key.endsWith("-time") && !key.endsWith("-format");
}) })
@ -45,8 +48,8 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
}) })
.filter(i => !isNaN(i)) .filter(i => !isNaN(i))
console.debug("Layer", layer.layerDef.id, "has following tiles in available in localstorage", indexes.map(i => Tiles.tile_from_index(i).join("/")).join(", ")) console.debug("Layer", layer.layerDef.id, "has following tiles in available in localstorage", knownTiles.map(i => Tiles.tile_from_index(i).join("/")).join(", "))
for (const index of indexes) { for (const index of knownTiles) {
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" + index; const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" + index;
const version = localStorage.getItem(prefix + "-format") const version = localStorage.getItem(prefix + "-format")
@ -55,78 +58,54 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
localStorage.removeItem(prefix) localStorage.removeItem(prefix)
localStorage.removeItem(prefix+"-time") localStorage.removeItem(prefix+"-time")
localStorage.removeItem(prefix+"-format") localStorage.removeItem(prefix+"-format")
undefinedTiles.add(index) this. undefinedTiles.add(index)
console.log("Dropped old format tile", prefix) console.log("Dropped old format tile", prefix)
} }
} }
const zLevels = indexes.map(i => i % 100) const self = this
const indexesSet = new Set(indexes) state.currentBounds.map(bounds => {
const maxZoom = Math.max(...zLevels)
const minZoom = Math.min(...zLevels)
const self = this;
const neededTiles = state.locationControl.map( if(bounds === undefined){
location => { return;
if (!layer.isDisplayed.data) {
// No need to download! - the layer is disabled
return undefined;
}
if (location.zoom < layer.layerDef.minzoom) {
// No need to download! - the layer is disabled
return undefined;
}
// Yup, this is cheating to just get the bounds here
const bounds = state.leafletMap.data?.getBounds()
if (bounds === undefined) {
// We'll retry later
return undefined
}
const needed = []
for (let z = minZoom; z <= maxZoom; z++) {
const tileRange = Tiles.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
const neededZ = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(z, x, y))
.filter(i => !self.loadedTiles.has(i) && !undefinedTiles.has(i) && indexesSet.has(i))
needed.push(...neededZ)
}
if (needed.length === 0) {
return undefined
}
return needed
} }
, [layer.isDisplayed, state.leafletMap]).stabilized(50); for (const knownTile of knownTiles) {
neededTiles.addCallbackAndRunD(neededIndexes => { if(this.loadedTiles.has(knownTile)){
for (const neededIndex of neededIndexes) { continue;
// We load the features from localStorage
try {
const key = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" + neededIndex
const data = localStorage.getItem(key)
const features = JSON.parse(data)
const src = {
layer: layer,
features: new UIEventSource<{ feature: any; freshness: Date }[]>(features),
name: "FromLocalStorage(" + key + ")",
tileIndex: neededIndex,
bbox: BBox.fromTileIndex(neededIndex)
}
handleFeatureSource(src, neededIndex)
self.loadedTiles.set(neededIndex, src)
} catch (e) {
console.error("Could not load data tile from local storage due to", e)
undefinedTiles.add(neededIndex)
} }
if(this.undefinedTiles.has(knownTile)){
continue;
}
if(!bounds.overlapsWith(BBox.fromTileIndex(knownTile))){
continue;
}
self.loadTile(knownTile)
} }
}) })
} }
private loadTile( neededIndex: number){
try {
const key = SaveTileToLocalStorageActor.storageKey + "-" + this.layer.layerDef.id + "-" + neededIndex
const data = localStorage.getItem(key)
const features = JSON.parse(data)
const src = {
layer: this.layer,
features: new UIEventSource<{ feature: any; freshness: Date }[]>(features),
name: "FromLocalStorage(" + key + ")",
tileIndex: neededIndex,
bbox: BBox.fromTileIndex(neededIndex)
}
this.handleFeatureSource(src, neededIndex)
this.loadedTiles.set(neededIndex, src)
} catch (e) {
console.error("Could not load data tile from local storage due to", e)
this.undefinedTiles.add(neededIndex)
}
}
} }

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants { export default class Constants {
public static vNumber = "0.10.3"; public static vNumber = "0.10.5";
public static ImgurApiKey = '7070e7167f0a25a' public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2' public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"

View file

@ -92,7 +92,7 @@ export default class LayoutConfig {
throw "Widenfactor too small" throw "Widenfactor too small"
}else{ }else{
// Unofficial themes get away with this // Unofficial themes get away with this
console.warn("Detected a very small widenfactor, bumping this above 1.") console.warn("Detected a very small widenfactor for theme ", this.id ,", bumping this above 1.")
json.widenFactor = json.widenFactor + 1 json.widenFactor = json.widenFactor + 1
} }
} }

View file

@ -15,7 +15,7 @@ export class Tiles {
public static MapRange<T>(tileRange: TileRange, f: (x: number, y: number) => T): T[] { public static MapRange<T>(tileRange: TileRange, f: (x: number, y: number) => T): T[] {
const result: T[] = [] const result: T[] = []
const total = tileRange.total const total = tileRange.total
if(total > 5000){ if(total > 100000){
throw "Tilerange too big" throw "Tilerange too big"
} }
for (let x = tileRange.xstart; x <= tileRange.xend; x++) { for (let x = tileRange.xstart; x <= tileRange.xend; x++) {

View file

@ -50,13 +50,11 @@ export default class ThemeIntroductionPanel extends Combine {
) )
super([ super([
layout.description.Clone(), layout.description.Clone().SetClass("blcok mb-4"),
"<br/><br/>",
toTheMap, toTheMap,
loginStatus, loginStatus.SetClass("block"),
layout.descriptionTail?.Clone(), layout.descriptionTail?.Clone().SetClass("block mt-4"),
"<br/>", languagePicker.SetClass("block mt-4"),
languagePicker,
...layout.CustomCodeSnippets() ...layout.CustomCodeSnippets()
]) ])

View file

@ -14,7 +14,10 @@
"nl": "Een kaart om toeristisch relevante info op aan te duiden" "nl": "Een kaart om toeristisch relevante info op aan te duiden"
}, },
"description": { "description": {
"nl": "Op deze kaart kan je info zien die relevant is voor toerisme, zoals:<br/><ul><li>Eetgelegenheden</li><li>Cafés en bars</li><li>(Fiets)oplaadpunten</li><li>Fietspompen, fietserverhuur en fietswinkels</li><li>Uitkijktorens</li><li>...</li></ul> Zie je fouten op de kaart? Dan kan je zelf makkelijk aanpasingen maken, die zichtbaar zijn voor iedereen. Hiervoor dien je een gratis OpenStreetMap account voor te maken.<br/><br/>Met de steun van Toerisme Vlaanderen<img src='./assets/themes/toerisme_vlaanderen/logo.png' />" "nl": "Op deze kaart kan je info zien die relevant is voor toerisme, zoals:<br/><ul><li>Eetgelegenheden</li><li>Cafés en bars</li><li>(Fiets)oplaadpunten</li><li>Fietspompen, fietserverhuur en fietswinkels</li><li>Uitkijktorens</li><li>...</li></ul> Zie je fouten op de kaart? Dan kan je zelf makkelijk aanpasingen maken, die zichtbaar zijn voor iedereen. Hiervoor dien je een gratis OpenStreetMap account voor te maken."
},
"descriptionTail": {
"nl": "Met de steun van Toerisme Vlaanderen<img style='height:5rem; width: auto;' src='./assets/themes/toerisme_vlaanderen/logo.png' />"
}, },
"icon": "./assets/svg/star.svg", "icon": "./assets/svg/star.svg",
"startZoom": 8, "startZoom": 8,
@ -28,16 +31,38 @@
"cafe_pub" "cafe_pub"
], ],
"override": { "override": {
"minzoom": 16 "minzoom": 17
} }
}, },
"charging_station", {
"toilet", "builtin": [
"bench", "bench",
"waste_basket", "picnic_table",
"bike_repair_station", "waste_basket"
"binocular", ],
"observation_tower" "override": {
"minzoom": 19
}
},
{
"builtin": [
"charging_station",
"toilet",
"bike_repair_station"
],
"override": {
"minzoom": 14
}
},
{
"builtin": [
"binocular",
"observation_tower"
],
"override": {
"minzoom": 10
}
}
], ],
"hideFromOverview": true "hideFromOverview": true
} }

View file

@ -20,5 +20,20 @@
"uploadingMultiple": "A enviar {count} imagens…", "uploadingMultiple": "A enviar {count} imagens…",
"uploadingPicture": "A enviar a sua imagem…", "uploadingPicture": "A enviar a sua imagem…",
"addPicture": "Adicionar imagem" "addPicture": "Adicionar imagem"
},
"index": {
"#": "Estes textos são mostrados acima dos botões do tema quando nenhum tema é carregado",
"title": "Bem-vindo(a) ao MapComplete",
"intro": "O MapComplete é um visualizador e editor do OpenStreetMap, que mostra informações sobre um tema específico.",
"pickTheme": "Escolha um tema abaixo para começar."
},
"delete": {
"reasons": {
"notFound": "Não foi possível encontrar este elemento"
},
"explanations": {
"selectReason": "Por favor, selecione a razão porque este elemento deve ser eliminado",
"hardDelete": "Este ponto será eliminado no OpenStreetMap. Pode ser recuperado por um contribuidor com experiência"
}
} }
} }

View file

@ -61,6 +61,23 @@
} }
}, },
"question": "Este lugar é acessível a utilizadores de cadeiras de rodas?" "question": "Este lugar é acessível a utilizadores de cadeiras de rodas?"
},
"dog-access": {
"mappings": {
"0": {
"then": "Os cães são permitidos"
},
"1": {
"then": "Os cães <b>não</b> são permitidos"
},
"2": {
"then": "Os cães são permitidos, mas têm de ser presos pela trela"
},
"3": {
"then": "Os cães são permitidos e podem correr livremente"
}
},
"question": "Os cães são permitidos neste estabelecimento?"
} }
} }
} }