forked from MapComplete/MapComplete
		
	Merge branch 'develop' into feature/move-flox
This commit is contained in:
		
						commit
						11d5ccf93f
					
				
					 35 changed files with 773 additions and 243 deletions
				
			
		| 
						 | 
				
			
			@ -26,12 +26,16 @@ The following values are always calculated, by default, by MapComplete and are a
 | 
			
		|||
The latitude and longitude of the point (or centerpoint in the case of a way/area)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### _surface, _surface:ha 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
The surface area of the feature, in square meters and in hectare. Not set on points and ways
 | 
			
		||||
 | 
			
		||||
This is a lazy metatag and is only calculated when needed
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### _length, _length:km 
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,6 +44,8 @@ The surface area of the feature, in square meters and in hectare. Not set on poi
 | 
			
		|||
The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Theme-defined keys 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +53,8 @@ The total length of a feature in meters (and in kilometers, rounded to one decim
 | 
			
		|||
If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### _country 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -54,18 +62,15 @@ If 'units' is defined in the layoutConfig, then this metatagger will rewrite the
 | 
			
		|||
The country code of the property (with latlon2country)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### _isOpen, _isOpen:description 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### _width:needed, _width:needed:no_pedestrians, _width:difference 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present
 | 
			
		||||
This is a lazy metatag and is only calculated when needed
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### _direction:numerical, _direction:leftright 
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +80,8 @@ Legacy for a specific project calculating the needed width for safe traffic on a
 | 
			
		|||
_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### _now:date, _now:datetime, _loaded:date, _loaded:_datetime 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +89,8 @@ _direction:numerical is a normalized, numerical direction based on 'camera:direc
 | 
			
		|||
Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### _last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -89,6 +98,8 @@ Adds the time that the data got loaded - pretty much the time of downloading fro
 | 
			
		|||
Information about the last edit of this object.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 Calculating tags with Javascript 
 | 
			
		||||
----------------------------------
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -140,15 +151,15 @@ Some advanced functions are available on **feat** as well:
 | 
			
		|||
  - distanceTo
 | 
			
		||||
  - overlapWith
 | 
			
		||||
  - closest
 | 
			
		||||
  - closestn
 | 
			
		||||
  - memberships
 | 
			
		||||
  - score
 | 
			
		||||
 
 | 
			
		||||
### distanceTo 
 | 
			
		||||
 | 
			
		||||
 Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object 
 | 
			
		||||
 | 
			
		||||
  0. longitude
 | 
			
		||||
  1. latitude
 | 
			
		||||
  0. feature OR featureID OR longitude
 | 
			
		||||
  1. undefined OR latitude
 | 
			
		||||
 
 | 
			
		||||
### overlapWith 
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -160,10 +171,21 @@ For example to get all objects which overlap or embed from a layer, use `_contai
 | 
			
		|||
 
 | 
			
		||||
### closest 
 | 
			
		||||
 | 
			
		||||
 Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. 
 | 
			
		||||
 Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet laoded) 
 | 
			
		||||
 | 
			
		||||
  0. list of features
 | 
			
		||||
 
 | 
			
		||||
### closestn 
 | 
			
		||||
 | 
			
		||||
 Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet loaded)
 | 
			
		||||
 | 
			
		||||
If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name) 
 | 
			
		||||
 | 
			
		||||
  0. list of features or layer name
 | 
			
		||||
  1. amount of features
 | 
			
		||||
  2. unique tag key (optional)
 | 
			
		||||
  3. maxDistanceInMeters (optional)
 | 
			
		||||
 
 | 
			
		||||
### memberships 
 | 
			
		||||
 | 
			
		||||
 Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. 
 | 
			
		||||
| 
						 | 
				
			
			@ -171,12 +193,4 @@ For example to get all objects which overlap or embed from a layer, use `_contai
 | 
			
		|||
For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')` 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
### score 
 | 
			
		||||
 | 
			
		||||
 Given the path of an aspected routing json file, will calculate the score. This score is wrapped in a UIEventSource, so for further calculations, use `.map(score => ...)`
 | 
			
		||||
 | 
			
		||||
For example: `_comfort_score=feat.score('https://raw.githubusercontent.com/pietervdvn/AspectedRouting/master/Examples/bicycle/aspects/bicycle.comfort.json')` 
 | 
			
		||||
 | 
			
		||||
  0. path
 | 
			
		||||
 Generated from SimpleMetaTagger, ExtraFunction
 | 
			
		||||
| 
						 | 
				
			
			@ -5,11 +5,6 @@
 | 
			
		|||
### all_tags 
 | 
			
		||||
 | 
			
		||||
 Prints all key-value pairs of the object - used for debugging 
 | 
			
		||||
 | 
			
		||||
name | default | description
 | 
			
		||||
------ | --------- | -------------
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
#### Example usage 
 | 
			
		||||
 | 
			
		||||
 `{all_tags()}` 
 | 
			
		||||
| 
						 | 
				
			
			@ -20,11 +15,10 @@ name | default | description
 | 
			
		|||
name | default | description
 | 
			
		||||
------ | --------- | -------------
 | 
			
		||||
image key/prefix | image | The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... 
 | 
			
		||||
smart search | true | Also include images given via 'Wikidata', 'wikimedia_commons' and 'mapillary
 | 
			
		||||
 
 | 
			
		||||
#### Example usage 
 | 
			
		||||
 | 
			
		||||
 `{image_carousel(image,true)}` 
 | 
			
		||||
 `{image_carousel(image)}` 
 | 
			
		||||
### image_upload 
 | 
			
		||||
 | 
			
		||||
 Creates a button where a user can upload an image to IMGUR 
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +30,17 @@ image-key | image | Image tag to add the URL to (or image-tag:0, image-tag:1 whe
 | 
			
		|||
#### Example usage 
 | 
			
		||||
 | 
			
		||||
 `{image_upload(image)}` 
 | 
			
		||||
### wikipedia 
 | 
			
		||||
 | 
			
		||||
 A box showing the corresponding wikipedia article - based on the wikidata tag 
 | 
			
		||||
 | 
			
		||||
name | default | description
 | 
			
		||||
------ | --------- | -------------
 | 
			
		||||
keyToShowWikipediaFor | wikidata | Use the wikidata entry from this key to show the wikipedia article for
 | 
			
		||||
 
 | 
			
		||||
#### Example usage 
 | 
			
		||||
 | 
			
		||||
 `{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height 
 | 
			
		||||
### minimap 
 | 
			
		||||
 | 
			
		||||
 A small map showing the selected feature. Note that no styling is applied, wrap this in a div 
 | 
			
		||||
| 
						 | 
				
			
			@ -128,16 +133,25 @@ If you want to import a dataset, make sure that:
 | 
			
		|||
 | 
			
		||||
1. The dataset to import has a suitable license
 | 
			
		||||
2. The community has been informed of the import
 | 
			
		||||
3. The theme will filter out duplicate nodes4. All other requirements of the [import guidelines](https://wiki.openstreetmap.org/wiki/Import/Guidelines) have been followed 
 | 
			
		||||
3. All other requirements of the [import guidelines](https://wiki.openstreetmap.org/wiki/Import/Guidelines) have been followed
 | 
			
		||||
 | 
			
		||||
There are also some technicalities in your theme to keep in mind:
 | 
			
		||||
 | 
			
		||||
1. The new point will be added and will flow through the program as any other new point as if it came from OSM.
 | 
			
		||||
    This means that there should be a layer which will match the new tags and which will display it.
 | 
			
		||||
2. The original point from your geojson layer will gain the tag '_imported=yes'.
 | 
			
		||||
    This should be used to change the appearance or even to hide it (eg by changing the icon size to zero)
 | 
			
		||||
3. There should be a way for the theme to detect previously imported points, even after reloading.
 | 
			
		||||
    A reference number to the original dataset is an excellen way to do this    
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
name | default | description
 | 
			
		||||
------ | --------- | -------------
 | 
			
		||||
tags | undefined | Tags to copy-specification. This contains one or more pairs (seperated by a `;`), e.g. `amenity=fast_food; addr:housenumber={number}`. This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature. (Hint: prepare these values, e.g. with calculatedTags)
 | 
			
		||||
tags | undefined | Tags to copy-specification. This contains one or more pairs (seperated by a `;`), e.g. `amenity=fast_food; addr:housenumber=$number`. This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature. (Hint: prepare these values, e.g. with calculatedTags)
 | 
			
		||||
text | Import this data into OpenStreetMap | The text to show on the button
 | 
			
		||||
icon | ./assets/svg/addSmall.svg | A nice icon to show in the button
 | 
			
		||||
minzoom | 18 | How far the contributor must zoom in before being able to import the point
 | 
			
		||||
 
 | 
			
		||||
#### Example usage 
 | 
			
		||||
 | 
			
		||||
 `{import_button(,Import this data into OpenStreetMap,./assets/svg/addSmall.svg)}`
 | 
			
		||||
 
 | 
			
		||||
Generated from UI/SpecialVisualisations.ts
 | 
			
		||||
 `{import_button(,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,18)}` Generated from UI/SpecialVisualisations.ts
 | 
			
		||||
| 
						 | 
				
			
			@ -161,7 +161,7 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
 | 
			
		|||
 overpassUrl 
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
 Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter The default value is _https://overpass-api.de/api/interpreter_
 | 
			
		||||
 Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter The default value is _https://overpass-api.de/api/interpreter,https://overpass.kumi.systems/api/interpreter,https://overpass.openstreetmap.ru/cgi/interpreter_
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 overpassTimeout 
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -426,10 +426,10 @@ export class InitUiElements {
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        const clustering = State.state.layoutToUse.clustering
 | 
			
		||||
        const clusterCounter = TileHierarchyAggregator.createHierarchy()
 | 
			
		||||
        new ShowDataLayer({
 | 
			
		||||
            features: clusterCounter.getCountsForZoom(State.state.locationControl, State.state.layoutToUse.clustering.minNeededElements),
 | 
			
		||||
            features: clusterCounter.getCountsForZoom(clustering, State.state.locationControl, State.state.layoutToUse.clustering.minNeededElements),
 | 
			
		||||
            leafletMap: State.state.leafletMap,
 | 
			
		||||
            layerToShow: ShowTileInfo.styling,
 | 
			
		||||
            enablePopups: false
 | 
			
		||||
| 
						 | 
				
			
			@ -440,7 +440,7 @@ export class InitUiElements {
 | 
			
		|||
 | 
			
		||||
                clusterCounter.addTile(source)
 | 
			
		||||
 | 
			
		||||
                const clustering = State.state.layoutToUse.clustering
 | 
			
		||||
                // Do show features indicates if the 'showDataLayer' should be shown
 | 
			
		||||
                const doShowFeatures = source.features.map(
 | 
			
		||||
                    f => {
 | 
			
		||||
                        const z = State.state.locationControl.data.zoom
 | 
			
		||||
| 
						 | 
				
			
			@ -449,12 +449,24 @@ export class InitUiElements {
 | 
			
		|||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        const bounds = State.state.currentBounds.data
 | 
			
		||||
                        if(bounds === undefined){
 | 
			
		||||
                            // Map is not yet displayed
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (!source.bbox.overlapsWith(bounds)) {
 | 
			
		||||
                            // Not within range -> features are hidden
 | 
			
		||||
                            return false
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                        if (z < source.layer.layerDef.minzoom) {
 | 
			
		||||
                            // Layer is always hidden for this zoom level
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (z >= clustering.maxZoom) {
 | 
			
		||||
                        if (z > clustering.maxZoom) {
 | 
			
		||||
                            return true
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -473,23 +485,15 @@ export class InitUiElements {
 | 
			
		|||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))?.totalValue > clustering.minNeededElements) {
 | 
			
		||||
                                // To much elements
 | 
			
		||||
                                return false
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                        const bounds = State.state.currentBounds.data
 | 
			
		||||
                        if(bounds === undefined){
 | 
			
		||||
                            // Map is not yet displayed
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                        if (!source.bbox.overlapsWith(bounds)) {
 | 
			
		||||
                            // Not within range
 | 
			
		||||
                            return false
 | 
			
		||||
                        }
 | 
			
		||||
                      
 | 
			
		||||
                        return true
 | 
			
		||||
                    }, [State.state.currentBounds]
 | 
			
		||||
                    }, [State.state.currentBounds, source.layer.isDisplayed]
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                new ShowDataLayer(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,8 +38,7 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
			
		|||
        readonly currentBounds: UIEventSource<BBox>
 | 
			
		||||
    }
 | 
			
		||||
    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(
 | 
			
		||||
        state: {
 | 
			
		||||
            readonly locationControl: UIEventSource<Loc>,
 | 
			
		||||
| 
						 | 
				
			
			@ -49,10 +48,11 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
			
		|||
            readonly overpassMaxZoom: UIEventSource<number>,
 | 
			
		||||
            readonly currentBounds: UIEventSource<BBox>
 | 
			
		||||
        },
 | 
			
		||||
        options?: {
 | 
			
		||||
        options: {
 | 
			
		||||
            padToTiles: UIEventSource<number>,
 | 
			
		||||
            isActive?: UIEventSource<boolean>,
 | 
			
		||||
            relationTracker: RelationsTracker,
 | 
			
		||||
            onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[]) => void
 | 
			
		||||
            onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void
 | 
			
		||||
        }) {
 | 
			
		||||
 | 
			
		||||
        this.state = state
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +61,7 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
			
		|||
        this.relationsTracker = options.relationTracker
 | 
			
		||||
        const self = this;
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private update() {
 | 
			
		||||
    private update(paddedZoomLevel: number) {
 | 
			
		||||
        if (!this._isActive.data) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const self = this;
 | 
			
		||||
        this.updateAsync().then(bboxDate => {
 | 
			
		||||
        this.updateAsync(paddedZoomLevel).then(bboxDate => {
 | 
			
		||||
            if(bboxDate === undefined || self.onBboxLoaded === undefined){
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            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) {
 | 
			
		||||
            console.log("Still running a query, not updating");
 | 
			
		||||
            return undefined;
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +109,7 @@ export default class OverpassFeatureSource implements FeatureSource {
 | 
			
		|||
            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) {
 | 
			
		||||
            return undefined;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -116,16 +116,15 @@ export class BBox {
 | 
			
		|||
        return this.minLat
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pad(factor: number): BBox {
 | 
			
		||||
        const latDiff = this.maxLat - this.minLat
 | 
			
		||||
        const lat = (this.maxLat + this.minLat) / 2
 | 
			
		||||
        const lonDiff = this.maxLon - this.minLon
 | 
			
		||||
        const lon = (this.maxLon + this.minLon) / 2
 | 
			
		||||
    pad(factor: number, maxIncrease = 2): BBox {
 | 
			
		||||
        
 | 
			
		||||
        const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor)
 | 
			
		||||
        const lonDiff =Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor)
 | 
			
		||||
        return new BBox([[
 | 
			
		||||
            lon - lonDiff * factor,
 | 
			
		||||
            lat - latDiff * factor
 | 
			
		||||
        ], [lon + lonDiff * factor,
 | 
			
		||||
            lat + latDiff * factor]])
 | 
			
		||||
            this.minLon - lonDiff,
 | 
			
		||||
            this.minLat  - latDiff
 | 
			
		||||
        ], [this.maxLon + lonDiff,
 | 
			
		||||
            this.maxLat + latDiff]])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toLeaflet() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,11 +4,10 @@
 | 
			
		|||
 * Technically, more an Actor then a featuresource, but it fits more neatly this ay
 | 
			
		||||
 */
 | 
			
		||||
import {FeatureSourceForLayer} from "../FeatureSource";
 | 
			
		||||
import SimpleMetaTagger from "../../SimpleMetaTagger";
 | 
			
		||||
 | 
			
		||||
export default class SaveTileToLocalStorageActor {
 | 
			
		||||
    public static readonly storageKey: string = "cached-features";
 | 
			
		||||
    public static readonly formatVersion: string = "1"
 | 
			
		||||
    public static readonly formatVersion: string = "2"
 | 
			
		||||
 | 
			
		||||
    constructor(source: FeatureSourceForLayer, tileIndex: number) {
 | 
			
		||||
        
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +36,5 @@ export default class SaveTileToLocalStorageActor {
 | 
			
		|||
        }catch(e){
 | 
			
		||||
            console.error("Could not mark tile ", key, "as visited")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -58,7 +58,7 @@ export default class FeaturePipeline {
 | 
			
		|||
    private readonly freshnesses = new Map<string, TileFreshnessCalculator>();
 | 
			
		||||
 | 
			
		||||
    private readonly oldestAllowedDate: Date = new Date(new Date().getTime() - 60 * 60 * 24 * 30 * 1000);
 | 
			
		||||
    private readonly osmSourceZoomLevel = 14
 | 
			
		||||
    private readonly osmSourceZoomLevel = 15
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        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
 | 
			
		||||
                    TiledFeatureSource.createHierarchy(src, {
 | 
			
		||||
                        layer: src.layer,
 | 
			
		||||
                        minZoomLevel: 14,
 | 
			
		||||
                        minZoomLevel: this.osmSourceZoomLevel,
 | 
			
		||||
                        dontEnforceMinZoom: true,
 | 
			
		||||
                        registerTile: (tile) => {
 | 
			
		||||
                            new RegisteringAllFromFeatureSourceActor(tile)
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +155,7 @@ export default class FeaturePipeline {
 | 
			
		|||
                            tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                }else{
 | 
			
		||||
                } else {
 | 
			
		||||
                    new RegisteringAllFromFeatureSourceActor(src)
 | 
			
		||||
                    perLayerHierarchy.get(id).registerTile(src)
 | 
			
		||||
                    src.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(src))
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +200,7 @@ export default class FeaturePipeline {
 | 
			
		|||
        new PerLayerFeatureSourceSplitter(state.filteredLayers,
 | 
			
		||||
            (source) => TiledFeatureSource.createHierarchy(source, {
 | 
			
		||||
                layer: source.layer,
 | 
			
		||||
                minZoomLevel: 14,
 | 
			
		||||
                minZoomLevel: source.layer.layerDef.minzoom,
 | 
			
		||||
                dontEnforceMinZoom: true,
 | 
			
		||||
                maxFeatureCount: state.layoutToUse.clustering.minNeededElements,
 | 
			
		||||
                maxZoomLevel: state.layoutToUse.clustering.maxZoom,
 | 
			
		||||
| 
						 | 
				
			
			@ -235,7 +235,7 @@ export default class FeaturePipeline {
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
        // Whenever fresh data comes in, we need to update the metatagging
 | 
			
		||||
        self.newDataLoadedSignal.stabilized(1000).addCallback(src => {
 | 
			
		||||
        self.newDataLoadedSignal.stabilized(1000).addCallback(_ => {
 | 
			
		||||
            self.updateAllMetaTagging()
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -276,15 +276,15 @@ export default class FeaturePipeline {
 | 
			
		|||
        const self = this
 | 
			
		||||
        return this.state.currentBounds.map(bbox => {
 | 
			
		||||
            if (bbox === undefined) {
 | 
			
		||||
                return
 | 
			
		||||
                return undefined
 | 
			
		||||
            }
 | 
			
		||||
            if (!isSufficientlyZoomed.data) {
 | 
			
		||||
                return;
 | 
			
		||||
                return undefined;
 | 
			
		||||
            }
 | 
			
		||||
            const osmSourceZoomLevel = self.osmSourceZoomLevel
 | 
			
		||||
            const range = bbox.containingTileRange(osmSourceZoomLevel)
 | 
			
		||||
            const tileIndexes = []
 | 
			
		||||
            if (range.total > 100) {
 | 
			
		||||
            if (range.total >= 100) {
 | 
			
		||||
                // Too much tiles!
 | 
			
		||||
                return []
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -294,7 +294,7 @@ export default class FeaturePipeline {
 | 
			
		|||
                if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
 | 
			
		||||
                    console.debug("Skipping tile", osmSourceZoomLevel, x, y, "as a decently fresh one is available")
 | 
			
		||||
                    // The cached tiles contain decently fresh data
 | 
			
		||||
                    return;
 | 
			
		||||
                    return undefined;
 | 
			
		||||
                }
 | 
			
		||||
                tileIndexes.push(i)
 | 
			
		||||
            })
 | 
			
		||||
| 
						 | 
				
			
			@ -327,28 +327,30 @@ export default class FeaturePipeline {
 | 
			
		|||
            }
 | 
			
		||||
 | 
			
		||||
            const range = bbox.containingTileRange(zoom)
 | 
			
		||||
            if (range.total > 100) {
 | 
			
		||||
            if (range.total >= 5000) {
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
            const self = this;
 | 
			
		||||
            const allFreshnesses = Tiles.MapRange(range, (x, y) => self.freshnessForVisibleLayers(zoom, x, y))
 | 
			
		||||
            return allFreshnesses.some(freshness => freshness === undefined || freshness < this.oldestAllowedDate)
 | 
			
		||||
 | 
			
		||||
        }, [state.locationControl])
 | 
			
		||||
 | 
			
		||||
        const self = this;
 | 
			
		||||
        const updater = new OverpassFeatureSource(state,
 | 
			
		||||
            {
 | 
			
		||||
                padToTiles: state.locationControl.map(l => Math.min(15, l.zoom + 1)),
 | 
			
		||||
                relationTracker: this.relationTracker,
 | 
			
		||||
                isActive: useOsmApi.map(b => !b && overpassIsActive.data, [overpassIsActive]),
 | 
			
		||||
                onBboxLoaded: ((bbox, date, downloadedLayers) => {
 | 
			
		||||
                    Tiles.MapRange(bbox.containingTileRange(self.osmSourceZoomLevel), (x, y) => {
 | 
			
		||||
                onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => {
 | 
			
		||||
                    Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => {
 | 
			
		||||
                       const tileIndex =  Tiles.tile_index(paddedToZoomLevel, x, y)
 | 
			
		||||
                        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)
 | 
			
		||||
                        })
 | 
			
		||||
                    })
 | 
			
		||||
 | 
			
		||||
                })
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,9 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
 | 
			
		|||
            throw "Invalid layer: geojsonSource expected"
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        const whitelistUrl = source.geojsonSource.replace("{z}_{x}_{y}.geojson", "overview.json")
 | 
			
		||||
        const whitelistUrl = source.geojsonSource
 | 
			
		||||
            .replace("{z}", ""+source.geojsonZoomLevel)
 | 
			
		||||
            .replace("{x}_{y}.geojson", "overview.json")
 | 
			
		||||
            .replace("{layer}",layer.layerDef.id)
 | 
			
		||||
        
 | 
			
		||||
        let whitelist = undefined
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +48,8 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
 | 
			
		|||
                if(whitelist !== undefined){
 | 
			
		||||
                    const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2])
 | 
			
		||||
                    if(!isWhiteListed){
 | 
			
		||||
                        return undefined;
 | 
			
		||||
                        console.log("Not whitelisted:",zxy, isWhiteListed, whitelist)
 | 
			
		||||
                    //    return undefined;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,16 @@
 | 
			
		|||
import FilteredLayer from "../../../Models/FilteredLayer";
 | 
			
		||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
 | 
			
		||||
import {UIEventSource} from "../../UIEventSource";
 | 
			
		||||
import Loc from "../../../Models/Loc";
 | 
			
		||||
import TileHierarchy from "./TileHierarchy";
 | 
			
		||||
import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor";
 | 
			
		||||
import {Tiles} from "../../../Models/TileRange";
 | 
			
		||||
import {BBox} from "../../BBox";
 | 
			
		||||
 | 
			
		||||
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> {
 | 
			
		||||
        const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layerId + "-"
 | 
			
		||||
| 
						 | 
				
			
			@ -29,14 +31,15 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
 | 
			
		|||
    constructor(layer: FilteredLayer,
 | 
			
		||||
                handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void,
 | 
			
		||||
                state: {
 | 
			
		||||
                    locationControl: UIEventSource<Loc>
 | 
			
		||||
                    leafletMap: any
 | 
			
		||||
                    currentBounds: UIEventSource<BBox>
 | 
			
		||||
                }) {
 | 
			
		||||
        this.layer = layer;
 | 
			
		||||
        this.handleFeatureSource = handleFeatureSource;
 | 
			
		||||
 | 
			
		||||
        const undefinedTiles = new Set<number>()
 | 
			
		||||
        
 | 
			
		||||
        this.undefinedTiles = new Set<number>()
 | 
			
		||||
        const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-"
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        const indexes: number[] = Object.keys(localStorage)
 | 
			
		||||
        const knownTiles: number[] = Object.keys(localStorage)
 | 
			
		||||
            .filter(key => {
 | 
			
		||||
                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))
 | 
			
		||||
 | 
			
		||||
        console.debug("Layer", layer.layerDef.id, "has following tiles in available in localstorage", indexes.map(i => Tiles.tile_from_index(i).join("/")).join(", "))
 | 
			
		||||
        for (const index of indexes) {
 | 
			
		||||
        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 knownTiles) {
 | 
			
		||||
 | 
			
		||||
            const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" + index;
 | 
			
		||||
            const version = localStorage.getItem(prefix + "-format")
 | 
			
		||||
| 
						 | 
				
			
			@ -55,78 +58,54 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
 | 
			
		|||
                localStorage.removeItem(prefix)
 | 
			
		||||
                localStorage.removeItem(prefix+"-time")
 | 
			
		||||
                localStorage.removeItem(prefix+"-format")
 | 
			
		||||
                undefinedTiles.add(index)
 | 
			
		||||
              this.  undefinedTiles.add(index)
 | 
			
		||||
                console.log("Dropped old format tile", prefix)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const zLevels = indexes.map(i => i % 100)
 | 
			
		||||
        const indexesSet = new Set(indexes)
 | 
			
		||||
        const maxZoom = Math.max(...zLevels)
 | 
			
		||||
        const minZoom = Math.min(...zLevels)
 | 
			
		||||
        const self = this;
 | 
			
		||||
        const self = this
 | 
			
		||||
        state.currentBounds.map(bounds => {
 | 
			
		||||
 | 
			
		||||
        const neededTiles = state.locationControl.map(
 | 
			
		||||
            location => {
 | 
			
		||||
                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
 | 
			
		||||
            if(bounds === undefined){
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            , [layer.isDisplayed, state.leafletMap]).stabilized(50);
 | 
			
		||||
            for (const knownTile of knownTiles) {
 | 
			
		||||
                
 | 
			
		||||
        neededTiles.addCallbackAndRunD(neededIndexes => {
 | 
			
		||||
            for (const neededIndex of neededIndexes) {
 | 
			
		||||
                // 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.loadedTiles.has(knownTile)){
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -209,7 +209,7 @@ export default class SimpleMetaTagger {
 | 
			
		|||
                configurable: true,
 | 
			
		||||
                get: () => {
 | 
			
		||||
                    delete feature.properties._isOpen
 | 
			
		||||
                    feature.properties._isOpen = ""
 | 
			
		||||
                    feature.properties._isOpen = undefined
 | 
			
		||||
                    const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id);
 | 
			
		||||
                    tagsSource.addCallbackAndRunD(tags => {
 | 
			
		||||
                        if (tags.opening_hours === undefined || tags._country === undefined) {
 | 
			
		||||
| 
						 | 
				
			
			@ -230,7 +230,8 @@ export default class SimpleMetaTagger {
 | 
			
		|||
                                const oldNextChange = tags["_isOpen:nextTrigger"] ?? 0;
 | 
			
		||||
 | 
			
		||||
                                if (oldNextChange > (new Date()).getTime() &&
 | 
			
		||||
                                    tags["_isOpen:oldvalue"] === tags["opening_hours"]) {
 | 
			
		||||
                                    tags["_isOpen:oldvalue"] === tags["opening_hours"]
 | 
			
		||||
                                && tags["_isOpen"] !== undefined) {
 | 
			
		||||
                                    // Already calculated and should not yet be triggered
 | 
			
		||||
                                    return false;
 | 
			
		||||
                                }
 | 
			
		||||
| 
						 | 
				
			
			@ -267,7 +268,7 @@ export default class SimpleMetaTagger {
 | 
			
		|||
                        }
 | 
			
		||||
 | 
			
		||||
                    })
 | 
			
		||||
                    return feature.properties["_isOpen"]
 | 
			
		||||
                    return undefined
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import {Utils} from "../Utils";
 | 
			
		|||
 | 
			
		||||
export default class Constants {
 | 
			
		||||
 | 
			
		||||
    public static vNumber = "0.10.3";
 | 
			
		||||
    public static vNumber = "0.10.5";
 | 
			
		||||
    public static ImgurApiKey = '7070e7167f0a25a'
 | 
			
		||||
    public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
 | 
			
		||||
    public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
 | 
			
		||||
import {LayerConfigJson} from "./LayerConfigJson";
 | 
			
		||||
import UnitConfigJson from "./UnitConfigJson";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Defines the entire theme.
 | 
			
		||||
| 
						 | 
				
			
			@ -253,7 +252,7 @@ export interface LayoutConfigJson {
 | 
			
		|||
     * If set to [[lat0, lon0], [lat1, lon1]], the map will not scroll outside of those bounds.
 | 
			
		||||
     * Off by default, which will enable panning to the entire world
 | 
			
		||||
     */
 | 
			
		||||
    lockLocation?: boolean | [[number, number], [number, number]];
 | 
			
		||||
    lockLocation?: boolean | [[number, number], [number, number]] | number[][];
 | 
			
		||||
 | 
			
		||||
    enableUserBadge?: boolean;
 | 
			
		||||
    enableShareScreen?: boolean;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ export default class LayoutConfig {
 | 
			
		|||
    public layers: LayerConfig[];
 | 
			
		||||
    public readonly clustering?: {
 | 
			
		||||
        maxZoom: number,
 | 
			
		||||
        minNeededElements: number
 | 
			
		||||
        minNeededElements: number,
 | 
			
		||||
    };
 | 
			
		||||
    public readonly hideFromOverview: boolean;
 | 
			
		||||
    public lockLocation: boolean | [[number, number], [number, number]];
 | 
			
		||||
| 
						 | 
				
			
			@ -92,7 +92,7 @@ export default class LayoutConfig {
 | 
			
		|||
                throw "Widenfactor too small"
 | 
			
		||||
            }else{
 | 
			
		||||
                // 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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -139,12 +139,17 @@ export default class LayoutConfig {
 | 
			
		|||
 | 
			
		||||
        this.clustering = {
 | 
			
		||||
            maxZoom: 16,
 | 
			
		||||
            minNeededElements: 25
 | 
			
		||||
            minNeededElements: 25,
 | 
			
		||||
        };
 | 
			
		||||
        if (json.clustering) {
 | 
			
		||||
        if(json.clustering === false){
 | 
			
		||||
            this.clustering = {
 | 
			
		||||
                maxZoom: 0,
 | 
			
		||||
                minNeededElements: 100000,
 | 
			
		||||
            };
 | 
			
		||||
        }else         if (json.clustering) {
 | 
			
		||||
            this.clustering = {
 | 
			
		||||
                maxZoom: json.clustering.maxZoom ?? 18,
 | 
			
		||||
                minNeededElements: json.clustering.minNeededElements ?? 25
 | 
			
		||||
                minNeededElements: json.clustering.minNeededElements ?? 25,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -153,7 +158,7 @@ export default class LayoutConfig {
 | 
			
		|||
        if (json.hideInOverview) {
 | 
			
		||||
            throw "The json for " + this.id + " contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
 | 
			
		||||
        }
 | 
			
		||||
        this.lockLocation = json.lockLocation ?? undefined;
 | 
			
		||||
        this.lockLocation = <[[number, number], [number, number]]> json.lockLocation ?? undefined;
 | 
			
		||||
        this.enableUserBadge = json.enableUserBadge ?? true;
 | 
			
		||||
        this.enableShareScreen = json.enableShareScreen ?? true;
 | 
			
		||||
        this.enableMoreQuests = json.enableMoreQuests ?? true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ export class Tiles {
 | 
			
		|||
    public static MapRange<T>(tileRange: TileRange, f: (x: number, y: number) => T): T[] {
 | 
			
		||||
        const result: T[] = []
 | 
			
		||||
        const total = tileRange.total
 | 
			
		||||
        if(total > 5000){
 | 
			
		||||
        if(total > 100000){
 | 
			
		||||
            throw "Tilerange too big"
 | 
			
		||||
        }
 | 
			
		||||
        for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,13 +50,11 @@ export default class ThemeIntroductionPanel extends Combine {
 | 
			
		|||
            )
 | 
			
		||||
 | 
			
		||||
        super([
 | 
			
		||||
            layout.description.Clone(),
 | 
			
		||||
            "<br/><br/>",
 | 
			
		||||
            layout.description.Clone().SetClass("blcok mb-4"),
 | 
			
		||||
            toTheMap,
 | 
			
		||||
            loginStatus,
 | 
			
		||||
            layout.descriptionTail?.Clone(),
 | 
			
		||||
            "<br/>",
 | 
			
		||||
            languagePicker,
 | 
			
		||||
            loginStatus.SetClass("block"),
 | 
			
		||||
            layout.descriptionTail?.Clone().SetClass("block mt-4"),
 | 
			
		||||
            languagePicker?.SetClass("block mt-4"),
 | 
			
		||||
            ...layout.CustomCodeSnippets()
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,7 @@ export class TileHierarchyAggregator implements FeatureSource {
 | 
			
		|||
    public readonly name;
 | 
			
		||||
 | 
			
		||||
    private readonly featuresStatic = []
 | 
			
		||||
    private readonly featureProperties: { count: string, tileId: string, id: string };
 | 
			
		||||
    private readonly featureProperties: { count: string, kilocount: string, tileId: string, id: string };
 | 
			
		||||
 | 
			
		||||
    private constructor(parent: TileHierarchyAggregator, z: number, x: number, y: number) {
 | 
			
		||||
        this._parent = parent;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +36,8 @@ export class TileHierarchyAggregator implements FeatureSource {
 | 
			
		|||
        const totals = {
 | 
			
		||||
            id: ""+this._tileIndex,
 | 
			
		||||
            tileId: ""+this._tileIndex,
 | 
			
		||||
            count: ""+0
 | 
			
		||||
            count: `0`,
 | 
			
		||||
            kilocount: "0"
 | 
			
		||||
        }
 | 
			
		||||
        this.featureProperties = totals
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +109,7 @@ export class TileHierarchyAggregator implements FeatureSource {
 | 
			
		|||
            this.features.setData(TileHierarchyAggregator.empty)
 | 
			
		||||
        } else {
 | 
			
		||||
            this.featureProperties.count = "" + total;
 | 
			
		||||
            this.featureProperties.kilocount = "" +Math.floor(total/1000);
 | 
			
		||||
            this.features.data = this.featuresStatic
 | 
			
		||||
            this.features.ping()
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -153,13 +155,18 @@ export class TileHierarchyAggregator implements FeatureSource {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    getCountsForZoom(locationControl: UIEventSource<{ zoom : number }>, cutoff: number = 0) : FeatureSource{
 | 
			
		||||
    getCountsForZoom(clusteringConfig: {maxZoom: number}, locationControl: UIEventSource<{ zoom : number }>, cutoff: number = 0) : FeatureSource{
 | 
			
		||||
        const self = this
 | 
			
		||||
        
 | 
			
		||||
        const empty = []
 | 
			
		||||
        return new StaticFeatureSource(
 | 
			
		||||
            locationControl.map(loc => {
 | 
			
		||||
                const features = []
 | 
			
		||||
                const targetZoom = loc.zoom
 | 
			
		||||
                
 | 
			
		||||
                if(targetZoom > clusteringConfig.maxZoom){
 | 
			
		||||
                    return empty
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                const features = []
 | 
			
		||||
                self.visitSubTiles(aggr => {
 | 
			
		||||
                    if(aggr.totalValue < cutoff) {
 | 
			
		||||
                        return false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,7 +55,11 @@ export default class SpecialVisualizations {
 | 
			
		|||
                            if (!tags.hasOwnProperty(key)) {
 | 
			
		||||
                                continue
 | 
			
		||||
                            }
 | 
			
		||||
                            parts.push([key, tags[key] ?? "<b>undefined</b>"]);
 | 
			
		||||
                            let v = tags[key]
 | 
			
		||||
                            if(v === ""){
 | 
			
		||||
                                v = "<b>empty string</b>"
 | 
			
		||||
                            }
 | 
			
		||||
                            parts.push([key, v ?? "<b>undefined</b>"]);
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        for(const key of calculatedTags){
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										26
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								Utils.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -78,18 +78,6 @@ export class Utils {
 | 
			
		|||
        return res;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static DoEvery(millis: number, f: (() => void)) {
 | 
			
		||||
        if (Utils.runningFromConsole) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        window.setTimeout(
 | 
			
		||||
            function () {
 | 
			
		||||
                f();
 | 
			
		||||
                Utils.DoEvery(millis, f);
 | 
			
		||||
            }
 | 
			
		||||
            , millis)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static NoNull<T>(array: T[]): T[] {
 | 
			
		||||
        const ls: T[] = [];
 | 
			
		||||
        for (const t of array) {
 | 
			
		||||
| 
						 | 
				
			
			@ -440,5 +428,19 @@ export class Utils {
 | 
			
		|||
            window.setTimeout(resolve, timeMillis);
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public static toHumanTime(seconds): string{
 | 
			
		||||
        seconds = Math.floor(seconds)
 | 
			
		||||
        let minutes = Math.floor(seconds / 60)
 | 
			
		||||
        seconds = seconds % 60
 | 
			
		||||
        let hours = Math.floor(minutes / 60)
 | 
			
		||||
        minutes = minutes % 60
 | 
			
		||||
        let days = Math.floor(hours / 24)
 | 
			
		||||
        hours = hours % 24
 | 
			
		||||
        if(days > 0){
 | 
			
		||||
            return days+"days"+" "+hours+"h"
 | 
			
		||||
        }
 | 
			
		||||
            return hours+":"+Utils.TwoDigits(minutes)+":"+Utils.TwoDigits(seconds)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,8 +28,8 @@
 | 
			
		|||
        "render": "<div class='rounded-full text-xl font-bold' style='width: 2rem; height: 2rem; background: white'>{count}</div>",
 | 
			
		||||
        "mappings": [
 | 
			
		||||
            {
 | 
			
		||||
                "if": "count>99",
 | 
			
		||||
                "then": "<div class='rounded-full text-xl font-bold flex flex-col' style='width: 2.5rem; height: 2.5rem; background: white'>>99</div>"
 | 
			
		||||
                "if": "count>1000",
 | 
			
		||||
                "then": "<div class='rounded-full text-xl font-bold flex flex-col' style='width: 2.5rem; height: 2.5rem; background: white'>{kilocount}K</div>"
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -156,35 +156,40 @@
 | 
			
		|||
  "dog-access": {
 | 
			
		||||
    "question": {
 | 
			
		||||
      "en": "Are dogs allowed in this business?",
 | 
			
		||||
      "nl": "Zijn honden toegelaten in deze zaak?"
 | 
			
		||||
      "nl": "Zijn honden toegelaten in deze zaak?",
 | 
			
		||||
      "pt": "Os cães são permitidos neste estabelecimento?"
 | 
			
		||||
    },
 | 
			
		||||
    "mappings": [
 | 
			
		||||
      {
 | 
			
		||||
        "if": "dog=yes",
 | 
			
		||||
        "then": {
 | 
			
		||||
          "en": "Dogs are allowed",
 | 
			
		||||
          "nl": "honden zijn toegelaten"
 | 
			
		||||
          "nl": "honden zijn toegelaten",
 | 
			
		||||
          "pt": "Os cães são permitidos"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "if": "dog=no",
 | 
			
		||||
        "then": {
 | 
			
		||||
          "en": "Dogs are <b>not</b> allowed",
 | 
			
		||||
          "nl": "honden zijn <b>niet</b> toegelaten"
 | 
			
		||||
          "nl": "honden zijn <b>niet</b> toegelaten",
 | 
			
		||||
          "pt": "Os cães <b>não</b> são permitidos"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "if": "dog=leashed",
 | 
			
		||||
        "then": {
 | 
			
		||||
          "en": "Dogs are allowed, but they have to be leashed",
 | 
			
		||||
          "nl": "honden zijn <b>enkel aan de leiband</b> welkom"
 | 
			
		||||
          "nl": "honden zijn <b>enkel aan de leiband</b> welkom",
 | 
			
		||||
          "pt": "Os cães são permitidos, mas têm de ser presos pela trela"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "if": "dog=unleashed",
 | 
			
		||||
        "then": {
 | 
			
		||||
          "en": "Dogs are allowed and can run around freely",
 | 
			
		||||
          "nl": "honden zijn welkom en mogen vrij rondlopen"
 | 
			
		||||
          "nl": "honden zijn welkom en mogen vrij rondlopen",
 | 
			
		||||
          "pt": "Os cães são permitidos e podem correr livremente"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -247,7 +252,8 @@
 | 
			
		|||
      "en": "Which methods of payment are accepted here?",
 | 
			
		||||
      "nl": "Welke betaalmiddelen worden hier geaccepteerd?",
 | 
			
		||||
      "pt": "Que métodos de pagamento são aceites aqui?",
 | 
			
		||||
      "pt_BR": "Quais métodos de pagamento são aceitos aqui?"
 | 
			
		||||
      "pt_BR": "Quais métodos de pagamento são aceitos aqui?",
 | 
			
		||||
      "id": "Metode pembayaran manakah yang di terima disini?"
 | 
			
		||||
    },
 | 
			
		||||
    "multiAnswer": true,
 | 
			
		||||
    "mappings": [
 | 
			
		||||
| 
						 | 
				
			
			@ -258,7 +264,8 @@
 | 
			
		|||
          "en": "Cash is accepted here",
 | 
			
		||||
          "nl": "Cash geld wordt hier aanvaard",
 | 
			
		||||
          "pt": "Aceitam pagamento com dinheiro aqui",
 | 
			
		||||
          "pt_BR": "Dinheiro é aceito aqui"
 | 
			
		||||
          "pt_BR": "Dinheiro é aceito aqui",
 | 
			
		||||
          "id": "Disini menerima pembayaran tunai"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
| 
						 | 
				
			
			@ -268,7 +275,8 @@
 | 
			
		|||
          "en": "Payment cards are accepted here",
 | 
			
		||||
          "nl": "Betalen met bankkaarten kan hier",
 | 
			
		||||
          "pt": "Aceitam pagamento com cartões bancários aqui",
 | 
			
		||||
          "pt_BR": "Cartões de pagamento são aceitos aqui"
 | 
			
		||||
          "pt_BR": "Cartões de pagamento são aceitos aqui",
 | 
			
		||||
          "id": "Disini menerima pembayaran dengan kartu"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -292,7 +300,8 @@
 | 
			
		|||
      "pl": "Na jakim poziomie znajduje się ta funkcja?",
 | 
			
		||||
      "pt_BR": "Em que nível esse recurso está localizado?",
 | 
			
		||||
      "ru": "На каком этаже находится этот объект?",
 | 
			
		||||
      "pt": "Em que nível se encontra este elemento?"
 | 
			
		||||
      "pt": "Em que nível se encontra este elemento?",
 | 
			
		||||
      "id": "Pada tingkat apa fitur ini diletakkan?"
 | 
			
		||||
    },
 | 
			
		||||
    "render": {
 | 
			
		||||
      "en": "Located on the {level}th floor",
 | 
			
		||||
| 
						 | 
				
			
			@ -369,7 +378,8 @@
 | 
			
		|||
          "fr": "Premier étage",
 | 
			
		||||
          "pl": "Znajduje się na pierwszym piętrze",
 | 
			
		||||
          "sv": "Ligger på första våningen",
 | 
			
		||||
          "pt": "Está no primeiro andar"
 | 
			
		||||
          "pt": "Está no primeiro andar",
 | 
			
		||||
          "id": "Berlokasi di lantai pertama"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,8 +32,8 @@
 | 
			
		|||
  "enablePdfDownload": true,
 | 
			
		||||
  "enableDownload": true,
 | 
			
		||||
  "hideFromOverview": true,
 | 
			
		||||
  "clustering": {
 | 
			
		||||
    "#": "Disable clustering for this theme",
 | 
			
		||||
  "clustering": {
 | 
			
		||||
    "maxZoom": 0
 | 
			
		||||
  },
 | 
			
		||||
  "layers": [
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +112,7 @@
 | 
			
		|||
          "geoJsonZoomLevel": 12,
 | 
			
		||||
          "isOsmCache": true
 | 
			
		||||
        },
 | 
			
		||||
        "minzoom": "13",
 | 
			
		||||
        "minzoom": 10,
 | 
			
		||||
        "icon": {
 | 
			
		||||
          "render": "circle:#FE6F32;./assets/themes/natuurpunt/trail.svg",
 | 
			
		||||
          "mappings": [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								assets/themes/postboxes/license_info.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								assets/themes/postboxes/license_info.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
[
 | 
			
		||||
  {
 | 
			
		||||
    "path": "post_office.svg",
 | 
			
		||||
    "license": "CC BY-SA 4.0",
 | 
			
		||||
    "authors": [
 | 
			
		||||
      "https://github.com/emojione/emojione/graphs/contributors"
 | 
			
		||||
    ],
 | 
			
		||||
    "sources": [
 | 
			
		||||
      "https://commons.wikimedia.org/wiki/File:Emojione_BW_1F3E4.svg",
 | 
			
		||||
      "https://github.com/emojione/emojione/blob/2.2.7/assets/svg_bw/1f3e4.svg"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "path": "postbox.svg",
 | 
			
		||||
    "license": "CC BY 4.0",
 | 
			
		||||
    "authors": [
 | 
			
		||||
      "Vincent Le Moign",
 | 
			
		||||
      "https://twitter.com/webalys"
 | 
			
		||||
    ],
 | 
			
		||||
    "sources": [
 | 
			
		||||
      "https://upload.wikimedia.org/wikipedia/commons/6/6d/726-postbox.svg",
 | 
			
		||||
      "http://emoji.streamlineicons.com"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										1
									
								
								assets/themes/postboxes/post_office.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/themes/postboxes/post_office.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" enable-background="new 0 0 64 64"><path d="m6.219 22.625h3.75v5.625h-3.75z"/><path d="m11.844 22.625h3.75v5.625h-3.75z"/><path d="m20.281 22.625h3.75v5.625h-3.75z"/><path d="m25.906 22.625h3.75v5.625h-3.75z"/><path d="m34.344 22.625h3.75v5.625h-3.75z"/><path d="m39.969 22.625h3.75v5.625h-3.75z"/><path d="m48.406 22.625h3.75v5.625h-3.75z"/><path d="m54.031 22.625h3.75v5.625h-3.75z"/><path d="m6.219 33.875h3.75v5.625h-3.75z"/><path d="m11.844 33.875h3.75v5.625h-3.75z"/><path d="m20.281 33.875h3.75v5.625h-3.75z"/><path d="m25.906 33.875h3.75v5.625h-3.75z"/><path d="m34.344 33.875h3.75v5.625h-3.75z"/><path d="m39.969 33.875h3.75v5.625h-3.75z"/><path d="m48.406 33.875h3.75v5.625h-3.75z"/><path d="m54.031 33.875h3.75v5.625h-3.75z"/><path d="m16.063 48.875h5.625v5.625h-5.625z"/><path d="m27.313 47h3.75v4.688h-3.75z"/><path d="m42.313 48.875h5.625v5.625h-5.625z"/><path d="m49.813 48.875h5.625v5.625h-5.625z"/><path d="m8.563 48.875h5.625v5.625h-5.625z"/><path d="m32.938 47h3.75v4.688h-3.75z"/><path d="m30.146 11.078c.163.041.365.054.642.054 1.018 0 1.646-.553 1.646-1.481 0-.835-.54-1.333-1.496-1.333-.389 0-.651.04-.791.081v2.679z"/><path d="m37.529 11.401c0 1.778.778 3.031 2.061 3.031 1.295 0 2.035-1.32 2.035-3.085 0-1.63-.728-3.03-2.048-3.03-1.295 0-2.048 1.319-2.048 3.084"/><path d="m62 17.938v-13.125c0-.516-.422-.938-.938-.938h-.938v-1.875h-56.249v1.875h-.937c-.516 0-.938.422-.938.938v13.125c0 .516.422.938.938.938v39.374c-.516 0-.938.422-.938.938v1.875c0 .515.422.937.938.937h58.125c.515 0 .937-.422.937-.937v-1.875c0-.516-.422-.938-.938-.938v-39.375c.516 0 .938-.422.938-.937m-57.187 12.187v-9.375h54.375v9.375h-54.375m54.375 1.875v9.375h-54.375v-9.375h54.375m-18.75 26.25v-11.25h-1.875v11.25h-1.875v-4.688h-3.75v4.688h-1.875v-4.688h-3.75v4.688h-1.875v-11.25h-1.875v11.25h-1.875v-1.875h-15v-9.375h15v-3.75h20.625v3.75h15v9.375h-15v1.875h-1.875m11.303-49.69v-1.725h6.509v1.725h-2.312v7.354h-1.922v-7.354h-2.275m-4.38 3.542c-1.407-.525-2.323-1.361-2.323-2.68 0-1.549 1.206-2.735 3.203-2.735.954 0 1.658.215 2.161.458l-.427 1.657c-.339-.176-.943-.431-1.771-.431-.831 0-1.231.404-1.231.875 0 .579.478.835 1.57 1.28 1.496.592 2.199 1.428 2.199 2.707 0 1.522-1.095 2.815-3.418 2.815-.967 0-1.923-.269-2.399-.554l.389-1.697c.517.284 1.308.567 2.124.567.879 0 1.344-.392 1.344-.983-.001-.566-.402-.888-1.421-1.279m-3.714-.821c0 2.976-1.683 4.782-4.158 4.782-2.513 0-3.982-2.034-3.982-4.621 0-2.721 1.621-4.754 4.12-4.754 2.602 0 4.02 2.087 4.02 4.593m-9.316-1.684c0 .889-.276 1.643-.779 2.155-.652.66-1.62.957-2.751.957-.252 0-.479-.015-.654-.041v3.246h-1.897v-8.957c.591-.107 1.419-.188 2.587-.188 1.183 0 2.023.243 2.59.728.54.457.904 1.211.904 2.1m-10.934-1.927h1.103c0 5.152-4.197 9.33-9.375 9.33s-9.375-4.178-9.375-9.33h4.413c0 .95.205 1.85.567 2.666.184-2.563 2.324-4.586 4.945-4.586 2.743 0 4.965 2.211 4.965 4.938 0 1.078-.352 2.072-.94 2.885 2.189-1.074 3.697-3.311 3.697-5.903"/><path d="m15.676 7.395c-1.823 0-3.309 1.476-3.309 3.293 0 1.819 1.485 3.293 3.309 3.293 1.827 0 3.31-1.474 3.31-3.293-.001-1.817-1.483-3.293-3.31-3.293"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3 KiB  | 
							
								
								
									
										143
									
								
								assets/themes/postboxes/postbox.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								assets/themes/postboxes/postbox.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,143 @@
 | 
			
		|||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
 | 
			
		||||
<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 | 
			
		||||
	 viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
 | 
			
		||||
<style type="text/css">
 | 
			
		||||
	.st0{opacity:0.15;fill:#45413C;}
 | 
			
		||||
	.st1{fill:#BF8256;}
 | 
			
		||||
	.st2{fill:#915E3A;}
 | 
			
		||||
	.st3{fill:none;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st4{fill:#DEA47A;}
 | 
			
		||||
	.st5{fill:#DAEDF7;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st6{fill:#BF8256;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st7{fill:#FF6242;}
 | 
			
		||||
	.st8{fill:#FF866E;}
 | 
			
		||||
	.st9{fill:#DEBB7E;}
 | 
			
		||||
	.st10{fill:#B89558;}
 | 
			
		||||
	.st11{fill:#656769;}
 | 
			
		||||
	.st12{fill:#525252;}
 | 
			
		||||
	.st13{fill:#E04122;}
 | 
			
		||||
	.st14{fill:#FFFFFF;}
 | 
			
		||||
	.st15{fill:#F0F0F0;}
 | 
			
		||||
	.st16{fill:#00B8F0;}
 | 
			
		||||
	.st17{fill:#4ACFFF;}
 | 
			
		||||
	.st18{fill:#C0DCEB;}
 | 
			
		||||
	.st19{fill:#8CA4B8;}
 | 
			
		||||
	.st20{fill:#FF6242;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st21{fill:#87898C;}
 | 
			
		||||
	.st22{fill:#E0E0E0;}
 | 
			
		||||
	.st23{fill:#E8F4FA;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st24{fill:#656769;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st25{fill:#DAEDF7;}
 | 
			
		||||
	.st26{fill:#E8F4FA;}
 | 
			
		||||
	.st27{fill:#ADC4D9;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st28{fill:#87898C;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st29{fill:#BDBEC0;}
 | 
			
		||||
	.st30{fill:#FFFFFF;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st31{fill:#ADC4D9;}
 | 
			
		||||
	.st32{fill:none;stroke:#00AED9;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st33{fill:#FFFACF;}
 | 
			
		||||
	.st34{fill:#FFE500;}
 | 
			
		||||
	.st35{fill:#915E3A;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st36{fill:#FFAA54;}
 | 
			
		||||
	.st37{fill:#627B8C;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st38{fill:#FFFEF2;}
 | 
			
		||||
	.st39{fill:#FFFCE5;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st40{fill:#00F5BC;}
 | 
			
		||||
	.st41{fill:#FFFCE5;}
 | 
			
		||||
	.st42{fill:#FFFEF2;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st43{fill:#8CFFE4;}
 | 
			
		||||
	.st44{fill:#FFF5E3;}
 | 
			
		||||
	.st45{fill:#F7E5C6;}
 | 
			
		||||
	.st46{fill:#F7E5C6;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st47{fill:#F0D5A8;}
 | 
			
		||||
	.st48{fill:#FF87AF;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st49{fill:#45413C;}
 | 
			
		||||
	.st50{fill:#BDBEC0;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st51{fill:#E0E0E0;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st52{fill:#DEBB7E;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st53{fill:#F5EBFF;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st54{fill:#BF8DF2;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st55{fill:#E4FFD1;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st56{fill:#F0FFE5;}
 | 
			
		||||
	.st57{fill:#C8FFA1;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st58{fill:#6DD627;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st59{fill:#E5FEFF;}
 | 
			
		||||
	.st60{fill:#FFAA54;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st61{fill:#9CEB60;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st62{fill:#FFF48C;}
 | 
			
		||||
	.st63{fill:#00B8F0;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st64{fill:none;stroke:#4F4B45;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st65{fill:#FFFFFF;stroke:#4F4B45;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st66{fill:#FFF5E3;stroke:#4F4B45;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st67{fill:#F7E5C6;stroke:#4F4B45;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st68{fill:#6DD627;}
 | 
			
		||||
	.st69{fill:#EBCB00;}
 | 
			
		||||
	.st70{fill:#46B000;}
 | 
			
		||||
	.st71{fill:none;stroke:#E0E0E0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st72{fill:#9CEB60;}
 | 
			
		||||
	.st73{fill:#FFCC99;}
 | 
			
		||||
	.st74{fill:#BF8DF2;}
 | 
			
		||||
	.st75{fill:#9F5AE5;}
 | 
			
		||||
	.st76{fill:#DABFF5;}
 | 
			
		||||
	.st77{fill:#F0F0F0;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st78{fill:#E5F8FF;}
 | 
			
		||||
	.st79{fill:#B8ECFF;}
 | 
			
		||||
	.st80{fill:#D9FDFF;}
 | 
			
		||||
	.st81{fill:#C0DCEB;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st82{fill:#46B000;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st83{fill:none;stroke:#FFFFFF;stroke-miterlimit:10;}
 | 
			
		||||
	.st84{fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st85{fill:#FFE500;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st86{fill:#80DDFF;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st87{fill:#FFFFFF;stroke:#45413C;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st88{fill:#009FD9;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st89{fill:#E04122;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st90{fill:#009FD9;}
 | 
			
		||||
	.st91{fill:#FFFFFF;stroke:#45413C;stroke-miterlimit:10;}
 | 
			
		||||
	.st92{fill:#009FD9;stroke:#45413C;stroke-miterlimit:10;}
 | 
			
		||||
	.st93{fill:#FF8A14;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st94{fill:none;stroke:#009FD9;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st95{fill:#525252;stroke:#45413C;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st96{fill:none;stroke:#46B000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st97{fill:none;stroke:#E04122;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st98{fill:#6DD627;stroke:#45413C;stroke-miterlimit:10;}
 | 
			
		||||
	.st99{fill:none;stroke:#45413C;stroke-linejoin:round;stroke-miterlimit:10;}
 | 
			
		||||
	.st100{fill:#46B000;stroke:#45413C;stroke-miterlimit:10;}
 | 
			
		||||
	.st101{fill:#00DFEB;}
 | 
			
		||||
	.st102{fill:#00AD85;}
 | 
			
		||||
	.st103{fill:#E04122;stroke:#45413C;stroke-miterlimit:10;}
 | 
			
		||||
</style>
 | 
			
		||||
<g id="XMLID_15623_">
 | 
			
		||||
	<ellipse id="XMLID_15640_" class="st0" cx="24" cy="44.4" rx="12.3" ry="1.6"/>
 | 
			
		||||
	<path id="XMLID_15639_" class="st7" d="M34,33.9H14c-1.8,0-3.2-1.4-3.2-3.2V6.1c0-1.8,1.4-3.2,3.2-3.2h20c1.8,0,3.2,1.4,3.2,3.2
 | 
			
		||||
		v24.6C37.2,32.5,35.8,33.9,34,33.9z"/>
 | 
			
		||||
	<path id="XMLID_15638_" class="st8" d="M34,2.9H14c-1.8,0-3.2,1.4-3.2,3.2v2.5c0-1.8,1.4-3.2,3.2-3.2h20c1.8,0,3.2,1.4,3.2,3.2V6.1
 | 
			
		||||
		C37.2,4.3,35.8,2.9,34,2.9z"/>
 | 
			
		||||
	<path id="XMLID_15637_" class="st3" d="M34,33.9H14c-1.8,0-3.2-1.4-3.2-3.2V6.1c0-1.8,1.4-3.2,3.2-3.2h20c1.8,0,3.2,1.4,3.2,3.2
 | 
			
		||||
		v24.6C37.2,32.5,35.8,33.9,34,33.9z"/>
 | 
			
		||||
	<path id="XMLID_15636_" class="st9" d="M26.2,33.9h-4.5v9.5c0,0.6,0.5,1.1,1.1,1.1h2.3c0.6,0,1.1-0.5,1.1-1.1V33.9z"/>
 | 
			
		||||
	<rect id="XMLID_15635_" x="21.8" y="33.9" class="st10" width="4.5" height="2.7"/>
 | 
			
		||||
	<path id="XMLID_15634_" class="st3" d="M26.2,33.9h-4.5v9.5c0,0.6,0.5,1.1,1.1,1.1h2.3c0.6,0,1.1-0.5,1.1-1.1V33.9z"/>
 | 
			
		||||
	<path id="XMLID_15633_" class="st11" d="M32.5,14.5h-17c-0.6,0-1.1-0.5-1.1-1.1v-1.3c0-0.6,0.5-1.1,1.1-1.1h17
 | 
			
		||||
		c0.6,0,1.1,0.5,1.1,1.1v1.3C33.6,14.1,33.1,14.5,32.5,14.5z"/>
 | 
			
		||||
	<path id="XMLID_15632_" class="st12" d="M32.5,11.1h-17c-0.6,0-1.1,0.5-1.1,1.1v1.3c0-0.6,0.5-1.1,1.1-1.1h17
 | 
			
		||||
		c0.6,0,1.1,0.5,1.1,1.1v-1.3C33.6,11.5,33.1,11.1,32.5,11.1z"/>
 | 
			
		||||
	<path id="XMLID_15631_" class="st3" d="M32.5,14.5h-17c-0.6,0-1.1-0.5-1.1-1.1v-1.3c0-0.6,0.5-1.1,1.1-1.1h17
 | 
			
		||||
		c0.6,0,1.1,0.5,1.1,1.1v1.3C33.6,14.1,33.1,14.5,32.5,14.5z"/>
 | 
			
		||||
	<path class="st13" d="M33.9,6.6H14.1c-0.6,0-1.1,0.5-1.1,1.1c0,0.6,0.5,1.1,1.1,1.1h19.8c0.6,0,1.1-0.5,1.1-1.1
 | 
			
		||||
		C35,7.1,34.5,6.6,33.9,6.6z"/>
 | 
			
		||||
	<path id="XMLID_15630_" class="st7" d="M14.1,7.7h19.8c0.4,0,0.8,0.2,1,0.6C35,8.1,35,7.9,35,7.7c0-0.6-0.5-1.1-1.1-1.1H14.1
 | 
			
		||||
		c-0.6,0-1.1,0.5-1.1,1.1c0,0.2,0.1,0.4,0.1,0.6C13.3,7.9,13.7,7.7,14.1,7.7z"/>
 | 
			
		||||
	<path class="st3" d="M33.9,6.6H14.1c-0.6,0-1.1,0.5-1.1,1.1c0,0.6,0.5,1.1,1.1,1.1h19.8c0.6,0,1.1-0.5,1.1-1.1
 | 
			
		||||
		C35,7.1,34.5,6.6,33.9,6.6z"/>
 | 
			
		||||
	<path id="XMLID_15629_" class="st14" d="M33.6,21.2h-9.8c-0.4,0-0.8-0.2-1-0.6l-4.3-9.5h11.3l4.2,9.3
 | 
			
		||||
		C34.2,20.8,33.9,21.2,33.6,21.2z"/>
 | 
			
		||||
	<polygon id="XMLID_15628_" class="st15" points="30.9,13.3 29.8,11.1 18.5,11.1 19.6,13.3 	"/>
 | 
			
		||||
	<path id="XMLID_15627_" class="st3" d="M33.6,21.2h-9.8c-0.4,0-0.8-0.2-1-0.6l-4.3-9.5h11.3l4.2,9.3C34.2,20.8,33.9,21.2,33.6,21.2
 | 
			
		||||
		z"/>
 | 
			
		||||
	<polyline id="XMLID_15626_" class="st3" points="34,20.9 26.5,14.7 27.3,11.1 	"/>
 | 
			
		||||
	<line id="XMLID_15625_" class="st3" x1="27.6" y1="15.7" x2="23.2" y2="21"/>
 | 
			
		||||
	<line id="XMLID_15624_" class="st3" x1="26.7" y1="13.7" x2="21.5" y2="11.1"/>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 8.8 KiB  | 
							
								
								
									
										174
									
								
								assets/themes/postboxes/postboxes.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								assets/themes/postboxes/postboxes.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,174 @@
 | 
			
		|||
{
 | 
			
		||||
  "id": "postboxes",
 | 
			
		||||
  "title": {
 | 
			
		||||
    "en": "Postbox and Post Office Map"
 | 
			
		||||
  },
 | 
			
		||||
  "shortDescription": {
 | 
			
		||||
    "en": "A map showing postboxes and post offices"
 | 
			
		||||
  },
 | 
			
		||||
  "description": {
 | 
			
		||||
    "en": "On this map you can find and add data of post offices and post boxes. You can use this map to find where you can mail your next postcard! :)<br/>Spotted an error or is a post box missing? You can edit this map with a free OpenStreetMap account. "
 | 
			
		||||
  },
 | 
			
		||||
  "language": [
 | 
			
		||||
    "en"
 | 
			
		||||
  ],
 | 
			
		||||
  "maintainer": "",
 | 
			
		||||
  "icon": "./assets/themes/postboxes/postbox.svg",
 | 
			
		||||
  "version": "0",
 | 
			
		||||
  "startLat": 53.5511,
 | 
			
		||||
  "startLon": 9.9937,
 | 
			
		||||
  "startZoom": 13,
 | 
			
		||||
  "widenFactor": 1.5,
 | 
			
		||||
  "defaultBackgroundId": "CartoDB.Voyager",
 | 
			
		||||
  "clustering": {
 | 
			
		||||
    "maxZoom": 14,
 | 
			
		||||
    "minNeededElements": 100
 | 
			
		||||
  },
 | 
			
		||||
  "layers": [
 | 
			
		||||
    {
 | 
			
		||||
      "id": "postboxes",
 | 
			
		||||
      "name": {
 | 
			
		||||
        "en": "Postboxes"
 | 
			
		||||
      },
 | 
			
		||||
      "minzoom": 12,
 | 
			
		||||
      "source": {
 | 
			
		||||
        "osmTags": "amenity=post_box"
 | 
			
		||||
      },
 | 
			
		||||
      "title": {
 | 
			
		||||
        "render": {
 | 
			
		||||
          "en": "Postbox"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "description": {
 | 
			
		||||
        "en": "The layer showing postboxes."
 | 
			
		||||
      },
 | 
			
		||||
      "tagRenderings": [
 | 
			
		||||
        "images",
 | 
			
		||||
        {
 | 
			
		||||
          "id": "minimap",
 | 
			
		||||
          "render": "{minimap(18): height: 5rem; overflow: hidden; border-radius:3rem; }"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "icon": {
 | 
			
		||||
        "render": "./assets/themes/postboxes/postbox.svg"
 | 
			
		||||
      },
 | 
			
		||||
      "width": {
 | 
			
		||||
        "render": "1"
 | 
			
		||||
      },
 | 
			
		||||
      "iconSize": {
 | 
			
		||||
        "render": "40,40,bottom"
 | 
			
		||||
      },
 | 
			
		||||
      "color": {
 | 
			
		||||
        "render": "#DADADA"
 | 
			
		||||
      },
 | 
			
		||||
      "presets": [
 | 
			
		||||
        {
 | 
			
		||||
          "tags": [
 | 
			
		||||
            "amenity=post_box"
 | 
			
		||||
          ],
 | 
			
		||||
          "title": {
 | 
			
		||||
            "en": "postbox"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "wayHandling": 2,
 | 
			
		||||
      "deletion": {
 | 
			
		||||
        "softDeletionTags": {
 | 
			
		||||
          "and": [
 | 
			
		||||
            "amenity=",
 | 
			
		||||
            "razed:amenity=post_box"
 | 
			
		||||
          ]
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "id": "postoffices",
 | 
			
		||||
      "name": {
 | 
			
		||||
        "en": "Post offices"
 | 
			
		||||
      },
 | 
			
		||||
      "minzoom": 12,
 | 
			
		||||
      "source": {
 | 
			
		||||
        "osmTags": "amenity=post_office"
 | 
			
		||||
      },
 | 
			
		||||
      "title": {
 | 
			
		||||
        "render": {
 | 
			
		||||
          "en": "Post Office"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "description": {
 | 
			
		||||
        "en": "A layer showing post offices."
 | 
			
		||||
      },
 | 
			
		||||
      "tagRenderings": [
 | 
			
		||||
        "images",
 | 
			
		||||
        {
 | 
			
		||||
          "id": "minimap",
 | 
			
		||||
          "render": "{minimap(18): height: 5rem; overflow: hidden; border-radius:3rem; }"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "render": {
 | 
			
		||||
            "en": "Opening Hours: {opening_hours_table()}"
 | 
			
		||||
          },
 | 
			
		||||
          "freeform": {
 | 
			
		||||
            "key": "opening_hours",
 | 
			
		||||
            "type": "opening_hours"
 | 
			
		||||
          },
 | 
			
		||||
          "question": {
 | 
			
		||||
            "en": "What are the opening hours for this post office?"
 | 
			
		||||
          },
 | 
			
		||||
          "mappings": [
 | 
			
		||||
            {
 | 
			
		||||
              "if": "opening_hours=24/7",
 | 
			
		||||
              "then": {
 | 
			
		||||
                "en": "24/7 opened (including holidays)"
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          ],
 | 
			
		||||
          "id": "OH"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "icon": {
 | 
			
		||||
        "render": "square:white;./assets/themes/postboxes/post_office.svg"
 | 
			
		||||
      },
 | 
			
		||||
      "iconOverlays": [
 | 
			
		||||
        {
 | 
			
		||||
          "if": "opening_hours~*",
 | 
			
		||||
          "then": "isOpen",
 | 
			
		||||
          "badge": true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "width": {
 | 
			
		||||
        "render": "1"
 | 
			
		||||
      },
 | 
			
		||||
      "iconSize": {
 | 
			
		||||
        "render": "40,40,bottom"
 | 
			
		||||
      },
 | 
			
		||||
      "color": {
 | 
			
		||||
        "render": "#DADADA"
 | 
			
		||||
      },
 | 
			
		||||
      "presets": [
 | 
			
		||||
        {
 | 
			
		||||
          "tags": [
 | 
			
		||||
            "amenity=post_office"
 | 
			
		||||
          ],
 | 
			
		||||
          "title": {
 | 
			
		||||
            "en": "Post Office"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "wayHandling": 2,
 | 
			
		||||
      "filter": [
 | 
			
		||||
        {
 | 
			
		||||
          "id": "is_open",
 | 
			
		||||
          "options": [
 | 
			
		||||
            {
 | 
			
		||||
              "question": {
 | 
			
		||||
                "en": "Currently open"
 | 
			
		||||
              },
 | 
			
		||||
              "osmTags": "_isOpen=yes"
 | 
			
		||||
            }
 | 
			
		||||
          ]
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,10 @@
 | 
			
		|||
    "nl": "Een kaart om toeristisch relevante info op aan te duiden"
 | 
			
		||||
  },
 | 
			
		||||
  "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",
 | 
			
		||||
  "startZoom": 8,
 | 
			
		||||
| 
						 | 
				
			
			@ -28,16 +31,38 @@
 | 
			
		|||
        "cafe_pub"
 | 
			
		||||
      ],
 | 
			
		||||
      "override": {
 | 
			
		||||
        "minzoom": 16
 | 
			
		||||
        "minzoom": 17
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "charging_station",
 | 
			
		||||
    "toilet",
 | 
			
		||||
    "bench",
 | 
			
		||||
    "waste_basket",
 | 
			
		||||
    "bike_repair_station",
 | 
			
		||||
    "binocular",
 | 
			
		||||
    "observation_tower"
 | 
			
		||||
    {
 | 
			
		||||
      "builtin": [
 | 
			
		||||
        "bench",
 | 
			
		||||
        "picnic_table",
 | 
			
		||||
        "waste_basket"
 | 
			
		||||
      ],
 | 
			
		||||
      "override": {
 | 
			
		||||
        "minzoom": 19
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "builtin": [
 | 
			
		||||
        "charging_station",
 | 
			
		||||
        "toilet",
 | 
			
		||||
        "bike_repair_station"
 | 
			
		||||
      ],
 | 
			
		||||
      "override": {
 | 
			
		||||
        "minzoom": 14
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "builtin": [
 | 
			
		||||
        "binocular",
 | 
			
		||||
        "observation_tower"
 | 
			
		||||
      ],
 | 
			
		||||
      "override": {
 | 
			
		||||
        "minzoom": 10
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "hideFromOverview": true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								index.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -63,7 +63,6 @@ if (path !== "index.html" && path !== "") {
 | 
			
		|||
defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout, "The layout to load into MapComplete").data;
 | 
			
		||||
let layoutToUse: LayoutConfig = AllKnownLayouts.allKnownLayouts.get(defaultLayout.toLowerCase());
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const userLayoutParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme");
 | 
			
		||||
 | 
			
		||||
// Workaround/legacy to keep the old paramters working as I renamed some of them
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,16 +36,6 @@
 | 
			
		|||
        "splitTitle": "Choose on the map where to split this road",
 | 
			
		||||
        "hasBeenSplit": "This way has been split"
 | 
			
		||||
    },
 | 
			
		||||
    "move": {
 | 
			
		||||
        "loginToMove": "You must be logged in to move a point",
 | 
			
		||||
        "inviteToMove": "Move this point",
 | 
			
		||||
        "selectReason": "Why do you move this object?",
 | 
			
		||||
        "reasonRelocation": "The object has been relocated to a totally different location",
 | 
			
		||||
        "reasonInaccurate": "The location of this object is inaccurate and should be moved a few meter",
 | 
			
		||||
        "onlyPoints": "Only points can be moved",
 | 
			
		||||
        "isWay": "This feature is a way",
 | 
			
		||||
        "cancel": "Cancel move"
 | 
			
		||||
    },
 | 
			
		||||
    "delete": {
 | 
			
		||||
        "delete": "Delete",
 | 
			
		||||
        "cancel": "Cancel",
 | 
			
		||||
| 
						 | 
				
			
			@ -130,7 +120,7 @@
 | 
			
		|||
        },
 | 
			
		||||
        "morescreen": {
 | 
			
		||||
            "intro": "<h3>More thematic maps?</h3>Do you enjoy collecting geodata? <br/>There are more themes available.",
 | 
			
		||||
            "requestATheme": "If you want a custom-built quest, request it in the issue tracker",
 | 
			
		||||
            "requestATheme": "If you want a custom-built theme, request it in the issue tracker",
 | 
			
		||||
            "streetcomplete": "Another, similar application is <a href='https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete' class='underline hover:text-blue-800' class='underline hover:text-blue-800' target='_blank'>StreetComplete</a>.",
 | 
			
		||||
            "createYourOwnTheme": "Create your own MapComplete theme from scratch"
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			@ -231,7 +221,7 @@
 | 
			
		|||
        },
 | 
			
		||||
        "wikipedia": {
 | 
			
		||||
            "wikipediaboxTitle": "Wikipedia",
 | 
			
		||||
            "failed":"Loading the wikipedia entry failed",
 | 
			
		||||
            "failed": "Loading the wikipedia entry failed",
 | 
			
		||||
            "loading": "Loading Wikipedia...",
 | 
			
		||||
            "noWikipediaPage": "This wikidata item has no corresponding wikipedia page yet."
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -256,5 +246,15 @@
 | 
			
		|||
        "tos": "If you create a review, you agree to <a href='https://mangrove.reviews/terms' target='_blank'>the TOS and privacy policy of Mangrove.reviews</a>",
 | 
			
		||||
        "attribution": "Reviews are powered by <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> and are available under <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.",
 | 
			
		||||
        "plz_login": "Login to leave a review"
 | 
			
		||||
    },
 | 
			
		||||
    "move": {
 | 
			
		||||
        "loginToMove": "You must be logged in to move a point",
 | 
			
		||||
        "inviteToMove": "Move this point",
 | 
			
		||||
        "selectReason": "Why do you move this object?",
 | 
			
		||||
        "reasonRelocation": "The object has been relocated to a totally different location",
 | 
			
		||||
        "reasonInaccurate": "The location of this object is inaccurate and should be moved a few meter",
 | 
			
		||||
        "onlyPoints": "Only points can be moved",
 | 
			
		||||
        "isWay": "This feature is a way",
 | 
			
		||||
        "cancel": "Cancel move"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -20,5 +20,20 @@
 | 
			
		|||
        "uploadingMultiple": "A enviar {count} imagens…",
 | 
			
		||||
        "uploadingPicture": "A enviar a sua 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"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,25 @@
 | 
			
		|||
        "email": {
 | 
			
		||||
            "question": "Apa alamat surel dari {name}?"
 | 
			
		||||
        },
 | 
			
		||||
        "level": {
 | 
			
		||||
            "mappings": {
 | 
			
		||||
                "3": {
 | 
			
		||||
                    "then": "Berlokasi di lantai pertama"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "question": "Pada tingkat apa fitur ini diletakkan?"
 | 
			
		||||
        },
 | 
			
		||||
        "payment-options": {
 | 
			
		||||
            "mappings": {
 | 
			
		||||
                "0": {
 | 
			
		||||
                    "then": "Disini menerima pembayaran tunai"
 | 
			
		||||
                },
 | 
			
		||||
                "1": {
 | 
			
		||||
                    "then": "Disini menerima pembayaran dengan kartu"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "question": "Metode pembayaran manakah yang di terima disini?"
 | 
			
		||||
        },
 | 
			
		||||
        "phone": {
 | 
			
		||||
            "question": "Nomor telepon dari {name|?"
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,23 @@
 | 
			
		|||
        "description": {
 | 
			
		||||
            "question": "Ainda há algo de relevante que não tenha podido dar nas perguntas anteriores? Adicione-o aqui.<br/><span style='font-size: small'>Não repita factos já declarados</span>"
 | 
			
		||||
        },
 | 
			
		||||
        "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?"
 | 
			
		||||
        },
 | 
			
		||||
        "email": {
 | 
			
		||||
            "question": "Qual é o endereço de e-mail de {name}?"
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1371,5 +1371,56 @@
 | 
			
		|||
        "description": "On this map, you'll find waste baskets near you. If a waste basket is missing on this map, you can add it yourself",
 | 
			
		||||
        "shortDescription": "A map with waste baskets",
 | 
			
		||||
        "title": "Waste Basket"
 | 
			
		||||
    },
 | 
			
		||||
    "postboxes": {
 | 
			
		||||
        "description": "On this map you can find and add data of post offices and post boxes. You can use this map to find where you can mail your next postcard! :)<br/>Spotted an error or is a post box missing? You can edit this map with a free OpenStreetMap account. ",
 | 
			
		||||
        "layers": {
 | 
			
		||||
            "0": {
 | 
			
		||||
                "description": "The layer showing postboxes.",
 | 
			
		||||
                "name": "Postboxes",
 | 
			
		||||
                "presets": {
 | 
			
		||||
                    "0": {
 | 
			
		||||
                        "title": "postbox"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "title": {
 | 
			
		||||
                    "render": "Postbox"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "1": {
 | 
			
		||||
                "description": "A layer showing post offices.",
 | 
			
		||||
                "filter": {
 | 
			
		||||
                    "0": {
 | 
			
		||||
                        "options": {
 | 
			
		||||
                            "0": {
 | 
			
		||||
                                "question": "Currently open"
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "name": "Post offices",
 | 
			
		||||
                "presets": {
 | 
			
		||||
                    "0": {
 | 
			
		||||
                        "title": "Post Office"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "tagRenderings": {
 | 
			
		||||
                    "OH": {
 | 
			
		||||
                        "mappings": {
 | 
			
		||||
                            "0": {
 | 
			
		||||
                                "then": "24/7 opened (including holidays)"
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        "question": "What are the opening hours for this post office?",
 | 
			
		||||
                        "render": "Opening Hours: {opening_hours_table()}"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "title": {
 | 
			
		||||
                    "render": "Post Office"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "shortDescription": "A map showing postboxes and post offices",
 | 
			
		||||
        "title": "Postbox and Post Office Map"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1025,6 +1025,7 @@
 | 
			
		|||
    },
 | 
			
		||||
    "toerisme_vlaanderen": {
 | 
			
		||||
        "description": "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' />",
 | 
			
		||||
        "descriptionTail": "Met de steun van Toerisme Vlaanderen<img style='height:5rem; width: auto;' src='./assets/themes/toerisme_vlaanderen/logo.png' />",
 | 
			
		||||
        "shortDescription": "Een kaart om toeristisch relevante info op aan te duiden",
 | 
			
		||||
        "title": "Toeristisch relevante info"
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import FeatureSource, {FeatureSourceForLayer} from "../Logic/FeatureSource/Featu
 | 
			
		|||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
 | 
			
		||||
import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource";
 | 
			
		||||
import Constants from "../Models/Constants";
 | 
			
		||||
import {GeoOperations} from "../Logic/GeoOperations";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ScriptUtils.fixUtils()
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +74,7 @@ async function downloadRaw(targetdir: string, r: TileRange, theme: LayoutConfig,
 | 
			
		|||
    let downloaded = 0
 | 
			
		||||
    let failed = 0
 | 
			
		||||
    let skipped = 0
 | 
			
		||||
    const startTime = new Date().getTime()
 | 
			
		||||
    for (let x = r.xstart; x <= r.xend; x++) {
 | 
			
		||||
        for (let y = r.ystart; y <= r.yend; y++) {
 | 
			
		||||
            downloaded++;
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +84,11 @@ async function downloadRaw(targetdir: string, r: TileRange, theme: LayoutConfig,
 | 
			
		|||
                skipped++
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            console.log("x:", (x - r.xstart), "/", (r.xend - r.xstart), "; y:", (y - r.ystart), "/", (r.yend - r.ystart), "; total: ", downloaded, "/", r.total, "failed: ", failed, "skipped: ", skipped)
 | 
			
		||||
            const runningSeconds = (new Date().getTime() - startTime) / 1000
 | 
			
		||||
            const resting = failed + (r.total - downloaded)
 | 
			
		||||
            const perTile=   (runningSeconds / (downloaded - skipped))
 | 
			
		||||
            const estimated =Math.floor(resting * perTile)
 | 
			
		||||
            console.log("total: ", downloaded, "/", r.total, "failed: ", failed, "skipped: ", skipped, "running time: ",Utils.toHumanTime(runningSeconds)+"s", "estimated left: ", Utils.toHumanTime(estimated), "("+Math.floor(perTile)+"s/tile)")
 | 
			
		||||
 | 
			
		||||
            const boundsArr = Tiles.tile_bounds(r.zoomlevel, x, y)
 | 
			
		||||
            const bounds = {
 | 
			
		||||
| 
						 | 
				
			
			@ -91,7 +97,7 @@ async function downloadRaw(targetdir: string, r: TileRange, theme: LayoutConfig,
 | 
			
		|||
                east: Math.max(boundsArr[0][1], boundsArr[1][1]),
 | 
			
		||||
                west: Math.min(boundsArr[0][1], boundsArr[1][1])
 | 
			
		||||
            }
 | 
			
		||||
            const overpass = createOverpassObject(theme, relationTracker, Constants.defaultOverpassUrls[(downloaded + failed) % Constants.defaultOverpassUrls.length])
 | 
			
		||||
            const overpass = createOverpassObject(theme, relationTracker, Constants.defaultOverpassUrls[(failed) % Constants.defaultOverpassUrls.length])
 | 
			
		||||
            const url = overpass.buildQuery("[bbox:" + bounds.south + "," + bounds.west + "," + bounds.north + "," + bounds.east + "]")
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
| 
						 | 
				
			
			@ -170,11 +176,11 @@ function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extr
 | 
			
		|||
/**
 | 
			
		||||
 * Load all the tiles into memory from disk
 | 
			
		||||
 */
 | 
			
		||||
function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string, pointsOnlyLayers: string[]) {
 | 
			
		||||
    function handleLayer(source: FeatureSourceForLayer) {
 | 
			
		||||
        const layer = source.layer.layerDef;
 | 
			
		||||
        const targetZoomLevel = layer.source.geojsonZoomLevel ?? 0
 | 
			
		||||
        
 | 
			
		||||
        const layerId = layer.id
 | 
			
		||||
        if (layer.source.isOsmCacheLayer !== true) {
 | 
			
		||||
            return;
 | 
			
		||||
| 
						 | 
				
			
			@ -200,13 +206,10 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT
 | 
			
		|||
        // At this point, we have all the features of the entire area.
 | 
			
		||||
        // However, we want to export them per tile of a fixed size, so we use a dynamicTileSOurce to split it up
 | 
			
		||||
        TiledFeatureSource.createHierarchy(source, {
 | 
			
		||||
            minZoomLevel: 14,
 | 
			
		||||
            maxZoomLevel: 14,
 | 
			
		||||
            minZoomLevel: targetZoomLevel,
 | 
			
		||||
            maxZoomLevel: targetZoomLevel,
 | 
			
		||||
            maxFeatureCount: undefined,
 | 
			
		||||
            registerTile: tile => {
 | 
			
		||||
                if (tile.z < 12) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if (tile.features.data.length === 0) {
 | 
			
		||||
                    return
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -229,7 +232,7 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT
 | 
			
		|||
 | 
			
		||||
        // All the tiles are written at this point
 | 
			
		||||
        // Only thing left to do is to create the index
 | 
			
		||||
        const path = targetdir + "_" + layerId + "_overview.json"
 | 
			
		||||
        const path = targetdir + "_" + layerId + "_" + targetZoomLevel + "_overview.json"
 | 
			
		||||
        const perX = {}
 | 
			
		||||
        createdTiles.map(i => Tiles.tile_from_index(i)).forEach(([z, x, y]) => {
 | 
			
		||||
            const key = "" + x
 | 
			
		||||
| 
						 | 
				
			
			@ -240,7 +243,18 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT
 | 
			
		|||
        })
 | 
			
		||||
        writeFileSync(path, JSON.stringify(perX))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // And, if needed, to create a points-only layer
 | 
			
		||||
        if(pointsOnlyLayers.indexOf(layer.id) >= 0){
 | 
			
		||||
            const features = source.features.data.map(f => f.feature)
 | 
			
		||||
            const points = features.map(feature => GeoOperations.centerpoint(feature))
 | 
			
		||||
            console.log("Writing points overview for ", layerId)
 | 
			
		||||
            const targetPath = targetdir+"_"+layerId+"_points.geojson"
 | 
			
		||||
            // This is the geojson file containing all features for this tile
 | 
			
		||||
            writeFileSync(targetPath, JSON.stringify({
 | 
			
		||||
                type: "FeatureCollection",
 | 
			
		||||
                features: points
 | 
			
		||||
            }, null, " "))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    new PerLayerFeatureSourceSplitter(
 | 
			
		||||
| 
						 | 
				
			
			@ -255,10 +269,11 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async function main(args: string[]) {
 | 
			
		||||
 | 
			
		||||
    if (args.length == 0) {
 | 
			
		||||
        console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name]")
 | 
			
		||||
        console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...]")
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const themeName = args[0]
 | 
			
		||||
| 
						 | 
				
			
			@ -269,6 +284,12 @@ async function main(args: string[]) {
 | 
			
		|||
    const lat1 = Number(args[5])
 | 
			
		||||
    const lon1 = Number(args[6])
 | 
			
		||||
    
 | 
			
		||||
    let generatePointLayersFor = []
 | 
			
		||||
    if(args[7] == "--generate-point-overview"){
 | 
			
		||||
        generatePointLayersFor = args[8].split(",")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    const tileRange = Tiles.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1)
 | 
			
		||||
 | 
			
		||||
    const theme = AllKnownLayouts.allKnownLayouts.get(themeName)
 | 
			
		||||
| 
						 | 
				
			
			@ -293,7 +314,7 @@ async function main(args: string[]) {
 | 
			
		|||
 | 
			
		||||
    const extraFeatures = await downloadExtraData(theme);
 | 
			
		||||
    const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures)
 | 
			
		||||
    postProcess(allFeaturesSource, theme, relationTracker, targetdir)
 | 
			
		||||
    sliceToTiles(allFeaturesSource, theme, relationTracker, targetdir, generatePointLayersFor)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue