forked from MapComplete/MapComplete
		
	Merge feature branch
This commit is contained in:
		
						commit
						a6c752037b
					
				
					 28 changed files with 931 additions and 149 deletions
				
			
		|  | @ -12,7 +12,7 @@ import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte" | |||
| import Link from "../../UI/Base/Link" | ||||
| 
 | ||||
| export default class PanoramaxImageProvider extends ImageProvider { | ||||
|     public static readonly singleton = new PanoramaxImageProvider() | ||||
|     public static readonly singleton: PanoramaxImageProvider = new PanoramaxImageProvider() | ||||
|     private static readonly xyz = new PanoramaxXYZ() | ||||
|     private static defaultPanoramax = new AuthorizedPanoramax( | ||||
|         Constants.panoramax.url, | ||||
|  | @ -126,7 +126,11 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|         if (!Panoramax.isId(value)) { | ||||
|             return undefined | ||||
|         } | ||||
|         return [await this.getInfoFor(value).then((r) => this.featureToImage(<any>r))] | ||||
|         return [await this.getInfo(value)] | ||||
|     } | ||||
| 
 | ||||
|     public async getInfo(hash: string): Promise<ProvidedImage> { | ||||
|       return  await this.getInfoFor(hash).then((r) => this.featureToImage(<any>r)) | ||||
|     } | ||||
| 
 | ||||
|     getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> { | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ export default class OsmObjectDownloader { | |||
|         readonly isUploading: Store<boolean> | ||||
|     } | ||||
|     private readonly backend: string | ||||
|     private historyCache = new Map<string, UIEventSource<OsmObject[]>>() | ||||
|     private historyCache = new Map<string, Promise<OsmObject[]>>() | ||||
| 
 | ||||
|     constructor( | ||||
|         backend: string = "https://api.openstreetmap.org", | ||||
|  | @ -75,49 +75,51 @@ export default class OsmObjectDownloader { | |||
|         return await this.applyPendingChanges(obj) | ||||
|     } | ||||
| 
 | ||||
|     public DownloadHistory(id: NodeId): UIEventSource<OsmNode[]> | ||||
| 
 | ||||
|     public DownloadHistory(id: WayId): UIEventSource<OsmWay[]> | ||||
| 
 | ||||
|     public DownloadHistory(id: RelationId): UIEventSource<OsmRelation[]> | ||||
| 
 | ||||
|     public DownloadHistory(id: OsmId): UIEventSource<OsmObject[]> | ||||
| 
 | ||||
|     public DownloadHistory(id: string): UIEventSource<OsmObject[]> { | ||||
|         if (this.historyCache.has(id)) { | ||||
|             return this.historyCache.get(id) | ||||
|         } | ||||
|     private async _downloadHistoryUncached(id: string): Promise<OsmObject[]> { | ||||
|         const splitted = id.split("/") | ||||
|         const type = splitted[0] | ||||
|         const idN = Number(splitted[1]) | ||||
|         const src = new UIEventSource<OsmObject[]>([]) | ||||
|         this.historyCache.set(id, src) | ||||
|         Utils.downloadJsonCached( | ||||
|         const data = await Utils.downloadJsonCached( | ||||
|             `${this.backend}api/0.6/${type}/${idN}/history`, | ||||
|             10 * 60 * 1000 | ||||
|         ).then((data) => { | ||||
|             const elements: any[] = data.elements | ||||
|             const osmObjects: OsmObject[] = [] | ||||
|             for (const element of elements) { | ||||
|                 let osmObject: OsmObject = null | ||||
|                 element.nodes = [] | ||||
|                 switch (type) { | ||||
|                     case "node": | ||||
|                         osmObject = new OsmNode(idN, element) | ||||
|                         break | ||||
|                     case "way": | ||||
|                         osmObject = new OsmWay(idN, element) | ||||
|                         break | ||||
|                     case "relation": | ||||
|                         osmObject = new OsmRelation(idN, element) | ||||
|                         break | ||||
|                 } | ||||
|                 osmObject?.SaveExtraData(element, []) | ||||
|                 osmObjects.push(osmObject) | ||||
|         ) | ||||
|         const elements: [] = data["elements"] | ||||
|         const osmObjects: OsmObject[] = [] | ||||
|         for (const element of elements) { | ||||
|             let osmObject: OsmObject = null | ||||
|             element["nodes"] = [] | ||||
|             switch (type) { | ||||
|                 case "node": | ||||
|                     osmObject = new OsmNode(idN, element) | ||||
|                     break | ||||
|                 case "way": | ||||
|                     osmObject = new OsmWay(idN, element) | ||||
|                     break | ||||
|                 case "relation": | ||||
|                     osmObject = new OsmRelation(idN, element) | ||||
|                     break | ||||
|             } | ||||
|             src.setData(osmObjects) | ||||
|         }) | ||||
|         return src | ||||
|             osmObject?.SaveExtraData(element, []) | ||||
|             osmObjects.push(osmObject) | ||||
|         } | ||||
|         return osmObjects | ||||
|     } | ||||
| 
 | ||||
|     public downloadHistory(id: NodeId): Promise<OsmNode[]> | ||||
| 
 | ||||
|     public downloadHistory(id: WayId): Promise<OsmWay[]> | ||||
| 
 | ||||
|     public downloadHistory(id: RelationId): Promise<OsmRelation[]> | ||||
| 
 | ||||
|     public downloadHistory(id: OsmId): Promise<OsmObject[]> | ||||
| 
 | ||||
|     public async downloadHistory(id: string): Promise<OsmObject[]> { | ||||
|         if (this.historyCache.has(id)) { | ||||
|             return this.historyCache.get(id) | ||||
|         } | ||||
|         const promise = this._downloadHistoryUncached(id) | ||||
|         this.historyCache.set(id, promise) | ||||
|         return promise | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -26,7 +26,10 @@ export class Overpass { | |||
|     ) { | ||||
|         this._timeout = timeout ?? new ImmutableStore<number>(90) | ||||
|         this._interpreterUrl = interpreterUrl | ||||
|         const optimized = filter.optimize() | ||||
|         if (filter === undefined && !extraScripts) { | ||||
|             throw "Filter is undefined. This is probably a bug. Alternatively, pass an 'extraScript'" | ||||
|         } | ||||
|         const optimized = filter?.optimize() | ||||
|         if (optimized === true || optimized === false) { | ||||
|             throw "Invalid filter: optimizes to true of false" | ||||
|         } | ||||
|  | @ -85,7 +88,7 @@ export class Overpass { | |||
|      * new Overpass(new Tag("key","value"), [], "").buildScript("{{bbox}}") // => `[out:json][timeout:90]{{bbox}};(nwr["key"="value"];);out body;out meta;>;out skel qt;`
 | ||||
|      */ | ||||
|     public buildScript(bbox: string, postCall: string = "", pretty = false): string { | ||||
|         const filters = this._filter.asOverpass() | ||||
|         const filters = this._filter?.asOverpass() ?? [] | ||||
|         let filter = "" | ||||
|         for (const filterOr of filters) { | ||||
|             if (pretty) { | ||||
|  | @ -97,12 +100,13 @@ export class Overpass { | |||
|             } | ||||
|         } | ||||
|         for (const extraScript of this._extraScripts) { | ||||
|             filter += "(" + extraScript + ");" | ||||
|             filter += extraScript | ||||
|         } | ||||
|         return `[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${ | ||||
|             this._includeMeta ? "out meta;" : "" | ||||
|         }>;out skel qt;` | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs the actual script to execute on Overpass with geocoding | ||||
|      * 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink' | ||||
|  |  | |||
|  | @ -727,6 +727,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | |||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse the number and round to the nearest int | ||||
|      * | ||||
|      * @param source | ||||
|      * UIEventSource.asInt(new UIEventSource("123")).data // => 123
 | ||||
|  |  | |||
|  | @ -41,6 +41,7 @@ export default class Constants { | |||
|         "usersettings", | ||||
|         "icons", | ||||
|         "filters", | ||||
|         "usertouched" | ||||
|     ] as const | ||||
|     /** | ||||
|      * Layer IDs of layers which have special properties through built-in hooks | ||||
|  |  | |||
|  | @ -306,7 +306,7 @@ export default class ThemeConfig implements ThemeInformation { | |||
|         return { untranslated, total } | ||||
|     } | ||||
| 
 | ||||
|     public getMatchingLayer(tags: Record<string, string>): LayerConfig | undefined { | ||||
|     public getMatchingLayer(tags: Record<string, string>, blacklistLayers?: Set<string>): LayerConfig | undefined { | ||||
|         if (tags === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
|  | @ -314,6 +314,9 @@ export default class ThemeConfig implements ThemeInformation { | |||
|             return this.getLayer("current_view") | ||||
|         } | ||||
|         for (const layer of this.layers) { | ||||
|             if(blacklistLayers?.has(layer.id)){ | ||||
|                 continue | ||||
|             } | ||||
|             if (!layer.source) { | ||||
|                 if (layer.isShown?.matchesProperties(tags)) { | ||||
|                     return layer | ||||
|  |  | |||
|  | @ -1061,7 +1061,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|     /** | ||||
|      * Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the theme | ||||
|      */ | ||||
|     public getMatchingLayer(properties: Record<string, string>) { | ||||
|     public getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined { | ||||
|         const id = properties.id | ||||
| 
 | ||||
|         if (id.startsWith("summary_")) { | ||||
|  |  | |||
|  | @ -3,24 +3,15 @@ | |||
|   import type { Feature } from "geojson" | ||||
|   import SelectedElementView from "../BigComponents/SelectedElementView.svelte" | ||||
|   import SelectedElementTitle from "../BigComponents/SelectedElementTitle.svelte" | ||||
|   import UserRelatedState from "../../Logic/State/UserRelatedState" | ||||
|   import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource" | ||||
|   import Loading from "./Loading.svelte" | ||||
|   import { onDestroy } from "svelte" | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|   import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider" | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
| 
 | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let selected: Feature | ||||
|   let tags = state.featureProperties.getStore(selected.properties.id) | ||||
| 
 | ||||
|   export let absolute = true | ||||
|   function getLayer(properties: Record<string, string>): LayerConfig { | ||||
|     return state.getMatchingLayer(properties) | ||||
|   } | ||||
| 
 | ||||
|   let layer = getLayer(selected.properties) | ||||
|   let layer = state.getMatchingLayer(selected.properties) | ||||
| 
 | ||||
|   let stillMatches = tags.map( | ||||
|     (tags) => !layer?.source?.osmTags || layer?.source?.osmTags?.matchesProperties(tags) | ||||
|  |  | |||
|  | @ -279,6 +279,11 @@ | |||
|       </Page> | ||||
|     </div> | ||||
| 
 | ||||
|     <a class="flex" href={window.location.protocol + "//" + window.location.host + "/inspector.html"}> | ||||
|       <MagnifyingGlassCircle class="mr-2 h-6 w-6" /> | ||||
|       <Tr t={Translations.t.inspector.menu} /> | ||||
|     </a> | ||||
| 
 | ||||
|     <a class="flex" href="https://github.com/pietervdvn/MapComplete/" target="_blank"> | ||||
|       <Github class="h-6 w-6" /> | ||||
|       <Tr t={Translations.t.general.attribution.gotoSourceCode} /> | ||||
|  |  | |||
|  | @ -1,23 +1,21 @@ | |||
| <script lang="ts"> | ||||
|   import type { Feature } from "geojson" | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { ariaLabel } from "../../Utils/ariaLabel" | ||||
|   import { CloseButton } from "flowbite-svelte" | ||||
| 
 | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let layer: LayerConfig | ||||
|   export let selectedElement: Feature | ||||
|   let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore( | ||||
|   let tags: UIEventSource<Record<string, string>> = state?.featureProperties?.getStore( | ||||
|     selectedElement.properties.id | ||||
|   ) | ||||
|   $: { | ||||
|     tags = state.featureProperties.getStore(selectedElement.properties.id) | ||||
|     tags = state?.featureProperties?.getStore(selectedElement.properties.id) | ||||
|   } | ||||
| 
 | ||||
|   let isTesting = state.featureSwitchIsTesting | ||||
|  |  | |||
							
								
								
									
										46
									
								
								src/UI/History/AggregateImages.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/UI/History/AggregateImages.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| <script lang="ts"> | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { HistoryUtils } from "./HistoryUtils" | ||||
|   import type { Feature } from "geojson" | ||||
|   import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" | ||||
|   import { OsmObject } from "../../Logic/Osm/OsmObject" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import AttributedPanoramaxImage from "./AttributedPanoramaxImage.svelte" | ||||
| 
 | ||||
|   export let onlyShowUsername: string[] | ||||
|   export let features: Feature[] | ||||
| 
 | ||||
|   const downloader = new OsmObjectDownloader() | ||||
|   let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise( | ||||
|     Promise.all(features.map(f => downloader.downloadHistory(f.properties.id))) | ||||
|   ) | ||||
|   let imageKeys = new Set(...["panoramax", "image:streetsign", "image:menu"].map(k => { | ||||
|     const result: string[] = [k] | ||||
|     for (let i = 0; i < 10; i++) { | ||||
|       result.push(k + ":" + i) | ||||
|     } | ||||
|     return result | ||||
|   })) | ||||
|   let usernamesSet = new Set(onlyShowUsername) | ||||
|   let allDiffs: Store<{ | ||||
|     key: string; | ||||
|     value?: string; | ||||
|     oldValue?: string | ||||
|   }[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernamesSet)) | ||||
| 
 | ||||
|   let addedImages = allDiffs.mapD(diffs => [].concat(...diffs.filter(({ key }) => imageKeys.has(key)))) | ||||
| 
 | ||||
| </script> | ||||
| {#if $allDiffs === undefined} | ||||
|   <Loading /> | ||||
| {:else if $addedImages.length === 0} | ||||
|   No images added by this contributor | ||||
| {:else} | ||||
|   <div class="flex"> | ||||
|     {#each $addedImages as imgDiff} | ||||
|       <div class="w-48 h-48"> | ||||
|         <AttributedPanoramaxImage hash={imgDiff.value} /> | ||||
|       </div> | ||||
|     {/each} | ||||
|   </div> | ||||
| {/if} | ||||
							
								
								
									
										105
									
								
								src/UI/History/AggregateView.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/UI/History/AggregateView.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | |||
| <script lang="ts"> | ||||
|   import type { Feature } from "geojson" | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" | ||||
|   import { OsmObject } from "../../Logic/Osm/OsmObject" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import { HistoryUtils } from "./HistoryUtils" | ||||
|   import * as shared_questions from "../../assets/generated/layers/questions.json" | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import AccordionSingle from "../Flowbite/AccordionSingle.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
| 
 | ||||
|   export let onlyShowUsername: string[] | ||||
|   export let features: Feature[] | ||||
| 
 | ||||
|   let usernames = new Set(onlyShowUsername) | ||||
| 
 | ||||
|   const downloader = new OsmObjectDownloader() | ||||
|   let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise( | ||||
|     Promise.all(features.map(f => downloader.downloadHistory(f.properties.id))) | ||||
|   ) | ||||
|   let allDiffs: Store<{ | ||||
|     key: string; | ||||
|     value?: string; | ||||
|     oldValue?: string | ||||
|   }[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernames)) | ||||
| 
 | ||||
|   const trs = shared_questions.tagRenderings.map(tr => new TagRenderingConfig(tr)) | ||||
| 
 | ||||
|   function detectQuestion(key: string): TagRenderingConfig { | ||||
|     return trs.find(tr => tr.freeform?.key === key) | ||||
|   } | ||||
| 
 | ||||
|   const mergedCount: Store<{ | ||||
|     key: string; | ||||
|     tr: TagRenderingConfig; | ||||
|     count: number; | ||||
|     values: { value: string; count: number }[] | ||||
|   }[]> = allDiffs.mapD(allDiffs => { | ||||
|     const keyCounts = new Map<string, Map<string, number>>() | ||||
|     for (const diff of allDiffs) { | ||||
|       const k = diff.key | ||||
|       if (!keyCounts.has(k)) { | ||||
|         keyCounts.set(k, new Map<string, number>()) | ||||
|       } | ||||
|       const valueCounts = keyCounts.get(k) | ||||
|       const v = diff.value ?? "" | ||||
|       valueCounts.set(v, 1 + (valueCounts.get(v) ?? 0)) | ||||
|     } | ||||
| 
 | ||||
|     const perKey: { | ||||
|       key: string, tr: TagRenderingConfig, count: number, values: | ||||
|         { value: string, count: number }[] | ||||
|     }[] = [] | ||||
|     keyCounts.forEach((values, key) => { | ||||
|       const keyTotal: { value: string, count: number }[] = [] | ||||
|       values.forEach((count, value) => { | ||||
|         keyTotal.push({ value, count }) | ||||
|       }) | ||||
|       let countForKey = 0 | ||||
|       for (const { count } of keyTotal) { | ||||
|         countForKey += count | ||||
|       } | ||||
|       keyTotal.sort((a, b) => b.count - a.count) | ||||
|       const tr = detectQuestion(key) | ||||
|       perKey.push({ count: countForKey, tr, key, values: keyTotal }) | ||||
|     }) | ||||
|     perKey.sort((a, b) => b.count - a.count) | ||||
| 
 | ||||
|     return perKey | ||||
|   }) | ||||
| 
 | ||||
|   const t = Translations.t.inspector | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#if allHistories === undefined} | ||||
|   <Loading /> | ||||
| {:else if $allDiffs !== undefined} | ||||
|   {#each $mergedCount as diff} | ||||
|     <h3> | ||||
|       {#if diff.tr} | ||||
|         <Tr t={diff.tr.question} /> | ||||
|       {:else} | ||||
|         {diff.key} | ||||
|       {/if} | ||||
|     </h3> | ||||
|     <AccordionSingle> | ||||
|       <span slot="header"> | ||||
| <Tr t={t.answeredCountTimes.Subs(diff)} /> | ||||
|       </span> | ||||
|       <ul> | ||||
|         {#each diff.values as value} | ||||
|           <li> | ||||
|             <b>{value.value}</b> | ||||
|             {#if value.count > 1} | ||||
|               - {value.count} | ||||
|             {/if} | ||||
|           </li> | ||||
|         {/each} | ||||
|       </ul> | ||||
|     </AccordionSingle> | ||||
|   {/each} | ||||
| {/if} | ||||
							
								
								
									
										13
									
								
								src/UI/History/AttributedPanoramaxImage.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/UI/History/AttributedPanoramaxImage.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| <script lang="ts"> | ||||
|   import AttributedImage from "../Image/AttributedImage.svelte" | ||||
|   import PanoramaxImageProvider from "../../Logic/ImageProviders/Panoramax" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" | ||||
| 
 | ||||
|   export let hash: string | ||||
|   let image: UIEventSource<ProvidedImage> = UIEventSource.FromPromise(PanoramaxImageProvider.singleton.getInfo(hash)) | ||||
| </script> | ||||
| 
 | ||||
| {#if $image !== undefined} | ||||
|   <AttributedImage image={$image}></AttributedImage> | ||||
| {/if} | ||||
							
								
								
									
										106
									
								
								src/UI/History/History.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/UI/History/History.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * Shows a history of the object which focuses on changes made by a certain username | ||||
|    */ | ||||
|   import type { OsmId } from "../../Models/OsmFeature" | ||||
|   import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import { HistoryUtils } from "./HistoryUtils" | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
| 
 | ||||
|   export let onlyShowChangesBy: string[] | ||||
|   export let id: OsmId | ||||
| 
 | ||||
|   let usernames = new Set(onlyShowChangesBy) | ||||
|   let fullHistory = UIEventSource.FromPromise(new OsmObjectDownloader().downloadHistory(id)) | ||||
| 
 | ||||
|   let partOfLayer = fullHistory.mapD(history => history.map(step => ({ | ||||
|     step, | ||||
|     layer: HistoryUtils.determineLayer(step.tags) | ||||
|   }))) | ||||
|   let filteredHistory = partOfLayer.mapD(history => | ||||
|     history.filter(({ step }) => { | ||||
|       if (usernames.size == 0) { | ||||
|         return true | ||||
|       } | ||||
|       console.log("Checking if ", step.tags["_last_edit:contributor"],"is contained in", onlyShowChangesBy) | ||||
|       return usernames.has(step.tags["_last_edit:contributor"]) | ||||
| 
 | ||||
|     }).map(({ step, layer }) => { | ||||
|       const diff = HistoryUtils.tagHistoryDiff(step, fullHistory.data) | ||||
|       return { step, layer, diff } | ||||
|     })) | ||||
| 
 | ||||
|   let lastStep = filteredHistory.mapD(history => history.at(-1)) | ||||
|   let allGeometry = filteredHistory.mapD(all => !all.some(x => x.diff.length > 0)) | ||||
|   /** | ||||
|    * These layers are only shown if there are tag changes as well | ||||
|    */ | ||||
|   const ignoreLayersIfNoChanges: ReadonlySet<string> = new Set(["walls_and_buildings"]) | ||||
|   const t = Translations.t.inspector.previousContributors | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#if !$allGeometry || !ignoreLayersIfNoChanges.has($lastStep?.layer?.id)} | ||||
|   {#if $lastStep?.layer} | ||||
|     <a href={"https://openstreetmap.org/" + $lastStep.step.tags.id} target="_blank"> | ||||
|       <h3 class="flex items-center gap-x-2"> | ||||
|         <div class="w-8 h-8 shrink-0 inline-block"> | ||||
|           <ToSvelte construct={$lastStep.layer?.defaultIcon($lastStep.step.tags)} /> | ||||
|         </div> | ||||
|         <Tr t={$lastStep.layer?.title?.GetRenderValue($lastStep.step.tags)?.Subs($lastStep.step.tags)} /> | ||||
|       </h3> | ||||
|     </a> | ||||
|   {/if} | ||||
| 
 | ||||
|   {#if !$filteredHistory} | ||||
|     <Loading>Loading history...</Loading> | ||||
|   {:else if $filteredHistory.length === 0} | ||||
|     <Tr t={t.onlyGeometry} /> | ||||
|   {:else} | ||||
|     <table class="w-full m-1"> | ||||
|       {#each $filteredHistory as { step, layer }} | ||||
| 
 | ||||
|         {#if step.version === 1} | ||||
|           <tr> | ||||
|             <td colspan="3"> | ||||
|               <h3> | ||||
|                 <Tr t={t.createdBy.Subs({contributor: step.tags["_last_edit:contributor"]})} /> | ||||
|               </h3> | ||||
|             </td> | ||||
|           </tr> | ||||
|         {/if} | ||||
|         {#if HistoryUtils.tagHistoryDiff(step, $fullHistory).length === 0} | ||||
|           <tr> | ||||
|             <td class="font-bold justify-center flex w-full" colspan="3"> | ||||
|               <Tr t={t.onlyGeometry} /> | ||||
|             </td> | ||||
|           </tr> | ||||
|         {:else} | ||||
|           {#each HistoryUtils.tagHistoryDiff(step, $fullHistory) as diff} | ||||
|             <tr> | ||||
|               <td><a href={"https://osm.org/changeset/"+step.tags["_last_edit:changeset"]} | ||||
|                      target="_blank">{step.version}</a></td> | ||||
|               <td>{layer?.id ?? "Unknown layer"}</td> | ||||
|               {#if diff.oldValue === undefined} | ||||
|                 <td>{diff.key}</td> | ||||
|                 <td>{diff.value}</td> | ||||
|               {:else if diff.value === undefined } | ||||
|                 <td>{diff.key}</td> | ||||
|                 <td class="line-through"> {diff.value}</td> | ||||
|               {:else} | ||||
|                 <td>{diff.key}</td> | ||||
|                 <td><span class="line-through"> {diff.oldValue}</span> → {diff.value}</td> | ||||
|               {/if} | ||||
| 
 | ||||
| 
 | ||||
|             </tr> | ||||
|           {/each} | ||||
|         {/if} | ||||
|       {/each} | ||||
|     </table> | ||||
|   {/if} | ||||
| {/if} | ||||
							
								
								
									
										51
									
								
								src/UI/History/HistoryUtils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/UI/History/HistoryUtils.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | |||
| import * as all_layers from "../../assets/generated/themes/personal.json" | ||||
| import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" | ||||
| import { OsmObject } from "../../Logic/Osm/OsmObject" | ||||
| 
 | ||||
| export class HistoryUtils { | ||||
| 
 | ||||
|     public static readonly personalTheme = new ThemeConfig(<any> all_layers, true) | ||||
|     private static ignoredLayers = new Set<string>(["fixme"]) | ||||
|     public static determineLayer(properties: Record<string, string>){ | ||||
|         return this.personalTheme.getMatchingLayer(properties, this.ignoredLayers) | ||||
|     } | ||||
| 
 | ||||
|     public static tagHistoryDiff(step: OsmObject, history: OsmObject[]): { | ||||
|         key: string, | ||||
|         value?: string, | ||||
|         oldValue?: string, | ||||
|         step: OsmObject | ||||
|     }[] { | ||||
|         const previous = history[step.version - 2] | ||||
|         if (!previous) { | ||||
|             return Object.keys(step.tags).filter(key => !key.startsWith("_") && key !== "id").map(key => ({ | ||||
|                 key, value: step.tags[key], step | ||||
|             })) | ||||
|         } | ||||
|         const previousTags = previous.tags | ||||
|         return Object.keys(step.tags).filter(key => !key.startsWith("_") ) | ||||
|             .map(key => { | ||||
|                 const value = step.tags[key] | ||||
|                 const oldValue = previousTags[key] | ||||
|                 return { | ||||
|                     key, value, oldValue, step | ||||
|                 } | ||||
|             }).filter(ch => ch.oldValue !== ch.value) | ||||
|     } | ||||
| 
 | ||||
|     public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: Set<string>){ | ||||
|         const allDiffs: {key: string, oldValue?: string, value?: string}[] = [].concat(...histories.map( | ||||
|             history => { | ||||
|                 const filtered = history.filter(step => !onlyShowUsername || onlyShowUsername?.has(step.tags["_last_edit:contributor"] )) | ||||
|                 const diffs: { | ||||
|                     key: string; | ||||
|                     value?: string; | ||||
|                     oldValue?: string | ||||
|                 }[][] = filtered.map(step => HistoryUtils.tagHistoryDiff(step, history)) | ||||
|                 return [].concat(...diffs) | ||||
|             } | ||||
|         )) | ||||
|         return allDiffs | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										107
									
								
								src/UI/History/PreviouslySpiedUsers.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/UI/History/PreviouslySpiedUsers.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte" | ||||
|   import { createEventDispatcher } from "svelte" | ||||
|   import { XCircleIcon } from "@babeard/svelte-heroicons/solid" | ||||
|   import AccordionSingle from "../Flowbite/AccordionSingle.svelte" | ||||
|   import Dropdown from "../Base/Dropdown.svelte" | ||||
| 
 | ||||
|   export let osmConnection: OsmConnection | ||||
|   export let inspectedContributors: UIEventSource<{ | ||||
|     name: string, | ||||
|     visitedTime: string, | ||||
|     label: string | ||||
|   }[]> | ||||
|   let dispatch = createEventDispatcher<{ selectUser: string }>() | ||||
| 
 | ||||
|   let labels = UIEventSource.asObject<string[]>(osmConnection.getPreference("previously-spied-labels"), []) | ||||
|   let labelField = "" | ||||
| 
 | ||||
|   function remove(user: string) { | ||||
|     inspectedContributors.set(inspectedContributors.data.filter(entry => entry.name !== user)) | ||||
|   } | ||||
| 
 | ||||
|   function addLabel() { | ||||
|     if (labels.data.indexOf(labelField) >= 0) { | ||||
|       return | ||||
|     } | ||||
|     labels.data.push(labelField) | ||||
|     labels.ping() | ||||
|     labelField = "" | ||||
|   } | ||||
| 
 | ||||
|   function sort(key: string) { | ||||
|     console.log("Sorting on", key) | ||||
|     inspectedContributors.data.sort((a, b) => (a[key] ?? "").localeCompare(b[key] ?? "")) | ||||
|     inspectedContributors.ping() | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <LoginToggle ignoreLoading state={{osmConnection}}> | ||||
|   <table class="w-full"> | ||||
|     <tr> | ||||
|       <td> | ||||
|         <button class="as-link cursor-pointer" on:click={() => sort("name")}> | ||||
|           Contributor | ||||
|         </button> | ||||
|       </td> | ||||
|       <td> | ||||
| 
 | ||||
|         <button class="as-link cursor-pointer" on:click={() => sort("visitedTime")}> | ||||
|           Visited time | ||||
|         </button> | ||||
|       </td> | ||||
|       <td> | ||||
|         <button class="as-link cursor-pointer" on:click={() => sort("label")}>Label</button> | ||||
|       </td> | ||||
|       <td>Remove</td> | ||||
|     </tr> | ||||
|     {#each $inspectedContributors as c} | ||||
|       <tr> | ||||
|         <td> | ||||
|           <button class="as-link" on:click={() => dispatch("selectUser", c.name)}>{c.name}</button> | ||||
|         </td> | ||||
|         <td> | ||||
|           {c.visitedTime} | ||||
|         </td> | ||||
|         <td> | ||||
|           <select bind:value={c.label} on:change={() => inspectedContributors.ping()}> | ||||
|             <option value={undefined}><i>No label</i></option> | ||||
|             {#each $labels as l} | ||||
|               <option value={l}>{l}</option> | ||||
|             {/each} | ||||
|           </select> | ||||
|         </td> | ||||
|         <td> | ||||
|           <XCircleIcon class="w-6 h-6" on:click={() => remove(c.name)} /> | ||||
|         </td> | ||||
|       </tr> | ||||
|     {/each} | ||||
|   </table> | ||||
| 
 | ||||
|   <AccordionSingle> | ||||
| 
 | ||||
|     <div slot="header">Labels</div> | ||||
|     {#if $labels.length === 0} | ||||
|       No labels | ||||
|     {:else} | ||||
|       {#each $labels as label} | ||||
|         <div class="mx-2">{label} | ||||
|           <button class:disabled={!$inspectedContributors.some(c => c.label === label)} on:click={() => {dispatch("selectUser", | ||||
|         inspectedContributors.data.filter(c =>c.label === label).map(c => c .name).join(";") | ||||
|         )}}>See all changes for these users | ||||
|           </button> | ||||
|         </div> | ||||
|       {/each} | ||||
|     {/if} | ||||
|     <div class="interactive flex m-2 items-center gap-x-2 rounded-lg p-2"> | ||||
|       <div class="shrink-0">Create a new label</div> | ||||
|       <input bind:value={labelField} type="text" /> | ||||
|       <button on:click={() => addLabel()} class:disabled={!(labelField?.length > 0) } class="disabled shrink-0">Add | ||||
|         label | ||||
|       </button> | ||||
|     </div> | ||||
|   </AccordionSingle> | ||||
| </LoginToggle> | ||||
|  | @ -28,22 +28,24 @@ | |||
|   export let imgClass: string = undefined | ||||
|   export let state: SpecialVisualizationState = undefined | ||||
|   export let attributionFormat: "minimal" | "medium" | "large" = "medium" | ||||
|   export let previewedImage: UIEventSource<ProvidedImage> | ||||
|   export let previewedImage: UIEventSource<ProvidedImage> = undefined | ||||
|   export let canZoom = previewedImage !== undefined | ||||
|   let loaded = false | ||||
|   let showBigPreview = new UIEventSource(false) | ||||
|   onDestroy( | ||||
|     showBigPreview.addCallbackAndRun((shown) => { | ||||
|       if (!shown) { | ||||
|         previewedImage.set(undefined) | ||||
|         previewedImage?.set(undefined) | ||||
|       } | ||||
|     }) | ||||
|   ) | ||||
|   if(previewedImage){ | ||||
|   onDestroy( | ||||
|     previewedImage.addCallbackAndRun((previewedImage) => { | ||||
|       showBigPreview.set(previewedImage?.id === image.id) | ||||
|     }) | ||||
|   ) | ||||
|   } | ||||
| 
 | ||||
|   function highlight(entered: boolean = true) { | ||||
|     if (!entered) { | ||||
|  | @ -82,7 +84,7 @@ | |||
|       class="normal-background" | ||||
|       on:click={() => { | ||||
|         console.log("Closing") | ||||
|         previewedImage.set(undefined) | ||||
|         previewedImage?.set(undefined) | ||||
|       }} | ||||
|     /> | ||||
|   </div> | ||||
|  | @ -124,7 +126,7 @@ | |||
|       {#if canZoom && loaded} | ||||
|         <div | ||||
|           class="bg-black-transparent absolute right-0 top-0 rounded-bl-full" | ||||
|           on:click={() => previewedImage.set(image)} | ||||
|           on:click={() => previewedImage?.set(image)} | ||||
|         > | ||||
|           <MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" /> | ||||
|         </div> | ||||
|  |  | |||
							
								
								
									
										227
									
								
								src/UI/InspectorGUI.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								src/UI/InspectorGUI.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,227 @@ | |||
| <script lang="ts"> | ||||
|   import { UIEventSource } from "../Logic/UIEventSource" | ||||
|   import { QueryParameters } from "../Logic/Web/QueryParameters" | ||||
|   import ValidatedInput from "./InputElement/ValidatedInput.svelte" | ||||
|   import { Overpass } from "../Logic/Osm/Overpass" | ||||
|   import Constants from "../Models/Constants" | ||||
|   import MaplibreMap from "./Map/MaplibreMap.svelte" | ||||
|   import { MapLibreAdaptor } from "./Map/MapLibreAdaptor" | ||||
|   import { Map as MlMap } from "maplibre-gl" | ||||
|   import ShowDataLayer from "./Map/ShowDataLayer" | ||||
|   import * as inspector_theme from "../assets/generated/themes/inspector.json" | ||||
| 
 | ||||
|   import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||
|   import type { Feature } from "geojson" | ||||
|   import Loading from "./Base/Loading.svelte" | ||||
|   import { linear } from "svelte/easing" | ||||
|   import { Drawer } from "flowbite-svelte" | ||||
|   import ThemeConfig from "../Models/ThemeConfig/ThemeConfig" | ||||
|   import History from "./History/History.svelte" | ||||
|   import TitledPanel from "./Base/TitledPanel.svelte" | ||||
|   import { XCircleIcon } from "@babeard/svelte-heroicons/solid" | ||||
|   import { Utils } from "../Utils" | ||||
|   import AggregateView from "./History/AggregateView.svelte" | ||||
|   import { HistoryUtils } from "./History/HistoryUtils" | ||||
|   import AggregateImages from "./History/AggregateImages.svelte" | ||||
|   import Page from "./Base/Page.svelte" | ||||
|   import PreviouslySpiedUsers from "./History/PreviouslySpiedUsers.svelte" | ||||
|   import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||
|   import MagnifyingGlassCircle from "@babeard/svelte-heroicons/outline/MagnifyingGlassCircle" | ||||
|   import Translations from "./i18n/Translations" | ||||
|   import Tr from "./Base/Tr.svelte" | ||||
| 
 | ||||
|   let username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user") | ||||
|   let step = new UIEventSource<"waiting" | "loading" | "done">("waiting") | ||||
|   let map = new UIEventSource<MlMap>(undefined) | ||||
|   let zoom = UIEventSource.asFloat(QueryParameters.GetQueryParameter("z", "0")) | ||||
|   let lat = UIEventSource.asFloat(QueryParameters.GetQueryParameter("lat", "0")) | ||||
|   let lon = UIEventSource.asFloat(QueryParameters.GetQueryParameter("lon", "0")) | ||||
|   let theme = new ThemeConfig(<any>inspector_theme, true) | ||||
|   let layer = theme.layers.find(l => l.id === "usertouched") | ||||
|   // Is this a dirty hack? Yes it is! | ||||
|   theme.getMatchingLayer = () => { | ||||
|     return layer | ||||
|   } | ||||
|   let loadingData = false | ||||
|   let selectedElement = new UIEventSource<Feature>(undefined) | ||||
| 
 | ||||
|   let maplibremap: MapLibreAdaptor = new MapLibreAdaptor(map, { | ||||
|     zoom, | ||||
|     location: new UIEventSource<{ lon: number; lat: number }>({ lat: lat.data, lon: lon.data }) | ||||
|   }) | ||||
|   maplibremap.location.stabilized(500).addCallbackAndRunD(l => { | ||||
|     lat.set(l.lat) | ||||
|     lon.set(l.lon) | ||||
|   }) | ||||
| 
 | ||||
|   let allLayers = HistoryUtils.personalTheme.layers | ||||
|     let layersNoFixme = allLayers.filter(l => l.id !== "fixme") | ||||
| let fixme = allLayers.find(l => l.id === "fixme") | ||||
|   let featuresStore = new UIEventSource<Feature[]>([]) | ||||
|   let features = new StaticFeatureSource(featuresStore) | ||||
|   ShowDataLayer.showMultipleLayers(map, features, [...layersNoFixme, fixme] , { | ||||
|     zoomToFeatures: true, | ||||
|     onClick: (f: Feature) => { | ||||
|       selectedElement.set(undefined) | ||||
|       Utils.waitFor(200).then(() => { | ||||
|         selectedElement.set(f) | ||||
|       }) | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   let osmConnection = new OsmConnection() | ||||
|   let inspectedContributors: UIEventSource<{ | ||||
|     name: string, | ||||
|     visitedTime: string, | ||||
|     label: string | ||||
|   }[]> = UIEventSource.asObject( | ||||
|     osmConnection.getPreference("spied-upon-users"), []) | ||||
| 
 | ||||
|   async function load() { | ||||
|     const user = username.data | ||||
|     if(user.indexOf(";")<0){ | ||||
| 
 | ||||
|       const inspectedData = inspectedContributors.data | ||||
|       const previousEntry = inspectedData.find(e => e.name === user) | ||||
|       if (previousEntry) { | ||||
|         previousEntry.visitedTime = new Date().toISOString() | ||||
|       } else { | ||||
|         inspectedData.push({ | ||||
|           label: undefined, | ||||
|           visitedTime: new Date().toISOString(), | ||||
|           name: user | ||||
|         }) | ||||
|       } | ||||
|       inspectedContributors.ping() | ||||
|     } | ||||
| 
 | ||||
|     step.setData("loading") | ||||
|     featuresStore.set([]) | ||||
|     const overpass = new Overpass(undefined, user.split(";").map(user => "nw(user_touched:\"" + user + "\");"), Constants.defaultOverpassUrls[0]) | ||||
|     if (!maplibremap.bounds.data) { | ||||
|       return | ||||
|     } | ||||
|     loadingData = true | ||||
|     const [data, date] = await overpass.queryGeoJson(maplibremap.bounds.data) | ||||
|     console.log("Overpass result:", data) | ||||
|     loadingData = false | ||||
|     console.log(data, date) | ||||
|     featuresStore.set(data.features) | ||||
|     console.log("Loaded", data.features.length) | ||||
|   } | ||||
| 
 | ||||
|   map.addCallbackAndRunD(() => { | ||||
|     // when the map is loaded: attempt to load the user given via Queryparams | ||||
|     if (username.data) { | ||||
|       console.log("Current username is", username.data) | ||||
|       load() | ||||
|     } | ||||
|     return true | ||||
|   }) | ||||
| 
 | ||||
|   let mode: "map" | "table" | "aggregate" | "images" = "map" | ||||
| 
 | ||||
|   let showPreviouslyVisited = new UIEventSource(true) | ||||
| const t = Translations.t.inspector | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col w-full h-full"> | ||||
| 
 | ||||
|   <div class="flex gap-x-2 items-center low-interaction p-2"> | ||||
|     <MagnifyingGlassCircle class="w-12 h-12"/> | ||||
|     <h1 class="flex-shrink-0 m-0 mx-2"> | ||||
|       <Tr t={t.title}/> | ||||
|     </h1> | ||||
|     <ValidatedInput type="string" value={username} on:submit={() => load()} /> | ||||
|     {#if loadingData} | ||||
|       <Loading /> | ||||
|     {:else} | ||||
|       <button class="primary" on:click={() => load()}> | ||||
|         <Tr t={t.load}/> | ||||
|       </button> | ||||
|     {/if} | ||||
|     <button on:click={() => showPreviouslyVisited.setData(true)}> | ||||
|      <Tr t={t.earlierInspected}/> | ||||
|     </button> | ||||
|     <a href="./index.html" class="button"> | ||||
|       <Tr t={t.backToIndex}/> | ||||
|     </a> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="flex"> | ||||
|     <button class:primary={mode === "map"} on:click={() => mode = "map"}> | ||||
|       <Tr t={t.mapView}/> | ||||
|     </button> | ||||
|     <button class:primary={mode === "table"} on:click={() => mode = "table"}> | ||||
|       <Tr t={t.tableView}/> | ||||
|     </button> | ||||
|     <button class:primary={mode === "aggregate"} on:click={() => mode = "aggregate"}> | ||||
|       <Tr t={t.aggregateView}/> | ||||
|     </button> | ||||
|     <button class:primary={mode === "images"} on:click={() => mode = "images"}> | ||||
|       <Tr t={t.images}/> | ||||
|     </button> | ||||
|   </div> | ||||
| 
 | ||||
|   {#if mode === "map"} | ||||
|     {#if $selectedElement !== undefined} | ||||
|       <!-- right modal with the selected element view --> | ||||
|       <Drawer | ||||
|         placement="right" | ||||
|         transitionType="fly" | ||||
|         activateClickOutside={false} | ||||
|         backdrop={false} | ||||
|         id="drawer-right" | ||||
|         width="w-full md:w-6/12 lg:w-5/12 xl:w-4/12" | ||||
|         rightOffset="inset-y-0 right-0" | ||||
|         transitionParams={{ | ||||
|         x: 640, | ||||
|         duration: 0, | ||||
|         easing: linear, | ||||
|       }} | ||||
|         divClass="overflow-y-auto z-50 bg-white" | ||||
|         hidden={$selectedElement === undefined} | ||||
|         on:close={() => { | ||||
|         selectedElement.setData(undefined) | ||||
|       }} | ||||
|       > | ||||
| 
 | ||||
|         <TitledPanel> | ||||
|           <div slot="title" class="flex justify-between"> | ||||
| 
 | ||||
|             <a target="_blank" rel="noopener" | ||||
|                href={"https://osm.org/"+$selectedElement.properties.id}>{$selectedElement.properties.id}</a> | ||||
|             <XCircleIcon class="w-6 h-6" on:click={() => selectedElement.set(undefined)} /> | ||||
|           </div> | ||||
| 
 | ||||
|           <History onlyShowChangesBy={$username} id={$selectedElement.properties.id}></History> | ||||
|         </TitledPanel> | ||||
|       </Drawer> | ||||
|     {/if} | ||||
| 
 | ||||
|     <div class="flex-grow overflow-hidden m-1 rounded-xl"> | ||||
|       <MaplibreMap map={map} mapProperties={maplibremap} autorecovery={true} /> | ||||
|     </div> | ||||
|   {:else if mode === "table"} | ||||
|     <div class="m-2 h-full overflow-y-auto"> | ||||
|       {#each $featuresStore as f} | ||||
|         <History onlyShowChangesBy={$username?.split(";")} id={f.properties.id} /> | ||||
|       {/each} | ||||
|     </div> | ||||
|   {:else if mode === "aggregate"} | ||||
|     <div class="m-2 h-full overflow-y-auto"> | ||||
|       <AggregateView features={$featuresStore} onlyShowUsername={$username?.split(";")} /> | ||||
|     </div> | ||||
|   {:else if mode === "images"} | ||||
|     <div class="m-2 h-full overflow-y-auto"> | ||||
|       <AggregateImages features={$featuresStore} onlyShowUsername={$username?.split(";")} /> | ||||
|     </div> | ||||
|   {/if} | ||||
| </div> | ||||
| 
 | ||||
| <Page shown={showPreviouslyVisited}> | ||||
|   <div slot="header">Earlier inspected constributors</div> | ||||
|   <PreviouslySpiedUsers {osmConnection} {inspectedContributors} on:selectUser={(e) => { | ||||
|     username.set(e.detail); load();showPreviouslyVisited.set(false) | ||||
|   }}  /> | ||||
| </Page> | ||||
							
								
								
									
										5
									
								
								src/UI/InspectorGUI.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/UI/InspectorGUI.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| import InspectorGUI from "./InspectorGUI.svelte" | ||||
| 
 | ||||
| new InspectorGUI({ | ||||
|     target: document.getElementById("main"), | ||||
| }) | ||||
|  | @ -97,7 +97,7 @@ export class DeleteFlowState { | |||
|                 if (allByMyself.data === null && useTheInternet) { | ||||
|                     // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
 | ||||
|                     const hist = this.objectDownloader | ||||
|                         .DownloadHistory(id) | ||||
|                         .downloadHistory(id) | ||||
|                         .map((versions) => | ||||
|                             versions.map((version) => | ||||
|                                 Number(version.tags["_last_edit:contributor:uid"]) | ||||
|  |  | |||
|  | @ -87,7 +87,7 @@ export interface SpecialVisualizationState { | |||
|     readonly geocodedImages: UIEventSource<Feature[]> | ||||
|     readonly searchState: SearchState | ||||
| 
 | ||||
|     getMatchingLayer(properties: Record<string, string>) | ||||
|     getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined | ||||
| 
 | ||||
|     showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer | ||||
|     reportError(message: string | Error | XMLHttpRequest, extramessage?: string): Promise<void> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue