forked from MapComplete/MapComplete
Full code cleanup
This commit is contained in:
parent
8e6ee8c87f
commit
bd21212eba
246 changed files with 19418 additions and 11729 deletions
|
@ -8,32 +8,31 @@ import {Utils} from "../Utils";
|
|||
import LanguagePicker from "./LanguagePicker";
|
||||
import IndexText from "./BigComponents/IndexText";
|
||||
import FeaturedMessage from "./BigComponents/FeaturedMessage";
|
||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
|
||||
|
||||
export default class AllThemesGui {
|
||||
constructor() {
|
||||
|
||||
try{
|
||||
|
||||
new FixedUiElement("").AttachTo("centermessage")
|
||||
const state = new UserRelatedState(undefined);
|
||||
const intro = new Combine([
|
||||
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
|
||||
.SetClass("absolute top-2 right-3"),
|
||||
new IndexText()
|
||||
]);
|
||||
new Combine([
|
||||
intro,
|
||||
new FeaturedMessage(),
|
||||
new MoreScreen(state, true),
|
||||
Translations.t.general.aboutMapcomplete
|
||||
.Subs({"osmcha_link": Utils.OsmChaLinkFor(7)})
|
||||
.SetClass("link-underline"),
|
||||
new FixedUiElement("v" + Constants.vNumber)
|
||||
]).SetClass("block m-5 lg:w-3/4 lg:ml-40")
|
||||
.SetStyle("pointer-events: all;")
|
||||
.AttachTo("topleft-tools");
|
||||
}catch (e) {
|
||||
|
||||
try {
|
||||
|
||||
new FixedUiElement("").AttachTo("centermessage")
|
||||
const state = new UserRelatedState(undefined);
|
||||
const intro = new Combine([
|
||||
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
|
||||
.SetClass("absolute top-2 right-3"),
|
||||
new IndexText()
|
||||
]);
|
||||
new Combine([
|
||||
intro,
|
||||
new FeaturedMessage(),
|
||||
new MoreScreen(state, true),
|
||||
Translations.t.general.aboutMapcomplete
|
||||
.Subs({"osmcha_link": Utils.OsmChaLinkFor(7)})
|
||||
.SetClass("link-underline"),
|
||||
new FixedUiElement("v" + Constants.vNumber)
|
||||
]).SetClass("block m-5 lg:w-3/4 lg:ml-40")
|
||||
.SetStyle("pointer-events: all;")
|
||||
.AttachTo("topleft-tools");
|
||||
} catch (e) {
|
||||
new FixedUiElement("Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!").SetClass("alert")
|
||||
.AttachTo("centermessage")
|
||||
}
|
||||
|
|
|
@ -3,26 +3,25 @@ import {VariableUiElement} from "./VariableUIElement";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Loading from "./Loading";
|
||||
|
||||
export default class AsyncLazy extends BaseUIElement{
|
||||
export default class AsyncLazy extends BaseUIElement {
|
||||
private readonly _f: () => Promise<BaseUIElement>;
|
||||
|
||||
|
||||
constructor(f: () => Promise<BaseUIElement>) {
|
||||
super();
|
||||
this._f = f;
|
||||
}
|
||||
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
// The caching of the BaseUIElement will guarantee that _f will only be called once
|
||||
|
||||
|
||||
return new VariableUiElement(
|
||||
UIEventSource.FromPromise(this._f()).map(el => {
|
||||
if(el === undefined){
|
||||
if (el === undefined) {
|
||||
return new Loading()
|
||||
}
|
||||
return el
|
||||
})
|
||||
|
||||
).ConstructElement()
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -30,13 +30,13 @@ export default class Combine extends BaseUIElement {
|
|||
if (subEl === undefined || subEl === null) {
|
||||
continue;
|
||||
}
|
||||
try{
|
||||
|
||||
const subHtml = subEl.ConstructElement()
|
||||
if (subHtml !== undefined) {
|
||||
el.appendChild(subHtml)
|
||||
}
|
||||
}catch(e){
|
||||
try {
|
||||
|
||||
const subHtml = subEl.ConstructElement()
|
||||
if (subHtml !== undefined) {
|
||||
el.appendChild(subHtml)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not generate subelement in combine due to ", e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export default class Img extends BaseUIElement {
|
|||
fallbackImage?: string
|
||||
}) {
|
||||
super();
|
||||
if(src === undefined || src === "undefined"){
|
||||
if (src === undefined || src === "undefined") {
|
||||
throw "Undefined src for image"
|
||||
}
|
||||
this._src = src;
|
||||
|
@ -44,7 +44,7 @@ export default class Img extends BaseUIElement {
|
|||
}
|
||||
el.onerror = () => {
|
||||
if (self._options?.fallbackImage) {
|
||||
if(el.src === self._options.fallbackImage){
|
||||
if (el.src === self._options.fallbackImage) {
|
||||
// Sigh... nothing to be done anymore
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class Lazy extends BaseUIElement{
|
||||
export default class Lazy extends BaseUIElement {
|
||||
private readonly _f: () => BaseUIElement;
|
||||
|
||||
|
||||
constructor(f: () => BaseUIElement) {
|
||||
super();
|
||||
this._f = f;
|
||||
}
|
||||
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
// The caching of the BaseUIElement will guarantee that _f will only be called once
|
||||
return this._f().ConstructElement();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import Combine from "./Combine";
|
||||
import Svg from "../../Svg";
|
||||
|
@ -6,11 +5,11 @@ import Translations from "../i18n/Translations";
|
|||
|
||||
export default class Loading extends Combine {
|
||||
constructor(msg?: Translation | string) {
|
||||
const t = Translations.T(msg ) ?? Translations.t.general.loading.Clone();
|
||||
const t = Translations.T(msg) ?? Translations.t.general.loading.Clone();
|
||||
t.SetClass("pl-2")
|
||||
super([
|
||||
Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"),
|
||||
t
|
||||
t
|
||||
])
|
||||
this.SetClass("flex p-1")
|
||||
}
|
||||
|
|
|
@ -17,8 +17,10 @@ export interface MinimapOptions {
|
|||
}
|
||||
|
||||
export interface MinimapObj {
|
||||
readonly leafletMap: UIEventSource<any>,
|
||||
installBounds(factor: number | BBox, showRange?: boolean) : void
|
||||
readonly leafletMap: UIEventSource<any>,
|
||||
|
||||
installBounds(factor: number | BBox, showRange?: boolean): void
|
||||
|
||||
TakeScreenshot(): Promise<any>;
|
||||
}
|
||||
|
||||
|
|
|
@ -103,6 +103,12 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
})
|
||||
}
|
||||
|
||||
public async TakeScreenshot() {
|
||||
const screenshotter = new SimpleMapScreenshoter();
|
||||
screenshotter.addTo(this.leafletMap.data);
|
||||
return await screenshotter.takeScreen('image')
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const div = document.createElement("div")
|
||||
div.id = this._id;
|
||||
|
@ -148,8 +154,8 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
const self = this;
|
||||
let currentLayer = this._background.data.layer()
|
||||
let latLon = <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0]
|
||||
if(isNaN(latLon[0]) || isNaN(latLon[1])){
|
||||
latLon = [0,0]
|
||||
if (isNaN(latLon[0]) || isNaN(latLon[1])) {
|
||||
latLon = [0, 0]
|
||||
}
|
||||
const options = {
|
||||
center: latLon,
|
||||
|
@ -279,10 +285,4 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
|
||||
this.leafletMap.setData(map)
|
||||
}
|
||||
|
||||
public async TakeScreenshot(){
|
||||
const screenshotter = new SimpleMapScreenshoter();
|
||||
screenshotter.addTo(this.leafletMap.data);
|
||||
return await screenshotter.takeScreen('image')
|
||||
}
|
||||
}
|
|
@ -19,8 +19,8 @@ import Img from "./Img";
|
|||
export default class ScrollableFullScreen extends UIElement {
|
||||
private static readonly empty = new FixedUiElement("");
|
||||
private static _currentlyOpen: ScrollableFullScreen;
|
||||
private hashToShow: string;
|
||||
public isShown: UIEventSource<boolean>;
|
||||
private hashToShow: string;
|
||||
private _component: BaseUIElement;
|
||||
private _fullscreencomponent: BaseUIElement;
|
||||
|
||||
|
@ -61,13 +61,6 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
})
|
||||
}
|
||||
|
||||
private clear() {
|
||||
ScrollableFullScreen.empty.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen");
|
||||
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
|
||||
fs.classList.add("hidden")
|
||||
}
|
||||
|
||||
InnerRender(): BaseUIElement {
|
||||
return this._component;
|
||||
}
|
||||
|
@ -80,6 +73,13 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
fs.classList.remove("hidden")
|
||||
}
|
||||
|
||||
private clear() {
|
||||
ScrollableFullScreen.empty.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen");
|
||||
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
|
||||
fs.classList.add("hidden")
|
||||
}
|
||||
|
||||
private BuildComponent(title: BaseUIElement, content: BaseUIElement, isShown: UIEventSource<boolean>) {
|
||||
const returnToTheMap =
|
||||
new Combine([
|
||||
|
|
|
@ -6,7 +6,7 @@ import {VariableUiElement} from "./VariableUIElement";
|
|||
|
||||
export class TabbedComponent extends Combine {
|
||||
|
||||
constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[],
|
||||
constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[],
|
||||
openedTab: (UIEventSource<number> | number) = 0,
|
||||
options?: {
|
||||
leftOfHeader?: BaseUIElement
|
||||
|
@ -15,13 +15,13 @@ export class TabbedComponent extends Combine {
|
|||
|
||||
const openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0))
|
||||
|
||||
const tabs: BaseUIElement[] = [options?.leftOfHeader ]
|
||||
const tabs: BaseUIElement[] = [options?.leftOfHeader]
|
||||
const contentElements: BaseUIElement[] = [];
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let element = elements[i];
|
||||
const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i))
|
||||
openedTabSrc.addCallbackAndRun(selected => {
|
||||
if(selected >= elements.length){
|
||||
if (selected >= elements.length) {
|
||||
selected = 0
|
||||
}
|
||||
if (selected === i) {
|
||||
|
@ -40,7 +40,7 @@ export class TabbedComponent extends Combine {
|
|||
}
|
||||
|
||||
const header = new Combine(tabs).SetClass("tabs-header-bar")
|
||||
if(options?.styleHeader){
|
||||
if (options?.styleHeader) {
|
||||
options.styleHeader(header)
|
||||
}
|
||||
const actualContent = new VariableUiElement(
|
||||
|
|
|
@ -7,9 +7,9 @@ export default class Title extends BaseUIElement {
|
|||
|
||||
constructor(embedded: string | BaseUIElement, level: number = 3) {
|
||||
super()
|
||||
if(typeof embedded === "string"){
|
||||
this._embedded = new FixedUiElement(embedded)
|
||||
}else{
|
||||
if (typeof embedded === "string") {
|
||||
this._embedded = new FixedUiElement(embedded)
|
||||
} else {
|
||||
this._embedded = embedded
|
||||
}
|
||||
this._level = level;
|
||||
|
|
|
@ -45,7 +45,9 @@ export default abstract class BaseUIElement {
|
|||
* Adds all the relevant classes, space separated
|
||||
*/
|
||||
public SetClass(clss: string) {
|
||||
if(clss == undefined){return }
|
||||
if (clss == undefined) {
|
||||
return
|
||||
}
|
||||
const all = clss.split(" ").map(clsName => clsName.trim());
|
||||
let recordedChange = false;
|
||||
for (let c of all) {
|
||||
|
|
|
@ -15,7 +15,7 @@ import {Utils} from "../../Utils";
|
|||
*/
|
||||
export default class Attribution extends Combine {
|
||||
|
||||
constructor(location: UIEventSource<Loc>,
|
||||
constructor(location: UIEventSource<Loc>,
|
||||
userDetails: UIEventSource<UserDetails>,
|
||||
layoutToUse: LayoutConfig,
|
||||
currentBounds: UIEventSource<BBox>) {
|
||||
|
|
|
@ -36,8 +36,8 @@ export default class CopyrightPanel extends Combine {
|
|||
locationControl: UIEventSource<Loc>,
|
||||
osmConnection: OsmConnection
|
||||
}, contributions: UIEventSource<Map<string, number>>) {
|
||||
|
||||
const t =Translations.t.general.attribution
|
||||
|
||||
const t = Translations.t.general.attribution
|
||||
const layoutToUse = state.layoutToUse
|
||||
const josmState = new UIEventSource<string>(undefined)
|
||||
// Reset after 15s
|
||||
|
@ -53,7 +53,7 @@ export default class CopyrightPanel extends Combine {
|
|||
newTab: true
|
||||
}),
|
||||
new SubtleButton(Svg.statistics_ui().SetStyle(iconStyle), t.openOsmcha.Subs({theme: state.layoutToUse.title}), {
|
||||
url: Utils.OsmChaLinkFor(31, state.layoutToUse.id),
|
||||
url: Utils.OsmChaLinkFor(31, state.layoutToUse.id),
|
||||
newTab: true
|
||||
}),
|
||||
new VariableUiElement(state.locationControl.map(location => {
|
||||
|
@ -63,42 +63,45 @@ export default class CopyrightPanel extends Combine {
|
|||
|
||||
new VariableUiElement(state.locationControl.map(location => {
|
||||
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
|
||||
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {url: mapillaryLink, newTab: true})
|
||||
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {
|
||||
url: mapillaryLink,
|
||||
newTab: true
|
||||
})
|
||||
})),
|
||||
new VariableUiElement(josmState.map(state => {
|
||||
if(state === undefined){
|
||||
if (state === undefined) {
|
||||
return undefined
|
||||
}
|
||||
state = state.toUpperCase()
|
||||
if(state === "OK"){
|
||||
if (state === "OK") {
|
||||
return t.josmOpened.SetClass("thanks")
|
||||
}
|
||||
return t.josmNotOpened.SetClass("alert")
|
||||
})),
|
||||
new Toggle(
|
||||
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle) , t.editJosm).onClick(() => {
|
||||
const bounds: any = state.currentBounds.data;
|
||||
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}`
|
||||
Utils.download(josmLink).then(answer => josmState.setData(answer.replace(/\n/g, '').trim())).catch(_ => josmState.setData("ERROR"))
|
||||
}), undefined, state.osmConnection.userDetails.map(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible)),
|
||||
|
||||
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
|
||||
const bounds: any = state.currentBounds.data;
|
||||
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}`
|
||||
Utils.download(josmLink).then(answer => josmState.setData(answer.replace(/\n/g, '').trim())).catch(_ => josmState.setData("ERROR"))
|
||||
}), undefined, state.osmConnection.userDetails.map(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible)),
|
||||
|
||||
].map(button => button.SetStyle("max-height: 3rem"))
|
||||
|
||||
|
||||
const iconAttributions = Utils.NoNull(Array.from(layoutToUse.ExtractImages()))
|
||||
.map(CopyrightPanel.IconAttribution)
|
||||
|
||||
let maintainer : BaseUIElement= undefined
|
||||
if(layoutToUse.maintainer !== undefined && layoutToUse.maintainer !== "" && layoutToUse.maintainer.toLowerCase() !== "mapcomplete"){
|
||||
|
||||
let maintainer: BaseUIElement = undefined
|
||||
if (layoutToUse.maintainer !== undefined && layoutToUse.maintainer !== "" && layoutToUse.maintainer.toLowerCase() !== "mapcomplete") {
|
||||
maintainer = Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.maintainer})
|
||||
}
|
||||
|
||||
|
||||
super([
|
||||
Translations.t.general.attribution.attributionContent,
|
||||
maintainer,
|
||||
|
@ -106,7 +109,7 @@ export default class CopyrightPanel extends Combine {
|
|||
new FixedUiElement(layoutToUse.credits),
|
||||
new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.currentBounds),
|
||||
new VariableUiElement(contributions.map(contributions => {
|
||||
if(contributions === undefined){
|
||||
if (contributions === undefined) {
|
||||
return ""
|
||||
}
|
||||
const sorted = Array.from(contributions, ([name, value]) => ({
|
||||
|
|
|
@ -12,26 +12,25 @@ import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {meta} from "@turf/turf";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
export class DownloadPanel extends Toggle {
|
||||
|
||||
|
||||
constructor() {
|
||||
const state: {
|
||||
featurePipeline: FeaturePipeline,
|
||||
layoutToUse: LayoutConfig,
|
||||
currentBounds: UIEventSource<BBox>
|
||||
} = State.state
|
||||
|
||||
|
||||
|
||||
const t = Translations.t.general.download
|
||||
const name = State.state.layoutToUse.id;
|
||||
|
||||
|
||||
const includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()])
|
||||
const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0)
|
||||
|
||||
|
||||
|
||||
const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
|
||||
new Combine([t.downloadGeojson.Clone().SetClass("font-bold"),
|
||||
t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col"))
|
||||
|
@ -42,7 +41,7 @@ export class DownloadPanel extends Toggle {
|
|||
mimetype: "application/vnd.geo+json"
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine(
|
||||
[t.downloadCSV.Clone().SetClass("font-bold"),
|
||||
|
@ -59,9 +58,9 @@ export class DownloadPanel extends Toggle {
|
|||
|
||||
const downloadButtons = new Combine(
|
||||
[new Title(t.title),
|
||||
buttonGeoJson,
|
||||
buttonGeoJson,
|
||||
buttonCSV,
|
||||
includeMetaToggle,
|
||||
includeMetaToggle,
|
||||
t.licenseInfo.Clone().SetClass("link-underline")])
|
||||
.SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
|
||||
|
||||
|
@ -107,7 +106,7 @@ export class DownloadPanel extends Toggle {
|
|||
}
|
||||
|
||||
return {
|
||||
type:"FeatureCollection",
|
||||
type: "FeatureCollection",
|
||||
features: resultFeatures
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ export default class FeaturedMessage extends Combine {
|
|||
if (wm.end_date <= now) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if (welcome_message !== undefined) {
|
||||
console.warn("Multiple applicable messages today:", welcome_message.featured_theme)
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export default class FeaturedMessage extends Combine {
|
|||
message: wm.message,
|
||||
featured_theme: wm.featured_theme
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
return all_messages
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
osmConnection: OsmConnection,
|
||||
featureSwitchShareScreen: UIEventSource<boolean>,
|
||||
featureSwitchMoreQuests: UIEventSource<boolean>,
|
||||
locationControl: UIEventSource<Loc>,
|
||||
backgroundLayer: UIEventSource<BaseLayer>,
|
||||
locationControl: UIEventSource<Loc>,
|
||||
backgroundLayer: UIEventSource<BaseLayer>,
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
} & UserRelatedState) {
|
||||
const layoutToUse = state.layoutToUse;
|
||||
|
@ -70,10 +70,10 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
tabs.push({
|
||||
header: Svg.add_img,
|
||||
content:
|
||||
new Combine([
|
||||
Translations.t.general.morescreen.intro,
|
||||
new MoreScreen(state)
|
||||
]).SetClass("flex flex-col")
|
||||
new Combine([
|
||||
Translations.t.general.morescreen.intro,
|
||||
new MoreScreen(state)
|
||||
]).SetClass("flex flex-col")
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)
|
||||
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)]
|
||||
|
||||
|
||||
|
||||
tabsWithAboutMc.push({
|
||||
header: Svg.help,
|
||||
content: new Combine([Translations.t.general.aboutMapcomplete
|
||||
|
|
|
@ -9,7 +9,6 @@ import Toggle from "../Input/Toggle";
|
|||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import Loading from "../Base/Loading";
|
||||
import CreateNewWayAction from "../../Logic/Osm/Actions/CreateNewWayAction";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import {Changes} from "../../Logic/Osm/Changes";
|
||||
|
@ -32,7 +31,6 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
|
|||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||
import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import CreateWayWithPointReuseAction from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
|
||||
|
@ -282,8 +280,8 @@ export default class ImportButton extends Toggle {
|
|||
}
|
||||
|
||||
public static createConfirmForWay(o: ImportButtonState,
|
||||
isImported: UIEventSource<boolean>,
|
||||
importClicked: UIEventSource<boolean>): BaseUIElement {
|
||||
isImported: UIEventSource<boolean>,
|
||||
importClicked: UIEventSource<boolean>): BaseUIElement {
|
||||
|
||||
const confirmationMap = Minimap.createMiniMap({
|
||||
allowMoving: true,
|
||||
|
@ -301,8 +299,8 @@ export default class ImportButton extends Toggle {
|
|||
allElements: o.state.allElements,
|
||||
layers: o.state.filteredLayers
|
||||
})
|
||||
|
||||
let action : OsmChangeAction & {getPreview() : Promise<FeatureSource>}
|
||||
|
||||
let action: OsmChangeAction & { getPreview(): Promise<FeatureSource> }
|
||||
|
||||
const theme = o.state.layoutToUse.id
|
||||
const changes = o.state.changes
|
||||
|
@ -320,7 +318,7 @@ export default class ImportButton extends Toggle {
|
|||
)
|
||||
|
||||
confirm = async () => {
|
||||
changes.applyAction (action)
|
||||
changes.applyAction(action)
|
||||
return o.feature.properties.id
|
||||
}
|
||||
|
||||
|
@ -332,8 +330,8 @@ export default class ImportButton extends Toggle {
|
|||
} else if (geom.type === "Polygon") {
|
||||
coordinates = geom.coordinates[0]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
action = new CreateWayWithPointReuseAction(
|
||||
o.newTags.data,
|
||||
coordinates,
|
||||
|
@ -341,13 +339,13 @@ export default class ImportButton extends Toggle {
|
|||
o.state,
|
||||
[{
|
||||
withinRangeOfM: 1,
|
||||
ifMatches: new Tag("_is_part_of_building","true"),
|
||||
mode:"move_osm_point"
|
||||
|
||||
ifMatches: new Tag("_is_part_of_building", "true"),
|
||||
mode: "move_osm_point"
|
||||
|
||||
}]
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
confirm = async () => {
|
||||
changes.applyAction(action)
|
||||
return undefined
|
||||
|
|
|
@ -45,8 +45,8 @@ export default class LeftControls extends Combine {
|
|||
state,
|
||||
new ContributorCount(state).Contributors
|
||||
),
|
||||
"copyright",
|
||||
guiState.copyrightViewIsOpened
|
||||
"copyright",
|
||||
guiState.copyrightViewIsOpened
|
||||
);
|
||||
|
||||
const copyrightButton = new Toggle(
|
||||
|
|
|
@ -39,110 +39,6 @@ export default class MoreScreen extends Combine {
|
|||
]);
|
||||
}
|
||||
|
||||
|
||||
private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses): BaseUIElement {
|
||||
return new VariableUiElement(state.installedThemes.map(customThemes => {
|
||||
if (customThemes.length <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
const customThemeButtons = customThemes.map(theme => MoreScreen.createLinkButton(state, theme.layout, theme.definition)?.SetClass(buttonClass))
|
||||
return new Combine([
|
||||
Translations.t.general.customThemeIntro.Clone(),
|
||||
new Combine(customThemeButtons).SetClass(themeListClasses)
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string) {
|
||||
const t = Translations.t.general.morescreen
|
||||
const prefix = "mapcomplete-hidden-theme-"
|
||||
const hiddenTotal = AllKnownLayouts.layoutsList.filter(layout => layout.hideFromOverview).length
|
||||
return new Toggle(
|
||||
new VariableUiElement(
|
||||
state.osmConnection.preferencesHandler.preferences.map(allPreferences => {
|
||||
const knownThemes = Utils.NoNull(Object.keys(allPreferences)
|
||||
.filter(key => key.startsWith(prefix))
|
||||
.map(key => key.substring(prefix.length, key.length - "-enabled".length))
|
||||
.map(theme => {
|
||||
return AllKnownLayouts.allKnownLayouts.get(theme);
|
||||
}))
|
||||
if (knownThemes.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const knownLayouts = new Combine(knownThemes.map(layout =>
|
||||
MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass)
|
||||
)).SetClass(themeListStyle)
|
||||
|
||||
return new Combine([
|
||||
new Title(t.previouslyHiddenTitle),
|
||||
t.hiddenExplanation.Subs({hidden_discovered: ""+knownThemes.length,total_hidden: ""+hiddenTotal}),
|
||||
knownLayouts
|
||||
])
|
||||
|
||||
})
|
||||
).SetClass("flex flex-col"),
|
||||
undefined,
|
||||
state.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string): BaseUIElement {
|
||||
let officialThemes = AllKnownLayouts.layoutsList
|
||||
|
||||
let buttons = officialThemes.map((layout) => {
|
||||
if (layout === undefined) {
|
||||
console.trace("Layout is undefined")
|
||||
return undefined
|
||||
}
|
||||
if(layout.hideFromOverview){
|
||||
return undefined;
|
||||
}
|
||||
const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass);
|
||||
if (layout.id === personal.id) {
|
||||
return new VariableUiElement(
|
||||
state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
|
||||
.map(csCount => {
|
||||
if (csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||
return undefined
|
||||
} else {
|
||||
return button
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return button;
|
||||
})
|
||||
|
||||
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: { osmConnection: OsmConnection }): 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
|
||||
* @private
|
||||
|
@ -161,7 +57,7 @@ export default class MoreScreen extends Combine {
|
|||
console.error("ID is undefined for layout", layout);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
if (layout.id === state?.layoutToUse?.id) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -210,5 +106,111 @@ export default class MoreScreen extends Combine {
|
|||
]), {url: linkText, newTab: false});
|
||||
}
|
||||
|
||||
private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses): BaseUIElement {
|
||||
return new VariableUiElement(state.installedThemes.map(customThemes => {
|
||||
if (customThemes.length <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
const customThemeButtons = customThemes.map(theme => MoreScreen.createLinkButton(state, theme.layout, theme.definition)?.SetClass(buttonClass))
|
||||
return new Combine([
|
||||
Translations.t.general.customThemeIntro.Clone(),
|
||||
new Combine(customThemeButtons).SetClass(themeListClasses)
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string) {
|
||||
const t = Translations.t.general.morescreen
|
||||
const prefix = "mapcomplete-hidden-theme-"
|
||||
const hiddenTotal = AllKnownLayouts.layoutsList.filter(layout => layout.hideFromOverview).length
|
||||
return new Toggle(
|
||||
new VariableUiElement(
|
||||
state.osmConnection.preferencesHandler.preferences.map(allPreferences => {
|
||||
const knownThemes = Utils.NoNull(Object.keys(allPreferences)
|
||||
.filter(key => key.startsWith(prefix))
|
||||
.map(key => key.substring(prefix.length, key.length - "-enabled".length))
|
||||
.map(theme => {
|
||||
return AllKnownLayouts.allKnownLayouts.get(theme);
|
||||
}))
|
||||
if (knownThemes.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const knownLayouts = new Combine(knownThemes.map(layout =>
|
||||
MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass)
|
||||
)).SetClass(themeListStyle)
|
||||
|
||||
return new Combine([
|
||||
new Title(t.previouslyHiddenTitle),
|
||||
t.hiddenExplanation.Subs({
|
||||
hidden_discovered: "" + knownThemes.length,
|
||||
total_hidden: "" + hiddenTotal
|
||||
}),
|
||||
knownLayouts
|
||||
])
|
||||
|
||||
})
|
||||
).SetClass("flex flex-col"),
|
||||
undefined,
|
||||
state.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string): BaseUIElement {
|
||||
let officialThemes = AllKnownLayouts.layoutsList
|
||||
|
||||
let buttons = officialThemes.map((layout) => {
|
||||
if (layout === undefined) {
|
||||
console.trace("Layout is undefined")
|
||||
return undefined
|
||||
}
|
||||
if (layout.hideFromOverview) {
|
||||
return undefined;
|
||||
}
|
||||
const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass);
|
||||
if (layout.id === personal.id) {
|
||||
return new VariableUiElement(
|
||||
state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
|
||||
.map(csCount => {
|
||||
if (csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||
return undefined
|
||||
} else {
|
||||
return button
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return button;
|
||||
})
|
||||
|
||||
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: { osmConnection: OsmConnection }): 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
|
||||
});
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -9,19 +9,19 @@ import AllKnownLayers from "../../Customizations/AllKnownLayers";
|
|||
|
||||
export default class RightControls extends Combine {
|
||||
|
||||
constructor(state:MapState) {
|
||||
|
||||
constructor(state: MapState) {
|
||||
|
||||
const geolocatioHandler = new GeoLocationHandler(
|
||||
state
|
||||
)
|
||||
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: AllKnownLayers.sharedLayers.get("gps_location"),
|
||||
leafletMap: state.leafletMap,
|
||||
enablePopups: true,
|
||||
features: geolocatioHandler.currentLocation
|
||||
})
|
||||
|
||||
|
||||
const geolocationButton = new Toggle(
|
||||
new MapControlButton(
|
||||
geolocatioHandler
|
||||
|
|
|
@ -15,7 +15,7 @@ import FilteredLayer from "../../Models/FilteredLayer";
|
|||
|
||||
export default class ShareScreen extends Combine {
|
||||
|
||||
constructor(state: {layoutToUse: LayoutConfig, locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]>}) {
|
||||
constructor(state: { layoutToUse: LayoutConfig, locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]> }) {
|
||||
const layout = state?.layoutToUse;
|
||||
const tr = Translations.t.general.sharescreen;
|
||||
|
||||
|
@ -62,40 +62,39 @@ export default class ShareScreen extends Combine {
|
|||
}
|
||||
|
||||
|
||||
|
||||
const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = state.backgroundLayer;
|
||||
const currentBackground = new VariableUiElement(currentLayer.map(layer => {
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""});
|
||||
}));
|
||||
const includeCurrentBackground = new Toggle(
|
||||
new Combine([check(), currentBackground]),
|
||||
new Combine([nocheck(), currentBackground]),
|
||||
new UIEventSource<boolean>(true)
|
||||
).ToggleOnClick()
|
||||
optionCheckboxes.push(includeCurrentBackground);
|
||||
optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => {
|
||||
if (includeBG) {
|
||||
return "background=" + currentLayer.data.id
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [currentLayer]));
|
||||
const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = state.backgroundLayer;
|
||||
const currentBackground = new VariableUiElement(currentLayer.map(layer => {
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""});
|
||||
}));
|
||||
const includeCurrentBackground = new Toggle(
|
||||
new Combine([check(), currentBackground]),
|
||||
new Combine([nocheck(), currentBackground]),
|
||||
new UIEventSource<boolean>(true)
|
||||
).ToggleOnClick()
|
||||
optionCheckboxes.push(includeCurrentBackground);
|
||||
optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => {
|
||||
if (includeBG) {
|
||||
return "background=" + currentLayer.data.id
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [currentLayer]));
|
||||
|
||||
|
||||
const includeLayerChoices = new Toggle(
|
||||
new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]),
|
||||
new UIEventSource<boolean>(true)
|
||||
).ToggleOnClick()
|
||||
optionCheckboxes.push(includeLayerChoices);
|
||||
const includeLayerChoices = new Toggle(
|
||||
new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]),
|
||||
new UIEventSource<boolean>(true)
|
||||
).ToggleOnClick()
|
||||
optionCheckboxes.push(includeLayerChoices);
|
||||
|
||||
optionParts.push(includeLayerChoices.isEnabled.map((includeLayerSelection) => {
|
||||
if (includeLayerSelection) {
|
||||
return Utils.NoNull(state.filteredLayers.data.map(fLayerToParam)).join("&")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, state.filteredLayers.data.map((flayer) => flayer.isDisplayed)));
|
||||
optionParts.push(includeLayerChoices.isEnabled.map((includeLayerSelection) => {
|
||||
if (includeLayerSelection) {
|
||||
return Utils.NoNull(state.filteredLayers.data.map(fLayerToParam)).join("&")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, state.filteredLayers.data.map((flayer) => flayer.isDisplayed)));
|
||||
|
||||
|
||||
const switches = [
|
||||
|
|
|
@ -105,7 +105,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
selectedPreset.setData(undefined)
|
||||
}
|
||||
|
||||
const message =Translations.t.general.add.addNew.Subs({category: preset.name});
|
||||
const message = Translations.t.general.add.addNew.Subs({category: preset.name});
|
||||
return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset,
|
||||
message,
|
||||
state.LastClickLocation.data,
|
||||
|
|
233
UI/DefaultGUI.ts
233
UI/DefaultGUI.ts
|
@ -46,123 +46,6 @@ export default class DefaultGUI {
|
|||
this.SetupMap()
|
||||
}
|
||||
|
||||
|
||||
private SetupMap(){
|
||||
const state = this.state;
|
||||
const guiState = this._guiState;
|
||||
|
||||
// Attach the map
|
||||
state.mainMapObject.SetClass("w-full h-full")
|
||||
.AttachTo("leafletDiv")
|
||||
|
||||
this.setupClickDialogOnMap(
|
||||
guiState.filterViewIsOpened,
|
||||
state
|
||||
)
|
||||
|
||||
|
||||
new ShowDataLayer({
|
||||
leafletMap: state.leafletMap,
|
||||
layerToShow: AllKnownLayers.sharedLayers.get("home_location"),
|
||||
features: state.homeLocation,
|
||||
enablePopups: false,
|
||||
})
|
||||
|
||||
state.leafletMap.addCallbackAndRunD(_ => {
|
||||
// Lets assume that all showDataLayers are initialized at this point
|
||||
state.selectedElement.ping()
|
||||
State.state.locationControl.ping();
|
||||
return true;
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private SetupUIElements(){
|
||||
const state = this.state;
|
||||
const guiState = this._guiState;
|
||||
|
||||
const self =this
|
||||
Toggle.If(state.featureSwitchUserbadge,
|
||||
() => new UserBadge(state)
|
||||
).AttachTo("userbadge")
|
||||
|
||||
Toggle.If(state.featureSwitchSearch,
|
||||
() => new SearchAndGo(state))
|
||||
.AttachTo("searchbox");
|
||||
|
||||
|
||||
let iframePopout: () => BaseUIElement = undefined;
|
||||
|
||||
if (window !== window.top) {
|
||||
// MapComplete is running in an iframe
|
||||
iframePopout = () => new VariableUiElement(state.locationControl.map(loc => {
|
||||
const url = `${window.location.origin}${window.location.pathname}?z=${loc.zoom ?? 0}&lat=${loc.lat ?? 0}&lon=${loc.lon ?? 0}`;
|
||||
const link = new Link(Svg.pop_out_img, url, true).SetClass("block w-full h-full p-1.5")
|
||||
return new MapControlButton(link)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
new Toggle(new Lazy(() => self.InitWelcomeMessage()),
|
||||
Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout),
|
||||
state.featureSwitchWelcomeMessage
|
||||
).AttachTo("messagesbox");
|
||||
|
||||
|
||||
new LeftControls(state, guiState).AttachTo("bottom-left");
|
||||
new RightControls(state).AttachTo("bottom-right");
|
||||
|
||||
new CenterMessageBox(state).AttachTo("centermessage");
|
||||
document
|
||||
.getElementById("centermessage")
|
||||
.classList.add("pointer-events-none");
|
||||
|
||||
// We have to ping the welcomeMessageIsOpened and other isOpened-stuff to activate the FullScreenMessage if needed
|
||||
for (const state of guiState.allFullScreenStates) {
|
||||
if(state.data){
|
||||
state.ping()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* At last, if the map moves or an element is selected, we close all the panels just as well
|
||||
*/
|
||||
|
||||
state.selectedElement.addCallbackAndRunD((_) => {
|
||||
guiState.allFullScreenStates.forEach(s => s.setData(false))
|
||||
});
|
||||
}
|
||||
|
||||
private InitWelcomeMessage() : BaseUIElement{
|
||||
const isOpened = this._guiState.welcomeMessageIsOpened
|
||||
const fullOptions = new FullWelcomePaneWithTabs(isOpened, this._guiState.welcomeMessageOpenedTab, this.state);
|
||||
|
||||
// ?-Button on Desktop, opens panel with close-X.
|
||||
const help = new MapControlButton(Svg.help_svg());
|
||||
help.onClick(() => isOpened.setData(true));
|
||||
|
||||
|
||||
const openedTime = new Date().getTime();
|
||||
this.state.locationControl.addCallback(() => {
|
||||
if (new Date().getTime() - openedTime < 15 * 1000) {
|
||||
// Don't autoclose the first 15 secs when the map is moving
|
||||
return;
|
||||
}
|
||||
isOpened.setData(false);
|
||||
});
|
||||
|
||||
this.state.selectedElement.addCallbackAndRunD((_) => {
|
||||
isOpened.setData(false);
|
||||
});
|
||||
|
||||
return new Toggle(
|
||||
fullOptions.SetClass("welcomeMessage pointer-events-auto"),
|
||||
help.SetClass("pointer-events-auto"),
|
||||
isOpened
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, state: FeaturePipelineState) {
|
||||
|
||||
function setup() {
|
||||
|
@ -207,4 +90,120 @@ export default class DefaultGUI {
|
|||
|
||||
}
|
||||
|
||||
private SetupMap() {
|
||||
const state = this.state;
|
||||
const guiState = this._guiState;
|
||||
|
||||
// Attach the map
|
||||
state.mainMapObject.SetClass("w-full h-full")
|
||||
.AttachTo("leafletDiv")
|
||||
|
||||
this.setupClickDialogOnMap(
|
||||
guiState.filterViewIsOpened,
|
||||
state
|
||||
)
|
||||
|
||||
|
||||
new ShowDataLayer({
|
||||
leafletMap: state.leafletMap,
|
||||
layerToShow: AllKnownLayers.sharedLayers.get("home_location"),
|
||||
features: state.homeLocation,
|
||||
enablePopups: false,
|
||||
})
|
||||
|
||||
state.leafletMap.addCallbackAndRunD(_ => {
|
||||
// Lets assume that all showDataLayers are initialized at this point
|
||||
state.selectedElement.ping()
|
||||
State.state.locationControl.ping();
|
||||
return true;
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private SetupUIElements() {
|
||||
const state = this.state;
|
||||
const guiState = this._guiState;
|
||||
|
||||
const self = this
|
||||
Toggle.If(state.featureSwitchUserbadge,
|
||||
() => new UserBadge(state)
|
||||
).AttachTo("userbadge")
|
||||
|
||||
Toggle.If(state.featureSwitchSearch,
|
||||
() => new SearchAndGo(state))
|
||||
.AttachTo("searchbox");
|
||||
|
||||
|
||||
let iframePopout: () => BaseUIElement = undefined;
|
||||
|
||||
if (window !== window.top) {
|
||||
// MapComplete is running in an iframe
|
||||
iframePopout = () => new VariableUiElement(state.locationControl.map(loc => {
|
||||
const url = `${window.location.origin}${window.location.pathname}?z=${loc.zoom ?? 0}&lat=${loc.lat ?? 0}&lon=${loc.lon ?? 0}`;
|
||||
const link = new Link(Svg.pop_out_img, url, true).SetClass("block w-full h-full p-1.5")
|
||||
return new MapControlButton(link)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
new Toggle(new Lazy(() => self.InitWelcomeMessage()),
|
||||
Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout),
|
||||
state.featureSwitchWelcomeMessage
|
||||
).AttachTo("messagesbox");
|
||||
|
||||
|
||||
new LeftControls(state, guiState).AttachTo("bottom-left");
|
||||
new RightControls(state).AttachTo("bottom-right");
|
||||
|
||||
new CenterMessageBox(state).AttachTo("centermessage");
|
||||
document
|
||||
.getElementById("centermessage")
|
||||
.classList.add("pointer-events-none");
|
||||
|
||||
// We have to ping the welcomeMessageIsOpened and other isOpened-stuff to activate the FullScreenMessage if needed
|
||||
for (const state of guiState.allFullScreenStates) {
|
||||
if (state.data) {
|
||||
state.ping()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* At last, if the map moves or an element is selected, we close all the panels just as well
|
||||
*/
|
||||
|
||||
state.selectedElement.addCallbackAndRunD((_) => {
|
||||
guiState.allFullScreenStates.forEach(s => s.setData(false))
|
||||
});
|
||||
}
|
||||
|
||||
private InitWelcomeMessage(): BaseUIElement {
|
||||
const isOpened = this._guiState.welcomeMessageIsOpened
|
||||
const fullOptions = new FullWelcomePaneWithTabs(isOpened, this._guiState.welcomeMessageOpenedTab, this.state);
|
||||
|
||||
// ?-Button on Desktop, opens panel with close-X.
|
||||
const help = new MapControlButton(Svg.help_svg());
|
||||
help.onClick(() => isOpened.setData(true));
|
||||
|
||||
|
||||
const openedTime = new Date().getTime();
|
||||
this.state.locationControl.addCallback(() => {
|
||||
if (new Date().getTime() - openedTime < 15 * 1000) {
|
||||
// Don't autoclose the first 15 secs when the map is moving
|
||||
return;
|
||||
}
|
||||
isOpened.setData(false);
|
||||
});
|
||||
|
||||
this.state.selectedElement.addCallbackAndRunD((_) => {
|
||||
isOpened.setData(false);
|
||||
});
|
||||
|
||||
return new Toggle(
|
||||
fullOptions.SetClass("welcomeMessage pointer-events-auto"),
|
||||
help.SetClass("pointer-events-auto"),
|
||||
isOpened
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -4,13 +4,13 @@ import Constants from "../Models/Constants";
|
|||
import Hash from "../Logic/Web/Hash";
|
||||
|
||||
export class DefaultGuiState {
|
||||
static state: DefaultGuiState;
|
||||
public readonly welcomeMessageIsOpened: UIEventSource<boolean>;
|
||||
public readonly downloadControlIsOpened: UIEventSource<boolean>;
|
||||
public readonly filterViewIsOpened: UIEventSource<boolean>;
|
||||
public readonly copyrightViewIsOpened: UIEventSource<boolean>;
|
||||
public readonly welcomeMessageOpenedTab: UIEventSource<number>
|
||||
public readonly allFullScreenStates: UIEventSource<boolean>[] = []
|
||||
static state: DefaultGuiState;
|
||||
|
||||
constructor() {
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
|||
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
|
||||
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
|
||||
import {BBox} from "../Logic/BBox";
|
||||
|
||||
/**
|
||||
* Creates screenshoter to take png screenshot
|
||||
* Creates jspdf and downloads it
|
||||
|
@ -79,10 +80,10 @@ export default class ExportPDF {
|
|||
minimap.AttachTo(options.freeDivId)
|
||||
|
||||
// Next: we prepare the features. Only fully contained features are shown
|
||||
minimap.leafletMap .addCallbackAndRunD(leaflet => {
|
||||
minimap.leafletMap.addCallbackAndRunD(leaflet => {
|
||||
const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2))
|
||||
options.features.GetTilesPerLayerWithin(bounds, tile => {
|
||||
if(tile.layer.layerDef.minzoom > l.zoom){
|
||||
if (tile.layer.layerDef.minzoom > l.zoom) {
|
||||
return
|
||||
}
|
||||
new ShowDataLayer(
|
||||
|
@ -95,7 +96,7 @@ export default class ExportPDF {
|
|||
}
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
State.state.AddAllOverlaysToMap(minimap.leafletMap)
|
||||
|
@ -107,9 +108,8 @@ export default class ExportPDF {
|
|||
}
|
||||
|
||||
private async CreatePdf(minimap: MinimapObj) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
console.log("PDF creation started")
|
||||
const t = Translations.t.general.pdf;
|
||||
const layout = this._layout
|
||||
|
|
|
@ -12,10 +12,10 @@ export class AttributedImage extends Combine {
|
|||
let img: BaseUIElement;
|
||||
let attr: BaseUIElement
|
||||
img = new Img(imageInfo.url, false, {
|
||||
fallbackImage: imageInfo.provider === Mapillary.singleton ? "./assets/svg/blocked.svg" : undefined
|
||||
fallbackImage: imageInfo.provider === Mapillary.singleton ? "./assets/svg/blocked.svg" : undefined
|
||||
});
|
||||
attr = new Attribution(imageInfo.provider.GetAttributionFor(imageInfo.url),
|
||||
imageInfo.provider.SourceIcon(),
|
||||
imageInfo.provider.SourceIcon(),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -13,10 +13,10 @@ export default class Attribution extends VariableUiElement {
|
|||
}
|
||||
super(
|
||||
license.map((license: LicenseInfo) => {
|
||||
if(license === undefined){
|
||||
if (license === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
return new Combine([
|
||||
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
|
||||
|
||||
|
|
|
@ -15,19 +15,19 @@ export default class DeleteImage extends Toggle {
|
|||
const isDeletedBadge = Translations.t.image.isDeleted.Clone()
|
||||
.SetClass("rounded-full p-1")
|
||||
.SetStyle("color:white;background:#ff8c8c")
|
||||
.onClick(async() => {
|
||||
await State.state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, {
|
||||
changeType: "answer",
|
||||
theme: "test"
|
||||
}))
|
||||
.onClick(async () => {
|
||||
await State.state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, {
|
||||
changeType: "answer",
|
||||
theme: "test"
|
||||
}))
|
||||
});
|
||||
|
||||
const deleteButton = Translations.t.image.doDelete.Clone()
|
||||
.SetClass("block w-full pl-4 pr-4")
|
||||
.SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;")
|
||||
.onClick( async() => {
|
||||
await State.state?.changes?.applyAction(
|
||||
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data,{
|
||||
.onClick(async () => {
|
||||
await State.state?.changes?.applyAction(
|
||||
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, {
|
||||
changeType: "answer",
|
||||
theme: "test"
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@ import ImageProvider from "../../Logic/ImageProviders/ImageProvider";
|
|||
|
||||
export class ImageCarousel extends Toggle {
|
||||
|
||||
constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider }[]>,
|
||||
constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider }[]>,
|
||||
tags: UIEventSource<any>,
|
||||
keys: string[]) {
|
||||
const uiElements = images.map((imageURLS: { key: string, url: string, provider: ImageProvider }[]) => {
|
||||
|
|
|
@ -16,13 +16,13 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
|||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
|
||||
|
||||
|
||||
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
|
||||
|
||||
|
||||
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image", text: string = undefined) {
|
||||
const perId = ImageUploadFlow.uploadCountsPerId
|
||||
const id = tagsSource.data.id
|
||||
if(!perId.has(id)){
|
||||
if (!perId.has(id)) {
|
||||
perId.set(id, new UIEventSource<number>(0))
|
||||
}
|
||||
const uploadedCount = perId.get(id)
|
||||
|
@ -39,7 +39,7 @@ export class ImageUploadFlow extends Toggle {
|
|||
key = imagePrefix + ":" + freeIndex;
|
||||
}
|
||||
console.log("Adding image:" + key, url);
|
||||
uploadedCount.data ++
|
||||
uploadedCount.data++
|
||||
uploadedCount.ping()
|
||||
Promise.resolve(State.state.changes
|
||||
.applyAction(new ChangeTagAction(
|
||||
|
@ -50,17 +50,17 @@ export class ImageUploadFlow extends Toggle {
|
|||
}
|
||||
)))
|
||||
})
|
||||
|
||||
|
||||
const licensePicker = new LicensePicker()
|
||||
|
||||
const t = Translations.t.image;
|
||||
|
||||
let labelContent : BaseUIElement
|
||||
if(text === undefined) {
|
||||
labelContent = Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3 text-4xl ")
|
||||
}else{
|
||||
labelContent = new FixedUiElement(text).SetClass("block align-middle mt-1 ml-3 text-2xl ")
|
||||
}
|
||||
|
||||
let labelContent: BaseUIElement
|
||||
if (text === undefined) {
|
||||
labelContent = Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3 text-4xl ")
|
||||
} else {
|
||||
labelContent = new FixedUiElement(text).SetClass("block align-middle mt-1 ml-3 text-2xl ")
|
||||
}
|
||||
const label = new Combine([
|
||||
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "),
|
||||
labelContent
|
||||
|
@ -74,17 +74,17 @@ export class ImageUploadFlow extends Toggle {
|
|||
|
||||
|
||||
for (var i = 0; i < filelist.length; i++) {
|
||||
const sizeInBytes= filelist[i].size
|
||||
const sizeInBytes = filelist[i].size
|
||||
console.log(filelist[i].name + " has a size of " + sizeInBytes + " Bytes");
|
||||
if(sizeInBytes > uploader.maxFileSizeInMegabytes * 1000000){
|
||||
if (sizeInBytes > uploader.maxFileSizeInMegabytes * 1000000) {
|
||||
alert(Translations.t.image.toBig.Subs({
|
||||
actual_size: (Math.floor(sizeInBytes / 1000000)) + "MB",
|
||||
max_size: uploader.maxFileSizeInMegabytes+"MB"
|
||||
max_size: uploader.maxFileSizeInMegabytes + "MB"
|
||||
}).txt)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log("Received images from the user, starting upload")
|
||||
const license = licensePicker.GetValue()?.data ?? "CC0"
|
||||
|
||||
|
@ -114,31 +114,31 @@ export class ImageUploadFlow extends Toggle {
|
|||
|
||||
const uploadFlow: BaseUIElement = new Combine([
|
||||
new VariableUiElement(uploader.queue.map(q => q.length).map(l => {
|
||||
if(l == 0){
|
||||
if (l == 0) {
|
||||
return undefined;
|
||||
}
|
||||
if(l == 1){
|
||||
return t.uploadingPicture.Clone().SetClass("alert")
|
||||
}else{
|
||||
if (l == 1) {
|
||||
return t.uploadingPicture.Clone().SetClass("alert")
|
||||
} else {
|
||||
return t.uploadingMultiple.Subs({count: "" + l}).SetClass("alert")
|
||||
}
|
||||
})),
|
||||
new VariableUiElement(uploader.failed.map(q => q.length).map(l => {
|
||||
if(l==0){
|
||||
if (l == 0) {
|
||||
return undefined
|
||||
}
|
||||
return t.uploadFailed.Clone().SetClass("alert");
|
||||
})),
|
||||
new VariableUiElement(uploadedCount.map(l => {
|
||||
if(l == 0){
|
||||
return undefined;
|
||||
if (l == 0) {
|
||||
return undefined;
|
||||
}
|
||||
if(l == 1){
|
||||
if (l == 1) {
|
||||
return t.uploadDone.Clone().SetClass("thanks");
|
||||
}
|
||||
return t.uploadMultipleDone.Subs({count: l}).SetClass("thanks")
|
||||
})),
|
||||
|
||||
|
||||
fileSelector,
|
||||
Translations.t.image.respectPrivacy.Clone().SetStyle("font-size:small;"),
|
||||
licensePicker
|
||||
|
|
|
@ -15,9 +15,9 @@ export class FixedInputElement<T> extends InputElement<T> {
|
|||
comparator: ((t0: T, t1: T) => boolean) = undefined) {
|
||||
super();
|
||||
this._comparator = comparator ?? ((t0, t1) => t0 == t1);
|
||||
if(value instanceof UIEventSource){
|
||||
if (value instanceof UIEventSource) {
|
||||
this.value = value
|
||||
}else{
|
||||
} else {
|
||||
this.value = new UIEventSource<T>(value);
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ export default class LengthInput extends InputElement<string> {
|
|||
background: this.background,
|
||||
allowMoving: false,
|
||||
location: this._location,
|
||||
attribution:true,
|
||||
attribution: true,
|
||||
leafletOptions: {
|
||||
tap: true
|
||||
}
|
||||
|
|
|
@ -96,22 +96,22 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
|
|||
let min = undefined;
|
||||
let matchedWay = undefined;
|
||||
for (const feature of self._snapTo.data ?? []) {
|
||||
try{
|
||||
|
||||
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat])
|
||||
if (min === undefined) {
|
||||
min = nearestPointOnLine
|
||||
matchedWay = feature.feature;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
|
||||
if (min.properties.dist > nearestPointOnLine.properties.dist) {
|
||||
min = nearestPointOnLine
|
||||
matchedWay = feature.feature;
|
||||
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat])
|
||||
if (min === undefined) {
|
||||
min = nearestPointOnLine
|
||||
matchedWay = feature.feature;
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
}catch(e){
|
||||
console.log("Snapping to a nearest point failed for ", feature.feature,"due to ", e)
|
||||
if (min.properties.dist > nearestPointOnLine.properties.dist) {
|
||||
min = nearestPointOnLine
|
||||
matchedWay = feature.feature;
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Snapping to a nearest point failed for ", feature.feature, "due to ", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,8 +167,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
|
|||
installBounds(factor: number | BBox, showRange?: boolean): void {
|
||||
this.map.installBounds(factor, showRange)
|
||||
}
|
||||
|
||||
TakeScreenshot(): Promise<any> {
|
||||
return this.map.TakeScreenshot()
|
||||
return this.map.TakeScreenshot()
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
|
|
|
@ -18,16 +18,8 @@ export default class Toggle extends VariableUiElement {
|
|||
this.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
public ToggleOnClick(): Toggle {
|
||||
const self = this;
|
||||
this.onClick(() => {
|
||||
self.isEnabled.setData(!self.isEnabled.data);
|
||||
})
|
||||
return this;
|
||||
}
|
||||
|
||||
public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement {
|
||||
if(constructor === undefined){
|
||||
public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement {
|
||||
if (constructor === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new Toggle(
|
||||
|
@ -35,6 +27,14 @@ export default class Toggle extends VariableUiElement {
|
|||
undefined,
|
||||
condition
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
public ToggleOnClick(): Toggle {
|
||||
const self = this;
|
||||
this.onClick(() => {
|
||||
self.isEnabled.setData(!self.isEnabled.data);
|
||||
})
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -271,7 +271,7 @@ export default class ValidatedTextField {
|
|||
if (args[0]) {
|
||||
zoom = Number(args[0])
|
||||
if (isNaN(zoom)) {
|
||||
console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ",args[0]," (using 19 instead)")
|
||||
console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ", args[0], " (using 19 instead)")
|
||||
zoom = 19
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
|||
|
||||
export default class VariableInputElement<T> extends InputElement<T> {
|
||||
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
private readonly value: UIEventSource<T>;
|
||||
private readonly element: BaseUIElement
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
private readonly upstream: UIEventSource<InputElement<T>>;
|
||||
|
||||
constructor(upstream: UIEventSource<InputElement<T>>) {
|
||||
|
@ -23,13 +23,12 @@ export default class VariableInputElement<T> extends InputElement<T> {
|
|||
return this.value;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this.element.ConstructElement();
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this.upstream.data.IsValid(t);
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this.element.ConstructElement();
|
||||
}
|
||||
|
||||
}
|
|
@ -28,12 +28,12 @@ export default class OpeningHoursInput extends InputElement<string> {
|
|||
this._value = value;
|
||||
let valueWithoutPrefix = value
|
||||
if (prefix !== "" && postfix !== "") {
|
||||
|
||||
|
||||
valueWithoutPrefix = value.map(str => {
|
||||
if (str === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if(str === ""){
|
||||
if (str === "") {
|
||||
return ""
|
||||
}
|
||||
if (str.startsWith(prefix) && str.endsWith(postfix)) {
|
||||
|
@ -44,7 +44,7 @@ export default class OpeningHoursInput extends InputElement<string> {
|
|||
if (noPrefix === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if(noPrefix === ""){
|
||||
if (noPrefix === "") {
|
||||
return ""
|
||||
}
|
||||
if (noPrefix.startsWith(prefix) && noPrefix.endsWith(postfix)) {
|
||||
|
|
|
@ -16,9 +16,9 @@ export default class EditableTagRendering extends Toggle {
|
|||
constructor(tags: UIEventSource<any>,
|
||||
configuration: TagRenderingConfig,
|
||||
units: Unit [],
|
||||
options:{
|
||||
editMode? : UIEventSource<boolean> ,
|
||||
innerElementClasses?: string
|
||||
options: {
|
||||
editMode?: UIEventSource<boolean>,
|
||||
innerElementClasses?: string
|
||||
}
|
||||
) {
|
||||
|
||||
|
@ -28,7 +28,7 @@ export default class EditableTagRendering extends Toggle {
|
|||
const renderingIsShown = tags.map(tags =>
|
||||
configuration.IsKnown(tags) &&
|
||||
(configuration?.condition?.matchesProperties(tags) ?? true))
|
||||
|
||||
|
||||
super(
|
||||
new Lazy(() => {
|
||||
const editMode = options.editMode ?? new UIEventSource<boolean>(false)
|
||||
|
@ -40,8 +40,8 @@ export default class EditableTagRendering extends Toggle {
|
|||
renderingIsShown
|
||||
)
|
||||
}
|
||||
|
||||
private static CreateRendering(tags: UIEventSource<any>, configuration: TagRenderingConfig, units: Unit[], editMode: UIEventSource<boolean>) : BaseUIElement{
|
||||
|
||||
private static CreateRendering(tags: UIEventSource<any>, configuration: TagRenderingConfig, units: Unit[], editMode: UIEventSource<boolean>): BaseUIElement {
|
||||
const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration)
|
||||
answer.SetClass("w-full")
|
||||
let rendering = answer;
|
||||
|
|
|
@ -77,23 +77,23 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
renderingsForGroup.push(questionBox)
|
||||
} else {
|
||||
let classes = innerClasses
|
||||
let isHeader = renderingsForGroup.length === 0 && i > 0
|
||||
if(isHeader){
|
||||
let isHeader = renderingsForGroup.length === 0 && i > 0
|
||||
if (isHeader) {
|
||||
// This is the first element of a group!
|
||||
// It should act as header and be sticky
|
||||
classes= ""
|
||||
classes = ""
|
||||
}
|
||||
|
||||
const etr = new EditableTagRendering(tags, tr, layerConfig.units,{
|
||||
|
||||
const etr = new EditableTagRendering(tags, tr, layerConfig.units, {
|
||||
innerElementClasses: innerClasses
|
||||
})
|
||||
if(isHeader){
|
||||
if (isHeader) {
|
||||
etr.SetClass("sticky top-0")
|
||||
}
|
||||
renderingsForGroup.push(etr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
allRenderings.push(...renderingsForGroup)
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ export default class MoveWizard extends Toggle {
|
|||
changes: Changes,
|
||||
layoutToUse: LayoutConfig,
|
||||
allElements: ElementStorage
|
||||
}, options : MoveConfig) {
|
||||
}, options: MoveConfig) {
|
||||
|
||||
const t = Translations.t.move
|
||||
const loginButton = new Toggle(
|
||||
|
@ -64,7 +64,7 @@ export default class MoveWizard extends Toggle {
|
|||
minZoom: 6
|
||||
})
|
||||
}
|
||||
if(options.enableImproveAccuracy){
|
||||
if (options.enableImproveAccuracy) {
|
||||
reasons.push({
|
||||
text: t.reasons.reasonInaccurate,
|
||||
invitingText: t.inviteToMove.reasonInaccurate,
|
||||
|
@ -79,8 +79,8 @@ export default class MoveWizard extends Toggle {
|
|||
|
||||
const currentStep = new UIEventSource<"start" | "reason" | "pick_location" | "moved">("start")
|
||||
const moveReason = new UIEventSource<MoveReason>(undefined)
|
||||
let moveButton : BaseUIElement;
|
||||
if(reasons.length === 1){
|
||||
let moveButton: BaseUIElement;
|
||||
if (reasons.length === 1) {
|
||||
const reason = reasons[0]
|
||||
moveReason.setData(reason)
|
||||
moveButton = new SubtleButton(
|
||||
|
@ -89,7 +89,7 @@ export default class MoveWizard extends Toggle {
|
|||
).onClick(() => {
|
||||
currentStep.setData("pick_location")
|
||||
})
|
||||
}else{
|
||||
} else {
|
||||
moveButton = new SubtleButton(
|
||||
Svg.move_ui().SetStyle("height: 1.5rem; width: auto"),
|
||||
t.inviteToMove.generic
|
||||
|
@ -97,7 +97,7 @@ export default class MoveWizard extends Toggle {
|
|||
currentStep.setData("reason")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
const moveAgainButton = new SubtleButton(
|
||||
Svg.move_ui(),
|
||||
|
@ -107,8 +107,6 @@ export default class MoveWizard extends Toggle {
|
|||
})
|
||||
|
||||
|
||||
|
||||
|
||||
const selectReason = new Combine(reasons.map(r => new SubtleButton(r.icon, r.text).onClick(() => {
|
||||
moveReason.setData(r)
|
||||
currentStep.setData("pick_location")
|
||||
|
@ -129,16 +127,16 @@ export default class MoveWizard extends Toggle {
|
|||
})
|
||||
|
||||
let background: string[]
|
||||
if(typeof reason.background == "string"){
|
||||
if (typeof reason.background == "string") {
|
||||
background = [reason.background]
|
||||
}else{
|
||||
} else {
|
||||
background = reason.background
|
||||
}
|
||||
|
||||
|
||||
const locationInput = new LocationInput({
|
||||
minZoom: reason.minZoom,
|
||||
centerLocation: loc,
|
||||
mapBackground:AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background))
|
||||
mapBackground: AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background))
|
||||
})
|
||||
|
||||
if (reason.lockBounds) {
|
||||
|
@ -198,8 +196,8 @@ export default class MoveWizard extends Toggle {
|
|||
moveDisallowedReason.setData(t.isWay)
|
||||
} else if (id.startsWith("relation")) {
|
||||
moveDisallowedReason.setData(t.isRelation)
|
||||
} else if(id.indexOf("-") < 0) {
|
||||
|
||||
} else if (id.indexOf("-") < 0) {
|
||||
|
||||
OsmObject.DownloadReferencingWays(id).then(referencing => {
|
||||
if (referencing.length > 0) {
|
||||
console.log("Got a referencing way, move not allowed")
|
||||
|
@ -207,7 +205,7 @@ export default class MoveWizard extends Toggle {
|
|||
}
|
||||
})
|
||||
OsmObject.DownloadReferencingRelations(id).then(partOf => {
|
||||
if(partOf.length > 0){
|
||||
if (partOf.length > 0) {
|
||||
moveDisallowedReason.setData(t.partOfRelation)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -32,6 +32,7 @@ export interface MultiApplyParams {
|
|||
|
||||
class MultiApplyExecutor {
|
||||
|
||||
private static executorCache = new Map<string, MultiApplyExecutor>()
|
||||
private readonly originalValues = new Map<string, string>()
|
||||
private readonly params: MultiApplyParams;
|
||||
|
||||
|
@ -48,7 +49,7 @@ class MultiApplyExecutor {
|
|||
const self = this;
|
||||
const relevantValues = p.tagsSource.map(tags => {
|
||||
const currentValues = p.keysToApply.map(key => tags[key])
|
||||
// By stringifying, we have a very clear ping when they changec
|
||||
// By stringifying, we have a very clear ping when they changec
|
||||
return JSON.stringify(currentValues);
|
||||
})
|
||||
relevantValues.addCallbackD(_ => {
|
||||
|
@ -57,6 +58,15 @@ class MultiApplyExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
public static GetApplicator(id: string, params: MultiApplyParams): MultiApplyExecutor {
|
||||
if (MultiApplyExecutor.executorCache.has(id)) {
|
||||
return MultiApplyExecutor.executorCache.get(id)
|
||||
}
|
||||
const applicator = new MultiApplyExecutor(params)
|
||||
MultiApplyExecutor.executorCache.set(id, applicator)
|
||||
return applicator
|
||||
}
|
||||
|
||||
public applyTaggingOnOtherFeatures() {
|
||||
console.log("Multi-applying changes...")
|
||||
const featuresToChange = this.params.featureIds.data
|
||||
|
@ -103,17 +113,6 @@ class MultiApplyExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
private static executorCache = new Map<string, MultiApplyExecutor>()
|
||||
|
||||
public static GetApplicator(id: string, params: MultiApplyParams): MultiApplyExecutor {
|
||||
if (MultiApplyExecutor.executorCache.has(id)) {
|
||||
return MultiApplyExecutor.executorCache.get(id)
|
||||
}
|
||||
const applicator = new MultiApplyExecutor(params)
|
||||
MultiApplyExecutor.executorCache.set(id, applicator)
|
||||
return applicator
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class MultiApply extends Toggle {
|
||||
|
|
|
@ -16,7 +16,7 @@ import Lazy from "../Base/Lazy";
|
|||
export default class QuestionBox extends VariableUiElement {
|
||||
|
||||
constructor(tagsSource: UIEventSource<any>, tagRenderings: TagRenderingConfig[], units: Unit[]) {
|
||||
|
||||
|
||||
const skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
||||
|
||||
tagRenderings = tagRenderings
|
||||
|
@ -31,20 +31,20 @@ export default class QuestionBox extends VariableUiElement {
|
|||
const tagRenderingQuestions = tagRenderings
|
||||
.map((tagRendering, i) =>
|
||||
new Lazy(() => new TagRenderingQuestion(tagsSource, tagRendering,
|
||||
{
|
||||
units: units,
|
||||
afterSave: () => {
|
||||
// We save and indicate progress by pinging and recalculating
|
||||
skippedQuestions.ping();
|
||||
},
|
||||
cancelButton: Translations.t.general.skip.Clone()
|
||||
.SetClass("btn btn-secondary mr-3")
|
||||
.onClick(() => {
|
||||
skippedQuestions.data.push(i);
|
||||
{
|
||||
units: units,
|
||||
afterSave: () => {
|
||||
// We save and indicate progress by pinging and recalculating
|
||||
skippedQuestions.ping();
|
||||
})
|
||||
}
|
||||
)));
|
||||
},
|
||||
cancelButton: Translations.t.general.skip.Clone()
|
||||
.SetClass("btn btn-secondary mr-3")
|
||||
.onClick(() => {
|
||||
skippedQuestions.data.push(i);
|
||||
skippedQuestions.ping();
|
||||
})
|
||||
}
|
||||
)));
|
||||
|
||||
const skippedQuestionsButton = Translations.t.general.skippedQuestions
|
||||
.onClick(() => {
|
||||
|
|
|
@ -23,7 +23,7 @@ export default class SplitRoadWizard extends Toggle {
|
|||
source: {osmTags: "_cutposition=yes"},
|
||||
mapRendering: [
|
||||
{
|
||||
location: ["point","centroid"],
|
||||
location: ["point", "centroid"],
|
||||
icon: {render: "circle:white;./assets/svg/scissors.svg"},
|
||||
iconSize: {render: "30,30,center"}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||
|
@ -20,6 +19,7 @@ We don't actually import it here. It is imported in the 'MinimapImplementation'-
|
|||
*/
|
||||
export default class ShowDataLayer {
|
||||
|
||||
private static dataLayerIds = 0
|
||||
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||
private readonly _enablePopups: boolean;
|
||||
private readonly _features: RenderingMultiPlexerFeatureSource
|
||||
|
@ -30,7 +30,6 @@ export default class ShowDataLayer {
|
|||
private _cleanCount = 0;
|
||||
private geoLayer = undefined;
|
||||
private isDirty = false;
|
||||
|
||||
/**
|
||||
* If the selected element triggers, this is used to lookup the correct layer and to open the popup
|
||||
* Used to avoid a lot of callbacks on the selected element
|
||||
|
@ -39,9 +38,7 @@ export default class ShowDataLayer {
|
|||
* @private
|
||||
*/
|
||||
private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>()
|
||||
|
||||
private readonly showDataLayerid: number;
|
||||
private static dataLayerIds = 0
|
||||
|
||||
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
|
||||
this._leafletMap = options.leafletMap;
|
||||
|
@ -161,24 +158,24 @@ export default class ShowDataLayer {
|
|||
const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource<any>(feat.properties);
|
||||
let offsettedLine;
|
||||
tagsSource
|
||||
.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags))
|
||||
.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags))
|
||||
.withEqualityStabilized((a, b) => {
|
||||
if(a === b){
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
if(a === undefined || b === undefined){
|
||||
if (a === undefined || b === undefined) {
|
||||
return false
|
||||
}
|
||||
return a.offset === b.offset && a.color === b.color && a.weight === b.weight && a.dashArray === b.dashArray
|
||||
})
|
||||
.addCallbackAndRunD(lineStyle => {
|
||||
if (offsettedLine !== undefined) {
|
||||
self.geoLayer.removeLayer(offsettedLine)
|
||||
}
|
||||
offsettedLine = L.polyline(coords, lineStyle);
|
||||
this.postProcessFeature(feat, offsettedLine)
|
||||
offsettedLine.addTo(this.geoLayer)
|
||||
})
|
||||
if (offsettedLine !== undefined) {
|
||||
self.geoLayer.removeLayer(offsettedLine)
|
||||
}
|
||||
offsettedLine = L.polyline(coords, lineStyle);
|
||||
this.postProcessFeature(feat, offsettedLine)
|
||||
offsettedLine.addTo(this.geoLayer)
|
||||
})
|
||||
} else {
|
||||
this.geoLayer.addData(feat);
|
||||
}
|
||||
|
@ -192,7 +189,7 @@ export default class ShowDataLayer {
|
|||
const bounds = this.geoLayer.getBounds()
|
||||
mp.fitBounds(bounds, {animate: false})
|
||||
} catch (e) {
|
||||
console.debug("Invalid bounds",e)
|
||||
console.debug("Invalid bounds", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,7 +289,7 @@ export default class ShowDataLayer {
|
|||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Add the feature to the index to open the popup when needed
|
||||
this.leafletLayersPerId.set(feature.properties.id + feature.geometry.type, {
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import * as L from "leaflet";
|
||||
|
||||
export default class ShowOverlayLayer {
|
||||
|
||||
public static implementation: (config: TilesourceConfig,
|
||||
leafletMap: UIEventSource<any>,
|
||||
isShown?: UIEventSource<boolean>) => void;
|
||||
|
||||
|
||||
constructor(config: TilesourceConfig,
|
||||
leafletMap: UIEventSource<any>,
|
||||
isShown: UIEventSource<boolean> = undefined) {
|
||||
if(ShowOverlayLayer.implementation === undefined){
|
||||
if (ShowOverlayLayer.implementation === undefined) {
|
||||
throw "Call ShowOverlayLayerImplemenation.initialize() first before using this"
|
||||
}
|
||||
ShowOverlayLayer.implementation(config, leafletMap, isShown)
|
||||
ShowOverlayLayer.implementation(config, leafletMap, isShown)
|
||||
}
|
||||
}
|
|
@ -4,14 +4,14 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import ShowOverlayLayer from "./ShowOverlayLayer";
|
||||
|
||||
export default class ShowOverlayLayerImplementation {
|
||||
|
||||
public static Implement(){
|
||||
|
||||
public static Implement() {
|
||||
ShowOverlayLayer.implementation = ShowOverlayLayerImplementation.AddToMap
|
||||
}
|
||||
|
||||
|
||||
public static AddToMap(config: TilesourceConfig,
|
||||
leafletMap: UIEventSource<any>,
|
||||
isShown: UIEventSource<boolean> = undefined){
|
||||
isShown: UIEventSource<boolean> = undefined) {
|
||||
leafletMap.map(leaflet => {
|
||||
if (leaflet === undefined) {
|
||||
return;
|
||||
|
@ -41,5 +41,5 @@ export default class ShowOverlayLayerImplementation {
|
|||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -6,6 +6,7 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
|
|||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
import * as clusterstyle from "../../assets/layers/cluster_style/cluster_style.json"
|
||||
|
||||
export default class ShowTileInfo {
|
||||
public static readonly styling = new LayerConfig(
|
||||
clusterstyle, "tileinfo", true)
|
||||
|
|
|
@ -10,6 +10,12 @@ import FilteredLayer from "../../Models/FilteredLayer";
|
|||
* A feature source containing but a single feature, which keeps stats about a tile
|
||||
*/
|
||||
export class TileHierarchyAggregator implements FeatureSource {
|
||||
private static readonly empty = []
|
||||
public totalValue: number = 0
|
||||
public showCount: number = 0
|
||||
public hiddenCount: number = 0
|
||||
public readonly features = new UIEventSource<{ feature: any, freshness: Date }[]>(TileHierarchyAggregator.empty)
|
||||
public readonly name;
|
||||
private _parent: TileHierarchyAggregator;
|
||||
private _root: TileHierarchyAggregator;
|
||||
private _z: number;
|
||||
|
@ -17,21 +23,12 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
private _y: number;
|
||||
private _tileIndex: number
|
||||
private _counter: SingleTileCounter
|
||||
|
||||
private _subtiles: [TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator] = [undefined, undefined, undefined, undefined]
|
||||
public totalValue: number = 0
|
||||
public showCount: number = 0
|
||||
public hiddenCount: number = 0
|
||||
|
||||
private static readonly empty = []
|
||||
public readonly features = new UIEventSource<{ feature: any, freshness: Date }[]>(TileHierarchyAggregator.empty)
|
||||
public readonly name;
|
||||
|
||||
private readonly featuresStatic = []
|
||||
private readonly featureProperties: { count: string, kilocount: string, tileId: string, id: string, showCount: string, totalCount: string };
|
||||
private readonly _state: { filteredLayers: UIEventSource<FilteredLayer[]> };
|
||||
private readonly updateSignal = new UIEventSource<any>(undefined)
|
||||
|
||||
|
||||
private constructor(parent: TileHierarchyAggregator,
|
||||
state: {
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
|
@ -45,7 +42,7 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
this._y = y;
|
||||
this._tileIndex = Tiles.tile_index(z, x, y)
|
||||
this.name = "Count(" + this._tileIndex + ")"
|
||||
|
||||
|
||||
const totals = {
|
||||
id: "" + this._tileIndex,
|
||||
tileId: "" + this._tileIndex,
|
||||
|
@ -87,6 +84,10 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
this.featuresStatic.push({feature: box, freshness: now})
|
||||
}
|
||||
|
||||
public static createHierarchy(state: { filteredLayers: UIEventSource<FilteredLayer[]> }) {
|
||||
return new TileHierarchyAggregator(undefined, state, 0, 0, 0)
|
||||
}
|
||||
|
||||
public getTile(tileIndex): TileHierarchyAggregator {
|
||||
if (tileIndex === this._tileIndex) {
|
||||
return this;
|
||||
|
@ -103,6 +104,61 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
return this._subtiles[subtileIndex]?.getTile(tileIndex)
|
||||
}
|
||||
|
||||
public addTile(source: FeatureSourceForLayer & Tiled) {
|
||||
const self = this;
|
||||
if (source.tileIndex === this._tileIndex) {
|
||||
if (this._counter === undefined) {
|
||||
this._counter = new SingleTileCounter(this._tileIndex)
|
||||
this._counter.countsPerLayer.addCallbackAndRun(_ => self.update())
|
||||
}
|
||||
this._counter.addTileCount(source)
|
||||
} else {
|
||||
|
||||
// We have to give it to one of the subtiles
|
||||
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex)
|
||||
while (tileZ - 1 > this._z) {
|
||||
tileX = Math.floor(tileX / 2)
|
||||
tileY = Math.floor(tileY / 2)
|
||||
tileZ--
|
||||
}
|
||||
const xDiff = tileX - (2 * this._x)
|
||||
const yDiff = tileY - (2 * this._y)
|
||||
|
||||
const subtileIndex = yDiff * 2 + xDiff;
|
||||
if (this._subtiles[subtileIndex] === undefined) {
|
||||
this._subtiles[subtileIndex] = new TileHierarchyAggregator(this, this._state, tileZ, tileX, tileY)
|
||||
}
|
||||
this._subtiles[subtileIndex].addTile(source)
|
||||
}
|
||||
this.updateSignal.setData(source)
|
||||
}
|
||||
|
||||
getCountsForZoom(clusteringConfig: { maxZoom: number }, locationControl: UIEventSource<{ zoom: number }>, cutoff: number = 0): FeatureSource {
|
||||
const self = this
|
||||
const empty = []
|
||||
const features = locationControl.map(loc => loc.zoom).map(targetZoom => {
|
||||
if (targetZoom - 1 > clusteringConfig.maxZoom) {
|
||||
return empty
|
||||
}
|
||||
|
||||
const features = []
|
||||
self.visitSubTiles(aggr => {
|
||||
if (aggr.showCount < cutoff) {
|
||||
return false
|
||||
}
|
||||
if (aggr._z === targetZoom) {
|
||||
features.push(...aggr.features.data)
|
||||
return false
|
||||
}
|
||||
return aggr._z <= targetZoom;
|
||||
})
|
||||
|
||||
return features
|
||||
}, [this.updateSignal.stabilized(500)])
|
||||
|
||||
return new StaticFeatureSource(features, true);
|
||||
}
|
||||
|
||||
private update() {
|
||||
const newMap = new Map<string, number>()
|
||||
let total = 0
|
||||
|
@ -162,71 +218,12 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
}
|
||||
}
|
||||
|
||||
public addTile(source: FeatureSourceForLayer & Tiled) {
|
||||
const self = this;
|
||||
if (source.tileIndex === this._tileIndex) {
|
||||
if (this._counter === undefined) {
|
||||
this._counter = new SingleTileCounter(this._tileIndex)
|
||||
this._counter.countsPerLayer.addCallbackAndRun(_ => self.update())
|
||||
}
|
||||
this._counter.addTileCount(source)
|
||||
} else {
|
||||
|
||||
// We have to give it to one of the subtiles
|
||||
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex)
|
||||
while (tileZ - 1 > this._z) {
|
||||
tileX = Math.floor(tileX / 2)
|
||||
tileY = Math.floor(tileY / 2)
|
||||
tileZ--
|
||||
}
|
||||
const xDiff = tileX - (2 * this._x)
|
||||
const yDiff = tileY - (2 * this._y)
|
||||
|
||||
const subtileIndex = yDiff * 2 + xDiff;
|
||||
if (this._subtiles[subtileIndex] === undefined) {
|
||||
this._subtiles[subtileIndex] = new TileHierarchyAggregator(this, this._state, tileZ, tileX, tileY)
|
||||
}
|
||||
this._subtiles[subtileIndex].addTile(source)
|
||||
}
|
||||
this.updateSignal.setData(source)
|
||||
}
|
||||
|
||||
public static createHierarchy(state: { filteredLayers: UIEventSource<FilteredLayer[]> }) {
|
||||
return new TileHierarchyAggregator(undefined, state, 0, 0, 0)
|
||||
}
|
||||
|
||||
private visitSubTiles(f: (aggr: TileHierarchyAggregator) => boolean) {
|
||||
const visitFurther = f(this)
|
||||
if (visitFurther) {
|
||||
this._subtiles.forEach(tile => tile?.visitSubTiles(f))
|
||||
}
|
||||
}
|
||||
|
||||
getCountsForZoom(clusteringConfig: { maxZoom: number }, locationControl: UIEventSource<{ zoom: number }>, cutoff: number = 0): FeatureSource {
|
||||
const self = this
|
||||
const empty = []
|
||||
const features = locationControl.map(loc => loc.zoom).map(targetZoom => {
|
||||
if (targetZoom - 1 > clusteringConfig.maxZoom) {
|
||||
return empty
|
||||
}
|
||||
|
||||
const features = []
|
||||
self.visitSubTiles(aggr => {
|
||||
if (aggr.showCount < cutoff) {
|
||||
return false
|
||||
}
|
||||
if (aggr._z === targetZoom) {
|
||||
features.push(...aggr.features.data)
|
||||
return false
|
||||
}
|
||||
return aggr._z <= targetZoom;
|
||||
})
|
||||
|
||||
return features
|
||||
}, [this.updateSignal.stabilized(500)])
|
||||
|
||||
return new StaticFeatureSource(features, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -236,11 +233,10 @@ class SingleTileCounter implements Tiled {
|
|||
public readonly bbox: BBox;
|
||||
public readonly tileIndex: number;
|
||||
public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>())
|
||||
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
|
||||
public readonly z: number
|
||||
public readonly x: number
|
||||
public readonly y: number
|
||||
|
||||
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
|
||||
|
||||
constructor(tileIndex: number) {
|
||||
this.tileIndex = tileIndex
|
||||
|
|
|
@ -33,13 +33,10 @@ import AllKnownLayers from "../Customizations/AllKnownLayers";
|
|||
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
|
||||
import Link from "./Base/Link";
|
||||
import List from "./Base/List";
|
||||
import {OsmConnection} from "../Logic/Osm/OsmConnection";
|
||||
import {SubtleButton} from "./Base/SubtleButton";
|
||||
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction";
|
||||
import {And} from "../Logic/Tags/And";
|
||||
import Toggle from "./Input/Toggle";
|
||||
import Img from "./Base/Img";
|
||||
import FilteredLayer from "../Models/FilteredLayer";
|
||||
import {DefaultGuiState} from "./DefaultGuiState";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
|
@ -568,22 +565,22 @@ export default class SpecialVisualizations {
|
|||
}
|
||||
const targetIdKey = args[3]
|
||||
const t = Translations.t.general.apply_button
|
||||
|
||||
|
||||
const tagsExplanation = new VariableUiElement(tagsToApply.map(tagsToApply => {
|
||||
const tagsStr = tagsToApply.map(t => t.asHumanString(false, true)).join("&");
|
||||
let el: BaseUIElement = new FixedUiElement(tagsStr)
|
||||
if(targetIdKey !== undefined){
|
||||
const targetId = tags.data[targetIdKey] ?? tags.data.id
|
||||
el = t.appliedOnAnotherObject.Subs({tags: tagsStr , id: targetId })
|
||||
if (targetIdKey !== undefined) {
|
||||
const targetId = tags.data[targetIdKey] ?? tags.data.id
|
||||
el = t.appliedOnAnotherObject.Subs({tags: tagsStr, id: targetId})
|
||||
}
|
||||
return el;
|
||||
}
|
||||
)).SetClass("subtle")
|
||||
|
||||
|
||||
const applied = new UIEventSource(false)
|
||||
const applyButton = new SubtleButton(image, new Combine([msg, tagsExplanation]).SetClass("flex flex-col"))
|
||||
.onClick(() => {
|
||||
const targetId = tags.data[ targetIdKey] ?? tags.data.id
|
||||
const targetId = tags.data[targetIdKey] ?? tags.data.id
|
||||
const changeAction = new ChangeTagAction(targetId,
|
||||
new And(tagsToApply.data),
|
||||
tags.data, // We pass in the tags of the selected element, not the tags of the target element!
|
||||
|
@ -596,11 +593,11 @@ export default class SpecialVisualizations {
|
|||
applied.setData(true)
|
||||
})
|
||||
|
||||
|
||||
|
||||
return new Toggle(
|
||||
new Toggle(
|
||||
t.isApplied.SetClass("thanks"),
|
||||
applyButton,
|
||||
t.isApplied.SetClass("thanks"),
|
||||
applyButton,
|
||||
applied
|
||||
)
|
||||
, undefined, state.osmConnection.isLoggedIn)
|
||||
|
|
|
@ -4,7 +4,6 @@ import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
|
|||
import {Translation} from "../i18n/Translation";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Loading from "../Base/Loading";
|
||||
import {Transform} from "stream";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Combine from "../Base/Combine";
|
||||
import Img from "../Base/Img";
|
||||
|
@ -16,6 +15,43 @@ import {Utils} from "../../Utils";
|
|||
|
||||
export default class WikidataPreviewBox extends VariableUiElement {
|
||||
|
||||
private static isHuman = [
|
||||
{p: 31/*is a*/, q: 5 /* human */},
|
||||
]
|
||||
// @ts-ignore
|
||||
private static extraProperties: {
|
||||
requires?: { p: number, q?: number }[],
|
||||
property: string,
|
||||
display: Translation | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>
|
||||
}[] = [
|
||||
{
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
property: "P21",
|
||||
display: new Map([
|
||||
['Q6581097', () => Svg.gender_male_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q6581072', () => Svg.gender_female_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q1097630', () => Svg.gender_inter_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q1052281', () => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transwomen'*/],
|
||||
['Q2449503', () => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transmen'*/],
|
||||
['Q48270', () => Svg.gender_queer_ui().SetStyle("width: 1rem; height: auto")]
|
||||
])
|
||||
},
|
||||
{
|
||||
property: "P569",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: new Translation({
|
||||
"*": "Born: {value}"
|
||||
})
|
||||
},
|
||||
{
|
||||
property: "P570",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: new Translation({
|
||||
"*": "Died: {value}"
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
constructor(wikidataId: UIEventSource<string>) {
|
||||
let inited = false;
|
||||
const wikidata = wikidataId
|
||||
|
@ -45,6 +81,7 @@ export default class WikidataPreviewBox extends VariableUiElement {
|
|||
}))
|
||||
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
public static WikidataResponsePreview(wikidata: WikidataResponse): BaseUIElement {
|
||||
let link = new Link(
|
||||
|
@ -57,7 +94,7 @@ export default class WikidataPreviewBox extends VariableUiElement {
|
|||
let info = new Combine([
|
||||
new Combine(
|
||||
[Translation.fromMap(wikidata.labels)?.SetClass("font-bold"),
|
||||
link]).SetClass("flex justify-between"),
|
||||
link]).SetClass("flex justify-between"),
|
||||
Translation.fromMap(wikidata.descriptions),
|
||||
WikidataPreviewBox.QuickFacts(wikidata)
|
||||
]).SetClass("flex flex-col link-underline")
|
||||
|
@ -80,87 +117,49 @@ export default class WikidataPreviewBox extends VariableUiElement {
|
|||
return info
|
||||
}
|
||||
|
||||
private static isHuman = [
|
||||
{p: 31/*is a*/, q: 5 /* human */},
|
||||
]
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
private static extraProperties: {
|
||||
requires?: { p: number, q?: number }[],
|
||||
property: string,
|
||||
display: Translation | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>
|
||||
}[] = [
|
||||
{
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
property: "P21",
|
||||
display: new Map([
|
||||
['Q6581097', () => Svg.gender_male_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q6581072', () => Svg.gender_female_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q1097630',() => Svg.gender_inter_ui().SetStyle("width: 1rem; height: auto")],
|
||||
['Q1052281',() => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transwomen'*/],
|
||||
['Q2449503',() => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transmen'*/],
|
||||
['Q48270',() => Svg.gender_queer_ui().SetStyle("width: 1rem; height: auto")]
|
||||
])
|
||||
},
|
||||
{
|
||||
property: "P569",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: new Translation({
|
||||
"*":"Born: {value}"
|
||||
})
|
||||
},
|
||||
{
|
||||
property: "P570",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: new Translation({
|
||||
"*":"Died: {value}"
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
public static QuickFacts(wikidata: WikidataResponse): BaseUIElement {
|
||||
|
||||
const els : BaseUIElement[] = []
|
||||
|
||||
const els: BaseUIElement[] = []
|
||||
for (const extraProperty of WikidataPreviewBox.extraProperties) {
|
||||
let hasAllRequirements = true
|
||||
for (const requirement of extraProperty.requires) {
|
||||
if(!wikidata.claims?.has("P"+requirement.p)){
|
||||
if (!wikidata.claims?.has("P" + requirement.p)) {
|
||||
hasAllRequirements = false;
|
||||
break
|
||||
}
|
||||
if(!wikidata.claims?.get("P"+requirement.p).has("Q"+requirement.q)){
|
||||
if (!wikidata.claims?.get("P" + requirement.p).has("Q" + requirement.q)) {
|
||||
hasAllRequirements = false;
|
||||
break
|
||||
}
|
||||
}
|
||||
if(!hasAllRequirements){
|
||||
if (!hasAllRequirements) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
const key = extraProperty.property
|
||||
const display = extraProperty.display
|
||||
const value: string[] = Array.from(wikidata.claims.get(key))
|
||||
if(value === undefined){
|
||||
if (value === undefined) {
|
||||
continue
|
||||
}
|
||||
if(display instanceof Translation){
|
||||
if (display instanceof Translation) {
|
||||
els.push(display.Subs({value: value.join(", ")}).SetClass("m-2"))
|
||||
continue
|
||||
}
|
||||
const constructors = Utils.NoNull(value.map(property => display.get(property)))
|
||||
const elems = constructors.map(v => {
|
||||
if(typeof v === "string"){
|
||||
if (typeof v === "string") {
|
||||
return new FixedUiElement(v)
|
||||
}else{
|
||||
} else {
|
||||
return v();
|
||||
}
|
||||
})
|
||||
els.push(new Combine(elems).SetClass("flex m-2"))
|
||||
}
|
||||
if(els.length === 0){
|
||||
if (els.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
return new Combine(els).SetClass("flex")
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ export default class WikipediaBox extends Combine {
|
|||
|
||||
}
|
||||
const wikidata = <WikidataResponse>maybewikidata["success"]
|
||||
if(wikidata === undefined){
|
||||
if (wikidata === undefined) {
|
||||
return "failed"
|
||||
}
|
||||
if (wikidata.wikisites.size === 0) {
|
||||
|
@ -118,13 +118,13 @@ export default class WikipediaBox extends Combine {
|
|||
return wp.failed.Clone().SetClass("alert p-4")
|
||||
}
|
||||
if (status[0] == "no page") {
|
||||
const [_, wd] = <[string, WikidataResponse]> status
|
||||
const [_, wd] = <[string, WikidataResponse]>status
|
||||
return new Combine([
|
||||
WikidataPreviewBox.WikidataResponsePreview(wd),
|
||||
wp.noWikipediaPage.Clone().SetClass("subtle")]).SetClass("flex flex-col p-4")
|
||||
}
|
||||
|
||||
const [pagetitle, language, wd] = <[string, string, WikidataResponse]> status
|
||||
const [pagetitle, language, wd] = <[string, string, WikidataResponse]>status
|
||||
return WikipediaBox.createContents(pagetitle, language, wd)
|
||||
|
||||
})
|
||||
|
@ -134,27 +134,27 @@ export default class WikipediaBox extends Combine {
|
|||
const titleElement = new VariableUiElement(wikiLink.map(state => {
|
||||
if (typeof state !== "string") {
|
||||
const [pagetitle, _] = state
|
||||
if(pagetitle === "no page"){
|
||||
const wd = <WikidataResponse> state[1]
|
||||
return new Title( Translation.fromMap(wd.labels),3)
|
||||
if (pagetitle === "no page") {
|
||||
const wd = <WikidataResponse>state[1]
|
||||
return new Title(Translation.fromMap(wd.labels), 3)
|
||||
}
|
||||
return new Title(pagetitle, 3)
|
||||
}
|
||||
//return new Title(Translations.t.general.wikipedia.wikipediaboxTitle.Clone(), 2)
|
||||
return new Title(wikidataId,3)
|
||||
return new Title(wikidataId, 3)
|
||||
|
||||
}))
|
||||
|
||||
const linkElement = new VariableUiElement(wikiLink.map(state => {
|
||||
if (typeof state !== "string") {
|
||||
const [pagetitle, language] = state
|
||||
if(pagetitle === "no page"){
|
||||
const wd = <WikidataResponse> state[1]
|
||||
return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "),
|
||||
"https://www.wikidata.org/wiki/"+wd.id
|
||||
if (pagetitle === "no page") {
|
||||
const wd = <WikidataResponse>state[1]
|
||||
return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "),
|
||||
"https://www.wikidata.org/wiki/" + wd.id
|
||||
, true)
|
||||
}
|
||||
|
||||
|
||||
const url = `https://${language}.wikipedia.org/wiki/${pagetitle}`
|
||||
return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "), url, true)
|
||||
}
|
||||
|
@ -202,7 +202,7 @@ export default class WikipediaBox extends Combine {
|
|||
return new Combine([
|
||||
quickFacts?.SetClass("border-2 border-grey rounded-lg m-1 mb-0"),
|
||||
new VariableUiElement(contents)
|
||||
.SetClass("block pl-6 pt-2")])
|
||||
.SetClass("block pl-6 pt-2")])
|
||||
}
|
||||
|
||||
}
|
|
@ -34,6 +34,38 @@ export class Translation extends BaseUIElement {
|
|||
return this.textFor(Translation.forcedLanguage ?? Locale.language.data)
|
||||
}
|
||||
|
||||
static ExtractAllTranslationsFrom(object: any, context = ""): { context: string, tr: Translation }[] {
|
||||
const allTranslations: { context: string, tr: Translation }[] = []
|
||||
for (const key in object) {
|
||||
const v = object[key]
|
||||
if (v === undefined || v === null) {
|
||||
continue
|
||||
}
|
||||
if (v instanceof Translation) {
|
||||
allTranslations.push({context: context + "." + key, tr: v})
|
||||
continue
|
||||
}
|
||||
if (typeof v === "object") {
|
||||
allTranslations.push(...Translation.ExtractAllTranslationsFrom(v, context + "." + key))
|
||||
|
||||
}
|
||||
}
|
||||
return allTranslations
|
||||
}
|
||||
|
||||
static fromMap(transl: Map<string, string>) {
|
||||
const translations = {}
|
||||
let hasTranslation = false;
|
||||
transl?.forEach((value, key) => {
|
||||
translations[key] = value
|
||||
hasTranslation = true
|
||||
})
|
||||
if (!hasTranslation) {
|
||||
return undefined
|
||||
}
|
||||
return new Translation(translations);
|
||||
}
|
||||
|
||||
public textFor(language: string): string {
|
||||
if (this.translations["*"]) {
|
||||
return this.translations["*"];
|
||||
|
@ -195,36 +227,4 @@ export class Translation extends BaseUIElement {
|
|||
}
|
||||
return allIcons.filter(icon => icon != undefined)
|
||||
}
|
||||
|
||||
static ExtractAllTranslationsFrom(object: any, context = ""): { context: string, tr: Translation }[] {
|
||||
const allTranslations: { context: string, tr: Translation }[] = []
|
||||
for (const key in object) {
|
||||
const v = object[key]
|
||||
if (v === undefined || v === null) {
|
||||
continue
|
||||
}
|
||||
if (v instanceof Translation) {
|
||||
allTranslations.push({context: context +"." + key, tr: v})
|
||||
continue
|
||||
}
|
||||
if (typeof v === "object") {
|
||||
allTranslations.push(...Translation.ExtractAllTranslationsFrom(v, context + "." + key))
|
||||
continue
|
||||
}
|
||||
}
|
||||
return allTranslations
|
||||
}
|
||||
|
||||
static fromMap(transl: Map<string, string>) {
|
||||
const translations = {}
|
||||
let hasTranslation = false;
|
||||
transl?.forEach((value, key) => {
|
||||
translations[key] = value
|
||||
hasTranslation = true
|
||||
})
|
||||
if(!hasTranslation){
|
||||
return undefined
|
||||
}
|
||||
return new Translation(translations);
|
||||
}
|
||||
}
|
|
@ -25,8 +25,8 @@ export default class Translations {
|
|||
if (t === undefined || t === null) {
|
||||
return undefined;
|
||||
}
|
||||
if(typeof t === "number"){
|
||||
t = ""+t
|
||||
if (typeof t === "number") {
|
||||
t = "" + t
|
||||
}
|
||||
if (typeof t === "string") {
|
||||
return new Translation({"*": t}, context);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue