forked from MapComplete/MapComplete
		
	Move AllTagsPanel to separate class; various small fixes
This commit is contained in:
		
							parent
							
								
									27f12b1f9d
								
							
						
					
					
						commit
						9f5c506e17
					
				
					 12 changed files with 85 additions and 411 deletions
				
			
		
							
								
								
									
										46
									
								
								UI/AllTagsPanel.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								UI/AllTagsPanel.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | ||||||
|  | import {VariableUiElement} from "./Base/VariableUIElement"; | ||||||
|  | import {UIEventSource} from "../Logic/UIEventSource"; | ||||||
|  | import Table from "./Base/Table"; | ||||||
|  | 
 | ||||||
|  | export class AllTagsPanel extends VariableUiElement { | ||||||
|  | 
 | ||||||
|  |     constructor(tags: UIEventSource<any>, state?) { | ||||||
|  | 
 | ||||||
|  |         const calculatedTags = [].concat( | ||||||
|  |            // SimpleMetaTagger.lazyTags,
 | ||||||
|  |             ...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? [])) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         super(tags.map(tags => { | ||||||
|  |             const parts = []; | ||||||
|  |             for (const key in tags) { | ||||||
|  |                 if (!tags.hasOwnProperty(key)) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 let v = tags[key] | ||||||
|  |                 if (v === "") { | ||||||
|  |                     v = "<b>empty string</b>" | ||||||
|  |                 } | ||||||
|  |                 parts.push([key, v ?? "<b>undefined</b>"]); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             for (const key of calculatedTags) { | ||||||
|  |                 const value = tags[key] | ||||||
|  |                 if (value === undefined) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 let type = ""; | ||||||
|  |                 if (typeof value !== "string") { | ||||||
|  |                     type = " <i>" + (typeof value) + "</i>" | ||||||
|  |                 } | ||||||
|  |                 parts.push(["<i>" + key + "</i>", value]) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return new Table( | ||||||
|  |                 ["key", "value"], | ||||||
|  |                 parts | ||||||
|  |             ) | ||||||
|  |                 .SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table") | ||||||
|  |         })) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -16,7 +16,7 @@ export class VariableUiElement extends BaseUIElement { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     AsMarkdown(): string { |     AsMarkdown(): string { | ||||||
|         const d = this._contents.data; |         const d = this._contents?.data; | ||||||
|         if (typeof d === "string") { |         if (typeof d === "string") { | ||||||
|             return d; |             return d; | ||||||
|         } |         } | ||||||
|  | @ -29,7 +29,7 @@ export class VariableUiElement extends BaseUIElement { | ||||||
|     protected InnerConstructElement(): HTMLElement { |     protected InnerConstructElement(): HTMLElement { | ||||||
|         const el = document.createElement("span"); |         const el = document.createElement("span"); | ||||||
|         const self = this; |         const self = this; | ||||||
|         this._contents.addCallbackAndRun((contents) => { |         this._contents?.addCallbackAndRun((contents) => { | ||||||
|             if (self.isDestroyed) { |             if (self.isDestroyed) { | ||||||
|                 return true; |                 return true; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -94,7 +94,7 @@ export default abstract class BaseUIElement { | ||||||
|      * The same as 'Render', but creates a HTML element instead of the HTML representation |      * The same as 'Render', but creates a HTML element instead of the HTML representation | ||||||
|      */ |      */ | ||||||
|     public ConstructElement(): HTMLElement { |     public ConstructElement(): HTMLElement { | ||||||
|         if (Utils.runningFromConsole) { |         if (typeof window === undefined) { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ export default class DeleteImage extends Toggle { | ||||||
|                 tags.map(tags => (tags[key] ?? "") !== "") |                 tags.map(tags => (tags[key] ?? "") !== "") | ||||||
|             ), |             ), | ||||||
|             undefined /*Login (and thus editing) is disabled*/, |             undefined /*Login (and thus editing) is disabled*/, | ||||||
|             state.osmConnection.isLoggedIn |             state?.osmConnection?.isLoggedIn | ||||||
|         ) |         ) | ||||||
|         this.SetClass("cursor-pointer") |         this.SetClass("cursor-pointer") | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -169,7 +169,7 @@ export class ImageUploadFlow extends Toggle { | ||||||
|                 state?.osmConnection?.isLoggedIn |                 state?.osmConnection?.isLoggedIn | ||||||
|             ), |             ), | ||||||
|             undefined /* Nothing as the user badge is disabled*/, |             undefined /* Nothing as the user badge is disabled*/, | ||||||
|             state.featureSwitchUserbadge |             state?.featureSwitchUserbadge | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -21,9 +21,9 @@ import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
| import {FlowStep} from "./FlowStep"; | import {FlowStep} from "./FlowStep"; | ||||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||||
| import {AllTagsPanel} from "../SpecialVisualizations"; |  | ||||||
| import Title from "../Base/Title"; | import Title from "../Base/Title"; | ||||||
| import CheckBoxes from "../Input/Checkboxes"; | import CheckBoxes from "../Input/Checkboxes"; | ||||||
|  | import {AllTagsPanel} from "../AllTagsPanel"; | ||||||
| 
 | 
 | ||||||
| class PreviewPanel extends ScrollableFullScreen { | class PreviewPanel extends ScrollableFullScreen { | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ export default class Toggle extends VariableUiElement { | ||||||
| 
 | 
 | ||||||
|     constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: UIEventSource<boolean> = new UIEventSource<boolean>(false)) { |     constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: UIEventSource<boolean> = new UIEventSource<boolean>(false)) { | ||||||
|         super( |         super( | ||||||
|             isEnabled.map(isEnabled => isEnabled ? showEnabled : showDisabled) |             isEnabled?.map(isEnabled => isEnabled ? showEnabled : showDisabled) | ||||||
|         ); |         ); | ||||||
|         this.isEnabled = isEnabled |         this.isEnabled = isEnabled | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
|  | import opening_hours from "opening_hours"; | ||||||
| 
 | 
 | ||||||
| export interface OpeningHour { | export interface OpeningHour { | ||||||
|     weekday: number, // 0 is monday, 1 is tuesday, ...
 |     weekday: number, // 0 is monday, 1 is tuesday, ...
 | ||||||
|  | @ -458,6 +459,17 @@ export class OH { | ||||||
|         return [changeHours, changeHourText] |         return [changeHours, changeHourText] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static CreateOhObject(tags: object & {_lat: number, _lon: number, _country?: string}, textToParse: string){ | ||||||
|  |         // noinspection JSPotentiallyInvalidConstructorUsage
 | ||||||
|  |         return new opening_hours(textToParse, { | ||||||
|  |             lat: tags._lat, | ||||||
|  |             lon: tags._lon, | ||||||
|  |             address: { | ||||||
|  |                 country_code: tags._country | ||||||
|  |             }, | ||||||
|  |         }, {tag_key: "opening_hours"}); | ||||||
|  |     } | ||||||
|  |      | ||||||
|     /* |     /* | ||||||
|  Calculates when the business is opened (or on holiday) between two dates. |  Calculates when the business is opened (or on holiday) between two dates. | ||||||
|  Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ... |  Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ... | ||||||
|  | @ -599,6 +611,12 @@ export class OH { | ||||||
|         } |         } | ||||||
|         return ohs; |         return ohs; | ||||||
|     } |     } | ||||||
|  |     public static getMondayBefore(d) { | ||||||
|  |         d = new Date(d); | ||||||
|  |         const day = d.getDay(); | ||||||
|  |         const diff = d.getDate() - day + (day == 0 ? -6 : 1); // adjust when day is sunday
 | ||||||
|  |         return new Date(d.setDate(diff)); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
| import {OH} from "./OpeningHours"; | import {OH} from "./OpeningHours"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
| import opening_hours from "opening_hours"; |  | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
|  | @ -24,7 +23,6 @@ export default class OpeningHoursVisualization extends Toggle { | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     constructor(tags: UIEventSource<any>, state: { osmConnection?: OsmConnection }, key: string, prefix = "", postfix = "") { |     constructor(tags: UIEventSource<any>, state: { osmConnection?: OsmConnection }, key: string, prefix = "", postfix = "") { | ||||||
|         const tagsDirect = tags.data; |  | ||||||
|         const ohTable = new VariableUiElement(tags |         const ohTable = new VariableUiElement(tags | ||||||
|             .map(tags => { |             .map(tags => { | ||||||
|                 const value: string = tags[key]; |                 const value: string = tags[key]; | ||||||
|  | @ -41,16 +39,8 @@ export default class OpeningHoursVisualization extends Toggle { | ||||||
|                         return new FixedUiElement("No opening hours defined with key " + key).SetClass("alert") |                         return new FixedUiElement("No opening hours defined with key " + key).SetClass("alert") | ||||||
|                     } |                     } | ||||||
|                     try { |                     try { | ||||||
|                         // noinspection JSPotentiallyInvalidConstructorUsage
 |                         return OpeningHoursVisualization.CreateFullVisualisation( | ||||||
|                         const oh = new opening_hours(ohtext, { |                             OH.CreateOhObject(tags.data, ohtext)) | ||||||
|                             lat: tagsDirect._lat, |  | ||||||
|                             lon: tagsDirect._lon, |  | ||||||
|                             address: { |  | ||||||
|                                 country_code: tagsDirect._country |  | ||||||
|                             }, |  | ||||||
|                         }, {tag_key: "opening_hours"}); |  | ||||||
| 
 |  | ||||||
|                         return OpeningHoursVisualization.CreateFullVisualisation(oh) |  | ||||||
|                     } catch (e) { |                     } catch (e) { | ||||||
|                         console.warn(e, e.stack); |                         console.warn(e, e.stack); | ||||||
|                         return new Combine([Translations.t.general.opening_hours.error_loading, |                         return new Combine([Translations.t.general.opening_hours.error_loading, | ||||||
|  | @ -78,7 +68,7 @@ export default class OpeningHoursVisualization extends Toggle { | ||||||
| 
 | 
 | ||||||
|         const today = new Date(); |         const today = new Date(); | ||||||
|         today.setHours(0, 0, 0, 0); |         today.setHours(0, 0, 0, 0); | ||||||
|         const lastMonday = OpeningHoursVisualization.getMonday(today); |         const lastMonday = OH.getMondayBefore(today); | ||||||
|         const nextSunday = new Date(lastMonday); |         const nextSunday = new Date(lastMonday); | ||||||
|         nextSunday.setDate(nextSunday.getDate() + 7); |         nextSunday.setDate(nextSunday.getDate() + 7); | ||||||
| 
 | 
 | ||||||
|  | @ -283,11 +273,5 @@ export default class OpeningHoursVisualization extends Toggle { | ||||||
|         return Translations.t.general.opening_hours.closed_until.Subs({date: willOpenAt}) |         return Translations.t.general.opening_hours.closed_until.Subs({date: willOpenAt}) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static getMonday(d) { |  | ||||||
|         d = new Date(d); |  | ||||||
|         const day = d.getDay(); |  | ||||||
|         const diff = d.getDate() - day + (day == 0 ? -6 : 1); // adjust when day is sunday
 |  | ||||||
|         return new Date(d.setDate(diff)); |  | ||||||
|     } |  | ||||||
|     |     | ||||||
| } | } | ||||||
|  | @ -1,50 +1,5 @@ | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; |  | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; |  | ||||||
| import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; |  | ||||||
| import {ElementStorage} from "../../Logic/ElementStorage"; |  | ||||||
| import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource"; |  | ||||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; |  | ||||||
| /* |  | ||||||
| // 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. |  | ||||||
|  Even though actually importing this here would seem cleaner, we don't do this as this breaks some scripts: |  | ||||||
|  - Scripts are ran in ts-node |  | ||||||
|  - ts-node doesn't define the 'window'-object |  | ||||||
|  - Importing this will execute some code which needs the window object |  | ||||||
| 
 |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * The data layer shows all the given geojson elements with the appropriate icon etc |  | ||||||
|  */ |  | ||||||
| export default class ShowDataLayer { | export default class ShowDataLayer { | ||||||
| 
 | 
 | ||||||
|     private static dataLayerIds = 0 |  | ||||||
|     private readonly _leafletMap: UIEventSource<L.Map>; |  | ||||||
|     private readonly _enablePopups: boolean; |  | ||||||
|     private readonly _features: RenderingMultiPlexerFeatureSource |  | ||||||
|     private readonly _layerToShow: LayerConfig; |  | ||||||
|     private readonly _selectedElement: UIEventSource<any> |  | ||||||
|     private readonly allElements: ElementStorage |  | ||||||
|     // Used to generate a fresh ID when needed
 |  | ||||||
|     private _cleanCount = 0; |  | ||||||
|     private geoLayer = undefined; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * A collection of functions to call when the current geolayer is unregistered |  | ||||||
|      */ |  | ||||||
|     private unregister: (() => void)[] = []; |  | ||||||
|     private isDirty = false; |  | ||||||
|     /** |  | ||||||
|      * If the selected element triggers, this is used to lookup the correct layer and to open the popup |  | ||||||
|      * Used to avoid a lot of callbacks on the selected element |  | ||||||
|      * |  | ||||||
|      * Note: the key of this dictionary is 'feature.properties.id+features.geometry.type' as one feature might have multiple presentations |  | ||||||
|      * @private |  | ||||||
|      */ |  | ||||||
|     private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>() |  | ||||||
|     private readonly showDataLayerid: number; |  | ||||||
|     private readonly createPopup: (tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Creates a datalayer. |      * Creates a datalayer. | ||||||
|  | @ -52,299 +7,10 @@ export default class ShowDataLayer { | ||||||
|      * If 'createPopup' is set, this function is called every time that 'popupOpen' is called |      * If 'createPopup' is set, this function is called every time that 'popupOpen' is called | ||||||
|      * @param options |      * @param options | ||||||
|      */ |      */ | ||||||
|     constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) { |     constructor(options) { | ||||||
|         this._leafletMap = options.leafletMap; |  | ||||||
|         this.showDataLayerid = ShowDataLayer.dataLayerIds; |  | ||||||
|         ShowDataLayer.dataLayerIds++ |  | ||||||
|         if (options.features === undefined) { |  | ||||||
|             console.error("Invalid ShowDataLayer invocation: options.features is undefed") |  | ||||||
|             throw "Invalid ShowDataLayer invocation: options.features is undefed" |  | ||||||
|         } |  | ||||||
|         this._features = new RenderingMultiPlexerFeatureSource(options.features, options.layerToShow); |  | ||||||
|         this._layerToShow = options.layerToShow; |  | ||||||
|         this._selectedElement = options.selectedElement |  | ||||||
|         this.allElements = options.state?.allElements; |  | ||||||
|         this.createPopup = undefined; |  | ||||||
|         this._enablePopups = options.popup !== undefined; |  | ||||||
|         if (options.popup !== undefined) { |  | ||||||
|             this.createPopup = options.popup |  | ||||||
|         } |  | ||||||
|         const self = this; |  | ||||||
|         |         | ||||||
|         options.leafletMap.addCallback(_ => { |  | ||||||
|                 return self.update(options) |  | ||||||
|             } |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         this._features.features.addCallback(_ => self.update(options)); |  | ||||||
|         options.doShowLayer?.addCallback(doShow => { |  | ||||||
|             const mp = options.leafletMap.data; |  | ||||||
|             if (mp === null) { |  | ||||||
|                 self.Destroy() |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|             if (mp == undefined) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (doShow) { |  | ||||||
|                 if (self.isDirty) { |  | ||||||
|                     return self.update(options) |  | ||||||
|                 } else { |  | ||||||
|                     mp.addLayer(this.geoLayer) |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 if (this.geoLayer !== undefined) { |  | ||||||
|                     mp.removeLayer(this.geoLayer) |  | ||||||
|                     this.unregister.forEach(f => f()) |  | ||||||
|                     this.unregister = [] |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         this._selectedElement?.addCallbackAndRunD(selected => { |  | ||||||
|             self.openPopupOfSelectedElement(selected) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         this.update(options) |  | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private Destroy() { |  | ||||||
|         this.unregister.forEach(f => f()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private openPopupOfSelectedElement(selected) { |  | ||||||
|         if (selected === undefined) { |  | ||||||
|             return |  | ||||||
|         } |  | ||||||
|         if (this._leafletMap.data === undefined) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         const v = this.leafletLayersPerId.get(selected.properties.id + selected.geometry.type) |  | ||||||
|         if (v === undefined) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         const leafletLayer = v.leafletlayer |  | ||||||
|         const feature = v.feature |  | ||||||
|         if (leafletLayer.getPopup().isOpen()) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         if (selected.properties.id !== feature.properties.id) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (feature.id !== feature.properties.id) { |  | ||||||
|             // Probably a feature which has renamed
 |  | ||||||
|             // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too
 |  | ||||||
|             console.log("Not opening the popup for", feature, "as probably renamed") |  | ||||||
|             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
 |  | ||||||
|         ) { |  | ||||||
|             leafletLayer.openPopup() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private update(options: ShowDataLayerOptions): boolean { |  | ||||||
|         if (this._features.features.data === undefined) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         this.isDirty = true; |  | ||||||
|         if (options?.doShowLayer?.data === false) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         const mp = options.leafletMap.data; |  | ||||||
| 
 |  | ||||||
|         if (mp === null) { |  | ||||||
|             return true; // Unregister as the map has been destroyed
 |  | ||||||
|         } |  | ||||||
|         if (mp === undefined) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this._cleanCount++ |  | ||||||
|         // clean all the old stuff away, if any
 |  | ||||||
|         if (this.geoLayer !== undefined) { |  | ||||||
|             mp.removeLayer(this.geoLayer); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         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 selfLayer = this.geoLayer; |  | ||||||
|         const allFeats = this._features.features.data; |  | ||||||
|         for (const feat of allFeats) { |  | ||||||
|             if (feat === undefined) { |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|             try { |  | ||||||
|                 if (feat.geometry.type === "LineString") { |  | ||||||
|                     const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates) |  | ||||||
|                     const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource<any>(feat.properties); |  | ||||||
|                     let offsettedLine; |  | ||||||
|                     tagsSource |  | ||||||
|                         .map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags), [], undefined, true) |  | ||||||
|                         .withEqualityStabilized((a, b) => { |  | ||||||
|                             if (a === b) { |  | ||||||
|                                 return true |  | ||||||
|                             } |  | ||||||
|                             if (a === undefined || b === undefined) { |  | ||||||
|                                 return false |  | ||||||
|                             } |  | ||||||
|                             return a.offset === b.offset && a.color === b.color && a.weight === b.weight && a.dashArray === b.dashArray |  | ||||||
|                         }) |  | ||||||
|                         .addCallbackAndRunD(lineStyle => { |  | ||||||
|                             if (offsettedLine !== undefined) { |  | ||||||
|                                 self.geoLayer.removeLayer(offsettedLine) |  | ||||||
|                             } |  | ||||||
|                             // @ts-ignore
 |  | ||||||
|                             offsettedLine = L.polyline(coords, lineStyle); |  | ||||||
|                             this.postProcessFeature(feat, offsettedLine) |  | ||||||
|                             offsettedLine.addTo(this.geoLayer) |  | ||||||
| 
 |  | ||||||
|                             // If 'self.geoLayer' is not the same as the layer the feature is added to, we can safely remove this callback
 |  | ||||||
|                             return self.geoLayer !== selfLayer |  | ||||||
|                         }) |  | ||||||
|                 } else { |  | ||||||
|                     this.geoLayer.addData(feat); |  | ||||||
|                 } |  | ||||||
|             } catch (e) { |  | ||||||
|                 console.error("Could not add ", feat, "to the geojson layer in leaflet due to", e, e.stack) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (options.zoomToFeatures ?? false) { |  | ||||||
|             if (this.geoLayer.getLayers().length > 0) { |  | ||||||
|                 try { |  | ||||||
|                     const bounds = this.geoLayer.getBounds() |  | ||||||
|                     mp.fitBounds(bounds, {animate: false}) |  | ||||||
|                 } catch (e) { |  | ||||||
|                     console.debug("Invalid bounds", e) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (options.doShowLayer?.data ?? true) { |  | ||||||
|             mp.addLayer(this.geoLayer) |  | ||||||
|         } |  | ||||||
|         this.isDirty = false; |  | ||||||
|         this.openPopupOfSelectedElement(this._selectedElement?.data) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     private createStyleFor(feature) { |  | ||||||
|         const tagsSource = this.allElements?.addOrGetElement(feature) ?? new UIEventSource<any>(feature.properties); |  | ||||||
|         // Every object is tied to exactly one layer
 |  | ||||||
|         const layer = this._layerToShow |  | ||||||
| 
 |  | ||||||
|         const pointRenderingIndex = feature.pointRenderingIndex |  | ||||||
|         const lineRenderingIndex = feature.lineRenderingIndex |  | ||||||
| 
 |  | ||||||
|         if (pointRenderingIndex !== undefined) { |  | ||||||
|             const style = layer.mapRendering[pointRenderingIndex].GenerateLeafletStyle(tagsSource, this._enablePopups) |  | ||||||
|             return { |  | ||||||
|                 icon: style |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if (lineRenderingIndex !== undefined) { |  | ||||||
|             return layer.lineRendering[lineRenderingIndex].GenerateLeafletStyle(tagsSource.data) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         throw "Neither lineRendering nor mapRendering defined for " + feature |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private pointToLayer(feature, latLng): L.Layer { |  | ||||||
|         // Leaflet cannot handle geojson points natively
 |  | ||||||
|         // We have to convert them to the appropriate icon
 |  | ||||||
|         // Click handling is done in the next step
 |  | ||||||
| 
 |  | ||||||
|         const layer: LayerConfig = this._layerToShow |  | ||||||
|         if (layer === undefined) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         let tagSource = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties) |  | ||||||
|         const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) && this._enablePopups |  | ||||||
|         let style: any = layer.mapRendering[feature.pointRenderingIndex].GenerateLeafletStyle(tagSource, clickable); |  | ||||||
|         const baseElement = style.html; |  | ||||||
|         if (!this._enablePopups) { |  | ||||||
|             baseElement.SetStyle("cursor: initial !important") |  | ||||||
|         } |  | ||||||
|         style.html = style.html.ConstructElement() |  | ||||||
|         return L.marker(latLng, { |  | ||||||
|             icon: L.divIcon(style) |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Post processing - basically adding the popup |  | ||||||
|      * @param feature |  | ||||||
|      * @param leafletLayer |  | ||||||
|      * @private |  | ||||||
|      */ |  | ||||||
|     private postProcessFeature(feature, leafletLayer: L.Layer) { |  | ||||||
|         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 popup = L.popup({ |  | ||||||
|             autoPan: true, |  | ||||||
|             closeOnEscapeKey: true, |  | ||||||
|             closeButton: false, |  | ||||||
|             autoPanPaddingTopLeft: [15, 15], |  | ||||||
| 
 |  | ||||||
|         }, leafletLayer); |  | ||||||
| 
 |  | ||||||
|         leafletLayer.bindPopup(popup); |  | ||||||
| 
 |  | ||||||
|         let infobox: ScrollableFullScreen = undefined; |  | ||||||
|         const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this.showDataLayerid}-${this._cleanCount}-${feature.pointRenderingIndex ?? feature.lineRenderingIndex}-${feature.multiLineStringIndex ?? ""}` |  | ||||||
|         popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>`) |  | ||||||
|         const createpopup = this.createPopup; |  | ||||||
|         leafletLayer.on("popupopen", () => { |  | ||||||
|             if (infobox === undefined) { |  | ||||||
|                 const tags = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties); |  | ||||||
|                 infobox = createpopup(tags, layer); |  | ||||||
| 
 |  | ||||||
|                 infobox.isShown.addCallback(isShown => { |  | ||||||
|                     if (!isShown) { |  | ||||||
|                         leafletLayer.closePopup() |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|             infobox.AttachTo(id) |  | ||||||
|             infobox.Activate(); |  | ||||||
|             this.unregister.push(() => { |  | ||||||
|                 console.log("Destroying infobox") |  | ||||||
|                 infobox.Destroy(); |  | ||||||
|             }) |  | ||||||
|             if (this._selectedElement?.data?.properties?.id !== feature.properties.id) { |  | ||||||
|                 this._selectedElement?.setData(feature) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         // Add the feature to the index to open the popup when needed
 |  | ||||||
|         this.leafletLayersPerId.set(feature.properties.id + feature.geometry.type, { |  | ||||||
|             feature: feature, |  | ||||||
|             leafletlayer: leafletLayer |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
|    |    | ||||||
| } | } | ||||||
|  | @ -24,7 +24,6 @@ import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; | ||||||
| import Minimap from "./Base/Minimap"; | import Minimap from "./Base/Minimap"; | ||||||
| import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; | import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; | ||||||
| import WikipediaBox from "./Wikipedia/WikipediaBox"; | import WikipediaBox from "./Wikipedia/WikipediaBox"; | ||||||
| import SimpleMetaTagger from "../Logic/SimpleMetaTagger"; |  | ||||||
| import MultiApply from "./Popup/MultiApply"; | import MultiApply from "./Popup/MultiApply"; | ||||||
| import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; | import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; | ||||||
| import {SubtleButton} from "./Base/SubtleButton"; | import {SubtleButton} from "./Base/SubtleButton"; | ||||||
|  | @ -48,6 +47,7 @@ import {SubstitutedTranslation} from "./SubstitutedTranslation"; | ||||||
| import {TextField} from "./Input/TextField"; | import {TextField} from "./Input/TextField"; | ||||||
| import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata"; | import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata"; | ||||||
| import {Translation} from "./i18n/Translation"; | import {Translation} from "./i18n/Translation"; | ||||||
|  | import {AllTagsPanel} from "./AllTagsPanel"; | ||||||
| 
 | 
 | ||||||
| export interface SpecialVisualization { | export interface SpecialVisualization { | ||||||
|     funcName: string, |     funcName: string, | ||||||
|  | @ -58,49 +58,6 @@ export interface SpecialVisualization { | ||||||
|     getLayerDependencies?: (argument: string[]) => string[] |     getLayerDependencies?: (argument: string[]) => string[] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class AllTagsPanel extends VariableUiElement { |  | ||||||
| 
 |  | ||||||
|     constructor(tags: UIEventSource<any>, state?) { |  | ||||||
| 
 |  | ||||||
|         const calculatedTags = [].concat( |  | ||||||
|             SimpleMetaTagger.lazyTags, |  | ||||||
|             ...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? [])) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         super(tags.map(tags => { |  | ||||||
|             const parts = []; |  | ||||||
|             for (const key in tags) { |  | ||||||
|                 if (!tags.hasOwnProperty(key)) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
|                 let v = tags[key] |  | ||||||
|                 if (v === "") { |  | ||||||
|                     v = "<b>empty string</b>" |  | ||||||
|                 } |  | ||||||
|                 parts.push([key, v ?? "<b>undefined</b>"]); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             for (const key of calculatedTags) { |  | ||||||
|                 const value = tags[key] |  | ||||||
|                 if (value === undefined) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
|                 let type = ""; |  | ||||||
|                 if(typeof value !== "string"){ |  | ||||||
|                     type = " <i>"+(typeof value)+"</i>" |  | ||||||
|                 } |  | ||||||
|                 parts.push(["<i>" + key + "</i>", value]) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return new Table( |  | ||||||
|                 ["key", "value"], |  | ||||||
|                 parts |  | ||||||
|             ) |  | ||||||
|                 .SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table") |  | ||||||
|         })) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class CloseNoteButton implements SpecialVisualization { | class CloseNoteButton implements SpecialVisualization { | ||||||
|     public readonly funcName = "close_note" |     public readonly funcName = "close_note" | ||||||
|     public readonly docs = "Button to close a note. A predifined text can be defined to close the note with. If the note is already closed, will show a small text." |     public readonly docs = "Button to close a note. A predifined text can be defined to close the note with. If the note is already closed, will show a small text." | ||||||
|  | @ -347,6 +304,9 @@ export default class SpecialVisualizations { | ||||||
|                     example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`", |                     example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`", | ||||||
|                     constr: (state, tagSource, args, _) => { |                     constr: (state, tagSource, args, _) => { | ||||||
| 
 | 
 | ||||||
|  |                         if(state === undefined){ | ||||||
|  |                             return undefined | ||||||
|  |                         } | ||||||
|                         const keys = [...args] |                         const keys = [...args] | ||||||
|                         keys.splice(0, 1) |                         keys.splice(0, 1) | ||||||
|                         const featureStore = state.allElements.ContainingFeatures |                         const featureStore = state.allElements.ContainingFeatures | ||||||
|  | @ -655,7 +615,7 @@ export default class SpecialVisualizations { | ||||||
|                                 if (value === undefined) { |                                 if (value === undefined) { | ||||||
|                                     return undefined |                                     return undefined | ||||||
|                                 } |                                 } | ||||||
|                                 const allUnits = [].concat(...state.layoutToUse.layers.map(lyr => lyr.units)) |                                 const allUnits = [].concat(...(state?.layoutToUse?.layers?.map(lyr => lyr.units) ?? [])) | ||||||
|                                 const unit = allUnits.filter(unit => unit.isApplicableToKey(key))[0] |                                 const unit = allUnits.filter(unit => unit.isApplicableToKey(key))[0] | ||||||
|                                 if (unit === undefined) { |                                 if (unit === undefined) { | ||||||
|                                     return value; |                                     return value; | ||||||
|  |  | ||||||
|  | @ -54,7 +54,7 @@ export class SubstitutedTranslation extends VariableUiElement { | ||||||
|                         } |                         } | ||||||
|                         const viz = proto.special; |                         const viz = proto.special; | ||||||
|                         try { |                         try { | ||||||
|                             return viz.func.constr(state, tagsSource, proto.special.args, DefaultGuiState.state).SetStyle(proto.special.style); |                             return viz.func.constr(state, tagsSource, proto.special.args, DefaultGuiState.state)?.SetStyle(proto.special.style); | ||||||
|                         } catch (e) { |                         } catch (e) { | ||||||
|                             console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) |                             console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) | ||||||
|                             return new FixedUiElement(`Could not generate special rendering for ${viz.func.funcName}(${viz.args.join(", ")}) ${e}`).SetStyle("alert") |                             return new FixedUiElement(`Could not generate special rendering for ${viz.func.funcName}(${viz.args.join(", ")}) ${e}`).SetStyle("alert") | ||||||
|  | @ -80,7 +80,7 @@ export class SubstitutedTranslation extends VariableUiElement { | ||||||
|         } |         } | ||||||
|     }[] { |     }[] { | ||||||
| 
 | 
 | ||||||
|         for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) { |         for (const knownSpecial of extraMappings.concat(SpecialVisualizations.specialVisualizations)) { | ||||||
| 
 | 
 | ||||||
|             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 |             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 | ||||||
|             const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`); |             const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue