Full code cleanup
|
@ -1,4 +1,3 @@
|
|||
|
||||
Metatags
|
||||
==========
|
||||
|
||||
|
@ -6,9 +5,11 @@
|
|||
|
||||
Metatags are extra tags available, in order to display more data or to give better questions.
|
||||
|
||||
The are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.
|
||||
The are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an
|
||||
overview of the available metatags.
|
||||
|
||||
**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object
|
||||
**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a
|
||||
box in the popup for features which shows all the properties of the object
|
||||
|
||||
|
||||
Metatags calculated by MapComplete
|
||||
|
@ -16,104 +17,64 @@ The are calculated automatically on every feature when the data arrives in the w
|
|||
|
||||
|
||||
|
||||
The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme
|
||||
|
||||
The following values are always calculated, by default, by MapComplete and are available automatically on all elements
|
||||
in every theme
|
||||
|
||||
### _lat, _lon
|
||||
|
||||
|
||||
|
||||
The latitude and longitude of the point (or centerpoint in the case of a way/area)
|
||||
|
||||
|
||||
|
||||
|
||||
### _layer
|
||||
|
||||
|
||||
|
||||
The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined.
|
||||
|
||||
|
||||
|
||||
The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is
|
||||
defined.
|
||||
|
||||
### _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
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
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`)
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
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')
|
||||
|
||||
This is a lazy metatag and is only calculated when needed
|
||||
|
||||
|
||||
### _direction:numerical, _direction:leftright
|
||||
|
||||
|
||||
|
||||
_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
|
||||
|
||||
|
||||
|
||||
_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
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
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, _backend
|
||||
|
||||
|
||||
|
||||
Information about the last edit of this object.
|
||||
|
||||
|
||||
|
||||
|
||||
### sidewalk:left, sidewalk:right, generic_key:left:property, generic_key:right:property
|
||||
|
||||
|
||||
|
||||
Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined
|
||||
Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and
|
||||
similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much
|
||||
unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined
|
||||
|
||||
|
||||
|
||||
|
@ -123,20 +84,18 @@ Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' an
|
|||
|
||||
|
||||
|
||||
In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. `lat`, `lon`, `_country`), as detailed above.
|
||||
In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by
|
||||
default (e.g. `lat`, `lon`, `_country`), as detailed above.
|
||||
|
||||
It is also possible to calculate your own tags - but this requires some javascript knowledge.
|
||||
|
||||
|
||||
|
||||
Before proceeding, some warnings:
|
||||
|
||||
|
||||
|
||||
- DO NOT DO THIS AS BEGINNER
|
||||
- **Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to calculate a specific value
|
||||
- **THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs.
|
||||
|
||||
- **Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to
|
||||
calculate a specific value
|
||||
- **THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the
|
||||
internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs.
|
||||
|
||||
To enable this feature, add a field `calculatedTags` in the layer object, e.g.:
|
||||
|
||||
|
@ -154,16 +113,12 @@ To enable this feature, add a field `calculatedTags` in the layer object, e.g.:
|
|||
|
||||
````
|
||||
|
||||
|
||||
|
||||
The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:
|
||||
|
||||
|
||||
The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended
|
||||
geojson object:
|
||||
|
||||
- `area` contains the surface area (in square meters) of the object
|
||||
- `lat` and `lon` contain the latitude and longitude
|
||||
|
||||
|
||||
Some advanced functions are available on **feat** as well:
|
||||
|
||||
- [distanceTo](#distanceTo)
|
||||
|
@ -175,33 +130,44 @@ Some advanced functions are available on **feat** as well:
|
|||
|
||||
### 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
|
||||
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. feature OR featureID OR longitude
|
||||
1. undefined OR latitude
|
||||
|
||||
### overlapWith
|
||||
|
||||
Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.If the current feature is a point, all features that this point is embeded in are given.
|
||||
Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded
|
||||
in the feature is detected as well.If the current feature is a point, all features that this point is embeded in are
|
||||
given.
|
||||
|
||||
The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.
|
||||
The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list
|
||||
The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in
|
||||
m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature
|
||||
is a point. The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be
|
||||
the first in the list
|
||||
|
||||
For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`
|
||||
For example to get all objects which overlap or embed from a layer,
|
||||
use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`
|
||||
|
||||
0. ...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)
|
||||
|
||||
### 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. Returns a single geojson feature or undefined if nothing is found (or not yet laoded)
|
||||
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 or a layer name or '*' to get all 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)
|
||||
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)
|
||||
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 or '*' to get all features
|
||||
1. amount of features
|
||||
|
@ -214,11 +180,8 @@ If a 'unique tag key' is given, the tag with this key will only appear once (e.g
|
|||
|
||||
For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`
|
||||
|
||||
|
||||
|
||||
### get
|
||||
|
||||
Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ...
|
||||
|
||||
0. key
|
||||
Generated from SimpleMetaTagger, ExtraFunction
|
||||
0. key Generated from SimpleMetaTagger, ExtraFunction
|
|
@ -1,32 +1,42 @@
|
|||
|
||||
Rights of contributors
|
||||
======================
|
||||
|
||||
If a contributor is quite active within MapComplete, this contributor might be granted access to the main repository.
|
||||
|
||||
If you have access to the repository, you can make a fork of an already existing branch and push this new branch to github.
|
||||
This means that this branch will be _automatically built_ and be **deployed** to `https://pietervdvn.github.io/mc/<branchname>`. You can see the deploy process on [Github Actions](https://github.com/pietervdvn/MapComplete/actions).
|
||||
Don't worry about pushing too much. These deploys are free and totally automatic. They might fail if something is wrong, but this will hinder no-one.
|
||||
If you have access to the repository, you can make a fork of an already existing branch and push this new branch to
|
||||
github. This means that this branch will be _automatically built_ and be **deployed**
|
||||
to `https://pietervdvn.github.io/mc/<branchname>`. You can see the deploy process
|
||||
on [Github Actions](https://github.com/pietervdvn/MapComplete/actions). Don't worry about pushing too much. These
|
||||
deploys are free and totally automatic. They might fail if something is wrong, but this will hinder no-one.
|
||||
|
||||
Additionaly, some other maintainer might step in and merge the latest develop with your branch, making later pull requests easier.
|
||||
Additionaly, some other maintainer might step in and merge the latest develop with your branch, making later pull
|
||||
requests easier.
|
||||
|
||||
Don't worry about bugs
|
||||
----------------------
|
||||
|
||||
As a non-admin contributor, you can _not_ make changes to the `master` nor to the `develop` branch. This is because, as soon as master is changed, this is built and deployed on `mapcomplete.osm.be`, which a lot of people use. An error there will cause a lot of grieve.
|
||||
As a non-admin contributor, you can _not_ make changes to the `master` nor to the `develop` branch. This is because, as
|
||||
soon as master is changed, this is built and deployed on `mapcomplete.osm.be`, which a lot of people use. An error there
|
||||
will cause a lot of grieve.
|
||||
|
||||
A push on `develop` is automatically deployed to [pietervdvn.github.io/mc/develop] and is used by quite some people to. People using this version should know that this is a testing ground for new features and might contain a bug every now and then.
|
||||
A push on `develop` is automatically deployed to [pietervdvn.github.io/mc/develop] and is used by quite some people to.
|
||||
People using this version should know that this is a testing ground for new features and might contain a bug every now
|
||||
and then.
|
||||
|
||||
In other words, to get your theme deployed on the main instances, you'll still have to create a pull request. The maintainers will then doublecheck and pull it in.
|
||||
In other words, to get your theme deployed on the main instances, you'll still have to create a pull request. The
|
||||
maintainers will then doublecheck and pull it in.
|
||||
|
||||
If you have a local repository
|
||||
------------------------------
|
||||
|
||||
If you have made a fork earlier and have received contributor rights, you need to tell your local git repository that pushing to the main repository is possible.
|
||||
If you have made a fork earlier and have received contributor rights, you need to tell your local git repository that
|
||||
pushing to the main repository is possible.
|
||||
|
||||
To do this:
|
||||
|
||||
1. type `git remote add upstream git@github.com:pietervdvn/MapComplete`
|
||||
2. Run `git push upstream` to push your latest changes to the main repo (and not your fork). Running `git push` will push to your fork.
|
||||
2. Run `git push upstream` to push your latest changes to the main repo (and not your fork). Running `git push` will
|
||||
push to your fork.
|
||||
|
||||
Alternatively, if you don't have any unmerged changes, you can remove your local copy and clone `pietervdvn/MapComplete` again to start fresh.
|
||||
Alternatively, if you don't have any unmerged changes, you can remove your local copy and clone `pietervdvn/MapComplete`
|
||||
again to start fresh.
|
|
@ -28,7 +28,8 @@ To develop and build MapComplete, you
|
|||
|
||||
0. Make a fork and clone the repository.
|
||||
0. Install the nodejs version specified in [.tool-versions](./.tool-versions)
|
||||
- On linux: install npm first `sudo apt install npm`, then install `n` using npm: ` npm install -g n`, which can then install node with `n install <node-version>`
|
||||
- On linux: install npm first `sudo apt install npm`, then install `n` using npm: ` npm install -g n`, which can
|
||||
then install node with `n install <node-version>`
|
||||
- You can [use asdf to manage your runtime versions](https://asdf-vm.com/).
|
||||
0. Install `npm`. Linux: `sudo apt install npm` (or your favourite package manager), Windows: install
|
||||
nodeJS: https://nodejs.org/en/download/
|
||||
|
@ -106,7 +107,8 @@ Try removing `node_modules`, `package-lock.json` and `.cache`
|
|||
Misc setup
|
||||
----------
|
||||
|
||||
The json-git-merger is used to quickly merge translation files, [documentation here](https://github.com/jonatanpedersen/git-json-merge#single-project--directory)
|
||||
The json-git-merger is used to quickly merge translation
|
||||
files, [documentation here](https://github.com/jonatanpedersen/git-json-merge#single-project--directory)
|
||||
|
||||
Overview of package.json-scripts
|
||||
--------------------------------
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="664pt" height="566pt"
|
||||
viewBox="0.00 0.00 664.25 566.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
viewBox="0.00 0.00 664.25 566.00" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 562)">
|
||||
<title>G</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-562 660.25,-562 660.25,4 -4,4"/>
|
||||
|
@ -24,22 +24,30 @@
|
|||
<!-- init->denied -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>init->denied</title>
|
||||
<path fill="none" stroke="black" d="M188.23,-531.67C143.21,-517.82 54.16,-483.1 17.25,-417 -2.35,-381.91 14.04,-334.64 27.95,-305.79"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M188.23,-531.67C143.21,-517.82 54.16,-483.1 17.25,-417 -2.35,-381.91 14.04,-334.64 27.95,-305.79"/>
|
||||
<polygon fill="black" stroke="black" points="31.12,-307.26 32.51,-296.76 24.88,-304.1 31.12,-307.26"/>
|
||||
<text text-anchor="middle" x="132.25" y="-405.8" font-family="Times,serif" font-size="14.00">geolocation permanently denied</text>
|
||||
<text text-anchor="middle" x="132.25" y="-405.8" font-family="Times,serif" font-size="14.00">geolocation
|
||||
permanently denied
|
||||
</text>
|
||||
</g>
|
||||
<!-- getting_location -->
|
||||
<g id="node3" class="node">
|
||||
<title>getting_location</title>
|
||||
<ellipse fill="none" stroke="black" cx="366.25" cy="-279" rx="85.29" ry="18"/>
|
||||
<text text-anchor="middle" x="366.25" y="-275.3" font-family="Times,serif" font-size="14.00">getting_location</text>
|
||||
<text text-anchor="middle" x="366.25" y="-275.3" font-family="Times,serif" font-size="14.00">
|
||||
getting_location
|
||||
</text>
|
||||
</g>
|
||||
<!-- init->getting_location -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>init->getting_location</title>
|
||||
<path fill="none" stroke="black" d="M242.41,-538.69C294.16,-537.46 403.84,-531.59 427.25,-504 481.59,-439.95 469.34,-387.69 427.25,-315 424.07,-309.52 419.68,-304.83 414.7,-300.82"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M242.41,-538.69C294.16,-537.46 403.84,-531.59 427.25,-504 481.59,-439.95 469.34,-387.69 427.25,-315 424.07,-309.52 419.68,-304.83 414.7,-300.82"/>
|
||||
<polygon fill="black" stroke="black" points="416.68,-297.93 406.47,-295.09 412.67,-303.68 416.68,-297.93"/>
|
||||
<text text-anchor="middle" x="559.75" y="-405.8" font-family="Times,serif" font-size="14.00">previously granted flag set</text>
|
||||
<text text-anchor="middle" x="559.75" y="-405.8" font-family="Times,serif" font-size="14.00">previously
|
||||
granted flag set
|
||||
</text>
|
||||
</g>
|
||||
<!-- idle -->
|
||||
<g id="node4" class="node">
|
||||
|
@ -50,94 +58,122 @@
|
|||
<!-- init->idle -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>init->idle</title>
|
||||
<path fill="none" stroke="black" d="M212.27,-521.58C211.37,-511.6 211.62,-499.1 216.25,-489 218.98,-483.03 223.28,-477.64 228.03,-472.99"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M212.27,-521.58C211.37,-511.6 211.62,-499.1 216.25,-489 218.98,-483.03 223.28,-477.64 228.03,-472.99"/>
|
||||
<polygon fill="black" stroke="black" points="230.48,-475.49 235.73,-466.29 225.89,-470.21 230.48,-475.49"/>
|
||||
<text text-anchor="middle" x="321.75" y="-492.8" font-family="Times,serif" font-size="14.00">previously granted flag unset</text>
|
||||
<text text-anchor="middle" x="321.75" y="-492.8" font-family="Times,serif" font-size="14.00">previously
|
||||
granted flag unset
|
||||
</text>
|
||||
</g>
|
||||
<!-- location_found -->
|
||||
<g id="node6" class="node">
|
||||
<title>location_found</title>
|
||||
<ellipse fill="none" stroke="black" cx="366.25" cy="-192" rx="77.99" ry="18"/>
|
||||
<text text-anchor="middle" x="366.25" y="-188.3" font-family="Times,serif" font-size="14.00">location_found</text>
|
||||
<text text-anchor="middle" x="366.25" y="-188.3" font-family="Times,serif" font-size="14.00">
|
||||
location_found
|
||||
</text>
|
||||
</g>
|
||||
<!-- getting_location->location_found -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>getting_location->location_found</title>
|
||||
<path fill="none" stroke="black" d="M366.25,-260.8C366.25,-249.16 366.25,-233.55 366.25,-220.24"/>
|
||||
<polygon fill="black" stroke="black" points="369.75,-220.18 366.25,-210.18 362.75,-220.18 369.75,-220.18"/>
|
||||
<text text-anchor="middle" x="417.25" y="-231.8" font-family="Times,serif" font-size="14.00">location found</text>
|
||||
<text text-anchor="middle" x="417.25" y="-231.8" font-family="Times,serif" font-size="14.00">location
|
||||
found
|
||||
</text>
|
||||
</g>
|
||||
<!-- request_permission -->
|
||||
<g id="node5" class="node">
|
||||
<title>request_permission</title>
|
||||
<ellipse fill="none" stroke="black" cx="264.25" cy="-366" rx="102.08" ry="18"/>
|
||||
<text text-anchor="middle" x="264.25" y="-362.3" font-family="Times,serif" font-size="14.00">request_permission</text>
|
||||
<text text-anchor="middle" x="264.25" y="-362.3" font-family="Times,serif" font-size="14.00">
|
||||
request_permission
|
||||
</text>
|
||||
</g>
|
||||
<!-- idle->request_permission -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>idle->request_permission</title>
|
||||
<path fill="none" stroke="black" d="M282.79,-448.82C302.7,-444.93 328.29,-436.26 341.25,-417 349.95,-404.06 340.76,-393.72 326.08,-385.86"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M282.79,-448.82C302.7,-444.93 328.29,-436.26 341.25,-417 349.95,-404.06 340.76,-393.72 326.08,-385.86"/>
|
||||
<polygon fill="black" stroke="black" points="327.44,-382.63 316.9,-381.53 324.45,-388.96 327.44,-382.63"/>
|
||||
<text text-anchor="middle" x="371.75" y="-405.8" font-family="Times,serif" font-size="14.00">on click</text>
|
||||
</g>
|
||||
<!-- request_permission->denied -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>request_permission->denied</title>
|
||||
<path fill="none" stroke="black" d="M202.76,-351.6C180.78,-345.99 156.06,-338.72 134.25,-330 113.26,-321.61 90.96,-309.57 73.57,-299.42"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M202.76,-351.6C180.78,-345.99 156.06,-338.72 134.25,-330 113.26,-321.61 90.96,-309.57 73.57,-299.42"/>
|
||||
<polygon fill="black" stroke="black" points="75.34,-296.4 64.96,-294.3 71.77,-302.42 75.34,-296.4"/>
|
||||
<text text-anchor="middle" x="206.25" y="-318.8" font-family="Times,serif" font-size="14.00">permanently denied</text>
|
||||
<text text-anchor="middle" x="206.25" y="-318.8" font-family="Times,serif" font-size="14.00">permanently
|
||||
denied
|
||||
</text>
|
||||
</g>
|
||||
<!-- request_permission->getting_location -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>request_permission->getting_location</title>
|
||||
<path fill="none" stroke="black" d="M271.38,-348C276.49,-337.43 284.24,-324.16 294.25,-315 300.73,-309.07 308.39,-303.93 316.27,-299.56"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M271.38,-348C276.49,-337.43 284.24,-324.16 294.25,-315 300.73,-309.07 308.39,-303.93 316.27,-299.56"/>
|
||||
<polygon fill="black" stroke="black" points="317.92,-302.64 325.2,-294.94 314.71,-296.43 317.92,-302.64"/>
|
||||
<text text-anchor="middle" x="360.75" y="-318.8" font-family="Times,serif" font-size="14.00">granted (sets flag)</text>
|
||||
<text text-anchor="middle" x="360.75" y="-318.8" font-family="Times,serif" font-size="14.00">granted (sets
|
||||
flag)
|
||||
</text>
|
||||
</g>
|
||||
<!-- request_permission->idle -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>request_permission->idle</title>
|
||||
<path fill="none" stroke="black" d="M257.1,-384.15C255.12,-389.74 253.24,-396.04 252.25,-402 251.02,-409.35 250.95,-417.37 251.4,-424.8"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M257.1,-384.15C255.12,-389.74 253.24,-396.04 252.25,-402 251.02,-409.35 250.95,-417.37 251.4,-424.8"/>
|
||||
<polygon fill="black" stroke="black" points="247.92,-425.14 252.32,-434.78 254.89,-424.5 247.92,-425.14"/>
|
||||
<text text-anchor="middle" x="294.75" y="-405.8" font-family="Times,serif" font-size="14.00">not granted</text>
|
||||
<text text-anchor="middle" x="294.75" y="-405.8" font-family="Times,serif" font-size="14.00">not granted
|
||||
</text>
|
||||
</g>
|
||||
<!-- open_lock -->
|
||||
<g id="node7" class="node">
|
||||
<title>open_lock</title>
|
||||
<ellipse fill="none" stroke="black" cx="333.25" cy="-105" rx="55.79" ry="18"/>
|
||||
<text text-anchor="middle" x="333.25" y="-101.3" font-family="Times,serif" font-size="14.00">open_lock</text>
|
||||
<text text-anchor="middle" x="333.25" y="-101.3" font-family="Times,serif" font-size="14.00">open_lock
|
||||
</text>
|
||||
</g>
|
||||
<!-- location_found->open_lock -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>location_found->open_lock</title>
|
||||
<path fill="none" stroke="black" d="M359.57,-173.8C354.98,-161.97 348.79,-146.03 343.56,-132.58"/>
|
||||
<polygon fill="black" stroke="black" points="346.68,-130.94 339.8,-122.89 340.16,-133.47 346.68,-130.94"/>
|
||||
<text text-anchor="middle" x="448.25" y="-144.8" font-family="Times,serif" font-size="14.00">on click (zooms to location)</text>
|
||||
<text text-anchor="middle" x="448.25" y="-144.8" font-family="Times,serif" font-size="14.00">on click (zooms
|
||||
to location)
|
||||
</text>
|
||||
</g>
|
||||
<!-- open_lock->location_found -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>open_lock->location_found</title>
|
||||
<path fill="none" stroke="black" d="M295.44,-118.33C275.01,-127.12 256.04,-140.15 267.25,-156 273.92,-165.44 283.37,-172.35 293.8,-177.41"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M295.44,-118.33C275.01,-127.12 256.04,-140.15 267.25,-156 273.92,-165.44 283.37,-172.35 293.8,-177.41"/>
|
||||
<polygon fill="black" stroke="black" points="292.6,-180.7 303.17,-181.39 295.34,-174.26 292.6,-180.7"/>
|
||||
<text text-anchor="middle" x="305.25" y="-144.8" font-family="Times,serif" font-size="14.00">after 3 sec</text>
|
||||
<text text-anchor="middle" x="305.25" y="-144.8" font-family="Times,serif" font-size="14.00">after 3 sec
|
||||
</text>
|
||||
</g>
|
||||
<!-- closed_lock -->
|
||||
<g id="node8" class="node">
|
||||
<title>closed_lock</title>
|
||||
<ellipse fill="none" stroke="black" cx="454.25" cy="-18" rx="63.09" ry="18"/>
|
||||
<text text-anchor="middle" x="454.25" y="-14.3" font-family="Times,serif" font-size="14.00">closed_lock</text>
|
||||
<text text-anchor="middle" x="454.25" y="-14.3" font-family="Times,serif" font-size="14.00">closed_lock
|
||||
</text>
|
||||
</g>
|
||||
<!-- open_lock->closed_lock -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>open_lock->closed_lock</title>
|
||||
<path fill="none" stroke="black" d="M328.89,-87.05C327.23,-76.5 327.17,-63.24 334.25,-54 346.04,-38.59 364.24,-29.68 382.92,-24.6"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M328.89,-87.05C327.23,-76.5 327.17,-63.24 334.25,-54 346.04,-38.59 364.24,-29.68 382.92,-24.6"/>
|
||||
<polygon fill="black" stroke="black" points="383.78,-28 392.71,-22.28 382.17,-21.18 383.78,-28"/>
|
||||
<text text-anchor="middle" x="447.75" y="-57.8" font-family="Times,serif" font-size="14.00">on click (locks zoom to location)</text>
|
||||
<text text-anchor="middle" x="447.75" y="-57.8" font-family="Times,serif" font-size="14.00">on click (locks
|
||||
zoom to location)
|
||||
</text>
|
||||
</g>
|
||||
<!-- closed_lock->location_found -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>closed_lock->location_found</title>
|
||||
<path fill="none" stroke="black" d="M513.08,-24.74C531.5,-29.64 549.95,-38.41 561.25,-54 588.04,-90.95 580.67,-122.9 549.25,-156 535.31,-170.68 491.24,-179.37 449.85,-184.42"/>
|
||||
<path fill="none" stroke="black"
|
||||
d="M513.08,-24.74C531.5,-29.64 549.95,-38.41 561.25,-54 588.04,-90.95 580.67,-122.9 549.25,-156 535.31,-170.68 491.24,-179.37 449.85,-184.42"/>
|
||||
<polygon fill="black" stroke="black" points="449.22,-180.97 439.68,-185.6 450.02,-187.93 449.22,-180.97"/>
|
||||
<text text-anchor="middle" x="604.75" y="-101.3" font-family="Times,serif" font-size="14.00">on click</text>
|
||||
</g>
|
||||
|
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 10 KiB |
|
@ -6,13 +6,15 @@ Some highlights of new releases.
|
|||
0.10
|
||||
----
|
||||
|
||||
The 0.10 version contains a lot of refactorings on various core of the application, namely in the rendering stack, the fetching of data and uploading.
|
||||
The 0.10 version contains a lot of refactorings on various core of the application, namely in the rendering stack, the
|
||||
fetching of data and uploading.
|
||||
|
||||
Some highlights are:
|
||||
|
||||
1. The addition of fallback overpass servers
|
||||
2. Fetching data from OSM directly (especially useful in the personal theme)
|
||||
3. Splitting all the features per tile (with a maximum amount of features per tile, splitting further if needed), making everything a ton faster
|
||||
3. Splitting all the features per tile (with a maximum amount of features per tile, splitting further if needed), making
|
||||
everything a ton faster
|
||||
4. If a tile has too much features, the featuers are not shown. Instead, a rectangle with the feature amount is shown.
|
||||
|
||||
Furthermore, it contains a few new themes and theme updates:
|
||||
|
@ -31,9 +33,8 @@ Other various small improvements:
|
|||
0.8 and 0.9
|
||||
-----------
|
||||
|
||||
Addition of filters per layer
|
||||
Addition of a download-as-pdf for select themes
|
||||
Addition of a download-as-geojson and download-as-csv for select themes
|
||||
Addition of filters per layer Addition of a download-as-pdf for select themes Addition of a download-as-geojson and
|
||||
download-as-csv for select themes
|
||||
|
||||
...
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
|
||||
Available types for text fields
|
||||
=================================
|
||||
|
||||
|
||||
|
||||
The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them
|
||||
The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to
|
||||
activate them
|
||||
|
||||
## string
|
||||
|
||||
|
@ -24,15 +24,15 @@ A geographical direction, in degrees. 0° is north, 90° is east, ... Will retur
|
|||
|
||||
## length
|
||||
|
||||
A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]
|
||||
A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool.
|
||||
Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]
|
||||
|
||||
## wikidata
|
||||
|
||||
A wikidata identifier, e.g. Q42.
|
||||
|
||||
### Helper arguments
|
||||
|
||||
|
||||
|
||||
name | doc
|
||||
------ | -----
|
||||
key | the value of this tag will initialize search (default: name)
|
||||
|
@ -43,10 +43,10 @@ subarg | doc
|
|||
removePrefixes | remove these snippets of text from the start of the passed string to search
|
||||
removePostfixes | remove these snippets of text from the end of the passed string to search
|
||||
|
||||
|
||||
### Example usage
|
||||
|
||||
The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
|
||||
The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding
|
||||
with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
|
||||
|
||||
```
|
||||
"freeform": {
|
||||
|
@ -102,10 +102,9 @@ A phone number
|
|||
## opening_hours
|
||||
|
||||
Has extra elements to easily input when a POI is opened.
|
||||
|
||||
### Helper arguments
|
||||
|
||||
|
||||
|
||||
name | doc
|
||||
------ | -----
|
||||
options | A JSON-object of type `{ prefix: string, postfix: string }`.
|
||||
|
@ -115,7 +114,6 @@ subarg | doc
|
|||
prefix | Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse
|
||||
postfix | Piece of text that will always be added to the end of the generated opening hours
|
||||
|
||||
|
||||
### Example usage
|
||||
|
||||
To add a conditional (based on time) access restriction:
|
||||
|
@ -134,7 +132,8 @@ postfix | Piece of text that will always be added to the end of the generated op
|
|||
}
|
||||
```
|
||||
|
||||
*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`
|
||||
*Don't forget to pass the prefix and postfix in the rendering as
|
||||
well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`
|
||||
|
||||
## color
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
|
||||
### Special tag renderings
|
||||
|
||||
In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and
|
||||
visualizations to be reused by custom themes or even to query third-party API's.
|
||||
|
||||
|
||||
In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.
|
||||
|
||||
General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args
|
||||
|
||||
|
||||
General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do
|
||||
not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a
|
||||
comma in your args
|
||||
|
||||
- [all_tags](#all_tags)
|
||||
- [image_carousel](#image_carousel)
|
||||
|
@ -25,19 +23,18 @@ General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_nam
|
|||
- [multi_apply](#multi_apply)
|
||||
- [tag_apply](#tag_apply)
|
||||
|
||||
|
||||
|
||||
### all_tags
|
||||
|
||||
Prints all key-value pairs of the object - used for debugging
|
||||
|
||||
#### Example usage
|
||||
|
||||
`{all_tags()}`
|
||||
|
||||
|
||||
### image_carousel
|
||||
|
||||
Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
|
||||
Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported:
|
||||
Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -47,7 +44,6 @@ image key/prefix (multiple values allowed if comma-seperated) | image,mapillary,
|
|||
|
||||
`{image_carousel(image,mapillary,image,wikidata,wikimedia_commons,image,image)}`
|
||||
|
||||
|
||||
### image_upload
|
||||
|
||||
Creates a button where a user can upload an image to IMGUR
|
||||
|
@ -61,7 +57,6 @@ label | Add image | The text to show on the button
|
|||
|
||||
`{image_upload(image,Add image)}`
|
||||
|
||||
|
||||
### wikipedia
|
||||
|
||||
A box showing the corresponding wikipedia article - based on the wikidata tag
|
||||
|
@ -72,8 +67,9 @@ keyToShowWikipediaFor | wikidata | Use the wikidata entry from this key to show
|
|||
|
||||
#### 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
|
||||
|
||||
`{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
|
||||
|
||||
|
@ -86,12 +82,14 @@ idKey | id | (Matches all resting arguments) This argument should be the key of
|
|||
|
||||
#### Example usage
|
||||
|
||||
`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`
|
||||
|
||||
`{minimap()}`
|
||||
, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`
|
||||
|
||||
### sided_minimap
|
||||
|
||||
A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced
|
||||
A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as
|
||||
only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically
|
||||
introduced
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -101,39 +99,46 @@ side | _undefined_ | The side to show, either `left` or `right`
|
|||
|
||||
`{sided_minimap(left)}`
|
||||
|
||||
|
||||
### reviews
|
||||
|
||||
Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten
|
||||
Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed
|
||||
object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
subjectKey | name | The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>
|
||||
fallback | _undefined_ | The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value
|
||||
fallback | _
|
||||
undefined_ | The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value
|
||||
|
||||
#### Example usage
|
||||
|
||||
`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used
|
||||
|
||||
`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name
|
||||
will be used as identifier, otherwise 'play_forest' is used
|
||||
|
||||
### opening_hours_table
|
||||
|
||||
Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.
|
||||
Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag '
|
||||
opening_hours'.
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
key | opening_hours | The tagkey from which the table is constructed.
|
||||
prefix | _empty string_ | Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__
|
||||
postfix | _empty string_ | Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__
|
||||
prefix | _empty string_ | Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to
|
||||
indicate `(` if needed__
|
||||
postfix | _empty string_ | Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to
|
||||
indicate `)` if needed__
|
||||
|
||||
#### Example usage
|
||||
|
||||
A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`
|
||||
|
||||
A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with
|
||||
opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`
|
||||
|
||||
### live
|
||||
|
||||
Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}
|
||||
Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will
|
||||
download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will
|
||||
return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format},
|
||||
needed_value)}
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -143,8 +148,8 @@ path | _undefined_ | The path (or shorthand) that should be returned
|
|||
|
||||
#### Example usage
|
||||
|
||||
{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}
|
||||
|
||||
{live({url},{url:format},hour)}
|
||||
{live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}
|
||||
|
||||
### histogram
|
||||
|
||||
|
@ -155,13 +160,13 @@ name | default | description
|
|||
key | _undefined_ | The key to be read and to generate a histogram from
|
||||
title | _empty string_ | The text to put above the given values column
|
||||
countHeader | _empty string_ | The text to put above the counts
|
||||
colors* | _undefined_ | (Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`
|
||||
colors* | _
|
||||
undefined_ | (Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`
|
||||
|
||||
#### Example usage
|
||||
|
||||
`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram
|
||||
|
||||
|
||||
### share_link
|
||||
|
||||
Creates a link that (attempts to) open the native 'share'-screen
|
||||
|
@ -174,7 +179,6 @@ url | _undefined_ | The url to share (default: current URL)
|
|||
|
||||
{share_link()} to share the current page, {share_link(<some_url>)} to share the given url
|
||||
|
||||
|
||||
### canonical
|
||||
|
||||
Converts a short, canonical value into the long, translated text
|
||||
|
@ -187,10 +191,10 @@ key | _undefined_ | The key of the tag to give the canonical text for
|
|||
|
||||
{canonical(length)} will give 42 metre (in french)
|
||||
|
||||
|
||||
### import_button
|
||||
|
||||
This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes.
|
||||
This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but
|
||||
can be tested in unofficial themes.
|
||||
|
||||
#### Importing a dataset into OpenStreetMap: requirements
|
||||
|
||||
|
@ -198,100 +202,112 @@ 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. 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 feature 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 feature 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 excellent way to do this
|
||||
1. The new feature 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 feature 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 excellent way to do this
|
||||
4. When importing ways, the theme creator is also responsible of avoiding overlapping ways.
|
||||
|
||||
#### Disabled in unofficial themes
|
||||
|
||||
The import button can be tested in an unofficial theme by adding `test=true` or `backend=osm-test` as [URL-paramter](URL_Parameters.md).
|
||||
The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console.
|
||||
In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org
|
||||
|
||||
The import button can be tested in an unofficial theme by adding `test=true` or `backend=osm-test`
|
||||
as [URL-paramter](URL_Parameters.md). The import button will show up then. If in testmode, you can read the
|
||||
changeset-XML directly in the web console. In the case that MapComplete is pointed to the testing grounds, the edit will
|
||||
be made on https://master.apis.dev.openstreetmap.org
|
||||
|
||||
#### Specifying which tags to copy or add
|
||||
|
||||
The argument `tags` of the import button takes a `;`-seperated list of tags to add.
|
||||
|
||||
These can either be a tag to add, such as `amenity=fast_food` or can use a substitution, e.g. `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.
|
||||
These can either be a tag to add, such as `amenity=fast_food` or can use a substitution, e.g. `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.
|
||||
|
||||
If a value to substitute is undefined, empty string will be used instead.
|
||||
|
||||
This supports multiple values, e.g. `ref=$source:geometry:type/$source:geometry:ref`
|
||||
|
||||
Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with `[a-zA-Z0-9_:]*`). Sadly, delimiting with `{}` as these already mark the boundaries of the special rendering...
|
||||
|
||||
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
|
||||
|
||||
|
||||
|
||||
Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (
|
||||
matched with `[a-zA-Z0-9_:]*`). Sadly, delimiting with `{}` as these already mark the boundaries of the special
|
||||
rendering...
|
||||
|
||||
Note that these values can be prepare with javascript in the theme by using
|
||||
a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
targetLayer | _undefined_ | The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements
|
||||
targetLayer | _
|
||||
undefined_ | The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements
|
||||
tags | _undefined_ | The tags to add onto the new object - see specification above
|
||||
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
|
||||
Snap onto layer(s)/replace geometry with this other way | _undefined_ | - If the value corresponding with this key starts with 'way/' and the feature is a LineString or Polygon, the original OSM-way geometry will be changed to match the new geometry
|
||||
- If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list
|
||||
snap max distance | 5 | The maximum distance that this point will move to snap onto a layer (in meters)
|
||||
Snap onto layer(s)/replace geometry with this other way | _
|
||||
undefined_ | - If the value corresponding with this key starts with 'way/' and the feature is a LineString or Polygon, the original OSM-way geometry will be changed to match the new geometry
|
||||
|
||||
- If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To
|
||||
show multiple layers to snap onto, use a `;`-seperated list snap max distance | 5 | The maximum distance that this
|
||||
point will move to snap onto a layer (in meters)
|
||||
|
||||
#### Example usage
|
||||
|
||||
`{import_button(,,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,18,,5)}`
|
||||
|
||||
|
||||
### multi_apply
|
||||
|
||||
A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags
|
||||
A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll
|
||||
need calculatedTags
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
feature_ids | _undefined_ | A JSOn-serialized list of IDs of features to apply the tagging on
|
||||
keys | _undefined_ | One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.
|
||||
keys | _
|
||||
undefined_ | One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.
|
||||
text | _undefined_ | The text to show on the button
|
||||
autoapply | _undefined_ | A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown
|
||||
overwrite | _undefined_ | If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change
|
||||
autoapply | _
|
||||
undefined_ | A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown
|
||||
overwrite | _
|
||||
undefined_ | If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change
|
||||
|
||||
#### Example usage
|
||||
|
||||
{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}
|
||||
|
||||
{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology
|
||||
information on all nearby objects with the same name)}
|
||||
|
||||
### tag_apply
|
||||
|
||||
Shows a big button; clicking this button will apply certain tags onto the feature.
|
||||
|
||||
The first argument takes a specification of which tags to add.
|
||||
These can either be a tag to add, such as `amenity=fast_food` or can use a substitution, e.g. `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.
|
||||
The first argument takes a specification of which tags to add. These can either be a tag to add, such
|
||||
as `amenity=fast_food` or can use a substitution, e.g. `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.
|
||||
|
||||
If a value to substitute is undefined, empty string will be used instead.
|
||||
|
||||
This supports multiple values, e.g. `ref=$source:geometry:type/$source:geometry:ref`
|
||||
|
||||
Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with `[a-zA-Z0-9_:]*`). Sadly, delimiting with `{}` as these already mark the boundaries of the special rendering...
|
||||
|
||||
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
|
||||
Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (
|
||||
matched with `[a-zA-Z0-9_:]*`). Sadly, delimiting with `{}` as these already mark the boundaries of the special
|
||||
rendering...
|
||||
|
||||
Note that these values can be prepare with javascript in the theme by using
|
||||
a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
tags_to_apply | _undefined_ | A specification of the tags to apply
|
||||
message | _undefined_ | The text to show to the contributor
|
||||
image | _undefined_ | An image to show to the contributor on the button
|
||||
id_of_object_to_apply_this_one | _undefined_ | If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element
|
||||
id_of_object_to_apply_this_one | _undefined_ | If specified, applies the the tags onto _
|
||||
another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _
|
||||
selected_ element
|
||||
|
||||
#### Example usage
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime
|
||||
from matplotlib import pyplot
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from matplotlib import pyplot
|
||||
|
||||
|
||||
def pyplot_init():
|
||||
|
@ -17,6 +17,7 @@ def genKeys(data, type):
|
|||
keys = map(lambda key: datetime.strptime(key, "%Y-%m-%dT%H:%M:%S.000Z"), keys)
|
||||
return list(keys)
|
||||
|
||||
|
||||
def createPie(options):
|
||||
data = options["plot"]["count"]
|
||||
keys = genKeys(data, options["interpetKeysAs"])
|
||||
|
@ -28,6 +29,7 @@ def createPie(options):
|
|||
pyplot_init()
|
||||
pyplot.pie(values, labels=keys, startangle=(90 - 360 * first_pct / 2))
|
||||
|
||||
|
||||
def createBar(options):
|
||||
data = options["plot"]["count"]
|
||||
keys = genKeys(data, options["interpetKeysAs"])
|
||||
|
@ -37,7 +39,6 @@ def createBar(options):
|
|||
pyplot.legend()
|
||||
|
||||
|
||||
|
||||
pyplot_init()
|
||||
title = sys.argv[1]
|
||||
pyplot.title = title
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
URL-parameters and URL-hash
|
||||
============================
|
||||
|
||||
|
@ -9,8 +8,8 @@ What is a URL parameter?
|
|||
|
||||
URL-parameters are extra parts of the URL used to set the state.
|
||||
|
||||
For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`,
|
||||
the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely:
|
||||
For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`, the
|
||||
URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely:
|
||||
|
||||
- The url-parameter `lat` is `51.0` in this instance
|
||||
- The url-parameter `lon` is `4.3` in this instance
|
||||
|
@ -23,7 +22,8 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
|
|||
fs-userbadge
|
||||
--------------
|
||||
|
||||
Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. The default value is _true_
|
||||
Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus
|
||||
disables editing all together, effectively putting MapComplete into read-only mode. The default value is _true_
|
||||
|
||||
|
||||
fs-search
|
||||
|
@ -47,7 +47,8 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
|
|||
fs-add-new
|
||||
------------
|
||||
|
||||
Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_
|
||||
Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default
|
||||
value is _true_
|
||||
|
||||
|
||||
fs-welcome-message
|
||||
|
@ -59,7 +60,8 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
|
|||
fs-iframe-popout
|
||||
------------------
|
||||
|
||||
Disables/Enables the iframe-popout button. If in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch) The default value is _true_
|
||||
Disables/Enables the iframe-popout button. If in iframe mode and the welcome message is hidden, a popout button to the
|
||||
full mapcomplete instance is shown instead (unless disabled with this switch) The default value is _true_
|
||||
|
||||
|
||||
fs-more-quests
|
||||
|
@ -101,13 +103,15 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
|
|||
backend
|
||||
---------
|
||||
|
||||
The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_
|
||||
The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default
|
||||
value is _osm_
|
||||
|
||||
|
||||
test
|
||||
------
|
||||
|
||||
If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_
|
||||
If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the
|
||||
console instead of actually uploaded to osm.org The default value is _false_
|
||||
|
||||
|
||||
debug
|
||||
|
@ -125,7 +129,8 @@ 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,https://overpass.kumi.systems/api/interpreter,https://overpass.openstreetmap.ru/cgi/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
|
||||
|
|
|
@ -5,7 +5,9 @@ import Loc from "../../Models/Loc";
|
|||
export interface AvailableBaseLayersObj {
|
||||
readonly osmCarto: BaseLayer;
|
||||
layerOverview: BaseLayer[];
|
||||
|
||||
AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]>
|
||||
|
||||
SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer>;
|
||||
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ import {UIEventSource} from "../UIEventSource";
|
|||
import Loc from "../../Models/Loc";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import * as editorlayerindex from "../../assets/editor-layer-index.json";
|
||||
import * as L from "leaflet";
|
||||
import {TileLayer} from "leaflet";
|
||||
import * as X from "leaflet-providers";
|
||||
import * as L from "leaflet";
|
||||
import {Utils} from "../../Utils";
|
||||
import {AvailableBaseLayersObj} from "./AvailableBaseLayers";
|
||||
|
||||
|
@ -28,102 +28,6 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
|
|||
|
||||
public layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex());
|
||||
|
||||
public AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
|
||||
const source = location.map(
|
||||
(currentLocation) => {
|
||||
|
||||
if (currentLocation === undefined) {
|
||||
return this.layerOverview;
|
||||
}
|
||||
|
||||
const currentLayers = source?.data; // A bit unorthodox - I know
|
||||
const newLayers = this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
|
||||
|
||||
if (currentLayers === undefined) {
|
||||
return newLayers;
|
||||
}
|
||||
if (newLayers.length !== currentLayers.length) {
|
||||
return newLayers;
|
||||
}
|
||||
for (let i = 0; i < newLayers.length; i++) {
|
||||
if (newLayers[i].name !== currentLayers[i].name) {
|
||||
return newLayers;
|
||||
}
|
||||
}
|
||||
|
||||
return currentLayers;
|
||||
});
|
||||
return source;
|
||||
}
|
||||
|
||||
public SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
|
||||
return this.AvailableLayersAt(location).map(available => {
|
||||
// First float all 'best layers' to the top
|
||||
available.sort((a, b) => {
|
||||
if (a.isBest && b.isBest) {
|
||||
return 0;
|
||||
}
|
||||
if (!a.isBest) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
)
|
||||
|
||||
if (preferedCategory.data === undefined) {
|
||||
return available[0]
|
||||
}
|
||||
|
||||
let prefered: string []
|
||||
if (typeof preferedCategory.data === "string") {
|
||||
prefered = [preferedCategory.data]
|
||||
} else {
|
||||
prefered = preferedCategory.data;
|
||||
}
|
||||
|
||||
prefered.reverse();
|
||||
for (const category of prefered) {
|
||||
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
|
||||
available.sort((a, b) => {
|
||||
if (a.category === category && b.category === category) {
|
||||
return 0;
|
||||
}
|
||||
if (a.category !== category) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
)
|
||||
}
|
||||
return available[0]
|
||||
})
|
||||
}
|
||||
|
||||
private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
|
||||
const availableLayers = [this.osmCarto]
|
||||
const globalLayers = [];
|
||||
for (const layerOverviewItem of this.layerOverview) {
|
||||
const layer = layerOverviewItem;
|
||||
|
||||
if (layer.feature?.geometry === undefined || layer.feature?.geometry === null) {
|
||||
globalLayers.push(layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lon === undefined || lat === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GeoOperations.inside([lon, lat], layer.feature)) {
|
||||
availableLayers.push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
return availableLayers.concat(globalLayers);
|
||||
}
|
||||
|
||||
private static LoadRasterIndex(): BaseLayer[] {
|
||||
const layers: BaseLayer[] = []
|
||||
// @ts-ignore
|
||||
|
@ -289,4 +193,100 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
|
|||
subdomains: domains
|
||||
});
|
||||
}
|
||||
|
||||
public AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
|
||||
const source = location.map(
|
||||
(currentLocation) => {
|
||||
|
||||
if (currentLocation === undefined) {
|
||||
return this.layerOverview;
|
||||
}
|
||||
|
||||
const currentLayers = source?.data; // A bit unorthodox - I know
|
||||
const newLayers = this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
|
||||
|
||||
if (currentLayers === undefined) {
|
||||
return newLayers;
|
||||
}
|
||||
if (newLayers.length !== currentLayers.length) {
|
||||
return newLayers;
|
||||
}
|
||||
for (let i = 0; i < newLayers.length; i++) {
|
||||
if (newLayers[i].name !== currentLayers[i].name) {
|
||||
return newLayers;
|
||||
}
|
||||
}
|
||||
|
||||
return currentLayers;
|
||||
});
|
||||
return source;
|
||||
}
|
||||
|
||||
public SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
|
||||
return this.AvailableLayersAt(location).map(available => {
|
||||
// First float all 'best layers' to the top
|
||||
available.sort((a, b) => {
|
||||
if (a.isBest && b.isBest) {
|
||||
return 0;
|
||||
}
|
||||
if (!a.isBest) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
)
|
||||
|
||||
if (preferedCategory.data === undefined) {
|
||||
return available[0]
|
||||
}
|
||||
|
||||
let prefered: string []
|
||||
if (typeof preferedCategory.data === "string") {
|
||||
prefered = [preferedCategory.data]
|
||||
} else {
|
||||
prefered = preferedCategory.data;
|
||||
}
|
||||
|
||||
prefered.reverse();
|
||||
for (const category of prefered) {
|
||||
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
|
||||
available.sort((a, b) => {
|
||||
if (a.category === category && b.category === category) {
|
||||
return 0;
|
||||
}
|
||||
if (a.category !== category) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
)
|
||||
}
|
||||
return available[0]
|
||||
})
|
||||
}
|
||||
|
||||
private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
|
||||
const availableLayers = [this.osmCarto]
|
||||
const globalLayers = [];
|
||||
for (const layerOverviewItem of this.layerOverview) {
|
||||
const layer = layerOverviewItem;
|
||||
|
||||
if (layer.feature?.geometry === undefined || layer.feature?.geometry === null) {
|
||||
globalLayers.push(layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lon === undefined || lat === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GeoOperations.inside([lon, lat], layer.feature)) {
|
||||
availableLayers.push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
return availableLayers.concat(globalLayers);
|
||||
}
|
||||
}
|
|
@ -115,7 +115,6 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
let lastUsed = 0;
|
||||
|
||||
|
||||
|
||||
const layersToDownload = []
|
||||
for (const layer of this.state.layoutToUse.layers) {
|
||||
|
||||
|
|
|
@ -81,5 +81,4 @@ export default class StrayClickHandler {
|
|||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -4,11 +4,11 @@ import {GeoOperations} from "./GeoOperations";
|
|||
|
||||
export class BBox {
|
||||
|
||||
static global: BBox = new BBox([[-180, -90], [180, 90]]);
|
||||
readonly maxLat: number;
|
||||
readonly maxLon: number;
|
||||
readonly minLat: number;
|
||||
readonly minLon: number;
|
||||
static global: BBox = new BBox([[-180, -90], [180, 90]]);
|
||||
|
||||
constructor(coordinates) {
|
||||
this.maxLat = -90;
|
||||
|
@ -45,6 +45,17 @@ export class BBox {
|
|||
return feature.bbox;
|
||||
}
|
||||
|
||||
static fromTile(z: number, x: number, y: number): BBox {
|
||||
return new BBox(Tiles.tile_bounds_lon_lat(z, x, y))
|
||||
}
|
||||
|
||||
static fromTileIndex(i: number): BBox {
|
||||
if (i === 0) {
|
||||
return BBox.global
|
||||
}
|
||||
return BBox.fromTile(...Tiles.tile_from_index(i))
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a tilerange which fully contains this bbox (thus might be a bit larger)
|
||||
* @param zoomlevel
|
||||
|
@ -83,24 +94,6 @@ export class BBox {
|
|||
return true;
|
||||
}
|
||||
|
||||
private check() {
|
||||
if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) {
|
||||
console.log(this);
|
||||
throw "BBOX has NAN";
|
||||
}
|
||||
}
|
||||
|
||||
static fromTile(z: number, x: number, y: number): BBox {
|
||||
return new BBox(Tiles.tile_bounds_lon_lat(z, x, y))
|
||||
}
|
||||
|
||||
static fromTileIndex(i: number): BBox {
|
||||
if (i === 0) {
|
||||
return BBox.global
|
||||
}
|
||||
return BBox.fromTile(...Tiles.tile_from_index(i))
|
||||
}
|
||||
|
||||
getEast() {
|
||||
return this.maxLon
|
||||
}
|
||||
|
@ -179,4 +172,11 @@ export class BBox {
|
|||
|
||||
|
||||
}
|
||||
|
||||
private check() {
|
||||
if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) {
|
||||
console.log(this);
|
||||
throw "BBOX has NAN";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ export default class ContributorCount {
|
|||
|
||||
public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>());
|
||||
private readonly state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> };
|
||||
private lastUpdate: Date = undefined;
|
||||
|
||||
constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> }) {
|
||||
this.state = state;
|
||||
|
@ -21,8 +22,6 @@ export default class ContributorCount {
|
|||
|
||||
}
|
||||
|
||||
private lastUpdate: Date = undefined;
|
||||
|
||||
private update(bbox: BBox) {
|
||||
if (bbox === undefined) {
|
||||
return;
|
||||
|
|
|
@ -65,39 +65,6 @@ export default class DetermineLayout {
|
|||
return [layoutToUse, undefined]
|
||||
}
|
||||
|
||||
private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
|
||||
console.log("Downloading map theme from ", link);
|
||||
|
||||
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`)
|
||||
.AttachTo("centermessage");
|
||||
|
||||
try {
|
||||
|
||||
const parsed = await Utils.downloadJson(link)
|
||||
console.log("Got ", parsed)
|
||||
LegacyJsonConvert.fixThemeConfig(parsed)
|
||||
try {
|
||||
parsed.id = link;
|
||||
return new LayoutConfig(parsed, false).patchImages(link, JSON.stringify(parsed));
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static LoadLayoutFromHash(
|
||||
userLayoutParam: UIEventSource<string>
|
||||
): [LayoutConfig, string] | null {
|
||||
|
@ -166,4 +133,37 @@ export default class DetermineLayout {
|
|||
.AttachTo("centermessage");
|
||||
}
|
||||
|
||||
private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
|
||||
console.log("Downloading map theme from ", link);
|
||||
|
||||
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`)
|
||||
.AttachTo("centermessage");
|
||||
|
||||
try {
|
||||
|
||||
const parsed = await Utils.downloadJson(link)
|
||||
console.log("Got ", parsed)
|
||||
LegacyJsonConvert.fixThemeConfig(parsed)
|
||||
try {
|
||||
parsed.id = link;
|
||||
return new LayoutConfig(parsed, false).patchImages(link, JSON.stringify(parsed));
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -299,6 +299,34 @@ export default class FeaturePipeline {
|
|||
|
||||
}
|
||||
|
||||
public GetAllFeaturesWithin(bbox: BBox): any[][] {
|
||||
const self = this
|
||||
const tiles = []
|
||||
Array.from(this.perLayerHierarchy.keys())
|
||||
.forEach(key => tiles.push(...self.GetFeaturesWithin(key, bbox)))
|
||||
return tiles;
|
||||
}
|
||||
|
||||
public GetFeaturesWithin(layerId: string, bbox: BBox): any[][] {
|
||||
if (layerId === "*") {
|
||||
return this.GetAllFeaturesWithin(bbox)
|
||||
}
|
||||
const requestedHierarchy = this.perLayerHierarchy.get(layerId)
|
||||
if (requestedHierarchy === undefined) {
|
||||
console.warn("Layer ", layerId, "is not defined. Try one of ", Array.from(this.perLayerHierarchy.keys()))
|
||||
return undefined;
|
||||
}
|
||||
return TileHierarchyTools.getTiles(requestedHierarchy, bbox)
|
||||
.filter(featureSource => featureSource.features?.data !== undefined)
|
||||
.map(featureSource => featureSource.features.data.map(fs => fs.feature))
|
||||
}
|
||||
|
||||
public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void) {
|
||||
Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => {
|
||||
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
|
||||
})
|
||||
}
|
||||
|
||||
private freshnessForVisibleLayers(z: number, x: number, y: number): Date {
|
||||
let oldestDate = undefined;
|
||||
for (const flayer of this.state.filteredLayers.data) {
|
||||
|
@ -438,32 +466,4 @@ export default class FeaturePipeline {
|
|||
|
||||
}
|
||||
|
||||
public GetAllFeaturesWithin(bbox: BBox): any[][] {
|
||||
const self = this
|
||||
const tiles = []
|
||||
Array.from(this.perLayerHierarchy.keys())
|
||||
.forEach(key => tiles.push(...self.GetFeaturesWithin(key, bbox)))
|
||||
return tiles;
|
||||
}
|
||||
|
||||
public GetFeaturesWithin(layerId: string, bbox: BBox): any[][] {
|
||||
if (layerId === "*") {
|
||||
return this.GetAllFeaturesWithin(bbox)
|
||||
}
|
||||
const requestedHierarchy = this.perLayerHierarchy.get(layerId)
|
||||
if (requestedHierarchy === undefined) {
|
||||
console.warn("Layer ", layerId, "is not defined. Try one of ", Array.from(this.perLayerHierarchy.keys()))
|
||||
return undefined;
|
||||
}
|
||||
return TileHierarchyTools.getTiles(requestedHierarchy, bbox)
|
||||
.filter(featureSource => featureSource.features?.data !== undefined)
|
||||
.map(featureSource => featureSource.features.data.map(fs => fs.feature))
|
||||
}
|
||||
|
||||
public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void) {
|
||||
Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => {
|
||||
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {BBox} from "../BBox";
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
|
||||
const featuresPerLayer = new Map<string, { feature, freshness } []>();
|
||||
const noLayerFound = []
|
||||
|
||||
function addTo(layer: FilteredLayer, feature: { feature, freshness }) {
|
||||
const id = layer.layerDef.id
|
||||
const list = featuresPerLayer.get(id)
|
||||
|
|
|
@ -11,9 +11,9 @@ import {ChangeDescription, ChangeDescriptionTools} from "../../Osm/Actions/Chang
|
|||
export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string;
|
||||
public readonly layer: FilteredLayer
|
||||
private readonly source: IndexedFeatureSource;
|
||||
private readonly changes: Changes;
|
||||
public readonly layer: FilteredLayer
|
||||
|
||||
constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) {
|
||||
this.source = source;
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
|
||||
|
@ -14,10 +13,10 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name;
|
||||
public readonly layer: FilteredLayer
|
||||
private readonly _sources: UIEventSource<FeatureSource[]>;
|
||||
public readonly tileIndex: number;
|
||||
public readonly bbox: BBox;
|
||||
public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(new Set())
|
||||
private readonly _sources: UIEventSource<FeatureSource[]>;
|
||||
|
||||
constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
|
||||
this.tileIndex = tileIndex;
|
||||
|
|
|
@ -18,6 +18,8 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
locationControl: UIEventSource<{ zoom: number }>; selectedElement: UIEventSource<any>,
|
||||
allElements: ElementStorage
|
||||
};
|
||||
private readonly _alreadyRegistered = new Set<UIEventSource<any>>();
|
||||
private readonly _is_dirty = new UIEventSource(false)
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
|
@ -55,24 +57,6 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
this.update();
|
||||
}
|
||||
|
||||
private readonly _alreadyRegistered = new Set<UIEventSource<any>>();
|
||||
private readonly _is_dirty = new UIEventSource(false)
|
||||
|
||||
private registerCallback(feature: any, layer: LayerConfig) {
|
||||
const src = this.state.allElements.addOrGetElement(feature)
|
||||
if (this._alreadyRegistered.has(src)) {
|
||||
return
|
||||
}
|
||||
this._alreadyRegistered.add(src)
|
||||
if (layer.isShown !== undefined) {
|
||||
|
||||
const self = this;
|
||||
src.map(tags => layer.isShown?.GetRenderValue(tags, "yes").txt).addCallbackAndRunD(isShown => {
|
||||
self._is_dirty.setData(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public update() {
|
||||
const self = this;
|
||||
const layer = this.upstream.layer;
|
||||
|
@ -116,4 +100,19 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
this._is_dirty.setData(false)
|
||||
}
|
||||
|
||||
private registerCallback(feature: any, layer: LayerConfig) {
|
||||
const src = this.state.allElements.addOrGetElement(feature)
|
||||
if (this._alreadyRegistered.has(src)) {
|
||||
return
|
||||
}
|
||||
this._alreadyRegistered.add(src)
|
||||
if (layer.isShown !== undefined) {
|
||||
|
||||
const self = this;
|
||||
src.map(tags => layer.isShown?.GetRenderValue(tags, "yes").txt).addCallbackAndRunD(isShown => {
|
||||
self._is_dirty.setData(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,12 +15,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
public readonly name;
|
||||
public readonly isOsmCache: boolean
|
||||
private readonly seenids: Set<string> = new Set<string>()
|
||||
public readonly layer: FilteredLayer;
|
||||
|
||||
public readonly tileIndex
|
||||
public readonly bbox;
|
||||
|
||||
private readonly seenids: Set<string> = new Set<string>()
|
||||
/**
|
||||
* Only used if the actual source is a tiled geojson.
|
||||
* A big feature might be contained in multiple tiles.
|
||||
|
|
|
@ -9,9 +9,8 @@ import {Tiles} from "../../../Models/TileRange";
|
|||
* A tiled source which dynamically loads the required tiles at a fixed zoom level
|
||||
*/
|
||||
export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
private readonly _loadedTiles = new Set<number>();
|
||||
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>;
|
||||
private readonly _loadedTiles = new Set<number>();
|
||||
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
|
|
|
@ -13,9 +13,10 @@ import {Or} from "../../Tags/Or";
|
|||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
|
||||
export default class OsmFeatureSource {
|
||||
private readonly _backend: string;
|
||||
|
||||
public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly downloadedTiles = new Set<number>()
|
||||
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
|
||||
private readonly _backend: string;
|
||||
private readonly filteredLayers: UIEventSource<FilteredLayer[]>;
|
||||
private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void;
|
||||
private isActive: UIEventSource<boolean>;
|
||||
|
@ -28,11 +29,8 @@ export default class OsmFeatureSource {
|
|||
},
|
||||
markTileVisited?: (tileId: number) => void
|
||||
};
|
||||
public readonly downloadedTiles = new Set<number>()
|
||||
private readonly allowedTags: TagsFilter;
|
||||
|
||||
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
|
||||
|
||||
constructor(options: {
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
|
||||
isActive: UIEventSource<boolean>,
|
||||
|
|
|
@ -17,11 +17,8 @@ The GeoJSon files (not tiled) are then added to this list
|
|||
|
||||
A single FeatureSourcePerLayer is then further handled by splitting it into a tile hierarchy.
|
||||
|
||||
|
||||
|
||||
In order to keep thins snappy, they are distributed over a tiled database per layer.
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
`cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up
|
|
@ -8,9 +8,8 @@ import {BBox} from "../../BBox";
|
|||
|
||||
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
|
||||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>();
|
||||
|
||||
public readonly layer: FilteredLayer;
|
||||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>();
|
||||
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void;
|
||||
|
||||
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import {Utils} from "../../../Utils";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import TileHierarchy from "./TileHierarchy";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
|
@ -28,13 +27,13 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
public readonly containedIds: UIEventSource<Set<string>>
|
||||
|
||||
public readonly bbox: BBox;
|
||||
public readonly tileIndex: number;
|
||||
private upper_left: TiledFeatureSource
|
||||
private upper_right: TiledFeatureSource
|
||||
private lower_left: TiledFeatureSource
|
||||
private lower_right: TiledFeatureSource
|
||||
private readonly maxzoom: number;
|
||||
private readonly options: TiledFeatureSourceOptions
|
||||
public readonly tileIndex: number;
|
||||
|
||||
private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) {
|
||||
this.z = z;
|
||||
|
|
|
@ -13,44 +13,6 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
|
|||
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 + "-"
|
||||
const freshnesses = new Map<number, Date>()
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (!(key.startsWith(prefix) && key.endsWith("-time"))) {
|
||||
continue
|
||||
}
|
||||
const index = Number(key.substring(prefix.length, key.length - "-time".length))
|
||||
const time = Number(localStorage.getItem(key))
|
||||
const freshness = new Date()
|
||||
freshness.setTime(time)
|
||||
freshnesses.set(index, freshness)
|
||||
}
|
||||
return freshnesses
|
||||
}
|
||||
|
||||
|
||||
static cleanCacheForLayer(layer: LayerConfig) {
|
||||
const now = new Date()
|
||||
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.id + "-"
|
||||
console.log("Cleaning tiles of ", prefix, "with max age",layer.maxAgeOfCache)
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (!(key.startsWith(prefix) && key.endsWith("-time"))) {
|
||||
continue
|
||||
}
|
||||
const index = Number(key.substring(prefix.length, key.length - "-time".length))
|
||||
const time = Number(localStorage.getItem(key))
|
||||
const timeDiff = (now.getTime() - time) / 1000
|
||||
|
||||
if(timeDiff >= layer.maxAgeOfCache){
|
||||
const k = prefix+index;
|
||||
localStorage.removeItem(k)
|
||||
localStorage.removeItem(k+"-format")
|
||||
localStorage.removeItem(k+"-time")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(layer: FilteredLayer,
|
||||
handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void,
|
||||
state: {
|
||||
|
@ -110,6 +72,43 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
|
|||
|
||||
}
|
||||
|
||||
public static GetFreshnesses(layerId: string): Map<number, Date> {
|
||||
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layerId + "-"
|
||||
const freshnesses = new Map<number, Date>()
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (!(key.startsWith(prefix) && key.endsWith("-time"))) {
|
||||
continue
|
||||
}
|
||||
const index = Number(key.substring(prefix.length, key.length - "-time".length))
|
||||
const time = Number(localStorage.getItem(key))
|
||||
const freshness = new Date()
|
||||
freshness.setTime(time)
|
||||
freshnesses.set(index, freshness)
|
||||
}
|
||||
return freshnesses
|
||||
}
|
||||
|
||||
static cleanCacheForLayer(layer: LayerConfig) {
|
||||
const now = new Date()
|
||||
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.id + "-"
|
||||
console.log("Cleaning tiles of ", prefix, "with max age", layer.maxAgeOfCache)
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (!(key.startsWith(prefix) && key.endsWith("-time"))) {
|
||||
continue
|
||||
}
|
||||
const index = Number(key.substring(prefix.length, key.length - "-time".length))
|
||||
const time = Number(localStorage.getItem(key))
|
||||
const timeDiff = (now.getTime() - time) / 1000
|
||||
|
||||
if (timeDiff >= layer.maxAgeOfCache) {
|
||||
const k = prefix + index;
|
||||
localStorage.removeItem(k)
|
||||
localStorage.removeItem(k + "-format")
|
||||
localStorage.removeItem(k + "-time")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadTile(neededIndex: number) {
|
||||
try {
|
||||
const key = SaveTileToLocalStorageActor.storageKey + "-" + this.layer.layerDef.id + "-" + neededIndex
|
||||
|
|
|
@ -3,6 +3,9 @@ import {BBox} from "./BBox";
|
|||
|
||||
export class GeoOperations {
|
||||
|
||||
private static readonly _earthRadius = 6378137;
|
||||
private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2;
|
||||
|
||||
static surfaceAreaInSqMeters(feature: any) {
|
||||
return turf.area(feature);
|
||||
}
|
||||
|
@ -292,10 +295,6 @@ export class GeoOperations {
|
|||
return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n")
|
||||
}
|
||||
|
||||
|
||||
private static readonly _earthRadius = 6378137;
|
||||
private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2;
|
||||
|
||||
//Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913
|
||||
public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] {
|
||||
const lon = lonLat[0];
|
||||
|
@ -320,6 +319,31 @@ export class GeoOperations {
|
|||
return turf.toWgs84(geojson)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to remove points which do not contribute much to the general outline.
|
||||
* Points for which the angle is ~ 180° are removed
|
||||
* @param coordinates
|
||||
* @constructor
|
||||
*/
|
||||
public static SimplifyCoordinates(coordinates: [number, number][]) {
|
||||
const newCoordinates = []
|
||||
for (let i = 1; i < coordinates.length - 1; i++) {
|
||||
const coordinate = coordinates[i];
|
||||
const prev = coordinates[i - 1]
|
||||
const next = coordinates[i + 1]
|
||||
const b0 = turf.bearing(prev, coordinate, {final: true})
|
||||
const b1 = turf.bearing(coordinate, next)
|
||||
|
||||
const diff = Math.abs(b1 - b0)
|
||||
if (diff < 2) {
|
||||
continue
|
||||
}
|
||||
newCoordinates.push(coordinate)
|
||||
}
|
||||
return newCoordinates
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the intersection between two features.
|
||||
* Returns the length if intersecting a linestring and a (multi)polygon (in meters), returns a surface area (in m²) if intersecting two (multi)polygons
|
||||
|
@ -412,31 +436,6 @@ export class GeoOperations {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to remove points which do not contribute much to the general outline.
|
||||
* Points for which the angle is ~ 180° are removed
|
||||
* @param coordinates
|
||||
* @constructor
|
||||
*/
|
||||
public static SimplifyCoordinates(coordinates: [number, number][]){
|
||||
const newCoordinates = []
|
||||
for (let i = 1; i < coordinates.length - 1; i++){
|
||||
const coordinate = coordinates[i];
|
||||
const prev = coordinates[i - 1]
|
||||
const next = coordinates[i + 1]
|
||||
const b0 = turf.bearing(prev, coordinate, {final: true})
|
||||
const b1 = turf.bearing(coordinate, next)
|
||||
|
||||
const diff = Math.abs(b1 - b0)
|
||||
if(diff < 2){
|
||||
continue
|
||||
}
|
||||
newCoordinates.push(coordinate)
|
||||
}
|
||||
return newCoordinates
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -10,11 +10,6 @@ export default class GenericImageProvider extends ImageProvider {
|
|||
this._valuePrefixBlacklist = valuePrefixBlacklist;
|
||||
}
|
||||
|
||||
|
||||
protected DownloadAttribution(url: string) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
|
||||
if (this._valuePrefixBlacklist.some(prefix => value.startsWith(prefix))) {
|
||||
|
@ -39,5 +34,9 @@ export default class GenericImageProvider extends ImageProvider {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
protected DownloadAttribution(url: string) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -27,8 +27,6 @@ export default abstract class ImageProvider {
|
|||
|
||||
public abstract SourceIcon(backlinkSource?: string): BaseUIElement;
|
||||
|
||||
protected abstract DownloadAttribution(url: string): Promise<LicenseInfo>;
|
||||
|
||||
/**
|
||||
* Given a properies object, maps it onto _all_ the available pictures for this imageProvider
|
||||
*/
|
||||
|
@ -77,4 +75,6 @@ export default abstract class ImageProvider {
|
|||
|
||||
public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>;
|
||||
|
||||
protected abstract DownloadAttribution(url: string): Promise<LicenseInfo>;
|
||||
|
||||
}
|
|
@ -8,9 +8,8 @@ import {LicenseInfo} from "./LicenseInfo";
|
|||
export class Imgur extends ImageProvider {
|
||||
|
||||
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
||||
public readonly defaultKeyPrefixes: string[] = ["image"];
|
||||
|
||||
public static readonly singleton = new Imgur();
|
||||
public readonly defaultKeyPrefixes: string[] = ["image"];
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
|
@ -89,6 +88,17 @@ export class Imgur extends ImageProvider {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
if (Imgur.defaultValuePrefix.some(prefix => value.startsWith(prefix))) {
|
||||
return [Promise.resolve({
|
||||
url: value,
|
||||
key: key,
|
||||
provider: this
|
||||
})]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
protected DownloadAttribution: (url: string) => Promise<LicenseInfo> = async (url: string) => {
|
||||
const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0];
|
||||
|
||||
|
@ -112,16 +122,5 @@ export class Imgur extends ImageProvider {
|
|||
return licenseInfo
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
if (Imgur.defaultValuePrefix.some(prefix => value.startsWith(prefix))) {
|
||||
return [Promise.resolve({
|
||||
url: value,
|
||||
key: key,
|
||||
provider: this
|
||||
})]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -6,9 +6,8 @@ export default class ImgurUploader {
|
|||
public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
private readonly _handleSuccessUrl: (string) => void;
|
||||
|
||||
public maxFileSizeInMegabytes = 10;
|
||||
private readonly _handleSuccessUrl: (string) => void;
|
||||
|
||||
constructor(handleSuccessUrl: (string) => void) {
|
||||
this._handleSuccessUrl = handleSuccessUrl;
|
||||
|
|
|
@ -7,11 +7,10 @@ import Constants from "../../Models/Constants";
|
|||
|
||||
export class Mapillary extends ImageProvider {
|
||||
|
||||
defaultKeyPrefixes = ["mapillary","image"]
|
||||
|
||||
public static readonly singleton = new Mapillary();
|
||||
private static readonly valuePrefix = "https://a.mapillary.com"
|
||||
public static readonly valuePrefixes = [Mapillary.valuePrefix, "http://mapillary.com", "https://mapillary.com", "http://www.mapillary.com", "https://www.mapillary.com"]
|
||||
defaultKeyPrefixes = ["mapillary", "image"]
|
||||
|
||||
private static ExtractKeyFromURL(value: string, failIfNoMath = false): {
|
||||
key: string,
|
||||
|
@ -59,6 +58,31 @@ export class Mapillary extends ImageProvider {
|
|||
return [this.PrepareUrlAsync(key, value)]
|
||||
}
|
||||
|
||||
protected async DownloadAttribution(url: string): Promise<LicenseInfo> {
|
||||
|
||||
const keyV = Mapillary.ExtractKeyFromURL(url)
|
||||
if (keyV.isApiv4) {
|
||||
const license = new LicenseInfo()
|
||||
license.artist = "Contributor name unavailable";
|
||||
license.license = "CC BY-SA 4.0";
|
||||
// license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||
license.attributionRequired = true;
|
||||
return license
|
||||
|
||||
}
|
||||
const key = keyV.key
|
||||
|
||||
const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
|
||||
const data = await Utils.downloadJson(metadataURL)
|
||||
const license = new LicenseInfo();
|
||||
license.artist = data.properties?.username;
|
||||
license.licenseShortName = "CC BY-SA 4.0";
|
||||
license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||
license.attributionRequired = true;
|
||||
|
||||
return license
|
||||
}
|
||||
|
||||
private async PrepareUrlAsync(key: string, value: string): Promise<ProvidedImage> {
|
||||
const failIfNoMatch = key.indexOf("mapillary") < 0
|
||||
const keyV = Mapillary.ExtractKeyFromURL(value, failIfNoMatch)
|
||||
|
@ -85,29 +109,4 @@ export class Mapillary extends ImageProvider {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async DownloadAttribution(url: string): Promise<LicenseInfo> {
|
||||
|
||||
const keyV = Mapillary.ExtractKeyFromURL(url)
|
||||
if (keyV.isApiv4) {
|
||||
const license = new LicenseInfo()
|
||||
license.artist = "Contributor name unavailable";
|
||||
license.license = "CC BY-SA 4.0";
|
||||
// license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||
license.attributionRequired = true;
|
||||
return license
|
||||
|
||||
}
|
||||
const key = keyV.key
|
||||
|
||||
const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
|
||||
const data = await Utils.downloadJson(metadataURL)
|
||||
const license = new LicenseInfo();
|
||||
license.artist = data.properties?.username;
|
||||
license.licenseShortName = "CC BY-SA 4.0";
|
||||
license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||
license.attributionRequired = true;
|
||||
|
||||
return license
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import {Utils} from "../../Utils";
|
||||
import ImageProvider, {ProvidedImage} from "./ImageProvider";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import Svg from "../../Svg";
|
||||
|
@ -7,10 +6,6 @@ import Wikidata from "../Web/Wikidata";
|
|||
|
||||
export class WikidataImageProvider extends ImageProvider {
|
||||
|
||||
public SourceIcon(backlinkSource?: string): BaseUIElement {
|
||||
throw Svg.wikidata_svg();
|
||||
}
|
||||
|
||||
public static readonly singleton = new WikidataImageProvider()
|
||||
public readonly defaultKeyPrefixes = ["wikidata"]
|
||||
|
||||
|
@ -18,8 +13,8 @@ export class WikidataImageProvider extends ImageProvider {
|
|||
super()
|
||||
}
|
||||
|
||||
protected DownloadAttribution(url: string): Promise<any> {
|
||||
throw new Error("Method not implemented; shouldn't be needed!");
|
||||
public SourceIcon(backlinkSource?: string): BaseUIElement {
|
||||
throw Svg.wikidata_svg();
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
|
@ -51,4 +46,8 @@ export class WikidataImageProvider extends ImageProvider {
|
|||
return allImages
|
||||
}
|
||||
|
||||
protected DownloadAttribution(url: string): Promise<any> {
|
||||
throw new Error("Method not implemented; shouldn't be needed!");
|
||||
}
|
||||
|
||||
}
|
|
@ -12,10 +12,10 @@ import Wikimedia from "../Web/Wikimedia";
|
|||
export class WikimediaImageProvider extends ImageProvider {
|
||||
|
||||
|
||||
private readonly commons_key = "wikimedia_commons"
|
||||
public readonly defaultKeyPrefixes = [this.commons_key,"image"]
|
||||
public static readonly singleton = new WikimediaImageProvider();
|
||||
public static readonly commonsPrefixes = ["https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", "File:"]
|
||||
private readonly commons_key = "wikimedia_commons"
|
||||
public readonly defaultKeyPrefixes = [this.commons_key, "image"]
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
|
@ -30,6 +30,40 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
|
||||
}
|
||||
|
||||
private static PrepareUrl(value: string): string {
|
||||
|
||||
if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
return value;
|
||||
}
|
||||
return (`https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(value)}?width=500&height=400`)
|
||||
}
|
||||
|
||||
private static startsWithCommonsPrefix(value: string): boolean {
|
||||
return WikimediaImageProvider.commonsPrefixes.some(prefix => value.startsWith(prefix))
|
||||
}
|
||||
|
||||
private static removeCommonsPrefix(value: string): string {
|
||||
if (value.startsWith("https://upload.wikimedia.org/")) {
|
||||
value = value.substring(value.lastIndexOf("/") + 1)
|
||||
value = decodeURIComponent(value)
|
||||
if (!value.startsWith("File:")) {
|
||||
value = "File:" + value
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
for (const prefix of WikimediaImageProvider.commonsPrefixes) {
|
||||
if (value.startsWith(prefix)) {
|
||||
let part = value.substr(prefix.length)
|
||||
if (prefix.startsWith("http")) {
|
||||
part = decodeURIComponent(part)
|
||||
}
|
||||
return part
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
SourceIcon(backlink: string): BaseUIElement {
|
||||
const img = Svg.wikimedia_commons_white_svg()
|
||||
.SetStyle("width:2em;height: 2em");
|
||||
|
@ -44,12 +78,38 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
|
||||
}
|
||||
|
||||
private static PrepareUrl(value: string): string {
|
||||
public PrepUrl(value: string): ProvidedImage {
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
|
||||
if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
return value;
|
||||
if (value.startsWith("File:")) {
|
||||
return this.UrlForImage(value)
|
||||
}
|
||||
return (`https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(value)}?width=500&height=400`)
|
||||
|
||||
// We do a last effort and assume this is a file
|
||||
return this.UrlForImage("File:" + value)
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
if (key !== undefined && key !== this.commons_key && !hasCommonsPrefix) {
|
||||
return []
|
||||
}
|
||||
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
if (value.startsWith("Category:")) {
|
||||
const urls = await Wikimedia.GetCategoryContents(value)
|
||||
return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image)))
|
||||
}
|
||||
if (value.startsWith("File:")) {
|
||||
return [Promise.resolve(this.UrlForImage(value))]
|
||||
}
|
||||
if (value.startsWith("http")) {
|
||||
// PRobably an error
|
||||
return []
|
||||
}
|
||||
// We do a last effort and assume this is a file
|
||||
return [Promise.resolve(this.UrlForImage("File:" + value))]
|
||||
}
|
||||
|
||||
protected async DownloadAttribution(filename: string): Promise<LicenseInfo> {
|
||||
|
@ -104,66 +164,6 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
return {url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this}
|
||||
}
|
||||
|
||||
private static startsWithCommonsPrefix(value: string): boolean{
|
||||
return WikimediaImageProvider.commonsPrefixes.some(prefix => value.startsWith(prefix))
|
||||
}
|
||||
|
||||
private static removeCommonsPrefix(value: string): string{
|
||||
if(value.startsWith("https://upload.wikimedia.org/")){
|
||||
value = value.substring(value.lastIndexOf("/") + 1)
|
||||
value = decodeURIComponent(value)
|
||||
if(!value.startsWith("File:")){
|
||||
value = "File:"+value
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
for (const prefix of WikimediaImageProvider.commonsPrefixes) {
|
||||
if(value.startsWith(prefix)){
|
||||
let part = value.substr(prefix.length)
|
||||
if(prefix.startsWith("http")){
|
||||
part = decodeURIComponent(part)
|
||||
}
|
||||
return part
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public PrepUrl(value: string): ProvidedImage {
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
|
||||
if (value.startsWith("File:")) {
|
||||
return this.UrlForImage(value)
|
||||
}
|
||||
|
||||
// We do a last effort and assume this is a file
|
||||
return this.UrlForImage("File:" + value)
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
if(key !== undefined && key !== this.commons_key && !hasCommonsPrefix){
|
||||
return []
|
||||
}
|
||||
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
if (value.startsWith("Category:")) {
|
||||
const urls = await Wikimedia.GetCategoryContents(value)
|
||||
return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image)))
|
||||
}
|
||||
if (value.startsWith("File:")) {
|
||||
return [Promise.resolve(this.UrlForImage(value))]
|
||||
}
|
||||
if (value.startsWith("http")) {
|
||||
// PRobably an error
|
||||
return []
|
||||
}
|
||||
// We do a last effort and assume this is a file
|
||||
return [Promise.resolve(this.UrlForImage("File:" + value))]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -147,7 +147,8 @@ export default class MetaTagging {
|
|||
}
|
||||
}
|
||||
|
||||
}} )
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -166,7 +166,8 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
|
|||
|
||||
nodeIdsToUse.push({
|
||||
lat, lon,
|
||||
nodeId : newNodeAction.newElementIdNumber})
|
||||
nodeId: newNodeAction.newElementIdNumber
|
||||
})
|
||||
continue
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface RelationSplitInput {
|
|||
originalNodes: number[],
|
||||
allWaysNodesInOrder: number[][]
|
||||
}
|
||||
|
||||
abstract class AbstractRelationSplitHandler extends OsmChangeAction {
|
||||
protected readonly _input: RelationSplitInput;
|
||||
protected readonly _theme: string;
|
||||
|
@ -101,7 +102,10 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
|
|||
let commonPoint = commonStartPoint ?? commonEndPoint
|
||||
|
||||
// Let's select the way to keep
|
||||
const idToKeep : {id: number} = this._input.allWaysNodesInOrder.map((nodes, i) => ({nodes: nodes, id: this._input.allWayIdsInOrder[i]}))
|
||||
const idToKeep: { id: number } = this._input.allWaysNodesInOrder.map((nodes, i) => ({
|
||||
nodes: nodes,
|
||||
id: this._input.allWayIdsInOrder[i]
|
||||
}))
|
||||
.filter(nodesId => {
|
||||
const nds = nodesId.nodes
|
||||
return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint
|
||||
|
|
|
@ -53,61 +53,6 @@ export class ChangesetHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private handleIdRewrite(node: any, type: string): [string, string] {
|
||||
const oldId = parseInt(node.attributes.old_id.value);
|
||||
if (node.attributes.new_id === undefined) {
|
||||
// We just removed this point!
|
||||
const element = this.allElements.getEventSourceById("node/" + oldId);
|
||||
element.data._deleted = "yes"
|
||||
element.ping();
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = parseInt(node.attributes.new_id.value);
|
||||
const result: [string, string] = [type + "/" + oldId, type + "/" + newId]
|
||||
if (!(oldId !== undefined && newId !== undefined &&
|
||||
!isNaN(oldId) && !isNaN(newId))) {
|
||||
return undefined;
|
||||
}
|
||||
if (oldId == newId) {
|
||||
return undefined;
|
||||
}
|
||||
console.log("Rewriting id: ", type + "/" + oldId, "-->", type + "/" + newId);
|
||||
const element = this.allElements.getEventSourceById("node/" + oldId);
|
||||
if(element === undefined){
|
||||
// Element to rewrite not found, probably a node or relation that is not rendered
|
||||
return undefined
|
||||
}
|
||||
element.data.id = type + "/" + newId;
|
||||
this.allElements.addElementById(type + "/" + newId, element);
|
||||
this.allElements.ContainingFeatures.set(type + "/" + newId, this.allElements.ContainingFeatures.get(type + "/" + oldId))
|
||||
element.ping();
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseUploadChangesetResponse(response: XMLDocument): void {
|
||||
const nodes = response.getElementsByTagName("node");
|
||||
const mappings = new Map<string, string>()
|
||||
// @ts-ignore
|
||||
for (const node of nodes) {
|
||||
const mapping = this.handleIdRewrite(node, "node")
|
||||
if (mapping !== undefined) {
|
||||
mappings.set(mapping[0], mapping[1])
|
||||
}
|
||||
}
|
||||
|
||||
const ways = response.getElementsByTagName("way");
|
||||
// @ts-ignore
|
||||
for (const way of ways) {
|
||||
const mapping = this.handleIdRewrite(way, "way")
|
||||
if (mapping !== undefined) {
|
||||
mappings.set(mapping[0], mapping[1])
|
||||
}
|
||||
}
|
||||
this.changes.registerIdRewrites(mappings)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The full logic to upload a change to one or more elements.
|
||||
*
|
||||
|
@ -207,6 +152,60 @@ export class ChangesetHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private handleIdRewrite(node: any, type: string): [string, string] {
|
||||
const oldId = parseInt(node.attributes.old_id.value);
|
||||
if (node.attributes.new_id === undefined) {
|
||||
// We just removed this point!
|
||||
const element = this.allElements.getEventSourceById("node/" + oldId);
|
||||
element.data._deleted = "yes"
|
||||
element.ping();
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = parseInt(node.attributes.new_id.value);
|
||||
const result: [string, string] = [type + "/" + oldId, type + "/" + newId]
|
||||
if (!(oldId !== undefined && newId !== undefined &&
|
||||
!isNaN(oldId) && !isNaN(newId))) {
|
||||
return undefined;
|
||||
}
|
||||
if (oldId == newId) {
|
||||
return undefined;
|
||||
}
|
||||
console.log("Rewriting id: ", type + "/" + oldId, "-->", type + "/" + newId);
|
||||
const element = this.allElements.getEventSourceById("node/" + oldId);
|
||||
if (element === undefined) {
|
||||
// Element to rewrite not found, probably a node or relation that is not rendered
|
||||
return undefined
|
||||
}
|
||||
element.data.id = type + "/" + newId;
|
||||
this.allElements.addElementById(type + "/" + newId, element);
|
||||
this.allElements.ContainingFeatures.set(type + "/" + newId, this.allElements.ContainingFeatures.get(type + "/" + oldId))
|
||||
element.ping();
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseUploadChangesetResponse(response: XMLDocument): void {
|
||||
const nodes = response.getElementsByTagName("node");
|
||||
const mappings = new Map<string, string>()
|
||||
// @ts-ignore
|
||||
for (const node of nodes) {
|
||||
const mapping = this.handleIdRewrite(node, "node")
|
||||
if (mapping !== undefined) {
|
||||
mappings.set(mapping[0], mapping[1])
|
||||
}
|
||||
}
|
||||
|
||||
const ways = response.getElementsByTagName("way");
|
||||
// @ts-ignore
|
||||
for (const way of ways) {
|
||||
const mapping = this.handleIdRewrite(way, "way")
|
||||
if (mapping !== undefined) {
|
||||
mappings.set(mapping[0], mapping[1])
|
||||
}
|
||||
}
|
||||
this.changes.registerIdRewrites(mappings)
|
||||
|
||||
}
|
||||
|
||||
private async CloseChangeset(changesetId: number = undefined): Promise<void> {
|
||||
const self = this
|
||||
|
|
|
@ -50,18 +50,19 @@ export class OsmConnection {
|
|||
_dryRun: boolean;
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public changesetHandler: ChangesetHandler;
|
||||
private fakeUser: boolean;
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||
private readonly _iframeMode: Boolean | boolean;
|
||||
private readonly _singlePage: boolean;
|
||||
public readonly _oauth_config: {
|
||||
oauth_consumer_key: string,
|
||||
oauth_secret: string,
|
||||
url: string
|
||||
};
|
||||
private fakeUser: boolean;
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||
private readonly _iframeMode: Boolean | boolean;
|
||||
private readonly _singlePage: boolean;
|
||||
private isChecking = false;
|
||||
|
||||
constructor(options:{dryRun?: false | boolean,
|
||||
constructor(options: {
|
||||
dryRun?: false | boolean,
|
||||
fakeUser?: false | boolean,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
|
@ -69,7 +70,8 @@ export class OsmConnection {
|
|||
// Used to keep multiple changesets open and to write to the correct changeset
|
||||
layoutName: string,
|
||||
singlePage?: boolean,
|
||||
osmConfiguration?: "osm" | "osm-test" }
|
||||
osmConfiguration?: "osm" | "osm-test"
|
||||
}
|
||||
) {
|
||||
this.fakeUser = options.fakeUser ?? false;
|
||||
this._singlePage = options.singlePage ?? true;
|
||||
|
|
|
@ -170,41 +170,6 @@ export abstract class OsmObject {
|
|||
const elements: any[] = data.elements;
|
||||
return OsmObject.ParseObjects(elements);
|
||||
}
|
||||
protected static isPolygon(tags: any): boolean {
|
||||
for (const tagsKey in tags) {
|
||||
if (!tags.hasOwnProperty(tagsKey)) {
|
||||
continue
|
||||
}
|
||||
const polyGuide = OsmObject.polygonFeatures.get(tagsKey)
|
||||
if (polyGuide === undefined) {
|
||||
continue
|
||||
}
|
||||
if ((polyGuide.values === null)) {
|
||||
// We match all
|
||||
return !polyGuide.blacklist
|
||||
}
|
||||
// is the key contained?
|
||||
return polyGuide.values.has(tags[tagsKey])
|
||||
}
|
||||
}
|
||||
|
||||
private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> {
|
||||
const result = new Map<string, { values: Set<string>, blacklist: boolean }>();
|
||||
for (const polygonFeature of polygon_features) {
|
||||
const key = polygonFeature.key;
|
||||
|
||||
if (polygonFeature.polygon === "all") {
|
||||
result.set(key, {values: null, blacklist: false})
|
||||
continue
|
||||
}
|
||||
|
||||
const blacklist = polygonFeature.polygon === "blacklist"
|
||||
result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist})
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ParseObjects(elements: any[]): OsmObject[] {
|
||||
const objects: OsmObject[] = [];
|
||||
|
@ -242,6 +207,42 @@ export abstract class OsmObject {
|
|||
return objects;
|
||||
}
|
||||
|
||||
protected static isPolygon(tags: any): boolean {
|
||||
for (const tagsKey in tags) {
|
||||
if (!tags.hasOwnProperty(tagsKey)) {
|
||||
continue
|
||||
}
|
||||
const polyGuide = OsmObject.polygonFeatures.get(tagsKey)
|
||||
if (polyGuide === undefined) {
|
||||
continue
|
||||
}
|
||||
if ((polyGuide.values === null)) {
|
||||
// We match all
|
||||
return !polyGuide.blacklist
|
||||
}
|
||||
// is the key contained?
|
||||
return polyGuide.values.has(tags[tagsKey])
|
||||
}
|
||||
}
|
||||
|
||||
private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> {
|
||||
const result = new Map<string, { values: Set<string>, blacklist: boolean }>();
|
||||
for (const polygonFeature of polygon_features) {
|
||||
const key = polygonFeature.key;
|
||||
|
||||
if (polygonFeature.polygon === "all") {
|
||||
result.set(key, {values: null, blacklist: false})
|
||||
continue
|
||||
}
|
||||
|
||||
const blacklist = polygonFeature.polygon === "blacklist"
|
||||
result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist})
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// The centerpoint of the feature, as [lat, lon]
|
||||
public abstract centerpoint(): [number, number];
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import State from "../../State";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
|
||||
export interface Relation {
|
||||
|
@ -21,10 +20,6 @@ export default class RelationsTracker {
|
|||
constructor() {
|
||||
}
|
||||
|
||||
public RegisterRelations(overpassJson: any): void {
|
||||
this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an overview of the relations - except for multipolygons. We don't care about those
|
||||
* @param overpassJson
|
||||
|
@ -39,6 +34,10 @@ export default class RelationsTracker {
|
|||
return relations
|
||||
}
|
||||
|
||||
public RegisterRelations(overpassJson: any): void {
|
||||
this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson))
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a mapping of {memberId --> {role in relation, id of relation} }
|
||||
* @param relations
|
||||
|
|
|
@ -49,6 +49,8 @@ export default class SimpleMetaTagger {
|
|||
return true;
|
||||
}
|
||||
)
|
||||
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTagger.metatags.filter(tagger => tagger.isLazy)
|
||||
.map(tagger => tagger.keys));
|
||||
private static latlon = new SimpleMetaTagger({
|
||||
keys: ["_lat", "_lon"],
|
||||
doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)"
|
||||
|
@ -78,83 +80,6 @@ export default class SimpleMetaTagger {
|
|||
return true;
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme.
|
||||
* These changes are performed in-place.
|
||||
*
|
||||
* Returns 'true' is at least one change has been made
|
||||
* @param tags
|
||||
*/
|
||||
public static removeBothTagging(tags: any): boolean{
|
||||
let somethingChanged = false
|
||||
/**
|
||||
* Sets the key onto the properties (but doesn't overwrite if already existing)
|
||||
*/
|
||||
function set(k, value) {
|
||||
if (tags[k] === undefined || tags[k] === "") {
|
||||
tags[k] = value
|
||||
somethingChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
if (tags["sidewalk"]) {
|
||||
|
||||
const v = tags["sidewalk"]
|
||||
switch (v) {
|
||||
case "none":
|
||||
case "no":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "no");
|
||||
break
|
||||
case "both":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
case "left":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "no");
|
||||
break;
|
||||
case "right":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
default:
|
||||
set("sidewalk:left", v);
|
||||
set("sidewalk:right", v);
|
||||
break;
|
||||
}
|
||||
delete tags["sidewalk"]
|
||||
somethingChanged = true
|
||||
}
|
||||
|
||||
|
||||
const regex = /\([^:]*\):both:\(.*\)/
|
||||
for (const key in tags) {
|
||||
const v = tags[key]
|
||||
if (key.endsWith(":both")) {
|
||||
const strippedKey = key.substring(0, key.length - ":both".length)
|
||||
set(strippedKey + ":left", v)
|
||||
set(strippedKey + ":right", v)
|
||||
delete tags[key]
|
||||
continue
|
||||
}
|
||||
|
||||
const match = key.match(regex)
|
||||
if (match !== null) {
|
||||
const strippedKey = match[1]
|
||||
const property = match[1]
|
||||
set(strippedKey + ":left:" + property, v)
|
||||
set(strippedKey + ":right:" + property, v)
|
||||
console.log("Left-right rewritten " + key)
|
||||
delete tags[key]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return somethingChanged
|
||||
}
|
||||
|
||||
private static noBothButLeftRight = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["sidewalk:left", "sidewalk:right", "generic_key:left:property", "generic_key:right:property"],
|
||||
|
@ -451,9 +376,6 @@ export default class SimpleMetaTagger {
|
|||
SimpleMetaTagger.noBothButLeftRight
|
||||
|
||||
];
|
||||
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTagger.metatags.filter(tagger => tagger.isLazy)
|
||||
.map(tagger => tagger.keys));
|
||||
|
||||
public readonly keys: string[];
|
||||
public readonly doc: string;
|
||||
public readonly isLazy: boolean;
|
||||
|
@ -481,6 +403,83 @@ export default class SimpleMetaTagger {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme.
|
||||
* These changes are performed in-place.
|
||||
*
|
||||
* Returns 'true' is at least one change has been made
|
||||
* @param tags
|
||||
*/
|
||||
public static removeBothTagging(tags: any): boolean {
|
||||
let somethingChanged = false
|
||||
|
||||
/**
|
||||
* Sets the key onto the properties (but doesn't overwrite if already existing)
|
||||
*/
|
||||
function set(k, value) {
|
||||
if (tags[k] === undefined || tags[k] === "") {
|
||||
tags[k] = value
|
||||
somethingChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
if (tags["sidewalk"]) {
|
||||
|
||||
const v = tags["sidewalk"]
|
||||
switch (v) {
|
||||
case "none":
|
||||
case "no":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "no");
|
||||
break
|
||||
case "both":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
case "left":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "no");
|
||||
break;
|
||||
case "right":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
default:
|
||||
set("sidewalk:left", v);
|
||||
set("sidewalk:right", v);
|
||||
break;
|
||||
}
|
||||
delete tags["sidewalk"]
|
||||
somethingChanged = true
|
||||
}
|
||||
|
||||
|
||||
const regex = /\([^:]*\):both:\(.*\)/
|
||||
for (const key in tags) {
|
||||
const v = tags[key]
|
||||
if (key.endsWith(":both")) {
|
||||
const strippedKey = key.substring(0, key.length - ":both".length)
|
||||
set(strippedKey + ":left", v)
|
||||
set(strippedKey + ":right", v)
|
||||
delete tags[key]
|
||||
continue
|
||||
}
|
||||
|
||||
const match = key.match(regex)
|
||||
if (match !== null) {
|
||||
const strippedKey = match[1]
|
||||
const property = match[1]
|
||||
set(strippedKey + ":left:" + property, v)
|
||||
set(strippedKey + ":right:" + property, v)
|
||||
console.log("Left-right rewritten " + key)
|
||||
delete tags[key]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return somethingChanged
|
||||
}
|
||||
|
||||
public static HelpText(): BaseUIElement {
|
||||
const subElements: (string | BaseUIElement)[] = [
|
||||
new Combine([
|
||||
|
|
|
@ -14,7 +14,6 @@ import {QueryParameters} from "../Web/QueryParameters";
|
|||
import * as personal from "../../assets/themes/personal/personal.json";
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
|
||||
import {Coord} from "@turf/turf";
|
||||
|
||||
/**
|
||||
* Contains all the leaflet-map related state
|
||||
|
@ -123,7 +122,21 @@ export default class MapState extends UserRelatedState {
|
|||
this.AddAllOverlaysToMap(this.leafletMap)
|
||||
}
|
||||
|
||||
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) {
|
||||
const initialized = new Set()
|
||||
for (const overlayToggle of this.overlayToggles) {
|
||||
new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed)
|
||||
initialized.add(overlayToggle.config)
|
||||
}
|
||||
|
||||
for (const tileLayerSource of this.layoutToUse.tileLayerSources) {
|
||||
if (initialized.has(tileLayerSource)) {
|
||||
continue
|
||||
}
|
||||
new ShowOverlayLayer(tileLayerSource, leafletMap)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private lockBounds() {
|
||||
const layout = this.layoutToUse;
|
||||
|
@ -201,21 +214,5 @@ export default class MapState extends UserRelatedState {
|
|||
return new UIEventSource<FilteredLayer[]>(flayers);
|
||||
}
|
||||
|
||||
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) {
|
||||
const initialized = new Set()
|
||||
for (const overlayToggle of this.overlayToggles) {
|
||||
new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed)
|
||||
initialized.add(overlayToggle.config)
|
||||
}
|
||||
|
||||
for (const tileLayerSource of this.layoutToUse.tileLayerSources) {
|
||||
if (initialized.has(tileLayerSource)) {
|
||||
continue
|
||||
}
|
||||
new ShowOverlayLayer(tileLayerSource, leafletMap)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import {Utils} from "../Utils";
|
||||
import * as Events from "events";
|
||||
|
||||
export class UIEventSource<T> {
|
||||
|
||||
|
@ -76,27 +75,6 @@ export class UIEventSource<T> {
|
|||
return src
|
||||
}
|
||||
|
||||
public AsPromise(): Promise<T>{
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
if(self.data !== undefined){
|
||||
resolve(self.data)
|
||||
}else{
|
||||
self.addCallbackD(data => {
|
||||
resolve(data)
|
||||
return true; // return true to unregister as we only need to be called once
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public WaitForPromise(promise: Promise<T>, onFail: ((any) => void)): UIEventSource<T> {
|
||||
const self = this;
|
||||
promise?.then(d => self.setData(d))
|
||||
promise?.catch(err =>onFail(err))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated.
|
||||
* If the promise fails, the value will stay undefined
|
||||
|
@ -110,20 +88,6 @@ export class UIEventSource<T> {
|
|||
return src
|
||||
}
|
||||
|
||||
public withEqualityStabilized(comparator: (t:T | undefined, t1:T | undefined) => boolean): UIEventSource<T>{
|
||||
let oldValue = undefined;
|
||||
return this.map(v => {
|
||||
if(v == oldValue){
|
||||
return oldValue
|
||||
}
|
||||
if(comparator(oldValue, v)){
|
||||
return oldValue
|
||||
}
|
||||
oldValue = v;
|
||||
return v;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a UIEVentSource with a list, returns a new UIEventSource which is only updated if the _contents_ of the list are different.
|
||||
* E.g.
|
||||
|
@ -168,6 +132,57 @@ export class UIEventSource<T> {
|
|||
return stable
|
||||
}
|
||||
|
||||
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
return source.map(
|
||||
(str) => {
|
||||
let parsed = parseFloat(str);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
},
|
||||
[],
|
||||
(fl) => {
|
||||
if (fl === undefined || isNaN(fl)) {
|
||||
return undefined;
|
||||
}
|
||||
return ("" + fl).substr(0, 8);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public AsPromise(): Promise<T> {
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (self.data !== undefined) {
|
||||
resolve(self.data)
|
||||
} else {
|
||||
self.addCallbackD(data => {
|
||||
resolve(data)
|
||||
return true; // return true to unregister as we only need to be called once
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public WaitForPromise(promise: Promise<T>, onFail: ((any) => void)): UIEventSource<T> {
|
||||
const self = this;
|
||||
promise?.then(d => self.setData(d))
|
||||
promise?.catch(err => onFail(err))
|
||||
return this
|
||||
}
|
||||
|
||||
public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): UIEventSource<T> {
|
||||
let oldValue = undefined;
|
||||
return this.map(v => {
|
||||
if (v == oldValue) {
|
||||
return oldValue
|
||||
}
|
||||
if (comparator(oldValue, v)) {
|
||||
return oldValue
|
||||
}
|
||||
oldValue = v;
|
||||
return v;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a callback
|
||||
*
|
||||
|
@ -335,20 +350,4 @@ export class UIEventSource<T> {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
return source.map(
|
||||
(str) => {
|
||||
let parsed = parseFloat(str);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
},
|
||||
[],
|
||||
(fl) => {
|
||||
if (fl === undefined || isNaN(fl)) {
|
||||
return undefined;
|
||||
}
|
||||
return ("" + fl).substr(0, 8);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -31,7 +31,8 @@ export default class Wikipedia {
|
|||
|
||||
public static GetArticle(options: {
|
||||
pageName: string,
|
||||
language?: "en" | string}): UIEventSource<{ success: string } | { error: any }>{
|
||||
language?: "en" | string
|
||||
}): UIEventSource<{ success: string } | { error: any }> {
|
||||
const key = (options.language ?? "en") + ":" + options.pageName
|
||||
const cached = Wikipedia._cache.get(key)
|
||||
if (cached !== undefined) {
|
||||
|
@ -69,12 +70,10 @@ export default class Wikipedia {
|
|||
}
|
||||
|
||||
|
||||
|
||||
const links = Array.from(content.getElementsByTagName("a"))
|
||||
|
||||
// Rewrite relative links to absolute links + open them in a new tab
|
||||
links.filter(link => link.getAttribute("href")?.startsWith("/") ?? false).
|
||||
forEach(link => {
|
||||
links.filter(link => link.getAttribute("href")?.startsWith("/") ?? false).forEach(link => {
|
||||
link.target = '_blank'
|
||||
// note: link.getAttribute("href") gets the textual value, link.href is the rewritten version which'll contain the host for relative paths
|
||||
link.href = `https://${language}.wikipedia.org${link.getAttribute("href")}`;
|
||||
|
|
|
@ -18,7 +18,6 @@ export default class Constants {
|
|||
]
|
||||
|
||||
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
moreScreenUnlock: 1,
|
||||
|
|
|
@ -89,6 +89,7 @@ export class Denomination {
|
|||
|
||||
value = value.toLowerCase()
|
||||
const self = this;
|
||||
|
||||
function startsWith(key) {
|
||||
if (self.prefix) {
|
||||
return value.startsWith(key)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import LayerConfig from "./ThemeConfig/LayerConfig";
|
||||
import {And} from "../Logic/Tags/And";
|
||||
import FilterConfig from "./ThemeConfig/FilterConfig";
|
||||
|
||||
export default interface FilteredLayer {
|
||||
|
|
|
@ -216,8 +216,6 @@ export interface LayerConfigJson {
|
|||
}) [],
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* All the extra questions for filtering
|
||||
*/
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
import {LayerConfigJson} from "./LayerConfigJson";
|
||||
import TilesourceConfigJson from "./TilesourceConfigJson";
|
||||
|
||||
|
|
|
@ -16,8 +16,7 @@ export default interface UnitConfigJson {
|
|||
|
||||
}
|
||||
|
||||
export interface ApplicableUnitJson
|
||||
{
|
||||
export interface ApplicableUnitJson {
|
||||
/**
|
||||
* The canonical value which will be added to the text.
|
||||
* e.g. "m" for meters
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
|
||||
import WithContextLoader from "./WithContextLoader";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import TagRenderingConfig from "./TagRenderingConfig";
|
||||
import {Utils} from "../../Utils";
|
||||
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
|
||||
|
|
|
@ -77,21 +77,6 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
this.rotation = this.tr("rotation", "0");
|
||||
}
|
||||
|
||||
|
||||
public ExtractImages(): Set<string> {
|
||||
const parts: Set<string>[] = [];
|
||||
parts.push(this.icon?.ExtractImages(true));
|
||||
parts.push(
|
||||
...this.iconBadges?.map((overlay) => overlay.then.ExtractImages(true))
|
||||
);
|
||||
|
||||
const allIcons = new Set<string>();
|
||||
for (const part of parts) {
|
||||
part?.forEach(allIcons.add, allIcons);
|
||||
}
|
||||
return allIcons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a single HTML spec (either a single image path OR "image_path_to_known_svg:fill-colour", returns a fixedUIElement containing that
|
||||
* The element will fill 100% and be positioned absolutely with top:0 and left: 0
|
||||
|
@ -130,6 +115,20 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
}
|
||||
}
|
||||
|
||||
public ExtractImages(): Set<string> {
|
||||
const parts: Set<string>[] = [];
|
||||
parts.push(this.icon?.ExtractImages(true));
|
||||
parts.push(
|
||||
...this.iconBadges?.map((overlay) => overlay.then.ExtractImages(true))
|
||||
);
|
||||
|
||||
const allIcons = new Set<string>();
|
||||
for (const part of parts) {
|
||||
part?.forEach(allIcons.add, allIcons);
|
||||
}
|
||||
return allIcons;
|
||||
}
|
||||
|
||||
public GetSimpleIcon(tags: UIEventSource<any>): BaseUIElement {
|
||||
const self = this;
|
||||
if (this.icon === undefined) {
|
||||
|
@ -147,48 +146,6 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
})).SetClass("w-full h-full block")
|
||||
}
|
||||
|
||||
private GetBadges(tags: UIEventSource<any>): BaseUIElement {
|
||||
if (this.iconBadges.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return new VariableUiElement(
|
||||
tags.map(tags => {
|
||||
|
||||
const badgeElements = this.iconBadges.map(badge => {
|
||||
|
||||
if (!badge.if.matchesProperties(tags)) {
|
||||
// Doesn't match...
|
||||
return undefined
|
||||
}
|
||||
|
||||
const htmlDefs = Utils.SubstituteKeys(badge.then.GetRenderValue(tags)?.txt, tags)
|
||||
const badgeElement= PointRenderingConfig.FromHtmlMulti(htmlDefs, "0", true)?.SetClass("block relative")
|
||||
if(badgeElement === undefined){
|
||||
return undefined;
|
||||
}
|
||||
return new Combine([badgeElement]).SetStyle("width: 1.5rem").SetClass("block")
|
||||
|
||||
})
|
||||
|
||||
return new Combine(badgeElements).SetClass("inline-flex h-full")
|
||||
})).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
|
||||
}
|
||||
|
||||
private GetLabel(tags: UIEventSource<any>): BaseUIElement {
|
||||
if (this.label === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const self = this;
|
||||
return new VariableUiElement(tags.map(tags => {
|
||||
const label = self.label
|
||||
?.GetRenderValue(tags)
|
||||
?.Subs(tags)
|
||||
?.SetClass("block text-center")
|
||||
return new Combine([label]).SetClass("flex flex-col items-center mt-1")
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
public GenerateLeafletStyle(
|
||||
tags: UIEventSource<any>,
|
||||
clickable: boolean,
|
||||
|
@ -264,4 +221,46 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
};
|
||||
}
|
||||
|
||||
private GetBadges(tags: UIEventSource<any>): BaseUIElement {
|
||||
if (this.iconBadges.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return new VariableUiElement(
|
||||
tags.map(tags => {
|
||||
|
||||
const badgeElements = this.iconBadges.map(badge => {
|
||||
|
||||
if (!badge.if.matchesProperties(tags)) {
|
||||
// Doesn't match...
|
||||
return undefined
|
||||
}
|
||||
|
||||
const htmlDefs = Utils.SubstituteKeys(badge.then.GetRenderValue(tags)?.txt, tags)
|
||||
const badgeElement = PointRenderingConfig.FromHtmlMulti(htmlDefs, "0", true)?.SetClass("block relative")
|
||||
if (badgeElement === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new Combine([badgeElement]).SetStyle("width: 1.5rem").SetClass("block")
|
||||
|
||||
})
|
||||
|
||||
return new Combine(badgeElements).SetClass("inline-flex h-full")
|
||||
})).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
|
||||
}
|
||||
|
||||
private GetLabel(tags: UIEventSource<any>): BaseUIElement {
|
||||
if (this.label === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const self = this;
|
||||
return new VariableUiElement(tags.map(tags => {
|
||||
const label = self.label
|
||||
?.GetRenderValue(tags)
|
||||
?.Subs(tags)
|
||||
?.SetClass("block text-center")
|
||||
return new Combine([label]).SetClass("flex flex-col items-center mt-1")
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -39,7 +39,8 @@ export default class SourceConfig {
|
|||
if (params.geojsonSource !== undefined && params.geojsonSourceLevel !== undefined) {
|
||||
if (!["x", "y", "x_min", "x_max", "y_min", "Y_max"].some(toSearch => params.geojsonSource.indexOf(toSearch) > 0)) {
|
||||
throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})`
|
||||
}}
|
||||
}
|
||||
}
|
||||
this.osmTags = params.osmTags ?? new RegexTag("id", /.*/);
|
||||
this.overpassScript = params.overpassScript;
|
||||
this.geojsonSource = params.geojsonSource;
|
||||
|
|
|
@ -260,6 +260,7 @@ export default class TagRenderingConfig {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the render values. Will return multiple render values if 'multianswer' is enabled.
|
||||
* The result will equal [GetRenderValue] if not 'multiAnswer'
|
||||
|
|
|
@ -4,8 +4,8 @@ import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
|
|||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class WithContextLoader {
|
||||
private readonly _json: any;
|
||||
protected readonly _context: string;
|
||||
private readonly _json: any;
|
||||
|
||||
constructor(json: any, context: string) {
|
||||
this._json = json;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {control} from "leaflet";
|
||||
import zoom = control.zoom;
|
||||
|
||||
|
||||
export interface TileRange {
|
||||
xstart: number,
|
||||
|
@ -27,24 +27,6 @@ export class Tiles {
|
|||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static tile2long(x, z) {
|
||||
return (x / Math.pow(2, z) * 360 - 180);
|
||||
}
|
||||
|
||||
private static tile2lat(y, z) {
|
||||
const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
|
||||
return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))));
|
||||
}
|
||||
|
||||
private static lon2tile(lon, zoom) {
|
||||
return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)));
|
||||
}
|
||||
|
||||
private static lat2tile(lat, zoom) {
|
||||
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tile bounds of the
|
||||
* @param z
|
||||
|
@ -56,7 +38,6 @@ export class Tiles {
|
|||
return [[Tiles.tile2lat(y, z), Tiles.tile2long(x, z)], [Tiles.tile2lat(y + 1, z), Tiles.tile2long(x + 1, z)]]
|
||||
}
|
||||
|
||||
|
||||
static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] {
|
||||
return [[Tiles.tile2long(x, z), Tiles.tile2lat(y, z)], [Tiles.tile2long(x + 1, z), Tiles.tile2lat(y + 1, z)]]
|
||||
}
|
||||
|
@ -74,6 +55,7 @@ export class Tiles {
|
|||
static tile_index(z: number, x: number, y: number): number {
|
||||
return ((x * (2 << z)) + y) * 100 + z
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a tile index number, returns [z, x, y]
|
||||
* @param index
|
||||
|
@ -114,5 +96,22 @@ export class Tiles {
|
|||
}
|
||||
}
|
||||
|
||||
private static tile2long(x, z) {
|
||||
return (x / Math.pow(2, z) * 360 - 180);
|
||||
}
|
||||
|
||||
private static tile2lat(y, z) {
|
||||
const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
|
||||
return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))));
|
||||
}
|
||||
|
||||
private static lon2tile(lon, zoom) {
|
||||
return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)));
|
||||
}
|
||||
|
||||
private static lat2tile(lat, zoom) {
|
||||
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -37,7 +37,9 @@ export class Unit {
|
|||
const possiblePostFixes = new Set<string>()
|
||||
|
||||
function addPostfixesOf(str) {
|
||||
if(str === undefined){return}
|
||||
if (str === undefined) {
|
||||
return
|
||||
}
|
||||
str = str.toLowerCase()
|
||||
for (let i = 0; i < str.length + 1; i++) {
|
||||
const substr = str.substring(0, i)
|
||||
|
|
17
README.md
|
@ -2,8 +2,8 @@
|
|||
|
||||
> Let a thousand flowers bloom
|
||||
|
||||
**MapComplete is an OpenStreetMap viewer and editor.** It shows map features on a certain topic, and allows to see, edit and
|
||||
add new features to the map. It can be seen as a
|
||||
**MapComplete is an OpenStreetMap viewer and editor.** It shows map features on a certain topic, and allows to see, edit
|
||||
and add new features to the map. It can be seen as a
|
||||
webversion [crossover of StreetComplete and MapContrib](Docs/MapComplete_vs_other_editors.md). It tries to be just as
|
||||
easy to use as StreetComplete, but it allows to focus on one single theme per instance (e.g. nature, bicycle
|
||||
infrastructure, ...)
|
||||
|
@ -15,20 +15,23 @@ infrastructure, ...)
|
|||
- Easy to set up a custom theme
|
||||
- Easy to fall down the rabbit hole of OSM
|
||||
|
||||
**The basic functionality is** to download some map features from Overpass and then ask certain questions. An answer is sent
|
||||
back to directly to OpenStreetMap.
|
||||
**The basic functionality is** to download some map features from Overpass and then ask certain questions. An answer is
|
||||
sent back to directly to OpenStreetMap.
|
||||
|
||||
Furthermore, it shows images present in the `image` tag or, if a `wikidata` or `wikimedia_commons`-tag is present, it
|
||||
follows those to get these images too.
|
||||
|
||||
**An explicit non-goal** of MapComplete is to modify geometries of ways. Although adding a point to a way or splitting a way
|
||||
in two parts might be added one day.
|
||||
**An explicit non-goal** of MapComplete is to modify geometries of ways. Although adding a point to a way or splitting a
|
||||
way in two parts might be added one day.
|
||||
|
||||
**More about MapComplete:** [Watch Pieter's talk on the 2021 State Of The Map Conference](https://media.ccc.de/v/sotm2021-9448-introduction-and-review-of-mapcomplete) ([YouTube](https://www.youtube.com/watch?v=zTtMn6fNbYY)) about the history, vision and future of MapComplete.
|
||||
**More about
|
||||
MapComplete:** [Watch Pieter's talk on the 2021 State Of The Map Conference](https://media.ccc.de/v/sotm2021-9448-introduction-and-review-of-mapcomplete) ([YouTube](https://www.youtube.com/watch?v=zTtMn6fNbYY))
|
||||
about the history, vision and future of MapComplete.
|
||||
|
||||
# Creating your own theme
|
||||
|
||||
It is possible to quickly make and distribute your own theme
|
||||
|
||||
- [please read the documentation on how to do this](Docs/Making_Your_Own_Theme.md).
|
||||
|
||||
## Examples
|
||||
|
|
1
State.ts
|
@ -15,5 +15,4 @@ export default class State extends FeaturePipelineState {
|
|||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import {Utils} from "../Utils";
|
|||
import LanguagePicker from "./LanguagePicker";
|
||||
import IndexText from "./BigComponents/IndexText";
|
||||
import FeaturedMessage from "./BigComponents/FeaturedMessage";
|
||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
|
||||
|
||||
export default class AllThemesGui {
|
||||
constructor() {
|
||||
|
|
|
@ -21,7 +21,6 @@ export default class AsyncLazy extends BaseUIElement{
|
|||
}
|
||||
return el
|
||||
})
|
||||
|
||||
).ConstructElement()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import Combine from "./Combine";
|
||||
import Svg from "../../Svg";
|
||||
|
|
|
@ -18,7 +18,9 @@ export interface MinimapOptions {
|
|||
|
||||
export interface MinimapObj {
|
||||
readonly leafletMap: UIEventSource<any>,
|
||||
|
||||
installBounds(factor: number | BBox, showRange?: boolean): void
|
||||
|
||||
TakeScreenshot(): Promise<any>;
|
||||
}
|
||||
|
||||
|
|
|
@ -103,6 +103,12 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
})
|
||||
}
|
||||
|
||||
public async TakeScreenshot() {
|
||||
const screenshotter = new SimpleMapScreenshoter();
|
||||
screenshotter.addTo(this.leafletMap.data);
|
||||
return await screenshotter.takeScreen('image')
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const div = document.createElement("div")
|
||||
div.id = this._id;
|
||||
|
@ -279,10 +285,4 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
|
||||
this.leafletMap.setData(map)
|
||||
}
|
||||
|
||||
public async TakeScreenshot(){
|
||||
const screenshotter = new SimpleMapScreenshoter();
|
||||
screenshotter.addTo(this.leafletMap.data);
|
||||
return await screenshotter.takeScreen('image')
|
||||
}
|
||||
}
|
|
@ -19,8 +19,8 @@ import Img from "./Img";
|
|||
export default class ScrollableFullScreen extends UIElement {
|
||||
private static readonly empty = new FixedUiElement("");
|
||||
private static _currentlyOpen: ScrollableFullScreen;
|
||||
private hashToShow: string;
|
||||
public isShown: UIEventSource<boolean>;
|
||||
private hashToShow: string;
|
||||
private _component: BaseUIElement;
|
||||
private _fullscreencomponent: BaseUIElement;
|
||||
|
||||
|
@ -61,13 +61,6 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
})
|
||||
}
|
||||
|
||||
private clear() {
|
||||
ScrollableFullScreen.empty.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen");
|
||||
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
|
||||
fs.classList.add("hidden")
|
||||
}
|
||||
|
||||
InnerRender(): BaseUIElement {
|
||||
return this._component;
|
||||
}
|
||||
|
@ -80,6 +73,13 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
fs.classList.remove("hidden")
|
||||
}
|
||||
|
||||
private clear() {
|
||||
ScrollableFullScreen.empty.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen");
|
||||
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
|
||||
fs.classList.add("hidden")
|
||||
}
|
||||
|
||||
private BuildComponent(title: BaseUIElement, content: BaseUIElement, isShown: UIEventSource<boolean>) {
|
||||
const returnToTheMap =
|
||||
new Combine([
|
||||
|
|
|
@ -45,7 +45,9 @@ export default abstract class BaseUIElement {
|
|||
* Adds all the relevant classes, space separated
|
||||
*/
|
||||
public SetClass(clss: string) {
|
||||
if(clss == undefined){return }
|
||||
if (clss == undefined) {
|
||||
return
|
||||
}
|
||||
const all = clss.split(" ").map(clsName => clsName.trim());
|
||||
let recordedChange = false;
|
||||
for (let c of all) {
|
||||
|
|
|
@ -63,7 +63,10 @@ export default class CopyrightPanel extends Combine {
|
|||
|
||||
new VariableUiElement(state.locationControl.map(location => {
|
||||
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
|
||||
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {url: mapillaryLink, newTab: true})
|
||||
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {
|
||||
url: mapillaryLink,
|
||||
newTab: true
|
||||
})
|
||||
})),
|
||||
new VariableUiElement(josmState.map(state => {
|
||||
if (state === undefined) {
|
||||
|
|
|
@ -12,7 +12,6 @@ import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {meta} from "@turf/turf";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
export class DownloadPanel extends Toggle {
|
||||
|
|
|
@ -9,7 +9,6 @@ import Toggle from "../Input/Toggle";
|
|||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import Loading from "../Base/Loading";
|
||||
import CreateNewWayAction from "../../Logic/Osm/Actions/CreateNewWayAction";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import {Changes} from "../../Logic/Osm/Changes";
|
||||
|
@ -32,7 +31,6 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
|
|||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||
import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import CreateWayWithPointReuseAction from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
|
||||
|
|
|
@ -39,110 +39,6 @@ export default class MoreScreen extends Combine {
|
|||
]);
|
||||
}
|
||||
|
||||
|
||||
private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses): BaseUIElement {
|
||||
return new VariableUiElement(state.installedThemes.map(customThemes => {
|
||||
if (customThemes.length <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
const customThemeButtons = customThemes.map(theme => MoreScreen.createLinkButton(state, theme.layout, theme.definition)?.SetClass(buttonClass))
|
||||
return new Combine([
|
||||
Translations.t.general.customThemeIntro.Clone(),
|
||||
new Combine(customThemeButtons).SetClass(themeListClasses)
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string) {
|
||||
const t = Translations.t.general.morescreen
|
||||
const prefix = "mapcomplete-hidden-theme-"
|
||||
const hiddenTotal = AllKnownLayouts.layoutsList.filter(layout => layout.hideFromOverview).length
|
||||
return new Toggle(
|
||||
new VariableUiElement(
|
||||
state.osmConnection.preferencesHandler.preferences.map(allPreferences => {
|
||||
const knownThemes = Utils.NoNull(Object.keys(allPreferences)
|
||||
.filter(key => key.startsWith(prefix))
|
||||
.map(key => key.substring(prefix.length, key.length - "-enabled".length))
|
||||
.map(theme => {
|
||||
return AllKnownLayouts.allKnownLayouts.get(theme);
|
||||
}))
|
||||
if (knownThemes.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const knownLayouts = new Combine(knownThemes.map(layout =>
|
||||
MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass)
|
||||
)).SetClass(themeListStyle)
|
||||
|
||||
return new Combine([
|
||||
new Title(t.previouslyHiddenTitle),
|
||||
t.hiddenExplanation.Subs({hidden_discovered: ""+knownThemes.length,total_hidden: ""+hiddenTotal}),
|
||||
knownLayouts
|
||||
])
|
||||
|
||||
})
|
||||
).SetClass("flex flex-col"),
|
||||
undefined,
|
||||
state.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string): BaseUIElement {
|
||||
let officialThemes = AllKnownLayouts.layoutsList
|
||||
|
||||
let buttons = officialThemes.map((layout) => {
|
||||
if (layout === undefined) {
|
||||
console.trace("Layout is undefined")
|
||||
return undefined
|
||||
}
|
||||
if(layout.hideFromOverview){
|
||||
return undefined;
|
||||
}
|
||||
const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass);
|
||||
if (layout.id === personal.id) {
|
||||
return new VariableUiElement(
|
||||
state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
|
||||
.map(csCount => {
|
||||
if (csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||
return undefined
|
||||
} else {
|
||||
return button
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return button;
|
||||
})
|
||||
|
||||
let customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
|
||||
buttons.splice(0, 0, customGeneratorLink);
|
||||
|
||||
return new Combine(buttons)
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
|
||||
* */
|
||||
private static createCustomGeneratorButton(state: { osmConnection: OsmConnection }): VariableUiElement {
|
||||
const tr = Translations.t.general.morescreen;
|
||||
return new VariableUiElement(
|
||||
state.osmConnection.userDetails.map(userDetails => {
|
||||
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
|
||||
return new SubtleButton(null, tr.requestATheme.Clone(), {
|
||||
url: "https://github.com/pietervdvn/MapComplete/issues",
|
||||
newTab: true
|
||||
});
|
||||
}
|
||||
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), {
|
||||
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
|
||||
newTab: false
|
||||
});
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a button linking to the given theme
|
||||
* @private
|
||||
|
@ -210,5 +106,111 @@ export default class MoreScreen extends Combine {
|
|||
]), {url: linkText, newTab: false});
|
||||
}
|
||||
|
||||
private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses): BaseUIElement {
|
||||
return new VariableUiElement(state.installedThemes.map(customThemes => {
|
||||
if (customThemes.length <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
const customThemeButtons = customThemes.map(theme => MoreScreen.createLinkButton(state, theme.layout, theme.definition)?.SetClass(buttonClass))
|
||||
return new Combine([
|
||||
Translations.t.general.customThemeIntro.Clone(),
|
||||
new Combine(customThemeButtons).SetClass(themeListClasses)
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string) {
|
||||
const t = Translations.t.general.morescreen
|
||||
const prefix = "mapcomplete-hidden-theme-"
|
||||
const hiddenTotal = AllKnownLayouts.layoutsList.filter(layout => layout.hideFromOverview).length
|
||||
return new Toggle(
|
||||
new VariableUiElement(
|
||||
state.osmConnection.preferencesHandler.preferences.map(allPreferences => {
|
||||
const knownThemes = Utils.NoNull(Object.keys(allPreferences)
|
||||
.filter(key => key.startsWith(prefix))
|
||||
.map(key => key.substring(prefix.length, key.length - "-enabled".length))
|
||||
.map(theme => {
|
||||
return AllKnownLayouts.allKnownLayouts.get(theme);
|
||||
}))
|
||||
if (knownThemes.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const knownLayouts = new Combine(knownThemes.map(layout =>
|
||||
MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass)
|
||||
)).SetClass(themeListStyle)
|
||||
|
||||
return new Combine([
|
||||
new Title(t.previouslyHiddenTitle),
|
||||
t.hiddenExplanation.Subs({
|
||||
hidden_discovered: "" + knownThemes.length,
|
||||
total_hidden: "" + hiddenTotal
|
||||
}),
|
||||
knownLayouts
|
||||
])
|
||||
|
||||
})
|
||||
).SetClass("flex flex-col"),
|
||||
undefined,
|
||||
state.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string): BaseUIElement {
|
||||
let officialThemes = AllKnownLayouts.layoutsList
|
||||
|
||||
let buttons = officialThemes.map((layout) => {
|
||||
if (layout === undefined) {
|
||||
console.trace("Layout is undefined")
|
||||
return undefined
|
||||
}
|
||||
if (layout.hideFromOverview) {
|
||||
return undefined;
|
||||
}
|
||||
const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass);
|
||||
if (layout.id === personal.id) {
|
||||
return new VariableUiElement(
|
||||
state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
|
||||
.map(csCount => {
|
||||
if (csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||
return undefined
|
||||
} else {
|
||||
return button
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return button;
|
||||
})
|
||||
|
||||
let customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
|
||||
buttons.splice(0, 0, customGeneratorLink);
|
||||
|
||||
return new Combine(buttons)
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
|
||||
* */
|
||||
private static createCustomGeneratorButton(state: { osmConnection: OsmConnection }): VariableUiElement {
|
||||
const tr = Translations.t.general.morescreen;
|
||||
return new VariableUiElement(
|
||||
state.osmConnection.userDetails.map(userDetails => {
|
||||
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
|
||||
return new SubtleButton(null, tr.requestATheme.Clone(), {
|
||||
url: "https://github.com/pietervdvn/MapComplete/issues",
|
||||
newTab: true
|
||||
});
|
||||
}
|
||||
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), {
|
||||
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
|
||||
newTab: false
|
||||
});
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -62,7 +62,6 @@ export default class ShareScreen extends Combine {
|
|||
}
|
||||
|
||||
|
||||
|
||||
const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = state.backgroundLayer;
|
||||
const currentBackground = new VariableUiElement(currentLayer.map(layer => {
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""});
|
||||
|
|
|
@ -46,6 +46,49 @@ export default class DefaultGUI {
|
|||
this.SetupMap()
|
||||
}
|
||||
|
||||
public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, state: FeaturePipelineState) {
|
||||
|
||||
function setup() {
|
||||
let presetCount = 0;
|
||||
for (const layer of state.layoutToUse.layers) {
|
||||
for (const preset of layer.presets) {
|
||||
presetCount++;
|
||||
}
|
||||
}
|
||||
if (presetCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPointDialogIsShown = new UIEventSource<boolean>(false);
|
||||
const addNewPoint = new ScrollableFullScreen(
|
||||
() => Translations.t.general.add.title.Clone(),
|
||||
() => new SimpleAddUI(newPointDialogIsShown, filterViewIsOpened, state),
|
||||
"new",
|
||||
newPointDialogIsShown
|
||||
);
|
||||
addNewPoint.isShown.addCallback((isShown) => {
|
||||
if (!isShown) {
|
||||
state.LastClickLocation.setData(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
new StrayClickHandler(
|
||||
state.LastClickLocation,
|
||||
state.selectedElement,
|
||||
state.filteredLayers,
|
||||
state.leafletMap,
|
||||
addNewPoint
|
||||
);
|
||||
}
|
||||
|
||||
state.featureSwitchAddNew.addCallbackAndRunD(addNewAllowed => {
|
||||
if (addNewAllowed) {
|
||||
setup()
|
||||
return true;
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private SetupMap() {
|
||||
const state = this.state;
|
||||
|
@ -163,48 +206,4 @@ export default class DefaultGUI {
|
|||
|
||||
}
|
||||
|
||||
public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, state: FeaturePipelineState) {
|
||||
|
||||
function setup() {
|
||||
let presetCount = 0;
|
||||
for (const layer of state.layoutToUse.layers) {
|
||||
for (const preset of layer.presets) {
|
||||
presetCount++;
|
||||
}
|
||||
}
|
||||
if (presetCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPointDialogIsShown = new UIEventSource<boolean>(false);
|
||||
const addNewPoint = new ScrollableFullScreen(
|
||||
() => Translations.t.general.add.title.Clone(),
|
||||
() => new SimpleAddUI(newPointDialogIsShown, filterViewIsOpened, state),
|
||||
"new",
|
||||
newPointDialogIsShown
|
||||
);
|
||||
addNewPoint.isShown.addCallback((isShown) => {
|
||||
if (!isShown) {
|
||||
state.LastClickLocation.setData(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
new StrayClickHandler(
|
||||
state.LastClickLocation,
|
||||
state.selectedElement,
|
||||
state.filteredLayers,
|
||||
state.leafletMap,
|
||||
addNewPoint
|
||||
);
|
||||
}
|
||||
|
||||
state.featureSwitchAddNew.addCallbackAndRunD(addNewAllowed => {
|
||||
if (addNewAllowed) {
|
||||
setup()
|
||||
return true;
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -4,13 +4,13 @@ import Constants from "../Models/Constants";
|
|||
import Hash from "../Logic/Web/Hash";
|
||||
|
||||
export class DefaultGuiState {
|
||||
static state: DefaultGuiState;
|
||||
public readonly welcomeMessageIsOpened: UIEventSource<boolean>;
|
||||
public readonly downloadControlIsOpened: UIEventSource<boolean>;
|
||||
public readonly filterViewIsOpened: UIEventSource<boolean>;
|
||||
public readonly copyrightViewIsOpened: UIEventSource<boolean>;
|
||||
public readonly welcomeMessageOpenedTab: UIEventSource<number>
|
||||
public readonly allFullScreenStates: UIEventSource<boolean>[] = []
|
||||
static state: DefaultGuiState;
|
||||
|
||||
constructor() {
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
|||
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
|
||||
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
|
||||
import {BBox} from "../Logic/BBox";
|
||||
|
||||
/**
|
||||
* Creates screenshoter to take png screenshot
|
||||
* Creates jspdf and downloads it
|
||||
|
@ -109,7 +110,6 @@ export default class ExportPDF {
|
|||
private async CreatePdf(minimap: MinimapObj) {
|
||||
|
||||
|
||||
|
||||
console.log("PDF creation started")
|
||||
const t = Translations.t.general.pdf;
|
||||
const layout = this._layout
|
||||
|
|
|
@ -167,6 +167,7 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
|
|||
installBounds(factor: number | BBox, showRange?: boolean): void {
|
||||
this.map.installBounds(factor, showRange)
|
||||
}
|
||||
|
||||
TakeScreenshot(): Promise<any> {
|
||||
return this.map.TakeScreenshot()
|
||||
}
|
||||
|
|
|
@ -18,14 +18,6 @@ export default class Toggle extends VariableUiElement {
|
|||
this.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
public ToggleOnClick(): Toggle {
|
||||
const self = this;
|
||||
this.onClick(() => {
|
||||
self.isEnabled.setData(!self.isEnabled.data);
|
||||
})
|
||||
return this;
|
||||
}
|
||||
|
||||
public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement {
|
||||
if (constructor === undefined) {
|
||||
return undefined
|
||||
|
@ -37,4 +29,12 @@ export default class Toggle extends VariableUiElement {
|
|||
)
|
||||
|
||||
}
|
||||
|
||||
public ToggleOnClick(): Toggle {
|
||||
const self = this;
|
||||
this.onClick(() => {
|
||||
self.isEnabled.setData(!self.isEnabled.data);
|
||||
})
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -5,9 +5,9 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
|||
|
||||
export default class VariableInputElement<T> extends InputElement<T> {
|
||||
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
private readonly value: UIEventSource<T>;
|
||||
private readonly element: BaseUIElement
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
private readonly upstream: UIEventSource<InputElement<T>>;
|
||||
|
||||
constructor(upstream: UIEventSource<InputElement<T>>) {
|
||||
|
@ -23,13 +23,12 @@ export default class VariableInputElement<T> extends InputElement<T> {
|
|||
return this.value;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this.element.ConstructElement();
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this.upstream.data.IsValid(t);
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this.element.ConstructElement();
|
||||
}
|
||||
|
||||
}
|
|
@ -107,8 +107,6 @@ export default class MoveWizard extends Toggle {
|
|||
})
|
||||
|
||||
|
||||
|
||||
|
||||
const selectReason = new Combine(reasons.map(r => new SubtleButton(r.icon, r.text).onClick(() => {
|
||||
moveReason.setData(r)
|
||||
currentStep.setData("pick_location")
|
||||
|
|
|
@ -32,6 +32,7 @@ export interface MultiApplyParams {
|
|||
|
||||
class MultiApplyExecutor {
|
||||
|
||||
private static executorCache = new Map<string, MultiApplyExecutor>()
|
||||
private readonly originalValues = new Map<string, string>()
|
||||
private readonly params: MultiApplyParams;
|
||||
|
||||
|
@ -57,6 +58,15 @@ class MultiApplyExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
public static GetApplicator(id: string, params: MultiApplyParams): MultiApplyExecutor {
|
||||
if (MultiApplyExecutor.executorCache.has(id)) {
|
||||
return MultiApplyExecutor.executorCache.get(id)
|
||||
}
|
||||
const applicator = new MultiApplyExecutor(params)
|
||||
MultiApplyExecutor.executorCache.set(id, applicator)
|
||||
return applicator
|
||||
}
|
||||
|
||||
public applyTaggingOnOtherFeatures() {
|
||||
console.log("Multi-applying changes...")
|
||||
const featuresToChange = this.params.featureIds.data
|
||||
|
@ -103,17 +113,6 @@ class MultiApplyExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
private static executorCache = new Map<string, MultiApplyExecutor>()
|
||||
|
||||
public static GetApplicator(id: string, params: MultiApplyParams): MultiApplyExecutor {
|
||||
if (MultiApplyExecutor.executorCache.has(id)) {
|
||||
return MultiApplyExecutor.executorCache.get(id)
|
||||
}
|
||||
const applicator = new MultiApplyExecutor(params)
|
||||
MultiApplyExecutor.executorCache.set(id, applicator)
|
||||
return applicator
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class MultiApply extends Toggle {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||
|
@ -20,6 +19,7 @@ We don't actually import it here. It is imported in the 'MinimapImplementation'-
|
|||
*/
|
||||
export default class ShowDataLayer {
|
||||
|
||||
private static dataLayerIds = 0
|
||||
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||
private readonly _enablePopups: boolean;
|
||||
private readonly _features: RenderingMultiPlexerFeatureSource
|
||||
|
@ -30,7 +30,6 @@ export default class ShowDataLayer {
|
|||
private _cleanCount = 0;
|
||||
private geoLayer = undefined;
|
||||
private isDirty = false;
|
||||
|
||||
/**
|
||||
* If the selected element triggers, this is used to lookup the correct layer and to open the popup
|
||||
* Used to avoid a lot of callbacks on the selected element
|
||||
|
@ -39,9 +38,7 @@ export default class ShowDataLayer {
|
|||
* @private
|
||||
*/
|
||||
private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>()
|
||||
|
||||
private readonly showDataLayerid: number;
|
||||
private static dataLayerIds = 0
|
||||
|
||||
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
|
||||
this._leafletMap = options.leafletMap;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import * as L from "leaflet";
|
||||
|
||||
export default class ShowOverlayLayer {
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
|
|||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
import * as clusterstyle from "../../assets/layers/cluster_style/cluster_style.json"
|
||||
|
||||
export default class ShowTileInfo {
|
||||
public static readonly styling = new LayerConfig(
|
||||
clusterstyle, "tileinfo", true)
|
||||
|
|
|
@ -10,6 +10,12 @@ import FilteredLayer from "../../Models/FilteredLayer";
|
|||
* A feature source containing but a single feature, which keeps stats about a tile
|
||||
*/
|
||||
export class TileHierarchyAggregator implements FeatureSource {
|
||||
private static readonly empty = []
|
||||
public totalValue: number = 0
|
||||
public showCount: number = 0
|
||||
public hiddenCount: number = 0
|
||||
public readonly features = new UIEventSource<{ feature: any, freshness: Date }[]>(TileHierarchyAggregator.empty)
|
||||
public readonly name;
|
||||
private _parent: TileHierarchyAggregator;
|
||||
private _root: TileHierarchyAggregator;
|
||||
private _z: number;
|
||||
|
@ -17,16 +23,7 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
private _y: number;
|
||||
private _tileIndex: number
|
||||
private _counter: SingleTileCounter
|
||||
|
||||
private _subtiles: [TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator] = [undefined, undefined, undefined, undefined]
|
||||
public totalValue: number = 0
|
||||
public showCount: number = 0
|
||||
public hiddenCount: number = 0
|
||||
|
||||
private static readonly empty = []
|
||||
public readonly features = new UIEventSource<{ feature: any, freshness: Date }[]>(TileHierarchyAggregator.empty)
|
||||
public readonly name;
|
||||
|
||||
private readonly featuresStatic = []
|
||||
private readonly featureProperties: { count: string, kilocount: string, tileId: string, id: string, showCount: string, totalCount: string };
|
||||
private readonly _state: { filteredLayers: UIEventSource<FilteredLayer[]> };
|
||||
|
@ -87,6 +84,10 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
this.featuresStatic.push({feature: box, freshness: now})
|
||||
}
|
||||
|
||||
public static createHierarchy(state: { filteredLayers: UIEventSource<FilteredLayer[]> }) {
|
||||
return new TileHierarchyAggregator(undefined, state, 0, 0, 0)
|
||||
}
|
||||
|
||||
public getTile(tileIndex): TileHierarchyAggregator {
|
||||
if (tileIndex === this._tileIndex) {
|
||||
return this;
|
||||
|
@ -103,6 +104,61 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
return this._subtiles[subtileIndex]?.getTile(tileIndex)
|
||||
}
|
||||
|
||||
public addTile(source: FeatureSourceForLayer & Tiled) {
|
||||
const self = this;
|
||||
if (source.tileIndex === this._tileIndex) {
|
||||
if (this._counter === undefined) {
|
||||
this._counter = new SingleTileCounter(this._tileIndex)
|
||||
this._counter.countsPerLayer.addCallbackAndRun(_ => self.update())
|
||||
}
|
||||
this._counter.addTileCount(source)
|
||||
} else {
|
||||
|
||||
// We have to give it to one of the subtiles
|
||||
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex)
|
||||
while (tileZ - 1 > this._z) {
|
||||
tileX = Math.floor(tileX / 2)
|
||||
tileY = Math.floor(tileY / 2)
|
||||
tileZ--
|
||||
}
|
||||
const xDiff = tileX - (2 * this._x)
|
||||
const yDiff = tileY - (2 * this._y)
|
||||
|
||||
const subtileIndex = yDiff * 2 + xDiff;
|
||||
if (this._subtiles[subtileIndex] === undefined) {
|
||||
this._subtiles[subtileIndex] = new TileHierarchyAggregator(this, this._state, tileZ, tileX, tileY)
|
||||
}
|
||||
this._subtiles[subtileIndex].addTile(source)
|
||||
}
|
||||
this.updateSignal.setData(source)
|
||||
}
|
||||
|
||||
getCountsForZoom(clusteringConfig: { maxZoom: number }, locationControl: UIEventSource<{ zoom: number }>, cutoff: number = 0): FeatureSource {
|
||||
const self = this
|
||||
const empty = []
|
||||
const features = locationControl.map(loc => loc.zoom).map(targetZoom => {
|
||||
if (targetZoom - 1 > clusteringConfig.maxZoom) {
|
||||
return empty
|
||||
}
|
||||
|
||||
const features = []
|
||||
self.visitSubTiles(aggr => {
|
||||
if (aggr.showCount < cutoff) {
|
||||
return false
|
||||
}
|
||||
if (aggr._z === targetZoom) {
|
||||
features.push(...aggr.features.data)
|
||||
return false
|
||||
}
|
||||
return aggr._z <= targetZoom;
|
||||
})
|
||||
|
||||
return features
|
||||
}, [this.updateSignal.stabilized(500)])
|
||||
|
||||
return new StaticFeatureSource(features, true);
|
||||
}
|
||||
|
||||
private update() {
|
||||
const newMap = new Map<string, number>()
|
||||
let total = 0
|
||||
|
@ -162,71 +218,12 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
}
|
||||
}
|
||||
|
||||
public addTile(source: FeatureSourceForLayer & Tiled) {
|
||||
const self = this;
|
||||
if (source.tileIndex === this._tileIndex) {
|
||||
if (this._counter === undefined) {
|
||||
this._counter = new SingleTileCounter(this._tileIndex)
|
||||
this._counter.countsPerLayer.addCallbackAndRun(_ => self.update())
|
||||
}
|
||||
this._counter.addTileCount(source)
|
||||
} else {
|
||||
|
||||
// We have to give it to one of the subtiles
|
||||
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex)
|
||||
while (tileZ - 1 > this._z) {
|
||||
tileX = Math.floor(tileX / 2)
|
||||
tileY = Math.floor(tileY / 2)
|
||||
tileZ--
|
||||
}
|
||||
const xDiff = tileX - (2 * this._x)
|
||||
const yDiff = tileY - (2 * this._y)
|
||||
|
||||
const subtileIndex = yDiff * 2 + xDiff;
|
||||
if (this._subtiles[subtileIndex] === undefined) {
|
||||
this._subtiles[subtileIndex] = new TileHierarchyAggregator(this, this._state, tileZ, tileX, tileY)
|
||||
}
|
||||
this._subtiles[subtileIndex].addTile(source)
|
||||
}
|
||||
this.updateSignal.setData(source)
|
||||
}
|
||||
|
||||
public static createHierarchy(state: { filteredLayers: UIEventSource<FilteredLayer[]> }) {
|
||||
return new TileHierarchyAggregator(undefined, state, 0, 0, 0)
|
||||
}
|
||||
|
||||
private visitSubTiles(f: (aggr: TileHierarchyAggregator) => boolean) {
|
||||
const visitFurther = f(this)
|
||||
if (visitFurther) {
|
||||
this._subtiles.forEach(tile => tile?.visitSubTiles(f))
|
||||
}
|
||||
}
|
||||
|
||||
getCountsForZoom(clusteringConfig: { maxZoom: number }, locationControl: UIEventSource<{ zoom: number }>, cutoff: number = 0): FeatureSource {
|
||||
const self = this
|
||||
const empty = []
|
||||
const features = locationControl.map(loc => loc.zoom).map(targetZoom => {
|
||||
if (targetZoom - 1 > clusteringConfig.maxZoom) {
|
||||
return empty
|
||||
}
|
||||
|
||||
const features = []
|
||||
self.visitSubTiles(aggr => {
|
||||
if (aggr.showCount < cutoff) {
|
||||
return false
|
||||
}
|
||||
if (aggr._z === targetZoom) {
|
||||
features.push(...aggr.features.data)
|
||||
return false
|
||||
}
|
||||
return aggr._z <= targetZoom;
|
||||
})
|
||||
|
||||
return features
|
||||
}, [this.updateSignal.stabilized(500)])
|
||||
|
||||
return new StaticFeatureSource(features, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -236,11 +233,10 @@ class SingleTileCounter implements Tiled {
|
|||
public readonly bbox: BBox;
|
||||
public readonly tileIndex: number;
|
||||
public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>())
|
||||
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
|
||||
public readonly z: number
|
||||
public readonly x: number
|
||||
public readonly y: number
|
||||
|
||||
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
|
||||
|
||||
constructor(tileIndex: number) {
|
||||
this.tileIndex = tileIndex
|
||||
|
|
|
@ -33,13 +33,10 @@ import AllKnownLayers from "../Customizations/AllKnownLayers";
|
|||
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
|
||||
import Link from "./Base/Link";
|
||||
import List from "./Base/List";
|
||||
import {OsmConnection} from "../Logic/Osm/OsmConnection";
|
||||
import {SubtleButton} from "./Base/SubtleButton";
|
||||
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction";
|
||||
import {And} from "../Logic/Tags/And";
|
||||
import Toggle from "./Input/Toggle";
|
||||
import Img from "./Base/Img";
|
||||
import FilteredLayer from "../Models/FilteredLayer";
|
||||
import {DefaultGuiState} from "./DefaultGuiState";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
|
|
|
@ -4,7 +4,6 @@ import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
|
|||
import {Translation} from "../i18n/Translation";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Loading from "../Base/Loading";
|
||||
import {Transform} from "stream";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Combine from "../Base/Combine";
|
||||
import Img from "../Base/Img";
|
||||
|
@ -16,6 +15,43 @@ import {Utils} from "../../Utils";
|
|||
|
||||
export default class WikidataPreviewBox extends VariableUiElement {
|
||||
|
||||
private static isHuman = [
|
||||
{p: 31/*is a*/, q: 5 /* human */},
|
||||
]
|
||||
// @ts-ignore
|
||||
private static extraProperties: {
|
||||
requires?: { p: number, q?: number }[],
|
||||
property: string,
|
||||
display: Translation | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>
|
||||
}[] = [
|
||||
{
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
property: "P21",
|
||||
display: new Map([
|
||||
['Q6581097', () => Svg.gender_male_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q6581072', () => Svg.gender_female_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q1097630', () => Svg.gender_inter_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q1052281', () => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transwomen'*/],
|
||||
['Q2449503', () => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transmen'*/],
|
||||
['Q48270', () => Svg.gender_queer_ui().SetStyle("width: 1rem; height: auto")]
|
||||
])
|
||||
},
|
||||
{
|
||||
property: "P569",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: new Translation({
|
||||
"*": "Born: {value}"
|
||||
})
|
||||
},
|
||||
{
|
||||
property: "P570",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: new Translation({
|
||||
"*": "Died: {value}"
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
constructor(wikidataId: UIEventSource<string>) {
|
||||
let inited = false;
|
||||
const wikidata = wikidataId
|
||||
|
@ -45,6 +81,7 @@ export default class WikidataPreviewBox extends VariableUiElement {
|
|||
}))
|
||||
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
public static WikidataResponsePreview(wikidata: WikidataResponse): BaseUIElement {
|
||||
let link = new Link(
|
||||
|
@ -80,44 +117,6 @@ export default class WikidataPreviewBox extends VariableUiElement {
|
|||
return info
|
||||
}
|
||||
|
||||
private static isHuman = [
|
||||
{p: 31/*is a*/, q: 5 /* human */},
|
||||
]
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
private static extraProperties: {
|
||||
requires?: { p: number, q?: number }[],
|
||||
property: string,
|
||||
display: Translation | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>
|
||||
}[] = [
|
||||
{
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
property: "P21",
|
||||
display: new Map([
|
||||
['Q6581097', () => Svg.gender_male_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q6581072', () => Svg.gender_female_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q1097630',() => Svg.gender_inter_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q1052281',() => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transwomen'*/],
|
||||
['Q2449503',() => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transmen'*/],
|
||||
['Q48270',() => Svg.gender_queer_ui().SetStyle("width: 1rem; height: auto")]
|
||||
])
|
||||
},
|
||||
{
|
||||
property: "P569",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: new Translation({
|
||||
"*":"Born: {value}"
|
||||
})
|
||||
},
|
||||
{
|
||||
property: "P570",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: new Translation({
|
||||
"*":"Died: {value}"
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
public static QuickFacts(wikidata: WikidataResponse): BaseUIElement {
|
||||
|
||||
const els: BaseUIElement[] = []
|
||||
|
|
|
@ -34,6 +34,38 @@ export class Translation extends BaseUIElement {
|
|||
return this.textFor(Translation.forcedLanguage ?? Locale.language.data)
|
||||
}
|
||||
|
||||
static ExtractAllTranslationsFrom(object: any, context = ""): { context: string, tr: Translation }[] {
|
||||
const allTranslations: { context: string, tr: Translation }[] = []
|
||||
for (const key in object) {
|
||||
const v = object[key]
|
||||
if (v === undefined || v === null) {
|
||||
continue
|
||||
}
|
||||
if (v instanceof Translation) {
|
||||
allTranslations.push({context: context + "." + key, tr: v})
|
||||
continue
|
||||
}
|
||||
if (typeof v === "object") {
|
||||
allTranslations.push(...Translation.ExtractAllTranslationsFrom(v, context + "." + key))
|
||||
|
||||
}
|
||||
}
|
||||
return allTranslations
|
||||
}
|
||||
|
||||
static fromMap(transl: Map<string, string>) {
|
||||
const translations = {}
|
||||
let hasTranslation = false;
|
||||
transl?.forEach((value, key) => {
|
||||
translations[key] = value
|
||||
hasTranslation = true
|
||||
})
|
||||
if (!hasTranslation) {
|
||||
return undefined
|
||||
}
|
||||
return new Translation(translations);
|
||||
}
|
||||
|
||||
public textFor(language: string): string {
|
||||
if (this.translations["*"]) {
|
||||
return this.translations["*"];
|
||||
|
@ -195,36 +227,4 @@ export class Translation extends BaseUIElement {
|
|||
}
|
||||
return allIcons.filter(icon => icon != undefined)
|
||||
}
|
||||
|
||||
static ExtractAllTranslationsFrom(object: any, context = ""): { context: string, tr: Translation }[] {
|
||||
const allTranslations: { context: string, tr: Translation }[] = []
|
||||
for (const key in object) {
|
||||
const v = object[key]
|
||||
if (v === undefined || v === null) {
|
||||
continue
|
||||
}
|
||||
if (v instanceof Translation) {
|
||||
allTranslations.push({context: context +"." + key, tr: v})
|
||||
continue
|
||||
}
|
||||
if (typeof v === "object") {
|
||||
allTranslations.push(...Translation.ExtractAllTranslationsFrom(v, context + "." + key))
|
||||
continue
|
||||
}
|
||||
}
|
||||
return allTranslations
|
||||
}
|
||||
|
||||
static fromMap(transl: Map<string, string>) {
|
||||
const translations = {}
|
||||
let hasTranslation = false;
|
||||
transl?.forEach((value, key) => {
|
||||
translations[key] = value
|
||||
hasTranslation = true
|
||||
})
|
||||
if(!hasTranslation){
|
||||
return undefined
|
||||
}
|
||||
return new Translation(translations);
|
||||
}
|
||||
}
|
9
Utils.ts
|
@ -10,11 +10,6 @@ export class Utils {
|
|||
public static runningFromConsole = typeof window === "undefined";
|
||||
public static readonly assets_path = "./assets/svg/";
|
||||
public static externalDownloadFunction: (url: string, headers?: any) => Promise<any>;
|
||||
private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"]
|
||||
private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"]
|
||||
private static injectedDownloads = {}
|
||||
private static _download_cache = new Map<string, { promise: Promise<any>, timestamp: number }>()
|
||||
|
||||
public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`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.
|
||||
|
||||
|
@ -26,6 +21,10 @@ Remark that the syntax is slightly different then expected; it uses '$' to note
|
|||
|
||||
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
|
||||
`
|
||||
private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"]
|
||||
private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"]
|
||||
private static injectedDownloads = {}
|
||||
private static _download_cache = new Map<string, { promise: Promise<any>, timestamp: number }>()
|
||||
|
||||
static EncodeXmlValue(str) {
|
||||
if (typeof str !== "string") {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 17.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
width="35px" height="32px" viewBox="0 0 35 32" enable-background="new 0 0 35 32" xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#828282" d="M27.464,2.314c-0.053-0.132-0.159-0.235-0.292-0.284c-0.133-0.048-0.281-0.039-0.406,0.027L14.86,8.339
|
||||
|
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
@ -9,12 +9,15 @@ If you want to add a missing socket type, then:
|
|||
|
||||
- Add all the properties in 'types.csv'
|
||||
- Add an icon. (Note: icons are way better as pictures as they are perceived more abstractly)
|
||||
- Update license_info.json with the copyright info of the new icon. Note that we strive to have Creative-commons icons only (though there are exceptions)
|
||||
- Update license_info.json with the copyright info of the new icon. Note that we strive to have Creative-commons icons
|
||||
only (though there are exceptions)
|
||||
|
||||
AT this point, most of the work should be done; feel free to send a PR. If you would like to test it locally first (which is recommended) and have a working dev environment, then run:
|
||||
AT this point, most of the work should be done; feel free to send a PR. If you would like to test it locally first (
|
||||
which is recommended) and have a working dev environment, then run:
|
||||
|
||||
- Run 'ts-node csvToJson.ts' which will generate a new charging_station.json based on the protojson
|
||||
- Run`npm run query:licenses` to get an interactive program to add the license of your artwork, followed by `npm run generate:licenses`
|
||||
- Run`npm run query:licenses` to get an interactive program to add the license of your artwork, followed
|
||||
by `npm run generate:licenses`
|
||||
- Run `npm run generate:layeroverview` to generate the layer files
|
||||
- Run `npm run start` to run the instance
|
||||
|
||||
|
@ -30,6 +33,6 @@ The columns in the CSV file are:
|
|||
- countryWhiteList: Only show this plug type in these countries
|
||||
- countryBlackList: Don't show this plug type in these countries. NOt compatibel with the whiteList
|
||||
- commonVoltages, commonCurrents, commonOutputs: common values for these tags
|
||||
- associatedVehicleTypes and neverAssociatedWith: these work in tandem to hide options.
|
||||
If every associated vehicle type is `no`, then the option is hidden
|
||||
If at least one `neverAssociatedVehicleType` is `yes` and none of the associated types is yes, then the option is hidden too
|
||||
- associatedVehicleTypes and neverAssociatedWith: these work in tandem to hide options. If every associated vehicle type
|
||||
is `no`, then the option is hidden If at least one `neverAssociatedVehicleType` is `yes` and none of the associated
|
||||
types is yes, then the option is hidden too
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="46.74284mm"
|
||||
height="36.190933mm"
|
||||
viewBox="0 0 46.74284 36.190933"
|
||||
|
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 5.1 KiB |
|
@ -5,10 +5,9 @@
|
|||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="46.74284mm"
|
||||
height="36.190933mm"
|
||||
viewBox="0 0 46.74284 36.190933"
|
||||
|
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 6 KiB |
|
@ -413,7 +413,12 @@
|
|||
}
|
||||
],
|
||||
"condition": {
|
||||
"or": ["maxstay~*","motorcar=yes","hgv=yes","bus=yes"]
|
||||
"or": [
|
||||
"maxstay~*",
|
||||
"motorcar=yes",
|
||||
"hgv=yes",
|
||||
"bus=yes"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -497,7 +502,8 @@
|
|||
"nl": "Wat is het telefoonnummer van de beheerder van dit oplaadpunt?"
|
||||
},
|
||||
"render": {
|
||||
"en": "In case of problems, call <a href='tel:{phone}'>{phone}</a>", "nl": "Bij problemen, bel naar <a href='tel:{phone}'>{phone}</a>"
|
||||
"en": "In case of problems, call <a href='tel:{phone}'>{phone}</a>",
|
||||
"nl": "Bij problemen, bel naar <a href='tel:{phone}'>{phone}</a>"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "phone",
|
||||
|
|
|
@ -115,7 +115,6 @@ function run(file, protojson) {
|
|||
}
|
||||
|
||||
|
||||
|
||||
overview_question_answers.push(json)
|
||||
|
||||
// We add a second time for any amount to trigger a visualisation; but this is not an answer option
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
|
@ -20,8 +19,10 @@
|
|||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata
|
||||
id="metadata38"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs36" /><sodipodi:namedview
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata>
|
||||
<defs
|
||||
id="defs36"/>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
|
@ -40,34 +41,41 @@
|
|||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg32" /><g
|
||||
inkscape:current-layer="svg32"/>
|
||||
<g
|
||||
id="g4"
|
||||
transform="translate(-84.147,-70.403)"><path
|
||||
d="m 235.402,78.403 c 2.47,0 4.745,2.021 4.745,4.49 v 42.02 c 0,2.47 -2.354,4.49 -4.823,4.49 H 96.971 c -2.47,0 -4.824,-2.021 -4.824,-4.49 v -42.02 c 0,-2.47 2.354,-4.49 4.824,-4.49 h 138.176 m 0.177,-8 H 96.971 c -7.015,0 -12.824,5.475 -12.824,12.49 v 42.02 c 0,7.015 5.809,12.49 12.824,12.49 h 138.353 c 7.016,0 12.823,-5.475 12.823,-12.49 v -42.02 c 0,-7.015 -5.807,-12.49 -12.823,-12.49 z"
|
||||
id="path2"
|
||||
inkscape:connector-curvature="0" /></g><g
|
||||
inkscape:connector-curvature="0" /></g>
|
||||
<g
|
||||
id="g26"
|
||||
transform="translate(-84.147,-70.403)"><g
|
||||
id="g8"><path
|
||||
d="m 129.147,112.403 c 0,0.55 -0.45,1 -1,1 h -12 c -0.55,0 -1,-0.45 -1,-1 v -10 c 0,-0.55 0.45,-1 1,-1 h 12 c 0.55,0 1,0.45 1,1 z"
|
||||
id="path6"
|
||||
inkscape:connector-curvature="0" /></g><g
|
||||
inkscape:connector-curvature="0" /></g>
|
||||
<g
|
||||
id="g12"><path
|
||||
d="m 159.147,112.403 c 0,0.55 -0.45,1 -1,1 h -12 c -0.55,0 -1,-0.45 -1,-1 v -10 c 0,-0.55 0.45,-1 1,-1 h 12 c 0.55,0 1,0.45 1,1 z"
|
||||
id="path10"
|
||||
inkscape:connector-curvature="0" /></g><g
|
||||
inkscape:connector-curvature="0" /></g>
|
||||
<g
|
||||
id="g16"><path
|
||||
d="m 185.147,112.403 c 0,0.55 -0.45,1 -1,1 h -12 c -0.55,0 -1,-0.45 -1,-1 v -10 c 0,-0.55 0.45,-1 1,-1 h 12 c 0.55,0 1,0.45 1,1 z"
|
||||
id="path14"
|
||||
inkscape:connector-curvature="0" /></g><g
|
||||
inkscape:connector-curvature="0" /></g>
|
||||
<g
|
||||
id="g20"><path
|
||||
d="m 215.147,112.403 c 0,0.55 -0.45,1 -1,1 h -12 c -0.55,0 -1,-0.45 -1,-1 v -10 c 0,-0.55 0.45,-1 1,-1 h 12 c 0.55,0 1,0.45 1,1 z"
|
||||
id="path18"
|
||||
inkscape:connector-curvature="0" /></g><g
|
||||
inkscape:connector-curvature="0" /></g>
|
||||
<g
|
||||
id="g24"><path
|
||||
d="m 233.147,104.403 c 0,1.65 -1.35,3 -3,3 h -129 c -1.65,0 -3,-1.35 -3,-3 v -16 c 0,-1.65 1.35,-3 3,-3 h 129 c 1.65,0 3,1.35 3,3 z"
|
||||
id="path22"
|
||||
inkscape:connector-curvature="0" /></g></g><path
|
||||
inkscape:connector-curvature="0" /></g></g>
|
||||
<path
|
||||
id="path1334"
|
||||
d="m 145.86777,120.95936 -15.39372,-8.88977 v 6.34152 H 51.79247 l 15.3202,-16.18591 c 1.3038,-1.04508 3.0097,-1.77819 4.76452,-1.81881 7.09895,0 11.31463,-0.002 12.8664,-0.005 1.05185,2.99679 3.87728,5.15986 7.23616,5.15986 4.24921,0 7.69763,-3.44811 7.69763,-7.69887 0,-4.2526 -3.4481,-7.69947 -7.69763,-7.69947 -3.35888,0 -6.18431,2.16183 -7.23616,5.15616 l -12.71565,-0.003 c -3.44626,0 -7.05742,1.89079 -9.35585,4.10739 0.0627,-0.0659 0.12798,-0.13598 -0.003,8.5e-4 -0.0486,0.0547 -16.2542,17.171 -16.2542,17.171 -1.30104,1.04353 -3.00602,1.77204 -4.7596,1.81388 h -8.90084 c -1.17983,-5.88006 -6.37229,-10.31016 -12.6009,-10.31016 -7.10142,0 -12.85779,5.75637 -12.85779,12.85563 0,7.10141 5.75637,12.8581 12.85779,12.8581 6.22984,0 11.4223,-4.43381 12.60213,-10.31879 h 8.74486 c 0.0224,0 0.045,8.5e-4 0.0677,0 H 60.9103 c 1.7502,0.0446 3.45302,0.77404 4.75283,1.81881 0,0 16.2019,17.115 16.25205,17.17038 0.12983,0.13752 0.0652,0.0659 8.5e-4,8.5e-4 2.29843,2.21629 5.91113,4.10585 9.358,4.10585 l 12.25418,-0.003 v 5.16139 h 15.39773 v -15.39539 h -15.39773 v 5.15463 c 0,0 -3.22752,-0.006 -12.40431,-0.006 -1.75512,-0.0403 -3.46287,-0.7725 -4.76606,-1.81757 l -15.32358,-16.189 h 59.44012 v 6.35168 z"
|
||||
inkscape:connector-curvature="0"
|
||||
|
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.8 KiB |
|
@ -5,10 +5,9 @@
|
|||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="31.41128mm"
|
||||
height="21.6535mm"
|
||||
viewBox="0 0 31.41128 21.6535"
|
||||
|
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.7 KiB |
|
@ -3,7 +3,6 @@
|
|||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
width="14"
|
||||
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.6 KiB |