Merge branch 'refactoring/new-ui' into develop
This commit is contained in:
commit
8e22ae9aee
163 changed files with 4624 additions and 6819 deletions
|
@ -12,12 +12,11 @@ import Combine from "../../UI/Base/Combine";
|
|||
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
import {UIElement} from "../../UI/UIElement";
|
||||
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
|
||||
import SourceConfig from "./SourceConfig";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import SubstitutingTag from "../../Logic/Tags/SubstitutingTag";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
|
||||
export default class LayerConfig {
|
||||
|
||||
|
@ -290,11 +289,11 @@ export default class LayerConfig {
|
|||
|
||||
}
|
||||
|
||||
public GenerateLeafletStyle(tags: UIEventSource<any>, clickable: boolean):
|
||||
public GenerateLeafletStyle(tags: UIEventSource<any>, clickable: boolean, widthHeight= "100%"):
|
||||
{
|
||||
icon:
|
||||
{
|
||||
html: UIElement,
|
||||
html: BaseUIElement,
|
||||
iconSize: [number, number],
|
||||
iconAnchor: [number, number],
|
||||
popupAnchor: [number, number],
|
||||
|
@ -325,7 +324,7 @@ export default class LayerConfig {
|
|||
|
||||
function render(tr: TagRenderingConfig, deflt?: string) {
|
||||
const str = (tr?.GetRenderValue(tags.data)?.txt ?? deflt);
|
||||
return SubstitutedTranslation.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
|
||||
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
|
||||
}
|
||||
|
||||
const iconSize = render(this.iconSize, "40,40,center").split(",");
|
||||
|
@ -361,7 +360,7 @@ export default class LayerConfig {
|
|||
const iconUrlStatic = render(this.icon);
|
||||
const self = this;
|
||||
const mappedHtml = tags.map(tgs => {
|
||||
function genHtmlFromString(sourcePart: string): UIElement {
|
||||
function genHtmlFromString(sourcePart: string): BaseUIElement {
|
||||
if (sourcePart.indexOf("html:") == 0) {
|
||||
// We use § as a replacement for ;
|
||||
const html = sourcePart.substring("html:".length)
|
||||
|
@ -370,7 +369,7 @@ export default class LayerConfig {
|
|||
}
|
||||
|
||||
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
|
||||
let html: UIElement = new FixedUiElement(`<img src="${sourcePart}" style="${style}" />`);
|
||||
let html: BaseUIElement = new FixedUiElement(`<img src="${sourcePart}" style="${style}" />`);
|
||||
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/)
|
||||
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
||||
html = new Combine([
|
||||
|
@ -387,7 +386,7 @@ export default class LayerConfig {
|
|||
const iconUrl = render(self.icon);
|
||||
const rotation = render(self.rotation, "0deg");
|
||||
|
||||
let htmlParts: UIElement[] = [];
|
||||
let htmlParts: BaseUIElement[] = [];
|
||||
let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != ""));
|
||||
for (const sourcePart of sourceParts) {
|
||||
htmlParts.push(genHtmlFromString(sourcePart))
|
||||
|
@ -399,7 +398,7 @@ export default class LayerConfig {
|
|||
continue;
|
||||
}
|
||||
if (iconOverlay.badge) {
|
||||
const badgeParts: UIElement[] = [];
|
||||
const badgeParts: BaseUIElement[] = [];
|
||||
const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != "");
|
||||
|
||||
for (const badgePartStr of partDefs) {
|
||||
|
@ -437,7 +436,7 @@ export default class LayerConfig {
|
|||
} catch (e) {
|
||||
console.error(e, tgs)
|
||||
}
|
||||
return new Combine(htmlParts).Render();
|
||||
return new Combine(htmlParts);
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -152,11 +152,10 @@ export default class LayoutConfig {
|
|||
);
|
||||
}
|
||||
|
||||
const defaultClustering = {
|
||||
this.clustering = {
|
||||
maxZoom: 16,
|
||||
minNeededElements: 500
|
||||
};
|
||||
this.clustering = defaultClustering;
|
||||
if (json.clustering) {
|
||||
this.clustering = {
|
||||
maxZoom: json.clustering.maxZoom ?? 18,
|
||||
|
@ -164,7 +163,7 @@ export default class LayoutConfig {
|
|||
}
|
||||
for (const layer of this.layers) {
|
||||
if (layer.wayHandling !== LayerConfig.WAYHANDLING_CENTER_ONLY) {
|
||||
console.error("WARNING: In order to allow clustering, every layer must be set to CENTER_ONLY. Layer", layer.id, "does not respect this for layout", this.id);
|
||||
console.debug("WARNING: In order to allow clustering, every layer must be set to CENTER_ONLY. Layer", layer.id, "does not respect this for layout", this.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -240,6 +240,46 @@ export default class TagRenderingConfig {
|
|||
return this.question === null && this.condition === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the render values. Will return multiple render values if 'multianswer' is enabled.
|
||||
* The result will equal [GetRenderValue] if not 'multiAnswer'
|
||||
* @param tags
|
||||
* @constructor
|
||||
*/
|
||||
public GetRenderValues(tags: any): Translation[]{
|
||||
if(!this.multiAnswer){
|
||||
return [this.GetRenderValue(tags)]
|
||||
}
|
||||
|
||||
// A flag to check that the freeform key isn't matched multiple times
|
||||
// If it is undefined, it is "used" already, or at least we don't have to check for it anymore
|
||||
let freeformKeyUsed = this.freeform?.key === undefined;
|
||||
// We run over all the mappings first, to check if the mapping matches
|
||||
const applicableMappings: Translation[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => {
|
||||
if (mapping.if === undefined) {
|
||||
return mapping.then;
|
||||
}
|
||||
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
|
||||
if(!freeformKeyUsed){
|
||||
if(mapping.if.usedKeys().indexOf(this.freeform.key) >= 0){
|
||||
// This mapping matches the freeform key - we mark the freeform key to be ignored!
|
||||
freeformKeyUsed = true;
|
||||
}
|
||||
}
|
||||
return mapping.then;
|
||||
}
|
||||
return undefined;
|
||||
}))
|
||||
|
||||
|
||||
|
||||
if (!freeformKeyUsed
|
||||
&& tags[this.freeform.key] !== undefined) {
|
||||
applicableMappings.push(this.render)
|
||||
}
|
||||
return applicableMappings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the correct rendering value (or undefined if not known)
|
||||
* @constructor
|
||||
|
|
|
@ -28,7 +28,16 @@ Note that 'map' will also absorb some changes, e.g. `const someEventSource : UIE
|
|||
An object which receives an UIEventSource is responsible of responding onto changes of this object. This is especially true for UI-components
|
||||
|
||||
UI
|
||||
--
|
||||
--```
|
||||
|
||||
export default class MyComponent {
|
||||
|
||||
constructor(neededParameters, neededUIEventSources) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
|
@ -177,9 +186,22 @@ export default class MyComponent extends Combine {
|
|||
|
||||
```
|
||||
|
||||
Assets
|
||||
------
|
||||
|
||||
### Themes
|
||||
|
||||
Theme and layer configuration files go into /assets/layers and assets/themes
|
||||
|
||||
### Images
|
||||
|
||||
Other files (mostly images that are part of the core of mapcomplete) go into 'assets/svg' and are usable with `Svg.image_file_ui()`. Run `npm run generate:images` if you added a new image
|
||||
|
||||
|
||||
Logic
|
||||
-----
|
||||
|
||||
With the
|
||||
The last part is the business logic of the application, found in 'Logic'. Actors are small objects which react to UIEventSources to update other eventSources.
|
||||
|
||||
State.state is a big singleton object containing a lot of the state of the entire application. That one is a bit a mess
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
Metatags
|
||||
--------
|
||||
|
||||
Metatags
|
||||
==========
|
||||
|
||||
|
||||
|
||||
Metatags are extra tags available, in order to display more data or to give better questions.
|
||||
|
||||
|
@ -7,85 +10,155 @@ The are calculated automatically on every feature when the data arrives in the w
|
|||
|
||||
**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
|
||||
|
||||
### \_lat, \_lon
|
||||
|
||||
Metatags calculated by MapComplete
|
||||
------------------------------------
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
### _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
|
||||
### _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
|
||||
|
||||
|
||||
### _country
|
||||
|
||||
|
||||
### \_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')
|
||||
### _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
|
||||
|
||||
|
||||
### \_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
|
||||
|
||||
### \_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, _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
|
||||
|
||||
|
||||
### _now:date, _now:datetime, _loaded:date, _loaded:_datetime
|
||||
|
||||
|
||||
### \_now:date, \_now:datetime, \_loaded:date, \_loaded:\_datetime
|
||||
|
||||
Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely
|
||||
|
||||
### \_last\_edit:contributor, \_last\_edit:contributor:uid, \_last\_edit:changeset, \_last\_edit:timestamp, \_version\_number
|
||||
|
||||
### _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
|
||||
--------------------------------
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
In the layer object, add a field **calculatedTags**, e.g.:
|
||||
|
||||
"calculatedTags": \[ "\_someKey=javascript-expression", "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator", "\_distanceCloserThen3Km=feat.distanceTo( some\_lon, some\_lat) < 3 ? 'yes' : 'no'" \]
|
||||
- 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.
|
||||
|
||||
The above code will be executed for every feature in the layer. The feature is accessible as **feat** and is an amended geojson object: - **area** contains the surface area (in square meters) of the object - **lat** and **lon** contain the latitude and longitude Some advanced functions are available on **feat** as well:
|
||||
|
||||
* distanceTo
|
||||
* overlapWith
|
||||
* closest
|
||||
* memberships
|
||||
To enable this feature, add a field `calculatedTags` in the layer object, e.g.:
|
||||
|
||||
### 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
|
||||
"calculatedTags": [
|
||||
|
||||
* longitude
|
||||
* latitude
|
||||
"_someKey=javascript-expression",
|
||||
|
||||
### overlapWith
|
||||
"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",
|
||||
|
||||
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
|
||||
"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'"
|
||||
|
||||
* ...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.
|
||||
|
||||
* list of features
|
||||
|
||||
### memberships
|
||||
The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:
|
||||
|
||||
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(';')`
|
||||
|
||||
|
||||
- `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
|
||||
|
||||
### distanceTo
|
||||
|
||||
Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object
|
||||
|
||||
0. longitude
|
||||
1. latitude
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
0. list of features
|
||||
|
||||
### memberships
|
||||
|
||||
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(';')`
|
||||
|
||||
|
||||
Generated from SimpleMetaTagger, ExtraFunction
|
|
@ -60,4 +60,4 @@ Has extra elements to easily input when a POI is opened
|
|||
|
||||
## color
|
||||
|
||||
Shows a color picker
|
||||
Shows a color picker Generated from ValidatedTextField.ts
|
|
@ -1,61 +1 @@
|
|||
### 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()}** or **{func\_name(arg, someotherarg)}**. Note that you _do not_ need to use quotes around your arguments, the comma is enough to seperate 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
|
||||
|
||||
**Example usage:** {all\_tags()}
|
||||
|
||||
### image\_carousel
|
||||
|
||||
Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
|
||||
|
||||
1. **image key/prefix**: The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Default: image
|
||||
2. **smart search**: Also include images given via 'Wikidata', 'wikimedia\_commons' and 'mapillary Default: true
|
||||
|
||||
**Example usage:** {image\_carousel(image,true)}
|
||||
|
||||
### image\_upload
|
||||
|
||||
Creates a button where a user can upload an image to IMGUR
|
||||
|
||||
1. **image-key**: Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added) Default: image
|
||||
|
||||
**Example usage:** {image\_upload(image)}
|
||||
|
||||
### 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
|
||||
|
||||
1. **subjectKey**: The key to use to determine the subject. If specified, the subject will be **tags\[subjectKey\]** Default: name
|
||||
2. **fallback**: The identifier to use, if _tags\[subjectKey\]_ as specified above is not available. This is effectively a fallback value
|
||||
|
||||
**Example usage:** **{reviews()} **for a vanilla review, **{reviews(name, play\_forest)}** to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play\_forest' is used****
|
||||
|
||||
### ****opening\_hours\_table****
|
||||
|
||||
****Creates an opening-hours table. Usage: {opening\_hours\_table(opening\_hours)} to create a table of the tag 'opening\_hours'.
|
||||
|
||||
1. **key**: The tagkey from which the table is constructed. Default: opening\_hours
|
||||
|
||||
**Example usage:** {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)}
|
||||
|
||||
1. **Url**: The URL to load
|
||||
2. **Shorthands**: A list of shorthands, of the format 'shorthandname:path.path.path'. Seperated by ;
|
||||
3. **path**: 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)}
|
||||
|
||||
### share\_link
|
||||
|
||||
Creates a link that (attempts to) open the native 'share'-screen
|
||||
|
||||
1. **url**: The url to share (default: current URL)
|
||||
|
||||
**Example usage:** {share\_link()} to share the current page, {share\_link()} to share the given url****
|
||||
<h3>Special tag renderings</h3> 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 <b>{func_name()}</b> or <b>{func_name(arg, someotherarg)}</b>. Note that you <i>do not</i> need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args <h3>all_tags</h3> Prints all key-value pairs of the object - used for debugging <ol> </ol> <b>Example usage: </b> {all_tags()} <h3>image_carousel</h3> 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) <ol> <li> <b>image key/prefix</b>: The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Default: <span class='literal-code'>image</span> </li> <li> <b>smart search</b>: Also include images given via 'Wikidata', 'wikimedia_commons' and 'mapillary Default: <span class='literal-code'>true</span> </li> </ol> <b>Example usage: </b> {image_carousel(image,true)} <h3>image_upload</h3> Creates a button where a user can upload an image to IMGUR <ol> <li> <b>image-key</b>: Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added) Default: <span class='literal-code'>image</span> </li> </ol> <b>Example usage: </b> {image_upload(image)} <h3>reviews</h3> 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 <ol> <li> <b>subjectKey</b>: The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b> Default: <span class='literal-code'>name</span> </li> <li> <b>fallback</b>: The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value </li> </ol> <b>Example usage: </b> <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 <h3>opening_hours_table</h3> Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'. <ol> <li> <b>key</b>: The tagkey from which the table is constructed. Default: <span class='literal-code'>opening_hours</span> </li> </ol> <b>Example usage: </b> {opening_hours_table(opening_hours)} <h3>live</h3> 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)} <ol> <li> <b>Url</b>: The URL to load </li> <li> <b>Shorthands</b>: A list of shorthands, of the format 'shorthandname:path.path.path'. Seperated by ; </li> <li> <b>path</b>: The path (or shorthand) that should be returned </li> </ol> <b>Example usage: </b> {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)} <h3>share_link</h3> Creates a link that (attempts to) open the native 'share'-screen <ol> <li> <b>url</b>: The url to share (default: current URL) </li> </ol> <b>Example usage: </b> {share_link()} to share the current page, {share_link(<some_url>)} to share the given url Generated from UI/SpecialVisualisations.ts
|
|
@ -33,22 +33,22 @@
|
|||
},
|
||||
{
|
||||
"key": "cyclestreet",
|
||||
"description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "maxspeed",
|
||||
"description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"key": "overtaking:motor_vehicle",
|
||||
"description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "proposed:cyclestreet",
|
||||
"description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.",
|
||||
"description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
|
@ -113,22 +113,22 @@
|
|||
},
|
||||
{
|
||||
"key": "cyclestreet",
|
||||
"description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "maxspeed",
|
||||
"description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"key": "overtaking:motor_vehicle",
|
||||
"description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "proposed:cyclestreet",
|
||||
"description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.",
|
||||
"description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
|
@ -203,22 +203,22 @@
|
|||
},
|
||||
{
|
||||
"key": "cyclestreet",
|
||||
"description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "maxspeed",
|
||||
"description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"key": "overtaking:motor_vehicle",
|
||||
"description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "proposed:cyclestreet",
|
||||
"description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.",
|
||||
"description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
URL-parameters and URL-hash
|
||||
============================
|
||||
|
||||
|
@ -18,125 +19,128 @@ 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.
|
||||
|
||||
custom-css (broken)
|
||||
------------
|
||||
If specified, the custom css from the given link will be loaded additionaly
|
||||
|
||||
|
||||
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_
|
||||
|
||||
layout
|
||||
--------
|
||||
The layout to load into MapComplete
|
||||
|
||||
userlayout
|
||||
------------
|
||||
If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways:
|
||||
|
||||
- The hash of the URL contains a base64-encoded .json-file containing the theme definition
|
||||
- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator
|
||||
- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme
|
||||
The default value is _false_
|
||||
|
||||
layer-control-toggle
|
||||
layer-control-toggle
|
||||
----------------------
|
||||
Whether or not the layer control is shown
|
||||
The default value is _false_
|
||||
|
||||
tab
|
||||
Whether or not the layer control is shown The default value is _false_
|
||||
|
||||
|
||||
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_
|
||||
|
||||
z
|
||||
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
|
||||
---
|
||||
The initial/current zoom level
|
||||
The default value is set by the theme
|
||||
|
||||
lat
|
||||
The initial/current zoom level The default value is _0_
|
||||
|
||||
|
||||
lat
|
||||
-----
|
||||
The initial/current latitude
|
||||
The default value is set by the theme
|
||||
|
||||
lon
|
||||
The initial/current latitude The default value is _0_
|
||||
|
||||
|
||||
lon
|
||||
-----
|
||||
The initial/current longitude of the app
|
||||
The default value is set by the theme
|
||||
|
||||
fs-userbadge
|
||||
The initial/current longitude of the app The default value is _0_
|
||||
|
||||
|
||||
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_
|
||||
|
||||
fs-search
|
||||
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
|
||||
-----------
|
||||
Disables/Enables the search bar
|
||||
The default value is _true_
|
||||
|
||||
fs-layers
|
||||
Disables/Enables the search bar The default value is _true_
|
||||
|
||||
|
||||
fs-layers
|
||||
-----------
|
||||
Disables/Enables the layer control
|
||||
The default value is _true_
|
||||
|
||||
fs-add-new
|
||||
Disables/Enables the layer control The default value is _true_
|
||||
|
||||
|
||||
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_
|
||||
|
||||
fs-welcome-message
|
||||
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
|
||||
--------------------
|
||||
Disables/enables the help menu or welcome message
|
||||
The default value is _true_
|
||||
|
||||
fs-iframe
|
||||
Disables/enables the help menu or welcome message The default value is _true_
|
||||
|
||||
|
||||
fs-iframe
|
||||
-----------
|
||||
Disables/Enables the iframe-popup
|
||||
The default value is _false_
|
||||
|
||||
fs-more-quests
|
||||
Disables/Enables the iframe-popup The default value is _false_
|
||||
|
||||
|
||||
fs-more-quests
|
||||
----------------
|
||||
Disables/Enables the 'More Quests'-tab in the welcome message
|
||||
The default value is _true_
|
||||
|
||||
fs-share-screen
|
||||
Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_
|
||||
|
||||
|
||||
fs-share-screen
|
||||
-----------------
|
||||
Disables/Enables the 'Share-screen'-tab in the welcome message
|
||||
The default value is _true_
|
||||
|
||||
fs-geolocation
|
||||
Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_
|
||||
|
||||
|
||||
fs-geolocation
|
||||
----------------
|
||||
Disables/Enables the geolocation button
|
||||
The default value is _true_
|
||||
|
||||
fs-all-questions
|
||||
Disables/Enables the geolocation button The default value is _true_
|
||||
|
||||
|
||||
fs-all-questions
|
||||
------------------
|
||||
Always show all questions
|
||||
The default value is _false_
|
||||
|
||||
debug
|
||||
Always show all questions The default value is _false_
|
||||
|
||||
|
||||
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_
|
||||
|
||||
|
||||
debug
|
||||
-------
|
||||
If true, shows some extra debugging help such as all the available tags on every object
|
||||
The default value is _false_
|
||||
|
||||
backend
|
||||
If true, shows some extra debugging help such as all the available tags on every object The default value is _false_
|
||||
|
||||
|
||||
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_
|
||||
|
||||
oauth_token
|
||||
-------------
|
||||
Used to complete the login
|
||||
No default value set
|
||||
The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_
|
||||
|
||||
background
|
||||
|
||||
custom-css
|
||||
------------
|
||||
The id of the background layer to start with
|
||||
The default value is _OSM_ (overridden by the theme)
|
||||
|
||||
layer-<layerid>
|
||||
--------------
|
||||
Wether or not layer with layer-id is shown
|
||||
The default value is _true_
|
||||
If specified, the custom css from the given link will be loaded additionaly The default value is __
|
||||
|
||||
|
||||
background
|
||||
------------
|
||||
|
||||
The id of the background layer to start with The default value is _osm_
|
||||
|
||||
|
||||
layer-<layer-id>
|
||||
------------------
|
||||
|
||||
Wether or not the layer with id <layer-id> is shown The default value is _true_ Generated from QueryParameters
|
|
@ -1,5 +1,5 @@
|
|||
import {FixedUiElement} from "./UI/Base/FixedUiElement";
|
||||
import CheckBox from "./UI/Input/CheckBox";
|
||||
import Toggle from "./UI/Input/Toggle";
|
||||
import {Basemap} from "./UI/BigComponents/Basemap";
|
||||
import State from "./State";
|
||||
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
||||
|
@ -21,11 +21,9 @@ import * as L from "leaflet";
|
|||
import Img from "./UI/Base/Img";
|
||||
import UserDetails from "./Logic/Osm/OsmConnection";
|
||||
import Attribution from "./UI/BigComponents/Attribution";
|
||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||
import LayerResetter from "./Logic/Actors/LayerResetter";
|
||||
import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
|
||||
import LayerControlPanel from "./UI/BigComponents/LayerControlPanel";
|
||||
import FeatureSwitched from "./UI/Base/FeatureSwitched";
|
||||
import ShowDataLayer from "./UI/ShowDataLayer";
|
||||
import Hash from "./Logic/Web/Hash";
|
||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||
|
@ -39,9 +37,9 @@ import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
|
|||
import AttributionPanel from "./UI/BigComponents/AttributionPanel";
|
||||
import ContributorCount from "./Logic/ContributorCount";
|
||||
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
|
||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||
import AllKnownLayers from "./Customizations/AllKnownLayers";
|
||||
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||
|
||||
export class InitUiElements {
|
||||
|
||||
|
@ -170,13 +168,14 @@ export class InitUiElements {
|
|||
marker.addTo(State.state.leafletMap.data)
|
||||
});
|
||||
|
||||
const geolocationButton = new FeatureSwitched(
|
||||
const geolocationButton = new Toggle(
|
||||
new MapControlButton(
|
||||
new GeoLocationHandler(
|
||||
State.state.currentGPSLocation,
|
||||
State.state.leafletMap,
|
||||
State.state.layoutToUse
|
||||
)),
|
||||
undefined,
|
||||
State.state.featureSwitchGeolocation);
|
||||
|
||||
const plus = new MapControlButton(
|
||||
|
@ -193,7 +192,7 @@ export class InitUiElements {
|
|||
State.state.locationControl.ping();
|
||||
})
|
||||
|
||||
new Combine([plus, min, geolocationButton].map(el => el.SetClass("m-1")))
|
||||
new Combine([plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1")))
|
||||
.SetClass("flex flex-col")
|
||||
.AttachTo("bottom-right");
|
||||
|
||||
|
@ -211,13 +210,12 @@ export class InitUiElements {
|
|||
|
||||
// Reset the loading message once things are loaded
|
||||
new CenterMessageBox().AttachTo("centermessage");
|
||||
|
||||
// At last, zoom to the needed location if the focus is on an element
|
||||
document.getElementById("centermessage").classList.add("pointer-events-none")
|
||||
|
||||
|
||||
}
|
||||
|
||||
static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>) {
|
||||
static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>): [LayoutConfig, string]{
|
||||
try {
|
||||
let hash = location.hash.substr(1);
|
||||
const layoutFromBase64 = userLayoutParam.data;
|
||||
|
@ -249,7 +247,7 @@ export class InitUiElements {
|
|||
// @ts-ignore
|
||||
const layoutToUse = new LayoutConfig(json, false);
|
||||
userLayoutParam.setData(layoutToUse.id);
|
||||
return layoutToUse;
|
||||
return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];
|
||||
} catch (e) {
|
||||
|
||||
new FixedUiElement("Error: could not parse the custom layout:<br/> " + e).AttachTo("centermessage");
|
||||
|
@ -272,11 +270,10 @@ export class InitUiElements {
|
|||
|
||||
// ?-Button on Desktop, opens panel with close-X.
|
||||
const help = new MapControlButton(Svg.help_svg());
|
||||
new CheckBox(
|
||||
help.onClick(() => isOpened.setData(true))
|
||||
new Toggle(
|
||||
fullOptions
|
||||
.SetClass("welcomeMessage")
|
||||
.onClick(() => {/*Catch the click*/
|
||||
}),
|
||||
.SetClass("welcomeMessage"),
|
||||
help
|
||||
, isOpened
|
||||
).AttachTo("messagesbox");
|
||||
|
@ -307,22 +304,23 @@ export class InitUiElements {
|
|||
)
|
||||
|
||||
;
|
||||
const copyrightButton = new CheckBox(
|
||||
const copyrightButton = new Toggle(
|
||||
copyrightNotice,
|
||||
new MapControlButton(Svg.osm_copyright_svg()),
|
||||
copyrightNotice.isShown
|
||||
).SetClass("p-0.5")
|
||||
).ToggleOnClick()
|
||||
.SetClass("p-0.5")
|
||||
|
||||
const layerControlPanel = new LayerControlPanel(
|
||||
State.state.layerControlIsOpened)
|
||||
.SetClass("block p-1 rounded-full");
|
||||
const layerControlButton = new CheckBox(
|
||||
const layerControlButton = new Toggle(
|
||||
layerControlPanel,
|
||||
new MapControlButton(Svg.layers_svg()),
|
||||
State.state.layerControlIsOpened
|
||||
)
|
||||
).ToggleOnClick()
|
||||
|
||||
const layerControl = new CheckBox(
|
||||
const layerControl = new Toggle(
|
||||
layerControlButton,
|
||||
"",
|
||||
State.state.featureSwitchLayers
|
||||
|
@ -351,9 +349,8 @@ export class InitUiElements {
|
|||
private static InitBaseMap() {
|
||||
|
||||
State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers;
|
||||
State.state.backgroundLayer = QueryParameters.GetQueryParameter("background",
|
||||
State.state.layoutToUse.data.defaultBackgroundId ?? AvailableBaseLayers.osmCarto.id,
|
||||
"The id of the background layer to start with")
|
||||
|
||||
State.state.backgroundLayer = State.state.backgroundLayerId
|
||||
.map((selectedId: string) => {
|
||||
const available = State.state.availableBackgroundLayers.data;
|
||||
for (const layer of available) {
|
||||
|
@ -362,9 +359,8 @@ export class InitUiElements {
|
|||
}
|
||||
}
|
||||
return AvailableBaseLayers.osmCarto;
|
||||
}, [], layer => layer.id);
|
||||
|
||||
|
||||
}, [State.state.availableBackgroundLayers], layer => layer.id);
|
||||
|
||||
new LayerResetter(
|
||||
State.state.backgroundLayer, State.state.locationControl,
|
||||
State.state.availableBackgroundLayers, State.state.layoutToUse.map((layout: LayoutConfig) => layout.defaultBackgroundId));
|
||||
|
|
|
@ -6,6 +6,8 @@ import Svg from "../../Svg";
|
|||
import Img from "../../UI/Base/Img";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||
|
||||
export default class GeoLocationHandler extends UIElement {
|
||||
|
||||
|
@ -52,19 +54,19 @@ export default class GeoLocationHandler extends UIElement {
|
|||
private readonly _previousLocationGrant: UIEventSource<string> = LocalStorageSource.Get("geolocation-permissions");
|
||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||
|
||||
|
||||
private readonly _element: BaseUIElement;
|
||||
|
||||
constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>) {
|
||||
super(undefined);
|
||||
super();
|
||||
this._currentGPSLocation = currentGPSLocation;
|
||||
this._leafletMap = leafletMap;
|
||||
this._layoutToUse = layoutToUse;
|
||||
this._hasLocation = currentGPSLocation.map((location) => location !== undefined);
|
||||
this.dumbMode = false;
|
||||
|
||||
const self = this;
|
||||
import("../../vendor/Leaflet.AccuratePosition.js").then(() => {
|
||||
self.init();
|
||||
})
|
||||
|
||||
const currentPointer = this._isActive.map(isActive => {
|
||||
if (isActive && !self._hasLocation.data) {
|
||||
|
@ -74,62 +76,35 @@ export default class GeoLocationHandler extends UIElement {
|
|||
}, [this._hasLocation])
|
||||
currentPointer.addCallbackAndRun(pointerClass => {
|
||||
self.SetClass(pointerClass);
|
||||
self.Update()
|
||||
})
|
||||
this._element = new VariableUiElement(
|
||||
this._hasLocation.map(hasLocation => {
|
||||
|
||||
if (hasLocation) {
|
||||
return Svg.crosshair_blue_ui()
|
||||
}
|
||||
if (self._isActive.data) {
|
||||
return Svg.crosshair_blue_center_ui();
|
||||
}
|
||||
return Svg.crosshair_ui();
|
||||
}, [this._isActive])
|
||||
);
|
||||
|
||||
this.onClick(() => self.init(true))
|
||||
|
||||
self.init(false)
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
if (this._hasLocation.data) {
|
||||
return Svg.crosshair_blue_img;
|
||||
}
|
||||
if (this._isActive.data) {
|
||||
return Svg.crosshair_blue_center_img;
|
||||
}
|
||||
|
||||
return Svg.crosshair_img;
|
||||
protected InnerRender(): string | BaseUIElement {
|
||||
return this._element
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
private init(askPermission: boolean) {
|
||||
|
||||
const self = this;
|
||||
htmlElement.onclick = function () {
|
||||
self.StartGeolocating();
|
||||
}
|
||||
|
||||
htmlElement.oncontextmenu = function (e) {
|
||||
self.StartGeolocating();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.ListenTo(this._hasLocation);
|
||||
this.ListenTo(this._isActive);
|
||||
this.ListenTo(this._permission);
|
||||
|
||||
const self = this;
|
||||
|
||||
function onAccuratePositionProgress(e) {
|
||||
self._currentGPSLocation.setData({latlng: e.latlng, accuracy: e.accuracy});
|
||||
}
|
||||
|
||||
function onAccuratePositionFound(e) {
|
||||
self._currentGPSLocation.setData({latlng: e.latlng, accuracy: e.accuracy});
|
||||
}
|
||||
|
||||
function onAccuratePositionError(e) {
|
||||
console.log("onerror", e.message);
|
||||
|
||||
}
|
||||
|
||||
const map = this._leafletMap.data;
|
||||
map.on('accuratepositionprogress', onAccuratePositionProgress);
|
||||
map.on('accuratepositionfound', onAccuratePositionFound);
|
||||
map.on('accuratepositionerror', onAccuratePositionError);
|
||||
|
||||
|
||||
this._currentGPSLocation.addCallback((location) => {
|
||||
self._previousLocationGrant.setData("granted");
|
||||
|
@ -178,12 +153,13 @@ export default class GeoLocationHandler extends UIElement {
|
|||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
if (this._previousLocationGrant.data === "granted") {
|
||||
if (askPermission) {
|
||||
self.StartGeolocating(true);
|
||||
} else if (this._previousLocationGrant.data === "granted") {
|
||||
this._previousLocationGrant.setData("");
|
||||
self.StartGeolocating(false);
|
||||
}
|
||||
|
||||
this.HideOnEmpty(true);
|
||||
}
|
||||
|
||||
private locate() {
|
||||
|
@ -211,7 +187,7 @@ export default class GeoLocationHandler extends UIElement {
|
|||
private MoveToCurrentLoction(targetZoom = 16) {
|
||||
const location = this._currentGPSLocation.data;
|
||||
this._lastUserRequest = undefined;
|
||||
|
||||
|
||||
|
||||
if (this._currentGPSLocation.data.latlng[0] === 0 && this._currentGPSLocation.data.latlng[1] === 0) {
|
||||
console.debug("Not moving to GPS-location: it is null island")
|
||||
|
|
|
@ -53,7 +53,6 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
return false;
|
||||
}
|
||||
let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
|
||||
console.debug("overpass source: minzoom is ", minzoom)
|
||||
return location.zoom >= minzoom;
|
||||
}, [layoutToUse]
|
||||
);
|
||||
|
|
|
@ -47,13 +47,13 @@ export default class StrayClickHandler {
|
|||
popupAnchor: [0, -45]
|
||||
})
|
||||
});
|
||||
const popup = L.popup().setContent(uiToShow.Render());
|
||||
const popup = L.popup().setContent("<div id='strayclick'></div>");
|
||||
self._lastMarker.addTo(leafletMap.data);
|
||||
self._lastMarker.bindPopup(popup);
|
||||
|
||||
self._lastMarker.on("click", () => {
|
||||
uiToShow.AttachTo("strayclick")
|
||||
uiToShow.Activate();
|
||||
uiToShow.Update();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import {UIEventSource} from "../UIEventSource";
|
|||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import Locale from "../../UI/i18n/Locale";
|
||||
import {UIElement} from "../../UI/UIElement";
|
||||
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
|
||||
class TitleElement extends UIElement {
|
||||
class TitleElement extends UIEventSource<string> {
|
||||
|
||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||
private readonly _selectedFeature: UIEventSource<any>;
|
||||
private readonly _allElementsStorage: ElementStorage;
|
||||
|
@ -15,42 +15,44 @@ class TitleElement extends UIElement {
|
|||
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
||||
selectedFeature: UIEventSource<any>,
|
||||
allElementsStorage: ElementStorage) {
|
||||
super(layoutToUse);
|
||||
super("MapComplete");
|
||||
|
||||
this._layoutToUse = layoutToUse;
|
||||
this._selectedFeature = selectedFeature;
|
||||
this._allElementsStorage = allElementsStorage;
|
||||
this.ListenTo(Locale.language);
|
||||
this.ListenTo(this._selectedFeature)
|
||||
this.dumbMode = false;
|
||||
}
|
||||
|
||||
this.syncWith(
|
||||
this._selectedFeature.map(
|
||||
selected => {
|
||||
const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ??"MapComplete"
|
||||
|
||||
InnerRender(): string {
|
||||
if(selected === undefined){
|
||||
return defaultTitle
|
||||
}
|
||||
|
||||
const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ?? "MapComplete"
|
||||
const feature = this._selectedFeature.data;
|
||||
|
||||
if (feature === undefined) {
|
||||
return defaultTitle;
|
||||
}
|
||||
const layout = layoutToUse.data;
|
||||
const tags = selected.properties;
|
||||
|
||||
|
||||
const layout = this._layoutToUse.data;
|
||||
const properties = this._selectedFeature.data.properties;
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.title === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
const tagsSource = allElementsStorage.getEventSourceById(tags.id)
|
||||
const title = new TagRenderingAnswer(tagsSource, layer.title)
|
||||
return new Combine([defaultTitle, " | ", title]).ConstructElement().innerText;
|
||||
}
|
||||
}
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.title === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (layer.source.osmTags.matchesProperties(properties)) {
|
||||
const tags = this._allElementsStorage.getEventSourceById(feature.properties.id);
|
||||
if (tags == undefined) {
|
||||
return defaultTitle;
|
||||
return defaultTitle
|
||||
}
|
||||
const title = new TagRenderingAnswer(tags, layer.title)
|
||||
return new Combine([defaultTitle, " | ", title]).Render();
|
||||
}
|
||||
}
|
||||
return defaultTitle;
|
||||
, [Locale.language, layoutToUse]
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -59,14 +61,8 @@ export default class TitleHandler {
|
|||
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
||||
selectedFeature: UIEventSource<any>,
|
||||
allElementsStorage: ElementStorage) {
|
||||
|
||||
selectedFeature.addCallbackAndRun(_ => {
|
||||
const title = new TitleElement(layoutToUse, selectedFeature, allElementsStorage)
|
||||
const d = document.createElement('div');
|
||||
d.innerHTML = title.InnerRender();
|
||||
// We pass everything into a div to strip out images etc...
|
||||
document.title = (d.textContent || d.innerText);
|
||||
new TitleElement(layoutToUse, selectedFeature, allElementsStorage).addCallbackAndRun(title => {
|
||||
document.title = title
|
||||
})
|
||||
|
||||
}
|
||||
}
|
|
@ -1,46 +1,48 @@
|
|||
import {GeoOperations} from "./GeoOperations";
|
||||
import {UIElement} from "../UI/UIElement";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import {Relation} from "./Osm/ExtractRelations";
|
||||
import State from "../State";
|
||||
import {Utils} from "../Utils";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import List from "../UI/Base/List";
|
||||
import Title from "../UI/Base/Title";
|
||||
|
||||
export class ExtraFunction {
|
||||
|
||||
|
||||
static readonly intro = `<h2>Calculating tags with Javascript</h2>
|
||||
static readonly intro = new Combine([
|
||||
new Title("Calculating tags with Javascript", 2),
|
||||
"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:",
|
||||
new List([
|
||||
"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.:",
|
||||
"````",
|
||||
"\"calculatedTags\": [",
|
||||
" \"_someKey=javascript-expression\",",
|
||||
" \"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator\",",
|
||||
" \"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ",
|
||||
" ]",
|
||||
"````",
|
||||
"",
|
||||
"The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:",
|
||||
|
||||
<p>In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. <b>lat</b>, <b>lon</b>, <b>_country</b>), as detailed above.</p>
|
||||
new List([
|
||||
"`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:"
|
||||
]).SetClass("flex-col").AsMarkdown();
|
||||
|
||||
<p>It is also possible to calculate your own tags - but this requires some javascript knowledge. </p>
|
||||
|
||||
Before proceeding, some warnings:
|
||||
|
||||
<ul>
|
||||
<li> DO NOT DO THIS AS BEGINNER</li>
|
||||
<li> <b>Only do this if all other techniques fail</b>. This should <i>not</i> be done to create a rendering effect, only to calculate a specific value</li>
|
||||
<li> <b>THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES</b>. 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.</li>
|
||||
</ul>
|
||||
In the layer object, add a field <b>calculatedTags</b>, e.g.:
|
||||
|
||||
<div class="code">
|
||||
"calculatedTags": [
|
||||
"_someKey=javascript-expression",
|
||||
"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",
|
||||
"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'"
|
||||
]
|
||||
</div>
|
||||
|
||||
The above code will be executed for every feature in the layer. The feature is accessible as <b>feat</b> and is an amended geojson object:
|
||||
- <b>area</b> contains the surface area (in square meters) of the object
|
||||
- <b>lat</b> and <b>lon</b> contain the latitude and longitude
|
||||
|
||||
Some advanced functions are available on <b>feat</b> as well:
|
||||
|
||||
`
|
||||
private static readonly OverlapFunc = new ExtraFunction(
|
||||
"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 <code>{ feat: GeoJSONFeature, overlap: number}[]</code> where <code>overlap</code> is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or <code>undefined</code> 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",
|
||||
["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"],
|
||||
(params, feat) => {
|
||||
return (...layerIds: string[]) => {
|
||||
|
@ -72,7 +74,7 @@ Some advanced functions are available on <b>feat</b> as well:
|
|||
if (typeof arg0 === "string") {
|
||||
// This is an identifier
|
||||
const feature = State.state.allElements.ContainingFeatures.get(arg0);
|
||||
if(feature === undefined){
|
||||
if (feature === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
arg0 = feature;
|
||||
|
@ -138,9 +140,9 @@ Some advanced functions are available on <b>feat</b> as well:
|
|||
|
||||
private static readonly Memberships = new ExtraFunction(
|
||||
"memberships",
|
||||
"Gives a list of <code>{role: string, relation: Relation}</code>-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. " +
|
||||
"\n\n" +
|
||||
"For example: <code>_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')</code>",
|
||||
"For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`",
|
||||
[],
|
||||
(params, _) => {
|
||||
return () => params.relations ?? [];
|
||||
|
@ -167,25 +169,19 @@ Some advanced functions are available on <b>feat</b> as well:
|
|||
}
|
||||
}
|
||||
|
||||
public static HelpText(): UIElement {
|
||||
public static HelpText(): BaseUIElement {
|
||||
|
||||
const elems = []
|
||||
for (const func of ExtraFunction.allFuncs) {
|
||||
elems.push(new Title(func._name, 3),
|
||||
func._doc,
|
||||
new List(func._args, true))
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
ExtraFunction.intro,
|
||||
"<ul>",
|
||||
...ExtraFunction.allFuncs.map(func =>
|
||||
new Combine([
|
||||
"<li>", func._name, "</li>"
|
||||
])
|
||||
),
|
||||
"</ul>",
|
||||
...ExtraFunction.allFuncs.map(func =>
|
||||
new Combine([
|
||||
"<h3>" + func._name + "</h3>",
|
||||
func._doc,
|
||||
"<ul>",
|
||||
...func._args.map(arg => "<li>" + arg + "</li>"),
|
||||
"</ul>"
|
||||
])
|
||||
)
|
||||
new List(ExtraFunction.allFuncs.map(func => func._name)),
|
||||
...elems
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,12 +10,11 @@ import Constants from "../../Models/Constants";
|
|||
|
||||
export class ChangesetHandler {
|
||||
|
||||
public readonly currentChangeset: UIEventSource<string>;
|
||||
private readonly _dryRun: boolean;
|
||||
private readonly userDetails: UIEventSource<UserDetails>;
|
||||
private readonly auth: any;
|
||||
|
||||
public readonly currentChangeset: UIEventSource<string>;
|
||||
|
||||
constructor(layoutName: string, dryRun: boolean, osmConnection: OsmConnection, auth) {
|
||||
this._dryRun = dryRun;
|
||||
this.userDetails = osmConnection.userDetails;
|
||||
|
@ -27,14 +26,34 @@ export class ChangesetHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) {
|
||||
const nodes = response.getElementsByTagName("node");
|
||||
// @ts-ignore
|
||||
for (const node of nodes) {
|
||||
const oldId = parseInt(node.attributes.old_id.value);
|
||||
const newId = parseInt(node.attributes.new_id.value);
|
||||
if (oldId !== undefined && newId !== undefined &&
|
||||
!isNaN(oldId) && !isNaN(newId)) {
|
||||
if (oldId == newId) {
|
||||
continue;
|
||||
}
|
||||
console.log("Rewriting id: ", oldId, "-->", newId);
|
||||
const element = allElements.getEventSourceById("node/" + oldId);
|
||||
element.data.id = "node/" + newId;
|
||||
allElements.addElementById("node/" + newId, element);
|
||||
element.ping();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UploadChangeset(
|
||||
layout: LayoutConfig,
|
||||
allElements: ElementStorage,
|
||||
generateChangeXML: (csid: string) => string,
|
||||
continuation: () => void) {
|
||||
|
||||
if(this.userDetails.data.csCount == 0){
|
||||
continuation: () => void) {
|
||||
|
||||
if (this.userDetails.data.csCount == 0) {
|
||||
// The user became a contributor!
|
||||
this.userDetails.data.csCount = 1;
|
||||
this.userDetails.ping();
|
||||
|
@ -51,7 +70,7 @@ export class ChangesetHandler {
|
|||
|
||||
if (this.currentChangeset.data === undefined || this.currentChangeset.data === "") {
|
||||
// We have to open a new changeset
|
||||
this.OpenChangeset(layout,(csId) => {
|
||||
this.OpenChangeset(layout, (csId) => {
|
||||
this.currentChangeset.setData(csId);
|
||||
const changeset = generateChangeXML(csId);
|
||||
console.log(changeset);
|
||||
|
@ -86,31 +105,61 @@ export class ChangesetHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
|
||||
}) {
|
||||
if (changesetId === undefined) {
|
||||
changesetId = this.currentChangeset.data;
|
||||
}
|
||||
if (changesetId === undefined) {
|
||||
return;
|
||||
}
|
||||
console.log("closing changeset", changesetId);
|
||||
this.currentChangeset.setData("");
|
||||
this.auth.xhr({
|
||||
method: 'PUT',
|
||||
path: '/api/0.6/changeset/' + changesetId + '/close',
|
||||
}, function (err, response) {
|
||||
if (response == null) {
|
||||
|
||||
console.log("err", err);
|
||||
}
|
||||
console.log("Closed changeset ", changesetId)
|
||||
|
||||
if (continuation !== undefined) {
|
||||
continuation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private OpenChangeset(
|
||||
layout : LayoutConfig,
|
||||
layout: LayoutConfig,
|
||||
continuation: (changesetId: string) => void) {
|
||||
|
||||
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
|
||||
|
||||
let surveySource = "";
|
||||
if (State.state.currentGPSLocation.data !== undefined) {
|
||||
surveySource = '<tag k="source" v="survey"/>'
|
||||
}
|
||||
let path = window.location.pathname;
|
||||
path = path.substr(1, path.lastIndexOf("/"));
|
||||
const metadata = [
|
||||
["created_by", `MapComplete ${Constants.vNumber}`],
|
||||
["comment", `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`],
|
||||
["theme", layout.id],
|
||||
["language", Locale.language.data],
|
||||
["host", window.location.host],
|
||||
["path", path],
|
||||
["source", State.state.currentGPSLocation.data !== undefined ? "survey" : undefined],
|
||||
["imagery", State.state.backgroundLayer.data.id],
|
||||
["theme-creator", layout.maintainer]
|
||||
]
|
||||
.filter(kv => (kv[1] ?? "") !== "")
|
||||
.map(kv => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`)
|
||||
.join("\n")
|
||||
|
||||
this.auth.xhr({
|
||||
method: 'PUT',
|
||||
path: '/api/0.6/changeset/create',
|
||||
options: {header: {'Content-Type': 'text/xml'}},
|
||||
content: [`<osm><changeset>`,
|
||||
`<tag k="created_by" v="MapComplete ${Constants.vNumber}" />`,
|
||||
`<tag k="comment" v="Adding data with #MapComplete for theme #${layout.id}${commentExtra}"/>`,
|
||||
`<tag k="theme" v="${layout.id}"/>`,
|
||||
`<tag k="language" v="${Locale.language.data}"/>`,
|
||||
`<tag k="host" v="${escapeHtml(window.location.host)}"/>`,
|
||||
`<tag k="imagery" v="${State.state.backgroundLayer.data.id}"/>`,
|
||||
surveySource,
|
||||
(layout.maintainer ?? "") !== "" ? `<tag k="theme-creator" v="${escapeHtml(layout.maintainer)}"/>` : "",
|
||||
metadata,
|
||||
`</changeset></osm>`].join("")
|
||||
}, function (err, response) {
|
||||
if (response === undefined) {
|
||||
|
@ -147,52 +196,5 @@ export class ChangesetHandler {
|
|||
});
|
||||
}
|
||||
|
||||
public CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
|
||||
}) {
|
||||
if (changesetId === undefined) {
|
||||
changesetId = this.currentChangeset.data;
|
||||
}
|
||||
if (changesetId === undefined) {
|
||||
return;
|
||||
}
|
||||
console.log("closing changeset", changesetId);
|
||||
this.currentChangeset.setData("");
|
||||
this.auth.xhr({
|
||||
method: 'PUT',
|
||||
path: '/api/0.6/changeset/' + changesetId + '/close',
|
||||
}, function (err, response) {
|
||||
if (response == null) {
|
||||
|
||||
console.log("err", err);
|
||||
}
|
||||
console.log("Closed changeset ", changesetId)
|
||||
|
||||
if (continuation !== undefined) {
|
||||
continuation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) {
|
||||
const nodes = response.getElementsByTagName("node");
|
||||
// @ts-ignore
|
||||
for (const node of nodes) {
|
||||
const oldId = parseInt(node.attributes.old_id.value);
|
||||
const newId = parseInt(node.attributes.new_id.value);
|
||||
if (oldId !== undefined && newId !== undefined &&
|
||||
!isNaN(oldId) && !isNaN(newId)) {
|
||||
if(oldId == newId){
|
||||
continue;
|
||||
}
|
||||
console.log("Rewriting id: ", oldId, "-->", newId);
|
||||
const element = allElements.getEventSourceById("node/" + oldId);
|
||||
element.data.id = "node/" + newId;
|
||||
allElements.addElementById("node/" + newId, element);
|
||||
element.ping();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -39,6 +39,7 @@ export class OsmConnection {
|
|||
}
|
||||
public auth;
|
||||
public userDetails: UIEventSource<UserDetails>;
|
||||
public isLoggedIn: UIEventSource<boolean>
|
||||
_dryRun: boolean;
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public changesetHandler: ChangesetHandler;
|
||||
|
@ -64,6 +65,14 @@ export class OsmConnection {
|
|||
|
||||
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(), "userDetails");
|
||||
this.userDetails.data.dryRun = dryRun;
|
||||
const self =this;
|
||||
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
|
||||
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()
|
||||
}
|
||||
});
|
||||
this._dryRun = dryRun;
|
||||
|
||||
this.updateAuthObject();
|
||||
|
@ -215,14 +224,15 @@ export class OsmConnection {
|
|||
});
|
||||
}
|
||||
|
||||
private CheckForMessagesContinuously() {
|
||||
const self = this;
|
||||
window.setTimeout(() => {
|
||||
if (self.userDetails.data.loggedIn) {
|
||||
console.log("Checking for messages")
|
||||
this.AttemptLogin();
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
private CheckForMessagesContinuously(){
|
||||
const self =this;
|
||||
UIEventSource.Chronic(5 * 60 * 1000).addCallback(_ => {
|
||||
if (self.isLoggedIn .data) {
|
||||
console.log("Checking for messages")
|
||||
self.AttemptLogin();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -5,13 +5,15 @@ import {Tag} from "./Tags/Tag";
|
|||
import {Or} from "./Tags/Or";
|
||||
import {Utils} from "../Utils";
|
||||
import opening_hours from "opening_hours";
|
||||
import {UIElement} from "../UI/UIElement";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import Title from "../UI/Base/Title";
|
||||
import {FixedUiElement} from "../UI/Base/FixedUiElement";
|
||||
|
||||
|
||||
const cardinalDirections = {
|
||||
N: 0, NNE: 22.5, NE: 45, ENE: 67.5,
|
||||
E: 90, ESE: 112.5, SE: 135, SSE: 157.5,
|
||||
N: 0, NNE: 22.5, NE: 45, ENE: 67.5,
|
||||
E: 90, ESE: 112.5, SE: 135, SSE: 157.5,
|
||||
S: 180, SSW: 202.5, SW: 225, WSW: 247.5,
|
||||
W: 270, WNW: 292.5, NW: 315, NNW: 337.5
|
||||
}
|
||||
|
@ -31,20 +33,20 @@ export default class SimpleMetaTagger {
|
|||
(feature) => {/*Note: also handled by 'UpdateTagsFromOsmAPI'*/
|
||||
|
||||
const tgs = feature.properties;
|
||||
|
||||
function move(src: string, target: string){
|
||||
if(tgs[src] === undefined){
|
||||
|
||||
function move(src: string, target: string) {
|
||||
if (tgs[src] === undefined) {
|
||||
return;
|
||||
}
|
||||
tgs[target] = tgs[src]
|
||||
delete tgs[src]
|
||||
}
|
||||
|
||||
move("user","_last_edit:contributor")
|
||||
move("uid","_last_edit:contributor:uid")
|
||||
move("changeset","_last_edit:changeset")
|
||||
move("timestamp","_last_edit:timestamp")
|
||||
move("version","_version_number")
|
||||
|
||||
move("user", "_last_edit:contributor")
|
||||
move("uid", "_last_edit:contributor:uid")
|
||||
move("changeset", "_last_edit:changeset")
|
||||
move("timestamp", "_last_edit:timestamp")
|
||||
move("version", "_version_number")
|
||||
}
|
||||
)
|
||||
private static latlon = new SimpleMetaTagger({
|
||||
|
@ -100,9 +102,13 @@ export default class SimpleMetaTagger {
|
|||
|
||||
SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => {
|
||||
try {
|
||||
const oldCountry = feature.properties["_country"];
|
||||
feature.properties["_country"] = countries[0].trim().toLowerCase();
|
||||
const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id);
|
||||
tagsSource.ping();
|
||||
if (oldCountry !== feature.properties["_country"]) {
|
||||
const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id);
|
||||
tagsSource.ping();
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
|
@ -375,28 +381,27 @@ export default class SimpleMetaTagger {
|
|||
SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback)
|
||||
}
|
||||
|
||||
static HelpText(): UIElement {
|
||||
const subElements: UIElement[] = [
|
||||
static HelpText(): BaseUIElement {
|
||||
const subElements: (string | BaseUIElement)[] = [
|
||||
new Combine([
|
||||
"<h2>Metatags</h2>",
|
||||
"<p>Metatags are extra tags available, in order to display more data or to give better questions.</p>",
|
||||
"<p>The are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.</p>",
|
||||
"<p><b>Hint:</b> when using metatags, add the <a href='URL_Parameters.md'>query parameter</a> <code>debug=true</code> to the URL. This will include a box in the popup for features which shows all the properties of the object</p>"
|
||||
])
|
||||
|
||||
new Title("Metatags", 1),
|
||||
"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.",
|
||||
"**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"
|
||||
]).SetClass("flex-col")
|
||||
|
||||
];
|
||||
|
||||
subElements.push(new Title("Metatags calculated by MapComplete", 2))
|
||||
subElements.push(new FixedUiElement("The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme"))
|
||||
for (const metatag of SimpleMetaTagger.metatags) {
|
||||
subElements.push(
|
||||
new Combine([
|
||||
"<h3>", metatag.keys.join(", "), "</h3>",
|
||||
metatag.doc]
|
||||
)
|
||||
new Title(metatag.keys.join(", "), 3),
|
||||
metatag.doc
|
||||
)
|
||||
}
|
||||
|
||||
return new Combine(subElements)
|
||||
return new Combine(subElements).SetClass("flex-col")
|
||||
}
|
||||
|
||||
addMetaTags(features: { feature: any, freshness: Date }[]) {
|
||||
|
|
|
@ -92,9 +92,16 @@ export class UIEventSource<T> {
|
|||
}
|
||||
}
|
||||
|
||||
public map<J>(f: ((T) => J),
|
||||
/**
|
||||
* Monoidal map:
|
||||
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
|
||||
* @param f: The transforming function
|
||||
* @param extraSources: also trigger the update if one of these sources change
|
||||
* @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData
|
||||
*/
|
||||
public map<J>(f: ((t: T) => J),
|
||||
extraSources: UIEventSource<any>[] = [],
|
||||
g: ((J) => T) = undefined): UIEventSource<J> {
|
||||
g: ((j:J, t:T) => T) = undefined): UIEventSource<J> {
|
||||
const self = this;
|
||||
|
||||
const newSource = new UIEventSource<J>(
|
||||
|
@ -113,7 +120,7 @@ export class UIEventSource<T> {
|
|||
|
||||
if (g !== undefined) {
|
||||
newSource.addCallback((latest) => {
|
||||
self.setData(g(latest));
|
||||
self.setData(g(latest, self.data));
|
||||
})
|
||||
}
|
||||
|
||||
|
|
29
Logic/Web/ImageAttributionSource.ts
Normal file
29
Logic/Web/ImageAttributionSource.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {LicenseInfo} from "./Wikimedia";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
|
||||
|
||||
export default abstract class ImageAttributionSource {
|
||||
|
||||
|
||||
private _cache = new Map<string, UIEventSource<LicenseInfo>>()
|
||||
|
||||
GetAttributionFor(url: string): UIEventSource<LicenseInfo> {
|
||||
const cached = this._cache.get(url);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
const src = this.DownloadAttribution(url)
|
||||
this._cache.set(url, src)
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public abstract SourceIcon(backlinkSource?: string) : BaseUIElement;
|
||||
protected abstract DownloadAttribution(url: string): UIEventSource<LicenseInfo>;
|
||||
public PrepareUrl(value: string): string{
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,20 +1,25 @@
|
|||
// @ts-ignore
|
||||
import $ from "jquery"
|
||||
import {LicenseInfo} from "./Wikimedia";
|
||||
import ImageAttributionSource from "./ImageAttributionSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
|
||||
export class Imgur {
|
||||
export class Imgur extends ImageAttributionSource {
|
||||
|
||||
public static readonly singleton = new Imgur();
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static uploadMultiple(
|
||||
title: string, description: string, blobs: FileList,
|
||||
handleSuccessfullUpload: ((imageURL: string) => void),
|
||||
allDone: (() => void),
|
||||
onFail: ((reason: string) => void),
|
||||
offset:number) {
|
||||
offset: number = 0) {
|
||||
|
||||
if(offset === undefined){
|
||||
throw "Offset undefined - not uploading to prevent to much uploads!"
|
||||
}
|
||||
if (blobs.length == offset) {
|
||||
allDone();
|
||||
return;
|
||||
|
@ -35,55 +40,11 @@ export class Imgur {
|
|||
);
|
||||
|
||||
|
||||
}
|
||||
static getDescriptionOfImage(url: string,
|
||||
handleDescription: ((license: LicenseInfo) => void)) {
|
||||
|
||||
const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0];
|
||||
|
||||
const apiUrl = 'https://api.imgur.com/3/image/'+hash;
|
||||
const apiKey = '7070e7167f0a25a';
|
||||
|
||||
const settings = {
|
||||
async: true,
|
||||
crossDomain: true,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
type: 'GET',
|
||||
url: apiUrl,
|
||||
headers: {
|
||||
Authorization: 'Client-ID ' + apiKey,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
// @ts-ignore
|
||||
$.ajax(settings).done(function (response) {
|
||||
const descr: string = response.data.description ?? "";
|
||||
const data: any = {};
|
||||
for (const tag of descr.split("\n")) {
|
||||
const kv = tag.split(":");
|
||||
const k = kv[0];
|
||||
const v = kv[1].replace("\r", "");
|
||||
data[k] = v;
|
||||
}
|
||||
|
||||
|
||||
const licenseInfo = new LicenseInfo();
|
||||
|
||||
licenseInfo.licenseShortName = data.license;
|
||||
licenseInfo.artist = data.author;
|
||||
|
||||
handleDescription(licenseInfo);
|
||||
|
||||
}).fail((reason) => {
|
||||
console.log("Getting metadata from to IMGUR failed", reason)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
static uploadImage(title: string, description: string, blob,
|
||||
handleSuccessfullUpload: ((imageURL: string) => void),
|
||||
onFail: (reason:string) => void) {
|
||||
onFail: (reason: string) => void) {
|
||||
|
||||
const apiUrl = 'https://api.imgur.com/3/image';
|
||||
const apiKey = '7070e7167f0a25a';
|
||||
|
@ -121,4 +82,55 @@ export class Imgur {
|
|||
});
|
||||
}
|
||||
|
||||
SourceIcon(): BaseUIElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected DownloadAttribution(url: string): UIEventSource<LicenseInfo> {
|
||||
const src = new UIEventSource<LicenseInfo>(undefined)
|
||||
|
||||
|
||||
const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0];
|
||||
|
||||
const apiUrl = 'https://api.imgur.com/3/image/' + hash;
|
||||
const apiKey = '7070e7167f0a25a';
|
||||
|
||||
const settings = {
|
||||
async: true,
|
||||
crossDomain: true,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
type: 'GET',
|
||||
url: apiUrl,
|
||||
headers: {
|
||||
Authorization: 'Client-ID ' + apiKey,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
// @ts-ignore
|
||||
$.ajax(settings).done(function (response) {
|
||||
const descr: string = response.data.description ?? "";
|
||||
const data: any = {};
|
||||
for (const tag of descr.split("\n")) {
|
||||
const kv = tag.split(":");
|
||||
const k = kv[0];
|
||||
data[k] = kv[1].replace("\r", "");
|
||||
}
|
||||
|
||||
|
||||
const licenseInfo = new LicenseInfo();
|
||||
|
||||
licenseInfo.licenseShortName = data.license;
|
||||
licenseInfo.artist = data.author;
|
||||
|
||||
src.setData(licenseInfo)
|
||||
|
||||
}).fail((reason) => {
|
||||
console.log("Getting metadata from to IMGUR failed", reason)
|
||||
});
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
}
|
41
Logic/Web/ImgurUploader.ts
Normal file
41
Logic/Web/ImgurUploader.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Imgur} from "./Imgur";
|
||||
|
||||
export default class ImgurUploader {
|
||||
|
||||
public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
private readonly _handleSuccessUrl: (string) => void;
|
||||
|
||||
constructor(handleSuccessUrl: (string) => void) {
|
||||
this._handleSuccessUrl = handleSuccessUrl;
|
||||
}
|
||||
|
||||
public uploadMany(title: string, description: string, files: FileList) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
this.queue.data.push(files.item(i).name)
|
||||
}
|
||||
this.queue.ping()
|
||||
|
||||
const self = this;
|
||||
this.queue.setData([...self.queue.data])
|
||||
Imgur.uploadMultiple(title,
|
||||
description,
|
||||
files,
|
||||
function (url) {
|
||||
console.log("File saved at", url);
|
||||
self.success.setData([...self.success.data, url]);
|
||||
self._handleSuccessUrl(url);
|
||||
},
|
||||
function () {
|
||||
console.log("All uploads completed");
|
||||
},
|
||||
|
||||
function (failReason) {
|
||||
console.log("Upload failed due to ", failReason)
|
||||
self.failed.setData([...self.failed.data, failReason])
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,26 +1,57 @@
|
|||
import $ from "jquery"
|
||||
import {LicenseInfo} from "./Wikimedia";
|
||||
import ImageAttributionSource from "./ImageAttributionSource";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
export class Mapillary {
|
||||
export class Mapillary extends ImageAttributionSource {
|
||||
|
||||
public static readonly singleton = new Mapillary();
|
||||
|
||||
static getDescriptionOfImage(key: string,
|
||||
handleDescription: ((license: LicenseInfo) => void)) {
|
||||
const url = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
|
||||
private constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
const settings = {
|
||||
async: true,
|
||||
type: 'GET',
|
||||
url: url
|
||||
};
|
||||
$.getJSON(url, function(data) {
|
||||
private static ExtractKeyFromURL(value: string) {
|
||||
if (value.startsWith("https://a.mapillary.com")) {
|
||||
return value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1);
|
||||
}
|
||||
const matchApi = value.match(/https?:\/\/images.mapillary.com\/([^/]*)/)
|
||||
if (matchApi !== null) {
|
||||
return matchApi[1];
|
||||
}
|
||||
|
||||
if (value.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) {
|
||||
// Extract the key of the image
|
||||
value = value.substring("https://www.mapillary.com/map/im/".length);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
SourceIcon(backlinkSource?: string): BaseUIElement {
|
||||
return Svg.mapillary_svg();
|
||||
}
|
||||
|
||||
PrepareUrl(value: string): string {
|
||||
const key = Mapillary.ExtractKeyFromURL(value)
|
||||
return `https://images.mapillary.com/${key}/thumb-640.jpg?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
|
||||
}
|
||||
|
||||
protected DownloadAttribution(url: string): UIEventSource<LicenseInfo> {
|
||||
|
||||
const key = Mapillary.ExtractKeyFromURL(url)
|
||||
const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
|
||||
const source = new UIEventSource<LicenseInfo>(undefined)
|
||||
$.getJSON(metadataURL, function (data) {
|
||||
const license = new LicenseInfo();
|
||||
license.artist = data.properties?.username;
|
||||
license.licenseShortName = "CC BY-SA 4.0";
|
||||
license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||
license.attributionRequired = true;
|
||||
handleDescription(license);
|
||||
source.setData(license);
|
||||
})
|
||||
|
||||
return source
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@
|
|||
*/
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import Hash from "./Hash";
|
||||
import {Utils} from "../../Utils";
|
||||
import Title from "../../UI/Base/Title";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
|
||||
export class QueryParameters {
|
||||
|
||||
|
@ -12,6 +15,58 @@ export class QueryParameters {
|
|||
private static defaults = {}
|
||||
|
||||
private static documentation = {}
|
||||
private static QueryParamDocsIntro = "\n" +
|
||||
"URL-parameters and URL-hash\n" +
|
||||
"============================\n" +
|
||||
"\n" +
|
||||
"This document gives an overview of which URL-parameters can be used to influence MapComplete.\n" +
|
||||
"\n" +
|
||||
"What is a URL parameter?\n" +
|
||||
"------------------------\n" +
|
||||
"\n" +
|
||||
"URL-parameters are extra parts of the URL used to set the state.\n" +
|
||||
"\n" +
|
||||
"For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`,\n" +
|
||||
"the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all seperated by `&`, namely:\n" +
|
||||
"\n" +
|
||||
"- The url-parameter `lat` is `51.0` in this instance\n" +
|
||||
"- The url-parameter `lon` is `4.3` in this instance\n" +
|
||||
"- The url-parameter `z` is `5` in this instance\n" +
|
||||
"- The url-parameter `test` is `true` in this instance\n" +
|
||||
"\n" +
|
||||
"Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case."
|
||||
|
||||
public static GetQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource<string> {
|
||||
if (!this.initialized) {
|
||||
this.init();
|
||||
}
|
||||
QueryParameters.documentation[key] = documentation;
|
||||
if (deflt !== undefined) {
|
||||
QueryParameters.defaults[key] = deflt;
|
||||
}
|
||||
if (QueryParameters.knownSources[key] !== undefined) {
|
||||
return QueryParameters.knownSources[key];
|
||||
}
|
||||
QueryParameters.addOrder(key);
|
||||
const source = new UIEventSource<string>(deflt, "&" + key);
|
||||
QueryParameters.knownSources[key] = source;
|
||||
source.addCallback(() => QueryParameters.Serialize())
|
||||
return source;
|
||||
}
|
||||
|
||||
public static GenerateQueryParameterDocs(): string {
|
||||
const docs = [QueryParameters.QueryParamDocsIntro];
|
||||
for (const key in QueryParameters.documentation) {
|
||||
const c = new Combine([
|
||||
new Title(key, 2),
|
||||
QueryParameters.documentation[key],
|
||||
QueryParameters.defaults[key] === undefined ? "No default value set" : `The default value is _${QueryParameters.defaults[key]}_`
|
||||
|
||||
])
|
||||
docs.push(c.AsMarkdown())
|
||||
}
|
||||
return docs.join("\n\n");
|
||||
}
|
||||
|
||||
private static addOrder(key) {
|
||||
if (this.order.indexOf(key) < 0) {
|
||||
|
@ -25,7 +80,11 @@ export class QueryParameters {
|
|||
return;
|
||||
}
|
||||
this.initialized = true;
|
||||
|
||||
|
||||
if (Utils.runningFromConsole) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window?.location?.search) {
|
||||
const params = window.location.search.substr(1).split("&");
|
||||
for (const param of params) {
|
||||
|
@ -38,7 +97,7 @@ export class QueryParameters {
|
|||
QueryParameters.knownSources[key] = source;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window["mapcomplete_query_parameter_overview"] = () => {
|
||||
console.log(QueryParameters.GenerateQueryParameterDocs())
|
||||
}
|
||||
|
@ -50,7 +109,7 @@ export class QueryParameters {
|
|||
if (QueryParameters.knownSources[key]?.data === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (QueryParameters.knownSources[key].data === "undefined") {
|
||||
continue;
|
||||
}
|
||||
|
@ -62,41 +121,8 @@ export class QueryParameters {
|
|||
parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data))
|
||||
}
|
||||
// Don't pollute the history every time a parameter changes
|
||||
|
||||
|
||||
history.replaceState(null, "", "?" + parts.join("&") + Hash.Current());
|
||||
|
||||
}
|
||||
|
||||
public static GetQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource<string> {
|
||||
if(!this.initialized){
|
||||
this.init();
|
||||
}
|
||||
QueryParameters.documentation[key] = documentation;
|
||||
if (deflt !== undefined) {
|
||||
QueryParameters.defaults[key] = deflt;
|
||||
}
|
||||
if (QueryParameters.knownSources[key] !== undefined) {
|
||||
return QueryParameters.knownSources[key];
|
||||
}
|
||||
QueryParameters.addOrder(key);
|
||||
const source = new UIEventSource<string>(deflt, "&"+key);
|
||||
QueryParameters.knownSources[key] = source;
|
||||
source.addCallback(() => QueryParameters.Serialize())
|
||||
return source;
|
||||
}
|
||||
|
||||
public static GenerateQueryParameterDocs(): string {
|
||||
const docs = [];
|
||||
for (const key in QueryParameters.documentation) {
|
||||
docs.push([
|
||||
" "+key+" ",
|
||||
"-".repeat(key.length + 2),
|
||||
QueryParameters.documentation[key],
|
||||
QueryParameters.defaults[key] === undefined ? "No default value set" : `The default value is _${QueryParameters.defaults[key]}_`
|
||||
|
||||
].join("\n"))
|
||||
}
|
||||
return docs.join("\n\n");
|
||||
}
|
||||
|
||||
}
|
|
@ -1,47 +1,28 @@
|
|||
import * as $ from "jquery"
|
||||
import ImageAttributionSource from "./ImageAttributionSource";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import Svg from "../../Svg";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import Link from "../../UI/Base/Link";
|
||||
|
||||
/**
|
||||
* This module provides endpoints for wikipedia/wikimedia and others
|
||||
*/
|
||||
export class Wikimedia {
|
||||
export class Wikimedia extends ImageAttributionSource {
|
||||
|
||||
|
||||
public static readonly singleton = new Wikimedia();
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
private static knownLicenses = {};
|
||||
|
||||
static ImageNameToUrl(filename: string, width: number = 500, height: number = 200): string {
|
||||
filename = encodeURIComponent(filename);
|
||||
return "https://commons.wikimedia.org/wiki/Special:FilePath/" + filename + "?width=" + width + "&height=" + height;
|
||||
}
|
||||
|
||||
static LicenseData(filename: string, handle: ((LicenseInfo) => void)): void {
|
||||
if (filename in this.knownLicenses) {
|
||||
return this.knownLicenses[filename];
|
||||
}
|
||||
if (filename === "") {
|
||||
return;
|
||||
}
|
||||
const url = "https://en.wikipedia.org/w/" +
|
||||
"api.php?action=query&prop=imageinfo&iiprop=extmetadata&" +
|
||||
"titles=" + filename +
|
||||
"&format=json&origin=*";
|
||||
$.getJSON(url, function (data) {
|
||||
const licenseInfo = new LicenseInfo();
|
||||
const license = data.query.pages[-1].imageinfo[0].extmetadata;
|
||||
|
||||
licenseInfo.artist = license.Artist?.value;
|
||||
licenseInfo.license = license.License?.value;
|
||||
licenseInfo.copyrighted = license.Copyrighted?.value;
|
||||
licenseInfo.attributionRequired = license.AttributionRequired?.value;
|
||||
licenseInfo.usageTerms = license.UsageTerms?.value;
|
||||
licenseInfo.licenseShortName = license.LicenseShortName?.value;
|
||||
licenseInfo.credit = license.Credit?.value;
|
||||
licenseInfo.description = license.ImageDescription?.value;
|
||||
|
||||
Wikimedia.knownLicenses[filename] = licenseInfo;
|
||||
handle(licenseInfo);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
static GetCategoryFiles(categoryName: string, handleCategory: ((ImagesInCategory: ImagesInCategory) => void),
|
||||
alreadyLoaded = 0,
|
||||
continueParameter: { k: string, param: string } = undefined) {
|
||||
|
@ -111,6 +92,71 @@ export class Wikimedia {
|
|||
});
|
||||
}
|
||||
|
||||
private static ExtractFileName(url: string) {
|
||||
if (!url.startsWith("http")) {
|
||||
return url;
|
||||
}
|
||||
const path = new URL(url).pathname
|
||||
return path.substring(path.lastIndexOf("/") + 1);
|
||||
|
||||
}
|
||||
|
||||
SourceIcon(backlink: string): BaseUIElement {
|
||||
const img = Svg.wikimedia_commons_white_svg()
|
||||
.SetStyle("width:2em;height: 2em");
|
||||
if (backlink === undefined) {
|
||||
return img
|
||||
}
|
||||
|
||||
|
||||
return new Link(Svg.wikimedia_commons_white_img,
|
||||
`https://commons.wikimedia.org/wiki/${backlink}`, true)
|
||||
|
||||
|
||||
}
|
||||
|
||||
PrepareUrl(value: string): string {
|
||||
|
||||
if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
return value;
|
||||
}
|
||||
return Wikimedia.ImageNameToUrl(value, 500, 400)
|
||||
.replace(/'/g, '%27');
|
||||
}
|
||||
|
||||
protected DownloadAttribution(filename: string): UIEventSource<LicenseInfo> {
|
||||
|
||||
const source = new UIEventSource<LicenseInfo>(undefined);
|
||||
|
||||
filename = Wikimedia.ExtractFileName(filename)
|
||||
|
||||
if (filename === "") {
|
||||
return source;
|
||||
}
|
||||
|
||||
const url = "https://en.wikipedia.org/w/" +
|
||||
"api.php?action=query&prop=imageinfo&iiprop=extmetadata&" +
|
||||
"titles=" + filename +
|
||||
"&format=json&origin=*";
|
||||
console.log("Getting attribution at ", url)
|
||||
$.getJSON(url, function (data) {
|
||||
const licenseInfo = new LicenseInfo();
|
||||
const license = data.query.pages[-1].imageinfo[0].extmetadata;
|
||||
|
||||
licenseInfo.artist = license.Artist?.value;
|
||||
licenseInfo.license = license.License?.value;
|
||||
licenseInfo.copyrighted = license.Copyrighted?.value;
|
||||
licenseInfo.attributionRequired = license.AttributionRequired?.value;
|
||||
licenseInfo.usageTerms = license.UsageTerms?.value;
|
||||
licenseInfo.licenseShortName = license.LicenseShortName?.value;
|
||||
licenseInfo.credit = license.Credit?.value;
|
||||
licenseInfo.description = license.ImageDescription?.value;
|
||||
source.setData(licenseInfo);
|
||||
});
|
||||
return source;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,10 @@ import { Utils } from "../Utils";
|
|||
|
||||
export default class Constants {
|
||||
|
||||
public static vNumber = "0.7.5b";
|
||||
public static vNumber = "0.8.0-rc0";
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
addNewPointsUnlock: 0,
|
||||
moreScreenUnlock: 1,
|
||||
personalLayoutUnlock: 15,
|
||||
historyLinkVisible: 20,
|
||||
|
|
25
State.ts
25
State.ts
|
@ -70,10 +70,6 @@ export default class State {
|
|||
readonly layerDef: LayerConfig;
|
||||
}[]>([])
|
||||
|
||||
/**
|
||||
* The message that should be shown at the center of the screen
|
||||
*/
|
||||
public readonly centerMessage = new UIEventSource<string>("");
|
||||
|
||||
/**
|
||||
The latest element that was selected
|
||||
|
@ -106,6 +102,8 @@ export default class State {
|
|||
*/
|
||||
public readonly locationControl = new UIEventSource<Loc>(undefined);
|
||||
public backgroundLayer;
|
||||
public readonly backgroundLayerId: UIEventSource<string>;
|
||||
|
||||
/* Last location where a click was registered
|
||||
*/
|
||||
public readonly LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined)
|
||||
|
@ -127,7 +125,7 @@ export default class State {
|
|||
public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "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 >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map<number>(
|
||||
str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n
|
||||
);
|
||||
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
const self = this;
|
||||
|
||||
|
@ -214,8 +212,25 @@ export default class State {
|
|||
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'")
|
||||
|
||||
}
|
||||
{
|
||||
// Some other feature switches
|
||||
const customCssQP = QueryParameters.GetQueryParameter("custom-css", "", "If specified, the custom css from the given link will be loaded additionaly");
|
||||
if (customCssQP.data !== undefined && customCssQP.data !== "") {
|
||||
Utils.LoadCustomCss(customCssQP.data);
|
||||
}
|
||||
|
||||
|
||||
this.backgroundLayerId = QueryParameters.GetQueryParameter("background",
|
||||
layoutToUse?.defaultBackgroundId ?? "osm",
|
||||
"The id of the background layer to start with")
|
||||
|
||||
}
|
||||
|
||||
|
||||
if(Utils.runningFromConsole){
|
||||
return;
|
||||
}
|
||||
|
||||
this.osmConnection = new OsmConnection(
|
||||
this.featureSwitchIsTesting.data,
|
||||
QueryParameters.GetQueryParameter("oauth_token", undefined,
|
||||
|
|
126
Svg.ts
126
Svg.ts
File diff suppressed because one or more lines are too long
|
@ -1,38 +1,29 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Locale from "../i18n/Locale";
|
||||
import Translations from "../i18n/Translations";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class Button extends UIElement {
|
||||
private _text: UIElement;
|
||||
export class Button extends BaseUIElement {
|
||||
private _text: BaseUIElement;
|
||||
private _onclick: () => void;
|
||||
private _clss: string;
|
||||
|
||||
constructor(text: string | UIElement, onclick: (() => void), clss: string = "") {
|
||||
super(Locale.language);
|
||||
constructor(text: string | UIElement, onclick: (() => void)) {
|
||||
super();
|
||||
this._text = Translations.W(text);
|
||||
this._onclick = onclick;
|
||||
if (clss !== "") {
|
||||
|
||||
this._clss = "class='" + clss + "'";
|
||||
}else{
|
||||
this._clss = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
return "<form>" +
|
||||
"<button id='button-"+this.id+"' type='button' "+this._clss+">" + this._text.Render() + "</button>" +
|
||||
"</form>";
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self = this;
|
||||
document.getElementById("button-"+this.id).onclick = function(){
|
||||
self._onclick();
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = this._text.ConstructElement();
|
||||
if(el === undefined){
|
||||
return undefined;
|
||||
}
|
||||
const form = document.createElement("form")
|
||||
const button = document.createElement("button")
|
||||
button.type = "button"
|
||||
button.appendChild(el)
|
||||
button.onclick = this._onclick
|
||||
form.appendChild(button)
|
||||
return form;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import {Utils} from "../../Utils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class Combine extends UIElement {
|
||||
private readonly uiElements: UIElement[];
|
||||
export default class Combine extends BaseUIElement {
|
||||
private readonly uiElements: BaseUIElement[];
|
||||
|
||||
constructor(uiElements: (string | UIElement)[]) {
|
||||
constructor(uiElements: (string | BaseUIElement)[]) {
|
||||
super();
|
||||
this.uiElements = Utils.NoNull(uiElements)
|
||||
.map(el => {
|
||||
|
@ -15,18 +15,33 @@ export default class Combine extends UIElement {
|
|||
return el;
|
||||
});
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("span")
|
||||
|
||||
InnerRender(): string {
|
||||
return this.uiElements.map(ui => {
|
||||
if(ui === undefined || ui === null){
|
||||
return "";
|
||||
try{
|
||||
|
||||
|
||||
for (const subEl of this.uiElements) {
|
||||
if(subEl === undefined || subEl === null){
|
||||
continue;
|
||||
}
|
||||
if(ui.Render === undefined){
|
||||
console.error("Not a UI-element", ui);
|
||||
return "";
|
||||
const subHtml = subEl.ConstructElement()
|
||||
if(subHtml !== undefined){
|
||||
el.appendChild(subHtml)
|
||||
}
|
||||
return ui.Render();
|
||||
}).join("");
|
||||
}
|
||||
}catch(e){
|
||||
const domExc = e as DOMException
|
||||
console.error("DOMException: ", domExc.name)
|
||||
el.appendChild(new FixedUiElement("Could not generate this combine!").SetClass("alert").ConstructElement())
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return this.uiElements.map(el => el.AsMarkdown()).join(this.HasClass("flex-col") ? "\n\n" : " ");
|
||||
}
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export default class FeatureSwitched extends UIElement{
|
||||
private readonly _upstream: UIElement;
|
||||
private readonly _swtch: UIEventSource<boolean>;
|
||||
|
||||
constructor(upstream :UIElement,
|
||||
swtch: UIEventSource<boolean>) {
|
||||
super(swtch);
|
||||
this._upstream = upstream;
|
||||
this._swtch = swtch;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
if(this._swtch.data){
|
||||
return this._upstream.Render();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,25 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class FixedUiElement extends UIElement {
|
||||
export class FixedUiElement extends BaseUIElement {
|
||||
private _html: string;
|
||||
|
||||
constructor(html: string) {
|
||||
super(undefined);
|
||||
super();
|
||||
this._html = html ?? "";
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return this._html;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const e = document.createElement("span")
|
||||
e.innerHTML = this._html
|
||||
return e;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
return this._html;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,19 +1,41 @@
|
|||
import Constants from "../../Models/Constants";
|
||||
import {Utils} from "../../Utils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class Img {
|
||||
export default class Img extends BaseUIElement {
|
||||
private _src: string;
|
||||
private readonly _rawSvg: boolean;
|
||||
|
||||
public static runningFromConsole = false;
|
||||
constructor(src: string, rawSvg = false) {
|
||||
super();
|
||||
this._src = src;
|
||||
this._rawSvg = rawSvg;
|
||||
}
|
||||
|
||||
static AsData(source:string){
|
||||
if(Utils.runningFromConsole){
|
||||
return source;
|
||||
}
|
||||
return `data:image/svg+xml;base64,${(btoa(source))}`;
|
||||
}
|
||||
static AsData(source: string) {
|
||||
if (Utils.runningFromConsole) {
|
||||
return source;
|
||||
}
|
||||
return `data:image/svg+xml;base64,${(btoa(source))}`;
|
||||
}
|
||||
|
||||
static AsImageElement(source: string, css_class: string = "", style=""): string{
|
||||
static AsImageElement(source: string, css_class: string = "", style = ""): string {
|
||||
return `<img class="${css_class}" style="${style}" alt="" src="${Img.AsData(source)}">`;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
|
||||
if (this._rawSvg) {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = this._src
|
||||
return e;
|
||||
}
|
||||
|
||||
const el = document.createElement("img")
|
||||
el.src = this._src;
|
||||
el.onload = () => {
|
||||
el.style.opacity = "1"
|
||||
}
|
||||
return el;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
|
||||
export default class LazyElement extends UIElement {
|
||||
|
||||
|
||||
public Activate: () => void;
|
||||
private _content: UIElement = undefined;
|
||||
private readonly _loadingContent: string;
|
||||
|
||||
constructor(content: (() => UIElement), loadingContent = "Rendering...") {
|
||||
super();
|
||||
this._loadingContent = loadingContent;
|
||||
this.dumbMode = false;
|
||||
const self = this;
|
||||
this.Activate = () => {
|
||||
if (this._content === undefined) {
|
||||
self._content = content();
|
||||
}
|
||||
self.Update();
|
||||
// @ts-ignore
|
||||
if (this._content.Activate) {
|
||||
// THis is ugly - I know
|
||||
// @ts-ignore
|
||||
this._content.Activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
if (this._content === undefined) {
|
||||
return this._loadingContent;
|
||||
}
|
||||
return this._content.Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,24 +1,44 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
|
||||
export default class Link extends UIElement {
|
||||
private readonly _embeddedShow: UIElement;
|
||||
private readonly _target: string;
|
||||
private readonly _newTab: string;
|
||||
export default class Link extends BaseUIElement {
|
||||
private readonly _href: string | UIEventSource<string>;
|
||||
private readonly _embeddedShow: BaseUIElement;
|
||||
private readonly _newTab: boolean;
|
||||
|
||||
constructor(embeddedShow: UIElement | string, target: string, newTab: boolean = false) {
|
||||
constructor(embeddedShow: BaseUIElement | string, href: string | UIEventSource<string>, newTab: boolean = false) {
|
||||
super();
|
||||
this._embeddedShow = Translations.W(embeddedShow);
|
||||
this._target = target;
|
||||
this._newTab = "";
|
||||
if (newTab) {
|
||||
this._newTab = "target='_blank'"
|
||||
}
|
||||
this._embeddedShow =Translations.W(embeddedShow);
|
||||
this._href = href;
|
||||
this._newTab = newTab;
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return `<a href="${this._target}" ${this._newTab}>${this._embeddedShow.Render()}</a>`;
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const embeddedShow = this._embeddedShow?.ConstructElement();
|
||||
if(embeddedShow === undefined){
|
||||
return undefined;
|
||||
}
|
||||
const el = document.createElement("a")
|
||||
if(typeof this._href === "string"){
|
||||
el.href = this._href
|
||||
}else{
|
||||
this._href.addCallbackAndRun(href => {
|
||||
el.href = href;
|
||||
})
|
||||
}
|
||||
if (this._newTab) {
|
||||
el.target = "_blank"
|
||||
}
|
||||
el.appendChild(embeddedShow)
|
||||
return el;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
// @ts-ignore
|
||||
return `[${this._embeddedShow.AsMarkdown()}](${this._href.data ?? this._href})`;
|
||||
}
|
||||
|
||||
}
|
43
UI/Base/List.ts
Normal file
43
UI/Base/List.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import {Utils} from "../../Utils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export default class List extends BaseUIElement {
|
||||
private readonly uiElements: BaseUIElement[];
|
||||
private readonly _ordered: boolean;
|
||||
|
||||
constructor(uiElements: (string | BaseUIElement)[], ordered = false) {
|
||||
super();
|
||||
this._ordered = ordered;
|
||||
this.uiElements = Utils.NoNull(uiElements)
|
||||
.map(Translations.W);
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement(this._ordered ? "ol" : "ul")
|
||||
|
||||
for (const subEl of this.uiElements) {
|
||||
if(subEl === undefined || subEl === null){
|
||||
continue;
|
||||
}
|
||||
const subHtml = subEl.ConstructElement()
|
||||
if(subHtml !== undefined){
|
||||
const item = document.createElement("li")
|
||||
item.appendChild(subHtml)
|
||||
el.appendChild(item)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
|
||||
export default class Ornament extends UIElement {
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
|
||||
export default class PageSplit extends UIElement{
|
||||
private _left: UIElement;
|
||||
private _right: UIElement;
|
||||
private _leftPercentage: number;
|
||||
|
||||
constructor(left: UIElement, right:UIElement,
|
||||
leftPercentage: number = 50) {
|
||||
super();
|
||||
this._left = left;
|
||||
this._right = right;
|
||||
this._leftPercentage = leftPercentage;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return `<span class="page-split" style="height: min-content"><span style="flex:0 0 ${this._leftPercentage}%">${this._left.Render()}</span><span style="flex: 0 0 ${100-this._leftPercentage}%">${this._right.Render()}</span></span>`;
|
||||
}
|
||||
|
||||
}
|
|
@ -5,20 +5,27 @@ import Ornament from "./Ornament";
|
|||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
/**
|
||||
* Wraps some contents into a panel that scrolls the content _under_ the title
|
||||
*
|
||||
* The scrollableFullScreen is a bit of a peculiar component:
|
||||
* - It shows a title and some contents, constructed from the respective functions passed into the constructor
|
||||
* - When the element is 'activated', one clone of title+contents is attached to the fullscreen
|
||||
* - The element itself will - upon rendering - also show the title and contents (allthough it'll be a different clone)
|
||||
*
|
||||
*
|
||||
*/
|
||||
export default class ScrollableFullScreen extends UIElement {
|
||||
private static readonly empty = new FixedUiElement("");
|
||||
public isShown: UIEventSource<boolean>;
|
||||
private _component: UIElement;
|
||||
private _fullscreencomponent: UIElement;
|
||||
private _component: BaseUIElement;
|
||||
private _fullscreencomponent: BaseUIElement;
|
||||
private static readonly _actor = ScrollableFullScreen.InitActor();
|
||||
private _hashToSet: string;
|
||||
private static _currentlyOpen : ScrollableFullScreen;
|
||||
|
||||
constructor(title: ((mode: string) => UIElement), content: ((mode: string) => UIElement),
|
||||
constructor(title: ((mode: string) => BaseUIElement), content: ((mode: string) => BaseUIElement),
|
||||
hashToSet: string,
|
||||
isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
) {
|
||||
|
@ -29,7 +36,6 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown)
|
||||
.SetClass("hidden md:block");
|
||||
this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown);
|
||||
this.dumbMode = false;
|
||||
const self = this;
|
||||
isShown.addCallback(isShown => {
|
||||
if (isShown) {
|
||||
|
@ -40,8 +46,8 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
})
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._component.Render();
|
||||
InnerRender(): BaseUIElement {
|
||||
return this._component;
|
||||
}
|
||||
|
||||
Activate(): void {
|
||||
|
@ -55,7 +61,7 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
fs.classList.remove("hidden")
|
||||
}
|
||||
|
||||
private BuildComponent(title: UIElement, content: UIElement, isShown: UIEventSource<boolean>) {
|
||||
private BuildComponent(title: BaseUIElement, content:BaseUIElement, isShown: UIEventSource<boolean>) {
|
||||
const returnToTheMap =
|
||||
new Combine([
|
||||
Svg.back_svg().SetClass("block md:hidden"),
|
||||
|
|
|
@ -1,57 +1,58 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Combine from "./Combine";
|
||||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Link from "./Link";
|
||||
import Img from "./Img";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
|
||||
|
||||
export class SubtleButton extends Combine {
|
||||
export class SubtleButton extends UIElement {
|
||||
|
||||
constructor(imageUrl: string | UIElement, message: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined) {
|
||||
super(SubtleButton.generateContent(imageUrl, message, linkTo));
|
||||
private readonly _element: BaseUIElement
|
||||
|
||||
constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, linkTo: { url: string | UIEventSource<string>, newTab?: boolean } = undefined) {
|
||||
super();
|
||||
this._element = SubtleButton.generateContent(imageUrl, message, linkTo)
|
||||
this.SetClass("block flex p-3 my-2 bg-blue-100 rounded-lg hover:shadow-xl hover:bg-blue-200 link-no-underline")
|
||||
|
||||
}
|
||||
|
||||
private static generateContent(imageUrl: string | UIElement, messageT: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined): (UIElement | string)[] {
|
||||
private static generateContent(imageUrl: string | BaseUIElement, messageT: string | BaseUIElement, linkTo: { url: string | UIEventSource<string>, newTab?: boolean } = undefined): BaseUIElement {
|
||||
const message = Translations.W(messageT);
|
||||
if (message !== null) {
|
||||
message.dumbMode = false;
|
||||
}
|
||||
message
|
||||
let img;
|
||||
if ((imageUrl ?? "") === "") {
|
||||
img = new FixedUiElement("");
|
||||
img = undefined;
|
||||
} else if (typeof (imageUrl) === "string") {
|
||||
img = new FixedUiElement(`<img style="width: 100%;" src="${imageUrl}" alt="">`);
|
||||
img = new Img(imageUrl)
|
||||
} else {
|
||||
img = imageUrl;
|
||||
}
|
||||
img.SetClass("block flex items-center justify-center h-11 w-11 flex-shrink0")
|
||||
img?.SetClass("block flex items-center justify-center h-11 w-11 flex-shrink0 mr-4")
|
||||
const image = new Combine([img])
|
||||
.SetClass("flex-shrink-0");
|
||||
|
||||
|
||||
if (message !== null && message.IsEmpty()) {
|
||||
// Message == null: special case to force empty text
|
||||
return [];
|
||||
}
|
||||
|
||||
if (linkTo != undefined) {
|
||||
return [
|
||||
`<a class='flex group' href="${linkTo.url}" ${linkTo.newTab ? 'target="_blank"' : ""}>`,
|
||||
if (linkTo == undefined) {
|
||||
return new Combine([
|
||||
image,
|
||||
`<div class='ml-4 overflow-ellipsis'>`,
|
||||
message,
|
||||
`</div>`,
|
||||
`</a>`
|
||||
];
|
||||
message?.SetClass("block overflow-ellipsis"),
|
||||
]).SetClass("flex group w-full");
|
||||
}
|
||||
|
||||
return [
|
||||
image,
|
||||
message,
|
||||
];
|
||||
|
||||
return new Link(
|
||||
new Combine([
|
||||
image,
|
||||
message?.SetClass("block overflow-ellipsis")
|
||||
]).SetClass("flex group w-full"),
|
||||
linkTo.url,
|
||||
linkTo.newTab ?? false
|
||||
)
|
||||
}
|
||||
|
||||
protected InnerRender(): string | BaseUIElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,39 +1,42 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "./Combine";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "./VariableUIElement";
|
||||
|
||||
export class TabbedComponent extends UIElement {
|
||||
export class TabbedComponent extends Combine {
|
||||
|
||||
private headers: UIElement[] = [];
|
||||
private content: UIElement[] = [];
|
||||
constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) {
|
||||
|
||||
constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) {
|
||||
super(typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0)));
|
||||
const self = this;
|
||||
const openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0))
|
||||
|
||||
const tabs: BaseUIElement[] = []
|
||||
const contentElements: BaseUIElement[] = [];
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let element = elements[i];
|
||||
this.headers.push(Translations.W(element.header).onClick(() => self._source.setData(i)));
|
||||
const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i))
|
||||
openedTabSrc.addCallbackAndRun(selected => {
|
||||
if(selected === i){
|
||||
header.SetClass("tab-active")
|
||||
header.RemoveClass("tab-non-active")
|
||||
}else{
|
||||
header.SetClass("tab-non-active")
|
||||
header.RemoveClass("tab-active")
|
||||
}
|
||||
})
|
||||
const content = Translations.W(element.content)
|
||||
this.content.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
let headerBar = "";
|
||||
for (let i = 0; i < this.headers.length; i++) {
|
||||
let header = this.headers[i];
|
||||
|
||||
if (!this.content[i].IsEmpty()) {
|
||||
headerBar += `<div class=\'tab-single-header ${i == this._source.data ? 'tab-active' : 'tab-non-active'}\'>` +
|
||||
header.Render() + "</div>"
|
||||
}
|
||||
content.SetClass("relative p-4 w-full inline-block")
|
||||
contentElements.push(content);
|
||||
const tab = header.SetClass("block tab-single-header")
|
||||
tabs.push(tab)
|
||||
}
|
||||
|
||||
const header = new Combine(tabs).SetClass("block tabs-header-bar")
|
||||
const actualContent = new VariableUiElement(
|
||||
openedTabSrc.map(i => contentElements[i])
|
||||
)
|
||||
super([header, actualContent])
|
||||
|
||||
headerBar = "<div class='tabs-header-bar'>" + headerBar + "</div>"
|
||||
|
||||
const content = this.content[this._source.data];
|
||||
return headerBar + "<div class='tab-content'>" + (content?.Render() ?? "") + "</div>";
|
||||
}
|
||||
|
||||
}
|
71
UI/Base/Table.ts
Normal file
71
UI/Base/Table.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {Utils} from "../../Utils";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export default class Table extends BaseUIElement {
|
||||
|
||||
private readonly _header: BaseUIElement[];
|
||||
private readonly _contents: BaseUIElement[][];
|
||||
private readonly _contentStyle: string[][];
|
||||
|
||||
constructor(header: (BaseUIElement | string)[],
|
||||
contents: (BaseUIElement | string)[][],
|
||||
contentStyle?: string[][]) {
|
||||
super();
|
||||
this._contentStyle = contentStyle ?? [];
|
||||
this._header = header?.map(Translations.W);
|
||||
this._contents = contents.map(row => row.map(Translations.W));
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const table = document.createElement("table")
|
||||
|
||||
const headerElems = Utils.NoNull((this._header ?? []).map(elems => elems.ConstructElement()))
|
||||
if (headerElems.length > 0) {
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
headerElems.forEach(headerElem => {
|
||||
const td = document.createElement("th")
|
||||
td.appendChild(headerElem)
|
||||
tr.appendChild(td)
|
||||
})
|
||||
table.appendChild(tr)
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._contents.length; i++){
|
||||
let row = this._contents[i];
|
||||
const tr = document.createElement("tr")
|
||||
for (let j = 0; j < row.length; j++){
|
||||
let elem = row[j];
|
||||
const htmlElem = elem?.ConstructElement()
|
||||
if (htmlElem === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let style = undefined;
|
||||
if(this._contentStyle !== undefined && this._contentStyle[i] !== undefined && this._contentStyle[j]!== undefined){
|
||||
style = this._contentStyle[i][j]
|
||||
}
|
||||
|
||||
const td = document.createElement("td")
|
||||
td.style.cssText = style;
|
||||
td.appendChild(htmlElem)
|
||||
tr.appendChild(td)
|
||||
}
|
||||
table.appendChild(tr)
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
|
||||
const headerMarkdownParts = this._header.map(hel => hel?.AsMarkdown() ?? " ")
|
||||
const header =headerMarkdownParts.join(" | ");
|
||||
const headerSep = headerMarkdownParts.map(part => '-'.repeat(part.length + 2)).join("|")
|
||||
const table = this._contents.map(row => row.map(el => el.AsMarkdown()?? " ").join("|")).join("\n")
|
||||
|
||||
return [header, headerSep, table, ""].join("\n")
|
||||
}
|
||||
|
||||
}
|
37
UI/Base/Title.ts
Normal file
37
UI/Base/Title.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export default class Title extends BaseUIElement{
|
||||
private readonly _embedded: BaseUIElement;
|
||||
private readonly _level: number;
|
||||
constructor(embedded: string | BaseUIElement, level: number =3 ) {
|
||||
super()
|
||||
this._embedded = Translations.W(embedded);
|
||||
this._level = level;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = this._embedded.ConstructElement()
|
||||
if(el === undefined){
|
||||
return undefined;
|
||||
}
|
||||
const h = document.createElement("h"+this._level)
|
||||
h.appendChild(el)
|
||||
return h;
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
const embedded = " " +this._embedded.AsMarkdown()+" ";
|
||||
|
||||
if(this._level == 1){
|
||||
return "\n"+embedded+"\n"+"=".repeat(embedded.length)+"\n\n"
|
||||
}
|
||||
|
||||
if(this._level == 2){
|
||||
return "\n"+embedded+"\n"+"-".repeat(embedded.length)+"\n\n"
|
||||
}
|
||||
|
||||
return "\n"+"#".repeat( this._level)+embedded +"\n\n";
|
||||
}
|
||||
}
|
|
@ -1,16 +1,46 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class VariableUiElement extends UIElement {
|
||||
private _html: UIEventSource<string>;
|
||||
export class VariableUiElement extends BaseUIElement {
|
||||
|
||||
constructor(html: UIEventSource<string>) {
|
||||
super(html);
|
||||
this._html = html;
|
||||
private _element : HTMLElement;
|
||||
|
||||
constructor(contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>) {
|
||||
super();
|
||||
|
||||
this._element = document.createElement("span")
|
||||
const el = this._element
|
||||
contents.addCallbackAndRun(contents => {
|
||||
while (el.firstChild) {
|
||||
el.removeChild(
|
||||
el.lastChild
|
||||
)
|
||||
}
|
||||
|
||||
if (contents === undefined) {
|
||||
return el;
|
||||
}
|
||||
if (typeof contents === "string") {
|
||||
el.innerHTML = contents
|
||||
} else if (contents instanceof Array) {
|
||||
for (const content of contents) {
|
||||
const c = content.ConstructElement();
|
||||
if (c !== undefined && c !== null) {
|
||||
el.appendChild(c)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
const c = contents.ConstructElement();
|
||||
if (c !== undefined && c !== null) {
|
||||
el.appendChild(c)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._html.data;
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
|
||||
export class VerticalCombine extends UIElement {
|
||||
private readonly _elements: UIElement[];
|
||||
|
||||
constructor(elements: UIElement[]) {
|
||||
super(undefined);
|
||||
this._elements = elements;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
let html = "";
|
||||
for (const element of this._elements) {
|
||||
if (element !== undefined && !element.IsEmpty()) {
|
||||
html += "<div>" + element.Render() + "</div>";
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
}
|
166
UI/BaseUIElement.ts
Normal file
166
UI/BaseUIElement.ts
Normal file
|
@ -0,0 +1,166 @@
|
|||
import {Utils} from "../Utils";
|
||||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
|
||||
/**
|
||||
* A thin wrapper around a html element, which allows to generate a HTML-element.
|
||||
*
|
||||
* Assumes a read-only configuration, so it has no 'ListenTo'
|
||||
*/
|
||||
export default abstract class BaseUIElement {
|
||||
|
||||
private clss: Set<string> = new Set<string>();
|
||||
private style: string;
|
||||
private _onClick: () => void;
|
||||
private _onHover: UIEventSource<boolean>;
|
||||
|
||||
protected _constructedHtmlElement: HTMLElement;
|
||||
|
||||
|
||||
protected abstract InnerConstructElement(): HTMLElement;
|
||||
|
||||
public onClick(f: (() => void)) {
|
||||
this._onClick = f;
|
||||
this.SetClass("clickable")
|
||||
if(this._constructedHtmlElement !== undefined){
|
||||
this._constructedHtmlElement.onclick = f;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
AttachTo(divId: string) {
|
||||
let element = document.getElementById(divId);
|
||||
if (element === null) {
|
||||
throw "SEVERE: could not attach UIElement to " + divId;
|
||||
}
|
||||
|
||||
while (element.firstChild) {
|
||||
//The list is LIVE so it will re-index each call
|
||||
element.removeChild(element.firstChild);
|
||||
}
|
||||
const el = this.ConstructElement();
|
||||
if(el !== undefined){
|
||||
element.appendChild(el)
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Adds all the relevant classes, space seperated
|
||||
*/
|
||||
public SetClass(clss: string) {
|
||||
const all = clss.split(" ").map(clsName => clsName.trim());
|
||||
let recordedChange = false;
|
||||
for (let c of all) {
|
||||
c = c.trim();
|
||||
if (this.clss.has(clss)) {
|
||||
continue;
|
||||
}
|
||||
if(c === undefined || c === ""){
|
||||
continue;
|
||||
}
|
||||
this.clss.add(c);
|
||||
recordedChange = true;
|
||||
}
|
||||
if (recordedChange) {
|
||||
this._constructedHtmlElement?.classList.add(...Array.from(this.clss));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public RemoveClass(clss: string): BaseUIElement {
|
||||
if (this.clss.has(clss)) {
|
||||
this.clss.delete(clss);
|
||||
this._constructedHtmlElement?.classList.remove(clss)
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public HasClass(clss: string): boolean{
|
||||
return this.clss.has(clss)
|
||||
}
|
||||
|
||||
public SetStyle(style: string): BaseUIElement {
|
||||
this.style = style;
|
||||
if(this._constructedHtmlElement !== undefined){
|
||||
this._constructedHtmlElement.style.cssText = style;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The same as 'Render', but creates a HTML element instead of the HTML representation
|
||||
*/
|
||||
public ConstructElement(): HTMLElement {
|
||||
if (Utils.runningFromConsole) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this._constructedHtmlElement !== undefined) {
|
||||
return this._constructedHtmlElement
|
||||
}
|
||||
|
||||
if(this.InnerConstructElement === undefined){
|
||||
throw "ERROR! This is not a correct baseUIElement: "+this.constructor.name
|
||||
}
|
||||
try{
|
||||
|
||||
|
||||
const el = this.InnerConstructElement();
|
||||
|
||||
if(el === undefined){
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this._constructedHtmlElement = el;
|
||||
const style = this.style
|
||||
if (style !== undefined && style !== "") {
|
||||
el.style.cssText = style
|
||||
}
|
||||
if (this.clss.size > 0) {
|
||||
try{
|
||||
el.classList.add(...Array.from(this.clss))
|
||||
}catch(e){
|
||||
console.error("Invalid class name detected in:", Array.from(this.clss).join(" "),"\nErr msg is ",e)
|
||||
}
|
||||
}
|
||||
|
||||
if (this._onClick !== undefined) {
|
||||
const self = this;
|
||||
el.onclick = (e) => {
|
||||
// @ts-ignore
|
||||
if (e.consumed) {
|
||||
return;
|
||||
}
|
||||
self._onClick();
|
||||
// @ts-ignore
|
||||
e.consumed = true;
|
||||
}
|
||||
el.style.pointerEvents = "all";
|
||||
el.style.cursor = "pointer";
|
||||
}
|
||||
|
||||
if (this._onHover !== undefined) {
|
||||
const self = this;
|
||||
el.addEventListener('mouseover', () => self._onHover.setData(true));
|
||||
el.addEventListener('mouseout', () => self._onHover.setData(false));
|
||||
}
|
||||
|
||||
if (this._onHover !== undefined) {
|
||||
const self = this;
|
||||
el.addEventListener('mouseover', () => self._onHover.setData(true));
|
||||
el.addEventListener('mouseout', () => self._onHover.setData(false));
|
||||
}
|
||||
|
||||
return el}catch(e){
|
||||
const domExc = e as DOMException;
|
||||
if(domExc){
|
||||
console.log("An exception occured", domExc.code, domExc.message, domExc.name )
|
||||
}
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
public AsMarkdown(): string{
|
||||
throw "AsMarkdown is not implemented by "+this.constructor.name
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Link from "../Base/Link";
|
||||
import Svg from "../../Svg";
|
||||
import Combine from "../Base/Combine";
|
||||
|
@ -8,67 +7,57 @@ import Constants from "../../Models/Constants";
|
|||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import Loc from "../../Models/Loc";
|
||||
import * as L from "leaflet"
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
/**
|
||||
* The bottom right attribution panel in the leaflet map
|
||||
*/
|
||||
export default class Attribution extends UIElement {
|
||||
|
||||
private readonly _location: UIEventSource<Loc>;
|
||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||
private readonly _userDetails: UIEventSource<UserDetails>;
|
||||
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||
export default class Attribution extends Combine {
|
||||
|
||||
constructor(location: UIEventSource<Loc>,
|
||||
userDetails: UIEventSource<UserDetails>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>,
|
||||
leafletMap: UIEventSource<L.Map>) {
|
||||
super(location);
|
||||
this._layoutToUse = layoutToUse;
|
||||
this.ListenTo(layoutToUse);
|
||||
this._userDetails = userDetails;
|
||||
this._leafletMap = leafletMap;
|
||||
this.ListenTo(userDetails);
|
||||
this._location = location;
|
||||
this.SetClass("map-attribution");
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const location: Loc = this._location?.data;
|
||||
const userDetails = this._userDetails?.data;
|
||||
|
||||
|
||||
const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true);
|
||||
const reportBug = new Link(Svg.bug_img, "https://github.com/pietervdvn/MapComplete/issues", true);
|
||||
const reportBug = new Link(Svg.bug_ui().SetClass("small-image"), "https://github.com/pietervdvn/MapComplete/issues", true);
|
||||
|
||||
const layoutId = this._layoutToUse?.data?.id;
|
||||
const layoutId = layoutToUse?.data?.id;
|
||||
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%222020-07-05%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_img, osmChaLink, true)
|
||||
let editHere: (UIElement | string) = "";
|
||||
let mapillary: UIElement = undefined;
|
||||
if (location !== undefined) {
|
||||
const idLink = `https://www.openstreetmap.org/edit?editor=id#map=${location.zoom}/${location.lat}/${location.lon}`
|
||||
editHere = new Link(Svg.pencil_img, idLink, true);
|
||||
|
||||
const mapillaryLink: string = `https://www.mapillary.com/app/?focus=map&lat=${location.lat}&lng=${location.lon}&z=${Math.max(location.zoom - 1, 1)}`;
|
||||
mapillary = new Link(Svg.mapillary_black_img, mapillaryLink, true);
|
||||
|
||||
}
|
||||
const stats = new Link(Svg.statistics_ui().SetClass("small-image"), osmChaLink, true)
|
||||
|
||||
|
||||
let editWithJosm: (UIElement | string) = ""
|
||||
if (location !== undefined &&
|
||||
this._leafletMap?.data !== undefined &&
|
||||
userDetails.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked) {
|
||||
const bounds: any = this._leafletMap.data.getBounds();
|
||||
const top = bounds.getNorth();
|
||||
const bottom = bounds.getSouth();
|
||||
const right = bounds.getEast();
|
||||
const left = bounds.getWest();
|
||||
const idLink = location.map(location => `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}`)
|
||||
const editHere = new Link(Svg.pencil_ui().SetClass("small-image"), idLink, true)
|
||||
|
||||
const mapillaryLink = location.map(location => `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`)
|
||||
const mapillary = new Link(Svg.mapillary_black_ui().SetClass("small-image"), mapillaryLink, true);
|
||||
|
||||
|
||||
|
||||
let editWithJosm = new VariableUiElement(
|
||||
userDetails.map(userDetails => {
|
||||
|
||||
if (userDetails.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked) {
|
||||
return undefined;
|
||||
}
|
||||
const bounds: any = leafletMap?.data?.getBounds();
|
||||
if(bounds === undefined){
|
||||
return undefined
|
||||
}
|
||||
const top = bounds.getNorth();
|
||||
const bottom = bounds.getSouth();
|
||||
const right = bounds.getEast();
|
||||
const left = bounds.getWest();
|
||||
|
||||
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
|
||||
return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true);
|
||||
},
|
||||
[location]
|
||||
)
|
||||
)
|
||||
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]);
|
||||
|
||||
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
|
||||
editWithJosm = new Link(Svg.josm_logo_img, josmLink, true);
|
||||
}
|
||||
return new Combine([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]).Render();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ import SmallLicense from "../../Models/smallLicense";
|
|||
import {Utils} from "../../Utils";
|
||||
import Link from "../Base/Link";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {UIElement} from "../UIElement";
|
||||
import * as contributors from "../../assets/contributors.json"
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
/**
|
||||
* The attribution panel shown on mobile
|
||||
|
@ -26,7 +26,7 @@ export default class AttributionPanel extends Combine {
|
|||
((layoutToUse.data.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.data.maintainer}),
|
||||
layoutToUse.data.credits,
|
||||
"<br/>",
|
||||
new Attribution(undefined, undefined, State.state.layoutToUse, undefined),
|
||||
new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.leafletMap),
|
||||
"<br/>",
|
||||
|
||||
new VariableUiElement(contributions.map(contributions => {
|
||||
|
@ -44,16 +44,14 @@ export default class AttributionPanel extends Combine {
|
|||
const contribs = links.join(", ")
|
||||
|
||||
if (hiddenCount == 0) {
|
||||
|
||||
|
||||
return Translations.t.general.attribution.mapContributionsBy.Subs({
|
||||
contributors: contribs
|
||||
}).InnerRender()
|
||||
})
|
||||
} else {
|
||||
return Translations.t.general.attribution.mapContributionsByAndHidden.Subs({
|
||||
contributors: contribs,
|
||||
hiddenCount: hiddenCount
|
||||
}).InnerRender();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -68,7 +66,7 @@ export default class AttributionPanel extends Combine {
|
|||
this.SetStyle("max-width: calc(100vw - 5em); width: 40em;")
|
||||
}
|
||||
|
||||
private static CodeContributors(): UIElement {
|
||||
private static CodeContributors(): BaseUIElement {
|
||||
|
||||
const total = contributors.contributors.length;
|
||||
let filtered = contributors.contributors
|
||||
|
@ -89,7 +87,7 @@ export default class AttributionPanel extends Combine {
|
|||
});
|
||||
}
|
||||
|
||||
private static IconAttribution(iconPath: string): UIElement {
|
||||
private static IconAttribution(iconPath: string): BaseUIElement {
|
||||
if (iconPath.startsWith("http")) {
|
||||
iconPath = "." + new URL(iconPath).pathname;
|
||||
}
|
||||
|
|
|
@ -1,38 +1,35 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import Translations from "../i18n/Translations";
|
||||
import State from "../../State";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
export default class BackgroundSelector extends UIElement {
|
||||
|
||||
private _dropdown: UIElement;
|
||||
private readonly _availableLayers: UIEventSource<BaseLayer[]>;
|
||||
export default class BackgroundSelector extends VariableUiElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const self = this;
|
||||
this._availableLayers = State.state.availableBackgroundLayers;
|
||||
this._availableLayers.addCallbackAndRun(available => self.CreateDropDown(available));
|
||||
}
|
||||
const available = State.state.availableBackgroundLayers.map(available => {
|
||||
const baseLayers: { value: BaseLayer, shown: string }[] = [];
|
||||
for (const i in available) {
|
||||
if(!available.hasOwnProperty(i)){
|
||||
continue;
|
||||
}
|
||||
const layer: BaseLayer = available[i];
|
||||
baseLayers.push({value: layer, shown: layer.name ?? "id:" + layer.id});
|
||||
}
|
||||
return baseLayers
|
||||
}
|
||||
)
|
||||
|
||||
private CreateDropDown(available) {
|
||||
if(available.length === 0){
|
||||
return;
|
||||
}
|
||||
|
||||
const baseLayers: { value: BaseLayer, shown: string }[] = [];
|
||||
for (const i in available) {
|
||||
const layer: BaseLayer = available[i];
|
||||
baseLayers.push({value: layer, shown: layer.name ?? "id:" + layer.id});
|
||||
}
|
||||
super(
|
||||
available.map(baseLayers => {
|
||||
if (baseLayers.length <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
return new DropDown(Translations.t.general.backgroundMap.Clone(), baseLayers, State.state.backgroundLayer)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
this._dropdown = new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.backgroundLayer);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._dropdown.Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import * as L from "leaflet"
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {UIElement} from "../UIElement";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class Basemap {
|
||||
|
||||
|
@ -12,14 +12,14 @@ export class Basemap {
|
|||
constructor(leafletElementId: string,
|
||||
location: UIEventSource<Loc>,
|
||||
currentLayer: UIEventSource<BaseLayer>,
|
||||
lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
||||
extraAttribution: UIElement) {
|
||||
lastClickLocation?: UIEventSource<{ lat: number, lon: number }>,
|
||||
extraAttribution?: BaseUIElement) {
|
||||
this.map = L.map(leafletElementId, {
|
||||
center: [location.data.lat ?? 0, location.data.lon ?? 0],
|
||||
zoom: location.data.zoom ?? 2,
|
||||
layers: [currentLayer.data.layer],
|
||||
zoomControl: false
|
||||
|
||||
zoomControl: false,
|
||||
attributionControl: extraAttribution !== undefined
|
||||
});
|
||||
|
||||
L.control.scale(
|
||||
|
@ -35,9 +35,11 @@ export class Basemap {
|
|||
this.map.setMaxBounds(
|
||||
[[-100, -200], [100, 200]]
|
||||
);
|
||||
this.map.attributionControl.setPrefix(
|
||||
extraAttribution.Render() + " | <a href='https://osm.org'>OpenStreetMap</a>");
|
||||
|
||||
this.map.attributionControl.setPrefix(
|
||||
"<span id='leaflet-attribution'></span> | <a href='https://osm.org'>OpenStreetMap</a>");
|
||||
|
||||
extraAttribution.AttachTo('leaflet-attribution')
|
||||
const self = this;
|
||||
|
||||
let previousLayer = currentLayer.data;
|
||||
|
@ -69,12 +71,12 @@ export class Basemap {
|
|||
|
||||
this.map.on("click", function (e) {
|
||||
// @ts-ignore
|
||||
lastClickLocation.setData({lat: e.latlng.lat, lon: e.latlng.lng})
|
||||
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng})
|
||||
});
|
||||
|
||||
this.map.on("contextmenu", function (e) {
|
||||
// @ts-ignore
|
||||
lastClickLocation.setData({lat: e.latlng.lat, lon: e.latlng.lng});
|
||||
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import State from "../../State";
|
||||
import ThemeIntroductionPanel from "./ThemeIntroductionPanel";
|
||||
import * as personal from "../../assets/themes/personalLayout/personalLayout.json";
|
||||
|
@ -7,47 +6,39 @@ import Svg from "../../Svg";
|
|||
import Translations from "../i18n/Translations";
|
||||
import ShareScreen from "./ShareScreen";
|
||||
import MoreScreen from "./MoreScreen";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Constants from "../../Models/Constants";
|
||||
import Combine from "../Base/Combine";
|
||||
import Locale from "../i18n/Locale";
|
||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Toggle from "../Input/Toggle";
|
||||
|
||||
export default class FullWelcomePaneWithTabs extends UIElement {
|
||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||
private readonly _userDetails: UIEventSource<UserDetails>;
|
||||
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
||||
|
||||
private readonly _component: UIElement;
|
||||
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
super(State.state.layoutToUse);
|
||||
this._layoutToUse = State.state.layoutToUse;
|
||||
this._userDetails = State.state.osmConnection.userDetails;
|
||||
const layoutToUse = this._layoutToUse.data;
|
||||
|
||||
|
||||
this._component = new ScrollableFullScreen(
|
||||
const layoutToUse = State.state.layoutToUse.data;
|
||||
super (
|
||||
() => layoutToUse.title.Clone(),
|
||||
() => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails),
|
||||
() => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails, isShown),
|
||||
"welcome" ,isShown
|
||||
)
|
||||
}
|
||||
|
||||
private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource<boolean>): { header: string | BaseUIElement; content: BaseUIElement }[]{
|
||||
|
||||
private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource<UserDetails>) {
|
||||
|
||||
let welcome: UIElement = new ThemeIntroductionPanel();
|
||||
let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown);
|
||||
if (layoutToUse.id === personal.id) {
|
||||
welcome = new PersonalLayersPanel();
|
||||
}
|
||||
const tabs = [
|
||||
const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [
|
||||
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
|
||||
{
|
||||
header: Svg.osm_logo_img,
|
||||
content: Translations.t.general.openStreetMapIntro.Clone().SetClass("link-underline") as UIElement
|
||||
content: Translations.t.general.openStreetMapIntro.Clone().SetClass("link-underline")
|
||||
},
|
||||
|
||||
]
|
||||
|
@ -64,25 +55,27 @@ export default class FullWelcomePaneWithTabs extends UIElement {
|
|||
});
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource<UserDetails>, isShown: UIEventSource<boolean>) {
|
||||
|
||||
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)
|
||||
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)]
|
||||
tabsWithAboutMc.push({
|
||||
header: Svg.help,
|
||||
content: new VariableUiElement(userDetails.map(userdetails => {
|
||||
if (userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) {
|
||||
return ""
|
||||
}
|
||||
return new Combine([Translations.t.general.aboutMapcomplete, "<br/>Version " + Constants.vNumber]).SetClass("link-underline").Render();
|
||||
}, [Locale.language]))
|
||||
content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "<br/>Version " + Constants.vNumber])
|
||||
.SetClass("link-underline")
|
||||
}
|
||||
);
|
||||
|
||||
return new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab)
|
||||
.ListenTo(userDetails);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._component.Render();
|
||||
|
||||
return new Toggle(
|
||||
new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab),
|
||||
new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab),
|
||||
userDetails.map((userdetails: UserDetails) =>
|
||||
userdetails.loggedIn &&
|
||||
userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import State from "../../State";
|
||||
import BackgroundSelector from "./BackgroundSelector";
|
||||
import LayerSelection from "./LayerSelection";
|
||||
|
@ -7,6 +6,7 @@ import {FixedUiElement} from "../Base/FixedUiElement";
|
|||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class LayerControlPanel extends ScrollableFullScreen {
|
||||
|
||||
|
@ -14,13 +14,12 @@ export default class LayerControlPanel extends ScrollableFullScreen {
|
|||
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown);
|
||||
}
|
||||
|
||||
private static GenTitle(): UIElement {
|
||||
const title = Translations.t.general.layerSelection.title.SetClass("text-2xl break-words font-bold p-2")
|
||||
return title.Clone();
|
||||
private static GenTitle():BaseUIElement {
|
||||
return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2")
|
||||
}
|
||||
|
||||
private static GeneratePanel() {
|
||||
let layerControlPanel: UIElement = new FixedUiElement("");
|
||||
private static GeneratePanel() : BaseUIElement {
|
||||
let layerControlPanel: BaseUIElement = new FixedUiElement("");
|
||||
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
||||
layerControlPanel = new BackgroundSelector();
|
||||
layerControlPanel.SetStyle("margin:1em");
|
||||
|
|
|
@ -1,84 +1,69 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import State from "../../State";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
/**
|
||||
* Shows the panel with all layers and a toggle for each of them
|
||||
*/
|
||||
export default class LayerSelection extends UIElement {
|
||||
export default class LayerSelection extends Combine {
|
||||
|
||||
private _checkboxes: UIElement[];
|
||||
private activeLayers: UIEventSource<{
|
||||
readonly isDisplayed: UIEventSource<boolean>,
|
||||
readonly layerDef: LayerConfig;
|
||||
}[]>;
|
||||
|
||||
constructor(activeLayers: UIEventSource<{
|
||||
readonly isDisplayed: UIEventSource<boolean>,
|
||||
readonly layerDef: LayerConfig;
|
||||
}[]>) {
|
||||
super(activeLayers);
|
||||
if(activeLayers === undefined){
|
||||
|
||||
if (activeLayers === undefined) {
|
||||
throw "ActiveLayers should be defined..."
|
||||
}
|
||||
this.activeLayers = activeLayers;
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const checkboxes: BaseUIElement[] = [];
|
||||
|
||||
this._checkboxes = [];
|
||||
|
||||
for (const layer of this.activeLayers.data) {
|
||||
for (const layer of activeLayers.data) {
|
||||
const leafletStyle = layer.layerDef.GenerateLeafletStyle(
|
||||
new UIEventSource<any>({id: "node/-1"}),
|
||||
false)
|
||||
const leafletHtml = leafletStyle.icon.html;
|
||||
const icon =
|
||||
new FixedUiElement(leafletHtml.Render())
|
||||
.SetClass("single-layer-selection-toggle")
|
||||
let iconUnselected: UIElement = new FixedUiElement(leafletHtml.Render())
|
||||
const icon = new Combine([leafletStyle.icon.html]).SetClass("single-layer-selection-toggle")
|
||||
let iconUnselected: BaseUIElement = new Combine([leafletStyle.icon.html])
|
||||
.SetClass("single-layer-selection-toggle")
|
||||
.SetStyle("opacity:0.2;");
|
||||
|
||||
const name = Translations.WT(layer.layerDef.name)?.Clone()
|
||||
?.SetStyle("font-size:large;margin-left: 0.5em;");
|
||||
|
||||
if((name ?? "") === ""){
|
||||
if ((name ?? "") === "") {
|
||||
continue
|
||||
}
|
||||
|
||||
const zoomStatus = new VariableUiElement(State.state.locationControl.map(location => {
|
||||
if (location.zoom < layer.layerDef.minzoom) {
|
||||
return Translations.t.general.layerSelection.zoomInToSeeThisLayer
|
||||
return Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone()
|
||||
.SetClass("alert")
|
||||
.SetStyle("display: block ruby;width:min-content;")
|
||||
.Render();
|
||||
}
|
||||
return ""
|
||||
}))
|
||||
const style = "display:flex;align-items:center;"
|
||||
const styleWhole = "display:flex; flex-wrap: wrap"
|
||||
this._checkboxes.push(new CheckBox(
|
||||
checkboxes.push(new Toggle(
|
||||
new Combine([new Combine([icon, name]).SetStyle(style), zoomStatus])
|
||||
.SetStyle(styleWhole),
|
||||
new Combine([new Combine([iconUnselected, "<del>", name, "</del>"]).SetStyle(style), zoomStatus])
|
||||
.SetStyle(styleWhole),
|
||||
layer.isDisplayed)
|
||||
layer.isDisplayed).ToggleOnClick()
|
||||
.SetStyle("margin:0.3em;")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return new Combine(this._checkboxes)
|
||||
.SetStyle("display:flex;flex-direction:column;")
|
||||
.Render();
|
||||
}
|
||||
super(checkboxes)
|
||||
this.SetStyle("display:flex;flex-direction:column;")
|
||||
|
||||
}
|
||||
}
|
20
UI/BigComponents/LicensePicker.ts
Normal file
20
UI/BigComponents/LicensePicker.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {DropDown} from "../Input/DropDown";
|
||||
import Translations from "../i18n/Translations";
|
||||
import State from "../../State";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export default class LicensePicker extends DropDown<string>{
|
||||
|
||||
constructor() {
|
||||
super(Translations.t.image.willBePublished.Clone(),
|
||||
[
|
||||
{value: "CC0", shown: Translations.t.image.cco.Clone()},
|
||||
{value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs.Clone()},
|
||||
{value: "CC-BY 4.0", shown: Translations.t.image.ccb.Clone()}
|
||||
],
|
||||
State.state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource<string>("CC0")
|
||||
)
|
||||
this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left");
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||
|
@ -11,87 +10,94 @@ import * as personal from "../../assets/themes/personalLayout/personalLayout.jso
|
|||
import Constants from "../../Models/Constants";
|
||||
import LanguagePicker from "../LanguagePicker";
|
||||
import IndexText from "./IndexText";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class MoreScreen extends UIElement {
|
||||
private readonly _onMainScreen: boolean;
|
||||
|
||||
private _component: UIElement;
|
||||
export default class MoreScreen extends Combine {
|
||||
|
||||
|
||||
constructor(onMainScreen: boolean = false) {
|
||||
super(State.state.locationControl);
|
||||
this._onMainScreen = onMainScreen;
|
||||
this.ListenTo(State.state.osmConnection.userDetails);
|
||||
this.ListenTo(State.state.installedThemes);
|
||||
super(MoreScreen.Init(onMainScreen, State.state));
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
private static Init(onMainScreen: boolean, state: State): BaseUIElement [] {
|
||||
const tr = Translations.t.general.morescreen;
|
||||
|
||||
const els: UIElement[] = []
|
||||
|
||||
const themeButtons: UIElement[] = []
|
||||
|
||||
for (const layout of AllKnownLayouts.layoutsList) {
|
||||
if (layout.id === personal.id) {
|
||||
if (State.state.osmConnection.userDetails.data.csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
themeButtons.push(this.createLinkButton(layout));
|
||||
}
|
||||
|
||||
|
||||
els.push(new VariableUiElement(
|
||||
State.state.osmConnection.userDetails.map(userDetails => {
|
||||
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
|
||||
return new SubtleButton(null, tr.requestATheme, {url:"https://github.com/pietervdvn/MapComplete/issues", newTab: true}).Render();
|
||||
}
|
||||
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme, {
|
||||
url: "./customGenerator.html",
|
||||
newTab: false
|
||||
}).Render();
|
||||
})
|
||||
));
|
||||
|
||||
els.push(new Combine(themeButtons))
|
||||
|
||||
|
||||
const customThemesNames = State.state.installedThemes.data ?? [];
|
||||
|
||||
if (customThemesNames.length > 0) {
|
||||
els.push(Translations.t.general.customThemeIntro)
|
||||
|
||||
for (const installed of State.state.installedThemes.data) {
|
||||
els.push(this.createLinkButton(installed.layout, installed.definition));
|
||||
}
|
||||
}
|
||||
|
||||
let intro: UIElement = tr.intro;
|
||||
const themeButtonsElement = new Combine(els)
|
||||
|
||||
if (this._onMainScreen) {
|
||||
let intro: BaseUIElement = tr.intro.Clone();
|
||||
let themeButtonStyle = ""
|
||||
let themeListStyle = ""
|
||||
if (onMainScreen) {
|
||||
intro = new Combine([
|
||||
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
|
||||
.SetClass("absolute top-2 right-3"),
|
||||
new IndexText()
|
||||
])
|
||||
themeButtons.map(e => e?.SetClass("h-32 min-h-32 max-h-32 overflow-ellipsis overflow-hidden"))
|
||||
themeButtonsElement.SetClass("md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4")
|
||||
themeButtonStyle = "h-32 min-h-32 max-h-32 overflow-ellipsis overflow-hidden"
|
||||
themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
|
||||
}
|
||||
|
||||
|
||||
|
||||
this._component = new Combine([
|
||||
return[
|
||||
intro,
|
||||
themeButtonsElement,
|
||||
tr.streetcomplete.SetClass("block text-base mx-10 my-3 mb-10")
|
||||
]);
|
||||
return this._component.Render();
|
||||
MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle),
|
||||
MoreScreen.createUnofficialThemeList(themeButtonStyle)?.SetClass(themeListStyle),
|
||||
tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10")
|
||||
];
|
||||
}
|
||||
|
||||
private static createUnofficialThemeList(buttonClass: string): BaseUIElement{
|
||||
return new VariableUiElement(State.state.installedThemes.map(customThemes => {
|
||||
const els : BaseUIElement[] = []
|
||||
if (customThemes.length > 0) {
|
||||
els.push(Translations.t.general.customThemeIntro.Clone())
|
||||
|
||||
const customThemesElement = new Combine(
|
||||
customThemes.map(theme => MoreScreen.createLinkButton(theme.layout, theme.definition)?.SetClass(buttonClass))
|
||||
)
|
||||
els.push(customThemesElement)
|
||||
}
|
||||
return els;
|
||||
}));
|
||||
}
|
||||
|
||||
private createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined) {
|
||||
private static createOfficialThemesList(state: State, buttonClass: string): BaseUIElement {
|
||||
let officialThemes = AllKnownLayouts.layoutsList
|
||||
if (State.state.osmConnection.userDetails.data.csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||
officialThemes = officialThemes.filter(theme => theme.id !== personal.id)
|
||||
}
|
||||
let buttons = officialThemes.map((layout) => MoreScreen.createLinkButton(layout)?.SetClass(buttonClass))
|
||||
|
||||
let customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
|
||||
buttons.splice(0, 0, customGeneratorLink);
|
||||
|
||||
return new Combine(buttons)
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
|
||||
* */
|
||||
private static createCustomGeneratorButton(state: State): VariableUiElement {
|
||||
const tr = Translations.t.general.morescreen;
|
||||
return new VariableUiElement(
|
||||
state.osmConnection.userDetails.map(userDetails => {
|
||||
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
|
||||
return new SubtleButton(null, tr.requestATheme.Clone(), {
|
||||
url: "https://github.com/pietervdvn/MapComplete/issues",
|
||||
newTab: true
|
||||
});
|
||||
}
|
||||
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), {
|
||||
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
|
||||
newTab: false
|
||||
});
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a button linking to the given theme
|
||||
* @param layout
|
||||
* @param customThemeDefinition
|
||||
* @private
|
||||
*/
|
||||
private static createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined): BaseUIElement {
|
||||
if (layout === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -100,17 +106,14 @@ export default class MoreScreen extends UIElement {
|
|||
return undefined;
|
||||
}
|
||||
if (layout.hideFromOverview) {
|
||||
const pref = State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled");
|
||||
this.ListenTo(pref);
|
||||
if (pref.data !== "true") {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (layout.id === State.state.layoutToUse.data?.id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const currentLocation = State.state.locationControl.data;
|
||||
const currentLocation = State.state.locationControl;
|
||||
|
||||
let path = window.location.pathname;
|
||||
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
|
||||
path = path.substr(0, path.lastIndexOf("/"));
|
||||
|
@ -119,29 +122,42 @@ export default class MoreScreen extends UIElement {
|
|||
path = "."
|
||||
}
|
||||
|
||||
const params = `z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}`
|
||||
let linkText =
|
||||
`${path}/${layout.id.toLowerCase()}.html?${params}`
|
||||
|
||||
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
|
||||
let linkSuffix = ""
|
||||
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
|
||||
linkText = `${path}/index.html?layout=${layout.id}&${params}`
|
||||
linkPrefix = `${path}/index.html?layout=${layout.id}&`
|
||||
}
|
||||
|
||||
if (customThemeDefinition) {
|
||||
linkText = `${path}/index.html?userlayout=${layout.id}&${params}#${customThemeDefinition}`
|
||||
|
||||
linkPrefix = `${path}/index.html?userlayout=${layout.id}&`
|
||||
linkSuffix = `#${customThemeDefinition}`
|
||||
}
|
||||
|
||||
let description = Translations.W(layout.shortDescription);
|
||||
const linkText = currentLocation.map(currentLocation => {
|
||||
const params = [
|
||||
["z", currentLocation?.zoom],
|
||||
["lat", currentLocation?.lat],
|
||||
["lon",currentLocation?.lon]
|
||||
].filter(part => part[1] !== undefined)
|
||||
.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([
|
||||
`<dt class='text-lg leading-6 font-medium text-gray-900 group-hover:text-blue-800'>`,
|
||||
Translations.W(layout.title),
|
||||
Translations.WT(layout.title).Clone(),
|
||||
`</dt>`,
|
||||
`<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`,
|
||||
description ?? "",
|
||||
description.Clone().SetClass("subtle") ?? "",
|
||||
`</dd>`,
|
||||
]), {url: linkText, newTab: false});
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,137 +1,123 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import * as personal from "../../assets/themes/personalLayout/personalLayout.json"
|
||||
import Locale from "../i18n/Locale";
|
||||
export default class PersonalLayersPanel extends UIElement {
|
||||
private checkboxes: UIElement[] = [];
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||
import Img from "../Base/Img";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export default class PersonalLayersPanel extends VariableUiElement {
|
||||
|
||||
constructor() {
|
||||
super(State.state.favouriteLayers);
|
||||
this.ListenTo(State.state.osmConnection.userDetails);
|
||||
this.ListenTo(Locale.language);
|
||||
this.UpdateView([]);
|
||||
const self = this;
|
||||
State.state.installedThemes.addCallback(extraThemes => {
|
||||
self.UpdateView(extraThemes.map(layout => layout.layout));
|
||||
self.Update();
|
||||
})
|
||||
}
|
||||
super(
|
||||
State.state.installedThemes.map(installedThemes => {
|
||||
const t = Translations.t.favourite;
|
||||
|
||||
// Lets get all the layers
|
||||
const allThemes = AllKnownLayouts.layoutsList.concat(installedThemes.map(layout => layout.layout))
|
||||
.filter(theme => !theme.hideFromOverview)
|
||||
|
||||
private UpdateView(extraThemes: LayoutConfig[]) {
|
||||
this.checkboxes = [];
|
||||
const favs = State.state.favouriteLayers.data ?? [];
|
||||
const controls = new Map<string, UIEventSource<boolean>>();
|
||||
const allLayouts = AllKnownLayouts.layoutsList.concat(extraThemes);
|
||||
for (const layout of allLayouts) {
|
||||
if (layout.id === personal.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(layout.hideFromOverview){
|
||||
continue;
|
||||
}
|
||||
|
||||
const header =
|
||||
new Combine([
|
||||
`<img style="max-width: 3em;max-height: 3em; float: left; padding: 0.1em; margin-right: 0.3em;" src='${layout.icon}'>`,
|
||||
"<b>",
|
||||
layout.title,
|
||||
"</b><br/>",
|
||||
layout.shortDescription ?? ""
|
||||
]).SetClass("block p1 overflow-auto rounded")
|
||||
.SetStyle("background: #eee;")
|
||||
this.checkboxes.push(header);
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
if(layer === undefined){
|
||||
console.warn("Undefined layer for ",layout.id)
|
||||
continue;
|
||||
}
|
||||
if (typeof layer === "string") {
|
||||
continue;
|
||||
}
|
||||
let icon :UIElement = layer.GenerateLeafletStyle(new UIEventSource<any>({id:"node/-1"}), false).icon.html
|
||||
?? Svg.checkmark_svg();
|
||||
let iconUnset =new FixedUiElement(icon.Render());
|
||||
icon.SetClass("single-layer-selection-toggle")
|
||||
iconUnset.SetClass("single-layer-selection-toggle")
|
||||
|
||||
|
||||
let name = layer.name ?? layer.id;
|
||||
if (name === undefined) {
|
||||
continue;
|
||||
}
|
||||
const content = new Combine([
|
||||
"<b>",
|
||||
name,
|
||||
"</b> ",
|
||||
layer.description !== undefined ? new Combine(["<br/>", layer.description]) : "",
|
||||
])
|
||||
|
||||
|
||||
const cb = new CheckBox(
|
||||
new SubtleButton(
|
||||
icon,
|
||||
content),
|
||||
new SubtleButton(
|
||||
iconUnset.SetStyle("opacity:0.1"),
|
||||
new Combine(["<del>",
|
||||
content,
|
||||
"</del>"
|
||||
])),
|
||||
controls[layer.id] ?? (favs.indexOf(layer.id) >= 0)
|
||||
);
|
||||
cb.SetClass("custom-layer-checkbox");
|
||||
controls[layer.id] = cb.isEnabled;
|
||||
|
||||
cb.isEnabled.addCallback((isEnabled) => {
|
||||
const favs = State.state.favouriteLayers;
|
||||
if (isEnabled) {
|
||||
if(favs.data.indexOf(layer.id)>= 0){
|
||||
return; // Already added
|
||||
const allLayers = []
|
||||
{
|
||||
const seenLayers = new Set<string>()
|
||||
for (const layers of allThemes.map(theme => theme.layers)) {
|
||||
for (const layer of layers) {
|
||||
if (seenLayers.has(layer.id)) {
|
||||
continue
|
||||
}
|
||||
seenLayers.add(layer.id)
|
||||
allLayers.push(layer)
|
||||
}
|
||||
favs.data.push(layer.id);
|
||||
} else {
|
||||
favs.data.splice(favs.data.indexOf(layer.id), 1);
|
||||
}
|
||||
favs.ping();
|
||||
})
|
||||
}
|
||||
|
||||
this.checkboxes.push(cb);
|
||||
// Time to create a panel based on them!
|
||||
const panel: BaseUIElement = new Combine(allLayers.map(PersonalLayersPanel.CreateLayerToggle));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
State.state.favouriteLayers.addCallback((layers) => {
|
||||
for (const layerId of layers) {
|
||||
controls[layerId]?.setData(true);
|
||||
}
|
||||
});
|
||||
|
||||
return new Toggle(
|
||||
new Combine([
|
||||
t.panelIntro.Clone(),
|
||||
panel
|
||||
]).SetClass("flex flex-col"),
|
||||
new SubtleButton(
|
||||
Svg.osm_logo_ui(),
|
||||
t.loginNeeded.Clone().SetClass("text-center")
|
||||
).onClick(() => State.state.osmConnection.AttemptLogin()),
|
||||
State.state.osmConnection.isLoggedIn
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const t = Translations.t.favourite;
|
||||
const userDetails = State.state.osmConnection.userDetails.data;
|
||||
if(!userDetails.loggedIn){
|
||||
return t.loginNeeded.Render();
|
||||
}
|
||||
/***
|
||||
* Creates a toggle for the given layer, which'll update State.state.favouriteLayers right away
|
||||
* @param layer
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
private static CreateLayerToggle(layer: LayerConfig): Toggle {
|
||||
const iconUrl = layer.icon.GetRenderValue({id: "node/-1"}).txt
|
||||
let icon :BaseUIElement =new Combine([ layer.GenerateLeafletStyle(
|
||||
new UIEventSource<any>({id: "node/-1"}),
|
||||
false,
|
||||
"2em"
|
||||
).icon.html]).SetClass("relative")
|
||||
let iconUnset =new Combine([ layer.GenerateLeafletStyle(
|
||||
new UIEventSource<any>({id: "node/-1"}),
|
||||
false,
|
||||
"2em"
|
||||
).icon.html]).SetClass("relative")
|
||||
|
||||
return new Combine([
|
||||
t.panelIntro,
|
||||
...this.checkboxes
|
||||
]).Render();
|
||||
iconUnset.SetStyle("opacity:0.1")
|
||||
|
||||
let name = layer.name ;
|
||||
if (name === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const content = new Combine([
|
||||
Translations.WT(name).Clone().SetClass("font-bold"),
|
||||
Translations.WT(layer.description)?.Clone()
|
||||
]).SetClass("flex flex-col")
|
||||
|
||||
const contentUnselected = new Combine([
|
||||
Translations.WT(name).Clone().SetClass("font-bold"),
|
||||
Translations.WT(layer.description)?.Clone()
|
||||
]).SetClass("flex flex-col line-through")
|
||||
|
||||
return new Toggle(
|
||||
new SubtleButton(
|
||||
icon,
|
||||
content ),
|
||||
new SubtleButton(
|
||||
iconUnset,
|
||||
contentUnselected
|
||||
),
|
||||
State.state.favouriteLayers.map(favLayers => {
|
||||
return favLayers.indexOf(layer.id) >= 0
|
||||
}, [], (selected, current) => {
|
||||
if (!selected && current.indexOf(layer.id) <= 0) {
|
||||
// Not selected and not contained: nothing to change: we return current as is
|
||||
return current;
|
||||
}
|
||||
if (selected && current.indexOf(layer.id) >= 0) {
|
||||
// Selected and contained: this is fine!
|
||||
return current;
|
||||
}
|
||||
const clone = [...current]
|
||||
if (selected) {
|
||||
clone.push(layer.id)
|
||||
} else {
|
||||
clone.splice(clone.indexOf(layer.id), 1)
|
||||
}
|
||||
return clone
|
||||
})
|
||||
).ToggleOnClick();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Locale from "../i18n/Locale";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Svg from "../../Svg";
|
||||
|
@ -9,75 +8,76 @@ 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";
|
||||
|
||||
export default class SearchAndGo extends UIElement {
|
||||
|
||||
private _placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
|
||||
private _searchField = new TextField({
|
||||
placeholder: new VariableUiElement(
|
||||
this._placeholder.map(uiElement => uiElement.InnerRender(), [Locale.language])
|
||||
),
|
||||
value: new UIEventSource<string>("")
|
||||
}
|
||||
);
|
||||
|
||||
private _foundEntries = new UIEventSource([]);
|
||||
private _goButton = Svg.search_ui().SetClass('w-8 h-8 full-rounded border-black float-right');
|
||||
export default class SearchAndGo extends Combine {
|
||||
|
||||
constructor() {
|
||||
super(undefined);
|
||||
this.ListenTo(this._foundEntries);
|
||||
const goButton = Svg.search_ui().SetClass('w-8 h-8 full-rounded border-black float-right');
|
||||
|
||||
const self = this;
|
||||
this._searchField.enterPressed.addCallback(() => {
|
||||
self.RunSearch();
|
||||
});
|
||||
const placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
|
||||
const searchField = new TextField({
|
||||
placeholder: new VariableUiElement(
|
||||
placeholder.map(uiElement => uiElement, [Locale.language])
|
||||
),
|
||||
value: new UIEventSource<string>(""),
|
||||
|
||||
inputStyle: " background: transparent;\n" +
|
||||
" border: none;\n" +
|
||||
" font-size: large;\n" +
|
||||
" width: 100%;\n" +
|
||||
" box-sizing: border-box;\n" +
|
||||
" color: var(--foreground-color);"
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
searchField.SetClass("relative float-left mt-0 ml-2")
|
||||
searchField.SetStyle("width: calc(100% - 3em)")
|
||||
|
||||
this._goButton.onClick(function () {
|
||||
self.RunSearch();
|
||||
});
|
||||
super([searchField, goButton])
|
||||
|
||||
}
|
||||
this.SetClass("block h-8")
|
||||
this.SetStyle("background: var(--background-color); color: var(--foreground-color); pointer-evetns:all;")
|
||||
|
||||
InnerRender(): string {
|
||||
return this._searchField.Render() +
|
||||
this._goButton.Render();
|
||||
|
||||
}
|
||||
// Triggered by 'enter' or onclick
|
||||
function runSearch() {
|
||||
const searchString = searchField.GetValue().data;
|
||||
if (searchString === undefined || searchString === "") {
|
||||
return;
|
||||
}
|
||||
searchField.GetValue().setData("");
|
||||
placeholder.setData(Translations.t.general.search.searching);
|
||||
Geocoding.Search(searchString, (result) => {
|
||||
|
||||
console.log("Search result", result)
|
||||
if (result.length == 0) {
|
||||
placeholder.setData(Translations.t.general.search.nothing);
|
||||
return;
|
||||
}
|
||||
|
||||
const poi = result[0];
|
||||
const bb = poi.boundingbox;
|
||||
const bounds: [[number, number], [number, number]] = [
|
||||
[bb[0], bb[2]],
|
||||
[bb[1], bb[3]]
|
||||
]
|
||||
State.state.selectedElement.setData(undefined);
|
||||
Hash.hash.setData(poi.osm_type + "/" + poi.osm_id);
|
||||
State.state.leafletMap.data.fitBounds(bounds);
|
||||
placeholder.setData(Translations.t.general.search.search);
|
||||
},
|
||||
() => {
|
||||
searchField.GetValue().setData("");
|
||||
placeholder.setData(Translations.t.general.search.error);
|
||||
});
|
||||
|
||||
// Triggered by 'enter' or onclick
|
||||
private RunSearch() {
|
||||
const searchString = this._searchField.GetValue().data;
|
||||
if (searchString === undefined || searchString === "") {
|
||||
return;
|
||||
}
|
||||
this._searchField.GetValue().setData("");
|
||||
this._placeholder.setData(Translations.t.general.search.searching);
|
||||
const self = this;
|
||||
Geocoding.Search(searchString, (result) => {
|
||||
|
||||
console.log("Search result", result)
|
||||
if (result.length == 0) {
|
||||
self._placeholder.setData(Translations.t.general.search.nothing);
|
||||
return;
|
||||
}
|
||||
|
||||
const poi = result[0];
|
||||
const bb = poi.boundingbox;
|
||||
const bounds: [[number, number], [number, number]] = [
|
||||
[bb[0], bb[2]],
|
||||
[bb[1], bb[3]]
|
||||
]
|
||||
State.state.selectedElement. setData(undefined);
|
||||
Hash.hash.setData(poi.osm_type+"/"+poi.osm_id);
|
||||
State.state.leafletMap.data.fitBounds(bounds);
|
||||
self._placeholder.setData(Translations.t.general.search.search);
|
||||
},
|
||||
() => {
|
||||
self._searchField.GetValue().setData("");
|
||||
self._placeholder.setData(Translations.t.general.search.error);
|
||||
});
|
||||
|
||||
searchField.enterPressed.addCallback(runSearch);
|
||||
goButton.onClick(runSearch);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,29 +1,28 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class ShareButton extends UIElement{
|
||||
private _embedded: UIElement;
|
||||
private _shareData: { text: string; title: string; url: string };
|
||||
export default class ShareButton extends BaseUIElement{
|
||||
private _embedded: BaseUIElement;
|
||||
private _shareData: () => { text: string; title: string; url: string };
|
||||
|
||||
constructor(embedded: UIElement, shareData: {
|
||||
constructor(embedded: BaseUIElement, generateShareData: () => {
|
||||
text: string,
|
||||
title: string,
|
||||
url: string
|
||||
}) {
|
||||
super();
|
||||
this._embedded = embedded;
|
||||
this._shareData = shareData;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return `<button type="button" class="share-button" id="${this.id}">${this._embedded.Render()}</button>`
|
||||
this._shareData = generateShareData;
|
||||
this.SetClass("share-button")
|
||||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self= this;
|
||||
htmlElement.addEventListener('click', () => {
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const e = document.createElement("button")
|
||||
e.type = "button"
|
||||
e.appendChild(this._embedded.ConstructElement())
|
||||
|
||||
e.addEventListener('click', () => {
|
||||
if (navigator.share) {
|
||||
navigator.share(self._shareData).then(() => {
|
||||
navigator.share(this._shareData()).then(() => {
|
||||
console.log('Thanks for sharing!');
|
||||
})
|
||||
.catch(err => {
|
||||
|
@ -33,6 +32,9 @@ export default class ShareButton extends UIElement{
|
|||
console.log('web share not supported');
|
||||
}
|
||||
});
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
import {VerticalCombine} from "../Base/VerticalCombine";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
|
@ -9,29 +7,23 @@ import {SubtleButton} from "../Base/SubtleButton";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import State from "../../State";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Constants from "../../Models/Constants";
|
||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class ShareScreen extends UIElement {
|
||||
private readonly _options: UIElement;
|
||||
private readonly _iframeCode: UIElement;
|
||||
public iframe: UIEventSource<string>;
|
||||
private readonly _link: UIElement;
|
||||
private readonly _linkStatus: UIEventSource<string | UIElement>;
|
||||
private readonly _editLayout: UIElement;
|
||||
export default class ShareScreen extends Combine {
|
||||
|
||||
constructor(layout: LayoutConfig = undefined, layoutDefinition: string = undefined) {
|
||||
super(undefined)
|
||||
layout = layout ?? State.state?.layoutToUse?.data;
|
||||
layoutDefinition = layoutDefinition ?? State.state?.layoutDefinition;
|
||||
const tr = Translations.t.general.sharescreen;
|
||||
|
||||
const optionCheckboxes: UIElement[] = []
|
||||
const optionCheckboxes: BaseUIElement[] = []
|
||||
const optionParts: (UIEventSource<string>)[] = [];
|
||||
this.SetClass("link-underline")
|
||||
|
||||
function check() {
|
||||
return Svg.checkmark_svg().SetStyle("width: 1.5em; display:inline-block;");
|
||||
}
|
||||
|
@ -40,11 +32,11 @@ export default class ShareScreen extends UIElement {
|
|||
return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;");
|
||||
}
|
||||
|
||||
const includeLocation = new CheckBox(
|
||||
new Combine([check(), tr.fsIncludeCurrentLocation]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLocation]),
|
||||
true
|
||||
)
|
||||
const includeLocation = new Toggle(
|
||||
new Combine([check(), tr.fsIncludeCurrentLocation.Clone()]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLocation.Clone()]),
|
||||
new UIEventSource<boolean>(true)
|
||||
).ToggleOnClick()
|
||||
optionCheckboxes.push(includeLocation);
|
||||
|
||||
const currentLocation = State.state?.locationControl;
|
||||
|
@ -54,7 +46,10 @@ export default class ShareScreen extends UIElement {
|
|||
return null;
|
||||
}
|
||||
if (includeL) {
|
||||
return `z=${currentLocation.data.zoom}&lat=${currentLocation.data.lat}&lon=${currentLocation.data.lon}`
|
||||
return [["z", currentLocation.data?.zoom], ["lat", currentLocation.data?.lat], ["lon", currentLocation.data?.lon]]
|
||||
.filter(p => p[1] !== undefined)
|
||||
.map(p => p[0]+"="+p[1])
|
||||
.join("&")
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -73,13 +68,13 @@ export default class ShareScreen extends UIElement {
|
|||
|
||||
const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = State.state.backgroundLayer;
|
||||
const currentBackground = new VariableUiElement(currentLayer.map(layer => {
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}).Render();
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""});
|
||||
}));
|
||||
const includeCurrentBackground = new CheckBox(
|
||||
const includeCurrentBackground = new Toggle(
|
||||
new Combine([check(), currentBackground]),
|
||||
new Combine([nocheck(), currentBackground]),
|
||||
true
|
||||
)
|
||||
new UIEventSource<boolean>(true)
|
||||
).ToggleOnClick()
|
||||
optionCheckboxes.push(includeCurrentBackground);
|
||||
optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => {
|
||||
if (includeBG) {
|
||||
|
@ -90,11 +85,11 @@ export default class ShareScreen extends UIElement {
|
|||
}, [currentLayer]));
|
||||
|
||||
|
||||
const includeLayerChoices = new CheckBox(
|
||||
new Combine([check(), tr.fsIncludeCurrentLayers]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLayers]),
|
||||
true
|
||||
)
|
||||
const includeLayerChoices = new Toggle(
|
||||
new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]),
|
||||
new UIEventSource<boolean>(true)
|
||||
).ToggleOnClick()
|
||||
optionCheckboxes.push(includeLayerChoices);
|
||||
|
||||
optionParts.push(includeLayerChoices.isEnabled.map((includeLayerSelection) => {
|
||||
|
@ -120,10 +115,11 @@ export default class ShareScreen extends UIElement {
|
|||
|
||||
for (const swtch of switches) {
|
||||
|
||||
const checkbox = new CheckBox(
|
||||
new Combine([check(), Translations.W(swtch.human)]),
|
||||
new Combine([nocheck(), Translations.W(swtch.human)]), !swtch.reverse
|
||||
);
|
||||
const checkbox = new Toggle(
|
||||
new Combine([check(), Translations.W(swtch.human.Clone())]),
|
||||
new Combine([nocheck(), Translations.W(swtch.human.Clone())]),
|
||||
new UIEventSource<boolean>(!swtch.reverse)
|
||||
).ToggleOnClick();
|
||||
optionCheckboxes.push(checkbox);
|
||||
optionParts.push(checkbox.isEnabled.map((isEn) => {
|
||||
if (isEn) {
|
||||
|
@ -143,7 +139,7 @@ export default class ShareScreen extends UIElement {
|
|||
}
|
||||
|
||||
|
||||
this._options = new VerticalCombine(optionCheckboxes)
|
||||
const options = new Combine(optionCheckboxes).SetClass("flex flex-col")
|
||||
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
|
||||
|
||||
const host = window.location.host;
|
||||
|
@ -173,12 +169,10 @@ export default class ShareScreen extends UIElement {
|
|||
}, optionParts);
|
||||
|
||||
|
||||
this.iframe = url.map(url => `<iframe src="${url}" width="100%" height="100%" title="${layout?.title?.txt ?? "MapComplete"} with MapComplete"></iframe>`);
|
||||
|
||||
this._iframeCode = new VariableUiElement(
|
||||
const iframeCode = new VariableUiElement(
|
||||
url.map((url) => {
|
||||
return `<span class='literal-code iframe-code-block'>
|
||||
<iframe src="${url}" width="100%" height="100%" title="${layout.title?.txt ?? "MapComplete"} with MapComplete"></iframe>
|
||||
<iframe src="${url}" width="100%" height="100%" style="min-width: 25Opx; min-height: 250ox" title="${layout.title?.txt ?? "MapComplete"} with MapComplete"></iframe>
|
||||
</span>`
|
||||
})
|
||||
);
|
||||
|
@ -186,9 +180,9 @@ export default class ShareScreen extends UIElement {
|
|||
|
||||
|
||||
|
||||
this._editLayout = new FixedUiElement("");
|
||||
let editLayout : BaseUIElement= new FixedUiElement("");
|
||||
if ((layoutDefinition !== undefined && State.state?.osmConnection !== undefined)) {
|
||||
this._editLayout =
|
||||
editLayout =
|
||||
new VariableUiElement(
|
||||
State.state.osmConnection.userDetails.map(
|
||||
userDetails => {
|
||||
|
@ -197,28 +191,24 @@ export default class ShareScreen extends UIElement {
|
|||
}
|
||||
|
||||
return new SubtleButton(Svg.pencil_ui(),
|
||||
new Combine([tr.editThisTheme.SetClass("bold"), "<br/>",
|
||||
tr.editThemeDescription]),
|
||||
{url: `./customGenerator.html#${State.state.layoutDefinition}`, newTab: true}).Render();
|
||||
new Combine([tr.editThisTheme.Clone().SetClass("bold"), "<br/>",
|
||||
tr.editThemeDescription.Clone()]),
|
||||
{url: `./customGenerator.html#${State.state.layoutDefinition}`, newTab: true});
|
||||
|
||||
}
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
this._linkStatus = new UIEventSource<string | Translation>("");
|
||||
this.ListenTo(this._linkStatus);
|
||||
const self = this;
|
||||
this._link = new VariableUiElement(
|
||||
url.map((url) => {
|
||||
return `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`
|
||||
})
|
||||
const linkStatus = new UIEventSource<string | Translation>("");
|
||||
const link = new VariableUiElement(
|
||||
url.map((url) => `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`)
|
||||
).onClick(async () => {
|
||||
|
||||
const shareData = {
|
||||
title: Translations.W(layout.id)?.InnerRender() ?? "",
|
||||
text: Translations.W(layout.description)?.InnerRender() ?? "",
|
||||
url: self._link.data,
|
||||
title: Translations.W(layout.title)?.ConstructElement().innerText ?? "",
|
||||
text: Translations.W(layout.description)?.ConstructElement().innerText ?? "",
|
||||
url: url.data,
|
||||
}
|
||||
|
||||
function rejected() {
|
||||
|
@ -230,17 +220,17 @@ export default class ShareScreen extends UIElement {
|
|||
copyText.setSelectionRange(0, 99999); /*For mobile devices*/
|
||||
|
||||
document.execCommand("copy");
|
||||
const copied = tr.copiedToClipboard;
|
||||
const copied = tr.copiedToClipboard.Clone();
|
||||
copied.SetClass("thanks")
|
||||
self._linkStatus.setData(copied);
|
||||
linkStatus.setData(copied);
|
||||
}
|
||||
|
||||
try {
|
||||
navigator.share(shareData)
|
||||
.then(() => {
|
||||
const thx = tr.thanksForSharing;
|
||||
const thx = tr.thanksForSharing.Clone();
|
||||
thx.SetClass("thanks");
|
||||
this._linkStatus.setData(thx);
|
||||
linkStatus.setData(thx);
|
||||
}, rejected)
|
||||
.catch(rejected)
|
||||
} catch (err) {
|
||||
|
@ -249,22 +239,19 @@ export default class ShareScreen extends UIElement {
|
|||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
super ([
|
||||
editLayout,
|
||||
tr.intro.Clone(),
|
||||
link,
|
||||
new VariableUiElement(linkStatus),
|
||||
tr.addToHomeScreen.Clone(),
|
||||
tr.embedIntro.Clone(),
|
||||
options,
|
||||
iframeCode,
|
||||
])
|
||||
this.SetClass("flex flex-col link-underline")
|
||||
|
||||
const tr = Translations.t.general.sharescreen;
|
||||
|
||||
return new VerticalCombine([
|
||||
this._editLayout,
|
||||
tr.intro,
|
||||
this._link,
|
||||
Translations.W(this._linkStatus.data),
|
||||
tr.addToHomeScreen,
|
||||
tr.embedIntro,
|
||||
this._options,
|
||||
this._iframeCode,
|
||||
]).Render()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,250 +1,232 @@
|
|||
/**
|
||||
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
|
||||
*/
|
||||
import Locale from "../i18n/Locale";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Svg from "../../Svg";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Constants from "../../Models/Constants";
|
||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
|
||||
export default class SimpleAddUI extends UIElement {
|
||||
private readonly _loginButton: UIElement;
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
* - A list of presets which can be added by the user
|
||||
* - A 'confirm-selection' button (or alternatively: please enable the layer)
|
||||
* - A 'something is wrong - please soom in further'
|
||||
* - A 'read your unread messages before adding a point'
|
||||
*/
|
||||
|
||||
private readonly _confirmPreset: UIEventSource<{
|
||||
description: string | UIElement,
|
||||
name: string | UIElement,
|
||||
icon: UIElement,
|
||||
tags: Tag[],
|
||||
layerToAddTo: {
|
||||
layerDef: LayerConfig,
|
||||
isDisplayed: UIEventSource<boolean>
|
||||
}
|
||||
}>
|
||||
= new UIEventSource(undefined);
|
||||
interface PresetInfo {
|
||||
description: string | Translation,
|
||||
name: string | BaseUIElement,
|
||||
icon: BaseUIElement,
|
||||
tags: Tag[],
|
||||
layerToAddTo: {
|
||||
layerDef: LayerConfig,
|
||||
isDisplayed: UIEventSource<boolean>
|
||||
}
|
||||
}
|
||||
|
||||
private _component: UIElement;
|
||||
|
||||
private readonly openLayerControl: UIElement;
|
||||
private readonly cancelButton: UIElement;
|
||||
private readonly goToInboxButton: UIElement = new SubtleButton(Svg.envelope_ui(),
|
||||
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false});
|
||||
export default class SimpleAddUI extends Toggle {
|
||||
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
super(State.state.locationControl.map(loc => loc.zoom));
|
||||
const self = this;
|
||||
this.ListenTo(Locale.language);
|
||||
this.ListenTo(State.state.osmConnection.userDetails);
|
||||
this.ListenTo(State.state.layerUpdater.runningQuery);
|
||||
this.ListenTo(this._confirmPreset);
|
||||
this.ListenTo(State.state.locationControl);
|
||||
State.state.filteredLayers.data?.map(layer => {
|
||||
self.ListenTo(layer.isDisplayed)
|
||||
})
|
||||
|
||||
this._loginButton = Translations.t.general.add.pleaseLogin.Clone().onClick(() => State.state.osmConnection.AttemptLogin());
|
||||
|
||||
const loginButton = Translations.t.general.add.pleaseLogin.Clone().onClick(State.state.osmConnection.AttemptLogin);
|
||||
const readYourMessages = new Combine([
|
||||
Translations.t.general.readYourMessages.Clone().SetClass("alert"),
|
||||
new SubtleButton(Svg.envelope_ui(),
|
||||
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false})
|
||||
]);
|
||||
|
||||
|
||||
|
||||
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
|
||||
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
|
||||
|
||||
function createNewPoint(tags: any[]){
|
||||
const loc = State.state.LastClickLocation.data;
|
||||
let feature = State.state.changes.createElement(tags, loc.lat, loc.lon);
|
||||
State.state.selectedElement.setData(feature);
|
||||
}
|
||||
|
||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
||||
|
||||
const addUi = new VariableUiElement(
|
||||
selectedPreset.map(preset => {
|
||||
if (preset === undefined) {
|
||||
return presetsOverview
|
||||
}
|
||||
return SimpleAddUI.CreateConfirmButton(preset,
|
||||
tags => {
|
||||
createNewPoint(tags)
|
||||
selectedPreset.setData(undefined)
|
||||
}, () => {
|
||||
selectedPreset.setData(undefined)
|
||||
})
|
||||
}
|
||||
))
|
||||
|
||||
|
||||
super(
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
|
||||
addUi,
|
||||
State.state.layerUpdater.runningQuery
|
||||
),
|
||||
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert") ,
|
||||
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
|
||||
),
|
||||
readYourMessages,
|
||||
State.state.osmConnection.userDetails.map((userdetails: UserDetails) =>
|
||||
userdetails.csCount >= Constants.userJourney.addNewPointWithUnreadMessagesUnlock ||
|
||||
userdetails.unreadMessages == 0)
|
||||
),
|
||||
loginButton,
|
||||
State.state.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
|
||||
this.SetStyle("font-size:large");
|
||||
this.cancelButton = new SubtleButton(Svg.close_ui(),
|
||||
Translations.t.general.cancel
|
||||
).onClick(() => {
|
||||
self._confirmPreset.setData(undefined);
|
||||
})
|
||||
|
||||
this.openLayerControl = new SubtleButton(Svg.layers_ui(),
|
||||
Translations.t.general.add.openLayerControl
|
||||
).onClick(() => {
|
||||
State.state.layerControlIsOpened.setData(true);
|
||||
})
|
||||
|
||||
// IS shown is the state of the dialog - we reset the choice if the dialog dissappears
|
||||
isShown.addCallback(isShown =>
|
||||
{
|
||||
if(!isShown){
|
||||
self._confirmPreset.setData(undefined)
|
||||
}
|
||||
})
|
||||
// If the click location changes, we reset the dialog as well
|
||||
State.state.LastClickLocation.addCallback(() => {
|
||||
self._confirmPreset.setData(undefined)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
this._component = this.CreateContent();
|
||||
return this._component.Render();
|
||||
|
||||
}
|
||||
|
||||
private CreatePresetsPanel(): UIElement {
|
||||
const userDetails = State.state.osmConnection.userDetails;
|
||||
if (userDetails === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!userDetails.data.loggedIn) {
|
||||
return this._loginButton;
|
||||
}
|
||||
|
||||
if (userDetails.data.unreadMessages > 0 && userDetails.data.csCount < Constants.userJourney.addNewPointWithUnreadMessagesUnlock) {
|
||||
return new Combine([
|
||||
Translations.t.general.readYourMessages.Clone().SetClass("alert"),
|
||||
this.goToInboxButton
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
if (userDetails.data.csCount < Constants.userJourney.addNewPointsUnlock) {
|
||||
return new Combine(["<span class='alert'>",
|
||||
Translations.t.general.fewChangesBefore,
|
||||
"</span>"]);
|
||||
}
|
||||
|
||||
if (State.state.locationControl.data.zoom < Constants.userJourney.minZoomLevelToAddNewPoints) {
|
||||
return Translations.t.general.add.zoomInFurther.SetClass("alert")
|
||||
}
|
||||
|
||||
if (State.state.layerUpdater.runningQuery.data) {
|
||||
return Translations.t.general.add.stillLoading
|
||||
}
|
||||
|
||||
const presetButtons = this.CreatePresetButtons()
|
||||
return new Combine(presetButtons)
|
||||
}
|
||||
|
||||
|
||||
private CreateContent(): UIElement {
|
||||
const confirmPanel = this.CreateConfirmPanel();
|
||||
if (confirmPanel !== undefined) {
|
||||
return confirmPanel;
|
||||
}
|
||||
|
||||
let intro: UIElement = Translations.t.general.add.intro;
|
||||
private static CreateConfirmButton(preset: PresetInfo,
|
||||
confirm: (tags: any[]) => void,
|
||||
cancel: () => void): BaseUIElement {
|
||||
|
||||
let testMode: UIElement = undefined;
|
||||
if (State.state.osmConnection?.userDetails?.data?.dryRun) {
|
||||
testMode = new Combine([
|
||||
"<span class='alert'>",
|
||||
"Test mode - changes won't be saved",
|
||||
"</span>"
|
||||
]);
|
||||
}
|
||||
|
||||
let presets = this.CreatePresetsPanel();
|
||||
return new Combine([intro, testMode, presets])
|
||||
|
||||
|
||||
}
|
||||
|
||||
private CreateConfirmPanel(): UIElement {
|
||||
const preset = this._confirmPreset.data;
|
||||
if (preset === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const confirmButton = new SubtleButton(preset.icon,
|
||||
new Combine([
|
||||
"<b>",
|
||||
Translations.t.general.add.confirmButton.Subs({category: preset.name}),
|
||||
"</b>"])).SetClass("break-words");
|
||||
confirmButton.onClick(
|
||||
this.CreatePoint(preset.tags)
|
||||
);
|
||||
Translations.t.general.add.addNew.Subs({category: preset.name}),
|
||||
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
|
||||
]).SetClass("flex flex-col")
|
||||
).SetClass("font-bold break-words")
|
||||
.onClick(() => confirm(preset.tags));
|
||||
|
||||
if (!this._confirmPreset.data.layerToAddTo.isDisplayed.data) {
|
||||
return new Combine([
|
||||
Translations.t.general.add.layerNotEnabled.Subs({layer: this._confirmPreset.data.layerToAddTo.layerDef.name})
|
||||
.SetClass("alert"),
|
||||
this.openLayerControl,
|
||||
|
||||
this.cancelButton
|
||||
]);
|
||||
}
|
||||
const openLayerControl =
|
||||
new SubtleButton(
|
||||
Svg.layers_ui(),
|
||||
new Combine([
|
||||
Translations.t.general.add.layerNotEnabled
|
||||
.Subs({layer: preset.layerToAddTo.layerDef.name})
|
||||
.SetClass("alert"),
|
||||
Translations.t.general.add.openLayerControl
|
||||
])
|
||||
)
|
||||
|
||||
.onClick(() => State.state.layerControlIsOpened.setData(true))
|
||||
|
||||
const openLayerOrConfirm = new Toggle(
|
||||
confirmButton,
|
||||
openLayerControl,
|
||||
preset.layerToAddTo.isDisplayed
|
||||
)
|
||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset);
|
||||
|
||||
let tagInfo = "";
|
||||
const csCount = State.state.osmConnection.userDetails.data.csCount;
|
||||
if (csCount > Constants.userJourney.tagsVisibleAt) {
|
||||
tagInfo = this._confirmPreset.data.tags.map(t => t.asHumanString(csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&");
|
||||
tagInfo = `<br/>More information about the preset: ${tagInfo}`
|
||||
}
|
||||
const cancelButton = new SubtleButton(Svg.close_ui(),
|
||||
Translations.t.general.cancel
|
||||
).onClick(cancel )
|
||||
|
||||
return new Combine([
|
||||
Translations.t.general.add.confirmIntro.Subs({title: this._confirmPreset.data.name}),
|
||||
State.state.osmConnection.userDetails.data.dryRun ? "<span class='alert'>TESTING - changes won't be saved</span>" : "",
|
||||
confirmButton,
|
||||
this.cancelButton,
|
||||
Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
|
||||
State.state.osmConnection.userDetails.data.dryRun ?
|
||||
Translations.t.general.testing.Clone().SetClass("alert") : undefined ,
|
||||
openLayerOrConfirm,
|
||||
cancelButton,
|
||||
preset.description,
|
||||
tagInfo
|
||||
|
||||
])
|
||||
]).SetClass("flex flex-col")
|
||||
|
||||
}
|
||||
|
||||
private CreatePresetButtons() {
|
||||
private static CreateTagInfoFor(preset: PresetInfo, optionallyLinkToWiki = true) {
|
||||
const csCount = State.state.osmConnection.userDetails.data.csCount;
|
||||
return new Toggle(
|
||||
Translations.t.general.presetInfo.Subs({
|
||||
tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"),
|
||||
}).SetStyle("word-break: break-all"),
|
||||
|
||||
undefined,
|
||||
State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt)
|
||||
);
|
||||
}
|
||||
|
||||
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
|
||||
const presetButtons = SimpleAddUI.CreatePresetButtons(selectedPreset)
|
||||
let intro: BaseUIElement = Translations.t.general.add.intro;
|
||||
|
||||
let testMode: BaseUIElement = undefined;
|
||||
if (State.state.osmConnection?.userDetails?.data?.dryRun) {
|
||||
testMode = Translations.t.general.testing.Clone().SetClass("alert")
|
||||
}
|
||||
|
||||
return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col")
|
||||
|
||||
}
|
||||
|
||||
private static CreatePresetSelectButton(preset: PresetInfo){
|
||||
|
||||
const tagInfo =SimpleAddUI.CreateTagInfoFor(preset, false);
|
||||
return new SubtleButton(
|
||||
preset.icon,
|
||||
new Combine([
|
||||
Translations.t.general.add.addNew.Subs({
|
||||
category: preset.name
|
||||
}).SetClass("font-bold"),
|
||||
Translations.WT(preset.description)?.FirstSentence(),
|
||||
tagInfo?.SetClass("subtle")
|
||||
]).SetClass("flex flex-col")
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Generates the list with all the buttons.*/
|
||||
private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
|
||||
const allButtons = [];
|
||||
const self = this;
|
||||
for (const layer of State.state.filteredLayers.data) {
|
||||
|
||||
if(layer.isDisplayed.data === false && State.state.featureSwitchLayers){
|
||||
continue;
|
||||
}
|
||||
|
||||
const presets = layer.layerDef.presets;
|
||||
for (const preset of presets) {
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
let icon: UIElement = new FixedUiElement(layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html.Render()).SetClass("simple-add-ui-icon");
|
||||
|
||||
const csCount = State.state.osmConnection.userDetails.data.csCount;
|
||||
let tagInfo = undefined;
|
||||
if (csCount > Constants.userJourney.tagsVisibleAt) {
|
||||
const presets = preset.tags.map(t => new Combine([t.asHumanString(false, true), " "]).SetClass("subtle break-words"))
|
||||
tagInfo = new Combine(presets)
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
|
||||
.SetClass("w-12 h-12 block relative");
|
||||
const presetInfo: PresetInfo = {
|
||||
tags: preset.tags,
|
||||
layerToAddTo: layer,
|
||||
name: preset.title,
|
||||
description: preset.description,
|
||||
icon: icon
|
||||
}
|
||||
const button: UIElement =
|
||||
new SubtleButton(
|
||||
icon,
|
||||
new Combine([
|
||||
"<b>",
|
||||
preset.title,
|
||||
"</b>",
|
||||
preset.description !== undefined ? new Combine(["<br/>", preset.description.FirstSentence()]) : "",
|
||||
"<br/>",
|
||||
tagInfo
|
||||
])
|
||||
).onClick(
|
||||
() => {
|
||||
self._confirmPreset.setData({
|
||||
tags: preset.tags,
|
||||
layerToAddTo: layer,
|
||||
name: preset.title,
|
||||
description: preset.description,
|
||||
icon: icon
|
||||
});
|
||||
self.Update();
|
||||
}
|
||||
)
|
||||
|
||||
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
|
||||
button.onClick(() => {
|
||||
selectedPreset.setData(presetInfo)
|
||||
})
|
||||
allButtons.push(button);
|
||||
}
|
||||
}
|
||||
return allButtons;
|
||||
return new Combine(allButtons).SetClass("flex flex-col");
|
||||
}
|
||||
|
||||
private CreatePoint(tags: Tag[]) {
|
||||
return () => {
|
||||
console.log("Create Point Triggered")
|
||||
const loc = State.state.LastClickLocation.data;
|
||||
let feature = State.state.changes.createElement(tags, loc.lat, loc.lon);
|
||||
State.state.selectedElement.setData(feature);
|
||||
this._confirmPreset.setData(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
public OnClose(){
|
||||
console.log("On close triggered")
|
||||
this._confirmPreset.setData(undefined)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,61 +1,72 @@
|
|||
import Locale from "../i18n/Locale";
|
||||
import {UIElement} from "../UIElement";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import LanguagePicker from "../LanguagePicker";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export default class ThemeIntroductionPanel extends UIElement {
|
||||
private languagePicker: UIElement;
|
||||
export default class ThemeIntroductionPanel extends VariableUiElement {
|
||||
|
||||
private readonly loginStatus: UIElement;
|
||||
private _layout: UIEventSource<LayoutConfig>;
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
|
||||
|
||||
constructor() {
|
||||
super(State.state.osmConnection.userDetails);
|
||||
this.ListenTo(Locale.language);
|
||||
this.languagePicker = LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language, Translations.t.general.pickLanguage);
|
||||
this._layout = State.state.layoutToUse;
|
||||
this.ListenTo(State.state.layoutToUse);
|
||||
const languagePicker =
|
||||
new VariableUiElement(
|
||||
State.state.layoutToUse.map(layout => LanguagePicker.CreateLanguagePicker(layout.language, Translations.t.general.pickLanguage.Clone()))
|
||||
)
|
||||
;
|
||||
|
||||
const toTheMap = new SubtleButton(
|
||||
undefined,
|
||||
Translations.t.general.openTheMap.Clone().SetClass("text-xl font-bold w-full text-center")
|
||||
).onClick(() =>{
|
||||
isShown.setData(false)
|
||||
}).SetClass("only-on-mobile")
|
||||
|
||||
const plzLogIn =
|
||||
Translations.t.general.loginWithOpenStreetMap
|
||||
new SubtleButton(
|
||||
Svg.osm_logo_ui(),
|
||||
|
||||
new Combine([Translations.t.general.loginWithOpenStreetMap
|
||||
.Clone().SetClass("text-xl font-bold"),
|
||||
Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold")]
|
||||
).SetClass("flex flex-col text-center w-full")
|
||||
)
|
||||
.onClick(() => {
|
||||
State.state.osmConnection.AttemptLogin()
|
||||
});
|
||||
|
||||
|
||||
const welcomeBack = Translations.t.general.welcomeBack.Clone();
|
||||
|
||||
|
||||
const welcomeBack = Translations.t.general.welcomeBack;
|
||||
|
||||
this.loginStatus = new VariableUiElement(
|
||||
State.state.osmConnection.userDetails.map(
|
||||
userdetails => {
|
||||
if (State.state.featureSwitchUserbadge.data) {
|
||||
return "";
|
||||
}
|
||||
return (userdetails.loggedIn ? welcomeBack : plzLogIn).Render();
|
||||
}
|
||||
|
||||
const loginStatus =
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
welcomeBack,
|
||||
plzLogIn,
|
||||
State.state.osmConnection.isLoggedIn
|
||||
),
|
||||
undefined,
|
||||
State.state.featureSwitchUserbadge
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
super(State.state.layoutToUse.map (layout => new Combine([
|
||||
layout.description.Clone(),
|
||||
"<br/><br/>",
|
||||
toTheMap,
|
||||
loginStatus,
|
||||
layout.descriptionTail.Clone(),
|
||||
"<br/>",
|
||||
languagePicker,
|
||||
...layout.CustomCodeSnippets()
|
||||
])))
|
||||
|
||||
this.SetClass("link-underline")
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const layout : LayoutConfig = this._layout.data;
|
||||
return new Combine([
|
||||
layout.description,
|
||||
"<br/><br/>",
|
||||
this.loginStatus,
|
||||
layout.descriptionTail,
|
||||
"<br/>",
|
||||
this.languagePicker,
|
||||
...layout.CustomCodeSnippets()
|
||||
]).Render()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
54
UI/BigComponents/UploadFlowStateUI.ts
Normal file
54
UI/BigComponents/UploadFlowStateUI.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
/**
|
||||
* Shows that 'images are uploading', 'all images are uploaded' as relevant...
|
||||
*/
|
||||
export default class UploadFlowStateUI extends UIElement{
|
||||
|
||||
private readonly _element: BaseUIElement
|
||||
|
||||
constructor(queue: UIEventSource<string[]>, failed: UIEventSource<string[]>, success: UIEventSource<string[]>) {
|
||||
super();
|
||||
const t = Translations.t.image;
|
||||
|
||||
this._element = new VariableUiElement(
|
||||
|
||||
queue.map(queue => {
|
||||
const failedReasons = failed.data
|
||||
const successCount = success.data.length
|
||||
const pendingCount = queue.length - successCount - failedReasons.length;
|
||||
|
||||
let stateMessages : BaseUIElement[] = []
|
||||
|
||||
if(pendingCount == 1){
|
||||
stateMessages.push(t.uploadingPicture.Clone().SetClass("alert"))
|
||||
}
|
||||
if(pendingCount > 1){
|
||||
stateMessages.push(t.uploadingMultiple.Subs({count: ""+pendingCount}).SetClass("alert"))
|
||||
}
|
||||
if(failedReasons.length > 0){
|
||||
stateMessages.push(t.uploadFailed.Clone().SetClass("alert"))
|
||||
}
|
||||
if(successCount > 0 && pendingCount == 0){
|
||||
stateMessages.push(t.uploadDone.SetClass("thanks"))
|
||||
}
|
||||
|
||||
stateMessages.forEach(msg => msg.SetStyle("display: block ruby"))
|
||||
|
||||
return stateMessages
|
||||
}, [failed, success])
|
||||
|
||||
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected InnerRender(): string | BaseUIElement {
|
||||
return this._element
|
||||
}
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
/**
|
||||
* Handles and updates the user badge
|
||||
*/
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
|
@ -12,133 +9,127 @@ import {FixedUiElement} from "../Base/FixedUiElement";
|
|||
import LanguagePicker from "../LanguagePicker";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Link from "../Base/Link";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Img from "../Base/Img";
|
||||
|
||||
export default class UserBadge extends UIElement {
|
||||
private _userDetails: UIEventSource<UserDetails>;
|
||||
private _logout: UIElement;
|
||||
private _homeButton: UIElement;
|
||||
private _languagePicker: UIElement;
|
||||
|
||||
private _loginButton: UIElement;
|
||||
export default class UserBadge extends Toggle {
|
||||
|
||||
constructor() {
|
||||
super(State.state.osmConnection.userDetails);
|
||||
this._userDetails = State.state.osmConnection.userDetails;
|
||||
this._languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language) ?? new FixedUiElement(""))
|
||||
.SetStyle("width:min-content;");
|
||||
|
||||
this._loginButton = Translations.t.general.loginWithOpenStreetMap
|
||||
|
||||
const userDetails = State.state.osmConnection.userDetails;
|
||||
|
||||
const loginButton = Translations.t.general.loginWithOpenStreetMap
|
||||
.Clone()
|
||||
.SetClass("userbadge-login pt-3 w-full")
|
||||
.onClick(() => State.state.osmConnection.AttemptLogin());
|
||||
this._logout =
|
||||
|
||||
|
||||
const logout =
|
||||
Svg.logout_svg()
|
||||
.onClick(() => {
|
||||
State.state.osmConnection.LogOut();
|
||||
});
|
||||
|
||||
this._userDetails.addCallback(function () {
|
||||
const profilePic = document.getElementById("profile-pic");
|
||||
if (profilePic) {
|
||||
|
||||
profilePic.onload = function () {
|
||||
profilePic.style.opacity = "1"
|
||||
};
|
||||
}
|
||||
});
|
||||
const userBadge = userDetails.map(user => {
|
||||
{
|
||||
const homeButton = new VariableUiElement(
|
||||
userDetails.map((userinfo) => {
|
||||
if (userinfo.home) {
|
||||
return Svg.home_ui();
|
||||
}
|
||||
return " ";
|
||||
})
|
||||
).onClick(() => {
|
||||
const home = State.state.osmConnection.userDetails.data?.home;
|
||||
if (home === undefined) {
|
||||
return;
|
||||
}
|
||||
State.state.leafletMap.data.setView([home.lat, home.lon], 16);
|
||||
});
|
||||
|
||||
this._homeButton = new VariableUiElement(
|
||||
this._userDetails.map((userinfo) => {
|
||||
if (userinfo.home) {
|
||||
return Svg.home_ui().Render();
|
||||
const linkStyle = "flex items-baseline"
|
||||
const languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language) ?? new FixedUiElement(""))
|
||||
.SetStyle("width:min-content;");
|
||||
|
||||
let messageSpan =
|
||||
new Link(
|
||||
new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle),
|
||||
'https://www.openstreetmap.org/messages/inbox',
|
||||
true
|
||||
)
|
||||
|
||||
|
||||
const csCount =
|
||||
new Link(
|
||||
new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle),
|
||||
`https://www.openstreetmap.org/user/${user.name}/history`,
|
||||
true);
|
||||
|
||||
|
||||
if (user.unreadMessages > 0) {
|
||||
messageSpan = new Link(
|
||||
new Combine([Svg.envelope, "" + user.unreadMessages]),
|
||||
'https://www.openstreetmap.org/messages/inbox',
|
||||
true
|
||||
).SetClass("alert")
|
||||
}
|
||||
return " ";
|
||||
})
|
||||
).onClick(() => {
|
||||
const home = State.state.osmConnection.userDetails.data?.home;
|
||||
if (home === undefined) {
|
||||
return;
|
||||
|
||||
let dryrun = new FixedUiElement("");
|
||||
if (user.dryRun) {
|
||||
dryrun = new FixedUiElement("TESTING").SetClass("alert");
|
||||
}
|
||||
|
||||
const settings =
|
||||
new Link(Svg.gear_svg(),
|
||||
`https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`,
|
||||
true)
|
||||
|
||||
|
||||
const userIcon = new Link(
|
||||
new Img(user.img)
|
||||
.SetClass("rounded-full opacity-0 m-0 p-0 duration-500 w-16 h16 float-left")
|
||||
,
|
||||
`https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}`,
|
||||
true
|
||||
);
|
||||
|
||||
|
||||
const userName = new Link(
|
||||
new FixedUiElement(user.name),
|
||||
`https://www.openstreetmap.org/user/${user.name}`,
|
||||
true);
|
||||
|
||||
|
||||
const userStats = new Combine([
|
||||
homeButton,
|
||||
settings,
|
||||
messageSpan,
|
||||
csCount,
|
||||
languagePicker,
|
||||
logout
|
||||
])
|
||||
.SetClass("userstats")
|
||||
|
||||
const usertext = new Combine([
|
||||
userName,
|
||||
dryrun,
|
||||
userStats
|
||||
]).SetClass("usertext")
|
||||
|
||||
return new Combine([
|
||||
userIcon,
|
||||
usertext,
|
||||
]).SetClass("h-16")
|
||||
}
|
||||
State.state.leafletMap.data.setView([home.lat, home.lon], 16);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const user = this._userDetails.data;
|
||||
if (!user.loggedIn) {
|
||||
return this._loginButton.Render();
|
||||
}
|
||||
|
||||
const linkStyle = "flex items-baseline"
|
||||
|
||||
let messageSpan: UIElement =
|
||||
new Link(
|
||||
new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle),
|
||||
'https://www.openstreetmap.org/messages/inbox',
|
||||
true
|
||||
)
|
||||
|
||||
|
||||
const csCount =
|
||||
new Link(
|
||||
new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle),
|
||||
`https://www.openstreetmap.org/user/${user.name}/history`,
|
||||
true);
|
||||
|
||||
|
||||
if (user.unreadMessages > 0) {
|
||||
messageSpan = new Link(
|
||||
new Combine([Svg.envelope, "" + user.unreadMessages]),
|
||||
'https://www.openstreetmap.org/messages/inbox',
|
||||
true
|
||||
).SetClass("alert")
|
||||
}
|
||||
|
||||
let dryrun: UIElement = new FixedUiElement("");
|
||||
if (user.dryRun) {
|
||||
dryrun = new FixedUiElement("TESTING").SetClass("alert");
|
||||
}
|
||||
|
||||
const settings =
|
||||
new Link(Svg.gear_svg(),
|
||||
`https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`,
|
||||
true)
|
||||
|
||||
|
||||
const userIcon = new Link(
|
||||
new FixedUiElement(`<img id='profile-pic' src='${user.img}' alt='profile-pic'/>`),
|
||||
`https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}`,
|
||||
true
|
||||
);
|
||||
|
||||
|
||||
const userName = new Link(
|
||||
new FixedUiElement(user.name),
|
||||
`https://www.openstreetmap.org/user/${user.name}`,
|
||||
true);
|
||||
|
||||
|
||||
const userStats = new Combine([
|
||||
this._homeButton,
|
||||
settings,
|
||||
messageSpan,
|
||||
csCount,
|
||||
this._languagePicker,
|
||||
this._logout
|
||||
])
|
||||
.SetClass("userstats")
|
||||
|
||||
const usertext = new Combine([
|
||||
userName,
|
||||
dryrun,
|
||||
userStats
|
||||
]).SetClass("usertext")
|
||||
|
||||
return new Combine([
|
||||
userIcon,
|
||||
usertext,
|
||||
]).Render()
|
||||
super(
|
||||
new VariableUiElement(userBadge),
|
||||
loginButton,
|
||||
State.state.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,59 +1,46 @@
|
|||
import {UIElement} from "./UIElement";
|
||||
import Translations from "./i18n/Translations";
|
||||
import State from "../State";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
|
||||
export default class CenterMessageBox extends UIElement {
|
||||
export default class CenterMessageBox extends VariableUiElement {
|
||||
|
||||
constructor() {
|
||||
super(State.state.centerMessage);
|
||||
const state = State.state;
|
||||
const updater = State.state.layerUpdater;
|
||||
const t = Translations.t.centerMessage;
|
||||
const message = updater.runningQuery.map(
|
||||
isRunning => {
|
||||
if (isRunning) {
|
||||
return {el: t.loadingData};
|
||||
}
|
||||
if (!updater.sufficientlyZoomed.data) {
|
||||
return {el: t.zoomIn}
|
||||
}
|
||||
if (updater.timeout.data > 0) {
|
||||
return {el: t.retrying.Subs({count: "" + updater.timeout.data})}
|
||||
}
|
||||
return {el: t.ready, isDone: true}
|
||||
|
||||
this.ListenTo(State.state.locationControl);
|
||||
this.ListenTo(State.state.layerUpdater.timeout);
|
||||
this.ListenTo(State.state.layerUpdater.runningQuery);
|
||||
this.ListenTo(State.state.layerUpdater.sufficientlyZoomed);
|
||||
}
|
||||
},
|
||||
[updater.timeout, updater.sufficientlyZoomed, state.locationControl]
|
||||
)
|
||||
|
||||
super(message.map(toShow => toShow.el))
|
||||
|
||||
this.SetClass("block " +
|
||||
"rounded-3xl bg-white text-xl font-bold text-center pointer-events-none p-4")
|
||||
this.SetStyle("transition: opacity 750ms linear")
|
||||
|
||||
private static prep(): { innerHtml: string, done: boolean } {
|
||||
if (State.state.centerMessage.data != "") {
|
||||
return {innerHtml: State.state.centerMessage.data, done: false};
|
||||
}
|
||||
const lu = State.state.layerUpdater;
|
||||
if (lu.timeout.data > 0) {
|
||||
return {
|
||||
innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.timeout.data}).Render(),
|
||||
done: false
|
||||
};
|
||||
}
|
||||
message.addCallbackAndRun(toShow => {
|
||||
const isDone = toShow.isDone ?? false;
|
||||
if (isDone) {
|
||||
this.SetStyle("transition: opacity 750ms linear; opacity: 0")
|
||||
} else {
|
||||
this.SetStyle("transition: opacity 750ms linear; opacity: 0.75")
|
||||
|
||||
if (lu.runningQuery.data) {
|
||||
return {innerHtml: Translations.t.centerMessage.loadingData.Render(), done: false};
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
if (!lu.sufficientlyZoomed.data) {
|
||||
return {innerHtml: Translations.t.centerMessage.zoomIn.Render(), done: false};
|
||||
} else {
|
||||
return {innerHtml: Translations.t.centerMessage.ready.Render(), done: true};
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return CenterMessageBox.prep().innerHtml;
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
const pstyle = htmlElement.parentElement.style;
|
||||
if (State.state.centerMessage.data != "") {
|
||||
pstyle.opacity = "1";
|
||||
pstyle.pointerEvents = "all";
|
||||
return;
|
||||
}
|
||||
pstyle.pointerEvents = "none";
|
||||
|
||||
if (CenterMessageBox.prep().done) {
|
||||
pstyle.opacity = "0";
|
||||
} else {
|
||||
pstyle.opacity = "0.5";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import Combine from "../Base/Combine";
|
||||
import {GenerateEmpty} from "./GenerateEmpty";
|
||||
import LayerPanelWithPreview from "./LayerPanelWithPreview";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import {MultiInput} from "../Input/MultiInput";
|
||||
import TagRenderingPanel from "./TagRenderingPanel";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
export default class AllLayersPanel extends UIElement {
|
||||
|
||||
|
||||
private panel: UIElement;
|
||||
private readonly _config: UIEventSource<LayoutConfigJson>;
|
||||
private readonly languages: UIEventSource<string[]>;
|
||||
private readonly userDetails: UserDetails;
|
||||
private readonly currentlySelected: UIEventSource<SingleSetting<any>>;
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>,
|
||||
languages: UIEventSource<any>, userDetails: UserDetails) {
|
||||
super(undefined);
|
||||
this.userDetails = userDetails;
|
||||
this._config = config;
|
||||
this.languages = languages;
|
||||
|
||||
this.createPanels(userDetails);
|
||||
const self = this;
|
||||
this.dumbMode = false;
|
||||
config.map<number>(config => config.layers.length).addCallback(() => self.createPanels(userDetails));
|
||||
|
||||
}
|
||||
|
||||
|
||||
private createPanels(userDetails: UserDetails) {
|
||||
const self = this;
|
||||
const tabs = [];
|
||||
|
||||
|
||||
const roamingTags = new MultiInput("Add a tagrendering",
|
||||
() => GenerateEmpty.createEmptyTagRendering(),
|
||||
() => {
|
||||
return new TagRenderingPanel(self.languages, self.currentlySelected, self.userDetails)
|
||||
|
||||
}, undefined, {allowMovement: true});
|
||||
new SingleSetting(this._config, roamingTags, "roamingRenderings", "Roaming Renderings", "These tagrenderings are shown everywhere");
|
||||
|
||||
|
||||
const backgroundLayers = AvailableBaseLayers.layerOverview.map(baselayer => ({shown:
|
||||
baselayer.name, value: baselayer.id}));
|
||||
const dropDown = new DropDown("Choose the default background layer",
|
||||
[{value: "osm",shown:"OpenStreetMap <b>(default)</b>"}, ...backgroundLayers])
|
||||
new SingleSetting(self._config, dropDown, "defaultBackgroundId", "Default background layer",
|
||||
"Selects the background layer that is used by default. If this layer is not available at the given point, OSM-Carto will be ued");
|
||||
|
||||
const layers = this._config.data.layers;
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
tabs.push({
|
||||
header: new VariableUiElement(this._config.map((config: LayoutConfigJson) => {
|
||||
const layer = config.layers[i];
|
||||
if (typeof layer !== "string") {
|
||||
try {
|
||||
const iconTagRendering = new TagRenderingConfig(layer["icon"], undefined, "icon")
|
||||
const icon = iconTagRendering.GetRenderValue({"id": "node/-1"}).txt;
|
||||
return `<img src='${icon}'>`
|
||||
} catch (e) {
|
||||
return Svg.bug_img
|
||||
// Nothing to do here
|
||||
}
|
||||
}
|
||||
return Svg.help_img;
|
||||
})),
|
||||
content: new LayerPanelWithPreview(this._config, this.languages, i, userDetails)
|
||||
});
|
||||
}
|
||||
tabs.push({
|
||||
header: Svg.layersAdd_img,
|
||||
content: new Combine([
|
||||
"<h2>Layer editor</h2>",
|
||||
"In this tab page, you can add and edit the layers of the theme. Click the layers above or add a new layer to get started.",
|
||||
new SubtleButton(
|
||||
Svg.layersAdd_ui(),
|
||||
"Add a new layer"
|
||||
).onClick(() => {
|
||||
self._config.data.layers.push(GenerateEmpty.createEmptyLayer())
|
||||
self._config.ping();
|
||||
}),
|
||||
"<h2>Default background layer</h2>",
|
||||
dropDown,
|
||||
"<h2>TagRenderings for every layer</h2>",
|
||||
"Define tag renderings and questions here that should be shown on every layer of the theme.",
|
||||
roamingTags
|
||||
]
|
||||
),
|
||||
})
|
||||
|
||||
this.panel = new TabbedComponent(tabs, new UIEventSource<number>(Math.max(0, layers.length - 1)));
|
||||
this.Update();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.panel.Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import GeneralSettings from "./GeneralSettings";
|
||||
import Combine from "../Base/Combine";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||
import PageSplit from "../Base/PageSplit";
|
||||
import AllLayersPanel from "./AllLayersPanel";
|
||||
import SharePanel from "./SharePanel";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import SavePanel from "./SavePanel";
|
||||
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
|
||||
import HelpText from "./HelpText";
|
||||
import Svg from "../../Svg";
|
||||
import Constants from "../../Models/Constants";
|
||||
import LZString from "lz-string";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class CustomGeneratorPanel extends UIElement {
|
||||
private mainPanel: UIElement;
|
||||
private loginButton: UIElement;
|
||||
|
||||
private readonly connection: OsmConnection;
|
||||
|
||||
constructor(connection: OsmConnection, layout: LayoutConfigJson) {
|
||||
super(connection.userDetails);
|
||||
this.connection = connection;
|
||||
this.SetClass("main-tabs");
|
||||
this.loginButton = new SubtleButton("", "Login to create a custom theme").onClick(() => connection.AttemptLogin())
|
||||
const self = this;
|
||||
self.mainPanel = new FixedUiElement("Attempting to log in...");
|
||||
connection.OnLoggedIn(userDetails => {
|
||||
self.InitMainPanel(layout, userDetails, connection);
|
||||
self.Update();
|
||||
})
|
||||
}
|
||||
|
||||
private InitMainPanel(layout: LayoutConfigJson, userDetails: UserDetails, connection: OsmConnection) {
|
||||
const es = new UIEventSource(layout);
|
||||
const encoded = es.map(config => LZString.compressToBase64(Utils.MinifyJSON(JSON.stringify(config, null, 0))));
|
||||
encoded.addCallback(encoded => LocalStorageSource.Get("last-custom-theme"))
|
||||
const liveUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}#${encoded}`)
|
||||
const testUrl = encoded.map(encoded => `./index.html?test=true&userlayout=${es.data.id}#${encoded}`)
|
||||
const iframe = testUrl.map(url => `<iframe src='${url}' width='100%' height='99%' style="box-sizing: border-box" title='Theme Preview'></iframe>`);
|
||||
const currentSetting = new UIEventSource<SingleSetting<any>>(undefined)
|
||||
const generalSettings = new GeneralSettings(es, currentSetting);
|
||||
const languages = generalSettings.languages;
|
||||
|
||||
const chronic = UIEventSource.Chronic(120 * 1000)
|
||||
.map(date => {
|
||||
if (es.data.id == undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (es.data.id === "") {
|
||||
return undefined;
|
||||
}
|
||||
const pref = connection.GetLongPreference("installed-theme-" + es.data.id);
|
||||
pref.setData(encoded.data);
|
||||
return date;
|
||||
});
|
||||
|
||||
const preview = new Combine([
|
||||
new VariableUiElement(iframe)
|
||||
]).SetClass("preview")
|
||||
this.mainPanel = new TabbedComponent([
|
||||
{
|
||||
header: Svg.gear_img,
|
||||
content:
|
||||
new PageSplit(
|
||||
generalSettings.SetStyle("width: 50vw;"),
|
||||
new Combine([
|
||||
new HelpText(currentSetting).SetStyle("height:calc(100% - 65vh); width: 100%; display:block; overflow-y: auto"),
|
||||
preview.SetStyle("height:65vh; width:100%; display:block")
|
||||
]).SetStyle("position:relative; width: 50%;")
|
||||
)
|
||||
},
|
||||
{
|
||||
header: Svg.layers_img,
|
||||
content: new AllLayersPanel(es, languages, userDetails)
|
||||
},
|
||||
{
|
||||
header: Svg.floppy_img,
|
||||
content: new SavePanel(this.connection, es, chronic)
|
||||
|
||||
},
|
||||
{
|
||||
header:Svg.share_img,
|
||||
content: new SharePanel(es, liveUrl, userDetails)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
const ud = this.connection.userDetails.data;
|
||||
if (!ud.loggedIn) {
|
||||
return new Combine([
|
||||
"<h3>Not Logged in</h3>",
|
||||
"You need to be logged in in order to create a custom theme",
|
||||
this.loginButton
|
||||
]).Render();
|
||||
}
|
||||
const journey = Constants.userJourney;
|
||||
if (ud.csCount <= journey.themeGeneratorReadOnlyUnlock) {
|
||||
return new Combine([
|
||||
"<h3>Too little experience</h3>",
|
||||
`<p>Creating your own (readonly) themes can only be done if you have more then <b>${journey.themeGeneratorReadOnlyUnlock}</b> changesets made</p>`,
|
||||
`<p>Making a theme including survey options can be done at <b>${journey.themeGeneratorFullUnlock}</b> changesets</p>`
|
||||
]).Render();
|
||||
}
|
||||
return this.mainPanel.Render()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import Combine from "../Base/Combine";
|
||||
import SettingsTable from "./SettingsTable";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||
|
||||
|
||||
export default class GeneralSettingsPanel extends UIElement {
|
||||
private panel: Combine;
|
||||
|
||||
public languages : UIEventSource<string[]>;
|
||||
|
||||
constructor(configuration: UIEventSource<LayoutConfigJson>, currentSetting: UIEventSource<SingleSetting<any>>) {
|
||||
super(undefined);
|
||||
|
||||
|
||||
const languagesField =
|
||||
ValidatedTextField.Mapped(
|
||||
str => {
|
||||
console.log("Language from str", str);
|
||||
return str?.split(";")?.map(str => str.trim().toLowerCase());
|
||||
},
|
||||
languages => languages.join(";"));
|
||||
this.languages = languagesField.GetValue();
|
||||
|
||||
const version = new TextField();
|
||||
const current_datetime = new Date();
|
||||
let formatted_date = current_datetime.getFullYear() + "-" + (current_datetime.getMonth() + 1) + "-" + current_datetime.getDate() + " " + current_datetime.getHours() + ":" + current_datetime.getMinutes() + ":" + current_datetime.getSeconds()
|
||||
version.GetValue().setData(formatted_date);
|
||||
|
||||
|
||||
const locationRemark = "<br/>Note that, as soon as an URL-parameter sets the location or a location is known due to a previous visit, that the theme-set location is ignored"
|
||||
|
||||
const settingsTable = new SettingsTable(
|
||||
[
|
||||
new SingleSetting(configuration, new TextField({placeholder:"id"}), "id",
|
||||
"Identifier", "The identifier of this theme. This should be a lowercase, unique string"),
|
||||
new SingleSetting(configuration, version, "version", "Version",
|
||||
"A version to indicate the theme version. Ideal is the date you created or updated the theme"),
|
||||
new SingleSetting(configuration, languagesField, "language",
|
||||
"Supported languages", "Which languages do you want to support in this theme? Type the two letter code representing your language, seperated by <span class='literal-code'>;</span>. For example:<span class='literal-code'>en;nl</span> "),
|
||||
new SingleSetting(configuration, new MultiLingualTextFields(this.languages), "title",
|
||||
"Title", "The title as shown in the welcome message, in the browser title bar, in the more screen, ..."),
|
||||
new SingleSetting(configuration, new MultiLingualTextFields(this.languages), "shortDescription","Short description",
|
||||
"The short description is shown as subtext in the social preview and on the 'more screen'-buttons. It should be at most one sentence of around ~25words"),
|
||||
new SingleSetting(configuration, new MultiLingualTextFields(this.languages, true),
|
||||
"description", "Description", "The description is shown in the welcome-message when opening MapComplete. It is a small text welcoming users"),
|
||||
new SingleSetting(configuration, new TextField({placeholder: "URL to icon"}), "icon",
|
||||
"Icon", "A visual representation for your theme; used as logo in the welcomeMessage. If your theme is official, used as favicon and webapp logo",
|
||||
{
|
||||
showIconPreview: true
|
||||
}),
|
||||
|
||||
new SingleSetting(configuration, ValidatedTextField.NumberInput("nat", n => n < 23), "startZoom","Initial zoom level",
|
||||
"When a user first loads MapComplete, this zoomlevel is shown."+locationRemark),
|
||||
new SingleSetting(configuration, ValidatedTextField.NumberInput("float", n => (n < 90 && n > -90)), "startLat","Initial latitude",
|
||||
"When a user first loads MapComplete, this latitude is shown as location."+locationRemark),
|
||||
new SingleSetting(configuration, ValidatedTextField.NumberInput("float", n => (n < 180 && n > -180)), "startLon","Initial longitude",
|
||||
"When a user first loads MapComplete, this longitude is shown as location."+locationRemark),
|
||||
|
||||
new SingleSetting(configuration, ValidatedTextField.NumberInput("pfloat", n => (n < 0.5 )), "widenFactor","Query widening",
|
||||
"When a query is run, the data within bounds of the visible map is loaded.\n" +
|
||||
"However, users tend to pan and zoom a lot. It is pretty annoying if every single pan means a reloading of the data.\n" +
|
||||
"For this, the bounds are widened in order to make a small pan still within bounds of the loaded data.\n" +
|
||||
"IF widenfactor is 0, this feature is disabled. A recommended value is between 0.5 and 0.01 (the latter for very dense queries)"),
|
||||
|
||||
new SingleSetting(configuration, new TextField({placeholder: "URL to social image"}), "socialImage",
|
||||
"og:image (aka Social Image)", "<span class='alert'>Only works on incorporated themes</span>" +
|
||||
"The Social Image is set as og:image for the HTML-site and helps social networks to show a preview", {showIconPreview: true})
|
||||
], currentSetting);
|
||||
|
||||
this.panel = new Combine([
|
||||
"<h3>General theme settings</h3>",
|
||||
settingsTable
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return this.panel.Render();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
|
||||
|
||||
export class GenerateEmpty {
|
||||
public static createEmptyLayer(): LayerConfigJson {
|
||||
return {
|
||||
id: "yourlayer",
|
||||
name: {},
|
||||
minzoom: 12,
|
||||
overpassTags: {and: [""]},
|
||||
title: {},
|
||||
description: {},
|
||||
tagRenderings: [],
|
||||
hideUnderlayingFeaturesMinPercentage: 0,
|
||||
icon: {
|
||||
render: "./assets/svg/bug.svg"
|
||||
},
|
||||
width: {
|
||||
render: "8"
|
||||
},
|
||||
iconSize: {
|
||||
render: "40,40,center"
|
||||
},
|
||||
color:{
|
||||
render: "#00f"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static createEmptyLayout(): LayoutConfigJson {
|
||||
return {
|
||||
id: "id",
|
||||
title: {},
|
||||
shortDescription: {},
|
||||
description: {},
|
||||
language: [],
|
||||
maintainer: "",
|
||||
icon: "./assets/svg/bug.svg",
|
||||
version: "0",
|
||||
startLat: 0,
|
||||
startLon: 0,
|
||||
startZoom: 1,
|
||||
widenFactor: 0.05,
|
||||
socialImage: "",
|
||||
|
||||
layers: [
|
||||
GenerateEmpty.createEmptyLayer()
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
public static createTestLayout(): LayoutConfigJson {
|
||||
return {
|
||||
id: "test",
|
||||
title: {"en": "Test layout"},
|
||||
shortDescription: {},
|
||||
description: {"en": "A layout for testing"},
|
||||
language: ["en"],
|
||||
maintainer: "Pieter Vander Vennet",
|
||||
icon: "./assets/svg/bug.svg",
|
||||
version: "0",
|
||||
startLat: 0,
|
||||
startLon: 0,
|
||||
startZoom: 1,
|
||||
widenFactor: 0.05,
|
||||
socialImage: "",
|
||||
layers: [{
|
||||
id: "testlayer",
|
||||
name: {en:"Testing layer"},
|
||||
minzoom: 15,
|
||||
overpassTags: {and: ["highway=residential"]},
|
||||
title: {},
|
||||
description: {"en": "Some Description"},
|
||||
icon: {render: {en: "./assets/svg/pencil.svg"}},
|
||||
width: {render: {en: "5"}},
|
||||
tagRenderings: [{
|
||||
render: {"en":"Test Rendering"}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
public static createEmptyTagRendering(): TagRenderingConfigJson {
|
||||
return {};
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Combine from "../Base/Combine";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
export default class HelpText extends UIElement {
|
||||
|
||||
private helpText: UIElement;
|
||||
private returnButton: UIElement;
|
||||
|
||||
constructor(currentSetting: UIEventSource<SingleSetting<any>>) {
|
||||
super();
|
||||
this.returnButton = new SubtleButton(Svg.close_ui(),
|
||||
new VariableUiElement(
|
||||
currentSetting.map(currentSetting => {
|
||||
if (currentSetting === undefined) {
|
||||
return "";
|
||||
}
|
||||
return "Return to general help";
|
||||
}
|
||||
)
|
||||
))
|
||||
.ListenTo(currentSetting)
|
||||
.SetClass("small-button")
|
||||
.onClick(() => currentSetting.setData(undefined));
|
||||
|
||||
|
||||
this.helpText = new VariableUiElement(currentSetting.map((setting: SingleSetting<any>) => {
|
||||
if (setting === undefined) {
|
||||
return "<h1>Welcome to the Custom Theme Builder</h1>" +
|
||||
"Here, one can make their own custom mapcomplete themes.<br/>" +
|
||||
"Fill out the fields to get a working mapcomplete theme. More information on the selected field will appear here when you click it.<br/>" +
|
||||
"Want to see how the quests are doing in number of visits? All the stats are open on <a href='https://pietervdvn.goatcounter.com' target='_blank'>goatcounter</a>";
|
||||
}
|
||||
|
||||
return new Combine(["<h1>", setting._name, "</h1>", setting._description.Render()]).Render();
|
||||
}))
|
||||
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([this.helpText,
|
||||
this.returnButton,
|
||||
]).Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,251 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import SettingsTable from "./SettingsTable";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Combine from "../Base/Combine";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import AndOrTagInput from "../Input/AndOrTagInput";
|
||||
import TagRenderingPanel from "./TagRenderingPanel";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
|
||||
import {MultiInput} from "../Input/MultiInput";
|
||||
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
|
||||
import PresetInputPanel from "./PresetInputPanel";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||
import Svg from "../../Svg";
|
||||
import Constants from "../../Models/Constants";
|
||||
|
||||
/**
|
||||
* Shows the configuration for a single layer
|
||||
*/
|
||||
export default class LayerPanel extends UIElement {
|
||||
private readonly _config: UIEventSource<LayoutConfigJson>;
|
||||
|
||||
private readonly settingsTable: UIElement;
|
||||
private readonly mapRendering: UIElement;
|
||||
|
||||
private readonly deleteButton: UIElement;
|
||||
|
||||
public readonly titleRendering: UIElement;
|
||||
|
||||
public readonly selectedTagRendering: UIEventSource<TagRenderingPanel>
|
||||
= new UIEventSource<TagRenderingPanel>(undefined);
|
||||
private tagRenderings: UIElement;
|
||||
private presetsPanel: UIElement;
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>,
|
||||
languages: UIEventSource<string[]>,
|
||||
index: number,
|
||||
currentlySelected: UIEventSource<SingleSetting<any>>,
|
||||
userDetails: UserDetails) {
|
||||
super();
|
||||
this._config = config;
|
||||
this.mapRendering = this.setupRenderOptions(config, languages, index, currentlySelected, userDetails);
|
||||
|
||||
const actualDeleteButton = new SubtleButton(
|
||||
Svg.delete_icon_ui(),
|
||||
"Yes, delete this layer"
|
||||
).onClick(() => {
|
||||
config.data.layers.splice(index, 1);
|
||||
config.ping();
|
||||
});
|
||||
|
||||
this.deleteButton = new CheckBox(
|
||||
new Combine(
|
||||
[
|
||||
"<h3>Confirm layer deletion</h3>",
|
||||
new SubtleButton(
|
||||
Svg.close_ui(),
|
||||
"No, don't delete"
|
||||
),
|
||||
"<span class='alert'>Deleting a layer can not be undone!</span>",
|
||||
actualDeleteButton
|
||||
]
|
||||
),
|
||||
new SubtleButton(
|
||||
Svg.delete_icon_ui(),
|
||||
"Remove this layer"
|
||||
)
|
||||
)
|
||||
|
||||
function setting(input: InputElement<any>, path: string | string[], name: string, description: string | UIElement): SingleSetting<any> {
|
||||
let pathPre = ["layers", index];
|
||||
if (typeof (path) === "string") {
|
||||
pathPre.push(path);
|
||||
} else {
|
||||
pathPre = pathPre.concat(path);
|
||||
}
|
||||
|
||||
return new SingleSetting<any>(config, input, pathPre, name, description);
|
||||
}
|
||||
|
||||
|
||||
this.settingsTable = new SettingsTable([
|
||||
setting(new TextField({placeholder:"Layer id"}), "id", "Id", "An identifier for this layer<br/>This should be a simple, lowercase, human readable string that is used to identify the layer."),
|
||||
setting(new MultiLingualTextFields(languages), "name", "Name", "The human-readable name of this layer<br/>Used in the layer control panel and the 'Personal theme'"),
|
||||
setting(new MultiLingualTextFields(languages, true), "description", "Description", "A description for this layer.<br/>Shown in the layer selections and in the personal theme"),
|
||||
setting(ValidatedTextField.NumberInput("nat", n => n < 23), "minzoom", "Minimal zoom",
|
||||
"The minimum zoomlevel needed to load and show this layer."),
|
||||
setting(new DropDown("", [
|
||||
{value: 0, shown: "Show ways and areas as ways and lines"},
|
||||
{value: 2, shown: "Show both the ways/areas and the centerpoints"},
|
||||
{value: 1, shown: "Show everything as centerpoint"}]), "wayHandling", "Way handling",
|
||||
"Describes how ways and areas are represented on the map: areas can be represented as the area itself, or it can be converted into the centerpoint"),
|
||||
setting(new AndOrTagInput(), ["overpassTags"], "Overpass query",
|
||||
"The tags of the objects to load from overpass"),
|
||||
|
||||
],
|
||||
currentlySelected);
|
||||
const self = this;
|
||||
|
||||
const popupTitleRendering = new TagRenderingPanel(languages, currentlySelected, userDetails, {
|
||||
title: "Popup title",
|
||||
description: "This is the rendering shown as title in the popup for this element",
|
||||
disableQuestions: true
|
||||
});
|
||||
|
||||
new SingleSetting(config, popupTitleRendering, ["layers", index, "title"], "Popup title", "This is the rendering shown as title in the popup");
|
||||
this.titleRendering = popupTitleRendering;
|
||||
this.registerTagRendering(popupTitleRendering);
|
||||
|
||||
|
||||
const renderings = config.map(config => {
|
||||
const layer = config.layers[index] as LayerConfigJson;
|
||||
// @ts-ignore
|
||||
const renderings : TagRenderingConfigJson[] = layer.tagRenderings ;
|
||||
return renderings;
|
||||
});
|
||||
const tagRenderings = new MultiInput<TagRenderingConfigJson>("Add a tag rendering/question",
|
||||
() => ({}),
|
||||
() => {
|
||||
const tagPanel = new TagRenderingPanel(languages, currentlySelected, userDetails)
|
||||
self.registerTagRendering(tagPanel);
|
||||
return tagPanel;
|
||||
}, renderings,
|
||||
{allowMovement: true});
|
||||
|
||||
tagRenderings.GetValue().addCallback(
|
||||
tagRenderings => {
|
||||
(config.data.layers[index] as LayerConfigJson).tagRenderings = tagRenderings;
|
||||
config.ping();
|
||||
}
|
||||
)
|
||||
|
||||
if (userDetails.csCount >= Constants.userJourney.themeGeneratorFullUnlock) {
|
||||
|
||||
const presetPanel = new MultiInput("Add a preset",
|
||||
() => ({tags: [], title: {}}),
|
||||
() => new PresetInputPanel(currentlySelected, languages),
|
||||
undefined, {allowMovement: true});
|
||||
new SingleSetting(config, presetPanel, ["layers", index, "presets"], "Presets", "")
|
||||
this.presetsPanel = presetPanel;
|
||||
} else {
|
||||
this.presetsPanel = new FixedUiElement(`Creating a custom theme which also edits OSM is only unlocked after ${Constants.userJourney.themeGeneratorFullUnlock} changesets`).SetClass("alert");
|
||||
}
|
||||
|
||||
function loadTagRenderings() {
|
||||
const values = (config.data.layers[index] as LayerConfigJson).tagRenderings;
|
||||
const renderings: TagRenderingConfigJson[] = [];
|
||||
for (const value of values) {
|
||||
if (typeof (value) !== "string") {
|
||||
renderings.push(value);
|
||||
}
|
||||
|
||||
}
|
||||
tagRenderings.GetValue().setData(renderings);
|
||||
}
|
||||
|
||||
loadTagRenderings();
|
||||
|
||||
this.tagRenderings = tagRenderings;
|
||||
|
||||
|
||||
}
|
||||
|
||||
private setupRenderOptions(config: UIEventSource<LayoutConfigJson>,
|
||||
languages: UIEventSource<string[]>,
|
||||
index: number,
|
||||
currentlySelected: UIEventSource<SingleSetting<any>>,
|
||||
userDetails: UserDetails
|
||||
): UIElement {
|
||||
const iconSelect = new TagRenderingPanel(
|
||||
languages, currentlySelected, userDetails,
|
||||
{
|
||||
title: "Icon",
|
||||
description: "A visual representation for this layer and for the points on the map.",
|
||||
disableQuestions: true,
|
||||
noLanguage: true
|
||||
});
|
||||
const size = new TagRenderingPanel(languages, currentlySelected, userDetails,
|
||||
{
|
||||
title: "Icon Size",
|
||||
description: "The size of the icons on the map in pixels. Can vary based on the tagging",
|
||||
disableQuestions: true,
|
||||
noLanguage: true
|
||||
});
|
||||
const color = new TagRenderingPanel(languages, currentlySelected, userDetails,
|
||||
{
|
||||
title: "Way and area color",
|
||||
description: "The color or a shown way or area. Can vary based on the tagging",
|
||||
disableQuestions: true,
|
||||
noLanguage: true
|
||||
});
|
||||
const stroke = new TagRenderingPanel(languages, currentlySelected, userDetails,
|
||||
{
|
||||
title: "Stroke width",
|
||||
description: "The width of lines representing ways and the outline of areas. Can vary based on the tags",
|
||||
disableQuestions: true,
|
||||
noLanguage: true
|
||||
});
|
||||
this.registerTagRendering(iconSelect);
|
||||
this.registerTagRendering(size);
|
||||
this.registerTagRendering(color);
|
||||
this.registerTagRendering(stroke);
|
||||
|
||||
function setting(input: InputElement<any>, path, isIcon: boolean = false): SingleSetting<TagRenderingConfigJson> {
|
||||
return new SingleSetting(config, input, ["layers", index, path], undefined, undefined)
|
||||
}
|
||||
|
||||
return new SettingsTable([
|
||||
setting(iconSelect, "icon"),
|
||||
setting(size, "iconSize"),
|
||||
setting(color, "color"),
|
||||
setting(stroke, "width")
|
||||
], currentlySelected);
|
||||
}
|
||||
|
||||
private registerTagRendering(
|
||||
tagRenderingPanel: TagRenderingPanel) {
|
||||
|
||||
tagRenderingPanel.IsHovered().addCallback(isHovering => {
|
||||
if (!isHovering) {
|
||||
return;
|
||||
}
|
||||
this.selectedTagRendering.setData(tagRenderingPanel);
|
||||
})
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([
|
||||
"<h2>General layer settings</h2>",
|
||||
this.settingsTable,
|
||||
"<h2>Popup contents</h2>",
|
||||
this.titleRendering,
|
||||
this.tagRenderings,
|
||||
"<h2>Presets</h2>",
|
||||
"Does this theme support adding a new point?<br/>If this should be the case, add a preset. Make sure that the preset tags do match the overpass-tags, otherwise it might seem like the newly added points dissapear ",
|
||||
this.presetsPanel,
|
||||
"<h2>Map rendering options</h2>",
|
||||
this.mapRendering,
|
||||
"<h2>Layer delete</h2>",
|
||||
this.deleteButton
|
||||
]).Render();
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import LayerPanel from "./LayerPanel";
|
||||
import HelpText from "./HelpText";
|
||||
import {MultiTagInput} from "../Input/MultiTagInput";
|
||||
import {FromJSON} from "../../Customizations/JSON/FromJSON";
|
||||
import Combine from "../Base/Combine";
|
||||
import PageSplit from "../Base/PageSplit";
|
||||
import TagRenderingPreview from "./TagRenderingPreview";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
|
||||
|
||||
export default class LayerPanelWithPreview extends UIElement{
|
||||
private panel: UIElement;
|
||||
constructor(config: UIEventSource<any>, languages: UIEventSource<string[]>, index: number, userDetails: UserDetails) {
|
||||
super();
|
||||
|
||||
const currentlySelected = new UIEventSource<(SingleSetting<any>)>(undefined);
|
||||
const layer = new LayerPanel(config, languages, index, currentlySelected, userDetails);
|
||||
const helpText = new HelpText(currentlySelected);
|
||||
|
||||
const previewTagInput = new MultiTagInput();
|
||||
previewTagInput.GetValue().setData(["id=123456"]);
|
||||
|
||||
const previewTagValue = previewTagInput.GetValue().map(tags => {
|
||||
const properties = {};
|
||||
for (const str of tags) {
|
||||
const tag = FromJSON.SimpleTag(str);
|
||||
if (tag !== undefined) {
|
||||
properties[tag.key] = tag.value;
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
});
|
||||
|
||||
const preview = new TagRenderingPreview(layer.selectedTagRendering, previewTagValue);
|
||||
|
||||
this.panel = new PageSplit(
|
||||
layer.SetClass("scrollable"),
|
||||
new Combine([
|
||||
helpText,
|
||||
"</br>",
|
||||
"<h2>Testing tags</h2>",
|
||||
previewTagInput,
|
||||
"<h2>Tag Rendering preview</h2>",
|
||||
preview
|
||||
|
||||
]), 60
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.panel.Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import {InputElement} from "../Input/InputElement";
|
||||
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import SettingsTable from "./SettingsTable";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import AndOrTagInput from "../Input/AndOrTagInput";
|
||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
|
||||
export default class MappingInput extends InputElement<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }> {
|
||||
|
||||
private readonly _value: UIEventSource<{ if: AndOrTagConfigJson; then: any; hideInAnswer?: boolean }>;
|
||||
private readonly _panel: UIElement;
|
||||
|
||||
constructor(languages: UIEventSource<any>, disableQuestions: boolean = false) {
|
||||
super();
|
||||
const currentSelected = new UIEventSource<SingleSetting<any>>(undefined);
|
||||
this._value = new UIEventSource<{ if: AndOrTagConfigJson, then: any, hideInAnswer?: boolean }>({
|
||||
if: undefined,
|
||||
then: undefined
|
||||
});
|
||||
const self = this;
|
||||
|
||||
function setting(inputElement: InputElement<any>, path: string, name: string, description: string | UIElement) {
|
||||
return new SingleSetting(self._value, inputElement, path, name, description);
|
||||
}
|
||||
|
||||
const withQuestions = [setting(new DropDown("",
|
||||
[{value: false, shown: "Can be used as answer"}, {value: true, shown: "Not an answer option"}]),
|
||||
"hideInAnswer", "Answer option",
|
||||
"Sometimes, multiple tags for the same meaning are used (e.g. <span class='literal-code'>access=yes</span> and <span class='literal-code'>access=public</span>)." +
|
||||
"Use this toggle to disable an anwer. Alternatively an implied/assumed rendering can be used. In order to do this:" +
|
||||
"use a single tag in the 'if' with <i>no</i> value defined, e.g. <span class='literal-code'>indoor=</span>. The mapping will then be shown as default until explicitly changed"
|
||||
)];
|
||||
|
||||
this._panel = new SettingsTable([
|
||||
setting(new AndOrTagInput(), "if", "If matches", "If this condition matches, the template <b>then</b> below will be used"),
|
||||
setting(new MultiLingualTextFields(languages),
|
||||
"then", "Then show", "If the condition above matches, this template <b>then</b> below will be shown to the user."),
|
||||
...(disableQuestions ? [] : withQuestions)
|
||||
|
||||
], currentSelected).SetClass("bordered tag-mapping");
|
||||
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return this._panel.Render();
|
||||
}
|
||||
|
||||
|
||||
GetValue(): UIEventSource<{ if: AndOrTagConfigJson; then: any; hideInAnswer?: boolean }> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
IsValid(t: { if: AndOrTagConfigJson; then: any; hideInAnswer: boolean }): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import {InputElement} from "../Input/InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {MultiTagInput} from "../Input/MultiTagInput";
|
||||
import SettingsTable from "./SettingsTable";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
||||
import Combine from "../Base/Combine";
|
||||
|
||||
export default class PresetInputPanel extends InputElement<{
|
||||
title: string | any,
|
||||
tags: string[],
|
||||
description?: string | any
|
||||
}> {
|
||||
private readonly _value: UIEventSource<{
|
||||
title: string | any,
|
||||
tags: string[],
|
||||
description?: string | any
|
||||
}>;
|
||||
private readonly panel: UIElement;
|
||||
|
||||
|
||||
constructor(currentlySelected: UIEventSource<SingleSetting<any>>, languages: UIEventSource<string[]>) {
|
||||
super();
|
||||
this._value = new UIEventSource({tags: [], title: {}});
|
||||
|
||||
|
||||
const self = this;
|
||||
function s(input: InputElement<any>, path: string, name: string, description: string){
|
||||
return new SingleSetting(self._value, input, path, name, description)
|
||||
}
|
||||
this.panel = new SettingsTable([
|
||||
s(new MultiTagInput(), "tags","Preset tags","These tags will be applied on the newly created point"),
|
||||
s(new MultiLingualTextFields(languages), "title","Preset title","This little text is shown in bold on the 'create new point'-button" ),
|
||||
s(new MultiLingualTextFields(languages), "description","Description", "This text is shown in the button as description when creating a new point")
|
||||
], currentlySelected).SetStyle("display: block; border: 1px solid black; border-radius: 1em;padding: 1em;");
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([this.panel]).Render();
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<{
|
||||
title: string | any,
|
||||
tags: string[],
|
||||
description?: string | any
|
||||
}> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
IsValid(t: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import Combine from "../Base/Combine";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
export default class SavePanel extends UIElement {
|
||||
private json: UIElement;
|
||||
private lastSaveEl: UIElement;
|
||||
private loadFromJson: UIElement;
|
||||
|
||||
constructor(
|
||||
connection: OsmConnection,
|
||||
config: UIEventSource<LayoutConfigJson>,
|
||||
chronic: UIEventSource<Date>) {
|
||||
super();
|
||||
|
||||
|
||||
this.lastSaveEl = new VariableUiElement(chronic
|
||||
.map(date => {
|
||||
if (date === undefined) {
|
||||
return new FixedUiElement("Your theme will be saved automatically within two minutes... Click here to force saving").SetClass("alert").Render()
|
||||
}
|
||||
return "Your theme was last saved at " + date.toISOString()
|
||||
})).onClick(() => chronic.setData(new Date()));
|
||||
|
||||
const jsonStr = config.map(config =>
|
||||
JSON.stringify(config, null, 2));
|
||||
|
||||
|
||||
const jsonTextField = new TextField({
|
||||
placeholder: "JSON Config",
|
||||
value: jsonStr,
|
||||
textArea: true,
|
||||
textAreaRows: 20
|
||||
});
|
||||
this.json = jsonTextField;
|
||||
this.loadFromJson = new SubtleButton(Svg.reload_ui(), "<b>Load the JSON file below</b>")
|
||||
.onClick(() => {
|
||||
try{
|
||||
const json = jsonTextField.GetValue().data;
|
||||
const parsed : LayoutConfigJson = JSON.parse(json);
|
||||
config.setData(parsed);
|
||||
}catch(e){
|
||||
alert("Invalid JSON: "+e)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([
|
||||
"<h3>Save your theme</h3>",
|
||||
this.lastSaveEl,
|
||||
"<h3>JSON configuration</h3>",
|
||||
"The url hash is actually no more then a BASE64-encoding of the below JSON. This json contains the full configuration of the theme.<br/>" +
|
||||
"This configuration is mainly useful for debugging",
|
||||
"<br/>",
|
||||
this.loadFromJson,
|
||||
this.json
|
||||
]).SetClass("scrollable")
|
||||
.Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import SingleSetting from "./SingleSetting";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import PageSplit from "../Base/PageSplit";
|
||||
import Combine from "../Base/Combine";
|
||||
|
||||
export default class SettingsTable extends UIElement {
|
||||
|
||||
private _col1: UIElement[] = [];
|
||||
private _col2: UIElement[] = [];
|
||||
|
||||
public selectedSetting: UIEventSource<SingleSetting<any>>;
|
||||
|
||||
constructor(elements: (SingleSetting<any> | string)[],
|
||||
currentSelectedSetting?: UIEventSource<SingleSetting<any>>) {
|
||||
super(undefined);
|
||||
const self = this;
|
||||
this.selectedSetting = currentSelectedSetting ?? new UIEventSource<SingleSetting<any>>(undefined);
|
||||
for (const element of elements) {
|
||||
if(typeof element === "string"){
|
||||
this._col1.push(new FixedUiElement(element));
|
||||
this._col2.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
let title: UIElement = element._name === undefined ? null : new FixedUiElement(element._name);
|
||||
this._col1.push(title);
|
||||
this._col2.push(element._value);
|
||||
element._value.SetStyle("display:block");
|
||||
element._value.IsSelected.addCallback(isSelected => {
|
||||
if (isSelected) {
|
||||
self.selectedSetting.setData(element);
|
||||
} else if (self.selectedSetting.data === element) {
|
||||
self.selectedSetting.setData(undefined);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
let elements = [];
|
||||
|
||||
for (let i = 0; i < this._col1.length; i++) {
|
||||
if(this._col1[i] !== null && this._col2[i] !== null){
|
||||
elements.push(new PageSplit(this._col1[i], this._col2[i], 25));
|
||||
}else if(this._col1[i] !== null){
|
||||
elements.push(this._col1[i])
|
||||
}else{
|
||||
elements.push(this._col2[i])
|
||||
}
|
||||
}
|
||||
|
||||
return new Combine(elements).Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import Combine from "../Base/Combine";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
|
||||
export default class SharePanel extends UIElement {
|
||||
private _config: UIEventSource<LayoutConfigJson>;
|
||||
|
||||
private _panel: UIElement;
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>, liveUrl: UIEventSource<string>, userDetails: UserDetails) {
|
||||
super(undefined);
|
||||
this._config = config;
|
||||
|
||||
this._panel = new Combine([
|
||||
"<h2>Share</h2>",
|
||||
"Share the following link with friends:<br/>",
|
||||
new VariableUiElement(liveUrl.map(url => `<a href='${url}' target="_blank">${url}</a>`)),
|
||||
"<h2>Publish on some website</h2>",
|
||||
|
||||
"It is possible to load a JSON-file from the wide internet, but you'll need some (public CORS-enabled) server.",
|
||||
`Put the raw json online, and use ${window.location.host}?userlayout=https://<your-url-here>.json`,
|
||||
"Please note: it used to be possible to load from the wiki - this is not possible anymore due to technical reasons.",
|
||||
"</div>"
|
||||
]);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._panel.Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Combine from "../Base/Combine";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
export default class SingleSetting<T> {
|
||||
public _value: InputElement<T>;
|
||||
public _name: string;
|
||||
public _description: UIElement;
|
||||
public _options: { showIconPreview?: boolean };
|
||||
|
||||
constructor(config: UIEventSource<any>,
|
||||
value: InputElement<T>,
|
||||
path: string | (string | number)[],
|
||||
name: string,
|
||||
description: string | UIElement,
|
||||
options?: {
|
||||
showIconPreview?: boolean
|
||||
}
|
||||
) {
|
||||
this._value = value;
|
||||
this._name = name;
|
||||
this._description = Translations.W(description);
|
||||
|
||||
this._options = options ?? {};
|
||||
if (this._options.showIconPreview) {
|
||||
this._description = new Combine([
|
||||
this._description,
|
||||
"<h3>Icon preview</h3>",
|
||||
new VariableUiElement(this._value.GetValue().map(url => `<img src='${url}' class="image-large-preview">`))
|
||||
]);
|
||||
}
|
||||
|
||||
if(typeof (path) === "string"){
|
||||
path = [path];
|
||||
}
|
||||
const lastPart = path[path.length - 1];
|
||||
path.splice(path.length - 1, 1);
|
||||
|
||||
function assignValue(value) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
// We have to rewalk every time as parts might be new
|
||||
let configPart = config.data;
|
||||
for (const pathPart of path) {
|
||||
let newConfigPart = configPart[pathPart];
|
||||
if (newConfigPart === undefined) {
|
||||
if (typeof (pathPart) === "string") {
|
||||
configPart[pathPart] = {};
|
||||
} else {
|
||||
configPart[pathPart] = [];
|
||||
}
|
||||
newConfigPart = configPart[pathPart];
|
||||
}
|
||||
configPart = newConfigPart;
|
||||
}
|
||||
configPart[lastPart] = value;
|
||||
config.ping();
|
||||
}
|
||||
|
||||
function loadValue() {
|
||||
let configPart = config.data;
|
||||
for (const pathPart of path) {
|
||||
configPart = configPart[pathPart];
|
||||
if (configPart === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const loadedValue = configPart[lastPart];
|
||||
if (loadedValue !== undefined) {
|
||||
value.GetValue().setData(loadedValue);
|
||||
}
|
||||
}
|
||||
loadValue();
|
||||
config.addCallback(() => loadValue());
|
||||
|
||||
value.GetValue().addCallback(assignValue);
|
||||
assignValue(this._value.GetValue().data);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import SettingsTable from "./SettingsTable";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import Combine from "../Base/Combine";
|
||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
||||
import AndOrTagInput from "../Input/AndOrTagInput";
|
||||
import {MultiTagInput} from "../Input/MultiTagInput";
|
||||
import {MultiInput} from "../Input/MultiInput";
|
||||
import MappingInput from "./MappingInput";
|
||||
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
|
||||
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||
import SpecialVisualizations from "../SpecialVisualizations";
|
||||
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
|
||||
import Constants from "../../Models/Constants";
|
||||
|
||||
export default class TagRenderingPanel extends InputElement<TagRenderingConfigJson> {
|
||||
|
||||
public IsImage = false;
|
||||
public options: { title?: string; description?: string; disableQuestions?: boolean; isImage?: boolean; };
|
||||
public readonly validText: UIElement;
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private intro: UIElement;
|
||||
private settingsTable: UIElement;
|
||||
private readonly _value: UIEventSource<TagRenderingConfigJson>;
|
||||
|
||||
constructor(languages: UIEventSource<string[]>,
|
||||
currentlySelected: UIEventSource<SingleSetting<any>>,
|
||||
userDetails: UserDetails,
|
||||
options?: {
|
||||
title?: string,
|
||||
description?: string,
|
||||
disableQuestions?: boolean,
|
||||
isImage?: boolean,
|
||||
noLanguage?: boolean
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.SetClass("bordered");
|
||||
this.SetClass("min-height");
|
||||
|
||||
this.options = options ?? {};
|
||||
const questionsNotUnlocked = userDetails.csCount < Constants.userJourney.themeGeneratorFullUnlock;
|
||||
this.options.disableQuestions =
|
||||
(this.options.disableQuestions ?? false) ||
|
||||
questionsNotUnlocked;
|
||||
|
||||
this.intro = new Combine(["<h3>", options?.title ?? "TagRendering", "</h3>",
|
||||
options?.description ?? "A tagrendering converts OSM-tags into a value on screen. Fill out the field 'render' with the text that should appear. Note that `{key}` will be replaced with the corresponding `value`, if present.<br/>For specific known tags (e.g. if `foo=bar`, make a mapping). "])
|
||||
this.IsImage = options?.isImage ?? false;
|
||||
|
||||
const value = new UIEventSource<TagRenderingConfigJson>({});
|
||||
this._value = value;
|
||||
|
||||
function setting(input: InputElement<any>, id: string | string[], name: string, description: string | UIElement): SingleSetting<any> {
|
||||
return new SingleSetting<any>(value, input, id, name, description);
|
||||
}
|
||||
|
||||
this._value.addCallback(value => {
|
||||
let doPing = false;
|
||||
if (value?.freeform?.key == "") {
|
||||
value.freeform = undefined;
|
||||
doPing = true;
|
||||
}
|
||||
|
||||
if (value?.render == "") {
|
||||
value.render = undefined;
|
||||
doPing = true;
|
||||
}
|
||||
|
||||
if (doPing) {
|
||||
this._value.ping();
|
||||
}
|
||||
})
|
||||
|
||||
const questionSettings = [
|
||||
|
||||
|
||||
setting(options?.noLanguage ? new TextField({placeholder: "question"}) : new MultiLingualTextFields(languages)
|
||||
, "question", "Question", "If the key or mapping doesn't match, this question is asked"),
|
||||
|
||||
"<h3>Freeform key</h3>",
|
||||
setting(ValidatedTextField.KeyInput(true), ["freeform", "key"], "Freeform key<br/>",
|
||||
"If specified, the rendering will search if this key is present." +
|
||||
"If it is, the rendering above will be used to display the element.<br/>" +
|
||||
"The rendering will go into question mode if <ul><li>this key is not present</li><li>No single mapping matches</li><li>A question is given</li>"),
|
||||
|
||||
setting(ValidatedTextField.TypeDropdown(), ["freeform", "type"], "Freeform type",
|
||||
"The type of this freeform text field, in order to validate"),
|
||||
setting(new MultiTagInput(), ["freeform", "addExtraTags"], "Extra tags on freeform",
|
||||
"When the freeform text field is used, the user might mean a predefined key. This field allows to add extra tags, e.g. <span class='literal-code'>fixme=User used a freeform field - to check</span>"),
|
||||
|
||||
];
|
||||
|
||||
const settings: (string | SingleSetting<any>)[] = [
|
||||
setting(
|
||||
options?.noLanguage ? new TextField({placeholder: "Rendering"}) :
|
||||
new MultiLingualTextFields(languages), "render", "Value to show",
|
||||
"Renders this value. Note that <span class='literal-code'>{key}</span>-parts are substituted by the corresponding values of the element. If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value." +
|
||||
"<br/><br/>" +
|
||||
"Furhtermore, some special functions are supported:" + SpecialVisualizations.HelpMessage.Render()),
|
||||
|
||||
questionsNotUnlocked ? `You need at least ${Constants.userJourney.themeGeneratorFullUnlock} changesets to unlock the 'question'-field and to use your theme to edit OSM data` : "",
|
||||
...(options?.disableQuestions ? [] : questionSettings),
|
||||
|
||||
"<h3>Mappings</h3>",
|
||||
setting(new MultiInput<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }>("Add a mapping",
|
||||
() => ({if: {and: []}, then: {}}),
|
||||
() => new MappingInput(languages, options?.disableQuestions ?? false),
|
||||
undefined, {allowMovement: true}), "mappings",
|
||||
"If a tag matches, then show the first respective text", ""),
|
||||
|
||||
"<h3>Condition</h3>",
|
||||
setting(new AndOrTagInput(), "condition", "Only show this tagrendering if the following condition applies",
|
||||
"Only show this tag rendering if these tags matches. Optional field.<br/>Note that the Overpass-tags are already always included in this object"),
|
||||
|
||||
|
||||
];
|
||||
|
||||
this.settingsTable = new SettingsTable(settings, currentlySelected);
|
||||
|
||||
|
||||
this.validText = new VariableUiElement(value.map((json: TagRenderingConfigJson) => {
|
||||
try {
|
||||
new TagRenderingConfig(json, undefined, options?.title ?? "");
|
||||
return "";
|
||||
} catch (e) {
|
||||
return "<span class='alert'>" + e + "</span>"
|
||||
}
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([
|
||||
this.intro,
|
||||
this.settingsTable,
|
||||
this.validText]).Render();
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<TagRenderingConfigJson> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsValid(t: TagRenderingConfigJson): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import TagRenderingPanel from "./TagRenderingPanel";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
|
||||
import EditableTagRendering from "../Popup/EditableTagRendering";
|
||||
|
||||
export default class TagRenderingPreview extends UIElement {
|
||||
|
||||
private readonly previewTagValue: UIEventSource<any>;
|
||||
private selectedTagRendering: UIEventSource<TagRenderingPanel>;
|
||||
private panel: UIElement;
|
||||
|
||||
constructor(selectedTagRendering: UIEventSource<TagRenderingPanel>,
|
||||
previewTagValue: UIEventSource<any>) {
|
||||
super(selectedTagRendering);
|
||||
this.selectedTagRendering = selectedTagRendering;
|
||||
this.previewTagValue = previewTagValue;
|
||||
this.panel = this.GetPanel(undefined);
|
||||
const self = this;
|
||||
this.selectedTagRendering.addCallback(trp => {
|
||||
self.panel = self.GetPanel(trp);
|
||||
self.Update();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private GetPanel(tagRenderingPanel: TagRenderingPanel): UIElement {
|
||||
if (tagRenderingPanel === undefined) {
|
||||
return new FixedUiElement("No tag rendering selected at the moment. Hover over a tag rendering to see what it looks like");
|
||||
}
|
||||
|
||||
let es = tagRenderingPanel.GetValue();
|
||||
|
||||
let rendering: UIElement;
|
||||
const self = this;
|
||||
try {
|
||||
rendering =
|
||||
new VariableUiElement(es.map(tagRenderingConfig => {
|
||||
try {
|
||||
const tr = new EditableTagRendering(self.previewTagValue, new TagRenderingConfig(tagRenderingConfig, undefined,"preview"));
|
||||
return tr.Render();
|
||||
} catch (e) {
|
||||
return new Combine(["Could not show this tagrendering:", e.message]).Render();
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
} catch (e) {
|
||||
console.error("User defined tag rendering incorrect:", e);
|
||||
rendering = new FixedUiElement(e).SetClass("alert");
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
"<h3>",
|
||||
tagRenderingPanel.options.title ?? "Extra tag rendering",
|
||||
"</h3>",
|
||||
tagRenderingPanel.options.description ?? "This tag rendering will appear in the popup",
|
||||
"<br/><br/>",
|
||||
rendering]);
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.panel.Render();
|
||||
}
|
||||
|
||||
}
|
19
UI/Image/AttributedImage.ts
Normal file
19
UI/Image/AttributedImage.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import Attribution from "./Attribution";
|
||||
import Img from "../Base/Img";
|
||||
import ImageAttributionSource from "../../Logic/Web/ImageAttributionSource";
|
||||
|
||||
|
||||
export class AttributedImage extends Combine {
|
||||
|
||||
constructor(urlSource: string, imgSource: ImageAttributionSource) {
|
||||
urlSource = imgSource.PrepareUrl(urlSource)
|
||||
super([
|
||||
new Img( urlSource),
|
||||
new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())
|
||||
]);
|
||||
this.SetClass('block relative h-full');
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,18 +1,33 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import Translations from "../i18n/Translations";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LicenseInfo} from "../../Logic/Web/Wikimedia";
|
||||
|
||||
export default class Attribution extends Combine {
|
||||
export default class Attribution extends VariableUiElement {
|
||||
|
||||
constructor(author: UIElement | string, license: UIElement | string, icon: UIElement) {
|
||||
super([
|
||||
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em"),
|
||||
new Combine([
|
||||
Translations.W(author).SetClass("block font-bold"),
|
||||
Translations.W((license ?? "") === "undefined" ? "CC0" : (license ?? ""))
|
||||
]).SetClass("flex flex-col")
|
||||
]);
|
||||
this.SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg");
|
||||
constructor(license: UIEventSource<LicenseInfo>, icon: BaseUIElement) {
|
||||
if (license === undefined) {
|
||||
throw "No license source given in the attribution element"
|
||||
}
|
||||
super(
|
||||
license.map((license : LicenseInfo) => {
|
||||
|
||||
if (license?.artist === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
|
||||
|
||||
new Combine([
|
||||
Translations.W(license.artist).SetClass("block font-bold"),
|
||||
Translations.W((license.license ?? "") === "" ? "CC0" : (license.license ?? ""))
|
||||
]).SetClass("flex flex-col")
|
||||
]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg")
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,56 +1,55 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Translations from "../i18n/Translations";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Combine from "../Base/Combine";
|
||||
import State from "../../State";
|
||||
import Svg from "../../Svg";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
|
||||
export default class DeleteImage extends UIElement {
|
||||
private readonly key: string;
|
||||
private readonly tags: UIEventSource<any>;
|
||||
|
||||
private readonly isDeletedBadge: UIElement;
|
||||
private readonly deleteDialog: UIElement;
|
||||
export default class DeleteImage extends Toggle {
|
||||
|
||||
constructor(key: string, tags: UIEventSource<any>) {
|
||||
super(tags);
|
||||
this.tags = tags;
|
||||
this.key = key;
|
||||
|
||||
this.isDeletedBadge = Translations.t.image.isDeleted;
|
||||
const oldValue = tags.data[key]
|
||||
const isDeletedBadge = Translations.t.image.isDeleted.Clone()
|
||||
.SetClass("rounded-full p-1")
|
||||
.SetStyle("color:white;background:#ff8c8c")
|
||||
.onClick(() => {
|
||||
State.state?.changes?.addTag(tags.data.id, new Tag(key, oldValue), tags);
|
||||
});
|
||||
|
||||
const deleteButton = Translations.t.image.doDelete.Clone()
|
||||
.SetClass("block w-full pl-4 pr-4")
|
||||
.SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;")
|
||||
.onClick(() => {
|
||||
State.state?.changes.addTag(tags.data.id, new Tag(key, ""));
|
||||
State.state?.changes?.addTag(tags.data.id, new Tag(key, ""), tags);
|
||||
});
|
||||
|
||||
const cancelButton = Translations.t.general.cancel.SetClass("bg-white pl-4 pr-4").SetStyle( "border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;");
|
||||
this.deleteDialog = new CheckBox(
|
||||
const cancelButton = Translations.t.general.cancel.Clone().SetClass("bg-white pl-4 pr-4").SetStyle("border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;");
|
||||
const openDelete = Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;")
|
||||
const deleteDialog = new Toggle(
|
||||
new Combine([
|
||||
deleteButton,
|
||||
cancelButton
|
||||
]).SetClass("flex flex-col background-black"),
|
||||
Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;")
|
||||
openDelete
|
||||
)
|
||||
|
||||
}
|
||||
cancelButton.onClick(() => deleteDialog.isEnabled.setData(false))
|
||||
openDelete.onClick(() => deleteDialog.isEnabled.setData(true))
|
||||
|
||||
InnerRender(): string {
|
||||
if(! State.state?.featureSwitchUserbadge?.data){
|
||||
return "";
|
||||
}
|
||||
|
||||
const value = this.tags.data[this.key];
|
||||
if (value === undefined || value === "") {
|
||||
return this.isDeletedBadge.Render();
|
||||
}
|
||||
|
||||
return this.deleteDialog.Render();
|
||||
super(
|
||||
new Toggle(
|
||||
deleteDialog,
|
||||
isDeletedBadge,
|
||||
tags.map(tags => (tags[key] ?? "") !== "")
|
||||
),
|
||||
undefined /*Login (and thus editing) is disabled*/,
|
||||
State.state?.featureSwitchUserbadge ?? new UIEventSource<boolean>(true)
|
||||
)
|
||||
this.SetClass("cursor-pointer")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,39 +1,43 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {SlideShow} from "./SlideShow";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import DeleteImage from "./DeleteImage";
|
||||
import {WikimediaImage} from "./WikimediaImage";
|
||||
import {ImgurImage} from "./ImgurImage";
|
||||
import {MapillaryImage} from "./MapillaryImage";
|
||||
import {SimpleImageElement} from "./SimpleImageElement";
|
||||
import {AttributedImage} from "./AttributedImage";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Img from "../Base/Img";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import ImageAttributionSource from "../../Logic/Web/ImageAttributionSource";
|
||||
import {Wikimedia} from "../../Logic/Web/Wikimedia";
|
||||
import {Mapillary} from "../../Logic/Web/Mapillary";
|
||||
import {Imgur} from "../../Logic/Web/Imgur";
|
||||
|
||||
export class ImageCarousel extends UIElement{
|
||||
export class ImageCarousel extends Toggle {
|
||||
|
||||
public readonly slideshow: UIElement;
|
||||
|
||||
constructor(images: UIEventSource<{key: string, url:string}[]>, tags: UIEventSource<any>) {
|
||||
super(images);
|
||||
const uiElements = images.map((imageURLS: {key: string, url:string}[]) => {
|
||||
const uiElements: UIElement[] = [];
|
||||
constructor(images: UIEventSource<{ key: string, url: string }[]>, tags: UIEventSource<any>) {
|
||||
const uiElements = images.map((imageURLS: { key: string, url: string }[]) => {
|
||||
const uiElements: BaseUIElement[] = [];
|
||||
for (const url of imageURLS) {
|
||||
let image = ImageCarousel.CreateImageElement(url.url)
|
||||
if(url.key !== undefined){
|
||||
if (url.key !== undefined) {
|
||||
image = new Combine([
|
||||
image,
|
||||
new DeleteImage(url.key, tags).SetClass("delete-image-marker absolute top-0 left-0 pl-3")
|
||||
]).SetClass("relative");
|
||||
}
|
||||
image
|
||||
.SetClass("w-full block")
|
||||
image
|
||||
.SetClass("w-full block")
|
||||
.SetStyle("min-width: 50px; background: grey;")
|
||||
uiElements.push(image);
|
||||
}
|
||||
return uiElements;
|
||||
});
|
||||
|
||||
this.slideshow = new SlideShow(uiElements).HideOnEmpty(true);
|
||||
super(
|
||||
new SlideShow(uiElements).SetClass("w-full"),
|
||||
undefined,
|
||||
uiElements.map(els => els.length > 0)
|
||||
)
|
||||
this.SetClass("block w-full");
|
||||
this.slideshow.SetClass("w-full");
|
||||
}
|
||||
|
||||
/***
|
||||
|
@ -41,23 +45,22 @@ export class ImageCarousel extends UIElement{
|
|||
* @param url
|
||||
* @constructor
|
||||
*/
|
||||
private static CreateImageElement(url: string): UIElement {
|
||||
private static CreateImageElement(url: string): BaseUIElement {
|
||||
// @ts-ignore
|
||||
let attrSource : ImageAttributionSource = undefined;
|
||||
if (url.startsWith("File:")) {
|
||||
return new WikimediaImage(url);
|
||||
attrSource = Wikimedia.singleton
|
||||
} else if (url.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
const commons = url.substr("https://commons.wikimedia.org/wiki/".length);
|
||||
return new WikimediaImage(commons);
|
||||
attrSource = Wikimedia.singleton;
|
||||
} else if (url.toLowerCase().startsWith("https://i.imgur.com/")) {
|
||||
return new ImgurImage(url);
|
||||
attrSource = Imgur.singleton
|
||||
} else if (url.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) {
|
||||
return new MapillaryImage(url);
|
||||
attrSource = Mapillary.singleton
|
||||
} else {
|
||||
return new SimpleImageElement(new UIEventSource<string>(url));
|
||||
return new Img(url);
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.slideshow.Render();
|
||||
|
||||
return new AttributedImage(url, attrSource)
|
||||
|
||||
}
|
||||
}
|
|
@ -1,210 +1,103 @@
|
|||
import $ from "jquery"
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {Imgur} from "../../Logic/Web/Imgur";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Svg from "../../Svg";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import LicensePicker from "../BigComponents/LicensePicker";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import FileSelectorButton from "../Input/FileSelectorButton";
|
||||
import ImgurUploader from "../../Logic/Web/ImgurUploader";
|
||||
import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI";
|
||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||
|
||||
export class ImageUploadFlow extends UIElement {
|
||||
private readonly _licensePicker: UIElement;
|
||||
private readonly _tags: UIEventSource<any>;
|
||||
private readonly _selectedLicence: UIEventSource<string>;
|
||||
private readonly _isUploading: UIEventSource<number> = new UIEventSource<number>(0)
|
||||
private readonly _didFail: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _allDone: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _connectButton: UIElement;
|
||||
private readonly _imagePrefix: string;
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
|
||||
constructor(tags: UIEventSource<any>, imagePrefix: string = "image") {
|
||||
super(State.state.osmConnection.userDetails);
|
||||
this._tags = tags;
|
||||
this._imagePrefix = imagePrefix;
|
||||
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image") {
|
||||
const uploader = new ImgurUploader(url => {
|
||||
// A file was uploaded - we add it to the tags of the object
|
||||
|
||||
this.ListenTo(this._isUploading);
|
||||
this.ListenTo(this._didFail);
|
||||
this.ListenTo(this._allDone);
|
||||
const tags = tagsSource.data
|
||||
let key = imagePrefix
|
||||
if (tags[imagePrefix] !== undefined) {
|
||||
let freeIndex = 0;
|
||||
while (tags[imagePrefix + ":" + freeIndex] !== undefined) {
|
||||
freeIndex++;
|
||||
}
|
||||
key = imagePrefix + ":" + freeIndex;
|
||||
}
|
||||
console.log("Adding image:" + key, url);
|
||||
State.state.changes.addTag(tags.id, new Tag(key, url), tagsSource);
|
||||
})
|
||||
|
||||
const licensePicker = new DropDown(Translations.t.image.willBePublished,
|
||||
[
|
||||
{value: "CC0", shown: Translations.t.image.cco},
|
||||
{value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs},
|
||||
{value: "CC-BY 4.0", shown: Translations.t.image.ccb}
|
||||
],
|
||||
State.state.osmConnection.GetPreference("pictures-license"),
|
||||
"","",
|
||||
"flex flex-col sm:flex-row"
|
||||
);
|
||||
licensePicker.SetStyle("float:left");
|
||||
|
||||
const licensePicker = new LicensePicker()
|
||||
|
||||
const t = Translations.t.image;
|
||||
const label = new Combine([
|
||||
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1"),
|
||||
Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3")
|
||||
]).SetClass("p-2 border-4 border-black rounded-full text-4xl font-bold h-full align-middle w-full flex justify-center")
|
||||
|
||||
const fileSelector = new FileSelectorButton(label)
|
||||
fileSelector.GetValue().addCallback(filelist => {
|
||||
if (filelist === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._licensePicker = licensePicker;
|
||||
this._selectedLicence = licensePicker.GetValue();
|
||||
console.log("Received images from the user, starting upload")
|
||||
const license = licensePicker.GetValue()?.data ?? "CC0"
|
||||
|
||||
this._connectButton = t.pleaseLogin.Clone()
|
||||
const tags = tagsSource.data;
|
||||
|
||||
const layout = State.state?.layoutToUse?.data
|
||||
let matchingLayer: LayerConfig = undefined
|
||||
for (const layer of layout?.layers ?? []) {
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
matchingLayer = layer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const title = matchingLayer?.title?.GetRenderValue(tags)?.ConstructElement().innerText ?? tags.name ?? "Unknown area";
|
||||
const description = [
|
||||
"author:" + State.state.osmConnection.userDetails.data.name,
|
||||
"license:" + license,
|
||||
"osmid:" + tags.id,
|
||||
].join("\n");
|
||||
|
||||
uploader.uploadMany(title, description, filelist)
|
||||
|
||||
})
|
||||
|
||||
|
||||
const uploadStateUi = new UploadFlowStateUI(uploader.queue, uploader.failed, uploader.success)
|
||||
|
||||
const uploadFlow: BaseUIElement = new Combine([
|
||||
fileSelector,
|
||||
Translations.t.image.respectPrivacy.Clone().SetStyle("font-size:small;"),
|
||||
licensePicker,
|
||||
uploadStateUi
|
||||
]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center")
|
||||
|
||||
|
||||
const pleaseLoginButton = t.pleaseLogin.Clone()
|
||||
.onClick(() => State.state.osmConnection.AttemptLogin())
|
||||
.SetClass("login-button-friendly");
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
if(!State.state.featureSwitchUserbadge.data){
|
||||
return "";
|
||||
}
|
||||
|
||||
const t = Translations.t.image;
|
||||
if (State.state.osmConnection.userDetails === undefined) {
|
||||
return ""; // No user details -> logging in is probably disabled or smthing
|
||||
}
|
||||
|
||||
if (!State.state.osmConnection.userDetails.data.loggedIn) {
|
||||
return this._connectButton.Render();
|
||||
}
|
||||
|
||||
let currentState: UIElement[] = [];
|
||||
if (this._isUploading.data == 1) {
|
||||
currentState.push(t.uploadingPicture);
|
||||
} else if (this._isUploading.data > 0) {
|
||||
currentState.push(t.uploadingMultiple.Subs({count: ""+this._isUploading.data}));
|
||||
}
|
||||
|
||||
if (this._didFail.data) {
|
||||
currentState.push(t.uploadFailed);
|
||||
}
|
||||
|
||||
if (this._allDone.data) {
|
||||
currentState.push(t.uploadDone)
|
||||
}
|
||||
|
||||
let currentStateHtml : UIElement = new FixedUiElement("");
|
||||
if (currentState.length > 0) {
|
||||
currentStateHtml = new Combine(currentState);
|
||||
if (!this._allDone.data) {
|
||||
currentStateHtml.SetClass("alert");
|
||||
}else{
|
||||
currentStateHtml.SetClass("thanks");
|
||||
}
|
||||
currentStateHtml.SetStyle("display:block ruby")
|
||||
}
|
||||
|
||||
const extraInfo = new Combine([
|
||||
Translations.t.image.respectPrivacy.SetStyle("font-size:small;"),
|
||||
"<br/>",
|
||||
this._licensePicker,
|
||||
"<br/>",
|
||||
currentStateHtml,
|
||||
"<br/>"
|
||||
]);
|
||||
|
||||
const label = new Combine([
|
||||
Svg.camera_plus_svg().SetStyle("width: 36px;height: 36px;padding: 0.1em;margin-top: 5px;border-radius: 0;float: left;display:block"),
|
||||
Translations.t.image.addPicture
|
||||
]).SetClass("image-upload-flow-button")
|
||||
|
||||
const actualInputElement =
|
||||
`<input style='display: none' id='fileselector-${this.id}' type='file' accept='image/*' name='picField' multiple='multiple' alt=''/>`;
|
||||
|
||||
const form = "<form id='fileselector-form-" + this.id + "'>" +
|
||||
`<label for='fileselector-${this.id}'>` +
|
||||
label.Render() +
|
||||
"</label>" +
|
||||
actualInputElement +
|
||||
"</form>";
|
||||
|
||||
return new Combine([
|
||||
form,
|
||||
extraInfo
|
||||
]).SetClass("image-upload-flow")
|
||||
.SetStyle("margin-top: 1em;margin-bottom: 2em;text-align: center;")
|
||||
.Render();
|
||||
}
|
||||
|
||||
|
||||
private handleSuccessfulUpload(url) {
|
||||
const tags = this._tags.data;
|
||||
let key = this._imagePrefix;
|
||||
if (tags[this._imagePrefix] !== undefined) {
|
||||
|
||||
let freeIndex = 0;
|
||||
while (tags[this._imagePrefix + ":" + freeIndex] !== undefined) {
|
||||
freeIndex++;
|
||||
}
|
||||
key = this._imagePrefix + ":" + freeIndex;
|
||||
}
|
||||
console.log("Adding image:" + key, url);
|
||||
State.state.changes.addTag(tags.id, new Tag(key, url));
|
||||
}
|
||||
|
||||
private handleFiles(files) {
|
||||
console.log("Received images from the user, starting upload")
|
||||
this._isUploading.setData(files.length);
|
||||
this._allDone.setData(false);
|
||||
|
||||
if (this._selectedLicence.data === undefined) {
|
||||
this._selectedLicence.setData("CC0");
|
||||
}
|
||||
|
||||
|
||||
const tags = this._tags.data;
|
||||
const title = tags.name ?? "Unknown area";
|
||||
const description = [
|
||||
"author:" + State.state.osmConnection.userDetails.data.name,
|
||||
"license:" + (this._selectedLicence.data ?? "CC0"),
|
||||
"wikidata:" + tags.wikidata,
|
||||
"osmid:" + tags.id,
|
||||
"name:" + tags.name
|
||||
].join("\n");
|
||||
|
||||
const self = this;
|
||||
|
||||
Imgur.uploadMultiple(title,
|
||||
description,
|
||||
files,
|
||||
function (url) {
|
||||
console.log("File saved at", url);
|
||||
self._isUploading.setData(self._isUploading.data - 1);
|
||||
self.handleSuccessfulUpload(url);
|
||||
},
|
||||
function () {
|
||||
console.log("All uploads completed");
|
||||
self._allDone.setData(true);
|
||||
},
|
||||
function (failReason) {
|
||||
console.log("Upload failed due to ", failReason)
|
||||
// No need to call something from the options -> we handle this here
|
||||
self._didFail.setData(true);
|
||||
self._isUploading.data--;
|
||||
self._isUploading.ping();
|
||||
}, 0
|
||||
super(
|
||||
new Toggle(
|
||||
/*We can show the actual upload button!*/
|
||||
uploadFlow,
|
||||
/* User not logged in*/ pleaseLoginButton,
|
||||
State.state?.osmConnection?.isLoggedIn
|
||||
),
|
||||
undefined /* Nothing as the user badge is disabled*/,
|
||||
State.state.featureSwitchUserbadge
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
|
||||
this._licensePicker.Update()
|
||||
const form = document.getElementById('fileselector-form-' + this.id) as HTMLFormElement
|
||||
const selector = document.getElementById('fileselector-' + this.id)
|
||||
const self = this
|
||||
|
||||
function submitHandler() {
|
||||
self.handleFiles($(selector).prop('files'))
|
||||
}
|
||||
|
||||
if (selector != null && form != null) {
|
||||
selector.onchange = function () {
|
||||
submitHandler()
|
||||
}
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault()
|
||||
submitHandler()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LicenseInfo} from "../../Logic/Web/Wikimedia";
|
||||
import {Imgur} from "../../Logic/Web/Imgur";
|
||||
import Combine from "../Base/Combine";
|
||||
import Attribution from "./Attribution";
|
||||
import {SimpleImageElement} from "./SimpleImageElement";
|
||||
|
||||
|
||||
export class ImgurImage extends UIElement {
|
||||
|
||||
|
||||
/***
|
||||
* Dictionary from url to alreayd known license info
|
||||
*/
|
||||
private static allLicenseInfos: any = {};
|
||||
private readonly _imageMeta: UIEventSource<LicenseInfo>;
|
||||
private readonly _imageLocation: string;
|
||||
|
||||
constructor(source: string) {
|
||||
super(undefined)
|
||||
this._imageLocation = source;
|
||||
if (ImgurImage.allLicenseInfos[source] !== undefined) {
|
||||
this._imageMeta = ImgurImage.allLicenseInfos[source];
|
||||
} else {
|
||||
this._imageMeta = new UIEventSource<LicenseInfo>(null);
|
||||
ImgurImage.allLicenseInfos[source] = this._imageMeta;
|
||||
const self = this;
|
||||
Imgur.getDescriptionOfImage(source, (license) => {
|
||||
self._imageMeta.setData(license)
|
||||
})
|
||||
}
|
||||
|
||||
this.ListenTo(this._imageMeta);
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const image = new SimpleImageElement( new UIEventSource (this._imageLocation));
|
||||
|
||||
if(this._imageMeta.data === null){
|
||||
return image.Render();
|
||||
}
|
||||
|
||||
const meta = this._imageMeta.data;
|
||||
return new Combine([
|
||||
image,
|
||||
new Attribution(meta.artist, meta.license, undefined),
|
||||
|
||||
]).SetClass('block relative')
|
||||
.Render();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LicenseInfo} from "../../Logic/Web/Wikimedia";
|
||||
import {Mapillary} from "../../Logic/Web/Mapillary";
|
||||
import Svg from "../../Svg";
|
||||
import {SimpleImageElement} from "./SimpleImageElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import Attribution from "./Attribution";
|
||||
|
||||
|
||||
export class MapillaryImage extends UIElement {
|
||||
|
||||
/***
|
||||
* Dictionary from url to already known license info
|
||||
*/
|
||||
private static allLicenseInfos: any = {};
|
||||
private readonly _imageMeta: UIEventSource<LicenseInfo>;
|
||||
private readonly _imageLocation: string;
|
||||
|
||||
constructor(source: string) {
|
||||
super()
|
||||
|
||||
if (source.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) {
|
||||
source = source.substring("https://www.mapillary.com/map/im/".length);
|
||||
}
|
||||
|
||||
this._imageLocation = source;
|
||||
if (MapillaryImage.allLicenseInfos[source] !== undefined) {
|
||||
this._imageMeta = MapillaryImage.allLicenseInfos[source];
|
||||
} else {
|
||||
this._imageMeta = new UIEventSource<LicenseInfo>(null);
|
||||
MapillaryImage.allLicenseInfos[source] = this._imageMeta;
|
||||
const self = this;
|
||||
Mapillary.getDescriptionOfImage(source, (license) => {
|
||||
self._imageMeta.setData(license)
|
||||
})
|
||||
}
|
||||
|
||||
this.ListenTo(this._imageMeta);
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const url = `https://images.mapillary.com/${this._imageLocation}/thumb-640.jpg?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`;
|
||||
const image = new SimpleImageElement(new UIEventSource<string>(url))
|
||||
|
||||
const meta = this._imageMeta?.data;
|
||||
if (!meta) {
|
||||
return image.Render();
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
image,
|
||||
new Attribution(meta.artist, meta.license, Svg.mapillary_svg())
|
||||
]).SetClass("relative block").Render();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
|
||||
export class SimpleImageElement extends UIElement {
|
||||
|
||||
constructor(source: UIEventSource<string>) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return "<img src='" + this._source.data + "' alt='img'>";
|
||||
}
|
||||
|
||||
}
|
|
@ -1,56 +1,49 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {Utils} from "../../Utils";
|
||||
import Combine from "../Base/Combine";
|
||||
// @ts-ignore
|
||||
import $ from "jquery"
|
||||
|
||||
export class SlideShow extends UIElement {
|
||||
export class SlideShow extends BaseUIElement {
|
||||
|
||||
private readonly _embeddedElements: UIEventSource<UIElement[]>
|
||||
|
||||
constructor(
|
||||
embeddedElements: UIEventSource<UIElement[]>) {
|
||||
super(embeddedElements);
|
||||
this._embeddedElements = embeddedElements;
|
||||
this._embeddedElements.addCallbackAndRun(elements => {
|
||||
for (const element of elements ?? []) {
|
||||
element.SetClass("slick-carousel-content")
|
||||
private readonly embeddedElements: UIEventSource<BaseUIElement[]>;
|
||||
|
||||
constructor(embeddedElements: UIEventSource<BaseUIElement[]>) {
|
||||
super()
|
||||
this.embeddedElements =embeddedElements;
|
||||
this.SetStyle("scroll-snap-type: x mandatory; overflow-x: scroll")
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("div")
|
||||
el.style.minWidth = "min-content"
|
||||
el.style.display = "flex"
|
||||
el.style.justifyContent = "center"
|
||||
this.embeddedElements.addCallbackAndRun(elements => {
|
||||
|
||||
if(elements.length > 1){
|
||||
el.style.justifyContent = "unset"
|
||||
}
|
||||
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.lastChild)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine(
|
||||
this._embeddedElements.data,
|
||||
).SetClass("block slick-carousel")
|
||||
.Render();
|
||||
}
|
||||
|
||||
Update() {
|
||||
super.Update();
|
||||
for (const uiElement of this._embeddedElements.data) {
|
||||
uiElement.Update();
|
||||
}
|
||||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
require("slick-carousel")
|
||||
if(this._embeddedElements.data.length == 0){
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
$('.slick-carousel').not('.slick-initialized').slick({
|
||||
autoplay: true,
|
||||
arrows: true,
|
||||
dots: true,
|
||||
lazyLoad: 'progressive',
|
||||
variableWidth: true,
|
||||
centerMode: true,
|
||||
centerPadding: "60px",
|
||||
adaptive: true
|
||||
elements = Utils.NoNull(elements).map(el => new Combine([el])
|
||||
.SetClass("block relative ml-1 bg-gray-200 m-1 rounded slideshow-item")
|
||||
.SetStyle("min-width: 150px; width: max-content; height: var(--image-carousel-height);max-height: var(--image-carousel-height);scroll-snap-align: start;")
|
||||
)
|
||||
|
||||
for (const element of elements ?? []) {
|
||||
el.appendChild(element.ConstructElement())
|
||||
}
|
||||
});
|
||||
|
||||
const wrapper = document.createElement("div")
|
||||
wrapper.style.maxWidth = "100%"
|
||||
wrapper.style.overflowX = "auto"
|
||||
wrapper.appendChild(el)
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {LicenseInfo, Wikimedia} from "../../Logic/Web/Wikimedia";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Svg from "../../Svg";
|
||||
import Link from "../Base/Link";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SimpleImageElement} from "./SimpleImageElement";
|
||||
import Attribution from "./Attribution";
|
||||
|
||||
|
||||
export class WikimediaImage extends UIElement {
|
||||
|
||||
|
||||
static allLicenseInfos: any = {};
|
||||
private readonly _imageMeta: UIEventSource<LicenseInfo>;
|
||||
private readonly _imageLocation: string;
|
||||
|
||||
constructor(source: string) {
|
||||
super(undefined)
|
||||
this._imageLocation = source;
|
||||
if (WikimediaImage.allLicenseInfos[source] !== undefined) {
|
||||
this._imageMeta = WikimediaImage.allLicenseInfos[source];
|
||||
} else {
|
||||
this._imageMeta = new UIEventSource<LicenseInfo>(new LicenseInfo());
|
||||
WikimediaImage.allLicenseInfos[source] = this._imageMeta;
|
||||
const self = this;
|
||||
Wikimedia.LicenseData(source, (info) => {
|
||||
self._imageMeta.setData(info);
|
||||
})
|
||||
}
|
||||
|
||||
this.ListenTo(this._imageMeta);
|
||||
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const url = Wikimedia.ImageNameToUrl(this._imageLocation, 500, 400)
|
||||
.replace(/'/g, '%27');
|
||||
const image = new SimpleImageElement(new UIEventSource<string>(url))
|
||||
const meta = this._imageMeta?.data;
|
||||
|
||||
if (!meta) {
|
||||
return image.Render();
|
||||
}
|
||||
new Link(Svg.wikimedia_commons_white_img,
|
||||
`https://commons.wikimedia.org/wiki/${this._imageLocation}`, true)
|
||||
.SetStyle("width:2em;height: 2em");
|
||||
|
||||
return new Combine([
|
||||
image,
|
||||
new Attribution(meta.artist, meta.license, Svg.wikimedia_commons_white_svg())
|
||||
]).SetClass("relative block").Render()
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import CheckBox from "./CheckBox";
|
||||
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
|
||||
import {MultiTagInput} from "./MultiTagInput";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
class AndOrConfig implements AndOrTagConfigJson {
|
||||
public and: (string | AndOrTagConfigJson)[] = undefined;
|
||||
public or: (string | AndOrTagConfigJson)[] = undefined;
|
||||
}
|
||||
|
||||
|
||||
export default class AndOrTagInput extends InputElement<AndOrTagConfigJson> {
|
||||
|
||||
private readonly _rawTags = new MultiTagInput();
|
||||
private readonly _subAndOrs: AndOrTagInput[] = [];
|
||||
private readonly _isAnd: UIEventSource<boolean> = new UIEventSource<boolean>(true);
|
||||
private readonly _isAndButton;
|
||||
private readonly _addBlock: UIElement;
|
||||
private readonly _value: UIEventSource<AndOrConfig> = new UIEventSource<AndOrConfig>(undefined);
|
||||
|
||||
public bottomLeftButton: UIElement;
|
||||
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const self = this;
|
||||
this._isAndButton = new CheckBox(
|
||||
new SubtleButton(Svg.ampersand_ui(), null).SetClass("small-button"),
|
||||
new SubtleButton(Svg.or_ui(), null).SetClass("small-button"),
|
||||
this._isAnd);
|
||||
|
||||
|
||||
this._addBlock =
|
||||
new SubtleButton(Svg.addSmall_ui(), "Add an and/or-expression")
|
||||
.SetClass("small-button")
|
||||
.onClick(() => {self.createNewBlock()});
|
||||
|
||||
|
||||
this._isAnd.addCallback(() => self.UpdateValue());
|
||||
this._rawTags.GetValue().addCallback(() => {
|
||||
self.UpdateValue()
|
||||
});
|
||||
|
||||
this.IsSelected = this._rawTags.IsSelected;
|
||||
|
||||
this._value.addCallback(tags => self.loadFromValue(tags));
|
||||
|
||||
}
|
||||
|
||||
private createNewBlock(){
|
||||
const inputEl = new AndOrTagInput();
|
||||
inputEl.GetValue().addCallback(() => this.UpdateValue());
|
||||
const deleteButton = this.createDeleteButton(inputEl.id);
|
||||
inputEl.bottomLeftButton = deleteButton;
|
||||
this._subAndOrs.push(inputEl);
|
||||
this.Update();
|
||||
}
|
||||
|
||||
private createDeleteButton(elementId: string): UIElement {
|
||||
const self = this;
|
||||
return new SubtleButton(Svg.delete_icon_ui(), null).SetClass("small-button")
|
||||
.onClick(() => {
|
||||
for (let i = 0; i < self._subAndOrs.length; i++) {
|
||||
if (self._subAndOrs[i].id === elementId) {
|
||||
self._subAndOrs.splice(i, 1);
|
||||
self.Update();
|
||||
self.UpdateValue();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private loadFromValue(value: AndOrTagConfigJson) {
|
||||
this._isAnd.setData(value.and !== undefined);
|
||||
const tags = value.and ?? value.or;
|
||||
const rawTags: string[] = [];
|
||||
const subTags: AndOrTagConfigJson[] = [];
|
||||
for (const tag of tags) {
|
||||
|
||||
if (typeof (tag) === "string") {
|
||||
rawTags.push(tag);
|
||||
} else {
|
||||
subTags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < rawTags.length; i++) {
|
||||
if (this._rawTags.GetValue().data[i] !== rawTags[i]) {
|
||||
// For some reason, 'setData' isn't stable as the comparison between the lists fails
|
||||
// Probably because we generate a new list object every timee
|
||||
// So we compare again here and update only if we find a difference
|
||||
this._rawTags.GetValue().setData(rawTags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while(this._subAndOrs.length < subTags.length){
|
||||
this.createNewBlock();
|
||||
}
|
||||
|
||||
for (let i = 0; i < subTags.length; i++){
|
||||
let subTag = subTags[i];
|
||||
this._subAndOrs[i].GetValue().setData(subTag);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private UpdateValue() {
|
||||
const tags: (string | AndOrTagConfigJson)[] = [];
|
||||
tags.push(...this._rawTags.GetValue().data);
|
||||
|
||||
for (const subAndOr of this._subAndOrs) {
|
||||
const subAndOrData = subAndOr._value.data;
|
||||
if (subAndOrData === undefined) {
|
||||
continue;
|
||||
}
|
||||
console.log(subAndOrData);
|
||||
tags.push(subAndOrData);
|
||||
}
|
||||
|
||||
const tagConfig = new AndOrConfig();
|
||||
|
||||
if (this._isAnd.data) {
|
||||
tagConfig.and = tags;
|
||||
} else {
|
||||
tagConfig.or = tags;
|
||||
}
|
||||
this._value.setData(tagConfig);
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<AndOrTagConfigJson> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const leftColumn = new Combine([
|
||||
this._isAndButton,
|
||||
"<br/>",
|
||||
this.bottomLeftButton ?? ""
|
||||
]);
|
||||
const tags = new Combine([
|
||||
this._rawTags,
|
||||
...this._subAndOrs,
|
||||
this._addBlock
|
||||
]).Render();
|
||||
return `<span class="bordered"><table><tr><td>${leftColumn.Render()}</td><td>${tags}</td></tr></table></span>`;
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: AndOrTagConfigJson): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export default class CheckBox extends UIElement{
|
||||
public readonly isEnabled: UIEventSource<boolean>;
|
||||
private readonly _showEnabled: UIElement;
|
||||
private readonly _showDisabled: UIElement;
|
||||
|
||||
constructor(showEnabled: string | UIElement, showDisabled: string | UIElement, data: UIEventSource<boolean> | boolean = false) {
|
||||
super(undefined);
|
||||
this.isEnabled =
|
||||
data instanceof UIEventSource ? data : new UIEventSource(data ?? false);
|
||||
this.ListenTo(this.isEnabled);
|
||||
this._showEnabled = Translations.W(showEnabled);
|
||||
this._showDisabled =Translations.W(showDisabled);
|
||||
const self = this;
|
||||
this.onClick(() => {
|
||||
self.isEnabled.setData(!self.isEnabled.data);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
if (this.isEnabled.data) {
|
||||
return Translations.W(this._showEnabled).Render();
|
||||
} else {
|
||||
return Translations.W(this._showDisabled).Render();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,82 +1,98 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import {UIElement} from "../UIElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
/**
|
||||
* Supports multi-input
|
||||
*/
|
||||
export default class CheckBoxes extends InputElement<number[]> {
|
||||
private static _nextId = 0;
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly value: UIEventSource<number[]>
|
||||
private readonly _elements: BaseUIElement[];
|
||||
|
||||
private readonly value: UIEventSource<number[]>;
|
||||
private readonly _elements: UIElement[]
|
||||
|
||||
|
||||
constructor(elements: UIElement[]) {
|
||||
super(undefined);
|
||||
constructor(elements: BaseUIElement[], value = new UIEventSource<number[]>([])) {
|
||||
super();
|
||||
this.value = value;
|
||||
this._elements = Utils.NoNull(elements);
|
||||
this.dumbMode = false;
|
||||
this.SetClass("flex flex-col")
|
||||
|
||||
this.value = new UIEventSource<number[]>([])
|
||||
this.ListenTo(this.value);
|
||||
}
|
||||
|
||||
|
||||
IsValid(ts: number[]): boolean {
|
||||
return ts !== undefined;
|
||||
|
||||
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<number[]> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("form")
|
||||
|
||||
private IdFor(i) {
|
||||
return 'checkmark-' + this.id + '-' + i;
|
||||
}
|
||||
const value = this.value;
|
||||
const elements = this._elements;
|
||||
|
||||
InnerRender(): string {
|
||||
let body = "";
|
||||
for (let i = 0; i < this._elements.length; i++) {
|
||||
let el = this._elements[i];
|
||||
const htmlElement =
|
||||
`<input type="checkbox" id="${this.IdFor(i)}"><label for="${this.IdFor(i)}">${el.Render()}</label><br/>`;
|
||||
body += htmlElement;
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
|
||||
}
|
||||
|
||||
return `<form id='${this.id}'>${body}</form>`;
|
||||
}
|
||||
let inputI = elements[i];
|
||||
const input = document.createElement("input")
|
||||
const id = CheckBoxes._nextId
|
||||
CheckBoxes._nextId++;
|
||||
input.id = "checkbox" + id
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self = this;
|
||||
input.type = "checkbox"
|
||||
input.classList.add("p-1","cursor-pointer","m-3","pl-3","mr-0")
|
||||
|
||||
for (let i = 0; i < this._elements.length; i++) {
|
||||
const el = document.getElementById(this.IdFor(i));
|
||||
const label = document.createElement("label")
|
||||
label.htmlFor = input.id
|
||||
label.appendChild(inputI.ConstructElement())
|
||||
label.classList.add("block","w-full","p-2","cursor-pointer","bg-red")
|
||||
|
||||
const wrapper = document.createElement("span")
|
||||
wrapper.classList.add("flex","w-full","border", "border-gray-400","m-1")
|
||||
wrapper.appendChild(input)
|
||||
wrapper.appendChild(label)
|
||||
el.appendChild(wrapper)
|
||||
|
||||
if(this.value.data.indexOf(i) >= 0){
|
||||
// @ts-ignore
|
||||
el.checked = true;
|
||||
}
|
||||
value.addCallbackAndRun(selectedValues => {
|
||||
if (selectedValues === undefined) {
|
||||
return;
|
||||
}
|
||||
if (selectedValues.indexOf(i) >= 0) {
|
||||
input.checked = true;
|
||||
}
|
||||
|
||||
el.onchange = () => {
|
||||
const index = self.value.data.indexOf(i);
|
||||
// @ts-ignore
|
||||
if(el.checked && index < 0){
|
||||
self.value.data.push(i);
|
||||
self.value.ping();
|
||||
}else if(index >= 0){
|
||||
self.value.data.splice(index,1);
|
||||
self.value.ping();
|
||||
|
||||
if(input.checked){
|
||||
wrapper.classList.remove("border-gray-400")
|
||||
wrapper.classList.add("border-black")
|
||||
}else{
|
||||
wrapper.classList.add("border-gray-400")
|
||||
wrapper.classList.remove("border-black")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
input.onchange = () => {
|
||||
// Index = index in the list of already checked items
|
||||
const index = value.data.indexOf(i);
|
||||
if (input.checked && index < 0) {
|
||||
value.data.push(i);
|
||||
value.ping();
|
||||
} else if (index >= 0) {
|
||||
value.data.splice(index, 1);
|
||||
value.ping();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,50 +1,36 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class ColorPicker extends InputElement<string> {
|
||||
|
||||
private readonly value: UIEventSource<string>
|
||||
|
||||
private readonly _element : HTMLElement
|
||||
constructor(
|
||||
value?: UIEventSource<string>
|
||||
value: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
) {
|
||||
super();
|
||||
this.value = value ?? new UIEventSource<string>(undefined);
|
||||
const self = this;
|
||||
this.value = value ;
|
||||
|
||||
const el = document.createElement("input")
|
||||
this._element = el;
|
||||
|
||||
el.type = "color"
|
||||
|
||||
this.value.addCallbackAndRun(v => {
|
||||
if(v === undefined){
|
||||
return;
|
||||
}
|
||||
self.SetValue(v);
|
||||
el.value =v
|
||||
});
|
||||
|
||||
el.oninput = () => {
|
||||
const hex = el.value;
|
||||
value.setData(hex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return `<span id="${this.id}"><input type='color' id='color-${this.id}'></span>`;
|
||||
}
|
||||
|
||||
private SetValue(color: string){
|
||||
const field = document.getElementById("color-" + this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
field.value = color;
|
||||
}
|
||||
|
||||
protected InnerUpdate() {
|
||||
const field = document.getElementById("color-" + this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
field.oninput = () => {
|
||||
const hex = field["value"];
|
||||
self.value.setData(hex);
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import {UIElement} from "../UIElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class CombinedInputElement<T> extends InputElement<T> {
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._combined.ConstructElement();
|
||||
}
|
||||
private readonly _a: InputElement<T>;
|
||||
private readonly _b: UIElement;
|
||||
private readonly _combined: UIElement;
|
||||
private readonly _b: BaseUIElement;
|
||||
private readonly _combined: BaseUIElement;
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
|
||||
constructor(a: InputElement<T>, b: InputElement<T>) {
|
||||
super();
|
||||
this._a = a;
|
||||
|
@ -23,11 +25,6 @@ export default class CombinedInputElement<T> extends InputElement<T> {
|
|||
return this._a.GetValue();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._combined.Render();
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this._a.IsValid(t);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import {InputElement} from "./InputElement";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import Svg from "../../Svg";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -9,34 +10,29 @@ import Svg from "../../Svg";
|
|||
*/
|
||||
export default class DirectionInput extends InputElement<string> {
|
||||
|
||||
private readonly value: UIEventSource<string>;
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly value: UIEventSource<string>;
|
||||
|
||||
constructor(value?: UIEventSource<string>) {
|
||||
super();
|
||||
this.dumbMode = false;
|
||||
this.value = value ?? new UIEventSource<string>(undefined);
|
||||
|
||||
this.value.addCallbackAndRun(rotation => {
|
||||
const selfElement = document.getElementById(this.id);
|
||||
if (selfElement === null) {
|
||||
return;
|
||||
}
|
||||
const cone = selfElement.getElementsByClassName("direction-svg")[0] as HTMLElement
|
||||
cone.style.transform = `rotate(${rotation}deg)`;
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([
|
||||
`<div id="direction-leaflet-div-${this.id}" style="width:100%;height: 100%;position: absolute;top:0;left:0;border-radius:100%;"></div>`,
|
||||
IsValid(str: string): boolean {
|
||||
const t = Number(str);
|
||||
return !isNaN(t) && t >= 0 && t <= 360;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
|
||||
|
||||
const element = new Combine([
|
||||
new FixedUiElement("").SetClass("w-full h-full absolute top-0 left-O rounded-full"),
|
||||
Svg.direction_svg().SetStyle(
|
||||
`position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`)
|
||||
.SetClass("direction-svg"),
|
||||
|
@ -44,11 +40,21 @@ export default class DirectionInput extends InputElement<string> {
|
|||
"position: absolute;top: 0;left: 0;width: 100%;height: 100%;")
|
||||
])
|
||||
.SetStyle("position:relative;display:block;width: min(100%, 25em); padding-top: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em")
|
||||
.Render();
|
||||
.ConstructElement()
|
||||
|
||||
|
||||
this.value.addCallbackAndRun(rotation => {
|
||||
const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement
|
||||
cone.style.transform = `rotate(${rotation}deg)`;
|
||||
|
||||
})
|
||||
|
||||
this.RegisterTriggers(element)
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
private RegisterTriggers(htmlElement: HTMLElement) {
|
||||
const self = this;
|
||||
|
||||
function onPosChange(x: number, y: number) {
|
||||
|
@ -79,19 +85,16 @@ export default class DirectionInput extends InputElement<string> {
|
|||
}
|
||||
|
||||
htmlElement.onmouseup = (ev) => {
|
||||
isDown = false; ev.preventDefault();
|
||||
isDown = false;
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
htmlElement.onmousemove = (ev: MouseEvent) => {
|
||||
if (isDown) {
|
||||
onPosChange(ev.clientX, ev.clientY);
|
||||
} ev.preventDefault();
|
||||
}
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
IsValid(str: string): boolean {
|
||||
const t = Number(str);
|
||||
return !isNaN(t) && t >= 0 && t <= 360;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,50 +1,92 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class DropDown<T> extends InputElement<T> {
|
||||
|
||||
private readonly _label: UIElement;
|
||||
private readonly _values: { value: T; shown: UIElement }[];
|
||||
private static _nextDropdownId = 0;
|
||||
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
private readonly _element: HTMLElement;
|
||||
|
||||
private readonly _value: UIEventSource<T>;
|
||||
private readonly _values: { value: T; shown: string | BaseUIElement }[];
|
||||
|
||||
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _label_class: string;
|
||||
private readonly _select_class: string;
|
||||
private _form_style: string;
|
||||
|
||||
constructor(label: string | UIElement,
|
||||
values: { value: T, shown: string | UIElement }[],
|
||||
constructor(label: string | BaseUIElement,
|
||||
values: { value: T, shown: string | BaseUIElement }[],
|
||||
value: UIEventSource<T> = undefined,
|
||||
label_class: string = "",
|
||||
select_class: string = "",
|
||||
form_style: string = "flex") {
|
||||
super(undefined);
|
||||
this._form_style = form_style;
|
||||
this._value = value ?? new UIEventSource<T>(undefined);
|
||||
this._label = Translations.W(label);
|
||||
this._label_class = label_class || '';
|
||||
this._select_class = select_class || '';
|
||||
this._values = values.map(v => {
|
||||
return {
|
||||
value: v.value,
|
||||
shown: Translations.W(v.shown)
|
||||
options?: {
|
||||
select_class?: string
|
||||
}
|
||||
}
|
||||
);
|
||||
for (const v of this._values) {
|
||||
this.ListenTo(v.shown._source);
|
||||
) {
|
||||
super();
|
||||
value = value ?? new UIEventSource<T>(undefined)
|
||||
this._value = value
|
||||
this._values = values;
|
||||
if (values.length <= 1) {
|
||||
return;
|
||||
}
|
||||
this.ListenTo(this._value);
|
||||
|
||||
this.onClick(() => {}) // by registering a click, the click event is consumed and doesn't bubble furter to other elements, e.g. checkboxes
|
||||
const id = DropDown._nextDropdownId;
|
||||
DropDown._nextDropdownId++;
|
||||
|
||||
|
||||
const el = document.createElement("form")
|
||||
this._element = el;
|
||||
el.id = "dropdown" + id;
|
||||
|
||||
{
|
||||
const labelEl = Translations.W(label).ConstructElement()
|
||||
if (labelEl !== undefined) {
|
||||
const labelHtml = document.createElement("label")
|
||||
labelHtml.appendChild(labelEl)
|
||||
labelHtml.htmlFor = el.id;
|
||||
el.appendChild(labelHtml)
|
||||
}
|
||||
}
|
||||
|
||||
options = options ?? {}
|
||||
options.select_class = options.select_class ?? 'bg-indigo-100 p-1 rounded hover:bg-indigo-200'
|
||||
|
||||
|
||||
{
|
||||
const select = document.createElement("select")
|
||||
select.classList.add(...(options.select_class.split(" ") ?? []))
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
|
||||
const option = document.createElement("option")
|
||||
option.value = "" + i
|
||||
option.appendChild(Translations.W(values[i].shown).ConstructElement())
|
||||
select.appendChild(option)
|
||||
}
|
||||
el.appendChild(select)
|
||||
|
||||
|
||||
select.onchange = (() => {
|
||||
var index = select.selectedIndex;
|
||||
value.setData(values[index].value);
|
||||
});
|
||||
|
||||
value.addCallbackAndRun(selected => {
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i].value;
|
||||
if (value === selected) {
|
||||
select.selectedIndex = i;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
this.onClick(() => {
|
||||
}) // by registering a click, the click event is consumed and doesn't bubble further to other elements, e.g. checkboxes
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
for (const value of this._values) {
|
||||
if (value.value === t) {
|
||||
|
@ -54,44 +96,8 @@ export class DropDown<T> extends InputElement<T> {
|
|||
return false
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
if(this._values.length <=1){
|
||||
return "";
|
||||
}
|
||||
|
||||
let options = "";
|
||||
for (let i = 0; i < this._values.length; i++) {
|
||||
options += "<option value='" + i + "'>" + this._values[i].shown.InnerRender() + "</option>"
|
||||
}
|
||||
|
||||
return `<form class="${this._form_style}">` +
|
||||
`<label class='${this._label_class}' for='dropdown-${this.id}'>${this._label.Render()}</label>` +
|
||||
`<select class='${this._select_class}' name='dropdown-${this.id}' id='dropdown-${this.id}'>` +
|
||||
options +
|
||||
`</select>` +
|
||||
`</form>`;
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
protected InnerUpdate(element) {
|
||||
var e = document.getElementById("dropdown-" + this.id);
|
||||
if(e === null){
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
e.onchange = (() => {
|
||||
// @ts-ignore
|
||||
var index = parseInt(e.selectedIndex);
|
||||
self._value.setData(self._values[index].value);
|
||||
});
|
||||
|
||||
var t = this._value.data;
|
||||
for (let i = 0; i < this._values.length ; i++) {
|
||||
const value = this._values[i].value;
|
||||
if (value === t) {
|
||||
// @ts-ignore
|
||||
e.selectedIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
UI/Input/FileSelectorButton.ts
Normal file
66
UI/Input/FileSelectorButton.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export default class FileSelectorButton extends InputElement<FileList> {
|
||||
|
||||
private static _nextid;
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
private readonly _value = new UIEventSource(undefined);
|
||||
private readonly _label: BaseUIElement;
|
||||
private readonly _acceptType: string;
|
||||
|
||||
constructor(label: BaseUIElement, acceptType: string = "image/*") {
|
||||
super();
|
||||
this._label = label;
|
||||
this._acceptType = acceptType;
|
||||
this.SetClass("block cursor-pointer")
|
||||
label.SetClass("cursor-pointer")
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<FileList> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsValid(t: FileList): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const self = this;
|
||||
const el = document.createElement("form")
|
||||
const label = document.createElement("label")
|
||||
label.appendChild(this._label.ConstructElement())
|
||||
el.appendChild(label)
|
||||
|
||||
const actualInputElement = document.createElement("input");
|
||||
actualInputElement.style.cssText = "display:none";
|
||||
actualInputElement.type = "file";
|
||||
actualInputElement.accept = this._acceptType;
|
||||
actualInputElement.name = "picField";
|
||||
actualInputElement.multiple = true;
|
||||
actualInputElement.id = "fileselector" + FileSelectorButton._nextid;
|
||||
FileSelectorButton._nextid++;
|
||||
|
||||
label.htmlFor = actualInputElement.id;
|
||||
|
||||
actualInputElement.onchange = () => {
|
||||
if (actualInputElement.files !== null) {
|
||||
self._value.setData(actualInputElement.files)
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener('submit', e => {
|
||||
if (actualInputElement.files !== null) {
|
||||
self._value.setData(actualInputElement.files)
|
||||
}
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
el.appendChild(actualInputElement)
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,44 +1,46 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Translations from "../i18n/Translations";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class FixedInputElement<T> extends InputElement<T> {
|
||||
private readonly rendering: UIElement;
|
||||
private readonly value: UIEventSource<T>;
|
||||
public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _comparator: (t0: T, t1: T) => boolean;
|
||||
|
||||
constructor(rendering: UIElement | string,
|
||||
private readonly _el : HTMLElement;
|
||||
|
||||
constructor(rendering: BaseUIElement | string,
|
||||
value: T,
|
||||
comparator: ((t0: T, t1: T) => boolean ) = undefined) {
|
||||
super(undefined);
|
||||
super();
|
||||
this._comparator = comparator ?? ((t0, t1) => t0 == t1);
|
||||
this.value = new UIEventSource<T>(value);
|
||||
this.rendering = typeof (rendering) === 'string' ? new FixedUiElement(rendering) : rendering;
|
||||
const self = this;
|
||||
|
||||
const selected = this.IsSelected;
|
||||
this._el = document.createElement("span")
|
||||
this._el.addEventListener("mouseout", () => selected.setData(false))
|
||||
const e = Translations.W(rendering)?.ConstructElement()
|
||||
if(e){
|
||||
this._el.appendChild( e)
|
||||
}
|
||||
|
||||
this.onClick(() => {
|
||||
self.IsSelected.setData(true)
|
||||
selected.setData(true)
|
||||
})
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._el;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this.value;
|
||||
}
|
||||
InnerRender(): string {
|
||||
return this.rendering.Render();
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this._comparator(t, this.value.data);
|
||||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self = this;
|
||||
htmlElement.addEventListener("mouseout", () => self.IsSelected.setData(false))
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export abstract class InputElement<T> extends UIElement{
|
||||
export abstract class InputElement<T> extends BaseUIElement{
|
||||
|
||||
abstract GetValue() : UIEventSource<T>;
|
||||
abstract IsSelected: UIEventSource<boolean>;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue