Full code cleanup

This commit is contained in:
pietervdvn 2021-11-07 16:34:51 +01:00
parent 8e6ee8c87f
commit bd21212eba
246 changed files with 19418 additions and 11729 deletions

View file

@ -1,4 +1,3 @@
Metatags Metatags
========== ==========
@ -6,9 +5,11 @@
Metatags are extra tags available, in order to display more data or to give better questions. 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 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 ### _lat, _lon
The latitude and longitude of the point (or centerpoint in the case of a way/area) The latitude and longitude of the point (or centerpoint in the case of a way/area)
### _layer ### _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 ### _surface, _surface:ha
The surface area of the feature, in square meters and in hectare. Not set on points and ways 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 This is a lazy metatag and is only calculated when needed
### _length, _length:km ### _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 ### 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 ### _country
The country code of the property (with latlon2country) The country code of the property (with latlon2country)
### _isOpen, _isOpen:description ### _isOpen, _isOpen:description
If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no') 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 This is a lazy metatag and is only calculated when needed
### _direction:numerical, _direction:leftright ### _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
_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 left-looking on the map or 'right-looking' on the map
### _now:date, _now:datetime, _loaded:date, _loaded:_datetime ### _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 ### _last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number, _backend
Information about the last edit of this object. Information about the last edit of this object.
### sidewalk:left, sidewalk:right, generic_key:left:property, generic_key:right:property ### 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
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 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. It is also possible to calculate your own tags - but this requires some javascript knowledge.
Before proceeding, some warnings: Before proceeding, some warnings:
- DO NOT DO THIS AS BEGINNER - 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 - **Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to
- **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. 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.: 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 - `area` contains the surface area (in square meters) of the object
- `lat` and `lon` contain the latitude and longitude - `lat` and `lon` contain the latitude and longitude
Some advanced functions are available on **feat** as well: Some advanced functions are available on **feat** as well:
- [distanceTo](#distanceTo) - [distanceTo](#distanceTo)
@ -175,33 +130,44 @@ Some advanced functions are available on **feat** as well:
### distanceTo ### 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 0. feature OR featureID OR longitude
1. undefined OR latitude 1. undefined OR latitude
### overlapWith ### 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 returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in
The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list 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) 0. ...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)
### closest ### 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 0. list of features or a layer name or '*' to get all features
### closestn ### 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 0. list of features or layer name or '*' to get all features
1. amount of 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(';')` For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`
### get ### get
Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ... Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ...
0. key 0. key Generated from SimpleMetaTagger, ExtraFunction
Generated from SimpleMetaTagger, ExtraFunction

View file

@ -1,32 +1,42 @@
Rights of contributors Rights of contributors
====================== ======================
If a contributor is quite active within MapComplete, this contributor might be granted access to the main repository. 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. If you have access to the repository, you can make a fork of an already existing branch and push this new branch to
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). github. This means that this branch will be _automatically built_ and be **deployed**
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. 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 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 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: To do this:
1. type `git remote add upstream git@github.com:pietervdvn/MapComplete` 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.

View file

@ -28,7 +28,8 @@ To develop and build MapComplete, you
0. Make a fork and clone the repository. 0. Make a fork and clone the repository.
0. Install the nodejs version specified in [.tool-versions](./.tool-versions) 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/). - 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 0. Install `npm`. Linux: `sudo apt install npm` (or your favourite package manager), Windows: install
nodeJS: https://nodejs.org/en/download/ nodeJS: https://nodejs.org/en/download/
@ -106,7 +107,8 @@ Try removing `node_modules`, `package-lock.json` and `.cache`
Misc setup 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 Overview of package.json-scripts
-------------------------------- --------------------------------

View file

@ -5,7 +5,7 @@
--> -->
<!-- Title: G Pages: 1 --> <!-- Title: G Pages: 1 -->
<svg width="664pt" height="566pt" <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)"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 562)">
<title>G</title> <title>G</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-562 660.25,-562 660.25,4 -4,4"/> <polygon fill="white" stroke="transparent" points="-4,4 -4,-562 660.25,-562 660.25,4 -4,4"/>
@ -24,22 +24,30 @@
<!-- init&#45;&gt;denied --> <!-- init&#45;&gt;denied -->
<g id="edge1" class="edge"> <g id="edge1" class="edge">
<title>init&#45;&gt;denied</title> <title>init&#45;&gt;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"/> <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> </g>
<!-- getting_location --> <!-- getting_location -->
<g id="node3" class="node"> <g id="node3" class="node">
<title>getting_location</title> <title>getting_location</title>
<ellipse fill="none" stroke="black" cx="366.25" cy="-279" rx="85.29" ry="18"/> <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> </g>
<!-- init&#45;&gt;getting_location --> <!-- init&#45;&gt;getting_location -->
<g id="edge2" class="edge"> <g id="edge2" class="edge">
<title>init&#45;&gt;getting_location</title> <title>init&#45;&gt;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"/> <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> </g>
<!-- idle --> <!-- idle -->
<g id="node4" class="node"> <g id="node4" class="node">
@ -50,94 +58,122 @@
<!-- init&#45;&gt;idle --> <!-- init&#45;&gt;idle -->
<g id="edge3" class="edge"> <g id="edge3" class="edge">
<title>init&#45;&gt;idle</title> <title>init&#45;&gt;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"/> <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> </g>
<!-- location_found --> <!-- location_found -->
<g id="node6" class="node"> <g id="node6" class="node">
<title>location_found</title> <title>location_found</title>
<ellipse fill="none" stroke="black" cx="366.25" cy="-192" rx="77.99" ry="18"/> <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> </g>
<!-- getting_location&#45;&gt;location_found --> <!-- getting_location&#45;&gt;location_found -->
<g id="edge8" class="edge"> <g id="edge8" class="edge">
<title>getting_location&#45;&gt;location_found</title> <title>getting_location&#45;&gt;location_found</title>
<path fill="none" stroke="black" d="M366.25,-260.8C366.25,-249.16 366.25,-233.55 366.25,-220.24"/> <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"/> <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> </g>
<!-- request_permission --> <!-- request_permission -->
<g id="node5" class="node"> <g id="node5" class="node">
<title>request_permission</title> <title>request_permission</title>
<ellipse fill="none" stroke="black" cx="264.25" cy="-366" rx="102.08" ry="18"/> <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> </g>
<!-- idle&#45;&gt;request_permission --> <!-- idle&#45;&gt;request_permission -->
<g id="edge4" class="edge"> <g id="edge4" class="edge">
<title>idle&#45;&gt;request_permission</title> <title>idle&#45;&gt;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"/> <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> <text text-anchor="middle" x="371.75" y="-405.8" font-family="Times,serif" font-size="14.00">on click</text>
</g> </g>
<!-- request_permission&#45;&gt;denied --> <!-- request_permission&#45;&gt;denied -->
<g id="edge7" class="edge"> <g id="edge7" class="edge">
<title>request_permission&#45;&gt;denied</title> <title>request_permission&#45;&gt;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"/> <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> </g>
<!-- request_permission&#45;&gt;getting_location --> <!-- request_permission&#45;&gt;getting_location -->
<g id="edge5" class="edge"> <g id="edge5" class="edge">
<title>request_permission&#45;&gt;getting_location</title> <title>request_permission&#45;&gt;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"/> <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> </g>
<!-- request_permission&#45;&gt;idle --> <!-- request_permission&#45;&gt;idle -->
<g id="edge6" class="edge"> <g id="edge6" class="edge">
<title>request_permission&#45;&gt;idle</title> <title>request_permission&#45;&gt;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"/> <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> </g>
<!-- open_lock --> <!-- open_lock -->
<g id="node7" class="node"> <g id="node7" class="node">
<title>open_lock</title> <title>open_lock</title>
<ellipse fill="none" stroke="black" cx="333.25" cy="-105" rx="55.79" ry="18"/> <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> </g>
<!-- location_found&#45;&gt;open_lock --> <!-- location_found&#45;&gt;open_lock -->
<g id="edge9" class="edge"> <g id="edge9" class="edge">
<title>location_found&#45;&gt;open_lock</title> <title>location_found&#45;&gt;open_lock</title>
<path fill="none" stroke="black" d="M359.57,-173.8C354.98,-161.97 348.79,-146.03 343.56,-132.58"/> <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"/> <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> </g>
<!-- open_lock&#45;&gt;location_found --> <!-- open_lock&#45;&gt;location_found -->
<g id="edge10" class="edge"> <g id="edge10" class="edge">
<title>open_lock&#45;&gt;location_found</title> <title>open_lock&#45;&gt;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"/> <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> </g>
<!-- closed_lock --> <!-- closed_lock -->
<g id="node8" class="node"> <g id="node8" class="node">
<title>closed_lock</title> <title>closed_lock</title>
<ellipse fill="none" stroke="black" cx="454.25" cy="-18" rx="63.09" ry="18"/> <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> </g>
<!-- open_lock&#45;&gt;closed_lock --> <!-- open_lock&#45;&gt;closed_lock -->
<g id="edge11" class="edge"> <g id="edge11" class="edge">
<title>open_lock&#45;&gt;closed_lock</title> <title>open_lock&#45;&gt;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"/> <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> </g>
<!-- closed_lock&#45;&gt;location_found --> <!-- closed_lock&#45;&gt;location_found -->
<g id="edge12" class="edge"> <g id="edge12" class="edge">
<title>closed_lock&#45;&gt;location_found</title> <title>closed_lock&#45;&gt;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"/> <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> <text text-anchor="middle" x="604.75" y="-101.3" font-family="Times,serif" font-size="14.00">on click</text>
</g> </g>

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -6,13 +6,15 @@ Some highlights of new releases.
0.10 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: Some highlights are:
1. The addition of fallback overpass servers 1. The addition of fallback overpass servers
2. Fetching data from OSM directly (especially useful in the personal theme) 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. 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: Furthermore, it contains a few new themes and theme updates:
@ -31,9 +33,8 @@ Other various small improvements:
0.8 and 0.9 0.8 and 0.9
----------- -----------
Addition of filters per layer Addition of filters per layer Addition of a download-as-pdf for select themes Addition of a download-as-geojson and
Addition of a download-as-pdf for select themes download-as-csv for select themes
Addition of a download-as-geojson and download-as-csv for select themes
... ...

View file

@ -1,10 +1,10 @@
Available types for text fields 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 ## string
@ -24,15 +24,15 @@ A geographical direction, in degrees. 0° is north, 90° is east, ... Will retur
## length ## 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 ## wikidata
A wikidata identifier, e.g. Q42. A wikidata identifier, e.g. Q42.
### Helper arguments ### Helper arguments
name | doc name | doc
------ | ----- ------ | -----
key | the value of this tag will initialize search (default: name) 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 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 removePostfixes | remove these snippets of text from the end of the passed string to search
### Example usage ### 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": { "freeform": {
@ -102,10 +102,9 @@ A phone number
## opening_hours ## opening_hours
Has extra elements to easily input when a POI is opened. Has extra elements to easily input when a POI is opened.
### Helper arguments ### Helper arguments
name | doc name | doc
------ | ----- ------ | -----
options | A JSON-object of type `{ prefix: string, postfix: string }`. 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 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 postfix | Piece of text that will always be added to the end of the generated opening hours
### Example usage ### Example usage
To add a conditional (based on time) access restriction: 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 ## color

View file

@ -1,13 +1,11 @@
### Special tag renderings ### 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.
General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do
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. 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) - [all_tags](#all_tags)
- [image_carousel](#image_carousel) - [image_carousel](#image_carousel)
@ -25,19 +23,18 @@ General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_nam
- [multi_apply](#multi_apply) - [multi_apply](#multi_apply)
- [tag_apply](#tag_apply) - [tag_apply](#tag_apply)
### all_tags ### all_tags
Prints all key-value pairs of the object - used for debugging Prints all key-value pairs of the object - used for debugging
#### Example usage #### Example usage
`{all_tags()}` `{all_tags()}`
### image_carousel ### 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 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_carousel(image,mapillary,image,wikidata,wikimedia_commons,image,image)}`
### image_upload ### image_upload
Creates a button where a user can upload an image to IMGUR 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)}` `{image_upload(image,Add image)}`
### wikipedia ### wikipedia
A box showing the corresponding wikipedia article - based on the wikidata tag 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 #### 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 ### minimap
@ -86,12 +82,14 @@ idKey | id | (Matches all resting arguments) This argument should be the key of
#### Example usage #### 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 ### 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 name | default | description
------ | --------- | ------------- ------ | --------- | -------------
@ -101,39 +99,46 @@ side | _undefined_ | The side to show, either `left` or `right`
`{sided_minimap(left)}` `{sided_minimap(left)}`
### reviews ### 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 name | default | description
------ | --------- | ------------- ------ | --------- | -------------
subjectKey | name | The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b> 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 #### 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 ### 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 name | default | description
------ | --------- | ------------- ------ | --------- | -------------
key | opening_hours | The tagkey from which the table is constructed. 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__ prefix | _empty string_ | Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to
postfix | _empty string_ | Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__ 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 #### 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 ### 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 name | default | description
------ | --------- | ------------- ------ | --------- | -------------
@ -143,8 +148,8 @@ path | _undefined_ | The path (or shorthand) that should be returned
#### Example usage #### 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 ### histogram
@ -155,13 +160,13 @@ name | default | description
key | _undefined_ | The key to be read and to generate a histogram from 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 title | _empty string_ | The text to put above the given values column
countHeader | _empty string_ | The text to put above the counts 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 #### Example usage
`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram `{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram
### share_link ### share_link
Creates a link that (attempts to) open the native 'share'-screen 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 {share_link()} to share the current page, {share_link(<some_url>)} to share the given url
### canonical ### canonical
Converts a short, canonical value into the long, translated text 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) {canonical(length)} will give 42 metre (in french)
### import_button ### 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 #### 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 1. The dataset to import has a suitable license
2. The community has been informed of the import 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: 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. 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
This means that there should be a layer which will match the new tags and which will display it. 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'. 2. The original feature from your geojson layer will gain the tag '_imported=yes'. This should be used to change the
This should be used to change the appearance or even to hide it (eg by changing the icon size to zero) 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. 3. There should be a way for the theme to detect previously imported points, even after reloading. A reference number to
A reference number to the original dataset is an excellent way to do this the original dataset is an excellent way to do this
4. When importing ways, the theme creator is also responsible of avoiding overlapping ways. 4. When importing ways, the theme creator is also responsible of avoiding overlapping ways.
#### Disabled in unofficial themes #### 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 can be tested in an unofficial theme by adding `test=true` or `backend=osm-test`
The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console. as [URL-paramter](URL_Parameters.md). The import button will show up then. If in testmode, you can read the
In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org 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 #### Specifying which tags to copy or add
The argument `tags` of the import button takes a `;`-seperated list of tags to 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`. 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. . 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. 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` 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... 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
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript) 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 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 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 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 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 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 Snap onto layer(s)/replace geometry with this other way | _
- 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 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
snap max distance | 5 | The maximum distance that this point will move to snap onto a layer (in meters)
- 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 #### Example usage
`{import_button(,,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,18,,5)}` `{import_button(,,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,18,,5)}`
### multi_apply ### 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 name | default | description
------ | --------- | ------------- ------ | --------- | -------------
feature_ids | _undefined_ | A JSOn-serialized list of IDs of features to apply the tagging on 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 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 autoapply | _
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 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 #### 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 ### tag_apply
Shows a big button; clicking this button will apply certain tags onto the feature. 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. The first argument takes a specification of which tags to add. These can either be a tag to add, such
These can either be a tag to add, such as `amenity=fast_food` or can use a substitution, e.g. `addr:housenumber=$number`. as `amenity=fast_food` or can use a substitution, e.g. `addr:housenumber=$number`. This new point will then have the
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. 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. 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` 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... 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
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript) 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 name | default | description
------ | --------- | ------------- ------ | --------- | -------------
tags_to_apply | _undefined_ | A specification of the tags to apply tags_to_apply | _undefined_ | A specification of the tags to apply
message | _undefined_ | The text to show to the contributor message | _undefined_ | The text to show to the contributor
image | _undefined_ | An image to show to the contributor on the button 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 #### Example usage

View file

@ -1,7 +1,7 @@
from datetime import datetime
from matplotlib import pyplot
import json import json
import sys import sys
from datetime import datetime
from matplotlib import pyplot
def pyplot_init(): 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) keys = map(lambda key: datetime.strptime(key, "%Y-%m-%dT%H:%M:%S.000Z"), keys)
return list(keys) return list(keys)
def createPie(options): def createPie(options):
data = options["plot"]["count"] data = options["plot"]["count"]
keys = genKeys(data, options["interpetKeysAs"]) keys = genKeys(data, options["interpetKeysAs"])
@ -28,6 +29,7 @@ def createPie(options):
pyplot_init() pyplot_init()
pyplot.pie(values, labels=keys, startangle=(90 - 360 * first_pct / 2)) pyplot.pie(values, labels=keys, startangle=(90 - 360 * first_pct / 2))
def createBar(options): def createBar(options):
data = options["plot"]["count"] data = options["plot"]["count"]
keys = genKeys(data, options["interpetKeysAs"]) keys = genKeys(data, options["interpetKeysAs"])
@ -37,7 +39,6 @@ def createBar(options):
pyplot.legend() pyplot.legend()
pyplot_init() pyplot_init()
title = sys.argv[1] title = sys.argv[1]
pyplot.title = title pyplot.title = title

View file

@ -1,4 +1,3 @@
URL-parameters and URL-hash 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. 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`, For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`, the
the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely: 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 `lat` is `51.0` in this instance
- The url-parameter `lon` is `4.3` 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 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 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 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 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 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 fs-more-quests
@ -101,13 +103,15 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
backend 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 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 debug
@ -125,7 +129,8 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
overpassUrl 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 overpassTimeout

View file

@ -5,7 +5,9 @@ import Loc from "../../Models/Loc";
export interface AvailableBaseLayersObj { export interface AvailableBaseLayersObj {
readonly osmCarto: BaseLayer; readonly osmCarto: BaseLayer;
layerOverview: BaseLayer[]; layerOverview: BaseLayer[];
AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]>
SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer>; SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer>;
} }

View file

@ -3,9 +3,9 @@ import {UIEventSource} from "../UIEventSource";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import {GeoOperations} from "../GeoOperations"; import {GeoOperations} from "../GeoOperations";
import * as editorlayerindex from "../../assets/editor-layer-index.json"; import * as editorlayerindex from "../../assets/editor-layer-index.json";
import * as L from "leaflet";
import {TileLayer} from "leaflet"; import {TileLayer} from "leaflet";
import * as X from "leaflet-providers"; import * as X from "leaflet-providers";
import * as L from "leaflet";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {AvailableBaseLayersObj} from "./AvailableBaseLayers"; import {AvailableBaseLayersObj} from "./AvailableBaseLayers";
@ -28,102 +28,6 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
public layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex()); 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[] { private static LoadRasterIndex(): BaseLayer[] {
const layers: BaseLayer[] = [] const layers: BaseLayer[] = []
// @ts-ignore // @ts-ignore
@ -289,4 +193,100 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
subdomains: domains 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);
}
} }

View file

@ -115,7 +115,6 @@ export default class OverpassFeatureSource implements FeatureSource {
let lastUsed = 0; let lastUsed = 0;
const layersToDownload = [] const layersToDownload = []
for (const layer of this.state.layoutToUse.layers) { for (const layer of this.state.layoutToUse.layers) {

View file

@ -81,5 +81,4 @@ export default class StrayClickHandler {
} }
} }

View file

@ -4,11 +4,11 @@ import {GeoOperations} from "./GeoOperations";
export class BBox { export class BBox {
static global: BBox = new BBox([[-180, -90], [180, 90]]);
readonly maxLat: number; readonly maxLat: number;
readonly maxLon: number; readonly maxLon: number;
readonly minLat: number; readonly minLat: number;
readonly minLon: number; readonly minLon: number;
static global: BBox = new BBox([[-180, -90], [180, 90]]);
constructor(coordinates) { constructor(coordinates) {
this.maxLat = -90; this.maxLat = -90;
@ -45,6 +45,17 @@ export class BBox {
return feature.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) * Constructs a tilerange which fully contains this bbox (thus might be a bit larger)
* @param zoomlevel * @param zoomlevel
@ -83,24 +94,6 @@ export class BBox {
return true; 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() { getEast() {
return this.maxLon 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";
}
}
} }

View file

@ -8,6 +8,7 @@ export default class ContributorCount {
public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>()); 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 readonly state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> };
private lastUpdate: Date = undefined;
constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> }) { constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> }) {
this.state = state; this.state = state;
@ -21,8 +22,6 @@ export default class ContributorCount {
} }
private lastUpdate: Date = undefined;
private update(bbox: BBox) { private update(bbox: BBox) {
if (bbox === undefined) { if (bbox === undefined) {
return; return;

View file

@ -65,39 +65,6 @@ export default class DetermineLayout {
return [layoutToUse, undefined] 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( public static LoadLayoutFromHash(
userLayoutParam: UIEventSource<string> userLayoutParam: UIEventSource<string>
): [LayoutConfig, string] | null { ): [LayoutConfig, string] | null {
@ -166,4 +133,37 @@ export default class DetermineLayout {
.AttachTo("centermessage"); .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;
}
}
} }

View file

@ -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 { private freshnessForVisibleLayers(z: number, x: number, y: number): Date {
let oldestDate = undefined; let oldestDate = undefined;
for (const flayer of this.state.filteredLayers.data) { 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)
})
}
} }

View file

@ -1,5 +1,4 @@
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
import FilteredLayer from "../../Models/FilteredLayer"; import FilteredLayer from "../../Models/FilteredLayer";
import {BBox} from "../BBox"; import {BBox} from "../BBox";

View file

@ -35,6 +35,7 @@ export default class PerLayerFeatureSourceSplitter {
const featuresPerLayer = new Map<string, { feature, freshness } []>(); const featuresPerLayer = new Map<string, { feature, freshness } []>();
const noLayerFound = [] const noLayerFound = []
function addTo(layer: FilteredLayer, feature: { feature, freshness }) { function addTo(layer: FilteredLayer, feature: { feature, freshness }) {
const id = layer.layerDef.id const id = layer.layerDef.id
const list = featuresPerLayer.get(id) const list = featuresPerLayer.get(id)

View file

@ -11,9 +11,9 @@ import {ChangeDescription, ChangeDescriptionTools} from "../../Osm/Actions/Chang
export default class ChangeGeometryApplicator implements FeatureSourceForLayer { export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name: string; public readonly name: string;
public readonly layer: FilteredLayer
private readonly source: IndexedFeatureSource; private readonly source: IndexedFeatureSource;
private readonly changes: Changes; private readonly changes: Changes;
public readonly layer: FilteredLayer
constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) { constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) {
this.source = source; this.source = source;

View file

@ -5,7 +5,6 @@
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import {Utils} from "../../../Utils";
import {Tiles} from "../../../Models/TileRange"; import {Tiles} from "../../../Models/TileRange";
import {BBox} from "../../BBox"; 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 features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name; public readonly name;
public readonly layer: FilteredLayer public readonly layer: FilteredLayer
private readonly _sources: UIEventSource<FeatureSource[]>;
public readonly tileIndex: number; public readonly tileIndex: number;
public readonly bbox: BBox; public readonly bbox: BBox;
public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(new Set()) 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[]>) { constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
this.tileIndex = tileIndex; this.tileIndex = tileIndex;

View file

@ -18,6 +18,8 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
locationControl: UIEventSource<{ zoom: number }>; selectedElement: UIEventSource<any>, locationControl: UIEventSource<{ zoom: number }>; selectedElement: UIEventSource<any>,
allElements: ElementStorage allElements: ElementStorage
}; };
private readonly _alreadyRegistered = new Set<UIEventSource<any>>();
private readonly _is_dirty = new UIEventSource(false)
constructor( constructor(
state: { state: {
@ -55,24 +57,6 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
this.update(); 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() { public update() {
const self = this; const self = this;
const layer = this.upstream.layer; const layer = this.upstream.layer;
@ -116,4 +100,19 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
this._is_dirty.setData(false) 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)
})
}
}
} }

View file

@ -15,12 +15,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name; public readonly name;
public readonly isOsmCache: boolean public readonly isOsmCache: boolean
private readonly seenids: Set<string> = new Set<string>()
public readonly layer: FilteredLayer; public readonly layer: FilteredLayer;
public readonly tileIndex public readonly tileIndex
public readonly bbox; public readonly bbox;
private readonly seenids: Set<string> = new Set<string>()
/** /**
* Only used if the actual source is a tiled geojson. * Only used if the actual source is a tiled geojson.
* A big feature might be contained in multiple tiles. * A big feature might be contained in multiple tiles.

View file

@ -9,9 +9,8 @@ import {Tiles} from "../../../Models/TileRange";
* A tiled source which dynamically loads the required tiles at a fixed zoom level * A tiled source which dynamically loads the required tiles at a fixed zoom level
*/ */
export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> { export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
private readonly _loadedTiles = new Set<number>();
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>; public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>;
private readonly _loadedTiles = new Set<number>();
constructor( constructor(
layer: FilteredLayer, layer: FilteredLayer,

View file

@ -13,9 +13,10 @@ import {Or} from "../../Tags/Or";
import {TagsFilter} from "../../Tags/TagsFilter"; import {TagsFilter} from "../../Tags/TagsFilter";
export default class OsmFeatureSource { export default class OsmFeatureSource {
private readonly _backend: string;
public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false) 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 filteredLayers: UIEventSource<FilteredLayer[]>;
private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void; private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void;
private isActive: UIEventSource<boolean>; private isActive: UIEventSource<boolean>;
@ -28,11 +29,8 @@ export default class OsmFeatureSource {
}, },
markTileVisited?: (tileId: number) => void markTileVisited?: (tileId: number) => void
}; };
public readonly downloadedTiles = new Set<number>()
private readonly allowedTags: TagsFilter; private readonly allowedTags: TagsFilter;
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
constructor(options: { constructor(options: {
handleTile: (tile: FeatureSourceForLayer & Tiled) => void; handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
isActive: UIEventSource<boolean>, isActive: UIEventSource<boolean>,

View file

@ -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. 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. In order to keep thins snappy, they are distributed over a tiled database per layer.
## Notes ## Notes
`cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up `cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up

View file

@ -8,9 +8,8 @@ import {BBox} from "../../BBox";
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> { export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, 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; public readonly layer: FilteredLayer;
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>();
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void; private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void;
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) { constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) {

View file

@ -1,6 +1,5 @@
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import {Utils} from "../../../Utils";
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import TileHierarchy from "./TileHierarchy"; import TileHierarchy from "./TileHierarchy";
import {Tiles} from "../../../Models/TileRange"; import {Tiles} from "../../../Models/TileRange";
@ -28,13 +27,13 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
public readonly containedIds: UIEventSource<Set<string>> public readonly containedIds: UIEventSource<Set<string>>
public readonly bbox: BBox; public readonly bbox: BBox;
public readonly tileIndex: number;
private upper_left: TiledFeatureSource private upper_left: TiledFeatureSource
private upper_right: TiledFeatureSource private upper_right: TiledFeatureSource
private lower_left: TiledFeatureSource private lower_left: TiledFeatureSource
private lower_right: TiledFeatureSource private lower_right: TiledFeatureSource
private readonly maxzoom: number; private readonly maxzoom: number;
private readonly options: TiledFeatureSourceOptions private readonly options: TiledFeatureSourceOptions
public readonly tileIndex: number;
private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) { private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) {
this.z = z; this.z = z;

View file

@ -13,44 +13,6 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
private readonly handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void; private readonly handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void;
private readonly undefinedTiles: Set<number>; 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, constructor(layer: FilteredLayer,
handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void, handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void,
state: { 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) { private loadTile(neededIndex: number) {
try { try {
const key = SaveTileToLocalStorageActor.storageKey + "-" + this.layer.layerDef.id + "-" + neededIndex const key = SaveTileToLocalStorageActor.storageKey + "-" + this.layer.layerDef.id + "-" + neededIndex

View file

@ -3,6 +3,9 @@ import {BBox} from "./BBox";
export class GeoOperations { export class GeoOperations {
private static readonly _earthRadius = 6378137;
private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2;
static surfaceAreaInSqMeters(feature: any) { static surfaceAreaInSqMeters(feature: any) {
return turf.area(feature); return turf.area(feature);
} }
@ -292,10 +295,6 @@ export class GeoOperations {
return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n") 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 //Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913
public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] { public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] {
const lon = lonLat[0]; const lon = lonLat[0];
@ -320,6 +319,31 @@ export class GeoOperations {
return turf.toWgs84(geojson) 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. * 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 * 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; 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
}
} }

View file

@ -10,11 +10,6 @@ export default class GenericImageProvider extends ImageProvider {
this._valuePrefixBlacklist = valuePrefixBlacklist; this._valuePrefixBlacklist = valuePrefixBlacklist;
} }
protected DownloadAttribution(url: string) {
return undefined
}
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
if (this._valuePrefixBlacklist.some(prefix => value.startsWith(prefix))) { if (this._valuePrefixBlacklist.some(prefix => value.startsWith(prefix))) {
@ -39,5 +34,9 @@ export default class GenericImageProvider extends ImageProvider {
return undefined; return undefined;
} }
protected DownloadAttribution(url: string) {
return undefined
}
} }

View file

@ -27,8 +27,6 @@ export default abstract class ImageProvider {
public abstract SourceIcon(backlinkSource?: string): BaseUIElement; 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 * 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>[]>; public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>;
protected abstract DownloadAttribution(url: string): Promise<LicenseInfo>;
} }

View file

@ -8,9 +8,8 @@ import {LicenseInfo} from "./LicenseInfo";
export class Imgur extends ImageProvider { export class Imgur extends ImageProvider {
public static readonly defaultValuePrefix = ["https://i.imgur.com"] public static readonly defaultValuePrefix = ["https://i.imgur.com"]
public readonly defaultKeyPrefixes: string[] = ["image"];
public static readonly singleton = new Imgur(); public static readonly singleton = new Imgur();
public readonly defaultKeyPrefixes: string[] = ["image"];
private constructor() { private constructor() {
super(); super();
@ -89,6 +88,17 @@ export class Imgur extends ImageProvider {
return undefined; 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) => { protected DownloadAttribution: (url: string) => Promise<LicenseInfo> = async (url: string) => {
const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0];
@ -112,16 +122,5 @@ export class Imgur extends ImageProvider {
return licenseInfo 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 []
}
} }

View file

@ -6,9 +6,8 @@ export default class ImgurUploader {
public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]); public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]);
public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]); public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]);
public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]); public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]);
private readonly _handleSuccessUrl: (string) => void;
public maxFileSizeInMegabytes = 10; public maxFileSizeInMegabytes = 10;
private readonly _handleSuccessUrl: (string) => void;
constructor(handleSuccessUrl: (string) => void) { constructor(handleSuccessUrl: (string) => void) {
this._handleSuccessUrl = handleSuccessUrl; this._handleSuccessUrl = handleSuccessUrl;

View file

@ -7,11 +7,10 @@ import Constants from "../../Models/Constants";
export class Mapillary extends ImageProvider { export class Mapillary extends ImageProvider {
defaultKeyPrefixes = ["mapillary","image"]
public static readonly singleton = new Mapillary(); public static readonly singleton = new Mapillary();
private static readonly valuePrefix = "https://a.mapillary.com" 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"] 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): { private static ExtractKeyFromURL(value: string, failIfNoMath = false): {
key: string, key: string,
@ -59,6 +58,31 @@ export class Mapillary extends ImageProvider {
return [this.PrepareUrlAsync(key, value)] 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> { private async PrepareUrlAsync(key: string, value: string): Promise<ProvidedImage> {
const failIfNoMatch = key.indexOf("mapillary") < 0 const failIfNoMatch = key.indexOf("mapillary") < 0
const keyV = Mapillary.ExtractKeyFromURL(value, failIfNoMatch) 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
}
} }

View file

@ -1,4 +1,3 @@
import {Utils} from "../../Utils";
import ImageProvider, {ProvidedImage} from "./ImageProvider"; import ImageProvider, {ProvidedImage} from "./ImageProvider";
import BaseUIElement from "../../UI/BaseUIElement"; import BaseUIElement from "../../UI/BaseUIElement";
import Svg from "../../Svg"; import Svg from "../../Svg";
@ -7,10 +6,6 @@ import Wikidata from "../Web/Wikidata";
export class WikidataImageProvider extends ImageProvider { export class WikidataImageProvider extends ImageProvider {
public SourceIcon(backlinkSource?: string): BaseUIElement {
throw Svg.wikidata_svg();
}
public static readonly singleton = new WikidataImageProvider() public static readonly singleton = new WikidataImageProvider()
public readonly defaultKeyPrefixes = ["wikidata"] public readonly defaultKeyPrefixes = ["wikidata"]
@ -18,8 +13,8 @@ export class WikidataImageProvider extends ImageProvider {
super() super()
} }
protected DownloadAttribution(url: string): Promise<any> { public SourceIcon(backlinkSource?: string): BaseUIElement {
throw new Error("Method not implemented; shouldn't be needed!"); throw Svg.wikidata_svg();
} }
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
@ -51,4 +46,8 @@ export class WikidataImageProvider extends ImageProvider {
return allImages return allImages
} }
protected DownloadAttribution(url: string): Promise<any> {
throw new Error("Method not implemented; shouldn't be needed!");
}
} }

View file

@ -12,10 +12,10 @@ import Wikimedia from "../Web/Wikimedia";
export class WikimediaImageProvider extends ImageProvider { 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 singleton = new WikimediaImageProvider();
public static readonly commonsPrefixes = ["https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", "File:"] 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() { private constructor() {
super(); 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 { SourceIcon(backlink: string): BaseUIElement {
const img = Svg.wikimedia_commons_white_svg() const img = Svg.wikimedia_commons_white_svg()
.SetStyle("width:2em;height: 2em"); .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/")) { if (value.startsWith("File:")) {
return value; 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> { 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} 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))]
}
} }

View file

@ -147,7 +147,8 @@ export default class MetaTagging {
} }
} }
}} ) }
})
} }

View file

@ -166,7 +166,8 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
nodeIdsToUse.push({ nodeIdsToUse.push({
lat, lon, lat, lon,
nodeId : newNodeAction.newElementIdNumber}) nodeId: newNodeAction.newElementIdNumber
})
continue continue
} }

View file

@ -10,6 +10,7 @@ export interface RelationSplitInput {
originalNodes: number[], originalNodes: number[],
allWaysNodesInOrder: number[][] allWaysNodesInOrder: number[][]
} }
abstract class AbstractRelationSplitHandler extends OsmChangeAction { abstract class AbstractRelationSplitHandler extends OsmChangeAction {
protected readonly _input: RelationSplitInput; protected readonly _input: RelationSplitInput;
protected readonly _theme: string; protected readonly _theme: string;
@ -101,7 +102,10 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
let commonPoint = commonStartPoint ?? commonEndPoint let commonPoint = commonStartPoint ?? commonEndPoint
// Let's select the way to keep // 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 => { .filter(nodesId => {
const nds = nodesId.nodes const nds = nodesId.nodes
return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint

View file

@ -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. * 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> { private async CloseChangeset(changesetId: number = undefined): Promise<void> {
const self = this const self = this

View file

@ -50,18 +50,19 @@ export class OsmConnection {
_dryRun: boolean; _dryRun: boolean;
public preferencesHandler: OsmPreferences; public preferencesHandler: OsmPreferences;
public changesetHandler: ChangesetHandler; public changesetHandler: ChangesetHandler;
private fakeUser: boolean;
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
private readonly _iframeMode: Boolean | boolean;
private readonly _singlePage: boolean;
public readonly _oauth_config: { public readonly _oauth_config: {
oauth_consumer_key: string, oauth_consumer_key: string,
oauth_secret: string, oauth_secret: string,
url: string url: string
}; };
private fakeUser: boolean;
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
private readonly _iframeMode: Boolean | boolean;
private readonly _singlePage: boolean;
private isChecking = false; private isChecking = false;
constructor(options:{dryRun?: false | boolean, constructor(options: {
dryRun?: false | boolean,
fakeUser?: false | boolean, fakeUser?: false | boolean,
allElements: ElementStorage, allElements: ElementStorage,
changes: Changes, changes: Changes,
@ -69,7 +70,8 @@ export class OsmConnection {
// Used to keep multiple changesets open and to write to the correct changeset // Used to keep multiple changesets open and to write to the correct changeset
layoutName: string, layoutName: string,
singlePage?: boolean, singlePage?: boolean,
osmConfiguration?: "osm" | "osm-test" } osmConfiguration?: "osm" | "osm-test"
}
) { ) {
this.fakeUser = options.fakeUser ?? false; this.fakeUser = options.fakeUser ?? false;
this._singlePage = options.singlePage ?? true; this._singlePage = options.singlePage ?? true;

View file

@ -170,41 +170,6 @@ export abstract class OsmObject {
const elements: any[] = data.elements; const elements: any[] = data.elements;
return OsmObject.ParseObjects(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[] { public static ParseObjects(elements: any[]): OsmObject[] {
const objects: OsmObject[] = []; const objects: OsmObject[] = [];
@ -242,6 +207,42 @@ export abstract class OsmObject {
return objects; 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] // The centerpoint of the feature, as [lat, lon]
public abstract centerpoint(): [number, number]; public abstract centerpoint(): [number, number];

View file

@ -1,4 +1,3 @@
import State from "../../State";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
export interface Relation { export interface Relation {
@ -21,10 +20,6 @@ export default class RelationsTracker {
constructor() { 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 * Gets an overview of the relations - except for multipolygons. We don't care about those
* @param overpassJson * @param overpassJson
@ -39,6 +34,10 @@ export default class RelationsTracker {
return relations return relations
} }
public RegisterRelations(overpassJson: any): void {
this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson))
}
/** /**
* Build a mapping of {memberId --> {role in relation, id of relation} } * Build a mapping of {memberId --> {role in relation, id of relation} }
* @param relations * @param relations

View file

@ -49,6 +49,8 @@ export default class SimpleMetaTagger {
return true; return true;
} }
) )
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTagger.metatags.filter(tagger => tagger.isLazy)
.map(tagger => tagger.keys));
private static latlon = new SimpleMetaTagger({ private static latlon = new SimpleMetaTagger({
keys: ["_lat", "_lon"], keys: ["_lat", "_lon"],
doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)" 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; 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( private static noBothButLeftRight = new SimpleMetaTagger(
{ {
keys: ["sidewalk:left", "sidewalk:right", "generic_key:left:property", "generic_key:right:property"], keys: ["sidewalk:left", "sidewalk:right", "generic_key:left:property", "generic_key:right:property"],
@ -451,9 +376,6 @@ export default class SimpleMetaTagger {
SimpleMetaTagger.noBothButLeftRight SimpleMetaTagger.noBothButLeftRight
]; ];
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTagger.metatags.filter(tagger => tagger.isLazy)
.map(tagger => tagger.keys));
public readonly keys: string[]; public readonly keys: string[];
public readonly doc: string; public readonly doc: string;
public readonly isLazy: boolean; 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 { public static HelpText(): BaseUIElement {
const subElements: (string | BaseUIElement)[] = [ const subElements: (string | BaseUIElement)[] = [
new Combine([ new Combine([

View file

@ -14,7 +14,6 @@ import {QueryParameters} from "../Web/QueryParameters";
import * as personal from "../../assets/themes/personal/personal.json"; import * as personal from "../../assets/themes/personal/personal.json";
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"; import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
import {Coord} from "@turf/turf";
/** /**
* Contains all the leaflet-map related state * Contains all the leaflet-map related state
@ -123,7 +122,21 @@ export default class MapState extends UserRelatedState {
this.AddAllOverlaysToMap(this.leafletMap) 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() { private lockBounds() {
const layout = this.layoutToUse; const layout = this.layoutToUse;
@ -201,21 +214,5 @@ export default class MapState extends UserRelatedState {
return new UIEventSource<FilteredLayer[]>(flayers); 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)
}
}
} }

View file

@ -1,5 +1,4 @@
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import * as Events from "events";
export class UIEventSource<T> { export class UIEventSource<T> {
@ -76,27 +75,6 @@ export class UIEventSource<T> {
return src 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. * Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated.
* If the promise fails, the value will stay undefined * If the promise fails, the value will stay undefined
@ -110,20 +88,6 @@ export class UIEventSource<T> {
return src 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. * Given a UIEVentSource with a list, returns a new UIEventSource which is only updated if the _contents_ of the list are different.
* E.g. * E.g.
@ -168,6 +132,57 @@ export class UIEventSource<T> {
return stable 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 * 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);
}
)
}
} }

View file

@ -31,7 +31,8 @@ export default class Wikipedia {
public static GetArticle(options: { public static GetArticle(options: {
pageName: string, 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 key = (options.language ?? "en") + ":" + options.pageName
const cached = Wikipedia._cache.get(key) const cached = Wikipedia._cache.get(key)
if (cached !== undefined) { if (cached !== undefined) {
@ -69,12 +70,10 @@ export default class Wikipedia {
} }
const links = Array.from(content.getElementsByTagName("a")) const links = Array.from(content.getElementsByTagName("a"))
// Rewrite relative links to absolute links + open them in a new tab // Rewrite relative links to absolute links + open them in a new tab
links.filter(link => link.getAttribute("href")?.startsWith("/") ?? false). links.filter(link => link.getAttribute("href")?.startsWith("/") ?? false).forEach(link => {
forEach(link => {
link.target = '_blank' 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 // 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")}`; link.href = `https://${language}.wikipedia.org${link.getAttribute("href")}`;

View file

@ -18,7 +18,6 @@ export default class Constants {
] ]
// The user journey states thresholds when a new feature gets unlocked // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {
moreScreenUnlock: 1, moreScreenUnlock: 1,

View file

@ -89,6 +89,7 @@ export class Denomination {
value = value.toLowerCase() value = value.toLowerCase()
const self = this; const self = this;
function startsWith(key) { function startsWith(key) {
if (self.prefix) { if (self.prefix) {
return value.startsWith(key) return value.startsWith(key)

View file

@ -1,6 +1,5 @@
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import LayerConfig from "./ThemeConfig/LayerConfig"; import LayerConfig from "./ThemeConfig/LayerConfig";
import {And} from "../Logic/Tags/And";
import FilterConfig from "./ThemeConfig/FilterConfig"; import FilterConfig from "./ThemeConfig/FilterConfig";
export default interface FilteredLayer { export default interface FilteredLayer {

View file

@ -216,8 +216,6 @@ export interface LayerConfigJson {
}) [], }) [],
/** /**
* All the extra questions for filtering * All the extra questions for filtering
*/ */

View file

@ -1,4 +1,3 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {LayerConfigJson} from "./LayerConfigJson"; import {LayerConfigJson} from "./LayerConfigJson";
import TilesourceConfigJson from "./TilesourceConfigJson"; import TilesourceConfigJson from "./TilesourceConfigJson";

View file

@ -16,8 +16,7 @@ export default interface UnitConfigJson {
} }
export interface ApplicableUnitJson export interface ApplicableUnitJson {
{
/** /**
* The canonical value which will be added to the text. * The canonical value which will be added to the text.
* e.g. "m" for meters * e.g. "m" for meters

View file

@ -1,6 +1,4 @@
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
import WithContextLoader from "./WithContextLoader"; import WithContextLoader from "./WithContextLoader";
import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "./TagRenderingConfig"; import TagRenderingConfig from "./TagRenderingConfig";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"; import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";

View file

@ -77,21 +77,6 @@ export default class PointRenderingConfig extends WithContextLoader {
this.rotation = this.tr("rotation", "0"); 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 * 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 * 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 { public GetSimpleIcon(tags: UIEventSource<any>): BaseUIElement {
const self = this; const self = this;
if (this.icon === undefined) { if (this.icon === undefined) {
@ -147,48 +146,6 @@ export default class PointRenderingConfig extends WithContextLoader {
})).SetClass("w-full h-full block") })).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( public GenerateLeafletStyle(
tags: UIEventSource<any>, tags: UIEventSource<any>,
clickable: boolean, 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")
}))
}
} }

View file

@ -39,7 +39,8 @@ export default class SourceConfig {
if (params.geojsonSource !== undefined && params.geojsonSourceLevel !== undefined) { 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)) { 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})` 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.osmTags = params.osmTags ?? new RegexTag("id", /.*/);
this.overpassScript = params.overpassScript; this.overpassScript = params.overpassScript;
this.geojsonSource = params.geojsonSource; this.geojsonSource = params.geojsonSource;

View file

@ -260,6 +260,7 @@ export default class TagRenderingConfig {
return false; return false;
} }
/** /**
* Gets all the render values. Will return multiple render values if 'multianswer' is enabled. * Gets all the render values. Will return multiple render values if 'multianswer' is enabled.
* The result will equal [GetRenderValue] if not 'multiAnswer' * The result will equal [GetRenderValue] if not 'multiAnswer'

View file

@ -4,8 +4,8 @@ import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
export default class WithContextLoader { export default class WithContextLoader {
private readonly _json: any;
protected readonly _context: string; protected readonly _context: string;
private readonly _json: any;
constructor(json: any, context: string) { constructor(json: any, context: string) {
this._json = json; this._json = json;

View file

@ -1,5 +1,5 @@
import {control} from "leaflet"; import {control} from "leaflet";
import zoom = control.zoom;
export interface TileRange { export interface TileRange {
xstart: number, xstart: number,
@ -27,24 +27,6 @@ export class Tiles {
return result; 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 * Calculates the tile bounds of the
* @param z * @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)]] 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]] { 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)]] 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 { static tile_index(z: number, x: number, y: number): number {
return ((x * (2 << z)) + y) * 100 + z return ((x * (2 << z)) + y) * 100 + z
} }
/** /**
* Given a tile index number, returns [z, x, y] * Given a tile index number, returns [z, x, y]
* @param index * @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)));
}
} }

View file

@ -37,7 +37,9 @@ export class Unit {
const possiblePostFixes = new Set<string>() const possiblePostFixes = new Set<string>()
function addPostfixesOf(str) { function addPostfixesOf(str) {
if(str === undefined){return} if (str === undefined) {
return
}
str = str.toLowerCase() str = str.toLowerCase()
for (let i = 0; i < str.length + 1; i++) { for (let i = 0; i < str.length + 1; i++) {
const substr = str.substring(0, i) const substr = str.substring(0, i)

View file

@ -2,8 +2,8 @@
> Let a thousand flowers bloom > 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 **MapComplete is an OpenStreetMap viewer and editor.** It shows map features on a certain topic, and allows to see, edit
add new features to the map. It can be seen as a 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 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 easy to use as StreetComplete, but it allows to focus on one single theme per instance (e.g. nature, bicycle
infrastructure, ...) infrastructure, ...)
@ -15,20 +15,23 @@ infrastructure, ...)
- Easy to set up a custom theme - Easy to set up a custom theme
- Easy to fall down the rabbit hole of OSM - 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 **The basic functionality is** to download some map features from Overpass and then ask certain questions. An answer is
back to directly to OpenStreetMap. 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 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. 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 **An explicit non-goal** of MapComplete is to modify geometries of ways. Although adding a point to a way or splitting a
in two parts might be added one day. 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 # Creating your own theme
It is possible to quickly make and distribute 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). - [please read the documentation on how to do this](Docs/Making_Your_Own_Theme.md).
## Examples ## Examples

View file

@ -15,5 +15,4 @@ export default class State extends FeaturePipelineState {
} }
} }

View file

@ -8,7 +8,6 @@ import {Utils} from "../Utils";
import LanguagePicker from "./LanguagePicker"; import LanguagePicker from "./LanguagePicker";
import IndexText from "./BigComponents/IndexText"; import IndexText from "./BigComponents/IndexText";
import FeaturedMessage from "./BigComponents/FeaturedMessage"; import FeaturedMessage from "./BigComponents/FeaturedMessage";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
export default class AllThemesGui { export default class AllThemesGui {
constructor() { constructor() {

View file

@ -21,7 +21,6 @@ export default class AsyncLazy extends BaseUIElement{
} }
return el return el
}) })
).ConstructElement() ).ConstructElement()
} }

View file

@ -1,4 +1,3 @@
import {FixedUiElement} from "./FixedUiElement";
import {Translation} from "../i18n/Translation"; import {Translation} from "../i18n/Translation";
import Combine from "./Combine"; import Combine from "./Combine";
import Svg from "../../Svg"; import Svg from "../../Svg";

View file

@ -18,7 +18,9 @@ export interface MinimapOptions {
export interface MinimapObj { export interface MinimapObj {
readonly leafletMap: UIEventSource<any>, readonly leafletMap: UIEventSource<any>,
installBounds(factor: number | BBox, showRange?: boolean): void installBounds(factor: number | BBox, showRange?: boolean): void
TakeScreenshot(): Promise<any>; TakeScreenshot(): Promise<any>;
} }

View file

@ -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 { protected InnerConstructElement(): HTMLElement {
const div = document.createElement("div") const div = document.createElement("div")
div.id = this._id; div.id = this._id;
@ -279,10 +285,4 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
this.leafletMap.setData(map) this.leafletMap.setData(map)
} }
public async TakeScreenshot(){
const screenshotter = new SimpleMapScreenshoter();
screenshotter.addTo(this.leafletMap.data);
return await screenshotter.takeScreen('image')
}
} }

View file

@ -19,8 +19,8 @@ import Img from "./Img";
export default class ScrollableFullScreen extends UIElement { export default class ScrollableFullScreen extends UIElement {
private static readonly empty = new FixedUiElement(""); private static readonly empty = new FixedUiElement("");
private static _currentlyOpen: ScrollableFullScreen; private static _currentlyOpen: ScrollableFullScreen;
private hashToShow: string;
public isShown: UIEventSource<boolean>; public isShown: UIEventSource<boolean>;
private hashToShow: string;
private _component: BaseUIElement; private _component: BaseUIElement;
private _fullscreencomponent: 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 { InnerRender(): BaseUIElement {
return this._component; return this._component;
} }
@ -80,6 +73,13 @@ export default class ScrollableFullScreen extends UIElement {
fs.classList.remove("hidden") 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>) { private BuildComponent(title: BaseUIElement, content: BaseUIElement, isShown: UIEventSource<boolean>) {
const returnToTheMap = const returnToTheMap =
new Combine([ new Combine([

View file

@ -45,7 +45,9 @@ export default abstract class BaseUIElement {
* Adds all the relevant classes, space separated * Adds all the relevant classes, space separated
*/ */
public SetClass(clss: string) { public SetClass(clss: string) {
if(clss == undefined){return } if (clss == undefined) {
return
}
const all = clss.split(" ").map(clsName => clsName.trim()); const all = clss.split(" ").map(clsName => clsName.trim());
let recordedChange = false; let recordedChange = false;
for (let c of all) { for (let c of all) {

View file

@ -63,7 +63,10 @@ export default class CopyrightPanel extends Combine {
new VariableUiElement(state.locationControl.map(location => { 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)}` 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 => { new VariableUiElement(josmState.map(state => {
if (state === undefined) { if (state === undefined) {

View file

@ -12,7 +12,6 @@ import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger"; import SimpleMetaTagger from "../../Logic/SimpleMetaTagger";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {meta} from "@turf/turf";
import {BBox} from "../../Logic/BBox"; import {BBox} from "../../Logic/BBox";
export class DownloadPanel extends Toggle { export class DownloadPanel extends Toggle {

View file

@ -9,7 +9,6 @@ import Toggle from "../Input/Toggle";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
import {Tag} from "../../Logic/Tags/Tag"; import {Tag} from "../../Logic/Tags/Tag";
import Loading from "../Base/Loading"; import Loading from "../Base/Loading";
import CreateNewWayAction from "../../Logic/Osm/Actions/CreateNewWayAction";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {Changes} from "../../Logic/Osm/Changes"; import {Changes} from "../../Logic/Osm/Changes";
@ -32,7 +31,6 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import BaseLayer from "../../Models/BaseLayer"; import BaseLayer from "../../Models/BaseLayer";
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"; import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
import CreateWayWithPointReuseAction from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"; import CreateWayWithPointReuseAction from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"; import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; import FeatureSource from "../../Logic/FeatureSource/FeatureSource";

View file

@ -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 * Creates a button linking to the given theme
* @private * @private
@ -210,5 +106,111 @@ export default class MoreScreen extends Combine {
]), {url: linkText, newTab: false}); ]), {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
});
})
)
}
} }

View file

@ -62,7 +62,6 @@ export default class ShareScreen extends Combine {
} }
const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = state.backgroundLayer; const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = state.backgroundLayer;
const currentBackground = new VariableUiElement(currentLayer.map(layer => { const currentBackground = new VariableUiElement(currentLayer.map(layer => {
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}); return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""});

View file

@ -46,6 +46,49 @@ export default class DefaultGUI {
this.SetupMap() 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() { private SetupMap() {
const state = this.state; 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;
}
})
}
} }

View file

@ -4,13 +4,13 @@ import Constants from "../Models/Constants";
import Hash from "../Logic/Web/Hash"; import Hash from "../Logic/Web/Hash";
export class DefaultGuiState { export class DefaultGuiState {
static state: DefaultGuiState;
public readonly welcomeMessageIsOpened: UIEventSource<boolean>; public readonly welcomeMessageIsOpened: UIEventSource<boolean>;
public readonly downloadControlIsOpened: UIEventSource<boolean>; public readonly downloadControlIsOpened: UIEventSource<boolean>;
public readonly filterViewIsOpened: UIEventSource<boolean>; public readonly filterViewIsOpened: UIEventSource<boolean>;
public readonly copyrightViewIsOpened: UIEventSource<boolean>; public readonly copyrightViewIsOpened: UIEventSource<boolean>;
public readonly welcomeMessageOpenedTab: UIEventSource<number> public readonly welcomeMessageOpenedTab: UIEventSource<number>
public readonly allFullScreenStates: UIEventSource<boolean>[] = [] public readonly allFullScreenStates: UIEventSource<boolean>[] = []
static state: DefaultGuiState;
constructor() { constructor() {

View file

@ -11,6 +11,7 @@ import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
import {BBox} from "../Logic/BBox"; import {BBox} from "../Logic/BBox";
/** /**
* Creates screenshoter to take png screenshot * Creates screenshoter to take png screenshot
* Creates jspdf and downloads it * Creates jspdf and downloads it
@ -109,7 +110,6 @@ export default class ExportPDF {
private async CreatePdf(minimap: MinimapObj) { private async CreatePdf(minimap: MinimapObj) {
console.log("PDF creation started") console.log("PDF creation started")
const t = Translations.t.general.pdf; const t = Translations.t.general.pdf;
const layout = this._layout const layout = this._layout

View file

@ -167,6 +167,7 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
installBounds(factor: number | BBox, showRange?: boolean): void { installBounds(factor: number | BBox, showRange?: boolean): void {
this.map.installBounds(factor, showRange) this.map.installBounds(factor, showRange)
} }
TakeScreenshot(): Promise<any> { TakeScreenshot(): Promise<any> {
return this.map.TakeScreenshot() return this.map.TakeScreenshot()
} }

View file

@ -18,14 +18,6 @@ export default class Toggle extends VariableUiElement {
this.isEnabled = isEnabled 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 { public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement {
if (constructor === undefined) { if (constructor === undefined) {
return 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;
}
} }

View file

@ -5,9 +5,9 @@ import {VariableUiElement} from "../Base/VariableUIElement";
export default class VariableInputElement<T> extends InputElement<T> { export default class VariableInputElement<T> extends InputElement<T> {
public readonly IsSelected: UIEventSource<boolean>;
private readonly value: UIEventSource<T>; private readonly value: UIEventSource<T>;
private readonly element: BaseUIElement private readonly element: BaseUIElement
public readonly IsSelected: UIEventSource<boolean>;
private readonly upstream: UIEventSource<InputElement<T>>; private readonly upstream: UIEventSource<InputElement<T>>;
constructor(upstream: UIEventSource<InputElement<T>>) { constructor(upstream: UIEventSource<InputElement<T>>) {
@ -23,13 +23,12 @@ export default class VariableInputElement<T> extends InputElement<T> {
return this.value; return this.value;
} }
protected InnerConstructElement(): HTMLElement {
return this.element.ConstructElement();
}
IsValid(t: T): boolean { IsValid(t: T): boolean {
return this.upstream.data.IsValid(t); return this.upstream.data.IsValid(t);
} }
protected InnerConstructElement(): HTMLElement {
return this.element.ConstructElement();
}
} }

View file

@ -107,8 +107,6 @@ export default class MoveWizard extends Toggle {
}) })
const selectReason = new Combine(reasons.map(r => new SubtleButton(r.icon, r.text).onClick(() => { const selectReason = new Combine(reasons.map(r => new SubtleButton(r.icon, r.text).onClick(() => {
moveReason.setData(r) moveReason.setData(r)
currentStep.setData("pick_location") currentStep.setData("pick_location")

View file

@ -32,6 +32,7 @@ export interface MultiApplyParams {
class MultiApplyExecutor { class MultiApplyExecutor {
private static executorCache = new Map<string, MultiApplyExecutor>()
private readonly originalValues = new Map<string, string>() private readonly originalValues = new Map<string, string>()
private readonly params: MultiApplyParams; 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() { public applyTaggingOnOtherFeatures() {
console.log("Multi-applying changes...") console.log("Multi-applying changes...")
const featuresToChange = this.params.featureIds.data 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 { export default class MultiApply extends Toggle {

View file

@ -1,4 +1,3 @@
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import FeatureInfoBox from "../Popup/FeatureInfoBox"; 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 { export default class ShowDataLayer {
private static dataLayerIds = 0
private readonly _leafletMap: UIEventSource<L.Map>; private readonly _leafletMap: UIEventSource<L.Map>;
private readonly _enablePopups: boolean; private readonly _enablePopups: boolean;
private readonly _features: RenderingMultiPlexerFeatureSource private readonly _features: RenderingMultiPlexerFeatureSource
@ -30,7 +30,6 @@ export default class ShowDataLayer {
private _cleanCount = 0; private _cleanCount = 0;
private geoLayer = undefined; private geoLayer = undefined;
private isDirty = false; private isDirty = false;
/** /**
* If the selected element triggers, this is used to lookup the correct layer and to open the popup * 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 * Used to avoid a lot of callbacks on the selected element
@ -39,9 +38,7 @@ export default class ShowDataLayer {
* @private * @private
*/ */
private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>() private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>()
private readonly showDataLayerid: number; private readonly showDataLayerid: number;
private static dataLayerIds = 0
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) { constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
this._leafletMap = options.leafletMap; this._leafletMap = options.leafletMap;

View file

@ -1,6 +1,5 @@
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import * as L from "leaflet";
export default class ShowOverlayLayer { export default class ShowOverlayLayer {

View file

@ -6,6 +6,7 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
import {GeoOperations} from "../../Logic/GeoOperations"; import {GeoOperations} from "../../Logic/GeoOperations";
import {Tiles} from "../../Models/TileRange"; import {Tiles} from "../../Models/TileRange";
import * as clusterstyle from "../../assets/layers/cluster_style/cluster_style.json" import * as clusterstyle from "../../assets/layers/cluster_style/cluster_style.json"
export default class ShowTileInfo { export default class ShowTileInfo {
public static readonly styling = new LayerConfig( public static readonly styling = new LayerConfig(
clusterstyle, "tileinfo", true) clusterstyle, "tileinfo", true)

View file

@ -10,6 +10,12 @@ import FilteredLayer from "../../Models/FilteredLayer";
* A feature source containing but a single feature, which keeps stats about a tile * A feature source containing but a single feature, which keeps stats about a tile
*/ */
export class TileHierarchyAggregator implements FeatureSource { 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 _parent: TileHierarchyAggregator;
private _root: TileHierarchyAggregator; private _root: TileHierarchyAggregator;
private _z: number; private _z: number;
@ -17,16 +23,7 @@ export class TileHierarchyAggregator implements FeatureSource {
private _y: number; private _y: number;
private _tileIndex: number private _tileIndex: number
private _counter: SingleTileCounter private _counter: SingleTileCounter
private _subtiles: [TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator] = [undefined, undefined, undefined, undefined] 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 featuresStatic = []
private readonly featureProperties: { count: string, kilocount: string, tileId: string, id: string, showCount: string, totalCount: string }; private readonly featureProperties: { count: string, kilocount: string, tileId: string, id: string, showCount: string, totalCount: string };
private readonly _state: { filteredLayers: UIEventSource<FilteredLayer[]> }; private readonly _state: { filteredLayers: UIEventSource<FilteredLayer[]> };
@ -87,6 +84,10 @@ export class TileHierarchyAggregator implements FeatureSource {
this.featuresStatic.push({feature: box, freshness: now}) 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 { public getTile(tileIndex): TileHierarchyAggregator {
if (tileIndex === this._tileIndex) { if (tileIndex === this._tileIndex) {
return this; return this;
@ -103,6 +104,61 @@ export class TileHierarchyAggregator implements FeatureSource {
return this._subtiles[subtileIndex]?.getTile(tileIndex) 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() { private update() {
const newMap = new Map<string, number>() const newMap = new Map<string, number>()
let total = 0 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) { private visitSubTiles(f: (aggr: TileHierarchyAggregator) => boolean) {
const visitFurther = f(this) const visitFurther = f(this)
if (visitFurther) { if (visitFurther) {
this._subtiles.forEach(tile => tile?.visitSubTiles(f)) 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 bbox: BBox;
public readonly tileIndex: number; public readonly tileIndex: number;
public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, 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 z: number
public readonly x: number public readonly x: number
public readonly y: number public readonly y: number
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
constructor(tileIndex: number) { constructor(tileIndex: number) {
this.tileIndex = tileIndex this.tileIndex = tileIndex

View file

@ -33,13 +33,10 @@ import AllKnownLayers from "../Customizations/AllKnownLayers";
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
import Link from "./Base/Link"; import Link from "./Base/Link";
import List from "./Base/List"; import List from "./Base/List";
import {OsmConnection} from "../Logic/Osm/OsmConnection";
import {SubtleButton} from "./Base/SubtleButton"; import {SubtleButton} from "./Base/SubtleButton";
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"; import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction";
import {And} from "../Logic/Tags/And"; import {And} from "../Logic/Tags/And";
import Toggle from "./Input/Toggle"; import Toggle from "./Input/Toggle";
import Img from "./Base/Img";
import FilteredLayer from "../Models/FilteredLayer";
import {DefaultGuiState} from "./DefaultGuiState"; import {DefaultGuiState} from "./DefaultGuiState";
export interface SpecialVisualization { export interface SpecialVisualization {

View file

@ -4,7 +4,6 @@ import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
import {Translation} from "../i18n/Translation"; import {Translation} from "../i18n/Translation";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import Loading from "../Base/Loading"; import Loading from "../Base/Loading";
import {Transform} from "stream";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import Img from "../Base/Img"; import Img from "../Base/Img";
@ -16,6 +15,43 @@ import {Utils} from "../../Utils";
export default class WikidataPreviewBox extends VariableUiElement { 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>) { constructor(wikidataId: UIEventSource<string>) {
let inited = false; let inited = false;
const wikidata = wikidataId const wikidata = wikidataId
@ -45,6 +81,7 @@ export default class WikidataPreviewBox extends VariableUiElement {
})) }))
} }
// @ts-ignore
public static WikidataResponsePreview(wikidata: WikidataResponse): BaseUIElement { public static WikidataResponsePreview(wikidata: WikidataResponse): BaseUIElement {
let link = new Link( let link = new Link(
@ -80,44 +117,6 @@ export default class WikidataPreviewBox extends VariableUiElement {
return info 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 { public static QuickFacts(wikidata: WikidataResponse): BaseUIElement {
const els: BaseUIElement[] = [] const els: BaseUIElement[] = []

View file

@ -34,6 +34,38 @@ export class Translation extends BaseUIElement {
return this.textFor(Translation.forcedLanguage ?? Locale.language.data) 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 { public textFor(language: string): string {
if (this.translations["*"]) { if (this.translations["*"]) {
return this.translations["*"]; return this.translations["*"];
@ -195,36 +227,4 @@ export class Translation extends BaseUIElement {
} }
return allIcons.filter(icon => icon != undefined) 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);
}
} }

View file

@ -10,11 +10,6 @@ export class Utils {
public static runningFromConsole = typeof window === "undefined"; public static runningFromConsole = typeof window === "undefined";
public static readonly assets_path = "./assets/svg/"; public static readonly assets_path = "./assets/svg/";
public static externalDownloadFunction: (url: string, headers?: any) => Promise<any>; 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\`. 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. 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) 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) { static EncodeXmlValue(str) {
if (typeof str !== "string") { if (typeof str !== "string") {

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- 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"> <!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"> width="35px" height="32px" viewBox="0 0 35 32" enable-background="new 0 0 35 32" xml:space="preserve">
<g> <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 <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

View file

@ -9,12 +9,15 @@ If you want to add a missing socket type, then:
- Add all the properties in 'types.csv' - Add all the properties in 'types.csv'
- Add an icon. (Note: icons are way better as pictures as they are perceived more abstractly) - 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 '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 generate:layeroverview` to generate the layer files
- Run `npm run start` to run the instance - 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 - countryWhiteList: Only show this plug type in these countries
- countryBlackList: Don't show this plug type in these countries. NOt compatibel with the whiteList - countryBlackList: Don't show this plug type in these countries. NOt compatibel with the whiteList
- commonVoltages, commonCurrents, commonOutputs: common values for these tags - commonVoltages, commonCurrents, commonOutputs: common values for these tags
- associatedVehicleTypes and neverAssociatedWith: these work in tandem to hide options. - associatedVehicleTypes and neverAssociatedWith: these work in tandem to hide options. If every associated vehicle type
If every associated vehicle type is `no`, then the option is hidden is `no`, then the option is hidden If at least one `neverAssociatedVehicleType` is `yes` and none of the associated
If at least one `neverAssociatedVehicleType` is `yes` and none of the associated types is yes, then the option is hidden too types is yes, then the option is hidden too

View file

@ -5,10 +5,9 @@
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns="http://www.w3.org/2000/svg"
width="46.74284mm" width="46.74284mm"
height="36.190933mm" height="36.190933mm"
viewBox="0 0 46.74284 36.190933" viewBox="0 0 46.74284 36.190933"

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -5,10 +5,9 @@
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns="http://www.w3.org/2000/svg"
width="46.74284mm" width="46.74284mm"
height="36.190933mm" height="36.190933mm"
viewBox="0 0 46.74284 36.190933" viewBox="0 0 46.74284 36.190933"

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -413,7 +413,12 @@
} }
], ],
"condition": { "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?" "nl": "Wat is het telefoonnummer van de beheerder van dit oplaadpunt?"
}, },
"render": { "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": { "freeform": {
"key": "phone", "key": "phone",

View file

@ -115,7 +115,6 @@ function run(file, protojson) {
} }
overview_question_answers.push(json) overview_question_answers.push(json)
// We add a second time for any amount to trigger a visualisation; but this is not an answer option // We add a second time for any amount to trigger a visualisation; but this is not an answer option

View file

@ -3,10 +3,9 @@
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns="http://www.w3.org/2000/svg"
version="1.1" version="1.1"
x="0px" x="0px"
y="0px" y="0px"
@ -20,8 +19,10 @@
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata
id="metadata38"><rdf:RDF><cc:Work id="metadata38"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type 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 rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata>
id="defs36" /><sodipodi:namedview <defs
id="defs36"/>
<sodipodi:namedview
pagecolor="#ffffff" pagecolor="#ffffff"
bordercolor="#666666" bordercolor="#666666"
borderopacity="1" borderopacity="1"
@ -40,34 +41,41 @@
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="svg32" /><g inkscape:current-layer="svg32"/>
<g
id="g4" id="g4"
transform="translate(-84.147,-70.403)"><path 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" 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" id="path2"
inkscape:connector-curvature="0" /></g><g inkscape:connector-curvature="0" /></g>
<g
id="g26" id="g26"
transform="translate(-84.147,-70.403)"><g transform="translate(-84.147,-70.403)"><g
id="g8"><path 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" 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" id="path6"
inkscape:connector-curvature="0" /></g><g inkscape:connector-curvature="0" /></g>
<g
id="g12"><path 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" 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" id="path10"
inkscape:connector-curvature="0" /></g><g inkscape:connector-curvature="0" /></g>
<g
id="g16"><path 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" 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" id="path14"
inkscape:connector-curvature="0" /></g><g inkscape:connector-curvature="0" /></g>
<g
id="g20"><path 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" 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" id="path18"
inkscape:connector-curvature="0" /></g><g inkscape:connector-curvature="0" /></g>
<g
id="g24"><path 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" 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" id="path22"
inkscape:connector-curvature="0" /></g></g><path inkscape:connector-curvature="0" /></g></g>
<path
id="path1334" 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" 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" inkscape:connector-curvature="0"

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -5,10 +5,9 @@
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns="http://www.w3.org/2000/svg"
width="31.41128mm" width="31.41128mm"
height="21.6535mm" height="21.6535mm"
viewBox="0 0 31.41128 21.6535" viewBox="0 0 31.41128 21.6535"

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-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="http://www.w3.org/2000/svg"
version="1.1" version="1.1"
width="14" width="14"

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Some files were not shown because too many files have changed in this diff Show more