forked from MapComplete/MapComplete
More refactoring, stabilizing rotation and direction_gradient
This commit is contained in:
parent
5fec108ba2
commit
778044d0fb
45 changed files with 656 additions and 640 deletions
65
UI/BigComponents/Attribution.ts
Normal file
65
UI/BigComponents/Attribution.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Link from "../Base/Link";
|
||||
import Svg from "../../Svg";
|
||||
import Combine from "../Base/Combine";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UserDetails} from "../../Logic/Osm/OsmConnection";
|
||||
import Constants from "../../Models/Constants";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import Loc from "../../Models/Loc";
|
||||
import LeafletMap from "../../Models/LeafletMap";
|
||||
|
||||
export default class Attribution extends UIElement {
|
||||
|
||||
private readonly _location: UIEventSource<Loc>;
|
||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||
private readonly _userDetails: UIEventSource<UserDetails>;
|
||||
private readonly _leafletMap: UIEventSource<LeafletMap>;
|
||||
|
||||
constructor(location: UIEventSource<Loc>,
|
||||
userDetails: UIEventSource<UserDetails>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>,
|
||||
leafletMap: UIEventSource<L.Map>) {
|
||||
super(location);
|
||||
this._layoutToUse = layoutToUse;
|
||||
this.ListenTo(layoutToUse);
|
||||
this._userDetails = userDetails;
|
||||
this._leafletMap = leafletMap;
|
||||
this.ListenTo(userDetails);
|
||||
this._location = location;
|
||||
this.SetClass("map-attribution");
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const location : Loc = this._location.data;
|
||||
const userDetails = this._userDetails.data;
|
||||
|
||||
const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true);
|
||||
const reportBug = new Link(Svg.bug_img, "https://github.com/pietervdvn/MapComplete/issues", true);
|
||||
|
||||
const layoutId = this._layoutToUse.data.id;
|
||||
const osmChaLink = `https://osmcha.org/?filters=%7B%22comment%22%3A%5B%7B%22label%22%3A%22%23${layoutId}%22%2C%22value%22%3A%22%23${layoutId}%22%7D%5D%2C%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22MapComplete%22%2C%22value%22%3A%22MapComplete%22%7D%5D%7D`
|
||||
const stats = new Link(Svg.statistics_img, osmChaLink, true)
|
||||
let editHere: (UIElement | string) = "";
|
||||
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);
|
||||
}
|
||||
let editWithJosm: (UIElement | string) = ""
|
||||
if (location !== undefined &&
|
||||
this._leafletMap.data !== undefined &&
|
||||
userDetails.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked) {
|
||||
const bounds = this._leafletMap.data.getBounds();
|
||||
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}`
|
||||
editWithJosm = new Link(Svg.josm_logo_img, josmLink, true);
|
||||
}
|
||||
return new Combine([mapComplete, reportBug, " | ", stats, " | ", editHere, editWithJosm]).Render();
|
||||
}
|
||||
|
||||
|
||||
}
|
38
UI/BigComponents/BackgroundSelector.ts
Normal file
38
UI/BigComponents/BackgroundSelector.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import Translations from "../i18n/Translations";
|
||||
import State from "../../State";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {BaseLayer} from "../../Models/BaseLayer";
|
||||
|
||||
export default class BackgroundSelector extends UIElement {
|
||||
|
||||
private _dropdown: UIElement;
|
||||
private readonly _availableLayers: UIEventSource<BaseLayer[]>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const self = this;
|
||||
this._availableLayers = State.state.availableBackgroundLayers;
|
||||
this._availableLayers.addCallbackAndRun(available => self.CreateDropDown(available));
|
||||
}
|
||||
|
||||
private CreateDropDown(available) {
|
||||
if(available.length === 0){
|
||||
return;
|
||||
}
|
||||
|
||||
const baseLayers: { value: BaseLayer, shown: string }[] = [];
|
||||
for (const i in available) {
|
||||
const layer: BaseLayer = available[i];
|
||||
baseLayers.push({value: layer, shown: layer.name ?? "id:" + layer.id});
|
||||
}
|
||||
|
||||
this._dropdown = new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.backgroundLayer);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._dropdown.Render();
|
||||
}
|
||||
|
||||
}
|
78
UI/BigComponents/Basemap.ts
Normal file
78
UI/BigComponents/Basemap.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import * as L from "leaflet"
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {UIElement} from "../UIElement";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
|
||||
export class Basemap {
|
||||
|
||||
|
||||
public readonly map: L.Map;
|
||||
|
||||
constructor(leafletElementId: string,
|
||||
location: UIEventSource<Loc>,
|
||||
currentLayer: UIEventSource<BaseLayer>,
|
||||
lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
||||
extraAttribution: UIElement) {
|
||||
this.map = L.map(leafletElementId, {
|
||||
center: [location.data.lat ?? 0, location.data.lon ?? 0],
|
||||
zoom: location.data.zoom ?? 2,
|
||||
layers: [currentLayer.data.layer],
|
||||
});
|
||||
|
||||
L.control.scale(
|
||||
{
|
||||
position: 'topright',
|
||||
}
|
||||
).addTo(this.map)
|
||||
|
||||
|
||||
// Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then
|
||||
// We give a bit of leeway for people on the edges
|
||||
// Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/
|
||||
this.map.setMaxBounds(
|
||||
[[-100, -200], [100, 200]]
|
||||
);
|
||||
this.map.attributionControl.setPrefix(
|
||||
extraAttribution.Render() + " | <a href='https://osm.org'>OpenStreetMap</a>");
|
||||
|
||||
this.map.zoomControl.setPosition("bottomright");
|
||||
const self = this;
|
||||
|
||||
let previousLayer = currentLayer.data;
|
||||
currentLayer.addCallbackAndRun(layer => {
|
||||
if (layer === previousLayer) {
|
||||
return;
|
||||
}
|
||||
if (previousLayer !== undefined) {
|
||||
self.map.removeLayer(previousLayer.layer);
|
||||
}
|
||||
previousLayer = layer;
|
||||
self.map.addLayer(layer.layer);
|
||||
})
|
||||
|
||||
|
||||
this.map.on("moveend", function () {
|
||||
location.data.zoom = self.map.getZoom();
|
||||
location.data.lat = self.map.getCenter().lat;
|
||||
location.data.lon = self.map.getCenter().lng;
|
||||
location.ping();
|
||||
});
|
||||
|
||||
this.map.on("click", function (e) {
|
||||
// @ts-ignore
|
||||
lastClickLocation.setData({lat: e.latlng.lat, lon: e.latlng.lng})
|
||||
});
|
||||
|
||||
this.map.on("contextmenu", function (e) {
|
||||
// @ts-ignore
|
||||
lastClickLocation.setData({lat: e.latlng.lat, lon: e.latlng.lng});
|
||||
// @ts-ignore
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
80
UI/BigComponents/FullWelcomePaneWithTabs.ts
Normal file
80
UI/BigComponents/FullWelcomePaneWithTabs.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import State from "../../State";
|
||||
import WelcomeMessage from "./WelcomeMessage";
|
||||
import * as personal from "../../assets/themes/personalLayout/personalLayout.json";
|
||||
import PersonalLayersPanel from "./PersonalLayersPanel";
|
||||
import Svg from "../../Svg";
|
||||
import Translations from "../i18n/Translations";
|
||||
import ShareScreen from "./ShareScreen";
|
||||
import MoreScreen from "./MoreScreen";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Constants from "../../Models/Constants";
|
||||
import Combine from "../Base/Combine";
|
||||
import Locale from "../i18n/Locale";
|
||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
|
||||
export default class FullWelcomePaneWithTabs extends UIElement {
|
||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||
private readonly _userDetails: UIEventSource<UserDetails>;
|
||||
|
||||
private readonly _component: UIElement;
|
||||
|
||||
constructor() {
|
||||
super(State.state.layoutToUse);
|
||||
this._layoutToUse = State.state.layoutToUse;
|
||||
this._userDetails = State.state.osmConnection.userDetails;
|
||||
|
||||
|
||||
const layoutToUse = this._layoutToUse.data;
|
||||
let welcome: UIElement = new WelcomeMessage();
|
||||
if (layoutToUse.id === personal.id) {
|
||||
welcome = new PersonalLayersPanel();
|
||||
}
|
||||
const tabs = [
|
||||
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
|
||||
{
|
||||
header: Svg.osm_logo_img,
|
||||
content: Translations.t.general.openStreetMapIntro as UIElement
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
if (State.state.featureSwitchShareScreen.data) {
|
||||
tabs.push({header: Svg.share_img, content: new ShareScreen()});
|
||||
}
|
||||
|
||||
if (State.state.featureSwitchMoreQuests.data) {
|
||||
|
||||
tabs.push({
|
||||
header: Svg.add_img,
|
||||
content: new MoreScreen()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
tabs.push({
|
||||
header: Svg.help,
|
||||
content: new VariableUiElement(this._userDetails.map(userdetails => {
|
||||
if (userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) {
|
||||
return ""
|
||||
}
|
||||
return new Combine([Translations.t.general.aboutMapcomplete, "<br/>Version " + Constants.vNumber]).Render();
|
||||
}, [Locale.language]))
|
||||
}
|
||||
);
|
||||
|
||||
this._component = new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab)
|
||||
.ListenTo(this._userDetails);
|
||||
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._component.Render();
|
||||
|
||||
}
|
||||
|
||||
}
|
33
UI/BigComponents/LayerControlPanel.ts
Normal file
33
UI/BigComponents/LayerControlPanel.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import State from "../../State";
|
||||
import BackgroundSelector from "./BackgroundSelector";
|
||||
import LayerSelection from "./LayerSelection";
|
||||
import Combine from "../Base/Combine";
|
||||
|
||||
export default class LayerControlPanel extends UIElement{
|
||||
private readonly _panel: UIElement;
|
||||
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let layerControlPanel: UIElement = undefined;
|
||||
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
||||
layerControlPanel = new BackgroundSelector();
|
||||
layerControlPanel.SetStyle("margin:1em");
|
||||
layerControlPanel.onClick(() => {
|
||||
});
|
||||
}
|
||||
|
||||
if (State.state.filteredLayers.data.length > 1) {
|
||||
const layerSelection = new LayerSelection();
|
||||
layerSelection.onClick(() => { });
|
||||
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]);
|
||||
}
|
||||
this._panel = layerControlPanel;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._panel.Render();
|
||||
}
|
||||
|
||||
}
|
59
UI/BigComponents/LayerSelection.ts
Normal file
59
UI/BigComponents/LayerSelection.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import State from "../../State";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export default class LayerSelection extends UIElement {
|
||||
|
||||
private readonly _checkboxes: UIElement[];
|
||||
|
||||
constructor() {
|
||||
super(undefined);
|
||||
this._checkboxes = [];
|
||||
|
||||
for (const layer of State.state.filteredLayers.data) {
|
||||
const leafletStyle = layer.layerDef.GenerateLeafletStyle(
|
||||
new UIEventSource<any>({id: "node/-1"}),
|
||||
false)
|
||||
const leafletHtml = leafletStyle.icon.html;
|
||||
const icon =
|
||||
new FixedUiElement(leafletHtml.Render())
|
||||
.SetClass("single-layer-selection-toggle")
|
||||
let iconUnselected: UIElement = new FixedUiElement(leafletHtml.Render())
|
||||
.SetClass("single-layer-selection-toggle")
|
||||
.SetStyle("opacity:0.2;");
|
||||
|
||||
const name = Translations.WT(layer.layerDef.name).Clone()
|
||||
.SetStyle("font-size:large;margin-left: 0.5em;");
|
||||
|
||||
|
||||
const zoomStatus = new VariableUiElement(State.state.locationControl.map(location => {
|
||||
if (location.zoom < layer.layerDef.minzoom) {
|
||||
return Translations.t.general.zoomInToSeeThisLayer
|
||||
.SetClass("alert")
|
||||
.SetStyle("display: block ruby;width:min-content;")
|
||||
.Render();
|
||||
}
|
||||
return ""
|
||||
}))
|
||||
const style = "display:flex;align-items:center;"
|
||||
this._checkboxes.push(new CheckBox(
|
||||
new Combine([icon, name, zoomStatus]).SetStyle(style),
|
||||
new Combine([iconUnselected, "<del>", name, "</del>", zoomStatus]).SetStyle(style),
|
||||
layer.isDisplayed)
|
||||
.SetStyle("margin:0.3em;")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine(this._checkboxes)
|
||||
.SetStyle("display:flex;flex-direction:column;")
|
||||
.Render();
|
||||
}
|
||||
|
||||
}
|
119
UI/BigComponents/MoreScreen.ts
Normal file
119
UI/BigComponents/MoreScreen.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import {VerticalCombine} from "../Base/VerticalCombine";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Translations from "../i18n/Translations";
|
||||
import * as personal from "../../assets/themes/personalLayout/personalLayout.json"
|
||||
import Constants from "../../Models/Constants";
|
||||
|
||||
export default class MoreScreen extends UIElement {
|
||||
|
||||
|
||||
constructor() {
|
||||
super(State.state.locationControl);
|
||||
this.ListenTo(State.state.osmConnection.userDetails);
|
||||
this.ListenTo(State.state.installedThemes);
|
||||
}
|
||||
|
||||
private createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined) {
|
||||
if (layout === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if(layout.id === undefined){
|
||||
console.error("ID is undefined for layout",layout);
|
||||
return undefined;
|
||||
}
|
||||
if (layout.hideFromOverview) {
|
||||
const pref = State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled");
|
||||
this.ListenTo(pref);
|
||||
if (pref.data !== "true") {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (layout.id === State.state.layoutToUse.data.id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const currentLocation = State.state.locationControl.data;
|
||||
let linkText =
|
||||
`./${layout.id.toLowerCase()}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
|
||||
|
||||
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
|
||||
linkText = `./index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
|
||||
}
|
||||
|
||||
if (customThemeDefinition) {
|
||||
linkText = `./index.html?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}`
|
||||
|
||||
}
|
||||
|
||||
let description = Translations.W(layout.shortDescription);
|
||||
if (description !== undefined) {
|
||||
description = new Combine(["<br/>", description]);
|
||||
}
|
||||
return new SubtleButton(layout.icon,
|
||||
new Combine([
|
||||
"<b>",
|
||||
Translations.W(layout.title),
|
||||
"</b>",
|
||||
description ?? "",
|
||||
]), {url: linkText, newTab: false});
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
const tr = Translations.t.general.morescreen;
|
||||
|
||||
const els: UIElement[] = []
|
||||
|
||||
els.push(new VariableUiElement(
|
||||
State.state.osmConnection.userDetails.map(userDetails => {
|
||||
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
|
||||
return tr.requestATheme.Render();
|
||||
}
|
||||
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme, {
|
||||
url: "./customGenerator.html",
|
||||
newTab: false
|
||||
}).Render();
|
||||
})
|
||||
));
|
||||
|
||||
|
||||
for (const k in AllKnownLayouts.allSets) {
|
||||
const layout : LayoutConfig = AllKnownLayouts.allSets[k];
|
||||
if (k === personal.id) {
|
||||
if (State.state.osmConnection.userDetails.data.csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (layout.id !== k) {
|
||||
continue; // This layout was added multiple time due to an uppercase
|
||||
}
|
||||
els.push(this.createLinkButton(layout));
|
||||
}
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return new VerticalCombine([
|
||||
tr.intro,
|
||||
new VerticalCombine(els),
|
||||
tr.streetcomplete
|
||||
]).Render();
|
||||
}
|
||||
|
||||
}
|
132
UI/BigComponents/PersonalLayersPanel.ts
Normal file
132
UI/BigComponents/PersonalLayersPanel.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import * as personal from "../../assets/themes/personalLayout/personalLayout.json"
|
||||
export default class PersonalLayersPanel extends UIElement {
|
||||
private checkboxes: UIElement[] = [];
|
||||
|
||||
constructor() {
|
||||
super(State.state.favouriteLayers);
|
||||
this.ListenTo(State.state.osmConnection.userDetails);
|
||||
|
||||
this.UpdateView([]);
|
||||
const self = this;
|
||||
State.state.installedThemes.addCallback(extraThemes => {
|
||||
self.UpdateView(extraThemes.map(layout => layout.layout));
|
||||
self.Update();
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private UpdateView(extraThemes: LayoutConfig[]) {
|
||||
this.checkboxes = [];
|
||||
const favs = State.state.favouriteLayers.data ?? [];
|
||||
const controls = new Map<string, UIEventSource<boolean>>();
|
||||
const allLayouts = AllKnownLayouts.layoutsList.concat(extraThemes);
|
||||
for (const layout of allLayouts) {
|
||||
if (layout.id === personal.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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 ?? ""
|
||||
]).SetStyle("background: #eee; display: block; padding: 0.5em; border-radius:0.5em; overflow:auto;")
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
State.state.favouriteLayers.addCallback((layers) => {
|
||||
for (const layerId of layers) {
|
||||
controls[layerId]?.setData(true);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const t = Translations.t.favourite;
|
||||
const userDetails = State.state.osmConnection.userDetails.data;
|
||||
if(!userDetails.loggedIn){
|
||||
return t.loginNeeded.Render();
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
t.panelIntro,
|
||||
...this.checkboxes
|
||||
]).Render();
|
||||
}
|
||||
|
||||
|
||||
}
|
80
UI/BigComponents/SearchAndGo.ts
Normal file
80
UI/BigComponents/SearchAndGo.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import Locale from "../i18n/Locale";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import {Geocoding} from "../../Logic/Osm/Geocoding";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export default class SearchAndGo extends UIElement {
|
||||
|
||||
private _placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
|
||||
private _searchField = new TextField({
|
||||
placeholder: new VariableUiElement(
|
||||
this._placeholder.map(uiElement => uiElement.InnerRender(), [Locale.language])
|
||||
),
|
||||
value: new UIEventSource<string>("")
|
||||
}
|
||||
);
|
||||
|
||||
private _foundEntries = new UIEventSource([]);
|
||||
private _goButton = Svg.search_ui().SetClass('search-go');
|
||||
|
||||
constructor() {
|
||||
super(undefined);
|
||||
this.ListenTo(this._foundEntries);
|
||||
|
||||
const self = this;
|
||||
this._searchField.enterPressed.addCallback(() => {
|
||||
self.RunSearch();
|
||||
});
|
||||
|
||||
this._goButton.onClick(function () {
|
||||
self.RunSearch();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._searchField.Render() +
|
||||
this._goButton.Render();
|
||||
|
||||
}
|
||||
|
||||
// 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 bb = result[0].boundingbox;
|
||||
const bounds: [[number, number], [number, number]] = [
|
||||
[bb[0], bb[2]],
|
||||
[bb[1], bb[3]]
|
||||
]
|
||||
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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
38
UI/BigComponents/ShareButton.ts
Normal file
38
UI/BigComponents/ShareButton.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
|
||||
export default class ShareButton extends UIElement{
|
||||
private _embedded: UIElement;
|
||||
private _shareData: { text: string; title: string; url: string };
|
||||
|
||||
constructor(embedded: UIElement, shareData: {
|
||||
text: string,
|
||||
title: string,
|
||||
url: string
|
||||
}) {
|
||||
super();
|
||||
this._embedded = embedded;
|
||||
this._shareData = shareData;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return `<button type="button" class="share-button" id="${this.id}">${this._embedded.Render()}</button>`
|
||||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self= this;
|
||||
htmlElement.addEventListener('click', () => {
|
||||
if (navigator.share) {
|
||||
navigator.share(self._shareData).then(() => {
|
||||
console.log('Thanks for sharing!');
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(`Couldn't share because of`, err.message);
|
||||
});
|
||||
} else {
|
||||
console.log('web share not supported');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
268
UI/BigComponents/ShareScreen.ts
Normal file
268
UI/BigComponents/ShareScreen.ts
Normal file
|
@ -0,0 +1,268 @@
|
|||
import {VerticalCombine} from "../Base/VerticalCombine";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import Svg from "../../Svg";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import State from "../../State";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Constants from "../../Models/Constants";
|
||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||
|
||||
export default class ShareScreen extends UIElement {
|
||||
private readonly _options: UIElement;
|
||||
private readonly _iframeCode: UIElement;
|
||||
public iframe: UIEventSource<string>;
|
||||
private readonly _link: UIElement;
|
||||
private readonly _linkStatus: UIEventSource<string | UIElement>;
|
||||
private readonly _editLayout: UIElement;
|
||||
|
||||
constructor(layout: LayoutConfig = undefined, layoutDefinition: string = undefined) {
|
||||
super(undefined)
|
||||
layout = layout ?? State.state?.layoutToUse?.data;
|
||||
layoutDefinition = layoutDefinition ?? State.state?.layoutDefinition;
|
||||
const tr = Translations.t.general.sharescreen;
|
||||
|
||||
const optionCheckboxes: UIElement[] = []
|
||||
const optionParts: (UIEventSource<string>)[] = [];
|
||||
|
||||
function check() {
|
||||
return Svg.checkmark_svg().SetStyle("width: 1.5em; display:inline-block;");
|
||||
}
|
||||
|
||||
function nocheck() {
|
||||
return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;");
|
||||
}
|
||||
|
||||
const includeLocation = new CheckBox(
|
||||
new Combine([check(), tr.fsIncludeCurrentLocation]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLocation]),
|
||||
true
|
||||
)
|
||||
optionCheckboxes.push(includeLocation);
|
||||
|
||||
const currentLocation = State.state?.locationControl;
|
||||
|
||||
optionParts.push(includeLocation.isEnabled.map((includeL) => {
|
||||
if (currentLocation === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (includeL) {
|
||||
return `z=${currentLocation.data.zoom}&lat=${currentLocation.data.lat}&lon=${currentLocation.data.lon}`
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [currentLocation]));
|
||||
|
||||
|
||||
function fLayerToParam(flayer: {isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig}) {
|
||||
if (flayer.isDisplayed.data) {
|
||||
return null; // Being displayed is the default
|
||||
}
|
||||
return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
|
||||
}
|
||||
|
||||
|
||||
if (State.state !== undefined) {
|
||||
|
||||
const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = State.state.backgroundLayer;
|
||||
const currentBackground = new VariableUiElement(currentLayer.map(layer => {
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}).Render();
|
||||
}));
|
||||
const includeCurrentBackground = new CheckBox(
|
||||
new Combine([check(), currentBackground]),
|
||||
new Combine([nocheck(), currentBackground]),
|
||||
true
|
||||
)
|
||||
optionCheckboxes.push(includeCurrentBackground);
|
||||
optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => {
|
||||
if (includeBG) {
|
||||
return "background=" + currentLayer.data.id
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [currentLayer]));
|
||||
|
||||
|
||||
const includeLayerChoices = new CheckBox(
|
||||
new Combine([check(), tr.fsIncludeCurrentLayers]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLayers]),
|
||||
true
|
||||
)
|
||||
optionCheckboxes.push(includeLayerChoices);
|
||||
|
||||
optionParts.push(includeLayerChoices.isEnabled.map((includeLayerSelection) => {
|
||||
if (includeLayerSelection) {
|
||||
return Utils.NoNull(State.state.filteredLayers.data.map(fLayerToParam)).join("&")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, State.state.filteredLayers.data.map((flayer) => flayer.isDisplayed)));
|
||||
|
||||
}
|
||||
|
||||
const switches = [
|
||||
{urlName: "fs-userbadge", human: tr.fsUserbadge},
|
||||
{urlName: "fs-search", human: tr.fsSearch},
|
||||
{urlName: "fs-welcome-message", human: tr.fsWelcomeMessage},
|
||||
{urlName: "fs-layers", human: tr.fsLayers},
|
||||
{urlName: "layer-control-toggle", human: tr.fsLayerControlToggle, reverse: true},
|
||||
{urlName: "fs-add-new", human: tr.fsAddNew},
|
||||
{urlName: "fs-geolocation", human: tr.fsGeolocation},
|
||||
]
|
||||
|
||||
|
||||
for (const swtch of switches) {
|
||||
|
||||
const checkbox = new CheckBox(
|
||||
new Combine([check(), Translations.W(swtch.human)]),
|
||||
new Combine([nocheck(), Translations.W(swtch.human)]), !swtch.reverse
|
||||
);
|
||||
optionCheckboxes.push(checkbox);
|
||||
optionParts.push(checkbox.isEnabled.map((isEn) => {
|
||||
if (isEn) {
|
||||
if(swtch.reverse){
|
||||
return `${swtch.urlName}=true`
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
if(swtch.reverse){
|
||||
return null;
|
||||
}
|
||||
return `${swtch.urlName}=false`
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
this._options = new VerticalCombine(optionCheckboxes)
|
||||
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
|
||||
|
||||
const host = window.location.host;
|
||||
let literalText = `https://${host}/${layout.id.toLowerCase()}.html`
|
||||
|
||||
const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data)));
|
||||
|
||||
let hash = "";
|
||||
if (layoutDefinition !== undefined) {
|
||||
literalText = `https://${host}/index.html`
|
||||
if (layout.id.startsWith("wiki:")) {
|
||||
parts.push("userlayout=" + encodeURIComponent(layout.id))
|
||||
} else {
|
||||
hash = ("#" + layoutDefinition)
|
||||
parts.push("userlayout=true");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (parts.length === 0) {
|
||||
return literalText + hash;
|
||||
}
|
||||
|
||||
return literalText + "?" + parts.join("&") + hash;
|
||||
}, optionParts);
|
||||
|
||||
|
||||
this.iframe = url.map(url => `<iframe src="${url}" width="100%" height="100%" title="${layout?.title?.txt ?? "MapComplete"} with MapComplete"></iframe>`);
|
||||
|
||||
this._iframeCode = new VariableUiElement(
|
||||
url.map((url) => {
|
||||
return `<span class='literal-code iframe-code-block'>
|
||||
<iframe src="${url}" width="100%" height="100%" title="${layout.title?.txt ?? "MapComplete"} with MapComplete"></iframe>
|
||||
</span>`
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
this._editLayout = new FixedUiElement("");
|
||||
if ((layoutDefinition !== undefined && State.state?.osmConnection !== undefined)) {
|
||||
this._editLayout =
|
||||
new VariableUiElement(
|
||||
State.state.osmConnection.userDetails.map(
|
||||
userDetails => {
|
||||
if (userDetails.csCount <= Constants.userJourney.themeGeneratorReadOnlyUnlock) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return new SubtleButton(Svg.pencil_ui(),
|
||||
new Combine([tr.editThisTheme.SetClass("bold"), "<br/>",
|
||||
tr.editThemeDescription]),
|
||||
{url: `./customGenerator.html#${State.state.layoutDefinition}`, newTab: true}).Render();
|
||||
|
||||
}
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
this._linkStatus = new UIEventSource<string | Translation>("");
|
||||
this.ListenTo(this._linkStatus);
|
||||
const self = this;
|
||||
this._link = new VariableUiElement(
|
||||
url.map((url) => {
|
||||
return `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`
|
||||
})
|
||||
).onClick(async () => {
|
||||
|
||||
const shareData = {
|
||||
title: Translations.W(layout.id)?.InnerRender() ?? "",
|
||||
text: Translations.W(layout.description)?.InnerRender() ?? "",
|
||||
url: self._link.data,
|
||||
}
|
||||
|
||||
function rejected() {
|
||||
const copyText = document.getElementById("code-link--copyable");
|
||||
|
||||
// @ts-ignore
|
||||
copyText.select();
|
||||
// @ts-ignore
|
||||
copyText.setSelectionRange(0, 99999); /*For mobile devices*/
|
||||
|
||||
document.execCommand("copy");
|
||||
const copied = tr.copiedToClipboard;
|
||||
copied.SetClass("thanks")
|
||||
self._linkStatus.setData(copied);
|
||||
}
|
||||
|
||||
try {
|
||||
navigator.share(shareData)
|
||||
.then(() => {
|
||||
const thx = tr.thanksForSharing;
|
||||
thx.SetClass("thanks");
|
||||
this._linkStatus.setData(thx);
|
||||
}, rejected)
|
||||
.catch(rejected)
|
||||
} catch (err) {
|
||||
rejected();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
205
UI/BigComponents/SimpleAddUI.ts
Normal file
205
UI/BigComponents/SimpleAddUI.ts
Normal file
|
@ -0,0 +1,205 @@
|
|||
/**
|
||||
* 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 {Tag, TagUtils} from "../../Logic/Tags";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Svg from "../../Svg";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Constants from "../../Models/Constants";
|
||||
|
||||
export default class SimpleAddUI extends UIElement {
|
||||
private readonly _addButtons: UIElement[];
|
||||
|
||||
private _loginButton : UIElement;
|
||||
|
||||
private _confirmPreset: UIEventSource<{
|
||||
description: string | UIElement,
|
||||
name: string | UIElement,
|
||||
icon: UIElement,
|
||||
tags: Tag[],
|
||||
layerToAddTo: {
|
||||
name: UIElement | string,
|
||||
isDisplayed: UIEventSource<boolean> }
|
||||
}>
|
||||
= new UIEventSource(undefined);
|
||||
private confirmButton: UIElement = undefined;
|
||||
private _confirmDescription: UIElement = undefined;
|
||||
private openLayerControl: UIElement;
|
||||
private cancelButton: UIElement;
|
||||
private goToInboxButton: UIElement = new SubtleButton(Svg.envelope_ui(),
|
||||
Translations.t.general.goToInbox, {url:"https://www.openstreetmap.org/messages/inbox", newTab: false});
|
||||
|
||||
constructor() {
|
||||
super(State.state.locationControl);
|
||||
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);
|
||||
|
||||
this._loginButton = Translations.t.general.add.pleaseLogin.Clone().onClick(() => State.state.osmConnection.AttemptLogin());
|
||||
|
||||
this._addButtons = [];
|
||||
this.SetStyle("font-size:large");
|
||||
|
||||
const self = this;
|
||||
for (const layer of State.state.filteredLayers.data) {
|
||||
|
||||
this.ListenTo(layer.isDisplayed);
|
||||
|
||||
const presets = layer.layerDef.presets;
|
||||
for (const preset of presets) {
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
let icon: UIElement = new FixedUiElement(layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html.Render()).SetClass("simple-add-ui-icon");
|
||||
|
||||
const csCount = State.state.osmConnection.userDetails.data.csCount;
|
||||
let tagInfo = "";
|
||||
if (csCount > Constants.userJourney.tagsVisibleAt) {
|
||||
tagInfo = preset.tags.map(t => t.asHumanString(false, true)).join("&");
|
||||
tagInfo = `<br/><span class='subtle'>${tagInfo}</span>`
|
||||
}
|
||||
const button: UIElement =
|
||||
new SubtleButton(
|
||||
icon,
|
||||
new Combine([
|
||||
"<b>",
|
||||
preset.title,
|
||||
"</b>",
|
||||
preset.description !== undefined ? new Combine(["<br/>", preset.description.FirstSentence()]) : "",
|
||||
tagInfo
|
||||
])
|
||||
).onClick(
|
||||
() => {
|
||||
self.confirmButton = new SubtleButton(icon,
|
||||
new Combine([
|
||||
"<b>",
|
||||
Translations.t.general.add.confirmButton.Subs({category: preset.title}),
|
||||
"</b>"]));
|
||||
self.confirmButton.onClick(self.CreatePoint(preset.tags));
|
||||
self._confirmDescription = preset.description;
|
||||
self._confirmPreset.setData({
|
||||
tags: preset.tags,
|
||||
layerToAddTo: layer,
|
||||
name: preset.title,
|
||||
description: preset.description,
|
||||
icon: icon
|
||||
});
|
||||
self.Update();
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
this._addButtons.push(button);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
||||
private CreatePoint(tags: Tag[]) {
|
||||
return () => {
|
||||
const loc = State.state.LastClickLocation.data;
|
||||
let feature = State.state.changes.createElement(tags, loc.lat, loc.lon);
|
||||
State.state.selectedElement.setData(feature);
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
const userDetails = State.state.osmConnection.userDetails;
|
||||
|
||||
if (this._confirmPreset.data !== undefined) {
|
||||
|
||||
if(!this._confirmPreset.data.layerToAddTo.isDisplayed.data){
|
||||
return new Combine([
|
||||
Translations.t.general.add.layerNotEnabled.Subs({layer: this._confirmPreset.data.layerToAddTo.name})
|
||||
.SetClass("alert"),
|
||||
this.openLayerControl,
|
||||
|
||||
this.cancelButton
|
||||
]).Render();
|
||||
}
|
||||
|
||||
let tagInfo = "";
|
||||
const csCount = State.state.osmConnection.userDetails.data.csCount;
|
||||
if (csCount > Constants.userJourney.tagsVisibleAt) {
|
||||
tagInfo = this._confirmPreset.data .tags.map(t => t.asHumanString(csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&");
|
||||
tagInfo = `<br/>More information about the preset: ${tagInfo}`
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
Translations.t.general.add.confirmIntro.Subs({title: this._confirmPreset.data.name}),
|
||||
userDetails.data.dryRun ? "<span class='alert'>TESTING - changes won't be saved</span>" : "",
|
||||
this.confirmButton,
|
||||
this.cancelButton,
|
||||
this._confirmDescription,
|
||||
tagInfo
|
||||
|
||||
]).Render();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
let header: UIElement = Translations.t.general.add.header;
|
||||
|
||||
|
||||
if (userDetails === undefined) {
|
||||
return header.Render();
|
||||
}
|
||||
|
||||
if (!userDetails.data.loggedIn) {
|
||||
return new Combine([header, this._loginButton]).Render()
|
||||
}
|
||||
|
||||
if (userDetails.data.unreadMessages > 0 && userDetails.data.csCount < Constants.userJourney.addNewPointWithUnreadMessagesUnlock) {
|
||||
return new Combine([header,
|
||||
Translations.t.general.readYourMessages.Clone().SetClass("alert"),
|
||||
this.goToInboxButton
|
||||
]).Render();
|
||||
|
||||
}
|
||||
|
||||
if (userDetails.data.dryRun) {
|
||||
header = new Combine([header,
|
||||
"<span class='alert'>",
|
||||
"Test mode - changes won't be saved",
|
||||
"</span>"
|
||||
]);
|
||||
}
|
||||
|
||||
if (userDetails.data.csCount < Constants.userJourney.addNewPointsUnlock) {
|
||||
return new Combine([header, "<span class='alert'>",
|
||||
Translations.t.general.fewChangesBefore,
|
||||
"</span>"]).Render();
|
||||
}
|
||||
|
||||
if (State.state.locationControl.data.zoom < Constants.userJourney.minZoomLevelToAddNewPoints) {
|
||||
return new Combine([header, Translations.t.general.add.zoomInFurther.SetClass("alert")]).Render()
|
||||
}
|
||||
|
||||
if (State.state.layerUpdater.runningQuery.data) {
|
||||
return new Combine([header, Translations.t.general.add.stillLoading]).Render()
|
||||
}
|
||||
|
||||
return header.Render() + new Combine(this._addButtons).SetClass("add-popup-all-buttons").Render();
|
||||
}
|
||||
|
||||
|
||||
}
|
144
UI/BigComponents/UserBadge.ts
Normal file
144
UI/BigComponents/UserBadge.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* Handles and updates the user badge
|
||||
*/
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {UserDetails} from "../../Logic/Osm/OsmConnection";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import LanguagePicker from "../LanguagePicker";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Link from "../Base/Link";
|
||||
|
||||
export default class UserBadge extends UIElement {
|
||||
private _userDetails: UIEventSource<UserDetails>;
|
||||
private _logout: UIElement;
|
||||
private _homeButton: UIElement;
|
||||
private _languagePicker: UIElement;
|
||||
|
||||
private _loginButton: UIElement;
|
||||
|
||||
constructor() {
|
||||
super(State.state.osmConnection.userDetails);
|
||||
this._userDetails = State.state.osmConnection.userDetails;
|
||||
this._languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language) ?? new FixedUiElement(""))
|
||||
.SetStyle("display:inline-block;width:min-content;");
|
||||
|
||||
this._loginButton = Translations.t.general.loginWithOpenStreetMap
|
||||
.Clone()
|
||||
.SetClass("userbadge-login")
|
||||
.onClick(() => State.state.osmConnection.AttemptLogin());
|
||||
this._logout =
|
||||
Svg.logout_svg()
|
||||
.onClick(() => {
|
||||
State.state.osmConnection.LogOut();
|
||||
});
|
||||
|
||||
this._userDetails.addCallback(function () {
|
||||
const profilePic = document.getElementById("profile-pic");
|
||||
if (profilePic) {
|
||||
|
||||
profilePic.onload = function () {
|
||||
profilePic.style.opacity = "1"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this._homeButton = new VariableUiElement(
|
||||
this._userDetails.map((userinfo) => {
|
||||
if (userinfo.home) {
|
||||
return Svg.home;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
).onClick(() => {
|
||||
const home = State.state.osmConnection.userDetails.data?.home;
|
||||
if (home === undefined) {
|
||||
return;
|
||||
}
|
||||
State.state.leafletMap.data.setView([home.lat, home.lon], 16);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const user = this._userDetails.data;
|
||||
if (!user.loggedIn) {
|
||||
return this._loginButton.Render();
|
||||
}
|
||||
|
||||
|
||||
let messageSpan: UIElement =
|
||||
new Link(
|
||||
new Combine([Svg.envelope, "" + user.totalMessages]),
|
||||
'https://www.openstreetmap.org/messages/inbox',
|
||||
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 csCount =
|
||||
new Link(
|
||||
new Combine([Svg.star, "" + user.csCount]),
|
||||
`https://www.openstreetmap.org/user/${user.name}/history`,
|
||||
true);
|
||||
|
||||
|
||||
const userStats = new Combine([
|
||||
this._homeButton,
|
||||
settings,
|
||||
messageSpan,
|
||||
csCount,
|
||||
this._logout,
|
||||
this._languagePicker])
|
||||
.SetClass("userstats")
|
||||
|
||||
const usertext = new Combine([
|
||||
userName,
|
||||
dryrun,
|
||||
userStats
|
||||
]).SetClass("usertext")
|
||||
|
||||
return new Combine([
|
||||
userIcon,
|
||||
usertext,
|
||||
]).Render()
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
56
UI/BigComponents/WelcomeMessage.ts
Normal file
56
UI/BigComponents/WelcomeMessage.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import Locale from "../i18n/Locale";
|
||||
import {UIElement} from "../UIElement";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import LanguagePicker from "../LanguagePicker";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
|
||||
export default class WelcomeMessage extends UIElement {
|
||||
private languagePicker: UIElement;
|
||||
|
||||
private readonly description: UIElement;
|
||||
private readonly plzLogIn: UIElement;
|
||||
private readonly welcomeBack: UIElement;
|
||||
private readonly tail: UIElement;
|
||||
|
||||
|
||||
constructor() {
|
||||
super(State.state.osmConnection.userDetails);
|
||||
this.ListenTo(Locale.language);
|
||||
this.languagePicker = LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language, Translations.t.general.pickLanguage);
|
||||
const layout = State.state.layoutToUse.data;
|
||||
|
||||
this.description = new Combine([
|
||||
"<h3>", layout.title, "</h3>",
|
||||
layout.description
|
||||
])
|
||||
this.plzLogIn =
|
||||
Translations.t.general.loginWithOpenStreetMap
|
||||
.onClick(() => {
|
||||
State.state.osmConnection.AttemptLogin()
|
||||
});
|
||||
this.welcomeBack = Translations.t.general.welcomeBack;
|
||||
this.tail = layout.descriptionTail;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
let loginStatus = undefined;
|
||||
if (State.state.featureSwitchUserbadge.data) {
|
||||
loginStatus = (State.state.osmConnection.userDetails.data.loggedIn ? this.welcomeBack :
|
||||
this.plzLogIn);
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
this.description,
|
||||
"<br/><br/>",
|
||||
loginStatus,
|
||||
this.tail,
|
||||
"<br/>",
|
||||
this.languagePicker
|
||||
]).Render()
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue