Add binoculars theme, auto reformat everything

This commit is contained in:
pietervdvn 2021-09-09 00:05:51 +02:00
parent 38dea806c5
commit 78d6482c88
586 changed files with 115573 additions and 111842 deletions

View file

@ -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",

View file

@ -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

View file

@ -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:

View file

@ -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)
}

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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!

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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!

View file

@ -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, "")

View file

@ -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

View file

@ -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.

View file

@ -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())
}
}

View file

@ -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({

View file

@ -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!

View file

@ -147,7 +147,7 @@ export default class OverpassFeatureSource implements FeatureSource {
}
const bounds = this._leafletMap.data?.getBounds();
if(bounds === undefined){
if (bounds === undefined) {
return;
}

View file

@ -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");

View file

@ -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

View file

@ -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>,

View file

@ -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;

View file

@ -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]
)
)

View file

@ -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];

View file

@ -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 {

View file

@ -58,7 +58,7 @@ export default class FeatureSourceMerger implements FeatureSource {
}
}
if(!somethingChanged){
if (!somethingChanged) {
// We don't bother triggering an update
return;
}

View file

@ -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 },

View file

@ -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)
}

View file

@ -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);

View file

@ -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")
}

View file

@ -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;

View file

@ -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;

View file

@ -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]

View file

@ -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>;
}

View file

@ -1,4 +1,3 @@
import $ from "jquery"
import {LicenseInfo} from "./Wikimedia";
import ImageAttributionSource from "./ImageAttributionSource";
import BaseUIElement from "../../UI/BaseUIElement";

View file

@ -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;

View file

@ -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!

View file

@ -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,

View file

@ -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 {
}
}
)

View file

@ -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()

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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)
}

View file

@ -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)

View file

@ -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();
}

View file

@ -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()}>

View file

@ -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);
});

View file

@ -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)
}
}

View file

@ -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();
}
})

View file

@ -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)) {

View file

@ -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[] {

View file

@ -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}]

View file

@ -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) {

View file

@ -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) {

View file

@ -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 }[]
}

View file

@ -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)

View file

@ -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) {

View file

@ -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()

View file

@ -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) {

View file

@ -1,4 +1,4 @@
import { Utils } from "../Utils";
import {Utils} from "../Utils";
export default class Constants {

View file

@ -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})`

View file

@ -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";

View file

@ -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}]`
}

View file

@ -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;

View file

@ -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

View file

@ -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).

View file

@ -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")

View file

@ -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;
}
}

View file

@ -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" : " ");
}
}

View file

@ -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;
}
}

View file

@ -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})`;
}
}

View file

@ -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"
}
}
}

View file

@ -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>");
}

View file

@ -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;
}
}

View file

@ -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")
}

View file

@ -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";
}
}

View file

@ -1,4 +1,4 @@
import { UIEventSource } from "../../Logic/UIEventSource";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
export class VariableUiElement extends BaseUIElement {

View file

@ -27,7 +27,7 @@ export default class AddNewMarker extends Combine {
}
}
}
if(icons.length === 1){
if (icons.length === 1) {
return icons[0]
}
icons.push(last)

View file

@ -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 {

View file

@ -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();

View file

@ -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];

View file

@ -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) {

View file

@ -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)
}

View file

@ -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(),

View file

@ -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([

View file

@ -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

View file

@ -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";

View file

@ -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 };

View file

@ -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,

View file

@ -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(),

View file

@ -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,

View file

@ -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])
);

View file

@ -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")

View file

@ -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,

View file

@ -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