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 {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||||
import {UIElement} from "../../UI/UIElement";
|
|
||||||
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
|
|
||||||
import SourceConfig from "./SourceConfig";
|
import SourceConfig from "./SourceConfig";
|
||||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
import {Tag} from "../../Logic/Tags/Tag";
|
||||||
import SubstitutingTag from "../../Logic/Tags/SubstitutingTag";
|
import SubstitutingTag from "../../Logic/Tags/SubstitutingTag";
|
||||||
|
import BaseUIElement from "../../UI/BaseUIElement";
|
||||||
|
|
||||||
export default class LayerConfig {
|
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:
|
icon:
|
||||||
{
|
{
|
||||||
html: UIElement,
|
html: BaseUIElement,
|
||||||
iconSize: [number, number],
|
iconSize: [number, number],
|
||||||
iconAnchor: [number, number],
|
iconAnchor: [number, number],
|
||||||
popupAnchor: [number, number],
|
popupAnchor: [number, number],
|
||||||
|
@ -325,7 +324,7 @@ export default class LayerConfig {
|
||||||
|
|
||||||
function render(tr: TagRenderingConfig, deflt?: string) {
|
function render(tr: TagRenderingConfig, deflt?: string) {
|
||||||
const str = (tr?.GetRenderValue(tags.data)?.txt ?? deflt);
|
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(",");
|
const iconSize = render(this.iconSize, "40,40,center").split(",");
|
||||||
|
@ -361,7 +360,7 @@ export default class LayerConfig {
|
||||||
const iconUrlStatic = render(this.icon);
|
const iconUrlStatic = render(this.icon);
|
||||||
const self = this;
|
const self = this;
|
||||||
const mappedHtml = tags.map(tgs => {
|
const mappedHtml = tags.map(tgs => {
|
||||||
function genHtmlFromString(sourcePart: string): UIElement {
|
function genHtmlFromString(sourcePart: string): BaseUIElement {
|
||||||
if (sourcePart.indexOf("html:") == 0) {
|
if (sourcePart.indexOf("html:") == 0) {
|
||||||
// We use § as a replacement for ;
|
// We use § as a replacement for ;
|
||||||
const html = sourcePart.substring("html:".length)
|
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`;
|
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_]*):([^;]*)/)
|
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/)
|
||||||
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
||||||
html = new Combine([
|
html = new Combine([
|
||||||
|
@ -387,7 +386,7 @@ export default class LayerConfig {
|
||||||
const iconUrl = render(self.icon);
|
const iconUrl = render(self.icon);
|
||||||
const rotation = render(self.rotation, "0deg");
|
const rotation = render(self.rotation, "0deg");
|
||||||
|
|
||||||
let htmlParts: UIElement[] = [];
|
let htmlParts: BaseUIElement[] = [];
|
||||||
let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != ""));
|
let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != ""));
|
||||||
for (const sourcePart of sourceParts) {
|
for (const sourcePart of sourceParts) {
|
||||||
htmlParts.push(genHtmlFromString(sourcePart))
|
htmlParts.push(genHtmlFromString(sourcePart))
|
||||||
|
@ -399,7 +398,7 @@ export default class LayerConfig {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (iconOverlay.badge) {
|
if (iconOverlay.badge) {
|
||||||
const badgeParts: UIElement[] = [];
|
const badgeParts: BaseUIElement[] = [];
|
||||||
const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != "");
|
const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != "");
|
||||||
|
|
||||||
for (const badgePartStr of partDefs) {
|
for (const badgePartStr of partDefs) {
|
||||||
|
@ -437,7 +436,7 @@ export default class LayerConfig {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e, tgs)
|
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,
|
maxZoom: 16,
|
||||||
minNeededElements: 500
|
minNeededElements: 500
|
||||||
};
|
};
|
||||||
this.clustering = defaultClustering;
|
|
||||||
if (json.clustering) {
|
if (json.clustering) {
|
||||||
this.clustering = {
|
this.clustering = {
|
||||||
maxZoom: json.clustering.maxZoom ?? 18,
|
maxZoom: json.clustering.maxZoom ?? 18,
|
||||||
|
@ -164,7 +163,7 @@ export default class LayoutConfig {
|
||||||
}
|
}
|
||||||
for (const layer of this.layers) {
|
for (const layer of this.layers) {
|
||||||
if (layer.wayHandling !== LayerConfig.WAYHANDLING_CENTER_ONLY) {
|
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;
|
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)
|
* Gets the correct rendering value (or undefined if not known)
|
||||||
* @constructor
|
* @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
|
An object which receives an UIEventSource is responsible of responding onto changes of this object. This is especially true for UI-components
|
||||||
|
|
||||||
UI
|
UI
|
||||||
--
|
--```
|
||||||
|
|
||||||
|
export default class MyComponent {
|
||||||
|
|
||||||
|
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.
|
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
|
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.
|
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
|
**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)
|
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
|
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)
|
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
|
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
|
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.
|
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.
|
It is also possible to calculate your own tags - but this requires some javascript knowledge.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Before proceeding, some warnings:
|
Before proceeding, some warnings:
|
||||||
|
|
||||||
* DO NOT DO THIS AS BEGINNER
|
|
||||||
* **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
|
To enable this feature, add a field `calculatedTags` in the layer object, e.g.:
|
||||||
* overlapWith
|
|
||||||
* closest
|
````
|
||||||
* memberships
|
|
||||||
|
"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:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- `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
|
### distanceTo
|
||||||
|
|
||||||
Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object
|
Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object
|
||||||
|
|
||||||
* longitude
|
0. longitude
|
||||||
* latitude
|
1. latitude
|
||||||
|
|
||||||
### overlapWith
|
### overlapWith
|
||||||
|
|
||||||
Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point are given. The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point
|
Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point are given. The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point
|
||||||
|
|
||||||
* ...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)
|
0. ...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)
|
||||||
|
|
||||||
### closest
|
### closest
|
||||||
|
|
||||||
Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered.
|
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
|
0. list of features
|
||||||
|
|
||||||
### memberships
|
### 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(';')`
|
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
|
## color
|
||||||
|
|
||||||
Shows a color picker
|
Shows a color picker Generated from ValidatedTextField.ts
|
|
@ -1,61 +1 @@
|
||||||
### Special tag renderings
|
<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
|
||||||
|
|
||||||
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****
|
|
|
@ -33,22 +33,22 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "cyclestreet",
|
"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"
|
"value": "yes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "maxspeed",
|
"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"
|
"value": "30"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "overtaking:motor_vehicle",
|
"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"
|
"value": "no"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "proposed:cyclestreet",
|
"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": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -113,22 +113,22 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "cyclestreet",
|
"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"
|
"value": "yes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "maxspeed",
|
"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"
|
"value": "30"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "overtaking:motor_vehicle",
|
"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"
|
"value": "no"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "proposed:cyclestreet",
|
"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": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -203,22 +203,22 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "cyclestreet",
|
"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"
|
"value": "yes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "maxspeed",
|
"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"
|
"value": "30"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "overtaking:motor_vehicle",
|
"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"
|
"value": "no"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "proposed:cyclestreet",
|
"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": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
URL-parameters and URL-hash
|
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.
|
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
|
|
||||||
|
|
||||||
|
layer-control-toggle
|
||||||
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
|
|
||||||
----------------------
|
----------------------
|
||||||
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
|
The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_
|
||||||
-------------
|
|
||||||
Used to complete the login
|
|
||||||
No default value set
|
|
||||||
|
|
||||||
background
|
|
||||||
|
custom-css
|
||||||
------------
|
------------
|
||||||
The id of the background layer to start with
|
|
||||||
The default value is _OSM_ (overridden by the theme)
|
|
||||||
|
|
||||||
layer-<layerid>
|
If specified, the custom css from the given link will be loaded additionaly The default value is __
|
||||||
--------------
|
|
||||||
Wether or not layer with layer-id is shown
|
|
||||||
The default value is _true_
|
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 {FixedUiElement} from "./UI/Base/FixedUiElement";
|
||||||
import CheckBox from "./UI/Input/CheckBox";
|
import Toggle from "./UI/Input/Toggle";
|
||||||
import {Basemap} from "./UI/BigComponents/Basemap";
|
import {Basemap} from "./UI/BigComponents/Basemap";
|
||||||
import State from "./State";
|
import State from "./State";
|
||||||
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
||||||
|
@ -21,11 +21,9 @@ import * as L from "leaflet";
|
||||||
import Img from "./UI/Base/Img";
|
import Img from "./UI/Base/Img";
|
||||||
import UserDetails from "./Logic/Osm/OsmConnection";
|
import UserDetails from "./Logic/Osm/OsmConnection";
|
||||||
import Attribution from "./UI/BigComponents/Attribution";
|
import Attribution from "./UI/BigComponents/Attribution";
|
||||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
|
||||||
import LayerResetter from "./Logic/Actors/LayerResetter";
|
import LayerResetter from "./Logic/Actors/LayerResetter";
|
||||||
import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
|
import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
|
||||||
import LayerControlPanel from "./UI/BigComponents/LayerControlPanel";
|
import LayerControlPanel from "./UI/BigComponents/LayerControlPanel";
|
||||||
import FeatureSwitched from "./UI/Base/FeatureSwitched";
|
|
||||||
import ShowDataLayer from "./UI/ShowDataLayer";
|
import ShowDataLayer from "./UI/ShowDataLayer";
|
||||||
import Hash from "./Logic/Web/Hash";
|
import Hash from "./Logic/Web/Hash";
|
||||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||||
|
@ -39,9 +37,9 @@ import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
|
||||||
import AttributionPanel from "./UI/BigComponents/AttributionPanel";
|
import AttributionPanel from "./UI/BigComponents/AttributionPanel";
|
||||||
import ContributorCount from "./Logic/ContributorCount";
|
import ContributorCount from "./Logic/ContributorCount";
|
||||||
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
|
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
|
||||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
|
||||||
import AllKnownLayers from "./Customizations/AllKnownLayers";
|
import AllKnownLayers from "./Customizations/AllKnownLayers";
|
||||||
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
||||||
|
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||||
|
|
||||||
export class InitUiElements {
|
export class InitUiElements {
|
||||||
|
|
||||||
|
@ -170,13 +168,14 @@ export class InitUiElements {
|
||||||
marker.addTo(State.state.leafletMap.data)
|
marker.addTo(State.state.leafletMap.data)
|
||||||
});
|
});
|
||||||
|
|
||||||
const geolocationButton = new FeatureSwitched(
|
const geolocationButton = new Toggle(
|
||||||
new MapControlButton(
|
new MapControlButton(
|
||||||
new GeoLocationHandler(
|
new GeoLocationHandler(
|
||||||
State.state.currentGPSLocation,
|
State.state.currentGPSLocation,
|
||||||
State.state.leafletMap,
|
State.state.leafletMap,
|
||||||
State.state.layoutToUse
|
State.state.layoutToUse
|
||||||
)),
|
)),
|
||||||
|
undefined,
|
||||||
State.state.featureSwitchGeolocation);
|
State.state.featureSwitchGeolocation);
|
||||||
|
|
||||||
const plus = new MapControlButton(
|
const plus = new MapControlButton(
|
||||||
|
@ -193,7 +192,7 @@ export class InitUiElements {
|
||||||
State.state.locationControl.ping();
|
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")
|
.SetClass("flex flex-col")
|
||||||
.AttachTo("bottom-right");
|
.AttachTo("bottom-right");
|
||||||
|
|
||||||
|
@ -211,13 +210,12 @@ export class InitUiElements {
|
||||||
|
|
||||||
// Reset the loading message once things are loaded
|
// Reset the loading message once things are loaded
|
||||||
new CenterMessageBox().AttachTo("centermessage");
|
new CenterMessageBox().AttachTo("centermessage");
|
||||||
|
document.getElementById("centermessage").classList.add("pointer-events-none")
|
||||||
// At last, zoom to the needed location if the focus is on an element
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>) {
|
static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>): [LayoutConfig, string]{
|
||||||
try {
|
try {
|
||||||
let hash = location.hash.substr(1);
|
let hash = location.hash.substr(1);
|
||||||
const layoutFromBase64 = userLayoutParam.data;
|
const layoutFromBase64 = userLayoutParam.data;
|
||||||
|
@ -249,7 +247,7 @@ export class InitUiElements {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const layoutToUse = new LayoutConfig(json, false);
|
const layoutToUse = new LayoutConfig(json, false);
|
||||||
userLayoutParam.setData(layoutToUse.id);
|
userLayoutParam.setData(layoutToUse.id);
|
||||||
return layoutToUse;
|
return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
||||||
new FixedUiElement("Error: could not parse the custom layout:<br/> " + e).AttachTo("centermessage");
|
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.
|
// ?-Button on Desktop, opens panel with close-X.
|
||||||
const help = new MapControlButton(Svg.help_svg());
|
const help = new MapControlButton(Svg.help_svg());
|
||||||
new CheckBox(
|
help.onClick(() => isOpened.setData(true))
|
||||||
|
new Toggle(
|
||||||
fullOptions
|
fullOptions
|
||||||
.SetClass("welcomeMessage")
|
.SetClass("welcomeMessage"),
|
||||||
.onClick(() => {/*Catch the click*/
|
|
||||||
}),
|
|
||||||
help
|
help
|
||||||
, isOpened
|
, isOpened
|
||||||
).AttachTo("messagesbox");
|
).AttachTo("messagesbox");
|
||||||
|
@ -307,22 +304,23 @@ export class InitUiElements {
|
||||||
)
|
)
|
||||||
|
|
||||||
;
|
;
|
||||||
const copyrightButton = new CheckBox(
|
const copyrightButton = new Toggle(
|
||||||
copyrightNotice,
|
copyrightNotice,
|
||||||
new MapControlButton(Svg.osm_copyright_svg()),
|
new MapControlButton(Svg.osm_copyright_svg()),
|
||||||
copyrightNotice.isShown
|
copyrightNotice.isShown
|
||||||
).SetClass("p-0.5")
|
).ToggleOnClick()
|
||||||
|
.SetClass("p-0.5")
|
||||||
|
|
||||||
const layerControlPanel = new LayerControlPanel(
|
const layerControlPanel = new LayerControlPanel(
|
||||||
State.state.layerControlIsOpened)
|
State.state.layerControlIsOpened)
|
||||||
.SetClass("block p-1 rounded-full");
|
.SetClass("block p-1 rounded-full");
|
||||||
const layerControlButton = new CheckBox(
|
const layerControlButton = new Toggle(
|
||||||
layerControlPanel,
|
layerControlPanel,
|
||||||
new MapControlButton(Svg.layers_svg()),
|
new MapControlButton(Svg.layers_svg()),
|
||||||
State.state.layerControlIsOpened
|
State.state.layerControlIsOpened
|
||||||
)
|
).ToggleOnClick()
|
||||||
|
|
||||||
const layerControl = new CheckBox(
|
const layerControl = new Toggle(
|
||||||
layerControlButton,
|
layerControlButton,
|
||||||
"",
|
"",
|
||||||
State.state.featureSwitchLayers
|
State.state.featureSwitchLayers
|
||||||
|
@ -351,9 +349,8 @@ export class InitUiElements {
|
||||||
private static InitBaseMap() {
|
private static InitBaseMap() {
|
||||||
|
|
||||||
State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers;
|
State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers;
|
||||||
State.state.backgroundLayer = QueryParameters.GetQueryParameter("background",
|
|
||||||
State.state.layoutToUse.data.defaultBackgroundId ?? AvailableBaseLayers.osmCarto.id,
|
State.state.backgroundLayer = State.state.backgroundLayerId
|
||||||
"The id of the background layer to start with")
|
|
||||||
.map((selectedId: string) => {
|
.map((selectedId: string) => {
|
||||||
const available = State.state.availableBackgroundLayers.data;
|
const available = State.state.availableBackgroundLayers.data;
|
||||||
for (const layer of available) {
|
for (const layer of available) {
|
||||||
|
@ -362,8 +359,7 @@ export class InitUiElements {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return AvailableBaseLayers.osmCarto;
|
return AvailableBaseLayers.osmCarto;
|
||||||
}, [], layer => layer.id);
|
}, [State.state.availableBackgroundLayers], layer => layer.id);
|
||||||
|
|
||||||
|
|
||||||
new LayerResetter(
|
new LayerResetter(
|
||||||
State.state.backgroundLayer, State.state.locationControl,
|
State.state.backgroundLayer, State.state.locationControl,
|
||||||
|
|
|
@ -6,6 +6,8 @@ import Svg from "../../Svg";
|
||||||
import Img from "../../UI/Base/Img";
|
import Img from "../../UI/Base/Img";
|
||||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
|
import BaseUIElement from "../../UI/BaseUIElement";
|
||||||
|
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||||
|
|
||||||
export default class GeoLocationHandler extends UIElement {
|
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 _previousLocationGrant: UIEventSource<string> = LocalStorageSource.Get("geolocation-permissions");
|
||||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly _element: BaseUIElement;
|
||||||
|
|
||||||
constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
|
constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
|
||||||
leafletMap: UIEventSource<L.Map>,
|
leafletMap: UIEventSource<L.Map>,
|
||||||
layoutToUse: UIEventSource<LayoutConfig>) {
|
layoutToUse: UIEventSource<LayoutConfig>) {
|
||||||
super(undefined);
|
super();
|
||||||
this._currentGPSLocation = currentGPSLocation;
|
this._currentGPSLocation = currentGPSLocation;
|
||||||
this._leafletMap = leafletMap;
|
this._leafletMap = leafletMap;
|
||||||
this._layoutToUse = layoutToUse;
|
this._layoutToUse = layoutToUse;
|
||||||
this._hasLocation = currentGPSLocation.map((location) => location !== undefined);
|
this._hasLocation = currentGPSLocation.map((location) => location !== undefined);
|
||||||
this.dumbMode = false;
|
|
||||||
const self = this;
|
const self = this;
|
||||||
import("../../vendor/Leaflet.AccuratePosition.js").then(() => {
|
|
||||||
self.init();
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentPointer = this._isActive.map(isActive => {
|
const currentPointer = this._isActive.map(isActive => {
|
||||||
if (isActive && !self._hasLocation.data) {
|
if (isActive && !self._hasLocation.data) {
|
||||||
|
@ -74,62 +76,35 @@ export default class GeoLocationHandler extends UIElement {
|
||||||
}, [this._hasLocation])
|
}, [this._hasLocation])
|
||||||
currentPointer.addCallbackAndRun(pointerClass => {
|
currentPointer.addCallbackAndRun(pointerClass => {
|
||||||
self.SetClass(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) {
|
private init(askPermission: boolean) {
|
||||||
super.InnerUpdate(htmlElement);
|
|
||||||
|
|
||||||
const self = this;
|
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;
|
const map = this._leafletMap.data;
|
||||||
map.on('accuratepositionprogress', onAccuratePositionProgress);
|
|
||||||
map.on('accuratepositionfound', onAccuratePositionFound);
|
|
||||||
map.on('accuratepositionerror', onAccuratePositionError);
|
|
||||||
|
|
||||||
|
|
||||||
this._currentGPSLocation.addCallback((location) => {
|
this._currentGPSLocation.addCallback((location) => {
|
||||||
self._previousLocationGrant.setData("granted");
|
self._previousLocationGrant.setData("granted");
|
||||||
|
@ -178,12 +153,13 @@ export default class GeoLocationHandler extends UIElement {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
if (this._previousLocationGrant.data === "granted") {
|
if (askPermission) {
|
||||||
|
self.StartGeolocating(true);
|
||||||
|
} else if (this._previousLocationGrant.data === "granted") {
|
||||||
this._previousLocationGrant.setData("");
|
this._previousLocationGrant.setData("");
|
||||||
self.StartGeolocating(false);
|
self.StartGeolocating(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.HideOnEmpty(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private locate() {
|
private locate() {
|
||||||
|
|
|
@ -53,7 +53,6 @@ export default class OverpassFeatureSource implements FeatureSource {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
|
let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
|
||||||
console.debug("overpass source: minzoom is ", minzoom)
|
|
||||||
return location.zoom >= minzoom;
|
return location.zoom >= minzoom;
|
||||||
}, [layoutToUse]
|
}, [layoutToUse]
|
||||||
);
|
);
|
||||||
|
|
|
@ -47,13 +47,13 @@ export default class StrayClickHandler {
|
||||||
popupAnchor: [0, -45]
|
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.addTo(leafletMap.data);
|
||||||
self._lastMarker.bindPopup(popup);
|
self._lastMarker.bindPopup(popup);
|
||||||
|
|
||||||
self._lastMarker.on("click", () => {
|
self._lastMarker.on("click", () => {
|
||||||
|
uiToShow.AttachTo("strayclick")
|
||||||
uiToShow.Activate();
|
uiToShow.Activate();
|
||||||
uiToShow.Update();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,12 @@ import {UIEventSource} from "../UIEventSource";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import Translations from "../../UI/i18n/Translations";
|
import Translations from "../../UI/i18n/Translations";
|
||||||
import Locale from "../../UI/i18n/Locale";
|
import Locale from "../../UI/i18n/Locale";
|
||||||
import {UIElement} from "../../UI/UIElement";
|
|
||||||
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
|
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
|
||||||
import {ElementStorage} from "../ElementStorage";
|
import {ElementStorage} from "../ElementStorage";
|
||||||
import Combine from "../../UI/Base/Combine";
|
import Combine from "../../UI/Base/Combine";
|
||||||
|
|
||||||
class TitleElement extends UIElement {
|
class TitleElement extends UIEventSource<string> {
|
||||||
|
|
||||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||||
private readonly _selectedFeature: UIEventSource<any>;
|
private readonly _selectedFeature: UIEventSource<any>;
|
||||||
private readonly _allElementsStorage: ElementStorage;
|
private readonly _allElementsStorage: ElementStorage;
|
||||||
|
@ -15,42 +15,44 @@ class TitleElement extends UIElement {
|
||||||
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
||||||
selectedFeature: UIEventSource<any>,
|
selectedFeature: UIEventSource<any>,
|
||||||
allElementsStorage: ElementStorage) {
|
allElementsStorage: ElementStorage) {
|
||||||
super(layoutToUse);
|
super("MapComplete");
|
||||||
|
|
||||||
this._layoutToUse = layoutToUse;
|
this._layoutToUse = layoutToUse;
|
||||||
this._selectedFeature = selectedFeature;
|
this._selectedFeature = selectedFeature;
|
||||||
this._allElementsStorage = allElementsStorage;
|
this._allElementsStorage = allElementsStorage;
|
||||||
this.ListenTo(Locale.language);
|
|
||||||
this.ListenTo(this._selectedFeature)
|
|
||||||
this.dumbMode = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
this.syncWith(
|
||||||
|
this._selectedFeature.map(
|
||||||
|
selected => {
|
||||||
|
const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ??"MapComplete"
|
||||||
|
|
||||||
const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ?? "MapComplete"
|
if(selected === undefined){
|
||||||
const feature = this._selectedFeature.data;
|
return defaultTitle
|
||||||
|
}
|
||||||
|
|
||||||
if (feature === undefined) {
|
const layout = layoutToUse.data;
|
||||||
return defaultTitle;
|
const tags = selected.properties;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const layout = this._layoutToUse.data;
|
for (const layer of layout.layers) {
|
||||||
const properties = this._selectedFeature.data.properties;
|
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) {
|
return defaultTitle
|
||||||
if (layer.title === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (layer.source.osmTags.matchesProperties(properties)) {
|
|
||||||
const tags = this._allElementsStorage.getEventSourceById(feature.properties.id);
|
|
||||||
if (tags == undefined) {
|
|
||||||
return defaultTitle;
|
|
||||||
}
|
}
|
||||||
const title = new TagRenderingAnswer(tags, layer.title)
|
, [Locale.language, layoutToUse]
|
||||||
return new Combine([defaultTitle, " | ", title]).Render();
|
)
|
||||||
}
|
|
||||||
}
|
)
|
||||||
return defaultTitle;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -59,14 +61,8 @@ export default class TitleHandler {
|
||||||
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
||||||
selectedFeature: UIEventSource<any>,
|
selectedFeature: UIEventSource<any>,
|
||||||
allElementsStorage: ElementStorage) {
|
allElementsStorage: ElementStorage) {
|
||||||
|
new TitleElement(layoutToUse, selectedFeature, allElementsStorage).addCallbackAndRun(title => {
|
||||||
selectedFeature.addCallbackAndRun(_ => {
|
document.title = title
|
||||||
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);
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,46 +1,48 @@
|
||||||
import {GeoOperations} from "./GeoOperations";
|
import {GeoOperations} from "./GeoOperations";
|
||||||
import {UIElement} from "../UI/UIElement";
|
|
||||||
import Combine from "../UI/Base/Combine";
|
import Combine from "../UI/Base/Combine";
|
||||||
import {Relation} from "./Osm/ExtractRelations";
|
import {Relation} from "./Osm/ExtractRelations";
|
||||||
import State from "../State";
|
import State from "../State";
|
||||||
import {Utils} from "../Utils";
|
import {Utils} from "../Utils";
|
||||||
|
import BaseUIElement from "../UI/BaseUIElement";
|
||||||
|
import List from "../UI/Base/List";
|
||||||
|
import Title from "../UI/Base/Title";
|
||||||
|
|
||||||
export class ExtraFunction {
|
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(
|
private static readonly OverlapFunc = new ExtraFunction(
|
||||||
"overlapWith",
|
"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)"],
|
["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"],
|
||||||
(params, feat) => {
|
(params, feat) => {
|
||||||
return (...layerIds: string[]) => {
|
return (...layerIds: string[]) => {
|
||||||
|
@ -72,7 +74,7 @@ Some advanced functions are available on <b>feat</b> as well:
|
||||||
if (typeof arg0 === "string") {
|
if (typeof arg0 === "string") {
|
||||||
// This is an identifier
|
// This is an identifier
|
||||||
const feature = State.state.allElements.ContainingFeatures.get(arg0);
|
const feature = State.state.allElements.ContainingFeatures.get(arg0);
|
||||||
if(feature === undefined){
|
if (feature === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
arg0 = feature;
|
arg0 = feature;
|
||||||
|
@ -138,9 +140,9 @@ Some advanced functions are available on <b>feat</b> as well:
|
||||||
|
|
||||||
private static readonly Memberships = new ExtraFunction(
|
private static readonly Memberships = new ExtraFunction(
|
||||||
"memberships",
|
"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" +
|
"\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, _) => {
|
(params, _) => {
|
||||||
return () => params.relations ?? [];
|
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([
|
return new Combine([
|
||||||
ExtraFunction.intro,
|
ExtraFunction.intro,
|
||||||
"<ul>",
|
new List(ExtraFunction.allFuncs.map(func => func._name)),
|
||||||
...ExtraFunction.allFuncs.map(func =>
|
...elems
|
||||||
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>"
|
|
||||||
])
|
|
||||||
)
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,11 @@ import Constants from "../../Models/Constants";
|
||||||
|
|
||||||
export class ChangesetHandler {
|
export class ChangesetHandler {
|
||||||
|
|
||||||
|
public readonly currentChangeset: UIEventSource<string>;
|
||||||
private readonly _dryRun: boolean;
|
private readonly _dryRun: boolean;
|
||||||
private readonly userDetails: UIEventSource<UserDetails>;
|
private readonly userDetails: UIEventSource<UserDetails>;
|
||||||
private readonly auth: any;
|
private readonly auth: any;
|
||||||
|
|
||||||
public readonly currentChangeset: UIEventSource<string>;
|
|
||||||
|
|
||||||
constructor(layoutName: string, dryRun: boolean, osmConnection: OsmConnection, auth) {
|
constructor(layoutName: string, dryRun: boolean, osmConnection: OsmConnection, auth) {
|
||||||
this._dryRun = dryRun;
|
this._dryRun = dryRun;
|
||||||
this.userDetails = osmConnection.userDetails;
|
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(
|
public UploadChangeset(
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
generateChangeXML: (csid: string) => string,
|
generateChangeXML: (csid: string) => string,
|
||||||
continuation: () => void) {
|
continuation: () => void) {
|
||||||
|
|
||||||
if(this.userDetails.data.csCount == 0){
|
if (this.userDetails.data.csCount == 0) {
|
||||||
// The user became a contributor!
|
// The user became a contributor!
|
||||||
this.userDetails.data.csCount = 1;
|
this.userDetails.data.csCount = 1;
|
||||||
this.userDetails.ping();
|
this.userDetails.ping();
|
||||||
|
@ -51,7 +70,7 @@ export class ChangesetHandler {
|
||||||
|
|
||||||
if (this.currentChangeset.data === undefined || this.currentChangeset.data === "") {
|
if (this.currentChangeset.data === undefined || this.currentChangeset.data === "") {
|
||||||
// We have to open a new changeset
|
// We have to open a new changeset
|
||||||
this.OpenChangeset(layout,(csId) => {
|
this.OpenChangeset(layout, (csId) => {
|
||||||
this.currentChangeset.setData(csId);
|
this.currentChangeset.setData(csId);
|
||||||
const changeset = generateChangeXML(csId);
|
const changeset = generateChangeXML(csId);
|
||||||
console.log(changeset);
|
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(
|
private OpenChangeset(
|
||||||
layout : LayoutConfig,
|
layout: LayoutConfig,
|
||||||
continuation: (changesetId: string) => void) {
|
continuation: (changesetId: string) => void) {
|
||||||
|
|
||||||
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
|
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
|
||||||
|
|
||||||
let surveySource = "";
|
let path = window.location.pathname;
|
||||||
if (State.state.currentGPSLocation.data !== undefined) {
|
path = path.substr(1, path.lastIndexOf("/"));
|
||||||
surveySource = '<tag k="source" v="survey"/>'
|
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({
|
this.auth.xhr({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
path: '/api/0.6/changeset/create',
|
path: '/api/0.6/changeset/create',
|
||||||
options: {header: {'Content-Type': 'text/xml'}},
|
options: {header: {'Content-Type': 'text/xml'}},
|
||||||
content: [`<osm><changeset>`,
|
content: [`<osm><changeset>`,
|
||||||
`<tag k="created_by" v="MapComplete ${Constants.vNumber}" />`,
|
metadata,
|
||||||
`<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)}"/>` : "",
|
|
||||||
`</changeset></osm>`].join("")
|
`</changeset></osm>`].join("")
|
||||||
}, function (err, response) {
|
}, function (err, response) {
|
||||||
if (response === undefined) {
|
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 auth;
|
||||||
public userDetails: UIEventSource<UserDetails>;
|
public userDetails: UIEventSource<UserDetails>;
|
||||||
|
public isLoggedIn: UIEventSource<boolean>
|
||||||
_dryRun: boolean;
|
_dryRun: boolean;
|
||||||
public preferencesHandler: OsmPreferences;
|
public preferencesHandler: OsmPreferences;
|
||||||
public changesetHandler: ChangesetHandler;
|
public changesetHandler: ChangesetHandler;
|
||||||
|
@ -64,6 +65,14 @@ export class OsmConnection {
|
||||||
|
|
||||||
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(), "userDetails");
|
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(), "userDetails");
|
||||||
this.userDetails.data.dryRun = dryRun;
|
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._dryRun = dryRun;
|
||||||
|
|
||||||
this.updateAuthObject();
|
this.updateAuthObject();
|
||||||
|
@ -215,14 +224,15 @@ export class OsmConnection {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private CheckForMessagesContinuously() {
|
private CheckForMessagesContinuously(){
|
||||||
const self = this;
|
const self =this;
|
||||||
window.setTimeout(() => {
|
UIEventSource.Chronic(5 * 60 * 1000).addCallback(_ => {
|
||||||
if (self.userDetails.data.loggedIn) {
|
if (self.isLoggedIn .data) {
|
||||||
console.log("Checking for messages")
|
console.log("Checking for messages")
|
||||||
this.AttemptLogin();
|
self.AttemptLogin();
|
||||||
}
|
}
|
||||||
}, 5 * 60 * 1000);
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,15 @@ import {Tag} from "./Tags/Tag";
|
||||||
import {Or} from "./Tags/Or";
|
import {Or} from "./Tags/Or";
|
||||||
import {Utils} from "../Utils";
|
import {Utils} from "../Utils";
|
||||||
import opening_hours from "opening_hours";
|
import opening_hours from "opening_hours";
|
||||||
import {UIElement} from "../UI/UIElement";
|
|
||||||
import Combine from "../UI/Base/Combine";
|
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 = {
|
const cardinalDirections = {
|
||||||
N: 0, NNE: 22.5, NE: 45, ENE: 67.5,
|
N: 0, NNE: 22.5, NE: 45, ENE: 67.5,
|
||||||
E: 90, ESE: 112.5, SE: 135, SSE: 157.5,
|
E: 90, ESE: 112.5, SE: 135, SSE: 157.5,
|
||||||
S: 180, SSW: 202.5, SW: 225, WSW: 247.5,
|
S: 180, SSW: 202.5, SW: 225, WSW: 247.5,
|
||||||
W: 270, WNW: 292.5, NW: 315, NNW: 337.5
|
W: 270, WNW: 292.5, NW: 315, NNW: 337.5
|
||||||
}
|
}
|
||||||
|
@ -32,19 +34,19 @@ export default class SimpleMetaTagger {
|
||||||
|
|
||||||
const tgs = feature.properties;
|
const tgs = feature.properties;
|
||||||
|
|
||||||
function move(src: string, target: string){
|
function move(src: string, target: string) {
|
||||||
if(tgs[src] === undefined){
|
if (tgs[src] === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tgs[target] = tgs[src]
|
tgs[target] = tgs[src]
|
||||||
delete tgs[src]
|
delete tgs[src]
|
||||||
}
|
}
|
||||||
|
|
||||||
move("user","_last_edit:contributor")
|
move("user", "_last_edit:contributor")
|
||||||
move("uid","_last_edit:contributor:uid")
|
move("uid", "_last_edit:contributor:uid")
|
||||||
move("changeset","_last_edit:changeset")
|
move("changeset", "_last_edit:changeset")
|
||||||
move("timestamp","_last_edit:timestamp")
|
move("timestamp", "_last_edit:timestamp")
|
||||||
move("version","_version_number")
|
move("version", "_version_number")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static latlon = new SimpleMetaTagger({
|
private static latlon = new SimpleMetaTagger({
|
||||||
|
@ -100,9 +102,13 @@ export default class SimpleMetaTagger {
|
||||||
|
|
||||||
SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => {
|
SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => {
|
||||||
try {
|
try {
|
||||||
|
const oldCountry = feature.properties["_country"];
|
||||||
feature.properties["_country"] = countries[0].trim().toLowerCase();
|
feature.properties["_country"] = countries[0].trim().toLowerCase();
|
||||||
const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id);
|
if (oldCountry !== feature.properties["_country"]) {
|
||||||
tagsSource.ping();
|
const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id);
|
||||||
|
tagsSource.ping();
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
}
|
}
|
||||||
|
@ -375,28 +381,27 @@ export default class SimpleMetaTagger {
|
||||||
SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback)
|
SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
static HelpText(): UIElement {
|
static HelpText(): BaseUIElement {
|
||||||
const subElements: UIElement[] = [
|
const subElements: (string | BaseUIElement)[] = [
|
||||||
new Combine([
|
new Combine([
|
||||||
"<h2>Metatags</h2>",
|
new Title("Metatags", 1),
|
||||||
"<p>Metatags are extra tags available, in order to display more data or to give better questions.</p>",
|
"Metatags are extra tags available, in order to display more data or to give better questions.",
|
||||||
"<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>",
|
"The are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.",
|
||||||
"<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>"
|
"**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) {
|
for (const metatag of SimpleMetaTagger.metatags) {
|
||||||
subElements.push(
|
subElements.push(
|
||||||
new Combine([
|
new Title(metatag.keys.join(", "), 3),
|
||||||
"<h3>", metatag.keys.join(", "), "</h3>",
|
metatag.doc
|
||||||
metatag.doc]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Combine(subElements)
|
return new Combine(subElements).SetClass("flex-col")
|
||||||
}
|
}
|
||||||
|
|
||||||
addMetaTags(features: { feature: any, freshness: Date }[]) {
|
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>[] = [],
|
extraSources: UIEventSource<any>[] = [],
|
||||||
g: ((J) => T) = undefined): UIEventSource<J> {
|
g: ((j:J, t:T) => T) = undefined): UIEventSource<J> {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
const newSource = new UIEventSource<J>(
|
const newSource = new UIEventSource<J>(
|
||||||
|
@ -113,7 +120,7 @@ export class UIEventSource<T> {
|
||||||
|
|
||||||
if (g !== undefined) {
|
if (g !== undefined) {
|
||||||
newSource.addCallback((latest) => {
|
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
|
// @ts-ignore
|
||||||
import $ from "jquery"
|
import $ from "jquery"
|
||||||
import {LicenseInfo} from "./Wikimedia";
|
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(
|
static uploadMultiple(
|
||||||
title: string, description: string, blobs: FileList,
|
title: string, description: string, blobs: FileList,
|
||||||
handleSuccessfullUpload: ((imageURL: string) => void),
|
handleSuccessfullUpload: ((imageURL: string) => void),
|
||||||
allDone: (() => void),
|
allDone: (() => void),
|
||||||
onFail: ((reason: string) => 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) {
|
if (blobs.length == offset) {
|
||||||
allDone();
|
allDone();
|
||||||
return;
|
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,
|
static uploadImage(title: string, description: string, blob,
|
||||||
handleSuccessfullUpload: ((imageURL: string) => void),
|
handleSuccessfullUpload: ((imageURL: string) => void),
|
||||||
onFail: (reason:string) => void) {
|
onFail: (reason: string) => void) {
|
||||||
|
|
||||||
const apiUrl = 'https://api.imgur.com/3/image';
|
const apiUrl = 'https://api.imgur.com/3/image';
|
||||||
const apiKey = '7070e7167f0a25a';
|
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 $ from "jquery"
|
||||||
import {LicenseInfo} from "./Wikimedia";
|
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,
|
private constructor() {
|
||||||
handleDescription: ((license: LicenseInfo) => void)) {
|
super();
|
||||||
const url = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
|
}
|
||||||
|
|
||||||
const settings = {
|
private static ExtractKeyFromURL(value: string) {
|
||||||
async: true,
|
if (value.startsWith("https://a.mapillary.com")) {
|
||||||
type: 'GET',
|
return value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1);
|
||||||
url: url
|
}
|
||||||
};
|
const matchApi = value.match(/https?:\/\/images.mapillary.com\/([^/]*)/)
|
||||||
$.getJSON(url, function(data) {
|
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();
|
const license = new LicenseInfo();
|
||||||
license.artist = data.properties?.username;
|
license.artist = data.properties?.username;
|
||||||
license.licenseShortName = "CC BY-SA 4.0";
|
license.licenseShortName = "CC BY-SA 4.0";
|
||||||
license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||||
license.attributionRequired = true;
|
license.attributionRequired = true;
|
||||||
handleDescription(license);
|
source.setData(license);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return source
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,9 @@
|
||||||
*/
|
*/
|
||||||
import {UIEventSource} from "../UIEventSource";
|
import {UIEventSource} from "../UIEventSource";
|
||||||
import Hash from "./Hash";
|
import Hash from "./Hash";
|
||||||
|
import {Utils} from "../../Utils";
|
||||||
|
import Title from "../../UI/Base/Title";
|
||||||
|
import Combine from "../../UI/Base/Combine";
|
||||||
|
|
||||||
export class QueryParameters {
|
export class QueryParameters {
|
||||||
|
|
||||||
|
@ -12,6 +15,58 @@ export class QueryParameters {
|
||||||
private static defaults = {}
|
private static defaults = {}
|
||||||
|
|
||||||
private static documentation = {}
|
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) {
|
private static addOrder(key) {
|
||||||
if (this.order.indexOf(key) < 0) {
|
if (this.order.indexOf(key) < 0) {
|
||||||
|
@ -26,6 +81,10 @@ export class QueryParameters {
|
||||||
}
|
}
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
|
||||||
|
if (Utils.runningFromConsole) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (window?.location?.search) {
|
if (window?.location?.search) {
|
||||||
const params = window.location.search.substr(1).split("&");
|
const params = window.location.search.substr(1).split("&");
|
||||||
for (const param of params) {
|
for (const param of params) {
|
||||||
|
@ -66,37 +125,4 @@ export class QueryParameters {
|
||||||
history.replaceState(null, "", "?" + parts.join("&") + Hash.Current());
|
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 * 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
|
* 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 {
|
static ImageNameToUrl(filename: string, width: number = 500, height: number = 200): string {
|
||||||
filename = encodeURIComponent(filename);
|
filename = encodeURIComponent(filename);
|
||||||
return "https://commons.wikimedia.org/wiki/Special:FilePath/" + filename + "?width=" + width + "&height=" + height;
|
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),
|
static GetCategoryFiles(categoryName: string, handleCategory: ((ImagesInCategory: ImagesInCategory) => void),
|
||||||
alreadyLoaded = 0,
|
alreadyLoaded = 0,
|
||||||
continueParameter: { k: string, param: string } = undefined) {
|
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 {
|
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
|
// The user journey states thresholds when a new feature gets unlocked
|
||||||
public static userJourney = {
|
public static userJourney = {
|
||||||
addNewPointsUnlock: 0,
|
|
||||||
moreScreenUnlock: 1,
|
moreScreenUnlock: 1,
|
||||||
personalLayoutUnlock: 15,
|
personalLayoutUnlock: 15,
|
||||||
historyLinkVisible: 20,
|
historyLinkVisible: 20,
|
||||||
|
|
23
State.ts
23
State.ts
|
@ -70,10 +70,6 @@ export default class State {
|
||||||
readonly layerDef: LayerConfig;
|
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
|
The latest element that was selected
|
||||||
|
@ -106,6 +102,8 @@ export default class State {
|
||||||
*/
|
*/
|
||||||
public readonly locationControl = new UIEventSource<Loc>(undefined);
|
public readonly locationControl = new UIEventSource<Loc>(undefined);
|
||||||
public backgroundLayer;
|
public backgroundLayer;
|
||||||
|
public readonly backgroundLayerId: UIEventSource<string>;
|
||||||
|
|
||||||
/* Last location where a click was registered
|
/* Last location where a click was registered
|
||||||
*/
|
*/
|
||||||
public readonly LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined)
|
public readonly LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined)
|
||||||
|
@ -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'")
|
"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.osmConnection = new OsmConnection(
|
||||||
this.featureSwitchIsTesting.data,
|
this.featureSwitchIsTesting.data,
|
||||||
QueryParameters.GetQueryParameter("oauth_token", undefined,
|
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 {UIElement} from "../UIElement";
|
||||||
import Locale from "../i18n/Locale";
|
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class Button extends UIElement {
|
export class Button extends BaseUIElement {
|
||||||
private _text: UIElement;
|
private _text: BaseUIElement;
|
||||||
private _onclick: () => void;
|
private _onclick: () => void;
|
||||||
private _clss: string;
|
|
||||||
|
|
||||||
constructor(text: string | UIElement, onclick: (() => void), clss: string = "") {
|
constructor(text: string | UIElement, onclick: (() => void)) {
|
||||||
super(Locale.language);
|
super();
|
||||||
this._text = Translations.W(text);
|
this._text = Translations.W(text);
|
||||||
this._onclick = onclick;
|
this._onclick = onclick;
|
||||||
if (clss !== "") {
|
|
||||||
|
|
||||||
this._clss = "class='" + clss + "'";
|
|
||||||
}else{
|
|
||||||
this._clss = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
InnerRender(): string {
|
const el = this._text.ConstructElement();
|
||||||
|
if(el === undefined){
|
||||||
return "<form>" +
|
return undefined;
|
||||||
"<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();
|
|
||||||
}
|
}
|
||||||
|
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 {FixedUiElement} from "./FixedUiElement";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class Combine extends UIElement {
|
export default class Combine extends BaseUIElement {
|
||||||
private readonly uiElements: UIElement[];
|
private readonly uiElements: BaseUIElement[];
|
||||||
|
|
||||||
constructor(uiElements: (string | UIElement)[]) {
|
constructor(uiElements: (string | BaseUIElement)[]) {
|
||||||
super();
|
super();
|
||||||
this.uiElements = Utils.NoNull(uiElements)
|
this.uiElements = Utils.NoNull(uiElements)
|
||||||
.map(el => {
|
.map(el => {
|
||||||
|
@ -16,17 +16,32 @@ export default class Combine extends UIElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
return this.uiElements.map(ui => {
|
const el = document.createElement("span")
|
||||||
if(ui === undefined || ui === null){
|
|
||||||
return "";
|
try{
|
||||||
|
|
||||||
|
|
||||||
|
for (const subEl of this.uiElements) {
|
||||||
|
if(subEl === undefined || subEl === null){
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if(ui.Render === undefined){
|
const subHtml = subEl.ConstructElement()
|
||||||
console.error("Not a UI-element", ui);
|
if(subHtml !== undefined){
|
||||||
return "";
|
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,10 +1,10 @@
|
||||||
import {UIElement} from "../UIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class FixedUiElement extends UIElement {
|
export class FixedUiElement extends BaseUIElement {
|
||||||
private _html: string;
|
private _html: string;
|
||||||
|
|
||||||
constructor(html: string) {
|
constructor(html: string) {
|
||||||
super(undefined);
|
super();
|
||||||
this._html = html ?? "";
|
this._html = html ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,4 +12,14 @@ export class FixedUiElement extends UIElement {
|
||||||
return this._html;
|
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 {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){
|
static AsData(source: string) {
|
||||||
if(Utils.runningFromConsole){
|
if (Utils.runningFromConsole) {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
return `data:image/svg+xml;base64,${(btoa(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)}">`;
|
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 Translations from "../i18n/Translations";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
|
||||||
|
|
||||||
export default class Link extends UIElement {
|
export default class Link extends BaseUIElement {
|
||||||
private readonly _embeddedShow: UIElement;
|
private readonly _href: string | UIEventSource<string>;
|
||||||
private readonly _target: string;
|
private readonly _embeddedShow: BaseUIElement;
|
||||||
private readonly _newTab: string;
|
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();
|
super();
|
||||||
this._embeddedShow = Translations.W(embeddedShow);
|
this._embeddedShow =Translations.W(embeddedShow);
|
||||||
this._target = target;
|
this._href = href;
|
||||||
this._newTab = "";
|
this._newTab = newTab;
|
||||||
if (newTab) {
|
|
||||||
this._newTab = "target='_blank'"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
return `<a href="${this._target}" ${this._newTab}>${this._embeddedShow.Render()}</a>`;
|
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 {UIElement} from "../UIElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import Svg from "../../Svg";
|
|
||||||
import State from "../../State";
|
|
||||||
|
|
||||||
export default class Ornament extends UIElement {
|
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 {FixedUiElement} from "./FixedUiElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Hash from "../../Logic/Web/Hash";
|
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 {
|
export default class ScrollableFullScreen extends UIElement {
|
||||||
private static readonly empty = new FixedUiElement("");
|
private static readonly empty = new FixedUiElement("");
|
||||||
public isShown: UIEventSource<boolean>;
|
public isShown: UIEventSource<boolean>;
|
||||||
private _component: UIElement;
|
private _component: BaseUIElement;
|
||||||
private _fullscreencomponent: UIElement;
|
private _fullscreencomponent: BaseUIElement;
|
||||||
private static readonly _actor = ScrollableFullScreen.InitActor();
|
private static readonly _actor = ScrollableFullScreen.InitActor();
|
||||||
private _hashToSet: string;
|
private _hashToSet: string;
|
||||||
private static _currentlyOpen : ScrollableFullScreen;
|
private static _currentlyOpen : ScrollableFullScreen;
|
||||||
|
|
||||||
constructor(title: ((mode: string) => UIElement), content: ((mode: string) => UIElement),
|
constructor(title: ((mode: string) => BaseUIElement), content: ((mode: string) => BaseUIElement),
|
||||||
hashToSet: string,
|
hashToSet: string,
|
||||||
isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
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)
|
this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown)
|
||||||
.SetClass("hidden md:block");
|
.SetClass("hidden md:block");
|
||||||
this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown);
|
this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown);
|
||||||
this.dumbMode = false;
|
|
||||||
const self = this;
|
const self = this;
|
||||||
isShown.addCallback(isShown => {
|
isShown.addCallback(isShown => {
|
||||||
if (isShown) {
|
if (isShown) {
|
||||||
|
@ -40,8 +46,8 @@ export default class ScrollableFullScreen extends UIElement {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): BaseUIElement {
|
||||||
return this._component.Render();
|
return this._component;
|
||||||
}
|
}
|
||||||
|
|
||||||
Activate(): void {
|
Activate(): void {
|
||||||
|
@ -55,7 +61,7 @@ export default class ScrollableFullScreen extends UIElement {
|
||||||
fs.classList.remove("hidden")
|
fs.classList.remove("hidden")
|
||||||
}
|
}
|
||||||
|
|
||||||
private BuildComponent(title: UIElement, content: UIElement, isShown: UIEventSource<boolean>) {
|
private BuildComponent(title: BaseUIElement, content:BaseUIElement, isShown: UIEventSource<boolean>) {
|
||||||
const returnToTheMap =
|
const returnToTheMap =
|
||||||
new Combine([
|
new Combine([
|
||||||
Svg.back_svg().SetClass("block md:hidden"),
|
Svg.back_svg().SetClass("block md:hidden"),
|
||||||
|
|
|
@ -1,57 +1,58 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Combine from "./Combine";
|
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) {
|
private readonly _element: BaseUIElement
|
||||||
super(SubtleButton.generateContent(imageUrl, message, linkTo));
|
|
||||||
|
|
||||||
|
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")
|
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);
|
const message = Translations.W(messageT);
|
||||||
if (message !== null) {
|
message
|
||||||
message.dumbMode = false;
|
|
||||||
}
|
|
||||||
let img;
|
let img;
|
||||||
if ((imageUrl ?? "") === "") {
|
if ((imageUrl ?? "") === "") {
|
||||||
img = new FixedUiElement("");
|
img = undefined;
|
||||||
} else if (typeof (imageUrl) === "string") {
|
} else if (typeof (imageUrl) === "string") {
|
||||||
img = new FixedUiElement(`<img style="width: 100%;" src="${imageUrl}" alt="">`);
|
img = new Img(imageUrl)
|
||||||
} else {
|
} else {
|
||||||
img = imageUrl;
|
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])
|
const image = new Combine([img])
|
||||||
.SetClass("flex-shrink-0");
|
.SetClass("flex-shrink-0");
|
||||||
|
|
||||||
|
if (linkTo == undefined) {
|
||||||
if (message !== null && message.IsEmpty()) {
|
return new Combine([
|
||||||
// Message == null: special case to force empty text
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkTo != undefined) {
|
|
||||||
return [
|
|
||||||
`<a class='flex group' href="${linkTo.url}" ${linkTo.newTab ? 'target="_blank"' : ""}>`,
|
|
||||||
image,
|
image,
|
||||||
`<div class='ml-4 overflow-ellipsis'>`,
|
message?.SetClass("block overflow-ellipsis"),
|
||||||
message,
|
]).SetClass("flex group w-full");
|
||||||
`</div>`,
|
|
||||||
`</a>`
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 Translations from "../i18n/Translations";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
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[] = [];
|
constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) {
|
||||||
private content: UIElement[] = [];
|
|
||||||
|
|
||||||
constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) {
|
const openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0))
|
||||||
super(typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0)));
|
|
||||||
const self = this;
|
const tabs: BaseUIElement[] = []
|
||||||
|
const contentElements: BaseUIElement[] = [];
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
let element = elements[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)
|
const content = Translations.W(element.content)
|
||||||
this.content.push(content);
|
content.SetClass("relative p-4 w-full inline-block")
|
||||||
}
|
contentElements.push(content);
|
||||||
}
|
const tab = header.SetClass("block tab-single-header")
|
||||||
|
tabs.push(tab)
|
||||||
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>"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class VariableUiElement extends UIElement {
|
export class VariableUiElement extends BaseUIElement {
|
||||||
private _html: UIEventSource<string>;
|
|
||||||
|
|
||||||
constructor(html: UIEventSource<string>) {
|
private _element : HTMLElement;
|
||||||
super(html);
|
|
||||||
this._html = html;
|
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 {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
return this._html.data;
|
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 Link from "../Base/Link";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
|
@ -8,67 +7,57 @@ import Constants from "../../Models/Constants";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import Loc from "../../Models/Loc";
|
import Loc from "../../Models/Loc";
|
||||||
import * as L from "leaflet"
|
import * as L from "leaflet"
|
||||||
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bottom right attribution panel in the leaflet map
|
* The bottom right attribution panel in the leaflet map
|
||||||
*/
|
*/
|
||||||
export default class Attribution extends UIElement {
|
export default class Attribution extends Combine {
|
||||||
|
|
||||||
private readonly _location: UIEventSource<Loc>;
|
|
||||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
|
||||||
private readonly _userDetails: UIEventSource<UserDetails>;
|
|
||||||
private readonly _leafletMap: UIEventSource<L.Map>;
|
|
||||||
|
|
||||||
constructor(location: UIEventSource<Loc>,
|
constructor(location: UIEventSource<Loc>,
|
||||||
userDetails: UIEventSource<UserDetails>,
|
userDetails: UIEventSource<UserDetails>,
|
||||||
layoutToUse: UIEventSource<LayoutConfig>,
|
layoutToUse: UIEventSource<LayoutConfig>,
|
||||||
leafletMap: UIEventSource<L.Map>) {
|
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 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 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)
|
const stats = new Link(Svg.statistics_ui().SetClass("small-image"), 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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let editWithJosm: (UIElement | string) = ""
|
const idLink = location.map(location => `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}`)
|
||||||
if (location !== undefined &&
|
const editHere = new Link(Svg.pencil_ui().SetClass("small-image"), idLink, true)
|
||||||
this._leafletMap?.data !== undefined &&
|
|
||||||
userDetails.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked) {
|
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 bounds: any = this._leafletMap.data.getBounds();
|
const mapillary = new Link(Svg.mapillary_black_ui().SetClass("small-image"), mapillaryLink, true);
|
||||||
const top = bounds.getNorth();
|
|
||||||
const bottom = bounds.getSouth();
|
|
||||||
const right = bounds.getEast();
|
|
||||||
const left = bounds.getWest();
|
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 {Utils} from "../../Utils";
|
||||||
import Link from "../Base/Link";
|
import Link from "../Base/Link";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import * as contributors from "../../assets/contributors.json"
|
import * as contributors from "../../assets/contributors.json"
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attribution panel shown on mobile
|
* 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.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.data.maintainer}),
|
||||||
layoutToUse.data.credits,
|
layoutToUse.data.credits,
|
||||||
"<br/>",
|
"<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/>",
|
"<br/>",
|
||||||
|
|
||||||
new VariableUiElement(contributions.map(contributions => {
|
new VariableUiElement(contributions.map(contributions => {
|
||||||
|
@ -44,16 +44,14 @@ export default class AttributionPanel extends Combine {
|
||||||
const contribs = links.join(", ")
|
const contribs = links.join(", ")
|
||||||
|
|
||||||
if (hiddenCount == 0) {
|
if (hiddenCount == 0) {
|
||||||
|
|
||||||
|
|
||||||
return Translations.t.general.attribution.mapContributionsBy.Subs({
|
return Translations.t.general.attribution.mapContributionsBy.Subs({
|
||||||
contributors: contribs
|
contributors: contribs
|
||||||
}).InnerRender()
|
})
|
||||||
} else {
|
} else {
|
||||||
return Translations.t.general.attribution.mapContributionsByAndHidden.Subs({
|
return Translations.t.general.attribution.mapContributionsByAndHidden.Subs({
|
||||||
contributors: contribs,
|
contributors: contribs,
|
||||||
hiddenCount: hiddenCount
|
hiddenCount: hiddenCount
|
||||||
}).InnerRender();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,7 +66,7 @@ export default class AttributionPanel extends Combine {
|
||||||
this.SetStyle("max-width: calc(100vw - 5em); width: 40em;")
|
this.SetStyle("max-width: calc(100vw - 5em); width: 40em;")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CodeContributors(): UIElement {
|
private static CodeContributors(): BaseUIElement {
|
||||||
|
|
||||||
const total = contributors.contributors.length;
|
const total = contributors.contributors.length;
|
||||||
let filtered = contributors.contributors
|
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")) {
|
if (iconPath.startsWith("http")) {
|
||||||
iconPath = "." + new URL(iconPath).pathname;
|
iconPath = "." + new URL(iconPath).pathname;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,35 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {DropDown} from "../Input/DropDown";
|
import {DropDown} from "../Input/DropDown";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import BaseLayer from "../../Models/BaseLayer";
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
|
|
||||||
export default class BackgroundSelector extends UIElement {
|
export default class BackgroundSelector extends VariableUiElement {
|
||||||
|
|
||||||
private _dropdown: UIElement;
|
|
||||||
private readonly _availableLayers: UIEventSource<BaseLayer[]>;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
const available = State.state.availableBackgroundLayers.map(available => {
|
||||||
const self = this;
|
const baseLayers: { value: BaseLayer, shown: string }[] = [];
|
||||||
this._availableLayers = State.state.availableBackgroundLayers;
|
for (const i in available) {
|
||||||
this._availableLayers.addCallbackAndRun(available => self.CreateDropDown(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) {
|
super(
|
||||||
if(available.length === 0){
|
available.map(baseLayers => {
|
||||||
return;
|
if (baseLayers.length <= 1) {
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
|
return new DropDown(Translations.t.general.backgroundMap.Clone(), baseLayers, State.state.backgroundLayer)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
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});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 * as L from "leaflet"
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Loc from "../../Models/Loc";
|
import Loc from "../../Models/Loc";
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import BaseLayer from "../../Models/BaseLayer";
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class Basemap {
|
export class Basemap {
|
||||||
|
|
||||||
|
@ -12,14 +12,14 @@ export class Basemap {
|
||||||
constructor(leafletElementId: string,
|
constructor(leafletElementId: string,
|
||||||
location: UIEventSource<Loc>,
|
location: UIEventSource<Loc>,
|
||||||
currentLayer: UIEventSource<BaseLayer>,
|
currentLayer: UIEventSource<BaseLayer>,
|
||||||
lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
lastClickLocation?: UIEventSource<{ lat: number, lon: number }>,
|
||||||
extraAttribution: UIElement) {
|
extraAttribution?: BaseUIElement) {
|
||||||
this.map = L.map(leafletElementId, {
|
this.map = L.map(leafletElementId, {
|
||||||
center: [location.data.lat ?? 0, location.data.lon ?? 0],
|
center: [location.data.lat ?? 0, location.data.lon ?? 0],
|
||||||
zoom: location.data.zoom ?? 2,
|
zoom: location.data.zoom ?? 2,
|
||||||
layers: [currentLayer.data.layer],
|
layers: [currentLayer.data.layer],
|
||||||
zoomControl: false
|
zoomControl: false,
|
||||||
|
attributionControl: extraAttribution !== undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
L.control.scale(
|
L.control.scale(
|
||||||
|
@ -35,9 +35,11 @@ export class Basemap {
|
||||||
this.map.setMaxBounds(
|
this.map.setMaxBounds(
|
||||||
[[-100, -200], [100, 200]]
|
[[-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;
|
const self = this;
|
||||||
|
|
||||||
let previousLayer = currentLayer.data;
|
let previousLayer = currentLayer.data;
|
||||||
|
@ -69,12 +71,12 @@ export class Basemap {
|
||||||
|
|
||||||
this.map.on("click", function (e) {
|
this.map.on("click", function (e) {
|
||||||
// @ts-ignore
|
// @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) {
|
this.map.on("contextmenu", function (e) {
|
||||||
// @ts-ignore
|
// @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 State from "../../State";
|
||||||
import ThemeIntroductionPanel from "./ThemeIntroductionPanel";
|
import ThemeIntroductionPanel from "./ThemeIntroductionPanel";
|
||||||
import * as personal from "../../assets/themes/personalLayout/personalLayout.json";
|
import * as personal from "../../assets/themes/personalLayout/personalLayout.json";
|
||||||
|
@ -7,47 +6,39 @@ import Svg from "../../Svg";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import ShareScreen from "./ShareScreen";
|
import ShareScreen from "./ShareScreen";
|
||||||
import MoreScreen from "./MoreScreen";
|
import MoreScreen from "./MoreScreen";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import Locale from "../i18n/Locale";
|
|
||||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import Toggle from "../Input/Toggle";
|
||||||
|
|
||||||
export default class FullWelcomePaneWithTabs extends UIElement {
|
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
||||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
|
||||||
private readonly _userDetails: UIEventSource<UserDetails>;
|
|
||||||
|
|
||||||
private readonly _component: UIElement;
|
|
||||||
|
|
||||||
constructor(isShown: UIEventSource<boolean>) {
|
constructor(isShown: UIEventSource<boolean>) {
|
||||||
super(State.state.layoutToUse);
|
const layoutToUse = State.state.layoutToUse.data;
|
||||||
this._layoutToUse = State.state.layoutToUse;
|
super (
|
||||||
this._userDetails = State.state.osmConnection.userDetails;
|
|
||||||
const layoutToUse = this._layoutToUse.data;
|
|
||||||
|
|
||||||
|
|
||||||
this._component = new ScrollableFullScreen(
|
|
||||||
() => layoutToUse.title.Clone(),
|
() => layoutToUse.title.Clone(),
|
||||||
() => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails),
|
() => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails, isShown),
|
||||||
"welcome" ,isShown
|
"welcome" ,isShown
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource<UserDetails>) {
|
private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource<boolean>): { header: string | BaseUIElement; content: BaseUIElement }[]{
|
||||||
|
|
||||||
let welcome: UIElement = new ThemeIntroductionPanel();
|
let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown);
|
||||||
if (layoutToUse.id === personal.id) {
|
if (layoutToUse.id === personal.id) {
|
||||||
welcome = new PersonalLayersPanel();
|
welcome = new PersonalLayersPanel();
|
||||||
}
|
}
|
||||||
const tabs = [
|
const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [
|
||||||
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
|
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
|
||||||
{
|
{
|
||||||
header: Svg.osm_logo_img,
|
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,
|
header: Svg.help,
|
||||||
content: new VariableUiElement(userDetails.map(userdetails => {
|
content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "<br/>Version " + Constants.vNumber])
|
||||||
if (userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) {
|
.SetClass("link-underline")
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return new Combine([Translations.t.general.aboutMapcomplete, "<br/>Version " + Constants.vNumber]).SetClass("link-underline").Render();
|
|
||||||
}, [Locale.language]))
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab)
|
return new Toggle(
|
||||||
.ListenTo(userDetails);
|
new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab),
|
||||||
}
|
new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab),
|
||||||
|
userDetails.map((userdetails: UserDetails) =>
|
||||||
InnerRender(): string {
|
userdetails.loggedIn &&
|
||||||
return this._component.Render();
|
userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import BackgroundSelector from "./BackgroundSelector";
|
import BackgroundSelector from "./BackgroundSelector";
|
||||||
import LayerSelection from "./LayerSelection";
|
import LayerSelection from "./LayerSelection";
|
||||||
|
@ -7,6 +6,7 @@ import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class LayerControlPanel extends ScrollableFullScreen {
|
export default class LayerControlPanel extends ScrollableFullScreen {
|
||||||
|
|
||||||
|
@ -14,13 +14,12 @@ export default class LayerControlPanel extends ScrollableFullScreen {
|
||||||
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown);
|
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GenTitle(): UIElement {
|
private static GenTitle():BaseUIElement {
|
||||||
const title = Translations.t.general.layerSelection.title.SetClass("text-2xl break-words font-bold p-2")
|
return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2")
|
||||||
return title.Clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GeneratePanel() {
|
private static GeneratePanel() : BaseUIElement {
|
||||||
let layerControlPanel: UIElement = new FixedUiElement("");
|
let layerControlPanel: BaseUIElement = new FixedUiElement("");
|
||||||
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
||||||
layerControlPanel = new BackgroundSelector();
|
layerControlPanel = new BackgroundSelector();
|
||||||
layerControlPanel.SetStyle("margin:1em");
|
layerControlPanel.SetStyle("margin:1em");
|
||||||
|
|
|
@ -1,84 +1,69 @@
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import CheckBox from "../Input/CheckBox";
|
import Toggle from "../Input/Toggle";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the panel with all layers and a toggle for each of them
|
* 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<{
|
constructor(activeLayers: UIEventSource<{
|
||||||
readonly isDisplayed: UIEventSource<boolean>,
|
readonly isDisplayed: UIEventSource<boolean>,
|
||||||
readonly layerDef: LayerConfig;
|
readonly layerDef: LayerConfig;
|
||||||
}[]>) {
|
}[]>) {
|
||||||
super(activeLayers);
|
|
||||||
if(activeLayers === undefined){
|
if (activeLayers === undefined) {
|
||||||
throw "ActiveLayers should be defined..."
|
throw "ActiveLayers should be defined..."
|
||||||
}
|
}
|
||||||
this.activeLayers = activeLayers;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
const checkboxes: BaseUIElement[] = [];
|
||||||
|
|
||||||
this._checkboxes = [];
|
for (const layer of activeLayers.data) {
|
||||||
|
|
||||||
for (const layer of this.activeLayers.data) {
|
|
||||||
const leafletStyle = layer.layerDef.GenerateLeafletStyle(
|
const leafletStyle = layer.layerDef.GenerateLeafletStyle(
|
||||||
new UIEventSource<any>({id: "node/-1"}),
|
new UIEventSource<any>({id: "node/-1"}),
|
||||||
false)
|
false)
|
||||||
const leafletHtml = leafletStyle.icon.html;
|
const icon = new Combine([leafletStyle.icon.html]).SetClass("single-layer-selection-toggle")
|
||||||
const icon =
|
let iconUnselected: BaseUIElement = new Combine([leafletStyle.icon.html])
|
||||||
new FixedUiElement(leafletHtml.Render())
|
|
||||||
.SetClass("single-layer-selection-toggle")
|
|
||||||
let iconUnselected: UIElement = new FixedUiElement(leafletHtml.Render())
|
|
||||||
.SetClass("single-layer-selection-toggle")
|
.SetClass("single-layer-selection-toggle")
|
||||||
.SetStyle("opacity:0.2;");
|
.SetStyle("opacity:0.2;");
|
||||||
|
|
||||||
const name = Translations.WT(layer.layerDef.name)?.Clone()
|
const name = Translations.WT(layer.layerDef.name)?.Clone()
|
||||||
?.SetStyle("font-size:large;margin-left: 0.5em;");
|
?.SetStyle("font-size:large;margin-left: 0.5em;");
|
||||||
|
|
||||||
if((name ?? "") === ""){
|
if ((name ?? "") === "") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const zoomStatus = new VariableUiElement(State.state.locationControl.map(location => {
|
const zoomStatus = new VariableUiElement(State.state.locationControl.map(location => {
|
||||||
if (location.zoom < layer.layerDef.minzoom) {
|
if (location.zoom < layer.layerDef.minzoom) {
|
||||||
return Translations.t.general.layerSelection.zoomInToSeeThisLayer
|
return Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone()
|
||||||
.SetClass("alert")
|
.SetClass("alert")
|
||||||
.SetStyle("display: block ruby;width:min-content;")
|
.SetStyle("display: block ruby;width:min-content;")
|
||||||
.Render();
|
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}))
|
}))
|
||||||
const style = "display:flex;align-items:center;"
|
const style = "display:flex;align-items:center;"
|
||||||
const styleWhole = "display:flex; flex-wrap: wrap"
|
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])
|
new Combine([new Combine([icon, name]).SetStyle(style), zoomStatus])
|
||||||
.SetStyle(styleWhole),
|
.SetStyle(styleWhole),
|
||||||
new Combine([new Combine([iconUnselected, "<del>", name, "</del>"]).SetStyle(style), zoomStatus])
|
new Combine([new Combine([iconUnselected, "<del>", name, "</del>"]).SetStyle(style), zoomStatus])
|
||||||
.SetStyle(styleWhole),
|
.SetStyle(styleWhole),
|
||||||
layer.isDisplayed)
|
layer.isDisplayed).ToggleOnClick()
|
||||||
.SetStyle("margin:0.3em;")
|
.SetStyle("margin:0.3em;")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return new Combine(this._checkboxes)
|
super(checkboxes)
|
||||||
.SetStyle("display:flex;flex-direction:column;")
|
this.SetStyle("display:flex;flex-direction:column;")
|
||||||
.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
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 {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||||
|
@ -11,87 +10,94 @@ import * as personal from "../../assets/themes/personalLayout/personalLayout.jso
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import LanguagePicker from "../LanguagePicker";
|
import LanguagePicker from "../LanguagePicker";
|
||||||
import IndexText from "./IndexText";
|
import IndexText from "./IndexText";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class MoreScreen extends UIElement {
|
export default class MoreScreen extends Combine {
|
||||||
private readonly _onMainScreen: boolean;
|
|
||||||
|
|
||||||
private _component: UIElement;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(onMainScreen: boolean = false) {
|
constructor(onMainScreen: boolean = false) {
|
||||||
super(State.state.locationControl);
|
super(MoreScreen.Init(onMainScreen, State.state));
|
||||||
this._onMainScreen = onMainScreen;
|
|
||||||
this.ListenTo(State.state.osmConnection.userDetails);
|
|
||||||
this.ListenTo(State.state.installedThemes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
private static Init(onMainScreen: boolean, state: State): BaseUIElement [] {
|
||||||
|
|
||||||
const tr = Translations.t.general.morescreen;
|
const tr = Translations.t.general.morescreen;
|
||||||
|
let intro: BaseUIElement = tr.intro.Clone();
|
||||||
const els: UIElement[] = []
|
let themeButtonStyle = ""
|
||||||
|
let themeListStyle = ""
|
||||||
const themeButtons: UIElement[] = []
|
if (onMainScreen) {
|
||||||
|
|
||||||
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) {
|
|
||||||
intro = new Combine([
|
intro = new Combine([
|
||||||
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
|
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
|
||||||
.SetClass("absolute top-2 right-3"),
|
.SetClass("absolute top-2 right-3"),
|
||||||
new IndexText()
|
new IndexText()
|
||||||
])
|
])
|
||||||
themeButtons.map(e => e?.SetClass("h-32 min-h-32 max-h-32 overflow-ellipsis overflow-hidden"))
|
themeButtonStyle = "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")
|
themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return[
|
||||||
|
|
||||||
this._component = new Combine([
|
|
||||||
intro,
|
intro,
|
||||||
themeButtonsElement,
|
MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle),
|
||||||
tr.streetcomplete.SetClass("block text-base mx-10 my-3 mb-10")
|
MoreScreen.createUnofficialThemeList(themeButtonStyle)?.SetClass(themeListStyle),
|
||||||
]);
|
tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10")
|
||||||
return this._component.Render();
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined) {
|
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 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) {
|
if (layout === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -100,17 +106,14 @@ export default class MoreScreen extends UIElement {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (layout.hideFromOverview) {
|
if (layout.hideFromOverview) {
|
||||||
const pref = State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled");
|
return undefined;
|
||||||
this.ListenTo(pref);
|
|
||||||
if (pref.data !== "true") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (layout.id === State.state.layoutToUse.data?.id) {
|
if (layout.id === State.state.layoutToUse.data?.id) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLocation = State.state.locationControl.data;
|
const currentLocation = State.state.locationControl;
|
||||||
|
|
||||||
let path = window.location.pathname;
|
let path = window.location.pathname;
|
||||||
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
|
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
|
||||||
path = path.substr(0, path.lastIndexOf("/"));
|
path = path.substr(0, path.lastIndexOf("/"));
|
||||||
|
@ -119,29 +122,42 @@ export default class MoreScreen extends UIElement {
|
||||||
path = "."
|
path = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = `z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}`
|
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
|
||||||
let linkText =
|
let linkSuffix = ""
|
||||||
`${path}/${layout.id.toLowerCase()}.html?${params}`
|
|
||||||
|
|
||||||
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
|
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) {
|
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,
|
return new SubtleButton(layout.icon,
|
||||||
new Combine([
|
new Combine([
|
||||||
`<dt class='text-lg leading-6 font-medium text-gray-900 group-hover:text-blue-800'>`,
|
`<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>`,
|
`</dt>`,
|
||||||
`<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`,
|
`<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`,
|
||||||
description ?? "",
|
description.Clone().SetClass("subtle") ?? "",
|
||||||
`</dd>`,
|
`</dd>`,
|
||||||
]), {url: linkText, newTab: false});
|
]), {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 {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import CheckBox from "../Input/CheckBox";
|
import Toggle from "../Input/Toggle";
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import * as personal from "../../assets/themes/personalLayout/personalLayout.json"
|
import BaseUIElement from "../BaseUIElement";
|
||||||
import Locale from "../i18n/Locale";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
export default class PersonalLayersPanel extends UIElement {
|
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||||
private checkboxes: UIElement[] = [];
|
import Img from "../Base/Img";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
|
||||||
|
export default class PersonalLayersPanel extends VariableUiElement {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(State.state.favouriteLayers);
|
super(
|
||||||
this.ListenTo(State.state.osmConnection.userDetails);
|
State.state.installedThemes.map(installedThemes => {
|
||||||
this.ListenTo(Locale.language);
|
const t = Translations.t.favourite;
|
||||||
this.UpdateView([]);
|
|
||||||
const self = this;
|
|
||||||
State.state.installedThemes.addCallback(extraThemes => {
|
|
||||||
self.UpdateView(extraThemes.map(layout => layout.layout));
|
|
||||||
self.Update();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Lets get all the layers
|
||||||
|
const allThemes = AllKnownLayouts.layoutsList.concat(installedThemes.map(layout => layout.layout))
|
||||||
|
.filter(theme => !theme.hideFromOverview)
|
||||||
|
|
||||||
private UpdateView(extraThemes: LayoutConfig[]) {
|
const allLayers = []
|
||||||
this.checkboxes = [];
|
{
|
||||||
const favs = State.state.favouriteLayers.data ?? [];
|
const seenLayers = new Set<string>()
|
||||||
const controls = new Map<string, UIEventSource<boolean>>();
|
for (const layers of allThemes.map(theme => theme.layers)) {
|
||||||
const allLayouts = AllKnownLayouts.layoutsList.concat(extraThemes);
|
for (const layer of layers) {
|
||||||
for (const layout of allLayouts) {
|
if (seenLayers.has(layer.id)) {
|
||||||
if (layout.id === personal.id) {
|
continue
|
||||||
continue;
|
}
|
||||||
}
|
seenLayers.add(layer.id)
|
||||||
|
allLayers.push(layer)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
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;
|
* Creates a toggle for the given layer, which'll update State.state.favouriteLayers right away
|
||||||
const userDetails = State.state.osmConnection.userDetails.data;
|
* @param layer
|
||||||
if(!userDetails.loggedIn){
|
* @constructor
|
||||||
return t.loginNeeded.Render();
|
* @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([
|
iconUnset.SetStyle("opacity:0.1")
|
||||||
t.panelIntro,
|
|
||||||
...this.checkboxes
|
let name = layer.name ;
|
||||||
]).Render();
|
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 Locale from "../i18n/Locale";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {Translation} from "../i18n/Translation";
|
import {Translation} from "../i18n/Translation";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
|
@ -9,75 +8,76 @@ import {TextField} from "../Input/TextField";
|
||||||
import {Geocoding} from "../../Logic/Osm/Geocoding";
|
import {Geocoding} from "../../Logic/Osm/Geocoding";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Hash from "../../Logic/Web/Hash";
|
import Hash from "../../Logic/Web/Hash";
|
||||||
|
import Combine from "../Base/Combine";
|
||||||
|
|
||||||
export default class SearchAndGo extends UIElement {
|
export default class SearchAndGo extends Combine {
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(undefined);
|
const goButton = Svg.search_ui().SetClass('w-8 h-8 full-rounded border-black float-right');
|
||||||
this.ListenTo(this._foundEntries);
|
|
||||||
|
|
||||||
const self = this;
|
const placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
|
||||||
this._searchField.enterPressed.addCallback(() => {
|
const searchField = new TextField({
|
||||||
self.RunSearch();
|
placeholder: new VariableUiElement(
|
||||||
});
|
placeholder.map(uiElement => uiElement, [Locale.language])
|
||||||
|
),
|
||||||
|
value: new UIEventSource<string>(""),
|
||||||
|
|
||||||
this._goButton.onClick(function () {
|
inputStyle: " background: transparent;\n" +
|
||||||
self.RunSearch();
|
" border: none;\n" +
|
||||||
});
|
" font-size: large;\n" +
|
||||||
|
" width: 100%;\n" +
|
||||||
|
" box-sizing: border-box;\n" +
|
||||||
|
" color: var(--foreground-color);"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
InnerRender(): string {
|
searchField.SetClass("relative float-left mt-0 ml-2")
|
||||||
return this._searchField.Render() +
|
searchField.SetStyle("width: calc(100% - 3em)")
|
||||||
this._goButton.Render();
|
|
||||||
|
|
||||||
}
|
super([searchField, goButton])
|
||||||
|
|
||||||
|
this.SetClass("block h-8")
|
||||||
|
this.SetStyle("background: var(--background-color); color: var(--foreground-color); pointer-evetns:all;")
|
||||||
|
|
||||||
|
|
||||||
|
// 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{
|
export default class ShareButton extends BaseUIElement{
|
||||||
private _embedded: UIElement;
|
private _embedded: BaseUIElement;
|
||||||
private _shareData: { text: string; title: string; url: string };
|
private _shareData: () => { text: string; title: string; url: string };
|
||||||
|
|
||||||
constructor(embedded: UIElement, shareData: {
|
constructor(embedded: BaseUIElement, generateShareData: () => {
|
||||||
text: string,
|
text: string,
|
||||||
title: string,
|
title: string,
|
||||||
url: string
|
url: string
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this._embedded = embedded;
|
this._embedded = embedded;
|
||||||
this._shareData = shareData;
|
this._shareData = generateShareData;
|
||||||
|
this.SetClass("share-button")
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
return `<button type="button" class="share-button" id="${this.id}">${this._embedded.Render()}</button>`
|
const e = document.createElement("button")
|
||||||
}
|
e.type = "button"
|
||||||
|
e.appendChild(this._embedded.ConstructElement())
|
||||||
|
|
||||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
e.addEventListener('click', () => {
|
||||||
super.InnerUpdate(htmlElement);
|
|
||||||
const self= this;
|
|
||||||
htmlElement.addEventListener('click', () => {
|
|
||||||
if (navigator.share) {
|
if (navigator.share) {
|
||||||
navigator.share(self._shareData).then(() => {
|
navigator.share(this._shareData()).then(() => {
|
||||||
console.log('Thanks for sharing!');
|
console.log('Thanks for sharing!');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
@ -33,6 +32,9 @@ export default class ShareButton extends UIElement{
|
||||||
console.log('web share not supported');
|
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 {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import {Translation} from "../i18n/Translation";
|
import {Translation} from "../i18n/Translation";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
|
@ -9,29 +7,23 @@ import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import CheckBox from "../Input/CheckBox";
|
import Toggle from "../Input/Toggle";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class ShareScreen extends UIElement {
|
export default class ShareScreen extends Combine {
|
||||||
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;
|
|
||||||
|
|
||||||
constructor(layout: LayoutConfig = undefined, layoutDefinition: string = undefined) {
|
constructor(layout: LayoutConfig = undefined, layoutDefinition: string = undefined) {
|
||||||
super(undefined)
|
|
||||||
layout = layout ?? State.state?.layoutToUse?.data;
|
layout = layout ?? State.state?.layoutToUse?.data;
|
||||||
layoutDefinition = layoutDefinition ?? State.state?.layoutDefinition;
|
layoutDefinition = layoutDefinition ?? State.state?.layoutDefinition;
|
||||||
const tr = Translations.t.general.sharescreen;
|
const tr = Translations.t.general.sharescreen;
|
||||||
|
|
||||||
const optionCheckboxes: UIElement[] = []
|
const optionCheckboxes: BaseUIElement[] = []
|
||||||
const optionParts: (UIEventSource<string>)[] = [];
|
const optionParts: (UIEventSource<string>)[] = [];
|
||||||
this.SetClass("link-underline")
|
|
||||||
function check() {
|
function check() {
|
||||||
return Svg.checkmark_svg().SetStyle("width: 1.5em; display:inline-block;");
|
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;");
|
return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;");
|
||||||
}
|
}
|
||||||
|
|
||||||
const includeLocation = new CheckBox(
|
const includeLocation = new Toggle(
|
||||||
new Combine([check(), tr.fsIncludeCurrentLocation]),
|
new Combine([check(), tr.fsIncludeCurrentLocation.Clone()]),
|
||||||
new Combine([nocheck(), tr.fsIncludeCurrentLocation]),
|
new Combine([nocheck(), tr.fsIncludeCurrentLocation.Clone()]),
|
||||||
true
|
new UIEventSource<boolean>(true)
|
||||||
)
|
).ToggleOnClick()
|
||||||
optionCheckboxes.push(includeLocation);
|
optionCheckboxes.push(includeLocation);
|
||||||
|
|
||||||
const currentLocation = State.state?.locationControl;
|
const currentLocation = State.state?.locationControl;
|
||||||
|
@ -54,7 +46,10 @@ export default class ShareScreen extends UIElement {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (includeL) {
|
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 {
|
} else {
|
||||||
return null;
|
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 currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = State.state.backgroundLayer;
|
||||||
const currentBackground = new VariableUiElement(currentLayer.map(layer => {
|
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([check(), currentBackground]),
|
||||||
new Combine([nocheck(), currentBackground]),
|
new Combine([nocheck(), currentBackground]),
|
||||||
true
|
new UIEventSource<boolean>(true)
|
||||||
)
|
).ToggleOnClick()
|
||||||
optionCheckboxes.push(includeCurrentBackground);
|
optionCheckboxes.push(includeCurrentBackground);
|
||||||
optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => {
|
optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => {
|
||||||
if (includeBG) {
|
if (includeBG) {
|
||||||
|
@ -90,11 +85,11 @@ export default class ShareScreen extends UIElement {
|
||||||
}, [currentLayer]));
|
}, [currentLayer]));
|
||||||
|
|
||||||
|
|
||||||
const includeLayerChoices = new CheckBox(
|
const includeLayerChoices = new Toggle(
|
||||||
new Combine([check(), tr.fsIncludeCurrentLayers]),
|
new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]),
|
||||||
new Combine([nocheck(), tr.fsIncludeCurrentLayers]),
|
new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]),
|
||||||
true
|
new UIEventSource<boolean>(true)
|
||||||
)
|
).ToggleOnClick()
|
||||||
optionCheckboxes.push(includeLayerChoices);
|
optionCheckboxes.push(includeLayerChoices);
|
||||||
|
|
||||||
optionParts.push(includeLayerChoices.isEnabled.map((includeLayerSelection) => {
|
optionParts.push(includeLayerChoices.isEnabled.map((includeLayerSelection) => {
|
||||||
|
@ -120,10 +115,11 @@ export default class ShareScreen extends UIElement {
|
||||||
|
|
||||||
for (const swtch of switches) {
|
for (const swtch of switches) {
|
||||||
|
|
||||||
const checkbox = new CheckBox(
|
const checkbox = new Toggle(
|
||||||
new Combine([check(), Translations.W(swtch.human)]),
|
new Combine([check(), Translations.W(swtch.human.Clone())]),
|
||||||
new Combine([nocheck(), Translations.W(swtch.human)]), !swtch.reverse
|
new Combine([nocheck(), Translations.W(swtch.human.Clone())]),
|
||||||
);
|
new UIEventSource<boolean>(!swtch.reverse)
|
||||||
|
).ToggleOnClick();
|
||||||
optionCheckboxes.push(checkbox);
|
optionCheckboxes.push(checkbox);
|
||||||
optionParts.push(checkbox.isEnabled.map((isEn) => {
|
optionParts.push(checkbox.isEnabled.map((isEn) => {
|
||||||
if (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 url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
|
||||||
|
|
||||||
const host = window.location.host;
|
const host = window.location.host;
|
||||||
|
@ -173,12 +169,10 @@ export default class ShareScreen extends UIElement {
|
||||||
}, optionParts);
|
}, optionParts);
|
||||||
|
|
||||||
|
|
||||||
this.iframe = url.map(url => `<iframe src="${url}" width="100%" height="100%" title="${layout?.title?.txt ?? "MapComplete"} with MapComplete"></iframe>`);
|
const iframeCode = new VariableUiElement(
|
||||||
|
|
||||||
this._iframeCode = new VariableUiElement(
|
|
||||||
url.map((url) => {
|
url.map((url) => {
|
||||||
return `<span class='literal-code iframe-code-block'>
|
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>`
|
</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)) {
|
if ((layoutDefinition !== undefined && State.state?.osmConnection !== undefined)) {
|
||||||
this._editLayout =
|
editLayout =
|
||||||
new VariableUiElement(
|
new VariableUiElement(
|
||||||
State.state.osmConnection.userDetails.map(
|
State.state.osmConnection.userDetails.map(
|
||||||
userDetails => {
|
userDetails => {
|
||||||
|
@ -197,28 +191,24 @@ export default class ShareScreen extends UIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SubtleButton(Svg.pencil_ui(),
|
return new SubtleButton(Svg.pencil_ui(),
|
||||||
new Combine([tr.editThisTheme.SetClass("bold"), "<br/>",
|
new Combine([tr.editThisTheme.Clone().SetClass("bold"), "<br/>",
|
||||||
tr.editThemeDescription]),
|
tr.editThemeDescription.Clone()]),
|
||||||
{url: `./customGenerator.html#${State.state.layoutDefinition}`, newTab: true}).Render();
|
{url: `./customGenerator.html#${State.state.layoutDefinition}`, newTab: true});
|
||||||
|
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._linkStatus = new UIEventSource<string | Translation>("");
|
const linkStatus = new UIEventSource<string | Translation>("");
|
||||||
this.ListenTo(this._linkStatus);
|
const link = new VariableUiElement(
|
||||||
const self = this;
|
url.map((url) => `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`)
|
||||||
this._link = new VariableUiElement(
|
|
||||||
url.map((url) => {
|
|
||||||
return `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`
|
|
||||||
})
|
|
||||||
).onClick(async () => {
|
).onClick(async () => {
|
||||||
|
|
||||||
const shareData = {
|
const shareData = {
|
||||||
title: Translations.W(layout.id)?.InnerRender() ?? "",
|
title: Translations.W(layout.title)?.ConstructElement().innerText ?? "",
|
||||||
text: Translations.W(layout.description)?.InnerRender() ?? "",
|
text: Translations.W(layout.description)?.ConstructElement().innerText ?? "",
|
||||||
url: self._link.data,
|
url: url.data,
|
||||||
}
|
}
|
||||||
|
|
||||||
function rejected() {
|
function rejected() {
|
||||||
|
@ -230,17 +220,17 @@ export default class ShareScreen extends UIElement {
|
||||||
copyText.setSelectionRange(0, 99999); /*For mobile devices*/
|
copyText.setSelectionRange(0, 99999); /*For mobile devices*/
|
||||||
|
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
const copied = tr.copiedToClipboard;
|
const copied = tr.copiedToClipboard.Clone();
|
||||||
copied.SetClass("thanks")
|
copied.SetClass("thanks")
|
||||||
self._linkStatus.setData(copied);
|
linkStatus.setData(copied);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
navigator.share(shareData)
|
navigator.share(shareData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const thx = tr.thanksForSharing;
|
const thx = tr.thanksForSharing.Clone();
|
||||||
thx.SetClass("thanks");
|
thx.SetClass("thanks");
|
||||||
this._linkStatus.setData(thx);
|
linkStatus.setData(thx);
|
||||||
}, rejected)
|
}, rejected)
|
||||||
.catch(rejected)
|
.catch(rejected)
|
||||||
} catch (err) {
|
} 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
|
* 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 {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
import {Tag} from "../../Logic/Tags/Tag";
|
||||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
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<{
|
interface PresetInfo {
|
||||||
description: string | UIElement,
|
description: string | Translation,
|
||||||
name: string | UIElement,
|
name: string | BaseUIElement,
|
||||||
icon: UIElement,
|
icon: BaseUIElement,
|
||||||
tags: Tag[],
|
tags: Tag[],
|
||||||
layerToAddTo: {
|
layerToAddTo: {
|
||||||
layerDef: LayerConfig,
|
layerDef: LayerConfig,
|
||||||
isDisplayed: UIEventSource<boolean>
|
isDisplayed: UIEventSource<boolean>
|
||||||
}
|
}
|
||||||
}>
|
}
|
||||||
= new UIEventSource(undefined);
|
|
||||||
|
|
||||||
private _component: UIElement;
|
export default class SimpleAddUI extends Toggle {
|
||||||
|
|
||||||
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});
|
|
||||||
|
|
||||||
constructor(isShown: UIEventSource<boolean>) {
|
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.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,
|
const confirmButton = new SubtleButton(preset.icon,
|
||||||
new Combine([
|
new Combine([
|
||||||
"<b>",
|
Translations.t.general.add.addNew.Subs({category: preset.name}),
|
||||||
Translations.t.general.add.confirmButton.Subs({category: preset.name}),
|
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
|
||||||
"</b>"])).SetClass("break-words");
|
]).SetClass("flex flex-col")
|
||||||
confirmButton.onClick(
|
).SetClass("font-bold break-words")
|
||||||
this.CreatePoint(preset.tags)
|
.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
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
let tagInfo = "";
|
.onClick(() => State.state.layerControlIsOpened.setData(true))
|
||||||
const csCount = State.state.osmConnection.userDetails.data.csCount;
|
|
||||||
if (csCount > Constants.userJourney.tagsVisibleAt) {
|
const openLayerOrConfirm = new Toggle(
|
||||||
tagInfo = this._confirmPreset.data.tags.map(t => t.asHumanString(csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&");
|
confirmButton,
|
||||||
tagInfo = `<br/>More information about the preset: ${tagInfo}`
|
openLayerControl,
|
||||||
}
|
preset.layerToAddTo.isDisplayed
|
||||||
|
)
|
||||||
|
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset);
|
||||||
|
|
||||||
|
const cancelButton = new SubtleButton(Svg.close_ui(),
|
||||||
|
Translations.t.general.cancel
|
||||||
|
).onClick(cancel )
|
||||||
|
|
||||||
return new Combine([
|
return new Combine([
|
||||||
Translations.t.general.add.confirmIntro.Subs({title: this._confirmPreset.data.name}),
|
Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
|
||||||
State.state.osmConnection.userDetails.data.dryRun ? "<span class='alert'>TESTING - changes won't be saved</span>" : "",
|
State.state.osmConnection.userDetails.data.dryRun ?
|
||||||
confirmButton,
|
Translations.t.general.testing.Clone().SetClass("alert") : undefined ,
|
||||||
this.cancelButton,
|
openLayerOrConfirm,
|
||||||
|
cancelButton,
|
||||||
preset.description,
|
preset.description,
|
||||||
tagInfo
|
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 allButtons = [];
|
||||||
const self = this;
|
|
||||||
for (const layer of State.state.filteredLayers.data) {
|
for (const layer of State.state.filteredLayers.data) {
|
||||||
|
|
||||||
|
if(layer.isDisplayed.data === false && State.state.featureSwitchLayers){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const presets = layer.layerDef.presets;
|
const presets = layer.layerDef.presets;
|
||||||
for (const preset of 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;
|
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||||
let tagInfo = undefined;
|
let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
|
||||||
if (csCount > Constants.userJourney.tagsVisibleAt) {
|
.SetClass("w-12 h-12 block relative");
|
||||||
const presets = preset.tags.map(t => new Combine([t.asHumanString(false, true), " "]).SetClass("subtle break-words"))
|
const presetInfo: PresetInfo = {
|
||||||
tagInfo = new Combine(presets)
|
tags: preset.tags,
|
||||||
|
layerToAddTo: layer,
|
||||||
|
name: preset.title,
|
||||||
|
description: preset.description,
|
||||||
|
icon: icon
|
||||||
}
|
}
|
||||||
const button: UIElement =
|
|
||||||
new SubtleButton(
|
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
|
||||||
icon,
|
button.onClick(() => {
|
||||||
new Combine([
|
selectedPreset.setData(presetInfo)
|
||||||
"<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();
|
|
||||||
}
|
|
||||||
)
|
|
||||||
allButtons.push(button);
|
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 State from "../../State";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import LanguagePicker from "../LanguagePicker";
|
import LanguagePicker from "../LanguagePicker";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
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 {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
|
||||||
export default class ThemeIntroductionPanel extends UIElement {
|
export default class ThemeIntroductionPanel extends VariableUiElement {
|
||||||
private languagePicker: UIElement;
|
|
||||||
|
|
||||||
private readonly loginStatus: UIElement;
|
constructor(isShown: UIEventSource<boolean>) {
|
||||||
private _layout: UIEventSource<LayoutConfig>;
|
|
||||||
|
|
||||||
|
const languagePicker =
|
||||||
|
new VariableUiElement(
|
||||||
|
State.state.layoutToUse.map(layout => LanguagePicker.CreateLanguagePicker(layout.language, Translations.t.general.pickLanguage.Clone()))
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
constructor() {
|
const toTheMap = new SubtleButton(
|
||||||
super(State.state.osmConnection.userDetails);
|
undefined,
|
||||||
this.ListenTo(Locale.language);
|
Translations.t.general.openTheMap.Clone().SetClass("text-xl font-bold w-full text-center")
|
||||||
this.languagePicker = LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language, Translations.t.general.pickLanguage);
|
).onClick(() =>{
|
||||||
this._layout = State.state.layoutToUse;
|
isShown.setData(false)
|
||||||
this.ListenTo(State.state.layoutToUse);
|
}).SetClass("only-on-mobile")
|
||||||
|
|
||||||
const plzLogIn =
|
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(() => {
|
.onClick(() => {
|
||||||
State.state.osmConnection.AttemptLogin()
|
State.state.osmConnection.AttemptLogin()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const welcomeBack = Translations.t.general.welcomeBack;
|
const welcomeBack = Translations.t.general.welcomeBack.Clone();
|
||||||
|
|
||||||
this.loginStatus = new VariableUiElement(
|
|
||||||
State.state.osmConnection.userDetails.map(
|
|
||||||
userdetails => {
|
const loginStatus =
|
||||||
if (State.state.featureSwitchUserbadge.data) {
|
new Toggle(
|
||||||
return "";
|
new Toggle(
|
||||||
}
|
welcomeBack,
|
||||||
return (userdetails.loggedIn ? welcomeBack : plzLogIn).Render();
|
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")
|
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
|
* Handles and updates the user badge
|
||||||
*/
|
*/
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
|
@ -12,133 +9,127 @@ import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import LanguagePicker from "../LanguagePicker";
|
import LanguagePicker from "../LanguagePicker";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Link from "../Base/Link";
|
import Link from "../Base/Link";
|
||||||
|
import Toggle from "../Input/Toggle";
|
||||||
|
import Img from "../Base/Img";
|
||||||
|
|
||||||
export default class UserBadge extends UIElement {
|
export default class UserBadge extends Toggle {
|
||||||
private _userDetails: UIEventSource<UserDetails>;
|
|
||||||
private _logout: UIElement;
|
|
||||||
private _homeButton: UIElement;
|
|
||||||
private _languagePicker: UIElement;
|
|
||||||
|
|
||||||
private _loginButton: UIElement;
|
|
||||||
|
|
||||||
constructor() {
|
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()
|
.Clone()
|
||||||
.SetClass("userbadge-login pt-3 w-full")
|
.SetClass("userbadge-login pt-3 w-full")
|
||||||
.onClick(() => State.state.osmConnection.AttemptLogin());
|
.onClick(() => State.state.osmConnection.AttemptLogin());
|
||||||
this._logout =
|
|
||||||
|
|
||||||
|
const logout =
|
||||||
Svg.logout_svg()
|
Svg.logout_svg()
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
State.state.osmConnection.LogOut();
|
State.state.osmConnection.LogOut();
|
||||||
});
|
});
|
||||||
|
|
||||||
this._userDetails.addCallback(function () {
|
|
||||||
const profilePic = document.getElementById("profile-pic");
|
|
||||||
if (profilePic) {
|
|
||||||
|
|
||||||
profilePic.onload = function () {
|
const userBadge = userDetails.map(user => {
|
||||||
profilePic.style.opacity = "1"
|
{
|
||||||
};
|
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(
|
const linkStyle = "flex items-baseline"
|
||||||
this._userDetails.map((userinfo) => {
|
const languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language) ?? new FixedUiElement(""))
|
||||||
if (userinfo.home) {
|
.SetStyle("width:min-content;");
|
||||||
return Svg.home_ui().Render();
|
|
||||||
|
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 " ";
|
|
||||||
})
|
let dryrun = new FixedUiElement("");
|
||||||
).onClick(() => {
|
if (user.dryRun) {
|
||||||
const home = State.state.osmConnection.userDetails.data?.home;
|
dryrun = new FixedUiElement("TESTING").SetClass("alert");
|
||||||
if (home === undefined) {
|
}
|
||||||
return;
|
|
||||||
|
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
super(
|
||||||
|
new VariableUiElement(userBadge),
|
||||||
InnerRender(): string {
|
loginButton,
|
||||||
const user = this._userDetails.data;
|
State.state.osmConnection.isLoggedIn
|
||||||
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()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,59 +1,46 @@
|
||||||
import {UIElement} from "./UIElement";
|
|
||||||
import Translations from "./i18n/Translations";
|
import Translations from "./i18n/Translations";
|
||||||
import State from "../State";
|
import State from "../State";
|
||||||
|
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||||
|
|
||||||
export default class CenterMessageBox extends UIElement {
|
export default class CenterMessageBox extends VariableUiElement {
|
||||||
|
|
||||||
constructor() {
|
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);
|
[updater.timeout, updater.sufficientlyZoomed, state.locationControl]
|
||||||
this.ListenTo(State.state.layerUpdater.runningQuery);
|
)
|
||||||
this.ListenTo(State.state.layerUpdater.sufficientlyZoomed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static prep(): { innerHtml: string, done: boolean } {
|
super(message.map(toShow => toShow.el))
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lu.runningQuery.data) {
|
this.SetClass("block " +
|
||||||
return {innerHtml: Translations.t.centerMessage.loadingData.Render(), done: false};
|
"rounded-3xl bg-white text-xl font-bold text-center pointer-events-none p-4")
|
||||||
|
this.SetStyle("transition: opacity 750ms linear")
|
||||||
|
|
||||||
}
|
message.addCallbackAndRun(toShow => {
|
||||||
if (!lu.sufficientlyZoomed.data) {
|
const isDone = toShow.isDone ?? false;
|
||||||
return {innerHtml: Translations.t.centerMessage.zoomIn.Render(), done: false};
|
if (isDone) {
|
||||||
} else {
|
this.SetStyle("transition: opacity 750ms linear; opacity: 0")
|
||||||
return {innerHtml: Translations.t.centerMessage.ready.Render(), done: true};
|
} else {
|
||||||
}
|
this.SetStyle("transition: opacity 750ms linear; opacity: 0.75")
|
||||||
}
|
|
||||||
|
|
||||||
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 Combine from "../Base/Combine";
|
||||||
import Translations from "../i18n/Translations";
|
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) {
|
constructor(license: UIEventSource<LicenseInfo>, icon: BaseUIElement) {
|
||||||
super([
|
if (license === undefined) {
|
||||||
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em"),
|
throw "No license source given in the attribution element"
|
||||||
new Combine([
|
}
|
||||||
Translations.W(author).SetClass("block font-bold"),
|
super(
|
||||||
Translations.W((license ?? "") === "undefined" ? "CC0" : (license ?? ""))
|
license.map((license : LicenseInfo) => {
|
||||||
]).SetClass("flex flex-col")
|
|
||||||
]);
|
if (license?.artist === undefined) {
|
||||||
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");
|
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 {UIElement} from "../UIElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import CheckBox from "../Input/CheckBox";
|
import Toggle from "../Input/Toggle";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
import {Tag} from "../../Logic/Tags/Tag";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
|
|
||||||
export default class DeleteImage extends UIElement {
|
export default class DeleteImage extends Toggle {
|
||||||
private readonly key: string;
|
|
||||||
private readonly tags: UIEventSource<any>;
|
|
||||||
|
|
||||||
private readonly isDeletedBadge: UIElement;
|
|
||||||
private readonly deleteDialog: UIElement;
|
|
||||||
|
|
||||||
constructor(key: string, tags: UIEventSource<any>) {
|
constructor(key: string, tags: UIEventSource<any>) {
|
||||||
super(tags);
|
const oldValue = tags.data[key]
|
||||||
this.tags = tags;
|
const isDeletedBadge = Translations.t.image.isDeleted.Clone()
|
||||||
this.key = key;
|
.SetClass("rounded-full p-1")
|
||||||
|
.SetStyle("color:white;background:#ff8c8c")
|
||||||
this.isDeletedBadge = Translations.t.image.isDeleted;
|
.onClick(() => {
|
||||||
|
State.state?.changes?.addTag(tags.data.id, new Tag(key, oldValue), tags);
|
||||||
|
});
|
||||||
|
|
||||||
const deleteButton = Translations.t.image.doDelete.Clone()
|
const deleteButton = Translations.t.image.doDelete.Clone()
|
||||||
.SetClass("block w-full pl-4 pr-4")
|
.SetClass("block w-full pl-4 pr-4")
|
||||||
.SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;")
|
.SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;")
|
||||||
.onClick(() => {
|
.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;");
|
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;");
|
||||||
this.deleteDialog = new CheckBox(
|
const openDelete = Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;")
|
||||||
|
const deleteDialog = new Toggle(
|
||||||
new Combine([
|
new Combine([
|
||||||
deleteButton,
|
deleteButton,
|
||||||
cancelButton
|
cancelButton
|
||||||
]).SetClass("flex flex-col background-black"),
|
]).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 {
|
super(
|
||||||
if(! State.state?.featureSwitchUserbadge?.data){
|
new Toggle(
|
||||||
return "";
|
deleteDialog,
|
||||||
}
|
isDeletedBadge,
|
||||||
|
tags.map(tags => (tags[key] ?? "") !== "")
|
||||||
const value = this.tags.data[this.key];
|
),
|
||||||
if (value === undefined || value === "") {
|
undefined /*Login (and thus editing) is disabled*/,
|
||||||
return this.isDeletedBadge.Render();
|
State.state?.featureSwitchUserbadge ?? new UIEventSource<boolean>(true)
|
||||||
}
|
)
|
||||||
|
this.SetClass("cursor-pointer")
|
||||||
return this.deleteDialog.Render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,39 +1,43 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {SlideShow} from "./SlideShow";
|
import {SlideShow} from "./SlideShow";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import DeleteImage from "./DeleteImage";
|
import DeleteImage from "./DeleteImage";
|
||||||
import {WikimediaImage} from "./WikimediaImage";
|
import {AttributedImage} from "./AttributedImage";
|
||||||
import {ImgurImage} from "./ImgurImage";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
import {MapillaryImage} from "./MapillaryImage";
|
import Img from "../Base/Img";
|
||||||
import {SimpleImageElement} from "./SimpleImageElement";
|
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>) {
|
||||||
|
const uiElements = images.map((imageURLS: { key: string, url: string }[]) => {
|
||||||
constructor(images: UIEventSource<{key: string, url:string}[]>, tags: UIEventSource<any>) {
|
const uiElements: BaseUIElement[] = [];
|
||||||
super(images);
|
|
||||||
const uiElements = images.map((imageURLS: {key: string, url:string}[]) => {
|
|
||||||
const uiElements: UIElement[] = [];
|
|
||||||
for (const url of imageURLS) {
|
for (const url of imageURLS) {
|
||||||
let image = ImageCarousel.CreateImageElement(url.url)
|
let image = ImageCarousel.CreateImageElement(url.url)
|
||||||
if(url.key !== undefined){
|
if (url.key !== undefined) {
|
||||||
image = new Combine([
|
image = new Combine([
|
||||||
image,
|
image,
|
||||||
new DeleteImage(url.key, tags).SetClass("delete-image-marker absolute top-0 left-0 pl-3")
|
new DeleteImage(url.key, tags).SetClass("delete-image-marker absolute top-0 left-0 pl-3")
|
||||||
]).SetClass("relative");
|
]).SetClass("relative");
|
||||||
}
|
}
|
||||||
image
|
image
|
||||||
.SetClass("w-full block")
|
.SetClass("w-full block")
|
||||||
|
.SetStyle("min-width: 50px; background: grey;")
|
||||||
uiElements.push(image);
|
uiElements.push(image);
|
||||||
}
|
}
|
||||||
return uiElements;
|
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.SetClass("block w-full");
|
||||||
this.slideshow.SetClass("w-full");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/***
|
||||||
|
@ -41,23 +45,22 @@ export class ImageCarousel extends UIElement{
|
||||||
* @param url
|
* @param url
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
private static CreateImageElement(url: string): UIElement {
|
private static CreateImageElement(url: string): BaseUIElement {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
let attrSource : ImageAttributionSource = undefined;
|
||||||
if (url.startsWith("File:")) {
|
if (url.startsWith("File:")) {
|
||||||
return new WikimediaImage(url);
|
attrSource = Wikimedia.singleton
|
||||||
} else if (url.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
} else if (url.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||||
const commons = url.substr("https://commons.wikimedia.org/wiki/".length);
|
attrSource = Wikimedia.singleton;
|
||||||
return new WikimediaImage(commons);
|
|
||||||
} else if (url.toLowerCase().startsWith("https://i.imgur.com/")) {
|
} 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/")) {
|
} else if (url.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) {
|
||||||
return new MapillaryImage(url);
|
attrSource = Mapillary.singleton
|
||||||
} else {
|
} else {
|
||||||
return new SimpleImageElement(new UIEventSource<string>(url));
|
return new Img(url);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
return new AttributedImage(url, attrSource)
|
||||||
return this.slideshow.Render();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,210 +1,103 @@
|
||||||
import $ from "jquery"
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import Combine from "../Base/Combine";
|
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 Translations from "../i18n/Translations";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
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 {
|
export class ImageUploadFlow extends Toggle {
|
||||||
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;
|
|
||||||
|
|
||||||
constructor(tags: UIEventSource<any>, imagePrefix: string = "image") {
|
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image") {
|
||||||
super(State.state.osmConnection.userDetails);
|
const uploader = new ImgurUploader(url => {
|
||||||
this._tags = tags;
|
// A file was uploaded - we add it to the tags of the object
|
||||||
this._imagePrefix = imagePrefix;
|
|
||||||
|
|
||||||
this.ListenTo(this._isUploading);
|
const tags = tagsSource.data
|
||||||
this.ListenTo(this._didFail);
|
let key = imagePrefix
|
||||||
this.ListenTo(this._allDone);
|
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,
|
|
||||||
[
|
const licensePicker = new LicensePicker()
|
||||||
{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 t = Translations.t.image;
|
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")
|
||||||
|
|
||||||
this._licensePicker = licensePicker;
|
const fileSelector = new FileSelectorButton(label)
|
||||||
this._selectedLicence = licensePicker.GetValue();
|
fileSelector.GetValue().addCallback(filelist => {
|
||||||
|
if (filelist === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._connectButton = t.pleaseLogin.Clone()
|
console.log("Received images from the user, starting upload")
|
||||||
|
const license = licensePicker.GetValue()?.data ?? "CC0"
|
||||||
|
|
||||||
|
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())
|
.onClick(() => State.state.osmConnection.AttemptLogin())
|
||||||
.SetClass("login-button-friendly");
|
.SetClass("login-button-friendly");
|
||||||
|
super(
|
||||||
}
|
new Toggle(
|
||||||
|
/*We can show the actual upload button!*/
|
||||||
InnerRender(): string {
|
uploadFlow,
|
||||||
|
/* User not logged in*/ pleaseLoginButton,
|
||||||
if(!State.state.featureSwitchUserbadge.data){
|
State.state?.osmConnection?.isLoggedIn
|
||||||
return "";
|
),
|
||||||
}
|
undefined /* Nothing as the user badge is disabled*/,
|
||||||
|
State.state.featureSwitchUserbadge
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {UIElement} from "../UIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import {Utils} from "../../Utils";
|
||||||
import Combine from "../Base/Combine";
|
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(
|
private readonly embeddedElements: UIEventSource<BaseUIElement[]>;
|
||||||
embeddedElements: UIEventSource<UIElement[]>) {
|
|
||||||
super(embeddedElements);
|
constructor(embeddedElements: UIEventSource<BaseUIElement[]>) {
|
||||||
this._embeddedElements = embeddedElements;
|
super()
|
||||||
this._embeddedElements.addCallbackAndRun(elements => {
|
this.embeddedElements =embeddedElements;
|
||||||
for (const element of elements ?? []) {
|
this.SetStyle("scroll-snap-type: x mandatory; overflow-x: scroll")
|
||||||
element.SetClass("slick-carousel-content")
|
}
|
||||||
|
|
||||||
|
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 {
|
elements = Utils.NoNull(elements).map(el => new Combine([el])
|
||||||
return new Combine(
|
.SetClass("block relative ml-1 bg-gray-200 m-1 rounded slideshow-item")
|
||||||
this._embeddedElements.data,
|
.SetStyle("min-width: 150px; width: max-content; height: var(--image-carousel-height);max-height: var(--image-carousel-height);scroll-snap-align: start;")
|
||||||
).SetClass("block slick-carousel")
|
)
|
||||||
.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
Update() {
|
for (const element of elements ?? []) {
|
||||||
super.Update();
|
el.appendChild(element.ConstructElement())
|
||||||
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
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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,28 +1,25 @@
|
||||||
import {InputElement} from "./InputElement";
|
import {InputElement} from "./InputElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import {UIElement} from "../UIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supports multi-input
|
* Supports multi-input
|
||||||
*/
|
*/
|
||||||
export default class CheckBoxes extends InputElement<number[]> {
|
export default class CheckBoxes extends InputElement<number[]> {
|
||||||
|
private static _nextId = 0;
|
||||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
private readonly value: UIEventSource<number[]>
|
||||||
|
private readonly _elements: BaseUIElement[];
|
||||||
|
|
||||||
private readonly value: UIEventSource<number[]>;
|
constructor(elements: BaseUIElement[], value = new UIEventSource<number[]>([])) {
|
||||||
private readonly _elements: UIElement[]
|
super();
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
constructor(elements: UIElement[]) {
|
|
||||||
super(undefined);
|
|
||||||
this._elements = Utils.NoNull(elements);
|
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 {
|
IsValid(ts: number[]): boolean {
|
||||||
return ts !== undefined;
|
return ts !== undefined;
|
||||||
|
|
||||||
|
@ -32,51 +29,70 @@ export default class CheckBoxes extends InputElement<number[]> {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
const el = document.createElement("form")
|
||||||
|
|
||||||
private IdFor(i) {
|
const value = this.value;
|
||||||
return 'checkmark-' + this.id + '-' + i;
|
const elements = this._elements;
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
let inputI = elements[i];
|
||||||
|
const input = document.createElement("input")
|
||||||
|
const id = CheckBoxes._nextId
|
||||||
|
CheckBoxes._nextId++;
|
||||||
|
input.id = "checkbox" + id
|
||||||
|
|
||||||
return `<form id='${this.id}'>${body}</form>`;
|
input.type = "checkbox"
|
||||||
}
|
input.classList.add("p-1","cursor-pointer","m-3","pl-3","mr-0")
|
||||||
|
|
||||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
const label = document.createElement("label")
|
||||||
super.InnerUpdate(htmlElement);
|
label.htmlFor = input.id
|
||||||
const self = this;
|
label.appendChild(inputI.ConstructElement())
|
||||||
|
label.classList.add("block","w-full","p-2","cursor-pointer","bg-red")
|
||||||
|
|
||||||
for (let i = 0; i < this._elements.length; i++) {
|
const wrapper = document.createElement("span")
|
||||||
const el = document.getElementById(this.IdFor(i));
|
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){
|
value.addCallbackAndRun(selectedValues => {
|
||||||
// @ts-ignore
|
if (selectedValues === undefined) {
|
||||||
el.checked = true;
|
return;
|
||||||
}
|
}
|
||||||
|
if (selectedValues.indexOf(i) >= 0) {
|
||||||
|
input.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
el.onchange = () => {
|
|
||||||
const index = self.value.data.indexOf(i);
|
if(input.checked){
|
||||||
// @ts-ignore
|
wrapper.classList.remove("border-gray-400")
|
||||||
if(el.checked && index < 0){
|
wrapper.classList.add("border-black")
|
||||||
self.value.data.push(i);
|
}else{
|
||||||
self.value.ping();
|
wrapper.classList.add("border-gray-400")
|
||||||
}else if(index >= 0){
|
wrapper.classList.remove("border-black")
|
||||||
self.value.data.splice(index,1);
|
}
|
||||||
self.value.ping();
|
|
||||||
|
})
|
||||||
|
|
||||||
|
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 {InputElement} from "./InputElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {Utils} from "../../Utils";
|
|
||||||
|
|
||||||
export default class ColorPicker extends InputElement<string> {
|
export default class ColorPicker extends InputElement<string> {
|
||||||
|
|
||||||
private readonly value: UIEventSource<string>
|
private readonly value: UIEventSource<string>
|
||||||
|
private readonly _element : HTMLElement
|
||||||
constructor(
|
constructor(
|
||||||
value?: UIEventSource<string>
|
value: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.value = value ?? new UIEventSource<string>(undefined);
|
this.value = value ;
|
||||||
const self = this;
|
|
||||||
|
const el = document.createElement("input")
|
||||||
|
this._element = el;
|
||||||
|
|
||||||
|
el.type = "color"
|
||||||
|
|
||||||
this.value.addCallbackAndRun(v => {
|
this.value.addCallbackAndRun(v => {
|
||||||
if(v === undefined){
|
if(v === undefined){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.SetValue(v);
|
el.value =v
|
||||||
});
|
});
|
||||||
|
|
||||||
|
el.oninput = () => {
|
||||||
|
const hex = el.value;
|
||||||
|
value.setData(hex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
InnerRender(): string {
|
return this._element;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GetValue(): UIEventSource<string> {
|
GetValue(): UIEventSource<string> {
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import {InputElement} from "./InputElement";
|
import {InputElement} from "./InputElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {UIElement} from "../UIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class CombinedInputElement<T> extends InputElement<T> {
|
export default class CombinedInputElement<T> extends InputElement<T> {
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
return this._combined.ConstructElement();
|
||||||
|
}
|
||||||
private readonly _a: InputElement<T>;
|
private readonly _a: InputElement<T>;
|
||||||
private readonly _b: UIElement;
|
private readonly _b: BaseUIElement;
|
||||||
private readonly _combined: UIElement;
|
private readonly _combined: BaseUIElement;
|
||||||
public readonly IsSelected: UIEventSource<boolean>;
|
public readonly IsSelected: UIEventSource<boolean>;
|
||||||
|
|
||||||
constructor(a: InputElement<T>, b: InputElement<T>) {
|
constructor(a: InputElement<T>, b: InputElement<T>) {
|
||||||
super();
|
super();
|
||||||
this._a = a;
|
this._a = a;
|
||||||
|
@ -23,11 +25,6 @@ export default class CombinedInputElement<T> extends InputElement<T> {
|
||||||
return this._a.GetValue();
|
return this._a.GetValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this._combined.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
IsValid(t: T): boolean {
|
IsValid(t: T): boolean {
|
||||||
return this._a.IsValid(t);
|
return this._a.IsValid(t);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {InputElement} from "./InputElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,34 +10,29 @@ import Svg from "../../Svg";
|
||||||
*/
|
*/
|
||||||
export default class DirectionInput extends InputElement<string> {
|
export default class DirectionInput extends InputElement<string> {
|
||||||
|
|
||||||
private readonly value: UIEventSource<string>;
|
|
||||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
private readonly value: UIEventSource<string>;
|
||||||
|
|
||||||
constructor(value?: UIEventSource<string>) {
|
constructor(value?: UIEventSource<string>) {
|
||||||
super();
|
super();
|
||||||
this.dumbMode = false;
|
|
||||||
this.value = value ?? new UIEventSource<string>(undefined);
|
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> {
|
GetValue(): UIEventSource<string> {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
IsValid(str: string): boolean {
|
||||||
return new Combine([
|
const t = Number(str);
|
||||||
`<div id="direction-leaflet-div-${this.id}" style="width:100%;height: 100%;position: absolute;top:0;left:0;border-radius:100%;"></div>`,
|
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(
|
Svg.direction_svg().SetStyle(
|
||||||
`position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`)
|
`position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`)
|
||||||
.SetClass("direction-svg"),
|
.SetClass("direction-svg"),
|
||||||
|
@ -44,11 +40,21 @@ export default class DirectionInput extends InputElement<string> {
|
||||||
"position: absolute;top: 0;left: 0;width: 100%;height: 100%;")
|
"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")
|
.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) {
|
private RegisterTriggers(htmlElement: HTMLElement) {
|
||||||
super.InnerUpdate(htmlElement);
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
function onPosChange(x: number, y: number) {
|
function onPosChange(x: number, y: number) {
|
||||||
|
@ -79,19 +85,16 @@ export default class DirectionInput extends InputElement<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlElement.onmouseup = (ev) => {
|
htmlElement.onmouseup = (ev) => {
|
||||||
isDown = false; ev.preventDefault();
|
isDown = false;
|
||||||
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlElement.onmousemove = (ev: MouseEvent) => {
|
htmlElement.onmousemove = (ev: MouseEvent) => {
|
||||||
if (isDown) {
|
if (isDown) {
|
||||||
onPosChange(ev.clientX, ev.clientY);
|
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 {InputElement} from "./InputElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class DropDown<T> extends InputElement<T> {
|
export class DropDown<T> extends InputElement<T> {
|
||||||
|
|
||||||
private readonly _label: UIElement;
|
private static _nextDropdownId = 0;
|
||||||
private readonly _values: { value: T; shown: UIElement }[];
|
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
|
||||||
|
private readonly _element: HTMLElement;
|
||||||
|
|
||||||
private readonly _value: UIEventSource<T>;
|
private readonly _value: UIEventSource<T>;
|
||||||
|
private readonly _values: { value: T; shown: string | BaseUIElement }[];
|
||||||
|
|
||||||
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
constructor(label: string | BaseUIElement,
|
||||||
private readonly _label_class: string;
|
values: { value: T, shown: string | BaseUIElement }[],
|
||||||
private readonly _select_class: string;
|
|
||||||
private _form_style: string;
|
|
||||||
|
|
||||||
constructor(label: string | UIElement,
|
|
||||||
values: { value: T, shown: string | UIElement }[],
|
|
||||||
value: UIEventSource<T> = undefined,
|
value: UIEventSource<T> = undefined,
|
||||||
label_class: string = "",
|
options?: {
|
||||||
select_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)
|
|
||||||
}
|
}
|
||||||
}
|
) {
|
||||||
);
|
super();
|
||||||
for (const v of this._values) {
|
value = value ?? new UIEventSource<T>(undefined)
|
||||||
this.ListenTo(v.shown._source);
|
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> {
|
GetValue(): UIEventSource<T> {
|
||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
IsValid(t: T): boolean {
|
IsValid(t: T): boolean {
|
||||||
for (const value of this._values) {
|
for (const value of this._values) {
|
||||||
if (value.value === t) {
|
if (value.value === t) {
|
||||||
|
@ -54,44 +96,8 @@ export class DropDown<T> extends InputElement<T> {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
InnerRender(): string {
|
return this._element;
|
||||||
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 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 {InputElement} from "./InputElement";
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import Translations from "../i18n/Translations";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class FixedInputElement<T> extends InputElement<T> {
|
export class FixedInputElement<T> extends InputElement<T> {
|
||||||
private readonly rendering: UIElement;
|
|
||||||
private readonly value: UIEventSource<T>;
|
private readonly value: UIEventSource<T>;
|
||||||
public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
private readonly _comparator: (t0: T, t1: T) => boolean;
|
private readonly _comparator: (t0: T, t1: T) => boolean;
|
||||||
|
|
||||||
constructor(rendering: UIElement | string,
|
private readonly _el : HTMLElement;
|
||||||
|
|
||||||
|
constructor(rendering: BaseUIElement | string,
|
||||||
value: T,
|
value: T,
|
||||||
comparator: ((t0: T, t1: T) => boolean ) = undefined) {
|
comparator: ((t0: T, t1: T) => boolean ) = undefined) {
|
||||||
super(undefined);
|
super();
|
||||||
this._comparator = comparator ?? ((t0, t1) => t0 == t1);
|
this._comparator = comparator ?? ((t0, t1) => t0 == t1);
|
||||||
this.value = new UIEventSource<T>(value);
|
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(() => {
|
this.onClick(() => {
|
||||||
self.IsSelected.setData(true)
|
selected.setData(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
return this._el;
|
||||||
|
}
|
||||||
|
|
||||||
GetValue(): UIEventSource<T> {
|
GetValue(): UIEventSource<T> {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
InnerRender(): string {
|
|
||||||
return this.rendering.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
IsValid(t: T): boolean {
|
IsValid(t: T): boolean {
|
||||||
return this._comparator(t, this.value.data);
|
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 {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 GetValue() : UIEventSource<T>;
|
||||||
abstract IsSelected: UIEventSource<boolean>;
|
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