forked from MapComplete/MapComplete
Merge branch 'develop' into fix-answer-with-image-style-#491
This commit is contained in:
commit
ba2b4754a9
262 changed files with 27237 additions and 25052 deletions
|
@ -1,8 +1,8 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import Loc from "../../Models/Loc";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import {BBox} from "../../Logic/GeoOperations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
export interface MinimapOptions {
|
||||
background?: UIEventSource<BaseLayer>,
|
||||
|
@ -30,6 +30,8 @@ export default class Minimap {
|
|||
/**
|
||||
* Construct a minimap
|
||||
*/
|
||||
public static createMiniMap: (options: MinimapOptions) => (BaseUIElement & MinimapObj)
|
||||
public static createMiniMap: (options: MinimapOptions) => (BaseUIElement & MinimapObj) = (_) => {
|
||||
throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()"
|
||||
}
|
||||
|
||||
}
|
|
@ -4,10 +4,10 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import Loc from "../../Models/Loc";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import {BBox} from "../../Logic/GeoOperations";
|
||||
import * as L from "leaflet";
|
||||
import {Map} from "leaflet";
|
||||
import Minimap, {MinimapObj, MinimapOptions} from "./Minimap";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
|
||||
private static _nextId = 0;
|
||||
|
@ -50,7 +50,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
if (typeof factor === "number") {
|
||||
bounds = leaflet.getBounds()
|
||||
leaflet.setMaxBounds(bounds.pad(factor))
|
||||
}else{
|
||||
} else {
|
||||
// @ts-ignore
|
||||
leaflet.setMaxBounds(factor.toLeaflet())
|
||||
bounds = leaflet.getBounds()
|
||||
|
@ -114,8 +114,12 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
const self = this;
|
||||
// @ts-ignore
|
||||
const resizeObserver = new ResizeObserver(_ => {
|
||||
self.InitMap();
|
||||
self.leafletMap?.data?.invalidateSize()
|
||||
try {
|
||||
self.InitMap();
|
||||
self.leafletMap?.data?.invalidateSize()
|
||||
} catch (e) {
|
||||
console.error("Could not construct a minimap:", e)
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(div);
|
||||
|
@ -141,8 +145,12 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
const location = this._location;
|
||||
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]
|
||||
}
|
||||
const options = {
|
||||
center: <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0],
|
||||
center: latLon,
|
||||
zoom: location.data?.zoom ?? 2,
|
||||
layers: [currentLayer],
|
||||
zoomControl: false,
|
||||
|
|
|
@ -36,6 +36,8 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown)
|
||||
.SetClass("hidden md:block");
|
||||
this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown);
|
||||
|
||||
|
||||
const self = this;
|
||||
isShown.addCallback(isShown => {
|
||||
if (isShown) {
|
||||
|
|
|
@ -31,7 +31,7 @@ export class TabbedComponent extends Combine {
|
|||
tabs.push(tab)
|
||||
}
|
||||
|
||||
const header = new Combine(tabs).SetClass("block tabs-header-bar")
|
||||
const header = new Combine(tabs).SetClass("tabs-header-bar")
|
||||
const actualContent = new VariableUiElement(
|
||||
openedTabSrc.map(i => contentElements[i])
|
||||
)
|
||||
|
|
|
@ -2,22 +2,23 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class VariableUiElement extends BaseUIElement {
|
||||
private _element: HTMLElement;
|
||||
private readonly _contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>;
|
||||
|
||||
constructor(
|
||||
contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>
|
||||
) {
|
||||
constructor(contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>) {
|
||||
super();
|
||||
this._contents = contents;
|
||||
|
||||
this._element = document.createElement("span");
|
||||
const el = this._element;
|
||||
contents.addCallbackAndRun((contents) => {
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("span");
|
||||
this._contents.addCallbackAndRun((contents) => {
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.lastChild);
|
||||
}
|
||||
|
||||
if (contents === undefined) {
|
||||
return el;
|
||||
return
|
||||
}
|
||||
if (typeof contents === "string") {
|
||||
el.innerHTML = contents;
|
||||
|
@ -35,9 +36,6 @@ export class VariableUiElement extends BaseUIElement {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element;
|
||||
return el;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,6 +100,7 @@ export default abstract class BaseUIElement {
|
|||
throw "ERROR! This is not a correct baseUIElement: " + this.constructor.name
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const el = this.InnerConstructElement();
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import Constants from "../../Models/Constants";
|
|||
import Loc from "../../Models/Loc";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {BBox} from "../../Logic/GeoOperations";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
/**
|
||||
* The bottom right attribution panel in the leaflet map
|
||||
|
@ -16,13 +16,13 @@ export default class Attribution extends Combine {
|
|||
|
||||
constructor(location: UIEventSource<Loc>,
|
||||
userDetails: UIEventSource<UserDetails>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>,
|
||||
layoutToUse: LayoutConfig,
|
||||
currentBounds: UIEventSource<BBox>) {
|
||||
|
||||
const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true);
|
||||
const reportBug = new Link(Svg.bug_ui().SetClass("small-image"), "https://github.com/pietervdvn/MapComplete/issues", true);
|
||||
|
||||
const layoutId = layoutToUse?.data?.id;
|
||||
const layoutId = layoutToUse?.id;
|
||||
const now = new Date()
|
||||
// Note: getMonth is zero-index, we want 1-index but with one substracted, so it checks out!
|
||||
const startDate = now.getFullYear() + "-" + now.getMonth() + "-" + now.getDate()
|
||||
|
|
|
@ -20,11 +20,11 @@ export default class AttributionPanel extends Combine {
|
|||
|
||||
private static LicenseObject = AttributionPanel.GenerateLicenses();
|
||||
|
||||
constructor(layoutToUse: UIEventSource<LayoutConfig>, contributions: UIEventSource<Map<string, number>>) {
|
||||
constructor(layoutToUse: LayoutConfig, contributions: UIEventSource<Map<string, number>>) {
|
||||
super([
|
||||
Translations.t.general.attribution.attributionContent,
|
||||
((layoutToUse.data.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.data.maintainer}),
|
||||
layoutToUse.data.credits,
|
||||
((layoutToUse.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.maintainer}),
|
||||
layoutToUse.credits,
|
||||
"<br/>",
|
||||
new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.currentBounds),
|
||||
"<br/>",
|
||||
|
@ -65,7 +65,7 @@ export default class AttributionPanel extends Combine {
|
|||
"<br/>",
|
||||
AttributionPanel.CodeContributors(),
|
||||
"<h3>", Translations.t.general.attribution.iconAttribution.title.Clone().SetClass("pt-6 pb-3"), "</h3>",
|
||||
...Utils.NoNull(Array.from(layoutToUse.data.ExtractImages()))
|
||||
...Utils.NoNull(Array.from(layoutToUse.ExtractImages()))
|
||||
.map(AttributionPanel.IconAttribution)
|
||||
]);
|
||||
this.SetClass("flex flex-col link-underline overflow-hidden")
|
||||
|
|
|
@ -25,7 +25,9 @@ export default class BackgroundSelector extends VariableUiElement {
|
|||
if (baseLayers.length <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
return new DropDown(Translations.t.general.backgroundMap.Clone(), baseLayers, State.state.backgroundLayer)
|
||||
return new DropDown(Translations.t.general.backgroundMap.Clone(), baseLayers, State.state.backgroundLayer, {
|
||||
select_class: 'bg-indigo-100 p-1 rounded hover:bg-indigo-200 w-full'
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import State from "../../State";
|
|||
import {Utils} from "../../Utils";
|
||||
import Combine from "../Base/Combine";
|
||||
import CheckBoxes from "../Input/Checkboxes";
|
||||
import {BBox, GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Title from "../Base/Title";
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
||||
|
@ -13,19 +13,20 @@ 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: UIEventSource<LayoutConfig>,
|
||||
layoutToUse: LayoutConfig,
|
||||
currentBounds: UIEventSource<BBox>
|
||||
} = State.state
|
||||
|
||||
|
||||
const t = Translations.t.general.download
|
||||
const name = State.state.layoutToUse.data.id;
|
||||
const name = State.state.layoutToUse.id;
|
||||
|
||||
const includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()])
|
||||
const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0)
|
||||
|
|
|
@ -7,8 +7,6 @@ import Combine from "../Base/Combine";
|
|||
import Translations from "../i18n/Translations";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import Svg from "../../Svg";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import State from "../../State";
|
||||
|
@ -16,11 +14,6 @@ import FilteredLayer from "../../Models/FilteredLayer";
|
|||
import BackgroundSelector from "./BackgroundSelector";
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
||||
|
||||
|
||||
/**
|
||||
* Shows the filter
|
||||
*/
|
||||
|
||||
export default class FilterView extends VariableUiElement {
|
||||
constructor(filteredLayer: UIEventSource<FilteredLayer[]>) {
|
||||
const backgroundSelector = new Toggle(
|
||||
|
@ -101,26 +94,52 @@ export default class FilterView extends VariableUiElement {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
let listFilterElements: [BaseUIElement, UIEventSource<TagsFilter>][] = layer.filters.map(
|
||||
const filterIndexes = new Map<string, number>()
|
||||
layer.filters.forEach((f, i) => filterIndexes.set(f.id, i))
|
||||
|
||||
let listFilterElements: [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>][] = layer.filters.map(
|
||||
FilterView.createFilter
|
||||
);
|
||||
|
||||
const update = () => {
|
||||
let listTagsFilters = Utils.NoNull(
|
||||
listFilterElements.map((input) => input[1].data)
|
||||
);
|
||||
flayer.appliedFilters.setData(new And(listTagsFilters));
|
||||
};
|
||||
listFilterElements.forEach((inputElement, i) =>
|
||||
inputElement[1].addCallback((changed) => {
|
||||
const oldValue = flayer.appliedFilters.data
|
||||
|
||||
if(changed === undefined){
|
||||
// Lets figure out which filter should be removed
|
||||
// We know this inputElement corresponds with layer.filters[i]
|
||||
// SO, if there is a value in 'oldValue' with this filter, we have to recalculated
|
||||
if(!oldValue.some(f => f.filter === layer.filters[i])){
|
||||
// The filter to remove is already gone, we can stop
|
||||
return;
|
||||
}
|
||||
}else if(oldValue.some(f => f.filter === changed.filter && f.selected === changed.selected)){
|
||||
// The changed value is already there
|
||||
return;
|
||||
}
|
||||
const listTagsFilters = Utils.NoNull(
|
||||
listFilterElements.map((input) => input[1].data)
|
||||
);
|
||||
|
||||
listFilterElements.forEach((inputElement) =>
|
||||
inputElement[1].addCallback((_) => update())
|
||||
console.log(listTagsFilters, oldValue)
|
||||
flayer.appliedFilters.setData(listTagsFilters);
|
||||
})
|
||||
);
|
||||
|
||||
flayer.appliedFilters.addCallbackAndRun(appliedFilters => {
|
||||
if (appliedFilters === undefined || appliedFilters.and.length === 0) {
|
||||
listFilterElements.forEach(filter => filter[1].setData(undefined))
|
||||
return
|
||||
for (let i = 0; i < layer.filters.length; i++){
|
||||
const filter = layer.filters[i];
|
||||
let foundMatch = undefined
|
||||
for (const appliedFilter of appliedFilters) {
|
||||
if(appliedFilter.filter === filter){
|
||||
foundMatch = appliedFilter
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
listFilterElements[i][1].setData(foundMatch)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return new Combine(listFilterElements.map(input => input[0].SetClass("mt-3")))
|
||||
|
@ -128,7 +147,7 @@ export default class FilterView extends VariableUiElement {
|
|||
|
||||
}
|
||||
|
||||
private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<TagsFilter>] {
|
||||
private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>] {
|
||||
if (filterConfig.options.length === 1) {
|
||||
let option = filterConfig.options[0];
|
||||
|
||||
|
@ -136,26 +155,42 @@ export default class FilterView extends VariableUiElement {
|
|||
const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2");
|
||||
|
||||
const toggle = new Toggle(
|
||||
new Combine([icon, option.question.Clone()]).SetClass("flex"),
|
||||
new Combine([iconUnselected, option.question.Clone()]).SetClass("flex")
|
||||
new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"),
|
||||
new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass("flex")
|
||||
)
|
||||
.ToggleOnClick()
|
||||
.SetClass("block m-1")
|
||||
|
||||
return [toggle, toggle.isEnabled.map(enabled => enabled ? option.osmTags : undefined, [], tags => tags !== undefined)]
|
||||
const selected = {
|
||||
filter: filterConfig,
|
||||
selected: 0
|
||||
}
|
||||
return [toggle, toggle.isEnabled.map(enabled => enabled ? selected : undefined, [],
|
||||
f => f?.filter === filterConfig && f?.selected === 0)
|
||||
]
|
||||
}
|
||||
|
||||
let options = filterConfig.options;
|
||||
|
||||
const values = options.map((f, i) => ({
|
||||
filter: filterConfig, selected: i
|
||||
}))
|
||||
const radio = new RadioButton(
|
||||
options.map(
|
||||
(option) =>
|
||||
new FixedInputElement(option.question.Clone(), option.osmTags)
|
||||
(option, i) =>
|
||||
new FixedInputElement(option.question.Clone().SetClass("block"), i)
|
||||
),
|
||||
{
|
||||
dontStyle: true
|
||||
}
|
||||
);
|
||||
return [radio, radio.GetValue()]
|
||||
return [radio,
|
||||
radio.GetValue().map(
|
||||
i => values[i],
|
||||
[],
|
||||
selected => {
|
||||
return selected?.selected
|
||||
}
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import State from "../../State";
|
||||
import ThemeIntroductionPanel from "./ThemeIntroductionPanel";
|
||||
import * as personal from "../../assets/themes/personal/personal.json";
|
||||
import PersonalLayersPanel from "./PersonalLayersPanel";
|
||||
import Svg from "../../Svg";
|
||||
import Translations from "../i18n/Translations";
|
||||
import ShareScreen from "./ShareScreen";
|
||||
|
@ -21,7 +19,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
|
||||
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
const layoutToUse = State.state.layoutToUse.data;
|
||||
const layoutToUse = State.state.layoutToUse;
|
||||
super(
|
||||
() => layoutToUse.title.Clone(),
|
||||
() => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails, isShown),
|
||||
|
@ -32,9 +30,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource<boolean>): { header: string | BaseUIElement; content: BaseUIElement }[] {
|
||||
|
||||
let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown);
|
||||
if (layoutToUse.id === personal.id) {
|
||||
welcome = new PersonalLayersPanel();
|
||||
}
|
||||
|
||||
const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [
|
||||
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
|
||||
{
|
||||
|
|
|
@ -31,14 +31,14 @@ export default class ImportButton extends Toggle {
|
|||
const button = new SubtleButton(imageUrl, message)
|
||||
|
||||
|
||||
button.onClick(() => {
|
||||
button.onClick(async () => {
|
||||
if (isImported.data) {
|
||||
return
|
||||
}
|
||||
originalTags.data["_imported"] = "yes"
|
||||
originalTags.ping() // will set isImported as per its definition
|
||||
const newElementAction = new CreateNewNodeAction(newTags.data, lat, lon)
|
||||
State.state.changes.applyAction(newElementAction)
|
||||
await State.state.changes.applyAction(newElementAction)
|
||||
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
|
||||
newElementAction.newElementId
|
||||
))
|
||||
|
|
|
@ -11,8 +11,8 @@ import AllDownloads from "./AllDownloads";
|
|||
import FilterView from "./FilterView";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
||||
import {BBox} from "../../Logic/GeoOperations";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
export default class LeftControls extends Combine {
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ export default class MoreScreen extends Combine {
|
|||
if (layout.hideFromOverview) {
|
||||
return undefined;
|
||||
}
|
||||
if (layout.id === State.state.layoutToUse.data?.id) {
|
||||
if (layout.id === State.state.layoutToUse?.id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Translations from "../i18n/Translations";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
export default class PersonalLayersPanel extends VariableUiElement {
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
State.state.installedThemes.map(installedThemes => {
|
||||
const t = Translations.t.favourite;
|
||||
|
||||
// Lets get all the layers
|
||||
const allThemes = AllKnownLayouts.layoutsList.concat(installedThemes.map(layout => layout.layout))
|
||||
.filter(theme => !theme.hideFromOverview)
|
||||
|
||||
const allLayers = []
|
||||
{
|
||||
const seenLayers = new Set<string>()
|
||||
for (const layers of allThemes.map(theme => theme.layers)) {
|
||||
for (const layer of layers) {
|
||||
if (seenLayers.has(layer.id)) {
|
||||
continue
|
||||
}
|
||||
seenLayers.add(layer.id)
|
||||
allLayers.push(layer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Time to create a panel based on them!
|
||||
const panel: BaseUIElement = new Combine(allLayers.map(PersonalLayersPanel.CreateLayerToggle));
|
||||
|
||||
|
||||
return new Toggle(
|
||||
new Combine([
|
||||
t.panelIntro.Clone(),
|
||||
panel
|
||||
]).SetClass("flex flex-col"),
|
||||
new SubtleButton(
|
||||
Svg.osm_logo_ui(),
|
||||
t.loginNeeded.Clone().SetClass("text-center")
|
||||
).onClick(() => State.state.osmConnection.AttemptLogin()),
|
||||
State.state.osmConnection.isLoggedIn
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/***
|
||||
* Creates a toggle for the given layer, which'll update State.state.favouriteLayers right away
|
||||
* @param layer
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
private static CreateLayerToggle(layer: LayerConfig): Toggle {
|
||||
let icon: BaseUIElement = new Combine([layer.GenerateLeafletStyle(
|
||||
new UIEventSource<any>({id: "node/-1"}),
|
||||
false
|
||||
).icon.html]).SetClass("relative")
|
||||
let iconUnset = new Combine([layer.GenerateLeafletStyle(
|
||||
new UIEventSource<any>({id: "node/-1"}),
|
||||
false
|
||||
).icon.html]).SetClass("relative")
|
||||
|
||||
iconUnset.SetStyle("opacity:0.1")
|
||||
|
||||
let name = layer.name;
|
||||
if (name === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const content = new Combine([
|
||||
Translations.WT(name).Clone().SetClass("font-bold"),
|
||||
Translations.WT(layer.description)?.Clone()
|
||||
]).SetClass("flex flex-col")
|
||||
|
||||
const contentUnselected = new Combine([
|
||||
Translations.WT(name).Clone().SetClass("font-bold"),
|
||||
Translations.WT(layer.description)?.Clone()
|
||||
]).SetClass("flex flex-col line-through")
|
||||
|
||||
return new Toggle(
|
||||
new SubtleButton(
|
||||
icon,
|
||||
content),
|
||||
new SubtleButton(
|
||||
iconUnset,
|
||||
contentUnselected
|
||||
),
|
||||
State.state.favouriteLayers.map(favLayers => {
|
||||
return favLayers.indexOf(layer.id) >= 0
|
||||
}, [], (selected, current) => {
|
||||
if (!selected && current.indexOf(layer.id) <= 0) {
|
||||
// Not selected and not contained: nothing to change: we return current as is
|
||||
return current;
|
||||
}
|
||||
if (selected && current.indexOf(layer.id) >= 0) {
|
||||
// Selected and contained: this is fine!
|
||||
return current;
|
||||
}
|
||||
const clone = [...current]
|
||||
if (selected) {
|
||||
clone.push(layer.id)
|
||||
} else {
|
||||
clone.splice(clone.indexOf(layer.id), 1)
|
||||
}
|
||||
return clone
|
||||
})
|
||||
).ToggleOnClick();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -17,7 +17,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
|||
export default class ShareScreen extends Combine {
|
||||
|
||||
constructor(layout: LayoutConfig = undefined, layoutDefinition: string = undefined) {
|
||||
layout = layout ?? State.state?.layoutToUse?.data;
|
||||
layout = layout ?? State.state?.layoutToUse;
|
||||
layoutDefinition = layoutDefinition ?? State.state?.layoutDefinition;
|
||||
const tr = Translations.t.general.sharescreen;
|
||||
|
||||
|
|
|
@ -19,8 +19,7 @@ import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
|||
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
|
||||
import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
import {BBox} from "../../Logic/GeoOperations";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
|
@ -56,10 +55,9 @@ export default class SimpleAddUI extends Toggle {
|
|||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
||||
|
||||
|
||||
function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) {
|
||||
console.trace("Creating a new point")
|
||||
async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) {
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {snapOnto: snapOntoWay})
|
||||
State.state.changes.applyAction(newElementAction)
|
||||
await State.state.changes.applyAction(newElementAction)
|
||||
selectedPreset.setData(undefined)
|
||||
isShown.setData(false)
|
||||
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
|
||||
|
@ -224,14 +222,32 @@ export default class SimpleAddUI extends Toggle {
|
|||
]
|
||||
).SetClass("flex flex-col")
|
||||
).onClick(() => {
|
||||
preset.layerToAddTo.appliedFilters.setData(new And([]))
|
||||
preset.layerToAddTo.appliedFilters.setData([])
|
||||
cancel()
|
||||
})
|
||||
|
||||
const disableFiltersOrConfirm = new Toggle(
|
||||
openLayerOrConfirm,
|
||||
disableFilter,
|
||||
preset.layerToAddTo.appliedFilters.map(filters => filters === undefined || filters.normalize().and.length === 0)
|
||||
preset.layerToAddTo.appliedFilters.map(filters => {
|
||||
if(filters === undefined || filters.length === 0){
|
||||
return true;
|
||||
}
|
||||
for (const filter of filters) {
|
||||
if(filter.selected === 0 && filter.filter.options.length === 1){
|
||||
return false;
|
||||
}
|
||||
if(filter.selected !== undefined){
|
||||
const tags = filter.filter.options[filter.selected].osmTags
|
||||
if(tags !== undefined && tags["and"]?.length !== 0){
|
||||
// This actually doesn't filter anything at all
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -2,21 +2,17 @@ import State from "../../State";
|
|||
import Combine from "../Base/Combine";
|
||||
import LanguagePicker from "../LanguagePicker";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export default class ThemeIntroductionPanel extends VariableUiElement {
|
||||
export default class ThemeIntroductionPanel extends Combine {
|
||||
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
const layout = State.state.layoutToUse
|
||||
|
||||
const languagePicker =
|
||||
new VariableUiElement(
|
||||
State.state.layoutToUse.map(layout => LanguagePicker.CreateLanguagePicker(layout.language, Translations.t.general.pickLanguage.Clone()))
|
||||
)
|
||||
;
|
||||
const languagePicker = LanguagePicker.CreateLanguagePicker(layout.language, Translations.t.general.pickLanguage.Clone())
|
||||
|
||||
const toTheMap = new SubtleButton(
|
||||
undefined,
|
||||
|
@ -53,8 +49,7 @@ export default class ThemeIntroductionPanel extends VariableUiElement {
|
|||
State.state.featureSwitchUserbadge
|
||||
)
|
||||
|
||||
|
||||
super(State.state.layoutToUse.map(layout => new Combine([
|
||||
super([
|
||||
layout.description.Clone(),
|
||||
"<br/><br/>",
|
||||
toTheMap,
|
||||
|
@ -63,7 +58,7 @@ export default class ThemeIntroductionPanel extends VariableUiElement {
|
|||
"<br/>",
|
||||
languagePicker,
|
||||
...layout.CustomCodeSnippets()
|
||||
])))
|
||||
])
|
||||
|
||||
this.SetClass("link-underline")
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class UserBadge extends Toggle {
|
|||
});
|
||||
|
||||
const linkStyle = "flex items-baseline"
|
||||
const languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language) ?? new FixedUiElement(""))
|
||||
const languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.language) ?? new FixedUiElement(""))
|
||||
.SetStyle("width:min-content;");
|
||||
|
||||
let messageSpan =
|
||||
|
|
|
@ -5,7 +5,6 @@ import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter";
|
|||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import Minimap from "./Base/Minimap";
|
||||
import Loc from "../Models/Loc";
|
||||
import {BBox} from "../Logic/GeoOperations";
|
||||
import BaseLayer from "../Models/BaseLayer";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import Translations from "./i18n/Translations";
|
||||
|
@ -14,6 +13,7 @@ import Constants from "../Models/Constants";
|
|||
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
|
||||
|
|
|
@ -1,30 +1,19 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import Attribution from "./Attribution";
|
||||
import Img from "../Base/Img";
|
||||
import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource";
|
||||
import {ProvidedImage} from "../../Logic/ImageProviders/ImageProvider";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Loading from "../Base/Loading";
|
||||
|
||||
|
||||
export class AttributedImage extends Combine {
|
||||
|
||||
constructor(urlSource: string, imgSource: ImageAttributionSource) {
|
||||
const preparedUrl = imgSource.PrepareUrl(urlSource)
|
||||
constructor(imageInfo: ProvidedImage) {
|
||||
let img: BaseUIElement;
|
||||
let attr: BaseUIElement
|
||||
if (typeof preparedUrl === "string") {
|
||||
img = new Img(urlSource);
|
||||
attr = new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())
|
||||
} else {
|
||||
img = new VariableUiElement(preparedUrl.map(url => {
|
||||
if(url === undefined){
|
||||
return new Loading()
|
||||
}
|
||||
return new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'});
|
||||
}))
|
||||
attr = new VariableUiElement(preparedUrl.map(_ => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())))
|
||||
}
|
||||
img = new Img(imageInfo.url);
|
||||
attr = new Attribution(imageInfo.provider.GetAttributionFor(imageInfo.url),
|
||||
imageInfo.provider.SourceIcon(),
|
||||
)
|
||||
|
||||
|
||||
super([img, attr]);
|
||||
|
|
|
@ -3,7 +3,7 @@ import Translations from "../i18n/Translations";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LicenseInfo} from "../../Logic/ImageProviders/Wikimedia";
|
||||
import {LicenseInfo} from "../../Logic/ImageProviders/LicenseInfo";
|
||||
|
||||
export default class Attribution extends VariableUiElement {
|
||||
|
||||
|
@ -13,17 +13,16 @@ export default class Attribution extends VariableUiElement {
|
|||
}
|
||||
super(
|
||||
license.map((license: LicenseInfo) => {
|
||||
|
||||
if (license?.artist === undefined) {
|
||||
return undefined;
|
||||
if(license === undefined){
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
return new Combine([
|
||||
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
|
||||
|
||||
new Combine([
|
||||
Translations.W(license.artist).SetClass("block font-bold"),
|
||||
Translations.W((license.license ?? "") === "" ? "CC0" : (license.license ?? ""))
|
||||
Translations.W(license?.artist ?? ".").SetClass("block font-bold"),
|
||||
Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? ""))
|
||||
]).SetClass("flex flex-col")
|
||||
]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg")
|
||||
|
||||
|
|
|
@ -15,15 +15,15 @@ export default class DeleteImage extends Toggle {
|
|||
const isDeletedBadge = Translations.t.image.isDeleted.Clone()
|
||||
.SetClass("rounded-full p-1")
|
||||
.SetStyle("color:white;background:#ff8c8c")
|
||||
.onClick(() => {
|
||||
State.state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data))
|
||||
.onClick(async() => {
|
||||
await State.state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data))
|
||||
});
|
||||
|
||||
const deleteButton = Translations.t.image.doDelete.Clone()
|
||||
.SetClass("block w-full pl-4 pr-4")
|
||||
.SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;")
|
||||
.onClick(() => {
|
||||
State.state?.changes?.applyAction(
|
||||
.onClick( async() => {
|
||||
await State.state?.changes?.applyAction(
|
||||
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data)
|
||||
)
|
||||
});
|
||||
|
@ -48,7 +48,7 @@ export default class DeleteImage extends Toggle {
|
|||
tags.map(tags => (tags[key] ?? "") !== "")
|
||||
),
|
||||
undefined /*Login (and thus editing) is disabled*/,
|
||||
State.state?.featureSwitchUserbadge ?? new UIEventSource<boolean>(true)
|
||||
State.state.osmConnection.isLoggedIn
|
||||
)
|
||||
this.SetClass("cursor-pointer")
|
||||
}
|
||||
|
|
|
@ -4,29 +4,35 @@ import Combine from "../Base/Combine";
|
|||
import DeleteImage from "./DeleteImage";
|
||||
import {AttributedImage} from "./AttributedImage";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Img from "../Base/Img";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {Wikimedia} from "../../Logic/ImageProviders/Wikimedia";
|
||||
import {Imgur} from "../../Logic/ImageProviders/Imgur";
|
||||
import {Mapillary} from "../../Logic/ImageProviders/Mapillary";
|
||||
import ImageProvider from "../../Logic/ImageProviders/ImageProvider";
|
||||
|
||||
export class ImageCarousel extends Toggle {
|
||||
|
||||
constructor(images: UIEventSource<{ key: string, url: string }[]>, tags: UIEventSource<any>) {
|
||||
const uiElements = images.map((imageURLS: { key: string, url: string }[]) => {
|
||||
constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider }[]>, tags: UIEventSource<any>) {
|
||||
const uiElements = images.map((imageURLS: { key: string, url: string, provider: ImageProvider }[]) => {
|
||||
const uiElements: BaseUIElement[] = [];
|
||||
for (const url of imageURLS) {
|
||||
let image = ImageCarousel.CreateImageElement(url.url)
|
||||
if (url.key !== undefined) {
|
||||
image = new Combine([
|
||||
image,
|
||||
new DeleteImage(url.key, tags).SetClass("delete-image-marker absolute top-0 left-0 pl-3")
|
||||
]).SetClass("relative");
|
||||
|
||||
try {
|
||||
|
||||
let image = new AttributedImage(url)
|
||||
|
||||
if (url.key !== undefined) {
|
||||
image = new Combine([
|
||||
image,
|
||||
new DeleteImage(url.key, tags).SetClass("delete-image-marker absolute top-0 left-0 pl-3")
|
||||
]).SetClass("relative");
|
||||
}
|
||||
image
|
||||
.SetClass("w-full block")
|
||||
.SetStyle("min-width: 50px; background: grey;")
|
||||
uiElements.push(image);
|
||||
} catch (e) {
|
||||
console.error("Could not generate image element for", url.url, "due to", e)
|
||||
}
|
||||
image
|
||||
.SetClass("w-full block")
|
||||
.SetStyle("min-width: 50px; background: grey;")
|
||||
uiElements.push(image);
|
||||
|
||||
|
||||
}
|
||||
return uiElements;
|
||||
});
|
||||
|
@ -38,33 +44,4 @@ export class ImageCarousel extends Toggle {
|
|||
)
|
||||
this.SetClass("block w-full");
|
||||
}
|
||||
|
||||
/***
|
||||
* Creates either a 'simpleimage' or a 'wikimediaimage' based on the string
|
||||
* @param url
|
||||
* @constructor
|
||||
*/
|
||||
private static CreateImageElement(url: string): BaseUIElement {
|
||||
// @ts-ignore
|
||||
let attrSource: ImageAttributionSource = undefined;
|
||||
if (url.startsWith("File:")) {
|
||||
attrSource = Wikimedia.singleton
|
||||
} else if (url.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
attrSource = Wikimedia.singleton;
|
||||
} else if (url.toLowerCase().startsWith("https://i.imgur.com/")) {
|
||||
attrSource = Imgur.singleton
|
||||
} else if (url.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) {
|
||||
attrSource = Mapillary.singleton
|
||||
} else {
|
||||
return new Img(url);
|
||||
}
|
||||
|
||||
try {
|
||||
return new AttributedImage(url, attrSource)
|
||||
} catch (e) {
|
||||
console.error("Could not create an image: ", e)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -29,10 +29,10 @@ export class ImageUploadFlow extends Toggle {
|
|||
key = imagePrefix + ":" + freeIndex;
|
||||
}
|
||||
console.log("Adding image:" + key, url);
|
||||
State.state.changes
|
||||
Promise.resolve(State.state.changes
|
||||
.applyAction(new ChangeTagAction(
|
||||
tags.id, new Tag(key, url), tagsSource.data
|
||||
))
|
||||
)))
|
||||
})
|
||||
|
||||
|
||||
|
@ -55,7 +55,7 @@ export class ImageUploadFlow extends Toggle {
|
|||
|
||||
const tags = tagsSource.data;
|
||||
|
||||
const layout = State.state?.layoutToUse?.data
|
||||
const layout = State.state?.layoutToUse
|
||||
let matchingLayer: LayerConfig = undefined
|
||||
for (const layer of layout?.layers ?? []) {
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
|
|
|
@ -7,12 +7,13 @@ import Combine from "../Base/Combine";
|
|||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import {BBox, GeoOperations} from "../../Logic/GeoOperations";
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||
import * as L from "leaflet";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||
|
||||
export default class LocationInput extends InputElement<Loc> {
|
||||
|
||||
|
@ -39,7 +40,7 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
private readonly _maxSnapDistance: number
|
||||
private readonly _snappedPointTags: any;
|
||||
private readonly _bounds: UIEventSource<BBox>;
|
||||
public readonly _matching_layer: UIEventSource<LayerConfig>;
|
||||
public readonly _matching_layer: LayerConfig;
|
||||
|
||||
constructor(options: {
|
||||
mapBackground?: UIEventSource<BaseLayer>,
|
||||
|
@ -63,18 +64,17 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
|
||||
|
||||
if (self._snappedPointTags !== undefined) {
|
||||
this._matching_layer = State.state.layoutToUse.map(layout => {
|
||||
const layout = State.state.layoutToUse
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.source.osmTags.matchesProperties(self._snappedPointTags)) {
|
||||
return layer
|
||||
}
|
||||
let matchingLayer = LocationInput.matchLayer
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.source.osmTags.matchesProperties(self._snappedPointTags)) {
|
||||
matchingLayer = layer
|
||||
}
|
||||
console.error("No matching layer found for tags ", self._snappedPointTags)
|
||||
return LocationInput.matchLayer
|
||||
})
|
||||
}
|
||||
this._matching_layer = matchingLayer;
|
||||
} else {
|
||||
this._matching_layer = new UIEventSource<LayerConfig>(LocationInput.matchLayer)
|
||||
this._matching_layer = LocationInput.matchLayer
|
||||
}
|
||||
|
||||
this._snappedPoint = options.centerLocation.map(loc => {
|
||||
|
@ -125,7 +125,7 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
})
|
||||
|
||||
}
|
||||
this.mapBackground = options.mapBackground ?? State.state.backgroundLayer ?? new UIEventSource(AvailableBaseLayers.osmCarto)
|
||||
this.mapBackground = options.mapBackground ?? State.state?.backgroundLayer ?? new UIEventSource(AvailableBaseLayers.osmCarto)
|
||||
this.SetClass("block h-full")
|
||||
}
|
||||
|
||||
|
@ -144,7 +144,7 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
{
|
||||
location: this._centerLocation,
|
||||
background: this.mapBackground,
|
||||
attribution: this.mapBackground !== State.state.backgroundLayer,
|
||||
attribution: this.mapBackground !== State.state?.backgroundLayer,
|
||||
lastClickLocation: clickLocation,
|
||||
bounds: this._bounds
|
||||
}
|
||||
|
@ -176,11 +176,10 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
enablePopups: false,
|
||||
zoomToFeatures: false,
|
||||
leafletMap: map.leafletMap,
|
||||
layerToShow: this._matching_layer.data
|
||||
layerToShow: this._matching_layer
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
this.mapBackground.map(layer => {
|
||||
const leaflet = map.leafletMap.data
|
||||
if (leaflet === undefined || layer === undefined) {
|
||||
|
@ -192,20 +191,31 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
leaflet.setZoom(layer.max_zoom - 1)
|
||||
|
||||
}, [map.leafletMap])
|
||||
|
||||
const animatedHand = Svg.hand_ui()
|
||||
.SetStyle("width: 2rem; height: unset;")
|
||||
.SetClass("hand-drag-animation block pointer-events-none")
|
||||
|
||||
return new Combine([
|
||||
new Combine([
|
||||
Svg.move_arrows_ui()
|
||||
.SetClass("block relative pointer-events-none")
|
||||
.SetStyle("left: -2.5rem; top: -2.5rem; width: 5rem; height: 5rem")
|
||||
]).SetClass("block w-0 h-0 z-10 relative")
|
||||
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"),
|
||||
]).SetClass("block w-0 h-0 z-10 relative")
|
||||
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%; opacity: 0.5"),
|
||||
|
||||
new Combine([
|
||||
animatedHand])
|
||||
.SetClass("block w-0 h-0 z-10 relative")
|
||||
.SetStyle("left: calc(50% + 3rem); top: calc(50% + 2rem); opacity: 0.7"),
|
||||
|
||||
map
|
||||
.SetClass("z-0 relative block w-full h-full bg-gray-100")
|
||||
|
||||
]).ConstructElement();
|
||||
} catch (e) {
|
||||
console.error("Could not generate LocationInputElement:", e)
|
||||
return undefined;
|
||||
return new FixedUiElement("Constructing a locationInput failed due to" + e).SetClass("alert").ConstructElement();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,14 +51,14 @@ export default class DeleteWizard extends Toggle {
|
|||
const confirm = new UIEventSource<boolean>(false)
|
||||
|
||||
|
||||
function softDelete(reason: string, tagsToApply: { k: string, v: string }[]) {
|
||||
async function softDelete(reason: string, tagsToApply: { k: string, v: string }[]) {
|
||||
if (reason !== undefined) {
|
||||
tagsToApply.splice(0, 0, {
|
||||
k: "fixme",
|
||||
v: `A mapcomplete user marked this feature to be deleted (${reason})`
|
||||
})
|
||||
}
|
||||
(State.state?.changes ?? new Changes())
|
||||
await (State.state?.changes ?? new Changes())
|
||||
.applyAction(new ChangeTagAction(
|
||||
id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource.data
|
||||
))
|
||||
|
|
|
@ -31,8 +31,10 @@ export default class EditableTagRendering extends Toggle {
|
|||
|
||||
|
||||
const answerWithEditButton = new Combine([answer,
|
||||
new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)])
|
||||
.SetClass("flex justify-between w-full")
|
||||
new Toggle(editButton,
|
||||
undefined,
|
||||
State.state.osmConnection.isLoggedIn)
|
||||
]).SetClass("flex justify-between w-full")
|
||||
|
||||
|
||||
const cancelbutton =
|
||||
|
|
|
@ -5,7 +5,7 @@ import {SubtleButton} from "../Base/SubtleButton";
|
|||
import Minimap from "../Base/Minimap";
|
||||
import State from "../../State";
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||
import {BBox, GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {LeafletMouseEvent} from "leaflet";
|
||||
import Combine from "../Base/Combine";
|
||||
import {Button} from "../Base/Button";
|
||||
|
@ -15,6 +15,7 @@ import Title from "../Base/Title";
|
|||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
export default class SplitRoadWizard extends Toggle {
|
||||
private static splitLayerStyling = new LayerConfig({
|
||||
|
@ -71,7 +72,7 @@ export default class SplitRoadWizard extends Toggle {
|
|||
})
|
||||
|
||||
new ShowDataMultiLayer({
|
||||
features: new StaticFeatureSource([roadElement]),
|
||||
features: new StaticFeatureSource([roadElement], false),
|
||||
layers: State.state.filteredLayers,
|
||||
leafletMap: miniMap.leafletMap,
|
||||
enablePopups: false,
|
||||
|
|
|
@ -58,10 +58,10 @@ export default class TagRenderingQuestion extends Combine {
|
|||
console.error("MultiAnswer failed - probably not a single option was possible", configuration)
|
||||
throw "MultiAnswer failed - probably not a single option was possible"
|
||||
}
|
||||
const save = () => {
|
||||
const save = async () => {
|
||||
const selection = inputElement.GetValue().data;
|
||||
if (selection) {
|
||||
(State.state?.changes ?? new Changes())
|
||||
await (State.state?.changes ?? new Changes())
|
||||
.applyAction(new ChangeTagAction(
|
||||
tags.data.id, selection, tags.data
|
||||
))
|
||||
|
|
|
@ -17,6 +17,7 @@ export default class ShowDataLayer {
|
|||
// Used to generate a fresh ID when needed
|
||||
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
|
||||
|
@ -37,41 +38,72 @@ export default class ShowDataLayer {
|
|||
this._layerToShow = options.layerToShow;
|
||||
const self = this;
|
||||
|
||||
options.leafletMap.addCallbackAndRunD(_ => {
|
||||
self.update(options)
|
||||
}
|
||||
);
|
||||
|
||||
features.addCallback(_ => self.update(options));
|
||||
options.leafletMap.addCallback(_ => self.update(options));
|
||||
this.update(options);
|
||||
options.doShowLayer?.addCallbackAndRun(doShow => {
|
||||
const mp = options.leafletMap.data;
|
||||
if (mp == undefined) {
|
||||
return;
|
||||
}
|
||||
if (doShow) {
|
||||
if (self.isDirty) {
|
||||
self.update(options)
|
||||
} else {
|
||||
mp.addLayer(this.geoLayer)
|
||||
}
|
||||
} else {
|
||||
if(this.geoLayer !== undefined){
|
||||
mp.removeLayer(this.geoLayer)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
State.state.selectedElement.addCallbackAndRunD(selected => {
|
||||
if (self._leafletMap.data === undefined) {
|
||||
return;
|
||||
}
|
||||
const v = self.leafletLayersPerId.get(selected.properties.id)
|
||||
if(v === undefined){return;}
|
||||
if (v === undefined) {
|
||||
return;
|
||||
}
|
||||
const leafletLayer = v.leafletlayer
|
||||
const feature = v.feature
|
||||
if (leafletLayer.getPopup().isOpen()) {
|
||||
return;
|
||||
}
|
||||
if (selected.properties.id === feature.properties.id) {
|
||||
// A small sanity check to prevent infinite loops:
|
||||
if (selected.geometry.type === feature.geometry.type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again
|
||||
&& feature.id === feature.properties.id // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too
|
||||
) {
|
||||
leafletLayer.openPopup()
|
||||
}
|
||||
if (feature.id !== feature.properties.id) {
|
||||
console.trace("Not opening the popup for", feature)
|
||||
}
|
||||
|
||||
if (selected.properties.id !== feature.properties.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (feature.id !== feature.properties.id) {
|
||||
// Probably a feature which has renamed
|
||||
console.trace("Not opening the popup for", feature)
|
||||
return;
|
||||
}
|
||||
if (selected.geometry.type === feature.geometry.type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again
|
||||
&& feature.id === feature.properties.id // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too
|
||||
) {
|
||||
console.log("Opening popup of feature", feature)
|
||||
leafletLayer.openPopup()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private update(options) {
|
||||
private update(options: ShowDataLayerOptions) {
|
||||
if (this._features.data === undefined) {
|
||||
return;
|
||||
}
|
||||
this.isDirty = true;
|
||||
if (options?.doShowLayer?.data === false) {
|
||||
return;
|
||||
}
|
||||
const mp = options.leafletMap.data;
|
||||
|
||||
if (mp === undefined) {
|
||||
|
@ -83,21 +115,30 @@ export default class ShowDataLayer {
|
|||
mp.removeLayer(this.geoLayer);
|
||||
}
|
||||
|
||||
this.geoLayer= this.CreateGeojsonLayer()
|
||||
const self = this;
|
||||
const data = {
|
||||
type: "FeatureCollection",
|
||||
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 allFeats = this._features.data;
|
||||
for (const feat of allFeats) {
|
||||
if (feat === undefined) {
|
||||
continue
|
||||
}
|
||||
try{
|
||||
try {
|
||||
this.geoLayer.addData(feat);
|
||||
}catch(e){
|
||||
} catch (e) {
|
||||
console.error("Could not add ", feat, "to the geojson layer in leaflet")
|
||||
}
|
||||
}
|
||||
|
||||
mp.addLayer(this.geoLayer)
|
||||
|
||||
if (options.zoomToFeatures ?? false) {
|
||||
try {
|
||||
mp.fitBounds(this.geoLayer.getBounds(), {animate: false})
|
||||
|
@ -105,6 +146,11 @@ export default class ShowDataLayer {
|
|||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (options.doShowLayer?.data ?? true) {
|
||||
mp.addLayer(this.geoLayer)
|
||||
}
|
||||
this.isDirty = false;
|
||||
}
|
||||
|
||||
|
||||
|
@ -125,7 +171,10 @@ export default class ShowDataLayer {
|
|||
return;
|
||||
}
|
||||
|
||||
const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) : State.state.allElements.getEventSourceById(feature.properties.id)
|
||||
let tagSource = State.state.allElements.getEventSourceById(feature.properties.id)
|
||||
if(tagSource === undefined){
|
||||
tagSource = new UIEventSource<any>(feature.properties)
|
||||
}
|
||||
const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)
|
||||
const style = layer.GenerateLeafletStyle(tagSource, clickable);
|
||||
const baseElement = style.icon.html;
|
||||
|
@ -193,22 +242,9 @@ export default class ShowDataLayer {
|
|||
infobox.Activate();
|
||||
});
|
||||
|
||||
|
||||
// Add the feature to the index to open the popup when needed
|
||||
this.leafletLayersPerId.set(feature.properties.id, {feature: feature, leafletlayer: leafletLayer})
|
||||
}
|
||||
|
||||
private CreateGeojsonLayer(): L.Layer {
|
||||
const self = this;
|
||||
const data = {
|
||||
type: "FeatureCollection",
|
||||
features: []
|
||||
}
|
||||
// @ts-ignore
|
||||
return L.geoJSON(data, {
|
||||
style: feature => self.createStyleFor(feature),
|
||||
pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng),
|
||||
onEachFeature: (feature, leafletLayer) => self.postProcessFeature(feature, leafletLayer)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -6,4 +6,5 @@ export interface ShowDataLayerOptions {
|
|||
leafletMap: UIEventSource<L.Map>,
|
||||
enablePopups?: true | boolean,
|
||||
zoomToFeatures?: false | boolean,
|
||||
doShowLayer?: UIEventSource<boolean>
|
||||
}
|
61
UI/ShowDataLayer/ShowTileInfo.ts
Normal file
61
UI/ShowDataLayer/ShowTileInfo.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import FeatureSource, {Tiled} from "../../Logic/FeatureSource/FeatureSource";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import ShowDataLayer from "./ShowDataLayer";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
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)
|
||||
|
||||
constructor(options: {
|
||||
source: FeatureSource & Tiled, leafletMap: UIEventSource<any>, layer?: LayerConfig,
|
||||
doShowLayer?: UIEventSource<boolean>
|
||||
}) {
|
||||
|
||||
|
||||
const source = options.source
|
||||
const metaFeature: UIEventSource<any[]> =
|
||||
source.features.map(features => {
|
||||
const bbox = source.bbox
|
||||
const [z, x, y] = Tiles.tile_from_index(source.tileIndex)
|
||||
const box = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"z": z,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"tileIndex": source.tileIndex,
|
||||
"source": source.name,
|
||||
"count": features.length,
|
||||
tileId: source.name + "/" + source.tileIndex
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[bbox.minLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.minLat]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
const center = GeoOperations.centerpoint(box)
|
||||
return [box, center]
|
||||
})
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: ShowTileInfo.styling,
|
||||
features: new StaticFeatureSource(metaFeature, false),
|
||||
leafletMap: options.leafletMap,
|
||||
doShowLayer: options.doShowLayer
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
217
UI/ShowDataLayer/TileHierarchyAggregator.ts
Normal file
217
UI/ShowDataLayer/TileHierarchyAggregator.ts
Normal file
|
@ -0,0 +1,217 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../../Logic/FeatureSource/FeatureSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
export class TileHierarchyAggregator implements FeatureSource {
|
||||
private _parent: TileHierarchyAggregator;
|
||||
private _root: TileHierarchyAggregator;
|
||||
private _z: number;
|
||||
private _x: number;
|
||||
private _y: number;
|
||||
private _tileIndex: number
|
||||
private _counter: SingleTileCounter
|
||||
|
||||
private _subtiles: [TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator] = [undefined, undefined, undefined, undefined]
|
||||
public totalValue: 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, tileId: string, id: string };
|
||||
|
||||
private constructor(parent: TileHierarchyAggregator, z: number, x: number, y: number) {
|
||||
this._parent = parent;
|
||||
this._root = parent?._root ?? this
|
||||
this._z = z;
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._tileIndex = Tiles.tile_index(z, x, y)
|
||||
this.name = "Count(" + this._tileIndex + ")"
|
||||
|
||||
const totals = {
|
||||
id: ""+this._tileIndex,
|
||||
tileId: ""+this._tileIndex,
|
||||
count: ""+0
|
||||
}
|
||||
this.featureProperties = totals
|
||||
|
||||
const now = new Date()
|
||||
const feature = {
|
||||
"type": "Feature",
|
||||
"properties": totals,
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": Tiles.centerPointOf(z, x, y)
|
||||
}
|
||||
}
|
||||
this.featuresStatic.push({feature: feature, freshness: now})
|
||||
|
||||
const bbox = BBox.fromTile(z, x, y)
|
||||
const box = {
|
||||
"type": "Feature",
|
||||
"properties": totals,
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[bbox.minLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.minLat]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
this.featuresStatic.push({feature: box, freshness: now})
|
||||
}
|
||||
|
||||
public getTile(tileIndex): TileHierarchyAggregator {
|
||||
if (tileIndex === this._tileIndex) {
|
||||
return this;
|
||||
}
|
||||
let [tileZ, tileX, tileY] = Tiles.tile_from_index(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;
|
||||
return this._subtiles[subtileIndex]?.getTile(tileIndex)
|
||||
}
|
||||
|
||||
private update() {
|
||||
const newMap = new Map<string, number>()
|
||||
let total = 0
|
||||
this?._counter?.countsPerLayer?.data?.forEach((count, layerId) => {
|
||||
newMap.set(layerId, count)
|
||||
total += count
|
||||
})
|
||||
|
||||
for (const tile of this._subtiles) {
|
||||
if (tile === undefined) {
|
||||
continue;
|
||||
}
|
||||
total += tile.totalValue
|
||||
}
|
||||
this.totalValue = total
|
||||
this._parent?.update()
|
||||
|
||||
if (total === 0) {
|
||||
this.features.setData(TileHierarchyAggregator.empty)
|
||||
} else {
|
||||
this.featureProperties.count = "" + total;
|
||||
this.features.data = this.featuresStatic
|
||||
this.features.ping()
|
||||
}
|
||||
}
|
||||
|
||||
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, tileZ, tileX, tileY)
|
||||
}
|
||||
this._subtiles[subtileIndex].addTile(source)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static createHierarchy() {
|
||||
return new TileHierarchyAggregator(undefined, 0, 0, 0)
|
||||
}
|
||||
|
||||
private visitSubTiles(f : (aggr: TileHierarchyAggregator) => boolean){
|
||||
const visitFurther = f(this)
|
||||
if(visitFurther){
|
||||
this._subtiles.forEach(tile => tile?.visitSubTiles(f))
|
||||
}
|
||||
}
|
||||
|
||||
getCountsForZoom(locationControl: UIEventSource<{ zoom : number }>, cutoff: number = 0) : FeatureSource{
|
||||
const self = this
|
||||
|
||||
return new StaticFeatureSource(
|
||||
locationControl.map(loc => {
|
||||
const features = []
|
||||
const targetZoom = loc.zoom
|
||||
self.visitSubTiles(aggr => {
|
||||
if(aggr.totalValue < cutoff) {
|
||||
return false
|
||||
}
|
||||
if(aggr._z === targetZoom){
|
||||
features.push(...aggr.features.data)
|
||||
return false
|
||||
}
|
||||
return aggr._z <= targetZoom;
|
||||
|
||||
})
|
||||
|
||||
return features
|
||||
})
|
||||
, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps track of a single tile
|
||||
*/
|
||||
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
|
||||
|
||||
|
||||
constructor(tileIndex: number) {
|
||||
this.tileIndex = tileIndex
|
||||
this.bbox = BBox.fromTileIndex(tileIndex)
|
||||
const [z, x, y] = Tiles.tile_from_index(tileIndex)
|
||||
this.z = z;
|
||||
this.x = x;
|
||||
this.y = y
|
||||
}
|
||||
|
||||
public addTileCount(source: FeatureSourceForLayer) {
|
||||
const layer = source.layer.layerDef
|
||||
this.registeredLayers.set(layer.id, layer)
|
||||
const self = this
|
||||
|
||||
source.features.map(f => {
|
||||
const isDisplayed = source.layer.isDisplayed.data
|
||||
self.countsPerLayer.data.set(layer.id, isDisplayed ? f.length : 0)
|
||||
self.countsPerLayer.ping()
|
||||
}, [source.layer.isDisplayed])
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -13,20 +13,19 @@ import Translations from "./i18n/Translations";
|
|||
import ReviewForm from "./Reviews/ReviewForm";
|
||||
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization";
|
||||
import State from "../State";
|
||||
import {ImageSearcher} from "../Logic/Actors/ImageSearcher";
|
||||
import BaseUIElement from "./BaseUIElement";
|
||||
import Title from "./Base/Title";
|
||||
import Table from "./Base/Table";
|
||||
import Histogram from "./BigComponents/Histogram";
|
||||
import Loc from "../Models/Loc";
|
||||
import {Utils} from "../Utils";
|
||||
import BaseLayer from "../Models/BaseLayer";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import ImportButton from "./BigComponents/ImportButton";
|
||||
import {Tag} from "../Logic/Tags/Tag";
|
||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
|
||||
import Minimap from "./Base/Minimap";
|
||||
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
funcName: string,
|
||||
|
@ -67,18 +66,10 @@ export default class SpecialVisualizations {
|
|||
name: "image key/prefix",
|
||||
defaultValue: "image",
|
||||
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... "
|
||||
},
|
||||
{
|
||||
name: "smart search",
|
||||
defaultValue: "true",
|
||||
doc: "Also include images given via 'Wikidata', 'wikimedia_commons' and 'mapillary"
|
||||
}],
|
||||
}],
|
||||
constr: (state: State, tags, args) => {
|
||||
const imagePrefix = args[0];
|
||||
const loadSpecial = args[1].toLowerCase() === "true";
|
||||
const searcher: UIEventSource<{ key: string, url: string }[]> = ImageSearcher.construct(tags, imagePrefix, loadSpecial);
|
||||
|
||||
return new ImageCarousel(searcher, tags);
|
||||
return new ImageCarousel(AllImageProviders.LoadImagesFor(tags, imagePrefix), tags);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -316,10 +307,10 @@ export default class SpecialVisualizations {
|
|||
const generateShareData = () => {
|
||||
|
||||
|
||||
const title = state?.layoutToUse?.data?.title?.txt ?? "MapComplete";
|
||||
const title = state?.layoutToUse?.title?.txt ?? "MapComplete";
|
||||
|
||||
let matchingLayer: LayerConfig = undefined;
|
||||
for (const layer of (state?.layoutToUse?.data?.layers ?? [])) {
|
||||
for (const layer of (state?.layoutToUse?.layers ?? [])) {
|
||||
if (layer.source.osmTags.matchesProperties(tagSource?.data)) {
|
||||
matchingLayer = layer
|
||||
}
|
||||
|
@ -337,7 +328,7 @@ export default class SpecialVisualizations {
|
|||
return {
|
||||
title: name,
|
||||
url: url,
|
||||
text: state?.layoutToUse?.data?.shortDescription?.txt ?? "MapComplete"
|
||||
text: state?.layoutToUse?.shortDescription?.txt ?? "MapComplete"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,15 +354,14 @@ export default class SpecialVisualizations {
|
|||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const allUnits = [].concat(...state.layoutToUse.data.layers.map(lyr => lyr.units))
|
||||
const allUnits = [].concat(...state.layoutToUse.layers.map(lyr => lyr.units))
|
||||
const unit = allUnits.filter(unit => unit.isApplicableToKey(key))[0]
|
||||
if (unit === undefined) {
|
||||
return value;
|
||||
}
|
||||
return unit.asHumanLongValue(value);
|
||||
|
||||
},
|
||||
[state.layoutToUse])
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -410,7 +400,7 @@ There are also some technicalities in your theme to keep in mind:
|
|||
A reference number to the original dataset is an excellen way to do this
|
||||
`,
|
||||
constr: (state, tagSource, args) => {
|
||||
if (!state.layoutToUse.data.official && !state.featureSwitchIsTesting.data) {
|
||||
if (!state.layoutToUse.official && !state.featureSwitchIsTesting.data) {
|
||||
return new Combine([new FixedUiElement("The import button is disabled for unofficial themes to prevent accidents.").SetClass("alert"),
|
||||
new FixedUiElement("To test, add 'test=true' to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")])
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue