forked from MapComplete/MapComplete
commit
a2a4071022
18 changed files with 276 additions and 134 deletions
|
@ -45,20 +45,6 @@ export default class SelectedFeatureHandler {
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
hash.addCallback(() => self.setSelectedElementFromHash())
|
hash.addCallback(() => self.setSelectedElementFromHash())
|
||||||
|
|
||||||
state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD((_) => {
|
|
||||||
// New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet
|
|
||||||
if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) {
|
|
||||||
// This is an invalid hash anyway
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (state.selectedElement.data !== undefined) {
|
|
||||||
// We already have something selected
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.setSelectedElementFromHash()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.initialLoad()
|
this.initialLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ export class OsmConnection {
|
||||||
private readonly _singlePage: boolean
|
private readonly _singlePage: boolean
|
||||||
private isChecking = false
|
private isChecking = false
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options?: {
|
||||||
dryRun?: UIEventSource<boolean>
|
dryRun?: UIEventSource<boolean>
|
||||||
fakeUser?: false | boolean
|
fakeUser?: false | boolean
|
||||||
oauth_token?: UIEventSource<string>
|
oauth_token?: UIEventSource<string>
|
||||||
|
@ -71,6 +71,7 @@ export class OsmConnection {
|
||||||
osmConfiguration?: "osm" | "osm-test"
|
osmConfiguration?: "osm" | "osm-test"
|
||||||
attemptLogin?: true | boolean
|
attemptLogin?: true | boolean
|
||||||
}) {
|
}) {
|
||||||
|
options = options ?? {}
|
||||||
this.fakeUser = options.fakeUser ?? false
|
this.fakeUser = options.fakeUser ?? false
|
||||||
this._singlePage = options.singlePage ?? true
|
this._singlePage = options.singlePage ?? true
|
||||||
this._oauth_config =
|
this._oauth_config =
|
||||||
|
|
|
@ -230,10 +230,12 @@ export abstract class Store<T> {
|
||||||
|
|
||||||
const newSource = new UIEventSource<T>(this.data)
|
const newSource = new UIEventSource<T>(this.data)
|
||||||
|
|
||||||
|
const self = this
|
||||||
this.addCallback((latestData) => {
|
this.addCallback((latestData) => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (this.data == latestData) {
|
if (self.data == latestData) {
|
||||||
// compare by reference
|
// compare by reference.
|
||||||
|
// Note that 'latestData' and 'self.data' are both from the same UIEVentSource, but both are dereferenced at a different time
|
||||||
newSource.setData(latestData)
|
newSource.setData(latestData)
|
||||||
}
|
}
|
||||||
}, millisToStabilize)
|
}, millisToStabilize)
|
||||||
|
|
|
@ -414,6 +414,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
||||||
map.on("contextmenu", function (e) {
|
map.on("contextmenu", function (e) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng })
|
lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng })
|
||||||
|
map.setZoom(map.getZoom() + 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,12 +37,13 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
||||||
featurePipeline: FeaturePipeline
|
featurePipeline: FeaturePipeline
|
||||||
backgroundLayer: UIEventSource<BaseLayer>
|
backgroundLayer: UIEventSource<BaseLayer>
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||||
} & UserRelatedState
|
} & UserRelatedState,
|
||||||
|
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
|
||||||
) {
|
) {
|
||||||
const layoutToUse = state.layoutToUse
|
const layoutToUse = state.layoutToUse
|
||||||
super(
|
super(
|
||||||
() => layoutToUse.title.Clone(),
|
() => layoutToUse.title.Clone(),
|
||||||
() => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, isShown),
|
() => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, isShown, guistate),
|
||||||
"welcome",
|
"welcome",
|
||||||
isShown
|
isShown
|
||||||
)
|
)
|
||||||
|
@ -60,12 +61,13 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||||
} & UserRelatedState,
|
} & UserRelatedState,
|
||||||
isShown: UIEventSource<boolean>,
|
isShown: UIEventSource<boolean>,
|
||||||
currentTab: UIEventSource<number>
|
currentTab: UIEventSource<number>,
|
||||||
|
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
|
||||||
): { header: string | BaseUIElement; content: BaseUIElement }[] {
|
): { header: string | BaseUIElement; content: BaseUIElement }[] {
|
||||||
const tabs: { header: string | BaseUIElement; content: BaseUIElement }[] = [
|
const tabs: { header: string | BaseUIElement; content: BaseUIElement }[] = [
|
||||||
{
|
{
|
||||||
header: `<img src='${state.layoutToUse.icon}'>`,
|
header: `<img src='${state.layoutToUse.icon}'>`,
|
||||||
content: new ThemeIntroductionPanel(isShown, currentTab, state),
|
content: new ThemeIntroductionPanel(isShown, currentTab, state, guistate),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -113,11 +115,12 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||||
} & UserRelatedState,
|
} & UserRelatedState,
|
||||||
currentTab: UIEventSource<number>,
|
currentTab: UIEventSource<number>,
|
||||||
isShown: UIEventSource<boolean>
|
isShown: UIEventSource<boolean>,
|
||||||
|
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
|
||||||
) {
|
) {
|
||||||
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab)
|
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab, guistate)
|
||||||
const tabsWithAboutMc = [
|
const tabsWithAboutMc = [
|
||||||
...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab),
|
...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab, guistate),
|
||||||
]
|
]
|
||||||
|
|
||||||
tabsWithAboutMc.push({
|
tabsWithAboutMc.push({
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Svg from "../../Svg"
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||||
import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs"
|
import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs"
|
||||||
|
import LoggedInUserIndicator from "../LoggedInUserIndicator"
|
||||||
|
|
||||||
export default class ThemeIntroductionPanel extends Combine {
|
export default class ThemeIntroductionPanel extends Combine {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -20,7 +21,8 @@ export default class ThemeIntroductionPanel extends Combine {
|
||||||
featureSwitchUserbadge: UIEventSource<boolean>
|
featureSwitchUserbadge: UIEventSource<boolean>
|
||||||
layoutToUse: LayoutConfig
|
layoutToUse: LayoutConfig
|
||||||
osmConnection: OsmConnection
|
osmConnection: OsmConnection
|
||||||
}
|
},
|
||||||
|
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
|
||||||
) {
|
) {
|
||||||
const t = Translations.t.general
|
const t = Translations.t.general
|
||||||
const layout = state.layoutToUse
|
const layout = state.layoutToUse
|
||||||
|
@ -36,9 +38,18 @@ export default class ThemeIntroductionPanel extends Combine {
|
||||||
})
|
})
|
||||||
.SetClass("only-on-mobile")
|
.SetClass("only-on-mobile")
|
||||||
|
|
||||||
|
const loggedInUserInfo = new LoggedInUserIndicator(state.osmConnection, {
|
||||||
|
firstLine: Translations.t.general.welcomeBack.Clone(),
|
||||||
|
})
|
||||||
|
if (guistate?.userInfoIsOpened) {
|
||||||
|
loggedInUserInfo.onClick(() => {
|
||||||
|
guistate.userInfoIsOpened.setData(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const loginStatus = new Toggle(
|
const loginStatus = new Toggle(
|
||||||
new LoginToggle(
|
new LoginToggle(
|
||||||
undefined,
|
loggedInUserInfo,
|
||||||
new Combine([
|
new Combine([
|
||||||
Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"),
|
Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"),
|
||||||
Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold"),
|
Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold"),
|
||||||
|
@ -60,7 +71,7 @@ export default class ThemeIntroductionPanel extends Combine {
|
||||||
]).SetClass("flex flex-col mt-2"),
|
]).SetClass("flex flex-col mt-2"),
|
||||||
|
|
||||||
toTheMap,
|
toTheMap,
|
||||||
loginStatus.SetClass("block"),
|
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
|
||||||
layout.descriptionTail?.Clone().SetClass("block mt-4"),
|
layout.descriptionTail?.Clone().SetClass("block mt-4"),
|
||||||
|
|
||||||
languagePicker?.SetClass("block mt-4"),
|
languagePicker?.SetClass("block mt-4"),
|
||||||
|
|
|
@ -140,12 +140,17 @@ class UserInformationMainPanel extends Combine {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class UserInformationPanel extends ScrollableFullScreen {
|
export default class UserInformationPanel extends ScrollableFullScreen {
|
||||||
constructor(state: {
|
constructor(
|
||||||
layoutToUse: LayoutConfig
|
state: {
|
||||||
osmConnection: OsmConnection
|
layoutToUse: LayoutConfig
|
||||||
locationControl: UIEventSource<Loc>
|
osmConnection: OsmConnection
|
||||||
}) {
|
locationControl: UIEventSource<Loc>
|
||||||
const isOpened = new UIEventSource<boolean>(false)
|
},
|
||||||
|
options?: {
|
||||||
|
isOpened?: UIEventSource<boolean>
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const isOpened = options?.isOpened ?? new UIEventSource<boolean>(false)
|
||||||
super(
|
super(
|
||||||
() => {
|
() => {
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
|
|
|
@ -33,7 +33,6 @@ import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||||
import { GeoLocationState } from "../Logic/State/GeoLocationState"
|
import { GeoLocationState } from "../Logic/State/GeoLocationState"
|
||||||
import Hotkeys from "./Base/Hotkeys"
|
import Hotkeys from "./Base/Hotkeys"
|
||||||
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
|
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
|
||||||
import { Translation } from "./i18n/Translation"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default MapComplete GUI initializer
|
* The default MapComplete GUI initializer
|
||||||
|
@ -205,7 +204,9 @@ export default class DefaultGUI {
|
||||||
const self = this
|
const self = this
|
||||||
new Combine([
|
new Combine([
|
||||||
Toggle.If(state.featureSwitchUserbadge, () => {
|
Toggle.If(state.featureSwitchUserbadge, () => {
|
||||||
const userInfo = new UserInformationPanel(state)
|
const userInfo = new UserInformationPanel(state, {
|
||||||
|
isOpened: guiState.userInfoIsOpened,
|
||||||
|
})
|
||||||
|
|
||||||
const mapControl = new MapControlButton(
|
const mapControl = new MapControlButton(
|
||||||
new VariableUiElement(
|
new VariableUiElement(
|
||||||
|
@ -219,7 +220,7 @@ export default class DefaultGUI {
|
||||||
{
|
{
|
||||||
dontStyle: true,
|
dontStyle: true,
|
||||||
}
|
}
|
||||||
).onClick(() => userInfo.Activate())
|
).onClick(() => guiState.userInfoIsOpened.setData(true))
|
||||||
|
|
||||||
return new LoginToggle(
|
return new LoginToggle(
|
||||||
mapControl,
|
mapControl,
|
||||||
|
@ -292,7 +293,12 @@ export default class DefaultGUI {
|
||||||
|
|
||||||
private InitWelcomeMessage(): BaseUIElement {
|
private InitWelcomeMessage(): BaseUIElement {
|
||||||
const isOpened = this.guiState.welcomeMessageIsOpened
|
const isOpened = this.guiState.welcomeMessageIsOpened
|
||||||
new FullWelcomePaneWithTabs(isOpened, this.guiState.welcomeMessageOpenedTab, this.state)
|
new FullWelcomePaneWithTabs(
|
||||||
|
isOpened,
|
||||||
|
this.guiState.welcomeMessageOpenedTab,
|
||||||
|
this.state,
|
||||||
|
this.guiState
|
||||||
|
)
|
||||||
|
|
||||||
// ?-Button on Desktop, opens panel with close-X.
|
// ?-Button on Desktop, opens panel with close-X.
|
||||||
const help = new MapControlButton(Svg.help_svg())
|
const help = new MapControlButton(Svg.help_svg())
|
||||||
|
|
|
@ -9,6 +9,7 @@ export class DefaultGuiState {
|
||||||
public readonly filterViewIsOpened: UIEventSource<boolean>
|
public readonly filterViewIsOpened: UIEventSource<boolean>
|
||||||
public readonly copyrightViewIsOpened: UIEventSource<boolean>
|
public readonly copyrightViewIsOpened: UIEventSource<boolean>
|
||||||
public readonly currentViewControlIsOpened: UIEventSource<boolean>
|
public readonly currentViewControlIsOpened: UIEventSource<boolean>
|
||||||
|
public readonly userInfoIsOpened: UIEventSource<boolean>
|
||||||
public readonly welcomeMessageOpenedTab: UIEventSource<number>
|
public readonly welcomeMessageOpenedTab: UIEventSource<number>
|
||||||
public readonly allFullScreenStates: UIEventSource<boolean>[] = []
|
public readonly allFullScreenStates: UIEventSource<boolean>[] = []
|
||||||
|
|
||||||
|
@ -43,8 +44,14 @@ export class DefaultGuiState {
|
||||||
this.currentViewControlIsOpened = QueryParameters.GetBooleanQueryParameter(
|
this.currentViewControlIsOpened = QueryParameters.GetBooleanQueryParameter(
|
||||||
"currentview-toggle",
|
"currentview-toggle",
|
||||||
false,
|
false,
|
||||||
"Whether or not the current view box is shown"
|
"Whether or not the current view box is shown (metalayer showing current view, allows to do calculate stats for all in view)"
|
||||||
)
|
)
|
||||||
|
this.userInfoIsOpened = QueryParameters.GetBooleanQueryParameter(
|
||||||
|
"userinfo-toggle",
|
||||||
|
false,
|
||||||
|
"Whether or not the user info is shown"
|
||||||
|
)
|
||||||
|
|
||||||
const states = {
|
const states = {
|
||||||
download: this.downloadControlIsOpened,
|
download: this.downloadControlIsOpened,
|
||||||
filters: this.filterViewIsOpened,
|
filters: this.filterViewIsOpened,
|
||||||
|
@ -66,7 +73,8 @@ export class DefaultGuiState {
|
||||||
this.filterViewIsOpened,
|
this.filterViewIsOpened,
|
||||||
this.copyrightViewIsOpened,
|
this.copyrightViewIsOpened,
|
||||||
this.welcomeMessageIsOpened,
|
this.welcomeMessageIsOpened,
|
||||||
this.currentViewControlIsOpened
|
this.currentViewControlIsOpened,
|
||||||
|
this.userInfoIsOpened
|
||||||
)
|
)
|
||||||
|
|
||||||
for (let i = 0; i < this.allFullScreenStates.length; i++) {
|
for (let i = 0; i < this.allFullScreenStates.length; i++) {
|
||||||
|
|
42
UI/LoggedInUserIndicator.ts
Normal file
42
UI/LoggedInUserIndicator.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||||
|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
|
import Svg from "../Svg"
|
||||||
|
import Img from "./Base/Img"
|
||||||
|
import Combine from "./Base/Combine"
|
||||||
|
import { FixedUiElement } from "./Base/FixedUiElement"
|
||||||
|
import BaseUIElement from "./BaseUIElement"
|
||||||
|
|
||||||
|
export default class LoggedInUserIndicator extends VariableUiElement {
|
||||||
|
constructor(
|
||||||
|
osmConnection: OsmConnection,
|
||||||
|
options?: {
|
||||||
|
size?: "small" | "medium" | "large"
|
||||||
|
firstLine?: BaseUIElement
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
options = options ?? {}
|
||||||
|
let size = "w-8 h-8 mr-2"
|
||||||
|
if (options.size == "medium") {
|
||||||
|
size = "w-16 h-16 mr-4"
|
||||||
|
} else if (options.size == "large") {
|
||||||
|
size = "w-32 h-32 mr-6"
|
||||||
|
}
|
||||||
|
super(
|
||||||
|
osmConnection.userDetails.mapD((ud) => {
|
||||||
|
let img = Svg.person_svg().SetClass(
|
||||||
|
"rounded-full border border-black overflow-hidden"
|
||||||
|
)
|
||||||
|
if (ud.img) {
|
||||||
|
img = new Img(ud.img)
|
||||||
|
}
|
||||||
|
let contents: BaseUIElement = new FixedUiElement(ud.name).SetClass("font-bold")
|
||||||
|
if (options?.firstLine) {
|
||||||
|
contents = new Combine([options.firstLine, contents]).SetClass("flex flex-col")
|
||||||
|
}
|
||||||
|
return new Combine([img.SetClass("rounded-full " + size), contents]).SetClass(
|
||||||
|
"flex items-center"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -176,6 +176,13 @@ export default class DeleteWizard extends Toggle {
|
||||||
undefined,
|
undefined,
|
||||||
isShown
|
isShown
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const self = this
|
||||||
|
confirm.addCallbackAndRunD((dialogIsOpened) => {
|
||||||
|
if (dialogIsOpened) {
|
||||||
|
self.ScrollIntoView()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private static constructConfirmButton(
|
private static constructConfirmButton(
|
||||||
|
|
|
@ -284,5 +284,13 @@ export default class MoveWizard extends Toggle {
|
||||||
]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200"),
|
]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200"),
|
||||||
moveDisallowedReason.map((r) => r === undefined)
|
moveDisallowedReason.map((r) => r === undefined)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const self = this
|
||||||
|
currentStep.addCallback((state) => {
|
||||||
|
if (state === "start") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.ScrollIntoView()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import BaseLayer from "../../Models/BaseLayer"
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||||
|
|
||||||
export default class SplitRoadWizard extends Combine {
|
export default class SplitRoadWizard extends Combine {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -54,6 +55,7 @@ export default class SplitRoadWizard extends Combine {
|
||||||
changes: Changes
|
changes: Changes
|
||||||
layoutToUse: LayoutConfig
|
layoutToUse: LayoutConfig
|
||||||
allElements: ElementStorage
|
allElements: ElementStorage
|
||||||
|
selectedElement: UIEventSource<any>
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const t = Translations.t.split
|
const t = Translations.t.split
|
||||||
|
@ -79,9 +81,6 @@ export default class SplitRoadWizard extends Combine {
|
||||||
hasBeenSplit
|
hasBeenSplit
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
splitButton.onClick(() => {
|
|
||||||
splitClicked.setData(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Only show the splitButton if logged in, else show login prompt
|
// Only show the splitButton if logged in, else show login prompt
|
||||||
const loginBtn = t.loginToSplit
|
const loginBtn = t.loginToSplit
|
||||||
|
@ -110,6 +109,9 @@ export default class SplitRoadWizard extends Combine {
|
||||||
// We throw away the old map and splitpoints, and create a new map from scratch
|
// We throw away the old map and splitpoints, and create a new map from scratch
|
||||||
splitPoints.setData([])
|
splitPoints.setData([])
|
||||||
leafletMap.setData(SplitRoadWizard.setupMapComponent(id, splitPoints, state))
|
leafletMap.setData(SplitRoadWizard.setupMapComponent(id, splitPoints, state))
|
||||||
|
|
||||||
|
// Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219
|
||||||
|
ScrollableFullScreen.collapse()
|
||||||
})
|
})
|
||||||
|
|
||||||
saveButton.SetClass("btn btn-primary mr-3")
|
saveButton.SetClass("btn btn-primary mr-3")
|
||||||
|
@ -147,6 +149,11 @@ export default class SplitRoadWizard extends Combine {
|
||||||
new Toggle(mapView, splitToggle, splitClicked),
|
new Toggle(mapView, splitToggle, splitClicked),
|
||||||
])
|
])
|
||||||
this.dialogIsOpened = splitClicked
|
this.dialogIsOpened = splitClicked
|
||||||
|
const self = this
|
||||||
|
splitButton.onClick(() => {
|
||||||
|
splitClicked.setData(true)
|
||||||
|
self.ScrollIntoView()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private static setupMapComponent(
|
private static setupMapComponent(
|
||||||
|
|
|
@ -4,8 +4,10 @@ import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||||
import { ElementStorage } from "../../Logic/ElementStorage"
|
import { ElementStorage } from "../../Logic/ElementStorage"
|
||||||
import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource"
|
import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource"
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||||
import { LeafletMouseEvent } from "leaflet"
|
import { LeafletMouseEvent, PathOptions } from "leaflet"
|
||||||
import Hash from "../../Logic/Web/Hash"
|
import Hash from "../../Logic/Web/Hash"
|
||||||
|
import { BBox } from "../../Logic/BBox"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
/*
|
/*
|
||||||
// import 'leaflet-polylineoffset';
|
// import 'leaflet-polylineoffset';
|
||||||
We don't actually import it here. It is imported in the 'MinimapImplementation'-class, which'll result in a patched 'L' object.
|
We don't actually import it here. It is imported in the 'MinimapImplementation'-class, which'll result in a patched 'L' object.
|
||||||
|
@ -47,6 +49,7 @@ export default class ShowDataLayerImplementation {
|
||||||
string,
|
string,
|
||||||
{ feature: any; activateFunc: (event: LeafletMouseEvent) => void }
|
{ feature: any; activateFunc: (event: LeafletMouseEvent) => void }
|
||||||
>()
|
>()
|
||||||
|
|
||||||
private readonly showDataLayerid: number
|
private readonly showDataLayerid: number
|
||||||
private readonly createPopup: (
|
private readonly createPopup: (
|
||||||
tags: UIEventSource<any>,
|
tags: UIEventSource<any>,
|
||||||
|
@ -81,7 +84,7 @@ export default class ShowDataLayerImplementation {
|
||||||
}
|
}
|
||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
options.leafletMap.addCallback((_) => {
|
options.leafletMap.addCallback(() => {
|
||||||
return self.update(options)
|
return self.update(options)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -112,6 +115,10 @@ export default class ShowDataLayerImplementation {
|
||||||
})
|
})
|
||||||
|
|
||||||
this._selectedElement?.addCallbackAndRunD((selected) => {
|
this._selectedElement?.addCallbackAndRunD((selected) => {
|
||||||
|
if (selected === undefined) {
|
||||||
|
ScrollableFullScreen.collapse()
|
||||||
|
return
|
||||||
|
}
|
||||||
self.openPopupOfSelectedElement(selected)
|
self.openPopupOfSelectedElement(selected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -171,17 +178,8 @@ export default class ShowDataLayerImplementation {
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
const data = {
|
|
||||||
type: "FeatureCollection",
|
this.geoLayer = new L.LayerGroup()
|
||||||
features: [],
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
this.geoLayer = L.geoJSON(data, {
|
|
||||||
style: (feature) => self.createStyleFor(feature),
|
|
||||||
pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng),
|
|
||||||
onEachFeature: (feature, leafletLayer) =>
|
|
||||||
self.postProcessFeature(feature, leafletLayer),
|
|
||||||
})
|
|
||||||
|
|
||||||
const selfLayer = this.geoLayer
|
const selfLayer = this.geoLayer
|
||||||
const allFeats = this._features.features.data
|
const allFeats = this._features.features.data
|
||||||
|
@ -189,6 +187,31 @@ export default class ShowDataLayerImplementation {
|
||||||
if (feat === undefined) {
|
if (feat === undefined) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Why not one geojson layer with _all_ features, and attaching a right-click onto every feature individually?
|
||||||
|
// Because that somehow doesn't work :(
|
||||||
|
const feature = feat
|
||||||
|
const geojsonLayer = L.geoJSON(feature, {
|
||||||
|
style: (feature) => <PathOptions>self.createStyleFor(feature),
|
||||||
|
pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng),
|
||||||
|
onEachFeature: (feature, leafletLayer) =>
|
||||||
|
self.postProcessFeature(feature, leafletLayer),
|
||||||
|
})
|
||||||
|
if (feature.geometry.type === "Point") {
|
||||||
|
geojsonLayer.on({
|
||||||
|
contextmenu: (e) => {
|
||||||
|
const o = self.leafletLayersPerId.get(feature?.properties?.id)
|
||||||
|
o?.activateFunc(<LeafletMouseEvent>e)
|
||||||
|
Utils.preventDefaultOnMouseEvent(e.originalEvent)
|
||||||
|
},
|
||||||
|
dblclick: (e) => {
|
||||||
|
const o = self.leafletLayersPerId.get(feature?.properties?.id)
|
||||||
|
o?.activateFunc(<LeafletMouseEvent>e)
|
||||||
|
Utils.preventDefaultOnMouseEvent(e.originalEvent)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.geoLayer.addLayer(geojsonLayer)
|
||||||
try {
|
try {
|
||||||
if (feat.geometry.type === "LineString") {
|
if (feat.geometry.type === "LineString") {
|
||||||
const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates)
|
const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates)
|
||||||
|
@ -229,7 +252,7 @@ export default class ShowDataLayerImplementation {
|
||||||
return self.geoLayer !== selfLayer
|
return self.geoLayer !== selfLayer
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.geoLayer.addData(feat)
|
geojsonLayer.addData(feat)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -242,14 +265,14 @@ export default class ShowDataLayerImplementation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.zoomToFeatures ?? false) {
|
if ((options.zoomToFeatures ?? false) && allFeats.length > 0) {
|
||||||
if (this.geoLayer.getLayers().length > 0) {
|
let bound = undefined
|
||||||
try {
|
for (const feat of allFeats) {
|
||||||
const bounds = this.geoLayer.getBounds()
|
const fbound = BBox.get(feat)
|
||||||
mp.fitBounds(bounds, { animate: false })
|
bound = bound?.unionWith(fbound) ?? fbound
|
||||||
} catch (e) {
|
}
|
||||||
console.debug("Invalid bounds", e)
|
if (bound !== undefined) {
|
||||||
}
|
mp.fitBounds(bound?.toLeaflet(), { animate: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,29 +335,7 @@ export default class ShowDataLayerImplementation {
|
||||||
icon: L.divIcon(style),
|
icon: L.divIcon(style),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
private createActivateFunction(feature, key: string, layer: LayerConfig): (event) => void {
|
||||||
/**
|
|
||||||
* Post processing - basically adding the popup
|
|
||||||
* @param feature
|
|
||||||
* @param leafletLayer
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private postProcessFeature(feature, leafletLayer: L.Evented) {
|
|
||||||
const layer: LayerConfig = this._layerToShow
|
|
||||||
if (layer.title === undefined || !this._enablePopups) {
|
|
||||||
// No popup action defined -> Don't do anything
|
|
||||||
// or probably a map in the popup - no popups needed!
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const key = feature.properties.id
|
|
||||||
if (this.leafletLayersPerId.has(key)) {
|
|
||||||
const activate = this.leafletLayersPerId.get(key)
|
|
||||||
leafletLayer.addEventListener("click", activate.activateFunc)
|
|
||||||
if (Hash.hash.data === key) {
|
|
||||||
activate.activateFunc(null)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let infobox: ScrollableFullScreen = undefined
|
let infobox: ScrollableFullScreen = undefined
|
||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
|
@ -354,17 +355,36 @@ export default class ShowDataLayerImplementation {
|
||||||
self._selectedElement.setData(
|
self._selectedElement.setData(
|
||||||
self.allElements.ContainingFeatures.get(feature.id) ?? feature
|
self.allElements.ContainingFeatures.get(feature.id) ?? feature
|
||||||
)
|
)
|
||||||
event?.originalEvent?.preventDefault()
|
}
|
||||||
event?.originalEvent?.stopPropagation()
|
return activate
|
||||||
event?.originalEvent?.stopImmediatePropagation()
|
}
|
||||||
if (event?.originalEvent) {
|
/**
|
||||||
// This is a total workaround, as 'preventDefault' and everything above seems to be not working
|
* Post processing - basically adding the popup
|
||||||
event.originalEvent["dismissed"] = true
|
* @param feature
|
||||||
}
|
* @param leafletLayer
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private postProcessFeature(feature, leafletLayer: L.Evented) {
|
||||||
|
const layer: LayerConfig = this._layerToShow
|
||||||
|
if (layer.title === undefined || !this._enablePopups) {
|
||||||
|
// No popup action defined -> Don't do anything
|
||||||
|
// or probably a map in the popup - no popups needed!
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const key = feature.properties.id
|
||||||
|
let activate: (event) => void
|
||||||
|
if (this.leafletLayersPerId.has(key)) {
|
||||||
|
activate = this.leafletLayersPerId.get(key).activateFunc
|
||||||
|
} else {
|
||||||
|
activate = this.createActivateFunction(feature, key, layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
leafletLayer.addEventListener("click", activate)
|
// We also have to open on rightclick, doubleclick, ... as users sometimes do this. See #1219
|
||||||
|
leafletLayer.on({
|
||||||
|
dblclick: activate,
|
||||||
|
contextmenu: activate,
|
||||||
|
click: activate,
|
||||||
|
})
|
||||||
// Add the feature to the index to open the popup when needed
|
// Add the feature to the index to open the popup when needed
|
||||||
this.leafletLayersPerId.set(key, {
|
this.leafletLayersPerId.set(key, {
|
||||||
feature: feature,
|
feature: feature,
|
||||||
|
|
12
Utils.ts
12
Utils.ts
|
@ -900,7 +900,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
url: string,
|
url: string,
|
||||||
maxCacheTimeMs: number,
|
maxCacheTimeMs: number,
|
||||||
headers?: any
|
headers?: any
|
||||||
): Promise<any | { error: string; url: string; statuscode?: number }> {
|
): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> {
|
||||||
const cached = Utils._download_cache.get(url)
|
const cached = Utils._download_cache.get(url)
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
if (new Date().getTime() - cached.timestamp <= maxCacheTimeMs) {
|
if (new Date().getTime() - cached.timestamp <= maxCacheTimeMs) {
|
||||||
|
@ -1074,6 +1074,16 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static preventDefaultOnMouseEvent(event: any) {
|
||||||
|
event?.originalEvent?.preventDefault()
|
||||||
|
event?.originalEvent?.stopPropagation()
|
||||||
|
event?.originalEvent?.stopImmediatePropagation()
|
||||||
|
if (event?.originalEvent) {
|
||||||
|
// This is a total workaround, as 'preventDefault' and everything above seems to be not working
|
||||||
|
event.originalEvent["dismissed"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static OsmChaLinkFor(daysInThePast, theme = undefined): string {
|
public static OsmChaLinkFor(daysInThePast, theme = undefined): string {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const lastWeek = new Date(now.getTime() - daysInThePast * 24 * 60 * 60 * 1000)
|
const lastWeek = new Date(now.getTime() - daysInThePast * 24 * 60 * 60 * 1000)
|
||||||
|
|
|
@ -624,6 +624,10 @@ video {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.\!relative {
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
.sticky {
|
.sticky {
|
||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
@ -807,18 +811,22 @@ video {
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-4 {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-3 {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mr-2 {
|
.mr-2 {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mr-4 {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-6 {
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.ml-4 {
|
.ml-4 {
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -827,10 +835,6 @@ video {
|
||||||
margin-bottom: 6rem;
|
margin-bottom: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-4 {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-2 {
|
.mb-2 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@ -839,6 +843,10 @@ video {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ml-3 {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.ml-12 {
|
.ml-12 {
|
||||||
margin-left: 3rem;
|
margin-left: 3rem;
|
||||||
}
|
}
|
||||||
|
@ -960,6 +968,18 @@ video {
|
||||||
height: 16rem;
|
height: 16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-8 {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-16 {
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-32 {
|
||||||
|
height: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-10 {
|
.h-10 {
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
@ -988,22 +1008,10 @@ video {
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-8 {
|
|
||||||
height: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-32 {
|
|
||||||
height: 8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-96 {
|
.h-96 {
|
||||||
height: 24rem;
|
height: 24rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-16 {
|
|
||||||
height: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-0 {
|
.h-0 {
|
||||||
height: 0px;
|
height: 0px;
|
||||||
}
|
}
|
||||||
|
@ -1048,6 +1056,18 @@ video {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-8 {
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-16 {
|
||||||
|
width: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-32 {
|
||||||
|
width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.w-10 {
|
.w-10 {
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
}
|
}
|
||||||
|
@ -1072,10 +1092,6 @@ video {
|
||||||
width: 2.75rem;
|
width: 2.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-8 {
|
|
||||||
width: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-min {
|
.w-min {
|
||||||
width: -webkit-min-content;
|
width: -webkit-min-content;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
|
@ -1090,14 +1106,6 @@ video {
|
||||||
width: 24rem;
|
width: 24rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-32 {
|
|
||||||
width: 8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-16 {
|
|
||||||
width: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-auto {
|
.w-auto {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
@ -1374,6 +1382,10 @@ video {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.border-dotted {
|
||||||
|
border-style: dotted;
|
||||||
|
}
|
||||||
|
|
||||||
.border-black {
|
.border-black {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(0 0 0 / var(--tw-border-opacity));
|
border-color: rgb(0 0 0 / var(--tw-border-opacity));
|
||||||
|
@ -1864,6 +1876,11 @@ body {
|
||||||
box-sizing: initial !important;
|
box-sizing: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon img {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
/* prevent callout to copy image, etc when tap to hold */
|
||||||
|
}
|
||||||
|
|
||||||
.leaflet-control-attribution {
|
.leaflet-control-attribution {
|
||||||
display: block ruby;
|
display: block ruby;
|
||||||
}
|
}
|
||||||
|
@ -2738,6 +2755,10 @@ input {
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md\:border-t-2 {
|
||||||
|
border-top-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.md\:p-1 {
|
.md\:p-1 {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,6 +113,10 @@ body {
|
||||||
box-sizing: initial !important;
|
box-sizing: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon img {
|
||||||
|
-webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */
|
||||||
|
}
|
||||||
|
|
||||||
.leaflet-control-attribution {
|
.leaflet-control-attribution {
|
||||||
display: block ruby;
|
display: block ruby;
|
||||||
}
|
}
|
||||||
|
|
|
@ -327,7 +327,7 @@
|
||||||
"tuesday": "Tuesday",
|
"tuesday": "Tuesday",
|
||||||
"wednesday": "Wednesday"
|
"wednesday": "Wednesday"
|
||||||
},
|
},
|
||||||
"welcomeBack": "You are logged in, welcome back!",
|
"welcomeBack": "Welcome back!",
|
||||||
"welcomeExplanation": {
|
"welcomeExplanation": {
|
||||||
"addNew": "Tap the map to add a new POI.",
|
"addNew": "Tap the map to add a new POI.",
|
||||||
"browseMoreMaps": "Discover more maps",
|
"browseMoreMaps": "Discover more maps",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue