Add binoculars theme, auto reformat everything
This commit is contained in:
parent
38dea806c5
commit
78d6482c88
586 changed files with 115573 additions and 111842 deletions
|
@ -9,16 +9,14 @@
|
|||
"VARIANT": "14"
|
||||
}
|
||||
},
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [1234],
|
||||
|
||||
"forwardPorts": [
|
||||
1234
|
||||
],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
//"postCreateCommand": "npm run init",
|
||||
|
||||
|
|
9
.github/pull_request_template.md
vendored
9
.github/pull_request_template.md
vendored
|
@ -1,9 +1,8 @@
|
|||
|
||||
Opening a pull request on MapComplete
|
||||
=====================================
|
||||
|
||||
Hey! Thanks for opening a pull request on Mapcomplete! This probably means you want to add a new theme - if so, please follow the checklist below.
|
||||
If this pull request is for some other issue, please ignore the template.
|
||||
Hey! Thanks for opening a pull request on Mapcomplete! This probably means you want to add a new theme - if so, please
|
||||
follow the checklist below. If this pull request is for some other issue, please ignore the template.
|
||||
|
||||
Adding your new theme
|
||||
---------------------
|
||||
|
@ -14,4 +13,6 @@ To making merging smooth, please make sure that each of the following conditions
|
|||
|
||||
- [ ] The codebase is GPL-licensed. By opening a pull request, the new theme will be GPL too
|
||||
- [ ] All images are included in the pull request and no images are loaded from an external service (e.g. Wikipedia)
|
||||
- [ ] The [guidelines on how to make your own theme](https://github.com/pietervdvn/MapComplete/blob/master/Docs/Making_Your_Own_Theme.md) are read and followed
|
||||
- [ ]
|
||||
The [guidelines on how to make your own theme](https://github.com/pietervdvn/MapComplete/blob/master/Docs/Making_Your_Own_Theme.md)
|
||||
are read and followed
|
2
.github/workflows/pull_request_check.yml
vendored
2
.github/workflows/pull_request_check.yml
vendored
|
@ -1,7 +1,7 @@
|
|||
name: Pull request check
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, synchronize, ready_for_review, review_requested]
|
||||
types: [ opened, edited, synchronize, ready_for_review, review_requested ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
@ -5,17 +5,17 @@ import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
|
|||
|
||||
export default class SharedTagRenderings {
|
||||
|
||||
public static SharedTagRendering : Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields();
|
||||
public static SharedIcons : Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields(true);
|
||||
public static SharedTagRendering: Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields();
|
||||
public static SharedIcons: Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields(true);
|
||||
|
||||
private static generatedSharedFields(iconsOnly = false) : Map<string, TagRenderingConfig>{
|
||||
private static generatedSharedFields(iconsOnly = false): Map<string, TagRenderingConfig> {
|
||||
const dict = new Map<string, TagRenderingConfig>();
|
||||
|
||||
function add(key, store) {
|
||||
try {
|
||||
dict.set(key, new TagRenderingConfig(store[key], undefined, `SharedTagRenderings.${key}`))
|
||||
} catch (e) {
|
||||
if(!Utils.runningFromConsole){
|
||||
if (!Utils.runningFromConsole) {
|
||||
console.error("BUG: could not parse", key, " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", e)
|
||||
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
Architecture
|
||||
============
|
||||
|
||||
This document aims to give an architectural overview of how MapCompelte is built. It should give some feeling on how everything fits together.
|
||||
This document aims to give an architectural overview of how MapCompelte is built. It should give some feeling on how
|
||||
everything fits together.
|
||||
|
||||
Servers?
|
||||
--------
|
||||
|
@ -11,24 +12,30 @@ There are no servers for MapComplete, all services are configured by third parti
|
|||
Minimal HTML - Minimal CSS
|
||||
--------------------------
|
||||
|
||||
There is quasi no HTML. Most of the components are generated by TypeScript and attached dynamically. The html is a barebones skeleton which serves every theme.
|
||||
There is quasi no HTML. Most of the components are generated by TypeScript and attached dynamically. The html is a
|
||||
barebones skeleton which serves every theme.
|
||||
|
||||
|
||||
The UIEventSource
|
||||
-----------------
|
||||
|
||||
Most (but not all) objects in MapComplete get all the state they need as a parameter in the constructor. However, as is the case with most graphical applications, there are quite some dynamical values.
|
||||
Most (but not all) objects in MapComplete get all the state they need as a parameter in the constructor. However, as is
|
||||
the case with most graphical applications, there are quite some dynamical values.
|
||||
|
||||
All values which change regularly are wrapped into a [UIEventSource](https://github.com/pietervdvn/MapComplete/blob/master/Logic/UIEventSource.ts).
|
||||
An UiEventSource is a wrapper containing a value and offers the possibility to add a callback function which is called every time the value is changed (with setData)
|
||||
All values which change regularly are wrapped into
|
||||
a [UIEventSource](https://github.com/pietervdvn/MapComplete/blob/master/Logic/UIEventSource.ts). An UiEventSource is a
|
||||
wrapper containing a value and offers the possibility to add a callback function which is called every time the value is
|
||||
changed (with setData)
|
||||
|
||||
Furthermore, there are various helper functions, the most widely used one being `map` - generating a new event source with the new value applied.
|
||||
Note that 'map' will also absorb some changes, e.g. `const someEventSource : UIEventSource<string[]> = ... ; someEventSource.map(list = list.length)` will only trigger when the length of the list has changed.
|
||||
Furthermore, there are various helper functions, the most widely used one being `map` - generating a new event source
|
||||
with the new value applied. Note that 'map' will also absorb some changes,
|
||||
e.g. `const someEventSource : UIEventSource<string[]> = ... ; someEventSource.map(list = list.length)` will only trigger
|
||||
when the length of the list has changed.
|
||||
|
||||
An object which receives an UIEventSource is responsible of responding onto changes of this object. This is especially true for UI-components
|
||||
An object which receives an UIEventSource is responsible of responding onto changes of this object. This is especially
|
||||
true for UI-components
|
||||
|
||||
UI
|
||||
--```
|
||||
UI --```
|
||||
|
||||
export default class MyComponent {
|
||||
|
||||
|
@ -37,6 +44,7 @@ export default class MyComponent {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The Graphical User Interface is composed of various UI-elements. For every UI-element, there is a BaseUIElement which creates the actual HTMLElement when needed.
|
||||
|
@ -55,9 +63,9 @@ For example:
|
|||
|
||||
```
|
||||
|
||||
const src : UIEventSource<string> = ... // E.g. user input, data that will be updated...
|
||||
new VariableUIElement(src)
|
||||
.AttachTo('some-id') // attach it to the html
|
||||
const src : UIEventSource<string> = ... // E.g. user input, data that will be updated... new VariableUIElement(src)
|
||||
.AttachTo('some-id') // attach it to the html
|
||||
|
||||
```
|
||||
|
||||
Note that every component offers support for `onClick( someCallBack)`
|
||||
|
@ -109,6 +117,7 @@ This can be constructed as following:
|
|||
|
||||
|
||||
```
|
||||
|
||||
// We construct the dropdown element with values and labelshttps://tailwindcss.com/
|
||||
const isOpened = new Dropdown<string>(Translations.t.is_this_shop_opened_during_holidays,
|
||||
[
|
||||
|
@ -127,6 +136,7 @@ This can be constructed as following:
|
|||
)
|
||||
|
||||
return new Combine([isOpened, extraQuestion])
|
||||
|
||||
```
|
||||
|
||||
### Constructing a special class
|
||||
|
@ -144,6 +154,7 @@ export default class MyComponent {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
2. Construct the needed UI in the constructor
|
||||
|
@ -166,6 +177,7 @@ export default class MyComponent {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
3. You'll notice that you'll end up with one certain component (in this example the combine) to wrap it all together. Change the class to extend this type of component and use super to wrap it all up:
|
||||
|
|
|
@ -1,114 +1,89 @@
|
|||
|
||||
|
||||
Metatags
|
||||
Metatags
|
||||
==========
|
||||
|
||||
|
||||
|
||||
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
|
||||
------------------------------------
|
||||
|
||||
|
||||
|
||||
The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme
|
||||
|
||||
The following values are always calculated, by default, by MapComplete and are available automatically on all elements
|
||||
in every theme
|
||||
|
||||
### _lat, _lon
|
||||
|
||||
|
||||
|
||||
The latitude and longitude of the point (or centerpoint in the case of a way/area)
|
||||
|
||||
|
||||
### _surface, _surface:ha
|
||||
|
||||
|
||||
|
||||
The surface area of the feature, in square meters and in hectare. Not set on points and ways
|
||||
|
||||
|
||||
### _length, _length:km
|
||||
|
||||
|
||||
|
||||
The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter
|
||||
|
||||
The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the
|
||||
length of the perimeter
|
||||
|
||||
### Theme-defined keys
|
||||
|
||||
|
||||
|
||||
If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`)
|
||||
|
||||
If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical
|
||||
form (e.g. `1meter` will be rewritten to `1m`)
|
||||
|
||||
### _country
|
||||
|
||||
|
||||
|
||||
The country code of the property (with latlon2country)
|
||||
|
||||
|
||||
### _isOpen, _isOpen:description
|
||||
|
||||
|
||||
|
||||
If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')
|
||||
|
||||
|
||||
### _width:needed, _width:needed:no_pedestrians, _width:difference
|
||||
|
||||
|
||||
|
||||
Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present
|
||||
|
||||
Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:
|
||||
carriageway' is present
|
||||
|
||||
### _direction:numerical, _direction:leftright
|
||||
|
||||
|
||||
|
||||
_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map
|
||||
|
||||
_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only
|
||||
present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is
|
||||
left-looking on the map or 'right-looking' on the map
|
||||
|
||||
### _now:date, _now:datetime, _loaded:date, _loaded:_datetime
|
||||
|
||||
|
||||
|
||||
Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely
|
||||
|
||||
Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:
|
||||
mm, aka 'sortable' aka ISO-8601-but-not-entirely
|
||||
|
||||
### _last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number
|
||||
|
||||
|
||||
|
||||
Information about the last edit of this object.
|
||||
|
||||
|
||||
Calculating tags with Javascript
|
||||
Calculating tags with Javascript
|
||||
----------------------------------
|
||||
|
||||
|
||||
|
||||
In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. `lat`, `lon`, `_country`), as detailed above.
|
||||
In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by
|
||||
default (e.g. `lat`, `lon`, `_country`), as detailed above.
|
||||
|
||||
It is also possible to calculate your own tags - but this requires some javascript knowledge.
|
||||
|
||||
|
||||
|
||||
Before proceeding, some warnings:
|
||||
|
||||
|
||||
|
||||
- DO NOT DO THIS AS BEGINNER
|
||||
- **Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to calculate a specific value
|
||||
- **THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs.
|
||||
|
||||
- DO NOT DO THIS AS BEGINNER
|
||||
- **Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to
|
||||
calculate a specific value
|
||||
- **THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the
|
||||
internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs.
|
||||
|
||||
To enable this feature, add a field `calculatedTags` in the layer object, e.g.:
|
||||
|
||||
|
@ -126,56 +101,56 @@ To enable this feature, add a field `calculatedTags` in the layer object, e.g.:
|
|||
|
||||
````
|
||||
|
||||
The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended
|
||||
geojson object:
|
||||
|
||||
|
||||
The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:
|
||||
|
||||
|
||||
|
||||
- `area` contains the surface area (in square meters) of the object
|
||||
- `lat` and `lon` contain the latitude and longitude
|
||||
|
||||
- `area` contains the surface area (in square meters) of the object
|
||||
- `lat` and `lon` contain the latitude and longitude
|
||||
|
||||
Some advanced functions are available on **feat** as well:
|
||||
|
||||
- distanceTo
|
||||
- overlapWith
|
||||
- closest
|
||||
- memberships
|
||||
- score
|
||||
- distanceTo
|
||||
- overlapWith
|
||||
- closest
|
||||
- memberships
|
||||
- score
|
||||
|
||||
### distanceTo
|
||||
|
||||
Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object
|
||||
Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of
|
||||
coordinates, a geojson feature or the ID of an object
|
||||
|
||||
0. longitude
|
||||
1. latitude
|
||||
0. longitude
|
||||
1. latitude
|
||||
|
||||
### overlapWith
|
||||
|
||||
Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point 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
|
||||
Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a
|
||||
point, all features that embed the point 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
|
||||
|
||||
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
|
||||
|
||||
Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered.
|
||||
Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature.
|
||||
In the case of ways/polygons, only the centerpoint is considered.
|
||||
|
||||
0. list of features
|
||||
0. list of features
|
||||
|
||||
### memberships
|
||||
|
||||
Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of.
|
||||
Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of.
|
||||
|
||||
For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`
|
||||
|
||||
|
||||
|
||||
### score
|
||||
|
||||
Given the path of an aspected routing json file, will calculate the score. This score is wrapped in a UIEventSource, so for further calculations, use `.map(score => ...)`
|
||||
Given the path of an aspected routing json file, will calculate the score. This score is wrapped in a UIEventSource, so
|
||||
for further calculations, use `.map(score => ...)`
|
||||
|
||||
For example: `_comfort_score=feat.score('https://raw.githubusercontent.com/pietervdvn/AspectedRouting/master/Examples/bicycle/aspects/bicycle.comfort.json')`
|
||||
For
|
||||
example: `_comfort_score=feat.score('https://raw.githubusercontent.com/pietervdvn/AspectedRouting/master/Examples/bicycle/aspects/bicycle.comfort.json')`
|
||||
|
||||
0. path
|
||||
Generated from SimpleMetaTagger, ExtraFunction
|
||||
0. path Generated from SimpleMetaTagger, ExtraFunction
|
|
@ -1,113 +1,128 @@
|
|||
Development and deployment
|
||||
==========================
|
||||
|
||||
Development and deployment
|
||||
==========================
|
||||
There are various scripts to help setup MapComplete for deployment and develop-deployment.
|
||||
|
||||
There are various scripts to help setup MapComplete for deployment and develop-deployment.
|
||||
This documents attempts to shed some light on these scripts.
|
||||
|
||||
This documents attempts to shed some light on these scripts.
|
||||
Note: these scripts change every now and then - if the documentation here is incorrect or you run into troubles, do
|
||||
leave a message in [the issue tracker](https://github.com/pietervdvn/MapComplete/issues)
|
||||
|
||||
Note: these scripts change every now and then - if the documentation here is incorrect or you run into troubles, do leave a message in [the issue tracker](https://github.com/pietervdvn/MapComplete/issues)
|
||||
Architecture overview
|
||||
---------------------
|
||||
|
||||
Architecture overview
|
||||
---------------------
|
||||
At its core, MapComplete is a static (!) website. There are no servers to host.
|
||||
|
||||
At its core, MapComplete is a static (!) website. There are no servers to host.
|
||||
The data is fetched from Overpass/OSM/Wikidata/Wikipedia/Mapillary/... and written there directly. This means that any
|
||||
static file server will do to create a self-hosted version of MapComplete.
|
||||
|
||||
The data is fetched from Overpass/OSM/Wikidata/Wikipedia/Mapillary/... and written there directly. This means that any static file server will do to create a self-hosted version of MapComplete.
|
||||
Development
|
||||
-----------
|
||||
|
||||
Development
|
||||
-----------
|
||||
**Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make
|
||||
the switch ;) ). If you are using Visual Studio Code you can use
|
||||
a [WSL Remote](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) window, or use the
|
||||
Devcontainer (see more details later).
|
||||
|
||||
**Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make the switch ;) ). If you are using Visual Studio Code you can use a [WSL Remote](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) window, or use the Devcontainer (see more details later).
|
||||
|
||||
To develop and build MapComplete, you
|
||||
To develop and build MapComplete, you
|
||||
|
||||
0. Make sure you have a recent version of nodejs - at least 12.0, preferably 15
|
||||
0. Make a fork and clone the repository.
|
||||
1. Install `npm`. Linux: `sudo apt install npm` (or your favourite package manager), Windows: install nodeJS: https://nodejs.org/en/download/
|
||||
3. Run `npm run init` and generate some additional dependencies and generated files. Note that it'll install the dependencies too
|
||||
1. Install `npm`. Linux: `sudo apt install npm` (or your favourite package manager), Windows: install
|
||||
nodeJS: https://nodejs.org/en/download/
|
||||
3. Run `npm run init` and generate some additional dependencies and generated files. Note that it'll install the
|
||||
dependencies too
|
||||
4. Run `npm run start` to host a local testversion at http://localhost:1234/index.html
|
||||
5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
||||
5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename`
|
||||
or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (
|
||||
e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
||||
|
||||
Development using Windows
|
||||
------------------------
|
||||
Development using Windows
|
||||
------------------------
|
||||
|
||||
For Windows you can use the devcontainer, or the WSL subsystem.
|
||||
For Windows you can use the devcontainer, or the WSL subsystem.
|
||||
|
||||
To use the devcontainer in Visual Studio Code:
|
||||
To use the devcontainer in Visual Studio Code:
|
||||
|
||||
0. Make sure you have installed the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension and it's dependencies.
|
||||
0. Make sure you have installed
|
||||
the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
extension and it's dependencies.
|
||||
1. Make a fork and clone the repository.
|
||||
2. After cloning, Visual Studio Code will ask you if you want to use the devcontainer.
|
||||
3. Then you can either clone it again in a volume (for better performance), or open the current folder in a container.
|
||||
4. By now, you should be able to run `npm run start` to host a local testversion at http://localhost:1234/index.html
|
||||
5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
||||
5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename`
|
||||
or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (
|
||||
e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
||||
|
||||
To use the WSL in Visual Studio Code:
|
||||
To use the WSL in Visual Studio Code:
|
||||
|
||||
0. Make sure you have installed the [Remote - WSL]() extension and it's dependencies.
|
||||
1. Open a remote WSL window using the button in the bottom left.
|
||||
2. Make a fork and clone the repository.
|
||||
3. Install `npm` using `sudo apt install npm`.
|
||||
4. Run `npm run init` and generate some additional dependencies and generated files. Note that it'll install the dependencies too
|
||||
4. Run `npm run init` and generate some additional dependencies and generated files. Note that it'll install the
|
||||
dependencies too
|
||||
5. Run `npm run start` to host a local testversion at http://localhost:1234/index.html
|
||||
6. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
||||
6. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename`
|
||||
or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (
|
||||
e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
||||
|
||||
Automatic deployment
|
||||
--------------------
|
||||
|
||||
Currently, the master branch is automatically deployed to 'mapcomplete.osm.be' by a github action.
|
||||
|
||||
Every branch is automatically built (upon push) to 'pietervdvn.github.io/mc/<branchname>' by a github action.
|
||||
|
||||
|
||||
Automatic deployment
|
||||
--------------------
|
||||
Deploying a fork
|
||||
----------------
|
||||
|
||||
Currently, the master branch is automatically deployed to 'mapcomplete.osm.be' by a github action.
|
||||
A script creates a webpage for every theme automatically, with some customizations in order to:
|
||||
|
||||
Every branch is automatically built (upon push) to 'pietervdvn.github.io/mc/<branchname>' by a github action.
|
||||
- to have shorter urls
|
||||
- have individual social images
|
||||
- have individual web manifests
|
||||
|
||||
|
||||
Deploying a fork
|
||||
----------------
|
||||
|
||||
A script creates a webpage for every theme automatically, with some customizations in order to:
|
||||
|
||||
- to have shorter urls
|
||||
- have individual social images
|
||||
- have individual web manifests
|
||||
|
||||
|
||||
This script can be invoked with `npm run prepare-deploy`
|
||||
This script can be invoked with `npm run prepare-deploy`
|
||||
|
||||
If you want to deploy your fork:
|
||||
|
||||
0. `npm run prepare-deploy`
|
||||
1. `npm run build`
|
||||
2. Copy the entire `dist` folder to where you host your website. Visiting `index.html` gives you the landing page, visiting `yourwebsite/<theme>` should bring you to the appropriate theme.
|
||||
|
||||
2. Copy the entire `dist` folder to where you host your website. Visiting `index.html` gives you the landing page,
|
||||
visiting `yourwebsite/<theme>` should bring you to the appropriate theme.
|
||||
|
||||
Weird errors
|
||||
------------
|
||||
|
||||
Try removing `node_modules`, `package-lock.json` and `.cache`
|
||||
|
||||
Overview of package.json-scripts
|
||||
--------------------------------
|
||||
Overview of package.json-scripts
|
||||
--------------------------------
|
||||
|
||||
- `increase-memory`: this is a big (and memory-intensive) project to build and run, so we give nodejs some more RAM.
|
||||
- `start`: start a development server.
|
||||
- `test`: run the unit tests
|
||||
- `init`: Generates and downloads various assets which are needed to compile
|
||||
- `generate:editor-layer-index`: downloads the editor-layer-index-json from osmlab.github.io
|
||||
- `generate:images`: compiles the SVG's into an asset
|
||||
- `generate:translations`: compiles the translation file into a javascript file
|
||||
- `generate:layouts`: uses `index.html` as template to create all the theme index pages. You'll want to run `clean` when done
|
||||
- `generate:docs`: generates various documents, such as information about available metatags, information to put on the [OSM-wiki](https://wiki.openstreetmap.org/wiki/MapComplete),...
|
||||
- `generate:report`: downloads statistics from OsmCha, compiles neat graphs
|
||||
- `generate:cache:speelplekken`: creates an offline copy of all the data required for one specific (paid for) theme
|
||||
- `generate:layeroverview`: reads all the theme- and layerconfigurations, compiles them into a single JSON.
|
||||
- `reset:layeroverview`: if something is wrong with the layeroverview, creates an empty one
|
||||
- `generate:licenses`: compiles all the license info of images into a single json
|
||||
- `optimize:images`: attempts to make smaller pngs - optional to run before a deployment
|
||||
- `generate`: run all the necesary generate-scripts
|
||||
- `build`: actually bundle all the files into a single `dist/`-folder
|
||||
- `prepare-deploy`: create the layouts
|
||||
- `deploy:staging`,`deploy:pietervdvn`, `deploy:production`: deploy the latest code on various locations
|
||||
- `lint`: get depressed by the amount of warnings
|
||||
- `clean`: remove some generated files which are annoying in the repo
|
||||
- `increase-memory`: this is a big (and memory-intensive) project to build and run, so we give nodejs some more RAM.
|
||||
- `start`: start a development server.
|
||||
- `test`: run the unit tests
|
||||
- `init`: Generates and downloads various assets which are needed to compile
|
||||
- `generate:editor-layer-index`: downloads the editor-layer-index-json from osmlab.github.io
|
||||
- `generate:images`: compiles the SVG's into an asset
|
||||
- `generate:translations`: compiles the translation file into a javascript file
|
||||
- `generate:layouts`: uses `index.html` as template to create all the theme index pages. You'll want to run `clean` when
|
||||
done
|
||||
- `generate:docs`: generates various documents, such as information about available metatags, information to put on
|
||||
the [OSM-wiki](https://wiki.openstreetmap.org/wiki/MapComplete),...
|
||||
- `generate:report`: downloads statistics from OsmCha, compiles neat graphs
|
||||
- `generate:cache:speelplekken`: creates an offline copy of all the data required for one specific (paid for) theme
|
||||
- `generate:layeroverview`: reads all the theme- and layerconfigurations, compiles them into a single JSON.
|
||||
- `reset:layeroverview`: if something is wrong with the layeroverview, creates an empty one
|
||||
- `generate:licenses`: compiles all the license info of images into a single json
|
||||
- `optimize:images`: attempts to make smaller pngs - optional to run before a deployment
|
||||
- `generate`: run all the necesary generate-scripts
|
||||
- `build`: actually bundle all the files into a single `dist/`-folder
|
||||
- `prepare-deploy`: create the layouts
|
||||
- `deploy:staging`,`deploy:pietervdvn`, `deploy:production`: deploy the latest code on various locations
|
||||
- `lint`: get depressed by the amount of warnings
|
||||
- `clean`: remove some generated files which are annoying in the repo
|
||||
|
||||
|
|
|
@ -1,28 +1,45 @@
|
|||
Gebruikersgids MapComplete
|
||||
==========================
|
||||
|
||||
MapComplete is een website waar geodata op basis van [OpenStreetMap](https://osm.org), [Wikidata](https://wikidata.org) en andere open bronnen wordt gevisualiseerd en aangevuld.
|
||||
MapComplete is een website waar geodata op basis van [OpenStreetMap](https://osm.org), [Wikidata](https://wikidata.org)
|
||||
en andere open bronnen wordt gevisualiseerd en aangevuld.
|
||||
|
||||
De getoonde geodata is afhankelijk van het thema - zo is er bijvoorbeeld het thema voor [boekenruilkasten](https://mapcomplete.osm.be/bookcases) [cyclofix](http://mapcomplete.osm.be/cyclofix) die focust op fietspompen, fietswinkels, ...
|
||||
De getoonde geodata is afhankelijk van het thema - zo is er bijvoorbeeld het thema
|
||||
voor [boekenruilkasten](https://mapcomplete.osm.be/bookcases) [cyclofix](http://mapcomplete.osm.be/cyclofix) die focust
|
||||
op fietspompen, fietswinkels, ...
|
||||
|
||||
MapComplete mag gratis gebruikt worden. Om de data te raadplegen heb je geen account nodig - de webpagina bezoeken is voldoende.
|
||||
MapComplete mag gratis gebruikt worden. Om de data te raadplegen heb je geen account nodig - de webpagina bezoeken is
|
||||
voldoende.
|
||||
|
||||
### Data toevoegen
|
||||
|
||||
OpenStreetMap is een gedeelde databank van geodata. Om die correct en up to date te houden, voegen we _enkel_ gegevens toe waarvan we zeker weten dat die op dit moment ook zo zijn in de echte wereld. Twijfel je dus over een vraag? Sla de vraag dan over en ga opnieuw ter plaatse kijken. **Beter geen informatie dan foute informatie**.
|
||||
OpenStreetMap is een gedeelde databank van geodata. Om die correct en up to date te houden, voegen we _enkel_ gegevens
|
||||
toe waarvan we zeker weten dat die op dit moment ook zo zijn in de echte wereld. Twijfel je dus over een vraag? Sla de
|
||||
vraag dan over en ga opnieuw ter plaatse kijken. **Beter geen informatie dan foute informatie**.
|
||||
|
||||
Daarnaast verwachten we ook dat je op een vriendelijke en correcte manier omgaat met andere leden van de community. Je kan feedback of vragen krijgen over je aanpassingen - bijvoorbeeld wanneer andere bijdragers denken dat er een vergissing is gebeurd. Meestal is de vergissing met een paar heen- en weerberichtjes uitgeklaard. Vergissen is menselijk.
|
||||
Daarnaast verwachten we ook dat je op een vriendelijke en correcte manier omgaat met andere leden van de community. Je
|
||||
kan feedback of vragen krijgen over je aanpassingen - bijvoorbeeld wanneer andere bijdragers denken dat er een
|
||||
vergissing is gebeurd. Meestal is de vergissing met een paar heen- en weerberichtjes uitgeklaard. Vergissen is
|
||||
menselijk.
|
||||
|
||||
Als je bijdragen grote fouten bevatten, kunnen je wijzigingen ongedaan gemaakt worden door andere leden van OpenStreetMap. Dit is echter erg uitzonderlijk. Bij herhaaldelijke grote fouten, (vermoeden van) kwaad opzet of vandalisme kan je account geblokkeerd worden. Merk op dat MapComplete is opgezet om (grote) vergissingen te vermijden, dus dit is bijna onmogelijk.
|
||||
Als je bijdragen grote fouten bevatten, kunnen je wijzigingen ongedaan gemaakt worden door andere leden van
|
||||
OpenStreetMap. Dit is echter erg uitzonderlijk. Bij herhaaldelijke grote fouten, (vermoeden van) kwaad opzet of
|
||||
vandalisme kan je account geblokkeerd worden. Merk op dat MapComplete is opgezet om (grote) vergissingen te vermijden,
|
||||
dus dit is bijna onmogelijk.
|
||||
|
||||
Data hergebruiken
|
||||
-----------------
|
||||
|
||||
De getoonde data (locaties van POI, ...) mogen vrij en gratis hergebruikt worden voor alle doeleinden (ook commercieel), mits de vermelding `Data van OpenStreetMap, vrij beschikbaar onder ODBL` of een gelijkaardige zin. Hiervoor hoef je geen toestemming te vragen.
|
||||
De getoonde data (locaties van POI, ...) mogen vrij en gratis hergebruikt worden voor alle doeleinden (ook commercieel),
|
||||
mits de vermelding `Data van OpenStreetMap, vrij beschikbaar onder ODBL` of een gelijkaardige zin. Hiervoor hoef je geen
|
||||
toestemming te vragen.
|
||||
|
||||
Let op: aanpassingen aan de data worden ook als open data beschouwd. Let ook op wanneer je OpenStreetMap-data gaat mengen uit andere databronnen waar copyright op rust; vaak mag dit niet. Voor meer informatie, zie de [volledige copyrightnotice](https://osm.org/copyright).
|
||||
Let op: aanpassingen aan de data worden ook als open data beschouwd. Let ook op wanneer je OpenStreetMap-data gaat
|
||||
mengen uit andere databronnen waar copyright op rust; vaak mag dit niet. Voor meer informatie, zie
|
||||
de [volledige copyrightnotice](https://osm.org/copyright).
|
||||
|
||||
Om de data als computerbestand op te vragen, bestaan er [verschillende opties](https://learnosm.org/en/osm-data/getting-data/).
|
||||
Om de data als computerbestand op te vragen, bestaan
|
||||
er [verschillende opties](https://learnosm.org/en/osm-data/getting-data/).
|
||||
|
||||
Privacy
|
||||
-------
|
||||
|
@ -31,17 +48,24 @@ Privacy
|
|||
|
||||
Zolang je je niet aanmeldt, worden er geen persoonsgegevens opgeslaan.
|
||||
|
||||
Je computer stuurt echter wel een klein berichtje naar [pietervdvn.goatcounter.com](pietervdvn.goatcounter.com), waar statistieken over de bezoekersaantallen worden bijgehouden. Dit omvat een minimum aan technische gegevens en kan niet gelinkt worden aan een OSM-gebruikersaccount of persoon door de makers van MapComplete.
|
||||
Je computer stuurt echter wel een klein berichtje naar [pietervdvn.goatcounter.com](pietervdvn.goatcounter.com), waar
|
||||
statistieken over de bezoekersaantallen worden bijgehouden. Dit omvat een minimum aan technische gegevens en kan niet
|
||||
gelinkt worden aan een OSM-gebruikersaccount of persoon door de makers van MapComplete.
|
||||
|
||||
### Met account
|
||||
|
||||
Wanneer je een account maakt, is dit een account op OpenStreetMap. Voor je een account maakt, gelieve hun [privacy statement](https://wiki.osmfoundation.org/wiki/Privacy_Policy) te lezen.
|
||||
Wanneer je een account maakt, is dit een account op OpenStreetMap. Voor je een account maakt, gelieve
|
||||
hun [privacy statement](https://wiki.osmfoundation.org/wiki/Privacy_Policy) te lezen.
|
||||
|
||||
Een account maken is gratis, je moet enkel een email-adres en gebruikersnaam opgeven. Je email-adres is niet publiek zichtbaar, je gebruikersnaam wel.
|
||||
Een account maken is gratis, je moet enkel een email-adres en gebruikersnaam opgeven. Je email-adres is niet publiek
|
||||
zichtbaar, je gebruikersnaam wel.
|
||||
|
||||
Je kan opteren om een _pseudoniem_ te gebruiken - je gebruikersnaam hoeft niet je echte naam te zijn. Ook de naam van je huisdier of iets zelfbedacht mag gerust.
|
||||
Je kan opteren om een _pseudoniem_ te gebruiken - je gebruikersnaam hoeft niet je echte naam te zijn. Ook de naam van je
|
||||
huisdier of iets zelfbedacht mag gerust.
|
||||
|
||||
Wanneer je gegevens aanvult via MapComplete, zal je wijziging **publiek zichtbaar** zijn voor iedereen ter wereld. Dit betekent dus dat iedereen weet dat _gebruikersnaam_ op een bepaalde plaats op een bepaald uur aanwezig was.
|
||||
Wanneer je gegevens aanvult via MapComplete, zal je wijziging **publiek zichtbaar** zijn voor iedereen ter wereld. Dit
|
||||
betekent dus dat iedereen weet dat _gebruikersnaam_ op een bepaalde plaats op een bepaald uur aanwezig was.
|
||||
|
||||
**Ben je minderjarig?** Vraag toestemming aan je ouders voordat je een account aanmaakt en kies ervoor om je echte naam _niet_ te gebruiken.
|
||||
**Ben je minderjarig?** Vraag toestemming aan je ouders voordat je een account aanmaakt en kies ervoor om je echte
|
||||
naam _niet_ te gebruiken.
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
Making your own theme
|
||||
=====================
|
||||
|
||||
In MapComplete, it is relatively simple to make your own theme. This guide will give some information on how you can do this.
|
||||
In MapComplete, it is relatively simple to make your own theme. This guide will give some information on how you can do
|
||||
this.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
@ -11,17 +12,22 @@ Before you start, you should have the following qualifications:
|
|||
- You are a longtime contributor and do know the OpenStreetMap tagging scheme very well.
|
||||
- You are not afraid of editing a .JSON-file
|
||||
- You're theme will add well-understood tags (aka: the tags have a wiki page, are not controversial and are objective)
|
||||
- You are in contact with your local OpenStreetMap community and do know some other members to discuss tagging and to help testing
|
||||
- You are in contact with your local OpenStreetMap community and do know some other members to discuss tagging and to
|
||||
help testing
|
||||
|
||||
If you do not have those qualifications, reach out to the MapComplete community channel on [Telegram](https://t.me/MapComplete)
|
||||
If you do not have those qualifications, reach out to the MapComplete community channel
|
||||
on [Telegram](https://t.me/MapComplete)
|
||||
or [Matrix](https://app.element.io/#/room/#MapComplete:matrix.org).
|
||||
|
||||
The custom theme generator
|
||||
--------------------------
|
||||
|
||||
The custom theme generator is a special page of MapComplete, where one can create their own theme. It makes it easier to get started.
|
||||
The custom theme generator is a special page of MapComplete, where one can create their own theme. It makes it easier to
|
||||
get started.
|
||||
|
||||
However, the custom theme generator is extremely buggy and built before some updates. This means that some features are _not_ available through the custom theme generator. The custom theme generator is good to get the basics of the theme set up, but you will have to edit the raw JSON-file anyway afterwards.
|
||||
However, the custom theme generator is extremely buggy and built before some updates. This means that some features
|
||||
are _not_ available through the custom theme generator. The custom theme generator is good to get the basics of the
|
||||
theme set up, but you will have to edit the raw JSON-file anyway afterwards.
|
||||
|
||||
[A quick tutorial for the custom theme generator can be found here](https://www.youtube.com/watch?v=nVbFrNVPxPw).
|
||||
|
||||
|
@ -30,48 +36,67 @@ Loading your theme
|
|||
|
||||
If you have your .json file, there are three ways to distribute your theme:
|
||||
|
||||
- Take the entire JSON-file and [base64](https://www.base64encode.org/) encode it. Then open up the url `https://mapcomplete.osm.be?userlayout=true#<base64-encoded-json-here>`. Yes, this URL will be huge; and updates are difficult to distribute as you have to send a new URL to everyone. This is however excellent to have a 'quick and dirty' test version up and running as these links can be generated from the customThemeGenerator and can be quickly shared with a few other contributors.
|
||||
- Host the JSON file on a publicly accessible webserver (e.g. github) and open up `https://mapcomplete.osm.be?userlayout=<url-to-the-raw.json>`
|
||||
- Take the entire JSON-file and [base64](https://www.base64encode.org/) encode it. Then open up the
|
||||
url `https://mapcomplete.osm.be?userlayout=true#<base64-encoded-json-here>`. Yes, this URL will be huge; and updates
|
||||
are difficult to distribute as you have to send a new URL to everyone. This is however excellent to have a 'quick and
|
||||
dirty' test version up and running as these links can be generated from the customThemeGenerator and can be quickly
|
||||
shared with a few other contributors.
|
||||
- Host the JSON file on a publicly accessible webserver (e.g. github) and open
|
||||
up `https://mapcomplete.osm.be?userlayout=<url-to-the-raw.json>`
|
||||
- Ask to have your theme included into the official MapComplete - requirements below
|
||||
|
||||
### Getting your theme included into the official mapcomplete
|
||||
|
||||
Did you make an awesome theme that you want to share with the OpenStreetMap community? Have it included in the main application, which makes it more discoverable.
|
||||
Did you make an awesome theme that you want to share with the OpenStreetMap community? Have it included in the main
|
||||
application, which makes it more discoverable.
|
||||
|
||||
Your theme has to be:
|
||||
|
||||
0) Make sure the theme has an English version. This makes it easier for me to understand what is going on. The more other languages, the better of course!
|
||||
0) Make sure the theme has an English version. This makes it easier for me to understand what is going on. The more
|
||||
other languages, the better of course!
|
||||
1) Make sure your theme has good tagging
|
||||
3) Make sure there are somewhat decent icons. Note that there is _no_ styleguide at the moment though.
|
||||
|
||||
The preferred way to add your theme is via a Pull Request. A Pull Request is less work for the maintainer (which makes it really easy and for me to add it) and your name will be included in the git history (so you'll be listed as contributor). If that is not possible, send the .Json-file and assets, e.g. as a zip in an issue, per email, ...
|
||||
The preferred way to add your theme is via a Pull Request. A Pull Request is less work for the maintainer (which makes
|
||||
it really easy and for me to add it) and your name will be included in the git history (so you'll be listed as
|
||||
contributor). If that is not possible, send the .Json-file and assets, e.g. as a zip in an issue, per email, ...
|
||||
|
||||
*Via a pull request:*
|
||||
|
||||
1) Fork this repository
|
||||
2) Go to `assets/themes` and create a new directory `yourtheme`
|
||||
3) Create a new file `yourtheme.json`, paste the theme configuration in there. You can find your theme configuration in the customThemeBuilder (the tab with the *Floppy disk* icon)
|
||||
4) Copy all the images into this new directory. **No external sources are allowed!** External image sources leak privacy or can break.
|
||||
3) Create a new file `yourtheme.json`, paste the theme configuration in there. You can find your theme configuration in
|
||||
the customThemeBuilder (the tab with the *Floppy disk* icon)
|
||||
4) Copy all the images into this new directory. **No external sources are allowed!** External image sources leak privacy
|
||||
or can break.
|
||||
- Make sure the license is suitable, preferable a Creative Commons license or CC0-license.
|
||||
- If an SVG version is available, use the SVG version
|
||||
- Make sure all the links in `yourtheme.json` are updated. You can use `./assets/themes/yourtheme/yourimage.svg` instead of the HTML link
|
||||
- Make sure all the links in `yourtheme.json` are updated. You can use `./assets/themes/yourtheme/yourimage.svg`
|
||||
instead of the HTML link
|
||||
- Create a file `license_info.json` in the theme directory, which contains metadata on every artwork source
|
||||
5) Add your theme to the code base: add it into "assets/themes" and make sure all the images are there too. Running 'ts-node scripts/fixTheme <path to your theme>' will help downloading the images and attempts to get the licenses if on wikimedia.
|
||||
6) Add some finishing touches, such as a social image. See [this blog post](https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit) for some hints
|
||||
7) Test your theme: run the project as described in [development_deployment](Development_deployment.md)
|
||||
8) Happy with your theme? Time to open a Pull Request!
|
||||
9) Thanks a lot for improving MapComplete!
|
||||
5) Add your theme to the code base: add it into "assets/themes" and make sure all the images are there too. Running '
|
||||
ts-node scripts/fixTheme <path to your theme>' will help downloading the images and attempts to get the licenses if
|
||||
on wikimedia.
|
||||
6) Add some finishing touches, such as a social image.
|
||||
See [this blog post](https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit) for some
|
||||
hints
|
||||
7) Test your theme: run the project as described in [development_deployment](Development_deployment.md)
|
||||
8) Happy with your theme? Time to open a Pull Request!
|
||||
9) Thanks a lot for improving MapComplete!
|
||||
|
||||
|
||||
The .JSON-format
|
||||
----------------
|
||||
The .JSON-format
|
||||
----------------
|
||||
|
||||
There are three important levels in the .JSON-file:
|
||||
|
||||
- The toplevel describes the metadata of the entire theme. It contains the `title`, `description`, `icon`... of the theme. The most important object is `layers`, which is a list of objects describing layers.
|
||||
- A `layer` describes a layer. It contains the `name`, `icon`, `tags of objects to download from overpass`, and especially the `icon` and a way to ask dynamically render tags and ask questions. A lot of those fields (`icon`, `title`, ...) are actually a `TagRendering`
|
||||
- A `TagRendering` is an object describing a relationship between what should be shown on screen and the OSM-tagging. It works in two ways: if the correct tag is known, the appropriate text will be shown. If the tag is missing (and a question is defined), the question will be shown.
|
||||
|
||||
- The toplevel describes the metadata of the entire theme. It contains the `title`, `description`, `icon`... of the
|
||||
theme. The most important object is `layers`, which is a list of objects describing layers.
|
||||
- A `layer` describes a layer. It contains the `name`, `icon`, `tags of objects to download from overpass`, and
|
||||
especially the `icon` and a way to ask dynamically render tags and ask questions. A lot of those fields (`icon`
|
||||
, `title`, ...) are actually a `TagRendering`
|
||||
- A `TagRendering` is an object describing a relationship between what should be shown on screen and the OSM-tagging. It
|
||||
works in two ways: if the correct tag is known, the appropriate text will be shown. If the tag is missing (and a
|
||||
question is defined), the question will be shown.
|
||||
|
||||
Every field is documented in the source code itself - you can find them here:
|
||||
|
||||
|
@ -82,50 +107,65 @@ Every field is documented in the source code itself - you can find them here:
|
|||
|
||||
### MetaTags
|
||||
|
||||
There are few tags available that are calculated for convenience - e.g. the country an object is located at. [An overview of all these metatags is available here](Docs/CalculatedTags.md)
|
||||
There are few tags available that are calculated for convenience - e.g. the country an object is located
|
||||
at. [An overview of all these metatags is available here](Docs/CalculatedTags.md)
|
||||
|
||||
Some hints
|
||||
Some hints
|
||||
------------
|
||||
|
||||
### Everything is HTML
|
||||
|
||||
All the texts are actually *HTML*-snippets, so you can use `<b>` to add bold, or `<img src=...>` to add images to mappings or tagrenderings.
|
||||
All the texts are actually *HTML*-snippets, so you can use `<b>` to add bold, or `<img src=...>` to add images to
|
||||
mappings or tagrenderings.
|
||||
|
||||
Some remarks:
|
||||
|
||||
- links are disabled when answering a question (e.g. a link in a mapping) as it should trigger the answer - not trigger to open the link.
|
||||
- If you include images, e.g. to clarify a type, make sure these are _icons_ or _diagrams_ - not actual pictures! If users see a picture, they think it is a picture of _that actual object_, not a type to clarify the type. An icon is however perceived as something more abstract.
|
||||
- links are disabled when answering a question (e.g. a link in a mapping) as it should trigger the answer - not trigger
|
||||
to open the link.
|
||||
- If you include images, e.g. to clarify a type, make sure these are _icons_ or _diagrams_ - not actual pictures! If
|
||||
users see a picture, they think it is a picture of _that actual object_, not a type to clarify the type. An icon is
|
||||
however perceived as something more abstract.
|
||||
|
||||
Some pitfalls
|
||||
Some pitfalls
|
||||
---------------
|
||||
|
||||
### Not publishing
|
||||
|
||||
Not publishing because 'it is not good enough'. _Share your theme, even if it is still not great, let the community help it improve_
|
||||
Not publishing because 'it is not good enough'. _Share your theme, even if it is still not great, let the community help
|
||||
it improve_
|
||||
|
||||
### Thinking in terms of a question
|
||||
|
||||
Making a tagrendering as if it were a question only. If you have a question such as: _Does this bench have a backrest?_, it is very tempting to have as options _yes_ for `backrest=yes` and _no_ for `backrest=no`. However, when this data is known, it will simply show a lone _yes_ or _no_ which is very unclear.
|
||||
Making a tagrendering as if it were a question only. If you have a question such as: _Does this bench have a backrest?_,
|
||||
it is very tempting to have as options _yes_ for `backrest=yes` and _no_ for `backrest=no`. However, when this data is
|
||||
known, it will simply show a lone _yes_ or _no_ which is very unclear.
|
||||
|
||||
The correct way to handle this is to use _This bench does have a backrest_ and _This bench does not have a backrest_ as answers.
|
||||
The correct way to handle this is to use _This bench does have a backrest_ and _This bench does not have a backrest_ as
|
||||
answers.
|
||||
|
||||
One has to think first in terms of _what is shown to the user if it is known_, only then in terms of _what is the question I want to ask_
|
||||
One has to think first in terms of _what is shown to the user if it is known_, only then in terms of _what is the
|
||||
question I want to ask_
|
||||
|
||||
### Forgetting the casual/noob mapper
|
||||
|
||||
MapComplete is in the first place a tool to help *non-technical* people visualize their interest and contribute to it. In order to maximize contribution:
|
||||
MapComplete is in the first place a tool to help *non-technical* people visualize their interest and contribute to it.
|
||||
In order to maximize contribution:
|
||||
|
||||
1. Use simple language. Avoid difficult words and explain jargon
|
||||
2. Put the simple questions first and the difficult ones on the back. The contributor can then stop at a difficult point and go to the next POI
|
||||
2. Put the simple questions first and the difficult ones on the back. The contributor can then stop at a difficult point
|
||||
and go to the next POI
|
||||
3. Use symbols and images, also in the mappings on questions
|
||||
4. Make sure the icons (on the map and in the questions) are big enough, clear enough and contrast enough with the background map
|
||||
4. Make sure the icons (on the map and in the questions) are big enough, clear enough and contrast enough with the
|
||||
background map
|
||||
|
||||
### Using layers to distinguish on attributes
|
||||
|
||||
One layer should portray one kind of physical object, e.g. "benches" or "restaurants". It should contain all of them, disregarding other properties.
|
||||
One layer should portray one kind of physical object, e.g. "benches" or "restaurants". It should contain all of them,
|
||||
disregarding other properties.
|
||||
|
||||
One should not make one layer for benches with a backrest and one layer for benches without. This is confusing for users and poses problems: what if the backrest status is unknown? What if it is some weird value?
|
||||
Also, it isn't possible to 'move' an attribute to another layer.
|
||||
One should not make one layer for benches with a backrest and one layer for benches without. This is confusing for users
|
||||
and poses problems: what if the backrest status is unknown? What if it is some weird value? Also, it isn't possible to '
|
||||
move' an attribute to another layer.
|
||||
|
||||
Instead, make one layer for one kind of object and change the icon based on attributes.
|
||||
|
||||
|
@ -133,12 +173,19 @@ Instead, make one layer for one kind of object and change the icon based on attr
|
|||
|
||||
Using layers as filters - this doesn't work!
|
||||
|
||||
_All_ data is downloaded in one go and cached locally first. The layer selection (bottom left of the live app) then selects _anything_ that matches the criteria. This match is then passed of to the rendering layer, which selects the layer independently. This means that a feature can show up, even if it's layer is unselected!
|
||||
_All_ data is downloaded in one go and cached locally first. The layer selection (bottom left of the live app) then
|
||||
selects _anything_ that matches the criteria. This match is then passed of to the rendering layer, which selects the
|
||||
layer independently. This means that a feature can show up, even if it's layer is unselected!
|
||||
|
||||
For example, in the [cyclofix-theme](https://mapcomplete.osm.org/cyclofix), there is the layer with _bike-wash_ for do it yourself bikecleaning - points marked with `service:bicycle:cleaning`. However, a bicycle repair shop can offer this service too!
|
||||
For example, in the [cyclofix-theme](https://mapcomplete.osm.org/cyclofix), there is the layer with _bike-wash_ for do
|
||||
it yourself bikecleaning - points marked with `service:bicycle:cleaning`. However, a bicycle repair shop can offer this
|
||||
service too!
|
||||
|
||||
If all the layers are deselected except the bike wash layer, a shop having this tag will still match and will still show up as shop.
|
||||
If all the layers are deselected except the bike wash layer, a shop having this tag will still match and will still show
|
||||
up as shop.
|
||||
|
||||
### Not reading the .JSON-specs
|
||||
|
||||
There are a few advanced features to do fancy stuff available, which are documented only in the spec above - for example, reusing background images and substituting the colours or HTML-rendering. If you need advanced stuff, read it through!
|
||||
There are a few advanced features to do fancy stuff available, which are documented only in the spec above - for
|
||||
example, reusing background images and substituting the colours or HTML-rendering. If you need advanced stuff, read it
|
||||
through!
|
||||
|
|
|
@ -4,15 +4,18 @@
|
|||
|
||||
## StreetComplete
|
||||
|
||||
StreetComplete might look pretty similar to MapComplete at first glance - especially as it was a huge inspiration. However, there are a few huge differences between the two, especially in vision.
|
||||
StreetComplete might look pretty similar to MapComplete at first glance - especially as it was a huge inspiration.
|
||||
However, there are a few huge differences between the two, especially in vision.
|
||||
|
||||
### Vision
|
||||
|
||||
The core philosophy of StreetComplete is **OpenStreetMap is cool! Help to improve it by answering these questions**
|
||||
The core philosophy of MapComplete is **Here is a map of topic XYZ - enjoy it and update it if there is still some info missing**
|
||||
The core philosophy of MapComplete is **Here is a map of topic XYZ - enjoy it and update it if there is still some info
|
||||
missing**
|
||||
|
||||
This means that StreetComplete is mainly aimed towards people who are already OpenStreetMap-enthusiasts, whereas MapComplete is aimed to an audience interested in a certain topic.
|
||||
Of course, the next step is to attempt to inform that audience why having an open map is so cool and that they can contribute as well.
|
||||
This means that StreetComplete is mainly aimed towards people who are already OpenStreetMap-enthusiasts, whereas
|
||||
MapComplete is aimed to an audience interested in a certain topic. Of course, the next step is to attempt to inform that
|
||||
audience why having an open map is so cool and that they can contribute as well.
|
||||
|
||||
### Use cases
|
||||
|
||||
|
@ -29,7 +32,9 @@ MapComplete is made to
|
|||
|
||||
StreetComplete is an android app, so can only be used on Android Phones.
|
||||
|
||||
MapComplete is a web-app, and thus works on all devices. It can be installed as PWA to give an 'app-like'-experience, but can just as well be embedded in other websites. On the other hand PWA are a bit of a second class citizen compared to native apps.
|
||||
MapComplete is a web-app, and thus works on all devices. It can be installed as PWA to give an 'app-like'-experience,
|
||||
but can just as well be embedded in other websites. On the other hand PWA are a bit of a second class citizen compared
|
||||
to native apps.
|
||||
|
||||
### Feature comparison
|
||||
|
||||
|
@ -37,22 +42,26 @@ MapComplete is also an OpenStreetMap-viewer, while StreetComplete hides known va
|
|||
|
||||
MapComplete will not work offline.
|
||||
|
||||
In MapComplete it is easier to add more experimental, extremely detailed and more personal styles, as each topic is separated with its own part.
|
||||
In MapComplete it is easier to add more experimental, extremely detailed and more personal styles, as each topic is
|
||||
separated with its own part.
|
||||
|
||||
MapComplete is a bit more complex to use. One needs to go hunting for a specific map style rather than getting bunch of quests by default. And is likely to ask far more detailed question (email address and phone number of bicycle shop etc).
|
||||
After all if it would duplicate StreetComplete it would be a bit pointless and it is pretty hard to compete with SC by being easier to use and more newbie friendly, while there is space for "more complicated/detailed/involved editor working like SC".
|
||||
MapComplete is a bit more complex to use. One needs to go hunting for a specific map style rather than getting bunch of
|
||||
quests by default. And is likely to ask far more detailed question (email address and phone number of bicycle shop etc).
|
||||
After all if it would duplicate StreetComplete it would be a bit pointless and it is pretty hard to compete with SC by
|
||||
being easier to use and more newbie friendly, while there is space for "more complicated/detailed/involved editor
|
||||
working like SC".
|
||||
|
||||
Also, MapComplete has no requirement to make question easy to answer, making both possible to ask more questions than StreetComplete but making it more complicated to use.
|
||||
Also, MapComplete has no requirement to make question easy to answer, making both possible to ask more questions than
|
||||
StreetComplete but making it more complicated to use.
|
||||
|
||||
No support for splitting ways in MapComplete (as of now - hopefully it'll get added one day).
|
||||
|
||||
MapComplete allows the addition of new points, whereas StreetComplete does not.
|
||||
|
||||
|
||||
|
||||
## MapContrib
|
||||
|
||||
MapContrib is another very similar editor which served as inspiration. MapContrib offers - just like MapComplete - an extensible, thematic map view. However, I never understood the MapContrib user interface.
|
||||
MapContrib is another very similar editor which served as inspiration. MapContrib offers - just like MapComplete - an
|
||||
extensible, thematic map view. However, I never understood the MapContrib user interface.
|
||||
|
||||
MapContrib also allows to add new points and to edit tags - but it is very cumbersome.
|
||||
|
||||
|
|
|
@ -2,18 +2,19 @@
|
|||
|
||||
In this document, you'll find a short overview of which (paid) projects have been completed with MapComplete
|
||||
|
||||
|
||||
## Buurtnatuur
|
||||
|
||||
Commisioned by Groen (the Belgian Green Party): "a project to crowdsource data about parks, nature reserve and forests"
|
||||
|
||||
## Cyclofix
|
||||
|
||||
Cycle-related Points of interest (pumps, shops, drinking water, ...). Commissioned by "Brussels Mobility", executed during OSOC
|
||||
Cycle-related Points of interest (pumps, shops, drinking water, ...). Commissioned by "Brussels Mobility", executed
|
||||
during OSOC
|
||||
|
||||
## Speelplekken in de Antwerpse Zuidrand
|
||||
|
||||
Playgrounds, play forests and village green in the southern part of the Province of Antwerp; commisioned by Province of Antwerp
|
||||
Playgrounds, play forests and village green in the southern part of the Province of Antwerp; commisioned by Province of
|
||||
Antwerp
|
||||
|
||||
## Natuurpunt Map (planned)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
# 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
|
||||
|
||||
|
@ -21,7 +21,8 @@ A geographical direction, in degrees. 0° is north, 90° is east, ... Will retur
|
|||
|
||||
## length
|
||||
|
||||
A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]
|
||||
A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool.
|
||||
Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]
|
||||
|
||||
## wikidata
|
||||
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
|
||||
### 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 not_fcs 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
|
||||
In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and
|
||||
visualizations to be reused by custom themes or even to query third-party API's. General usage is `{func_name()}`
|
||||
, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_fcs 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
|
||||
|
||||
Prints all key-value pairs of the object - used for debugging
|
||||
Prints all key-value pairs of the object - used for debugging
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
||||
|
||||
#### Example usage
|
||||
|
||||
{all_tags()}
|
||||
{all_tags()}
|
||||
|
||||
### image_carousel
|
||||
|
||||
Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
|
||||
Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported:
|
||||
Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -24,10 +28,11 @@ smart search | true | Also include images given via 'Wikidata', 'wikimedia_commo
|
|||
|
||||
#### Example usage
|
||||
|
||||
{image_carousel(image,true)}
|
||||
{image_carousel(image,true)}
|
||||
|
||||
### 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
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -35,10 +40,11 @@ image-key | image | Image tag to add the URL to (or image-tag:0, image-tag:1 whe
|
|||
|
||||
#### Example usage
|
||||
|
||||
{image_upload(image)}
|
||||
{image_upload(image)}
|
||||
|
||||
### minimap
|
||||
|
||||
A small map showing the selected feature. Note that no styling is applied, wrap this in a div
|
||||
A small map showing the selected feature. Note that no styling is applied, wrap this in a div
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -47,10 +53,13 @@ idKey | id | (Matches all resting arguments) This argument should be the key of
|
|||
|
||||
#### Example usage
|
||||
|
||||
`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`
|
||||
`{minimap()}`
|
||||
, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`
|
||||
|
||||
### 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
|
||||
------ | --------- | -------------
|
||||
|
@ -59,10 +68,13 @@ fallback | undefined | The identifier to use, if <i>tags[subjectKey]</i> as spec
|
|||
|
||||
#### Example usage
|
||||
|
||||
<b>{reviews()}<b> for a vanilla review, <b>{reviews(name, play_forest)}</b> to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used
|
||||
<b>{reviews()}<b> for a vanilla review, <b>{reviews(name, play_forest)}</b> to review a play forest. If a name is known,
|
||||
the name will be used as identifier, otherwise 'play_forest' is used
|
||||
|
||||
### opening_hours_table
|
||||
|
||||
Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.
|
||||
Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag '
|
||||
opening_hours'.
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -70,10 +82,14 @@ key | opening_hours | The tagkey from which the table is constructed.
|
|||
|
||||
#### Example usage
|
||||
|
||||
{opening_hours_table(opening_hours)}
|
||||
{opening_hours_table(opening_hours)}
|
||||
|
||||
### 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
|
||||
------ | --------- | -------------
|
||||
|
@ -83,10 +99,12 @@ path | undefined | The path (or shorthand) that should be returned
|
|||
|
||||
#### Example usage
|
||||
|
||||
{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}
|
||||
{live({url},{url:format},hour)}
|
||||
{live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}
|
||||
|
||||
### histogram
|
||||
|
||||
Create a histogram for a list of given values, read from the properties.
|
||||
Create a histogram for a list of given values, read from the properties.
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -97,10 +115,11 @@ colors* | undefined | (Matches all resting arguments - optional) Matches a regex
|
|||
|
||||
#### 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
|
||||
|
||||
Creates a link that (attempts to) open the native 'share'-screen
|
||||
Creates a link that (attempts to) open the native 'share'-screen
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -108,10 +127,11 @@ url | undefined | The url to share (default: current URL)
|
|||
|
||||
#### Example usage
|
||||
|
||||
{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
|
||||
|
||||
Converts a short, canonical value into the long, translated text
|
||||
Converts a short, canonical value into the long, translated text
|
||||
|
||||
name | default | description
|
||||
------ | --------- | -------------
|
||||
|
@ -119,4 +139,4 @@ key | undefined | The key of the tag to give the canonical text for
|
|||
|
||||
#### Example usage
|
||||
|
||||
{canonical(length)} will give 42 metre (in french) Generated from UI/SpecialVisualisations.ts
|
||||
{canonical(length)} will give 42 metre (in french) Generated from UI/SpecialVisualisations.ts
|
|
@ -1,7 +1,8 @@
|
|||
Statistics
|
||||
==========
|
||||
|
||||
There are some fancy statistics available about MapComplete use. The most important once are listed below, some more graphs (and the scripts to generate them) are [in the tools directory](Tools/)
|
||||
There are some fancy statistics available about MapComplete use. The most important once are listed below, some more
|
||||
graphs (and the scripts to generate them) are [in the tools directory](Tools/)
|
||||
|
||||
All Time usage
|
||||
--------------
|
||||
|
@ -9,7 +10,8 @@ All Time usage
|
|||
![](Tools/CumulativeContributors.png)
|
||||
![](Tools/Cumulative%20changesets%20per%20contributor.png)
|
||||
|
||||
Note: in 2020, MapComplete would still make one changeset per answered question. This heavily skews the below graphs towards `buurtnatuur` and `cyclofìx`, two heavily used themes at the beginning.
|
||||
Note: in 2020, MapComplete would still make one changeset per answered question. This heavily skews the below graphs
|
||||
towards `buurtnatuur` and `cyclofìx`, two heavily used themes at the beginning.
|
||||
|
||||
![](Tools/Cumulative%20changesets%20per%20theme.png)
|
||||
![](Tools/Theme%20distribution.png)
|
||||
|
|
|
@ -1,48 +1,59 @@
|
|||
Tags format
|
||||
Tags format
|
||||
=============
|
||||
|
||||
When creating the `json` file describing your layer or theme, you'll have to add a few tags to describe what you want. This document gives an overview of what every expression means and how it behaves in edge cases.
|
||||
When creating the `json` file describing your layer or theme, you'll have to add a few tags to describe what you want.
|
||||
This document gives an overview of what every expression means and how it behaves in edge cases.
|
||||
|
||||
Strict equality
|
||||
---------------
|
||||
|
||||
Strict equality is denoted by `key=value`. This key matches __only if__ the keypair is present exactly as stated.
|
||||
|
||||
**Only normal tags (eventually in an `and`) can be used in places where they are uploaded**. Normal tags are used in the `mappings` of a [TagRendering] (unless `hideInAnswer` is specified), they are used in `addExtraTags` of [Freeform] and are used in the `tags`-list of a preset.
|
||||
**Only normal tags (eventually in an `and`) can be used in places where they are uploaded**. Normal tags are used in
|
||||
the `mappings` of a [TagRendering] (unless `hideInAnswer` is specified), they are used in `addExtraTags` of [Freeform]
|
||||
and are used in the `tags`-list of a preset.
|
||||
|
||||
If a different kind of tag specification is given, your theme will fail to parse.
|
||||
|
||||
### If key is not present
|
||||
|
||||
If you want to check if a key is not present, use `key=` (pronounce as *key is empty*). A tag collection will match this if `key` is missing or if `key` is a literal empty value.
|
||||
If you want to check if a key is not present, use `key=` (pronounce as *key is empty*). A tag collection will match this
|
||||
if `key` is missing or if `key` is a literal empty value.
|
||||
|
||||
### Removing a key
|
||||
|
||||
If a key should be deleted in the OpenStreetMap-database, specify `key=` as well. This can be used e.g. to remove a fixme or value from another mapping if another field is filled out.
|
||||
If a key should be deleted in the OpenStreetMap-database, specify `key=` as well. This can be used e.g. to remove a
|
||||
fixme or value from another mapping if another field is filled out.
|
||||
|
||||
Strict not equals
|
||||
-----------------
|
||||
|
||||
To check if a key does _not_ equal a certain value, use `key!=value`. This is converted behind the scenes to `key!~^value$`
|
||||
To check if a key does _not_ equal a certain value, use `key!=value`. This is converted behind the scenes
|
||||
to `key!~^value$`
|
||||
|
||||
### If key is present
|
||||
|
||||
This implies that, to check if a key is present, `key!=` can be used. This will only match if the key is present and not empty.
|
||||
This implies that, to check if a key is present, `key!=` can be used. This will only match if the key is present and not
|
||||
empty.
|
||||
|
||||
Number comparison
|
||||
-----------------
|
||||
|
||||
If the value of a tag is a number (e.g. `key=42`), one can use a filter `key<=42`, `key>=35`, `key>40` or `key<50` to match this, e.g. in conditions for renderings.
|
||||
These tags cannot be used to generate an answer nor can they be used to request data upstream from overpass.
|
||||
If the value of a tag is a number (e.g. `key=42`), one can use a filter `key<=42`, `key>=35`, `key>40` or `key<50` to
|
||||
match this, e.g. in conditions for renderings. These tags cannot be used to generate an answer nor can they be used to
|
||||
request data upstream from overpass.
|
||||
|
||||
Note that the value coming from OSM will first be stripped by removing all non-numeric characters. For example, `length=42 meter` will be interpreted as `length=42` and will thus match `length<=42` and `length>=42`.
|
||||
In special circumstances (e.g. `surface_area=42 m2` or `length=100 feet`), this will result in erronous values (`surface=422` or if a length in meters is compared to).
|
||||
However, this can be partially alleviated by using 'Units' to rewrite to a default format.
|
||||
Note that the value coming from OSM will first be stripped by removing all non-numeric characters. For
|
||||
example, `length=42 meter` will be interpreted as `length=42` and will thus match `length<=42` and `length>=42`. In
|
||||
special circumstances (e.g. `surface_area=42 m2` or `length=100 feet`), this will result in erronous
|
||||
values (`surface=422` or if a length in meters is compared to). However, this can be partially alleviated by using '
|
||||
Units' to rewrite to a default format.
|
||||
|
||||
Regex equals
|
||||
------------
|
||||
|
||||
A tag can also be tested against a regex with `key~regex`. Note that this regex __must match__ the entire value. If the value is allowed to appear anywhere as substring, use `key~.*regex.*`
|
||||
A tag can also be tested against a regex with `key~regex`. Note that this regex __must match__ the entire value. If the
|
||||
value is allowed to appear anywhere as substring, use `key~.*regex.*`
|
||||
|
||||
Equivalently, `key!~regex` can be used if you _don't_ want to match the regex in order to appear.
|
||||
|
||||
|
@ -52,16 +63,18 @@ Using other tags as variables
|
|||
|
||||
**This is an advanced feature - use with caution**
|
||||
|
||||
Some tags are automatically set or calculated - see [CalculatedTags](CalculatedTags.md) for an entire overview.
|
||||
If one wants to apply such a value as tag, use a substituting-tag such, for example`survey:date:={_date:now}`. Note that the separator between key and value here is `:=`.
|
||||
The text between `{` and `}` is interpreted as a key, and the respective value is substituted into the string.
|
||||
Some tags are automatically set or calculated - see [CalculatedTags](CalculatedTags.md) for an entire overview. If one
|
||||
wants to apply such a value as tag, use a substituting-tag such, for example`survey:date:={_date:now}`. Note that the
|
||||
separator between key and value here is `:=`. The text between `{` and `}` is interpreted as a key, and the respective
|
||||
value is substituted into the string.
|
||||
|
||||
One can also append, e.g. `key:={some_key} fixed text {some_other_key}`.
|
||||
|
||||
An assigning tag _cannot_ be used to query OpenStreetMap/Overpass.
|
||||
|
||||
If using a key or variable which might not be defined, add a condition in the mapping to hide the option.
|
||||
This is because, if `some_other_key` is not defined, one might actually upload the literal text `key={some_other_key}` to OSM - which we do not want.
|
||||
If using a key or variable which might not be defined, add a condition in the mapping to hide the option. This is
|
||||
because, if `some_other_key` is not defined, one might actually upload the literal text `key={some_other_key}` to OSM -
|
||||
which we do not want.
|
||||
|
||||
To mitigate this, use:
|
||||
|
||||
|
|
|
@ -1,26 +1,23 @@
|
|||
Talk at State of the Map
|
||||
========================
|
||||
|
||||
Talk at State of the Map
|
||||
========================
|
||||
I'm planning to do a talk/video on MapComplete on _State of the Map 2021_. This document outlines the structure of the
|
||||
talk and serves as my speaker notes.
|
||||
|
||||
I'm planning to do a talk/video on MapComplete on _State of the Map 2021_. This document outlines the structure of the talk and serves as my speaker notes.
|
||||
Overview
|
||||
--------
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
1. What is MapComplete? Three slides
|
||||
2. Already one year old!
|
||||
1. What is MapComplete? Three slides
|
||||
2. Already one year old!
|
||||
a. Where and how did it start? "This project isn't going anywhere"
|
||||
(buurtnatuur, cyclofix, Antwerpen, Natuurpunt, Oost-Vlaanderen?, Toerisme Vlaanderen?)
|
||||
b. Where are we now? Some cool statistics
|
||||
3. The vision
|
||||
a. Your granny can use it
|
||||
a. pragmatism - somewhat working today is better then something perfect tomorrow
|
||||
a. It's a trap! Cool thematic map (as iframe on website) -> the OSM rabbit hole -> easy start -> more features when mapping more -> oh, I can make a theme too!
|
||||
b. The pareto frontier of 'easy to use' vs 'number of features'
|
||||
c. The thematic approach (+ disabling/enabling features per theme/view)
|
||||
3. The vision a. Your granny can use it a. pragmatism - somewhat working today is better then something perfect tomorrow
|
||||
a. It's a trap! Cool thematic map (as iframe on website) -> the OSM rabbit hole -> easy start -> more features when
|
||||
mapping more -> oh, I can make a theme too!
|
||||
b. The pareto frontier of 'easy to use' vs 'number of features' c. The thematic approach (+ disabling/enabling
|
||||
features per theme/view)
|
||||
|
||||
|
||||
4. The future
|
||||
a. paid projects? Microgrant for a better theme creator?
|
||||
b. user contributed themes (see you next talk!)
|
||||
5. OpenStreetMap is Cool!
|
||||
4. The future a. paid projects? Microgrant for a better theme creator? b. user contributed themes (see you next talk!)
|
||||
5. OpenStreetMap is Cool!
|
||||
|
|
|
@ -216,12 +216,6 @@ function createGraph(
|
|||
}
|
||||
|
||||
class Histogram<K> {
|
||||
total(): number {
|
||||
let total = 0
|
||||
Array.from(this.counts.values()).forEach(i => total = total + i)
|
||||
return total
|
||||
}
|
||||
|
||||
public counts: Map<K, number> = new Map<K, number>()
|
||||
private sortAtEnd: K[] = []
|
||||
|
||||
|
@ -230,6 +224,11 @@ class Histogram<K> {
|
|||
keys?.forEach(key => self.bump(key))
|
||||
}
|
||||
|
||||
total(): number {
|
||||
let total = 0
|
||||
Array.from(this.counts.values()).forEach(i => total = total + i)
|
||||
return total
|
||||
}
|
||||
|
||||
public bump(key: K, increase = 1) {
|
||||
if (this.counts.has(key)) {
|
||||
|
@ -618,7 +617,7 @@ const geojson = {
|
|||
})
|
||||
}
|
||||
|
||||
writeFileSync("centerpoints.geojson",JSON.stringify(geojson, undefined, 2) )
|
||||
writeFileSync("centerpoints.geojson", JSON.stringify(geojson, undefined, 2))
|
||||
|
||||
|
||||
createGraphs(allFeatures, "")
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
URL-parameters and URL-hash
|
||||
============================
|
||||
|
||||
|
@ -9,8 +8,8 @@ What is a URL parameter?
|
|||
|
||||
URL-parameters are extra parts of the URL used to set the state.
|
||||
|
||||
For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`,
|
||||
the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely:
|
||||
For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`, the
|
||||
URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely:
|
||||
|
||||
- The url-parameter `lat` is `51.0` in this instance
|
||||
- The url-parameter `lon` is `4.3` in this instance
|
||||
|
@ -19,141 +18,147 @@ the URL-parameters are stated in the part between the `?` and the `#`. There are
|
|||
|
||||
Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
|
||||
|
||||
language
|
||||
language
|
||||
----------
|
||||
|
||||
The language to display mapcomplete in. Will be ignored in case a logged-in-user did set their language before. If the specified language does not exist, it will default to the first language in the theme.
|
||||
The language to display mapcomplete in. Will be ignored in case a logged-in-user did set their language before. If the
|
||||
specified language does not exist, it will default to the first language in the theme.
|
||||
|
||||
download-control-toggle
|
||||
download-control-toggle
|
||||
-------------------------
|
||||
|
||||
Whether or not the download panel is shown The default value is _false_
|
||||
Whether or not the download panel is shown The default value is _false_
|
||||
|
||||
|
||||
filter-toggle
|
||||
filter-toggle
|
||||
---------------
|
||||
|
||||
Whether or not the filter view is shown The default value is _false_
|
||||
Whether or not the filter view is shown The default value is _false_
|
||||
|
||||
|
||||
tab
|
||||
tab
|
||||
-----
|
||||
|
||||
The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_
|
||||
The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 =
|
||||
more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_
|
||||
|
||||
|
||||
z
|
||||
z
|
||||
---
|
||||
|
||||
The initial/current zoom level The default value is _0_
|
||||
The initial/current zoom level The default value is _0_
|
||||
|
||||
|
||||
lat
|
||||
lat
|
||||
-----
|
||||
|
||||
The initial/current latitude The default value is _0_
|
||||
The initial/current latitude The default value is _0_
|
||||
|
||||
|
||||
lon
|
||||
lon
|
||||
-----
|
||||
|
||||
The initial/current longitude of the app The default value is _0_
|
||||
The initial/current longitude of the app The default value is _0_
|
||||
|
||||
|
||||
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
|
||||
-----------
|
||||
|
||||
Disables/Enables the search bar The default value is _true_
|
||||
Disables/Enables the search bar The default value is _true_
|
||||
|
||||
|
||||
fs-background
|
||||
fs-background
|
||||
---------------
|
||||
|
||||
Disables/Enables the background layer control The default value is _true_
|
||||
Disables/Enables the background layer control The default value is _true_
|
||||
|
||||
|
||||
fs-filter
|
||||
fs-filter
|
||||
-----------
|
||||
|
||||
Disables/Enables the filter The default value is _true_
|
||||
Disables/Enables the filter The default value is _true_
|
||||
|
||||
|
||||
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
|
||||
--------------------
|
||||
|
||||
Disables/enables the help menu or welcome message The default value is _true_
|
||||
Disables/enables the help menu or welcome message The default value is _true_
|
||||
|
||||
|
||||
fs-iframe
|
||||
fs-iframe
|
||||
-----------
|
||||
|
||||
Disables/Enables the iframe-popup The default value is _false_
|
||||
Disables/Enables the iframe-popup The default value is _false_
|
||||
|
||||
|
||||
fs-more-quests
|
||||
fs-more-quests
|
||||
----------------
|
||||
|
||||
Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_
|
||||
Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_
|
||||
|
||||
|
||||
fs-share-screen
|
||||
fs-share-screen
|
||||
-----------------
|
||||
|
||||
Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_
|
||||
Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_
|
||||
|
||||
|
||||
fs-geolocation
|
||||
fs-geolocation
|
||||
----------------
|
||||
|
||||
Disables/Enables the geolocation button The default value is _true_
|
||||
Disables/Enables the geolocation button The default value is _true_
|
||||
|
||||
|
||||
fs-all-questions
|
||||
fs-all-questions
|
||||
------------------
|
||||
|
||||
Always show all questions The default value is _false_
|
||||
Always show all questions The default value is _false_
|
||||
|
||||
|
||||
fs-export
|
||||
fs-export
|
||||
-----------
|
||||
|
||||
Enable the export as GeoJSON and CSV button The default value is _false_
|
||||
Enable the export as GeoJSON and CSV button The default value is _false_
|
||||
|
||||
|
||||
fs-pdf
|
||||
fs-pdf
|
||||
--------
|
||||
|
||||
Enable the PDF download button The default value is _false_
|
||||
Enable the PDF download button The default value is _false_
|
||||
|
||||
|
||||
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
|
||||
-------
|
||||
|
||||
If true, shows some extra debugging help such as all the available tags on every object The default value is _false_
|
||||
If true, shows some extra debugging help such as all the available tags on every object The default value is _false_
|
||||
|
||||
|
||||
overpassUrl
|
||||
-------------
|
||||
|
||||
Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter The default value is _https://overpass.kumi.de/api/interpreter_
|
||||
Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter The default value
|
||||
is _https://overpass.kumi.de/api/interpreter_
|
||||
|
||||
|
||||
overpassTimeout
|
||||
|
@ -161,31 +166,32 @@ overpassTimeout
|
|||
|
||||
Set a different timeout (in seconds) for queries in overpass The default value is _60_
|
||||
|
||||
fake-user
|
||||
fake-user
|
||||
-----------
|
||||
|
||||
If true, 'dryrun' mode is activated and a fake user account is loaded The default value is _false_
|
||||
If true, 'dryrun' mode is activated and a fake user account is loaded The default value is _false_
|
||||
|
||||
|
||||
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_
|
||||
|
||||
|
||||
custom-css
|
||||
custom-css
|
||||
------------
|
||||
|
||||
If specified, the custom css from the given link will be loaded additionaly The default value is __
|
||||
If specified, the custom css from the given link will be loaded additionaly The default value is __
|
||||
|
||||
|
||||
background
|
||||
background
|
||||
------------
|
||||
|
||||
The id of the background layer to start with The default value is _osm_
|
||||
The id of the background layer to start with The default value is _osm_
|
||||
|
||||
|
||||
layer-<layer-id>
|
||||
layer-<layer-id>
|
||||
------------------
|
||||
|
||||
Wether or not the layer with id <layer-id> is shown The default value is _true_ Generated from QueryParameters
|
||||
Wether or not the layer with id <layer-id> is shown The default value is _true_ Generated from QueryParameters
|
|
@ -3,6 +3,8 @@ What is MapComplete?
|
|||
|
||||
MapComplete is a website where people can see a map of Points of Interest (POI) on a specific subject or theme.
|
||||
|
||||
If the visitor is logged in with their OpenStreetMap account, information about a selected POI can be enriched by answering questions in the respective popups; this data is sent back directly to OpenStreetMap.
|
||||
If the visitor is logged in with their OpenStreetMap account, information about a selected POI can be enriched by
|
||||
answering questions in the respective popups; this data is sent back directly to OpenStreetMap.
|
||||
|
||||
While there are some predefined themes available, it is also possible to create your own theme. Have a look on [the OpenStreetMap-wiki](https://wiki.openstreetmap.org/wiki/MapComplete) for a list of (un)official themes.
|
||||
While there are some predefined themes available, it is also possible to create your own theme. Have a look
|
||||
on [the OpenStreetMap-wiki](https://wiki.openstreetmap.org/wiki/MapComplete) for a list of (un)official themes.
|
|
@ -4,7 +4,6 @@ import Svg from "../../Svg";
|
|||
import Img from "../../UI/Base/Img";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
|
||||
|
@ -161,7 +160,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
} else {
|
||||
lastClick.setData(new Date())
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
lastClick.setData(new Date())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,10 @@ export default class InstalledThemes {
|
|||
}
|
||||
try {
|
||||
let layoutJson;
|
||||
try{
|
||||
try {
|
||||
layoutJson = JSON.parse(atob(customLayout.data))
|
||||
}catch(e){
|
||||
layoutJson = JSON.parse( Utils.UnMinify(LZString.decompressFromBase64(customLayout.data)))
|
||||
} catch (e) {
|
||||
layoutJson = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(customLayout.data)))
|
||||
}
|
||||
const layout = new LayoutConfig(layoutJson, false);
|
||||
installedThemes.push({
|
||||
|
|
|
@ -8,7 +8,7 @@ import Loc from "../../Models/Loc";
|
|||
*/
|
||||
export default class LayerResetter {
|
||||
|
||||
constructor( currentBackgroundLayer: UIEventSource<BaseLayer>,
|
||||
constructor(currentBackgroundLayer: UIEventSource<BaseLayer>,
|
||||
location: UIEventSource<Loc>,
|
||||
availableLayers: UIEventSource<BaseLayer[]>,
|
||||
defaultLayerId: UIEventSource<string> = undefined) {
|
||||
|
@ -28,7 +28,7 @@ export default class LayerResetter {
|
|||
if (availableLayer.min_zoom > location.data.zoom) {
|
||||
break;
|
||||
}
|
||||
if(availableLayer.id === defaultLayerId.data){
|
||||
if (availableLayer.id === defaultLayerId.data) {
|
||||
defaultLayer = availableLayer;
|
||||
}
|
||||
return; // All good - the current layer still works!
|
||||
|
|
|
@ -147,7 +147,7 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
}
|
||||
|
||||
const bounds = this._leafletMap.data?.getBounds();
|
||||
if(bounds === undefined){
|
||||
if (bounds === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ export default class PendingChangesUploader {
|
|||
|
||||
|
||||
function onunload(e) {
|
||||
if(changes.pendingChanges.data.length == 0){
|
||||
if (changes.pendingChanges.data.length == 0) {
|
||||
return;
|
||||
}
|
||||
changes.flushChanges("onbeforeunload - probably closing or something similar");
|
||||
|
|
|
@ -3,6 +3,7 @@ Actors
|
|||
|
||||
An **actor** is a module which converts one UIEventSource into another while performing logic.
|
||||
|
||||
Typically, it will only expose the constructor taking some UIEventSources (and configuration) and a few fields which are UIEVentSources.
|
||||
Typically, it will only expose the constructor taking some UIEventSources (and configuration) and a few fields which are
|
||||
UIEVentSources.
|
||||
|
||||
An actor should _never_ have a dependency on 'State' and should _never_ import it
|
|
@ -9,11 +9,10 @@ import OsmApiFeatureSource from "../FeatureSource/OsmApiFeatureSource";
|
|||
* Makes sure the hash shows the selected element and vice-versa.
|
||||
*/
|
||||
export default class SelectedFeatureHandler {
|
||||
private static readonly _no_trigger_on = ["welcome", "copyright", "layers", "new"]
|
||||
private readonly _featureSource: FeatureSource;
|
||||
private readonly _hash: UIEventSource<string>;
|
||||
private readonly _selectedFeature: UIEventSource<any>;
|
||||
|
||||
private static readonly _no_trigger_on = ["welcome", "copyright", "layers", "new"]
|
||||
private readonly _osmApiSource: OsmApiFeatureSource;
|
||||
|
||||
constructor(hash: UIEventSource<string>,
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import * as L from "leaflet";
|
||||
import Svg from "../../Svg";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import Img from "../../UI/Base/Img";
|
||||
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen";
|
||||
import AddNewMarker from "../../UI/BigComponents/AddNewMarker";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
|
@ -37,12 +35,12 @@ export default class StrayClickHandler {
|
|||
leafletMap.data?.removeLayer(self._lastMarker);
|
||||
}
|
||||
|
||||
if(lastClick === undefined){
|
||||
if (lastClick === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedElement.setData(undefined);
|
||||
const clickCoor : [number, number] = [lastClick.lat, lastClick.lon]
|
||||
const clickCoor: [number, number] = [lastClick.lat, lastClick.lon]
|
||||
self._lastMarker = L.marker(clickCoor, {
|
||||
icon: L.divIcon({
|
||||
html: new AddNewMarker(filteredLayers).ConstructElement(),
|
||||
|
@ -53,7 +51,7 @@ export default class StrayClickHandler {
|
|||
});
|
||||
const popup = L.popup({
|
||||
autoPan: true,
|
||||
autoPanPaddingTopLeft: [15,15],
|
||||
autoPanPaddingTopLeft: [15, 15],
|
||||
closeOnEscapeKey: true,
|
||||
autoClose: true
|
||||
}).setContent("<div id='strayclick' style='height: 65vh'></div>");
|
||||
|
@ -61,7 +59,7 @@ export default class StrayClickHandler {
|
|||
self._lastMarker.bindPopup(popup);
|
||||
|
||||
self._lastMarker.on("click", () => {
|
||||
if(leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints){
|
||||
if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) {
|
||||
self._lastMarker.closePopup()
|
||||
leafletMap.data.flyTo(clickCoor, Constants.userJourney.minZoomLevelToAddNewPoints)
|
||||
return;
|
||||
|
|
|
@ -24,9 +24,9 @@ class TitleElement extends UIEventSource<string> {
|
|||
this.syncWith(
|
||||
this._selectedFeature.map(
|
||||
selected => {
|
||||
const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ??"MapComplete"
|
||||
const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ?? "MapComplete"
|
||||
|
||||
if(selected === undefined){
|
||||
if (selected === undefined) {
|
||||
return defaultTitle
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,6 @@ class TitleElement extends UIEventSource<string> {
|
|||
}
|
||||
, [Locale.language, layoutToUse]
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ import {UIEventSource} from "./UIEventSource";
|
|||
|
||||
export class ElementStorage {
|
||||
|
||||
private _elements = new Map<string, UIEventSource<any>>();
|
||||
public ContainingFeatures = new Map<string, any>();
|
||||
private _elements = new Map<string, UIEventSource<any>>();
|
||||
|
||||
constructor() {
|
||||
|
||||
|
@ -31,7 +31,7 @@ export class ElementStorage {
|
|||
// At last, we overwrite the tag of the new feature to use the tags in the already existing event source
|
||||
feature.properties = es.data
|
||||
|
||||
if(!this.ContainingFeatures.has(elementId)){
|
||||
if (!this.ContainingFeatures.has(elementId)) {
|
||||
this.ContainingFeatures.set(elementId, feature);
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ export class ElementStorage {
|
|||
const debug_msg = []
|
||||
let somethingChanged = false;
|
||||
for (const k in newProperties) {
|
||||
if(!newProperties.hasOwnProperty(k)){
|
||||
if (!newProperties.hasOwnProperty(k)) {
|
||||
continue;
|
||||
}
|
||||
const v = newProperties[k];
|
||||
|
|
|
@ -14,10 +14,10 @@ export default class FeatureDuplicatorPerLayer implements FeatureSource {
|
|||
public readonly name;
|
||||
|
||||
constructor(layers: UIEventSource<FilteredLayer[]>, upstream: FeatureSource) {
|
||||
this.name = "FeatureDuplicator of "+upstream.name;
|
||||
this.name = "FeatureDuplicator of " + upstream.name;
|
||||
this.features = upstream.features.map(features => {
|
||||
const newFeatures: { feature: any, freshness: Date }[] = [];
|
||||
if(features === undefined){
|
||||
if (features === undefined) {
|
||||
return newFeatures;
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ export default class FeatureDuplicatorPerLayer implements FeatureSource {
|
|||
id: f.feature.id,
|
||||
type: f.feature.type,
|
||||
properties: f.feature.properties,
|
||||
_matching_layer_id : layer.layerDef.id
|
||||
_matching_layer_id: layer.layerDef.id
|
||||
}
|
||||
newFeatures.push({feature: newFeature, freshness: f.freshness});
|
||||
} else {
|
||||
|
|
|
@ -58,7 +58,7 @@ export default class FeatureSourceMerger implements FeatureSource {
|
|||
}
|
||||
}
|
||||
|
||||
if(!somethingChanged){
|
||||
if (!somethingChanged) {
|
||||
// We don't bother triggering an update
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -13,10 +13,10 @@ export default class GeoJsonSource implements FeatureSource {
|
|||
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
public readonly name;
|
||||
public readonly isOsmCache: boolean
|
||||
private onFail: ((errorMsg: any, url: string) => void) = undefined;
|
||||
private readonly layerId: string;
|
||||
private readonly seenids: Set<string> = new Set<string>()
|
||||
public readonly isOsmCache: boolean
|
||||
|
||||
private constructor(locationControl: UIEventSource<Loc>,
|
||||
flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig },
|
||||
|
|
|
@ -18,17 +18,17 @@ export default class LocalStorageSaver implements FeatureSource {
|
|||
|
||||
this.features.addCallbackAndRunD(features => {
|
||||
const now = new Date().getTime()
|
||||
features = features.filter(f => layout.data.cacheTimeout > Math.abs(now - f.freshness.getTime())/1000)
|
||||
features = features.filter(f => layout.data.cacheTimeout > Math.abs(now - f.freshness.getTime()) / 1000)
|
||||
|
||||
|
||||
if(features.length == 0){
|
||||
if (features.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const key = LocalStorageSaver.storageKey+layout.data.id
|
||||
const key = LocalStorageSaver.storageKey + layout.data.id
|
||||
localStorage.setItem(key, JSON.stringify(features));
|
||||
console.log("Saved ",features.length, "elements to",key)
|
||||
console.log("Saved ", features.length, "elements to", key)
|
||||
} catch (e) {
|
||||
console.warn("Could not save the features to local storage:", e)
|
||||
}
|
||||
|
|
|
@ -16,12 +16,12 @@ export default class LocalStorageSource implements FeatureSource {
|
|||
if (fromStorage == null) {
|
||||
return;
|
||||
}
|
||||
const loaded : { feature: any; freshness: Date | string }[]=
|
||||
const loaded: { feature: any; freshness: Date | string }[] =
|
||||
JSON.parse(fromStorage);
|
||||
|
||||
const parsed : { feature: any; freshness: Date }[]= loaded.map(ff => ({
|
||||
const parsed: { feature: any; freshness: Date }[] = loaded.map(ff => ({
|
||||
feature: ff.feature,
|
||||
freshness : typeof ff.freshness == "string" ? new Date(ff.freshness) : ff.freshness
|
||||
freshness: typeof ff.freshness == "string" ? new Date(ff.freshness) : ff.freshness
|
||||
}))
|
||||
|
||||
this.features.setData(parsed);
|
||||
|
|
|
@ -19,7 +19,7 @@ export default class MetaTaggingFeatureSource implements FeatureSource {
|
|||
const self = this;
|
||||
this.name = "MetaTagging of " + source.name
|
||||
|
||||
if(allFeaturesSource === undefined){
|
||||
if (allFeaturesSource === undefined) {
|
||||
throw ("UIEVentSource is undefined")
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export default class RememberingSource implements FeatureSource {
|
|||
|
||||
constructor(source: FeatureSource) {
|
||||
const self = this;
|
||||
this.name = "RememberingSource of "+source.name;
|
||||
this.name = "RememberingSource of " + source.name;
|
||||
const empty = [];
|
||||
this.features = source.features.map(features => {
|
||||
const oldFeatures = self.features?.data ?? empty;
|
||||
|
|
|
@ -185,6 +185,60 @@ export class GeoOperations {
|
|||
return turf.length(feature) * 1000
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the closest point on a way from a given point
|
||||
* @param way The road on which you want to find a point
|
||||
* @param point Point defined as [lon, lat]
|
||||
*/
|
||||
public static nearestPoint(way, point: [number, number]) {
|
||||
return turf.nearestPointOnLine(way, point, {units: "kilometers"});
|
||||
}
|
||||
|
||||
public static toCSV(features: any[]): string {
|
||||
|
||||
const headerValuesSeen = new Set<string>();
|
||||
const headerValuesOrdered: string[] = []
|
||||
|
||||
function addH(key) {
|
||||
if (!headerValuesSeen.has(key)) {
|
||||
headerValuesSeen.add(key)
|
||||
headerValuesOrdered.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
addH("_lat")
|
||||
addH("_lon")
|
||||
|
||||
const lines: string[] = []
|
||||
|
||||
for (const feature of features) {
|
||||
const properties = feature.properties;
|
||||
for (const key in properties) {
|
||||
if (!properties.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
addH(key)
|
||||
|
||||
}
|
||||
}
|
||||
headerValuesOrdered.sort()
|
||||
for (const feature of features) {
|
||||
const properties = feature.properties;
|
||||
let line = ""
|
||||
for (const key of headerValuesOrdered) {
|
||||
const value = properties[key]
|
||||
if (value === undefined) {
|
||||
line += ","
|
||||
} else {
|
||||
line += JSON.stringify(value) + ","
|
||||
}
|
||||
}
|
||||
lines.push(line)
|
||||
}
|
||||
|
||||
return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -277,60 +331,6 @@ export class GeoOperations {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the closest point on a way from a given point
|
||||
* @param way The road on which you want to find a point
|
||||
* @param point Point defined as [lon, lat]
|
||||
*/
|
||||
public static nearestPoint(way, point: [number, number]) {
|
||||
return turf.nearestPointOnLine(way, point, {units: "kilometers"});
|
||||
}
|
||||
|
||||
public static toCSV(features: any[]): string {
|
||||
|
||||
const headerValuesSeen = new Set<string>();
|
||||
const headerValuesOrdered: string[] = []
|
||||
|
||||
function addH(key) {
|
||||
if (!headerValuesSeen.has(key)) {
|
||||
headerValuesSeen.add(key)
|
||||
headerValuesOrdered.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
addH("_lat")
|
||||
addH("_lon")
|
||||
|
||||
const lines: string[] = []
|
||||
|
||||
for (const feature of features) {
|
||||
const properties = feature.properties;
|
||||
for (const key in properties) {
|
||||
if (!properties.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
addH(key)
|
||||
|
||||
}
|
||||
}
|
||||
headerValuesOrdered.sort()
|
||||
for (const feature of features) {
|
||||
const properties = feature.properties;
|
||||
let line = ""
|
||||
for (const key of headerValuesOrdered) {
|
||||
const value = properties[key]
|
||||
if (value === undefined) {
|
||||
line += ","
|
||||
} else {
|
||||
line += JSON.stringify(value) + ","
|
||||
}
|
||||
}
|
||||
lines.push(line)
|
||||
}
|
||||
|
||||
return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -364,7 +364,7 @@ export class BBox {
|
|||
static get(feature) {
|
||||
if (feature.bbox?.overlapsWith === undefined) {
|
||||
const turfBbox: number[] = turf.bbox(feature)
|
||||
feature.bbox = new BBox([[turfBbox[0], turfBbox[1]],[turfBbox[2], turfBbox[3]]]);
|
||||
feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]);
|
||||
}
|
||||
|
||||
return feature.bbox;
|
||||
|
|
|
@ -2,7 +2,7 @@ import {Mapillary} from "./Mapillary";
|
|||
import {Wikimedia} from "./Wikimedia";
|
||||
import {Imgur} from "./Imgur";
|
||||
|
||||
export default class AllImageProviders{
|
||||
export default class AllImageProviders {
|
||||
|
||||
public static ImageAttributionSource = [Imgur.singleton, Mapillary.singleton, Wikimedia.singleton]
|
||||
|
||||
|
|
|
@ -18,12 +18,13 @@ export default abstract class ImageAttributionSource {
|
|||
}
|
||||
|
||||
|
||||
public abstract SourceIcon(backlinkSource?: string): BaseUIElement;
|
||||
|
||||
public abstract SourceIcon(backlinkSource?: string) : BaseUIElement;
|
||||
protected abstract DownloadAttribution(url: string): UIEventSource<LicenseInfo>;
|
||||
/*Converts a value to a URL. Can return null if not applicable*/
|
||||
public PrepareUrl(value: string): string{
|
||||
public PrepareUrl(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
protected abstract DownloadAttribution(url: string): UIEventSource<LicenseInfo>;
|
||||
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import $ from "jquery"
|
||||
import {LicenseInfo} from "./Wikimedia";
|
||||
import ImageAttributionSource from "./ImageAttributionSource";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
|
|
|
@ -77,7 +77,7 @@ export class Wikimedia extends ImageAttributionSource {
|
|||
|
||||
static GetWikiData(id: number, handleWikidata: ((Wikidata) => void)) {
|
||||
const url = "https://www.wikidata.org/wiki/Special:EntityData/Q" + id + ".json";
|
||||
Utils.downloadJson(url).then (response => {
|
||||
Utils.downloadJson(url).then(response => {
|
||||
const entity = response.entities["Q" + id];
|
||||
const commons = entity.sitelinks.commonswiki;
|
||||
const wd = new Wikidata();
|
||||
|
@ -139,10 +139,10 @@ export class Wikimedia extends ImageAttributionSource {
|
|||
"titles=" + filename +
|
||||
"&format=json&origin=*";
|
||||
Utils.downloadJson(url).then(
|
||||
data =>{
|
||||
data => {
|
||||
const licenseInfo = new LicenseInfo();
|
||||
const license = (data.query.pages[-1].imageinfo ?? [])[0]?.extmetadata;
|
||||
if(license === undefined){
|
||||
if (license === undefined) {
|
||||
console.error("This file has no usable metedata or license attached... Please fix the license info file yourself!")
|
||||
source.setData(null)
|
||||
return;
|
||||
|
|
|
@ -18,6 +18,9 @@ interface Params {
|
|||
export default class MetaTagging {
|
||||
|
||||
|
||||
private static errorPrintCount = 0;
|
||||
private static readonly stopErrorOutputAt = 10;
|
||||
|
||||
/**
|
||||
* An actor which adds metatags on every feature in the given object
|
||||
* The features are a list of geojson-features, with a "properties"-field and geometry
|
||||
|
@ -86,9 +89,6 @@ export default class MetaTagging {
|
|||
|
||||
}
|
||||
|
||||
private static errorPrintCount = 0;
|
||||
private static readonly stopErrorOutputAt = 10;
|
||||
|
||||
private static createRetaggingFunc(layer: LayerConfig):
|
||||
((params: Params, feature: any) => void) {
|
||||
const calculatedTags: [string, string][] = layer.calculatedTags;
|
||||
|
@ -111,7 +111,7 @@ export default class MetaTagging {
|
|||
const f = (featuresPerLayer, feature: any) => {
|
||||
try {
|
||||
let result = func(feature);
|
||||
if(result instanceof UIEventSource){
|
||||
if (result instanceof UIEventSource) {
|
||||
result.addCallbackAndRunD(d => {
|
||||
if (typeof d !== "string") {
|
||||
// Make sure it is a string!
|
||||
|
|
|
@ -8,11 +8,10 @@ import {GeoOperations} from "../../GeoOperations";
|
|||
|
||||
export default class CreateNewNodeAction extends OsmChangeAction {
|
||||
|
||||
public newElementId: string = undefined
|
||||
private readonly _basicTags: Tag[];
|
||||
private readonly _lat: number;
|
||||
private readonly _lon: number;
|
||||
|
||||
public newElementId: string = undefined
|
||||
private readonly _snapOnto: OsmWay;
|
||||
private readonly _reusePointDistance: number;
|
||||
|
||||
|
@ -21,7 +20,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
this._basicTags = basicTags;
|
||||
this._lat = lat;
|
||||
this._lon = lon;
|
||||
if(lat === undefined || lon === undefined){
|
||||
if (lat === undefined || lon === undefined) {
|
||||
throw "Lat or lon are undefined!"
|
||||
}
|
||||
this._snapOnto = options?.snapOnto;
|
||||
|
@ -95,7 +94,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
return [
|
||||
newPointChange,
|
||||
{
|
||||
type:"way",
|
||||
type: "way",
|
||||
id: this._snapOnto.id,
|
||||
changes: {
|
||||
locations: locations,
|
||||
|
|
|
@ -30,7 +30,7 @@ export default class DeleteAction {
|
|||
* Does actually delete the feature; returns the event source 'this.isDeleted'
|
||||
* If deletion is not allowed, triggers the callback instead
|
||||
*/
|
||||
public DoDelete(reason: string, onNotAllowed : () => void): void {
|
||||
public DoDelete(reason: string, onNotAllowed: () => void): void {
|
||||
const isDeleted = this.isDeleted
|
||||
const self = this;
|
||||
let deletionStarted = false;
|
||||
|
@ -41,7 +41,7 @@ export default class DeleteAction {
|
|||
return;
|
||||
}
|
||||
|
||||
if(canBeDeleted.canBeDeleted === false){
|
||||
if (canBeDeleted.canBeDeleted === false) {
|
||||
// We aren't allowed to delete
|
||||
deletionStarted = true;
|
||||
onNotAllowed();
|
||||
|
@ -55,8 +55,6 @@ export default class DeleteAction {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
deletionStarted = true;
|
||||
OsmObject.DownloadObject(self._id).addCallbackAndRun(obj => {
|
||||
if (obj === undefined) {
|
||||
|
@ -207,7 +205,7 @@ export default class DeleteAction {
|
|||
canBeDeleted: false,
|
||||
reason: t.partOfOthers
|
||||
})
|
||||
}else{
|
||||
} else {
|
||||
// alright, this point can be safely deleted!
|
||||
state.setData({
|
||||
canBeDeleted: true,
|
||||
|
@ -216,7 +214,6 @@ export default class DeleteAction {
|
|||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {OsmRelation, OsmWay} from "../OsmObject";
|
||||
import {OsmRelation} from "../OsmObject";
|
||||
|
||||
export default class RelationSplitlHandler extends OsmChangeAction{
|
||||
export default class RelationSplitlHandler extends OsmChangeAction {
|
||||
|
||||
constructor(partOf: OsmRelation[], newWayIds: number[], originalNodes: number[]) {
|
||||
super()
|
||||
|
|
|
@ -24,7 +24,7 @@ export class Changes {
|
|||
public readonly pendingChanges = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||
private readonly isUploading = new UIEventSource(false);
|
||||
|
||||
private readonly previouslyCreated : OsmObject[] = []
|
||||
private readonly previouslyCreated: OsmObject[] = []
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
@ -70,6 +70,88 @@ export class Changes {
|
|||
.map(c => c.type + "/" + c.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new ID and updates the value for the next ID
|
||||
*/
|
||||
public getNewID() {
|
||||
return Changes._nextId--;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads all the pending changes in one go.
|
||||
* Triggered by the 'PendingChangeUploader'-actor in Actors
|
||||
*/
|
||||
public flushChanges(flushreason: string = undefined) {
|
||||
if (this.pendingChanges.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isUploading.data) {
|
||||
console.log("Is already uploading... Abort")
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.isUploading.setData(true)
|
||||
|
||||
console.log("Beginning upload... " + flushreason ?? "");
|
||||
// At last, we build the changeset and upload
|
||||
const self = this;
|
||||
const pending = self.pendingChanges.data;
|
||||
const neededIds = Changes.GetNeededIds(pending)
|
||||
console.log("Needed ids", neededIds)
|
||||
OsmObject.DownloadAll(neededIds, true).addCallbackAndRunD(osmObjects => {
|
||||
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
|
||||
try {
|
||||
|
||||
|
||||
const changes: {
|
||||
newObjects: OsmObject[],
|
||||
modifiedObjects: OsmObject[]
|
||||
deletedObjects: OsmObject[]
|
||||
|
||||
} = self.CreateChangesetObjects(pending, osmObjects)
|
||||
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
|
||||
console.log("No changes to be made")
|
||||
self.pendingChanges.setData([])
|
||||
self.isUploading.setData(false)
|
||||
return true; // Unregister the callback
|
||||
}
|
||||
|
||||
|
||||
State.state.osmConnection.UploadChangeset(
|
||||
State.state.layoutToUse.data,
|
||||
State.state.allElements,
|
||||
(csId) => Changes.createChangesetFor(csId, changes),
|
||||
() => {
|
||||
console.log("Upload successfull!")
|
||||
self.pendingChanges.setData([]);
|
||||
self.isUploading.setData(false)
|
||||
},
|
||||
() => {
|
||||
console.log("Upload failed - trying again later")
|
||||
return self.isUploading.setData(false);
|
||||
} // Failed - mark to try again
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
|
||||
self.pendingChanges.setData([])
|
||||
self.isUploading.setData(false)
|
||||
}
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
public applyAction(action: OsmChangeAction) {
|
||||
const changes = action.Perform(this)
|
||||
console.log("Received changes:", changes)
|
||||
this.pendingChanges.data.push(...changes);
|
||||
this.pendingChanges.ping();
|
||||
}
|
||||
|
||||
private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
|
||||
newObjects: OsmObject[],
|
||||
modifiedObjects: OsmObject[]
|
||||
|
@ -93,8 +175,8 @@ export class Changes {
|
|||
for (const change of changes) {
|
||||
const id = change.type + "/" + change.id
|
||||
if (!objects.has(id)) {
|
||||
if(change.id >= 0){
|
||||
throw "Did not get an object that should be known: "+id
|
||||
if (change.id >= 0) {
|
||||
throw "Did not get an object that should be known: " + id
|
||||
}
|
||||
// This is a new object that should be created
|
||||
states.set(id, "created")
|
||||
|
@ -221,86 +303,4 @@ export class Changes {
|
|||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new ID and updates the value for the next ID
|
||||
*/
|
||||
public getNewID() {
|
||||
return Changes._nextId--;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads all the pending changes in one go.
|
||||
* Triggered by the 'PendingChangeUploader'-actor in Actors
|
||||
*/
|
||||
public flushChanges(flushreason: string = undefined) {
|
||||
if (this.pendingChanges.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isUploading.data) {
|
||||
console.log("Is already uploading... Abort")
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.isUploading.setData(true)
|
||||
|
||||
console.log("Beginning upload... "+flushreason ?? "");
|
||||
// At last, we build the changeset and upload
|
||||
const self = this;
|
||||
const pending = self.pendingChanges.data;
|
||||
const neededIds = Changes.GetNeededIds(pending)
|
||||
console.log("Needed ids", neededIds)
|
||||
OsmObject.DownloadAll(neededIds, true).addCallbackAndRunD(osmObjects => {
|
||||
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
|
||||
try{
|
||||
|
||||
|
||||
const changes: {
|
||||
newObjects: OsmObject[],
|
||||
modifiedObjects: OsmObject[]
|
||||
deletedObjects: OsmObject[]
|
||||
|
||||
} = self.CreateChangesetObjects(pending, osmObjects)
|
||||
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
|
||||
console.log("No changes to be made")
|
||||
self.pendingChanges.setData([])
|
||||
self.isUploading.setData(false)
|
||||
return true; // Unregister the callback
|
||||
}
|
||||
|
||||
|
||||
State.state.osmConnection.UploadChangeset(
|
||||
State.state.layoutToUse.data,
|
||||
State.state.allElements,
|
||||
(csId) => Changes.createChangesetFor(csId, changes),
|
||||
() => {
|
||||
console.log("Upload successfull!")
|
||||
self.pendingChanges.setData([]);
|
||||
self.isUploading.setData(false)
|
||||
},
|
||||
() => {
|
||||
console.log("Upload failed - trying again later")
|
||||
return self.isUploading.setData(false);
|
||||
} // Failed - mark to try again
|
||||
)
|
||||
}catch(e){
|
||||
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
|
||||
self.pendingChanges.setData([])
|
||||
self.isUploading.setData(false)
|
||||
}
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
public applyAction(action: OsmChangeAction) {
|
||||
const changes = action.Perform(this)
|
||||
console.log("Received changes:", changes)
|
||||
this.pendingChanges.data.push(...changes);
|
||||
this.pendingChanges.ping();
|
||||
}
|
||||
}
|
|
@ -258,7 +258,7 @@ export class ChangesetHandler {
|
|||
}, function (err, response) {
|
||||
if (response === undefined) {
|
||||
console.log("err", err);
|
||||
if(options.onFail){
|
||||
if (options.onFail) {
|
||||
options.onFail()
|
||||
}
|
||||
return;
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface Relation {
|
|||
|
||||
export default class ExtractRelations {
|
||||
|
||||
public static RegisterRelations(overpassJson: any) : void{
|
||||
public static RegisterRelations(overpassJson: any): void {
|
||||
const memberships = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(overpassJson))
|
||||
State.state.knownRelations.setData(memberships)
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@ export class Geocoding {
|
|||
private static readonly host = "https://nominatim.openstreetmap.org/search?";
|
||||
|
||||
static Search(query: string,
|
||||
handleResult: ((places: { display_name: string, lat: number, lon: number, boundingbox: number[],
|
||||
osm_type: string, osm_id: string}[]) => void),
|
||||
handleResult: ((places: {
|
||||
display_name: string, lat: number, lon: number, boundingbox: number[],
|
||||
osm_type: string, osm_id: string
|
||||
}[]) => void),
|
||||
onFail: (() => void)) {
|
||||
const b = State.state.leafletMap.data.getBounds();
|
||||
const url = Geocoding.host + "format=json&limit=1&viewbox=" +
|
||||
`${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}`+
|
||||
`${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` +
|
||||
"&accept-language=nl&q=" + query;
|
||||
Utils.downloadJson(
|
||||
url)
|
||||
|
|
|
@ -47,10 +47,10 @@ export class OsmConnection {
|
|||
public auth;
|
||||
public userDetails: UIEventSource<UserDetails>;
|
||||
public isLoggedIn: UIEventSource<boolean>
|
||||
private fakeUser: boolean;
|
||||
_dryRun: boolean;
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public changesetHandler: ChangesetHandler;
|
||||
private fakeUser: boolean;
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||
private readonly _iframeMode: Boolean | boolean;
|
||||
private readonly _singlePage: boolean;
|
||||
|
@ -59,6 +59,7 @@ export class OsmConnection {
|
|||
oauth_secret: string,
|
||||
url: string
|
||||
};
|
||||
private isChecking = false;
|
||||
|
||||
constructor(dryRun: boolean,
|
||||
fakeUser: boolean,
|
||||
|
@ -77,17 +78,17 @@ export class OsmConnection {
|
|||
|
||||
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
|
||||
this.userDetails.data.dryRun = dryRun || fakeUser;
|
||||
if(fakeUser){
|
||||
if (fakeUser) {
|
||||
const ud = this.userDetails.data;
|
||||
ud.csCount = 5678
|
||||
ud.loggedIn= true;
|
||||
ud.loggedIn = true;
|
||||
ud.unreadMessages = 0
|
||||
ud.name = "Fake user"
|
||||
ud.totalMessages = 42;
|
||||
}
|
||||
const self =this;
|
||||
const self = this;
|
||||
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
|
||||
if(self.userDetails.data.loggedIn == false && isLoggedIn == true){
|
||||
if (self.userDetails.data.loggedIn == false && isLoggedIn == true) {
|
||||
// We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do
|
||||
// This means someone attempted to toggle this; so we attempt to login!
|
||||
self.AttemptLogin()
|
||||
|
@ -150,7 +151,7 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
public AttemptLogin() {
|
||||
if(this.fakeUser){
|
||||
if (this.fakeUser) {
|
||||
console.log("AttemptLogin called, but ignored as fakeUser is set")
|
||||
return;
|
||||
}
|
||||
|
@ -191,7 +192,7 @@ export class OsmConnection {
|
|||
data.loggedIn = true;
|
||||
console.log("Login completed, userinfo is ", userInfo);
|
||||
data.name = userInfo.getAttribute('display_name');
|
||||
data.uid= Number(userInfo.getAttribute("id"))
|
||||
data.uid = Number(userInfo.getAttribute("id"))
|
||||
data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count");
|
||||
|
||||
data.img = undefined;
|
||||
|
@ -249,15 +250,14 @@ export class OsmConnection {
|
|||
});
|
||||
}
|
||||
|
||||
private isChecking = false;
|
||||
private CheckForMessagesContinuously(){
|
||||
const self =this;
|
||||
if(this.isChecking){
|
||||
private CheckForMessagesContinuously() {
|
||||
const self = this;
|
||||
if (this.isChecking) {
|
||||
return;
|
||||
}
|
||||
this.isChecking = true;
|
||||
UIEventSource.Chronic(5 * 60 * 1000).addCallback(_ => {
|
||||
if (self.isLoggedIn .data) {
|
||||
if (self.isLoggedIn.data) {
|
||||
console.log("Checking for messages")
|
||||
self.AttemptLogin();
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ export abstract class OsmObject {
|
|||
const splitted = id.split("/");
|
||||
const type = splitted[0];
|
||||
const idN = Number(splitted[1]);
|
||||
if(idN <0){
|
||||
if (idN < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -438,7 +438,7 @@ export class OsmWay extends OsmObject {
|
|||
|
||||
for (const nodeId of element.nodes) {
|
||||
const node = nodeDict.get(nodeId)
|
||||
if(node === undefined){
|
||||
if (node === undefined) {
|
||||
console.error("Error: node ", nodeId, "not found in ", nodeDict)
|
||||
// This is probably part of a relation which hasn't been fully downloaded
|
||||
continue;
|
||||
|
@ -498,7 +498,7 @@ export class OsmRelation extends OsmObject {
|
|||
|
||||
let tags = this.TagsXML();
|
||||
let cs = ""
|
||||
if(changesetId !== undefined){
|
||||
if (changesetId !== undefined) {
|
||||
cs = `changeset="${changesetId}"`
|
||||
}
|
||||
return ` <relation id="${this.id}" ${cs} ${this.VersionXML()}>
|
||||
|
|
|
@ -29,7 +29,7 @@ export class OsmPreferences {
|
|||
return this.longPreferences[prefix + key];
|
||||
}
|
||||
|
||||
const source = new UIEventSource<string>(undefined, "long-osm-preference:"+prefix+key);
|
||||
const source = new UIEventSource<string>(undefined, "long-osm-preference:" + prefix + key);
|
||||
this.longPreferences[prefix + key] = source;
|
||||
|
||||
const allStartWith = prefix + key + "-combined";
|
||||
|
@ -107,7 +107,7 @@ export class OsmPreferences {
|
|||
if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) {
|
||||
this.UpdatePreferences();
|
||||
}
|
||||
const pref = new UIEventSource<string>(this.preferences.data[key],"osm-preference:"+key);
|
||||
const pref = new UIEventSource<string>(this.preferences.data[key], "osm-preference:" + key);
|
||||
pref.addCallback((v) => {
|
||||
this.SetPreference(key, v);
|
||||
});
|
||||
|
|
|
@ -15,9 +15,6 @@ export default class AspectedRouting {
|
|||
delete this.program.unit
|
||||
}
|
||||
|
||||
public evaluate(properties){
|
||||
return AspectedRouting.interpret(this.program, properties)
|
||||
}
|
||||
/**
|
||||
* Interprets the given Aspected-routing program for the given properties
|
||||
*/
|
||||
|
@ -191,4 +188,8 @@ export default class AspectedRouting {
|
|||
return result;
|
||||
}
|
||||
|
||||
public evaluate(properties) {
|
||||
return AspectedRouting.interpret(this.program, properties)
|
||||
}
|
||||
|
||||
}
|
|
@ -96,7 +96,7 @@ export default class SimpleMetaTagger {
|
|||
const value = feature.properties[key]
|
||||
const [, denomination] = unit.findDenomination(value)
|
||||
let canonical = denomination?.canonicalValue(value) ?? undefined;
|
||||
if(canonical === value){
|
||||
if (canonical === value) {
|
||||
break;
|
||||
}
|
||||
console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`)
|
||||
|
@ -110,7 +110,7 @@ export default class SimpleMetaTagger {
|
|||
}
|
||||
|
||||
}
|
||||
if(rewritten){
|
||||
if (rewritten) {
|
||||
State.state.allElements.getEventSourceById(feature.id).ping();
|
||||
}
|
||||
})
|
||||
|
|
|
@ -8,18 +8,6 @@ export class And extends TagsFilter {
|
|||
this.and = and
|
||||
}
|
||||
|
||||
normalize(){
|
||||
const ands = []
|
||||
for (const c of this.and) {
|
||||
if(c instanceof And){
|
||||
ands.push(...c.and)
|
||||
}else{
|
||||
ands.push(c)
|
||||
}
|
||||
}
|
||||
return new And(ands)
|
||||
}
|
||||
|
||||
private static combine(filter: string, choices: string[]): string[] {
|
||||
const values = [];
|
||||
for (const or of choices) {
|
||||
|
@ -28,6 +16,18 @@ export class And extends TagsFilter {
|
|||
return values;
|
||||
}
|
||||
|
||||
normalize() {
|
||||
const ands = []
|
||||
for (const c of this.and) {
|
||||
if (c instanceof And) {
|
||||
ands.push(...c.and)
|
||||
} else {
|
||||
ands.push(c)
|
||||
}
|
||||
}
|
||||
return new And(ands)
|
||||
}
|
||||
|
||||
matchesProperties(tags: any): boolean {
|
||||
for (const tagsFilter of this.and) {
|
||||
if (!tagsFilter.matchesProperties(tags)) {
|
||||
|
|
|
@ -5,7 +5,7 @@ export default class ComparingTag implements TagsFilter {
|
|||
private readonly _predicate: (value: string) => boolean;
|
||||
private readonly _representation: string;
|
||||
|
||||
constructor(key: string, predicate : (value:string | undefined) => boolean, representation: string = "") {
|
||||
constructor(key: string, predicate: (value: string | undefined) => boolean, representation: string = "") {
|
||||
this._key = key;
|
||||
this._predicate = predicate;
|
||||
this._representation = representation;
|
||||
|
@ -16,7 +16,7 @@ export default class ComparingTag implements TagsFilter {
|
|||
}
|
||||
|
||||
asHumanString(linkToWiki: boolean, shorten: boolean, properties: any) {
|
||||
return this._key+this._representation
|
||||
return this._key + this._representation
|
||||
}
|
||||
|
||||
asOverpass(): string[] {
|
||||
|
|
|
@ -16,7 +16,7 @@ export class RegexTag extends TagsFilter {
|
|||
}
|
||||
|
||||
private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean {
|
||||
if(fromTag === undefined){
|
||||
if (fromTag === undefined) {
|
||||
return;
|
||||
}
|
||||
if (typeof possibleRegex === "string") {
|
||||
|
@ -45,7 +45,7 @@ export class RegexTag extends TagsFilter {
|
|||
|
||||
matchesProperties(tags: any): boolean {
|
||||
for (const key in tags) {
|
||||
if(key === undefined){
|
||||
if (key === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (RegexTag.doesMatch(key, this.key)) {
|
||||
|
@ -86,14 +86,14 @@ export class RegexTag extends TagsFilter {
|
|||
}
|
||||
|
||||
asChange(properties: any): { k: string; v: string }[] {
|
||||
if(this.invert){
|
||||
if (this.invert) {
|
||||
return []
|
||||
}
|
||||
if (typeof this.key === "string") {
|
||||
if( typeof this.value === "string"){
|
||||
if (typeof this.value === "string") {
|
||||
return [{k: this.key, v: this.value}]
|
||||
}
|
||||
if(this.value.toString() != "/^..*$/"){
|
||||
if (this.value.toString() != "/^..*$/") {
|
||||
console.warn("Regex value in tag; using wildcard:", this.key, this.value)
|
||||
}
|
||||
return [{k: this.key, v: undefined}]
|
||||
|
|
|
@ -24,12 +24,12 @@ export class Tag extends TagsFilter {
|
|||
|
||||
matchesProperties(properties: any): boolean {
|
||||
for (const propertiesKey in properties) {
|
||||
if(!properties.hasOwnProperty(propertiesKey)){
|
||||
if (!properties.hasOwnProperty(propertiesKey)) {
|
||||
continue
|
||||
}
|
||||
if (this.key === propertiesKey) {
|
||||
const value = properties[propertiesKey];
|
||||
if(value === undefined){
|
||||
if (value === undefined) {
|
||||
continue
|
||||
}
|
||||
return value === this.value;
|
||||
|
@ -52,6 +52,7 @@ export class Tag extends TagsFilter {
|
|||
}
|
||||
return [`["${this.key}"="${this.value}"]`];
|
||||
}
|
||||
|
||||
asHumanString(linkToWiki?: boolean, shorten?: boolean) {
|
||||
let v = this.value;
|
||||
if (shorten) {
|
||||
|
|
|
@ -10,6 +10,15 @@ import {AndOrTagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
|
|||
import {isRegExp} from "util";
|
||||
|
||||
export class TagUtils {
|
||||
private static comparators
|
||||
: [string, (a: number, b: number) => boolean][]
|
||||
= [
|
||||
["<=", (a, b) => a <= b],
|
||||
[">=", (a, b) => a >= b],
|
||||
["<", (a, b) => a < b],
|
||||
[">", (a, b) => a > b],
|
||||
]
|
||||
|
||||
static ApplyTemplate(template: string, tags: any): string {
|
||||
for (const k in tags) {
|
||||
while (template.indexOf("{" + k + "}") >= 0) {
|
||||
|
@ -76,15 +85,15 @@ export class TagUtils {
|
|||
continue;
|
||||
}
|
||||
|
||||
if(allowRegex && tagsFilter instanceof RegexTag) {
|
||||
if (allowRegex && tagsFilter instanceof RegexTag) {
|
||||
const key = tagsFilter.key
|
||||
if(isRegExp(key)) {
|
||||
if (isRegExp(key)) {
|
||||
console.error("Invalid type to flatten the multiAnswer: key is a regex too", tagsFilter);
|
||||
throw "Invalid type to FlattenMultiAnswer"
|
||||
}
|
||||
const keystr = <string>key
|
||||
if (keyValues[keystr] === undefined) {
|
||||
keyValues[keystr ] = [];
|
||||
keyValues[keystr] = [];
|
||||
}
|
||||
keyValues[keystr].push(tagsFilter);
|
||||
continue;
|
||||
|
@ -139,8 +148,8 @@ export class TagUtils {
|
|||
const actualValue = properties[splitKey].split(";");
|
||||
for (const neededValue of neededValues) {
|
||||
|
||||
if(neededValue instanceof RegexTag) {
|
||||
if(!neededValue.matchesProperties(properties)) {
|
||||
if (neededValue instanceof RegexTag) {
|
||||
if (!neededValue.matchesProperties(properties)) {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
|
@ -170,15 +179,6 @@ export class TagUtils {
|
|||
}
|
||||
}
|
||||
|
||||
private static comparators
|
||||
: [string, (a: number, b: number) => boolean][]
|
||||
= [
|
||||
["<=", (a, b) => a <= b],
|
||||
[">=", (a, b) => a >= b],
|
||||
["<", (a, b) => a < b],
|
||||
[">", (a, b) => a > b],
|
||||
]
|
||||
|
||||
private static TagUnsafe(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter {
|
||||
|
||||
if (json === undefined) {
|
||||
|
|
|
@ -18,6 +18,6 @@ export abstract class TagsFilter {
|
|||
*
|
||||
* Note: properties are the already existing tags-object. It is only used in the substituting tag
|
||||
*/
|
||||
abstract asChange(properties:any): {k: string, v:string}[]
|
||||
abstract asChange(properties: any): { k: string, v: string }[]
|
||||
|
||||
}
|
|
@ -194,9 +194,9 @@ export class UIEventSourceTools {
|
|||
|
||||
private static readonly _download_cache = new Map<string, UIEventSource<any>>()
|
||||
|
||||
public static downloadJsonCached(url: string): UIEventSource<any>{
|
||||
public static downloadJsonCached(url: string): UIEventSource<any> {
|
||||
const cached = UIEventSourceTools._download_cache.get(url)
|
||||
if(cached !== undefined){
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
const src = new UIEventSource<any>(undefined)
|
||||
|
|
|
@ -24,7 +24,7 @@ export default class LiveQueryHandler {
|
|||
const source = new UIEventSource({});
|
||||
LiveQueryHandler[url] = source;
|
||||
|
||||
console.log("Fetching live data from a third-party (unknown) API:",url)
|
||||
console.log("Fetching live data from a third-party (unknown) API:", url)
|
||||
Utils.downloadJson(url).then(data => {
|
||||
for (const shorthandDescription of shorthandsSet) {
|
||||
|
||||
|
|
|
@ -5,15 +5,15 @@ import {UIEventSource} from "../UIEventSource";
|
|||
*/
|
||||
export class LocalStorageSource {
|
||||
|
||||
static GetParsed<T>(key: string, defaultValue : T) : UIEventSource<T>{
|
||||
static GetParsed<T>(key: string, defaultValue: T): UIEventSource<T> {
|
||||
return LocalStorageSource.Get(key).map(
|
||||
str => {
|
||||
if(str === undefined){
|
||||
if (str === undefined) {
|
||||
return defaultValue
|
||||
}
|
||||
try{
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
}catch{
|
||||
} catch {
|
||||
return defaultValue
|
||||
}
|
||||
}, [],
|
||||
|
@ -24,12 +24,12 @@ export class LocalStorageSource {
|
|||
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
|
||||
try {
|
||||
const saved = localStorage.getItem(key);
|
||||
const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:"+key);
|
||||
const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:" + key);
|
||||
|
||||
source.addCallback((data) => {
|
||||
try{
|
||||
try {
|
||||
localStorage.setItem(key, data);
|
||||
}catch(e){
|
||||
} catch (e) {
|
||||
// Probably exceeded the quota with this item!
|
||||
// Lets nuke everything
|
||||
localStorage.clear()
|
||||
|
|
|
@ -69,6 +69,10 @@ export class QueryParameters {
|
|||
return docs.join("\n\n");
|
||||
}
|
||||
|
||||
public static wasInitialized(key: string): boolean {
|
||||
return QueryParameters._wasInitialized.has(key)
|
||||
}
|
||||
|
||||
private static addOrder(key) {
|
||||
if (this.order.indexOf(key) < 0) {
|
||||
this.order.push(key)
|
||||
|
@ -105,10 +109,6 @@ export class QueryParameters {
|
|||
}
|
||||
}
|
||||
|
||||
public static wasInitialized(key: string) : boolean{
|
||||
return QueryParameters._wasInitialized.has(key)
|
||||
}
|
||||
|
||||
private static Serialize() {
|
||||
const parts = []
|
||||
for (const key of QueryParameters.order) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Utils } from "../Utils";
|
||||
import {Utils} from "../Utils";
|
||||
|
||||
export default class Constants {
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ export class Denomination {
|
|||
public readonly canonical: string;
|
||||
readonly default: boolean;
|
||||
readonly prefix: boolean;
|
||||
private readonly _human: Translation;
|
||||
public readonly alternativeDenominations: string [];
|
||||
private readonly _human: Translation;
|
||||
|
||||
constructor(json: UnitConfigJson, context: string) {
|
||||
context = `${context}.unit(${json.canonicalDenomination})`
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import {TagsFilter} from "../Logic/Tags/TagsFilter";
|
||||
import LayerConfig from "./ThemeConfig/LayerConfig";
|
||||
import {And} from "../Logic/Tags/And";
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@ export default class FilterConfig {
|
|||
}[];
|
||||
|
||||
constructor(json: FilterConfigJson, context: string) {
|
||||
if(json.options === undefined){
|
||||
if (json.options === undefined) {
|
||||
throw `A filter without options was given at ${context}`
|
||||
}
|
||||
|
||||
if(json.options.map === undefined){
|
||||
if (json.options.map === undefined) {
|
||||
throw `A filter was given where the options aren't a list at ${context}`
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ export default class FilterConfig {
|
|||
option.osmTags ?? {and: []},
|
||||
`${context}.options-[${i}].osmTags`
|
||||
);
|
||||
if(question === undefined){
|
||||
if (question === undefined) {
|
||||
throw `Invalid filter: no question given at ${context}[${i}]`
|
||||
}
|
||||
|
||||
|
|
|
@ -53,10 +53,9 @@ export default class LayoutConfig {
|
|||
*/
|
||||
public readonly cacheTimeout?: number;
|
||||
public readonly units: Unit[] = []
|
||||
private readonly _official: boolean;
|
||||
|
||||
public readonly overpassUrl: string;
|
||||
public readonly overpassTimeout: number;
|
||||
private readonly _official: boolean;
|
||||
|
||||
constructor(json: LayoutConfigJson, official = true, context?: string) {
|
||||
this._official = official;
|
||||
|
|
|
@ -6,7 +6,6 @@ import {TagUtils} from "../../Logic/Tags/TagUtils";
|
|||
import {And} from "../../Logic/Tags/And";
|
||||
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
|
||||
import {Utils} from "../../Utils";
|
||||
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
|
||||
|
||||
/***
|
||||
* The parsed version of TagRenderingConfigJSON
|
||||
|
|
97
README.md
97
README.md
|
@ -1,10 +1,12 @@
|
|||
|
||||
# MapComplete
|
||||
|
||||
> Let a thousand flowers bloom
|
||||
|
||||
MapComplete is an OpenStreetMap viewer and editor. It shows map features on a certain topic, and allows to see, edit and add new features to the map.
|
||||
It can be seen as a webversion [crossover of StreetComplete and MapContrib](Docs/MapComplete_vs_other_editors.md). It tries to be just as easy to use as StreetComplete, but it allows to focus on one single theme per instance (e.g. nature, bicycle infrastructure, ...)
|
||||
MapComplete is an OpenStreetMap viewer and editor. It shows map features on a certain topic, and allows to see, edit and
|
||||
add new features to the map. It can be seen as a
|
||||
webversion [crossover of StreetComplete and MapContrib](Docs/MapComplete_vs_other_editors.md). It tries to be just as
|
||||
easy to use as StreetComplete, but it allows to focus on one single theme per instance (e.g. nature, bicycle
|
||||
infrastructure, ...)
|
||||
|
||||
The design goals of MapComplete are to be:
|
||||
|
||||
|
@ -13,29 +15,39 @@ The design goals of MapComplete are to be:
|
|||
- Easy to set up a custom theme
|
||||
- Easy to fall down the rabbit hole of OSM
|
||||
|
||||
The basic functionality is to download some map features from Overpass and then ask certain questions. An answer is sent back to directly to OpenStreetMap.
|
||||
The basic functionality is to download some map features from Overpass and then ask certain questions. An answer is sent
|
||||
back to directly to OpenStreetMap.
|
||||
|
||||
Furthermore, it shows images present in the `image` tag or, if a `wikidata` or `wikimedia_commons`-tag is present, it follows those to get these images too.
|
||||
|
||||
An explicit non-goal of MapComplete is to modify geometries of ways. Although adding a point to a way or splitting a way in two parts might be added one day.
|
||||
Furthermore, it shows images present in the `image` tag or, if a `wikidata` or `wikimedia_commons`-tag is present, it
|
||||
follows those to get these images too.
|
||||
|
||||
An explicit non-goal of MapComplete is to modify geometries of ways. Although adding a point to a way or splitting a way
|
||||
in two parts might be added one day.
|
||||
|
||||
# Creating your own theme
|
||||
|
||||
It is possible to quickly make and distribute your own theme - [please read the documentation on how to do this](Docs/Making_Your_Own_Theme.md).
|
||||
It is possible to quickly make and distribute your own theme
|
||||
- [please read the documentation on how to do this](Docs/Making_Your_Own_Theme.md).
|
||||
|
||||
## Examples
|
||||
|
||||
- [Buurtnatuur.be](http://buurtnatuur.be), developed for the Belgian [Green party](https://www.groen.be/). They also funded the initial development!
|
||||
- [Cyclofix](https://pietervdvn.github.io/MapComplete/index.html?layout=cyclofix), further development on [Open Summer of Code](https://summerofcode.be/) funded by [Brussels Mobility](https://mobilite-mobiliteit.brussels/en). Landing page at https://cyclofix.osm.be/
|
||||
- [Bookcases](https://pietervdvn.github.io/MapComplete/index.html?quests=bookcases#element) cause I like to collect them.
|
||||
- [Map of Maps](https://pietervdvn.github.io/MapComplete/index.html?layout=maps&z=14&lat=50.650&lon=4.2668#element), after a tweet
|
||||
- [Buurtnatuur.be](http://buurtnatuur.be), developed for the Belgian [Green party](https://www.groen.be/). They also
|
||||
funded the initial development!
|
||||
- [Cyclofix](https://pietervdvn.github.io/MapComplete/index.html?layout=cyclofix), further development
|
||||
on [Open Summer of Code](https://summerofcode.be/) funded
|
||||
by [Brussels Mobility](https://mobilite-mobiliteit.brussels/en). Landing page at https://cyclofix.osm.be/
|
||||
- [Bookcases](https://pietervdvn.github.io/MapComplete/index.html?quests=bookcases#element) cause I like to collect
|
||||
them.
|
||||
- [Map of Maps](https://pietervdvn.github.io/MapComplete/index.html?layout=maps&z=14&lat=50.650&lon=4.2668#element),
|
||||
after a tweet
|
||||
|
||||
There are plenty more. Discover them in the app.
|
||||
|
||||
### Statistics
|
||||
|
||||
To see statistics, consult [OsmCha](https://osmcha.org/?filters=%7B%22comment%22%3A%5B%7B%22label%22%3A%22%23mapcomplete%22%2C%22value%22%3A%22%23mapcomplete%22%7D%5D%2C%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%7D) or the [analytics page](https://pietervdvn.goatcounter.com/)
|
||||
To see statistics,
|
||||
consult [OsmCha](https://osmcha.org/?filters=%7B%22comment%22%3A%5B%7B%22label%22%3A%22%23mapcomplete%22%2C%22value%22%3A%22%23mapcomplete%22%7D%5D%2C%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%7D)
|
||||
or the [analytics page](https://pietervdvn.goatcounter.com/)
|
||||
|
||||
## User journey
|
||||
|
||||
|
@ -49,7 +61,8 @@ A typical user journey would be:
|
|||
* The user might share the map and/or embed it in the third tab
|
||||
* The user might discover the other themes in the last tab
|
||||
|
||||
1. The user clicks that big tempting button 'login' in order to answer questions - there's enough of these login buttons... The user creates an account.
|
||||
1. The user clicks that big tempting button 'login' in order to answer questions - there's enough of these login
|
||||
buttons... The user creates an account.
|
||||
|
||||
2. The user answers a question! Hooray! The user transformed into a __contributor__ now.
|
||||
|
||||
|
@ -58,49 +71,57 @@ A typical user journey would be:
|
|||
3. The user adds a new POI somewhere
|
||||
|
||||
* Note that _all messages_ must be read before being able to add a point.
|
||||
* In other words, sending a message to a misbehaving MapComplete user acts as having a **zero-day-block**. This is added deliberately to make sure new users _have_ to read feedback from the community.
|
||||
* In other words, sending a message to a misbehaving MapComplete user acts as having a **zero-day-block**. This is
|
||||
added deliberately to make sure new users _have_ to read feedback from the community.
|
||||
|
||||
4. At 50 changesets, the [personal layout](https://pietervdvn.github.io/MapComplete/personal.html) is advertised. The personal theme is a theme where contributors can pick layers from all the official themes. Note that the personal theme is always available.
|
||||
4. At 50 changesets, the [personal layout](https://pietervdvn.github.io/MapComplete/personal.html) is advertised. The
|
||||
personal theme is a theme where contributors can pick layers from all the official themes. Note that the personal
|
||||
theme is always available.
|
||||
|
||||
5. At 200 changesets, the tags become visible when answering questions and when adding a new point from a preset. This is to give more control to power users and to teach new users the tagging scheme
|
||||
5. At 200 changesets, the tags become visible when answering questions and when adding a new point from a preset. This
|
||||
is to give more control to power users and to teach new users the tagging scheme
|
||||
|
||||
6. At 250 changesets, the tags get linked to the wiki
|
||||
|
||||
7. At 500 changesets, I expect contributors to be power users and to be comfortable with tagging scheme and such. The custom theme generator is unlocked.
|
||||
|
||||
7. At 500 changesets, I expect contributors to be power users and to be comfortable with tagging scheme and such. The
|
||||
custom theme generator is unlocked.
|
||||
|
||||
## License
|
||||
|
||||
GPLv3.0 + recommended pingback.
|
||||
|
||||
I love it to see where the project ends up. You are free to reuse the software (under GPL) but, when you have made your own change and are using it, I would like to know about it. Drop me a line, give a pingback in the issues,...
|
||||
I love it to see where the project ends up. You are free to reuse the software (under GPL) but, when you have made your
|
||||
own change and are using it, I would like to know about it. Drop me a line, give a pingback in the issues,...
|
||||
|
||||
## Dev
|
||||
|
||||
To develop or deploy a version of MapComplete, have a look [to the guide](Docs/Development_deployment.md).
|
||||
|
||||
|
||||
## Translating MapComplete
|
||||
|
||||
The core strings and builtin themes of MapComplete are translated on [Hosted Weblate](https://hosted.weblate.org/projects/mapcomplete/core/).
|
||||
You can easily make an account and start translating in their web-environment - no installation required.
|
||||
The core strings and builtin themes of MapComplete are translated
|
||||
on [Hosted Weblate](https://hosted.weblate.org/projects/mapcomplete/core/). You can easily make an account and start
|
||||
translating in their web-environment - no installation required.
|
||||
|
||||
[![Translation status](https://hosted.weblate.org/widgets/mapcomplete/-/multi-blue.svg)](https://hosted.weblate.org/engage/mapcomplete/)
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
### High-level overview
|
||||
|
||||
The website is purely static. This means that there is no database here, nor one is needed as all the data is kept in OpenStreetMap, Wikimedia (for images), Imgur. Settings are saved in the preferences-space of the OSM-website, amended by some local-storage if the user is not logged-in.
|
||||
The website is purely static. This means that there is no database here, nor one is needed as all the data is kept in
|
||||
OpenStreetMap, Wikimedia (for images), Imgur. Settings are saved in the preferences-space of the OSM-website, amended by
|
||||
some local-storage if the user is not logged-in.
|
||||
|
||||
When viewing, the data is loaded from overpass. The data is then converted (in the browser) to geojson, which is rendered by Leaflet.
|
||||
When viewing, the data is loaded from overpass. The data is then converted (in the browser) to geojson, which is
|
||||
rendered by Leaflet.
|
||||
|
||||
When a map feature is clicked, a popup shows the information, images and questions that are relevant for that object.
|
||||
The answers given by the user are sent (after a few seconds) to OpenStreetMap directly - if the user is logged in. If not logged in, the user is prompted to do so.
|
||||
|
||||
The UI-event-source is a class where the entire system is built upon, it acts as an observable object: another object can register for changes to update when needed.
|
||||
The answers given by the user are sent (after a few seconds) to OpenStreetMap directly - if the user is logged in. If
|
||||
not logged in, the user is prompted to do so.
|
||||
|
||||
The UI-event-source is a class where the entire system is built upon, it acts as an observable object: another object
|
||||
can register for changes to update when needed.
|
||||
|
||||
### Searching images
|
||||
|
||||
|
@ -114,15 +135,19 @@ Images are fetched from:
|
|||
|
||||
Images are uploaded to Imgur, as their API was way easier to handle. The URL is written into the changes.
|
||||
|
||||
The idea is that once in a while, the images are transferred to wikipedia or that we hook up wikimedia directly (but I need some help in getting their API working).
|
||||
The idea is that once in a while, the images are transferred to wikipedia or that we hook up wikimedia directly (but I
|
||||
need some help in getting their API working).
|
||||
|
||||
### Uploading changes
|
||||
|
||||
In order to avoid lots of small changesets, a changeset is opened and kept open. The changeset number is saved into the users preferences on OSM.
|
||||
In order to avoid lots of small changesets, a changeset is opened and kept open. The changeset number is saved into the
|
||||
users preferences on OSM.
|
||||
|
||||
Whenever a change is made -even adding a single tag - the change is uploaded into this changeset. If that fails, the changeset is probably closed and we open a new changeset.
|
||||
Whenever a change is made -even adding a single tag - the change is uploaded into this changeset. If that fails, the
|
||||
changeset is probably closed and we open a new changeset.
|
||||
|
||||
Note that changesets are closed automatically after one hour of inactivity, so we don't have to worry about closing them.
|
||||
Note that changesets are closed automatically after one hour of inactivity, so we don't have to worry about closing
|
||||
them.
|
||||
|
||||
# Documentation
|
||||
|
||||
|
@ -130,13 +155,11 @@ All documentation can be found in [here](Docs/)
|
|||
|
||||
# Privacy
|
||||
|
||||
Privacy is important, we try to leak as little information as possible.
|
||||
All major personal information is handled by OSM.
|
||||
Geolocation is available on mobile only through the device's GPS location (so no geolocation is sent of to Google).
|
||||
Privacy is important, we try to leak as little information as possible. All major personal information is handled by
|
||||
OSM. Geolocation is available on mobile only through the device's GPS location (so no geolocation is sent of to Google).
|
||||
|
||||
TODO: erase cookies of third party websites and API's
|
||||
|
||||
|
||||
# Attribution and Copyright
|
||||
|
||||
The code is available under GPL; all map data comes from OpenStreetMap (both foreground and background maps).
|
||||
|
|
|
@ -13,7 +13,7 @@ export class Button extends BaseUIElement {
|
|||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = this._text.ConstructElement();
|
||||
if(el === undefined){
|
||||
if (el === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const form = document.createElement("form")
|
||||
|
|
|
@ -12,6 +12,10 @@ export class CenterFlexedElement extends BaseUIElement {
|
|||
return this._html;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return this._html;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const e = document.createElement("div");
|
||||
e.innerHTML = this._html;
|
||||
|
@ -25,8 +29,4 @@ export class CenterFlexedElement extends BaseUIElement {
|
|||
e.style.alignItems = "center";
|
||||
return e;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return this._html;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,22 +16,26 @@ export default class Combine extends BaseUIElement {
|
|||
});
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return this.uiElements.map(el => el.AsMarkdown()).join(this.HasClass("flex-col") ? "\n\n" : " ");
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("span")
|
||||
|
||||
try{
|
||||
try {
|
||||
|
||||
|
||||
for (const subEl of this.uiElements) {
|
||||
if(subEl === undefined || subEl === null){
|
||||
if (subEl === undefined || subEl === null) {
|
||||
continue;
|
||||
}
|
||||
const subHtml = subEl.ConstructElement()
|
||||
if(subHtml !== undefined){
|
||||
if (subHtml !== undefined) {
|
||||
el.appendChild(subHtml)
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
} catch (e) {
|
||||
const domExc = e as DOMException
|
||||
console.error("DOMException: ", domExc.name)
|
||||
el.appendChild(new FixedUiElement("Could not generate this combine!").SetClass("alert").ConstructElement())
|
||||
|
@ -40,8 +44,4 @@ export default class Combine extends BaseUIElement {
|
|||
return el;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return this.uiElements.map(el => el.AsMarkdown()).join(this.HasClass("flex-col") ? "\n\n" : " ");
|
||||
}
|
||||
|
||||
}
|
|
@ -12,14 +12,14 @@ export class FixedUiElement extends BaseUIElement {
|
|||
return this._html;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return this._html;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const e = document.createElement("span")
|
||||
e.innerHTML = this._html
|
||||
return e;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return this._html;
|
||||
}
|
||||
|
||||
}
|
|
@ -10,21 +10,26 @@ export default class Link extends BaseUIElement {
|
|||
|
||||
constructor(embeddedShow: BaseUIElement | string, href: string | UIEventSource<string>, newTab: boolean = false) {
|
||||
super();
|
||||
this._embeddedShow =Translations.W(embeddedShow);
|
||||
this._embeddedShow = Translations.W(embeddedShow);
|
||||
this._href = href;
|
||||
this._newTab = newTab;
|
||||
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
// @ts-ignore
|
||||
return `[${this._embeddedShow.AsMarkdown()}](${this._href.data ?? this._href})`;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const embeddedShow = this._embeddedShow?.ConstructElement();
|
||||
if(embeddedShow === undefined){
|
||||
if (embeddedShow === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const el = document.createElement("a")
|
||||
if(typeof this._href === "string"){
|
||||
if (typeof this._href === "string") {
|
||||
el.href = this._href
|
||||
}else{
|
||||
} else {
|
||||
this._href.addCallbackAndRun(href => {
|
||||
el.href = href;
|
||||
})
|
||||
|
@ -36,9 +41,4 @@ export default class Link extends BaseUIElement {
|
|||
return el;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
// @ts-ignore
|
||||
return `[${this._embeddedShow.AsMarkdown()}](${this._href.data ?? this._href})`;
|
||||
}
|
||||
|
||||
}
|
|
@ -13,15 +13,24 @@ export default class List extends BaseUIElement {
|
|||
.map(Translations.W);
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
if (this._ordered) {
|
||||
return "\n\n" + this.uiElements.map((el, i) => " " + i + ". " + el.AsMarkdown().replace(/\n/g, ' \n')).join("\n") + "\n"
|
||||
} else {
|
||||
return "\n\n" + this.uiElements.map(el => " - " + el.AsMarkdown().replace(/\n/g, ' \n')).join("\n") + "\n"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement(this._ordered ? "ol" : "ul")
|
||||
|
||||
for (const subEl of this.uiElements) {
|
||||
if(subEl === undefined || subEl === null){
|
||||
if (subEl === undefined || subEl === null) {
|
||||
continue;
|
||||
}
|
||||
const subHtml = subEl.ConstructElement()
|
||||
if(subHtml !== undefined){
|
||||
if (subHtml !== undefined) {
|
||||
const item = document.createElement("li")
|
||||
item.appendChild(subHtml)
|
||||
el.appendChild(item)
|
||||
|
@ -31,13 +40,4 @@ export default class List extends BaseUIElement {
|
|||
return el;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
if(this._ordered){
|
||||
return "\n\n"+this.uiElements.map((el, i) => " "+i+". "+el.AsMarkdown().replace(/\n/g, ' \n') ).join("\n") + "\n"
|
||||
}else{
|
||||
return "\n\n"+this.uiElements.map(el => " - "+el.AsMarkdown().replace(/\n/g, ' \n') ).join("\n")+"\n"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -122,9 +122,9 @@ export default class Minimap extends BaseUIElement {
|
|||
);
|
||||
|
||||
if (this._attribution !== undefined) {
|
||||
if(this._attribution === true){
|
||||
if (this._attribution === true) {
|
||||
map.attributionControl.setPrefix(false)
|
||||
}else{
|
||||
} else {
|
||||
map.attributionControl.setPrefix(
|
||||
"<span id='leaflet-attribution'></span>");
|
||||
}
|
||||
|
|
|
@ -18,12 +18,12 @@ import BaseUIElement from "../BaseUIElement";
|
|||
*/
|
||||
export default class ScrollableFullScreen extends UIElement {
|
||||
private static readonly empty = new FixedUiElement("");
|
||||
private static readonly _actor = ScrollableFullScreen.InitActor();
|
||||
private static _currentlyOpen: ScrollableFullScreen;
|
||||
public isShown: UIEventSource<boolean>;
|
||||
private _component: BaseUIElement;
|
||||
private _fullscreencomponent: BaseUIElement;
|
||||
private static readonly _actor = ScrollableFullScreen.InitActor();
|
||||
private _hashToSet: string;
|
||||
private static _currentlyOpen : ScrollableFullScreen;
|
||||
|
||||
constructor(title: ((mode: string) => BaseUIElement), content: ((mode: string) => BaseUIElement),
|
||||
hashToSet: string,
|
||||
|
@ -46,6 +46,23 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
})
|
||||
}
|
||||
|
||||
private static clear() {
|
||||
ScrollableFullScreen.empty.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen");
|
||||
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
|
||||
fs.classList.add("hidden")
|
||||
Hash.hash.setData(undefined);
|
||||
}
|
||||
|
||||
private static InitActor() {
|
||||
Hash.hash.addCallback(hash => {
|
||||
if (hash === undefined || hash === "") {
|
||||
ScrollableFullScreen.clear()
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
InnerRender(): BaseUIElement {
|
||||
return this._component;
|
||||
}
|
||||
|
@ -53,7 +70,7 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
Activate(): void {
|
||||
this.isShown.setData(true)
|
||||
this._fullscreencomponent.AttachTo("fullscreen");
|
||||
if(this._hashToSet != undefined){
|
||||
if (this._hashToSet != undefined) {
|
||||
Hash.hash.setData(this._hashToSet)
|
||||
}
|
||||
const fs = document.getElementById("fullscreen");
|
||||
|
@ -61,7 +78,7 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
fs.classList.remove("hidden")
|
||||
}
|
||||
|
||||
private BuildComponent(title: BaseUIElement, content:BaseUIElement, isShown: UIEventSource<boolean>) {
|
||||
private BuildComponent(title: BaseUIElement, content: BaseUIElement, isShown: UIEventSource<boolean>) {
|
||||
const returnToTheMap =
|
||||
new Combine([
|
||||
Svg.back_svg().SetClass("block md:hidden"),
|
||||
|
@ -86,21 +103,4 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
|
||||
}
|
||||
|
||||
private static clear() {
|
||||
ScrollableFullScreen.empty.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen");
|
||||
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
|
||||
fs.classList.add("hidden")
|
||||
Hash.hash.setData(undefined);
|
||||
}
|
||||
|
||||
private static InitActor(){
|
||||
Hash.hash.addCallback(hash => {
|
||||
if(hash === undefined || hash === ""){
|
||||
ScrollableFullScreen.clear()
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -16,10 +16,10 @@ export class TabbedComponent extends Combine {
|
|||
let element = elements[i];
|
||||
const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i))
|
||||
openedTabSrc.addCallbackAndRun(selected => {
|
||||
if(selected === i){
|
||||
if (selected === i) {
|
||||
header.SetClass("tab-active")
|
||||
header.RemoveClass("tab-non-active")
|
||||
}else{
|
||||
} else {
|
||||
header.SetClass("tab-non-active")
|
||||
header.RemoveClass("tab-active")
|
||||
}
|
||||
|
|
|
@ -1,36 +1,37 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export default class Title extends BaseUIElement{
|
||||
export default class Title extends BaseUIElement {
|
||||
private readonly _embedded: BaseUIElement;
|
||||
private readonly _level: number;
|
||||
constructor(embedded: string | BaseUIElement, level: number =3 ) {
|
||||
|
||||
constructor(embedded: string | BaseUIElement, level: number = 3) {
|
||||
super()
|
||||
this._embedded = Translations.W(embedded);
|
||||
this._level = level;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
const embedded = " " + this._embedded.AsMarkdown() + " ";
|
||||
|
||||
if (this._level == 1) {
|
||||
return "\n" + embedded + "\n" + "=".repeat(embedded.length) + "\n\n"
|
||||
}
|
||||
|
||||
if (this._level == 2) {
|
||||
return "\n" + embedded + "\n" + "-".repeat(embedded.length) + "\n\n"
|
||||
}
|
||||
|
||||
return "\n" + "#".repeat(this._level) + embedded + "\n\n";
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = this._embedded.ConstructElement()
|
||||
if(el === undefined){
|
||||
if (el === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const h = document.createElement("h"+this._level)
|
||||
const h = document.createElement("h" + this._level)
|
||||
h.appendChild(el)
|
||||
return h;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
const embedded = " " +this._embedded.AsMarkdown()+" ";
|
||||
|
||||
if(this._level == 1){
|
||||
return "\n"+embedded+"\n"+"=".repeat(embedded.length)+"\n\n"
|
||||
}
|
||||
|
||||
if(this._level == 2){
|
||||
return "\n"+embedded+"\n"+"-".repeat(embedded.length)+"\n\n"
|
||||
}
|
||||
|
||||
return "\n"+"#".repeat( this._level)+embedded +"\n\n";
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class VariableUiElement extends BaseUIElement {
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class AddNewMarker extends Combine {
|
|||
}
|
||||
}
|
||||
}
|
||||
if(icons.length === 1){
|
||||
if (icons.length === 1) {
|
||||
return icons[0]
|
||||
}
|
||||
icons.push(last)
|
||||
|
|
|
@ -9,8 +9,6 @@ import {DownloadPanel} from "./DownloadPanel";
|
|||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import ExportPDF from "../ExportPDF";
|
||||
import {Browser} from "leaflet";
|
||||
import ie = Browser.ie;
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export default class AllDownloads extends ScrollableFullScreen {
|
||||
|
|
|
@ -25,7 +25,7 @@ export default class Attribution extends Combine {
|
|||
const layoutId = layoutToUse?.data?.id;
|
||||
const now = new Date()
|
||||
// Note: getMonth is zero-index, we want 1-index but with one substracted, so it checks out!
|
||||
const startDate = now.getFullYear()+"-"+now.getMonth()+"-"+now.getDate()
|
||||
const startDate = now.getFullYear() + "-" + now.getMonth() + "-" + now.getDate()
|
||||
const osmChaLink = `https://osmcha.org/?filters=%7B%22comment%22%3A%5B%7B%22label%22%3A%22%23${layoutId}%22%2C%22value%22%3A%22%23${layoutId}%22%7D%5D%2C%22date__gte%22%3A%5B%7B%22label%22%3A%22${startDate}%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22MapComplete%22%2C%22value%22%3A%22MapComplete%22%7D%5D%7D`
|
||||
const stats = new Link(Svg.statistics_ui().SetClass("small-image"), osmChaLink, true)
|
||||
|
||||
|
@ -37,7 +37,6 @@ export default class Attribution extends Combine {
|
|||
const mapillary = new Link(Svg.mapillary_black_ui().SetClass("small-image"), mapillaryLink, true);
|
||||
|
||||
|
||||
|
||||
let editWithJosm = new VariableUiElement(
|
||||
userDetails.map(userDetails => {
|
||||
|
||||
|
@ -45,7 +44,7 @@ export default class Attribution extends Combine {
|
|||
return undefined;
|
||||
}
|
||||
const bounds: any = leafletMap?.data?.getBounds();
|
||||
if(bounds === undefined){
|
||||
if (bounds === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const top = bounds.getNorth();
|
||||
|
|
|
@ -10,7 +10,7 @@ export default class BackgroundSelector extends VariableUiElement {
|
|||
const available = State.state.availableBackgroundLayers.map(available => {
|
||||
const baseLayers: { value: BaseLayer, shown: string }[] = [];
|
||||
for (const i in available) {
|
||||
if(!available.hasOwnProperty(i)){
|
||||
if (!available.hasOwnProperty(i)) {
|
||||
continue;
|
||||
}
|
||||
const layer: BaseLayer = available[i];
|
||||
|
|
|
@ -21,14 +21,14 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
const layoutToUse = State.state.layoutToUse.data;
|
||||
super (
|
||||
super(
|
||||
() => layoutToUse.title.Clone(),
|
||||
() => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails, isShown),
|
||||
"welcome" ,isShown
|
||||
"welcome", isShown
|
||||
)
|
||||
}
|
||||
|
||||
private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource<boolean>): { header: string | BaseUIElement; content: BaseUIElement }[]{
|
||||
private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource<boolean>): { header: string | BaseUIElement; content: BaseUIElement }[] {
|
||||
|
||||
let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown);
|
||||
if (layoutToUse.id === personal.id) {
|
||||
|
|
|
@ -50,7 +50,7 @@ export default class Histogram<T> extends VariableUiElement {
|
|||
let actualAssignColor = undefined;
|
||||
if (assignColor === undefined) {
|
||||
actualAssignColor = fallbackColor;
|
||||
}else{
|
||||
} else {
|
||||
actualAssignColor = (keyValue: string) => {
|
||||
return assignColor(keyValue) ?? fallbackColor(keyValue)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import Translations from "../i18n/Translations";
|
|||
import State from "../../State";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export default class LicensePicker extends DropDown<string>{
|
||||
export default class LicensePicker extends DropDown<string> {
|
||||
|
||||
constructor() {
|
||||
super(Translations.t.image.willBePublished.Clone(),
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class MoreScreen extends Combine {
|
|||
themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
|
||||
}
|
||||
|
||||
return[
|
||||
return [
|
||||
intro,
|
||||
MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle),
|
||||
MoreScreen.createUnofficialThemeList(themeButtonStyle)?.SetClass(themeListStyle),
|
||||
|
@ -43,9 +43,9 @@ export default class MoreScreen extends Combine {
|
|||
];
|
||||
}
|
||||
|
||||
private static createUnofficialThemeList(buttonClass: string): BaseUIElement{
|
||||
private static createUnofficialThemeList(buttonClass: string): BaseUIElement {
|
||||
return new VariableUiElement(State.state.installedThemes.map(customThemes => {
|
||||
const els : BaseUIElement[] = []
|
||||
const els: BaseUIElement[] = []
|
||||
if (customThemes.length > 0) {
|
||||
els.push(Translations.t.general.customThemeIntro.Clone())
|
||||
|
||||
|
@ -62,18 +62,18 @@ export default class MoreScreen extends Combine {
|
|||
let officialThemes = AllKnownLayouts.layoutsList
|
||||
|
||||
let buttons = officialThemes.map((layout) => {
|
||||
if(layout === undefined){
|
||||
if (layout === undefined) {
|
||||
console.trace("Layout is undefined")
|
||||
return undefined
|
||||
}
|
||||
const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass);
|
||||
if(layout.id === personal.id){
|
||||
if (layout.id === personal.id) {
|
||||
return new VariableUiElement(
|
||||
State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
|
||||
.map(csCount => {
|
||||
if(csCount < Constants.userJourney.personalLayoutUnlock){
|
||||
if (csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||
return undefined
|
||||
}else{
|
||||
} else {
|
||||
return button
|
||||
}
|
||||
})
|
||||
|
@ -155,16 +155,14 @@ export default class MoreScreen extends Combine {
|
|||
const params = [
|
||||
["z", currentLocation?.zoom],
|
||||
["lat", currentLocation?.lat],
|
||||
["lon",currentLocation?.lon]
|
||||
["lon", currentLocation?.lon]
|
||||
].filter(part => part[1] !== undefined)
|
||||
.map(part => part[0]+"="+part[1])
|
||||
.map(part => part[0] + "=" + part[1])
|
||||
.join("&")
|
||||
return `${linkPrefix}${params}${linkSuffix}`;
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
let description = Translations.WT(layout.shortDescription).Clone();
|
||||
return new SubtleButton(layout.icon,
|
||||
new Combine([
|
||||
|
|
|
@ -7,7 +7,6 @@ import {SubtleButton} from "../Base/SubtleButton";
|
|||
import Translations from "../i18n/Translations";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Img from "../Base/Img";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
|
@ -62,18 +61,18 @@ export default class PersonalLayersPanel extends VariableUiElement {
|
|||
* @private
|
||||
*/
|
||||
private static CreateLayerToggle(layer: LayerConfig): Toggle {
|
||||
let icon :BaseUIElement =new Combine([ layer.GenerateLeafletStyle(
|
||||
let icon: BaseUIElement = new Combine([layer.GenerateLeafletStyle(
|
||||
new UIEventSource<any>({id: "node/-1"}),
|
||||
false
|
||||
).icon.html]).SetClass("relative")
|
||||
let iconUnset =new Combine([ layer.GenerateLeafletStyle(
|
||||
let iconUnset = new Combine([layer.GenerateLeafletStyle(
|
||||
new UIEventSource<any>({id: "node/-1"}),
|
||||
false
|
||||
).icon.html]).SetClass("relative")
|
||||
|
||||
iconUnset.SetStyle("opacity:0.1")
|
||||
|
||||
let name = layer.name ;
|
||||
let name = layer.name;
|
||||
if (name === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -90,7 +89,7 @@ export default class PersonalLayersPanel extends VariableUiElement {
|
|||
return new Toggle(
|
||||
new SubtleButton(
|
||||
icon,
|
||||
content ),
|
||||
content),
|
||||
new SubtleButton(
|
||||
iconUnset,
|
||||
contentUnselected
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import Locale from "../i18n/Locale";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import { VariableUiElement } from "../Base/VariableUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import { TextField } from "../Input/TextField";
|
||||
import { Geocoding } from "../../Logic/Osm/Geocoding";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import {Geocoding} from "../../Logic/Osm/Geocoding";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
import Combine from "../Base/Combine";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class ShareButton extends BaseUIElement{
|
||||
export default class ShareButton extends BaseUIElement {
|
||||
private _embedded: BaseUIElement;
|
||||
private _shareData: () => { text: string; title: string; url: string };
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ export default class ShareScreen extends Combine {
|
|||
if (includeL) {
|
||||
return [["z", currentLocation.data?.zoom], ["lat", currentLocation.data?.lat], ["lon", currentLocation.data?.lon]]
|
||||
.filter(p => p[1] !== undefined)
|
||||
.map(p => p[0]+"="+p[1])
|
||||
.map(p => p[0] + "=" + p[1])
|
||||
.join("&")
|
||||
} else {
|
||||
return null;
|
||||
|
@ -56,7 +56,7 @@ export default class ShareScreen extends Combine {
|
|||
}, [currentLocation]));
|
||||
|
||||
|
||||
function fLayerToParam(flayer: {isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig}) {
|
||||
function fLayerToParam(flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }) {
|
||||
if (flayer.isDisplayed.data) {
|
||||
return null; // Being displayed is the default
|
||||
}
|
||||
|
@ -123,12 +123,12 @@ export default class ShareScreen extends Combine {
|
|||
optionCheckboxes.push(checkbox);
|
||||
optionParts.push(checkbox.isEnabled.map((isEn) => {
|
||||
if (isEn) {
|
||||
if(swtch.reverse){
|
||||
if (swtch.reverse) {
|
||||
return `${swtch.urlName}=true`
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
if(swtch.reverse){
|
||||
if (swtch.reverse) {
|
||||
return null;
|
||||
}
|
||||
return `${swtch.urlName}=false`
|
||||
|
@ -178,9 +178,7 @@ export default class ShareScreen extends Combine {
|
|||
);
|
||||
|
||||
|
||||
|
||||
|
||||
let editLayout : BaseUIElement= new FixedUiElement("");
|
||||
let editLayout: BaseUIElement = new FixedUiElement("");
|
||||
if ((layoutDefinition !== undefined && State.state?.osmConnection !== undefined)) {
|
||||
editLayout =
|
||||
new VariableUiElement(
|
||||
|
@ -240,7 +238,7 @@ export default class ShareScreen extends Combine {
|
|||
});
|
||||
|
||||
|
||||
super ([
|
||||
super([
|
||||
editLayout,
|
||||
tr.intro.Clone(),
|
||||
link,
|
||||
|
|
|
@ -82,7 +82,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
createNewPoint(tags, location, undefined)
|
||||
} else {
|
||||
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD(way => {
|
||||
createNewPoint(tags, location,<OsmWay> way)
|
||||
createNewPoint(tags, location, <OsmWay>way)
|
||||
return true;
|
||||
})
|
||||
}
|
||||
|
@ -222,7 +222,6 @@ export default class SimpleAddUI extends Toggle {
|
|||
)
|
||||
|
||||
|
||||
|
||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset);
|
||||
|
||||
const cancelButton = new SubtleButton(Svg.close_ui(),
|
||||
|
|
|
@ -21,7 +21,7 @@ export default class ThemeIntroductionPanel extends VariableUiElement {
|
|||
const toTheMap = new SubtleButton(
|
||||
undefined,
|
||||
Translations.t.general.openTheMap.Clone().SetClass("text-xl font-bold w-full text-center")
|
||||
).onClick(() =>{
|
||||
).onClick(() => {
|
||||
isShown.setData(false)
|
||||
}).SetClass("only-on-mobile")
|
||||
|
||||
|
@ -42,7 +42,6 @@ export default class ThemeIntroductionPanel extends VariableUiElement {
|
|||
const welcomeBack = Translations.t.general.welcomeBack.Clone();
|
||||
|
||||
|
||||
|
||||
const loginStatus =
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
|
@ -55,7 +54,7 @@ export default class ThemeIntroductionPanel extends VariableUiElement {
|
|||
)
|
||||
|
||||
|
||||
super(State.state.layoutToUse.map (layout => new Combine([
|
||||
super(State.state.layoutToUse.map(layout => new Combine([
|
||||
layout.description.Clone(),
|
||||
"<br/><br/>",
|
||||
toTheMap,
|
||||
|
|
|
@ -6,31 +6,30 @@ import Translations from "../i18n/Translations";
|
|||
/**
|
||||
* Shows that 'images are uploading', 'all images are uploaded' as relevant...
|
||||
*/
|
||||
export default class UploadFlowStateUI extends VariableUiElement{
|
||||
export default class UploadFlowStateUI extends VariableUiElement {
|
||||
|
||||
|
||||
constructor(queue: UIEventSource<string[]>, failed: UIEventSource<string[]>, success: UIEventSource<string[]>) {
|
||||
const t = Translations.t.image;
|
||||
|
||||
super(
|
||||
|
||||
queue.map(queue => {
|
||||
const failedReasons = failed.data
|
||||
const successCount = success.data.length
|
||||
const pendingCount = queue.length - successCount - failedReasons.length;
|
||||
|
||||
let stateMessages : BaseUIElement[] = []
|
||||
let stateMessages: BaseUIElement[] = []
|
||||
|
||||
if(pendingCount == 1){
|
||||
if (pendingCount == 1) {
|
||||
stateMessages.push(t.uploadingPicture.Clone().SetClass("alert"))
|
||||
}
|
||||
if(pendingCount > 1){
|
||||
stateMessages.push(t.uploadingMultiple.Subs({count: ""+pendingCount}).SetClass("alert"))
|
||||
if (pendingCount > 1) {
|
||||
stateMessages.push(t.uploadingMultiple.Subs({count: "" + pendingCount}).SetClass("alert"))
|
||||
}
|
||||
if(failedReasons.length > 0){
|
||||
if (failedReasons.length > 0) {
|
||||
stateMessages.push(t.uploadFailed.Clone().SetClass("alert"))
|
||||
}
|
||||
if(successCount > 0 && pendingCount == 0){
|
||||
if (successCount > 0 && pendingCount == 0) {
|
||||
stateMessages.push(t.uploadDone.SetClass("thanks"))
|
||||
}
|
||||
|
||||
|
@ -38,8 +37,6 @@ export default class UploadFlowStateUI extends VariableUiElement{
|
|||
|
||||
return stateMessages
|
||||
}, [failed, success])
|
||||
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
|
|
@ -107,10 +107,10 @@ export default class UserBadge extends Toggle {
|
|||
const userIcon =
|
||||
(user.img === undefined ? Svg.osm_logo_ui() : new Img(user.img)).SetClass("rounded-full opacity-0 m-0 p-0 duration-500 w-16 min-width-16 h16 float-left")
|
||||
.onClick(() => {
|
||||
if(usertext.HasClass("w-0")){
|
||||
if (usertext.HasClass("w-0")) {
|
||||
usertext.RemoveClass("w-0")
|
||||
usertext.SetClass("w-min pl-2")
|
||||
}else{
|
||||
} else {
|
||||
usertext.RemoveClass("w-min")
|
||||
usertext.RemoveClass("pl-2")
|
||||
usertext.SetClass("w-0")
|
||||
|
|
|
@ -25,6 +25,7 @@ import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
|||
|
||||
export default class ExportPDF {
|
||||
// dimensions of the map in milimeter
|
||||
public isRunning = new UIEventSource(true)
|
||||
// A4: 297 * 210mm
|
||||
private readonly mapW = 297;
|
||||
private readonly mapH = 210;
|
||||
|
@ -33,8 +34,6 @@ export default class ExportPDF {
|
|||
private readonly _layout: UIEventSource<LayoutConfig>;
|
||||
private _screenhotTaken = false;
|
||||
|
||||
public isRunning = new UIEventSource(true)
|
||||
|
||||
constructor(
|
||||
options: {
|
||||
freeDivId: string,
|
||||
|
|
|
@ -9,7 +9,7 @@ export class AttributedImage extends Combine {
|
|||
constructor(urlSource: string, imgSource: ImageAttributionSource) {
|
||||
urlSource = imgSource.PrepareUrl(urlSource)
|
||||
super([
|
||||
new Img( urlSource),
|
||||
new Img(urlSource),
|
||||
new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())
|
||||
]);
|
||||
this.SetClass('block relative h-full');
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue