diff --git a/Customizations/LayerDefinition.ts b/Customizations/LayerDefinition.ts index bd64aa4..945f299 100644 --- a/Customizations/LayerDefinition.ts +++ b/Customizations/LayerDefinition.ts @@ -11,13 +11,22 @@ import {TagRenderingOptions} from "./TagRendering"; export class LayerDefinition { + /** + * This name is shown in the 'add XXX button' + */ name: string; newElementTags: Tag[] icon: string; minzoom: number; overpassFilter: TagsFilter; + /** + * This UIElement is rendered as title element in the popup + */ title: TagRenderingOptions; + /** + * These are the questions/shown attributes in the popup + */ elementsToShow: TagRenderingOptions[]; style: (tags: any) => { color: string, icon: any }; diff --git a/Customizations/TagRendering.ts b/Customizations/TagRendering.ts index 2158baa..8c11961 100644 --- a/Customizations/TagRendering.ts +++ b/Customizations/TagRendering.ts @@ -23,9 +23,25 @@ export class TagRenderingOptions { constructor(options: { + + /** + * What is the priority of the question. + * By default, in the popup of a feature, only one question is shown at the same time. If multiple questions are unanswered, the question with the highest priority is asked first + */ priority?: number + /** + * This is the string that is shown in the popup if this tag is missing. + * + * If 'question' is undefined, then the question is never asked at all + * If the question is "" (empty string) then the question is + */ question?: string, + + /** + * Optional: + * if defined, this a common piece of tag that is shown in front of every mapping (except freeform) + */ primer?: string, tagsPreprocessor?: ((tags: any) => any), freeform?: { @@ -34,6 +50,18 @@ export class TagRenderingOptions { placeholder?: string, extraTags?: TagsFilter, }, + /** + * Mappings convert a well-known tag combination into a user friendly text. + * It converts e.g. 'access=yes' into 'this area can be accessed' + * + * If there are multiple tags that should be matched, And can be used. All tags in AND will be added when the question is picked (and the corresponding text will only be shown if all tags are present). + * If AND is used, it is best practice to make sure every used tag is in every option (with empty string) to erase extra tags. + * + * If a 'k' is null, then this one is shown by default. It can be used to force a default value, e.g. to show that the name of a POI is not (yet) known . + * A mapping where 'k' is null will not be shown as option in the radio buttons. + * + * + */ mappings?: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[] }) { this.options = options; diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index 3d66d03..87ba7c6 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -190,15 +190,16 @@ export class FilteredLayer { }); - const uiElement = self._showOnPopup(eventSource); - layer.bindPopup(uiElement.Render()); layer.on("click", function (e) { console.log("Selected ", feature) self._selectedElement.setData(feature.properties); - + const uiElement = self._showOnPopup(eventSource); + const popup = L.popup() + .setContent(uiElement.Render()) + .setLatLng(e.latlng) + .openOn(self._map.map); uiElement.Update(); uiElement.Activate(); - L.DomEvent.stop(e); // Marks the event as consumed }); diff --git a/Logic/ImageSearcher.ts b/Logic/ImageSearcher.ts index 08a630e..7e7b5dd 100644 --- a/Logic/ImageSearcher.ts +++ b/Logic/ImageSearcher.ts @@ -3,6 +3,7 @@ import {ImagesInCategory, Wikidata, Wikimedia} from "./Wikimedia"; import {WikimediaImage} from "../UI/Image/WikimediaImage"; import {SimpleImageElement} from "../UI/Image/SimpleImageElement"; import {UIElement} from "../UI/UIElement"; +import {Changes} from "./Changes"; /** @@ -15,18 +16,20 @@ export class ImageSearcher extends UIEventSource { private readonly _wdItem = new UIEventSource(""); private readonly _commons = new UIEventSource(""); private _activated: boolean = false; - - constructor(tags: UIEventSource) { + private _changes: Changes; + + constructor(tags: UIEventSource, + changes: Changes) { super([]); this._tags = tags; - + this._changes = changes; const self = this; this._wdItem.addCallback(() => { - // Load the wikidata item, then detect usage on 'commons' - let wikidataId = self._wdItem.data; - // @ts-ignore + // Load the wikidata item, then detect usage on 'commons' + let wikidataId = self._wdItem.data; + // @ts-ignore if (wikidataId.startsWith("Q")) { wikidataId = wikidataId.substr(1); } @@ -34,6 +37,7 @@ export class ImageSearcher extends UIEventSource { self.AddImage(wd.image); Wikimedia.GetCategoryFiles(wd.commonsWiki, (images: ImagesInCategory) => { for (const image of images.images) { + // @ts-ignore if (image.startsWith("File:")) { self.AddImage(image); } @@ -67,17 +71,48 @@ export class ImageSearcher extends UIEventSource { } private AddImage(url: string) { - if(url === undefined || url === null){ + if (url === undefined || url === null) { return; } - if (this.data.indexOf(url) < 0) { - this.data.push(url); - this.ping(); + + for (const el of this.data) { + if (el === url) { + return; + } } + + this.data.push(url); + this.ping(); + } + + private ImageKey(url: string): string { + const tgs = this._tags.data; + for (const key in tgs) { + if (tgs[key] === url) { + return key; + } + } + return undefined; + } + + public IsDeletable(url: string): boolean { + return this.ImageKey(url) !== undefined; + } + + public Delete(url: string): void { + + const key = this.ImageKey(url); + if(key === undefined){ + return; + } + console.log("Deleting image..."); + + // this._changes.addChange(this._tags.data.id, key, ""); + } public Activate() { - if(this._activated){ + if (this._activated) { return; } this._activated = true; diff --git a/UI/FeatureInfoBox.ts b/UI/FeatureInfoBox.ts index b2fa05c..fbd42ac 100644 --- a/UI/FeatureInfoBox.ts +++ b/UI/FeatureInfoBox.ts @@ -44,7 +44,7 @@ export class FeatureInfoBox extends UIElement { this._userDetails = userDetails; this.ListenTo(userDetails); - this._imageElement = new ImageCarousel(this._tagsES); + this._imageElement = new ImageCarousel(this._tagsES, changes); this._infoboxes = []; for (const tagRenderingOption of elementsToShow) { diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 372488d..05682c4 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -3,6 +3,9 @@ import {ImageSearcher} from "../../Logic/ImageSearcher"; import {UIEventSource} from "../UIEventSource"; import {SlideShow} from "../SlideShow"; import {FixedUiElement} from "../Base/FixedUiElement"; +import {VerticalCombine} from "../Base/VerticalCombine"; +import {Changes} from "../../Logic/Changes"; +import {VariableUiElement} from "../Base/VariableUIElement"; export class ImageCarousel extends UIElement { /** @@ -20,26 +23,54 @@ export class ImageCarousel extends UIElement { public readonly slideshow: SlideShow; - constructor(tags: UIEventSource) { + private readonly _uiElements: UIEventSource; + + private readonly _deleteButtonText = new UIEventSource(""); + private readonly _deleteButton: UIElement; + + constructor(tags: UIEventSource, changes: Changes) { super(tags); - this.searcher = new ImageSearcher(tags); + const self = this; + this.searcher = new ImageSearcher(tags, changes); - let uiElements = this.searcher.map((imageURLS: string[]) => { + this._uiElements = this.searcher.map((imageURLS: string[]) => { const uiElements: UIElement[] = []; for (const url of imageURLS) { - uiElements.push(ImageSearcher.CreateImageElement(url)); + const image = ImageSearcher.CreateImageElement(url); + uiElements.push(image); } return uiElements; }); - + this.slideshow = new SlideShow( - uiElements, + this._uiElements, new FixedUiElement("")).HideOnEmpty(true); + + + this._deleteButtonText = this.slideshow._currentSlide.map((i) => { + if(self.searcher.IsDeletable(self.searcher.data[i])){ + return "DELETE"; + }else{ + return ""; + } + }); + + this._deleteButton = new VariableUiElement(this._deleteButtonText) + .HideOnEmpty(true) + .onClick(() => { + self.searcher.Delete(self.searcher.data[self.slideshow._currentSlide.data]); + }); } InnerRender(): string { - return this.slideshow.Render(); + return this.slideshow.Render() ; + // + this._deleteButton.Render(); + } + + InnerUpdate(htmlElement: HTMLElement) { + super.InnerUpdate(htmlElement); + this._deleteButton.Update(); } diff --git a/UI/SlideShow.ts b/UI/SlideShow.ts index 2be2312..8602d4c 100644 --- a/UI/SlideShow.ts +++ b/UI/SlideShow.ts @@ -6,7 +6,7 @@ export class SlideShow extends UIElement { private readonly _embeddedElements: UIEventSource - private readonly _currentSlide: UIEventSource = new UIEventSource(0); + public readonly _currentSlide: UIEventSource = new UIEventSource(0); private readonly _noimages: UIElement; private _prev: FixedUiElement; private _next: FixedUiElement; @@ -84,6 +84,8 @@ export class SlideShow extends UIElement { for (const embeddedElement of this._embeddedElements.data) { embeddedElement.Activate(); } + this._next.Update(); + this._prev.Update(); } } \ No newline at end of file diff --git a/UI/UIElement.ts b/UI/UIElement.ts index f1bb779..5a72043 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -1,5 +1,4 @@ import {UIEventSource} from "./UIEventSource"; -import {Playground} from "../Layers/Playground"; export abstract class UIElement { diff --git a/index.css b/index.css index 6a0e9c5..f95ddb0 100644 --- a/index.css +++ b/index.css @@ -457,14 +457,15 @@ body { text-align: center; } - - +.slide > span { + max-height: 40vh; +} .slide > span img { height: auto; width: auto; max-width: 100%; - max-height: 50vh; + max-height: 30vh; border-radius: 1em; } @@ -497,7 +498,7 @@ body { position: absolute; bottom: 0; - left: 5em; /* Offset for the go left button*/ + left: 6em; /* Offset for the go left button*/ padding: 0.25em; margin-bottom: 0.25em; border-radius: 0.5em;